@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.
Files changed (53) hide show
  1. package/CLAUDE.md +1 -1
  2. package/Dockerfile +16 -3
  3. package/README.md +50 -7
  4. package/dist/config/server-config.d.ts +2 -0
  5. package/dist/config/server-config.d.ts.map +1 -1
  6. package/dist/config/server-config.js +14 -0
  7. package/dist/config/server-config.js.map +1 -1
  8. package/dist/index.js +28 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.d.ts +40 -0
  11. package/dist/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.d.ts.map +1 -0
  12. package/dist/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.js +113 -0
  13. package/dist/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.js.map +1 -0
  14. package/dist/mcp-server/tools/definitions/eurostat-dataframe-query.tool.d.ts +34 -0
  15. package/dist/mcp-server/tools/definitions/eurostat-dataframe-query.tool.d.ts.map +1 -0
  16. package/dist/mcp-server/tools/definitions/eurostat-dataframe-query.tool.js +98 -0
  17. package/dist/mcp-server/tools/definitions/eurostat-dataframe-query.tool.js.map +1 -0
  18. package/dist/mcp-server/tools/definitions/eurostat-download-dataset.tool.d.ts +93 -0
  19. package/dist/mcp-server/tools/definitions/eurostat-download-dataset.tool.d.ts.map +1 -0
  20. package/dist/mcp-server/tools/definitions/eurostat-download-dataset.tool.js +362 -0
  21. package/dist/mcp-server/tools/definitions/eurostat-download-dataset.tool.js.map +1 -0
  22. package/dist/mcp-server/tools/definitions/eurostat-get-dataset-info.tool.js +2 -2
  23. package/dist/mcp-server/tools/definitions/eurostat-get-dataset-info.tool.js.map +1 -1
  24. package/dist/mcp-server/tools/definitions/eurostat-get-dimension-values.tool.js +2 -2
  25. package/dist/mcp-server/tools/definitions/eurostat-get-dimension-values.tool.js.map +1 -1
  26. package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.d.ts +9 -0
  27. package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.d.ts.map +1 -1
  28. package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.js +68 -10
  29. package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.js.map +1 -1
  30. package/dist/mcp-server/tools/definitions/eurostat-search-datasets.tool.js +3 -3
  31. package/dist/mcp-server/tools/definitions/eurostat-search-datasets.tool.js.map +1 -1
  32. package/dist/services/canvas-accessor.d.ts +31 -0
  33. package/dist/services/canvas-accessor.d.ts.map +1 -0
  34. package/dist/services/canvas-accessor.js +64 -0
  35. package/dist/services/canvas-accessor.js.map +1 -0
  36. package/dist/services/eurostat-bulk/eurostat-bulk-service.d.ts +89 -0
  37. package/dist/services/eurostat-bulk/eurostat-bulk-service.d.ts.map +1 -0
  38. package/dist/services/eurostat-bulk/eurostat-bulk-service.js +501 -0
  39. package/dist/services/eurostat-bulk/eurostat-bulk-service.js.map +1 -0
  40. package/dist/services/eurostat-bulk/types.d.ts +98 -0
  41. package/dist/services/eurostat-bulk/types.d.ts.map +1 -0
  42. package/dist/services/eurostat-bulk/types.js +82 -0
  43. package/dist/services/eurostat-bulk/types.js.map +1 -0
  44. package/dist/services/eurostat-data/eurostat-data-service.d.ts +63 -8
  45. package/dist/services/eurostat-data/eurostat-data-service.d.ts.map +1 -1
  46. package/dist/services/eurostat-data/eurostat-data-service.js +107 -13
  47. package/dist/services/eurostat-data/eurostat-data-service.js.map +1 -1
  48. package/dist/services/eurostat-data/types.d.ts +30 -0
  49. package/dist/services/eurostat-data/types.d.ts.map +1 -1
  50. package/dist/services/eurostat-data/types.js +4 -0
  51. package/dist/services/eurostat-data/types.js.map +1 -1
  52. package/package.json +2 -1
  53. 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.3.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 directory, assigning ownership to the 'bun' user.
79
- RUN mkdir -p /var/log/eurostat-mcp-server && chown -R bun:bun /var/log/eurostat-mcp-server
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>5 Tools • 1 Resource</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
- [![Version](https://img.shields.io/badge/Version-0.3.0-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/eurostat-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.30.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/eurostat-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/eurostat-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.14-blueviolet.svg?style=flat-square)](https://bun.sh/)
12
+ [![Version](https://img.shields.io/badge/Version-0.5.0-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/eurostat-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.30.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/eurostat-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/eurostat-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.14-blueviolet.svg?style=flat-square)](https://bun.sh/)
13
13
 
14
14
  </div>
15
15
 
@@ -31,7 +31,7 @@
31
31
 
32
32
  ## Tools
33
33
 
34
- 5 tools for discovering and querying Eurostat statistical datasets:
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
- - Returned 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 rows were dropped. Filter the query to shrink what Eurostat sends — the cap bounds the decode, not the transfer
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;;;;iBAoBtB,CAAC;AAIH,wBAAgB,eAAe,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAOpE"}
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;KAC3C,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC"}
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 avoid async response errors on large datasets.`,
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;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,iEAAiE,CAAC;AAC1G,OAAO,EAAE,oBAAoB,EAAE,MAAM,+DAA+D,CAAC;AACrG,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,4BAA4B,EAAE,MAAM,6DAA6D,CAAC;AAC3G,OAAO,EAAE,uBAAuB,EAAE,MAAM,mDAAmD,CAAC;AAE5F,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,qBAAqB;IAC3B,KAAK,EAAE,qBAAqB;IAC5B,KAAK,EAAE;QACL,sBAAsB;QACtB,oBAAoB;QACpB,sBAAsB;QACtB,0BAA0B;QAC1B,oBAAoB;KACrB;IACD,SAAS,EAAE,CAAC,uBAAuB,CAAC;IACpC,OAAO,EAAE,EAAE;IACX,YAAY,EAAE;;0EAE0D;IACxE,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;IACrD,CAAC;CACF,CAAC,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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"}