@pipeworx/mcp-ecb 0.1.2 → 0.1.3
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/README.md +5 -5
- package/package.json +3 -3
- package/server.json +1 -1
- package/src/index.ts +111 -1
- package/src/server.ts +5 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
European Central Bank Data Portal MCP — exchange rates, interest rates, monetary aggregates, HICP, bank lending. No auth.
|
|
4
4
|
|
|
5
|
-
Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to
|
|
5
|
+
Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1819+ live data sources. This is an independent, unofficial integration — not affiliated with, endorsed by, or published by the upstream provider.
|
|
6
6
|
|
|
7
7
|
## Tools
|
|
8
8
|
|
|
@@ -71,19 +71,19 @@ directly, instead of just this one's:
|
|
|
71
71
|
}
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
Both URLs reach the same gateway and the same
|
|
74
|
+
Both URLs reach the same gateway and the same 1819+ data sources. The
|
|
75
75
|
only difference is which pack's tools are listed **directly**; `ask_pipeworx`
|
|
76
76
|
reaches all of them from either one.
|
|
77
77
|
|
|
78
78
|
## No MCP client? Call it over HTTP
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
curl -X POST https://gateway.pipeworx.io/v1/tools/
|
|
81
|
+
curl -X POST https://gateway.pipeworx.io/v1/tools/ecb_policy_rate \
|
|
82
82
|
-H 'Content-Type: application/json' \
|
|
83
|
-
-d '{
|
|
83
|
+
-d '{}'
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
No account needed for the first calls. Inspect any tool: `GET https://gateway.pipeworx.io/v1/tools/
|
|
86
|
+
No account needed for the first calls. Inspect any tool: `GET https://gateway.pipeworx.io/v1/tools/ecb_policy_rate`. Find one: `POST https://gateway.pipeworx.io/v1/tools/search_packs` with `{"query":"..."}`.
|
|
87
87
|
|
|
88
88
|
## Standalone (no gateway account)
|
|
89
89
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipeworx/mcp-ecb",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "ECB Data Portal MCP — European Central Bank statistical data",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@cloudflare/workers-types": "^4.20260405.1"
|
|
27
27
|
},
|
|
28
28
|
"pipeworx": {
|
|
29
|
-
"sourceHash": "v1-
|
|
30
|
-
"sourceCommit": "
|
|
29
|
+
"sourceHash": "v1-bfe61ace2c12fd6d1410588f76f6e3e05ed66708c4644931b3dc6a680afb929b",
|
|
30
|
+
"sourceCommit": "f5821587e59206a76fc200d1b9cbeaad15a549ab"
|
|
31
31
|
}
|
|
32
32
|
}
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.pipeworx-io/ecb",
|
|
4
4
|
"title": "Ecb",
|
|
5
5
|
"description": "ECB Data Portal MCP — European Central Bank statistical data",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.3",
|
|
7
7
|
"websiteUrl": "https://pipeworx.io/packs/ecb",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/pipeworx-io/mcp-ecb",
|
package/src/index.ts
CHANGED
|
@@ -687,6 +687,10 @@ function collapse(s: string): string {
|
|
|
687
687
|
* API docs: https://data.ecb.europa.eu/help/api/
|
|
688
688
|
*
|
|
689
689
|
* Tools:
|
|
690
|
+
* - ecb_policy_rate: current deposit facility rate / main refinancing rate /
|
|
691
|
+
* marginal lending rate, each with effective_since,
|
|
692
|
+
* previous_rate and decision history (FM flow, the three
|
|
693
|
+
* official "key ECB interest rates" — fleet #3206)
|
|
690
694
|
* - exchange_rate: EUR → currency rate (convenience over EXR flow)
|
|
691
695
|
* - hicp_inflation: Harmonised CPI YoY change (convenience over ICP flow)
|
|
692
696
|
* - get_data: generic SDMX query against any flow
|
|
@@ -704,7 +708,27 @@ async function pwFetch(url: string | URL, init?: RequestInit): Promise<Response>
|
|
|
704
708
|
|
|
705
709
|
const BASE = 'https://data-api.ecb.europa.eu/service';
|
|
706
710
|
|
|
711
|
+
// The three official "key ECB interest rates", FM flow, confirmed against the
|
|
712
|
+
// live series. DFR is the ECB's primary policy lever since the 2024 corridor
|
|
713
|
+
// narrowing (deposits now drive short-term money-market rates); MRO and MLFR
|
|
714
|
+
// are published alongside it and move on the same Governing Council dates.
|
|
715
|
+
const POLICY_RATE_SERIES: { field: string; label: string; key: string }[] = [
|
|
716
|
+
{ field: 'deposit_facility_rate', label: 'Deposit facility rate', key: 'D.U2.EUR.4F.KR.DFR.LEV' },
|
|
717
|
+
{ field: 'main_refinancing_rate', label: 'Main refinancing operations rate (fixed-rate tenders)', key: 'D.U2.EUR.4F.KR.MRR_FR.LEV' },
|
|
718
|
+
{ field: 'marginal_lending_facility_rate', label: 'Marginal lending facility rate', key: 'D.U2.EUR.4F.KR.MLFR.LEV' },
|
|
719
|
+
];
|
|
720
|
+
|
|
707
721
|
const tools: McpToolExport['tools'] = [
|
|
722
|
+
{
|
|
723
|
+
name: 'ecb_policy_rate',
|
|
724
|
+
description:
|
|
725
|
+
"The European Central Bank's three official key interest rates — the deposit facility rate (the ECB's " +
|
|
726
|
+
'primary policy rate since the 2024 operational-framework reform), the main refinancing operations rate, ' +
|
|
727
|
+
'and the marginal lending facility rate. For each: the current rate, the date it took effect, the previous ' +
|
|
728
|
+
'rate and when that one took effect, plus recent decision history (FM flow, series DFR/MRR_FR/MLFR). ' +
|
|
729
|
+
"Use this for \"ECB interest rate\" / \"ECB deposit rate\" / \"has the ECB cut rates\" instead of guessing an FM series key.",
|
|
730
|
+
inputSchema: { type: 'object', properties: {} },
|
|
731
|
+
},
|
|
708
732
|
{
|
|
709
733
|
name: 'ecb_exchange_rate',
|
|
710
734
|
description:
|
|
@@ -777,6 +801,8 @@ const tools: McpToolExport['tools'] = [
|
|
|
777
801
|
|
|
778
802
|
async function callTool(name: string, args: Record<string, unknown>): Promise<unknown> {
|
|
779
803
|
switch (name) {
|
|
804
|
+
case 'ecb_policy_rate':
|
|
805
|
+
return policyRate();
|
|
780
806
|
case 'ecb_exchange_rate':
|
|
781
807
|
return exchangeRate(args);
|
|
782
808
|
case 'ecb_hicp_inflation':
|
|
@@ -790,6 +816,76 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<un
|
|
|
790
816
|
}
|
|
791
817
|
}
|
|
792
818
|
|
|
819
|
+
/** {rate, effective_since, previous_rate, previous_rate_effective_since, decision_history, data_as_of} from an ascending {period,value} series. */
|
|
820
|
+
function computeRateChanges(obs: { period: string; value: number | null }[]) {
|
|
821
|
+
const pts = obs.filter((o) => o.value !== null) as { period: string; value: number }[];
|
|
822
|
+
if (pts.length === 0) return null;
|
|
823
|
+
const changes: { rate: number; effective_since: string }[] = [{ rate: pts[0].value, effective_since: pts[0].period }];
|
|
824
|
+
for (let i = 1; i < pts.length; i++) {
|
|
825
|
+
if (pts[i].value !== changes[changes.length - 1].rate) {
|
|
826
|
+
changes.push({ rate: pts[i].value, effective_since: pts[i].period });
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const current = changes[changes.length - 1];
|
|
830
|
+
const previous = changes.length > 1 ? changes[changes.length - 2] : null;
|
|
831
|
+
return {
|
|
832
|
+
rate: current.rate,
|
|
833
|
+
effective_since: current.effective_since,
|
|
834
|
+
previous_rate: previous?.rate ?? null,
|
|
835
|
+
previous_rate_effective_since: previous?.effective_since ?? null,
|
|
836
|
+
decision_history: changes.slice(-5).reverse(),
|
|
837
|
+
data_as_of: pts[pts.length - 1].period,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
async function policyRate() {
|
|
842
|
+
// ~6 years of daily observations — comfortably more than enough to see the
|
|
843
|
+
// last several Governing Council rate decisions.
|
|
844
|
+
const results = await Promise.all(
|
|
845
|
+
POLICY_RATE_SERIES.map(async (s) => {
|
|
846
|
+
try {
|
|
847
|
+
const data = await fetchData('FM', s.key, { start_period: '2020-01-01' });
|
|
848
|
+
const series = data.series[0]?.observations ?? [];
|
|
849
|
+
return { field: s.field, label: s.label, computed: computeRateChanges(series), fetchError: null as string | null };
|
|
850
|
+
} catch (err) {
|
|
851
|
+
// A fixed, verified-live key failing is an upstream fault, not a bad
|
|
852
|
+
// query — surface it per-field rather than failing the whole call,
|
|
853
|
+
// since the other two rates may well still be answerable.
|
|
854
|
+
return { field: s.field, label: s.label, computed: null, fetchError: err instanceof Error ? err.message : String(err) };
|
|
855
|
+
}
|
|
856
|
+
}),
|
|
857
|
+
);
|
|
858
|
+
const missing = results.filter((r) => r.computed === null);
|
|
859
|
+
if (missing.length === results.length) {
|
|
860
|
+
return {
|
|
861
|
+
error: 'no_data',
|
|
862
|
+
message:
|
|
863
|
+
'The ECB returned no observations for any of the key interest rate series (FM flow, DFR/MRR_FR/MLFR). ' +
|
|
864
|
+
'The series shape may have changed upstream; try get_data with flow_ref "FM" directly.',
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
const primary = results.find((r) => r.field === 'deposit_facility_rate')!;
|
|
868
|
+
const out: Record<string, unknown> = {
|
|
869
|
+
unit: 'percent',
|
|
870
|
+
// Aggregate fields first: the deposit facility rate is the ECB's primary
|
|
871
|
+
// policy lever since the 2024 corridor reform, so it carries the
|
|
872
|
+
// top-level {rate, effective_since, previous_rate, ...} shape directly —
|
|
873
|
+
// a caller asking "the ECB rate" does not have to know there are three.
|
|
874
|
+
...(primary.computed ?? {}),
|
|
875
|
+
};
|
|
876
|
+
for (const r of results) {
|
|
877
|
+
out[r.field] = r.computed
|
|
878
|
+
? { label: r.label, ...r.computed }
|
|
879
|
+
: { label: r.label, error: 'no_data', message: r.fetchError ?? `The ECB returned no observations for ${r.label}.` };
|
|
880
|
+
}
|
|
881
|
+
out.note =
|
|
882
|
+
"The ECB's three official key interest rates (deposit facility, main refinancing, marginal lending). " +
|
|
883
|
+
'The deposit facility rate has been the primary signal of the ECB monetary-policy stance since the ' +
|
|
884
|
+
"2024 operational-framework reform narrowed the corridor; top-level fields describe it. All three " +
|
|
885
|
+
'normally change together, on the same Governing Council decision date.';
|
|
886
|
+
return out;
|
|
887
|
+
}
|
|
888
|
+
|
|
793
889
|
async function exchangeRate(args: Record<string, unknown>) {
|
|
794
890
|
const currency = reqStr(args, 'currency', '"USD"').toUpperCase();
|
|
795
891
|
const freq = ((args.frequency as string) ?? 'D').toUpperCase();
|
|
@@ -836,9 +932,23 @@ async function fetchData(
|
|
|
836
932
|
if (opts.last_n) params.set('lastNObservations', String(opts.last_n));
|
|
837
933
|
const url = `${BASE}/data/${encodeURIComponent(flow)}/${encodeURIComponent(key)}?${params}`;
|
|
838
934
|
const res = await pwFetch(url, { headers: { Accept: 'application/json' } });
|
|
839
|
-
if (res.status === 404)
|
|
935
|
+
if (res.status === 404) {
|
|
936
|
+
throw new Error(
|
|
937
|
+
`not_found: the ECB has no series matching key "${key}" in flow "${flow}". The key does not exist at that ` +
|
|
938
|
+
'shape, not just empty for this period — check the dimension values, or call list_dataflows for valid flow_refs.',
|
|
939
|
+
);
|
|
940
|
+
}
|
|
840
941
|
if (!res.ok) {
|
|
841
942
|
const body = await res.text();
|
|
943
|
+
// A malformed key (wrong dimension count) gets ECB's generic error page —
|
|
944
|
+
// HTML, not SDMX JSON. Leaking it verbatim reads as a Pipeworx bug and
|
|
945
|
+
// buries the one useful fact (the request was malformed) under markup.
|
|
946
|
+
if (body.trimStart().startsWith('<')) {
|
|
947
|
+
throw new Error(
|
|
948
|
+
`user_error: the ECB rejected key "${key}" for flow "${flow}" as malformed (HTTP ${res.status}). ` +
|
|
949
|
+
'SDMX keys are dot-separated dimension values in a fixed order for that flow — check the dimension count.',
|
|
950
|
+
);
|
|
951
|
+
}
|
|
842
952
|
throw new Error(`ECB error: ${res.status} ${body.slice(0, 200)}`);
|
|
843
953
|
}
|
|
844
954
|
// ECB answers a 200 with a ZERO-BYTE body during an outage, and res.json()
|
package/src/server.ts
CHANGED
|
@@ -8,8 +8,9 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
9
9
|
import pack from './index.js';
|
|
10
10
|
|
|
11
|
+
|
|
11
12
|
const server = new Server(
|
|
12
|
-
{ name: '@pipeworx/mcp-ecb', version: '0.1.
|
|
13
|
+
{ name: '@pipeworx/mcp-ecb', version: '0.1.3' },
|
|
13
14
|
{ capabilities: { tools: {} } },
|
|
14
15
|
);
|
|
15
16
|
|
|
@@ -24,7 +25,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
24
25
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
25
26
|
const { name, arguments: args } = request.params;
|
|
26
27
|
try {
|
|
27
|
-
const
|
|
28
|
+
const callArgs = { ...((args ?? {}) as Record<string, unknown>) };
|
|
29
|
+
|
|
30
|
+
const result = await pack.callTool(name, callArgs);
|
|
28
31
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
29
32
|
} catch (err) {
|
|
30
33
|
return {
|