@cyanheads/exchange-rates-mcp-server 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +53 -33
- package/CLAUDE.md +53 -33
- package/Dockerfile +9 -6
- package/LICENSE +1 -1
- package/README.md +24 -15
- package/changelog/0.1.x/0.1.5.md +29 -0
- package/changelog/0.2.x/0.2.0.md +41 -0
- package/changelog/template.md +5 -3
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/resources/definitions/fx-rates-latest.resource.d.ts.map +1 -1
- package/dist/mcp-server/resources/definitions/fx-rates-latest.resource.js +4 -3
- package/dist/mcp-server/resources/definitions/fx-rates-latest.resource.js.map +1 -1
- package/dist/mcp-server/tools/definitions/fx-convert-currency.tool.d.ts +10 -0
- package/dist/mcp-server/tools/definitions/fx-convert-currency.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/fx-convert-currency.tool.js +29 -5
- package/dist/mcp-server/tools/definitions/fx-convert-currency.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/fx-dataframe-describe.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/fx-dataframe-describe.tool.js +7 -4
- package/dist/mcp-server/tools/definitions/fx-dataframe-describe.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/fx-dataframe-query.tool.d.ts +6 -1
- package/dist/mcp-server/tools/definitions/fx-dataframe-query.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/fx-dataframe-query.tool.js +30 -10
- package/dist/mcp-server/tools/definitions/fx-dataframe-query.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/fx-get-rate.tool.d.ts +10 -0
- package/dist/mcp-server/tools/definitions/fx-get-rate.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/fx-get-rate.tool.js +29 -5
- package/dist/mcp-server/tools/definitions/fx-get-rate.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/fx-get-rates.tool.d.ts +11 -1
- package/dist/mcp-server/tools/definitions/fx-get-rates.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/fx-get-rates.tool.js +33 -7
- package/dist/mcp-server/tools/definitions/fx-get-rates.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/fx-get-timeseries.tool.d.ts +13 -1
- package/dist/mcp-server/tools/definitions/fx-get-timeseries.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/fx-get-timeseries.tool.js +93 -44
- package/dist/mcp-server/tools/definitions/fx-get-timeseries.tool.js.map +1 -1
- package/dist/services/canvas/canvas-accessor.d.ts.map +1 -1
- package/dist/services/frankfurter/errors.d.ts +42 -0
- package/dist/services/frankfurter/errors.d.ts.map +1 -0
- package/dist/services/frankfurter/errors.js +64 -0
- package/dist/services/frankfurter/errors.js.map +1 -0
- package/dist/services/frankfurter/frankfurter-service.d.ts +42 -8
- package/dist/services/frankfurter/frankfurter-service.d.ts.map +1 -1
- package/dist/services/frankfurter/frankfurter-service.js +146 -67
- package/dist/services/frankfurter/frankfurter-service.js.map +1 -1
- package/dist/services/frankfurter/types.d.ts +14 -0
- package/dist/services/frankfurter/types.d.ts.map +1 -1
- package/package.json +12 -11
- package/server.json +5 -5
package/AGENTS.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** exchange-rates-mcp-server
|
|
4
|
-
**Version:** 0.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.
|
|
4
|
+
**Version:** 0.2.0
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.11.0`
|
|
6
6
|
**Engines:** Bun ≥1.3.0, Node ≥24.0.0
|
|
7
7
|
**MCP SDK:** `@modelcontextprotocol/sdk` ^1.29.0
|
|
8
8
|
**Zod:** ^4.4.3
|
|
@@ -35,7 +35,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
35
35
|
- **Logic throws, framework catches.** Tool/resource handlers are pure — throw on failure, no `try/catch`. Plain `Error` is fine; the framework catches, classifies, and formats. Use error factories (`notFound()`, `validationError()`, etc.) when the error code matters.
|
|
36
36
|
- **Use `ctx.log`** for request-scoped logging. No `console` calls.
|
|
37
37
|
- **Use `ctx.state`** for tenant-scoped storage. Never access persistence directly.
|
|
38
|
-
- **Check `ctx.elicit
|
|
38
|
+
- **Check `ctx.elicit`** for presence before calling.
|
|
39
39
|
- **Secrets in env vars only** — never hardcoded.
|
|
40
40
|
- **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
|
|
41
41
|
|
|
@@ -144,9 +144,23 @@ export function getServerConfig() {
|
|
|
144
144
|
|
|
145
145
|
`parseEnvConfig` maps Zod schema paths → env var names so errors name the variable (`MY_API_KEY`) not the path (`apiKey`). Throws `ConfigurationError`, which the framework prints as a clean startup banner.
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag can't be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else, so `=false` actually disables.
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
### Server identity and instructions
|
|
150
|
+
|
|
151
|
+
`createApp()` accepts optional identity fields forwarded to the SDK's `initialize` response and the server manifest (`/.well-known/mcp.json`):
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
await createApp({
|
|
155
|
+
name: 'exchange-rates-mcp-server',
|
|
156
|
+
title: 'exchange-rates-mcp-server', // display name — the hyphenated repo name, never Title Case
|
|
157
|
+
instructions: 'Use fx_list_currencies first to disambiguate "dollars".', // session-level context
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`description` and `websiteUrl` are also accepted but stay unset here — `description` derives from `package.json`, and an explicit copy is drift.
|
|
162
|
+
|
|
163
|
+
`instructions` is optional server-level orientation, sent on every `initialize` as session-level context. Use it for deployment guidance (connection aliases, regional notes, scope hints) instead of repeating the same context across tool descriptions. Client adoption is uneven, but there's no downside when set.
|
|
150
164
|
|
|
151
165
|
---
|
|
152
166
|
|
|
@@ -157,13 +171,14 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
157
171
|
| Property | Description |
|
|
158
172
|
|:---------|:------------|
|
|
159
173
|
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. |
|
|
160
|
-
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
|
|
161
|
-
| `ctx.elicit` | Ask user for structured input. **Check for presence first:** `if (ctx.elicit) { ... }` |
|
|
162
|
-
| `ctx.
|
|
174
|
+
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
|
|
175
|
+
| `ctx.elicit` | Ask user for structured input — form call `(message, schema)` or `.url(message, url)` for an external link. **Check for presence first:** `if (ctx.elicit) { ... }` |
|
|
176
|
+
| `ctx.enrich` | Success-path agent context (empty-result notices, query echo, pagination totals) — `ctx.enrich(...)` or `.notice()` / `.total()` / `.echo()` / `.truncated()`. Reaches `structuredContent` and `content[]`; lands only when the definition declares an `enrichment` block (no-op otherwise). |
|
|
177
|
+
| `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
|
|
163
178
|
| `ctx.signal` | `AbortSignal` for cancellation. |
|
|
164
179
|
| `ctx.progress` | Task progress (present when `task: true`) — `.setTotal(n)`, `.increment()`, `.update(message)`. |
|
|
165
180
|
| `ctx.requestId` | Unique request ID. |
|
|
166
|
-
| `ctx.tenantId` | Tenant ID from JWT
|
|
181
|
+
| `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
|
|
167
182
|
|
|
168
183
|
---
|
|
169
184
|
|
|
@@ -171,7 +186,7 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
171
186
|
|
|
172
187
|
Handlers throw — the framework catches, classifies, and formats.
|
|
173
188
|
|
|
174
|
-
**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
|
|
189
|
+
**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 (≥ 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); override with an explicit `{ recovery: { hint: '...' } }` when dynamic runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring.
|
|
175
190
|
|
|
176
191
|
```ts
|
|
177
192
|
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
@@ -183,7 +198,7 @@ errors: [
|
|
|
183
198
|
],
|
|
184
199
|
async handler(input, ctx) {
|
|
185
200
|
const item = await db.find(input.id);
|
|
186
|
-
if (!item) throw ctx.fail('no_match', `No item ${input.id}
|
|
201
|
+
if (!item) throw ctx.fail('no_match', `No item ${input.id}`, ctx.recoveryFor('no_match'));
|
|
187
202
|
return item;
|
|
188
203
|
}
|
|
189
204
|
```
|
|
@@ -222,8 +237,9 @@ src/
|
|
|
222
237
|
canvas/
|
|
223
238
|
canvas-accessor.ts # Module-level DataCanvas accessor (set via createApp setup())
|
|
224
239
|
frankfurter/
|
|
225
|
-
|
|
226
|
-
|
|
240
|
+
errors.ts # Structured service failures — reason/field on McpError.data, read via failureOf()
|
|
241
|
+
frankfurter-service.ts # Frankfurter HTTP client — getRate, getRates, getTimeSeries, listCurrencies
|
|
242
|
+
types.ts # Domain types (ResolvedRate, SeriesRow, TimeSeriesResult, …)
|
|
227
243
|
mcp-server/
|
|
228
244
|
tools/definitions/
|
|
229
245
|
fx-convert-currency.tool.ts # Convert amount between two currencies
|
|
@@ -253,7 +269,7 @@ src/
|
|
|
253
269
|
|
|
254
270
|
## Skills
|
|
255
271
|
|
|
256
|
-
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.
|
|
272
|
+
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. `bun run list-skills` prints the full registry.
|
|
257
273
|
|
|
258
274
|
**Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
|
|
259
275
|
|
|
@@ -273,7 +289,6 @@ Available skills:
|
|
|
273
289
|
| `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
|
|
274
290
|
| `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
|
|
275
291
|
| `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
|
|
276
|
-
| `devcheck` | Lint, format, typecheck, audit |
|
|
277
292
|
| `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
|
|
278
293
|
| `git-wrapup` | Land working-tree changes as a versioned commit + annotated tag — version bump, changelog, verify, tag. Local only. |
|
|
279
294
|
| `release-and-publish` | Push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
|
|
@@ -281,12 +296,14 @@ Available skills:
|
|
|
281
296
|
| `orchestrations` | Chain task skills into a gated multi-phase pipeline — build-out, QA-fix, update-ship — when you can spawn sub-agents |
|
|
282
297
|
| `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
|
|
283
298
|
| `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
|
|
299
|
+
| `techniques` | Catalog of response/data-shaping techniques — overflow handling, payload shaping, retrieval patterns |
|
|
284
300
|
| `api-auth` | Auth modes, scopes, JWT/OAuth |
|
|
285
301
|
| `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
|
|
286
302
|
| `api-config` | AppConfig, parseConfig, env vars |
|
|
287
303
|
| `api-context` | Context interface, logger, state, progress |
|
|
288
304
|
| `api-errors` | McpError, JsonRpcErrorCode, error patterns |
|
|
289
305
|
| `api-linter` | Definition linter rule catalog — invoked by `bun run lint:mcp` and `devcheck` |
|
|
306
|
+
| `api-mirror` | MirrorService: persistent self-refreshing local mirror (embedded SQLite + FTS5) of a bulk upstream dataset — Tier 3 opt-in |
|
|
290
307
|
| `api-services` | LLM, Speech, Graph services |
|
|
291
308
|
| `api-testing` | createMockContext, test patterns |
|
|
292
309
|
| `api-utils` | Formatting, parsing, security, pagination, scheduling, telemetry helpers |
|
|
@@ -301,31 +318,34 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
301
318
|
|
|
302
319
|
## Commands
|
|
303
320
|
|
|
304
|
-
**Runtime:** Scripts use
|
|
321
|
+
**Runtime:** Scripts use Bun's native TypeScript execution — `bun run <cmd>` is the standard invocation. `npm run <cmd>` also works (npm delegates to bun).
|
|
305
322
|
|
|
306
323
|
| Command | Purpose |
|
|
307
324
|
|:--------|:--------|
|
|
308
|
-
| `
|
|
309
|
-
| `
|
|
310
|
-
| `
|
|
311
|
-
| `
|
|
325
|
+
| `bun run build` | Compile TypeScript |
|
|
326
|
+
| `bun run rebuild` | Clean + build |
|
|
327
|
+
| `bun run clean` | Remove build artifacts |
|
|
328
|
+
| `bun run devcheck` | Lint + format + typecheck + security + changelog sync |
|
|
312
329
|
| `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. |
|
|
313
|
-
| `
|
|
314
|
-
| `
|
|
315
|
-
| `
|
|
316
|
-
| `
|
|
317
|
-
| `
|
|
318
|
-
| `
|
|
319
|
-
| `
|
|
320
|
-
| `
|
|
321
|
-
| `
|
|
322
|
-
| `
|
|
330
|
+
| `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
|
|
331
|
+
| `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |
|
|
332
|
+
| `bun run list-skills` | Print the skill registry |
|
|
333
|
+
| `bun run tree` | Generate directory structure doc |
|
|
334
|
+
| `bun run format` | Auto-fix formatting (safe fixes only) |
|
|
335
|
+
| `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
|
|
336
|
+
| `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
|
|
337
|
+
| `bun run start:stdio` | Production mode (stdio) |
|
|
338
|
+
| `bun run start:http` | Production mode (HTTP) |
|
|
339
|
+
| `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
|
|
340
|
+
| `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
|
|
341
|
+
| `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
|
|
342
|
+
| `bun run publish-mcp` | Publish to MCP Registry via mcp-publisher |
|
|
323
343
|
|
|
324
344
|
---
|
|
325
345
|
|
|
326
346
|
## Bundling
|
|
327
347
|
|
|
328
|
-
`
|
|
348
|
+
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on. This server uses DataCanvas, so its bundle ships without the DuckDB native — `@duckdb/node-api` is an optional peer loaded lazily, so `fx_dataframe_*` report an actionable install hint and every other tool works normally. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
|
|
329
349
|
|
|
330
350
|
**Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match.
|
|
331
351
|
|
|
@@ -343,14 +363,14 @@ Each per-version file opens with YAML frontmatter:
|
|
|
343
363
|
---
|
|
344
364
|
summary: "One-line headline, ≤350 chars" # required — powers the rollup index
|
|
345
365
|
breaking: false # optional — true flags breaking changes
|
|
346
|
-
security: false # optional — true
|
|
366
|
+
security: false # optional — true ONLY for a source-code security fix, never a dependency CVE bump
|
|
347
367
|
---
|
|
348
368
|
|
|
349
369
|
# 0.1.0 — YYYY-MM-DD
|
|
350
370
|
...
|
|
351
371
|
```
|
|
352
372
|
|
|
353
|
-
`breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section. When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
|
|
373
|
+
`breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section — set it only for a security fix in this server's *own source code*, never for a routine dependency or transitive CVE bump (record those under `## Dependencies`). When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
|
|
354
374
|
|
|
355
375
|
`agent-notes` is an optional free-form field for maintenance agents processing the release downstream. Content here won't appear in the rendered CHANGELOG — it's consumed by agents running the `maintenance` skill. Use it for adoption instructions that don't fit the human-facing sections: new files to create, fields to populate, one-time migration steps. Omit entirely when there's nothing to say.
|
|
356
376
|
|
package/CLAUDE.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** exchange-rates-mcp-server
|
|
4
|
-
**Version:** 0.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.
|
|
4
|
+
**Version:** 0.2.0
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.11.0`
|
|
6
6
|
**Engines:** Bun ≥1.3.0, Node ≥24.0.0
|
|
7
7
|
**MCP SDK:** `@modelcontextprotocol/sdk` ^1.29.0
|
|
8
8
|
**Zod:** ^4.4.3
|
|
@@ -35,7 +35,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
35
35
|
- **Logic throws, framework catches.** Tool/resource handlers are pure — throw on failure, no `try/catch`. Plain `Error` is fine; the framework catches, classifies, and formats. Use error factories (`notFound()`, `validationError()`, etc.) when the error code matters.
|
|
36
36
|
- **Use `ctx.log`** for request-scoped logging. No `console` calls.
|
|
37
37
|
- **Use `ctx.state`** for tenant-scoped storage. Never access persistence directly.
|
|
38
|
-
- **Check `ctx.elicit
|
|
38
|
+
- **Check `ctx.elicit`** for presence before calling.
|
|
39
39
|
- **Secrets in env vars only** — never hardcoded.
|
|
40
40
|
- **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
|
|
41
41
|
|
|
@@ -144,9 +144,23 @@ export function getServerConfig() {
|
|
|
144
144
|
|
|
145
145
|
`parseEnvConfig` maps Zod schema paths → env var names so errors name the variable (`MY_API_KEY`) not the path (`apiKey`). Throws `ConfigurationError`, which the framework prints as a clean startup banner.
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag can't be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else, so `=false` actually disables.
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
### Server identity and instructions
|
|
150
|
+
|
|
151
|
+
`createApp()` accepts optional identity fields forwarded to the SDK's `initialize` response and the server manifest (`/.well-known/mcp.json`):
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
await createApp({
|
|
155
|
+
name: 'exchange-rates-mcp-server',
|
|
156
|
+
title: 'exchange-rates-mcp-server', // display name — the hyphenated repo name, never Title Case
|
|
157
|
+
instructions: 'Use fx_list_currencies first to disambiguate "dollars".', // session-level context
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`description` and `websiteUrl` are also accepted but stay unset here — `description` derives from `package.json`, and an explicit copy is drift.
|
|
162
|
+
|
|
163
|
+
`instructions` is optional server-level orientation, sent on every `initialize` as session-level context. Use it for deployment guidance (connection aliases, regional notes, scope hints) instead of repeating the same context across tool descriptions. Client adoption is uneven, but there's no downside when set.
|
|
150
164
|
|
|
151
165
|
---
|
|
152
166
|
|
|
@@ -157,13 +171,14 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
157
171
|
| Property | Description |
|
|
158
172
|
|:---------|:------------|
|
|
159
173
|
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. |
|
|
160
|
-
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
|
|
161
|
-
| `ctx.elicit` | Ask user for structured input. **Check for presence first:** `if (ctx.elicit) { ... }` |
|
|
162
|
-
| `ctx.
|
|
174
|
+
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
|
|
175
|
+
| `ctx.elicit` | Ask user for structured input — form call `(message, schema)` or `.url(message, url)` for an external link. **Check for presence first:** `if (ctx.elicit) { ... }` |
|
|
176
|
+
| `ctx.enrich` | Success-path agent context (empty-result notices, query echo, pagination totals) — `ctx.enrich(...)` or `.notice()` / `.total()` / `.echo()` / `.truncated()`. Reaches `structuredContent` and `content[]`; lands only when the definition declares an `enrichment` block (no-op otherwise). |
|
|
177
|
+
| `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
|
|
163
178
|
| `ctx.signal` | `AbortSignal` for cancellation. |
|
|
164
179
|
| `ctx.progress` | Task progress (present when `task: true`) — `.setTotal(n)`, `.increment()`, `.update(message)`. |
|
|
165
180
|
| `ctx.requestId` | Unique request ID. |
|
|
166
|
-
| `ctx.tenantId` | Tenant ID from JWT
|
|
181
|
+
| `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
|
|
167
182
|
|
|
168
183
|
---
|
|
169
184
|
|
|
@@ -171,7 +186,7 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
171
186
|
|
|
172
187
|
Handlers throw — the framework catches, classifies, and formats.
|
|
173
188
|
|
|
174
|
-
**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
|
|
189
|
+
**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 (≥ 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); override with an explicit `{ recovery: { hint: '...' } }` when dynamic runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring.
|
|
175
190
|
|
|
176
191
|
```ts
|
|
177
192
|
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
@@ -183,7 +198,7 @@ errors: [
|
|
|
183
198
|
],
|
|
184
199
|
async handler(input, ctx) {
|
|
185
200
|
const item = await db.find(input.id);
|
|
186
|
-
if (!item) throw ctx.fail('no_match', `No item ${input.id}
|
|
201
|
+
if (!item) throw ctx.fail('no_match', `No item ${input.id}`, ctx.recoveryFor('no_match'));
|
|
187
202
|
return item;
|
|
188
203
|
}
|
|
189
204
|
```
|
|
@@ -222,8 +237,9 @@ src/
|
|
|
222
237
|
canvas/
|
|
223
238
|
canvas-accessor.ts # Module-level DataCanvas accessor (set via createApp setup())
|
|
224
239
|
frankfurter/
|
|
225
|
-
|
|
226
|
-
|
|
240
|
+
errors.ts # Structured service failures — reason/field on McpError.data, read via failureOf()
|
|
241
|
+
frankfurter-service.ts # Frankfurter HTTP client — getRate, getRates, getTimeSeries, listCurrencies
|
|
242
|
+
types.ts # Domain types (ResolvedRate, SeriesRow, TimeSeriesResult, …)
|
|
227
243
|
mcp-server/
|
|
228
244
|
tools/definitions/
|
|
229
245
|
fx-convert-currency.tool.ts # Convert amount between two currencies
|
|
@@ -253,7 +269,7 @@ src/
|
|
|
253
269
|
|
|
254
270
|
## Skills
|
|
255
271
|
|
|
256
|
-
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.
|
|
272
|
+
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. `bun run list-skills` prints the full registry.
|
|
257
273
|
|
|
258
274
|
**Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
|
|
259
275
|
|
|
@@ -273,7 +289,6 @@ Available skills:
|
|
|
273
289
|
| `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
|
|
274
290
|
| `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
|
|
275
291
|
| `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
|
|
276
|
-
| `devcheck` | Lint, format, typecheck, audit |
|
|
277
292
|
| `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
|
|
278
293
|
| `git-wrapup` | Land working-tree changes as a versioned commit + annotated tag — version bump, changelog, verify, tag. Local only. |
|
|
279
294
|
| `release-and-publish` | Push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
|
|
@@ -281,12 +296,14 @@ Available skills:
|
|
|
281
296
|
| `orchestrations` | Chain task skills into a gated multi-phase pipeline — build-out, QA-fix, update-ship — when you can spawn sub-agents |
|
|
282
297
|
| `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
|
|
283
298
|
| `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
|
|
299
|
+
| `techniques` | Catalog of response/data-shaping techniques — overflow handling, payload shaping, retrieval patterns |
|
|
284
300
|
| `api-auth` | Auth modes, scopes, JWT/OAuth |
|
|
285
301
|
| `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
|
|
286
302
|
| `api-config` | AppConfig, parseConfig, env vars |
|
|
287
303
|
| `api-context` | Context interface, logger, state, progress |
|
|
288
304
|
| `api-errors` | McpError, JsonRpcErrorCode, error patterns |
|
|
289
305
|
| `api-linter` | Definition linter rule catalog — invoked by `bun run lint:mcp` and `devcheck` |
|
|
306
|
+
| `api-mirror` | MirrorService: persistent self-refreshing local mirror (embedded SQLite + FTS5) of a bulk upstream dataset — Tier 3 opt-in |
|
|
290
307
|
| `api-services` | LLM, Speech, Graph services |
|
|
291
308
|
| `api-testing` | createMockContext, test patterns |
|
|
292
309
|
| `api-utils` | Formatting, parsing, security, pagination, scheduling, telemetry helpers |
|
|
@@ -301,31 +318,34 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
301
318
|
|
|
302
319
|
## Commands
|
|
303
320
|
|
|
304
|
-
**Runtime:** Scripts use
|
|
321
|
+
**Runtime:** Scripts use Bun's native TypeScript execution — `bun run <cmd>` is the standard invocation. `npm run <cmd>` also works (npm delegates to bun).
|
|
305
322
|
|
|
306
323
|
| Command | Purpose |
|
|
307
324
|
|:--------|:--------|
|
|
308
|
-
| `
|
|
309
|
-
| `
|
|
310
|
-
| `
|
|
311
|
-
| `
|
|
325
|
+
| `bun run build` | Compile TypeScript |
|
|
326
|
+
| `bun run rebuild` | Clean + build |
|
|
327
|
+
| `bun run clean` | Remove build artifacts |
|
|
328
|
+
| `bun run devcheck` | Lint + format + typecheck + security + changelog sync |
|
|
312
329
|
| `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. |
|
|
313
|
-
| `
|
|
314
|
-
| `
|
|
315
|
-
| `
|
|
316
|
-
| `
|
|
317
|
-
| `
|
|
318
|
-
| `
|
|
319
|
-
| `
|
|
320
|
-
| `
|
|
321
|
-
| `
|
|
322
|
-
| `
|
|
330
|
+
| `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
|
|
331
|
+
| `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |
|
|
332
|
+
| `bun run list-skills` | Print the skill registry |
|
|
333
|
+
| `bun run tree` | Generate directory structure doc |
|
|
334
|
+
| `bun run format` | Auto-fix formatting (safe fixes only) |
|
|
335
|
+
| `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
|
|
336
|
+
| `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
|
|
337
|
+
| `bun run start:stdio` | Production mode (stdio) |
|
|
338
|
+
| `bun run start:http` | Production mode (HTTP) |
|
|
339
|
+
| `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
|
|
340
|
+
| `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
|
|
341
|
+
| `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
|
|
342
|
+
| `bun run publish-mcp` | Publish to MCP Registry via mcp-publisher |
|
|
323
343
|
|
|
324
344
|
---
|
|
325
345
|
|
|
326
346
|
## Bundling
|
|
327
347
|
|
|
328
|
-
`
|
|
348
|
+
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on. This server uses DataCanvas, so its bundle ships without the DuckDB native — `@duckdb/node-api` is an optional peer loaded lazily, so `fx_dataframe_*` report an actionable install hint and every other tool works normally. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
|
|
329
349
|
|
|
330
350
|
**Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match.
|
|
331
351
|
|
|
@@ -343,14 +363,14 @@ Each per-version file opens with YAML frontmatter:
|
|
|
343
363
|
---
|
|
344
364
|
summary: "One-line headline, ≤350 chars" # required — powers the rollup index
|
|
345
365
|
breaking: false # optional — true flags breaking changes
|
|
346
|
-
security: false # optional — true
|
|
366
|
+
security: false # optional — true ONLY for a source-code security fix, never a dependency CVE bump
|
|
347
367
|
---
|
|
348
368
|
|
|
349
369
|
# 0.1.0 — YYYY-MM-DD
|
|
350
370
|
...
|
|
351
371
|
```
|
|
352
372
|
|
|
353
|
-
`breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section. When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
|
|
373
|
+
`breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section — set it only for a security fix in this server's *own source code*, never for a routine dependency or transitive CVE bump (record those under `## Dependencies`). When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
|
|
354
374
|
|
|
355
375
|
`agent-notes` is an optional free-form field for maintenance agents processing the release downstream. Content here won't appear in the rendered CHANGELOG — it's consumed by agents running the `maintenance` skill. Use it for adoption instructions that don't fit the human-facing sections: new files to create, fields to populate, one-time migration steps. Omit entirely when there's nothing to say.
|
|
356
376
|
|
package/Dockerfile
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# This stage installs all dependencies (including dev), builds the TypeScript
|
|
5
5
|
# source code into JavaScript, and prepares the production assets.
|
|
6
6
|
# ==============================================================================
|
|
7
|
-
FROM oven/bun:1.3 AS build
|
|
7
|
+
FROM oven/bun:1.3.14 AS build
|
|
8
8
|
|
|
9
9
|
WORKDIR /usr/src/app
|
|
10
10
|
|
|
@@ -16,8 +16,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
16
16
|
# Copy dependency manifests for optimized layer caching
|
|
17
17
|
COPY package.json bun.lock ./
|
|
18
18
|
|
|
19
|
-
# Install all dependencies (including dev dependencies for building)
|
|
20
|
-
|
|
19
|
+
# Install all dependencies (including dev dependencies for building).
|
|
20
|
+
# The BuildKit cache mount persists Bun's global package cache across builds.
|
|
21
|
+
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
22
|
+
bun install --frozen-lockfile --ignore-scripts
|
|
21
23
|
|
|
22
24
|
# Copy the rest of the source code
|
|
23
25
|
COPY . .
|
|
@@ -33,7 +35,7 @@ RUN bun run build
|
|
|
33
35
|
# application. It uses a slim base image and only includes production
|
|
34
36
|
# dependencies and build artifacts.
|
|
35
37
|
# ==============================================================================
|
|
36
|
-
FROM oven/bun:1.3-slim AS production
|
|
38
|
+
FROM oven/bun:1.3.14-slim AS production
|
|
37
39
|
|
|
38
40
|
WORKDIR /usr/src/app
|
|
39
41
|
|
|
@@ -61,8 +63,9 @@ COPY --from=build /usr/src/app/node_modules ./node_modules
|
|
|
61
63
|
# These are not bundled by default to keep the base image lean. Enable at build time
|
|
62
64
|
# with: docker build --build-arg OTEL_ENABLED=true
|
|
63
65
|
ARG OTEL_ENABLED=true
|
|
64
|
-
RUN
|
|
65
|
-
|
|
66
|
+
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
67
|
+
if [ "$OTEL_ENABLED" = "true" ]; then \
|
|
68
|
+
bun add --omit=dev --ignore-scripts @hono/otel \
|
|
66
69
|
@opentelemetry/instrumentation-http \
|
|
67
70
|
@opentelemetry/exporter-metrics-otlp-http \
|
|
68
71
|
@opentelemetry/exporter-trace-otlp-http \
|
package/LICENSE
CHANGED
|
@@ -186,7 +186,7 @@ Apache License
|
|
|
186
186
|
same "printed page" as the copyright notice for easier
|
|
187
187
|
identification within third-party archives.
|
|
188
188
|
|
|
189
|
-
Copyright
|
|
189
|
+
Copyright 2026 Casey Hand @cyanheads
|
|
190
190
|
|
|
191
191
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
192
|
you may not use this file except in compliance with the License.
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
<div align="center">
|
|
9
9
|
|
|
10
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/exchange-rates-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/exchange-rates-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -29,17 +29,19 @@
|
|
|
29
29
|
|
|
30
30
|
## Tools
|
|
31
31
|
|
|
32
|
-
Seven tools for working with ECB FX rate data — currency lookup and disambiguation, point-in-time rates and conversions, historical time-series retrieval, and SQL analytics over the DataCanvas workspace that long time-series calls produce
|
|
32
|
+
Seven tools for working with ECB FX rate data — currency lookup and disambiguation, point-in-time rates and conversions, historical time-series retrieval, and SQL analytics over the DataCanvas workspace that long time-series calls produce.
|
|
33
|
+
|
|
34
|
+
The two `fx_dataframe_*` tools require DataCanvas. With `CANVAS_PROVIDER_TYPE` unset (the default) they are not advertised in `tools/list` at all, so a client never sees a tool it cannot call; the HTTP landing page still lists them as disabled cards hinting `CANVAS_PROVIDER_TYPE=duckdb`, so operators can tell they exist. In that mode `fx_get_timeseries` returns every range inline:
|
|
33
35
|
|
|
34
36
|
| Tool | Description |
|
|
35
37
|
|:-----|:------------|
|
|
36
38
|
| `fx_list_currencies` | List all ~30 ECB-supported ISO 4217 currencies with full names. Use before converting to disambiguate "dollars" (USD vs AUD vs CAD vs HKD vs SGD). |
|
|
37
|
-
| `fx_get_rates` | Snapshot of all available rates for a base currency at latest or a historical date. Optional `symbols` filter for smaller responses. |
|
|
39
|
+
| `fx_get_rates` | Snapshot of all available rates for a base currency at latest or a historical date. Optional `symbols` filter for smaller responses; listing the base itself returns a rate of 1 for it. |
|
|
38
40
|
| `fx_get_rate` | Exchange rate for a single currency pair at latest or a historical date. Surfaces `date_snapped` when a weekend/holiday request returns the prior business-day rate. |
|
|
39
41
|
| `fx_convert_currency` | Convert an amount between any two currencies at latest or a historical rate. Cross-rates are triangulated through EUR. Returns converted amount, rate used, rate date, and whether the date was snapped. |
|
|
40
|
-
| `fx_get_timeseries` | Historical daily rates for a currency pair over a date range. Short ranges (≤90 days) are returned inline; long ranges spill to
|
|
41
|
-
| `fx_dataframe_describe` | List DataCanvas tables and their columns from a prior `fx_get_timeseries` call. Required first step before `fx_dataframe_query`. |
|
|
42
|
-
| `fx_dataframe_query` | Run a read-only SQL SELECT against a DataCanvas table produced by `fx_get_timeseries`. Supports aggregations, GROUP BY, window functions, and JOINs across multiple registered tables. |
|
|
42
|
+
| `fx_get_timeseries` | Historical daily rates for a currency pair over a date range, never including a date outside it. Short ranges (≤90 days) are returned inline; when DataCanvas is enabled, long ranges spill to it with a `canvas_id` for SQL follow-up. |
|
|
43
|
+
| `fx_dataframe_describe` | List DataCanvas tables and their columns from a prior `fx_get_timeseries` call. Required first step before `fx_dataframe_query`. Needs `CANVAS_PROVIDER_TYPE=duckdb`. |
|
|
44
|
+
| `fx_dataframe_query` | Run a read-only SQL SELECT against a DataCanvas table produced by `fx_get_timeseries`. Supports aggregations, GROUP BY, window functions, and JOINs across multiple registered tables. Needs `CANVAS_PROVIDER_TYPE=duckdb`. |
|
|
43
45
|
|
|
44
46
|
### `fx_list_currencies`
|
|
45
47
|
|
|
@@ -56,6 +58,7 @@ Full rates snapshot for a base currency in one call.
|
|
|
56
58
|
|
|
57
59
|
- Returns all available quote currencies at a given date (default: latest)
|
|
58
60
|
- Optional `symbols` parameter narrows the response to specific quote currencies
|
|
61
|
+
- Naming the base currency in `symbols` is valid — it is answered locally with a rate of 1 rather than sent upstream, which keeps a self-quote from failing
|
|
59
62
|
- Useful for seeding bulk comparison workflows or discovering what's available
|
|
60
63
|
|
|
61
64
|
---
|
|
@@ -66,6 +69,7 @@ Point-in-time exchange rate for a single pair.
|
|
|
66
69
|
|
|
67
70
|
- Returns the rate, the actual rate date, and `date_snapped: true` when the API silently moved a weekend/holiday request to the prior business day
|
|
68
71
|
- Cross-rates (neither side EUR) are triangulated in a single API call — no extra round trip
|
|
72
|
+
- A same-currency pair returns a rate of 1 without a self-quote reaching the API, but still reports the publication date the ECB actually had for that currency, so `rate_date` and `date_snapped` read the same as for any other pair
|
|
69
73
|
- Use `fx_convert_currency` when you need the converted amount; use this tool when you only need the rate number
|
|
70
74
|
|
|
71
75
|
---
|
|
@@ -75,7 +79,7 @@ Point-in-time exchange rate for a single pair.
|
|
|
75
79
|
Convert an amount between any two currencies.
|
|
76
80
|
|
|
77
81
|
- Handles EUR ↔ any, any ↔ EUR, and cross-rate (USD → JPY via EUR) in one upstream call
|
|
78
|
-
- Returns `
|
|
82
|
+
- Returns `quote_amount`, `rate`, `rate_date`, `date_snapped`, plus `rate_type` and `source` provenance on every response
|
|
79
83
|
- Historical conversions supported back to 1999-01-04 (ECB launch date)
|
|
80
84
|
|
|
81
85
|
---
|
|
@@ -87,7 +91,10 @@ Historical rate series and DataCanvas SQL analytics.
|
|
|
87
91
|
`fx_get_timeseries` returns a date-keyed series (business days only — ECB publishes once per business day):
|
|
88
92
|
|
|
89
93
|
- Short ranges (≤ `FX_TIMESERIES_CANVAS_THRESHOLD_DAYS`, default 90 days) → inline `rates` map + metadata
|
|
90
|
-
- Long ranges → first N rows inline + `canvas_id`, `table_name`, and `
|
|
94
|
+
- Long ranges, **when DataCanvas is enabled** → first N rows inline + `canvas_id`, `table_name`, and `spilled: true` — the full series is registered as a DuckDB-backed table. Without `CANVAS_PROVIDER_TYPE=duckdb` a long range comes back inline with `spilled: false` and a `notice` saying the threshold was crossed but no canvas was configured
|
|
95
|
+
- Requesting the same currency on both sides returns a rate of 1 on each publication day in the range, taken from the ECB's real calendar rather than a synthetic Mon–Fri loop
|
|
96
|
+
|
|
97
|
+
The response never carries a date outside the requested range. Frankfurter snaps a range that opens on a weekend or bank holiday back to the prior publication day; those rows are dropped, so `start_date` and `end_date` always sit inside the window you asked for. A range covering only non-publication days therefore returns an empty `rates` map with `rate_count: 0` and a `notice` explaining that the ECB published nothing in that window — distinguishable from an error.
|
|
91
98
|
|
|
92
99
|
Once a `canvas_id` is in hand:
|
|
93
100
|
|
|
@@ -115,7 +122,7 @@ Built on [`@cyanheads/mcp-ts-core`](https://www.npmjs.com/package/@cyanheads/mcp
|
|
|
115
122
|
|
|
116
123
|
- Declarative tool and resource definitions — single file per primitive, framework handles registration and validation
|
|
117
124
|
- Unified error handling — handlers throw, framework catches, classifies, and formats
|
|
118
|
-
- Typed error contracts with recovery hints — `unsupported_currency`, `date_out_of_range`, `canvas_not_found`, `invalid_query`
|
|
125
|
+
- Typed error contracts with recovery hints — `unsupported_currency`, `date_out_of_range`, `canvas_not_found`, `missing_table`, `invalid_query`
|
|
119
126
|
- Pluggable auth: `none`, `jwt`, `oauth`
|
|
120
127
|
- Structured logging with optional OpenTelemetry tracing
|
|
121
128
|
- STDIO and Streamable HTTP transports
|
|
@@ -123,17 +130,19 @@ Built on [`@cyanheads/mcp-ts-core`](https://www.npmjs.com/package/@cyanheads/mcp
|
|
|
123
130
|
ECB FX–specific:
|
|
124
131
|
|
|
125
132
|
- Keyless access via [Frankfurter](https://www.frankfurter.dev/) — a Cloudflare-fronted ECB proxy; no API keys required
|
|
126
|
-
- Cross-rate triangulation: any pair works
|
|
133
|
+
- Cross-rate triangulation: any pair works — USD → JPY is one upstream call, cross-rated through EUR on Frankfurter's side
|
|
127
134
|
- Weekend/holiday date semantics: `date_snapped` flag surfaces when the API returns a different date than requested
|
|
128
135
|
- ECB data covers ~30 major currencies from 1999-01-04 to present; `fx_list_currencies` always reflects the live set
|
|
129
|
-
-
|
|
136
|
+
- Identity pairs never surface an upstream rejection: `fx_get_rate`, `fx_get_rates`, and `fx_get_timeseries` all return a rate of 1 for a currency against itself, dated to the days the ECB actually published for that currency rather than to the calendar dates requested
|
|
137
|
+
- DataCanvas integration: when enabled, `fx_get_timeseries` spills long ranges to DuckDB for aggregations and trend analysis
|
|
130
138
|
- Rate provenance on every response: `rate_type: "ECB reference (mid-market)"` and `source: "ECB via Frankfurter"` — explicitly mid-market, not tradeable bid/ask
|
|
131
139
|
|
|
132
140
|
Agent-friendly output:
|
|
133
141
|
|
|
134
142
|
- Rate provenance on every response — `rate_type`, `source`, `rate_date`, and `date_snapped` so agents can reason about trust and freshness
|
|
135
143
|
- Structured error contracts — typed `reason` fields (`unsupported_currency`, `date_out_of_range`, `invalid_query`, …) let callers branch on failure type, not string parsing
|
|
136
|
-
- Discriminated DataCanvas output — `
|
|
144
|
+
- Discriminated DataCanvas output — `spilled: true` plus `canvas_id` signal when a time-series was staged for SQL follow-up rather than returned inline
|
|
145
|
+
- Success-path `notice` enrichment — explains an empty series or a long range that stayed inline, so a legitimate zero-result never reads as a failure
|
|
137
146
|
|
|
138
147
|
---
|
|
139
148
|
|
|
@@ -210,7 +219,7 @@ Or with Docker:
|
|
|
210
219
|
}
|
|
211
220
|
```
|
|
212
221
|
|
|
213
|
-
To enable DataCanvas for long time-series SQL analytics
|
|
222
|
+
To enable DataCanvas for long time-series SQL analytics — which also registers `fx_dataframe_describe` and `fx_dataframe_query`, skipped from `tools/list` otherwise — add `CANVAS_PROVIDER_TYPE=duckdb`:
|
|
214
223
|
|
|
215
224
|
```json
|
|
216
225
|
{
|
|
@@ -276,8 +285,8 @@ All configuration is validated at startup via Zod schemas. Environment variables
|
|
|
276
285
|
| Variable | Description | Default |
|
|
277
286
|
|:---------|:------------|:--------|
|
|
278
287
|
| `FRANKFURTER_BASE_URL` | Frankfurter API base URL. Override for local testing or a self-hosted instance. | `https://api.frankfurter.dev/v1` |
|
|
279
|
-
| `FX_TIMESERIES_CANVAS_THRESHOLD_DAYS` | Day range above which `fx_get_timeseries` spills to DataCanvas
|
|
280
|
-
| `CANVAS_PROVIDER_TYPE` | Canvas engine. Set to `duckdb` to enable DataCanvas for `fx_get_timeseries` long-range spillover. | `none` |
|
|
288
|
+
| `FX_TIMESERIES_CANVAS_THRESHOLD_DAYS` | Day range above which `fx_get_timeseries` spills to DataCanvas, when one is configured. | `90` |
|
|
289
|
+
| `CANVAS_PROVIDER_TYPE` | Canvas engine. Set to `duckdb` to enable DataCanvas for `fx_get_timeseries` long-range spillover and to register the two `fx_dataframe_*` tools. At `none` they are skipped from `tools/list`. | `none` |
|
|
281
290
|
| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
|
|
282
291
|
| `MCP_HTTP_PORT` | Port for HTTP server. | `3010` |
|
|
283
292
|
| `MCP_AUTH_MODE` | Auth mode: `none`, `jwt`, or `oauth`. | `none` |
|