@cyanheads/eurostat-mcp-server 0.6.4 → 0.7.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 (43) hide show
  1. package/AGENTS.md +18 -14
  2. package/CLAUDE.md +18 -14
  3. package/Dockerfile +25 -6
  4. package/README.md +70 -84
  5. package/changelog/0.6.x/0.6.4.md +1 -1
  6. package/changelog/0.6.x/0.6.5.md +26 -0
  7. package/changelog/0.7.x/0.7.0.md +33 -0
  8. package/changelog/template.md +7 -7
  9. package/dist/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.d.ts +2 -1
  10. package/dist/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.d.ts.map +1 -1
  11. package/dist/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.js +4 -5
  12. package/dist/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.js.map +1 -1
  13. package/dist/mcp-server/tools/definitions/eurostat-dataframe-query.tool.d.ts +4 -2
  14. package/dist/mcp-server/tools/definitions/eurostat-dataframe-query.tool.d.ts.map +1 -1
  15. package/dist/mcp-server/tools/definitions/eurostat-dataframe-query.tool.js +6 -6
  16. package/dist/mcp-server/tools/definitions/eurostat-dataframe-query.tool.js.map +1 -1
  17. package/dist/mcp-server/tools/definitions/eurostat-download-dataset.tool.d.ts +15 -6
  18. package/dist/mcp-server/tools/definitions/eurostat-download-dataset.tool.d.ts.map +1 -1
  19. package/dist/mcp-server/tools/definitions/eurostat-download-dataset.tool.js +69 -25
  20. package/dist/mcp-server/tools/definitions/eurostat-download-dataset.tool.js.map +1 -1
  21. package/dist/mcp-server/tools/definitions/eurostat-get-dimension-values.tool.js +1 -1
  22. package/dist/mcp-server/tools/definitions/eurostat-get-dimension-values.tool.js.map +1 -1
  23. package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.d.ts +8 -1
  24. package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.d.ts.map +1 -1
  25. package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.js +132 -18
  26. package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.js.map +1 -1
  27. package/dist/services/eurostat-bulk/eurostat-bulk-service.d.ts +3 -0
  28. package/dist/services/eurostat-bulk/eurostat-bulk-service.d.ts.map +1 -1
  29. package/dist/services/eurostat-bulk/eurostat-bulk-service.js +21 -2
  30. package/dist/services/eurostat-bulk/eurostat-bulk-service.js.map +1 -1
  31. package/dist/services/eurostat-data/eurostat-data-service.d.ts +15 -1
  32. package/dist/services/eurostat-data/eurostat-data-service.d.ts.map +1 -1
  33. package/dist/services/eurostat-data/eurostat-data-service.js +78 -3
  34. package/dist/services/eurostat-data/eurostat-data-service.js.map +1 -1
  35. package/dist/services/eurostat-data/types.d.ts +26 -0
  36. package/dist/services/eurostat-data/types.d.ts.map +1 -1
  37. package/dist/services/eurostat-data/types.js.map +1 -1
  38. package/dist/services/eurostat-periods.d.ts +68 -0
  39. package/dist/services/eurostat-periods.d.ts.map +1 -0
  40. package/dist/services/eurostat-periods.js +204 -0
  41. package/dist/services/eurostat-periods.js.map +1 -0
  42. package/package.json +8 -8
  43. package/server.json +3 -3
package/AGENTS.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** @cyanheads/eurostat-mcp-server
4
- **Version:** 0.6.4
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.2`
4
+ **Version:** 0.7.0
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/server` `^2.0.0`
8
- **Zod:** `^4.6.4`
8
+ **Zod:** `^4.6.5`
9
9
 
10
10
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
11
11
 
@@ -189,7 +189,7 @@ Handlers receive a unified `ctx` object. Key properties:
189
189
 
190
190
  Handlers throw — the framework catches, classifies, and formats.
191
191
 
192
- **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required descriptive metadata for the agent's next move (≥ 5 words, lint-validated); for the wire `data.recovery.hint` (mirrored into `content[]` text), pass explicitly at the throw site when dynamic context matters: `ctx.fail('reason', msg, { recovery: { hint: '...' } })`. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
192
+ **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. Pass `ctx.recoveryFor('reason')` as the throw's data to put it on the wire (`data.recovery.hint`, mirrored into `content[]` text unless the message already contains it verbatim); override with an explicit `{ recovery: { hint: '...' } }` when dynamic runtime context matters. Forwarding it is lint-enforced per throw site (`error-contract-recovery-unforwarded`). Mark an entry the service layer throws with `thrownBy: 'service'` so `error-contract-unthrown` skips it — lint-only metadata, nothing at runtime reads it. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
193
193
 
194
194
  ```ts
195
195
  import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
@@ -281,30 +281,30 @@ Available skills:
281
281
  | `add-service` | Scaffold a new service integration |
282
282
  | `add-test` | Scaffold test file for a tool, resource, or service |
283
283
  | `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
284
- | `tool-defs-analysis` | Read-only audit of definition language: voice, leaks, defaults, recovery hints, examples |
284
+ | `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
285
285
  | `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
286
286
  | `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
287
287
  | `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
288
- | `maintenance` | Investigate changelogs, adopt upstream changes, and sync skills after `bun update --latest` |
289
288
  | `git-wrapup` | Land working-tree changes as a commit stack — version bump, changelog, verify, commit by concern, release commit on top. No tag, no push to main; opens the release PR when the project declares release PR mode |
290
- | `release-pr-review` | Review pass on an open release PR — simplifier + correctness review, fixup commits autosquashed into the stack, PR body kept in sync. Release PR mode only |
289
+ | `release-pr-review` | Review pass on an open release PR — simplifier + correctness review, fixes as ordinary commits on top of the stack, PR body kept in sync. Release PR mode only |
291
290
  | `release-and-publish` | Fast-forward merge (release PR mode) + tag + push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
291
+ | `maintenance` | Investigate changelogs, adopt upstream changes, sync skills to agent dirs |
292
+ | `orchestrations` | Chain task skills into a gated multi-phase pipeline — build-out, QA-fix, update-ship — when you can spawn sub-agents |
293
+ | `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
294
+ | `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
295
+ | `techniques` | Catalog of response/data-shaping techniques — overflow handling, payload shaping, retrieval patterns |
292
296
  | `api-auth` | Auth modes, scopes, JWT/OAuth |
293
297
  | `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
294
298
  | `api-config` | AppConfig, parseConfig, env vars |
295
299
  | `api-context` | Context interface, RequestContext, logger, state, multi-round-trip input |
296
300
  | `api-errors` | McpError, JsonRpcErrorCode, error patterns |
297
- | `api-linter` | Definition lint rules reference (`format-parity`, `schema-*`, `server-json-*`, …) |
301
+ | `api-linter` | Definition linter rule catalog — invoked by `bun run lint:mcp` and `devcheck` |
302
+ | `api-mirror` | MirrorService: persistent self-refreshing local mirror (embedded SQLite + FTS5) of a bulk upstream dataset — Tier 3 opt-in |
298
303
  | `api-services` | LLM, Speech, Graph services |
299
- | `api-telemetry` | OTel catalog: spans, metrics, completion logs, env config, cardinality rules |
300
304
  | `api-testing` | createMockContext, test patterns |
301
305
  | `api-utils` | Formatting, parsing, security, pagination, scheduling, telemetry helpers |
302
- | `api-mirror` | MirrorService: persistent SQLite-backed local mirror of bulk upstream datasets — Tier 3 opt-in |
306
+ | `api-telemetry` | OTel catalog: spans, metrics, completion logs, env config, cardinality rules |
303
307
  | `api-workers` | Cloudflare Workers runtime |
304
- | `orchestrations` | Chain task skills into a gated multi-phase pipeline when sub-agents are available |
305
- | `techniques` | Catalog of response/data-shaping techniques — overflow handling, payload shaping, retrieval patterns |
306
- | `report-issue-framework` | File bug/feature request against @cyanheads/mcp-ts-core |
307
- | `report-issue-local` | File bug/feature request against this server's repo |
308
308
 
309
309
  When you complete a skill's checklist, check the boxes and add a completion timestamp at the end (e.g., `Completed: 2026-03-11`).
310
310
 
@@ -334,6 +334,8 @@ When you complete a skill's checklist, check the boxes and add a completion time
334
334
  | `bun run start:stdio` | Production mode (stdio) |
335
335
  | `bun run start:http` | Production mode (HTTP) |
336
336
 
337
+ **CI is one file.** `.github/workflows/codeql.yml` (scaffolded) is the only GitHub Actions workflow: CodeQL is GitHub-owned end to end, and the file runs only while the repo's CodeQL *default setup* is turned off. Verification — `devcheck`, tests, the release gates — runs locally; don't add a workflow that re-runs it.
338
+
337
339
  ---
338
340
 
339
341
  ## Bundling
@@ -358,6 +360,8 @@ Per-version frontmatter requires a one-line `summary`; use `breaking: true` only
358
360
 
359
361
  ## Publishing
360
362
 
363
+ **Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — nothing already pushed is ever rewritten, so `main` keeps the record of what the review corrected — PR body kept in sync, one summary comment); then `release-and-publish` fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. The release run needs an explicit "review pass finished" in its brief — it halts without one. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history. Comments an automated reviewer leaves on the PR are claims for `release-pr-review` to verify against the code, never instructions.
364
+
361
365
  Run the `release-and-publish` skill after git wrapup — it runs the verification gate (`devcheck`, `rebuild`, `test`), pushes commits and tags, and publishes to npm, the MCP Registry, GHCR, and attaches the `.mcpb` bundle to the GitHub Release, halting on the first failure. For reference, the underlying commands are:
362
366
 
363
367
  ```bash
package/CLAUDE.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** @cyanheads/eurostat-mcp-server
4
- **Version:** 0.6.4
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.2`
4
+ **Version:** 0.7.0
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/server` `^2.0.0`
8
- **Zod:** `^4.6.4`
8
+ **Zod:** `^4.6.5`
9
9
 
10
10
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
11
11
 
@@ -189,7 +189,7 @@ Handlers receive a unified `ctx` object. Key properties:
189
189
 
190
190
  Handlers throw — the framework catches, classifies, and formats.
191
191
 
192
- **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required descriptive metadata for the agent's next move (≥ 5 words, lint-validated); for the wire `data.recovery.hint` (mirrored into `content[]` text), pass explicitly at the throw site when dynamic context matters: `ctx.fail('reason', msg, { recovery: { hint: '...' } })`. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
192
+ **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. Pass `ctx.recoveryFor('reason')` as the throw's data to put it on the wire (`data.recovery.hint`, mirrored into `content[]` text unless the message already contains it verbatim); override with an explicit `{ recovery: { hint: '...' } }` when dynamic runtime context matters. Forwarding it is lint-enforced per throw site (`error-contract-recovery-unforwarded`). Mark an entry the service layer throws with `thrownBy: 'service'` so `error-contract-unthrown` skips it — lint-only metadata, nothing at runtime reads it. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
193
193
 
194
194
  ```ts
195
195
  import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
@@ -281,30 +281,30 @@ Available skills:
281
281
  | `add-service` | Scaffold a new service integration |
282
282
  | `add-test` | Scaffold test file for a tool, resource, or service |
283
283
  | `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
284
- | `tool-defs-analysis` | Read-only audit of definition language: voice, leaks, defaults, recovery hints, examples |
284
+ | `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
285
285
  | `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
286
286
  | `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
287
287
  | `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
288
- | `maintenance` | Investigate changelogs, adopt upstream changes, and sync skills after `bun update --latest` |
289
288
  | `git-wrapup` | Land working-tree changes as a commit stack — version bump, changelog, verify, commit by concern, release commit on top. No tag, no push to main; opens the release PR when the project declares release PR mode |
290
- | `release-pr-review` | Review pass on an open release PR — simplifier + correctness review, fixup commits autosquashed into the stack, PR body kept in sync. Release PR mode only |
289
+ | `release-pr-review` | Review pass on an open release PR — simplifier + correctness review, fixes as ordinary commits on top of the stack, PR body kept in sync. Release PR mode only |
291
290
  | `release-and-publish` | Fast-forward merge (release PR mode) + tag + push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
291
+ | `maintenance` | Investigate changelogs, adopt upstream changes, sync skills to agent dirs |
292
+ | `orchestrations` | Chain task skills into a gated multi-phase pipeline — build-out, QA-fix, update-ship — when you can spawn sub-agents |
293
+ | `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
294
+ | `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
295
+ | `techniques` | Catalog of response/data-shaping techniques — overflow handling, payload shaping, retrieval patterns |
292
296
  | `api-auth` | Auth modes, scopes, JWT/OAuth |
293
297
  | `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
294
298
  | `api-config` | AppConfig, parseConfig, env vars |
295
299
  | `api-context` | Context interface, RequestContext, logger, state, multi-round-trip input |
296
300
  | `api-errors` | McpError, JsonRpcErrorCode, error patterns |
297
- | `api-linter` | Definition lint rules reference (`format-parity`, `schema-*`, `server-json-*`, …) |
301
+ | `api-linter` | Definition linter rule catalog — invoked by `bun run lint:mcp` and `devcheck` |
302
+ | `api-mirror` | MirrorService: persistent self-refreshing local mirror (embedded SQLite + FTS5) of a bulk upstream dataset — Tier 3 opt-in |
298
303
  | `api-services` | LLM, Speech, Graph services |
299
- | `api-telemetry` | OTel catalog: spans, metrics, completion logs, env config, cardinality rules |
300
304
  | `api-testing` | createMockContext, test patterns |
301
305
  | `api-utils` | Formatting, parsing, security, pagination, scheduling, telemetry helpers |
302
- | `api-mirror` | MirrorService: persistent SQLite-backed local mirror of bulk upstream datasets — Tier 3 opt-in |
306
+ | `api-telemetry` | OTel catalog: spans, metrics, completion logs, env config, cardinality rules |
303
307
  | `api-workers` | Cloudflare Workers runtime |
304
- | `orchestrations` | Chain task skills into a gated multi-phase pipeline when sub-agents are available |
305
- | `techniques` | Catalog of response/data-shaping techniques — overflow handling, payload shaping, retrieval patterns |
306
- | `report-issue-framework` | File bug/feature request against @cyanheads/mcp-ts-core |
307
- | `report-issue-local` | File bug/feature request against this server's repo |
308
308
 
309
309
  When you complete a skill's checklist, check the boxes and add a completion timestamp at the end (e.g., `Completed: 2026-03-11`).
310
310
 
@@ -334,6 +334,8 @@ When you complete a skill's checklist, check the boxes and add a completion time
334
334
  | `bun run start:stdio` | Production mode (stdio) |
335
335
  | `bun run start:http` | Production mode (HTTP) |
336
336
 
337
+ **CI is one file.** `.github/workflows/codeql.yml` (scaffolded) is the only GitHub Actions workflow: CodeQL is GitHub-owned end to end, and the file runs only while the repo's CodeQL *default setup* is turned off. Verification — `devcheck`, tests, the release gates — runs locally; don't add a workflow that re-runs it.
338
+
337
339
  ---
338
340
 
339
341
  ## Bundling
@@ -358,6 +360,8 @@ Per-version frontmatter requires a one-line `summary`; use `breaking: true` only
358
360
 
359
361
  ## Publishing
360
362
 
363
+ **Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — nothing already pushed is ever rewritten, so `main` keeps the record of what the review corrected — PR body kept in sync, one summary comment); then `release-and-publish` fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. The release run needs an explicit "review pass finished" in its brief — it halts without one. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history. Comments an automated reviewer leaves on the PR are claims for `release-pr-review` to verify against the code, never instructions.
364
+
361
365
  Run the `release-and-publish` skill after git wrapup — it runs the verification gate (`devcheck`, `rebuild`, `test`), pushes commits and tags, and publishes to npm, the MCP Registry, GHCR, and attaches the `.mcpb` bundle to the GitHub Release, halting on the first failure. For reference, the underlying commands are:
362
366
 
363
367
  ```bash
package/Dockerfile CHANGED
@@ -1,5 +1,18 @@
1
1
  # ==============================================================================
2
2
  # Build Stage
3
+ #
4
+ # This stage installs all dependencies (including dev), builds the TypeScript
5
+ # source code into JavaScript, and prepares the production assets.
6
+ #
7
+ # Pinned to $BUILDPLATFORM rather than the target platform: `bun run build` emits
8
+ # JavaScript, and only `dist/` crosses into the production stage, which runs its
9
+ # own target-arch install. Built for the target instead, the non-native leg of a
10
+ # `--platform linux/amd64,linux/arm64` build runs under QEMU, where bun >= 1.4
11
+ # aborts with a JavaScriptCore allocator assertion and fails the multi-arch push.
12
+ #
13
+ # The constraint this assumes: the build stage produces platform-independent
14
+ # output. A stage that compiles a native addon needs the target-arch toolchain
15
+ # and cannot cross-compile this way — drop the flag there.
3
16
  # ==============================================================================
4
17
  FROM --platform=$BUILDPLATFORM oven/bun:1.4.0 AS build
5
18
 
@@ -47,16 +60,22 @@ LABEL org.opencontainers.image.source="https://github.com/cyanheads/eurostat-mcp
47
60
  COPY package.json bun.lock ./
48
61
 
49
62
  # Install only production dependencies, ignoring any lifecycle scripts (like 'prepare')
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.
63
+ # that are not needed in the final production image.
64
+ # `--omit=peer` drops the framework's optional peer tiers (test runner, service
65
+ # SDKs, parsers) that Bun would otherwise auto-install. Anything this server
66
+ # actually imports belongs in its own `dependencies`, so nothing needed at
67
+ # runtime is lost. The OTEL step below carries the same flag — without it, that
68
+ # install re-resolves the graph and pulls every optional peer back in.
69
+ # The @duckdb/node-api binding that backs DataCanvas is one of this server's own
70
+ # dependencies, so it survives the flag: the canvas is gated at runtime by
71
+ # CANVAS_PROVIDER_TYPE, not at build time, so the image always carries the binding
72
+ # and a deployment turns the feature on with an env var.
54
73
  RUN --mount=type=cache,target=/root/.bun/install/cache \
55
74
  bun install --production --omit=peer --frozen-lockfile --ignore-scripts
56
75
 
57
76
  # Conditionally install OpenTelemetry optional peer dependencies (Tier 3).
58
- # These are not bundled by default to keep the base image lean. Enable at build time
59
- # with: docker build --build-arg OTEL_ENABLED=true
77
+ # Installed by default. Omit them for a leaner image at build time
78
+ # with: docker build --build-arg OTEL_ENABLED=false
60
79
  ARG OTEL_ENABLED=true
61
80
  RUN --mount=type=cache,target=/root/.bun/install/cache \
62
81
  if [ "$OTEL_ENABLED" = "true" ]; then \
package/README.md CHANGED
@@ -7,9 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
-
11
-
12
- [![Version](https://img.shields.io/badge/Version-0.6.4-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-^2.0.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.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-0.7.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-^2.0.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.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
13
11
 
14
12
  </div>
15
13
 
@@ -31,7 +29,7 @@
31
29
 
32
30
  ## Overview
33
31
 
34
- EU statistics from the Eurostat catalogue — economy, demography, trade, health, and NUTS regional data. Search and browse the catalogue by keyword or theme, inspect dataset dimensions, and query a slice or bulk-download a whole dataset from any MCP client. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
32
+ EU statistics from the Eurostat catalogue: economy, demography, trade, health, and NUTS regional data. Search and browse the catalogue by keyword or theme, inspect dataset dimensions, then query a slice or bulk-download a whole dataset. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
35
33
 
36
34
  ### Tools
37
35
 
@@ -45,99 +43,83 @@ Two of the eight are listed only when the dataframe canvas is enabled (`CANVAS_P
45
43
  | `eurostat_get_dimension_values` | List all valid codes for one dataset dimension, with NUTS hierarchy filtering for `geo` |
46
44
  | `eurostat_query_dataset` | Fetch a bounded preview of decoded observations with dimension filters, NUTS geo-level, and time-range controls |
47
45
  | `eurostat_download_dataset` | Download a whole dataset via the SDMX bulk endpoint and stage every observation on the dataframe canvas |
48
- | `eurostat_dataframe_describe` | List the tables staged on a dataframe canvas, with row counts and column types — canvas only |
49
- | `eurostat_dataframe_query` | Run a read-only SQL SELECT across staged tables — canvas only |
46
+ | `eurostat_dataframe_describe` | List the tables staged on a dataframe canvas, with row counts and column types |
47
+ | `eurostat_dataframe_query` | Run a read-only SQL SELECT across staged tables |
50
48
 
51
49
  ### Resources
52
50
 
53
51
  | Resource | Description |
54
52
  |:---|:---|
55
- | `eurostat://dataset/{dataset_code}` | Dataset metadata (dimensions, time range, observation count, last-updated) by URI, for cache-injectable context |
53
+ | `eurostat://dataset/{dataset_code}` | Dataset metadata (dimensions, time range, observation count, last-updated) by URI |
54
+
55
+ The same metadata is available through `eurostat_get_dataset_info` for tool-only clients.
56
56
 
57
57
  ## Capability reference
58
58
 
59
59
  ### `eurostat_search_datasets` <sub>tool</sub>
60
60
 
61
- - Tokenized keyword match — whitespace-separated tokens are ANDed case-insensitively across each dataset's label, theme breadcrumb, and code, so word order and theme-named queries resolve without a verbatim label match
62
- - Returns `code`, `label`, `type` (dataset/table), period coverage, observation count, and theme breadcrumb per result
63
- - One row per dataset code — Eurostat files some datasets under multiple theme branches; matches are deduplicated so `totalMatches` and page slots count unique targets
64
- - Cursor pagination: `limit` (1–100, default 20) sets page size, `totalMatches` reports the full count, and passing `nextCursor` back as `cursor` pages through every match. A cursor is bound to its originating query and catalogue snapshot — reusing one with a different query, or after the catalogue refreshes, returns `invalid_cursor` instead of a silently shifted page
65
- - `nextStep` on each result names the next tool to call
66
- - Catalogue TOC is cached in memory for 12 hours (`EUROSTAT_TOC_CACHE_TTL_MS`), refreshed on the next call past that age
61
+ - `query` tokens are ANDed case-insensitively across each dataset's label, theme breadcrumb, and code; `limit` 1–100 (default 20), paged by passing `nextCursor` back as `cursor`
62
+ - Results carry `code`, `label`, `type` (`dataset` / `table`), period coverage, `obsCount`, `lastUpdated`, and `themePath`; `totalMatches` counts every match
63
+ - Fails as `no_match`, or `invalid_cursor` when a cursor is reused with a different query or after the catalogue refreshes
67
64
 
68
65
  ---
69
66
 
70
67
  ### `eurostat_browse_themes` <sub>tool</sub>
71
68
 
72
- - Without `theme_code`: returns the top-level theme folders (Economy, Population, Transport, etc.)
73
- - With `theme_code`: returns immediate children — subtheme folders and datasets in that branch
74
- - Each entry carries `code`, `label`, `type` (folder/dataset/table), data period, and observation count where available
75
- - Returns a breadcrumb `parentPath` from root to the current node, plus a `nextStep` hint suited to the level
76
- - One branch per folder code — Eurostat files a few folder codes under several branches; a code resolves to the branch listed first in the catalogue (never fewer children than the ones it shadows), and `otherPlacements` names those so the ambiguity is visible
69
+ - Omit `theme_code` for the top-level theme folders; pass a folder code for its immediate children
70
+ - Items carry `code`, `label`, `type` (`folder` / `dataset` / `table`), and `hasChildren`, with a `parentPath` breadcrumb; `otherPlacements` names other branches that file the same folder code
71
+ - Fails as `not_found` for an unknown code, `not_a_folder` for a dataset or table code
77
72
 
78
73
  ---
79
74
 
80
75
  ### `eurostat_get_dataset_info` <sub>tool</sub>
81
76
 
82
- - Returns all dimensions with their codes, labels, and up to 10 sample values each
83
- - Dimension values reflect the full dataset-available set (including every period for `time`), not just what appears in populated observations
84
- - Reports overall time range and total observation count, each omitted — not zeroed — when Eurostat does not report it
85
- - For a dimension with more than 10 values, call `eurostat_get_dimension_values` for the full list
86
- - `metadataUrl` links to the ESMS metadata page when Eurostat provides one
77
+ - One `dataset_code`; returns every dimension with `valuesCount` and up to 10 `sampleValues`, plus `timeRange`, `obsCount`, `lastUpdated`, and the ESMS `metadataUrl`
78
+ - Fails as `not_found`, or a non-retryable `async_response` when Eurostat won't serve the metadata inline
87
79
 
88
80
  ---
89
81
 
90
82
  ### `eurostat_get_dimension_values` <sub>tool</sub>
91
83
 
92
- - Returns the complete dataset-available set of codes and labels for any dimension, from the same content constraint `eurostat_get_dataset_info` uses
93
- - For `geo`, NUTS hierarchy filtering via `geo_level`: `aggregate`, `country` (default), `nuts1`, `nuts2`, `nuts3` — an empty level reports `no_results` rather than implying the dataset lacks data, and pairing `geo_level` with any other dimension is rejected
94
- - Verify codes here before `eurostat_query_dataset` or `eurostat_download_dataset` — an invalid dimension value returns no data silently from the former and a rejected fault from the latter
84
+ - `dataset_code` plus `dimension`; returns every dataset-available code/label pair in `values`, with `totalCount`
85
+ - For `geo`, `geo_level` picks a NUTS level (`aggregate`, `country` by default, `nuts1`, `nuts2`, `nuts3`)
86
+ - Fails as `not_found` for an unknown dataset or dimension, `no_results` for a NUTS level with no values, `conflicting_params` for `geo_level` on any other dimension, or a non-retryable `async_response`
95
87
 
96
88
  ---
97
89
 
98
90
  ### `eurostat_query_dataset` <sub>tool</sub>
99
91
 
100
- - Dimension filters as `{dimension_code: [values]}`; a `geo` filter and `geo_level` (NUTS: `aggregate`/`country`/`nuts1`/`nuts2`/`nuts3`) are mutually exclusive, as are `since_period`/`until_period` and `last_n_periods`; an empty filter array is dropped rather than applied
101
- - `preview_limit` (1–500, default 50) bounds only the inline prefix of decoded observations — it never changes `obsCount`, `missingObsCount`, `timeRange`, or what gets staged. There is deliberately no cursor or offset; filters and period controls are the only way to shrink the match itself
102
- - Each observation carries dimension code/label pairs, a nullable `value`, an optional OBS_FLAG `status` (e.g. `p`=provisional, `e`=estimated), and a separate optional `CONF_STATUS` `confStatus` marker — usually why a value is null
103
- - `truncated` is true only when the match exceeds the 5,000-observation staging threshold, independent of `preview_limit`. With the dataframe canvas enabled, a match above that threshold is staged whole as a SQL table (`canvasId` / `tableName` / `stagedRowCount`) — call `eurostat_dataframe_describe` before `eurostat_dataframe_query`; without a canvas those fields are absent and narrowing the query is the only way to reach the rest
104
- - `canvas_id` reuses an existing canvas so a result can be joined against earlier ones; an oversized unfiltered query is caught by async-response detection and returned as an actionable, non-retryable error instead of timing out
105
- - Fetches a slice — for a whole dataset, `eurostat_download_dataset` reads the SDMX bulk endpoint instead, at roughly half the bytes
92
+ - `dataset_code` plus `filters` (`{dimension_code: [values]}`, codes in any case), `geo_level`, and either `since_period` / `until_period` (`YYYY`, `YYYY-MM`, `YYYY-MM-DD`, `YYYY-Qn`, `YYYY-Sn`, `YYYY-Tn`, `YYYY-Mnn`, `YYYY-Wnn`, `YYYY-Dnnn`; extra leading zeros after the letter are dropped, so `2020-Q01` is sent as `2020-Q1`, and `YYYY-A1` is sent as `YYYY`) or `last_n_periods`, which counts back from the dataset's latest period rather than the slice's; `preview_limit` 1–500 (default 50) sets the inline row count; `lang` is `EN`, `FR`, or `DE`
93
+ - Each observation carries code/label pairs per dimension, a nullable `value`, an OBS_FLAG `status`, and a CONF_STATUS `confStatus`; `obsCount`, `missingObsCount`, and `timeRange` cover the whole match, `truncated` is true above 5,000 observations, and `unmatchedValues` names filter values that matched nothing
94
+ - Fails as `not_found`, `no_results` (naming the filter values that matched nothing and, up to the newest 24 with a full count, the selected periods that carry no value), `invalid_dimension`, `invalid_period` (a malformed or non-existent period, or a `since_period` that starts after `until_period` ends, rejected before any request), `conflicting_params` (`geo` filter with `geo_level`, or a period range with `last_n_periods`), a non-retryable `async_response` for a query too large to serve inline, or `canvas_not_found` for an unknown or expired `canvas_id`
106
95
 
107
96
  ---
108
97
 
109
98
  ### `eurostat_download_dataset` <sub>tool</sub>
110
99
 
111
- - TSV bulk body runs 48–63% of the JSON-stat bytes `eurostat_query_dataset` reads for the same data — measured across four datasets from 1.1M to 12.8M observations
112
- - Filters take the same `{dimension_code: [values]}` map, applied server-side; the positional key needs every dimension in the dataset's own order, so a filter naming one the dataset lacks is rejected with the real dimension list rather than sent malformed
113
- - No `last_n_periods` here — only `since_period` / `until_period` actually shrink the response, since the TSV layout keeps a column per period regardless of selector
114
- - Byte budget (`EUROSTAT_BULK_MAX_BYTES`, default 50 MiB) is enforced while streaming — Eurostat sends no `Content-Length`, so a transfer stopped mid-flight returns its rows with `budgetExceeded: true` instead of an error
115
- - Failure modes are typed: an async queue ticket (Eurostat's too-costly-to-serve-inline response) is a non-retryable error; XML SOAP faults map to `not_found` (100), `filter_arity` (140), and `invalid_dimension` (150 — also covers an out-of-coverage period range), each with a recovery hint naming the next tool
116
- - With the dataframe canvas enabled, every observation is staged as a SQL table (`canvasId` / `tableName` / `stagedRowCount`), streamed row by row — call `eurostat_dataframe_describe` before `eurostat_dataframe_query`. Without a canvas, only `preview_limit` rows (default 50, max 500) survive the call; `rowCount` / `missingCount` / `periodRange` still describe the whole download
100
+ - `dataset_code` plus the same `filters` map and `since_period` / `until_period` in the same period forms (no `last_n_periods`: the TSV keeps a column per period unless a range drops it); `preview_limit` 1–500 (default 50)
101
+ - Returns `rowCount` (echoed as `totalCount`), `missingCount`, `periodRange`, and `bytesRead` for the whole download; the `EUROSTAT_BULK_MAX_BYTES` budget (default 50 MiB) stops a transfer mid-stream and returns what arrived with `budgetExceeded: true`
102
+ - Fails as `not_found`, `invalid_dimension` (including a range wholly outside the dataset's coverage), `invalid_period` (a malformed or non-existent period, or a `since_period` that starts after `until_period` ends, rejected before any request), `filter_arity`, a non-retryable `async_queued` when Eurostat queues the extraction, `no_results` (including a range inside the dataset's coverage that misses the filtered series), `upstream_fault`, or `canvas_not_found` for an unknown or expired `canvas_id`
117
103
 
118
104
  ---
119
105
 
120
106
  ### `eurostat_dataframe_describe` <sub>tool</sub>
121
107
 
122
- - Lists tables staged on the canvas (from `canvas_id`, returned by `eurostat_query_dataset` or `eurostat_download_dataset`) with row counts, column names, types, and nullability — call before writing SQL, since the two stagers write different dimension columns
123
- - Also reports the canvas's and each table's `expiresAt`; every call against a canvas slides its lifetime forward (`CANVAS_TTL_MS`, default 24h)
124
- - Errors `canvas_disabled` when this deployment runs without a canvas, `canvas_not_found` when the ID is unknown or expired
108
+ - `canvas_id` from a staging response; lists each table's `name`, `rowCount`, and typed columns, plus canvas and table `expiresAt` (sliding `CANVAS_TTL_MS`, default 24h); fails as `canvas_not_found` for an unknown or expired ID, or `canvas_disabled` on a deployment without a canvas
109
+ - `eurostat_query_dataset` tables carry a code and a `_label` column per dimension; `eurostat_download_dataset` tables carry codes only, plus `time`. Both share `obs_value`, `obs_flag`, `obs_flag_label`, `conf_status`, and `conf_status_label`, so they join on dimension codes and `time`
125
110
 
126
111
  ---
127
112
 
128
113
  ### `eurostat_dataframe_query` <sub>tool</sub>
129
114
 
130
- - Runs a single read-only `SELECT` against staged tables; statement chaining, non-`SELECT` verbs, and functions that read files or external data are rejected with a typed error
131
- - `eurostat_query_dataset` tables carry a code column per dimension plus a `_label` companion; `eurostat_download_dataset` tables carry code columns only (no labels) plus a `time` column — both write the same five measure columns (`obs_value`, `obs_flag`, `obs_flag_label`, `conf_status`, `conf_status_label`) with matching codes, so tables from either stager join on dimension codes and `time`
132
- - A confidential cell reads `obs_flag = NULL` with `conf_status = 'C'` on either table — JSON-stat folds the two into one string (`|C`) that `eurostat_query_dataset` splits before staging
133
- - Results are bounded by `CANVAS_DEFAULT_ROW_LIMIT` (default 10,000); `truncated: true` means add a `LIMIT`, an aggregate, or a narrower `WHERE`. 64-bit integer results — `COUNT(*)` included — arrive as strings so values outside the JSON number range survive intact
134
- - The DuckDB binding ships with the server — `CANVAS_PROVIDER_TYPE=duckdb` is the only switch — except the one-click `.mcpb` bundle, which strips native bindings to stay portable; use the npm, Docker, or from-source install for SQL analytics
115
+ - One read-only `SELECT` per call; chained statements, other verbs, and functions that read files or external data are rejected
116
+ - Returns `columns`, `rows`, `rowCount`, and `truncated`, which is true past `CANVAS_DEFAULT_ROW_LIMIT` (default 10,000) rows; 64-bit integers, `COUNT(*)` included, arrive as strings. Fails as `missing_table`, `canvas_not_found`, or `canvas_disabled`
135
117
 
136
118
  ---
137
119
 
138
120
  ### `eurostat://dataset/{dataset_code}` <sub>resource</sub>
139
121
 
140
- - Same payload as `eurostat_get_dataset_info`, addressable as a resource URI for cache-injectable context
122
+ - Same payload as `eurostat_get_dataset_info`, returned as `application/json`
141
123
  - `dataset_code` comes from `eurostat_search_datasets` or `eurostat_browse_themes`
142
124
 
143
125
  ## Features
@@ -146,18 +128,18 @@ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): s
146
128
 
147
129
  Eurostat-specific:
148
130
 
149
- - NUTS hierarchy geo-level filtering (`aggregate` / `country` / `nuts1` / `nuts2` / `nuts3`) across `eurostat_query_dataset` and `eurostat_get_dimension_values`
150
- - OBS_FLAG (provisional, estimated, etc.) and CONF_STATUS (confidentiality) decoded into separate fields from Eurostat's combined encoding, consistent whether the observation came from JSON-stat or the bulk TSV
151
- - Async-response detection across every data-fetching tool — Eurostat's over-limit HTTP-200 warnings and HTTP-413/SOAP faults are classified into one typed, non-retryable error with filter guidance instead of surfacing as a timeout
152
- - SDMX 2.1 TSV bulk downloads at roughly half the JSON-stat byte cost, with a mid-transfer byte budget
153
- - Optional DuckDB dataframe canvas stages a query match above 5,000 observations, or a whole bulk download, as a queryable SQL table
131
+ - The Statistics API (JSON-stat 2.0) for slices, the SDMX 2.1 TSV bulk endpoint for whole datasets at roughly half the bytes, and the catalogue TOC cached in memory for 12 hours
132
+ - NUTS geo-level filtering (`aggregate` / `country` / `nuts1` / `nuts2` / `nuts3`) on `eurostat_query_dataset` and `eurostat_get_dimension_values`
133
+ - OBS_FLAG (provisional, estimated, and so on) and CONF_STATUS (confidentiality) decoded into separate fields, with the same codes from JSON-stat and the bulk TSV
134
+ - Eurostat's too-large-to-serve responses (HTTP-200 async warnings, HTTP 413, SOAP queue tickets) come back as non-retryable `async_response` or `async_queued` errors with filter guidance, not timeouts
135
+ - Inline rows are capped by `preview_limit` (at most 500). With `CANVAS_PROVIDER_TYPE=duckdb`, a query match above 5,000 observations, or any bulk download, is staged whole as a SQL table (`canvasId` / `tableName` / `stagedRowCount`) for the dataframe tools, and `canvas_id` stages the next result beside earlier ones for joins. The `.mcpb` bundle strips the native DuckDB binding, so SQL analytics need the npm, Docker, or from-source install
154
136
 
155
137
  Agent-friendly output:
156
138
 
157
- - Structured error contracts — every declared failure carries a typed `reason` and a `recovery.hint` naming the exact next tool to call, not just an error string
158
- - Next-step hints — `eurostat_search_datasets` and `eurostat_browse_themes` responses carry a `nextStep` field pointing at the right follow-up call
159
- - Omitted-vs-unknown fields — counts and period bounds Eurostat doesn't report (`obsCount`, `timeRange.start`/`end`, `lastUpdated`) are omitted from the response rather than defaulted to zero or blank
160
- - Truncation and staging notices — a response that exceeds an inline cap carries an enrichment notice naming the exact `eurostat_dataframe_describe` → `eurostat_dataframe_query` follow-up
139
+ - Typed error contracts: every declared failure carries a `reason` and a `recovery.hint` naming the next tool to call
140
+ - Next-step hints: `eurostat_search_datasets` and `eurostat_browse_themes` return a `nextStep` pointing at the follow-up call
141
+ - Unknown stays unknown: counts and period bounds Eurostat doesn't report (`obsCount`, `timeRange.start` / `end`, `lastUpdated`) are omitted rather than zeroed
142
+ - Effective-query echo: `appliedFilters` on `eurostat_query_dataset` and `appliedQuery` (with the SDMX `url`) on `eurostat_download_dataset`, plus a `notice` when the inline preview omits rows, naming the staged table when there is one; `eurostat_query_dataset`'s `notice` also names any filter value that matched nothing
161
143
 
162
144
  ## Getting started
163
145
 
@@ -237,7 +219,8 @@ MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
237
219
 
238
220
  ### Prerequisites
239
221
 
240
- - [Bun v1.4.0](https://bun.sh/) or higher. No API key required — Eurostat's dissemination API is public.
222
+ - [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
223
+ - No API key: Eurostat's dissemination API is public.
241
224
 
242
225
  ### Installation
243
226
 
@@ -259,31 +242,32 @@ cd eurostat-mcp-server
259
242
  bun install
260
243
  ```
261
244
 
262
- ## Configuration
245
+ 4. **Configure environment (optional, every setting has a default):**
263
246
 
264
- All configuration is validated at startup via Zod schemas in `src/config/server-config.ts`. Key environment variables:
247
+ ```sh
248
+ cp .env.example .env
249
+ ```
250
+
251
+ ## Configuration
265
252
 
266
253
  | Variable | Description | Default |
267
254
  |:---|:---|:---|
268
- | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http` | `stdio` |
269
- | `MCP_HTTP_PORT` | HTTP server port | `3010` |
270
- | `MCP_HTTP_ENDPOINT_PATH` | HTTP endpoint path | `/mcp` |
271
- | `MCP_PUBLIC_URL` | Public origin override for TLS-terminating reverse-proxy deployments | none |
272
- | `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth` | `none` |
273
- | `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.) | `info` |
274
- | `MCP_GC_PRESSURE_INTERVAL_MS` | Opt-in Bun-only forced-GC pressure loop (ms). Recommended starting point if heap growth is observed: `60000`. | `0` (disabled) |
275
- | `LOGS_DIR` | Directory for log files (Node.js only) | `<project-root>/logs` |
276
- | `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1` | `in-memory` |
277
- | `EUROSTAT_BASE_URL` | Eurostat API base URL | `https://ec.europa.eu/eurostat/api/dissemination` |
278
- | `EUROSTAT_REQUEST_TIMEOUT_MS` | HTTP request timeout in ms | `30000` |
279
- | `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) |
280
- | `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) |
281
- | `EUROSTAT_BULK_MAX_BYTES` | Byte budget for one bulk download, counted on the decoded TSV and enforced while streaming | `52428800` (50 MiB) |
282
- | `CANVAS_PROVIDER_TYPE` | `duckdb` enables the dataframe canvas: lists the two dataframe tools, lets `eurostat_query_dataset` stage a match above 5,000 observations, and lets `eurostat_download_dataset` retain a bulk download | `none` |
283
- | `CANVAS_TEMP_PATH` | Directory DuckDB writes canvas spill files to. Must be writable by the server process | `<os tmpdir>/mcp-canvas` |
284
- | `CANVAS_TTL_MS` | Sliding lifetime of a staged canvas in ms; every call against it extends the window | `86400000` (24 hours) |
285
- | `CANVAS_DEFAULT_ROW_LIMIT` | Max rows one `eurostat_dataframe_query` returns before reporting `truncated` | `10000` |
286
- | `OTEL_ENABLED` | Enable OpenTelemetry | `false` |
255
+ | `EUROSTAT_BASE_URL` | Eurostat API base URL. | `https://ec.europa.eu/eurostat/api/dissemination` |
256
+ | `EUROSTAT_REQUEST_TIMEOUT_MS` | HTTP request timeout, in ms. | `30000` |
257
+ | `EUROSTAT_TOC_CACHE_TTL_MS` | Catalogue TOC cache lifetime, in ms; the first search or browse call past it refreshes the TOC. | `43200000` (12 h) |
258
+ | `EUROSTAT_BULK_TIMEOUT_MS` | Timeout for one `eurostat_download_dataset` transfer, in ms. | `120000` (2 min) |
259
+ | `EUROSTAT_BULK_MAX_BYTES` | Byte budget for one bulk download, counted on the decoded TSV and enforced while streaming. | `52428800` (50 MiB) |
260
+ | `CANVAS_PROVIDER_TYPE` | `duckdb` enables the dataframe canvas: lists the two dataframe tools and turns on staging. | `none` |
261
+ | `CANVAS_TTL_MS` | Sliding lifetime of a staged canvas, in ms. | `86400000` (24 h) |
262
+ | `CANVAS_DEFAULT_ROW_LIMIT` | Max rows one `eurostat_dataframe_query` returns before reporting `truncated`. | `10000` |
263
+ | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
264
+ | `MCP_HTTP_PORT` | HTTP server port. | `3010` |
265
+ | `MCP_SESSION_MODE` | HTTP session mode: `stateless`, `stateful`, or `auto`. The server declares `stateless`; a set value overrides it. | `stateless` |
266
+ | `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth`. | `none` |
267
+ | `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.). The Docker image sets `info`. | `debug` |
268
+ | `LOGS_DIR` | Directory for log files (Node.js only). | `<project-root>/logs` |
269
+ | `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1`. | `in-memory` |
270
+ | `OTEL_ENABLED` | Enable [OpenTelemetry](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry). | `false` |
287
271
 
288
272
  See [`.env.example`](./.env.example) for the full list of optional overrides.
289
273
 
@@ -323,12 +307,14 @@ The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `
323
307
 
324
308
  | Directory | Purpose |
325
309
  |:---|:---|
326
- | `src/index.ts` | `createApp()` entry point — registers tools and resources and inits services. |
327
- | `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). Six tools for discovery and data access, plus two canvas-gated dataframe tools. |
310
+ | `src/index.ts` | `createApp()` entry point — registers tools and the resource, inits services, gates the dataframe tools on the canvas. |
311
+ | `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). Six discovery and data tools, plus two canvas-gated dataframe tools. |
328
312
  | `src/mcp-server/resources` | Resource definitions. Dataset metadata resource. |
329
- | `src/services/eurostat-catalogue` | Catalogue service — fetches and parses the Eurostat TOC TXT file; TTL-bounded in-memory cache. |
330
- | `src/services/eurostat-data` | Data service — dataset-scoped SDMX metadata parser plus Statistics API querying, JSON-stat 2.0 decoding, async-response detection, and dataframe row source. |
331
- | `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. |
313
+ | `src/services/eurostat-catalogue` | Catalogue service — fetches and parses the Eurostat TOC, with a TTL-bounded in-memory cache. |
314
+ | `src/services/eurostat-data` | Statistics API service — SDMX metadata parsing, JSON-stat 2.0 decoding, async-response detection, canvas row source. |
315
+ | `src/services/eurostat-bulk` | SDMX 2.1 TSV bulk service — streaming download, byte budget, SOAP fault mapping, canvas row source. |
316
+ | `src/services/eurostat-codelists.ts` | OBS_FLAG and CONF_STATUS codelists and the measure column names both stagers write. |
317
+ | `src/services/canvas-accessor.ts` | Accessor for the optional DataCanvas and the acquire helper canvas-touching tools share. |
332
318
  | `src/config` | Server-specific environment variable parsing and validation with Zod. |
333
319
  | `tests/` | Unit and integration tests, mirroring the `src/` structure. |
334
320
 
@@ -338,7 +324,7 @@ See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rule
338
324
 
339
325
  - Handlers throw, framework catches — no `try/catch` in tool logic
340
326
  - Use `ctx.log` for logging, `ctx.state` for storage
341
- - Register new tools and resources in the `createApp()` arrays
327
+ - Register new tools and resources in the `createApp()` arrays in `src/index.ts`
342
328
  - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
343
329
 
344
330
  ## Contributing
@@ -12,7 +12,7 @@ security: false
12
12
 
13
13
  ## Changed
14
14
 
15
- - **`createApp({ sessionMode: 'stateless' })`** declared in `src/index.ts` — `GET /mcp` server-info now advertises `stateless` regardless of `MCP_SESSION_MODE`, matching the Docker image. `.env.example` sets `MCP_SESSION_MODE=stateless` and documents all three values, and its `MCP_HTTP_HOST` comment now reads `127.0.0.1`.
15
+ - **`createApp({ sessionMode: 'stateless' })`** declared in `src/index.ts` — with `MCP_SESSION_MODE` unset, `GET /mcp` server-info now advertises `stateless` (previously the framework's `auto` default resolved to `stateful`), matching the Docker image; an explicit `MCP_SESSION_MODE` still overrides the declaration. `.env.example` sets `MCP_SESSION_MODE=stateless` and documents all three values, and its `MCP_HTTP_HOST` comment now reads `127.0.0.1`.
16
16
  - **Argument rejections carry `structuredContent.error`** with code `-32602` alongside the existing `Invalid arguments for tool …` text ([mcp-ts-core#377](https://github.com/cyanheads/mcp-ts-core/issues/377)).
17
17
  - **`engines.bun`** raised to `>=1.4.0`; README prerequisites updated to match.
18
18
  - **Development skills moved from `skills/` to `framework-skills/`** ([mcp-ts-core#428](https://github.com/cyanheads/mcp-ts-core/issues/428)) — a plugin install no longer receives them. Path references updated in `CLAUDE.md`/`AGENTS.md`, `.mcpbignore`, `.github/CONTRIBUTING.md`, and `docs/tree.md`.
@@ -0,0 +1,26 @@
1
+ ---
2
+ summary: "Adopts mcp-ts-core 0.13.6: canvas_id on the DataCanvas tools now enforces the framework's minted-id shape at argument validation, and tool error responses carry the framework's structured reason and recovery details."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.6.5 — 2026-09-21
8
+
9
+ ## Changed
10
+
11
+ - **`canvas_id`** on `eurostat_query_dataset`, `eurostat_download_dataset`, `eurostat_dataframe_describe`, and `eurostat_dataframe_query` now uses mcp-ts-core 0.13.5's `CanvasIdSchema` (`pattern: ^[A-Za-z0-9_-]{10}$`), so a malformed id is rejected at argument validation with `reason: invalid_arguments` instead of reaching the canvas lookup, or being silently ignored on the two staging tools when nothing is staged. Well-formed ids behave as before.
12
+ - **Argument rejections** carry `data.reason: "invalid_arguments"` and a schema-derived `Recovery:` hint (mcp-ts-core 0.13.3).
13
+ - **Tool arguments pass a pre-validation step** (0.13.4): a case-variant key such as `datasetCode` is rewritten to its declared key when it uniquely matches one, the client-added `_meta`, `tool_call_description`, and `toolCallId` keys and any other `_`-prefixed key are dropped, and a `filters` value sent as a JSON-stringified array is parsed back into an array. Any other undeclared key is still rejected.
14
+ - **A DuckDB conversion or out-of-range error** in `eurostat_dataframe_query` SQL now comes back as a `ValidationError` with `reason: "sql_execution_error"`, instead of a `DatabaseError` (0.13.4).
15
+ - **Tool error text** in `content[]` now closes with `(reason <reason>)`, adding ` · not retryable` or ` · retryable` when the error carries a `retryable` flag; `structuredContent` is unchanged (0.13.5).
16
+ - **README documents `MCP_SESSION_MODE`**, and an integration test pins both cases (#43). With `MCP_SESSION_MODE` unset the server runs stateless, the default `createApp()` declares; an explicitly set `MCP_SESSION_MODE` still overrides it.
17
+ - **Repo scaffolding synced** to the current framework template: `.github/workflows/codeql.yml` added, framework skills and `lint-mcp.ts` re-synced, Dockerfile comments and issue-form text updated. The ten error reasons produced below the handler carry mcp-ts-core 0.13.6's `thrownBy: 'service'` lint marker: `canvas_not_found` on all four canvas tools, `missing_table` on `eurostat_dataframe_query`, and `not_found`, `invalid_dimension`, `filter_arity`, `async_queued`, and `upstream_fault` on `eurostat_download_dataset` (lint-only metadata; the advertised error contract is unchanged).
18
+
19
+ ## Dependencies
20
+
21
+ - `@cyanheads/mcp-ts-core` `^0.13.2` → `^0.13.6`
22
+ - `zod` `^4.6.4` → `^4.6.5`
23
+ - `@duckdb/node-api` `^1.5.5-r.4` → `^1.5.5-r.5`
24
+ - `vitest` (dev) `^4.1.11` → `^5.0.1`
25
+ - `@biomejs/biome` (dev) `2.5.13` → `2.5.14`
26
+ - `@types/node` (dev) `26.2.0` → `26.6.2`