# Data FAIR logo @data-fair/mcp ![GitHub License](https://img.shields.io/github/license/data-fair/mcp) ![GitHub package.json version](https://img.shields.io/github/package-json/v/data-fair/mcp) *A Model Context Protocol (MCP) server to interact with the Data Fair ecosystem.* Part of the [Data Fair](https://datafair.cloud) ecosystem. ## What it serves Two MCP servers over data-fair's merged platform OpenAPI document (`GET /data-fair/api/v1/platform/api-docs.json`, annotated with `x-agent`), turned into tools by [`@data-fair/openapi-mcp`](https://github.com/data-fair/openapi-mcp). The document is kept live: re-fetched conditionally on an interval, and clients are notified when the tools change. - the **platform server** — every tool of the document, cut down to what the caller's token scopes allow (`toolsForScopes`). Always authenticated; tool names carry the `datafair_` prefix; - the **datasets server** — the published catalog's six tools under the names earlier clients hard-coded (see Compatibility). Anonymous callers allowed. Both also serve `geocode_address` (an IGN geocoder, the one hand-written tool — no service document describes it), whatever the scopes. Nothing here declares a tool's name or schema: those come from the platform document. ## Standalone (Docker, stdio) Run the MCP server as a standalone Docker container that connects remotely to a Data Fair instance. This mode uses the **stdio** transport and is suited for integrating with LLM clients (e.g., Claude Desktop, VS Code). - `PORTAL_URL` is **required** — set it to the base URL of the Data Fair portal you want to query. - `DATA_FAIR_API_KEY` — optional API key for authenticating requests to the Data Fair instance. - `MCP_SERVER` — `datasets` (default: the seven historic tool names) or `platform` (the `datafair_` tools the credentials allow). `platform` needs credentials and exits at startup without any. - `HTTPS_PROXY` — optional forward proxy every call tunnels through (e.g. an identity-injecting proxy such as `@data-fair/nhi-proxy`); a TLS-intercepting proxy's CA goes in `NODE_EXTRA_CA_CERTS`. With an API key the tools are not filtered (data-fair checks the key on every call). Otherwise, at startup the server asks `/simple-directory/api/auth/my-session` who the environment injects (a proxy adding a session), and the scopes of that session cut the tool list for the life of the process. ```json { "mcpServers": { "data-fair-datasets": { "command": "docker", "args": ["run", "-i", "--rm", "-e", "PORTAL_URL=https://opendata.koumoul.com", "ghcr.io/data-fair/mcp"] } } } ``` ## Stack deployment (HTTP) Deploy the MCP server as a web service alongside Data Fair in the same infrastructure stack, using the **http** transport (`TRANSPORT=http`), published through the ingress at `/mcp-server/`. There is one deployment for every caller — external MCP clients and the agents service alike (parity: our own agents get no surface another agent cannot reach). Credentials are the caller's cookie session, a bearer access token or an API key, forwarded to data-fair on every call, whose permissions are the boundary. Cookie and bearer sessions are verified through `@data-fair/lib-express` against the directory (the server never decodes a JWT itself) and their scopes filter the platform server's tools; a session without scopes and an API key see every tool. A request without forwarded headers (an in-cluster caller) is read with the main site as its site. A cookie session may be anyone's, so an assistant in a portal can act as its user by relaying the user's cookies. A bearer token must be an NHI's (non-human identity) and name this server in its audience (`/mcp-server`, which simple-directory gives tokens requested for an MCP `resource`); a dashboard's user token is refused. When a request carries both, the bearer decides. MCP clients connect with OAuth on sites where simple-directory runs an authorization server (the main site and account main sites): a `401` from `/mcp` carries `WWW-Authenticate: Bearer resource_metadata="/mcp-server/.well-known/oauth-protected-resource/mcp"`, and that metadata (RFC 9728) names the site's issuer. For example, with Claude Code: ```sh claude mcp add --transport http datafair https:///mcp-server/mcp ``` Other sites, and sites with a path prefix, offer no OAuth access: use an API key there. This server has no general rate limiting of its own. nginx limits callers on the way in, and every tool call that costs anything is a data-fair call, limited there by the caller's identity or by the IP this server forwards (`X-Forwarded-For`, with the `TRUSTED_FORWARDER` secret that lets the ingress trust it; a caller acting for no address, an autonomous agent's planned task, forwards none). The one exception is `geocode_address`, which calls IGN: a global budget per instance and a per-caller one (forwarded IP, else identity) (`EXTRA_TOOLS_GEOCODE_ADDRESS_LIMITS`). See data-fair's `docs/architecture/agent-rate-limiting.md`. Routes: - `GET /` — the home page: both servers' URLs on the requesting site, how the platform server authenticates there (OAuth login on issuer sites, API key), client examples; french or english by `Accept-Language`, else `LOCALE` - `POST /mcp` — the platform server; `401` with a `WWW-Authenticate` challenge without credentials, and for a cookie or bearer that does not verify - `GET /.well-known/oauth-protected-resource/mcp` — the platform server's protected-resource metadata, on OAuth sites only (`404` elsewhere) - `POST /datasets/mcp` — the datasets server (see Compatibility). A cookie or bearer that does not verify is dropped, not forwarded, and the caller served anonymously; valid credentials are forwarded and their scopes filter the six tools - `GET /ai-catalog.json` — the site's AI Catalog (the MCP Server Card of the datasets server); route `/.well-known/ai-catalog.json` of every site host here - `GET /status` — services of the platform document, last refresh time, datasets tools missing from the document, active extra tools; in-cluster callers only ### Registry skills With `PRIVATE_REGISTRY_URL` set, `/mcp` also serves the registry's `skill` artefacts visible to the caller's active account (public ones for an API key). Deployment notes: - `PRIVATE_REGISTRY_URL` must be the registry's in-cluster URL: the registry honours its internal secret only for in-cluster requests. Through a public URL listings silently become public-only and downloads answer `403`, visible only in `/status` (`registry.error`). - `SECRET_REGISTRY` must equal the registry's internal-services secret. - `REGISTRY_CACHE_DIR` must be writable (an `emptyDir` volume on a read-only root filesystem) and not shared across replicas. ## Environment variables | Variable | Description | Default | Mode | |-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|-------------------| | `PORTAL_URL` | Base URL of the Data Fair portal. Required in standalone mode. | — | standalone | | `DATA_FAIR_API_KEY` | API key sent to Data Fair for authentication. | — | standalone | | `LOCALE` | Locale for tool titles: `en` or `fr`. | `en` | standalone/stack | | `TRUSTED_FORWARDER` | Secret sent to the ingress (`X-Trusted-Forwarder`) with the caller's IP, or with none for a caller acting for no address; see data-fair `docs/architecture/agent-rate-limiting.md`. | — | stack only | | `TRANSPORT` | Transport mode: `stdio` (standalone) or `http` (stack). | `stdio` | standalone/stack | | `PORT` | Port for the HTTP server to listen on. | `8080` | stack only | | `OBSERVER_ACTIVE` | Enable Prometheus metrics. | `true` | stack only | | `OBSERVER_PORT` | Port for the Prometheus metrics observer. | `9090` | stack only | | `MAIN_SITE_URL` | Origin of the main site. The platform document is fetched from it; upstream calls swap it for the requesting site's origin. Falls back to `PORTAL_URL` in stdio.| — | stack only | | `DOC_PATH` | Path of the platform OpenAPI document on the main site. | `/data-fair/api/v1/platform/api-docs.json` | standalone/stack | | `PRIVATE_DIRECTORY_URL` | In-cluster URL of simple-directory, used to verify sessions. | `http://simple-directory:8080` | stack only | | `MCP_SERVER` | Which server stdio serves: `datasets` or `platform`. | `datasets` | standalone | | `REFRESH_INTERVAL` | Seconds between conditional re-fetches of the platform document and its skills; `0` disables. | `300` | standalone + stack | | `UPSTREAM_PROXY_HOST` | Optional in-cluster reverse proxy (`host` or `host:port`, or a bracketed IPv6 literal such as `[::1]:8080`) every site host resolves to, skipping public DNS and the outer proxy hop. | — | stack only | | `FILE_URL_PREFIXES` | Comma-separated URL prefixes of the agents' file store (endpoint and bucket path, e.g. `https://s3.example.com/agents/`); the only URLs a file input is fetched from. Unset: tools that need a file are not offered. | — | standalone/stack | | `FILE_MAX_SIZE` | Largest file accepted, in bytes. | `104857600` | standalone/stack | | `FILE_TIMEOUT` | Seconds from the request to the last byte of a file. | `300` | standalone/stack | | `PRIVATE_REGISTRY_URL` | In-cluster URL of the registry whose `skill` artefacts the platform server serves (see Registry skills). Unset: no registry skills. | — | stack only | | `SECRET_REGISTRY` | The registry's internal-services secret. Required with `PRIVATE_REGISTRY_URL`. | — | stack only | | `REGISTRY_LISTING_TTL` | Seconds a registry listing is reused per account before the registry is asked again. | `60` | stack only | | `REGISTRY_CACHE_DIR` | Directory where skill packages are downloaded and extracted; must be writable, one per replica. | `/tmp/skills` | stack only | | `EXTRA_TOOLS_GEOCODE_ADDRESS_ACTIVE` | JSON boolean; whether `geocode_address` is served at all, e.g. `EXTRA_TOOLS_GEOCODE_ADDRESS_ACTIVE=false`. | `true` | standalone/stack | | `EXTRA_TOOLS_GEOCODE_ADDRESS_LIMITS` | JSON: `{ "global": { "nb", "duration" }, "perCaller": { "nb", "duration" } }`, calls to IGN per instance and per caller (IGN allows 50 requests per second per source IP: divide by replicas). | 40/s, 10/s | standalone/stack | ## Files and structured results **Files.** Tools that take a file take a URL marked `x-mcp-file` ([SEP-2631](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631); the same shape as ChatGPT's [`openai/fileParams`](https://developers.openai.com/apps-sdk/reference)): the agents service puts the file in its store and passes a presigned URL. This server fetches only URLs under `FILE_URL_PREFIXES`, with a plain request that carries none of the caller's credentials, none of `siteFetch`'s headers and not the rate-limit secret (a presigned URL authorizes itself), and streams the file to the requesting site with an exact `Content-Length` — data-fair refuses a chunked upload. Without a prefix, tools that need a file (`datafair_create_dataset_from_file`, `datafair_replace_dataset_file`) are not offered at all. Mechanics: [openapi-mcp `docs/x-agent.md#files`](https://github.com/data-fair/openapi-mcp/blob/main/docs/x-agent.md#files). With `NODE_USE_ENV_PROXY`, put the store's host in `NO_PROXY`. Tools that return a file (downloads, exports) send no request: they return an MCP [`resource_link`](https://modelcontextprotocol.io/specification/latest/server/tools) that the client fetches with its own credentials, so no file crosses this server in either direction. A link is built on the main site's URL, where the tools were built; this server moves it onto the site the call came through (`X-Forwarded-Host`), as `siteFetch` does for every request, so a caller on a portal receives a portal URL. An in-cluster caller without forwarded headers gets the main site's. **Structured results.** Tool results carry the parsed JSON response as `structuredContent` (the agents service's code mode works on it), projected like the text (`fields`, else `response_format`, else the concise set): a client may hand either one to the model, Claude Code shows the former. No tool publishes an `outputSchema`, which MCP leaves optional. Measured against data-fair's platform document (2026-10-13), a client that validates results — the TypeScript SDK 1.x does, in `listTools()` and `callTool()` — would fail: several response schemas embed the same nested `$id`, which one Ajv instance refuses to compile twice, so `listTools()` throws; and real responses do not match their schema (computed fields such as `userPermissions` under `additionalProperties: false`, required fields projected away by `select`), so `callTool()` refuses them. Output schemas come back when the documents' response schemas can be honoured. ## Compatibility The datasets server, `/mcp-server/datasets/mcp`, keeps its pre-1.0 URL and its seven tool names, unprefixed, each mapped to a platform tool: | Tool name | Platform tool | |-----------|---------------| | `list_datasets` | `datafair_list_datasets` | | `describe_dataset` | `datafair_describe_dataset` | | `search_data` | `datafair_search_data` | | `get_field_values` | `datafair_get_field_values` | | `aggregate_data` | `datafair_aggregate_data` | | `calculate_metric` | `datafair_calculate_metric` | | `geocode_address` | the hand-written IGN geocoder | Descriptions and input schemas come from the platform document and are read per session, so they may differ from what a 0.x client remembers. A mapped tool the document lacks is left out and reported in `/status`. Skills and guides are served only when all their tools are in the set. HTTP+SSE (`/sse`, `/messages`) is removed; only Streamable HTTP is served. In stdio, the datasets server is the default (`MCP_SERVER=datasets`). ## Sponsors Koumoul logo **[Koumoul](https://koumoul.com)** · *La donnée accessible*
Koumoul develops the Data Fair ecosystem and hosts it as an online service.
Click [here to support the development of this project](https://github.com/sponsors/koumoul-dev). ## 🔨 Development Take a look at the [contribution guidelines](./CONTRIBUTING.md).