@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
|
@@ -2,18 +2,32 @@
|
|
|
2
2
|
* @fileoverview Frankfurter API service — wraps ECB rate data from api.frankfurter.dev.
|
|
3
3
|
* @module services/frankfurter/frankfurter-service
|
|
4
4
|
*/
|
|
5
|
-
import type { Currency, FrankfurterRateResponse,
|
|
5
|
+
import type { Currency, FrankfurterRateResponse, ResolvedRate, TimeSeriesResult } from './types.js';
|
|
6
6
|
/** ECB data start date. Requests before this return 404 from Frankfurter. */
|
|
7
7
|
declare const ECB_START_DATE = "1999-01-04";
|
|
8
|
+
/**
|
|
9
|
+
* True when `value` is a real calendar date written as YYYY-MM-DD.
|
|
10
|
+
* Rejects both malformed shapes (`2024-6-1`) and impossible dates (`2024-02-31`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function isIsoDate(value: string): boolean;
|
|
8
13
|
/** Retrieve the singleton FrankfurterService (lazy-init). */
|
|
9
14
|
export declare function getFrankfurterService(): FrankfurterService;
|
|
10
15
|
/** @internal — exposed for testing only */
|
|
11
16
|
export declare function resetFrankfurterService(): void;
|
|
12
17
|
declare class FrankfurterService {
|
|
13
18
|
private readonly baseUrl;
|
|
19
|
+
private currencyCache?;
|
|
14
20
|
constructor();
|
|
15
|
-
/** Fetch all supported currencies as a sorted array. */
|
|
21
|
+
/** Fetch all supported currencies as a sorted array. Served from the day-long cache. */
|
|
16
22
|
listCurrencies(): Promise<Currency[]>;
|
|
23
|
+
/** The supported currency set, fetched once per {@link CURRENCY_CACHE_TTL_MS}. */
|
|
24
|
+
private currencies;
|
|
25
|
+
/**
|
|
26
|
+
* Reject currency codes outside the ECB set before they reach the URL.
|
|
27
|
+
* Frankfurter answers an unknown base and an unknown symbol with the same
|
|
28
|
+
* bodiless 404, so membership is the only way to name the offending input.
|
|
29
|
+
*/
|
|
30
|
+
private assertSupportedCurrencies;
|
|
17
31
|
/**
|
|
18
32
|
* Get the exchange rate for a currency pair on a given date.
|
|
19
33
|
* Handles cross-rate triangulation through EUR automatically.
|
|
@@ -22,6 +36,13 @@ declare class FrankfurterService {
|
|
|
22
36
|
* arbitrary base via its own math. We just set base={upper_base} and
|
|
23
37
|
* symbols={upper_quote} and let Frankfurter handle the triangulation.
|
|
24
38
|
*
|
|
39
|
+
* A self-pair is 1 by definition and a 422 upstream, so it is quoted against a
|
|
40
|
+
* proxy currency instead — the rate is still local, but its date comes from the
|
|
41
|
+
* day the ECB actually published for the base. That keeps an identity rate's
|
|
42
|
+
* `rateDate` and `dateSnapped` telling the same story as any other pair's, and
|
|
43
|
+
* a date the ECB never published for that currency fails rather than answering
|
|
44
|
+
* with a fabricated publication date.
|
|
45
|
+
*
|
|
25
46
|
* @param base - ISO 4217 base currency code
|
|
26
47
|
* @param quote - ISO 4217 quote currency code
|
|
27
48
|
* @param date - ISO 8601 date string, or 'latest'
|
|
@@ -30,16 +51,29 @@ declare class FrankfurterService {
|
|
|
30
51
|
/**
|
|
31
52
|
* Fetch all rates for a base currency at latest or historical date.
|
|
32
53
|
* Optionally filter to a specific set of symbols.
|
|
54
|
+
*
|
|
55
|
+
* A currency's rate against itself is 1 by definition, and Frankfurter rejects
|
|
56
|
+
* a request whose only symbol is the base (422 "bad currency pair"). The base
|
|
57
|
+
* is therefore stripped from the upstream `symbols` and injected locally.
|
|
33
58
|
*/
|
|
34
59
|
getRates(base: string, date: string, symbols?: string[]): Promise<FrankfurterRateResponse>;
|
|
35
60
|
/**
|
|
36
|
-
* Fetch a historical rate series for a currency pair
|
|
37
|
-
*
|
|
61
|
+
* Fetch a historical rate series for a currency pair, clipped to the requested
|
|
62
|
+
* window and suitable for canvas registration.
|
|
63
|
+
*
|
|
64
|
+
* Two upstream behaviours are absorbed here:
|
|
65
|
+
*
|
|
66
|
+
* - A range that opens on a weekend or a TARGET holiday is snapped back to the
|
|
67
|
+
* prior publication day, so the response can carry dates the caller never
|
|
68
|
+
* asked for. Those rows are dropped, which leaves an empty series whenever
|
|
69
|
+
* the window contains no publication day at all.
|
|
70
|
+
* - A self-pair (`base === quote`) is a 422 upstream. It is answered locally
|
|
71
|
+
* with a rate of 1 on the days the ECB actually published for that currency,
|
|
72
|
+
* read off a proxy quote rather than a hand-rolled Mon–Fri calendar — the
|
|
73
|
+
* server has no TARGET holiday table, and per-currency gaps exist too (ISK
|
|
74
|
+
* was suspended from the reference set for a decade).
|
|
38
75
|
*/
|
|
39
|
-
getTimeSeries(base: string, quote: string, startDate: string, endDate: string): Promise<
|
|
40
|
-
raw: FrankfurterSeriesResponse;
|
|
41
|
-
rows: SeriesRow[];
|
|
42
|
-
}>;
|
|
76
|
+
getTimeSeries(base: string, quote: string, startDate: string, endDate: string): Promise<TimeSeriesResult>;
|
|
43
77
|
private fetchJson;
|
|
44
78
|
}
|
|
45
79
|
export { ECB_START_DATE };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frankfurter-service.d.ts","sourceRoot":"","sources":["../../../src/services/frankfurter/frankfurter-service.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"frankfurter-service.d.ts","sourceRoot":"","sources":["../../../src/services/frankfurter/frankfurter-service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,KAAK,EACV,QAAQ,EAER,uBAAuB,EAEvB,YAAY,EAEZ,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB,6EAA6E;AAC7E,QAAA,MAAM,cAAc,eAAe,CAAC;AAQpC;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAIhD;AAkBD,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;IACjC,OAAO,CAAC,aAAa,CAAC,CAA8D;IAEpF,cAEC;IAID,wFAAwF;IAClF,cAAc,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,CAE1C;IAED,kFAAkF;YACpE,UAAU;IAaxB;;;;OAIG;YACW,yBAAyB;IAQvC;;;;;;;;;;;;;;;;;;OAkBG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAgC9E;IAID;;;;;;;OAOG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,uBAAuB,CAAC,CA0B/F;IAID;;;;;;;;;;;;;;;OAeG;IACG,aAAa,CACjB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,gBAAgB,CAAC,CAyC3B;IAID,OAAO,CAAC,SAAS;CAsClB;AAED,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
|
@@ -3,25 +3,37 @@
|
|
|
3
3
|
* @module services/frankfurter/frankfurter-service
|
|
4
4
|
*/
|
|
5
5
|
import { serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
|
|
6
|
-
import { withRetry } from '@cyanheads/mcp-ts-core/utils';
|
|
6
|
+
import { httpErrorFromResponse, withRetry } from '@cyanheads/mcp-ts-core/utils';
|
|
7
7
|
import { getServerConfig } from '../../config/server-config.js';
|
|
8
|
+
import { invalidDateFormat, unsupportedCurrency, upstreamNoData } from './errors.js';
|
|
8
9
|
/** ECB data start date. Requests before this return 404 from Frankfurter. */
|
|
9
10
|
const ECB_START_DATE = '1999-01-04';
|
|
10
|
-
/** YYYY-MM-DD
|
|
11
|
+
/** YYYY-MM-DD shape guard — calendar validity is checked separately in {@link isIsoDate}. */
|
|
11
12
|
const DATE_FORMAT_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
12
|
-
/**
|
|
13
|
-
const
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
/** The ECB reference set changes at most a few times a decade — cache it for a day. */
|
|
14
|
+
const CURRENCY_CACHE_TTL_MS = 86_400_000;
|
|
15
|
+
/**
|
|
16
|
+
* True when `value` is a real calendar date written as YYYY-MM-DD.
|
|
17
|
+
* Rejects both malformed shapes (`2024-6-1`) and impossible dates (`2024-02-31`).
|
|
18
|
+
*/
|
|
19
|
+
export function isIsoDate(value) {
|
|
20
|
+
if (!DATE_FORMAT_RE.test(value))
|
|
21
|
+
return false;
|
|
22
|
+
const parsed = new Date(`${value}T00:00:00Z`);
|
|
23
|
+
return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
|
|
19
24
|
}
|
|
20
|
-
/**
|
|
21
|
-
function
|
|
22
|
-
if (!
|
|
23
|
-
throw
|
|
24
|
-
|
|
25
|
+
/** Boundary guard for date inputs. Throws `invalid_date_format` naming the field at fault. */
|
|
26
|
+
function assertDateFormat(date, field) {
|
|
27
|
+
if (!isIsoDate(date))
|
|
28
|
+
throw invalidDateFormat(field, date);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A stand-in quote currency for a self-pair, which Frankfurter answers with a 422.
|
|
32
|
+
* Any second currency reveals the days the ECB actually published for the base;
|
|
33
|
+
* EUR is in the reference set for the whole ECB history, so it is the default.
|
|
34
|
+
*/
|
|
35
|
+
function proxyQuoteFor(base) {
|
|
36
|
+
return base === 'EUR' ? 'USD' : 'EUR';
|
|
25
37
|
}
|
|
26
38
|
let _service;
|
|
27
39
|
/** Retrieve the singleton FrankfurterService (lazy-init). */
|
|
@@ -35,16 +47,38 @@ export function resetFrankfurterService() {
|
|
|
35
47
|
}
|
|
36
48
|
class FrankfurterService {
|
|
37
49
|
baseUrl;
|
|
50
|
+
currencyCache;
|
|
38
51
|
constructor() {
|
|
39
52
|
this.baseUrl = getServerConfig().frankfurterBaseUrl;
|
|
40
53
|
}
|
|
41
54
|
// ── Currencies ──────────────────────────────────────────────────────────────
|
|
42
|
-
/** Fetch all supported currencies as a sorted array. */
|
|
55
|
+
/** Fetch all supported currencies as a sorted array. Served from the day-long cache. */
|
|
43
56
|
async listCurrencies() {
|
|
57
|
+
return (await this.currencies()).list;
|
|
58
|
+
}
|
|
59
|
+
/** The supported currency set, fetched once per {@link CURRENCY_CACHE_TTL_MS}. */
|
|
60
|
+
async currencies() {
|
|
61
|
+
const cached = this.currencyCache;
|
|
62
|
+
if (cached && Date.now() - cached.fetchedAt <= CURRENCY_CACHE_TTL_MS)
|
|
63
|
+
return cached;
|
|
44
64
|
const map = await this.fetchJson('/currencies');
|
|
45
|
-
|
|
65
|
+
const list = Object.entries(map)
|
|
46
66
|
.map(([code, name]) => ({ code, name }))
|
|
47
67
|
.sort((a, b) => a.code.localeCompare(b.code));
|
|
68
|
+
const fresh = { codes: new Set(list.map((c) => c.code)), fetchedAt: Date.now(), list };
|
|
69
|
+
this.currencyCache = fresh;
|
|
70
|
+
return fresh;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Reject currency codes outside the ECB set before they reach the URL.
|
|
74
|
+
* Frankfurter answers an unknown base and an unknown symbol with the same
|
|
75
|
+
* bodiless 404, so membership is the only way to name the offending input.
|
|
76
|
+
*/
|
|
77
|
+
async assertSupportedCurrencies(field, codes) {
|
|
78
|
+
const { codes: supported } = await this.currencies();
|
|
79
|
+
const unsupported = codes.filter((code) => !supported.has(code));
|
|
80
|
+
if (unsupported.length > 0)
|
|
81
|
+
throw unsupportedCurrency(field, unsupported);
|
|
48
82
|
}
|
|
49
83
|
// ── Rate (point-in-time) ────────────────────────────────────────────────────
|
|
50
84
|
/**
|
|
@@ -55,6 +89,13 @@ class FrankfurterService {
|
|
|
55
89
|
* arbitrary base via its own math. We just set base={upper_base} and
|
|
56
90
|
* symbols={upper_quote} and let Frankfurter handle the triangulation.
|
|
57
91
|
*
|
|
92
|
+
* A self-pair is 1 by definition and a 422 upstream, so it is quoted against a
|
|
93
|
+
* proxy currency instead — the rate is still local, but its date comes from the
|
|
94
|
+
* day the ECB actually published for the base. That keeps an identity rate's
|
|
95
|
+
* `rateDate` and `dateSnapped` telling the same story as any other pair's, and
|
|
96
|
+
* a date the ECB never published for that currency fails rather than answering
|
|
97
|
+
* with a fabricated publication date.
|
|
98
|
+
*
|
|
58
99
|
* @param base - ISO 4217 base currency code
|
|
59
100
|
* @param quote - ISO 4217 quote currency code
|
|
60
101
|
* @param date - ISO 8601 date string, or 'latest'
|
|
@@ -62,33 +103,22 @@ class FrankfurterService {
|
|
|
62
103
|
async getRate(base, quote, date) {
|
|
63
104
|
const upper_base = base.toUpperCase();
|
|
64
105
|
const upper_quote = quote.toUpperCase();
|
|
65
|
-
assertCurrencyFormat(upper_base, 'base_currency');
|
|
66
|
-
assertCurrencyFormat(upper_quote, 'quote_currency');
|
|
67
106
|
if (date !== 'latest')
|
|
68
107
|
assertDateFormat(date, 'date');
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
}
|
|
108
|
+
await this.assertSupportedCurrencies('base_currency', [upper_base]);
|
|
109
|
+
await this.assertSupportedCurrencies('quote_currency', [upper_quote]);
|
|
110
|
+
const identity = upper_base === upper_quote;
|
|
111
|
+
const quoted = identity ? proxyQuoteFor(upper_base) : upper_quote;
|
|
82
112
|
// Frankfurter supports arbitrary base — it triangulates through EUR internally.
|
|
83
|
-
// Single call: base={upper_base}&symbols={
|
|
113
|
+
// Single call: base={upper_base}&symbols={quoted}
|
|
84
114
|
const endpoint = date === 'latest' ? '/latest' : `/${encodeURIComponent(date)}`;
|
|
85
|
-
const params = new URLSearchParams({ base: upper_base, symbols:
|
|
115
|
+
const params = new URLSearchParams({ base: upper_base, symbols: quoted });
|
|
86
116
|
const url = `${endpoint}?${params}`;
|
|
87
117
|
const raw = await this.fetchJson(url);
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
throw
|
|
91
|
-
|
|
118
|
+
const quotedRate = raw.rates[quoted];
|
|
119
|
+
if (quotedRate === undefined)
|
|
120
|
+
throw upstreamNoData(url);
|
|
121
|
+
const rate = identity ? 1 : quotedRate;
|
|
92
122
|
const dateSnapped = date !== 'latest' && raw.date !== date;
|
|
93
123
|
return {
|
|
94
124
|
baseCurrency: upper_base,
|
|
@@ -104,47 +134,94 @@ class FrankfurterService {
|
|
|
104
134
|
/**
|
|
105
135
|
* Fetch all rates for a base currency at latest or historical date.
|
|
106
136
|
* Optionally filter to a specific set of symbols.
|
|
137
|
+
*
|
|
138
|
+
* A currency's rate against itself is 1 by definition, and Frankfurter rejects
|
|
139
|
+
* a request whose only symbol is the base (422 "bad currency pair"). The base
|
|
140
|
+
* is therefore stripped from the upstream `symbols` and injected locally.
|
|
107
141
|
*/
|
|
108
|
-
getRates(base, date, symbols) {
|
|
142
|
+
async getRates(base, date, symbols) {
|
|
109
143
|
const upper_base = base.toUpperCase();
|
|
110
|
-
|
|
144
|
+
const upper_symbols = symbols?.map((s) => s.toUpperCase()) ?? [];
|
|
111
145
|
if (date !== 'latest')
|
|
112
146
|
assertDateFormat(date, 'date');
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
147
|
+
await this.assertSupportedCurrencies('base_currency', [upper_base]);
|
|
148
|
+
if (upper_symbols.length > 0)
|
|
149
|
+
await this.assertSupportedCurrencies('symbols', upper_symbols);
|
|
150
|
+
const selfQuoted = upper_symbols.includes(upper_base);
|
|
151
|
+
const upstreamSymbols = upper_symbols.filter((code) => code !== upper_base);
|
|
117
152
|
const endpoint = date === 'latest' ? '/latest' : `/${encodeURIComponent(date)}`;
|
|
118
153
|
const params = new URLSearchParams({ base: upper_base });
|
|
119
|
-
|
|
120
|
-
|
|
154
|
+
/**
|
|
155
|
+
* When the base was the only symbol requested there is nothing left to ask
|
|
156
|
+
* for, so the filter is dropped — the unfiltered snapshot still carries the
|
|
157
|
+
* publication date the caller needs, and the rates map is rebuilt below.
|
|
158
|
+
*/
|
|
159
|
+
if (upstreamSymbols.length > 0)
|
|
160
|
+
params.set('symbols', upstreamSymbols.join(','));
|
|
121
161
|
const url = `${endpoint}?${params}`;
|
|
122
|
-
|
|
162
|
+
const raw = await this.fetchJson(url);
|
|
163
|
+
if (!selfQuoted)
|
|
164
|
+
return raw;
|
|
165
|
+
return {
|
|
166
|
+
...raw,
|
|
167
|
+
rates: upstreamSymbols.length > 0 ? { ...raw.rates, [upper_base]: 1 } : { [upper_base]: 1 },
|
|
168
|
+
};
|
|
123
169
|
}
|
|
124
170
|
// ── Time-series ─────────────────────────────────────────────────────────────
|
|
125
171
|
/**
|
|
126
|
-
* Fetch a historical rate series for a currency pair
|
|
127
|
-
*
|
|
172
|
+
* Fetch a historical rate series for a currency pair, clipped to the requested
|
|
173
|
+
* window and suitable for canvas registration.
|
|
174
|
+
*
|
|
175
|
+
* Two upstream behaviours are absorbed here:
|
|
176
|
+
*
|
|
177
|
+
* - A range that opens on a weekend or a TARGET holiday is snapped back to the
|
|
178
|
+
* prior publication day, so the response can carry dates the caller never
|
|
179
|
+
* asked for. Those rows are dropped, which leaves an empty series whenever
|
|
180
|
+
* the window contains no publication day at all.
|
|
181
|
+
* - A self-pair (`base === quote`) is a 422 upstream. It is answered locally
|
|
182
|
+
* with a rate of 1 on the days the ECB actually published for that currency,
|
|
183
|
+
* read off a proxy quote rather than a hand-rolled Mon–Fri calendar — the
|
|
184
|
+
* server has no TARGET holiday table, and per-currency gaps exist too (ISK
|
|
185
|
+
* was suspended from the reference set for a decade).
|
|
128
186
|
*/
|
|
129
187
|
async getTimeSeries(base, quote, startDate, endDate) {
|
|
130
188
|
const upper_base = base.toUpperCase();
|
|
131
189
|
const upper_quote = quote.toUpperCase();
|
|
132
|
-
assertCurrencyFormat(upper_base, 'base_currency');
|
|
133
|
-
assertCurrencyFormat(upper_quote, 'quote_currency');
|
|
134
190
|
assertDateFormat(startDate, 'start_date');
|
|
135
191
|
assertDateFormat(endDate, 'end_date');
|
|
136
|
-
|
|
192
|
+
await this.assertSupportedCurrencies('base_currency', [upper_base]);
|
|
193
|
+
await this.assertSupportedCurrencies('quote_currency', [upper_quote]);
|
|
194
|
+
const identity = upper_base === upper_quote;
|
|
195
|
+
const quoted = identity ? proxyQuoteFor(upper_base) : upper_quote;
|
|
196
|
+
const params = new URLSearchParams({ base: upper_base, symbols: quoted });
|
|
137
197
|
const url = `/${encodeURIComponent(startDate)}..${encodeURIComponent(endDate)}?${params}`;
|
|
138
198
|
const raw = await this.fetchJson(url);
|
|
139
199
|
const rows = Object.entries(raw.rates)
|
|
200
|
+
.filter(([date]) => date >= startDate && date <= endDate)
|
|
140
201
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
141
|
-
.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
202
|
+
.flatMap(([date, rateMap]) => {
|
|
203
|
+
/**
|
|
204
|
+
* A day the quoted currency is missing from is not a publication day for
|
|
205
|
+
* this pair — dropping it is the only honest option, since any stand-in
|
|
206
|
+
* number would read as a real ECB rate downstream.
|
|
207
|
+
*/
|
|
208
|
+
const quotedRate = rateMap[quoted];
|
|
209
|
+
if (quotedRate === undefined)
|
|
210
|
+
return [];
|
|
211
|
+
return [
|
|
212
|
+
{
|
|
213
|
+
date,
|
|
214
|
+
rate: identity ? 1 : quotedRate,
|
|
215
|
+
base_currency: upper_base,
|
|
216
|
+
quote_currency: upper_quote,
|
|
217
|
+
},
|
|
218
|
+
];
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
rows,
|
|
222
|
+
startDate: rows[0]?.date ?? startDate,
|
|
223
|
+
endDate: rows.at(-1)?.date ?? endDate,
|
|
224
|
+
};
|
|
148
225
|
}
|
|
149
226
|
// ── HTTP helpers ────────────────────────────────────────────────────────────
|
|
150
227
|
fetchJson(path) {
|
|
@@ -155,7 +232,7 @@ class FrankfurterService {
|
|
|
155
232
|
response = await fetch(url, {
|
|
156
233
|
headers: {
|
|
157
234
|
Accept: 'application/json',
|
|
158
|
-
'User-Agent': 'exchange-rates-mcp-server/0.1.
|
|
235
|
+
'User-Agent': 'exchange-rates-mcp-server/0.1.5',
|
|
159
236
|
},
|
|
160
237
|
signal: AbortSignal.timeout(10_000),
|
|
161
238
|
});
|
|
@@ -164,14 +241,16 @@ class FrankfurterService {
|
|
|
164
241
|
throw serviceUnavailable(`Frankfurter API unreachable: ${err.message}`, { url }, { cause: err });
|
|
165
242
|
}
|
|
166
243
|
if (!response.ok) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Currency codes are checked against the live ECB set before the request,
|
|
246
|
+
* so a 404 here means the ECB published no rates for the requested date —
|
|
247
|
+
* not an unknown currency. Both this and the 4xx codes from
|
|
248
|
+
* `httpErrorFromResponse` classify as non-transient, so a deterministic
|
|
249
|
+
* input failure is surfaced immediately instead of burning retries.
|
|
250
|
+
*/
|
|
251
|
+
if (response.status === 404)
|
|
252
|
+
throw upstreamNoData(url);
|
|
253
|
+
throw await httpErrorFromResponse(response, { data: { url }, service: 'Frankfurter' });
|
|
175
254
|
}
|
|
176
255
|
return response.json();
|
|
177
256
|
}, { maxRetries: 2, baseDelayMs: 500 });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frankfurter-service.js","sourceRoot":"","sources":["../../../src/services/frankfurter/frankfurter-service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"frankfurter-service.js","sourceRoot":"","sources":["../../../src/services/frankfurter/frankfurter-service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAWrF,6EAA6E;AAC7E,MAAM,cAAc,GAAG,YAAY,CAAC;AAEpC,6FAA6F;AAC7F,MAAM,cAAc,GAAG,qBAAqB,CAAC;AAE7C,uFAAuF;AACvF,MAAM,qBAAqB,GAAG,UAAU,CAAC;AAEzC;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9C,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,GAAG,KAAK,YAAY,CAAC,CAAC;IAC9C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,CAAC;AACxF,CAAC;AAED,8FAA8F;AAC9F,SAAS,gBAAgB,CAAC,IAAY,EAAE,KAAa;IACnD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;QAAE,MAAM,iBAAiB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AAC7D,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;AACxC,CAAC;AAED,IAAI,QAAwC,CAAC;AAE7C,6DAA6D;AAC7D,MAAM,UAAU,qBAAqB;IACnC,QAAQ,KAAK,IAAI,kBAAkB,EAAE,CAAC;IACtC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,2CAA2C;AAC3C,MAAM,UAAU,uBAAuB;IACrC,QAAQ,GAAG,SAAS,CAAC;AACvB,CAAC;AAED,MAAM,kBAAkB;IACL,OAAO,CAAS;IACzB,aAAa,CAA+D;IAEpF;QACE,IAAI,CAAC,OAAO,GAAG,eAAe,EAAE,CAAC,kBAAkB,CAAC;IACtD,CAAC;IAED,+EAA+E;IAE/E,wFAAwF;IACxF,KAAK,CAAC,cAAc;QAClB,OAAO,CAAC,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,IAAI,CAAC;IACxC,CAAC;IAED,kFAAkF;IAC1E,KAAK,CAAC,UAAU;QACtB,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC;QAClC,IAAI,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,SAAS,IAAI,qBAAqB;YAAE,OAAO,MAAM,CAAC;QAEpF,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,CAAc,aAAa,CAAC,CAAC;QAC7D,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC;aAC7B,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;aACvC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAChD,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC;QACvF,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,yBAAyB,CAAC,KAAa,EAAE,KAAe;QACpE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;QACrD,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QACjE,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,mBAAmB,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;IAC5E,CAAC;IAED,+EAA+E;IAE/E;;;;;;;;;;;;;;;;;;OAkBG;IACH,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,KAAa,EAAE,IAAY;QACrD,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QACtC,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;QACxC,IAAI,IAAI,KAAK,QAAQ;YAAE,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACtD,MAAM,IAAI,CAAC,yBAAyB,CAAC,eAAe,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;QACpE,MAAM,IAAI,CAAC,yBAAyB,CAAC,gBAAgB,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;QAEtE,MAAM,QAAQ,GAAG,UAAU,KAAK,WAAW,CAAC;QAC5C,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;QAElE,gFAAgF;QAChF,kDAAkD;QAClD,MAAM,QAAQ,GAAG,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;QAChF,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;QAC1E,MAAM,GAAG,GAAG,GAAG,QAAQ,IAAI,MAAM,EAAE,CAAC;QAEpC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,CAA0B,GAAG,CAAC,CAAC;QAC/D,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,UAAU,KAAK,SAAS;YAAE,MAAM,cAAc,CAAC,GAAG,CAAC,CAAC;QACxD,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;QAEvC,MAAM,WAAW,GAAG,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC;QAE3D,OAAO;YACL,YAAY,EAAE,UAAU;YACxB,aAAa,EAAE,WAAW;YAC1B,IAAI;YACJ,QAAQ,EAAE,GAAG,CAAC,IAAI;YAClB,WAAW;YACX,QAAQ,EAAE,4BAA4B;YACtC,MAAM,EAAE,qBAAqB;SAC9B,CAAC;IACJ,CAAC;IAED,+EAA+E;IAE/E;;;;;;;OAOG;IACH,KAAK,CAAC,QAAQ,CAAC,IAAY,EAAE,IAAY,EAAE,OAAkB;QAC3D,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QACtC,MAAM,aAAa,GAAG,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;QACjE,IAAI,IAAI,KAAK,QAAQ;YAAE,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACtD,MAAM,IAAI,CAAC,yBAAyB,CAAC,eAAe,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;QACpE,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,IAAI,CAAC,yBAAyB,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;QAE7F,MAAM,UAAU,GAAG,aAAa,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QACtD,MAAM,eAAe,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;QAE5E,MAAM,QAAQ,GAAG,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;QAChF,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;QACzD;;;;WAIG;QACH,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QACjF,MAAM,GAAG,GAAG,GAAG,QAAQ,IAAI,MAAM,EAAE,CAAC;QACpC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,CAA0B,GAAG,CAAC,CAAC;QAE/D,IAAI,CAAC,UAAU;YAAE,OAAO,GAAG,CAAC;QAC5B,OAAO;YACL,GAAG,GAAG;YACN,KAAK,EAAE,eAAe,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,KAAK,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE;SAC5F,CAAC;IACJ,CAAC;IAED,+EAA+E;IAE/E;;;;;;;;;;;;;;;OAeG;IACH,KAAK,CAAC,aAAa,CACjB,IAAY,EACZ,KAAa,EACb,SAAiB,EACjB,OAAe;QAEf,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QACtC,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;QACxC,gBAAgB,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;QAC1C,gBAAgB,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QACtC,MAAM,IAAI,CAAC,yBAAyB,CAAC,eAAe,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;QACpE,MAAM,IAAI,CAAC,yBAAyB,CAAC,gBAAgB,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;QAEtE,MAAM,QAAQ,GAAG,UAAU,KAAK,WAAW,CAAC;QAC5C,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;QAElE,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;QAC1E,MAAM,GAAG,GAAG,IAAI,kBAAkB,CAAC,SAAS,CAAC,KAAK,kBAAkB,CAAC,OAAO,CAAC,IAAI,MAAM,EAAE,CAAC;QAC1F,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,CAA4B,GAAG,CAAC,CAAC;QAEjE,MAAM,IAAI,GAAgB,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;aAChD,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,SAAS,IAAI,IAAI,IAAI,OAAO,CAAC;aACxD,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;aACtC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,EAAE;YAC3B;;;;eAIG;YACH,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;YACnC,IAAI,UAAU,KAAK,SAAS;gBAAE,OAAO,EAAE,CAAC;YACxC,OAAO;gBACL;oBACE,IAAI;oBACJ,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU;oBAC/B,aAAa,EAAE,UAAU;oBACzB,cAAc,EAAE,WAAW;iBAC5B;aACF,CAAC;QACJ,CAAC,CAAC,CAAC;QAEL,OAAO;YACL,IAAI;YACJ,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,SAAS;YACrC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,OAAO;SACtC,CAAC;IACJ,CAAC;IAED,+EAA+E;IAEvE,SAAS,CAAI,IAAY;QAC/B,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,CAAC;QACrC,OAAO,SAAS,CACd,KAAK,IAAI,EAAE;YACT,IAAI,QAAkB,CAAC;YACvB,IAAI,CAAC;gBACH,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;oBAC1B,OAAO,EAAE;wBACP,MAAM,EAAE,kBAAkB;wBAC1B,YAAY,EAAE,iCAAiC;qBAChD;oBACD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC;iBACpC,CAAC,CAAC;YACL,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,kBAAkB,CACtB,gCAAiC,GAAa,CAAC,OAAO,EAAE,EACxD,EAAE,GAAG,EAAE,EACP,EAAE,KAAK,EAAE,GAAY,EAAE,CACxB,CAAC;YACJ,CAAC;YAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB;;;;;;mBAMG;gBACH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;oBAAE,MAAM,cAAc,CAAC,GAAG,CAAC,CAAC;gBACvD,MAAM,MAAM,qBAAqB,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;YACzF,CAAC;YAED,OAAO,QAAQ,CAAC,IAAI,EAAgB,CAAC;QACvC,CAAC,EACD,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,CACpC,CAAC;IACJ,CAAC;CACF;AAED,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
|
@@ -56,4 +56,18 @@ export interface SeriesRow extends Record<string, unknown> {
|
|
|
56
56
|
quote_currency: string;
|
|
57
57
|
rate: number;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* A time series clipped to the caller's requested window. Frankfurter's own
|
|
61
|
+
* `start_date`/`end_date` echo the window it *served*, which opens earlier than
|
|
62
|
+
* the requested one whenever the range starts on a non-publication day. These
|
|
63
|
+
* bounds never fall outside what the caller asked for.
|
|
64
|
+
*/
|
|
65
|
+
export interface TimeSeriesResult {
|
|
66
|
+
/** Last publication date inside the requested window; the requested end when there is none. */
|
|
67
|
+
endDate: string;
|
|
68
|
+
/** Publication days inside the requested window, ascending by date. May be empty. */
|
|
69
|
+
rows: SeriesRow[];
|
|
70
|
+
/** First publication date inside the requested window; the requested start when there is none. */
|
|
71
|
+
startDate: string;
|
|
72
|
+
}
|
|
59
73
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/frankfurter/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,iDAAiD;AACjD,MAAM,WAAW,WAAW;IAC1B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB;AAED,4CAA4C;AAC5C,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wDAAwD;AACxD,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACnC;AAED,+DAA+D;AAC/D,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG;YAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IACtD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,gDAAgD;AAChD,MAAM,WAAW,YAAY;IAC3B,8BAA8B;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,WAAW,EAAE,OAAO,CAAC;IACrB,+BAA+B;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,QAAQ,EAAE,4BAA4B,CAAC;IACvC,+BAA+B;IAC/B,MAAM,EAAE,qBAAqB,CAAC;CAC/B;AAED,uGAAuG;AACvG,MAAM,WAAW,SAAU,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACxD,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/frankfurter/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,iDAAiD;AACjD,MAAM,WAAW,WAAW;IAC1B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB;AAED,4CAA4C;AAC5C,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wDAAwD;AACxD,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACnC;AAED,+DAA+D;AAC/D,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG;YAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IACtD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,gDAAgD;AAChD,MAAM,WAAW,YAAY;IAC3B,8BAA8B;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,WAAW,EAAE,OAAO,CAAC;IACrB,+BAA+B;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,QAAQ,EAAE,4BAA4B,CAAC;IACvC,+BAA+B;IAC/B,MAAM,EAAE,qBAAqB,CAAC;CAC/B;AAED,uGAAuG;AACvG,MAAM,WAAW,SAAU,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACxD,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+FAA+F;IAC/F,OAAO,EAAE,MAAM,CAAC;IAChB,qFAAqF;IACrF,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,kGAAkG;IAClG,SAAS,EAAE,MAAM,CAAC;CACnB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanheads/exchange-rates-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"url": "https://github.com/cyanheads/exchange-rates-mcp-server/issues"
|
|
67
67
|
},
|
|
68
68
|
"homepage": "https://github.com/cyanheads/exchange-rates-mcp-server#readme",
|
|
69
|
-
"author": "
|
|
69
|
+
"author": "Casey Hand <casey@caseyjhand.com> (https://caseyjhand.com)",
|
|
70
70
|
"funding": [
|
|
71
71
|
{
|
|
72
72
|
"type": "github",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
}
|
|
79
79
|
],
|
|
80
80
|
"license": "Apache-2.0",
|
|
81
|
-
"packageManager": "bun@1.3.
|
|
81
|
+
"packageManager": "bun@1.3.14",
|
|
82
82
|
"engines": {
|
|
83
83
|
"bun": ">=1.3.0",
|
|
84
84
|
"node": ">=24.0.0"
|
|
@@ -87,18 +87,19 @@
|
|
|
87
87
|
"access": "public"
|
|
88
88
|
},
|
|
89
89
|
"dependencies": {
|
|
90
|
-
"@cyanheads/mcp-ts-core": "^0.
|
|
91
|
-
"@duckdb/node-api": "^1.5.
|
|
90
|
+
"@cyanheads/mcp-ts-core": "^0.11.0",
|
|
91
|
+
"@duckdb/node-api": "^1.5.5-r.1",
|
|
92
92
|
"pino-pretty": "^13.1.3",
|
|
93
93
|
"zod": "^4.4.3"
|
|
94
94
|
},
|
|
95
95
|
"devDependencies": {
|
|
96
|
-
"@biomejs/biome": "
|
|
97
|
-
"@
|
|
96
|
+
"@biomejs/biome": "2.5.5",
|
|
97
|
+
"@socketsecurity/bun-security-scanner": "^1.1.2",
|
|
98
|
+
"@types/node": "26.1.1",
|
|
98
99
|
"depcheck": "^1.4.7",
|
|
99
|
-
"ignore": "^7.0.
|
|
100
|
-
"tsc-alias": "^1.
|
|
101
|
-
"typescript": "^
|
|
102
|
-
"vitest": "^4.1.
|
|
100
|
+
"ignore": "^7.0.6",
|
|
101
|
+
"tsc-alias": "^1.9.1",
|
|
102
|
+
"typescript": "^7.0.2",
|
|
103
|
+
"vitest": "^4.1.10"
|
|
103
104
|
}
|
|
104
105
|
}
|
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.2.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.2.0",
|
|
23
23
|
"packageArguments": [
|
|
24
24
|
{
|
|
25
25
|
"type": "positional",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
"name": "FX_TIMESERIES_CANVAS_THRESHOLD_DAYS",
|
|
43
|
-
"description": "Day range above which fx_get_timeseries spills results to DataCanvas for SQL analytics.",
|
|
43
|
+
"description": "Day range above which fx_get_timeseries spills results to DataCanvas for SQL analytics, when DataCanvas is enabled (CANVAS_PROVIDER_TYPE=duckdb). Longer ranges return inline otherwise.",
|
|
44
44
|
"format": "string",
|
|
45
45
|
"isRequired": false,
|
|
46
46
|
"default": "90"
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
63
63
|
"identifier": "@cyanheads/exchange-rates-mcp-server",
|
|
64
64
|
"runtimeHint": "bun",
|
|
65
|
-
"version": "0.
|
|
65
|
+
"version": "0.2.0",
|
|
66
66
|
"packageArguments": [
|
|
67
67
|
{
|
|
68
68
|
"type": "positional",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
"name": "FX_TIMESERIES_CANVAS_THRESHOLD_DAYS",
|
|
86
|
-
"description": "Day range above which fx_get_timeseries spills results to DataCanvas for SQL analytics.",
|
|
86
|
+
"description": "Day range above which fx_get_timeseries spills results to DataCanvas for SQL analytics, when DataCanvas is enabled (CANVAS_PROVIDER_TYPE=duckdb). Longer ranges return inline otherwise.",
|
|
87
87
|
"format": "string",
|
|
88
88
|
"isRequired": false,
|
|
89
89
|
"default": "90"
|