Faostat Mcp Server

io.github.cyanheadsPublished by GitHub account cyanheadsv0.6.0Updated Nov 4, 2026

UN FAOSTAT global food & agriculture statistics over a local SQLite mirror, via MCP.

VerifiedStreamable HTTPWeb executableDatabasesData & Analytics

Overview

Faostat's hosted MCP server (faostat.caseyjhand.com); no sign-in required; Streamable HTTP; 6 tools.

No AI overview yet: these facts come from the registry entry and the server itself.

Endpoint
faostat.caseyjhand.com
Last checked

Installation

In SourceWeft

  1. Open Faostat Mcp Server in the dashboard and add it to a workspace.
  2. Enable the server for the chats that should use its tools.

Web executable via Streamable HTTP. Remote servers run from the web runtime once configured in a workspace.

Use with an agent

Paste this prompt into Claude Code, Codex, Cursor or another agent — or into SourceWeft chat. The agent reads this server's install guide, shows you its publisher, endpoint and sign-in, and adds it to your client once you agree. Keys never go into a command.

Read https://sourceweft.com/mcp/io.github.cyanheads%2Ffaostat-mcp-server/install.md and connect the MCP server it describes to my client. Before changing any config, show me its publisher, endpoint and sign-in method and wait for my OK. Never put an API key in a command or in a file I would commit; ask me where it should come from.

Read the install guide the agent follows

Other MCP clients

Add this to your client's mcpServers config.

{
  "mcpServers": {
    "faostat": {
      "type": "http",
      "url": "https://faostat.caseyjhand.com/mcp"
    }
  }
}

README

@cyanheads/faostat-mcp-server

Global food & agriculture statistics from the UN FAOSTAT bulk-download corpus, served from a local SQLite mirror with a DataCanvas SQL surface, via MCP. STDIO or Streamable HTTP.

7 Tools • 1 Skill Resource • 0 Prompts

Public Hosted Server: https://faostat.caseyjhand.com/mcp


Overview

Global food and agriculture statistics from the UN's FAOSTAT: production, trade, food balances, food security, land use, fertilizer, and emissions for 245+ countries and territories since 1961. Resolve commodity and country codes, query the data, and run SQL over large results.

Tools

ToolDescription
faostat_list_domainsDiscover FAOSTAT statistical domains with codes, descriptions, last-update date, upstream row count, and local index status. The entry point — every query keys on a domain code.
faostat_resolve_codesResolve human terms to the codes a query needs (areas, items, elements), flagging each area as a country or an aggregate region.
faostat_query_observationsQuery a domain's cube by area(s), item(s), element(s), and year range. Inline preview for small results; large sets spill to a DataCanvas table.
faostat_commodity_profileWorkflow: assemble top producers, the production trend, and trade flows for one commodity from the production and trade domains in a single call.
faostat_dataframe_queryRun a read-only SQL SELECT against the canvas tables staged by the analytical tools.
faostat_dataframe_describeList the canvas tables staged this session, each with provenance, row count, and column schema.
faostat_dataframe_dropDrop one staged canvas table before its 2-hour TTL. Opt-in — callable only with FAOSTAT_DATAFRAME_DROP_ENABLED=true.

Capability reference

faostat_list_domains tool

  • Full FAOSTAT catalog (~69 domains) read live from the bulk manifest, annotated with local mirror status
  • code for an exact domain lookup; topic substring filter over code/name/topic; indexed_only to list only domains queryable from the local mirror
  • offset + limit (max 200, default 20) page the catalog — response reports totalMatches, truncated, and nextOffset
  • Each entry reports indexed / index_ready flags, local row count, and last completed sync

faostat_resolve_codes tool

  • FTS5 full-text query, substring name_contains, or exact code lookup within a dimension: area, item, or element; code_text looks up alphanumeric items
  • Item/element matches are scoped to codes present in the given domain's cube; area codes are shared across domains
  • Element matches include observed units; item_codes or item_codes_text restrict element discovery and units to selected items. Item/element lookups require that domain to be ready
  • Every area match is flagged country or aggregate (codes ≥ 5000, plus curated sub-threshold roll-ups such as China=351)
  • Verified item groups are marked aggregate by domain. Classification is incomplete: kind: null does not mean items can safely be summed together
  • limit (max 200, default 50) + offset page the match set
  • Typed errors: unknown_domain, index_not_ready, invalid_text_selector, invalid_item_scope, query_timeout

faostat_query_observations tool

  • Filters by area_codes / item_codes / item_codes_text / element_codes / source_codes and an inclusive year_start / year_end range
  • Alphanumeric items retain their exact item_code_text with item_code: null; numeric items retain their integer code
  • Period observations retain period (e.g. 2000-2002) and use its midpoint as year for filtering. Censored values retain the numeric bound plus value_qualifier (e.g. < with 2.5)
  • Source-specific rows carry source_code and source; omitting source_codes returns all sources, which must be compared separately rather than summed
  • An empty query with explicit item and element selections returns available_elements with labels and units for those items; this does not establish coverage for the requested areas, years, or sources
  • Aggregate regions excluded by default (include_aggregates: false); explicit area_codes bypass the exclusion
  • limit caps the inline page (default 200, max 1000); a match that exceeds it spills in full to a DataCanvas table (50,000-row staging cap) — faostat_dataframe_describe for its columns, then SQL via faostat_dataframe_query
  • Every row carries its data-quality flag (A/E/I/B/M/T/X, others per domain) — never dropped
  • Typed errors: domain_not_indexed, index_not_ready, canvas_disabled, canvas_not_found, invalid_year_range, conflicting_item_selectors, query_timeout

faostat_commodity_profile tool

  • Selects one QCL item with item_code or item_query (code takes precedence), then ranks top producers/exporters/importers and returns an annual production trend
  • Query selection prefers a unique exact label after trimming, collapsing whitespace, and case folding; otherwise requires a unique relevance match. For ambiguous terms such as maize or milk, call faostat_resolve_codes with domain: "QCL", dimension: "item", and the query, then pass the selected item_code (e.g. 56 for maize grain)
  • Rankings are per-country sums grouped by unit, each country at its own latest reporting year; countries only (aggregates excluded)
  • Returns a partial, production-only profile with a notice — rather than failing — when the trade domain (TCL) isn't indexed or still syncing
  • top_n caps each ranked list (max 50); when the merged observation set is too large to inline it spills to a DataCanvas table — faostat_dataframe_describe for its columns, then faostat_dataframe_query for further SQL
  • Every staging outcome is stated in the notice: the staged table, a set that fit inline, a staging failure, or a disabled canvas
  • Typed errors: missing_item, ambiguous_item, no_match, domain_not_indexed, index_not_ready, invalid_year_range, canvas_not_found, query_timeout

faostat_dataframe_query tool

  • Single-statement read-only SELECT over staged faostat_xxxxxxxx tables — joins, aggregates, window functions, and CTEs all work
  • Writes, DDL, DROP, COPY, PRAGMA, ATTACH, external-file functions, and system catalogs (information_schema, sqlite_master, duckdb_*) are rejected
  • Engine-setting introspection, query_table, dynamic query() SQL, and file table references are rejected; query() with a single literal read-only SELECT remains supported
  • row_limit caps the response (default 1000, max 10000); truncated means more rows exist, with no exact total computed on this path
  • Typed errors: canvas_disabled, canvas_not_found, missing_table, system_catalog_access, invalid_sql

faostat_dataframe_describe tool

  • Lists staged tables with source tool, query params, row count, column schema, and recorded creation/expiry timestamps
  • name describes one table outright; otherwise offset + limit (max 100, default 20) page the listing newest-first
  • Typed errors: canvas_disabled, canvas_not_found, missing_table

faostat_dataframe_drop tool

  • Opt-in: registered disabled — listed on the landing page with its enable hint, absent from tools/list — unless FAOSTAT_DATAFRAME_DROP_ENABLED=true
  • Drops one staged faostat_xxxxxxxx table and its provenance from the session canvas, or from the canvas a canvas_id names
  • Idempotent: a name not staged on that canvas returns dropped: false with a notice and changes nothing
  • Typed errors: canvas_disabled, canvas_not_found

Features

The faostat-guide covers reporting periods, censored values, source estimates, country versus regional totals, units, coverage, and complete SQL analysis across staged results. It ships with plugin, npm, Docker, and MCPB installs and is served through the MCP Skills extension as a skill resource.

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.

FAOSTAT-specific:

  • Persistent local SQLite mirror of the FAOSTAT bulk corpus via the framework MirrorService, with FTS5 over the dimension labels driving code resolution
  • Streaming bulk-ZIP ingester — skips domains whose upstream update date hasn't advanced, and stream-parses the normalized CSV into SQLite without materializing the full file in memory
  • Config-driven domain selection (FAOSTAT_DOMAINS) — the indexed set can grow without code changes, and the full catalog stays browsable regardless
  • DataCanvas SQL surface (DuckDB) for GROUP BY, ranking, and time-series analysis over spilled result sets

Agent-friendly output:

  • Country-vs-aggregate classification on every area, with aggregates excluded from sums by default — guards against double-counting World/continent rows with their member countries
  • Data-quality provenance — every observation carries its FAOSTAT flag (A/E/I/B/M/T/X, others per domain), never dropped from output
  • Graceful partial results — faostat_commodity_profile returns a production-only profile with a notice, rather than failing, when the trade domain isn't indexed
  • Typed error contracts — index_not_ready, domain_not_indexed, canvas_disabled, and others each carry a concrete recovery hint

Getting started

Connect to the hosted instance, no install:

sh
claude mcp add --transport http faostat-mcp-server https://faostat.caseyjhand.com/mcp

Or run it locally after building the mirror:

sh
claude mcp add faostat-mcp-server -- npx -y @cyanheads/faostat-mcp-server

For other clients, add this to the MCP config file:

json
{  "mcpServers": {    "faostat-mcp-server": {      "command": "npx",      "args": ["-y", "@cyanheads/faostat-mcp-server"]    }  }}

The bulk-download service is public and keyless. For Docker or a source build, see Running the server.

Configuration

VariableDescriptionDefault
FAOSTAT_DOMAINSComma-separated FAOSTAT domain codes to index into the local mirror. Domains outside this set appear in faostat_list_domains but are not queryable until added and re-synced.QCL,TCL,FBS,FS,RL,GLE,RFN,QV
FAOSTAT_MIRROR_PATHDirectory holding the per-domain SQLite stores and the shared dimension database. Created if absent../.faostat-mirror
FAOSTAT_BULK_BASE_URLFAOSTAT bulk-download service base URL (manifest + per-domain ZIPs).https://bulks-faostat.fao.org/production
FAOSTAT_REFRESH_CRONCron for the in-process incremental refresh (HTTP transport only). Omit to disable and run mirror:refresh out-of-band.—
CANVAS_PROVIDER_TYPEDataCanvas engine. duckdb enables the SQL surface; set none to disable analytical staging (the dataframe_* tools then report canvas_disabled and large queries refuse to spill).duckdb
FAOSTAT_DATAFRAME_DROP_ENABLEDSet true to make faostat_dataframe_drop callable. Off, the tool is registered disabled: absent from tools/list, shown on the landing page with this hint.false
MCP_TRANSPORT_TYPETransport: stdio or http.stdio
MCP_SESSION_MODEHTTP session mode: stateless, stateful, or auto (resolves to stateful). The server declares stateless in src/index.ts — no tool asks the caller for input mid-handler — and setting this overrides that declaration.stateless
MCP_HTTP_PORTPort for the HTTP server.3010
MCP_AUTH_MODEAuth mode: none, jwt, or oauth.none
MCP_LOG_LEVELLog level (RFC 5424).info
LOGS_DIRDirectory for log files (Node.js only).<project-root>/logs

See .env.example for the full list of optional overrides.

Running the server

Local development

Requires Bun v1.4.0 or higher (or Node.js v24+), plus disk for the mirror. The default domain set (QCL,TCL,FBS,FS,RL,GLE,RFN,QV, ∼37M rows) needs a few GB; drop TCL (∼17M rows) from FAOSTAT_DOMAINS on a constrained host.

sh
git clone https://github.com/cyanheads/faostat-mcp-server.gitcd faostat-mcp-server && bun installcp .env.example .env# Override the default domain set, mirror path, or refresh cron in .env.
  • Build and run:

    sh
    # One-time buildbun run rebuild
    # Run the built serverbun run start:stdio# orbun run start:http
  • Run checks and tests:

    sh
    bun run devcheck   # Lint, format, typecheck, securitybun run test       # Vitest suite under Bun, then under Nodebun run lint:mcp   # Validate MCP definitions against spec

Building the mirror

The corpus is not bundled. Before the data tools can answer queries, sync the selected domains into the local mirror:

sh
bun run mirror:init      # one-time bootstrap — downloads and indexes the FAOSTAT_DOMAINS setbun run mirror:refresh   # re-sync domains whose upstream update date has advanced; build missing planner statisticsbun run mirror:verify    # report sync status, local row counts, checkpoints, and dimension population

mirror:init is idempotent and resumable per domain — re-running after an interrupt re-streams only the unfinished domain ZIP. FAOSTAT_DOMAINS selects which domains are indexed; everything else in the catalog shows in faostat_list_domains with indexed: false until added and re-synced. On HTTP transport, set FAOSTAT_REFRESH_CRON to refresh in-process on a schedule; on stdio, run mirror:refresh out-of-band. The read tools return index_not_ready until the first sync completes.

After upgrading an older mirror, run mirror:refresh. FS and GLE rebuild once to recover period/text-item rows and separate source estimates, even when the upstream date is unchanged; they remain unready until that rebuild succeeds. Compatible numeric domains stay readable and gain the new indexes during refresh without a forced download. Until those indexes are prepared, domain-wide element/unit discovery can be slow; scope it with item_codes or item_codes_text when possible.

mirror:refresh also builds query-planner statistics (ANALYZE) for any selected domain that has none, even when its data is unchanged upstream — a mirror synced by an older release, or one whose last build failed. Otherwise the server builds them itself, in the background on a worker thread, the first time a data read touches that domain, and keeps answering meanwhile.

Docker

sh
docker run --rm -p 3010:3010 -v faostat-mirror:/usr/src/app/.faostat-mirror ghcr.io/cyanheads/faostat-mcp-server:latest

Build locally with docker build -t faostat-mcp-server .. For stdio from an MCP client, use "command": "docker" with "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "-v", "faostat-mirror:/usr/src/app/.faostat-mirror", "ghcr.io/cyanheads/faostat-mcp-server:latest"]. The image defaults to HTTP transport, stateless session mode, and logs to /var/log/faostat-mcp-server. A dedicated dependency stage installs the production tree for the target platform (DuckDB's prebuilt native binding included), so the slim runtime image carries no build toolchain and a multi-arch build never runs Bun under emulation. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them. Mount a volume at the mirror path to persist the corpus across container recreations, and bootstrap it inside the container:

sh
docker exec <container> bun run mirror:init      # one-time bootstrapdocker exec <container> bun run mirror:verify    # sync status, row counts, checkpoints, and dimension populationdocker exec <container> bun run mirror:refresh   # re-sync updated domains; build missing planner statistics

Project structure

DirectoryPurpose
src/index.tscreateApp() entry point — registers the seven tools (faostat_dataframe_drop behind its opt-in flag), wires the mirror and canvas in setup(), schedules the HTTP refresh.
src/configServer-specific environment variable parsing and validation with Zod.
skills/faostat-guidePublished agent guide, also served as an MCP skill resource.
src/mcp-server/tools/definitionsTool definitions (*.tool.ts).
src/services/faostat-mirrorThe bulk-download mirror service — manifest discovery, streaming ZIP ingester, CSV parsing, dimension store, SQLite-backed MirrorService wiring, and the two-worker read pool (read-pool.ts, read-worker.ts) that runs cube reads and statistics builds off the main thread.
src/services/canvas-accessor.ts, canvas-staging.tsDataCanvas accessor and the spill/query/describe/drop staging layer.
scripts/faostat-mirror-*.tsmirror:init / mirror:refresh / mirror:verify CLIs.
tests/Unit and integration tests mirroring src/.

Development guide

See CLAUDE.md/AGENTS.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 in the createApp() array in src/index.ts
  • Wrap external data: validate raw → normalize to domain type → return output schema; never fabricate missing fields, and carry the data-quality flag through

Data attribution

Data is sourced from FAOSTAT, the statistics division of the Food and Agriculture Organization of the United Nations (FAO). FAOSTAT data is published under CC BY-4.0; cite FAO as the source in downstream use. This project is not affiliated with or endorsed by the FAO.

Contributing

Issues are welcome. Run checks and tests before submitting:

sh
bun run devcheckbun run test

License

Apache-2.0 — see LICENSE for details.

Source: README.md at commit c5a4fea

Tools

6

faostat_list_domains

faostat-mcp-server: list domains

Discover FAOSTAT statistical domains (production, trade, food balances, food security, land use, agri-emissions, prices, value) with their codes, descriptions, last-update date, upstream row count, and local index status. Every query keys on a domain code from here. The `indexed` flag tells you which domains are queryable right now; un-indexed domains exist in the catalog but must be added to FAOSTAT_DOMAINS and re-synced before faostat_query_observations can read them. The catalog runs to ~69 domains with long descriptions, so responses are paged: narrow with `topic` / `indexed_only`, pass `code` to fetch one domain outright, or page with `offset` + `limit` — when the response reports `truncated`, pass the returned `nextOffset` to fetch the rest.

Parameterscodelimittopicoffsetindexed_only

faostat_resolve_codes

faostat-mcp-server: resolve codes

Resolve labels to the numeric codes or exact alphanumeric item identities faostat_query_observations needs: areas (countries/regions), items (commodities), and elements (metrics). Use query for full-text matching, name_contains for substring matching, code or code_text for exact identity, or omit them to list. Item and element matches occur in the selected domain; areas use shared vocabulary. For elements, optionally pass item_codes or item_codes_text to discover metrics and distinct units observed for those items; this does not establish area/year/source coverage. Empty item selectors match nothing. Page with offset and limit using nextOffset when truncated. Areas carry country/aggregate classification. Verified domain-specific item groups carry aggregate; null means unclassified, not safe-to-sum. Item groups can overlap their members even with matching units. Area aggregates include codes ≥ 5000 and curated roll-ups such as China=351; exclude overlapping regional and country rows when summing.

Parameterscodelimitquerydomainrequiredoffsetcode_textdimensionrequireditem_codesname_containsitem_codes_text

faostat_query_observations

faostat-mcp-server: query observations

Query a FAOSTAT domain's data cube by area(s), item(s), element(s), sources, and year range, returning values with units and data-quality flags. Resolve numeric codes or exact alphanumeric item identities with faostat_resolve_codes; element resolution accepts item selectors and returns units. Empty queries with explicit items and elements list available elements for those items across the domain, without establishing area/year/source coverage. Aggregate regions are excluded by default; include_aggregates=true returns roll-ups, and explicit area_codes are honored verbatim. Item groups can overlap their members even with matching units; they remain included, and kind=null does not establish safe-to-sum. Keep source methodologies separate. Small results return inline; large ones spill to a DataCanvas table (canvas_id + table_name); call faostat_dataframe_describe for columns, then faostat_dataframe_query for analysis. Honor every row's flag: commonly A=Official, B=time-series break, E=Estimated, I=Imputed, M=Missing, T=Unofficial, X=international organization; unrecognized flags never establish official status.

Parameterslimitdomainrequiredyear_endcanvas_idarea_codesitem_codesyear_startsource_codeselement_codesitem_codes_textinclude_aggregates

faostat_commodity_profile

faostat-mcp-server: commodity profile

Assemble a global profile for one QCL item in a single call: top-producing countries, the annual production trend, and trade flows (top exporters and importers). Supply item_code or item_query; code wins when both are supplied. A query selects a unique normalized exact label, otherwise a unique relevance match; ambiguous queries require an explicit code. Each ranking uses the selected item at each country's own latest year with data, grouped by unit. The trend is returned inline as year/value points. Country-level only (aggregates excluded). QCL is required; unavailable TCL yields a production-only profile with a notice. When the merged observation set is too large to inline, it spills to a DataCanvas table (canvas_id + table_name) — call faostat_dataframe_describe for its columns, then faostat_dataframe_query for deeper SQL.

Parameterstop_nyear_endcanvas_iditem_codeitem_queryyear_start

faostat_dataframe_query

faostat-mcp-server: dataframe query

Run a single-statement SELECT against canvas tables staged by faostat_query_observations and faostat_commodity_profile (faostat_xxxxxxxx). Use DuckDB joins, aggregates, window functions, and CTEs for rankings and time-series analysis. Item groups can overlap their members even with matching units; do not sum them together, and resolver kind=null does not establish safe-to-sum. Separate source methodologies and respect bounded values and multi-year periods. Read-only: writes, DDL, DROP, COPY, PRAGMA, ATTACH, and external-file functions are rejected; system catalogs are denied. List tables and columns with faostat_dataframe_describe. Preserve data-quality flags in projections: commonly A=Official, B=time-series break, E=Estimated, I=Imputed, M=Missing, T=Unofficial, X=international organization; unrecognized flags never establish official status.

Parameterssqlrequiredcanvas_idrow_limit

faostat_dataframe_describe

faostat-mcp-server: dataframe describe

List the canvas tables (faostat_xxxxxxxx) staged by faostat_query_observations and faostat_commodity_profile, each with its source tool, the query parameters that produced it, creation/expiry timestamps, row count, and column schema. Call this before faostat_dataframe_query to discover the exact table and column names to reference in SQL. Tables are listed newest-first and paged: pass `name` to describe one table outright, or page with `offset` + `limit` — when the response reports `truncated`, pass the returned `nextOffset` to fetch the rest.

Parametersnamelimitoffsetcanvas_id

Version history

8
  1. v0.6.0LatestNov 4, 2026
  2. v0.5.0Oct 29, 2026
  3. v0.4.1Oct 25, 2026
  4. v0.4.0Oct 18, 2026
  5. v0.3.0Oct 4, 2026
  6. v0.2.5Sep 30, 2026
  7. v0.2.4Sep 19, 2026
  8. v0.2.3Sep 16, 2026

Score

100% of the applicable checks met

How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.

  • Reached in the last 30 daysRequired25 pts
  • Auth declaredRequired10 pts
  • Tools listedRequired15 pts
  • DescriptionRequired15 pts
  • Logo10 pts
  • Documentation15 pts
  • License10 pts