@cyanheads/exchange-rates-mcp-server 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/AGENTS.md +392 -0
  2. package/CLAUDE.md +392 -0
  3. package/Dockerfile +105 -0
  4. package/LICENSE +201 -0
  5. package/README.md +340 -0
  6. package/changelog/0.1.x/0.1.1.md +32 -0
  7. package/changelog/template.md +127 -0
  8. package/dist/config/server-config.d.ts +13 -0
  9. package/dist/config/server-config.d.ts.map +1 -0
  10. package/dist/config/server-config.js +27 -0
  11. package/dist/config/server-config.js.map +1 -0
  12. package/dist/index.d.ts +7 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +31 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/mcp-server/prompts/definitions/index.d.ts +6 -0
  17. package/dist/mcp-server/prompts/definitions/index.d.ts.map +1 -0
  18. package/dist/mcp-server/prompts/definitions/index.js +6 -0
  19. package/dist/mcp-server/prompts/definitions/index.js.map +1 -0
  20. package/dist/mcp-server/resources/definitions/fx-currencies.resource.d.ts +14 -0
  21. package/dist/mcp-server/resources/definitions/fx-currencies.resource.d.ts.map +1 -0
  22. package/dist/mcp-server/resources/definitions/fx-currencies.resource.js +43 -0
  23. package/dist/mcp-server/resources/definitions/fx-currencies.resource.js.map +1 -0
  24. package/dist/mcp-server/resources/definitions/fx-rates-latest.resource.d.ts +15 -0
  25. package/dist/mcp-server/resources/definitions/fx-rates-latest.resource.d.ts.map +1 -0
  26. package/dist/mcp-server/resources/definitions/fx-rates-latest.resource.js +61 -0
  27. package/dist/mcp-server/resources/definitions/fx-rates-latest.resource.js.map +1 -0
  28. package/dist/mcp-server/resources/definitions/index.d.ts +7 -0
  29. package/dist/mcp-server/resources/definitions/index.d.ts.map +1 -0
  30. package/dist/mcp-server/resources/definitions/index.js +7 -0
  31. package/dist/mcp-server/resources/definitions/index.js.map +1 -0
  32. package/dist/mcp-server/tools/definitions/fx-convert-currency.tool.d.ts +33 -0
  33. package/dist/mcp-server/tools/definitions/fx-convert-currency.tool.d.ts.map +1 -0
  34. package/dist/mcp-server/tools/definitions/fx-convert-currency.tool.js +133 -0
  35. package/dist/mcp-server/tools/definitions/fx-convert-currency.tool.js.map +1 -0
  36. package/dist/mcp-server/tools/definitions/fx-dataframe-describe.tool.d.ts +28 -0
  37. package/dist/mcp-server/tools/definitions/fx-dataframe-describe.tool.d.ts.map +1 -0
  38. package/dist/mcp-server/tools/definitions/fx-dataframe-describe.tool.js +105 -0
  39. package/dist/mcp-server/tools/definitions/fx-dataframe-describe.tool.js.map +1 -0
  40. package/dist/mcp-server/tools/definitions/fx-dataframe-query.tool.d.ts +25 -0
  41. package/dist/mcp-server/tools/definitions/fx-dataframe-query.tool.d.ts.map +1 -0
  42. package/dist/mcp-server/tools/definitions/fx-dataframe-query.tool.js +125 -0
  43. package/dist/mcp-server/tools/definitions/fx-dataframe-query.tool.js.map +1 -0
  44. package/dist/mcp-server/tools/definitions/fx-get-rate.tool.d.ts +30 -0
  45. package/dist/mcp-server/tools/definitions/fx-get-rate.tool.d.ts.map +1 -0
  46. package/dist/mcp-server/tools/definitions/fx-get-rate.tool.js +121 -0
  47. package/dist/mcp-server/tools/definitions/fx-get-rate.tool.js.map +1 -0
  48. package/dist/mcp-server/tools/definitions/fx-get-rates.tool.d.ts +28 -0
  49. package/dist/mcp-server/tools/definitions/fx-get-rates.tool.d.ts.map +1 -0
  50. package/dist/mcp-server/tools/definitions/fx-get-rates.tool.js +115 -0
  51. package/dist/mcp-server/tools/definitions/fx-get-rates.tool.js.map +1 -0
  52. package/dist/mcp-server/tools/definitions/fx-get-timeseries.tool.d.ts +41 -0
  53. package/dist/mcp-server/tools/definitions/fx-get-timeseries.tool.d.ts.map +1 -0
  54. package/dist/mcp-server/tools/definitions/fx-get-timeseries.tool.js +229 -0
  55. package/dist/mcp-server/tools/definitions/fx-get-timeseries.tool.js.map +1 -0
  56. package/dist/mcp-server/tools/definitions/fx-list-currencies.tool.d.ts +14 -0
  57. package/dist/mcp-server/tools/definitions/fx-list-currencies.tool.d.ts.map +1 -0
  58. package/dist/mcp-server/tools/definitions/fx-list-currencies.tool.js +47 -0
  59. package/dist/mcp-server/tools/definitions/fx-list-currencies.tool.js.map +1 -0
  60. package/dist/mcp-server/tools/definitions/index.d.ts +12 -0
  61. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -0
  62. package/dist/mcp-server/tools/definitions/index.js +12 -0
  63. package/dist/mcp-server/tools/definitions/index.js.map +1 -0
  64. package/dist/services/canvas/canvas-accessor.d.ts +10 -0
  65. package/dist/services/canvas/canvas-accessor.d.ts.map +1 -0
  66. package/dist/services/canvas/canvas-accessor.js +12 -0
  67. package/dist/services/canvas/canvas-accessor.js.map +1 -0
  68. package/dist/services/frankfurter/frankfurter-service.d.ts +46 -0
  69. package/dist/services/frankfurter/frankfurter-service.d.ts.map +1 -0
  70. package/dist/services/frankfurter/frankfurter-service.js +181 -0
  71. package/dist/services/frankfurter/frankfurter-service.js.map +1 -0
  72. package/dist/services/frankfurter/types.d.ts +59 -0
  73. package/dist/services/frankfurter/types.d.ts.map +1 -0
  74. package/dist/services/frankfurter/types.js +6 -0
  75. package/dist/services/frankfurter/types.js.map +1 -0
  76. package/package.json +104 -0
  77. package/server.json +127 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * @fileoverview fx_get_timeseries — historical daily rates for a currency pair over a date range.
3
+ * @module mcp-server/tools/definitions/fx-get-timeseries.tool
4
+ */
5
+ import { tool, z } from '@cyanheads/mcp-ts-core';
6
+ import { spillover } from '@cyanheads/mcp-ts-core/canvas';
7
+ import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
8
+ import { getServerConfig } from '../../../config/server-config.js';
9
+ import { getCanvas } from '../../../services/canvas/canvas-accessor.js';
10
+ import { ECB_START_DATE, getFrankfurterService, } from '../../../services/frankfurter/frankfurter-service.js';
11
+ export const fxGetTimeseries = tool('fx_get_timeseries', {
12
+ description: 'Get historical daily exchange rates for a currency pair over a date range. ' +
13
+ 'ECB publishes on business days only — weekends and holidays produce no entry (not snapped). ' +
14
+ 'Short ranges (≤90 days by default) are returned inline as a date→rate map. ' +
15
+ 'Long ranges spill to DataCanvas: the response carries spilled=true, a canvas_id, and a table_name. ' +
16
+ 'Call fx_dataframe_describe to inspect the staged table, then fx_dataframe_query to run SQL against it.',
17
+ annotations: {
18
+ readOnlyHint: true,
19
+ idempotentHint: true,
20
+ openWorldHint: true,
21
+ },
22
+ input: z.object({
23
+ base_currency: z
24
+ .string()
25
+ .describe('ISO 4217 base currency code (e.g. USD). Call fx_list_currencies to get valid codes.'),
26
+ quote_currency: z
27
+ .string()
28
+ .describe('ISO 4217 quote currency code (e.g. EUR). Call fx_list_currencies to get valid codes.'),
29
+ start_date: z
30
+ .string()
31
+ .describe('ISO 8601 start date (YYYY-MM-DD). ECB data starts 1999-01-04. ' +
32
+ 'The actual first data point may be later if start_date falls on a weekend/holiday.'),
33
+ end_date: z
34
+ .string()
35
+ .describe('ISO 8601 end date (YYYY-MM-DD). Must be >= start_date. Future dates are not supported.'),
36
+ canvas_id: z
37
+ .string()
38
+ .optional()
39
+ .describe('Optional canvas ID from a prior call. Omit on the first call to start a fresh canvas; ' +
40
+ 'pass the returned canvas_id to append tables to an existing canvas.'),
41
+ }),
42
+ output: z.object({
43
+ base_currency: z.string().describe('Base currency code.'),
44
+ quote_currency: z.string().describe('Quote currency code.'),
45
+ start_date: z
46
+ .string()
47
+ .describe('Actual first date in the returned series (may differ from requested if that date had no ECB fix).'),
48
+ end_date: z
49
+ .string()
50
+ .describe('Actual last date in the returned series (may differ from requested if that date had no ECB fix).'),
51
+ rates: z
52
+ .record(z.string(), z.number())
53
+ .describe('Date → rate map for the inline result. Business days only. ' +
54
+ 'Empty when the result was spilled to canvas.'),
55
+ rate_count: z.number().describe('Total number of data points (business days) in the range.'),
56
+ rate_type: z
57
+ .string()
58
+ .describe('Always "ECB reference (mid-market)" — these are reference rates, not tradeable bid/ask.'),
59
+ source: z.string().describe('Always "ECB via Frankfurter" — the upstream data provider.'),
60
+ spilled: z
61
+ .boolean()
62
+ .describe('True when the full result was staged on the DataCanvas (range exceeded threshold).'),
63
+ canvas_id: z
64
+ .string()
65
+ .optional()
66
+ .describe('Canvas ID — present when spilled is true. Pass to fx_dataframe_query.'),
67
+ table_name: z
68
+ .string()
69
+ .optional()
70
+ .describe('Canvas table name — present when spilled is true. Use in fx_dataframe_query SQL.'),
71
+ }),
72
+ errors: [
73
+ {
74
+ reason: 'unsupported_currency',
75
+ code: JsonRpcErrorCode.InvalidParams,
76
+ when: 'base_currency or quote_currency is not in the ECB currency set.',
77
+ recovery: 'Call fx_list_currencies to get the list of valid currency codes.',
78
+ },
79
+ {
80
+ reason: 'date_out_of_range',
81
+ code: JsonRpcErrorCode.InvalidParams,
82
+ when: 'start_date is before 1999-01-04 or end_date is in the future.',
83
+ recovery: "ECB data starts 1999-01-04; omit end_date or use today's date.",
84
+ },
85
+ {
86
+ reason: 'invalid_range',
87
+ code: JsonRpcErrorCode.InvalidParams,
88
+ when: 'start_date is after end_date.',
89
+ recovery: 'Ensure start_date is before or equal to end_date.',
90
+ },
91
+ ],
92
+ async handler(input, ctx) {
93
+ const service = getFrankfurterService();
94
+ const config = getServerConfig();
95
+ const today = new Date().toISOString().slice(0, 10);
96
+ // Validate dates
97
+ if (input.start_date < ECB_START_DATE) {
98
+ throw ctx.fail('date_out_of_range', `start_date ${input.start_date} is before ECB data start ${ECB_START_DATE}.`, {
99
+ ...ctx.recoveryFor('date_out_of_range'),
100
+ });
101
+ }
102
+ if (input.end_date > today) {
103
+ throw ctx.fail('date_out_of_range', `end_date ${input.end_date} is in the future.`, {
104
+ ...ctx.recoveryFor('date_out_of_range'),
105
+ });
106
+ }
107
+ if (input.start_date > input.end_date) {
108
+ throw ctx.fail('invalid_range', `start_date ${input.start_date} is after end_date ${input.end_date}.`, {
109
+ ...ctx.recoveryFor('invalid_range'),
110
+ });
111
+ }
112
+ // Compute calendar days to decide inline vs spillover
113
+ const msPerDay = 86_400_000;
114
+ const dayCount = (new Date(input.end_date).getTime() - new Date(input.start_date).getTime()) / msPerDay + 1;
115
+ let raw;
116
+ let rows;
117
+ try {
118
+ ({ raw, rows } = await service.getTimeSeries(input.base_currency, input.quote_currency, input.start_date, input.end_date));
119
+ }
120
+ catch (err) {
121
+ const msg = err.message ?? '';
122
+ if (msg.includes('not found')) {
123
+ throw ctx.fail('unsupported_currency', `Currency "${input.base_currency}" or "${input.quote_currency}" not available from ECB.`, { ...ctx.recoveryFor('unsupported_currency') });
124
+ }
125
+ throw err;
126
+ }
127
+ const actualStart = raw.start_date;
128
+ const actualEnd = raw.end_date;
129
+ ctx.log.info('Fetched timeseries', {
130
+ base: input.base_currency,
131
+ quote: input.quote_currency,
132
+ start: actualStart,
133
+ end: actualEnd,
134
+ points: rows.length,
135
+ dayCount,
136
+ });
137
+ // Inline path — short range
138
+ const canvas = getCanvas();
139
+ const shouldSpill = canvas != null && dayCount > config.timeseriesCanvasThresholdDays;
140
+ if (!shouldSpill) {
141
+ const rateMap = {};
142
+ for (const row of rows)
143
+ rateMap[row.date] = row.rate;
144
+ return {
145
+ base_currency: input.base_currency.toUpperCase(),
146
+ quote_currency: input.quote_currency.toUpperCase(),
147
+ start_date: actualStart,
148
+ end_date: actualEnd,
149
+ rates: rateMap,
150
+ rate_count: rows.length,
151
+ rate_type: 'ECB reference (mid-market)',
152
+ source: 'ECB via Frankfurter',
153
+ spilled: false,
154
+ };
155
+ }
156
+ // Canvas spillover path
157
+ const instance = await canvas.acquire(input.canvas_id, ctx);
158
+ const tableName = `fx_${input.base_currency.toLowerCase()}_${input.quote_currency.toLowerCase()}`;
159
+ const spillResult = await spillover({
160
+ canvas: instance,
161
+ source: rows,
162
+ tableName,
163
+ previewChars: 40_000, // ~10k tokens preview
164
+ signal: ctx.signal,
165
+ });
166
+ ctx.log.info('Spilled to canvas', {
167
+ canvasId: instance.canvasId,
168
+ tableName,
169
+ rowCount: rows.length,
170
+ spilled: spillResult.spilled,
171
+ });
172
+ // Build inline rate map from preview rows for the response
173
+ const previewRates = {};
174
+ for (const row of spillResult.previewRows) {
175
+ const r = row;
176
+ previewRates[r.date] = r.rate;
177
+ }
178
+ if (spillResult.spilled) {
179
+ return {
180
+ base_currency: input.base_currency.toUpperCase(),
181
+ quote_currency: input.quote_currency.toUpperCase(),
182
+ start_date: actualStart,
183
+ end_date: actualEnd,
184
+ rates: previewRates,
185
+ rate_count: spillResult.handle.rowCount,
186
+ rate_type: 'ECB reference (mid-market)',
187
+ source: 'ECB via Frankfurter',
188
+ spilled: true,
189
+ canvas_id: instance.canvasId,
190
+ table_name: spillResult.handle.tableName,
191
+ };
192
+ }
193
+ // Fell under budget even at canvas threshold — return inline
194
+ return {
195
+ base_currency: input.base_currency.toUpperCase(),
196
+ quote_currency: input.quote_currency.toUpperCase(),
197
+ start_date: actualStart,
198
+ end_date: actualEnd,
199
+ rates: previewRates,
200
+ rate_count: spillResult.previewRows.length,
201
+ rate_type: 'ECB reference (mid-market)',
202
+ source: 'ECB via Frankfurter',
203
+ spilled: false,
204
+ };
205
+ },
206
+ format: (result) => {
207
+ const lines = [
208
+ `**${result.base_currency}/${result.quote_currency} time series** — ${result.start_date} to ${result.end_date}`,
209
+ `*${result.rate_count} business-day data points · ${result.rate_type} · ${result.source} · spilled: ${result.spilled}*`,
210
+ ];
211
+ if (result.spilled) {
212
+ lines.push(`\n📊 **Result staged on DataCanvas** (large range)`, `Canvas ID: \`${result.canvas_id}\``, `Table: \`${result.table_name}\``, `Use \`fx_dataframe_query\` with this canvas_id to run SQL. Preview (first entries below):`);
213
+ }
214
+ const rateEntries = Object.entries(result.rates)
215
+ .sort(([a], [b]) => a.localeCompare(b))
216
+ .slice(0, 20);
217
+ if (rateEntries.length > 0) {
218
+ lines.push('');
219
+ for (const [date, rate] of rateEntries) {
220
+ lines.push(`${date}: ${rate}`);
221
+ }
222
+ if (Object.keys(result.rates).length > 20) {
223
+ lines.push(`... (${Object.keys(result.rates).length - 20} more entries)`);
224
+ }
225
+ }
226
+ return [{ type: 'text', text: lines.join('\n') }];
227
+ },
228
+ });
229
+ //# sourceMappingURL=fx-get-timeseries.tool.js.map
@@ -0,0 +1 @@
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,14 @@
1
+ /**
2
+ * @fileoverview fx_list_currencies — lists all supported ISO 4217 currency codes.
3
+ * @module mcp-server/tools/definitions/fx-list-currencies.tool
4
+ */
5
+ import { z } from '@cyanheads/mcp-ts-core';
6
+ export declare const fxListCurrencies: import("@cyanheads/mcp-ts-core").ToolDefinition<z.ZodObject<{}, z.core.$strip>, z.ZodObject<{
7
+ currencies: z.ZodArray<z.ZodObject<{
8
+ code: z.ZodString;
9
+ name: z.ZodString;
10
+ }, z.core.$strip>>;
11
+ count: z.ZodNumber;
12
+ source: z.ZodString;
13
+ }, z.core.$strip>, undefined, undefined>;
14
+ //# sourceMappingURL=fx-list-currencies.tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fx-list-currencies.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-list-currencies.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAGjD,eAAO,MAAM,gBAAgB;;;;;;;wCA4C3B,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @fileoverview fx_list_currencies — lists all supported ISO 4217 currency codes.
3
+ * @module mcp-server/tools/definitions/fx-list-currencies.tool
4
+ */
5
+ import { tool, z } from '@cyanheads/mcp-ts-core';
6
+ import { getFrankfurterService } from '../../../services/frankfurter/frankfurter-service.js';
7
+ export const fxListCurrencies = tool('fx_list_currencies', {
8
+ description: 'List all supported ISO 4217 currency codes with their full names. ' +
9
+ 'Call this before converting to disambiguate "dollars" (USD vs AUD vs CAD vs HKD vs SGD) ' +
10
+ 'or to validate a user-supplied currency code. Covers the ~30 ECB reference currencies.',
11
+ annotations: {
12
+ readOnlyHint: true,
13
+ idempotentHint: true,
14
+ openWorldHint: true,
15
+ },
16
+ input: z.object({}),
17
+ output: z.object({
18
+ currencies: z
19
+ .array(z
20
+ .object({
21
+ code: z.string().describe('ISO 4217 currency code (e.g. USD, EUR, JPY).'),
22
+ name: z.string().describe('Full currency name (e.g. "United States Dollar").'),
23
+ })
24
+ .describe('A single supported currency.'))
25
+ .describe('All supported currencies, sorted alphabetically by code.'),
26
+ count: z.number().describe('Total number of supported currencies.'),
27
+ source: z.string().describe('Always "ECB via Frankfurter" — the upstream data provider.'),
28
+ }),
29
+ async handler(_input, ctx) {
30
+ const service = getFrankfurterService();
31
+ const currencies = await service.listCurrencies();
32
+ ctx.log.info('Listed currencies', { count: currencies.length });
33
+ return {
34
+ currencies,
35
+ count: currencies.length,
36
+ source: 'ECB via Frankfurter',
37
+ };
38
+ },
39
+ format: (result) => {
40
+ const lines = [
41
+ `**${result.count} supported currencies** (${result.source})\n`,
42
+ ...result.currencies.map((c) => `**${c.code}** — ${c.name}`),
43
+ ];
44
+ return [{ type: 'text', text: lines.join('\n') }];
45
+ },
46
+ });
47
+ //# sourceMappingURL=fx-list-currencies.tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fx-list-currencies.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/fx-list-currencies.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAC;AAEtF,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,oBAAoB,EAAE;IACzD,WAAW,EACT,oEAAoE;QACpE,0FAA0F;QAC1F,wFAAwF;IAC1F,WAAW,EAAE;QACX,YAAY,EAAE,IAAI;QAClB,cAAc,EAAE,IAAI;QACpB,aAAa,EAAE,IAAI;KACpB;IACD,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;IACnB,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,UAAU,EAAE,CAAC;aACV,KAAK,CACJ,CAAC;aACE,MAAM,CAAC;YACN,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,8CAA8C,CAAC;YACzE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,mDAAmD,CAAC;SAC/E,CAAC;aACD,QAAQ,CAAC,8BAA8B,CAAC,CAC5C;aACA,QAAQ,CAAC,0DAA0D,CAAC;QACvE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,uCAAuC,CAAC;QACnE,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,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,cAAc,EAAE,CAAC;QAClD,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,mBAAmB,EAAE,EAAE,KAAK,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;QAChE,OAAO;YACL,UAAU;YACV,KAAK,EAAE,UAAU,CAAC,MAAM;YACxB,MAAM,EAAE,qBAAqB;SAC9B,CAAC;IACJ,CAAC;IAED,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE;QACjB,MAAM,KAAK,GAAG;YACZ,KAAK,MAAM,CAAC,KAAK,4BAA4B,MAAM,CAAC,MAAM,KAAK;YAC/D,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;SAC7D,CAAC;QACF,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACpD,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @fileoverview Barrel export for all tool definitions.
3
+ * @module mcp-server/tools/definitions/index
4
+ */
5
+ export { fxConvertCurrency } from './fx-convert-currency.tool.js';
6
+ export { fxDataframeDescribe } from './fx-dataframe-describe.tool.js';
7
+ export { fxDataframeQuery } from './fx-dataframe-query.tool.js';
8
+ export { fxGetRate } from './fx-get-rate.tool.js';
9
+ export { fxGetRates } from './fx-get-rates.tool.js';
10
+ export { fxGetTimeseries } from './fx-get-timeseries.tool.js';
11
+ export { fxListCurrencies } from './fx-list-currencies.tool.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @fileoverview Barrel export for all tool definitions.
3
+ * @module mcp-server/tools/definitions/index
4
+ */
5
+ export { fxConvertCurrency } from './fx-convert-currency.tool.js';
6
+ export { fxDataframeDescribe } from './fx-dataframe-describe.tool.js';
7
+ export { fxDataframeQuery } from './fx-dataframe-query.tool.js';
8
+ export { fxGetRate } from './fx-get-rate.tool.js';
9
+ export { fxGetRates } from './fx-get-rates.tool.js';
10
+ export { fxGetTimeseries } from './fx-get-timeseries.tool.js';
11
+ export { fxListCurrencies } from './fx-list-currencies.tool.js';
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @fileoverview DataCanvas accessor — module-level singleton for the canvas service.
3
+ * @module services/canvas/canvas-accessor
4
+ */
5
+ import type { DataCanvas } from '@cyanheads/mcp-ts-core/canvas';
6
+ /** Set the canvas instance (called from createApp setup callback). */
7
+ export declare const setCanvas: (c: DataCanvas | undefined) => void;
8
+ /** Get the canvas instance, or undefined if canvas is not enabled. */
9
+ export declare const getCanvas: () => DataCanvas | undefined;
10
+ //# sourceMappingURL=canvas-accessor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-accessor.d.ts","sourceRoot":"","sources":["../../../src/services/canvas/canvas-accessor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAIhE,sEAAsE;AACtE,eAAO,MAAM,SAAS,GAAI,GAAG,UAAU,GAAG,SAAS,KAAG,IAErD,CAAC;AAEF,sEAAsE;AACtE,eAAO,MAAM,SAAS,QAAO,UAAU,GAAG,SAAoB,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @fileoverview DataCanvas accessor — module-level singleton for the canvas service.
3
+ * @module services/canvas/canvas-accessor
4
+ */
5
+ let _canvas;
6
+ /** Set the canvas instance (called from createApp setup callback). */
7
+ export const setCanvas = (c) => {
8
+ _canvas = c;
9
+ };
10
+ /** Get the canvas instance, or undefined if canvas is not enabled. */
11
+ export const getCanvas = () => _canvas;
12
+ //# sourceMappingURL=canvas-accessor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-accessor.js","sourceRoot":"","sources":["../../../src/services/canvas/canvas-accessor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,IAAI,OAA+B,CAAC;AAEpC,sEAAsE;AACtE,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAyB,EAAQ,EAAE;IAC3D,OAAO,GAAG,CAAC,CAAC;AACd,CAAC,CAAC;AAEF,sEAAsE;AACtE,MAAM,CAAC,MAAM,SAAS,GAAG,GAA2B,EAAE,CAAC,OAAO,CAAC"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @fileoverview Frankfurter API service — wraps ECB rate data from api.frankfurter.dev.
3
+ * @module services/frankfurter/frankfurter-service
4
+ */
5
+ import type { Currency, FrankfurterRateResponse, FrankfurterSeriesResponse, ResolvedRate, SeriesRow } from './types.js';
6
+ /** ECB data start date. Requests before this return 404 from Frankfurter. */
7
+ declare const ECB_START_DATE = "1999-01-04";
8
+ /** Retrieve the singleton FrankfurterService (lazy-init). */
9
+ export declare function getFrankfurterService(): FrankfurterService;
10
+ /** @internal — exposed for testing only */
11
+ export declare function resetFrankfurterService(): void;
12
+ declare class FrankfurterService {
13
+ private readonly baseUrl;
14
+ constructor();
15
+ /** Fetch all supported currencies as a sorted array. */
16
+ listCurrencies(): Promise<Currency[]>;
17
+ /**
18
+ * Get the exchange rate for a currency pair on a given date.
19
+ * Handles cross-rate triangulation through EUR automatically.
20
+ *
21
+ * Frankfurter's base must be EUR for ECB data — but it also supports
22
+ * arbitrary base via its own math. We just set base={upper_base} and
23
+ * symbols={upper_quote} and let Frankfurter handle the triangulation.
24
+ *
25
+ * @param base - ISO 4217 base currency code
26
+ * @param quote - ISO 4217 quote currency code
27
+ * @param date - ISO 8601 date string, or 'latest'
28
+ */
29
+ getRate(base: string, quote: string, date: string): Promise<ResolvedRate>;
30
+ /**
31
+ * Fetch all rates for a base currency at latest or historical date.
32
+ * Optionally filter to a specific set of symbols.
33
+ */
34
+ getRates(base: string, date: string, symbols?: string[]): Promise<FrankfurterRateResponse>;
35
+ /**
36
+ * Fetch a historical rate series for a currency pair.
37
+ * Returns raw API response plus derived rows suitable for canvas registration.
38
+ */
39
+ getTimeSeries(base: string, quote: string, startDate: string, endDate: string): Promise<{
40
+ raw: FrankfurterSeriesResponse;
41
+ rows: SeriesRow[];
42
+ }>;
43
+ private fetchJson;
44
+ }
45
+ export { ECB_START_DATE };
46
+ //# sourceMappingURL=frankfurter-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frankfurter-service.d.ts","sourceRoot":"","sources":["../../../src/services/frankfurter/frankfurter-service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EACV,QAAQ,EAER,uBAAuB,EACvB,yBAAyB,EACzB,YAAY,EACZ,SAAS,EACV,MAAM,YAAY,CAAC;AAEpB,6EAA6E;AAC7E,QAAA,MAAM,cAAc,eAAe,CAAC;AAuBpC,6DAA6D;AAC7D,wBAAgB,qBAAqB,IAAI,kBAAkB,CAG1D;AAED,2CAA2C;AAC3C,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAED,cAAM,kBAAkB;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;;IAQjC,wDAAwD;IAClD,cAAc,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IAS3C;;;;;;;;;;;OAWG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAgD/E;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAiB1F;;;OAGG;IACG,aAAa,CACjB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;QAAE,GAAG,EAAE,yBAAyB,CAAC;QAAC,IAAI,EAAE,SAAS,EAAE,CAAA;KAAE,CAAC;IA0BjE,OAAO,CAAC,SAAS;CAqClB;AAED,OAAO,EAAE,cAAc,EAAE,CAAC"}
@@ -0,0 +1,181 @@
1
+ /**
2
+ * @fileoverview Frankfurter API service — wraps ECB rate data from api.frankfurter.dev.
3
+ * @module services/frankfurter/frankfurter-service
4
+ */
5
+ import { serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
6
+ import { withRetry } from '@cyanheads/mcp-ts-core/utils';
7
+ import { getServerConfig } from '../../config/server-config.js';
8
+ /** ECB data start date. Requests before this return 404 from Frankfurter. */
9
+ const ECB_START_DATE = '1999-01-04';
10
+ /** YYYY-MM-DD format guard. */
11
+ const DATE_FORMAT_RE = /^\d{4}-\d{2}-\d{2}$/;
12
+ /** ISO 4217 currency code guard: 1-4 uppercase letters (covers standard 3-letter codes + rare 4-letter ones). */
13
+ const CURRENCY_CODE_RE = /^[A-Za-z]{1,4}$/;
14
+ /** Validate a date string is YYYY-MM-DD. Throws on bad format. */
15
+ function assertDateFormat(date, label) {
16
+ if (!DATE_FORMAT_RE.test(date)) {
17
+ throw new Error(`not found: ${label} "${date}" is not a valid YYYY-MM-DD date`);
18
+ }
19
+ }
20
+ /** Validate a currency code is safe to embed in a URL. Throws on bad format. */
21
+ function assertCurrencyFormat(code, label) {
22
+ if (!CURRENCY_CODE_RE.test(code)) {
23
+ throw new Error(`not found: ${label} "${code}" is not a valid ISO 4217 currency code`);
24
+ }
25
+ }
26
+ let _service;
27
+ /** Retrieve the singleton FrankfurterService (lazy-init). */
28
+ export function getFrankfurterService() {
29
+ _service ??= new FrankfurterService();
30
+ return _service;
31
+ }
32
+ /** @internal — exposed for testing only */
33
+ export function resetFrankfurterService() {
34
+ _service = undefined;
35
+ }
36
+ class FrankfurterService {
37
+ baseUrl;
38
+ constructor() {
39
+ this.baseUrl = getServerConfig().frankfurterBaseUrl;
40
+ }
41
+ // ── Currencies ──────────────────────────────────────────────────────────────
42
+ /** Fetch all supported currencies as a sorted array. */
43
+ async listCurrencies() {
44
+ const map = await this.fetchJson('/currencies');
45
+ return Object.entries(map)
46
+ .map(([code, name]) => ({ code, name }))
47
+ .sort((a, b) => a.code.localeCompare(b.code));
48
+ }
49
+ // ── Rate (point-in-time) ────────────────────────────────────────────────────
50
+ /**
51
+ * Get the exchange rate for a currency pair on a given date.
52
+ * Handles cross-rate triangulation through EUR automatically.
53
+ *
54
+ * Frankfurter's base must be EUR for ECB data — but it also supports
55
+ * arbitrary base via its own math. We just set base={upper_base} and
56
+ * symbols={upper_quote} and let Frankfurter handle the triangulation.
57
+ *
58
+ * @param base - ISO 4217 base currency code
59
+ * @param quote - ISO 4217 quote currency code
60
+ * @param date - ISO 8601 date string, or 'latest'
61
+ */
62
+ async getRate(base, quote, date) {
63
+ const upper_base = base.toUpperCase();
64
+ const upper_quote = quote.toUpperCase();
65
+ assertCurrencyFormat(upper_base, 'base_currency');
66
+ assertCurrencyFormat(upper_quote, 'quote_currency');
67
+ if (date !== 'latest')
68
+ assertDateFormat(date, 'date');
69
+ if (upper_base === upper_quote) {
70
+ // Identity rate — no API call needed
71
+ const today = new Date().toISOString().slice(0, 10);
72
+ return {
73
+ baseCurrency: upper_base,
74
+ quoteCurrency: upper_quote,
75
+ rate: 1,
76
+ rateDate: date === 'latest' ? today : date,
77
+ dateSnapped: false,
78
+ rateType: 'ECB reference (mid-market)',
79
+ source: 'ECB via Frankfurter',
80
+ };
81
+ }
82
+ // Frankfurter supports arbitrary base — it triangulates through EUR internally.
83
+ // Single call: base={upper_base}&symbols={upper_quote}
84
+ const endpoint = date === 'latest' ? '/latest' : `/${encodeURIComponent(date)}`;
85
+ const params = new URLSearchParams({ base: upper_base, symbols: upper_quote });
86
+ const url = `${endpoint}?${params}`;
87
+ const raw = await this.fetchJson(url);
88
+ const rate = raw.rates[upper_quote];
89
+ if (rate === undefined) {
90
+ throw new Error(`not found: ${upper_quote} not in response rates`);
91
+ }
92
+ const dateSnapped = date !== 'latest' && raw.date !== date;
93
+ return {
94
+ baseCurrency: upper_base,
95
+ quoteCurrency: upper_quote,
96
+ rate,
97
+ rateDate: raw.date,
98
+ dateSnapped,
99
+ rateType: 'ECB reference (mid-market)',
100
+ source: 'ECB via Frankfurter',
101
+ };
102
+ }
103
+ // ── Bulk rates snapshot ─────────────────────────────────────────────────────
104
+ /**
105
+ * Fetch all rates for a base currency at latest or historical date.
106
+ * Optionally filter to a specific set of symbols.
107
+ */
108
+ getRates(base, date, symbols) {
109
+ const upper_base = base.toUpperCase();
110
+ assertCurrencyFormat(upper_base, 'base_currency');
111
+ if (date !== 'latest')
112
+ assertDateFormat(date, 'date');
113
+ if (symbols) {
114
+ for (const sym of symbols)
115
+ assertCurrencyFormat(sym.toUpperCase(), 'symbol');
116
+ }
117
+ const endpoint = date === 'latest' ? '/latest' : `/${encodeURIComponent(date)}`;
118
+ const params = new URLSearchParams({ base: upper_base });
119
+ if (symbols && symbols.length > 0)
120
+ params.set('symbols', symbols.map((s) => s.toUpperCase()).join(','));
121
+ const url = `${endpoint}?${params}`;
122
+ return this.fetchJson(url);
123
+ }
124
+ // ── Time-series ─────────────────────────────────────────────────────────────
125
+ /**
126
+ * Fetch a historical rate series for a currency pair.
127
+ * Returns raw API response plus derived rows suitable for canvas registration.
128
+ */
129
+ async getTimeSeries(base, quote, startDate, endDate) {
130
+ const upper_base = base.toUpperCase();
131
+ const upper_quote = quote.toUpperCase();
132
+ assertCurrencyFormat(upper_base, 'base_currency');
133
+ assertCurrencyFormat(upper_quote, 'quote_currency');
134
+ assertDateFormat(startDate, 'start_date');
135
+ assertDateFormat(endDate, 'end_date');
136
+ const params = new URLSearchParams({ base: upper_base, symbols: upper_quote });
137
+ const url = `/${encodeURIComponent(startDate)}..${encodeURIComponent(endDate)}?${params}`;
138
+ const raw = await this.fetchJson(url);
139
+ const rows = Object.entries(raw.rates)
140
+ .sort(([a], [b]) => a.localeCompare(b))
141
+ .map(([date, rateMap]) => ({
142
+ date,
143
+ rate: rateMap[upper_quote] ?? 0,
144
+ base_currency: upper_base,
145
+ quote_currency: upper_quote,
146
+ }));
147
+ return { raw, rows };
148
+ }
149
+ // ── HTTP helpers ────────────────────────────────────────────────────────────
150
+ fetchJson(path) {
151
+ const url = `${this.baseUrl}${path}`;
152
+ return withRetry(async () => {
153
+ let response;
154
+ try {
155
+ response = await fetch(url, {
156
+ headers: {
157
+ Accept: 'application/json',
158
+ 'User-Agent': 'exchange-rates-mcp-server/0.1.1',
159
+ },
160
+ signal: AbortSignal.timeout(10_000),
161
+ });
162
+ }
163
+ catch (err) {
164
+ throw serviceUnavailable(`Frankfurter API unreachable: ${err.message}`, { url }, { cause: err });
165
+ }
166
+ if (!response.ok) {
167
+ if (response.status === 404) {
168
+ const body = await response.text().catch(() => '');
169
+ throw new Error(`not found: ${body || 'date out of range or unknown currency'}`);
170
+ }
171
+ throw serviceUnavailable(`Frankfurter API error ${response.status}`, {
172
+ url,
173
+ status: response.status,
174
+ });
175
+ }
176
+ return response.json();
177
+ }, { maxRetries: 2, baseDelayMs: 500 });
178
+ }
179
+ }
180
+ export { ECB_START_DATE };
181
+ //# sourceMappingURL=frankfurter-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frankfurter-service.js","sourceRoot":"","sources":["../../../src/services/frankfurter/frankfurter-service.ts"],"names":[],"mappings":"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"}