import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const API_KEY = process.env.BLS_API_KEY || "";
const BASE_URL = "https://api.bls.gov/publicAPI/v2/timeseries/data/";
interface BLSRequest {
seriesid: string[];
startyear: string;
endyear: string;
registrationkey?: string;
calculations?: boolean;
annualaverage?: boolean;
}
async function blsRequest(params: BLSRequest): Promise<any> {
const body: BLSRequest = {
...params,
registrationkey: API_KEY,
};
const response = await fetch(BASE_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`BLS API error: ${response.status}`);
}
const data = await response.json();
if (data.status !== "REQUEST_SUCCEEDED") {
throw new Error(`BLS API error: ${data.message?.join(", ") || "Unknown error"}`);
}
return data.Results;
}
// Common series ID prefixes for reference
const SERIES_GUIDE = {
CPI: {
prefix: "CUUR0000",
description: "Consumer Price Index - Urban",
examples: {
"CUUR0000SA0": "CPI - All items",
"CUUR0000SAF1": "CPI - Food",
"CUUR0000SAH1": "CPI - Housing",
"CUUR0000SAM": "CPI - Medical care",
"CUUR0000SAT": "CPI - Transportation",
},
},
UNEMPLOYMENT: {
prefix: "LNS1400",
description: "Unemployment rates",
examples: {
"LNS14000000": "National unemployment rate",
"LNS14000001": "Unemployment rate - Men",
"LNS14000002": "Unemployment rate - Women",
},
},
EMPLOYMENT: {
prefix: "CES",
description: "Employment by industry",
examples: {
"CES0000000001": "Total nonfarm employment",
"CES0500000001": "Total private employment",
"CES1000000001": "Mining and logging",
"CES2000000001": "Construction",
"CES3000000001": "Manufacturing",
},
},
QCEW: {
prefix: "ENU",
description: "Quarterly Census of Employment & Wages",
examples: {
"ENU0600010010": "California - All industries - All establishment sizes",
},
},
};
const server = new Server(
{ name: "bls", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "query",
description: "Fetch BLS time series data. Returns monthly/annual data for up to 50 series.",
inputSchema: {
type: "object",
properties: {
seriesIds: {
type: "array",
items: { type: "string" },
description: "Array of series IDs (max 50). Use series_guide to find IDs.",
},
startYear: { type: "string", description: "Start year (e.g., '2020')" },
endYear: { type: "string", description: "End year (e.g., '2024')" },
calculations: {
type: "boolean",
description: "Include percent changes (default: false)",
},
annualAverage: {
type: "boolean",
description: "Include annual averages (default: false)",
},
},
required: ["seriesIds", "startYear", "endYear"],
},
},
{
name: "series_guide",
description: "Show common BLS series IDs and how to construct them",
inputSchema: {
type: "object",
properties: {
category: {
type: "string",
description: "Category: 'CPI', 'UNEMPLOYMENT', 'EMPLOYMENT', 'QCEW', or 'ALL'",
},
},
required: [],
},
},
{
name: "get_cpi",
description: "Shortcut: Get Consumer Price Index data",
inputSchema: {
type: "object",
properties: {
startYear: { type: "string", description: "Start year" },
endYear: { type: "string", description: "End year" },
items: {
type: "string",
description: "Items to include: 'all', 'food', 'housing', 'medical', 'transport', or comma-separated",
},
},
required: ["startYear", "endYear"],
},
},
{
name: "get_unemployment",
description: "Shortcut: Get national unemployment rate",
inputSchema: {
type: "object",
properties: {
startYear: { type: "string", description: "Start year" },
endYear: { type: "string", description: "End year" },
},
required: ["startYear", "endYear"],
},
},
{
name: "get_employment",
description: "Shortcut: Get employment by major industry sector",
inputSchema: {
type: "object",
properties: {
startYear: { type: "string", description: "Start year" },
endYear: { type: "string", description: "End year" },
sector: {
type: "string",
description: "Sector: 'total', 'private', 'mining', 'construction', 'manufacturing', 'retail', 'finance', 'professional', 'healthcare', 'leisure', 'government', or 'all'",
},
},
required: ["startYear", "endYear"],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "query": {
const { seriesIds, startYear, endYear, calculations, annualAverage } = args as any;
const result = await blsRequest({
seriesid: seriesIds,
startyear: startYear,
endyear: endYear,
calculations,
annualaverage: annualAverage,
});
// Format the data more readably
const formatted = result.series.map((s: any) => ({
seriesId: s.seriesID,
data: s.data.map((d: any) => ({
year: d.year,
period: d.periodName,
value: d.value,
...(d.calculations ? { changes: d.calculations } : {}),
})),
}));
return {
content: [{ type: "text", text: JSON.stringify(formatted, null, 2) }],
};
}
case "series_guide": {
const { category = "ALL" } = args as any;
const cat = category.toUpperCase();
let guide: any;
if (cat === "ALL") {
guide = SERIES_GUIDE;
} else if (SERIES_GUIDE[cat as keyof typeof SERIES_GUIDE]) {
guide = { [cat]: SERIES_GUIDE[cat as keyof typeof SERIES_GUIDE] };
} else {
guide = { error: `Unknown category: ${category}`, available: Object.keys(SERIES_GUIDE) };
}
return {
content: [{ type: "text", text: JSON.stringify(guide, null, 2) }],
};
}
case "get_cpi": {
const { startYear, endYear, items = "all" } = args as any;
const itemMap: Record<string, string> = {
all: "CUUR0000SA0",
food: "CUUR0000SAF1",
housing: "CUUR0000SAH1",
medical: "CUUR0000SAM",
transport: "CUUR0000SAT",
energy: "CUUR0000SA0E",
apparel: "CUUR0000SAA",
};
let seriesIds: string[];
if (items === "all") {
seriesIds = [itemMap.all];
} else {
seriesIds = items.split(",").map((i: string) => itemMap[i.trim()] || i.trim());
}
const result = await blsRequest({
seriesid: seriesIds,
startyear: startYear,
endyear: endYear,
calculations: true,
});
const formatted = result.series.map((s: any) => ({
seriesId: s.seriesID,
description: Object.entries(itemMap).find(([_, v]) => v === s.seriesID)?.[0] || "Unknown",
data: s.data.map((d: any) => ({
year: d.year,
period: d.periodName,
value: d.value,
pctChange12Month: d.calculations?.pct_changes?.["12"] || null,
})),
}));
return {
content: [{ type: "text", text: JSON.stringify(formatted, null, 2) }],
};
}
case "get_unemployment": {
const { startYear, endYear } = args as any;
const result = await blsRequest({
seriesid: ["LNS14000000"],
startyear: startYear,
endyear: endYear,
});
const formatted = result.series[0].data.map((d: any) => ({
year: d.year,
period: d.periodName,
unemploymentRate: d.value,
}));
return {
content: [{ type: "text", text: JSON.stringify(formatted, null, 2) }],
};
}
case "get_employment": {
const { startYear, endYear, sector = "total" } = args as any;
const sectorMap: Record<string, string> = {
total: "CES0000000001",
private: "CES0500000001",
mining: "CES1000000001",
construction: "CES2000000001",
manufacturing: "CES3000000001",
retail: "CES4200000001",
finance: "CES5500000001",
professional: "CES6000000001",
healthcare: "CES6500000001",
leisure: "CES7000000001",
government: "CES9000000001",
};
let seriesIds: string[];
if (sector === "all") {
seriesIds = Object.values(sectorMap);
} else {
seriesIds = [sectorMap[sector] || sector];
}
const result = await blsRequest({
seriesid: seriesIds,
startyear: startYear,
endyear: endYear,
});
const formatted = result.series.map((s: any) => ({
seriesId: s.seriesID,
sector: Object.entries(sectorMap).find(([_, v]) => v === s.seriesID)?.[0] || "Unknown",
data: s.data.map((d: any) => ({
year: d.year,
period: d.periodName,
employmentThousands: d.value,
})),
}));
return {
content: [{ type: "text", text: JSON.stringify(formatted, null, 2) }],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
return {
content: [{ type: "text", text: `Error: ${error.message}` }],
isError: true,
};
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("BLS MCP server running");
}
main().catch(console.error);