@cyanheads/eurostat-mcp-server 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** @cyanheads/eurostat-mcp-server
4
- **Version:** 0.6.3
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.12.3`
6
- **Engines:** Bun ≥1.3.0, Node ≥24.0.0
4
+ **Version:** 0.6.4
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.2`
6
+ **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/server` `^2.0.0`
8
- **Zod:** `^4.4.3`
8
+ **Zod:** `^4.6.4`
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
 
@@ -149,6 +149,22 @@ For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("
149
149
 
150
150
  `instructions` is optional server-level orientation, sent on every `initialize` as session-level context. Use it for deployment guidance and cross-tool workflows instead of repeating the same context across tool descriptions.
151
151
 
152
+ ### Session posture and shutdown
153
+
154
+ Two more `createApp()` options shape how the server runs rather than how it presents itself:
155
+
156
+ ```ts
157
+ await createApp({
158
+ sessionMode: 'stateless', // or { default: 'stateful', require: 'stateful' }
159
+ setup(core) { startMyWatcher(core.config); },
160
+ async teardown() { await stopMyWatcher(); },
161
+ });
162
+ ```
163
+
164
+ `sessionMode` declares the HTTP session posture in `src/` instead of leaving it to a deployment's `MCP_SESSION_MODE`, which still wins whenever it carries a meaningful value (an empty string and an unsubstituted `${…}` placeholder read as unset and fall through to the option). Add `require: 'stateful'` when a tool asks the caller for input mid-handler via `ctx.requestInput`: startup then fails with a `ConfigurationError` rather than serving a mode in which a 2025-era client can never answer the prompt. Stdio is never refused. This server declares `stateless` — no tool gates on `ctx.requestInput`.
165
+
166
+ `teardown(core)` is the `setup()` counterpart — release a watcher, socket, or non-`unref()`'d timer there. It runs after the transport stops and before the logger closes, on every shutdown path, and a signal-triggered shutdown then exits the process explicitly (0, or 1 if a step never settles within the framework's 10 s ceiling).
167
+
152
168
  ---
153
169
 
154
170
  ## Context
@@ -173,7 +189,7 @@ Handlers receive a unified `ctx` object. Key properties:
173
189
 
174
190
  Handlers throw — the framework catches, classifies, and formats.
175
191
 
176
- **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`) bubble freely and don't need declaring.
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.
177
193
 
178
194
  ```ts
179
195
  import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
@@ -248,9 +264,9 @@ src/
248
264
 
249
265
  ## Skills
250
266
 
251
- Skills are modular instructions in `skills/` at the project root. Read them directly when a task matches — e.g., `skills/add-tool/SKILL.md` when adding a tool.
267
+ Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. The directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand these development skills to every agent that installs it. Keep `skills/` free for skills meant for those agents.
252
268
 
253
- **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). This makes skills available as context without needing to reference `skills/` paths manually. After framework updates, run the `maintenance` skill — it re-syncs the agent directory automatically (Phase B).
269
+ **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
254
270
 
255
271
  Available skills:
256
272
 
@@ -270,8 +286,9 @@ Available skills:
270
286
  | `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
271
287
  | `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
272
288
  | `maintenance` | Investigate changelogs, adopt upstream changes, and sync skills after `bun update --latest` |
273
- | `git-wrapup` | Version bump, changelog, commit, and tag — stops before push/publish |
274
- | `release-and-publish` | Ship a release: verification gate, push commits+tags, publish to npm / MCP Registry / GHCR / `.mcpb` bundle on GitHub Releases |
289
+ | `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 |
291
+ | `release-and-publish` | Fast-forward merge (release PR mode) + tag + push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
275
292
  | `api-auth` | Auth modes, scopes, JWT/OAuth |
276
293
  | `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
277
294
  | `api-config` | AppConfig, parseConfig, env vars |
@@ -301,14 +318,15 @@ When you complete a skill's checklist, check the boxes and add a completion time
301
318
  | `bun run rebuild` | Clean + build |
302
319
  | `bun run clean` | Remove build artifacts |
303
320
  | `bun run devcheck` | Lint + format + typecheck + security + packaging alignment |
304
- | `bun run audit:refresh` | Delete `bun.lock`, reinstall, and re-run `bun audit`. Use when `devcheck` flags a transitive advisory — Bun's `update` is sticky on transitive resolutions, so the advisory may be a stale-lockfile false positive. If it survives the refresh, it's real. |
321
+ | `bun run audit:fix` | `bun audit fix` — upgrade vulnerable packages to the lowest safe version within existing ranges (`--dry-run` previews, `--latest` rewrites ranges). First response when `devcheck` flags a transitive advisory; then `bun update <name>`, then `bun dedupe` |
322
+ | `bun run audit:refresh` | Delete `bun.lock` and reinstall. Last resort after `audit:fix`, `bun update <name>`, and `bun dedupe` — re-resolves every ranged dep (the framework pin included) and rewrites the lockfile as `lockfileVersion: 2` |
305
323
  | `bun run tree` | Generate directory structure doc |
306
324
  | `bun run format` | Auto-fix formatting |
307
325
  | `bun run format:unsafe` | Apply behavior-changing Biome fixes for manual review |
308
326
  | `bun run test` | Run tests |
309
327
  | `bun run lint:mcp` | Validate MCP definitions against spec |
310
328
  | `bun run lint:packaging` | Validate env var alignment between `manifest.json` and `server.json` (skipped cleanly when `manifest.json` is absent) |
311
- | `bun run list-skills` | List skills in `skills/` with name + description |
329
+ | `bun run list-skills` | List skills in `framework-skills/` with name + description |
312
330
  | `bun run release:github` | Create GitHub Release from the latest annotated tag (attach `.mcpb`, enforce title format) |
313
331
  | `bun run bundle` | Build and pack as `dist/eurostat-mcp-server.mcpb` for one-click Claude Desktop install |
314
332
  | `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
@@ -322,9 +340,9 @@ When you complete a skill's checklist, check the boxes and add a completion time
322
340
 
323
341
  `bun run bundle` produces `dist/eurostat-mcp-server.mcpb` for one-click install in Claude Desktop. MCPB is stdio-only — HTTP and Docker deployments are unaffected.
324
342
 
325
- **Adding an env var requires both files:** `server.json` stdio `environmentVariables[]` (registry discovery) and `manifest.json` `mcp_config.env` (bundle install UX, plus `user_config` if user-prompted). `bun run lint:packaging` (run by `devcheck`) verifies the env var names align.
343
+ **Adding an env var requires both files:** `server.json` stdio `environmentVariables[]` (registry discovery) and `manifest.json` `mcp_config.env` (bundle install UX, plus `user_config` if user-prompted). `bun run lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`.
326
344
 
327
- **README install badges** (Claude Desktop `.mcpb`, Cursor, VS Code) and config-generation commands are ship-time concerns — see the `polish-docs-meta` skill (`skills/polish-docs-meta/references/readme.md`).
345
+ **README install badges** (Claude Desktop `.mcpb`, Cursor, VS Code) and config-generation commands are ship-time concerns — see the `polish-docs-meta` skill (`framework-skills/polish-docs-meta/references/readme.md`).
328
346
 
329
347
  ---
330
348
 
@@ -334,6 +352,8 @@ The source of truth is one file per release under `changelog/<major.minor>.x/<ve
334
352
 
335
353
  Per-version frontmatter requires a one-line `summary`; use `breaking: true` only for consumer-facing breaks and `security: true` only for a security fix in this server's own source. Optional `agent-notes` carry downstream adoption instructions and do not render in `CHANGELOG.md`.
336
354
 
355
+ **Section order:** the Keep a Changelog sequence — Added, Changed, Deprecated, Removed, Fixed, Security — then `Dependencies` last. Include only sections with entries — don't ship empty headers.
356
+
337
357
  ---
338
358
 
339
359
  ## Publishing
@@ -380,7 +400,7 @@ import { getMyService } from '@/services/my-domain/my-service.js';
380
400
  - [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
381
401
  - [ ] Registered in `createApp()` arrays (directly or via barrel exports)
382
402
  - [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
383
- - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = package name; `interface.shortDescription` from `package.json` description
384
- - [ ] `.codex-plugin/mcp.json` updated — server name key matches `package.json` name; env vars added for any required API keys
385
- - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; inline `mcpServers` entry with server name key, env vars for any required API keys
403
+ - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
404
+ - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
405
+ - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
386
406
  - [ ] `bun run devcheck` passes
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.3
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.12.3`
6
- **Engines:** Bun ≥1.3.0, Node ≥24.0.0
4
+ **Version:** 0.6.4
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.2`
6
+ **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/server` `^2.0.0`
8
- **Zod:** `^4.4.3`
8
+ **Zod:** `^4.6.4`
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
 
@@ -149,6 +149,22 @@ For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("
149
149
 
150
150
  `instructions` is optional server-level orientation, sent on every `initialize` as session-level context. Use it for deployment guidance and cross-tool workflows instead of repeating the same context across tool descriptions.
151
151
 
152
+ ### Session posture and shutdown
153
+
154
+ Two more `createApp()` options shape how the server runs rather than how it presents itself:
155
+
156
+ ```ts
157
+ await createApp({
158
+ sessionMode: 'stateless', // or { default: 'stateful', require: 'stateful' }
159
+ setup(core) { startMyWatcher(core.config); },
160
+ async teardown() { await stopMyWatcher(); },
161
+ });
162
+ ```
163
+
164
+ `sessionMode` declares the HTTP session posture in `src/` instead of leaving it to a deployment's `MCP_SESSION_MODE`, which still wins whenever it carries a meaningful value (an empty string and an unsubstituted `${…}` placeholder read as unset and fall through to the option). Add `require: 'stateful'` when a tool asks the caller for input mid-handler via `ctx.requestInput`: startup then fails with a `ConfigurationError` rather than serving a mode in which a 2025-era client can never answer the prompt. Stdio is never refused. This server declares `stateless` — no tool gates on `ctx.requestInput`.
165
+
166
+ `teardown(core)` is the `setup()` counterpart — release a watcher, socket, or non-`unref()`'d timer there. It runs after the transport stops and before the logger closes, on every shutdown path, and a signal-triggered shutdown then exits the process explicitly (0, or 1 if a step never settles within the framework's 10 s ceiling).
167
+
152
168
  ---
153
169
 
154
170
  ## Context
@@ -173,7 +189,7 @@ Handlers receive a unified `ctx` object. Key properties:
173
189
 
174
190
  Handlers throw — the framework catches, classifies, and formats.
175
191
 
176
- **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`) bubble freely and don't need declaring.
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.
177
193
 
178
194
  ```ts
179
195
  import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
@@ -248,9 +264,9 @@ src/
248
264
 
249
265
  ## Skills
250
266
 
251
- Skills are modular instructions in `skills/` at the project root. Read them directly when a task matches — e.g., `skills/add-tool/SKILL.md` when adding a tool.
267
+ Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. The directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand these development skills to every agent that installs it. Keep `skills/` free for skills meant for those agents.
252
268
 
253
- **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). This makes skills available as context without needing to reference `skills/` paths manually. After framework updates, run the `maintenance` skill — it re-syncs the agent directory automatically (Phase B).
269
+ **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
254
270
 
255
271
  Available skills:
256
272
 
@@ -270,8 +286,9 @@ Available skills:
270
286
  | `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
271
287
  | `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
272
288
  | `maintenance` | Investigate changelogs, adopt upstream changes, and sync skills after `bun update --latest` |
273
- | `git-wrapup` | Version bump, changelog, commit, and tag — stops before push/publish |
274
- | `release-and-publish` | Ship a release: verification gate, push commits+tags, publish to npm / MCP Registry / GHCR / `.mcpb` bundle on GitHub Releases |
289
+ | `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 |
291
+ | `release-and-publish` | Fast-forward merge (release PR mode) + tag + push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
275
292
  | `api-auth` | Auth modes, scopes, JWT/OAuth |
276
293
  | `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
277
294
  | `api-config` | AppConfig, parseConfig, env vars |
@@ -301,14 +318,15 @@ When you complete a skill's checklist, check the boxes and add a completion time
301
318
  | `bun run rebuild` | Clean + build |
302
319
  | `bun run clean` | Remove build artifacts |
303
320
  | `bun run devcheck` | Lint + format + typecheck + security + packaging alignment |
304
- | `bun run audit:refresh` | Delete `bun.lock`, reinstall, and re-run `bun audit`. Use when `devcheck` flags a transitive advisory — Bun's `update` is sticky on transitive resolutions, so the advisory may be a stale-lockfile false positive. If it survives the refresh, it's real. |
321
+ | `bun run audit:fix` | `bun audit fix` — upgrade vulnerable packages to the lowest safe version within existing ranges (`--dry-run` previews, `--latest` rewrites ranges). First response when `devcheck` flags a transitive advisory; then `bun update <name>`, then `bun dedupe` |
322
+ | `bun run audit:refresh` | Delete `bun.lock` and reinstall. Last resort after `audit:fix`, `bun update <name>`, and `bun dedupe` — re-resolves every ranged dep (the framework pin included) and rewrites the lockfile as `lockfileVersion: 2` |
305
323
  | `bun run tree` | Generate directory structure doc |
306
324
  | `bun run format` | Auto-fix formatting |
307
325
  | `bun run format:unsafe` | Apply behavior-changing Biome fixes for manual review |
308
326
  | `bun run test` | Run tests |
309
327
  | `bun run lint:mcp` | Validate MCP definitions against spec |
310
328
  | `bun run lint:packaging` | Validate env var alignment between `manifest.json` and `server.json` (skipped cleanly when `manifest.json` is absent) |
311
- | `bun run list-skills` | List skills in `skills/` with name + description |
329
+ | `bun run list-skills` | List skills in `framework-skills/` with name + description |
312
330
  | `bun run release:github` | Create GitHub Release from the latest annotated tag (attach `.mcpb`, enforce title format) |
313
331
  | `bun run bundle` | Build and pack as `dist/eurostat-mcp-server.mcpb` for one-click Claude Desktop install |
314
332
  | `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
@@ -322,9 +340,9 @@ When you complete a skill's checklist, check the boxes and add a completion time
322
340
 
323
341
  `bun run bundle` produces `dist/eurostat-mcp-server.mcpb` for one-click install in Claude Desktop. MCPB is stdio-only — HTTP and Docker deployments are unaffected.
324
342
 
325
- **Adding an env var requires both files:** `server.json` stdio `environmentVariables[]` (registry discovery) and `manifest.json` `mcp_config.env` (bundle install UX, plus `user_config` if user-prompted). `bun run lint:packaging` (run by `devcheck`) verifies the env var names align.
343
+ **Adding an env var requires both files:** `server.json` stdio `environmentVariables[]` (registry discovery) and `manifest.json` `mcp_config.env` (bundle install UX, plus `user_config` if user-prompted). `bun run lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`.
326
344
 
327
- **README install badges** (Claude Desktop `.mcpb`, Cursor, VS Code) and config-generation commands are ship-time concerns — see the `polish-docs-meta` skill (`skills/polish-docs-meta/references/readme.md`).
345
+ **README install badges** (Claude Desktop `.mcpb`, Cursor, VS Code) and config-generation commands are ship-time concerns — see the `polish-docs-meta` skill (`framework-skills/polish-docs-meta/references/readme.md`).
328
346
 
329
347
  ---
330
348
 
@@ -334,6 +352,8 @@ The source of truth is one file per release under `changelog/<major.minor>.x/<ve
334
352
 
335
353
  Per-version frontmatter requires a one-line `summary`; use `breaking: true` only for consumer-facing breaks and `security: true` only for a security fix in this server's own source. Optional `agent-notes` carry downstream adoption instructions and do not render in `CHANGELOG.md`.
336
354
 
355
+ **Section order:** the Keep a Changelog sequence — Added, Changed, Deprecated, Removed, Fixed, Security — then `Dependencies` last. Include only sections with entries — don't ship empty headers.
356
+
337
357
  ---
338
358
 
339
359
  ## Publishing
@@ -380,7 +400,7 @@ import { getMyService } from '@/services/my-domain/my-service.js';
380
400
  - [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
381
401
  - [ ] Registered in `createApp()` arrays (directly or via barrel exports)
382
402
  - [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
383
- - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = package name; `interface.shortDescription` from `package.json` description
384
- - [ ] `.codex-plugin/mcp.json` updated — server name key matches `package.json` name; env vars added for any required API keys
385
- - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; inline `mcpServers` entry with server name key, env vars for any required API keys
403
+ - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
404
+ - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
405
+ - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
386
406
  - [ ] `bun run devcheck` passes
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
 
11
11
 
12
- [![Version](https://img.shields.io/badge/Version-0.6.3-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/)
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/)
13
13
 
14
14
  </div>
15
15
 
@@ -29,147 +29,135 @@
29
29
 
30
30
  ---
31
31
 
32
- ## Tools
32
+ ## Overview
33
33
 
34
- 6 tools for discovering and querying Eurostat statistical datasets, plus 2 more when the optional dataframe canvas is enabled:
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.
35
+
36
+ ### Tools
37
+
38
+ Two of the eight are listed only when the dataframe canvas is enabled (`CANVAS_PROVIDER_TYPE=duckdb`).
35
39
 
36
40
  | Tool | Description |
37
41
  |:---|:---|
38
42
  | `eurostat_search_datasets` | Search the Eurostat catalogue by keyword — returns codes, descriptions, period coverage, and theme breadcrumbs |
39
43
  | `eurostat_browse_themes` | Navigate the Eurostat theme hierarchy — list root themes or drill into subthemes and datasets |
40
- | `eurostat_get_dataset_info` | Fetch metadata for a dataset: dimensions with sample values, time range, observation count, and last-update date |
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
- | `eurostat_query_dataset` | Fetch a deterministic preview of 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 |
44
+ | `eurostat_get_dataset_info` | Fetch dataset metadata: dimensions with sample values, time range, observation count, and last-update date |
45
+ | `eurostat_get_dimension_values` | List all valid codes for one dataset dimension, with NUTS hierarchy filtering for `geo` |
46
+ | `eurostat_query_dataset` | Fetch a bounded preview of decoded observations with dimension filters, NUTS geo-level, and time-range controls |
47
+ | `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 |
45
49
  | `eurostat_dataframe_query` | Run a read-only SQL SELECT across staged tables — canvas only |
46
50
 
47
- ### `eurostat_search_datasets`
51
+ ### Resources
52
+
53
+ | Resource | Description |
54
+ |:---|:---|
55
+ | `eurostat://dataset/{dataset_code}` | Dataset metadata (dimensions, time range, observation count, last-updated) by URI, for cache-injectable context |
48
56
 
49
- Search the Eurostat dataset catalogue by keyword.
57
+ ## Capability reference
50
58
 
51
- - 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
52
- - Returns code, label, type (dataset/table), period coverage, observation count, and theme breadcrumb
53
- - One row per dataset code — Eurostat files some datasets under several theme branches; matches are deduplicated so `totalMatches` and page slots count unique query targets
54
- - Cursor pagination: `limit` (1–100, default 20) sets the page size, `totalMatches` reports the full count, and passing the returned `nextCursor` back as `cursor` pages through every match over a stable order. Cursors are bound to their 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
55
- - `nextStep` hint on each result points at the next tool to call
56
- - Catalogue TOC cached in memory for 12 hours (`EUROSTAT_TOC_CACHE_TTL_MS`), then refreshed on the next call
57
- - Pair with `eurostat_browse_themes` for structured domain exploration when keywords are unclear
59
+ ### `eurostat_search_datasets` <sub>tool</sub>
58
60
 
59
- ---
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
60
67
 
61
- ### `eurostat_browse_themes`
68
+ ---
62
69
 
63
- Navigate the Eurostat theme tree.
70
+ ### `eurostat_browse_themes` <sub>tool</sub>
64
71
 
65
- - Without `theme_code`: returns the top-level themes (Economy and finance, Population, Transport, etc.)
72
+ - Without `theme_code`: returns the top-level theme folders (Economy, Population, Transport, etc.)
66
73
  - With `theme_code`: returns immediate children — subtheme folders and datasets in that branch
67
- - Each entry includes code, label, type (folder/dataset/table), data period, and observation count where available
68
- - Returns a breadcrumb path from root to the current node, plus a `nextStep` hint suited to the level (drill into folders or inspect a dataset)
69
- - One branch per folder code — Eurostat files a few folder codes under several branches; a code resolves to the first one the catalogue lists, which never has fewer children than the branches it shadows, and `otherPlacements` names those so the ambiguity is visible
70
- - Use for structured discovery when you know the domain but not the exact dataset code
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
71
77
 
72
78
  ---
73
79
 
74
- ### `eurostat_get_dataset_info`
75
-
76
- Fetch metadata for a Eurostat dataset before querying it.
80
+ ### `eurostat_get_dataset_info` <sub>tool</sub>
77
81
 
78
82
  - Returns all dimensions with their codes, labels, and up to 10 sample values each
79
- - Reports overall time range and total observation count across all periods, each omitted when Eurostat does not report it
80
- - Combines the dataset-scoped SDMX dataflow descendants with its content constraint, so every dimension — including `time` — is described from the full dataset-available value set rather than from a populated observation slice
81
- - For dimensions with more than 10 values, use `eurostat_get_dimension_values` for the full list
82
- - Provides a link to the ESMS metadata page when available
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
83
87
 
84
88
  ---
85
89
 
86
- ### `eurostat_get_dimension_values`
87
-
88
- List all valid values for a specific dataset dimension.
90
+ ### `eurostat_get_dimension_values` <sub>tool</sub>
89
91
 
90
- - Retrieves the complete dataset-available set of codes and labels for any dimension (`unit`, `na_item`, `geo`, `time`, etc.) from the same SDMX content constraint used by dataset metadata
91
- - For the `geo` dimension, supports NUTS hierarchy filtering: `aggregate` (EU/EA totals), `country` (the default when omitted), `nuts1`, `nuts2`, and `nuts3`. The response reports the effective level; an empty level is `no_results`, not a claim that the dataset is missing. Pairing `geo_level` with any other dimension is rejected rather than ignored
92
- - Prevents silent no-data returns — invalid dimension values in `eurostat_query_dataset` return nothing without error; verify codes here first
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
93
95
 
94
96
  ---
95
97
 
96
- ### `eurostat_query_dataset`
97
-
98
- Fetch statistical data from a Eurostat dataset.
98
+ ### `eurostat_query_dataset` <sub>tool</sub>
99
99
 
100
- - Accepts dimension filters as a map of `{dimension_code: [value1, value2, ...]}`
101
- - NUTS geo-level filter (`aggregate`, `country`, `nuts1`, `nuts2`, `nuts3`) — mutually exclusive with a non-empty `geo` entry in filters; an empty array is treated as no filter and dropped
102
- - Time range via `since_period`/`until_period` (e.g., `"2020"`, `"2023-Q1"`) or `last_n_periods` for the N most recent
103
- - `preview_limit` controls the deterministic inline prefix (default 50, max 500). It does not change the match, totals, period coverage, or staged rows; filters and period controls reduce the match itself. There is deliberately no cursor or offset
104
- - Returns decoded observations with dimension codes and labels, numeric values, an `OBS_FLAG` status (`p` = provisional, `e` = estimated, etc.) and a separate `CONF_STATUS` confidentiality marker (`C` = confidential, usually the reason a value is null)
105
- - 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
106
- - `obsCount`, `missingObsCount`, and `timeRange` always describe the full match. `truncated` is independent of `preview_limit` and is true only when the match crosses the 5,000-observation staging threshold
107
- - With the dataframe canvas enabled, only a match above 5,000 observations is staged whole as a SQL table and the response returns `canvasId` / `tableName` / `stagedRowCount`; matches at or below 5,000 are never staged. The rows stream 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. Call `eurostat_dataframe_describe` before `eurostat_dataframe_query`. Without a canvas those fields and tool guidance are absent, and narrowing the query is the way to the rest
108
- - Pass `canvas_id` from an earlier response to stage several results side by side and join across them
109
- - Async-response detection — large unfiltered queries return an actionable, non-retryable error with filter guidance rather than silently timing out
110
- - Fetches a *slice*. When the target is a whole dataset, `eurostat_download_dataset` reads the SDMX bulk endpoint instead, at roughly half the bytes
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
111
106
 
112
107
  ---
113
108
 
114
- ### `eurostat_download_dataset`
109
+ ### `eurostat_download_dataset` <sub>tool</sub>
115
110
 
116
- Download a whole dataset through the SDMX 2.1 TSV bulk endpoint (`/sdmx/2.1/data/{dataset}?format=TSV`).
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
117
+
118
+ ---
117
119
 
118
- - 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
119
- - 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
120
- - 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
121
- - **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
122
- - **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
123
- - **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
124
- - 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
125
- - 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. Call `eurostat_dataframe_describe` before `eurostat_dataframe_query`
126
- - 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
120
+ ### `eurostat_dataframe_describe` <sub>tool</sub>
121
+
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
127
125
 
128
126
  ---
129
127
 
130
- ### `eurostat_dataframe_describe` / `eurostat_dataframe_query`
128
+ ### `eurostat_dataframe_query` <sub>tool</sub>
131
129
 
132
- 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.
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
133
135
 
134
- - `eurostat_dataframe_describe` lists the staged tables with row counts and column names and types — call it before writing SQL
135
- - `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
136
- - Staged columns are flat, and the two stagers write different dimension columns — 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`); `eurostat_download_dataset` gives code columns only, since the bulk endpoint carries 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`
137
- - Tables from the two stagers join on their dimension code columns and `time` — same names, same `VARCHAR` type, `obs_value` `DOUBLE` on both — and their measure columns carry the same codes for the same observation. JSON-stat has no `CONF_STATUS` field and folds the marker into the observation status as `|C`; `eurostat_query_dataset` splits it back out before staging, so a confidential cell reads `obs_flag = NULL` with `conf_status = 'C'` on either table
138
- - 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
136
+ ---
139
137
 
140
- ## Resource
138
+ ### `eurostat://dataset/{dataset_code}` <sub>resource</sub>
141
139
 
142
- | Type | Name | Description |
143
- |:---|:---|:---|
144
- | Resource | `eurostat://dataset/{dataset_code}` | Dataset metadata (dimensions, time range, obs count, last-updated) accessible by URI for cache-injectable context |
140
+ - Same payload as `eurostat_get_dataset_info`, addressable as a resource URI for cache-injectable context
141
+ - `dataset_code` comes from `eurostat_search_datasets` or `eurostat_browse_themes`
145
142
 
146
143
  ## Features
147
144
 
148
- Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core):
149
-
150
- - Declarative tool definitions — single file per tool, framework handles registration and validation
151
- - Unified error handling across all tools
152
- - Pluggable auth (`none`, `jwt`, `oauth`)
153
- - Swappable storage backends: `in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`
154
- - Structured logging with optional OpenTelemetry tracing
155
- - Runs locally (stdio/HTTP) or on Cloudflare Workers from the same codebase
145
+ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): stdio and Streamable HTTP transports, pluggable auth (`none` / `jwt` / `oauth`), swappable storage (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`), structured logging with optional OpenTelemetry tracing.
156
146
 
157
147
  Eurostat-specific:
158
148
 
159
- - TTL-bounded in-memory cache for the TOC file — reused across all search and browse calls, refreshed on the first call past its 12-hour lifetime, with the last loaded copy served if a refresh fails
160
- - JSON-stat 2.0 stride-based decoder for the Statistics API response format
161
- - Dataset-scoped SDMX metadata parsing — dataflow descendants supply labels, annotations, concepts, and partial codelists; the content constraint supplies the actual available positions, including every period
162
- - Async-response detection — Eurostat returns over-limit queries as either an HTTP-200 warning object or an HTTP-413 error array; both fail once with actionable filter guidance
163
- - NUTS hierarchy geo-level filtering across query and dimension-value tools
164
- - Status decoding against both published codelists — the `OBS_FLAG` observation flag (provisional, estimated, definition differs) and the `CONF_STATUS` confidentiality marker, each in its own field. JSON-stat folds the two into one string and SDMX TSV into one cell; both are split on their separator, so a given observation reads the same whichever endpoint served it
165
- - Optional DuckDB dataframe canvas — a query matching more than 5,000 observations is streamed row by row into a SQL table, reaching the rows outside the inline preview without a second request to Eurostat
166
- - 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
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
167
154
 
168
155
  Agent-friendly output:
169
156
 
170
- - 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
171
- - 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
172
- - Structured error contracts with typed reasons and recovery hints on all tools
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
173
161
 
174
162
  ## Getting started
175
163
 
@@ -249,7 +237,7 @@ MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
249
237
 
250
238
  ### Prerequisites
251
239
 
252
- - [Bun v1.3.2](https://bun.sh/) or higher. No API key required — Eurostat's dissemination API is public.
240
+ - [Bun v1.4.0](https://bun.sh/) or higher. No API key required — Eurostat's dissemination API is public.
253
241
 
254
242
  ### Installation
255
243
 
@@ -297,6 +285,8 @@ All configuration is validated at startup via Zod schemas in `src/config/server-
297
285
  | `CANVAS_DEFAULT_ROW_LIMIT` | Max rows one `eurostat_dataframe_query` returns before reporting `truncated` | `10000` |
298
286
  | `OTEL_ENABLED` | Enable OpenTelemetry | `false` |
299
287
 
288
+ See [`.env.example`](./.env.example) for the full list of optional overrides.
289
+
300
290
  ## Running the server
301
291
 
302
292
  ### Local development
@@ -317,12 +307,23 @@ All configuration is validated at startup via Zod schemas in `src/config/server-
317
307
  ```sh
318
308
  bun run devcheck # Lints, formats, type-checks, and more
319
309
  bun run test # Runs the test suite
310
+ bun run lint:mcp # Validates MCP definitions against spec
320
311
  ```
321
312
 
313
+ ### Docker
314
+
315
+ ```sh
316
+ docker build -t eurostat-mcp-server .
317
+ docker run --rm -p 3010:3010 eurostat-mcp-server
318
+ ```
319
+
320
+ The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `/var/log/eurostat-mcp-server`. OpenTelemetry peer dependencies are installed by default — build with `--build-arg OTEL_ENABLED=false` to omit them.
321
+
322
322
  ## Project structure
323
323
 
324
324
  | Directory | Purpose |
325
325
  |:---|:---|
326
+ | `src/index.ts` | `createApp()` entry point — registers tools and resources and inits services. |
326
327
  | `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). Six tools for discovery and data access, plus two canvas-gated dataframe tools. |
327
328
  | `src/mcp-server/resources` | Resource definitions. Dataset metadata resource. |
328
329
  | `src/services/eurostat-catalogue` | Catalogue service — fetches and parses the Eurostat TOC TXT file; TTL-bounded in-memory cache. |
@@ -338,10 +339,11 @@ See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rule
338
339
  - Handlers throw, framework catches — no `try/catch` in tool logic
339
340
  - Use `ctx.log` for logging, `ctx.state` for storage
340
341
  - Register new tools and resources in the `createApp()` arrays
342
+ - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
341
343
 
342
344
  ## Contributing
343
345
 
344
- Issues and pull requests are welcome. Run checks and tests before submitting:
346
+ Issues are welcome. Run checks and tests before submitting:
345
347
 
346
348
  ```sh
347
349
  bun run devcheck
@@ -0,0 +1,29 @@
1
+ ---
2
+ summary: "Adopts mcp-ts-core 0.13.2: the server declares stateless session mode in source, tool argument rejections carry a structured InvalidParams envelope, and the development skill tree moves from skills/ to framework-skills/ so plugin installs no longer receive it."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.6.4 — 2026-09-16
8
+
9
+ ## Added
10
+
11
+ - **`audit:fix`** runs `bun audit fix`, upgrading vulnerable packages to the lowest safe version within existing ranges. First response to a transitive advisory, ahead of `bun update <name>` and `bun dedupe`.
12
+
13
+ ## Changed
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`.
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
+ - **`engines.bun`** raised to `>=1.4.0`; README prerequisites updated to match.
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`.
19
+ - Framework skills, `scripts/*.ts`, and `changelog/template.md` re-synced to `@cyanheads/mcp-ts-core` 0.13.2.
20
+ - **Repo metadata refreshed** — `.claude-plugin/plugin.json` gains `$schema`, author email/url, and `keywords`; GitHub issue forms auto-assign to `cyanheads`, allow blank issues, and route framework bugs to mcp-ts-core; `config.yml` links the private security-advisory form.
21
+ - **README restructured** around an Overview, a capability reference per tool and resource, and a Docker section.
22
+
23
+ ## Dependencies
24
+
25
+ - `@cyanheads/mcp-ts-core` `^0.12.3` → `^0.13.2`
26
+ - `zod` `^4.4.3` → `^4.6.4`
27
+ - `@biomejs/biome` (dev) `2.5.9` → `2.5.13`
28
+ - `ignore` (dev) `^7.0.6` → `^7.0.9`
29
+ - `tsc-alias` (dev) `^1.9.2` → `^1.9.5`
@@ -117,30 +117,13 @@ security: false
117
117
  in that unrelated item's metadata.
118
118
 
119
119
  TAG ANNOTATIONS — the annotated tag body renders as the GitHub Release body
120
- via `gh release create --notes-from-tag`. The tag is a derivative of this
121
- changelog entry — a condensed, scannable version, not a copy. Format:
122
-
123
- <theme — omit version number, GitHub prepends it>
124
- ← blank line
125
- <1-2 sentence context: what this release does>
126
- ← blank line
127
- Dependency bumps: ← section header
128
- ← blank line
129
- - `@cyanheads/mcp-ts-core` ^0.9.1 → ^0.9.6 ← bullet
130
- ← blank line
131
- Changed: ← only sections with entries
132
- ← blank line
133
- - `format()` output includes `query` in text mode
134
- ← blank line
135
- Added:
136
- ← blank line
137
- - `manifest.json` scaffolded for MCPB bundle support
138
- - Install badges (Claude Desktop, Cursor, VS Code)
139
- ← blank line
140
- <N> tests pass; `bun run devcheck` clean. ← footer
141
-
142
- Never a flat comma-separated string. Always structured markdown with
143
- sections. The tag must scan well as a rendered GitHub Release page.
120
+ via `gh release create --notes-from-tag`. It is a condensed digest of this
121
+ entry, never a copy, and its format is owned by the `release-and-publish`
122
+ skill (step 4, "Create the annotated tag"): the entry's `summary:` as the
123
+ theme line without the version, flat headline bullets — no Keep-a-Changelog
124
+ section headers, no gates line — at most one deps line, issue backlinks,
125
+ and the changelog link last. In release-PR mode the `git-wrapup` skill
126
+ authors that digest as the PR body's `## Changes` and the tag copies it.
144
127
  -->
145
128
 
146
129
  ## Added
package/dist/index.js CHANGED
@@ -34,6 +34,12 @@ const CANVAS_OFF = {
34
34
  await createApp({
35
35
  name: 'eurostat-mcp-server',
36
36
  title: 'eurostat-mcp-server',
37
+ /**
38
+ * No tool gates on `ctx.requestInput`, so nothing here needs a durable
39
+ * session. Declaring it in source keeps the posture with the code rather than
40
+ * the deployment; `MCP_SESSION_MODE` still wins when it is set.
41
+ */
42
+ sessionMode: 'stateless',
37
43
  tools: [
38
44
  eurostatSearchDatasets,
39
45
  eurostatBrowseThemes,
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,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"}
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;;;;OAIG;IACH,WAAW,EAAE,WAAW;IACxB,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"}
@@ -9,7 +9,7 @@
9
9
  import { notFound, serviceUnavailable, validationError, } from '@cyanheads/mcp-ts-core/errors';
10
10
  import { fetchWithTimeout } from '@cyanheads/mcp-ts-core/utils';
11
11
  import { getServerConfig } from '../../config/server-config.js';
12
- import { CONF_STATUS_COLUMN, CONF_STATUS_LABEL_COLUMN, CONF_STATUS_LABELS, OBS_FLAG_COLUMN, OBS_FLAG_LABEL_COLUMN, OBS_FLAG_LABELS, OBS_VALUE_COLUMN, } from '../../services/eurostat-codelists.js';
12
+ import { CONF_STATUS_COLUMN, CONF_STATUS_LABEL_COLUMN, CONF_STATUS_LABELS, OBS_FLAG_COLUMN, OBS_FLAG_LABEL_COLUMN, OBS_FLAG_LABELS, OBS_VALUE_COLUMN, } from '../eurostat-codelists.js';
13
13
  import { TIME_COLUMN, } from './types.js';
14
14
  /**
15
15
  * Decoded characters buffered before an XML body is classified.
@@ -5,7 +5,7 @@
5
5
  import { McpError, notFound, serviceUnavailable, validationError, } from '@cyanheads/mcp-ts-core/errors';
6
6
  import { fetchWithTimeout, withRetry } from '@cyanheads/mcp-ts-core/utils';
7
7
  import { getServerConfig } from '../../config/server-config.js';
8
- import { CONF_STATUS_COLUMN, CONF_STATUS_LABEL_COLUMN, CONF_STATUS_LABELS, OBS_FLAG_COLUMN, OBS_FLAG_LABEL_COLUMN, OBS_FLAG_LABELS, OBS_VALUE_COLUMN, } from '../../services/eurostat-codelists.js';
8
+ import { CONF_STATUS_COLUMN, CONF_STATUS_LABEL_COLUMN, CONF_STATUS_LABELS, OBS_FLAG_COLUMN, OBS_FLAG_LABEL_COLUMN, OBS_FLAG_LABELS, OBS_VALUE_COLUMN, } from '../eurostat-codelists.js';
9
9
  import { parseSdmxDatasetMetadata } from './sdmx-metadata.js';
10
10
  import { OBS_CAP, } from './types.js';
11
11
  /** Separator between the observation flag and the confidentiality status inside a JSON-stat status. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/eurostat-mcp-server",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "mcpName": "io.github.cyanheads/eurostat-mcp-server",
5
5
  "description": "Search and query the Eurostat catalogue — EU economy, demography, trade, health, and NUTS regional data via MCP. STDIO or Streamable HTTP.",
6
6
  "type": "module",
@@ -28,6 +28,7 @@
28
28
  "format": "biome check --write .",
29
29
  "format:unsafe": "biome check --write --unsafe .",
30
30
  "test": "vitest run",
31
+ "audit:fix": "bun audit fix",
31
32
  "audit:refresh": "rm -f bun.lock && bun install && bun audit",
32
33
  "list-skills": "bun run scripts/list-skills.ts",
33
34
  "lint:mcp": "bun run scripts/lint-mcp.ts",
@@ -80,25 +81,25 @@
80
81
  "license": "Apache-2.0",
81
82
  "packageManager": "bun@1.4.0",
82
83
  "engines": {
83
- "bun": ">=1.3.0",
84
+ "bun": ">=1.4.0",
84
85
  "node": ">=24.0.0"
85
86
  },
86
87
  "publishConfig": {
87
88
  "access": "public"
88
89
  },
89
90
  "dependencies": {
90
- "@cyanheads/mcp-ts-core": "^0.12.3",
91
+ "@cyanheads/mcp-ts-core": "^0.13.2",
91
92
  "@duckdb/node-api": "^1.5.5-r.4",
92
93
  "pino-pretty": "^13.1.3",
93
- "zod": "^4.4.3"
94
+ "zod": "^4.6.4"
94
95
  },
95
96
  "devDependencies": {
96
- "@biomejs/biome": "2.5.9",
97
+ "@biomejs/biome": "2.5.13",
97
98
  "@socketsecurity/bun-security-scanner": "^1.1.2",
98
99
  "@types/node": "26.2.0",
99
100
  "depcheck": "^1.4.7",
100
- "ignore": "^7.0.6",
101
- "tsc-alias": "^1.9.2",
101
+ "ignore": "^7.0.9",
102
+ "tsc-alias": "^1.9.5",
102
103
  "typescript": "^7.0.2",
103
104
  "vitest": "^4.1.11"
104
105
  }
package/server.json CHANGED
@@ -12,14 +12,14 @@
12
12
  "url": "https://eurostat.caseyjhand.com/mcp"
13
13
  }
14
14
  ],
15
- "version": "0.6.3",
15
+ "version": "0.6.4",
16
16
  "packages": [
17
17
  {
18
18
  "registryType": "npm",
19
19
  "registryBaseUrl": "https://registry.npmjs.org",
20
20
  "identifier": "@cyanheads/eurostat-mcp-server",
21
21
  "runtimeHint": "bun",
22
- "version": "0.6.3",
22
+ "version": "0.6.4",
23
23
  "packageArguments": [
24
24
  {
25
25
  "type": "positional",
@@ -110,7 +110,7 @@
110
110
  "registryBaseUrl": "https://registry.npmjs.org",
111
111
  "identifier": "@cyanheads/eurostat-mcp-server",
112
112
  "runtimeHint": "bun",
113
- "version": "0.6.3",
113
+ "version": "0.6.4",
114
114
  "packageArguments": [
115
115
  {
116
116
  "type": "positional",
Binary file