@cyanheads/eurostat-mcp-server 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/Dockerfile +16 -3
- package/README.md +50 -7
- package/dist/config/server-config.d.ts +2 -0
- package/dist/config/server-config.d.ts.map +1 -1
- package/dist/config/server-config.js +14 -0
- package/dist/config/server-config.js.map +1 -1
- package/dist/index.js +28 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.d.ts +40 -0
- package/dist/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.js +113 -0
- package/dist/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/eurostat-dataframe-query.tool.d.ts +34 -0
- package/dist/mcp-server/tools/definitions/eurostat-dataframe-query.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/eurostat-dataframe-query.tool.js +98 -0
- package/dist/mcp-server/tools/definitions/eurostat-dataframe-query.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/eurostat-download-dataset.tool.d.ts +93 -0
- package/dist/mcp-server/tools/definitions/eurostat-download-dataset.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/eurostat-download-dataset.tool.js +362 -0
- package/dist/mcp-server/tools/definitions/eurostat-download-dataset.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/eurostat-get-dataset-info.tool.js +2 -2
- package/dist/mcp-server/tools/definitions/eurostat-get-dataset-info.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/eurostat-get-dimension-values.tool.js +2 -2
- package/dist/mcp-server/tools/definitions/eurostat-get-dimension-values.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.d.ts +9 -0
- package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.js +68 -10
- package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/eurostat-search-datasets.tool.js +3 -3
- package/dist/mcp-server/tools/definitions/eurostat-search-datasets.tool.js.map +1 -1
- package/dist/services/canvas-accessor.d.ts +31 -0
- package/dist/services/canvas-accessor.d.ts.map +1 -0
- package/dist/services/canvas-accessor.js +64 -0
- package/dist/services/canvas-accessor.js.map +1 -0
- package/dist/services/eurostat-bulk/eurostat-bulk-service.d.ts +89 -0
- package/dist/services/eurostat-bulk/eurostat-bulk-service.d.ts.map +1 -0
- package/dist/services/eurostat-bulk/eurostat-bulk-service.js +501 -0
- package/dist/services/eurostat-bulk/eurostat-bulk-service.js.map +1 -0
- package/dist/services/eurostat-bulk/types.d.ts +98 -0
- package/dist/services/eurostat-bulk/types.d.ts.map +1 -0
- package/dist/services/eurostat-bulk/types.js +82 -0
- package/dist/services/eurostat-bulk/types.js.map +1 -0
- package/dist/services/eurostat-data/eurostat-data-service.d.ts +63 -8
- package/dist/services/eurostat-data/eurostat-data-service.d.ts.map +1 -1
- package/dist/services/eurostat-data/eurostat-data-service.js +107 -13
- package/dist/services/eurostat-data/eurostat-data-service.js.map +1 -1
- package/dist/services/eurostat-data/types.d.ts +30 -0
- package/dist/services/eurostat-data/types.d.ts.map +1 -1
- package/dist/services/eurostat-data/types.js +4 -0
- package/dist/services/eurostat-data/types.js.map +1 -1
- package/package.json +2 -1
- package/server.json +85 -3
package/CLAUDE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Agent Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** @cyanheads/eurostat-mcp-server
|
|
4
|
-
**Version:** 0.
|
|
4
|
+
**Version:** 0.5.0
|
|
5
5
|
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.11.1`
|
|
6
6
|
**Engines:** Bun ≥1.3.0, Node ≥24.0.0
|
|
7
7
|
**Zod:** `^4.4.3`
|
package/Dockerfile
CHANGED
|
@@ -47,7 +47,10 @@ LABEL org.opencontainers.image.source="https://github.com/cyanheads/eurostat-mcp
|
|
|
47
47
|
COPY package.json bun.lock ./
|
|
48
48
|
|
|
49
49
|
# Install only production dependencies, ignoring any lifecycle scripts (like 'prepare')
|
|
50
|
-
# that are not needed in the final production image.
|
|
50
|
+
# that are not needed in the final production image. This installs every direct
|
|
51
|
+
# runtime dependency, including the @duckdb/node-api binding that backs DataCanvas —
|
|
52
|
+
# the canvas is gated at runtime by CANVAS_PROVIDER_TYPE, not at build time, so the
|
|
53
|
+
# image always carries the binding and a deployment turns the feature on with an env var.
|
|
51
54
|
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
52
55
|
bun install --production --frozen-lockfile --ignore-scripts
|
|
53
56
|
|
|
@@ -75,8 +78,16 @@ COPY --from=build /usr/src/app/dist ./dist
|
|
|
75
78
|
# The 'oven/bun' image already provides a non-root user named 'bun'.
|
|
76
79
|
# We will use this existing user for enhanced security.
|
|
77
80
|
|
|
78
|
-
# Create and set permissions for the log
|
|
79
|
-
|
|
81
|
+
# Create and set permissions for the log and canvas directories, assigning ownership
|
|
82
|
+
# to the 'bun' user. DuckDB writes spill files under CANVAS_TEMP_PATH and the framework
|
|
83
|
+
# sandboxes path exports to CANVAS_EXPORT_PATH. The export root is the one that breaks
|
|
84
|
+
# by default: it resolves under the working directory /usr/src/app, which this non-root
|
|
85
|
+
# user does not own. Both are pointed at owned directories below so neither depends on
|
|
86
|
+
# the image's writable-by-accident paths.
|
|
87
|
+
RUN mkdir -p /var/log/eurostat-mcp-server \
|
|
88
|
+
/var/lib/eurostat-mcp-server/canvas-tmp \
|
|
89
|
+
/var/lib/eurostat-mcp-server/canvas-exports \
|
|
90
|
+
&& chown -R bun:bun /var/log/eurostat-mcp-server /var/lib/eurostat-mcp-server
|
|
80
91
|
|
|
81
92
|
# Switch to the non-root user
|
|
82
93
|
USER bun
|
|
@@ -94,6 +105,8 @@ ENV MCP_SESSION_MODE="stateless"
|
|
|
94
105
|
ENV MCP_LOG_LEVEL="info"
|
|
95
106
|
ENV LOGS_DIR="/var/log/eurostat-mcp-server"
|
|
96
107
|
ENV MCP_FORCE_CONSOLE_LOGGING="true"
|
|
108
|
+
ENV CANVAS_TEMP_PATH="/var/lib/eurostat-mcp-server/canvas-tmp"
|
|
109
|
+
ENV CANVAS_EXPORT_PATH="/var/lib/eurostat-mcp-server/canvas-exports"
|
|
97
110
|
|
|
98
111
|
# Expose the port the server listens on
|
|
99
112
|
EXPOSE ${MCP_HTTP_PORT}
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<h1>@cyanheads/eurostat-mcp-server</h1>
|
|
3
3
|
<p><b>Search and query the Eurostat catalogue — EU economy, demography, trade, health, and NUTS regional data via MCP. STDIO or Streamable HTTP.</b>
|
|
4
|
-
<div>
|
|
4
|
+
<div>6 Tools (8 with the dataframe canvas) • 1 Resource</div>
|
|
5
5
|
</p>
|
|
6
6
|
</div>
|
|
7
7
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/eurostat-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/eurostat-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
13
13
|
|
|
14
14
|
</div>
|
|
15
15
|
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
## Tools
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
6 tools for discovering and querying Eurostat statistical datasets, plus 2 more when the optional dataframe canvas is enabled:
|
|
35
35
|
|
|
36
36
|
| Tool | Description |
|
|
37
37
|
|:---|:---|
|
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
| `eurostat_get_dataset_info` | Fetch metadata for a dataset: dimensions with sample values, time range, observation count, and last-update date |
|
|
41
41
|
| `eurostat_get_dimension_values` | List all valid codes for a specific dimension (e.g., all geo codes, all unit codes); supports NUTS hierarchy filtering |
|
|
42
42
|
| `eurostat_query_dataset` | Fetch decoded statistical observations with dimension filters, NUTS geo-level, and time-range controls |
|
|
43
|
+
| `eurostat_download_dataset` | Download a whole dataset through the SDMX 2.1 TSV bulk endpoint and stage every observation on the dataframe canvas |
|
|
44
|
+
| `eurostat_dataframe_describe` | List the tables staged on a dataframe canvas with their row counts and column types — canvas only |
|
|
45
|
+
| `eurostat_dataframe_query` | Run a read-only SQL SELECT across staged tables — canvas only |
|
|
43
46
|
|
|
44
47
|
### `eurostat_search_datasets`
|
|
45
48
|
|
|
@@ -99,8 +102,39 @@ Fetch statistical data from a Eurostat dataset.
|
|
|
99
102
|
- Time range via `since_period`/`until_period` (e.g., `"2020"`, `"2023-Q1"`) or `last_n_periods` for the N most recent
|
|
100
103
|
- Returns decoded observations with dimension codes and labels, numeric values, and status flags (`p` = provisional, `e` = estimated, etc.)
|
|
101
104
|
- Reports total observation count, missing value count, and the effective time range of the result, each period bound omitted when neither the observations nor Eurostat report it
|
|
102
|
-
-
|
|
105
|
+
- Inline rows are capped at 5,000, applied while decoding so a broad query never builds the rest; `obsCount`, `missingObsCount` and `timeRange` still describe the whole match, and `truncated` flags when the cap bit. Filter the query to shrink what Eurostat sends — the cap bounds the decode, not the transfer
|
|
106
|
+
- With the dataframe canvas enabled, a match past the cap is also staged whole as a SQL table and the response returns `canvasId` / `tableName` / `stagedRowCount`; the rows are streamed into the table one at a time from the response body already in memory, so nothing extra is fetched and the match is never materialized as an array. Without a canvas those fields are absent and narrowing the query is the way to the rest
|
|
107
|
+
- Pass `canvas_id` from an earlier response to stage several results side by side and join across them
|
|
103
108
|
- Async-response detection — large unfiltered queries return an actionable, non-retryable error with filter guidance rather than silently timing out
|
|
109
|
+
- Fetches a *slice*. When the target is a whole dataset, `eurostat_download_dataset` reads the SDMX bulk endpoint instead, at roughly half the bytes
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### `eurostat_download_dataset`
|
|
114
|
+
|
|
115
|
+
Download a whole dataset through the SDMX 2.1 TSV bulk endpoint (`/sdmx/2.1/data/{dataset}?format=TSV`).
|
|
116
|
+
|
|
117
|
+
- The TSV wire format runs 48–63% of the JSON-stat body `eurostat_query_dataset` reads for the same data, because the wide layout writes each dimension key once per row instead of once per observation. Measured across four datasets from 1.1M to 12.8M observations
|
|
118
|
+
- Filters take the same `{dimension_code: [value, ...]}` map as `eurostat_query_dataset` and are applied by Eurostat before the body is sent. They become a positional key on the request path, which must carry one position per dimension — the server builds it from the dataset's own dimension order, so a filter naming a dimension the dataset does not have is rejected with the real list rather than sent as a malformed key
|
|
119
|
+
- Narrow periods with `since_period` / `until_period`. There is deliberately no "last N periods": the TSV layout keeps a column for every period whichever selector is used, and `lastNObservations` merely blanks the unselected cells — measured at ~3× the equivalent JSON-stat body. `startPeriod` removes the columns
|
|
120
|
+
- **Byte budget enforced while streaming.** Eurostat sends the body chunked with no `Content-Length`, so the limit is applied as bytes arrive and the transfer is aborted the moment it is spent — not measured after the fact. A truncated download returns its rows with `budgetExceeded: true` rather than an error, so the work already paid for is not discarded. `EUROSTAT_BULK_MAX_BYTES` sets the ceiling
|
|
121
|
+
- **gzip is sniffed off the stream, not read from headers.** Eurostat compresses large bodies with no `Content-Encoding` header; the only header-level tell is a `.tsv.gz` filename on `Content-Disposition`, and the switch does not track dataset size, so the magic bytes are what decide
|
|
122
|
+
- **The asynchronous queue envelope is detected explicitly.** When an extraction is too costly to serve inline Eurostat answers HTTP 200 with a SOAP `syncResponse` ticket instead of data; read as TSV that yields a header row of XML and no observations, so it is classified up front as a non-retryable error naming what to narrow
|
|
123
|
+
- Errors arrive as XML SOAP faults, not JSON: faultcode 100 → `not_found`, 140 → `filter_arity`, 150 → `invalid_dimension` (which also covers a period range outside the dataset's coverage). Each maps to a typed reason with a recovery hint naming the tool to call next
|
|
124
|
+
- With the dataframe canvas enabled, every observation is staged as a SQL table and the response returns `canvasId` / `tableName` / `stagedRowCount`; rows stream into the table one at a time, so a multi-million-row download never materializes as an array. Only `preview_limit` rows (default 50, max 500) come back inline, and they are the leading rows of the staged table
|
|
125
|
+
- Without a canvas the download still runs so `rowCount`, `missingCount` and `periodRange` describe it, but only the preview is retained — the response says so plainly instead of implying the rest is reachable
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### `eurostat_dataframe_describe` / `eurostat_dataframe_query`
|
|
130
|
+
|
|
131
|
+
SQL over the results `eurostat_query_dataset` and `eurostat_download_dataset` stage. **Listed only when the dataframe canvas is enabled** (`CANVAS_PROVIDER_TYPE=duckdb`); the server is fully functional without it, and clients never see tools they cannot call.
|
|
132
|
+
|
|
133
|
+
- `eurostat_dataframe_describe` lists the staged tables with row counts and column names and types — call it before writing SQL
|
|
134
|
+
- `eurostat_dataframe_query` runs a single read-only `SELECT`. Statement chaining, non-`SELECT` verbs, and functions that read files or external data are rejected with a typed error
|
|
135
|
+
- Staged columns are flat, and the two stagers write different sets — call `eurostat_dataframe_describe` rather than assuming. `eurostat_query_dataset` gives each dimension a code column named after the dimension (`geo`) plus a label companion (`geo_label`), followed by `obs_value`, `obs_flag`, `obs_flag_label`. `eurostat_download_dataset` gives code columns only, since the bulk endpoint carries no labels, plus a `time` column and `obs_value`, `obs_flag`, `obs_flag_label`, `conf_status`, `conf_status_label`
|
|
136
|
+
- Tables from the two stagers join on their dimension code columns and `time` — same names, same `VARCHAR` type, with `obs_value` `DOUBLE` on both. `obs_flag` is the one column that shares a name without sharing a meaning: JSON-stat folds confidentiality into the observation status, so a confidential cell reads `obs_flag = '|C'` on a query-staged table and `obs_flag = NULL` with `conf_status = 'C'` on a bulk-staged one. Join on the codes, not on the flag
|
|
137
|
+
- The DuckDB binding ships with the server, so `CANVAS_PROVIDER_TYPE=duckdb` is the only switch. The exception is the one-click `.mcpb` bundle, which strips platform-specific native bindings to stay portable — a bundle install cannot run the canvas, so reach for the npm, Docker, or from-source install for SQL analytics
|
|
104
138
|
|
|
105
139
|
## Resource
|
|
106
140
|
|
|
@@ -126,10 +160,12 @@ Eurostat-specific:
|
|
|
126
160
|
- Async-response detection — Eurostat returns a warning object rather than an error for over-limit queries; the server intercepts it and returns an actionable error with filter guidance
|
|
127
161
|
- NUTS hierarchy geo-level filtering across query and dimension-value tools
|
|
128
162
|
- Status flag decoding (provisional, estimated, definition differs, etc.)
|
|
163
|
+
- Optional DuckDB dataframe canvas — a query matching more than the inline cap is streamed row by row into a SQL table, reaching the observations the cap drops without a second request to Eurostat
|
|
164
|
+
- SDMX 2.1 TSV bulk downloads with streaming gzip detection, a mid-transfer byte budget, wide-to-long expansion, and SOAP fault classification — the whole-dataset counterpart to the per-query path
|
|
129
165
|
|
|
130
166
|
Agent-friendly output:
|
|
131
167
|
|
|
132
|
-
- Discovery workflow: `eurostat_search_datasets` / `eurostat_browse_themes` → `eurostat_get_dataset_info` → `eurostat_get_dimension_values` → `eurostat_query_dataset`
|
|
168
|
+
- Discovery workflow: `eurostat_search_datasets` / `eurostat_browse_themes` → `eurostat_get_dataset_info` → `eurostat_get_dimension_values` → `eurostat_query_dataset` for a slice, or `eurostat_download_dataset` for the whole dataset
|
|
133
169
|
- Invalid dimension codes in query filters silently return no data from Eurostat — the `eurostat_get_dimension_values` tool prevents this by letting agents verify codes first
|
|
134
170
|
- Structured error contracts with typed reasons and recovery hints on all tools
|
|
135
171
|
|
|
@@ -251,6 +287,12 @@ All configuration is validated at startup via Zod schemas in `src/config/server-
|
|
|
251
287
|
| `EUROSTAT_BASE_URL` | Eurostat API base URL | `https://ec.europa.eu/eurostat/api/dissemination` |
|
|
252
288
|
| `EUROSTAT_REQUEST_TIMEOUT_MS` | HTTP request timeout in ms | `30000` |
|
|
253
289
|
| `EUROSTAT_TOC_CACHE_TTL_MS` | Catalogue TOC cache lifetime in ms — the first search or browse call past this age refreshes it | `43200000` (12 hours) |
|
|
290
|
+
| `EUROSTAT_BULK_TIMEOUT_MS` | HTTP timeout for one `eurostat_download_dataset` transfer in ms — held separate because a bulk body streams for minutes | `120000` (2 minutes) |
|
|
291
|
+
| `EUROSTAT_BULK_MAX_BYTES` | Byte budget for one bulk download, counted on the decoded TSV and enforced while streaming | `52428800` (50 MiB) |
|
|
292
|
+
| `CANVAS_PROVIDER_TYPE` | `duckdb` enables the dataframe canvas: lists the two dataframe tools, lets `eurostat_query_dataset` stage a match past its inline cap, and lets `eurostat_download_dataset` retain a bulk download | `none` |
|
|
293
|
+
| `CANVAS_TEMP_PATH` | Directory DuckDB writes canvas spill files to. Must be writable by the server process | `<os tmpdir>/mcp-canvas` |
|
|
294
|
+
| `CANVAS_TTL_MS` | Sliding lifetime of a staged canvas in ms; every call against it extends the window | `86400000` (24 hours) |
|
|
295
|
+
| `CANVAS_DEFAULT_ROW_LIMIT` | Max rows one `eurostat_dataframe_query` returns before reporting `truncated` | `10000` |
|
|
254
296
|
| `OTEL_ENABLED` | Enable OpenTelemetry | `false` |
|
|
255
297
|
|
|
256
298
|
## Running the server
|
|
@@ -279,10 +321,11 @@ All configuration is validated at startup via Zod schemas in `src/config/server-
|
|
|
279
321
|
|
|
280
322
|
| Directory | Purpose |
|
|
281
323
|
|:---|:---|
|
|
282
|
-
| `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). Five tools for discovery and data access. |
|
|
324
|
+
| `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). Five tools for discovery and data access, plus two canvas-gated dataframe tools. |
|
|
283
325
|
| `src/mcp-server/resources` | Resource definitions. Dataset metadata resource. |
|
|
284
326
|
| `src/services/eurostat-catalogue` | Catalogue service — fetches and parses the Eurostat TOC TXT file; TTL-bounded in-memory cache. |
|
|
285
|
-
| `src/services/eurostat-data` | Data service — Statistics API HTTP client, JSON-stat 2.0 decoder, async-response detection. |
|
|
327
|
+
| `src/services/eurostat-data` | Data service — Statistics API HTTP client, JSON-stat 2.0 decoder, async-response detection, dataframe row source. |
|
|
328
|
+
| `src/services/canvas-accessor.ts` | Module-level accessor for the optional DataCanvas, plus the acquire helper that names the misconfigured path on a permission failure. |
|
|
286
329
|
| `src/config` | Server-specific environment variable parsing and validation with Zod. |
|
|
287
330
|
| `tests/` | Unit and integration tests, mirroring the `src/` structure. |
|
|
288
331
|
|
|
@@ -7,6 +7,8 @@ declare const ServerConfigSchema: z.ZodObject<{
|
|
|
7
7
|
baseUrl: z.ZodDefault<z.ZodString>;
|
|
8
8
|
requestTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
9
9
|
tocCacheTtlMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
10
|
+
bulkTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
11
|
+
bulkMaxBytes: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
10
12
|
}, z.core.$strip>;
|
|
11
13
|
export declare function getServerConfig(): z.infer<typeof ServerConfigSchema>;
|
|
12
14
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB
|
|
1
|
+
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB;;;;;;iBAoCtB,CAAC;AAIH,wBAAgB,eAAe,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CASpE"}
|
|
@@ -22,6 +22,18 @@ const ServerConfigSchema = z.object({
|
|
|
22
22
|
.positive()
|
|
23
23
|
.default(43_200_000)
|
|
24
24
|
.describe('How long a fetched catalogue TOC stays usable before the next catalogue call refreshes it, in milliseconds. Defaults to 12 hours, matching the upstream twice-daily update cadence.'),
|
|
25
|
+
bulkTimeoutMs: z.coerce
|
|
26
|
+
.number()
|
|
27
|
+
.int()
|
|
28
|
+
.positive()
|
|
29
|
+
.default(120_000)
|
|
30
|
+
.describe('HTTP timeout for one SDMX bulk download, in milliseconds. Held separate from EUROSTAT_REQUEST_TIMEOUT_MS because a bulk body streams for minutes where a metadata call answers in seconds. Defaults to 2 minutes.'),
|
|
31
|
+
bulkMaxBytes: z.coerce
|
|
32
|
+
.number()
|
|
33
|
+
.int()
|
|
34
|
+
.positive()
|
|
35
|
+
.default(52_428_800)
|
|
36
|
+
.describe('Byte budget for one SDMX bulk download, measured on the decoded TSV rather than on the wire. Eurostat sends the body chunked with no Content-Length, so the budget is enforced while streaming and the transfer is aborted the moment it is reached. Defaults to 50 MiB.'),
|
|
25
37
|
});
|
|
26
38
|
let _config;
|
|
27
39
|
export function getServerConfig() {
|
|
@@ -29,6 +41,8 @@ export function getServerConfig() {
|
|
|
29
41
|
baseUrl: 'EUROSTAT_BASE_URL',
|
|
30
42
|
requestTimeoutMs: 'EUROSTAT_REQUEST_TIMEOUT_MS',
|
|
31
43
|
tocCacheTtlMs: 'EUROSTAT_TOC_CACHE_TTL_MS',
|
|
44
|
+
bulkTimeoutMs: 'EUROSTAT_BULK_TIMEOUT_MS',
|
|
45
|
+
bulkMaxBytes: 'EUROSTAT_BULK_MAX_BYTES',
|
|
32
46
|
});
|
|
33
47
|
return _config;
|
|
34
48
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,OAAO,EAAE,CAAC;SACP,MAAM,EAAE;SACR,GAAG,EAAE;SACL,OAAO,CAAC,iDAAiD,CAAC;SAC1D,QAAQ,CAAC,uBAAuB,CAAC;IACpC,gBAAgB,EAAE,CAAC,CAAC,MAAM;SACvB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,MAAM,CAAC;SACf,QAAQ,CAAC,sCAAsC,CAAC;IACnD,aAAa,EAAE,CAAC,CAAC,MAAM;SACpB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,UAAU,CAAC;SACnB,QAAQ,CACP,qLAAqL,CACtL;CACJ,CAAC,CAAC;AAEH,IAAI,OAAuD,CAAC;AAE5D,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,cAAc,CAAC,kBAAkB,EAAE;QAC7C,OAAO,EAAE,mBAAmB;QAC5B,gBAAgB,EAAE,6BAA6B;QAC/C,aAAa,EAAE,2BAA2B;
|
|
1
|
+
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,OAAO,EAAE,CAAC;SACP,MAAM,EAAE;SACR,GAAG,EAAE;SACL,OAAO,CAAC,iDAAiD,CAAC;SAC1D,QAAQ,CAAC,uBAAuB,CAAC;IACpC,gBAAgB,EAAE,CAAC,CAAC,MAAM;SACvB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,MAAM,CAAC;SACf,QAAQ,CAAC,sCAAsC,CAAC;IACnD,aAAa,EAAE,CAAC,CAAC,MAAM;SACpB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,UAAU,CAAC;SACnB,QAAQ,CACP,qLAAqL,CACtL;IACH,aAAa,EAAE,CAAC,CAAC,MAAM;SACpB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,OAAO,CAAC;SAChB,QAAQ,CACP,mNAAmN,CACpN;IACH,YAAY,EAAE,CAAC,CAAC,MAAM;SACnB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,UAAU,CAAC;SACnB,QAAQ,CACP,0QAA0Q,CAC3Q;CACJ,CAAC,CAAC;AAEH,IAAI,OAAuD,CAAC;AAE5D,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,cAAc,CAAC,kBAAkB,EAAE;QAC7C,OAAO,EAAE,mBAAmB;QAC5B,gBAAgB,EAAE,6BAA6B;QAC/C,aAAa,EAAE,2BAA2B;QAC1C,aAAa,EAAE,0BAA0B;QACzC,YAAY,EAAE,yBAAyB;KACxC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,15 +3,34 @@
|
|
|
3
3
|
* @fileoverview eurostat-mcp-server MCP server entry point.
|
|
4
4
|
* @module index
|
|
5
5
|
*/
|
|
6
|
-
import { createApp } from '@cyanheads/mcp-ts-core';
|
|
6
|
+
import { createApp, disabledTool } from '@cyanheads/mcp-ts-core';
|
|
7
|
+
import { config } from '@cyanheads/mcp-ts-core/config';
|
|
7
8
|
import { eurostatDatasetResource } from './mcp-server/resources/definitions/eurostat-dataset.resource.js';
|
|
8
9
|
import { eurostatBrowseThemes } from './mcp-server/tools/definitions/eurostat-browse-themes.tool.js';
|
|
10
|
+
import { eurostatDataframeDescribe } from './mcp-server/tools/definitions/eurostat-dataframe-describe.tool.js';
|
|
11
|
+
import { eurostatDataframeQuery } from './mcp-server/tools/definitions/eurostat-dataframe-query.tool.js';
|
|
12
|
+
import { eurostatDownloadDataset } from './mcp-server/tools/definitions/eurostat-download-dataset.tool.js';
|
|
9
13
|
import { eurostatGetDatasetInfo } from './mcp-server/tools/definitions/eurostat-get-dataset-info.tool.js';
|
|
10
14
|
import { eurostatGetDimensionValues } from './mcp-server/tools/definitions/eurostat-get-dimension-values.tool.js';
|
|
11
15
|
import { eurostatQueryDataset } from './mcp-server/tools/definitions/eurostat-query-dataset.tool.js';
|
|
12
16
|
import { eurostatSearchDatasets } from './mcp-server/tools/definitions/eurostat-search-datasets.tool.js';
|
|
17
|
+
import { setCanvas } from './services/canvas-accessor.js';
|
|
18
|
+
import { initEurostatBulkService } from './services/eurostat-bulk/eurostat-bulk-service.js';
|
|
13
19
|
import { initEurostatCatalogueService } from './services/eurostat-catalogue/eurostat-catalogue-service.js';
|
|
14
20
|
import { initEurostatDataService } from './services/eurostat-data/eurostat-data-service.js';
|
|
21
|
+
/**
|
|
22
|
+
* The dataframe tools address tables on a canvas, so without one they have
|
|
23
|
+
* nothing to talk to. `disabledTool` keeps them off `tools/list` — clients
|
|
24
|
+
* cannot call them — while leaving them visible on the landing page and the
|
|
25
|
+
* server card with the variable that turns them on, so an operator can see the
|
|
26
|
+
* capability exists rather than wondering why the README documents a tool the
|
|
27
|
+
* server does not advertise.
|
|
28
|
+
*/
|
|
29
|
+
const canvasEnabled = config.canvas.providerType !== 'none';
|
|
30
|
+
const CANVAS_OFF = {
|
|
31
|
+
reason: 'This deployment runs without a dataframe canvas, so there are no tables to address.',
|
|
32
|
+
hint: 'CANVAS_PROVIDER_TYPE=duckdb',
|
|
33
|
+
};
|
|
15
34
|
await createApp({
|
|
16
35
|
name: 'eurostat-mcp-server',
|
|
17
36
|
title: 'eurostat-mcp-server',
|
|
@@ -21,16 +40,23 @@ await createApp({
|
|
|
21
40
|
eurostatGetDatasetInfo,
|
|
22
41
|
eurostatGetDimensionValues,
|
|
23
42
|
eurostatQueryDataset,
|
|
43
|
+
eurostatDownloadDataset,
|
|
44
|
+
canvasEnabled ? eurostatDataframeDescribe : disabledTool(eurostatDataframeDescribe, CANVAS_OFF),
|
|
45
|
+
canvasEnabled ? eurostatDataframeQuery : disabledTool(eurostatDataframeQuery, CANVAS_OFF),
|
|
24
46
|
],
|
|
25
47
|
resources: [eurostatDatasetResource],
|
|
26
48
|
prompts: [],
|
|
27
49
|
instructions: `Eurostat MCP server — EU statistical data across the Eurostat catalogue.
|
|
28
50
|
Workflow: eurostat_search_datasets or eurostat_browse_themes to find a dataset code → eurostat_get_dataset_info to see dimensions → eurostat_get_dimension_values to list valid filter values → eurostat_query_dataset to fetch observations.
|
|
29
|
-
Apply dimension filters to
|
|
51
|
+
Apply dimension filters to keep queries small: an unfiltered query on a large dataset is slow and can return an async response error instead of data.
|
|
52
|
+
Two paths past eurostat_query_dataset's 5,000-observation inline cap. For a slice of a dataset, keep using eurostat_query_dataset: when the match is larger than the cap and this deployment runs a dataframe canvas, the whole match is also staged as a SQL table named in the response. For a whole dataset, use eurostat_download_dataset, which reads the SDMX bulk endpoint instead — about half the bytes — and stages every observation the same way, bounded by a byte budget it reports back. Read either staged table with eurostat_dataframe_query (eurostat_dataframe_describe lists the table and column names); the two stage different column sets, since the bulk endpoint carries dimension codes with no labels.
|
|
53
|
+
The dataframe tools are not listed when the canvas is off. There, narrowing the filters is what brings a capped match into reach, and eurostat_download_dataset retains only its inline preview.`,
|
|
30
54
|
landing: { requireAuth: false },
|
|
31
55
|
setup(core) {
|
|
32
56
|
initEurostatCatalogueService(core.config, core.storage);
|
|
33
57
|
initEurostatDataService(core.config, core.storage);
|
|
58
|
+
initEurostatBulkService(core.config, core.storage);
|
|
59
|
+
setCanvas(core.canvas);
|
|
34
60
|
},
|
|
35
61
|
});
|
|
36
62
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AACvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,iEAAiE,CAAC;AAC1G,OAAO,EAAE,oBAAoB,EAAE,MAAM,+DAA+D,CAAC;AACrG,OAAO,EAAE,yBAAyB,EAAE,MAAM,oEAAoE,CAAC;AAC/G,OAAO,EAAE,sBAAsB,EAAE,MAAM,iEAAiE,CAAC;AACzG,OAAO,EAAE,uBAAuB,EAAE,MAAM,kEAAkE,CAAC;AAC3G,OAAO,EAAE,sBAAsB,EAAE,MAAM,kEAAkE,CAAC;AAC1G,OAAO,EAAE,0BAA0B,EAAE,MAAM,sEAAsE,CAAC;AAClH,OAAO,EAAE,oBAAoB,EAAE,MAAM,+DAA+D,CAAC;AACrG,OAAO,EAAE,sBAAsB,EAAE,MAAM,iEAAiE,CAAC;AACzG,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,mDAAmD,CAAC;AAC5F,OAAO,EAAE,4BAA4B,EAAE,MAAM,6DAA6D,CAAC;AAC3G,OAAO,EAAE,uBAAuB,EAAE,MAAM,mDAAmD,CAAC;AAE5F;;;;;;;GAOG;AACH,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,KAAK,MAAM,CAAC;AAC5D,MAAM,UAAU,GAAG;IACjB,MAAM,EAAE,qFAAqF;IAC7F,IAAI,EAAE,6BAA6B;CAC3B,CAAC;AAEX,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,qBAAqB;IAC3B,KAAK,EAAE,qBAAqB;IAC5B,KAAK,EAAE;QACL,sBAAsB;QACtB,oBAAoB;QACpB,sBAAsB;QACtB,0BAA0B;QAC1B,oBAAoB;QACpB,uBAAuB;QACvB,aAAa,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,YAAY,CAAC,yBAAyB,EAAE,UAAU,CAAC;QAC/F,aAAa,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,YAAY,CAAC,sBAAsB,EAAE,UAAU,CAAC;KAC1F;IACD,SAAS,EAAE,CAAC,uBAAuB,CAAC;IACpC,OAAO,EAAE,EAAE;IACX,YAAY,EAAE;;;;iMAIiL;IAC/L,OAAO,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE;IAC/B,KAAK,CAAC,IAAI;QACR,4BAA4B,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QACxD,uBAAuB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QACnD,uBAAuB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QACnD,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzB,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tool for listing the tables staged on a Eurostat DataCanvas.
|
|
3
|
+
* @module mcp-server/tools/definitions/eurostat-dataframe-describe.tool
|
|
4
|
+
*/
|
|
5
|
+
import { z } from '@cyanheads/mcp-ts-core';
|
|
6
|
+
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
+
export declare const eurostatDataframeDescribe: import("@cyanheads/mcp-ts-core").ToolDefinition<z.ZodObject<{
|
|
8
|
+
canvas_id: z.ZodString;
|
|
9
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
10
|
+
canvasId: z.ZodString;
|
|
11
|
+
expiresAt: z.ZodString;
|
|
12
|
+
tables: z.ZodArray<z.ZodObject<{
|
|
13
|
+
name: z.ZodString;
|
|
14
|
+
kind: z.ZodEnum<{
|
|
15
|
+
table: "table";
|
|
16
|
+
view: "view";
|
|
17
|
+
}>;
|
|
18
|
+
rowCount: z.ZodNumber;
|
|
19
|
+
expiresAt: z.ZodOptional<z.ZodString>;
|
|
20
|
+
columns: z.ZodArray<z.ZodObject<{
|
|
21
|
+
name: z.ZodString;
|
|
22
|
+
type: z.ZodString;
|
|
23
|
+
nullable: z.ZodBoolean;
|
|
24
|
+
}, z.core.$strip>>;
|
|
25
|
+
}, z.core.$strip>>;
|
|
26
|
+
}, z.core.$strip>, readonly [{
|
|
27
|
+
readonly reason: "canvas_disabled";
|
|
28
|
+
readonly code: JsonRpcErrorCode.ServiceUnavailable;
|
|
29
|
+
readonly when: "This deployment runs without a dataframe canvas, so there is nothing to describe.";
|
|
30
|
+
readonly retryable: false;
|
|
31
|
+
readonly recovery: "Query the data inline with eurostat_query_dataset, narrowing dimension filters until the result fits.";
|
|
32
|
+
}, {
|
|
33
|
+
readonly reason: "canvas_not_found";
|
|
34
|
+
readonly code: JsonRpcErrorCode.NotFound;
|
|
35
|
+
readonly when: "The canvas_id is unknown or its lifetime has elapsed.";
|
|
36
|
+
readonly recovery: "Re-run eurostat_query_dataset to stage the data again and use the canvasId it returns.";
|
|
37
|
+
}], {
|
|
38
|
+
readonly notice: z.ZodOptional<z.ZodString>;
|
|
39
|
+
}>;
|
|
40
|
+
//# sourceMappingURL=eurostat-dataframe-describe.tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eurostat-dataframe-describe.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAGjE,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgIpC,CAAC"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tool for listing the tables staged on a Eurostat DataCanvas.
|
|
3
|
+
* @module mcp-server/tools/definitions/eurostat-dataframe-describe.tool
|
|
4
|
+
*/
|
|
5
|
+
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
6
|
+
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
+
import { acquireCanvas, getCanvas } from '../../../services/canvas-accessor.js';
|
|
8
|
+
export const eurostatDataframeDescribe = tool('eurostat_dataframe_describe', {
|
|
9
|
+
title: 'Describe Eurostat Dataframes',
|
|
10
|
+
description: 'List the tables staged on a Eurostat dataframe canvas, with their row counts and column names and types. Call this before eurostat_dataframe_query to learn the table and column names to write SQL against. The canvas_id comes from a eurostat_query_dataset or eurostat_download_dataset response that reported a staged table. Every observation column is flat, but the two stagers write different column sets, so read the columns reported here rather than assuming: eurostat_query_dataset gives each dimension a code column named after the dimension (e.g. "geo") plus a label companion (e.g. "geo_label"), alongside obs_value, obs_flag and obs_flag_label; eurostat_download_dataset gives code columns only — the bulk endpoint carries no labels — plus a "time" column and obs_value, obs_flag, obs_flag_label, conf_status and conf_status_label. The two also disagree on what obs_flag holds, which a column list cannot show: JSON-stat folds confidentiality into the observation status, so a confidential cell reads obs_flag "|C" on a eurostat_query_dataset table but obs_flag null with conf_status "C" on a eurostat_download_dataset one. Join tables from the two stagers on dimension codes and time; obs_flag does not mean the same thing on both sides.',
|
|
11
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
12
|
+
input: z.object({
|
|
13
|
+
canvas_id: z
|
|
14
|
+
.string()
|
|
15
|
+
.min(1)
|
|
16
|
+
.describe('Canvas identifier returned as canvasId by eurostat_query_dataset or eurostat_download_dataset. Identifies the workspace holding the staged tables.'),
|
|
17
|
+
}),
|
|
18
|
+
output: z.object({
|
|
19
|
+
canvasId: z.string().describe('Canvas identifier the tables were read from.'),
|
|
20
|
+
expiresAt: z
|
|
21
|
+
.string()
|
|
22
|
+
.describe('ISO 8601 timestamp when the canvas expires. Every call on it slides this forward.'),
|
|
23
|
+
tables: z
|
|
24
|
+
.array(z
|
|
25
|
+
.object({
|
|
26
|
+
name: z.string().describe('Table name to reference in SQL.'),
|
|
27
|
+
kind: z
|
|
28
|
+
.enum(['table', 'view'])
|
|
29
|
+
.describe('Whether the entry is a base table or a view.'),
|
|
30
|
+
rowCount: z.number().describe('Rows the table holds.'),
|
|
31
|
+
expiresAt: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('ISO 8601 expiry of this table specifically. Omitted when the table ages with the canvas rather than on its own clock.'),
|
|
35
|
+
columns: z
|
|
36
|
+
.array(z
|
|
37
|
+
.object({
|
|
38
|
+
name: z.string().describe('Column name.'),
|
|
39
|
+
type: z.string().describe('SQL column type (e.g. "VARCHAR", "DOUBLE").'),
|
|
40
|
+
nullable: z.boolean().describe('Whether the column admits NULL.'),
|
|
41
|
+
})
|
|
42
|
+
.describe('One column of the table.'))
|
|
43
|
+
.describe('Columns in declaration order.'),
|
|
44
|
+
})
|
|
45
|
+
.describe('One table or view staged on the canvas.'))
|
|
46
|
+
.describe('Tables staged on this canvas. Empty when nothing has been staged yet, or when every staged table has expired.'),
|
|
47
|
+
}),
|
|
48
|
+
enrichment: {
|
|
49
|
+
notice: z
|
|
50
|
+
.string()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('Guidance when the canvas holds no tables. Omitted when it holds at least one.'),
|
|
53
|
+
},
|
|
54
|
+
errors: [
|
|
55
|
+
{
|
|
56
|
+
reason: 'canvas_disabled',
|
|
57
|
+
code: JsonRpcErrorCode.ServiceUnavailable,
|
|
58
|
+
when: 'This deployment runs without a dataframe canvas, so there is nothing to describe.',
|
|
59
|
+
retryable: false,
|
|
60
|
+
recovery: 'Query the data inline with eurostat_query_dataset, narrowing dimension filters until the result fits.',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
reason: 'canvas_not_found',
|
|
64
|
+
code: JsonRpcErrorCode.NotFound,
|
|
65
|
+
when: 'The canvas_id is unknown or its lifetime has elapsed.',
|
|
66
|
+
recovery: 'Re-run eurostat_query_dataset to stage the data again and use the canvasId it returns.',
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
async handler(input, ctx) {
|
|
70
|
+
const canvas = getCanvas();
|
|
71
|
+
if (!canvas) {
|
|
72
|
+
throw ctx.fail('canvas_disabled', undefined, ctx.recoveryFor('canvas_disabled'));
|
|
73
|
+
}
|
|
74
|
+
const instance = await acquireCanvas(canvas, input.canvas_id, ctx);
|
|
75
|
+
const tables = await instance.describe();
|
|
76
|
+
ctx.log.info('Described canvas', { canvasId: instance.canvasId, tableCount: tables.length });
|
|
77
|
+
if (tables.length === 0) {
|
|
78
|
+
ctx.enrich.notice(`Canvas ${instance.canvasId} holds no tables. Staged tables expire on their own schedule — re-run eurostat_query_dataset to stage the data again.`);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
canvasId: instance.canvasId,
|
|
82
|
+
expiresAt: instance.expiresAt,
|
|
83
|
+
tables: tables.map((t) => ({
|
|
84
|
+
name: t.name,
|
|
85
|
+
kind: t.kind,
|
|
86
|
+
rowCount: t.rowCount,
|
|
87
|
+
...(t.expiresAt !== undefined && { expiresAt: t.expiresAt }),
|
|
88
|
+
columns: t.columns.map((c) => ({
|
|
89
|
+
name: c.name,
|
|
90
|
+
type: c.type,
|
|
91
|
+
nullable: c.nullable ?? true,
|
|
92
|
+
})),
|
|
93
|
+
})),
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
format: (result) => {
|
|
97
|
+
const lines = [
|
|
98
|
+
`# Canvas \`${result.canvasId}\``,
|
|
99
|
+
`**Expires:** ${result.expiresAt} | **Tables:** ${result.tables.length}\n`,
|
|
100
|
+
];
|
|
101
|
+
for (const table of result.tables) {
|
|
102
|
+
const expiry = table.expiresAt === undefined ? '' : ` | **expiresAt:** ${table.expiresAt}`;
|
|
103
|
+
lines.push(`## \`${table.name}\` (${table.kind})`);
|
|
104
|
+
lines.push(`**Rows:** ${table.rowCount}${expiry}`);
|
|
105
|
+
for (const col of table.columns) {
|
|
106
|
+
lines.push(`- \`${col.name}\` ${col.type} (nullable: ${col.nullable})`);
|
|
107
|
+
}
|
|
108
|
+
lines.push('');
|
|
109
|
+
}
|
|
110
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
//# sourceMappingURL=eurostat-dataframe-describe.tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eurostat-dataframe-describe.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tool for running read-only SQL against tables staged on a
|
|
3
|
+
* Eurostat DataCanvas.
|
|
4
|
+
* @module mcp-server/tools/definitions/eurostat-dataframe-query.tool
|
|
5
|
+
*/
|
|
6
|
+
import { z } from '@cyanheads/mcp-ts-core';
|
|
7
|
+
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
8
|
+
export declare const eurostatDataframeQuery: import("@cyanheads/mcp-ts-core").ToolDefinition<z.ZodObject<{
|
|
9
|
+
canvas_id: z.ZodString;
|
|
10
|
+
sql: z.ZodString;
|
|
11
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
12
|
+
canvasId: z.ZodString;
|
|
13
|
+
columns: z.ZodArray<z.ZodString>;
|
|
14
|
+
rows: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
15
|
+
rowCount: z.ZodNumber;
|
|
16
|
+
truncated: z.ZodBoolean;
|
|
17
|
+
}, z.core.$strip>, readonly [{
|
|
18
|
+
readonly reason: "canvas_disabled";
|
|
19
|
+
readonly code: JsonRpcErrorCode.ServiceUnavailable;
|
|
20
|
+
readonly when: "This deployment runs without a dataframe canvas, so there is nothing to query.";
|
|
21
|
+
readonly retryable: false;
|
|
22
|
+
readonly recovery: "Query the data inline with eurostat_query_dataset, narrowing dimension filters until the result fits.";
|
|
23
|
+
}, {
|
|
24
|
+
readonly reason: "canvas_not_found";
|
|
25
|
+
readonly code: JsonRpcErrorCode.NotFound;
|
|
26
|
+
readonly when: "The canvas_id is unknown or its lifetime has elapsed.";
|
|
27
|
+
readonly recovery: "Re-run eurostat_query_dataset to stage the data again and use the canvasId it returns.";
|
|
28
|
+
}, {
|
|
29
|
+
readonly reason: "missing_table";
|
|
30
|
+
readonly code: JsonRpcErrorCode.NotFound;
|
|
31
|
+
readonly when: "The SQL names a table that is not staged on this canvas, or that has expired.";
|
|
32
|
+
readonly recovery: "Call eurostat_dataframe_describe for the staged table names, or re-run eurostat_query_dataset to stage them again.";
|
|
33
|
+
}], undefined>;
|
|
34
|
+
//# sourceMappingURL=eurostat-dataframe-query.tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eurostat-dataframe-query.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/eurostat-dataframe-query.tool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAGjE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;cAiHjC,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tool for running read-only SQL against tables staged on a
|
|
3
|
+
* Eurostat DataCanvas.
|
|
4
|
+
* @module mcp-server/tools/definitions/eurostat-dataframe-query.tool
|
|
5
|
+
*/
|
|
6
|
+
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
7
|
+
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
8
|
+
import { acquireCanvas, getCanvas } from '../../../services/canvas-accessor.js';
|
|
9
|
+
export const eurostatDataframeQuery = tool('eurostat_dataframe_query', {
|
|
10
|
+
title: 'Query Eurostat Dataframes',
|
|
11
|
+
description: 'Run a read-only SQL SELECT against tables staged on a Eurostat dataframe canvas — the way to reach observations past the 5,000-row inline cap of eurostat_query_dataset and past the inline preview of a eurostat_download_dataset bulk download, and to aggregate, group, or join across staged tables without re-fetching from Eurostat. Call eurostat_dataframe_describe first for the table and column names, which differ between the two stagers. Only a single SELECT statement runs: statement chaining, non-SELECT verbs, and functions that read files or external data are rejected. Columns are flat — every dimension is a code column named after the dimension, the measure is obs_value, and the status flag is obs_flag / obs_flag_label; a "_label" companion per dimension exists only on tables eurostat_query_dataset staged, and conf_status / conf_status_label only on tables eurostat_download_dataset staged. Join the two stagers\' tables on dimension codes and time. Do not compare obs_flag across them: a confidential cell reads "|C" in obs_flag on a query table but null in obs_flag with "C" in conf_status on a bulk table.',
|
|
12
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
13
|
+
input: z.object({
|
|
14
|
+
canvas_id: z
|
|
15
|
+
.string()
|
|
16
|
+
.min(1)
|
|
17
|
+
.describe('Canvas identifier returned as canvasId by eurostat_query_dataset or eurostat_download_dataset. Identifies the workspace holding the staged tables.'),
|
|
18
|
+
sql: z
|
|
19
|
+
.string()
|
|
20
|
+
.min(1)
|
|
21
|
+
.describe("A single read-only SELECT statement. Reference tables by the names eurostat_dataframe_describe reports. Example: SELECT geo, geo_label, AVG(obs_value) AS mean FROM df_a1b2c3d4 WHERE time >= '2020' GROUP BY geo, geo_label ORDER BY mean DESC."),
|
|
22
|
+
}),
|
|
23
|
+
output: z.object({
|
|
24
|
+
canvasId: z.string().describe('Canvas identifier the query ran against.'),
|
|
25
|
+
columns: z.array(z.string()).describe('Column names in projection order.'),
|
|
26
|
+
rows: z
|
|
27
|
+
.array(z.record(z.string(), z.unknown()))
|
|
28
|
+
.describe('Result rows, each keyed by column name. Bounded by the canvas row limit. 64-bit integer results — COUNT(*) among them — arrive as strings so values outside the JSON number range survive intact; cast to DOUBLE in the SQL if a number is wanted.'),
|
|
29
|
+
rowCount: z
|
|
30
|
+
.number()
|
|
31
|
+
.describe('Rows materialized into this response. Equals the full result size unless truncated is true.'),
|
|
32
|
+
truncated: z
|
|
33
|
+
.boolean()
|
|
34
|
+
.describe('True when the result exceeded the canvas row limit and was cut short. Add a LIMIT, an aggregate, or a narrower WHERE clause to see the rest.'),
|
|
35
|
+
}),
|
|
36
|
+
errors: [
|
|
37
|
+
{
|
|
38
|
+
reason: 'canvas_disabled',
|
|
39
|
+
code: JsonRpcErrorCode.ServiceUnavailable,
|
|
40
|
+
when: 'This deployment runs without a dataframe canvas, so there is nothing to query.',
|
|
41
|
+
retryable: false,
|
|
42
|
+
recovery: 'Query the data inline with eurostat_query_dataset, narrowing dimension filters until the result fits.',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
reason: 'canvas_not_found',
|
|
46
|
+
code: JsonRpcErrorCode.NotFound,
|
|
47
|
+
when: 'The canvas_id is unknown or its lifetime has elapsed.',
|
|
48
|
+
recovery: 'Re-run eurostat_query_dataset to stage the data again and use the canvasId it returns.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
reason: 'missing_table',
|
|
52
|
+
code: JsonRpcErrorCode.NotFound,
|
|
53
|
+
when: 'The SQL names a table that is not staged on this canvas, or that has expired.',
|
|
54
|
+
recovery: 'Call eurostat_dataframe_describe for the staged table names, or re-run eurostat_query_dataset to stage them again.',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
async handler(input, ctx) {
|
|
58
|
+
const canvas = getCanvas();
|
|
59
|
+
if (!canvas) {
|
|
60
|
+
throw ctx.fail('canvas_disabled', undefined, ctx.recoveryFor('canvas_disabled'));
|
|
61
|
+
}
|
|
62
|
+
const instance = await acquireCanvas(canvas, input.canvas_id, ctx);
|
|
63
|
+
/**
|
|
64
|
+
* The SQL is not sanitized here on purpose. The canvas gate rejects
|
|
65
|
+
* anything that is not a single SELECT — statement count, statement type,
|
|
66
|
+
* an EXPLAIN-plan operator allowlist, and a table-function deny-list — and
|
|
67
|
+
* a rejection there is a typed ValidationError carrying the reason. A
|
|
68
|
+
* second, weaker string filter in front of it would only shadow those
|
|
69
|
+
* reasons with a vaguer message.
|
|
70
|
+
*/
|
|
71
|
+
const result = await instance.query(input.sql, { signal: ctx.signal });
|
|
72
|
+
ctx.log.info('Canvas query complete', {
|
|
73
|
+
canvasId: instance.canvasId,
|
|
74
|
+
rowCount: result.rowCount,
|
|
75
|
+
truncated: result.truncated === true,
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
canvasId: instance.canvasId,
|
|
79
|
+
columns: result.columns,
|
|
80
|
+
rows: result.rows,
|
|
81
|
+
rowCount: result.rowCount,
|
|
82
|
+
truncated: result.truncated === true,
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
format: (result) => {
|
|
86
|
+
const lines = [
|
|
87
|
+
`# Query result (canvas \`${result.canvasId}\`)`,
|
|
88
|
+
`**Rows:** ${result.rowCount} | **Truncated:** ${result.truncated}${result.truncated ? ' — add a LIMIT, an aggregate, or a narrower WHERE clause' : ''}`,
|
|
89
|
+
`**Columns:** ${result.columns.join(', ')}\n`,
|
|
90
|
+
];
|
|
91
|
+
// Every row renders, so content[] carries the same set structuredContent does.
|
|
92
|
+
for (const row of result.rows) {
|
|
93
|
+
lines.push(result.columns.map((col) => `${col}=${row[col] === null ? 'NULL' : row[col]}`).join(' | '));
|
|
94
|
+
}
|
|
95
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
//# sourceMappingURL=eurostat-dataframe-query.tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eurostat-dataframe-query.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/eurostat-dataframe-query.tool.ts"],"names":[],"mappings":"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"}
|