About bls-mcp-server
bls-mcp-server lets researchers, analysts, and other data users find and retrieve US Bureau of Labor Statistics series in plain language. It supports historical and latest observations, with optional SQL analysis for large result sets.
Key features
- 01Search series by text, survey, or area
- 02Retrieve historical BLS observations
- 03Fetch latest series values
- 04Calculate period-over-period changes
- 05Optional DuckDB-backed SQL analysis
Use cases
- 01Find the right series without decoding IDs
- 02Track inflation, employment, or wage measures
- 03Compare multiple series over a selected period
- 04Run SQL analysis on staged large results
MCP support
Exposes BLS survey, series search, and data retrieval tools to MCP clients, with optional dataframe tools when DuckDB is configured. Available over STDIO and Streamable HTTP, including a hosted endpoint.
Configuration
Local server config is the same for every client. Copy it into your client’s MCP config file.
npx -y @cyanheads/bls-mcp-serverBLS_API_KEYSecretOptional — works without one (25 req/day). With key: 500 req/day + calculations and annual averages. Register free at bls.gov/developers.MCP_LOG_LEVELSets the minimum log level for output (e.g., 'debug', 'info', 'warn').MCP_HTTP_HOSTThe hostname for the HTTP server.MCP_HTTP_PORTThe port to run the HTTP server on.MCP_HTTP_ENDPOINT_PATHThe endpoint path for the MCP server.MCP_AUTH_MODESecretAuthentication mode to use: 'none', 'jwt', or 'oauth'.Local servers need a runtime on your machine (e.g. Node.js 18+). Config file locations differ by client; see your client’s docs.
Tools
Local servers expose tools only once running on your machine, and the maintainer has not provided a tool list.
Details
From the project README@cyanheads/bls-labor-mcp-server
Fetch US Bureau of Labor Statistics data — CPI, unemployment, wages, JOLTS, and more via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://bls-labor.caseyjhand.com/mcp
Overview
US labor statistics from the Bureau of Labor Statistics: jobs, unemployment, wages, and inflation. Find series in plain language instead of decoding SeriesIDs, pull full history or the latest value, and run SQL over large multi-series results.
Tools
| Tool | Description |
|---|---|
bls_list_surveys | List BLS survey programs (CPI, CPS, CES, JOLTS, PPI, OEWS, …) with codes, descriptions, and calculation-support flags. |
bls_search_series | Search the BLS series catalog by natural language, survey, area, or keywords to resolve cryptic SeriesIDs. |
bls_get_series | Fetch time-series data for 1–50 BLS series by SeriesID, with optional year range and period-over-period calculations. |
bls_get_latest | Return the single most recent observation for one or more BLS series. |
bls_dataframe_describe | List canvas dataframes registered by bls_get_series or bls_dataframe_query — provenance, TTL, row count, column schema. Available when CANVAS_PROVIDER_TYPE=duckdb. |
bls_dataframe_query | Run a SELECT against canvas dataframes registered by bls_get_series or an earlier register_as. Supports JOINs, aggregates, window functions, CTEs. Available when CANVAS_PROVIDER_TYPE=duckdb. |
bls_dataframe_drop | Drop a canvas dataframe by name. Available when CANVAS_PROVIDER_TYPE=duckdb and BLS_DATAFRAME_DROP_ENABLED=true; TTL handles cleanup by default. |
Capability reference
bls_list_surveys tool
- Optional
categoryfilters survey programs by subject; returns codes, names, and calculation-support flags (allowsNetChange,allowsPercentChange,hasAnnualAverages). - Backed by the BLS surveys API with monthly caching. Annual-average support is advisory; read
bls_get_series'sannualAverageRowsfor the rows actually returned.
bls_search_series tool
- Search by text or SeriesID, with optional
survey,area(substring of area, title, or SeriesID), andseasonal_adjustment; blank filters are omitted.limitis 1–50 (default 10), withoffsetpagination. Without an area filter, place names prefer matching decoded areas; national series win equal-score ties within each preference group. Exact SeriesIDs rank first. - Returns decoded SeriesIDs, titles,
frequency, and independently optionalstartYear,endYear, andendPeriodcoverage (for example,1982–2005 Q04). Coverage describes the catalog snapshot, not continuous observations. FollownextOffsetwhiletruncatedis true;capped: truemakestotalCounta lower bound, and paging stops at the FTS candidate pool. - Searches the offline LABSTAT index without using API quota; OE/OEWS is opt-in through
BLS_CATALOG_INCLUDE_OES. Unindexed surveys remain fetchable by SeriesID.
bls_get_series tool
- Fetch 1–50 SeriesIDs per call, with a
start_year/end_yearwindow of up to 20 years, optionalcalculations, andannual_average. A start alone resolves the end to the current year capped at start + 19; an end alone is rejected. One batch consumes one API query; calculations depend on survey support (CPI/PPI return percent change only). - Returns observations with
available, preserves the raw-missing-value sentinel, and keeps valid series in mixed batches. Enrichment reports the applied year window, calculations, and annual-average rows. - Large results spill to
dataset.namewhenCANVAS_PROVIDER_TYPE=duckdb; otherwisecanvas_unavailableasks for a narrower window.
bls_get_latest tool
- Fetch the latest observation for up to 50 SeriesIDs (recommended ≤10). Each ID consumes one API query; use
bls_get_seriesfor a more efficient multi-series batch. - Returns successful
results[]beside per-seriesfailed[]entries.latestObservation.availabledistinguishes a published value from BLS's missing-value sentinel.
bls_dataframe_describe tool
- Optional
namedescribes one registered dataframe; omit it to list active dataframes for the tenant. RequiresCANVAS_PROVIDER_TYPE=duckdb. - Returns source tool, query params, row count, TTL (
created_at/expires_at), andcolumn_schema; BLS columns are nullable. Registered-query types outside the canvas set (DECIMAL,HUGEINT,SMALLINT, lists) report asVARCHAR. Expired entries are removed when their canvas drop succeeds.
bls_dataframe_query tool
- Run one SELECT against registered tables, with JOINs, aggregates, window functions, or CTEs.
row_limitdefaults to 1000 and caps at 10000; writes, external-file reads, and system catalogs are denied. - Engine settings (
current_setting,current_schema,current_schemas,in_search_path) andquery_tableare denied.query()accepts one plain SQL string literal, checked recursively up to four levels; computed SQL strings are rejected. Reference staged tables directly for analysis. - Returns rows without consuming BLS API quota. Optional
register_asstages the result with a fresh TTL for chained analysis; requiresCANVAS_PROVIDER_TYPE=duckdb.
bls_dataframe_drop tool
- Required
nameidentifies the dataframe to drop. Available only withCANVAS_PROVIDER_TYPE=duckdbandBLS_DATAFRAME_DROP_ENABLED=true; TTL handles cleanup by default. - Returns
dropped: falsefor a missing table. A failed drop returns retryablecanvas_drop_failedand leaves the dataframe registered.
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
BLS-specific:
- BLS API v2 client with retry/backoff and daily quota tracking
- Offline series catalog search against LABSTAT flat files, indexed as an on-disk SQLite/FTS5 store — zero API quota for discovery; the OES/OEWS wage survey (~6M series) is opt-in via
BLS_CATALOG_INCLUDE_OES - Typed error contracts for BLS-specific failure modes — quota exhaustion, locked database, calculations not supported
- Period-over-period net/percent-change calculations via BLS's own server-side flag, consistent with BLS's published numbers
- Optional DataCanvas spillover (DuckDB) for large multi-series result sets — schema discovery and SQL access without re-querying the API
Agent-friendly output:
- Provenance — canvas-spilled results carry a
dataset.namehandle plus row count and expiry;bls_search_seriesechoeseffectiveQuery,catalogSize, and whether the FTS candidate pool wascapped - Graceful partial failure —
bls_get_latestreturns per-itemfailed[](seriesId + error) alongside successfulresults[]instead of failing the whole batch;bls_get_serieskeeps valid series when another SeriesID in the same batch is invalid or empty - Discriminated outputs — every observation carries an
availableboolean for BLS's-missing-value sentinel, so callers branch on a typed field instead of parsing raw values - Actionable notices —
enrichment.noticeexplains empty results, canvas spillover, and unavailable data with concrete next steps (e.g. usingbls_search_seriesto verify a SeriesID)
Getting started
Public Hosted Instance
A public instance is available at https://bls-labor.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"bls-labor-mcp-server": {
"type": "streamable-http",
"url": "https://bls-labor.caseyjhand.com/mcp"
}
}
}
Self-Hosted / Local
Add the following to your MCP client configuration file. A free BLS API key unlocks 500 queries/day — register at bls.gov/developers. The server works without a key at 25 req/day.
{
"mcpServers": {
"bls-labor-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/bls-labor-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"BLS_API_KEY": "your-key-here"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"bls-labor-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/bls-labor-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"BLS_API_KEY": "your-key-here"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"bls-labor-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "-e", "BLS_API_KEY=your-key-here", "ghcr.io/cyanheads/bls-labor-mcp-server:latest"]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_SESSION_MODE=stateless MCP_HTTP_PORT=3010 BLS_API_KEY=... bun run start:http
# Server listens at http://localhost:3010/mcp
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- A free BLS API v2 key — register at bls.gov/developers. Grants 500 queries/day; the server also works without a key at 25 req/day.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/bls-labor-mcp-server.git
- Navigate into the directory:
cd bls-labor-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# edit .env and set BLS_API_KEY
Configuration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts.
| Variable | Description | Default |
|---|---|---|
BLS_API_KEY | BLS v2 API key. Optional — 25 req/day without, 500 req/day with. Register free at bls.gov/developers. | — |
BLS_BASE_URL | BLS API v2 base URL. | https://api.bls.gov/publicAPI/v2 |
BLS_CATALOG_BASE_URL | LABSTAT flat-file base URL. Override to point at a local mirror. | https://download.bls.gov/pub/time.series |
BLS_CATALOG_DB_PATH | On-disk SQLite catalog index — queried on demand and persisted across restarts. Empty uses an in-memory DB (re-harvested each boot). Mount a volume here in containers. | .cache/bls-catalog.db |
BLS_CATALOG_CACHE_TTL_HOURS | Catalog freshness window in hours — re-harvest once the index is older, checked at startup and hourly while running. | 168 (7 days) |
BLS_CATALOG_INCLUDE_OES | Include the OES/OEWS wage survey (~6M series / ~1.2 GB; multi-minute first harvest). Off by default — OES series stay fetchable by ID. | false |
BLS_OBSERVATIONS_MIRROR_ENABLED | Serve observations from a local SQLite mirror instead of the live API (requires a one-time bootstrap — see below). | false |
BLS_DATASET_TTL_SECONDS | Per-dataframe TTL for canvas-registered tables, in seconds. | 86400 (24 h) |
BLS_DATAFRAME_DROP_ENABLED | Expose bls_dataframe_drop. TTL handles cleanup by default. | false |
CANVAS_PROVIDER_TYPE | Set to duckdb to enable DataCanvas tabular spillover for large result sets. | none |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_SESSION_MODE | Session mode. This server uses stateless; valid schema values are auto, stateful, and stateless. Schema-default auto resolves to stateful. | stateless |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
OTEL_ENABLED | Enable OpenTelemetry instrumentation. | false |
OTEL_EXPORTER_OTLP_ENDPOINT | Base OTLP URL for traces and metrics; signal-specific endpoints override it. | — |
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT | Opt-in OTLP log endpoint. The base URL never enables log export. | — |
LOG_TOOL_FAILURE_PAYLOADS | Log failed-call arguments and results, redacted by key name; secrets in free-form values remain. | false |
LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES | UTF-8 byte cap per logged failure payload. | 16384 |
See .env.example for the full list of optional overrides.
Observation mirror (optional)
For high-volume workloads, an opt-in local mirror serves bls_get_series / bls_get_latest from an embedded SQLite store instead of the BLS API — eliminating the 500/day quota cap. It is off by default. To enable:
-
Set
BLS_OBSERVATIONS_MIRROR_ENABLED=true(and review theBLS_OBSERVATIONS_MIRROR_*vars in.env.example). -
Run the one-time bootstrap out-of-band — it downloads the full LABSTAT observation set and can take a while:
node dist/services/bls-observations/subprocess.js --init
The mirror harvests every survey in the catalog's survey list — OE included, whatever BLS_CATALOG_INCLUDE_OES says — so it covers CM and CI too (about 55 MB of cm.data.* / ci.data.* files). An incremental refresh reads only files published after the mirror's last one, so a mirror bootstrapped before CM and CI joined picks each file up at its next BLS publication; until then those series come from the live API when fallback is on. Until the bootstrap completes, requests fall back to the live API (unless BLS_OBSERVATIONS_MIRROR_FALLBACK_LIVE=false). When a live fallback fails — quota exhausted, BLS unreachable — the series the mirror served are still returned, and enrichment.notice names each unserved SeriesID with the failure's reason and recovery. Mirror rows carry no BLS calculations: with calculations: true, a mirror-served series comes back without calculation fields, enrichment.calculationsApplied is false, and the notice names it. On HTTP transport, an incremental refresh runs on the BLS_OBSERVATIONS_MIRROR_REFRESH_CRON schedule. In containers, mount a persistent volume at BLS_OBSERVATIONS_MIRROR_PATH.
Upgrading an existing mirror. A mirror bootstrapped before sentinel rows were stored is missing the periods BLS publishes with its - missing-value marker. Opening such a mirror clears its sync checkpoint once, so the next refresh re-reads every LABSTAT file and fills them in — no operator action beyond letting that refresh run, and it takes as long as a full read. The mirror keeps serving throughout.
Running the server
Local development
-
Build and run:
# One-time build bun run rebuild # Run the built server bun run start:stdio # or bun run start:http -
Run checks and tests:
bun run devcheck # Lint, format, typecheck, security bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Docker
docker build -t bls-labor-mcp-server .
docker run --rm -e BLS_API_KEY=your-key -e MCP_TRANSPORT_TYPE=http -p 3010:3010 bls-labor-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/bls-labor-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools and initializes services. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts). |
src/services/bls-api | BLS API v2 service — batch fetch, latest-value GET, surveys metadata. |
src/services/bls-catalog | LABSTAT flat-file catalog — offline series index and search. |
src/services/bls-observations | Optional LABSTAT observation mirror — embedded SQLite store, ingester, and refresh subprocess. |
src/services/bls-periods | Annual-average period semantics (M13/Q05/S03) shared by the API and mirror paths. |
src/services/canvas-bridge | DataCanvas bridge — dataframe registration, SQL gate, lifecycle management. |
docs/design.md | Full tool surface specification, service architecture, and error contracts. |
tests/ | Unit and integration tests mirroring src/. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storage bls_search_seriesis the anchor tool — design workflows to call it before the API tools- Wrap BLS API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
Apache-2.0 — see LICENSE for details.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.