bls-labor-mcp-server
Server Details
Fetch US Bureau of Labor Statistics data — CPI, unemployment, wages, JOLTS, and more via MCP.
- Status
- Healthy
- Uptime
- 100.0% over 38 days
- Last Tested
- Transport
- Streamable HTTP · MCP 2025-11-25
- URL
- Repository
- cyanheads/bls-labor-mcp-server
- GitHub Stars
- 1
- Server Listing
- @cyanheads/bls-labor-mcp-server
TDQS
Scored across 6 tools
Each tool has a clearly distinct purpose: describe dataframes, query dataframes, fetch latest series, fetch full series, list surveys, and search series. No overlapping functionality; agents can easily select the correct tool based on the task.
All tools follow a consistent 'bls_' prefix plus a verb-noun or verb-object pattern in snake_case (e.g., bls_get_series, bls_list_surveys, bls_dataframe_query). The naming is uniform and predictable.
With 6 tools, the server is well-scoped for its purpose: discovery, retrieval, and querying of BLS labor data. Each tool serves a necessary function without redundancy or excess.
The tool surface covers the full workflow from survey discovery (bls_list_surveys), series resolution (bls_search_series), data retrieval (bls_get_series, bls_get_latest), to data analysis (bls_dataframe_query). There are no obvious missing operations for the stated purpose of accessing and analyzing BLS data.
Available Tools
6 toolsbls_dataframe_describeDescribe BLS DataframesARead-onlyIdempotentInspect
List canvas dataframes materialized by bls_get_series, with provenance (source tool, query parameters), TTL, row count, and column schema. Use before writing SQL to confirm column names. Lazy-sweeps expired tables before responding. Requires CANVAS_PROVIDER_TYPE=duckdb.
| Name | Required | Description | Default |
|---|---|---|---|
| name | No | Optional table name (df_XXXXX_XXXXX) to describe a single dataframe. Omit to list all active dataframes. |
Output Schema
| Name | Required | Description |
|---|---|---|
| error | No | Present when the call failed. Absent on success. |
| dataframes | No | Active dataframes for this tenant, newest first. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already signal readOnlyHint=true and idempotentHint=true. The description adds useful behavioral context beyond annotations: it lazy-sweeps expired tables before responding and requires a specific DuckDB environment variable. This goes beyond the annotation baseline without contradicting it.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Four short, information-dense sentences with no fluff. The primary action and return contents are front-loaded, followed by usage context, behavioral notes, and environment prerequisites. Every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a tool with one optional parameter, a full output schema, strong annotations, and clear sibling context, the description is complete. It covers what the tool lists, why and when to use it, a side-effect behavior, and a required environment setting. Nothing important is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents the optional 'name' parameter, its expected format (df_XXXXX_XXXXX), and the omit-to-list-all behavior. The description adds no additional parameter-level meaning beyond what the schema provides, so the baseline of 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb ('List'), a specific resource ('canvas dataframes materialized by bls_get_series'), and the exact metadata returned (provenance, TTL, row count, column schema). This clearly distinguishes it from sibling query tools like bls_dataframe_query and from data-fetching tools like bls_get_series.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides explicit usage context: 'Use before writing SQL to confirm column names.' It also notes an environment requirement (CANVAS_PROVIDER_TYPE=duckdb). It does not explicitly name alternatives or state when not to use the tool, but the intended placement before SQL writing is clear enough.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
bls_dataframe_queryQuery BLS DataframesARead-onlyIdempotentInspect
Run a single-statement SELECT against the canvas dataframes registered by bls_get_series. Read-only: writes, DDL, DROP, COPY, PRAGMA, ATTACH, and external-file table functions are rejected. System catalogs (information_schema, pg_catalog, sqlite_master, duckdb_*) are denied at the bridge layer — use bls_dataframe_describe to list available dataframes. Supports JOINs, aggregates, window functions, and CTEs. Optional register_as persists the result as a new dataframe with a fresh TTL for chained analysis. Canvas SQL operations consume zero BLS API quota. Requires CANVAS_PROVIDER_TYPE=duckdb.
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes | Single-statement SELECT against df_<id> tables on the shared canvas. Reference dataframes by the names returned in bls_get_series responses or listed by bls_dataframe_describe. Standard DuckDB SQL — joins, aggregates, window functions, CTEs all supported. Example: SELECT series_id, year, period, value FROM df_AAAAA_BBBBB WHERE year >= '2020' ORDER BY year DESC. | |
| preview | No | Inline row preview count. Defaults to row_limit. Set lower (e.g. 50) when chaining via register_as and only a sample is needed immediately. | |
| row_limit | No | Hard cap on rows materialized in the response (default 1000, max 10000). Full results live on-canvas under register_as when provided. | |
| register_as | No | When set, persist the query result as a new dataframe under this name. Fresh TTL — not inherited from parent tables. Use to chain analyses without re-running source SQL or consuming additional BLS quota. |
Output Schema
| Name | Required | Description |
|---|---|---|
| cap | No | The preview or row_limit cap that was applied. |
| rows | No | Materialized rows, bounded by preview / row_limit. |
| error | No | Present when the call failed. Absent on success. |
| shown | No | Number of rows returned inline. |
| notice | No | Guidance when results were capped by preview or row_limit — names which parameter was the binding limiter and suggests how to retrieve the rest. Absent when all rows fit in the response. |
| columns | No | Column names in projection order. |
| row_count | No | Rows materialized by the query. Exact when register_as is used; otherwise equals row_limit when truncated is true. |
| truncated | No | True when the returned rows were capped. |
| expires_at | No | ISO 8601 expiry for the newly registered dataframe, when applicable. |
| registered_as | No | Set when register_as was supplied and the result was materialized. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and idempotentHint=true, but the description adds substantial context: the specific rejected operations (writes, DDL, DROP, COPY, PRAGMA, ATTACH, external-file functions), denial of system catalogs at the bridge layer, the requirement for CANVAS_PROVIDER_TYPE=duckdb, support for JOINs/aggregates/window functions/CTEs, and the fresh TTL behavior for register_as. This goes well beyond the annotations and gives an agent a clear model of what the tool can and cannot do.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is multi-sentence but every sentence earns its place: purpose, restrictions, catalog denial, supported features, register_as, quota, and environment. It is front-loaded with the primary purpose and then logically expands. It is slightly dense but well-organized, with no filler. A very minor reduction in length could improve scannability, but it is effective.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a complex SQL query tool with four parameters and an output schema, the description is thorough. It covers how to reference dataframes, what SQL features are supported, security restrictions, the environment requirement, and chaining via register_as. The existence of an output schema covers return format. Nothing critical is missing for an agent to invoke it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so each parameter has a description. The description adds extra value: it clarifies that dataframes are referenced as df_<id> tables derived from bls_get_series responses, and that register_as creates a dataframe with a fresh TTL not inherited from parents. It also mentions chaining and quota-free operation. These additions enhance understanding beyond the schema, though the schema already covers the basics.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb and resource: 'Run a single-statement SELECT against the canvas dataframes registered by bls_get_series.' It clearly distinguishes itself from sibling tools by focusing on querying existing dataframes, and it explicitly points to bls_dataframe_describe for listing available dataframes. The read-only scope and supported SQL features are unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly says to use bls_dataframe_describe to list dataframes, providing an alternative for a different purpose. It also states that Canvas SQL operations consume zero BLS API quota, implying a cost advantage over re-fetching via bls_get_series. It mentions the CANVAS_PROVIDER_TYPE=duckdb prerequisite. It does not explicitly say when not to use this tool versus siblings, but the context is clear enough for an agent to route correctly.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
bls_get_latestGet Latest BLS ObservationARead-onlyIdempotentInspect
Return the single most recent observation for one or more BLS series. Use for "what is X right now" questions — the current unemployment rate, the latest CPI reading, etc. Each series consumes one API query against the 500/day limit; for the current value of many series, bls_get_series with a 1-year window is more quota-efficient (one query for up to 50 series). Recommended limit: 10 series; maximum: 50.
| Name | Required | Description | Default |
|---|---|---|---|
| series_ids | Yes | One or more BLS SeriesIDs (1–50). Each consumes one daily API query. Use bls_search_series to resolve concepts to SeriesIDs. Recommended: ≤10 series. |
Output Schema
| Name | Required | Description |
|---|---|---|
| error | No | Present when the call failed. Absent on success. |
| failed | No | Series that failed to fetch. Inspect seriesId and error for per-item details. Not-found series appear here rather than as a tool-level error. |
| notice | No | Guidance when one or more series failed — e.g. to use bls_search_series to verify SeriesIDs. Absent when all series returned data. |
| results | No | Successfully fetched series with their latest observations. Series that failed appear in failed[] instead. |
| succeeded | No | Number of series with a successfully fetched observation. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already mark it read-only and idempotent, and the description adds useful behavioral context: each series consumes one API query against a 500/day limit, and the tool recommends a 10-series limit to manage quota. This goes beyond what the annotations convey and helps with call planning.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three focused sentences carry purpose, usage guidance, quota behavior, alternatives, and limits. The description is front-loaded and every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With one parameter, a full input schema, an output schema, and safety annotations, the description covers the essential selection criteria, quota impact, and the relevant alternative. Nothing critical is missing for correct invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema has 100% coverage for the single series_ids parameter, including types, bounds, and the recommendation to use bls_search_series. The description mostly restates this information, so it adds little new parameter-level meaning; the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource: 'Return the single most recent observation for one or more BLS series.' It also distinguishes itself from the sibling bls_get_series by noting the quota-efficiency tradeoff, so an agent can tell them apart without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly orients the tool to 'what is X right now' questions and names bls_get_series as the better alternative when many series are needed. It also provides concrete limits (recommended 10, maximum 50), giving clear selection criteria.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
bls_get_seriesGet BLS Time-Series DataARead-onlyInspect
Fetch time-series data for 1–50 BLS series by SeriesID in a single API request (one query against the 500/day limit). Supports optional year range (up to 20 years per request) and BLS-computed period-over-period calculations (net change and percent change; a survey returns whichever it supports and silently omits the rest — CPI and PPI return percent change only, the inflation rate). BLS can publish a '-' missing-value sentinel; check observation.available before arithmetic. Set annual_average to add each year's annual-average row, which is that year's mean rather than an additional period. When the total observation count would exceed the inline context budget, results spill to a canvas dataframe and the response includes a dataset.name handle. Call bls_dataframe_describe with that name to inspect the dataframe schema, then use the name in bls_dataframe_query SQL. Use bls_search_series first if you need to resolve a concept to a SeriesID.
| Name | Required | Description | Default |
|---|---|---|---|
| end_year | No | End year for the data range (inclusive). Supplying it without start_year is rejected before the request — BLS applies no default start year alongside an explicit end year. Pair it with start_year, or omit both for the API default window. | |
| series_ids | Yes | One or more BLS SeriesIDs (1–50). The entire batch counts as one API query. Use bls_search_series to resolve concepts to SeriesIDs. | |
| start_year | No | Start year for the data range (inclusive). The BLS API allows up to 20 years per request and requires both bounds or neither: supplying start_year alone resolves end_year to the current year, capped at start_year + 19 so the window stays inside the 20-year limit. Omit both for the API default (typically 3–20 years depending on survey). | |
| calculations | No | When true, request BLS-computed period-over-period calculations. The flag is a single boolean (you cannot select an individual calculation type), but the API returns whichever the survey supports and omits the rest — CPI and PPI return percent change only (the inflation rate), and a survey that supports neither simply returns its observations without calculation fields. Requesting calculations never fails, so it is always safe to set; consult bls_list_surveys (allowsNetChange / allowsPercentChange) only to predict which fields will come back. Monthly-cadence series return each supported change type over 1, 3, 6, and 12-month intervals; other cadences return a subset. | |
| annual_average | No | When true, add each year's annual-average row to the observations. An annual average is the mean of that year's real periods, returned as an extra row named "Annual" with period M13 (monthly series), Q05 (quarterly) or S03 (semiannual) — not an additional month or quarter, so it must be excluded from any sum or average over observations. Defaults to false, which returns real periods only; check available before aggregating. Independent of start_year/end_year. Surveys that publish no annual averages return the same rows either way; enrichment.annualAverageRows reports how many rows were actually added. |
Output Schema
| Name | Required | Description |
|---|---|---|
| error | No | Present when the call failed. Absent on success. |
| notice | No | Guidance for agents — names any SeriesID that returned zero observations, reports a resolved end_year the 20-year window capped, and reports the bls_dataframe_describe then bls_dataframe_query workflow when results spill to canvas. Absent when every requested series returned data over the window as asked and it all fit inline. |
| series | No | Series data, in request order. |
| dataset | No | Canvas dataframe handle — present when the observation volume exceeded the inline budget. Call bls_dataframe_describe with dataset.name to inspect column_schema, then use that table name in bls_dataframe_query SQL across the full data. |
| spilled | No | True when results spilled to canvas due to inline budget overflow. |
| endYearApplied | No | End year applied to the query. Resolved from start_year when end_year was omitted, so it can differ from the requested range; notice names the cap when the 20-year window decided it. Absent when no year range was in effect. |
| seriesRequested | No | Number of SeriesIDs requested. Do not compare it against series[] length to find empty series — a SeriesID that returned no data is still listed in series[] with observationCount 0. Check observationCount per entry, or read notice, which names every SeriesID that came back empty. |
| startYearApplied | No | Start year applied to the query, whether it was served live or from the local observation mirror. Absent when no year range was in effect. |
| annualAverageRows | No | How many observations across all series are annual-average rows (period M13/Q05/S03). Present only when annual_average is true; 0 means none of the requested surveys publish annual averages. |
| totalObservations | No | Total observation rows across all requested series. |
| calculationsApplied | No | Whether BLS net/percent-change calculations were requested. |
| annualAverageApplied | No | Whether annual-average rows were requested. When false, observations hold real periods only; filter on available before aggregation. |
| availableObservations | No | Rows with a published numeric value. |
| unavailableObservations | No | Rows carrying the BLS "-" missing-value sentinel. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations include readOnlyHint=true and openWorldHint=true, but the description adds substantial behavioral detail: the API's silent omission of calculations, the '-' missing-value sentinel, the annual average being a mean rather than a period, and the spill-to-dataframe behavior. The description does not contradict the annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is dense but front-loaded with the core function and limits, then covers important behavioral caveats. It is longer than strictly minimal, but each sentence adds value (e.g., sentinel, annual average semantics). It is well-structured but slightly verbose for the technical content, hence a 4.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity (5 parameters, 100% schema coverage, output schema present, multiple sibling tools), the description covers all critical aspects: request limits, year range rules, calculations behavior, annual average semantics, missing-value handling, and dataframe spill workflow. The presence of output schema and rich schema coverage means the description doesn't need to explain return values, so nothing essential is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the schema already documents all parameters well. The description adds context like the 20-year limit and the annual average's period code (M13, Q05), but it mostly paraphrases the schema. It does not introduce major new meaning beyond the schema, so a baseline of 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb ('Fetch') and resource ('time-series data for 1–50 BLS series'), and clearly distinguishes itself from siblings by mentioning use cases like bls_search_series for resolving concepts and the dataframe tools for spills. The scope and limits are explicit, so an agent knows exactly what this tool does relative to others.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It provides explicit when-to-use guidance: 'Use bls_search_series first if you need to resolve a concept to a SeriesID', and explains when results spill to a dataframe, directing to bls_dataframe_describe and bls_dataframe_query. It also clarifies when to omit year parameters. Alternatives are named and conditions are given, leaving no ambiguity.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
bls_list_surveysList BLS SurveysARead-onlyIdempotentInspect
List BLS survey programs with their abbreviation codes, full names, and metadata about calculation support and annual averages. Use to discover which survey covers a topic before calling bls_search_series; bls_search_series covers only the surveys in its offline index, and series in the others are fetched by SeriesID with bls_get_series. Optional category filter narrows results to prices, employment, wages, productivity, injuries, or time_use surveys.
| Name | Required | Description | Default |
|---|---|---|---|
| category | No | Optional category filter. One of: prices, employment, wages, productivity, injuries, time_use. A survey can appear under more than one category (CES under employment and wages). Omit to list all surveys. |
Output Schema
| Name | Required | Description |
|---|---|---|
| error | No | Present when the call failed. Absent on success. |
| total | No | Total surveys returned. |
| surveys | No | BLS survey programs matching the filter, sorted alphabetically by abbreviation. |
| categoryFilter | No | Category filter applied, if any. Absent when all surveys were listed. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already carry readOnlyHint=true and idempotentHint=true, so the description's job is lighter. It adds useful context about the nature of the data (metadata about calculation support and annual averages) and clarifies that the tool is a discovery step with optional filtering. No extra behaviors are disclosed, but given the annotations, this is adequate.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two sentences with zero redundancy. The core purpose is front-loaded, the usage guidance is embedded, and the category filter is mentioned last. Every sentence earns its place with no filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple list/discovery tool with one optional parameter and an existing output schema, the description covers purpose, usage context, and relationship to siblings. Nothing critical is missing—an agent can safely call it without needing additional information.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%; the category parameter is already fully explained in the schema (enum values, that a survey can appear under multiple categories). The description merely repeats the enum list, adding no new meaning. Baseline 3 is appropriate because the schema does the heavy lifting.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb ('List') and resource ('BLS survey programs') with details about what is included (abbreviation codes, full names, metadata). It distinguishes itself from siblings by explicitly contrasting coverage with bls_search_series and bls_get_series, so an agent can tell exactly which tool fits.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives explicit when-to-use guidance: 'Use to discover which survey covers a topic before calling bls_search_series.' It also explains the limitation of bls_search_series (offline index) and the alternative for other series (bls_get_series), making the decision path unambiguous.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
bls_search_seriesSearch BLS SeriesARead-onlyInspect
Search the BLS series catalog by natural language query, survey code, geographic area, or keywords to resolve cryptic SeriesIDs. Returns matching series with decoded components (survey, area, item, seasonal flag) and plain-language names, ranked by relevance and paged with limit and offset. Use this before bls_get_series when you have a concept but not a SeriesID. Operates offline against an index of the major surveys — no API quota consumed. Survey filter accepts the two-letter code of an indexed survey (CU, AP, CE, LN, LA, PC, WP, JT, EC, PR, MP, CW; OE only when the server sets BLS_CATALOG_INCLUDE_OES=true); series in other surveys are fetched by SeriesID with bls_get_series. Area filter accepts state names, MSA names, or FIPS area codes.
| Name | Required | Description | Default |
|---|---|---|---|
| area | No | State name, MSA name, or FIPS area code to narrow results to a geographic area — a case-insensitive substring of the area name, title, or SeriesID. Omit for national series, which then rank first among equal matches. | |
| limit | No | Maximum number of results to return (1–50, default 10). | |
| query | Yes | Natural language or keyword query (e.g. "unemployment rate", "CPI food", "nonfarm payrolls"). Also accepts a SeriesID directly for exact lookup. | |
| offset | No | Number of ranked results to skip, for paging past the first page (default 0). Pass nextOffset from the previous response to get the next page. | |
| survey | No | Two-letter LABSTAT survey abbreviation to filter results, case-insensitive (e.g. CU for CPI, CE for CES, LN for CPS, LA for LAUS, JT for JOLTS). Indexed surveys: CU, AP, CE, LN, LA, PC, WP, JT, EC, PR, MP, CW; OE (OEWS) only when the server sets BLS_CATALOG_INCLUDE_OES=true. Omit to search all indexed surveys. | |
| seasonal_adjustment | No | When true, return only seasonally adjusted series. When false, return only not-seasonally-adjusted. Omit to return both. |
Output Schema
| Name | Required | Description |
|---|---|---|
| cap | No | The result limit that capped the returned list. |
| error | No | Present when the call failed. Absent on success. |
| shown | No | Number of series returned in this response. |
| capped | No | True when the FTS candidate pool, after the survey/area/seasonal filters, reached the internal cap (~1000). totalCount is then a lower bound and offset paging stops at the pool. Narrow the query, add filters, or use a direct SeriesID to get an exact count. |
| notice | No | Guidance on the result: the survey is not in the offline index, the offset is past the last result, nothing matched, which offset fetches the next page, or, on the last page of a capped list, that other series may also match. Absent when this page ends an uncapped list with results. |
| series | No | Matching series, ordered by relevance. |
| truncated | No | True when ranked results remain past this page; nextOffset fetches them. |
| areaFilter | No | Area filter applied, trimmed. Absent when no area filter was passed or it was blank. |
| nextOffset | No | Offset of the next page. Present only when truncated is true. |
| totalCount | No | Total ranked results after every filter (area included), before limit and offset. A lower bound when capped is true — the catalog index may contain more matching series. |
| catalogSize | No | Total series in the loaded catalog index. Distinguishes an empty-result search from a failed catalog load. |
| limitApplied | No | Result limit in effect (defaults to 10 when omitted). |
| surveyFilter | No | Survey filter applied, trimmed and uppercased. Absent when no survey filter was passed or it was blank. |
| offsetApplied | No | Offset in effect (defaults to 0 when omitted). |
| effectiveQuery | No | Query string as the server received and searched on. Confirms interpretation for self-correction. |
| seasonalFilter | No | Seasonal-adjustment filter applied, if any. Absent when not passed. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and openWorldHint=true, so the safety profile is covered. The description adds valuable behavioral details: it operates offline against an index of major surveys, returns results ranked by relevance, pages with limit and offset, and has a conditional behavior (OE survey only when the server sets BLS_CATALOG_INCLUDE_OES=true). These go beyond the annotations and schema, giving the agent a full picture of the tool's behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured and front-loaded with the core purpose, followed by usage guidance and then specifics like offline operation and survey codes. Every sentence adds value—no filler or redundancy. It is appropriately sized for a tool with six parameters and a rich behavior profile.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity (6 params, output schema, annotations), the description is remarkably complete. It covers purpose, usage differentiation, offline operation, survey codes, conditional inclusion, area filter semantics, and paging. Since an output schema exists, the absence of detailed return format is acceptable. Nothing an agent needs to correctly invoke the tool is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents all parameters. The description adds a few extra nuances not in the schema, such as the survey code list and the OE conditional, and notes that the query also accepts a SeriesID for exact lookup. It also mentions that omitting the area filter lets national series rank first. These are helpful but not extensive, so a 4 is appropriate (above the baseline 3 for full schema coverage).
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb (search) and resource (BLS series catalog), explains the purpose (resolving cryptic SeriesIDs), and explicitly differentiates from the sibling bls_get_series by saying 'Use this before bls_get_series when you have a concept but not a SeriesID.' It also details what it returns (decoded components, plain-language names, ranked and paged). This is clear and distinct.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly instructs when to use this tool versus bls_get_series, noting that series not in the indexed surveys should be fetched by SeriesID with bls_get_series. It also highlights the offline operation and no API quota consumed, which is a practical advantage. This leaves no ambiguity about the appropriate context.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
2 tool updates
- Changed
bls_list_surveys1 field changed- changed
Input schema / properties / category / descriptionPrevious value: -"Optional category filter. One of: prices, employment, wages, productivity, injuries, time_use. Omit to list all surveys."New value: +"Optional category filter. One of: prices, employment, wages, productivity, injuries, time_use. A survey can appear under more than one category (CES under employment and wages). Omit to list all surveys."
- Changed
bls_search_series12 fields changed- changed
Input schema / properties / area / descriptionPrevious value: -"State name, MSA name, or FIPS area code to narrow results to a geographic area. Omit for national series."New value: +"State name, MSA name, or FIPS area code to narrow results to a geographic area — a case-insensitive substring of the area name, title, or SeriesID. Omit for national series, which then rank first among equal matches." - added
Input schema / properties / offsetAdded value: +{ + "default": 0, + "description": "Number of ranked results to skip, for paging past the first page (default 0). Pass nextOffset from the previous response to get the next page.", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" +} - changed
Input schema / properties / survey / descriptionPrevious value: -"Two-letter LABSTAT survey abbreviation to filter results (e.g. CU for CPI, CE for CES, LN for CPS, LA for LAUS, JT for JOLTS, OE for OEWS). Omit to search all loaded surveys."New value: +"Two-letter LABSTAT survey abbreviation to filter results, case-insensitive (e.g. CU for CPI, CE for CES, LN for CPS, LA for LAUS, JT for JOLTS). Indexed surveys: CU, AP, CE, LN, LA, PC, WP, JT, EC, PR, MP, CW; OE (OEWS) only when the server sets BLS_CATALOG_INCLUDE_OES=true. Omit to search all indexed surveys." - changed
Output schema / anyOfPrevious value: -[ - { - "not": { - "required": [ - "error" - ] - }, - "required": [ - "series", - "totalCount", - "capped", - "catalogSize", - "effectiveQuery", - "limitApplied" - ] - }, - { - "required": [ - "error" - ] - } -]New value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "series", + "totalCount", + "capped", + "catalogSize", + "effectiveQuery", + "limitApplied", + "offsetApplied" + ] + }, + { + "required": [ + "error" + ] + } +] - changed
Output schema / properties / areaFilter / descriptionPrevious value: -"Area filter applied, if any. Absent when no area filter was passed."New value: +"Area filter applied, trimmed. Absent when no area filter was passed or it was blank." - changed
Output schema / properties / capped / descriptionPrevious value: -"True when the FTS candidate pool reached the internal cap (~1000). totalCount is then a lower bound, not an exact match count. Narrow the query, add survey/area filters, or use a direct SeriesID to get an exact count."New value: +"True when the FTS candidate pool, after the survey/area/seasonal filters, reached the internal cap (~1000). totalCount is then a lower bound and offset paging stops at the pool. Narrow the query, add filters, or use a direct SeriesID to get an exact count." - added
Output schema / properties / nextOffsetAdded value: +{ + "description": "Offset of the next page. Present only when truncated is true.", + "type": "number" +} - changed
Output schema / properties / notice / descriptionPrevious value: -"Guidance when no results matched — e.g. how to broaden the query or remove filters. Absent when results are returned."New value: +"Guidance on the result: the survey is not in the offline index, the offset is past the last result, nothing matched, which offset fetches the next page, or, on the last page of a capped list, that other series may also match. Absent when this page ends an uncapped list with results." - added
Output schema / properties / offsetAppliedAdded value: +{ + "description": "Offset in effect (defaults to 0 when omitted).", + "type": "number" +} - changed
Output schema / properties / surveyFilter / descriptionPrevious value: -"Survey filter applied, if any. Absent when no survey filter was passed."New value: +"Survey filter applied, trimmed and uppercased. Absent when no survey filter was passed or it was blank." - changed
Output schema / properties / totalCount / descriptionPrevious value: -"Total candidates scored before the limit was applied. A lower bound when capped is true — the catalog index may contain more matching series."New value: +"Total ranked results after every filter (area included), before limit and offset. A lower bound when capped is true — the catalog index may contain more matching series." - changed
Output schema / properties / truncated / descriptionPrevious value: -"True when more candidates matched than the limit returned."New value: +"True when ranked results remain past this page; nextOffset fetches them."
2 tool updates
- Changed
bls_get_latest1 field changed- changed
Output schema / properties / failed / items / properties / error / descriptionPrevious value: -"Error message. Common values: \"Series does not exist\" (invalid SeriesID — use bls_search_series to find valid IDs), \"No observations returned\" (series exists but has no current data)."New value: +"Error message. Common values: \"Series does not exist\" (invalid SeriesID — use bls_search_series to find valid IDs), \"No observations returned\" (series exists but has no current data). BLS does not always name the series it rejected: an invalid SeriesID sometimes comes back as the generic \"Your request has failed. Please check your input parameters, and try your request again.\", which means the same thing here."
- Changed
bls_get_series6 fields changed- changed
Input schema / properties / end_year / descriptionPrevious value: -"End year for the data range (inclusive). Defaults to the current year when omitted."New value: +"End year for the data range (inclusive). Supplying it without start_year is rejected before the request — BLS applies no default start year alongside an explicit end year. Pair it with start_year, or omit both for the API default window." - changed
Input schema / properties / start_year / descriptionPrevious value: -"Start year for the data range (inclusive). The BLS API allows up to 20 years per request. Omit for the API default (typically 3–20 years depending on survey)."New value: +"Start year for the data range (inclusive). The BLS API allows up to 20 years per request and requires both bounds or neither: supplying start_year alone resolves end_year to the current year, capped at start_year + 19 so the window stays inside the 20-year limit. Omit both for the API default (typically 3–20 years depending on survey)." - changed
Output schema / properties / endYearApplied / descriptionPrevious value: -"End year in effect, when a range was requested."New value: +"End year applied to the query. Resolved from start_year when end_year was omitted, so it can differ from the requested range; notice names the cap when the 20-year window decided it. Absent when no year range was in effect." - changed
Output schema / properties / error / properties / data / properties / reason / descriptionPrevious value: -"Machine-readable failure mode. Declared by this tool: `invalid_api_key`: BLS rejected the configured BLS_API_KEY as invalid. `quota_exceeded`: The BLS API 500 query/day limit has been reached. `request_rejected`: BLS returned a non-success status with a message matching no known failure mode — e.g. a rejected combination of request parameters. `series_not_found`: One or more SeriesIDs do not exist in BLS data. `series_locked`: The BLS database is temporarily locked for the requested series. `no_data_for_period`: No data is available for the requested year range. `calculations_not_supported`: calculations=true was requested for a survey that does not support it. `canvas_unavailable`: The result set exceeds the inline budget and canvas (DuckDB) is not configured. `canvas_registration_failed`: The result set exceeds the inline budget and canvas is configured, but registering the dataframe failed. Other values are possible when a failure originates below the handler."New value: +"Machine-readable failure mode. Declared by this tool: `invalid_api_key`: BLS rejected the configured BLS_API_KEY as invalid. `quota_exceeded`: The BLS API 500 query/day limit has been reached. `request_rejected`: BLS returned a non-success status with a message matching no known failure mode — e.g. a rejected combination of request parameters. `series_not_found`: No requested series returned data and at least one SeriesID does not exist. A batch mixing an invalid SeriesID with one BLS has no data for lands here too, and names both in the message and recovery hint. `series_locked`: The BLS database is temporarily locked for the requested series. `no_data_for_period`: The requested year range is unusable before the request — start_year after end_year, a span of 20 years or more, or end_year without start_year — or BLS returned data for none of the requested series over the range. `calculations_not_supported`: calculations=true was requested for a survey that does not support it. `canvas_unavailable`: The result set exceeds the inline budget and canvas (DuckDB) is not configured. `canvas_registration_failed`: The result set exceeds the inline budget and canvas is configured, but registering the dataframe failed. Other values are possible when a failure originates below the handler." - changed
Output schema / properties / notice / descriptionPrevious value: -"Guidance for agents — names any SeriesID that returned zero observations, and reports the bls_dataframe_describe then bls_dataframe_query workflow when results spill to canvas. Absent when every requested series returned data and it all fit inline."New value: +"Guidance for agents — names any SeriesID that returned zero observations, reports a resolved end_year the 20-year window capped, and reports the bls_dataframe_describe then bls_dataframe_query workflow when results spill to canvas. Absent when every requested series returned data over the window as asked and it all fit inline." - changed
Output schema / properties / startYearApplied / descriptionPrevious value: -"Start year in effect, when a range was requested."New value: +"Start year applied to the query, whether it was served live or from the local observation mirror. Absent when no year range was in effect."
2 tool updates
- Changed
bls_get_latest3 fields changed- added
Output schema / properties / results / items / properties / latestObservation / properties / availableAdded value: +{ + "description": "False when BLS published the \"-\" missing-value sentinel for this period.", + "type": "boolean" +} - changed
Output schema / properties / results / items / properties / latestObservation / properties / value / descriptionPrevious value: -"Observation value as a string, matching BLS API output. Parse to float for arithmetic."New value: +"Raw observation value from BLS. The literal \"-\" means unavailable; check available before arithmetic and read footnotes for the reason." - changed
Output schema / properties / results / items / properties / latestObservation / requiredPrevious value: -[ - "year", - "period", - "value" -]New value: +[ + "year", + "period", + "value", + "available" +]
- Changed
bls_get_series11 fields changed- changed
Input schema / properties / annual_average / descriptionPrevious value: -"When true, add each year's annual-average row to the observations. An annual average is the mean of that year's real periods, returned as an extra row named \"Annual\" with period M13 (monthly series), Q05 (quarterly) or S03 (semiannual) — not an additional month or quarter, so it must be excluded from any sum or average over observations. Defaults to false, which returns real periods only and is safe to aggregate directly. Independent of start_year/end_year. Surveys that publish no annual averages return the same rows either way; enrichment.annualAverageRows reports how many rows were actually added."New value: +"When true, add each year's annual-average row to the observations. An annual average is the mean of that year's real periods, returned as an extra row named \"Annual\" with period M13 (monthly series), Q05 (quarterly) or S03 (semiannual) — not an additional month or quarter, so it must be excluded from any sum or average over observations. Defaults to false, which returns real periods only; check available before aggregating. Independent of start_year/end_year. Surveys that publish no annual averages return the same rows either way; enrichment.annualAverageRows reports how many rows were actually added." - changed
Output schema / anyOfPrevious value: -[ - { - "not": { - "required": [ - "error" - ] - }, - "required": [ - "series", - "spilled", - "totalObservations", - "seriesRequested", - "annualAverageApplied" - ] - }, - { - "required": [ - "error" - ] - } -]New value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "series", + "spilled", + "totalObservations", + "availableObservations", + "unavailableObservations", + "seriesRequested", + "annualAverageApplied" + ] + }, + { + "required": [ + "error" + ] + } +] - changed
Output schema / properties / annualAverageApplied / descriptionPrevious value: -"Whether annual-average rows were requested. When false, observations hold real periods only and can be summed or averaged directly."New value: +"Whether annual-average rows were requested. When false, observations hold real periods only; filter on available before aggregation." - added
Output schema / properties / availableObservationsAdded value: +{ + "description": "Rows with a published numeric value.", + "type": "number" +} - added
Output schema / properties / series / items / properties / availableObservationCountAdded value: +{ + "description": "Rows with a published numeric value, excluding the BLS \"-\" sentinel.", + "type": "number" +} - changed
Output schema / properties / series / items / properties / observationCount / descriptionPrevious value: -"Total observations for this series. When spilled to canvas, all observations are on the dataframe; inline only shows a preview."New value: +"Total period rows for this series, including unavailable BLS placeholder rows. When spilled to canvas, all rows are on the dataframe; inline only shows a preview." - added
Output schema / properties / series / items / properties / observations / items / properties / availableAdded value: +{ + "description": "False when BLS published the \"-\" missing-value sentinel for this period.", + "type": "boolean" +} - changed
Output schema / properties / series / items / properties / observations / items / properties / value / descriptionPrevious value: -"Observation value as a string matching BLS output. Parse to float for arithmetic."New value: +"Raw observation value from BLS. The literal \"-\" means unavailable; check available before arithmetic and read footnotes for the reason." - changed
Output schema / properties / series / items / properties / observations / items / requiredPrevious value: -[ - "year", - "period", - "value" -]New value: +[ + "year", + "period", + "value", + "available" +] - changed
Output schema / properties / series / items / requiredPrevious value: -[ - "seriesId", - "observationCount", - "observations" -]New value: +[ + "seriesId", + "observationCount", + "availableObservationCount", + "observations" +] - added
Output schema / properties / unavailableObservationsAdded value: +{ + "description": "Rows carrying the BLS \"-\" missing-value sentinel.", + "type": "number" +}
2 tool updates
- Changed
bls_dataframe_describe3 fields changed- removed
Output schema / properties / dataframes / items / properties / max_rowsRemoved value: -{ - "description": "Materialization cap that produced truncated, when applicable.", - "type": "number" -} - removed
Output schema / properties / dataframes / items / properties / truncatedRemoved value: -{ - "description": "True when the upstream had more rows than were materialized.", - "type": "boolean" -} - changed
Output schema / properties / dataframes / items / requiredPrevious value: -[ - "name", - "source_tool", - "query_params", - "created_at", - "expires_at", - "row_count", - "truncated", - "column_schema" -]New value: +[ + "name", + "source_tool", + "query_params", + "created_at", + "expires_at", + "row_count", + "column_schema" +]
- Changed
bls_get_series5 fields changed- changed
Output schema / properties / dataset / descriptionPrevious value: -"Canvas dataframe handle — present when the observation volume exceeded the inline budget. Use bls_dataframe_query with dataset.name to run SQL across the full data."New value: +"Canvas dataframe handle — present when the observation volume exceeded the inline budget. Call bls_dataframe_describe with dataset.name to inspect column_schema, then use that table name in bls_dataframe_query SQL across the full data." - changed
Output schema / properties / dataset / properties / name / descriptionPrevious value: -"Canvas table name (df_XXXXX_XXXXX). Pass to bls_dataframe_query."New value: +"Canvas table name (df_XXXXX_XXXXX). Pass to bls_dataframe_describe first to inspect column_schema, then use it in bls_dataframe_query SQL." - removed
Output schema / properties / dataset / properties / truncatedRemoved value: -{ - "description": "True when the upstream response had more rows than the canvas materialization cap.", - "type": "boolean" -} - changed
Output schema / properties / dataset / requiredPrevious value: -[ - "name", - "row_count", - "expires_at", - "truncated" -]New value: +[ + "name", + "row_count", + "expires_at" +] - changed
Output schema / properties / notice / descriptionPrevious value: -"Guidance for agents — names any SeriesID that returned zero observations, and reports when results spilled to canvas and SQL is needed for full access. Absent when every requested series returned data and it all fit inline."New value: +"Guidance for agents — names any SeriesID that returned zero observations, and reports the bls_dataframe_describe then bls_dataframe_query workflow when results spill to canvas. Absent when every requested series returned data and it all fit inline."
6 tool updates
- Changed
bls_dataframe_describe6 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / additionalPropertiesAdded value: +false - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Output schema / anyOfAdded value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "dataframes" + ] + }, + { + "required": [ + "error" + ] + } +] - added
Output schema / properties / errorAdded value: +{ + "additionalProperties": {}, + "description": "Present when the call failed. Absent on success.", + "properties": { + "code": { + "description": "JSON-RPC error code for this failure.", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "data": { + "additionalProperties": {}, + "properties": { + "reason": { + "description": "Machine-readable failure mode. Declared by this tool: `canvas_unavailable`: The DataCanvas service is not configured for this deployment. Other values are possible when a failure originates below the handler.", + "examples": [ + "canvas_unavailable" + ], + "type": "string" + }, + "recovery": { + "additionalProperties": {}, + "description": "Actionable next step for the caller.", + "properties": { + "hint": { + "type": "string" + } + }, + "required": [ + "hint" + ], + "type": "object" + }, + "retryable": { + "description": "Whether retrying may succeed.", + "type": "boolean" + } + }, + "type": "object" + }, + "message": { + "description": "Human-readable description of what went wrong.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" +} - removed
Output schema / requiredRemoved value: -[ - "dataframes" -]
- Changed
bls_dataframe_query10 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / additionalPropertiesAdded value: +false - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Output schema / anyOfAdded value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "columns", + "row_count", + "rows" + ] + }, + { + "required": [ + "error" + ] + } +] - added
Output schema / properties / capAdded value: +{ + "description": "The preview or row_limit cap that was applied.", + "type": "number" +} - added
Output schema / properties / errorAdded value: +{ + "additionalProperties": {}, + "description": "Present when the call failed. Absent on success.", + "properties": { + "code": { + "description": "JSON-RPC error code for this failure.", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "data": { + "additionalProperties": {}, + "properties": { + "reason": { + "description": "Machine-readable failure mode. Declared by this tool: `canvas_unavailable`: The DataCanvas service is not configured for this deployment. Other values are possible when a failure originates below the handler.", + "examples": [ + "canvas_unavailable" + ], + "type": "string" + }, + "recovery": { + "additionalProperties": {}, + "description": "Actionable next step for the caller.", + "properties": { + "hint": { + "type": "string" + } + }, + "required": [ + "hint" + ], + "type": "object" + }, + "retryable": { + "description": "Whether retrying may succeed.", + "type": "boolean" + } + }, + "type": "object" + }, + "message": { + "description": "Human-readable description of what went wrong.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" +} - changed
Output schema / properties / row_count / descriptionPrevious value: -"Total rows the query produced (may exceed rows.length when capped by row_limit)."New value: +"Rows materialized by the query. Exact when register_as is used; otherwise equals row_limit when truncated is true." - added
Output schema / properties / shownAdded value: +{ + "description": "Number of rows returned inline.", + "type": "number" +} - added
Output schema / properties / truncatedAdded value: +{ + "description": "True when the returned rows were capped.", + "type": "boolean" +} - removed
Output schema / requiredRemoved value: -[ - "columns", - "row_count", - "rows" -]
- Changed
bls_get_latest6 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / additionalPropertiesAdded value: +false - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Output schema / anyOfAdded value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "results", + "succeeded", + "failed" + ] + }, + { + "required": [ + "error" + ] + } +] - added
Output schema / properties / errorAdded value: +{ + "additionalProperties": {}, + "description": "Present when the call failed. Absent on success.", + "properties": { + "code": { + "description": "JSON-RPC error code for this failure.", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "data": { + "additionalProperties": {}, + "properties": { + "reason": { + "description": "Machine-readable failure mode. Declared by this tool: `invalid_api_key`: BLS rejected the configured BLS_API_KEY as invalid. `quota_exceeded`: The BLS API 500 query/day limit has been reached. `series_locked`: The BLS database is temporarily locked for the requested series. Other values are possible when a failure originates below the handler.", + "examples": [ + "invalid_api_key", + "quota_exceeded", + "series_locked" + ], + "type": "string" + }, + "recovery": { + "additionalProperties": {}, + "description": "Actionable next step for the caller.", + "properties": { + "hint": { + "type": "string" + } + }, + "required": [ + "hint" + ], + "type": "object" + }, + "retryable": { + "description": "Whether retrying may succeed.", + "type": "boolean" + } + }, + "type": "object" + }, + "message": { + "description": "Human-readable description of what went wrong.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" +} - removed
Output schema / requiredRemoved value: -[ - "results", - "succeeded", - "failed" -]
- Changed
bls_get_series6 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / additionalPropertiesAdded value: +false - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Output schema / anyOfAdded value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "series", + "spilled", + "totalObservations", + "seriesRequested", + "annualAverageApplied" + ] + }, + { + "required": [ + "error" + ] + } +] - added
Output schema / properties / errorAdded value: +{ + "additionalProperties": {}, + "description": "Present when the call failed. Absent on success.", + "properties": { + "code": { + "description": "JSON-RPC error code for this failure.", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "data": { + "additionalProperties": {}, + "properties": { + "reason": { + "description": "Machine-readable failure mode. Declared by this tool: `invalid_api_key`: BLS rejected the configured BLS_API_KEY as invalid. `quota_exceeded`: The BLS API 500 query/day limit has been reached. `request_rejected`: BLS returned a non-success status with a message matching no known failure mode — e.g. a rejected combination of request parameters. `series_not_found`: One or more SeriesIDs do not exist in BLS data. `series_locked`: The BLS database is temporarily locked for the requested series. `no_data_for_period`: No data is available for the requested year range. `calculations_not_supported`: calculations=true was requested for a survey that does not support it. `canvas_unavailable`: The result set exceeds the inline budget and canvas (DuckDB) is not configured. `canvas_registration_failed`: The result set exceeds the inline budget and canvas is configured, but registering the dataframe failed. Other values are possible when a failure originates below the handler.", + "examples": [ + "invalid_api_key", + "quota_exceeded", + "request_rejected", + "series_not_found", + "series_locked", + "no_data_for_period", + "calculations_not_supported", + "canvas_unavailable", + "canvas_registration_failed" + ], + "type": "string" + }, + "recovery": { + "additionalProperties": {}, + "description": "Actionable next step for the caller.", + "properties": { + "hint": { + "type": "string" + } + }, + "required": [ + "hint" + ], + "type": "object" + }, + "retryable": { + "description": "Whether retrying may succeed.", + "type": "boolean" + } + }, + "type": "object" + }, + "message": { + "description": "Human-readable description of what went wrong.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" +} - removed
Output schema / requiredRemoved value: -[ - "series", - "spilled", - "totalObservations", - "seriesRequested", - "annualAverageApplied" -]
- Changed
bls_list_surveys6 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / additionalPropertiesAdded value: +false - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Output schema / anyOfAdded value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "surveys", + "total" + ] + }, + { + "required": [ + "error" + ] + } +] - added
Output schema / properties / errorAdded value: +{ + "additionalProperties": {}, + "description": "Present when the call failed. Absent on success.", + "properties": { + "code": { + "description": "JSON-RPC error code for this failure.", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "data": { + "additionalProperties": {}, + "properties": { + "reason": { + "description": "Machine-readable failure mode. Declared by this tool: `invalid_api_key`: BLS rejected the configured BLS_API_KEY as invalid. `service_unavailable`: BLS /surveys API is unreachable or returns a non-200 response. `serialization_failure`: BLS /surveys response cannot be parsed (malformed JSON or unexpected schema). Other values are possible when a failure originates below the handler.", + "examples": [ + "invalid_api_key", + "service_unavailable", + "serialization_failure" + ], + "type": "string" + }, + "recovery": { + "additionalProperties": {}, + "description": "Actionable next step for the caller.", + "properties": { + "hint": { + "type": "string" + } + }, + "required": [ + "hint" + ], + "type": "object" + }, + "retryable": { + "description": "Whether retrying may succeed.", + "type": "boolean" + } + }, + "type": "object" + }, + "message": { + "description": "Human-readable description of what went wrong.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" +} - removed
Output schema / requiredRemoved value: -[ - "surveys", - "total" -]
- Changed
bls_search_series6 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / additionalPropertiesAdded value: +false - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Output schema / anyOfAdded value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "series", + "totalCount", + "capped", + "catalogSize", + "effectiveQuery", + "limitApplied" + ] + }, + { + "required": [ + "error" + ] + } +] - added
Output schema / properties / errorAdded value: +{ + "additionalProperties": {}, + "description": "Present when the call failed. Absent on success.", + "properties": { + "code": { + "description": "JSON-RPC error code for this failure.", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "data": { + "additionalProperties": {}, + "properties": { + "reason": { + "description": "Machine-readable failure mode. Declared by this tool: `catalog_unavailable`: The catalog index failed to load at startup. Other values are possible when a failure originates below the handler.", + "examples": [ + "catalog_unavailable" + ], + "type": "string" + }, + "recovery": { + "additionalProperties": {}, + "description": "Actionable next step for the caller.", + "properties": { + "hint": { + "type": "string" + } + }, + "required": [ + "hint" + ], + "type": "object" + }, + "retryable": { + "description": "Whether retrying may succeed.", + "type": "boolean" + } + }, + "type": "object" + }, + "message": { + "description": "Human-readable description of what went wrong.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" +} - removed
Output schema / requiredRemoved value: -[ - "series", - "totalCount", - "capped", - "catalogSize", - "effectiveQuery", - "limitApplied" -]
2 tool updates
- Changed
bls_get_series5 fields changed- added
Input schema / properties / annual_averageAdded value: +{ + "default": false, + "description": "When true, add each year's annual-average row to the observations. An annual average is the mean of that year's real periods, returned as an extra row named \"Annual\" with period M13 (monthly series), Q05 (quarterly) or S03 (semiannual) — not an additional month or quarter, so it must be excluded from any sum or average over observations. Defaults to false, which returns real periods only and is safe to aggregate directly. Independent of start_year/end_year. Surveys that publish no annual averages return the same rows either way; enrichment.annualAverageRows reports how many rows were actually added.", + "type": "boolean" +} - added
Output schema / properties / annualAverageAppliedAdded value: +{ + "description": "Whether annual-average rows were requested. When false, observations hold real periods only and can be summed or averaged directly.", + "type": "boolean" +} - added
Output schema / properties / annualAverageRowsAdded value: +{ + "description": "How many observations across all series are annual-average rows (period M13/Q05/S03). Present only when annual_average is true; 0 means none of the requested surveys publish annual averages.", + "type": "number" +} - changed
Output schema / properties / series / items / properties / observations / items / properties / period / descriptionPrevious value: -"Period code (e.g. M01–M13, Q01–Q05, A01)."New value: +"BLS period code: M01–M12 are months, Q01–Q04 quarters, S01–S02 semiannual halves. M13, Q05 and S03 are not further periods — each is the mean of that year's real observations, named \"Annual\", and appears only when annual_average is true. Exclude them from any sum or average over observations." - changed
Output schema / requiredPrevious value: -[ - "series", - "spilled", - "totalObservations", - "seriesRequested" -]New value: +[ + "series", + "spilled", + "totalObservations", + "seriesRequested", + "annualAverageApplied" +]
- Changed
bls_list_surveys1 field changed- changed
Output schema / properties / surveys / items / properties / hasAnnualAverages / descriptionPrevious value: -"True when the survey publishes annual average observations."New value: +"True when BLS reports that the survey publishes annual average observations. Advisory only: it does not predict whether a given series returns annual-average rows for bls_get_series with annual_average=true — LN, CE, LA and SM report true yet return none. Read annualAverageRows on that response for what actually came back."
1 tool update
- Changed
bls_get_series3 fields changed- changed
Input schema / properties / calculations / descriptionPrevious value: -"When true, request BLS-computed period-over-period calculations. The flag is a single boolean (you cannot select an individual calculation type), but the API returns whichever the survey supports: CPI and PPI return percent change only (the inflation rate), with no error. Only surveys that support neither net nor percent change (e.g. AP average price data) return an error — check bls_list_surveys first. Monthly-cadence series return each supported change type over 1, 3, 6, and 12-month intervals; other cadences return a subset."New value: +"When true, request BLS-computed period-over-period calculations. The flag is a single boolean (you cannot select an individual calculation type), but the API returns whichever the survey supports and omits the rest — CPI and PPI return percent change only (the inflation rate), and a survey that supports neither simply returns its observations without calculation fields. Requesting calculations never fails, so it is always safe to set; consult bls_list_surveys (allowsNetChange / allowsPercentChange) only to predict which fields will come back. Monthly-cadence series return each supported change type over 1, 3, 6, and 12-month intervals; other cadences return a subset." - changed
Output schema / properties / notice / descriptionPrevious value: -"Guidance for agents — e.g. when results spilled to canvas and SQL is needed for full access. Absent when all observations fit inline."New value: +"Guidance for agents — names any SeriesID that returned zero observations, and reports when results spilled to canvas and SQL is needed for full access. Absent when every requested series returned data and it all fit inline." - changed
Output schema / properties / seriesRequested / descriptionPrevious value: -"Number of SeriesIDs requested. Compare against the returned series[] length to detect series that returned no data."New value: +"Number of SeriesIDs requested. Do not compare it against series[] length to find empty series — a SeriesID that returned no data is still listed in series[] with observationCount 0. Check observationCount per entry, or read notice, which names every SeriesID that came back empty."
1 tool update
- Changed
bls_get_series5 fields changed- changed
Input schema / properties / calculations / descriptionPrevious value: -"When true, request BLS-computed period-over-period calculations. The flag is a single boolean (you cannot select an individual calculation type), but the API returns whichever the survey supports: CPI and PPI return percent change only (the inflation rate), with no error. Only surveys that support neither net nor percent change (e.g. AP average price data) return an error — check bls_list_surveys first."New value: +"When true, request BLS-computed period-over-period calculations. The flag is a single boolean (you cannot select an individual calculation type), but the API returns whichever the survey supports: CPI and PPI return percent change only (the inflation rate), with no error. Only surveys that support neither net nor percent change (e.g. AP average price data) return an error — check bls_list_surveys first. Monthly-cadence series return each supported change type over 1, 3, 6, and 12-month intervals; other cadences return a subset." - added
Output schema / properties / series / items / properties / observations / items / properties / netChange3MonthAdded value: +{ + "description": "3-month net change (when calculations=true).", + "type": "string" +} - added
Output schema / properties / series / items / properties / observations / items / properties / netChange6MonthAdded value: +{ + "description": "6-month net change (when calculations=true).", + "type": "string" +} - added
Output schema / properties / series / items / properties / observations / items / properties / pctChange3MonthAdded value: +{ + "description": "3-month percent change (when calculations=true).", + "type": "string" +} - added
Output schema / properties / series / items / properties / observations / items / properties / pctChange6MonthAdded value: +{ + "description": "6-month percent change (when calculations=true).", + "type": "string" +}
1 tool update
- Changed
bls_search_series7 fields changed- added
Output schema / properties / capAdded value: +{ + "description": "The result limit that capped the returned list.", + "type": "number" +} - changed
Output schema / properties / capped / descriptionPrevious value: -"True when the FTS candidate pool reached the internal cap (~1000). totalFound is then a lower bound, not an exact match count. Narrow the query, add survey/area filters, or use a direct SeriesID to get an exact count."New value: +"True when the FTS candidate pool reached the internal cap (~1000). totalCount is then a lower bound, not an exact match count. Narrow the query, add survey/area filters, or use a direct SeriesID to get an exact count." - added
Output schema / properties / shownAdded value: +{ + "description": "Number of series returned in this response.", + "type": "number" +} - added
Output schema / properties / totalCountAdded value: +{ + "description": "Total candidates scored before the limit was applied. A lower bound when capped is true — the catalog index may contain more matching series.", + "type": "number" +} - removed
Output schema / properties / totalFoundRemoved value: -{ - "description": "Total candidates scored before the limit was applied. A lower bound when capped is true — the catalog index may contain more matching series.", - "type": "number" -} - added
Output schema / properties / truncatedAdded value: +{ + "description": "True when more candidates matched than the limit returned.", + "type": "boolean" +} - changed
Output schema / requiredPrevious value: -[ - "series", - "totalFound", - "capped", - "catalogSize", - "effectiveQuery", - "limitApplied" -]New value: +[ + "series", + "totalCount", + "capped", + "catalogSize", + "effectiveQuery", + "limitApplied" +]
3 tool updates
- Changed
bls_dataframe_query1 field changed- changed
Output schema / properties / notice / descriptionPrevious value: -"Guidance when results were capped by row_limit — e.g. to use register_as to persist all rows or increase row_limit. Absent when all rows fit in the response."New value: +"Guidance when results were capped by preview or row_limit — names which parameter was the binding limiter and suggests how to retrieve the rest. Absent when all rows fit in the response."
- Changed
bls_get_latest2 fields changed- changed
Output schema / properties / results / descriptionPrevious value: -"Latest observation for each requested series, in request order."New value: +"Successfully fetched series with their latest observations. Series that failed appear in failed[] instead." - changed
Output schema / properties / results / items / properties / latestObservation / descriptionPrevious value: -"Most recent observation. Absent when the series returned no data."New value: +"Most recent observation."
- Changed
bls_search_series9 fields changed- added
Output schema / properties / cappedAdded value: +{ + "description": "True when the FTS candidate pool reached the internal cap (~1000). totalFound is then a lower bound, not an exact match count. Narrow the query, add survey/area filters, or use a direct SeriesID to get an exact count.", + "type": "boolean" +} - added
Output schema / properties / series / items / properties / areaAdded value: +{ + "description": "Geographic area name, when decoded.", + "type": "string" +} - removed
Output schema / properties / series / items / properties / areaNameRemoved value: -{ - "description": "Geographic area name, when decoded.", - "type": "string" -} - added
Output schema / properties / series / items / properties / itemAdded value: +{ + "description": "Item or subject name, when decoded.", + "type": "string" +} - removed
Output schema / properties / series / items / properties / itemNameRemoved value: -{ - "description": "Item or subject name, when decoded.", - "type": "string" -} - changed
Output schema / properties / series / items / properties / seasonal / descriptionPrevious value: -"True when seasonally adjusted."New value: +"Seasonality descriptor matching the data-tool form: \"Seasonally Adjusted\" or \"Not Seasonally Adjusted\"." - changed
Output schema / properties / series / items / properties / seasonal / typePrevious value: -"boolean"New value: +"string" - changed
Output schema / properties / totalFound / descriptionPrevious value: -"Total matches in the catalog before the limit was applied."New value: +"Total candidates scored before the limit was applied. A lower bound when capped is true — the catalog index may contain more matching series." - changed
Output schema / requiredPrevious value: -[ - "series", - "totalFound", - "catalogSize", - "effectiveQuery", - "limitApplied" -]New value: +[ + "series", + "totalFound", + "capped", + "catalogSize", + "effectiveQuery", + "limitApplied" +]
3 tool updates
- Changed
bls_get_series6 fields changed- changed
Input schema / properties / calculations / descriptionPrevious value: -"When true, request BLS-computed net change and percent change together (cannot request one independently). Not all surveys support this — check bls_list_surveys first. The API returns an error if requested for an unsupported survey."New value: +"When true, request BLS-computed period-over-period calculations. The flag is a single boolean (you cannot select an individual calculation type), but the API returns whichever the survey supports: CPI and PPI return percent change only (the inflation rate), with no error. Only surveys that support neither net nor percent change (e.g. AP average price data) return an error — check bls_list_surveys first." - added
Output schema / properties / calculationsAppliedAdded value: +{ + "description": "Whether BLS net/percent-change calculations were requested.", + "type": "boolean" +} - added
Output schema / properties / endYearAppliedAdded value: +{ + "description": "End year in effect, when a range was requested.", + "type": "number" +} - added
Output schema / properties / seriesRequestedAdded value: +{ + "description": "Number of SeriesIDs requested. Compare against the returned series[] length to detect series that returned no data.", + "type": "number" +} - added
Output schema / properties / startYearAppliedAdded value: +{ + "description": "Start year in effect, when a range was requested.", + "type": "number" +} - changed
Output schema / requiredPrevious value: -[ - "series", - "spilled", - "totalObservations" -]New value: +[ + "series", + "spilled", + "totalObservations", + "seriesRequested" +]
- Changed
bls_list_surveys1 field changed- added
Output schema / properties / categoryFilterAdded value: +{ + "description": "Category filter applied, if any. Absent when all surveys were listed.", + "type": "string" +}
- Changed
bls_search_series6 fields changed- added
Output schema / properties / areaFilterAdded value: +{ + "description": "Area filter applied, if any. Absent when no area filter was passed.", + "type": "string" +} - added
Output schema / properties / effectiveQueryAdded value: +{ + "description": "Query string as the server received and searched on. Confirms interpretation for self-correction.", + "type": "string" +} - added
Output schema / properties / limitAppliedAdded value: +{ + "description": "Result limit in effect (defaults to 10 when omitted).", + "type": "number" +} - added
Output schema / properties / seasonalFilterAdded value: +{ + "description": "Seasonal-adjustment filter applied, if any. Absent when not passed.", + "type": "boolean" +} - added
Output schema / properties / surveyFilterAdded value: +{ + "description": "Survey filter applied, if any. Absent when no survey filter was passed.", + "type": "string" +} - changed
Output schema / requiredPrevious value: -[ - "series", - "totalFound", - "catalogSize" -]New value: +[ + "series", + "totalFound", + "catalogSize", + "effectiveQuery", + "limitApplied" +]
1 tool update
- Changed
bls_get_latest3 fields changed- changed
Output schema / properties / failed / descriptionPrevious value: -"Series that failed to fetch, with per-item error details."New value: +"Series that failed to fetch. Inspect seriesId and error for per-item details. Not-found series appear here rather than as a tool-level error." - changed
Output schema / properties / failed / items / descriptionPrevious value: -"A series that could not be fetched."New value: +"A series that could not be fetched, e.g. due to an invalid SeriesID or empty data window." - changed
Output schema / properties / failed / items / properties / error / descriptionPrevious value: -"Error message."New value: +"Error message. Common values: \"Series does not exist\" (invalid SeriesID — use bls_search_series to find valid IDs), \"No observations returned\" (series exists but has no current data)."
4 tool updates
- Changed
bls_dataframe_query1 field changed- added
Output schema / properties / noticeAdded value: +{ + "description": "Guidance when results were capped by row_limit — e.g. to use register_as to persist all rows or increase row_limit. Absent when all rows fit in the response.", + "type": "string" +}
- Changed
bls_get_latest1 field changed- added
Output schema / properties / noticeAdded value: +{ + "description": "Guidance when one or more series failed — e.g. to use bls_search_series to verify SeriesIDs. Absent when all series returned data.", + "type": "string" +}
- Changed
bls_get_series3 fields changed- added
Output schema / properties / noticeAdded value: +{ + "description": "Guidance for agents — e.g. when results spilled to canvas and SQL is needed for full access. Absent when all observations fit inline.", + "type": "string" +} - added
Output schema / properties / totalObservationsAdded value: +{ + "description": "Total observation rows across all requested series.", + "type": "number" +} - changed
Output schema / requiredPrevious value: -[ - "series", - "spilled" -]New value: +[ + "series", + "spilled", + "totalObservations" +]
- Changed
bls_search_series5 fields changed- changed
Output schema / properties / catalogSize / descriptionPrevious value: -"Total series in the loaded catalog index. Used to distinguish an empty-result search from a failed catalog load."New value: +"Total series in the loaded catalog index. Distinguishes an empty-result search from a failed catalog load." - added
Output schema / properties / noticeAdded value: +{ + "description": "Guidance when no results matched — e.g. how to broaden the query or remove filters. Absent when results are returned.", + "type": "string" +} - removed
Output schema / properties / totalRemoved value: -{ - "description": "Total matches in the catalog before the limit was applied.", - "type": "number" -} - added
Output schema / properties / totalFoundAdded value: +{ + "description": "Total matches in the catalog before the limit was applied.", + "type": "number" +} - changed
Output schema / requiredPrevious value: -[ - "series", - "total", - "catalogSize" -]New value: +[ + "series", + "totalFound", + "catalogSize" +]
Related MCP Connectors
Econdata MCP — wraps BLS (Bureau of Labor Statistics) public API v2
BLS MCP — Bureau of Labor Statistics public data API (v2)
Query US Treasury national debt, interest rates, exchange rates, and fiscal datasets via MCP.
Query U.S. Census Bureau data, variables, and geography via MCP.
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
- AlicenseNot gradedqualityCmaintenanceEnables querying and retrieving official U.S. labor market and price data from the Bureau of Labor Statistics, including unemployment, CPI, PPI, and employment figures, via series search, latest observations, and popular series.4 npmMIT
- FlicenseNot gradedqualityDmaintenanceProvides access to U.S. labor market data including employment statistics, Consumer Price Index inflation rates, and wage information. Users can query specific time series data or use shortcuts for common economic indicators like unemployment and industry-specific employment.3-
- AlicenseAqualityCmaintenanceProvides curated US economic data from Treasury, FRED, BLS, BEA, and other sources through an MCP interface. Enables querying economic series, fetching data with provenance tracking, and accessing cached artifacts.9MIT
Glama MCP Gateway
Add one secure layer between your agents and this server.