faostat-mcp-server

v0.1.2 pre-1.0

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

faostat.caseyjhand.com/mcp
claude mcp add --transport http faostat-mcp-server https://faostat.caseyjhand.com/mcp
codex mcp add faostat-mcp-server --url https://faostat.caseyjhand.com/mcp
{
  "mcpServers": {
    "faostat-mcp-server": {
      "url": "https://faostat.caseyjhand.com/mcp"
    }
  }
}
gemini mcp add --transport http faostat-mcp-server https://faostat.caseyjhand.com/mcp
{
  "mcpServers": {
    "faostat-mcp-server": {
      "command": "bunx",
      "args": [
        "mcp-remote",
        "https://faostat.caseyjhand.com/mcp"
      ]
    }
  }
}
{
  "mcpServers": {
    "faostat-mcp-server": {
      "type": "http",
      "url": "https://faostat.caseyjhand.com/mcp"
    }
  }
}
curl -X POST https://faostat.caseyjhand.com/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2025-11-25" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'

Tools

6

faostat_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.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "faostat_list_domains",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "topic": {
      "description": "Case-insensitive substring filter over domain code, name, and topic (e.g. \"trade\", \"emissions\", \"QCL\"). Omit to list the full catalog.",
      "type": "string"
    },
    "indexed_only": {
      "default": false,
      "description": "When true, return only domains indexed in the local mirror (queryable now).",
      "type": "boolean"
    }
  },
  "required": [
    "indexed_only"
  ],
  "additionalProperties": false
}
view source ↗

faostat_resolve_codes

Resolve human terms to the opaque integer codes faostat_query_observations needs, within a dimension: areas (countries/regions), items (commodities), or elements (metrics like production, yield, import quantity). Pass `query` for fuzzy full-text matching ("maize" → item 56), `name_contains` for a substring filter, or `code` for an exact-code lookup; omit all three to list the whole dimension. Every area match is flagged `country` or `aggregate` — aggregates (World, continents, economic groupings; codes ≥ 5000) double-count if summed with their member countries, so resolve before querying and exclude aggregates unless you want the regional roll-up.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "faostat_resolve_codes",
    "arguments": {
      "domain": "<domain>",
      "dimension": "<dimension>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "domain": {
      "type": "string",
      "minLength": 1,
      "description": "FAOSTAT domain code to verify the index is ready (e.g. \"QCL\"). Dimension code lists (areas, items, elements) are shared across all indexed domains."
    },
    "dimension": {
      "type": "string",
      "enum": [
        "area",
        "item",
        "element"
      ],
      "description": "Which dimension to resolve: \"area\" (countries/regions), \"item\" (commodities), or \"element\" (metrics)."
    },
    "query": {
      "description": "Full-text search term, FTS5-matched against the dimension labels with prefix matching (e.g. \"wheat\", \"import quantity\"). Relevance-ranked.",
      "type": "string"
    },
    "name_contains": {
      "description": "Case-insensitive substring filter over the label. Used only when `query` is omitted.",
      "type": "string"
    },
    "code": {
      "description": "Exact code lookup. Takes precedence over `query`/`name_contains` when provided.",
      "type": "integer",
      "minimum": -9007199254740991,
      "maximum": 9007199254740991
    },
    "limit": {
      "default": 50,
      "description": "Maximum matches to return (max 200).",
      "type": "integer",
      "minimum": 1,
      "maximum": 200
    }
  },
  "required": [
    "domain",
    "dimension",
    "limit"
  ],
  "additionalProperties": false
}
view source ↗

faostat_query_observations

Query a FAOSTAT domain's data cube by area(s), item(s), element(s), and year range, returning observations (area, item, element, year, value, unit, and the data-quality flag). Resolve codes first with faostat_resolve_codes — the cube is unqueryable without them. Aggregate regions (World, continents, economic groupings) are EXCLUDED by default so a naive SUM does not double-count a region with its member countries; set include_aggregates=true to get the regional roll-ups, or pass explicit area_codes to query exactly what you name. Small result sets return inline; large ones spill to a DataCanvas table (returned canvas_id + table_name) for GROUP BY / ranking / time-series analysis via faostat_dataframe_query. Every row carries its flag (A=Official, E=Estimated, I=Imputed, B=break, X=external) — honor it; never treat estimated/imputed values as official.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "faostat_query_observations",
    "arguments": {
      "domain": "<domain>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "domain": {
      "type": "string",
      "minLength": 1,
      "description": "FAOSTAT domain code (e.g. \"QCL\"). Must be indexed locally."
    },
    "area_codes": {
      "description": "Area codes from faostat_resolve_codes. When set, aggregates are NOT auto-excluded — the codes are honored verbatim.",
      "type": "array",
      "items": {
        "type": "integer",
        "minimum": -9007199254740991,
        "maximum": 9007199254740991
      }
    },
    "item_codes": {
      "description": "Item codes from faostat_resolve_codes.",
      "type": "array",
      "items": {
        "type": "integer",
        "minimum": -9007199254740991,
        "maximum": 9007199254740991
      }
    },
    "element_codes": {
      "description": "Element codes from faostat_resolve_codes (e.g. 5510 Production).",
      "type": "array",
      "items": {
        "type": "integer",
        "minimum": -9007199254740991,
        "maximum": 9007199254740991
      }
    },
    "year_start": {
      "description": "Inclusive start year (e.g. 2000).",
      "type": "integer",
      "minimum": -9007199254740991,
      "maximum": 9007199254740991
    },
    "year_end": {
      "description": "Inclusive end year (e.g. 2022).",
      "type": "integer",
      "minimum": -9007199254740991,
      "maximum": 9007199254740991
    },
    "include_aggregates": {
      "default": false,
      "description": "When false (default), exclude aggregate-region rows (codes ≥ 5000) so sums are not double-counted. Set true for World/continent/grouping roll-ups. Ignored when explicit area_codes are passed.",
      "type": "boolean"
    },
    "limit": {
      "default": 200,
      "description": "Max observations returned inline when the result does not spill. Max 1000.",
      "type": "integer",
      "minimum": 1,
      "maximum": 1000
    },
    "canvas_id": {
      "description": "Canvas ID from a prior call to stage onto. Omit to start a fresh canvas (a new id is returned).",
      "type": "string"
    }
  },
  "required": [
    "domain",
    "include_aggregates",
    "limit"
  ],
  "additionalProperties": false
}
view source ↗

faostat_commodity_profile

Assemble a global profile for one commodity in a single call: top-producing countries, the multi-decade production trend, and trade flows (top exporters and importers). Accepts a commodity name, resolves it to item codes, then queries the production (QCL) and trade (TCL) domains and merges the results. Country-level only (aggregates excluded). When a required domain is not indexed locally, returns a partial profile with a notice naming the gap rather than failing. The full merged observation set spills to a DataCanvas table for deeper SQL via faostat_dataframe_query.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "faostat_commodity_profile",
    "arguments": {
      "item_query": "<item_query>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "item_query": {
      "type": "string",
      "minLength": 1,
      "description": "Commodity name to profile (e.g. \"maize\", \"wheat\", \"coffee green\")."
    },
    "year_start": {
      "description": "Inclusive start year for the trend (e.g. 1990).",
      "type": "integer",
      "minimum": -9007199254740991,
      "maximum": 9007199254740991
    },
    "year_end": {
      "description": "Inclusive end year for the trend (e.g. 2022).",
      "type": "integer",
      "minimum": -9007199254740991,
      "maximum": 9007199254740991
    },
    "top_n": {
      "default": 10,
      "description": "Number of top producers / exporters / importers to return. Max 50.",
      "type": "integer",
      "minimum": 1,
      "maximum": 50
    },
    "canvas_id": {
      "description": "Canvas ID from a prior call to stage onto. Omit to start a fresh canvas.",
      "type": "string"
    }
  },
  "required": [
    "item_query",
    "top_n"
  ],
  "additionalProperties": false
}
view source ↗

faostat_dataframe_query

Run a single-statement SELECT against the canvas tables staged by faostat_query_observations and faostat_commodity_profile (table names look like faostat_xxxxxxxx). Use this for cross-country and cross-item aggregation, GROUP BY rankings, joins, and time-series analysis over the full result set the inline preview only sampled. Standard DuckDB SQL — joins, aggregates, window functions, CTEs all work. Read-only: writes, DDL, DROP, COPY, PRAGMA, ATTACH, and external-file table functions are rejected; system catalogs (information_schema, sqlite_master, duckdb_*) are denied — list staged tables via faostat_dataframe_describe. Every row carries its data-quality `flag` (A=Official, E=Estimated, I=Imputed, …) — keep it in projections and honor it in interpretation.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "faostat_dataframe_query",
    "arguments": {
      "sql": "<sql>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "canvas_id": {
      "description": "Optional canvas ID from a prior faostat_query_observations / faostat_commodity_profile call. Omit to query the tables staged in this session (the common case).",
      "type": "string"
    },
    "sql": {
      "type": "string",
      "minLength": 1,
      "description": "Single-statement read-only SELECT against staged faostat_<id> tables. Columns: area_code, area, item_code, item, element_code, element, year, unit, value, flag. CAST(value AS DOUBLE) for arithmetic."
    },
    "row_limit": {
      "default": 1000,
      "description": "Hard cap on rows in the response. Default 1000, max 10000.",
      "type": "integer",
      "minimum": 1,
      "maximum": 10000
    }
  },
  "required": [
    "sql",
    "row_limit"
  ],
  "additionalProperties": false
}
view source ↗

faostat_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.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "faostat_dataframe_describe",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "canvas_id": {
      "description": "Optional canvas ID from a prior faostat_query_observations / faostat_commodity_profile call. Omit to list the tables staged in this session (the common case).",
      "type": "string"
    },
    "name": {
      "description": "Optional table name (faostat_xxxxxxxx) to describe a single staged table. Omit to list all.",
      "type": "string"
    }
  },
  "additionalProperties": false
}
view source ↗