@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
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Adopt @cyanheads/mcp-ts-core ^0.10.9; DataCanvas describe() filter and SQL-gate fixes reach fx_dataframe_* tools; new check-dependency-specifiers devcheck step + plugin-manifest packaging checks"
|
|
3
|
+
breaking: false
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 0.1.5 — 2026-06-20
|
|
8
|
+
|
|
9
|
+
Framework maintenance pass: `@cyanheads/mcp-ts-core` `^0.10.6` → `^0.10.9` plus dependency refresh.
|
|
10
|
+
|
|
11
|
+
## Added
|
|
12
|
+
|
|
13
|
+
- **`check-dependency-specifiers` devcheck step** — vendored `scripts/check-dependency-specifiers.ts`, registered in the devcheck run (flag `--no-dep-specifiers`); hard-fails on floating specifiers (`latest`, `*`, pre-release dist-tags) in `package.json`'s dependency sections and `bun.lock`'s `workspaces` map. Catches a `bun update --latest` writing a literal `latest` dist-tag into the lock that would re-resolve past the manifest range on the next install. ([#246](https://github.com/cyanheads/mcp-ts-core/issues/246))
|
|
14
|
+
- **Plugin-manifest packaging checks** — `devcheck.config.json` gains `packaging.pluginManifests: true`, enabling `lint:packaging` to validate `.claude-plugin/plugin.json` and `.codex-plugin/plugin.json`/`mcp.json`: non-empty descriptions, unscoped machine name in display fields, and the full scoped package name in the `npx -y` install arg. ([#240](https://github.com/cyanheads/mcp-ts-core/issues/240))
|
|
15
|
+
|
|
16
|
+
## Changed
|
|
17
|
+
|
|
18
|
+
- **DataCanvas describe() / SQL gate** — `fx_dataframe_describe` and `fx_dataframe_query` pick up two framework fixes: a `DuckdbProvider.describe({ tableName })` binder error on filtered table lookups is resolved ([#235](https://github.com/cyanheads/mcp-ts-core/issues/235)), and a `SELECT`-shaped query that fails to prepare (mistyped column, unknown function) now throws `ValidationError` with `reason: 'invalid_sql'` and the DuckDB binder detail, instead of the misleading `non_select_statement` ([#236](https://github.com/cyanheads/mcp-ts-core/issues/236)).
|
|
19
|
+
- **Fresh-scaffold devcheck guards** — `scripts/devcheck.ts`, `scripts/build-changelog.ts`, `scripts/check-framework-antipatterns.ts`, and `scripts/check-skill-versions.ts` re-synced with the framework's git-repo and worktree-deletion guards. ([#242](https://github.com/cyanheads/mcp-ts-core/issues/242), [#243](https://github.com/cyanheads/mcp-ts-core/issues/243), [#237](https://github.com/cyanheads/mcp-ts-core/issues/237))
|
|
20
|
+
- **Vendored skills** — 14 `skills/<name>/SKILL.md` re-synced to the installed framework version (`add-tool`, `api-auth`, `api-canvas`, `api-config`, `api-context`, `api-errors`, `api-services`, `api-telemetry`, `field-test`, `git-wrapup`, `orchestrations`, `polish-docs-meta`, `report-issue-local`, `tool-defs-analysis`).
|
|
21
|
+
- **`frankfurter-service.ts`** — outbound `User-Agent` string synced to the current version.
|
|
22
|
+
|
|
23
|
+
### Dependencies
|
|
24
|
+
|
|
25
|
+
- `@cyanheads/mcp-ts-core` ^0.10.6 → ^0.10.9
|
|
26
|
+
- `@duckdb/node-api` ^1.5.3-r.3 → ^1.5.4-r.1
|
|
27
|
+
- `@biomejs/biome` ^2.4.16 → ^2.5.0
|
|
28
|
+
- `@types/node` ^25.9.3 → ^26.0.0
|
|
29
|
+
- `vitest` ^4.1.8 → ^4.1.9
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "fx_convert_currency.amount rejects non-positive values; fx_get_timeseries clips to the requested range and handles same-currency pairs; DataCanvas tools gated off when unconfigured; mcp-ts-core ^0.11.0"
|
|
3
|
+
breaking: true
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 0.2.0 — 2026-07-31
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
- **`fx_get_timeseries` `notice` enrichment** — a success-path `ctx.enrich.notice()` explains an empty `rates` map (the range covers only non-publication days) or a long range that stayed inline because `CANVAS_PROVIDER_TYPE` isn't set, so a legitimate empty/inline result doesn't read as a failure.
|
|
12
|
+
- **`disabledTool()` gating** — `fx_dataframe_describe` and `fx_dataframe_query` are skipped from `tools/list` when DataCanvas isn't configured; they stay visible as disabled cards (hinting `CANVAS_PROVIDER_TYPE=duckdb`) on the HTML landing page and server manifest. ([#7](https://github.com/cyanheads/exchange-rates-mcp-server/issues/7))
|
|
13
|
+
- **Structured Frankfurter service errors** — `src/services/frankfurter/errors.ts` stamps `invalid_date_format`, `unsupported_currency`, and `upstream_no_data` reasons onto `McpError.data`; tool handlers read them via `failureOf()` instead of matching on error-message prose.
|
|
14
|
+
- **`fx_dataframe_query` `missing_table` error contract** — a query against a table not staged on the canvas (or evicted by TTL) now surfaces its own reason instead of falling through to `invalid_query`.
|
|
15
|
+
|
|
16
|
+
## Changed
|
|
17
|
+
|
|
18
|
+
- **`fx_convert_currency.amount` rejects non-positive values** — schema gains `.positive()`; zero and negative amounts are now rejected at parse time instead of silently converting. **Breaking**: a client that relied on converting a zero or negative amount must change its input. ([#5](https://github.com/cyanheads/exchange-rates-mcp-server/issues/5))
|
|
19
|
+
- **Date validation runs before range/currency checks** — `fx_get_rate`, `fx_get_rates`, `fx_get_timeseries`, and `fx_convert_currency` validate `YYYY-MM-DD` shape and calendar validity (new `isIsoDate()`) before any lexicographic range comparison or upstream call, surfacing a new `invalid_date_format` reason instead of misclassifying a malformed date as `unsupported_currency` or `invalid_range`. ([#4](https://github.com/cyanheads/exchange-rates-mcp-server/issues/4))
|
|
20
|
+
- **Same-currency identity handling extended** — `fx_get_rate` (already identity-aware) now dates its `rate: 1` result to the ECB's real publication day for that currency, via a proxy quote, instead of always `today`; `fx_get_timeseries` and `fx_get_rates` gain the same identity path, returning `rate: 1` per publication day or for a base-in-`symbols` request instead of surfacing Frankfurter's upstream 422. ([#6](https://github.com/cyanheads/exchange-rates-mcp-server/issues/6))
|
|
21
|
+
- **`fx_get_rates` misattributed `unsupported_currency`** — an invalid entry in `symbols` now names the offending currency instead of always blaming `base_currency`. ([#8](https://github.com/cyanheads/exchange-rates-mcp-server/issues/8))
|
|
22
|
+
- **`fx_dataframe_query` cancelled queries** — an aborted query (`ctx.signal`) is no longer misclassified as `invalid_query`; it now propagates as the framework's own Timeout instead of matching the SQL-gate's error-message check.
|
|
23
|
+
- **Currency codes checked against a live, day-cached ECB set** (`FrankfurterService.currencies()`) before any request, replacing the old regex format guard — an unsupported code is now named specifically instead of surfacing a generic upstream 404.
|
|
24
|
+
- **Template-lag adoption** — `bunfig.toml` (Socket install scanner + a 3-day `minimumReleaseAge` supply-chain guard, framework excluded), `Dockerfile` (`--ignore-scripts`, `--omit=dev` on the OTel install layer, BuildKit cache mounts, pinned `oven/bun:1.3.14` base images), `.gitignore` (`data/` → `/data/`, root-scoped only), `devcheck.config.json` (scanner added to the depcheck ignore list; `outdated.allowlist` cleared), `.env.example` (documents `CANVAS_TEMP_PATH`), new `.gitattributes`, `.github/SECURITY.md`, `.github/FUNDING.yml`; 19 framework skill files re-synced to the installed `mcp-ts-core` version.
|
|
25
|
+
|
|
26
|
+
## Fixed
|
|
27
|
+
|
|
28
|
+
- **`fx_get_timeseries` no longer returns dates outside the requested range** — Frankfurter's snap-to-prior-business-day on a weekend/holiday-opening range is now clipped: rows outside `[start_date, end_date]` are dropped, and a range covering only non-publication days returns an empty `rates` map with `rate_count: 0` plus an explanatory `notice` instead of a rate from before the requested window. ([#3](https://github.com/cyanheads/exchange-rates-mcp-server/issues/3))
|
|
29
|
+
- **`getTimeSeries()` no longer fabricates a `0` rate** for a day the quoted currency is missing from the upstream response — the row is dropped instead of reading as a real (and wrong) ECB rate.
|
|
30
|
+
|
|
31
|
+
### Dependencies
|
|
32
|
+
|
|
33
|
+
- `@cyanheads/mcp-ts-core` ^0.10.9 → ^0.11.0
|
|
34
|
+
- `@duckdb/node-api` ^1.5.4-r.1 → ^1.5.5-r.1 (0.11.0 raised this optional peer's floor)
|
|
35
|
+
- `typescript` ^6.0.3 → ^7.0.2
|
|
36
|
+
- `@biomejs/biome` ^2.5.0 → 2.5.5 (pinned)
|
|
37
|
+
- `@types/node` ^26.0.0 → 26.1.1 (pinned)
|
|
38
|
+
- `tsc-alias` ^1.8.17 → ^1.9.1
|
|
39
|
+
- `ignore` ^7.0.5 → ^7.0.6
|
|
40
|
+
- `vitest` ^4.1.9 → ^4.1.10
|
|
41
|
+
- new dev dependency: `@socketsecurity/bun-security-scanner` ^1.1.2
|
package/changelog/template.md
CHANGED
|
@@ -15,9 +15,11 @@ summary: ""
|
|
|
15
15
|
# usage. Flagged as `Breaking` in the rollup.
|
|
16
16
|
breaking: false
|
|
17
17
|
|
|
18
|
-
# Set `true`
|
|
19
|
-
#
|
|
20
|
-
#
|
|
18
|
+
# Set `true` ONLY for a security fix in THIS project's own source code — a
|
|
19
|
+
# vulnerability or hardening in code you ship. A dependency or transitive CVE
|
|
20
|
+
# bump is routine maintenance, NOT a security release: record it under
|
|
21
|
+
# `## Dependencies` (with the advisory ID) and leave this `false`. When true,
|
|
22
|
+
# pairs with the `## Security` section below and flags `Security` in the rollup.
|
|
21
23
|
security: false
|
|
22
24
|
|
|
23
25
|
# Optional free-form notes for maintenance agents processing this release.
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,23 @@
|
|
|
3
3
|
* @fileoverview exchange-rates-mcp-server MCP server entry point.
|
|
4
4
|
* @module index
|
|
5
5
|
*/
|
|
6
|
-
import { createApp } from '@cyanheads/mcp-ts-core';
|
|
6
|
+
import { createApp, disabledTool } from '@cyanheads/mcp-ts-core';
|
|
7
|
+
import { config } from '@cyanheads/mcp-ts-core/config';
|
|
7
8
|
import { fxCurrenciesResource, fxRatesLatestResource, } from './mcp-server/resources/definitions/index.js';
|
|
8
9
|
import { fxConvertCurrency, fxDataframeDescribe, fxDataframeQuery, fxGetRate, fxGetRates, fxGetTimeseries, fxListCurrencies, } from './mcp-server/tools/definitions/index.js';
|
|
9
10
|
import { setCanvas } from './services/canvas/canvas-accessor.js';
|
|
11
|
+
/**
|
|
12
|
+
* The dataframe tools are useless without a canvas to read — every call fails
|
|
13
|
+
* with "DataCanvas is not enabled", which is a worse client experience than not
|
|
14
|
+
* advertising them at all. Gate on the synchronously-parsed global config, not
|
|
15
|
+
* on `core.canvas` from `setup()`: this `tools` array is evaluated as part of
|
|
16
|
+
* the `createApp()` argument, so it is fully built before `setup()` ever runs.
|
|
17
|
+
*/
|
|
18
|
+
const canvasEnabled = config.canvas.providerType !== 'none';
|
|
19
|
+
const canvasGate = {
|
|
20
|
+
hint: 'CANVAS_PROVIDER_TYPE=duckdb',
|
|
21
|
+
reason: 'DataCanvas is not configured here, so fx_get_timeseries never stages a table to query.',
|
|
22
|
+
};
|
|
10
23
|
await createApp({
|
|
11
24
|
name: 'exchange-rates-mcp-server',
|
|
12
25
|
title: 'exchange-rates-mcp-server',
|
|
@@ -16,8 +29,8 @@ await createApp({
|
|
|
16
29
|
fxGetRate,
|
|
17
30
|
fxConvertCurrency,
|
|
18
31
|
fxGetTimeseries,
|
|
19
|
-
fxDataframeDescribe,
|
|
20
|
-
fxDataframeQuery,
|
|
32
|
+
canvasEnabled ? fxDataframeDescribe : disabledTool(fxDataframeDescribe, canvasGate),
|
|
33
|
+
canvasEnabled ? fxDataframeQuery : disabledTool(fxDataframeQuery, canvasGate),
|
|
21
34
|
],
|
|
22
35
|
resources: [fxCurrenciesResource, fxRatesLatestResource],
|
|
23
36
|
prompts: [],
|
|
@@ -25,7 +38,8 @@ await createApp({
|
|
|
25
38
|
'- Rates are mid-market ECB reference rates — not tradeable bid/ask.\n' +
|
|
26
39
|
'- Use fx_list_currencies first to disambiguate "dollars" (USD/AUD/CAD/HKD/SGD).\n' +
|
|
27
40
|
'- Cross-rates (e.g. USD→JPY) are triangulated through EUR automatically.\n' +
|
|
28
|
-
'- Long time-series (>90 days) spill to DataCanvas
|
|
41
|
+
'- Long time-series (>90 days) spill to DataCanvas when it is enabled (CANVAS_PROVIDER_TYPE=duckdb);\n' +
|
|
42
|
+
' use fx_dataframe_query for SQL. Without it, long ranges return inline and the dataframe tools are not listed.',
|
|
29
43
|
setup(core) {
|
|
30
44
|
setCanvas(core.canvas);
|
|
31
45
|
},
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AACvD,OAAO,EACL,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,6CAA6C,CAAC;AACrD,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,gBAAgB,EAChB,SAAS,EACT,UAAU,EACV,eAAe,EACf,gBAAgB,GACjB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;AAEjE;;;;;;GAMG;AACH,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,KAAK,MAAM,CAAC;AAC5D,MAAM,UAAU,GAAG;IACjB,IAAI,EAAE,6BAA6B;IACnC,MAAM,EAAE,wFAAwF;CACxF,CAAC;AAEX,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,2BAA2B;IACjC,KAAK,EAAE,2BAA2B;IAClC,KAAK,EAAE;QACL,gBAAgB;QAChB,UAAU;QACV,SAAS;QACT,iBAAiB;QACjB,eAAe;QACf,aAAa,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,YAAY,CAAC,mBAAmB,EAAE,UAAU,CAAC;QACnF,aAAa,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,YAAY,CAAC,gBAAgB,EAAE,UAAU,CAAC;KAC9E;IACD,SAAS,EAAE,CAAC,oBAAoB,EAAE,qBAAqB,CAAC;IACxD,OAAO,EAAE,EAAE;IAEX,YAAY,EACV,4FAA4F;QAC5F,uEAAuE;QACvE,mFAAmF;QACnF,4EAA4E;QAC5E,uGAAuG;QACvG,iHAAiH;IAEnH,KAAK,CAAC,IAAI;QACR,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzB,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fx-rates-latest.resource.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/fx-rates-latest.resource.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAY,CAAC,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"fx-rates-latest.resource.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/fx-rates-latest.resource.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAY,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAKrD,eAAO,MAAM,qBAAqB;;;;;;;;6BA+DhC,CAAC"}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { resource, z } from '@cyanheads/mcp-ts-core';
|
|
6
6
|
import { validationError } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
+
import { failureOf } from '../../../services/frankfurter/errors.js';
|
|
7
8
|
import { getFrankfurterService } from '../../../services/frankfurter/frankfurter-service.js';
|
|
8
9
|
export const fxRatesLatestResource = resource('fx://rates/latest/{base}', {
|
|
9
10
|
name: 'fx-rates-latest',
|
|
@@ -31,9 +32,9 @@ export const fxRatesLatestResource = resource('fx://rates/latest/{base}', {
|
|
|
31
32
|
raw = await service.getRates(params.base, 'latest');
|
|
32
33
|
}
|
|
33
34
|
catch (err) {
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
36
|
-
throw validationError(
|
|
35
|
+
const failure = failureOf(err);
|
|
36
|
+
if (failure?.reason === 'unsupported_currency') {
|
|
37
|
+
throw validationError(`${err.message} Call fx_list_currencies to get valid codes.`, { base: params.base, field: failure.field, reason: failure.reason }, { cause: err });
|
|
37
38
|
}
|
|
38
39
|
throw err;
|
|
39
40
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fx-rates-latest.resource.js","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/fx-rates-latest.resource.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAC;AAEtF,MAAM,CAAC,MAAM,qBAAqB,GAAG,QAAQ,CAAC,0BAA0B,EAAE;IACxE,IAAI,EAAE,iBAAiB;IACvB,WAAW,EACT,sEAAsE;QACtE,iFAAiF;IACnF,QAAQ,EAAE,kBAAkB;IAC5B,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,mDAAmD,CAAC;KAC/E,CAAC;IACF,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,yBAAyB,CAAC;QAC7D,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,2BAA2B,CAAC;QAC3D,KAAK,EAAE,CAAC;aACL,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;aAC9B,QAAQ,CACP,0FAA0F,CAC3F;QACH,SAAS,EAAE,CAAC;aACT,MAAM,EAAE;aACR,QAAQ,CACP,yFAAyF,CAC1F;QACH,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,4DAA4D,CAAC;KAC1F,CAAC;IAEF,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG;QACvB,MAAM,OAAO,GAAG,qBAAqB,EAAE,CAAC;QACxC,IAAI,GAAiD,CAAC;QACtD,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QACtD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,GAAG,
|
|
1
|
+
{"version":3,"file":"fx-rates-latest.resource.js","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/fx-rates-latest.resource.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,6 +20,11 @@ export declare const fxConvertCurrency: import("@cyanheads/mcp-ts-core").ToolDef
|
|
|
20
20
|
rate_type: z.ZodString;
|
|
21
21
|
source: z.ZodString;
|
|
22
22
|
}, z.core.$strip>, readonly [{
|
|
23
|
+
readonly reason: "invalid_date_format";
|
|
24
|
+
readonly code: JsonRpcErrorCode.ValidationError;
|
|
25
|
+
readonly when: "date is not a real calendar date written as YYYY-MM-DD.";
|
|
26
|
+
readonly recovery: "Pass an ISO 8601 calendar date in YYYY-MM-DD form, for example 2024-06-01.";
|
|
27
|
+
}, {
|
|
23
28
|
readonly reason: "unsupported_currency";
|
|
24
29
|
readonly code: JsonRpcErrorCode.ValidationError;
|
|
25
30
|
readonly when: "base_currency or quote_currency is not in the ECB currency set.";
|
|
@@ -29,5 +34,10 @@ export declare const fxConvertCurrency: import("@cyanheads/mcp-ts-core").ToolDef
|
|
|
29
34
|
readonly code: JsonRpcErrorCode.ValidationError;
|
|
30
35
|
readonly when: "date is before 1999-01-04 or in the future.";
|
|
31
36
|
readonly recovery: "ECB data starts 1999-01-04; omit date for the latest available rate.";
|
|
37
|
+
}, {
|
|
38
|
+
readonly reason: "upstream_no_data";
|
|
39
|
+
readonly code: JsonRpcErrorCode.NotFound;
|
|
40
|
+
readonly when: "Both currencies are supported but the ECB published no rate for this date.";
|
|
41
|
+
readonly recovery: "Try a more recent date — ECB history starts later for some currencies.";
|
|
32
42
|
}], undefined>;
|
|
33
43
|
//# sourceMappingURL=fx-convert-currency.tool.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fx-convert-currency.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-convert-currency.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"fx-convert-currency.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-convert-currency.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AASjE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAmL5B,CAAC"}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
6
6
|
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
-
import {
|
|
7
|
+
import { failureOf } from '../../../services/frankfurter/errors.js';
|
|
8
|
+
import { ECB_START_DATE, getFrankfurterService, isIsoDate, } from '../../../services/frankfurter/frankfurter-service.js';
|
|
8
9
|
export const fxConvertCurrency = tool('fx_convert_currency', {
|
|
9
10
|
description: 'Convert an amount between any two currencies at the latest or a historical rate. ' +
|
|
10
11
|
'Returns the converted amount, the rate used, the actual rate date, and whether the ' +
|
|
@@ -24,7 +25,8 @@ export const fxConvertCurrency = tool('fx_convert_currency', {
|
|
|
24
25
|
.describe('ISO 4217 target currency code (e.g. EUR). The amount will be expressed in this currency.'),
|
|
25
26
|
amount: z
|
|
26
27
|
.number()
|
|
27
|
-
.
|
|
28
|
+
.positive()
|
|
29
|
+
.describe('Amount in the base currency to convert. Must be greater than zero.'),
|
|
28
30
|
date: z
|
|
29
31
|
.string()
|
|
30
32
|
.optional()
|
|
@@ -52,6 +54,12 @@ export const fxConvertCurrency = tool('fx_convert_currency', {
|
|
|
52
54
|
source: z.string().describe('Always "ECB via Frankfurter" — the upstream data provider.'),
|
|
53
55
|
}),
|
|
54
56
|
errors: [
|
|
57
|
+
{
|
|
58
|
+
reason: 'invalid_date_format',
|
|
59
|
+
code: JsonRpcErrorCode.ValidationError,
|
|
60
|
+
when: 'date is not a real calendar date written as YYYY-MM-DD.',
|
|
61
|
+
recovery: 'Pass an ISO 8601 calendar date in YYYY-MM-DD form, for example 2024-06-01.',
|
|
62
|
+
},
|
|
55
63
|
{
|
|
56
64
|
reason: 'unsupported_currency',
|
|
57
65
|
code: JsonRpcErrorCode.ValidationError,
|
|
@@ -64,11 +72,21 @@ export const fxConvertCurrency = tool('fx_convert_currency', {
|
|
|
64
72
|
when: 'date is before 1999-01-04 or in the future.',
|
|
65
73
|
recovery: 'ECB data starts 1999-01-04; omit date for the latest available rate.',
|
|
66
74
|
},
|
|
75
|
+
{
|
|
76
|
+
reason: 'upstream_no_data',
|
|
77
|
+
code: JsonRpcErrorCode.NotFound,
|
|
78
|
+
when: 'Both currencies are supported but the ECB published no rate for this date.',
|
|
79
|
+
recovery: 'Try a more recent date — ECB history starts later for some currencies.',
|
|
80
|
+
},
|
|
67
81
|
],
|
|
68
82
|
async handler(input, ctx) {
|
|
69
83
|
const service = getFrankfurterService();
|
|
70
84
|
const date = input.date ?? 'latest';
|
|
71
85
|
if (date !== 'latest') {
|
|
86
|
+
/** Format first — the range comparisons below are lexicographic and would misread a malformed date. */
|
|
87
|
+
if (!isIsoDate(date)) {
|
|
88
|
+
throw ctx.fail('invalid_date_format', `date "${date}" is not a valid YYYY-MM-DD calendar date.`, { ...ctx.recoveryFor('invalid_date_format'), field: 'date' });
|
|
89
|
+
}
|
|
72
90
|
if (date < ECB_START_DATE) {
|
|
73
91
|
throw ctx.fail('date_out_of_range', `Date ${date} is before ECB data start ${ECB_START_DATE}.`, {
|
|
74
92
|
...ctx.recoveryFor('date_out_of_range'),
|
|
@@ -86,9 +104,15 @@ export const fxConvertCurrency = tool('fx_convert_currency', {
|
|
|
86
104
|
resolved = await service.getRate(input.base_currency, input.quote_currency, date);
|
|
87
105
|
}
|
|
88
106
|
catch (err) {
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
throw ctx.fail('unsupported_currency',
|
|
107
|
+
const failure = failureOf(err);
|
|
108
|
+
if (failure?.reason === 'unsupported_currency') {
|
|
109
|
+
throw ctx.fail('unsupported_currency', err.message, {
|
|
110
|
+
...ctx.recoveryFor('unsupported_currency'),
|
|
111
|
+
field: failure.field,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (failure?.reason === 'upstream_no_data') {
|
|
115
|
+
throw ctx.fail('upstream_no_data', `The ECB published no ${input.base_currency}/${input.quote_currency} rate for ${date === 'latest' ? 'the latest business day' : date}.`, { ...ctx.recoveryFor('upstream_no_data') });
|
|
92
116
|
}
|
|
93
117
|
throw err;
|
|
94
118
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fx-convert-currency.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-convert-currency.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EACL,cAAc,EACd,qBAAqB,
|
|
1
|
+
{"version":3,"file":"fx-convert-currency.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-convert-currency.tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fx-dataframe-describe.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-dataframe-describe.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"fx-dataframe-describe.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-dataframe-describe.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,gBAAgB,EAAgC,MAAM,+BAA+B,CAAC;AAG/F,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;cAsH9B,CAAC"}
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
* @module mcp-server/tools/definitions/fx-dataframe-describe.tool
|
|
4
4
|
*/
|
|
5
5
|
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
6
|
-
import { JsonRpcErrorCode, serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
|
|
6
|
+
import { JsonRpcErrorCode, McpError, serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
|
|
7
7
|
import { getCanvas } from '../../../services/canvas/canvas-accessor.js';
|
|
8
8
|
export const fxDataframeDescribe = tool('fx_dataframe_describe', {
|
|
9
9
|
description: 'List tables and columns staged on a DataCanvas from a prior fx_get_timeseries call. ' +
|
|
10
|
-
'Required first step before fx_dataframe_query — use it to discover table names and column schemas.'
|
|
10
|
+
'Required first step before fx_dataframe_query — use it to discover table names and column schemas. ' +
|
|
11
|
+
'Requires DataCanvas (CANVAS_PROVIDER_TYPE=duckdb) — without it this tool is not listed at all ' +
|
|
12
|
+
'and fx_get_timeseries returns every range inline.',
|
|
11
13
|
annotations: {
|
|
12
14
|
readOnlyHint: true,
|
|
13
15
|
idempotentHint: true,
|
|
@@ -59,8 +61,9 @@ export const fxDataframeDescribe = tool('fx_dataframe_describe', {
|
|
|
59
61
|
instance = await canvas.acquire(input.canvas_id, ctx);
|
|
60
62
|
}
|
|
61
63
|
catch (err) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
/** The canvas layer stamps a contract `reason` on its own throws — read that, don't match prose. */
|
|
65
|
+
const reason = err instanceof McpError ? err.data?.reason : undefined;
|
|
66
|
+
if (reason === 'canvas_not_found') {
|
|
64
67
|
throw ctx.fail('canvas_not_found', `Canvas "${input.canvas_id}" not found or has expired.`, {
|
|
65
68
|
...ctx.recoveryFor('canvas_not_found'),
|
|
66
69
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fx-dataframe-describe.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-dataframe-describe.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"fx-dataframe-describe.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-dataframe-describe.tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,10 +17,15 @@ export declare const fxDataframeQuery: import("@cyanheads/mcp-ts-core").ToolDefi
|
|
|
17
17
|
readonly code: JsonRpcErrorCode.NotFound;
|
|
18
18
|
readonly when: "canvas_id does not exist or has been evicted.";
|
|
19
19
|
readonly recovery: "Re-run fx_get_timeseries to obtain a fresh canvas_id.";
|
|
20
|
+
}, {
|
|
21
|
+
readonly reason: "missing_table";
|
|
22
|
+
readonly code: JsonRpcErrorCode.NotFound;
|
|
23
|
+
readonly when: "The SQL references a table that is not staged on this canvas, or whose TTL expired.";
|
|
24
|
+
readonly recovery: "Call fx_dataframe_describe to list the staged tables, or re-run fx_get_timeseries.";
|
|
20
25
|
}, {
|
|
21
26
|
readonly reason: "invalid_query";
|
|
22
27
|
readonly code: JsonRpcErrorCode.ValidationError;
|
|
23
|
-
readonly when: "SQL is not a SELECT, references unknown
|
|
28
|
+
readonly when: "SQL is not a SELECT, references unknown columns, or has a syntax error.";
|
|
24
29
|
readonly recovery: "Run fx_dataframe_describe first to verify table and column names.";
|
|
25
30
|
}], undefined>;
|
|
26
31
|
//# sourceMappingURL=fx-dataframe-query.tool.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fx-dataframe-query.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-dataframe-query.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"fx-dataframe-query.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-dataframe-query.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,gBAAgB,EAAgC,MAAM,+BAA+B,CAAC;AAG/F,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;cAyL3B,CAAC"}
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
* @module mcp-server/tools/definitions/fx-dataframe-query.tool
|
|
4
4
|
*/
|
|
5
5
|
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
6
|
-
import { JsonRpcErrorCode, serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
|
|
6
|
+
import { JsonRpcErrorCode, McpError, serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
|
|
7
7
|
import { getCanvas } from '../../../services/canvas/canvas-accessor.js';
|
|
8
8
|
export const fxDataframeQuery = tool('fx_dataframe_query', {
|
|
9
9
|
description: 'Run a read-only SQL SELECT against DataCanvas tables staged by fx_get_timeseries. ' +
|
|
10
10
|
'Supports aggregations, GROUP BY, window functions, and JOINs across multiple registered tables. ' +
|
|
11
|
-
'Run fx_dataframe_describe first to discover table names and column schemas.'
|
|
11
|
+
'Run fx_dataframe_describe first to discover table names and column schemas. ' +
|
|
12
|
+
'Requires DataCanvas (CANVAS_PROVIDER_TYPE=duckdb) — without it this tool is not listed at all ' +
|
|
13
|
+
'and fx_get_timeseries returns every range inline.',
|
|
12
14
|
annotations: {
|
|
13
15
|
readOnlyHint: true,
|
|
14
16
|
idempotentHint: true,
|
|
@@ -50,10 +52,16 @@ export const fxDataframeQuery = tool('fx_dataframe_query', {
|
|
|
50
52
|
when: 'canvas_id does not exist or has been evicted.',
|
|
51
53
|
recovery: 'Re-run fx_get_timeseries to obtain a fresh canvas_id.',
|
|
52
54
|
},
|
|
55
|
+
{
|
|
56
|
+
reason: 'missing_table',
|
|
57
|
+
code: JsonRpcErrorCode.NotFound,
|
|
58
|
+
when: 'The SQL references a table that is not staged on this canvas, or whose TTL expired.',
|
|
59
|
+
recovery: 'Call fx_dataframe_describe to list the staged tables, or re-run fx_get_timeseries.',
|
|
60
|
+
},
|
|
53
61
|
{
|
|
54
62
|
reason: 'invalid_query',
|
|
55
63
|
code: JsonRpcErrorCode.ValidationError,
|
|
56
|
-
when: 'SQL is not a SELECT, references unknown
|
|
64
|
+
when: 'SQL is not a SELECT, references unknown columns, or has a syntax error.',
|
|
57
65
|
recovery: 'Run fx_dataframe_describe first to verify table and column names.',
|
|
58
66
|
},
|
|
59
67
|
],
|
|
@@ -62,13 +70,14 @@ export const fxDataframeQuery = tool('fx_dataframe_query', {
|
|
|
62
70
|
if (!canvas) {
|
|
63
71
|
throw serviceUnavailable('DataCanvas is not enabled. Set CANVAS_PROVIDER_TYPE=duckdb.');
|
|
64
72
|
}
|
|
73
|
+
/** The canvas layer stamps a contract `reason` on its own throws — read that, don't match prose. */
|
|
74
|
+
const reasonOf = (err) => err instanceof McpError ? err.data?.reason : undefined;
|
|
65
75
|
let instance;
|
|
66
76
|
try {
|
|
67
77
|
instance = await canvas.acquire(input.canvas_id, ctx);
|
|
68
78
|
}
|
|
69
79
|
catch (err) {
|
|
70
|
-
|
|
71
|
-
if (msg.includes('not found') || msg.includes('NotFound')) {
|
|
80
|
+
if (reasonOf(err) === 'canvas_not_found') {
|
|
72
81
|
throw ctx.fail('canvas_not_found', `Canvas "${input.canvas_id}" not found or has expired.`, {
|
|
73
82
|
...ctx.recoveryFor('canvas_not_found'),
|
|
74
83
|
});
|
|
@@ -80,15 +89,26 @@ export const fxDataframeQuery = tool('fx_dataframe_query', {
|
|
|
80
89
|
result = await instance.query(input.query, { signal: ctx.signal });
|
|
81
90
|
}
|
|
82
91
|
catch (err) {
|
|
92
|
+
const reason = reasonOf(err);
|
|
93
|
+
if (reason === 'canvas_not_found') {
|
|
94
|
+
throw ctx.fail('canvas_not_found', `Canvas "${input.canvas_id}" not found or has expired.`, {
|
|
95
|
+
...ctx.recoveryFor('canvas_not_found'),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (reason === 'missing_table') {
|
|
99
|
+
throw ctx.fail('missing_table', err.message, {
|
|
100
|
+
...ctx.recoveryFor('missing_table'),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** An aborted query is a Timeout, not a rejected statement — don't blame the caller's SQL for it. */
|
|
104
|
+
if (reason === 'cancelled')
|
|
105
|
+
throw err;
|
|
83
106
|
const msg = err.message ?? '';
|
|
84
|
-
//
|
|
85
|
-
if (
|
|
107
|
+
// Remaining SQL-gate rejections: non-SELECT statements, binder and syntax errors.
|
|
108
|
+
if (reason !== undefined ||
|
|
86
109
|
msg.includes('SELECT') ||
|
|
87
110
|
msg.includes('syntax') ||
|
|
88
|
-
msg.includes('not found') ||
|
|
89
111
|
msg.includes('does not exist') ||
|
|
90
|
-
msg.includes('read_csv') ||
|
|
91
|
-
msg.includes('register_as_clash') ||
|
|
92
112
|
err.code === 'ValidationError') {
|
|
93
113
|
throw ctx.fail('invalid_query', `SQL query rejected: ${msg}`, {
|
|
94
114
|
...ctx.recoveryFor('invalid_query'),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fx-dataframe-query.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-dataframe-query.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"fx-dataframe-query.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-dataframe-query.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAC/F,OAAO,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;AAEjE,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,oBAAoB,EAAE;IACzD,WAAW,EACT,oFAAoF;QACpF,kGAAkG;QAClG,8EAA8E;QAC9E,gGAAgG;QAChG,mDAAmD;IACrD,WAAW,EAAE;QACX,YAAY,EAAE,IAAI;QAClB,cAAc,EAAE,IAAI;QACpB,aAAa,EAAE,KAAK;KACrB;IACD,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC;QACd,SAAS,EAAE,CAAC;aACT,MAAM,EAAE;aACR,QAAQ,CACP,2CAA2C;YACzC,+EAA+E,CAClF;QACH,KAAK,EAAE,CAAC;aACL,MAAM,EAAE;aACR,QAAQ,CACP,kGAAkG;YAChG,kDAAkD;YAClD,oFAAoF,CACvF;KACJ,CAAC;IACF,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,IAAI,EAAE,CAAC;aACJ,KAAK,CACJ,CAAC;aACE,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;aAC/B,QAAQ,CAAC,yEAAyE,CAAC,CACvF;aACA,QAAQ,CACP,yGAAyG,CAC1G;QACH,SAAS,EAAE,CAAC;aACT,MAAM,EAAE;aACR,QAAQ,CACP,gGAAgG;YAC9F,6FAA6F,CAChG;QACH,SAAS,EAAE,CAAC;aACT,OAAO,EAAE;aACT,QAAQ,CACP,4FAA4F;YAC1F,sDAAsD,CACzD;QACH,SAAS,EAAE,CAAC;aACT,MAAM,EAAE;aACR,QAAQ,CACP,6FAA6F,CAC9F;KACJ,CAAC;IAEF,MAAM,EAAE;QACN;YACE,MAAM,EAAE,kBAAkB;YAC1B,IAAI,EAAE,gBAAgB,CAAC,QAAQ;YAC/B,IAAI,EAAE,+CAA+C;YACrD,QAAQ,EAAE,uDAAuD;SAClE;QACD;YACE,MAAM,EAAE,eAAe;YACvB,IAAI,EAAE,gBAAgB,CAAC,QAAQ;YAC/B,IAAI,EAAE,qFAAqF;YAC3F,QAAQ,EACN,oFAAoF;SACvF;QACD;YACE,MAAM,EAAE,eAAe;YACvB,IAAI,EAAE,gBAAgB,CAAC,eAAe;YACtC,IAAI,EAAE,yEAAyE;YAC/E,QAAQ,EAAE,mEAAmE;SAC9E;KACF;IAED,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG;QACtB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;QAC3B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,kBAAkB,CAAC,6DAA6D,CAAC,CAAC;QAC1F,CAAC;QAED,oGAAoG;QACpG,MAAM,QAAQ,GAAG,CAAC,GAAY,EAAsB,EAAE,CACpD,GAAG,YAAY,QAAQ,CAAC,CAAC,CAAE,GAAG,CAAC,IAAwC,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;QAE9F,IAAI,QAAwB,CAAC;QAC7B,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QACxD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,kBAAkB,EAAE,CAAC;gBACzC,MAAM,GAAG,CAAC,IAAI,CACZ,kBAAkB,EAClB,WAAW,KAAK,CAAC,SAAS,6BAA6B,EACvD;oBACE,GAAG,GAAG,CAAC,WAAW,CAAC,kBAAkB,CAAC;iBACvC,CACF,CAAC;YACJ,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;QAED,IAAI,MAAmB,CAAC;QACxB,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QACrE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;YAC7B,IAAI,MAAM,KAAK,kBAAkB,EAAE,CAAC;gBAClC,MAAM,GAAG,CAAC,IAAI,CACZ,kBAAkB,EAClB,WAAW,KAAK,CAAC,SAAS,6BAA6B,EACvD;oBACE,GAAG,GAAG,CAAC,WAAW,CAAC,kBAAkB,CAAC;iBACvC,CACF,CAAC;YACJ,CAAC;YACD,IAAI,MAAM,KAAK,eAAe,EAAE,CAAC;gBAC/B,MAAM,GAAG,CAAC,IAAI,CAAC,eAAe,EAAG,GAAa,CAAC,OAAO,EAAE;oBACtD,GAAG,GAAG,CAAC,WAAW,CAAC,eAAe,CAAC;iBACpC,CAAC,CAAC;YACL,CAAC;YACD,qGAAqG;YACrG,IAAI,MAAM,KAAK,WAAW;gBAAE,MAAM,GAAG,CAAC;YACtC,MAAM,GAAG,GAAI,GAAa,CAAC,OAAO,IAAI,EAAE,CAAC;YACzC,kFAAkF;YAClF,IACE,MAAM,KAAK,SAAS;gBACpB,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBACtB,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBACtB,GAAG,CAAC,QAAQ,CAAC,gBAAgB,CAAC;gBAC7B,GAAyB,CAAC,IAAI,KAAK,iBAAiB,EACrD,CAAC;gBACD,MAAM,GAAG,CAAC,IAAI,CAAC,eAAe,EAAE,uBAAuB,GAAG,EAAE,EAAE;oBAC5D,GAAG,GAAG,CAAC,WAAW,CAAC,eAAe,CAAC;iBACpC,CAAC,CAAC;YACL,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;QAED,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,0BAA0B,EAAE;YACvC,QAAQ,EAAE,KAAK,CAAC,SAAS;YACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,KAAK;SACrC,CAAC,CAAC;QAEH,OAAO;YACL,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,SAAS,EAAE,MAAM,CAAC,QAAQ;YAC1B,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,KAAK;YACpC,SAAS,EAAE,KAAK,CAAC,SAAS;SAC3B,CAAC;IACJ,CAAC;IAED,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE;QACjB,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS;YAC9B,CAAC,CAAC,kHAAkH;YACpH,CAAC,CAAC,mBAAmB,CAAC;QACxB,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7B,OAAO;gBACL;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,mCAAmC,MAAM,CAAC,SAAS,OAAO,OAAO,EAAE;iBAC1E;aACF,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAG,KAAK,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;QACzC,MAAM,GAAG,GAAG,KAAK,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;QACvD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI;aACzB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;aACZ,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACxE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,SAAS,YAAY,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;QACzF,MAAM,IAAI,GACR,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,cAAc,KAAK,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC;QAC3F,MAAM,KAAK,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpD,OAAO;YACL;gBACE,IAAI,EAAE,MAAM;gBACZ,IAAI,EAAE,GAAG,KAAK,GAAG,IAAI,eAAe,MAAM,CAAC,SAAS,KAAK,OAAO,EAAE;aACnE;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -17,6 +17,11 @@ export declare const fxGetRate: import("@cyanheads/mcp-ts-core").ToolDefinition<
|
|
|
17
17
|
rate_type: z.ZodString;
|
|
18
18
|
source: z.ZodString;
|
|
19
19
|
}, z.core.$strip>, readonly [{
|
|
20
|
+
readonly reason: "invalid_date_format";
|
|
21
|
+
readonly code: JsonRpcErrorCode.ValidationError;
|
|
22
|
+
readonly when: "date is not a real calendar date written as YYYY-MM-DD.";
|
|
23
|
+
readonly recovery: "Pass an ISO 8601 calendar date in YYYY-MM-DD form, for example 2024-06-01.";
|
|
24
|
+
}, {
|
|
20
25
|
readonly reason: "unsupported_currency";
|
|
21
26
|
readonly code: JsonRpcErrorCode.ValidationError;
|
|
22
27
|
readonly when: "base_currency or quote_currency is not in the ECB currency set.";
|
|
@@ -26,5 +31,10 @@ export declare const fxGetRate: import("@cyanheads/mcp-ts-core").ToolDefinition<
|
|
|
26
31
|
readonly code: JsonRpcErrorCode.ValidationError;
|
|
27
32
|
readonly when: "date is before 1999-01-04 or in the future.";
|
|
28
33
|
readonly recovery: "ECB data starts 1999-01-04; omit date for the latest available rate.";
|
|
34
|
+
}, {
|
|
35
|
+
readonly reason: "upstream_no_data";
|
|
36
|
+
readonly code: JsonRpcErrorCode.NotFound;
|
|
37
|
+
readonly when: "Both currencies are supported but the ECB published no rate for this date.";
|
|
38
|
+
readonly recovery: "Try a more recent date — ECB history starts later for some currencies.";
|
|
29
39
|
}], undefined>;
|
|
30
40
|
//# sourceMappingURL=fx-get-rate.tool.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fx-get-rate.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-get-rate.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"fx-get-rate.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-get-rate.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AASjE,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAuKpB,CAAC"}
|
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
6
6
|
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
-
import {
|
|
7
|
+
import { failureOf } from '../../../services/frankfurter/errors.js';
|
|
8
|
+
import { ECB_START_DATE, getFrankfurterService, isIsoDate, } from '../../../services/frankfurter/frankfurter-service.js';
|
|
8
9
|
export const fxGetRate = tool('fx_get_rate', {
|
|
9
10
|
description: 'Get the exchange rate for a currency pair on a given date (default: latest). ' +
|
|
10
11
|
'Returns the rate, the actual rate date (which may differ from the requested date on ' +
|
|
11
12
|
'weekends/holidays — ECB publishes business days only), and source provenance. ' +
|
|
12
|
-
'Cross-rates are triangulated through EUR automatically. ' +
|
|
13
|
+
'Cross-rates are triangulated through EUR automatically. A same-currency pair returns a rate of 1, ' +
|
|
14
|
+
'dated to the same publication day any other pair would report for that date. ' +
|
|
13
15
|
'Use fx_convert_currency when you want the converted amount; use this tool when you only need the rate number.',
|
|
14
16
|
annotations: {
|
|
15
17
|
readOnlyHint: true,
|
|
@@ -46,6 +48,12 @@ export const fxGetRate = tool('fx_get_rate', {
|
|
|
46
48
|
source: z.string().describe('Always "ECB via Frankfurter" — the upstream data provider.'),
|
|
47
49
|
}),
|
|
48
50
|
errors: [
|
|
51
|
+
{
|
|
52
|
+
reason: 'invalid_date_format',
|
|
53
|
+
code: JsonRpcErrorCode.ValidationError,
|
|
54
|
+
when: 'date is not a real calendar date written as YYYY-MM-DD.',
|
|
55
|
+
recovery: 'Pass an ISO 8601 calendar date in YYYY-MM-DD form, for example 2024-06-01.',
|
|
56
|
+
},
|
|
49
57
|
{
|
|
50
58
|
reason: 'unsupported_currency',
|
|
51
59
|
code: JsonRpcErrorCode.ValidationError,
|
|
@@ -58,11 +66,21 @@ export const fxGetRate = tool('fx_get_rate', {
|
|
|
58
66
|
when: 'date is before 1999-01-04 or in the future.',
|
|
59
67
|
recovery: 'ECB data starts 1999-01-04; omit date for the latest available rate.',
|
|
60
68
|
},
|
|
69
|
+
{
|
|
70
|
+
reason: 'upstream_no_data',
|
|
71
|
+
code: JsonRpcErrorCode.NotFound,
|
|
72
|
+
when: 'Both currencies are supported but the ECB published no rate for this date.',
|
|
73
|
+
recovery: 'Try a more recent date — ECB history starts later for some currencies.',
|
|
74
|
+
},
|
|
61
75
|
],
|
|
62
76
|
async handler(input, ctx) {
|
|
63
77
|
const service = getFrankfurterService();
|
|
64
78
|
const date = input.date ?? 'latest';
|
|
65
79
|
if (date !== 'latest') {
|
|
80
|
+
/** Format first — the range comparisons below are lexicographic and would misread a malformed date. */
|
|
81
|
+
if (!isIsoDate(date)) {
|
|
82
|
+
throw ctx.fail('invalid_date_format', `date "${date}" is not a valid YYYY-MM-DD calendar date.`, { ...ctx.recoveryFor('invalid_date_format'), field: 'date' });
|
|
83
|
+
}
|
|
66
84
|
if (date < ECB_START_DATE) {
|
|
67
85
|
throw ctx.fail('date_out_of_range', `Date ${date} is before ECB data start ${ECB_START_DATE}.`, {
|
|
68
86
|
...ctx.recoveryFor('date_out_of_range'),
|
|
@@ -80,9 +98,15 @@ export const fxGetRate = tool('fx_get_rate', {
|
|
|
80
98
|
resolved = await service.getRate(input.base_currency, input.quote_currency, date);
|
|
81
99
|
}
|
|
82
100
|
catch (err) {
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
throw ctx.fail('unsupported_currency',
|
|
101
|
+
const failure = failureOf(err);
|
|
102
|
+
if (failure?.reason === 'unsupported_currency') {
|
|
103
|
+
throw ctx.fail('unsupported_currency', err.message, {
|
|
104
|
+
...ctx.recoveryFor('unsupported_currency'),
|
|
105
|
+
field: failure.field,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (failure?.reason === 'upstream_no_data') {
|
|
109
|
+
throw ctx.fail('upstream_no_data', `The ECB published no ${input.base_currency}/${input.quote_currency} rate for ${date === 'latest' ? 'the latest business day' : date}.`, { ...ctx.recoveryFor('upstream_no_data') });
|
|
86
110
|
}
|
|
87
111
|
throw err;
|
|
88
112
|
}
|