@cyanheads/exchange-rates-mcp-server 0.3.2 → 0.4.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 +5 -3
- package/CLAUDE.md +5 -3
- package/README.md +11 -7
- package/changelog/0.4.x/0.4.0.md +22 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/tools/definitions/fx-dataframe-query.tool.d.ts +4 -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 +52 -19
- package/dist/mcp-server/tools/definitions/fx-dataframe-query.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/fx-get-timeseries.tool.d.ts +2 -0
- 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 +113 -81
- package/dist/mcp-server/tools/definitions/fx-get-timeseries.tool.js.map +1 -1
- package/dist/utils/escape-markdown-table-cell.d.ts +11 -0
- package/dist/utils/escape-markdown-table-cell.d.ts.map +1 -0
- package/dist/utils/escape-markdown-table-cell.js +19 -0
- package/dist/utils/escape-markdown-table-cell.js.map +1 -0
- package/package.json +1 -1
- package/server.json +3 -3
package/AGENTS.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** exchange-rates-mcp-server
|
|
4
|
-
**Version:** 0.
|
|
4
|
+
**Version:** 0.4.0
|
|
5
5
|
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `0.12.8`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
7
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.0.0
|
|
@@ -246,14 +246,16 @@ src/
|
|
|
246
246
|
fx-convert-currency.tool.ts # Convert amount between two currencies
|
|
247
247
|
fx-get-rate.tool.ts # Exchange rate for a currency pair
|
|
248
248
|
fx-get-rates.tool.ts # All rates for a base currency snapshot
|
|
249
|
-
fx-get-timeseries.tool.ts # Historical daily rates; spills to DataCanvas for long ranges
|
|
249
|
+
fx-get-timeseries.tool.ts # Historical daily rates; inline pages of 500 days, spills to DataCanvas for long ranges
|
|
250
250
|
fx-list-currencies.tool.ts # All supported ISO 4217 currencies
|
|
251
251
|
fx-dataframe-describe.tool.ts # List DataCanvas tables from a prior timeseries call
|
|
252
|
-
fx-dataframe-query.tool.ts # SQL SELECT against DataCanvas tables
|
|
252
|
+
fx-dataframe-query.tool.ts # SQL SELECT against DataCanvas tables, bounded by row_limit
|
|
253
253
|
fx-dataframe-drop.tool.ts # Remove one staged table; opt-in via FX_ENABLE_CANVAS_DROP
|
|
254
254
|
resources/definitions/
|
|
255
255
|
fx-currencies.resource.ts # fx://currencies — stable currencies reference document
|
|
256
256
|
fx-rates-latest.resource.ts # fx://rates/latest/{base} — latest rates snapshot
|
|
257
|
+
utils/
|
|
258
|
+
escape-markdown-table-cell.ts # Escapes a value for one GFM table cell (fx_dataframe_query format())
|
|
257
259
|
```
|
|
258
260
|
|
|
259
261
|
---
|
package/CLAUDE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** exchange-rates-mcp-server
|
|
4
|
-
**Version:** 0.
|
|
4
|
+
**Version:** 0.4.0
|
|
5
5
|
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `0.12.8`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
7
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.0.0
|
|
@@ -246,14 +246,16 @@ src/
|
|
|
246
246
|
fx-convert-currency.tool.ts # Convert amount between two currencies
|
|
247
247
|
fx-get-rate.tool.ts # Exchange rate for a currency pair
|
|
248
248
|
fx-get-rates.tool.ts # All rates for a base currency snapshot
|
|
249
|
-
fx-get-timeseries.tool.ts # Historical daily rates; spills to DataCanvas for long ranges
|
|
249
|
+
fx-get-timeseries.tool.ts # Historical daily rates; inline pages of 500 days, spills to DataCanvas for long ranges
|
|
250
250
|
fx-list-currencies.tool.ts # All supported ISO 4217 currencies
|
|
251
251
|
fx-dataframe-describe.tool.ts # List DataCanvas tables from a prior timeseries call
|
|
252
|
-
fx-dataframe-query.tool.ts # SQL SELECT against DataCanvas tables
|
|
252
|
+
fx-dataframe-query.tool.ts # SQL SELECT against DataCanvas tables, bounded by row_limit
|
|
253
253
|
fx-dataframe-drop.tool.ts # Remove one staged table; opt-in via FX_ENABLE_CANVAS_DROP
|
|
254
254
|
resources/definitions/
|
|
255
255
|
fx-currencies.resource.ts # fx://currencies — stable currencies reference document
|
|
256
256
|
fx-rates-latest.resource.ts # fx://rates/latest/{base} — latest rates snapshot
|
|
257
|
+
utils/
|
|
258
|
+
escape-markdown-table-cell.ts # Escapes a value for one GFM table cell (fx_dataframe_query format())
|
|
257
259
|
```
|
|
258
260
|
|
|
259
261
|
---
|
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
|
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
Eight 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. Five are advertised by default; the three `fx_dataframe_*` tools need `CANVAS_PROVIDER_TYPE=duckdb`, and the destructive one among them additionally needs `FX_ENABLE_CANVAS_DROP=true`.
|
|
33
33
|
|
|
34
|
-
The three `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:
|
|
34
|
+
The three `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, paged at 500 publication days:
|
|
35
35
|
|
|
36
36
|
| Tool | Description |
|
|
37
37
|
|:-----|:------------|
|
|
@@ -39,9 +39,9 @@ The three `fx_dataframe_*` tools require DataCanvas. With `CANVAS_PROVIDER_TYPE`
|
|
|
39
39
|
| `fx_get_rates` | Snapshot of all available rates for a base currency at latest or a historical date. Surfaces `date_snapped` when a weekend/holiday request returns the prior business-day snapshot. Optional `symbols` filter for smaller responses; listing the base itself returns a rate of 1 for it. |
|
|
40
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. |
|
|
41
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. |
|
|
42
|
-
| `fx_get_timeseries` | Historical daily rates for a currency pair over a date range, never including a date outside it.
|
|
42
|
+
| `fx_get_timeseries` | Historical daily rates for a currency pair over a date range, never including a date outside it. Inline results come back in pages of 500 publication days, continued with `next_start_date`; when DataCanvas is enabled, long ranges (>90 days) spill to it with a `canvas_id` for SQL follow-up instead. |
|
|
43
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`. |
|
|
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. Returns at most `row_limit` rows (default 150, max 10,000). Needs `CANVAS_PROVIDER_TYPE=duckdb`. |
|
|
45
45
|
| `fx_dataframe_drop` | Permanently remove one staged table or view from a DataCanvas. Deletes staged analytical data only — ECB rate data is untouched and the series can be re-staged. Needs `CANVAS_PROVIDER_TYPE=duckdb` and `FX_ENABLE_CANVAS_DROP=true`; disabled otherwise. |
|
|
46
46
|
|
|
47
47
|
### `fx_list_currencies`
|
|
@@ -92,7 +92,9 @@ Historical rate series and DataCanvas SQL analytics.
|
|
|
92
92
|
`fx_get_timeseries` returns a date-keyed series (business days only — ECB publishes once per business day):
|
|
93
93
|
|
|
94
94
|
- Short ranges (≤ `FX_TIMESERIES_CANVAS_THRESHOLD_DAYS`, default 90 days) → inline `rates` map + metadata
|
|
95
|
-
- Long ranges, **when DataCanvas is enabled** →
|
|
95
|
+
- Long ranges, **when DataCanvas is enabled** → a preview inline + `canvas_id`, `table_name`, `spilled: true`, and a `notice` naming `fx_dataframe_describe` then `fx_dataframe_query` — the full series is registered as a DuckDB-backed table
|
|
96
|
+
- Long ranges **without DataCanvas** → returned inline with `spilled: false`, paged (below), and a `notice` saying the threshold was crossed but no canvas was configured
|
|
97
|
+
- Inline results are paged at 500 publication days. `rate_count` is always the total for the requested range; when a page is cut short the response carries `truncated: true` and `next_start_date`. Call again with `start_date` set to `next_start_date` and the same `end_date` for the next page — the final page has `truncated: false` and no `next_start_date`
|
|
96
98
|
- 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
|
|
97
99
|
|
|
98
100
|
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.
|
|
@@ -100,7 +102,7 @@ The response never carries a date outside the requested range. Frankfurter snaps
|
|
|
100
102
|
Once a `canvas_id` is in hand:
|
|
101
103
|
|
|
102
104
|
1. **`fx_dataframe_describe`** — list the tables and columns on the canvas (required before `fx_dataframe_query`)
|
|
103
|
-
2. **`fx_dataframe_query`** — run arbitrary SQL SELECT against the registered table; supports aggregations, GROUP BY, window functions, JOINs across tables from multiple `fx_get_timeseries` calls
|
|
105
|
+
2. **`fx_dataframe_query`** — run arbitrary SQL SELECT against the registered table; supports aggregations, GROUP BY, window functions, JOINs across tables from multiple `fx_get_timeseries` calls. Returns at most `row_limit` rows (default 150, max 10,000) on both response surfaces; past that, `truncated: true` and a `notice` give the `ORDER BY <column> LIMIT <n> OFFSET <m>` shape for the next page (`ORDER BY` is required for stable paging). Cell values are escaped in the Markdown table so pipes, angle brackets, and line breaks stay inside their cell; `structuredContent` keeps the raw values
|
|
104
106
|
|
|
105
107
|
The canvas uses a session-scoped TTL. To continue working with a prior series, call `fx_get_timeseries` again with the same parameters to obtain a fresh `canvas_id`.
|
|
106
108
|
|
|
@@ -143,7 +145,8 @@ Agent-friendly output:
|
|
|
143
145
|
- Rate provenance on every snapshot, rate, and conversion response — `rate_type`, `source`, `rate_date`, and `date_snapped` so agents can reason about trust and freshness
|
|
144
146
|
- Structured error contracts — typed `reason` fields (`unsupported_currency`, `date_out_of_range`, `invalid_query`, …) let callers branch on failure type, not string parsing
|
|
145
147
|
- Discriminated DataCanvas output — `spilled: true` plus `canvas_id` signal when a time-series was staged for SQL follow-up rather than returned inline
|
|
146
|
-
-
|
|
148
|
+
- Bounded responses — inline time-series pages continue from `next_start_date`, and SQL results cap at `row_limit`, so no call returns an unbounded payload
|
|
149
|
+
- Success-path `notice` enrichment — explains an empty series, where to continue a paged series, or which tools read a staged one, so a legitimate zero-result never reads as a failure
|
|
147
150
|
|
|
148
151
|
---
|
|
149
152
|
|
|
@@ -342,6 +345,7 @@ The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `
|
|
|
342
345
|
| `src/mcp-server/resources/` | Resource definitions — `fx://currencies` and `fx://rates/latest/{base}`. |
|
|
343
346
|
| `src/services/frankfurter/` | Frankfurter HTTP client, retry logic, and domain types. |
|
|
344
347
|
| `src/services/canvas/` | Module-level DataCanvas accessor for `fx_get_timeseries` spillover. |
|
|
348
|
+
| `src/utils/` | Output helpers — Markdown table-cell escaping for `fx_dataframe_query`. |
|
|
345
349
|
| `tests/` | Unit and integration tests mirroring `src/`. |
|
|
346
350
|
| `docs/` | Design document and idea notes. |
|
|
347
351
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "fx_get_timeseries pages inline results at 500 publication days and fx_dataframe_query defaults row_limit to 150 (max 10,000) — both breaking; spilled fx_get_timeseries responses name both dataframe tools, and query results escape Markdown table cells."
|
|
3
|
+
breaking: true
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 0.4.0 — 2026-09-12
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
- **`fx_get_timeseries` gains `truncated` and `next_start_date`** — inline results page at 500 publication days; pass `next_start_date` back as `start_date` for the next page. ([#11](https://github.com/cyanheads/exchange-rates-mcp-server/issues/11))
|
|
12
|
+
- **`fx_dataframe_query` gains `row_limit`** (1–10,000, default 150) — passed through to DataCanvas's `query()`; a capped result's `notice` gives the `ORDER BY <column> LIMIT <n> OFFSET <m>` paging shape. ([#12](https://github.com/cyanheads/exchange-rates-mcp-server/issues/12))
|
|
13
|
+
|
|
14
|
+
## Changed
|
|
15
|
+
|
|
16
|
+
- **`fx_get_timeseries` inline results are now bounded** — a canvas-disabled or under-threshold range returns at most 500 publication days per call instead of the full series; `rate_count` stays the total for the requested range. **Breaking**: a caller relying on one call returning an unbounded series must page with `next_start_date`. ([#11](https://github.com/cyanheads/exchange-rates-mcp-server/issues/11))
|
|
17
|
+
- **`fx_dataframe_query`'s default response drops from up to 10,000 rows to 150** — `format()` no longer independently slices `content` to 50 rows, so both surfaces always carry the same row set. **Breaking**: a caller relying on more than 150 rows per call must pass a larger `row_limit` or page with `ORDER BY … LIMIT … OFFSET …`. ([#12](https://github.com/cyanheads/exchange-rates-mcp-server/issues/12))
|
|
18
|
+
- **Spilled `fx_get_timeseries` responses name both dataframe tools** — the spill `notice`, the `canvas_id`/`table_name` descriptions, `format()`, and the server `instructions` all point to `fx_dataframe_describe` before `fx_dataframe_query`. ([#16](https://github.com/cyanheads/exchange-rates-mcp-server/issues/16))
|
|
19
|
+
|
|
20
|
+
## Fixed
|
|
21
|
+
|
|
22
|
+
- **`fx_dataframe_query` escapes Markdown table cells** — a value containing `|`, `<`, `>`, `*`, `_`, a backtick, brackets, tildes, `&`, or an embedded line break no longer breaks the table or is misread as Markdown/HTML; `structuredContent` is unaffected. ([#13](https://github.com/cyanheads/exchange-rates-mcp-server/issues/13))
|
package/dist/index.js
CHANGED
|
@@ -40,7 +40,9 @@ await createApp({
|
|
|
40
40
|
'- Use fx_list_currencies first to disambiguate "dollars" (USD/AUD/CAD/HKD/SGD).\n' +
|
|
41
41
|
'- Cross-rates (e.g. USD→JPY) are triangulated through EUR automatically.\n' +
|
|
42
42
|
'- Long time-series (>90 days) spill to DataCanvas when it is enabled (CANVAS_PROVIDER_TYPE=duckdb);\n' +
|
|
43
|
-
'
|
|
43
|
+
' call fx_dataframe_describe for the staged columns, then fx_dataframe_query for SQL.\n' +
|
|
44
|
+
' Without it, long ranges return inline in pages of 500 publication days (continue from next_start_date)\n' +
|
|
45
|
+
' and the dataframe tools are not listed.',
|
|
44
46
|
setup(core) {
|
|
45
47
|
setCanvas(core.canvas);
|
|
46
48
|
},
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AACvD,OAAO,EACL,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,6CAA6C,CAAC;AACrD,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,eAAe,EACf,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;QAC7E,aAAa,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,YAAY,CAAC,eAAe,EAAE,UAAU,CAAC;KAC5E;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,
|
|
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,eAAe,EACf,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;QAC7E,aAAa,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,YAAY,CAAC,eAAe,EAAE,UAAU,CAAC;KAC5E;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,yFAAyF;QACzF,4GAA4G;QAC5G,2CAA2C;IAE7C,KAAK,CAAC,IAAI;QACR,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzB,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -7,6 +7,7 @@ import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
|
7
7
|
export declare const fxDataframeQuery: import("@cyanheads/mcp-ts-core").ToolDefinition<z.ZodObject<{
|
|
8
8
|
canvas_id: z.ZodString;
|
|
9
9
|
query: z.ZodString;
|
|
10
|
+
row_limit: z.ZodDefault<z.ZodNumber>;
|
|
10
11
|
}, z.core.$strip>, z.ZodObject<{
|
|
11
12
|
rows: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
12
13
|
row_count: z.ZodNumber;
|
|
@@ -27,5 +28,7 @@ export declare const fxDataframeQuery: import("@cyanheads/mcp-ts-core").ToolDefi
|
|
|
27
28
|
readonly code: JsonRpcErrorCode.ValidationError;
|
|
28
29
|
readonly when: "SQL is not a SELECT, references unknown columns, or has a syntax error.";
|
|
29
30
|
readonly recovery: "Run fx_dataframe_describe first to verify table and column names.";
|
|
30
|
-
}],
|
|
31
|
+
}], {
|
|
32
|
+
readonly notice: z.ZodOptional<z.ZodString>;
|
|
33
|
+
}>;
|
|
31
34
|
//# 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,EAAgC,MAAM,+BAA+B,CAAC;
|
|
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;AAa/F,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;EAwN3B,CAAC"}
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
6
6
|
import { JsonRpcErrorCode, McpError, serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
|
|
7
7
|
import { getCanvas } from '../../../services/canvas/canvas-accessor.js';
|
|
8
|
+
import { escapeMarkdownTableCell } from '../../../utils/escape-markdown-table-cell.js';
|
|
9
|
+
/**
|
|
10
|
+
* Rows returned when the caller sets no row_limit. Every returned row renders on both
|
|
11
|
+
* response surfaces (~125 B/row serialized for the staged date/rate table), so 150 rows
|
|
12
|
+
* is ~19 KB on the wire — inside the framework's 24 KB outline budget, where 200 is not.
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_ROW_LIMIT = 150;
|
|
15
|
+
/** DataCanvas's own materialization ceiling — raising row_limit can never exceed an unbounded query. */
|
|
16
|
+
const MAX_ROW_LIMIT = 10_000;
|
|
8
17
|
export const fxDataframeQuery = tool('fx_dataframe_query', {
|
|
9
18
|
description: 'Run a read-only SQL SELECT against DataCanvas tables staged by fx_get_timeseries. ' +
|
|
10
19
|
'Supports aggregations, GROUP BY, window functions, and JOINs across multiple registered tables. ' +
|
|
@@ -26,25 +35,42 @@ export const fxDataframeQuery = tool('fx_dataframe_query', {
|
|
|
26
35
|
.describe('Read-only SQL SELECT statement. Reference tables by the names returned by fx_dataframe_describe ' +
|
|
27
36
|
'or the table_name field from fx_get_timeseries. ' +
|
|
28
37
|
"Example: SELECT date, rate FROM fx_usd_eur WHERE date > '2024-01-01' ORDER BY date"),
|
|
38
|
+
row_limit: z
|
|
39
|
+
.number()
|
|
40
|
+
.int()
|
|
41
|
+
.min(1)
|
|
42
|
+
.max(MAX_ROW_LIMIT)
|
|
43
|
+
.default(DEFAULT_ROW_LIMIT)
|
|
44
|
+
.describe(`Most rows to return (1–${MAX_ROW_LIMIT}, default ${DEFAULT_ROW_LIMIT}). When the query ` +
|
|
45
|
+
'produces more, truncated is true — page with ORDER BY <column> LIMIT <n> OFFSET <m> in the ' +
|
|
46
|
+
'SQL, or aggregate, rather than raising this toward the maximum.'),
|
|
29
47
|
}),
|
|
30
48
|
output: z.object({
|
|
31
49
|
rows: z
|
|
32
50
|
.array(z
|
|
33
51
|
.record(z.string(), z.unknown())
|
|
34
52
|
.describe('One result row — column-name → value pairs matching the SELECT columns.'))
|
|
35
|
-
.describe(
|
|
53
|
+
.describe(`Result rows, at most row_limit (default ${DEFAULT_ROW_LIMIT}). Each key is a column name from the query.`),
|
|
36
54
|
row_count: z
|
|
37
55
|
.number()
|
|
38
|
-
.describe('Rows returned
|
|
39
|
-
'not the full result size.
|
|
56
|
+
.describe('Rows returned — always the length of rows. When truncated is true this equals row_limit, ' +
|
|
57
|
+
'not the full result size.'),
|
|
40
58
|
truncated: z
|
|
41
59
|
.boolean()
|
|
42
|
-
.describe('True when the query produced more rows than
|
|
43
|
-
'
|
|
60
|
+
.describe('True when the query produced more rows than row_limit and rows holds only the first ' +
|
|
61
|
+
'row_limit of them. Fetch the rest with ORDER BY <column> LIMIT <n> OFFSET <m> — ORDER BY is ' +
|
|
62
|
+
'required for deterministic paging — or aggregate to shrink the result.'),
|
|
44
63
|
canvas_id: z
|
|
45
64
|
.string()
|
|
46
65
|
.describe('The canvas ID used — pass to a subsequent fx_dataframe_query or fx_dataframe_describe call.'),
|
|
47
66
|
}),
|
|
67
|
+
enrichment: {
|
|
68
|
+
notice: z
|
|
69
|
+
.string()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe('Present when truncated is true: how many rows came back and the ORDER BY … LIMIT … OFFSET ' +
|
|
72
|
+
'query shape that fetches the next page.'),
|
|
73
|
+
},
|
|
48
74
|
errors: [
|
|
49
75
|
{
|
|
50
76
|
reason: 'canvas_not_found',
|
|
@@ -86,7 +112,10 @@ export const fxDataframeQuery = tool('fx_dataframe_query', {
|
|
|
86
112
|
}
|
|
87
113
|
let result;
|
|
88
114
|
try {
|
|
89
|
-
result = await instance.query(input.query, {
|
|
115
|
+
result = await instance.query(input.query, {
|
|
116
|
+
rowLimit: input.row_limit,
|
|
117
|
+
signal: ctx.signal,
|
|
118
|
+
});
|
|
90
119
|
}
|
|
91
120
|
catch (err) {
|
|
92
121
|
const reason = reasonOf(err);
|
|
@@ -116,21 +145,28 @@ export const fxDataframeQuery = tool('fx_dataframe_query', {
|
|
|
116
145
|
}
|
|
117
146
|
throw err;
|
|
118
147
|
}
|
|
148
|
+
const truncated = result.truncated ?? false;
|
|
119
149
|
ctx.log.info('Executed dataframe query', {
|
|
120
150
|
canvasId: input.canvas_id,
|
|
121
151
|
rowCount: result.rowCount,
|
|
122
|
-
truncated
|
|
152
|
+
truncated,
|
|
123
153
|
});
|
|
154
|
+
if (truncated) {
|
|
155
|
+
ctx.enrich.notice(`The query produced more than ${input.row_limit} rows; rows holds the first ${input.row_limit}. ` +
|
|
156
|
+
`For the next page re-run it with ORDER BY <column> LIMIT ${input.row_limit} OFFSET ${input.row_limit} ` +
|
|
157
|
+
'(raise OFFSET by the limit each page). ORDER BY is required: without it DuckDB does not ' +
|
|
158
|
+
'guarantee the same row order across pages. Aggregating or filtering shrinks the result instead.');
|
|
159
|
+
}
|
|
124
160
|
return {
|
|
125
161
|
rows: result.rows,
|
|
126
162
|
row_count: result.rowCount,
|
|
127
|
-
truncated
|
|
163
|
+
truncated,
|
|
128
164
|
canvas_id: input.canvas_id,
|
|
129
165
|
};
|
|
130
166
|
},
|
|
131
167
|
format: (result) => {
|
|
132
168
|
const capNote = result.truncated
|
|
133
|
-
? '\n⚠️ *truncated:
|
|
169
|
+
? '\n⚠️ *truncated: yes — the query produced more rows than row_limit; only these were returned.*'
|
|
134
170
|
: '\n*truncated: no*';
|
|
135
171
|
if (result.rows.length === 0) {
|
|
136
172
|
return [
|
|
@@ -141,19 +177,16 @@ export const fxDataframeQuery = tool('fx_dataframe_query', {
|
|
|
141
177
|
];
|
|
142
178
|
}
|
|
143
179
|
const cols = Object.keys(result.rows[0] ?? {});
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
.
|
|
148
|
-
.map((r) =>
|
|
149
|
-
|
|
150
|
-
const total = result.truncated ? `${result.row_count}+ (capped)` : `${result.row_count}`;
|
|
151
|
-
const note = result.row_count > shown ? `\n*Showing ${shown} of ${total} rows*` : `\n*${total} rows*`;
|
|
152
|
-
const table = [header, sep, ...rowLines].join('\n');
|
|
180
|
+
const toRow = (cells) => `| ${cells.map(escapeMarkdownTableCell).join(' | ')} |`;
|
|
181
|
+
const table = [
|
|
182
|
+
toRow(cols),
|
|
183
|
+
`| ${cols.map(() => '---').join(' | ')} |`,
|
|
184
|
+
...result.rows.map((r) => toRow(cols.map((c) => String(r[c] ?? '')))),
|
|
185
|
+
].join('\n');
|
|
153
186
|
return [
|
|
154
187
|
{
|
|
155
188
|
type: 'text',
|
|
156
|
-
text: `${table}
|
|
189
|
+
text: `${table}\n*${result.row_count} rows* · canvas \`${result.canvas_id}\`${capNote}`,
|
|
157
190
|
},
|
|
158
191
|
];
|
|
159
192
|
},
|
|
@@ -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,QAAQ,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAC/F,OAAO,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;
|
|
1
|
+
{"version":3,"file":"fx-dataframe-query.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-dataframe-query.tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,6 +17,8 @@ export declare const fxGetTimeseries: import("@cyanheads/mcp-ts-core").ToolDefin
|
|
|
17
17
|
end_date: z.ZodString;
|
|
18
18
|
rates: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
19
19
|
rate_count: z.ZodNumber;
|
|
20
|
+
truncated: z.ZodBoolean;
|
|
21
|
+
next_start_date: z.ZodOptional<z.ZodString>;
|
|
20
22
|
rate_type: z.ZodString;
|
|
21
23
|
source: z.ZodString;
|
|
22
24
|
spilled: z.ZodBoolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fx-get-timeseries.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-get-timeseries.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"fx-get-timeseries.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-get-timeseries.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAkBjE,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0X1B,CAAC"}
|
|
@@ -9,17 +9,26 @@ import { getServerConfig } from '../../../config/server-config.js';
|
|
|
9
9
|
import { getCanvas } from '../../../services/canvas/canvas-accessor.js';
|
|
10
10
|
import { failureOf, unsupportedCurrencyCodesOf } from '../../../services/frankfurter/errors.js';
|
|
11
11
|
import { ECB_START_DATE, getFrankfurterService, isIsoDate, } from '../../../services/frankfurter/frankfurter-service.js';
|
|
12
|
+
/**
|
|
13
|
+
* Most publication days one inline response carries — roughly two ECB years, ~21 KB
|
|
14
|
+
* serialized across both response surfaces. Rows are date-keyed, so the continuation
|
|
15
|
+
* is the next start_date rather than an offset.
|
|
16
|
+
*/
|
|
17
|
+
const INLINE_PAGE_SIZE = 500;
|
|
12
18
|
export const fxGetTimeseries = tool('fx_get_timeseries', {
|
|
13
19
|
description: 'Get historical daily exchange rates for a currency pair over a date range. ' +
|
|
14
20
|
'ECB publishes on business days only — weekends and holidays produce no entry, and no date ' +
|
|
15
21
|
'outside the requested range is ever returned, so a range covering only non-publication days ' +
|
|
16
22
|
'comes back with an empty rates map and a notice explaining why. ' +
|
|
17
23
|
'A same-currency pair returns a rate of 1 on each publication day in the range. ' +
|
|
18
|
-
|
|
19
|
-
'
|
|
20
|
-
'carries
|
|
21
|
-
'
|
|
22
|
-
'
|
|
24
|
+
`Inline results are returned as a date→rate map paged at ${INLINE_PAGE_SIZE} publication days: ` +
|
|
25
|
+
'rate_count is always the total for the requested range, and when a page is cut short the ' +
|
|
26
|
+
'response carries truncated=true and next_start_date — call again with start_date set to ' +
|
|
27
|
+
'next_start_date and the same end_date for the next page. ' +
|
|
28
|
+
'When DataCanvas is enabled (CANVAS_PROVIDER_TYPE=duckdb) long ranges (>90 days by default) spill ' +
|
|
29
|
+
'to it instead: the response carries spilled=true, a canvas_id, and a table_name — call ' +
|
|
30
|
+
'fx_dataframe_describe to inspect the staged table, then fx_dataframe_query to run SQL against it. ' +
|
|
31
|
+
'Without DataCanvas long ranges are paged inline (spilled=false) and the notice says so.',
|
|
23
32
|
annotations: {
|
|
24
33
|
readOnlyHint: true,
|
|
25
34
|
idempotentHint: true,
|
|
@@ -50,20 +59,35 @@ export const fxGetTimeseries = tool('fx_get_timeseries', {
|
|
|
50
59
|
quote_currency: z.string().describe('Quote currency code.'),
|
|
51
60
|
start_date: z
|
|
52
61
|
.string()
|
|
53
|
-
.describe('First date in the
|
|
54
|
-
'requested start when that day had no ECB fix, and
|
|
62
|
+
.describe('First publication date in the requested range, and the first key in rates. Always inside ' +
|
|
63
|
+
'the requested range — later than the requested start when that day had no ECB fix, and ' +
|
|
64
|
+
'equal to it when the series is empty.'),
|
|
55
65
|
end_date: z
|
|
56
66
|
.string()
|
|
57
|
-
.describe('Last date in the
|
|
58
|
-
'requested end when that day had no ECB fix, and equal to it when the series is
|
|
67
|
+
.describe('Last publication date in the requested range. Always inside the requested range — earlier ' +
|
|
68
|
+
'than the requested end when that day had no ECB fix, and equal to it when the series is ' +
|
|
69
|
+
'empty. Later than the last key in rates when truncated is true.'),
|
|
59
70
|
rates: z
|
|
60
71
|
.record(z.string(), z.number())
|
|
61
|
-
.describe('Date → rate map
|
|
62
|
-
|
|
63
|
-
'
|
|
72
|
+
.describe('Date → rate map in date order, publication days inside the requested range only. Holds ' +
|
|
73
|
+
`every publication day when truncated is false; otherwise the first ${INLINE_PAGE_SIZE} of an ` +
|
|
74
|
+
'inline page (continue with next_start_date) or the preview of a spilled series (the full ' +
|
|
75
|
+
'series is on the canvas). Empty when the range contains no publication day at all.'),
|
|
64
76
|
rate_count: z
|
|
65
77
|
.number()
|
|
66
|
-
.describe('Total
|
|
78
|
+
.describe('Total publication days in the requested range (the start_date through end_date passed on ' +
|
|
79
|
+
'this call) — not the number of entries in rates, which is smaller when truncated is true. ' +
|
|
80
|
+
'On a continuation call it counts the days from that start_date onward.'),
|
|
81
|
+
truncated: z
|
|
82
|
+
.boolean()
|
|
83
|
+
.describe('True when rates holds only part of the series: an inline page (next_start_date continues ' +
|
|
84
|
+
'it) or a spilled preview (spilled is true). False when rates holds every publication day ' +
|
|
85
|
+
'in the requested range.'),
|
|
86
|
+
next_start_date: z
|
|
87
|
+
.string()
|
|
88
|
+
.optional()
|
|
89
|
+
.describe('Present only when an inline page was cut short: the first publication date not in rates. ' +
|
|
90
|
+
'Pass it as start_date with the same end_date to get the next page. Absent on the final page.'),
|
|
67
91
|
rate_type: z
|
|
68
92
|
.string()
|
|
69
93
|
.describe('Always "ECB reference (mid-market)" — these are reference rates, not tradeable bid/ask.'),
|
|
@@ -74,18 +98,22 @@ export const fxGetTimeseries = tool('fx_get_timeseries', {
|
|
|
74
98
|
canvas_id: z
|
|
75
99
|
.string()
|
|
76
100
|
.optional()
|
|
77
|
-
.describe('Canvas ID — present when spilled is true. Pass to
|
|
101
|
+
.describe('Canvas ID — present when spilled is true. Pass to fx_dataframe_describe to inspect the ' +
|
|
102
|
+
'staged table, then to fx_dataframe_query to run SQL.'),
|
|
78
103
|
table_name: z
|
|
79
104
|
.string()
|
|
80
105
|
.optional()
|
|
81
|
-
.describe('Canvas table name — present when spilled is true. Use in
|
|
106
|
+
.describe('Canvas table name — present when spilled is true. Use it as the FROM target in ' +
|
|
107
|
+
'fx_dataframe_query SQL; fx_dataframe_describe lists its columns.'),
|
|
82
108
|
}),
|
|
83
109
|
enrichment: {
|
|
84
110
|
notice: z
|
|
85
111
|
.string()
|
|
86
112
|
.optional()
|
|
87
|
-
.describe('Explains a result that would otherwise look broken
|
|
88
|
-
'
|
|
113
|
+
.describe('Explains a result that would otherwise look broken or incomplete: an empty series (no ' +
|
|
114
|
+
'publication day in the range); an inline page cut short (the start_date to continue ' +
|
|
115
|
+
'from, plus why a long range was not staged when DataCanvas is not configured); or a ' +
|
|
116
|
+
'spilled series (the canvas and table it was staged to, and the tools that read it).'),
|
|
89
117
|
},
|
|
90
118
|
errors: [
|
|
91
119
|
{
|
|
@@ -181,23 +209,7 @@ export const fxGetTimeseries = tool('fx_get_timeseries', {
|
|
|
181
209
|
});
|
|
182
210
|
const canvas = getCanvas();
|
|
183
211
|
const exceedsThreshold = dayCount > config.timeseriesCanvasThresholdDays;
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* An empty series and a long range that never spilled both look like failures
|
|
187
|
-
* from the outside. Say which one happened and why, on both client surfaces.
|
|
188
|
-
*/
|
|
189
|
-
if (rows.length === 0) {
|
|
190
|
-
ctx.enrich.notice(`The ECB published no ${input.base_currency.toUpperCase()}/${input.quote_currency.toUpperCase()} rate between ` +
|
|
191
|
-
`${input.start_date} and ${input.end_date}. Reference rates are published on TARGET business days ` +
|
|
192
|
-
'only, so a range covering just a weekend or a bank holiday is legitimately empty rather than ' +
|
|
193
|
-
'broken. Widen the range — a window spanning several weekdays will contain a publication day.');
|
|
194
|
-
}
|
|
195
|
-
else if (exceedsThreshold && canvas == null) {
|
|
196
|
-
ctx.enrich.notice(`This ${dayCount}-day range is past the ${config.timeseriesCanvasThresholdDays}-day spill threshold, but ` +
|
|
197
|
-
'DataCanvas is not configured on this server, so the full series is inline and spilled is false. ' +
|
|
198
|
-
'Set CANVAS_PROVIDER_TYPE=duckdb to stage long ranges for SQL instead.');
|
|
199
|
-
}
|
|
200
|
-
/** Fields every branch below returns identically — only rates/count/spill differ. */
|
|
212
|
+
/** Fields every branch below returns identically — only rates/count/paging/spill differ. */
|
|
201
213
|
const envelope = {
|
|
202
214
|
base_currency: input.base_currency.toUpperCase(),
|
|
203
215
|
quote_currency: input.quote_currency.toUpperCase(),
|
|
@@ -206,72 +218,92 @@ export const fxGetTimeseries = tool('fx_get_timeseries', {
|
|
|
206
218
|
rate_type: 'ECB reference (mid-market)',
|
|
207
219
|
source: 'ECB via Frankfurter',
|
|
208
220
|
};
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
221
|
+
if (canvas != null && exceedsThreshold) {
|
|
222
|
+
const instance = await canvas.acquire(input.canvas_id, ctx);
|
|
223
|
+
const tableName = `fx_${input.base_currency.toLowerCase()}_${input.quote_currency.toLowerCase()}`;
|
|
224
|
+
const spillResult = await spillover({
|
|
225
|
+
canvas: instance,
|
|
226
|
+
source: rows,
|
|
227
|
+
tableName,
|
|
228
|
+
previewChars: 40_000, // ~10k tokens preview
|
|
229
|
+
signal: ctx.signal,
|
|
230
|
+
});
|
|
231
|
+
ctx.log.info('Spilled to canvas', {
|
|
232
|
+
canvasId: instance.canvasId,
|
|
233
|
+
tableName,
|
|
234
|
+
rowCount: rows.length,
|
|
235
|
+
spilled: spillResult.spilled,
|
|
236
|
+
});
|
|
237
|
+
if (spillResult.spilled) {
|
|
238
|
+
const { rowCount, tableName: stagedTable } = spillResult.handle;
|
|
239
|
+
ctx.enrich.notice(`Staged ${rowCount} rows to table "${stagedTable}" on canvas ${instance.canvasId} — call ` +
|
|
240
|
+
'fx_dataframe_describe to inspect its columns, then fx_dataframe_query to run SQL over the ' +
|
|
241
|
+
'full series. rates holds only a preview.');
|
|
242
|
+
return {
|
|
243
|
+
...envelope,
|
|
244
|
+
rates: Object.fromEntries(spillResult.previewRows.map((row) => [row.date, row.rate])),
|
|
245
|
+
rate_count: rowCount,
|
|
246
|
+
truncated: true,
|
|
247
|
+
spilled: true,
|
|
248
|
+
canvas_id: instance.canvasId,
|
|
249
|
+
table_name: stagedTable,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// Every row fit the preview budget, so nothing was staged — page it inline below.
|
|
215
253
|
}
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
254
|
+
const page = rows.slice(0, INLINE_PAGE_SIZE);
|
|
255
|
+
const nextRow = rows[INLINE_PAGE_SIZE];
|
|
256
|
+
/**
|
|
257
|
+
* An empty series, a page cut short, and a long range that never spilled all look
|
|
258
|
+
* incomplete or broken from the outside. Say which one happened, on both surfaces.
|
|
259
|
+
* The dataframe tools are unregistered without a canvas, so nothing here names them.
|
|
260
|
+
*/
|
|
261
|
+
const unstaged = exceedsThreshold && canvas == null
|
|
262
|
+
? `This ${dayCount}-day range is past the ${config.timeseriesCanvasThresholdDays}-day spill threshold, but ` +
|
|
263
|
+
'DataCanvas is not configured on this server, so the series is returned inline and spilled is false. ' +
|
|
264
|
+
'Set CANVAS_PROVIDER_TYPE=duckdb to stage long ranges for SQL instead.'
|
|
265
|
+
: undefined;
|
|
266
|
+
if (rows.length === 0) {
|
|
267
|
+
ctx.enrich.notice(`The ECB published no ${envelope.base_currency}/${envelope.quote_currency} rate between ` +
|
|
268
|
+
`${input.start_date} and ${input.end_date}. Reference rates are published on TARGET business days ` +
|
|
269
|
+
'only, so a range covering just a weekend or a bank holiday is legitimately empty rather than ' +
|
|
270
|
+
'broken. Widen the range — a window spanning several weekdays will contain a publication day.');
|
|
271
|
+
}
|
|
272
|
+
else if (nextRow) {
|
|
273
|
+
const pageNote = `rates holds the first ${page.length} of ${rows.length} publication days ` +
|
|
274
|
+
`(${page[0]?.date} to ${page.at(-1)?.date}). Call fx_get_timeseries again with ` +
|
|
275
|
+
`start_date=${nextRow.date} and end_date=${input.end_date} for the next page.`;
|
|
276
|
+
ctx.enrich.notice(unstaged ? `${pageNote} ${unstaged}` : pageNote);
|
|
237
277
|
}
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
...envelope,
|
|
241
|
-
rates: previewRates,
|
|
242
|
-
rate_count: spillResult.handle.rowCount,
|
|
243
|
-
spilled: true,
|
|
244
|
-
canvas_id: instance.canvasId,
|
|
245
|
-
table_name: spillResult.handle.tableName,
|
|
246
|
-
};
|
|
278
|
+
else if (unstaged) {
|
|
279
|
+
ctx.enrich.notice(unstaged);
|
|
247
280
|
}
|
|
248
|
-
// Fell under budget even at canvas threshold — return inline
|
|
249
281
|
return {
|
|
250
282
|
...envelope,
|
|
251
|
-
rates:
|
|
252
|
-
rate_count:
|
|
283
|
+
rates: Object.fromEntries(page.map((row) => [row.date, row.rate])),
|
|
284
|
+
rate_count: rows.length,
|
|
285
|
+
truncated: nextRow !== undefined,
|
|
286
|
+
...(nextRow && { next_start_date: nextRow.date }),
|
|
253
287
|
spilled: false,
|
|
254
288
|
};
|
|
255
289
|
},
|
|
256
290
|
format: (result) => {
|
|
291
|
+
const rateEntries = Object.entries(result.rates).sort(([a], [b]) => a.localeCompare(b));
|
|
257
292
|
const lines = [
|
|
258
293
|
`**${result.base_currency}/${result.quote_currency} time series** — ${result.start_date} to ${result.end_date}`,
|
|
259
|
-
`*${result.rate_count} publication-day data points · ${result.rate_type} · ${result.source} · spilled: ${result.spilled}*`,
|
|
294
|
+
`*${result.rate_count} publication-day data points in range · ${result.rate_type} · ${result.source} · spilled: ${result.spilled}*`,
|
|
295
|
+
`*${rateEntries.length} shown · truncated: ${result.truncated}` +
|
|
296
|
+
(result.next_start_date ? ` · next_start_date: ${result.next_start_date}*` : '*'),
|
|
260
297
|
];
|
|
261
298
|
if (result.spilled) {
|
|
262
|
-
lines.push(`\n📊 **Result staged on DataCanvas** (large range)`, `Canvas ID: \`${result.canvas_id}\``, `Table: \`${result.table_name}\``, `
|
|
299
|
+
lines.push(`\n📊 **Result staged on DataCanvas** (large range)`, `Canvas ID: \`${result.canvas_id}\``, `Table: \`${result.table_name}\``, 'Call `fx_dataframe_describe` with this canvas_id to inspect the table columns, then ' +
|
|
300
|
+
'`fx_dataframe_query` to run SQL over the full series. Preview (first entries below):');
|
|
263
301
|
}
|
|
264
|
-
const rateEntries = Object.entries(result.rates)
|
|
265
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
266
|
-
.slice(0, 20);
|
|
267
302
|
if (rateEntries.length > 0) {
|
|
268
303
|
lines.push('');
|
|
269
304
|
for (const [date, rate] of rateEntries) {
|
|
270
305
|
lines.push(`${date}: ${rate}`);
|
|
271
306
|
}
|
|
272
|
-
if (Object.keys(result.rates).length > 20) {
|
|
273
|
-
lines.push(`... (${Object.keys(result.rates).length - 20} more entries)`);
|
|
274
|
-
}
|
|
275
307
|
}
|
|
276
308
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
277
309
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fx-get-timeseries.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-get-timeseries.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AACzF,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,SAAS,GACV,MAAM,+CAA+C,CAAC;AAGvD,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC,mBAAmB,EAAE;IACvD,WAAW,EACT,6EAA6E;QAC7E,4FAA4F;QAC5F,8FAA8F;QAC9F,kEAAkE;QAClE,iFAAiF;QACjF,
|
|
1
|
+
{"version":3,"file":"fx-get-timeseries.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-get-timeseries.tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Escapes a value for one cell of a GFM Markdown table.
|
|
3
|
+
* @module utils/escape-markdown-table-cell
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Renders `value` as the literal text of a single table cell: every metacharacter is
|
|
7
|
+
* backslash-escaped and each line break becomes `<br>`, so the value never leaves its
|
|
8
|
+
* row. A literal `<br>` in the data is escaped first and stays distinguishable from one.
|
|
9
|
+
*/
|
|
10
|
+
export declare function escapeMarkdownTableCell(value: string): string;
|
|
11
|
+
//# sourceMappingURL=escape-markdown-table-cell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escape-markdown-table-cell.d.ts","sourceRoot":"","sources":["../../src/utils/escape-markdown-table-cell.ts"],"names":[],"mappings":"AAAA;;;GAGG;AASH;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE7D"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Escapes a value for one cell of a GFM Markdown table.
|
|
3
|
+
* @module utils/escape-markdown-table-cell
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Markdown and HTML metacharacters that would split a table row (`|`), open a tag
|
|
7
|
+
* (`<`, `>`), or turn data into emphasis, code, links, strikethrough, or an entity.
|
|
8
|
+
* Backslash is in the set so an escape already present in the data stays literal.
|
|
9
|
+
*/
|
|
10
|
+
const METACHARACTERS = /[\\|<>*_`[\]~&]/g;
|
|
11
|
+
/**
|
|
12
|
+
* Renders `value` as the literal text of a single table cell: every metacharacter is
|
|
13
|
+
* backslash-escaped and each line break becomes `<br>`, so the value never leaves its
|
|
14
|
+
* row. A literal `<br>` in the data is escaped first and stays distinguishable from one.
|
|
15
|
+
*/
|
|
16
|
+
export function escapeMarkdownTableCell(value) {
|
|
17
|
+
return value.replace(METACHARACTERS, '\\$&').replace(/\r\n|\r|\n/g, '<br>');
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=escape-markdown-table-cell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escape-markdown-table-cell.js","sourceRoot":"","sources":["../../src/utils/escape-markdown-table-cell.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;GAIG;AACH,MAAM,cAAc,GAAG,kBAAkB,CAAC;AAE1C;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAa;IACnD,OAAO,KAAK,CAAC,OAAO,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;AAC9E,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanheads/exchange-rates-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"mcpName": "io.github.cyanheads/exchange-rates-mcp-server",
|
|
5
5
|
"description": "Convert currencies, get FX rates, and query historical ECB exchange rate data via MCP. STDIO or Streamable HTTP.",
|
|
6
6
|
"type": "module",
|
package/server.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"url": "https://github.com/cyanheads/exchange-rates-mcp-server",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.4.0",
|
|
10
10
|
"remotes": [
|
|
11
11
|
{
|
|
12
12
|
"type": "streamable-http",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
20
20
|
"identifier": "@cyanheads/exchange-rates-mcp-server",
|
|
21
21
|
"runtimeHint": "bun",
|
|
22
|
-
"version": "0.
|
|
22
|
+
"version": "0.4.0",
|
|
23
23
|
"packageArguments": [
|
|
24
24
|
{
|
|
25
25
|
"type": "positional",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
70
70
|
"identifier": "@cyanheads/exchange-rates-mcp-server",
|
|
71
71
|
"runtimeHint": "bun",
|
|
72
|
-
"version": "0.
|
|
72
|
+
"version": "0.4.0",
|
|
73
73
|
"packageArguments": [
|
|
74
74
|
{
|
|
75
75
|
"type": "positional",
|