@reuvenorg/israel-statistics-mcp
Version:
Model Context Protocol (MCP) server for Israeli Central Bureau of Statistics (CBS) price indices and economic data. Provides 9 tools for retrieving, analyzing, and calculating Israeli economic statistics including CPI, housing prices, producer indices, an
1,196 lines (1,180 loc) • 55.6 kB
JavaScript
#!/usr/bin/env node
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import z, { z as z$1 } from 'zod';
import xml2js from 'xml2js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
// package.json
var package_default = {
version: "1.0.0"};
// src/pkg.ts
var VERSION = package_default.version;
var SERVER_NAME = "israel-statistics-mcp";
var REPO_URL = "https://github.com/reuvenaor/israel-statistics-mcp";
var chapterSchema = z.string().regex(
/^[a-z]{1,3}$/,
"chapter must be a short lowercase id like 'a' or 'aa'"
).describe(
"Index chapter id. Known chapters: a=Consumer Price Index (groceries, retail) | aa=Housing Market Index | b=Producer Price Index Industrial | ba=Producer Price Index Exports | bb=Producer Price Index Services | c=Residential Building Input | ca=Commercial Building Input | d=Road Construction Input | e=Agriculture Input | f=Bus Input | fa=Public Minibus Input \u2014 call get_catalog_chapters for the current full list. Leave empty for all."
);
var languageSchema = z.enum(["he", "en"]).describe(
"Language for response. Options: he=Hebrew (default) | en=English. Use 'en' for English responses."
);
var periodSchema = z.enum(["M", "Q", "MQ", "QM"]).describe(
"Filter indices by update frequency. Options: M=monthly data only | Q=quarterly data only | MQ=both monthly and quarterly (most comprehensive) | QM=quarterly and monthly. Default shows all."
);
var searchTypeSchema = z.enum(["begins_with", "contains", "equals"]).describe(
"Search matching method. Options: contains=finds text anywhere in name (recommended) | begins_with=name starts with your text | equals=exact name match. Default: contains."
);
var currencySchema = z.enum(["new_sheqel", "old_sheqel", "lira"]).describe(
"Currency type. Options: new_sheqel=current Israeli Shekel (default, most common) | old_sheqel=pre-1980s Israeli Shekel | lira=historical Israeli Lira."
);
var oldFormatSchema = z.boolean().describe(
"Set to true if you need Hebrew text and the legacy display format. Use false (default) for English text and modern formatting."
);
// src/schemas/request.schema.ts
var MONTH_YEAR_PATTERN = /^(0[1-9]|1[0-2])-\d{4}$/;
var YEAR_MONTH_COMPACT_PATTERN = /^\d{4}(0[1-9]|1[0-2])$/;
var globalParamsSchema = {
lang: languageSchema.optional(),
page: z$1.number().min(1).optional().describe(
"Page number for pagination. Start with 1 for first page. Use with pagesize to navigate large result sets."
),
pagesize: z$1.number().min(1).max(1e3).optional().describe(
"Number of results per page (maximum 1000). Controls how many items to return. Use with page for pagination."
)
};
var getIndexTopicsSchema = z$1.object({
period: periodSchema.optional(),
searchText: z$1.string().optional().describe(
"Search for specific topics by name. For example, use 'housing' to find housing-related indices, or 'food' for food price indices."
),
searchType: searchTypeSchema.optional(),
...globalParamsSchema,
explanation: z$1.string().optional().describe("Additional explanation or context for the request")
});
var getCatalogChaptersSchema = z$1.object({
...globalParamsSchema,
explanation: z$1.string().optional().describe("Additional explanation or context for the request")
});
var getChapterTopicsSchema = z$1.object({
chapterId: chapterSchema,
...globalParamsSchema,
explanation: z$1.string().optional().describe("Additional explanation or context for the request")
});
var getSubjectCodesSchema = z$1.object({
subjectId: z$1.number().describe(
"The numeric ID of the topic you want index codes for. Get this ID first by calling getChapterTopics or getIndexTopics."
),
searchText: z$1.string().optional().describe(
"Filter index codes by description. For example, search 'bread' to find bread-related price indices within the topic."
),
searchType: searchTypeSchema.optional(),
...globalParamsSchema,
explanation: z$1.string().optional().describe("Additional explanation or context for the request")
});
var getIndexCalculatorSchema = z$1.object({
indexCode: z$1.number().describe(
"The numeric index code to use for price linkage calculation. Get this from getIndexData or getSubjectCodes first."
),
value: z$1.number().positive().describe(
"The original amount in the currency you want to link/adjust for inflation. Must be positive. For example, 100 for 100 shekels."
),
fromDate: z$1.string().describe(
"Starting date of the linkage calculation \u2014 when the original amount was valued. Use yyyy-mm-dd (recommended, e.g. '2020-01-15'). mm-dd-yyyy is also accepted; dd-mm-yyyy only when the day is >12 (otherwise it is read as mm-dd-yyyy per CBS convention)."
),
toDate: z$1.string().describe(
"Target date of the linkage calculation \u2014 what the amount is worth at this later date. Use yyyy-mm-dd (recommended, e.g. '2024-06-15'). Same format rules as fromDate."
),
currency: currencySchema.optional(),
...globalParamsSchema,
explanation: z$1.string().optional().describe("Additional explanation or context for the request")
});
var getMainIndicesSchema = z$1.object({
oldFormat: oldFormatSchema.optional(),
...globalParamsSchema,
explanation: z$1.string().optional().describe("Additional explanation or context for the request")
});
var getMainIndicesByPeriodSchema = z$1.object({
startDate: z$1.coerce.string().regex(
YEAR_MONTH_COMPACT_PATTERN,
"startDate must be yyyymm, e.g. '202001' for January 2020"
).describe(
"Starting period in yyyymm format, e.g., '202001' for January 2020. Cannot be earlier than 199701 (January 1997)."
),
endDate: z$1.coerce.string().regex(
YEAR_MONTH_COMPACT_PATTERN,
"endDate must be yyyymm, e.g. '202412' for December 2024"
).describe(
"Ending period in yyyymm format, e.g., '202412' for December 2024. Must not be earlier than startDate."
),
...globalParamsSchema,
explanation: z$1.string().optional().describe("Additional explanation or context for the request")
});
var getAllIndicesSchema = z$1.object({
oldFormat: oldFormatSchema.optional(),
chapter: chapterSchema.optional(),
...globalParamsSchema,
explanation: z$1.string().optional().describe("Additional explanation or context for the request")
});
var getIndexDataSchema = z$1.object({
code: z$1.coerce.string().regex(/^\d+$/, "code must be a numeric index code, e.g. '120010'").describe(
"The index code you want price data for (numeric, as string or number). Get this code first from getSubjectCodes or getIndexTopics. Example: '120010' for general CPI."
),
startPeriod: z$1.string().regex(
MONTH_YEAR_PATTERN,
"startPeriod must be mm-yyyy, e.g. '01-2020' for January 2020"
).optional().describe(
"Starting period in mm-yyyy format like '01-2020' for January 2020. Leave empty to get data from the beginning of the series."
),
endPeriod: z$1.string().regex(
MONTH_YEAR_PATTERN,
"endPeriod must be mm-yyyy, e.g. '12-2024' for December 2024"
).optional().describe(
"Ending period in mm-yyyy format like '12-2024' for December 2024. Leave empty to get data up to the most recent available."
),
last: z$1.number().int().positive().optional().describe(
"Get only the N most recent data points instead of the full series. Useful for getting just the latest values, e.g., use 12 for the last year of monthly data."
),
coef: z$1.boolean().optional().describe(
"Set to true to include linkage coefficients for inflation calculations. Only needed if you plan to do manual price adjustments."
),
...globalParamsSchema,
explanation: z$1.string().optional().describe("Additional explanation or context for the request")
});
var paginationSchema = z$1.object({
total_items: z$1.number().describe("Total number of items"),
page_size: z$1.number().describe("Items per page"),
current_page: z$1.number().describe("Current page number"),
last_page: z$1.number().describe("Last page number"),
first_url: z$1.string().nullable().describe("URL for first page"),
previous_url: z$1.string().nullable().describe("URL for previous page"),
current_url: z$1.string().describe("URL for current page"),
next_url: z$1.string().nullable().describe("URL for next page"),
last_url: z$1.string().describe("URL for last page"),
base_url: z$1.string().nullable().describe("Base URL")
}).describe("Pagination information");
var baseCodeSchema = z$1.object({
codeId: z$1.number().describe("Unique numeric identifier for this specific index code"),
codeName: z$1.string().describe("Full descriptive name of the index"),
codeNote: z$1.string().optional().describe("Additional notes or explanations about this index"),
codeLevel: z$1.number().optional().describe("Hierarchical level of this index (1=main, 3=sub, 6=detailed)"),
codeLine: z$1.number().optional().describe("Display line number for ordering indices"),
codeType: z$1.number().optional().describe("Type classification of the index"),
codeFromDate: z$1.string().nullable().optional().describe(
"Start date when this index became available (YYYY-MM-DD format)"
),
codeToDate: z$1.string().nullable().optional().describe(
"End date when this index was discontinued (YYYY-MM-DD format, null if still active)"
),
codeCalcFromDate: z$1.string().nullable().optional().describe("Start date for index calculations (YYYY-MM-DD format)"),
codeCalcToDate: z$1.string().nullable().optional().describe(
"End date for index calculations (YYYY-MM-DD format, null if still being calculated)"
),
isMonth: z$1.boolean().optional().describe(
"Whether this index is updated monthly (true) or at other intervals"
),
codePrefix: z$1.number().optional().describe("Prefix number used in the index coding system")
});
var subjectCodeSchema = baseCodeSchema.extend({
codeDescription: z$1.string().optional().nullable().describe("Additional description or notes about this index"),
period: z$1.string().optional().nullable().describe("Update frequency period (e.g., 'Monthly', 'Quarterly')"),
baseYear: z$1.string().optional().nullable().describe("Base year period for index calculations")
});
var baseSubjectSchema = z$1.object({
subjectId: z$1.number().describe("Unique numeric identifier for this subject/topic"),
subjectName: z$1.string().describe("Descriptive name of the subject/topic area")
});
var subjectWithCodesSchema = baseSubjectSchema.extend({
code: z$1.array(baseCodeSchema)
});
var baseChapterSchema = z$1.object({
chapterId: chapterSchema,
chapterName: z$1.string().describe("Full descriptive name of the index chapter"),
chapterOrder: z$1.number().describe("Display order number for the chapter in the system"),
mainCode: z$1.number().nullable().describe(
"Primary index code for this chapter (null for some chapters that don't have a main index)"
)
});
var chapterWithSubjectsSchema = baseChapterSchema.extend({
subject: z$1.array(subjectWithCodesSchema)
});
var baseIndexValueSchema = z$1.object({
value: z$1.number().nullable().describe(
"Index value for this specific base period (null when CBS ships a non-numeric value)"
),
base: z$1.string().describe("Base period description"),
chainingCoefficient: z$1.number().nullable().optional().describe(
"Coefficient used for linking indices across different base periods"
)
});
var baseDateEntrySchema = z$1.object({
year: z$1.number().describe("Year"),
month: z$1.number().describe("Month number"),
monthDesc: z$1.string().describe("Month name"),
percent: z$1.number().describe("Monthly percentage change"),
percentYear: z$1.number().describe("Yearly percentage change")
});
var currencyBaseSchema = z$1.object({
baseDesc: z$1.string().describe("Base period description"),
value: z$1.number().describe("Index value")
});
var dateEntryWithBasesSchema = baseDateEntrySchema.extend({
currBase: currencyBaseSchema,
prevBase: currencyBaseSchema.nullable().optional()
});
z$1.object({
code: z$1.number().describe("Index code"),
name: z$1.string().describe("Index name"),
date: z$1.array(dateEntryWithBasesSchema)
});
var xmlStringArray = z$1.array(z$1.string());
z$1.array(z$1.string()).transform((arr) => arr.map((str) => parseFloat(str)));
z$1.object({
_: z$1.string().describe("Index value"),
base: xmlStringArray.describe("Base period description"),
chainingCoefficient: xmlStringArray.optional().describe("Chaining coefficient for historical linkage")
});
var indexTopicsResponseSchema = z$1.object({
chapters: z$1.array(chapterWithSubjectsSchema).describe("Array of all index chapters available in the CBS system")
});
var prevBaseEntrySchema = z$1.object({
baseDesc: z$1.string().describe("Base period description"),
value: z$1.number().describe("Index value expressed in this base"),
coeff: z$1.number().nullable().optional().describe("Chaining coefficient to the previous base (with coef=true)")
}).passthrough();
var indexDataResponseSchema = z$1.object({
month: z$1.array(
z$1.object({
code: z$1.number().describe("Index code"),
name: z$1.string().describe("Index name (language follows lang param)"),
date: z$1.array(
z$1.object({
year: z$1.number().describe("Year"),
percent: z$1.number().nullable().describe("Monthly percentage change"),
percentYear: z$1.number().nullable().describe("Yearly percentage change"),
currBase: currencyBaseSchema.passthrough(),
// Single object normally; array of bases when coef=true
prevBase: z$1.union([prevBaseEntrySchema, z$1.array(prevBaseEntrySchema)]).nullable().optional(),
month: z$1.number().describe("Month number"),
monthDesc: z$1.string().describe("Month name")
}).passthrough()
).describe("Array of date entries with index values")
})
).nullable().describe(
"Monthly index data (null when the code is quarterly or no data matches)"
),
quarter: z$1.array(z$1.unknown()).nullable().optional().describe("Quarterly index data (if applicable)"),
paging: paginationSchema
}).describe(
"Index data response containing monthly/quarterly data with pagination"
);
var indexCalculatorResponseSchema = z$1.object({
request: z$1.object({
code: z$1.number().describe("The index code used for calculation"),
sum: z$1.number().describe("The original amount entered for linkage calculation"),
currency: z$1.string().describe("Currency type (NEW_SHEQEL, OLD_SHEQEL, LIRA)"),
from_date: z$1.string().describe(
"Starting date for linkage calculation in YYYY-MM-DD format"
),
to_date: z$1.string().describe("Target date for linkage calculation in YYYY-MM-DD format")
}).describe("Request parameters sent to the calculator"),
answer: z$1.object({
from_value: z$1.number().describe("Original value amount"),
to_value: z$1.number().describe("Linked/adjusted value at target date"),
base_year: z$1.string().describe("Base year for the index calculation"),
from_index_date: z$1.string().describe("Index period corresponding to the from_date"),
from_index_value: z$1.number().describe("Index value at the from_date"),
to_index_date: z$1.string().describe("Index period corresponding to the to_date"),
to_index_value: z$1.number().describe("Index value at the to_date"),
chaining_coefficient: z$1.number().nullable().describe(
"Coefficient used for chaining between base years (null for some index series)"
),
mult_min: z$1.number().nullable().describe(
"Minimum multiplication factor (null for index series without min/max bounds, e.g. 110050)"
),
mult_max: z$1.number().nullable().describe(
"Maximum multiplication factor (null for index series without min/max bounds)"
),
Koeff: z$1.number().nullable().describe("Additional coefficient factor (may be null)"),
change_percent: z$1.number().nullable().describe(
"Percentage change from original to linked value (may be null; compute from from_value/to_value when absent)"
)
}).describe(
"Calculation results showing original value linked to target date"
)
}).describe(
"Price linkage calculation result showing how a monetary amount changes in value over time due to inflation/deflation"
);
var chapterTopicsResponseSchema = z$1.object({
chapterId: chapterSchema,
chapterName: z$1.string().nullable().describe("Full descriptive name of the index chapter"),
chapterOrder: z$1.number().nullable().describe("Display order number for the chapter in the system"),
mainCode: z$1.number().nullable().describe("Primary index code for this chapter"),
subject: z$1.array(
z$1.object({
subjectId: z$1.number().describe(
"Unique numeric identifier for this subject/topic within the chapter"
),
subjectName: z$1.string().describe("Descriptive name of the subject/topic area"),
code: z$1.array(
z$1.object({
codeId: z$1.number().describe("Unique numeric index code"),
codeName: z$1.string().describe("Index name")
}).passthrough()
).nullable().describe(
"Array of index codes for this subject, or null if no codes available"
)
})
).describe(
"Array of subjects/topics available within the specified chapter"
)
}).describe(
"Topics and subjects available within a specific index chapter from index/catalog/chapter endpoint"
);
var subjectCodesResponseSchema = baseSubjectSchema.extend({
subjectName: z$1.string().nullable().describe("Descriptive name of the subject/topic area"),
chapterId: chapterSchema.optional().nullable(),
chapterName: z$1.string().optional().nullable().describe("Full descriptive name of the parent chapter"),
code: z$1.array(subjectCodeSchema).describe("Array of index codes available for the specified subject")
}).describe(
"Index codes and details for a specific subject/topic from index/catalog/subject endpoint"
);
var allIndicesResponseSchema = z$1.object({
indices: z$1.preprocess(
(v) => v == null || typeof v === "string" ? {} : v,
z$1.object({
UpdateDate: z$1.array(z$1.string()).default([]).describe("Last update timestamp in ISO format"),
chapter: z$1.array(
z$1.object({
name: z$1.array(z$1.string()).describe("Chapter name (e.g., 'Consumer Prices Index')"),
month: z$1.array(
z$1.object({
index: z$1.array(
z$1.object({
month: z$1.array(z$1.string()).describe("Month name (e.g., 'June')"),
year: z$1.array(z$1.string()).describe("Year as string (e.g., '2025')"),
code: z$1.array(z$1.string()).describe("Index code (e.g., '120010')"),
index_name: z$1.array(z$1.string()).describe("Full index name"),
title: z$1.array(z$1.string()).optional().describe("Additional title for some indices"),
percent: z$1.array(z$1.string()).optional().describe("Percentage change"),
index_base: z$1.array(
z$1.object({
_: z$1.string().describe("Index value"),
base: z$1.array(z$1.string()).describe(
"Base period (e.g., 'Average 2024')"
),
chaining_coefficient: z$1.array(z$1.string()).optional().describe(
"Chaining coefficient for historical linkage"
)
})
).optional().describe(
"Array of index values for different base periods"
)
})
).describe("Array of individual index entries")
})
).optional().describe("Array of month data")
})
).default([]).describe("Array of chapters containing index data")
})
)
}).describe(
"All indices response from XML API containing nested structure with chapters, months, and index data"
);
var mainIndicesXmlResponseSchema = z$1.object({
indices: z$1.preprocess(
(v) => v == null || typeof v === "string" ? {} : v,
z$1.object({
UpdateDate: z$1.array(z$1.string()).default([]).describe("Last update timestamp of the indices data (ISO format)"),
date: z$1.array(
z$1.object({
year: z$1.array(z$1.string()).describe("Year of the index data (YYYY format)"),
month: z$1.array(z$1.string()).describe("Month name of the index data (e.g., 'June', 'July')"),
code: z$1.array(
z$1.object({
code: z$1.array(z$1.string()).describe("Unique numeric index code identifier"),
name: z$1.array(z$1.string()).describe("Full descriptive name of the index"),
percent: z$1.array(z$1.string()).describe("Monthly percentage change of the index as string"),
index: z$1.array(
z$1.object({
_: z$1.string().describe("Index value for this specific base period"),
base: z$1.array(z$1.string()).describe(
"Base period description (e.g., 'Average 2024', 'Average 2022')"
),
chainingCoefficient: z$1.array(z$1.string()).optional().describe(
"Coefficient used for linking indices across different base periods"
)
})
)
})
)
})
).default([])
})
)
});
var mainIndicesByPeriodXmlResponseSchema = z$1.object({
indices: z$1.preprocess(
(v) => v == null || typeof v === "string" ? {} : v,
z$1.object({
ind: z$1.array(
z$1.object({
date: z$1.array(z$1.string()).describe("Date of the index data in YYYY-MM format"),
n: z$1.array(z$1.string()).optional().describe("Full descriptive name of the index"),
index: z$1.array(z$1.string()).describe("Index value as string (e.g., '102.3')"),
percent: z$1.array(z$1.string()).describe("Monthly percentage change as string (e.g., '0.3')"),
code: z$1.array(z$1.string()).describe("Unique numeric index code as string (e.g., '120010')"),
base: z$1.array(z$1.string()).describe("Base period description (e.g., 'Average 2022')")
})
).default([]).describe(
"Array of individual index entries (empty when no data matches the period range)"
)
})
)
});
var transformedIndexTopicsSchema = z$1.object({
topics: z$1.array(chapterWithSubjectsSchema).describe("Array of all index chapters with their topics and codes"),
summary: z$1.string().describe(
"Human-readable summary of the retrieved data, including count of total index codes found"
)
});
var transformedMainIndicesSchema = z$1.object({
indices: z$1.array(
z$1.object({
code: z$1.string().describe(
"Unique numeric index code identifier (e.g., '120010' for general Consumer Price Index)"
),
name: z$1.string().describe(
"Full descriptive name of the index (e.g., 'Consumer Price Index - General')"
),
percent: z$1.number().nullable().describe(
"Monthly percentage change of the index (positive = increase, negative = decrease; null when CBS omits the value)"
),
year: z$1.string().describe("Year of the index data (YYYY format)"),
month: z$1.string().describe("Month name of the index data (e.g., 'June', 'July')"),
indices: z$1.array(baseIndexValueSchema)
})
).describe(
"Array of main price indices with their values across different base periods"
),
updateDate: z$1.string().describe("Last update timestamp of the indices data (ISO format)"),
summary: z$1.string().describe(
"Human-readable summary of the retrieved data, including count and update information"
)
});
var transformedMainIndicesByPeriodSchema = z$1.object({
indices: z$1.array(
z$1.object({
code: z$1.string().describe("Unique numeric index code identifier (e.g., '120010')"),
name: z$1.string().describe("Full descriptive name of the index"),
percent: z$1.number().nullable().describe(
"Monthly percentage change of the index (null when CBS omits the value)"
),
date: z$1.string().describe("Date of the index data in YYYY-MM format"),
index: z$1.number().nullable().describe("Index value for this period (null when CBS omits it)"),
base: z$1.string().describe("Base period description (e.g., 'Average 2022')")
})
).describe("Array of main price indices for the specified period range"),
groupedByDate: z$1.record(z$1.string(), z$1.array(z$1.unknown())).describe("Indices grouped by date for easier navigation"),
dateRange: z$1.string().describe("The date range requested (startDate to endDate)"),
totalIndices: z$1.number().describe("Total number of indices returned"),
summary: z$1.string().describe("Human-readable summary of the data retrieval")
});
var catalogChaptersResponseSchema = z$1.object({
chapters: z$1.array(baseChapterSchema).describe("Array of all available index chapters")
});
// src/schemas/output.schema.ts
var summaryField = z$1.string().describe("Human-readable summary (includes housing warnings when relevant)");
var getIndexTopicsOutputSchema = transformedIndexTopicsSchema;
var getCatalogChaptersOutputSchema = z$1.object({
chapters: z$1.array(baseChapterSchema).describe("All available index chapters (a..fa)"),
summary: summaryField
});
var getChapterTopicsOutputSchema = z$1.object({
topics: chapterTopicsResponseSchema.shape.subject,
summary: summaryField
});
var getSubjectCodesOutputSchema = z$1.object({
codes: subjectCodesResponseSchema.shape.code,
summary: summaryField
});
var getIndexDataOutputSchema = z$1.object({
data: indexDataResponseSchema,
summary: summaryField
});
var getIndexCalculatorOutputSchema = z$1.object({
request: indexCalculatorResponseSchema.shape.request,
answer: indexCalculatorResponseSchema.shape.answer,
summary: summaryField
});
var getMainIndicesOutputSchema = transformedMainIndicesSchema;
var getMainIndicesByPeriodOutputSchema = transformedMainIndicesByPeriodSchema;
var getAllIndicesOutputSchema = z$1.object({
indices: z$1.unknown().describe(
"Raw CBS all-indices tree (chapters -> months -> index entries; xml2js array conventions)"
),
summary: summaryField
});
var API_BASE = "https://api.cbs.gov.il/";
var ALLOWED_HOST = "api.cbs.gov.il";
var REQUEST_TIMEOUT_MS = 3e4;
var MAX_RESPONSE_BYTES = 15 * 1024 * 1024;
var MAX_RETRIES = 1;
var RETRY_BASE_DELAY_MS = 500;
var USER_AGENT = `${SERVER_NAME}/${VERSION} (+${REPO_URL})`;
var ACCEPT = "application/json, application/xml;q=0.9, text/xml;q=0.8, */*;q=0.1";
var CbsApiError = class extends Error {
constructor(message, endpoint, status) {
super(message);
this.endpoint = endpoint;
this.status = status;
this.name = "CbsApiError";
}
endpoint;
status;
};
function isTimeoutError(err) {
return err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
}
function looksLikeHtml(text) {
const head = text.trimStart().slice(0, 100).toLowerCase();
return head.startsWith("<!doctype html") || head.startsWith("<html");
}
function looksLikeXml(text) {
const head = text.trimStart();
return head.startsWith("<?xml") || head.startsWith("<");
}
function parseXml(text) {
const parser = new xml2js.Parser({
explicitArray: true,
// Always arrays so single elements parse consistently
ignoreAttrs: false,
mergeAttrs: true
});
return parser.parseStringPromise(text);
}
function extractCbsError(payload) {
if (payload === null || typeof payload !== "object" || Array.isArray(payload))
return null;
const obj = payload;
for (const key of ["error", "Error", "error_message", "Message"]) {
const value = obj[key];
if (typeof value === "string" && value.length > 0) return value;
if (value && typeof value === "object") return JSON.stringify(value);
}
return null;
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function fetchWithRetry(url, endpoint) {
let lastError;
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
const response = await fetch(url, {
headers: { "User-Agent": USER_AGENT, Accept: ACCEPT },
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
redirect: "follow"
});
if (response.status >= 500 && attempt < MAX_RETRIES) {
lastError = new CbsApiError(
`CBS API error: HTTP ${response.status}`,
endpoint,
response.status
);
await delay(RETRY_BASE_DELAY_MS + Math.random() * 250);
continue;
}
return response;
} catch (err) {
if (isTimeoutError(err)) {
throw new CbsApiError(
`CBS API request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s`,
endpoint
);
}
lastError = err;
if (attempt < MAX_RETRIES) {
await delay(RETRY_BASE_DELAY_MS + Math.random() * 250);
continue;
}
throw new CbsApiError(
`CBS API request failed: ${err instanceof Error ? err.message : String(err)}`,
endpoint
);
}
}
throw lastError instanceof Error ? lastError : new CbsApiError("CBS API request failed", endpoint);
}
async function parseBody(response, endpoint) {
const contentLength = response.headers.get("content-length");
if (contentLength && Number(contentLength) > MAX_RESPONSE_BYTES) {
throw new CbsApiError(
`CBS API response too large (${contentLength} bytes)`,
endpoint,
response.status
);
}
const text = await response.text();
if (text.length > MAX_RESPONSE_BYTES) {
throw new CbsApiError(
`CBS API response too large (${text.length} bytes)`,
endpoint,
response.status
);
}
if (text.trim().length === 0) {
throw new CbsApiError("CBS API returned an empty response", endpoint);
}
if (looksLikeHtml(text)) {
throw new CbsApiError(
`CBS API returned an HTML page instead of data (HTTP ${response.status}) \u2014 the API may be temporarily unavailable`,
endpoint,
response.status
);
}
const contentType = response.headers.get("content-type") ?? "";
const asXml = contentType.includes("xml") || !contentType.includes("json") && looksLikeXml(text);
if (asXml) {
try {
return await parseXml(text);
} catch (err) {
throw new CbsApiError(
`CBS API returned malformed XML: ${err instanceof Error ? err.message : String(err)}`,
endpoint,
response.status
);
}
}
try {
return JSON.parse(text);
} catch {
throw new CbsApiError(
"CBS API returned malformed JSON",
endpoint,
response.status
);
}
}
function formatZodIssues(error) {
const issues = error.issues.slice(0, 8).map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
const extra = error.issues.length > 8 ? ` (+${error.issues.length - 8} more)` : "";
return issues + extra;
}
async function secureFetch(endpoint, params, schema, globalParams) {
const url = new URL(endpoint, API_BASE);
if (url.host !== ALLOWED_HOST || url.protocol !== "https:") {
throw new CbsApiError(
`Refusing request to non-CBS host "${url.host}"`,
endpoint
);
}
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
if (globalParams) {
if (globalParams.lang) {
url.searchParams.append("lang", globalParams.lang);
}
if (globalParams.page) {
url.searchParams.append("page", globalParams.page.toString());
}
if (globalParams.pagesize) {
const pagesize = Math.min(globalParams.pagesize, 1e3);
url.searchParams.append("pagesize", pagesize.toString());
}
}
const response = await fetchWithRetry(url, endpoint);
if (!response.ok) {
throw new CbsApiError(
`CBS API error: HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`,
endpoint,
response.status
);
}
const data = await parseBody(response, endpoint);
const cbsError = extractCbsError(data);
if (cbsError) {
throw new CbsApiError(`CBS API returned an error: ${cbsError}`, endpoint);
}
const result = schema.safeParse(data);
if (!result.success) {
throw new CbsApiError(
`CBS response validation failed for ${endpoint}: ${formatZodIssues(result.error)}`,
endpoint
);
}
return result.data;
}
// src/mcp/handlers/getIndexTopics.ts
async function getIndexTopics(args) {
const params = {
format: "json",
download: "false"
};
if (args?.period) params.period = args.period;
if (args?.searchText) params.q = args.searchText;
if (args?.searchType) params.string_match_type = args.searchType;
const globalParams = {
lang: args?.lang,
page: args?.page,
pagesize: args?.pagesize
};
const data = await secureFetch(
"index/catalog/tree",
params,
indexTopicsResponseSchema,
globalParams
);
const topicCount = data.chapters.flatMap(
(c) => c.subject.flatMap((s) => s.code)
).length;
return { topics: data.chapters, summary: `Found ${topicCount} index codes.` };
}
// src/mcp/handlers/getCatalogChapters.ts
async function getCatalogChapters(args) {
const globalParams = {
lang: args?.lang,
page: args?.page,
pagesize: args?.pagesize
};
const data = await secureFetch(
"index/catalog/catalog",
{ format: "json", download: "false" },
catalogChaptersResponseSchema,
globalParams
);
return {
chapters: data.chapters,
summary: `Found ${data.chapters.length} index chapters.`
};
}
// src/mcp/helpers/dates.ts
var DAYS_IN_MONTH = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
function checkedDate(year, month, day) {
if (year < 1900 || year > 2100) {
throw new Error(`Invalid date: year ${year} is out of range (1900-2100)`);
}
if (month < 1 || month > 12) {
throw new Error(`Invalid date: month ${month} is out of range (1-12)`);
}
if (day < 1 || day > DAYS_IN_MONTH[month - 1]) {
throw new Error(
`Invalid date: day ${day} is out of range for month ${month}`
);
}
const mm = String(month).padStart(2, "0");
const dd = String(day).padStart(2, "0");
return `${year}-${mm}-${dd}`;
}
function normalizeCalculatorDate(input) {
const cleaned = input.trim().replace(/\//g, "-");
const isoMatch = cleaned.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
if (isoMatch) {
return checkedDate(
Number(isoMatch[1]),
Number(isoMatch[2]),
Number(isoMatch[3])
);
}
const dashMatch = cleaned.match(/^(\d{1,2})-(\d{1,2})-(\d{4})$/);
if (dashMatch) {
const a = Number(dashMatch[1]);
const b = Number(dashMatch[2]);
const year = Number(dashMatch[3]);
if (a > 12 && b > 12) {
throw new Error(
`Invalid date "${input}": neither ${a} nor ${b} is a valid month`
);
}
if (a > 12) {
return checkedDate(year, b, a);
}
return checkedDate(year, a, b);
}
throw new Error(
`Invalid date "${input}": use yyyy-mm-dd (recommended), e.g. 2020-01-31`
);
}
function toYearMonth(value) {
if (!value) return null;
const v = value.trim();
let m = v.match(/^(\d{1,2})-(\d{4})$/);
if (m) return { year: Number(m[2]), month: Number(m[1]) };
m = v.match(/^(\d{4})(\d{2})$/);
if (m) return { year: Number(m[1]), month: Number(m[2]) };
m = v.match(/^(\d{4})-(\d{1,2})(?:-\d{1,2})?$/);
if (m) return { year: Number(m[1]), month: Number(m[2]) };
return null;
}
function compareYearMonth(a, b) {
return a.year * 12 + a.month - (b.year * 12 + b.month);
}
// src/mcp/helpers/housingWarnings.ts
var HOUSING_NAME_PATTERN = /housing|dwelling|apartment|real estate|מחירי דירות|מחירי הדירות|שוק הדיור|דיור|נדל"ן/i;
function isHousingCode(code) {
if (code == null) return false;
const s = String(code);
return /^18\d{4}$/.test(s);
}
function getProvisionalWindow(now = /* @__PURE__ */ new Date()) {
const publicationLag = now.getDate() >= 15 ? 2 : 3;
const end = new Date(now.getFullYear(), now.getMonth() - publicationLag, 1);
const start = new Date(end.getFullYear(), end.getMonth() - 5, 1);
const fmt = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
return {
start: { year: start.getFullYear(), month: start.getMonth() + 1 },
end: { year: end.getFullYear(), month: end.getMonth() + 1 },
label: `${fmt(start)} through ${fmt(end)}`
};
}
function checkHousingWarnings(input) {
const { chapter, code, indexName, targetPeriod } = input;
const isHousingRelated = chapter === "aa" || isHousingCode(code) || indexName != null && HOUSING_NAME_PATTERN.test(indexName);
if (!isHousingRelated) {
return { isHousingRelated: false, warnings: [] };
}
const warnings = [
"\u26A0\uFE0F Housing Price Index: bi-monthly publication based on transactions from 2-3 months before the publication date."
];
const window = getProvisionalWindow(input.now);
const target = toYearMonth(targetPeriod);
const targetBeforeWindow = target != null && compareYearMonth(target, window.start) < 0;
if (targetBeforeWindow) {
warnings.push(
`\u2705 The requested period predates the current provisional window (${window.label}), so these values are final.`
);
} else {
warnings.push(
`\u{1F504} Provisional data: the last 3 published indices (transactions ~${window.label}) may still be revised when late transaction reports arrive.`
);
warnings.push(
"\u{1F4A1} For price linkage over recent periods, prefer an end date before the provisional window or expect small revisions."
);
}
return { isHousingRelated, warnings };
}
function addHousingWarningsToSummary(summary, housingWarning) {
if (!housingWarning.isHousingRelated) {
return summary;
}
return `${summary} ${housingWarning.warnings.join(" ")}`;
}
// src/mcp/handlers/getChapterTopics.ts
async function getChapterTopics(args) {
const globalParams = {
lang: args.lang,
page: args.page,
pagesize: args.pagesize
};
const data = await secureFetch(
"index/catalog/chapter",
{ id: args.chapterId, format: "json", download: "false" },
chapterTopicsResponseSchema,
globalParams
);
const housingWarning = checkHousingWarnings({ chapter: args.chapterId });
const baseSummary = `Found ${data.subject.length} topics in chapter ${args.chapterId}.`;
return {
topics: data.subject,
summary: addHousingWarningsToSummary(baseSummary, housingWarning)
};
}
// src/mcp/handlers/getSubjectCodes.ts
async function getSubjectCodes(args) {
const params = {
id: args.subjectId.toString(),
format: "json",
download: "false"
};
if (args.searchText) params.q = args.searchText;
if (args.searchType) params.string_match_type = args.searchType;
const globalParams = {
lang: args.lang,
page: args.page,
pagesize: args.pagesize
};
const data = await secureFetch(
"index/catalog/subject",
params,
subjectCodesResponseSchema,
globalParams
);
return {
codes: data.code,
summary: `Found ${data.code.length} index codes for subject ${args.subjectId}.`
};
}
// src/mcp/handlers/getIndexData.ts
async function getIndexData(args) {
const params = {
id: args.code,
format: "json",
download: "false"
};
if (args.startPeriod) params.startPeriod = args.startPeriod;
if (args.endPeriod) params.endPeriod = args.endPeriod;
if (args.last) params.last = args.last.toString();
if (args.coef) params.coef = args.coef.toString();
const globalParams = {
lang: args.lang,
page: args.page,
pagesize: args.pagesize
};
const endpoint = `index/data/price`;
const data = await secureFetch(
endpoint,
params,
indexDataResponseSchema,
globalParams
);
const allDataPoints = data.month?.[0]?.date || [];
const values = allDataPoints.map((d) => d.currBase.value);
const avg = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
const housingWarning = checkHousingWarnings({
code: args.code,
indexName: data.month?.[0]?.name,
targetPeriod: args.endPeriod
});
const baseSummary = allDataPoints.length > 0 ? `Retrieved ${allDataPoints.length} data points. Average value: ${avg.toFixed(2)}.` : `No data points found for index ${args.code} in the requested range.`;
return {
data,
summary: addHousingWarningsToSummary(baseSummary, housingWarning)
};
}
// src/mcp/handlers/getIndexCalculator.ts
async function getIndexCalculator(args) {
const fromDate = normalizeCalculatorDate(args.fromDate);
const toDate = normalizeCalculatorDate(args.toDate);
const params = {
value: args.value.toString(),
date: fromDate,
toDate,
format: "json",
download: "false"
};
if (args.currency) params.currency = args.currency;
const globalParams = {
lang: args.lang,
page: args.page,
pagesize: args.pagesize
};
const data = await secureFetch(
`index/data/calculator/${args.indexCode}`,
params,
indexCalculatorResponseSchema,
globalParams
);
const { from_value, to_value, change_percent } = data.answer;
const effectivePercent = change_percent ?? (from_value !== 0 ? Number(((to_value - from_value) / from_value * 100).toFixed(2)) : null);
const housingWarning = checkHousingWarnings({
code: args.indexCode,
targetPeriod: toDate
});
const baseSummary = `Linked ${data.request.sum} from ${data.request.from_date} to ${data.request.to_date}: ${to_value}${effectivePercent != null ? ` (${effectivePercent}% change)` : ""}`;
return {
request: data.request,
answer: data.answer,
summary: addHousingWarningsToSummary(baseSummary, housingWarning)
};
}
// src/mcp/helpers/numbers.ts
function parseNumberOrNull(value) {
if (value == null) return null;
const n = Number.parseFloat(value);
return Number.isFinite(n) ? n : null;
}
// src/mcp/handlers/getMainIndices.ts
async function getMainIndices(args) {
const params = {
// price_selected is an XML-only endpoint — request what it actually serves
format: "xml",
download: "false"
};
if (args?.oldFormat) params.oldformat = "true";
const globalParams = {
lang: args?.lang,
page: args?.page,
pagesize: args?.pagesize
};
const data = await secureFetch(
"index/data/price_selected",
params,
mainIndicesXmlResponseSchema,
globalParams
);
const transformedIndices = data.indices.date.flatMap(
(dateEntry) => dateEntry.code.map((codeEntry) => ({
code: codeEntry.code[0],
// Single value per code entry
name: codeEntry.name[0],
// Single value per code entry
percent: parseNumberOrNull(codeEntry.percent[0]),
year: dateEntry.year[0],
// Single value per date entry
month: dateEntry.month[0],
// Single value per date entry
indices: codeEntry.index.map((idx) => ({
value: parseNumberOrNull(idx._),
// Text content, not in array
base: idx.base[0],
// Attributes are in arrays with explicitArray:true
chainingCoefficient: idx.chainingCoefficient ? parseNumberOrNull(idx.chainingCoefficient[0]) : void 0
}))
}))
);
const updateDate = data.indices.UpdateDate[0] ?? "unknown";
return {
indices: transformedIndices,
updateDate,
summary: transformedIndices.length > 0 ? `Retrieved ${transformedIndices.length} main indices updated on ${updateDate}.` : "No main indices data returned by CBS."
};
}
async function getMainIndicesByPeriod(args) {
if (args.startDate < "199701") {
throw new Error(
`startDate ${args.startDate} is before 199701 \u2014 CBS main indices begin January 1997`
);
}
if (args.endDate < args.startDate) {
throw new Error(
`endDate ${args.endDate} is earlier than startDate ${args.startDate}`
);
}
const params = {
StartDate: args.startDate,
EndDate: args.endDate,
format: "xml",
download: "false"
};
const globalParams = {
lang: args.lang,
page: args.page,
pagesize: args.pagesize
};
const data = await secureFetch(
"index/data/price_selected_b",
params,
mainIndicesByPeriodXmlResponseSchema,
globalParams
);
const transformedIndices = data.indices.ind.map((indEntry) => ({
code: indEntry.code[0],
// Get first element from array
name: indEntry.n?.[0] || "Unknown Index",
// Get first element from array (note: 'n', not 'name')
percent: parseNumberOrNull(indEntry.percent[0]),
date: indEntry.date[0],
// Get first element from array (YYYY-MM format)
index: parseNumberOrNull(indEntry.index[0]),
base: indEntry.base[0]
// Base period description
}));
const groupedByDate = transformedIndices.reduce(
(acc, curr) => {
const date = curr.date;
if (!acc[date]) {
acc[date] = [];
}
acc[date].push(curr);
return acc;
},
{}
);
return {
indices: transformedIndices,
groupedByDate,
dateRange: `${args.startDate} to ${args.endDate}`,
totalIndices: transformedIndices.length,
summary: transformedIndices.length > 0 ? `Retrieved ${transformedIndices.length} main indices from ${args.startDate} to ${args.endDate}.` : `No main indices found between ${args.startDate} and ${args.endDate}.`
};
}
// src/mcp/handlers/getAllIndices.ts
async function getAllIndices(args) {
const params = {
format: "xml",
download: "false"
};
if (args?.oldFormat) params.oldformat = "true";
if (args?.chapter) params.chapter = args.chapter;
const globalParams = {
lang: args?.lang,
page: args?.page,
pagesize: args?.pagesize
};
const data = await secureFetch(
"index/data/price_all",
params,
allIndicesResponseSchema,
globalParams
);
const housingWarning = checkHousingWarnings({ chapter: args?.chapter });
const chapterFilter = args?.chapter ? ` for chapter ${args.chapter}` : "";
const totalIndices = data.indices.chapter.reduce((total, chapter) => {
return total + (chapter.month?.reduce((monthTotal, monthData) => {
return monthTotal + monthData.index.length;
}, 0) || 0);
}, 0);
const baseSummary = `Retrieved ${totalIndices} indices${chapterFilter}.`;
return {
indices: data.indices,
summary: addHousingWarningsToSummary(baseSummary, housingWarning)
};
}
// src/mcp/tools.ts
function defineTool(def) {
return {
name: def.name,
title: def.title,
description: def.description,
inputSchema: def.inputSchema.shape,
outputSchema: def.outputSchema.shape,
handler: def.handler
};
}
var TOOLS = [
defineTool({
name: "get_index_topics",
title: "Search Index Topics",
description: "Browse or search the full CBS catalog tree of price-index topics and codes. The best starting point for keyword discovery (e.g. searchText='bread' or 'housing'). Returns chapters with their subjects and index codes; feed a codeId into get_index_data or get_index_calculator. Hebrew is the default language \u2014 pass lang='en' for English names.",
inputSchema: getIndexTopicsSchema,
outputSchema: getIndexTopicsOutputSchema,
handler: getIndexTopics
}),
defineTool({
name: "get_index_data",
title: "Get Index Time Series",
description: "Retrieve the historical time series for one index code (from get_subject_codes or get_index_topics). Periods use mm-yyyy (e.g. startPeriod='01-2020', endPeriod='12-2024'); use last=N for just the most recent N points; coef=true adds chaining coefficients to previous base periods. Housing indices (codes 18xxxx) are bi-monthly with provisional recent values \u2014 the summary warns when that matters.",
inputSchema: getIndexDataSchema,
outputSchema: getIndexDataOutputSchema,
handler: getIndexData
}),
defineTool({
name: "get_catalog_chapters",
title: "List Index Chapters",
description: "List all CBS index chapters (a=Consumer Price Index, aa=Housing Market, b/ba/bb=Producer Prices, c/ca=Building Inputs, d=Road Construction, e=Agriculture, f/fa=Bus & Minibus Inputs, plus any newly added). This is the authoritative chapter list \u2014 use a chapterId with get_chapter_topics or get_all_indices.",
inputSchema: getCatalogChaptersSchema,
outputSchema: getCatalogChaptersOutputSchema,
handler: getCatalogChapters
}),
defineTool({
name: "get_chapter_topics",
title: "Get Chapter Topics",
description: "List the subjects/topics inside one chapter (chapterId from get_catalog_chapters, e.g. 'aa' for the housing market). Returns subjectIds for get_subject_codes; code arrays may be null at this level \u2014 that is normal, drill down via get_subject_codes.",
inputSchema: getChapterTopicsSchema,
outputSchema: getChapterTopicsOutputSchema,
handler: getChapterTopics
}),
defineTool({
name: "get_subject_codes",
title: "Get Subject Index Codes",
description: "List the concrete index codes for one subject/topic (subjectId from get_chapter_topics or get_index_topics), optionally filtered by searchText. Each code includes its date coverage and update frequency \u2014 use codeId with get_index_data or get_index_calculator.",
inputSchema: getSubjectCodesSchema,
outputSchema: getSubjectCodesOutputSchema,
handler: getSubjectCodes
}),
defineTool({
name: "get_index_calculator",
title: "Calculate Price Linkage",
description: "Inflation-adjust a monetary amount between two dates using a specific index (the official CBS linkage calculator). Dates: use yyyy-mm-dd (recommended, e.g. '2020-01-15'); dd-mm-yyyy is auto-corrected only when unambiguous (day>12) \u2014 otherwise it is read as the CBS mm-dd-yyyy convention. Some series (e.g. 110050) legitimately return null coefficient bounds. For housing indices, avoid toDate values inside the provisional window flagged in the summary. Use indexCode 120010 for general CPI.",
inputSchema: getIndexCalculatorSchema