@cyanheads/un-comtrade-mcp-server
Access UN Comtrade international merchandise and services trade statistics — country lookups, HS commodity search, bilateral trade flows, balances, rankings, and data availability — via MCP. STDIO or Streamable HTTP.
9 Tools • 3 Resources
Overview
International merchandise and services trade statistics from UN Comtrade. Resolve country and HS commodity codes, fetch bilateral trade flows and services trade, and compute balances, partner/commodity rankings, and data availability.
Tools
Resources
Country and HS reference data are also reachable through comtrade_lookup_countries and comtrade_search_commodities. The workflow guide ships with npm, Docker, MCPB, and plugins, and is available over MCP as a skill resource.
Capability reference
comtrade_lookup_countries tool
- Accepts a partial or full country name, or an ISO alpha-2/alpha-3 code;
role filters to "reporter", "partner" (only entries in UN's partner-area list), or "any" (default)
validAsReporter flag on each match — most regional groupings (e.g. World) are partner-only, but some report (e.g. the European Union, 97)
include_groups (default true) toggles regional/economic groupings in the results
- Each match also pulls in entries sharing its ISO-3 identity, ordered exact ISO match, reporter-valid, current before expired, name, then code; reporter and partner-only area codes remain distinct
entryEffectiveDate and entryExpiredDate (YYYY-MM-DD) give each entity's validity, so historical entities (e.g. Fed. Rep. of Germany, ...1990) sort after current ones
- Country references load from UN static files at startup — no subscription key required. Rows that fail validation are dropped and logged; startup fails on an empty catalog or when fewer than half a file's rows are valid
comtrade_search_commodities tool
- Free-text keyword, partial description, or code-prefix search;
classification (case-insensitive, default HS) selects the current HS reference, an HS edition H0–H6, or SITC SS/S1–S4
aggr_level uses the selected catalog's native levels: HS chapters/headings/subheadings use 2/4/6; SITC has levels 1–5. Omit for all levels. The HS all-commodities TOTAL is level 0, which no aggr_level filter returns
limit (default 50) is the page size; totalMatches, shown, and truncated describe the page, and offset with the returned nextOffset continues it
recommendedQueryCode on each result — the code to pass as cmd_code in trade queries, with the same classification
comtrade_list_service_categories tool
- Lists the combined EB service categories, optionally filtered by keyword or
parent_code; numeric and alphabetic codes are distinct entries. parent_code: "200" lists the EB10 and EB02 top levels
schemes on each category names the EBOPS scheme catalogs that list its code (EB10, EB10S, EB02); it is empty when none does and absent, with a notice, when the scheme catalogs could not be loaded. ebParent carries the parent the combined EB catalog gives a code when it differs from parent (TOTAL for EB10's top-level codes, whose parent is 200)
limit (default 100) is the page size; totalMatches, shown, and truncated describe the page, and offset with the returned nextOffset continues it
- Returned
id is the service_code for comtrade_get_services_trade
comtrade_get_trade_flows tool
- Accepts a scalar
reporter_code or an array; omission requires an explicit partner_code. Reporter arrays serve the first 20 distinct codes and disclose the remainder in unservedReporters. Served codes of an array that return no rows are listed in reportersWithoutRecords (referenceStatus, optional suggestedReporterCode; omitted when the response is truncated). A scalar partner-only code with no rows gets a notice naming its reporter-valid counterpart (250 → 251)
- Requires
flow_code (M import / X export / RX re-export / RM re-import) and period values, all YYYY or all YYYYMM. freq is optional: it is inferred from the period length, mixed lengths are rejected, and an explicit value must match. Up to 120 periods are accepted; keyless calls serve the first 8 (12 with a subscription key). Periods past the cap, or whose request is rate limited or runs out of time, are listed in unservedPeriods with a notice; if every period fails the call returns rate_limited or deadline_exceeded
partner_code: 0 returns the World total; omit it for a per-partner breakdown. classification (case-insensitive, default HS) supports HS, H0–H6, and SITC S1–S4/SS. Explicit HS editions require authenticated data access; keyless HS returns as-reported editions, identified by each row's classificationCode
cmd_code[] accepts up to 200 codes and serves the first 20, listing the rest in unservedCmdCodes; omit it or pass ["TOTAL"] for cross-commodity totals. truncated: true plus truncationHint means the response hit the 500-record cap; isReported separates directly reported rows from UN-estimated ones
- Quantity and alternate quantity retain unit codes/abbreviations and estimation flags where available. UN Comtrade sends
0 for what it does not report, so qty/altQty of 0 with unit code -1, and netWgt/grossWgt/cifvalue/fobvalue of 0 beside a positive primaryValue, are omitted: an absent field means not reported, not zero. A 0 in a real unit stays a measurement
comtrade_get_trade_balance tool
- Returns
balanceUsd (signed), exportsUsd, importsUsd, and coverageRatio (exports / imports) per period. Optional cmd_code[] restricts the balance to HS or SITC commodities
partner_code defaults to World (0); a country code computes the reporter's bilateral balance and is echoed with the partner name. classification is case-insensitive with default HS; freq is optional and inferred from the period length (mixed lengths are rejected; an explicit value must match)
- Accepts up to 120 periods and 200 codes. Keyless calls serve the first 4 periods (two requests each; 12 with a subscription key) and 20 codes, listing the rest in
unservedPeriods/unservedCmdCodes; a period is served only when both flows complete, and one whose export or import request is rate limited or runs out of time is listed too. A notice warns when either flow hit the 500-record cap and the balance may be incomplete
- A partner-only reporter code (0 World, 250 Metropolitan France) with no data fails with
partner_only_reporter, naming the reporter-valid counterpart when one exists (250 → 251)
mirrorCaveat on every response: the balance reflects this reporter's own values, not the partner's mirror data
comtrade_get_top_partners tool
- Takes one
reporter_code, flow_code (M/X), and single period; optional single cmd_code (omit for total merchandise trade) in the selected classification (case-insensitive, default HS; H0–H6 with a subscription key, SITC S1–S4/SS)
- Infers annual or monthly frequency from the period; monthly values require a calendar month from
01 through 12
- Returns the top
limit partners (default 10; any larger value is honored, with no return-all threshold), each with rank, primaryValueUsd, and sharePercent; totalPartners and totalValueUsd cover every partner retrieved, and shares divide by totalValueUsd; classification names the catalog the cmd_code was read in
truncated: true when the fetch hit the 500-record cap. An empty result for a partner-only reporter code ends its notice with the reporter-valid counterpart (250 → 251)
comtrade_get_top_commodities tool
- Takes one
reporter_code, flow_code, and single period; aggr_level is 2 (HS chapter, default) or 4 (heading). partner_code defaults to World (0); set it to scope to one partner
- Infers frequency from
period. Fetches matching TOTAL to identify the reporter's HS edition, then queries that edition's category codes in paced chunks, splitting capped chunks
- Returns the top
limit categories (default 10; any larger value is honored, with no return-all threshold). Complete coverage permits ranks; shares use denominatorValueUsd from TOTAL, while totalValueUsd and totalCommodities cover every category retrieved
coverageComplete, coverageNotice, and missingCodes describe retrieval; codes not reached within the call's time budget are listed in missingCodes. Incomplete results retain available categories without ranks or shares; truncated remains true. A TOTAL with no rows ends the ranking with an empty result and truncated: false; for a partner-only reporter code its notice names the reporter-valid counterpart. Ranking can require several upstream requests
comtrade_get_data_availability tool
- All filters optional —
reporter_code, period, freq (A/M; inferred from period, A when period is omitted, and an explicit value must match it), type_code (C goods / S services, default C), classification (omitted or blank resolves to HS for goods, EB10 for services; upstream identifiers such as B4 and B5 are also accepted)
- Returns per-dataset
totalRecords, firstReleased, and lastReleased where available; omit all filters to browse goods availability
limit defaults to 50 and offset to 0. Results are sorted before slicing; totalDatasets, shown, and nextOffset support stable page traversal. truncated identifies a partial page. Paging still fetches the matching upstream availability list
- An empty result for a partner-only reporter code ends its notice with the reporter-valid counterpart (250 → 251)
- Annual data typically publishes 3–12 months after the reference year — call this before querying recent periods
comtrade_get_services_trade tool
- Takes one
reporter_code, flow_code (M imports/debits, X exports/credits), annual period values (YYYY), and optional partner_code (0 for all partners combined). Up to 120 periods are accepted; keyless calls serve the first 8 (12 with a subscription key). Periods past the cap, or whose request is rate limited or runs out of time, are listed in unservedPeriods with a notice
classification (case-insensitive, default EB; blank means EB) selects EB as reported, with each reporter-period in its own scheme (EB10, EB10S, or EB02 codes), or UN Comtrade's converted EB10/EB02 series. EB10S has no converted series and is rejected; its rows are served only under EB. Each row's classificationCode identifies its classification and isOriginalClassification says whether it is as reported or converted
service_code filters to one category from comtrade_list_service_categories (3 is transport; SA is manufacturing services), which lists each code's schemes. Omit it to get every reported category at mixed levels, with totals (200 for EB10 and EB02, S for EB10S) beside their components: don't sum the rows
- An empty result's notice names the code's scheme, routes to
comtrade_get_data_availability with classification: "EB", and suggests the converted series when one applies; for a partner-only reporter it names the reporter-valid counterpart
truncated: true plus truncationHint when the response hit the 500-record cap. Country and period coverage is more limited than for goods
comtrade://countries resource
- Complete country/area list as
application/json — M49 code, ISO identifiers, validAsReporter, isGroup, and entryEffectiveDate/entryExpiredDate (YYYY-MM-DD) where upstream lists them
- Loaded from the same startup reference cache
comtrade_lookup_countries searches
comtrade://hs-classification/{level} resource
level path param accepts 2 (chapters), 4 (headings), or 6 (subheadings); any other value throws a validation error
- Returns each code's
description, parent, and isLeaf; full leaf enumeration is too large to inject — use comtrade_search_commodities for keyword search
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.
Comtrade-specific:
- Country codes, current HS hierarchy, and EB categories load from UN static files at startup; historical HS/SITC editions, quantity units, and the
EB10.json/EB10S.json/EB02.json scheme catalogs (category scheme tags and the services empty-result notice) load lazily and cache successful responses. Invalid rows in a reference file are dropped and logged; startup fails on an empty catalog or when fewer than half a file's rows are valid
- Authenticated (
data/v1/get) and public preview (public/v1/preview) endpoints — tools fall back to the preview endpoint when no subscription key is set
- Parallel sub-request execution in workflow tools (
comtrade_get_trade_balance runs export and import fetches concurrently)
- Retry with exponential backoff on transient failures, honoring an upstream
Retry-After header when present
- Description enrichment uses the row's classification edition and preserves upstream labels. Optional reference failures retain trade values and codes with a notice
Agent-friendly output:
truncated: true plus a recovery hint when a public preview response reaches 500 records
isReported flag on trade flow records distinguishes directly-reported values from UN-estimated/aggregated rows
validAsReporter on country lookups prevents constructing invalid queries with partner-only area codes
mirrorCaveat on trade-balance output surfaces the methodological caveat without parsing error text
Getting started
Public Hosted Instance
A public instance is available at https://un-comtrade.caseyjhand.com/mcp — no installation required. It runs on the keyless public preview endpoint (500 records per upstream request), splitting served periods into paced requests that start at least 4 seconds apart. Each call serves 8 periods for flows and services and 4 for balance (two requests per period); periods past that, or that fail, come back in unservedPeriods. Point any MCP client at it via Streamable HTTP:
Self-Hosted / Local
Prerequisites: A UN Comtrade subscription key is optional. Without one, tools use the public preview endpoint (500 records per upstream request). A key selects authenticated access; retrieval and request limits depend on UN Comtrade and the subscription.
Add the following to your MCP client configuration file.
Or with npx (no Bun required):
Or with Docker:
For Streamable HTTP, set the transport and start the server:
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+). Development and Docker images use Bun 1.4.4.
- Optional: a UN Comtrade subscription key for authenticated API access subject to subscription limits. Without one, data tools use the public preview endpoint (500 records per upstream request). Reference/lookup tools (
comtrade_lookup_countries, comtrade_search_commodities, comtrade_list_service_categories) never require a key — they query static UN reference files.
Installation
- Clone the repository:
- Navigate into the directory:
- Install dependencies:
- Configure environment:
Configuration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts.
See .env.example for the full list of optional overrides.
Running the server
Local development
-
Build and run:
-
Run checks and tests:
Docker
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/un-comtrade-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catch in tool logic
- Use
ctx.log for request-scoped logging, ctx.state for tenant-scoped storage
- Register new tools and resources in
src/index.ts
- Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
- Use the matching classification's reference cache for commodity descriptions; lazy reference failures must preserve useful trade records and propagate caller cancellation
Data license
Use UN Comtrade data in accordance with the UN Comtrade license agreement, including its redistribution terms.
Contributing
Issues are welcome. Run checks and tests before submitting:
License
Apache-2.0 — see LICENSE for details.