@cyanheads/bls-labor-mcp-server
Enables SQL querying of BLS time-series data stored in DuckDB dataframes, supporting SELECT, joins, aggregates, window functions, and CTEs on data fetched via bls_get_series.
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 |
| List BLS survey programs (CPI, CPS, CES, JOLTS, PPI, OEWS, …) with codes, descriptions, and calculation-support flags. |
| Search the BLS series catalog by natural language, survey, area, or keywords to resolve cryptic SeriesIDs. |
| Fetch time-series data for 1–50 BLS series by SeriesID, with optional year range and period-over-period calculations. |
| Return the single most recent observation for one or more BLS series. |
| List canvas dataframes registered by |
| Run a SELECT against canvas dataframes registered by |
| Drop a canvas dataframe by name. Available when |
Related MCP server: BLS MCP
Capability reference
bls_list_surveys tool
Optional
categoryfilters survey programs by subject; returns codes, names, and calculation-support flags (allowsNetChange,allowsPercentChange,hasAnnualAverages). Each row'ssearchCoverage(indexed,composed, ornone) says whetherbls_search_seriesreaches the survey, withseriesIndexedfor indexed ones.Backed by the BLS surveys API with 30-day process-local 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, andseasonal_adjustment; blank filters are omitted.areatakes a USPS or 2-digit FIPS state code; a 3-, 4-, 5-, or 7-digit CSA, CPI area, county or CBSA, or state + place code; otherwise a substring of area, title, or SeriesID (all-digit values outside those lengths add a notice).limitdefaults to 10 and requests above 50 clamp to 50 with notice, withoffsetpagination. Exact SeriesIDs rank first, then series matching every query word, with any place the query names in full ahead of the rest; at equal score, current series rank first, then national ones, then the JOLTS aggregate.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. Undersurvey: "OE"without those rows, it composes OEWS SeriesIDs from the OE code tables, up to 50 per search; each is markedcomposed: trueand is unconfirmed untilbls_get_seriesreturns data for it. Unindexed surveys remain fetchable by SeriesID.
bls_get_series tool
Fetch up to 25 SeriesIDs without an API key or 50 with one 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 local-catalog titles when available and optional BLS
measuremetadata, and observations withavailable; preserves the raw-missing-value sentinel, and keeps valid series in mixed batches. Enrichment reports the applied year window, calculations actually returned, annual-summary rows, and any upstream reductions or advisories.SeriesIDs are trimmed, uppercased, and deduplicated before the cap;
unprocessedSeriesIdslists the exact ordered remainder without automatic extra batches. Omitted year bounds use BLS's three-calendar-year default, subject to coverage.Large results spill to
dataset.name. WithCANVAS_PROVIDER_TYPE=none, all fetched observations return inline.
bls_get_latest tool
Fetch the latest observation for up to 50 SeriesIDs (recommended ≤10). IDs are trimmed, uppercased, and deduplicated before the cap;
unprocessedSeriesIdslists the exact ordered remainder. Each processed ID consumes one API query; usebls_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. Unavailable whenCANVAS_PROVIDER_TYPE=none.Returns source tool, query params, row count, TTL (
created_at/expires_at, fixed at table creation), 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; larger values clamp to 10000, andpreviewclamps to the applied row limit with requested/applied notices. Zero preview remains valid; writes, external-file reads, and system catalogs are denied.Engine-setting functions (
current_setting,current_schema,current_schemas,in_search_path) andquery_tableare denied. Unqualifiedcurrent_schemakeyword or column references are also denied; qualify a real column with its table alias (for example,t.current_schema).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. Unavailable whenCANVAS_PROVIDER_TYPE=none.
bls_dataframe_drop tool
Required
nameidentifies the dataframe to drop. Available only withBLS_DATAFRAME_DROP_ENABLED=true, and never withCANVAS_PROVIDER_TYPE=none; 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_OESTyped 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
DataCanvas spillover (DuckDB, on by default) 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 wascappedGraceful 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 emptyDiscriminated outputs — every observation carries an
availableboolean for BLS's-missing-value sentinel, so callers branch on a typed field instead of parsing raw valuesActionable 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/mcpPrerequisites
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.gitNavigate into the directory:
cd bls-labor-mcp-serverInstall dependencies:
bun installConfigure environment:
cp .env.example .env
# edit .env and set BLS_API_KEYConfiguration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts.
Variable | Description | Default |
| BLS v2 API key. Optional — 25 req/day without, 500 req/day with. Register free at bls.gov/developers. | — |
| BLS API v2 base URL. |
|
| LABSTAT flat-file base URL. Override to point at a local mirror. |
|
| 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. |
|
| Per-survey catalog freshness window in hours — a survey re-harvests once its last success is older, and a failed survey's retry backoff is capped at this many hourly checks. Checked at startup and hourly while running. |
|
| Include the OES/OEWS wage survey (~6M series / ~1.26 GB; Bun-only; multi-minute first harvest). Off by default — OES series stay fetchable by ID, and search composes them from the small OE code tables. |
|
| Serve observations from a local SQLite mirror instead of the live API (requires a one-time bootstrap — see below). |
|
| Per-dataframe TTL for canvas-registered tables, in seconds. |
|
| Expose |
|
| DataCanvas provider for tabular spillover of large result sets and the dataframe tools. Set to |
|
| Transport: |
|
| HTTP server port. |
|
| Session mode. This server uses |
|
| Auth mode: |
|
| Log level (RFC 5424). |
|
| Directory for log files (Node.js only). |
|
| Enable OpenTelemetry instrumentation. |
|
| Base OTLP URL for traces and metrics; signal-specific endpoints override it. | — |
| Opt-in OTLP log endpoint. The base URL never enables log export. | — |
| Log failed-call arguments and results, redacted by key name; secrets in free-form values remain. |
|
| UTF-8 byte cap per logged failure payload. |
|
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 of the LABSTAT files instead of the BLS API, removing the 500/day quota cap; requests fall back to the live API until it is built. Enable it with BLS_OBSERVATIONS_MIRROR_ENABLED=true, then run the one-time bootstrap: node dist/services/bls-observations/subprocess.js --init. Settings, survey coverage, harvest checkpoint and exit codes, fallback and calculation behavior, and upgrade notes are in docs/observation-mirror.md.
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:httpRun 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-serverThe 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 |
|
|
| Server-specific environment variable parsing and validation with Zod. |
| Tool definitions ( |
| BLS API v2 service — batch fetch, latest-value GET, surveys metadata. |
| LABSTAT flat-file catalog — offline series index and search. |
| Optional LABSTAT observation mirror — embedded SQLite store, ingester, and refresh subprocess. |
| Period-code semantics shared by the API and mirror paths: annual-average codes ( |
| DataCanvas bridge — dataframe registration, SQL gate, lifecycle management. |
| Full tool surface specification, service architecture, and error contracts. |
| Observation mirror setup, coverage, and upgrade notes. |
| Unit and integration tests mirroring |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
Handlers throw, framework catches — no
try/catchin tool logicUse
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storagebls_search_seriesis the anchor tool — design workflows to call it before the API toolsWrap 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 testLicense
Apache-2.0 — see LICENSE for details.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
BLS MCP — Bureau of Labor Statistics public data API (v2)
Econdata MCP — wraps BLS (Bureau of Labor Statistics) public API v2…
BLS Occupational Employment and Wage Statistics market wages, bundled; no API key. 8 tools.
Public MCP server for discovering open jobs. Search, filter, and get application links.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables access to Bureau of Labor Statistics (BLS) economic data including Consumer Price Index, employment statistics, and other labor market indicators. Supports fetching data series, listing available datasets, and retrieving metadata through natural language queries.4MIT
- AlicenseAqualityDmaintenanceEnables users to query U.S. labor statistics, including employment, CPI, and wages, directly from the Bureau of Labor Statistics Public Data API. It provides tools to retrieve real-time economic time series data, browse popular series, and access survey metadata through natural language.688 npm1MIT
- AlicenseNot gradedqualityDmaintenanceEconomic data MCP server that connects FRED, BLS, BEA, IMF, World Bank, and ECB to any MCP-compatible client, with built-in methodology rules to guide LLMs in selecting appropriate economic indicators.MIT
- AlicenseAqualityDmaintenanceMCP server providing official US economic data including CPI inflation, famous prices, national debt, and Treasury interest rates. Enables AI agents to query historical and latest economic statistics via natural language.850 npmMIT