@cyanheads/eurostat-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.
- package/CLAUDE.md +391 -0
- package/Dockerfile +98 -0
- package/LICENSE +201 -0
- package/README.md +303 -0
- package/changelog/0.1.x/0.1.0.md +20 -0
- package/changelog/0.1.x/0.1.1.md +21 -0
- package/changelog/template.md +119 -0
- package/dist/config/server-config.d.ts +12 -0
- package/dist/config/server-config.d.ts.map +1 -0
- package/dist/config/server-config.js +28 -0
- package/dist/config/server-config.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server/resources/definitions/eurostat-dataset.resource.d.ts +28 -0
- package/dist/mcp-server/resources/definitions/eurostat-dataset.resource.d.ts.map +1 -0
- package/dist/mcp-server/resources/definitions/eurostat-dataset.resource.js +49 -0
- package/dist/mcp-server/resources/definitions/eurostat-dataset.resource.js.map +1 -0
- package/dist/mcp-server/tools/definitions/eurostat-browse-themes.tool.d.ts +30 -0
- package/dist/mcp-server/tools/definitions/eurostat-browse-themes.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/eurostat-browse-themes.tool.js +85 -0
- package/dist/mcp-server/tools/definitions/eurostat-browse-themes.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/eurostat-get-dataset-info.tool.d.ts +40 -0
- package/dist/mcp-server/tools/definitions/eurostat-get-dataset-info.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/eurostat-get-dataset-info.tool.js +106 -0
- package/dist/mcp-server/tools/definitions/eurostat-get-dataset-info.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/eurostat-get-dimension-values.tool.d.ts +31 -0
- package/dist/mcp-server/tools/definitions/eurostat-get-dimension-values.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/eurostat-get-dimension-values.tool.js +68 -0
- package/dist/mcp-server/tools/definitions/eurostat-get-dimension-values.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.d.ts +70 -0
- package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.js +152 -0
- package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/eurostat-search-datasets.tool.d.ts +31 -0
- package/dist/mcp-server/tools/definitions/eurostat-search-datasets.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/eurostat-search-datasets.tool.js +107 -0
- package/dist/mcp-server/tools/definitions/eurostat-search-datasets.tool.js.map +1 -0
- package/dist/services/eurostat-catalogue/eurostat-catalogue-service.d.ts +53 -0
- package/dist/services/eurostat-catalogue/eurostat-catalogue-service.d.ts.map +1 -0
- package/dist/services/eurostat-catalogue/eurostat-catalogue-service.js +254 -0
- package/dist/services/eurostat-catalogue/eurostat-catalogue-service.js.map +1 -0
- package/dist/services/eurostat-catalogue/types.d.ts +48 -0
- package/dist/services/eurostat-catalogue/types.d.ts.map +1 -0
- package/dist/services/eurostat-catalogue/types.js +6 -0
- package/dist/services/eurostat-catalogue/types.js.map +1 -0
- package/dist/services/eurostat-data/eurostat-data-service.d.ts +36 -0
- package/dist/services/eurostat-data/eurostat-data-service.d.ts.map +1 -0
- package/dist/services/eurostat-data/eurostat-data-service.js +278 -0
- package/dist/services/eurostat-data/eurostat-data-service.js.map +1 -0
- package/dist/services/eurostat-data/types.d.ts +109 -0
- package/dist/services/eurostat-data/types.d.ts.map +1 -0
- package/dist/services/eurostat-data/types.js +13 -0
- package/dist/services/eurostat-data/types.js.map +1 -0
- package/package.json +81 -0
- package/server.json +127 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Eurostat Catalogue Service — fetches and caches the TOC TXT file,
|
|
3
|
+
* provides dataset search and theme tree navigation.
|
|
4
|
+
* @module services/eurostat-catalogue/eurostat-catalogue-service
|
|
5
|
+
*/
|
|
6
|
+
import { notFound, serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
+
import { fetchWithTimeout, withRetry } from '@cyanheads/mcp-ts-core/utils';
|
|
8
|
+
import { getServerConfig } from '../../config/server-config.js';
|
|
9
|
+
// Context satisfies the runtime contract of RequestContext (requestId, tenantId, etc.)
|
|
10
|
+
// but lacks the [key: string]: unknown index signature required by fetchWithTimeout/withRetry.
|
|
11
|
+
const asReqCtx = (ctx) => ctx;
|
|
12
|
+
export class EurostatCatalogueService {
|
|
13
|
+
cache;
|
|
14
|
+
// config and storage accepted to match the standard service init pattern;
|
|
15
|
+
// this service uses only the Eurostat public API and per-request config.
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
|
17
|
+
constructor(_config, _storage) { }
|
|
18
|
+
/** Ensure the TOC is loaded, fetching if not yet cached. */
|
|
19
|
+
async ensureLoaded(ctx) {
|
|
20
|
+
if (this.cache)
|
|
21
|
+
return this.cache;
|
|
22
|
+
this.cache = await this.fetchAndParseToc(ctx);
|
|
23
|
+
return this.cache;
|
|
24
|
+
}
|
|
25
|
+
async fetchAndParseToc(ctx) {
|
|
26
|
+
const { baseUrl, requestTimeoutMs } = getServerConfig();
|
|
27
|
+
const url = `${baseUrl}/catalogue/toc/txt?lang=en`;
|
|
28
|
+
ctx.log.info('Fetching Eurostat TOC', { url });
|
|
29
|
+
const text = await withRetry(async () => {
|
|
30
|
+
const response = await fetchWithTimeout(url, requestTimeoutMs, asReqCtx(ctx), {
|
|
31
|
+
signal: ctx.signal,
|
|
32
|
+
});
|
|
33
|
+
const body = await response.text();
|
|
34
|
+
if (/^\s*<(!DOCTYPE\s+html|html[\s>])/i.test(body)) {
|
|
35
|
+
throw serviceUnavailable('Eurostat TOC returned HTML instead of TSV — likely rate-limited or temporarily unavailable.');
|
|
36
|
+
}
|
|
37
|
+
return body;
|
|
38
|
+
}, { operation: 'fetchToc', context: asReqCtx(ctx), baseDelayMs: 1000, signal: ctx.signal });
|
|
39
|
+
const entries = this.parseToc(text);
|
|
40
|
+
const codeIndex = new Map();
|
|
41
|
+
for (let i = 0; i < entries.length; i++) {
|
|
42
|
+
// noUncheckedIndexedAccess: entries[i] is always defined in a length-bounded loop
|
|
43
|
+
const e = entries[i];
|
|
44
|
+
if (e)
|
|
45
|
+
codeIndex.set(e.code, i);
|
|
46
|
+
}
|
|
47
|
+
ctx.log.info('TOC loaded', { entryCount: entries.length });
|
|
48
|
+
return { entries, codeIndex, loadedAt: new Date() };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse the TSV TOC format.
|
|
52
|
+
* Header: title, code, type, last update of data, last table structure change,
|
|
53
|
+
* data start, data end, values (datasets only — 7 vs 8 cols).
|
|
54
|
+
* Title column uses 4 spaces per depth level as indentation.
|
|
55
|
+
*/
|
|
56
|
+
parseToc(text) {
|
|
57
|
+
const lines = text.split('\n');
|
|
58
|
+
const entries = [];
|
|
59
|
+
// Stack of [depth, entryIndex] for tracking parent relationships.
|
|
60
|
+
const depthStack = [];
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
if (!line.trim())
|
|
63
|
+
continue;
|
|
64
|
+
// Split tab-separated fields. Each non-numeric field is double-quoted.
|
|
65
|
+
const cols = line.split('\t');
|
|
66
|
+
if (cols.length < 7)
|
|
67
|
+
continue;
|
|
68
|
+
// cols.length >= 7 is asserted above — these accesses are safe
|
|
69
|
+
const rawTitle = this.unquote(cols[0]);
|
|
70
|
+
if (!rawTitle)
|
|
71
|
+
continue;
|
|
72
|
+
// First column is the raw header row
|
|
73
|
+
const code = this.unquote(cols[1]);
|
|
74
|
+
const typeRaw = this.unquote(cols[2]);
|
|
75
|
+
// Skip header row
|
|
76
|
+
if (code === 'code')
|
|
77
|
+
continue;
|
|
78
|
+
if (!code)
|
|
79
|
+
continue;
|
|
80
|
+
const type = this.parseType(typeRaw);
|
|
81
|
+
const depth = this.measureDepth(rawTitle);
|
|
82
|
+
const label = rawTitle.trimStart();
|
|
83
|
+
// Resolve parent index from the depth stack
|
|
84
|
+
while (depthStack.length > 0) {
|
|
85
|
+
const top = depthStack[depthStack.length - 1];
|
|
86
|
+
if (top === undefined || top.depth < depth)
|
|
87
|
+
break;
|
|
88
|
+
depthStack.pop();
|
|
89
|
+
}
|
|
90
|
+
const stackTop = depthStack[depthStack.length - 1];
|
|
91
|
+
const parentIndex = stackTop !== undefined ? stackTop.index : -1;
|
|
92
|
+
const lastUpdated = cols[3] ? this.unquote(cols[3]) || undefined : undefined;
|
|
93
|
+
const dataStart = cols[5] ? this.unquote(cols[5]) || undefined : undefined;
|
|
94
|
+
const dataEnd = cols[6] ? this.unquote(cols[6]) || undefined : undefined;
|
|
95
|
+
// 8th column (index 7) is obs count — only present for datasets/tables
|
|
96
|
+
let obsCount;
|
|
97
|
+
const col7 = cols[7];
|
|
98
|
+
if (cols.length >= 8 && col7) {
|
|
99
|
+
const raw = col7.trim();
|
|
100
|
+
if (raw) {
|
|
101
|
+
const n = parseInt(raw, 10);
|
|
102
|
+
if (!isNaN(n))
|
|
103
|
+
obsCount = n;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const entry = {
|
|
107
|
+
label,
|
|
108
|
+
code,
|
|
109
|
+
type,
|
|
110
|
+
depth,
|
|
111
|
+
parentIndex,
|
|
112
|
+
...(lastUpdated && { lastUpdated }),
|
|
113
|
+
...(dataStart && { dataStart }),
|
|
114
|
+
...(dataEnd && { dataEnd }),
|
|
115
|
+
...(obsCount !== undefined && { obsCount }),
|
|
116
|
+
};
|
|
117
|
+
const idx = entries.length;
|
|
118
|
+
entries.push(entry);
|
|
119
|
+
depthStack.push({ depth, index: idx });
|
|
120
|
+
}
|
|
121
|
+
return entries;
|
|
122
|
+
}
|
|
123
|
+
unquote(s) {
|
|
124
|
+
const t = s?.trim() ?? '';
|
|
125
|
+
if (t.startsWith('"') && t.endsWith('"'))
|
|
126
|
+
return t.slice(1, -1).trim();
|
|
127
|
+
return t;
|
|
128
|
+
}
|
|
129
|
+
measureDepth(title) {
|
|
130
|
+
let spaces = 0;
|
|
131
|
+
for (const c of title) {
|
|
132
|
+
if (c === ' ')
|
|
133
|
+
spaces++;
|
|
134
|
+
else
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
return Math.floor(spaces / 4);
|
|
138
|
+
}
|
|
139
|
+
parseType(raw) {
|
|
140
|
+
if (raw === 'dataset')
|
|
141
|
+
return 'dataset';
|
|
142
|
+
if (raw === 'table')
|
|
143
|
+
return 'table';
|
|
144
|
+
return 'folder';
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Build a breadcrumb path from root to a given entry (exclusive of the entry itself).
|
|
148
|
+
*/
|
|
149
|
+
buildPath(entries, entryIndex) {
|
|
150
|
+
const path = [];
|
|
151
|
+
let current = entries[entryIndex];
|
|
152
|
+
while (current !== undefined && current.parentIndex >= 0) {
|
|
153
|
+
const parent = entries[current.parentIndex];
|
|
154
|
+
if (parent === undefined)
|
|
155
|
+
break;
|
|
156
|
+
path.unshift(parent.label);
|
|
157
|
+
current = parent;
|
|
158
|
+
}
|
|
159
|
+
return path;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Find the "root" of the TOC — the 11 second-level theme folders.
|
|
163
|
+
* The TOC has a single depth-0 root ("Database by themes"); we skip it
|
|
164
|
+
* and return its depth-1 children as the practical entry points.
|
|
165
|
+
*/
|
|
166
|
+
findRootChildren(entries) {
|
|
167
|
+
const rootIdx = entries.findIndex((e) => e.depth === 0 && e.type === 'folder');
|
|
168
|
+
if (rootIdx === -1) {
|
|
169
|
+
return entries.map((_, i) => i).filter((i) => (entries[i]?.depth ?? -1) === 0);
|
|
170
|
+
}
|
|
171
|
+
return entries.map((_, i) => i).filter((i) => entries[i]?.parentIndex === rootIdx);
|
|
172
|
+
}
|
|
173
|
+
/** Get immediate children of a folder code, or root theme folders if code is undefined. */
|
|
174
|
+
async browse(themeCode, ctx) {
|
|
175
|
+
const toc = await this.ensureLoaded(ctx);
|
|
176
|
+
if (!themeCode) {
|
|
177
|
+
const childIndexes = this.findRootChildren(toc.entries);
|
|
178
|
+
const items = childIndexes.flatMap((i) => {
|
|
179
|
+
const item = this.toBrowseItem(toc.entries, i);
|
|
180
|
+
return item ? [item] : [];
|
|
181
|
+
});
|
|
182
|
+
return { items, parentPath: [] };
|
|
183
|
+
}
|
|
184
|
+
const folderIdxMaybe = toc.codeIndex.get(themeCode);
|
|
185
|
+
const folderEntry = folderIdxMaybe !== undefined ? toc.entries[folderIdxMaybe] : undefined;
|
|
186
|
+
if (folderIdxMaybe === undefined ||
|
|
187
|
+
folderEntry === undefined ||
|
|
188
|
+
folderEntry.type !== 'folder') {
|
|
189
|
+
throw notFound(`Theme "${themeCode}" not found in the Eurostat TOC. Use eurostat_browse_themes without theme_code to see top-level themes, then navigate from there.`, { themeCode });
|
|
190
|
+
}
|
|
191
|
+
const folderIdx = folderIdxMaybe;
|
|
192
|
+
const childIndexes = toc.entries
|
|
193
|
+
.map((_, i) => i)
|
|
194
|
+
.filter((i) => toc.entries[i]?.parentIndex === folderIdx);
|
|
195
|
+
const items = childIndexes.flatMap((i) => {
|
|
196
|
+
const item = this.toBrowseItem(toc.entries, i);
|
|
197
|
+
return item ? [item] : [];
|
|
198
|
+
});
|
|
199
|
+
const parentPath = this.buildPath(toc.entries, folderIdx);
|
|
200
|
+
parentPath.push(folderEntry.label);
|
|
201
|
+
return { items, parentPath };
|
|
202
|
+
}
|
|
203
|
+
toBrowseItem(entries, index) {
|
|
204
|
+
const e = entries[index];
|
|
205
|
+
if (!e)
|
|
206
|
+
return;
|
|
207
|
+
const hasChildren = e.type === 'folder' && entries.some((c) => c.parentIndex === index);
|
|
208
|
+
return {
|
|
209
|
+
code: e.code,
|
|
210
|
+
label: e.label,
|
|
211
|
+
type: e.type,
|
|
212
|
+
hasChildren,
|
|
213
|
+
...(e.dataStart && { dataStart: e.dataStart }),
|
|
214
|
+
...(e.dataEnd && { dataEnd: e.dataEnd }),
|
|
215
|
+
...(e.obsCount !== undefined && { obsCount: e.obsCount }),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Search datasets by case-insensitive substring match against labels.
|
|
220
|
+
* Returns datasets only (not folders), up to `limit` results.
|
|
221
|
+
*/
|
|
222
|
+
async search(query, limit, ctx) {
|
|
223
|
+
const toc = await this.ensureLoaded(ctx);
|
|
224
|
+
const q = query.toLowerCase();
|
|
225
|
+
const matched = toc.entries
|
|
226
|
+
.map((e, i) => ({ e, i }))
|
|
227
|
+
.filter(({ e }) => e.type !== 'folder' && e.label.toLowerCase().includes(q));
|
|
228
|
+
const totalMatches = matched.length;
|
|
229
|
+
const sliced = matched.slice(0, limit);
|
|
230
|
+
const datasets = sliced.map(({ e, i }) => ({
|
|
231
|
+
code: e.code,
|
|
232
|
+
label: e.label,
|
|
233
|
+
type: e.type,
|
|
234
|
+
...(e.dataStart && { dataStart: e.dataStart }),
|
|
235
|
+
...(e.dataEnd && { dataEnd: e.dataEnd }),
|
|
236
|
+
...(e.lastUpdated && { lastUpdated: e.lastUpdated }),
|
|
237
|
+
...(e.obsCount !== undefined && { obsCount: e.obsCount }),
|
|
238
|
+
themePath: this.buildPath(toc.entries, i),
|
|
239
|
+
}));
|
|
240
|
+
return { datasets, totalMatches };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// --- Init/accessor pattern ---
|
|
244
|
+
let _service;
|
|
245
|
+
export function initEurostatCatalogueService(config, storage) {
|
|
246
|
+
_service = new EurostatCatalogueService(config, storage);
|
|
247
|
+
}
|
|
248
|
+
export function getEurostatCatalogueService() {
|
|
249
|
+
if (!_service) {
|
|
250
|
+
throw new Error('EurostatCatalogueService not initialized — call initEurostatCatalogueService() in setup()');
|
|
251
|
+
}
|
|
252
|
+
return _service;
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=eurostat-catalogue-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eurostat-catalogue-service.js","sourceRoot":"","sources":["../../../src/services/eurostat-catalogue/eurostat-catalogue-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Domain types for the Eurostat Catalogue (TOC) service.
|
|
3
|
+
* @module services/eurostat-catalogue/types
|
|
4
|
+
*/
|
|
5
|
+
/** A single entry from the parsed Eurostat TOC TXT file. */
|
|
6
|
+
export interface TocEntry {
|
|
7
|
+
/** Dataset or folder code (e.g., "nama_10_gdp", "econ"). */
|
|
8
|
+
code: string;
|
|
9
|
+
/** End of data coverage (e.g., "2025"). */
|
|
10
|
+
dataEnd?: string;
|
|
11
|
+
/** Start of data coverage (e.g., "1975"). */
|
|
12
|
+
dataStart?: string;
|
|
13
|
+
/** Depth level in the hierarchy (0-based). */
|
|
14
|
+
depth: number;
|
|
15
|
+
/** Human-readable label (with leading spaces stripped). */
|
|
16
|
+
label: string;
|
|
17
|
+
/** Date of last data update (e.g., "22.05.2026"). */
|
|
18
|
+
lastUpdated?: string;
|
|
19
|
+
/** Number of observations (datasets/tables only). */
|
|
20
|
+
obsCount?: number;
|
|
21
|
+
/** Index of parent entry in the flat entries array, or -1 for root. */
|
|
22
|
+
parentIndex: number;
|
|
23
|
+
/** Entry type from the raw TOC. */
|
|
24
|
+
type: 'dataset' | 'table' | 'folder';
|
|
25
|
+
}
|
|
26
|
+
/** A dataset result from search or browse. */
|
|
27
|
+
export interface DatasetResult {
|
|
28
|
+
code: string;
|
|
29
|
+
dataEnd?: string;
|
|
30
|
+
dataStart?: string;
|
|
31
|
+
label: string;
|
|
32
|
+
lastUpdated?: string;
|
|
33
|
+
obsCount?: number;
|
|
34
|
+
/** Breadcrumb path from root to this entry's parent folder. */
|
|
35
|
+
themePath: string[];
|
|
36
|
+
type: 'dataset' | 'table';
|
|
37
|
+
}
|
|
38
|
+
/** A theme or dataset item returned from browse. */
|
|
39
|
+
export interface BrowseItem {
|
|
40
|
+
code: string;
|
|
41
|
+
dataEnd?: string;
|
|
42
|
+
dataStart?: string;
|
|
43
|
+
hasChildren: boolean;
|
|
44
|
+
label: string;
|
|
45
|
+
obsCount?: number;
|
|
46
|
+
type: 'folder' | 'dataset' | 'table';
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/eurostat-catalogue/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,4DAA4D;AAC5D,MAAM,WAAW,QAAQ;IACvB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;CACtC;AAED,8CAA8C;AAC9C,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;CAC3B;AAED,oDAAoD;AACpD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;CACtC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/services/eurostat-catalogue/types.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Eurostat Data Service — HTTP client for the Statistics API (JSON-stat 2.0),
|
|
3
|
+
* includes JSON-stat parsing and stride-based index decoding.
|
|
4
|
+
* @module services/eurostat-data/eurostat-data-service
|
|
5
|
+
*/
|
|
6
|
+
import type { Context } from '@cyanheads/mcp-ts-core';
|
|
7
|
+
import type { AppConfig } from '@cyanheads/mcp-ts-core/config';
|
|
8
|
+
import type { StorageService } from '@cyanheads/mcp-ts-core/storage';
|
|
9
|
+
import type { DatasetMeta, DimensionValuesResult, GeoLevel, QueryResult } from './types.js';
|
|
10
|
+
export declare class EurostatDataService {
|
|
11
|
+
constructor(_config: AppConfig, _storage: StorageService);
|
|
12
|
+
private buildUrl;
|
|
13
|
+
private fetchJson;
|
|
14
|
+
private checkResponseErrors;
|
|
15
|
+
/**
|
|
16
|
+
* Decode a JSON-stat 2.0 response into labeled observations using stride-based indexing.
|
|
17
|
+
*/
|
|
18
|
+
private decodeObservations;
|
|
19
|
+
private extractAnnotation;
|
|
20
|
+
private extractMetadata;
|
|
21
|
+
/** Fetch dataset metadata using a minimal lastTimePeriod=1 query. */
|
|
22
|
+
getDatasetInfo(datasetCode: string, ctx: Context): Promise<DatasetMeta>;
|
|
23
|
+
/**
|
|
24
|
+
* Get all valid values for a specific dimension.
|
|
25
|
+
* For 'geo', applies geoLevel filter; for others, uses lastTimePeriod=1.
|
|
26
|
+
*/
|
|
27
|
+
getDimensionValues(datasetCode: string, dimension: string, geoLevel: GeoLevel | undefined, ctx: Context): Promise<DimensionValuesResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Query dataset observations with dimension filters.
|
|
30
|
+
* Returns decoded observations.
|
|
31
|
+
*/
|
|
32
|
+
queryDataset(datasetCode: string, filters: Record<string, string[]>, geoLevel: GeoLevel | undefined, sinceP: string | undefined, untilP: string | undefined, lastN: number | undefined, lang: string, ctx: Context): Promise<QueryResult>;
|
|
33
|
+
}
|
|
34
|
+
export declare function initEurostatDataService(config: AppConfig, storage: StorageService): void;
|
|
35
|
+
export declare function getEurostatDataService(): EurostatDataService;
|
|
36
|
+
//# sourceMappingURL=eurostat-data-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eurostat-data-service.d.ts","sourceRoot":"","sources":["../../../src/services/eurostat-data/eurostat-data-service.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAE/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAGrE,OAAO,KAAK,EACV,WAAW,EAEX,qBAAqB,EACrB,QAAQ,EAGR,WAAW,EACZ,MAAM,YAAY,CAAC;AAMpB,qBAAa,mBAAmB;gBAIlB,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,cAAc;IAExD,OAAO,CAAC,QAAQ;IAchB,OAAO,CAAC,SAAS;IA4BjB,OAAO,CAAC,mBAAmB;IAwC3B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAiE1B,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,eAAe;IAqCvB,qEAAqE;IAC/D,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC;IAO7E;;;OAGG;IACG,kBAAkB,CACtB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,GAAG,SAAS,EAC9B,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,qBAAqB,CAAC;IAqCjC;;;OAGG;IACG,YAAY,CAChB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EACjC,QAAQ,EAAE,QAAQ,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,WAAW,CAAC;CAoDxB;AAMD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAExF;AAED,wBAAgB,sBAAsB,IAAI,mBAAmB,CAO5D"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Eurostat Data Service — HTTP client for the Statistics API (JSON-stat 2.0),
|
|
3
|
+
* includes JSON-stat parsing and stride-based index decoding.
|
|
4
|
+
* @module services/eurostat-data/eurostat-data-service
|
|
5
|
+
*/
|
|
6
|
+
import { invalidParams, notFound, serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
+
import { fetchWithTimeout, withRetry } from '@cyanheads/mcp-ts-core/utils';
|
|
8
|
+
import { getServerConfig } from '../../config/server-config.js';
|
|
9
|
+
// Context satisfies the runtime contract of RequestContext but lacks the index signature
|
|
10
|
+
// required by fetchWithTimeout/withRetry.
|
|
11
|
+
const asReqCtx = (ctx) => ctx;
|
|
12
|
+
export class EurostatDataService {
|
|
13
|
+
// config and storage accepted to match the standard service init pattern;
|
|
14
|
+
// this service uses only the Eurostat public API and per-request config.
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
|
16
|
+
constructor(_config, _storage) { }
|
|
17
|
+
buildUrl(datasetCode, params) {
|
|
18
|
+
const { baseUrl } = getServerConfig();
|
|
19
|
+
const url = new URL(`${baseUrl}/statistics/1.0/data/${encodeURIComponent(datasetCode)}`);
|
|
20
|
+
url.searchParams.set('format', 'JSON');
|
|
21
|
+
for (const [key, value] of Object.entries(params)) {
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
for (const v of value)
|
|
24
|
+
url.searchParams.append(key, v);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
url.searchParams.set(key, value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return url;
|
|
31
|
+
}
|
|
32
|
+
fetchJson(url, ctx) {
|
|
33
|
+
const { requestTimeoutMs } = getServerConfig();
|
|
34
|
+
return withRetry(async () => {
|
|
35
|
+
const response = await fetchWithTimeout(url.toString(), requestTimeoutMs, asReqCtx(ctx), {
|
|
36
|
+
signal: ctx.signal,
|
|
37
|
+
});
|
|
38
|
+
const text = await response.text();
|
|
39
|
+
if (/^\s*<(!DOCTYPE\s+html|html[\s>])/i.test(text)) {
|
|
40
|
+
throw serviceUnavailable('Eurostat Statistics API returned HTML instead of JSON — likely rate-limited or temporarily unavailable.');
|
|
41
|
+
}
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(text);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw serviceUnavailable(`Eurostat Statistics API returned unparseable response. Response excerpt: ${text.slice(0, 200)}`);
|
|
48
|
+
}
|
|
49
|
+
this.checkResponseErrors(parsed, url.toString());
|
|
50
|
+
return parsed;
|
|
51
|
+
}, { operation: 'fetchJsonStat', context: asReqCtx(ctx), baseDelayMs: 1000, signal: ctx.signal });
|
|
52
|
+
}
|
|
53
|
+
checkResponseErrors(data, url) {
|
|
54
|
+
// Async response: query too large
|
|
55
|
+
if (data.warning?.status === 413) {
|
|
56
|
+
throw serviceUnavailable('Eurostat returned an asynchronous response — the query matched too many observations. ' +
|
|
57
|
+
'Add dimension filters (geo, unit, na_item, etc.) to reduce the result size and retry.', { reason: 'async_response', url });
|
|
58
|
+
}
|
|
59
|
+
// Error responses
|
|
60
|
+
if (Array.isArray(data.error) && data.error.length > 0) {
|
|
61
|
+
const err = data.error[0];
|
|
62
|
+
if (!err)
|
|
63
|
+
return; // noUncheckedIndexedAccess: length > 0 guarantees this, but guard for TS
|
|
64
|
+
if (err.status === 404 || err.id === 100) {
|
|
65
|
+
throw notFound(`Dataset not found. Eurostat error: ${err.label}. ` +
|
|
66
|
+
`Verify the dataset code with eurostat_search_datasets or eurostat_browse_themes.`, { reason: 'not_found', eurostatError: err });
|
|
67
|
+
}
|
|
68
|
+
if (err.status === 400) {
|
|
69
|
+
if (err.id === 150) {
|
|
70
|
+
throw invalidParams(`Invalid dimension code. Eurostat error: ${err.label}. ` +
|
|
71
|
+
`Use eurostat_get_dataset_info to see valid dimensions for this dataset.`, { reason: 'invalid_dimension', eurostatError: err });
|
|
72
|
+
}
|
|
73
|
+
throw invalidParams(`Bad request. Eurostat error: ${err.label}.`, {
|
|
74
|
+
reason: 'conflicting_params',
|
|
75
|
+
eurostatError: err,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
throw serviceUnavailable(`Eurostat API error ${err.status}: ${err.label}.`, {
|
|
79
|
+
eurostatError: err,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Decode a JSON-stat 2.0 response into labeled observations using stride-based indexing.
|
|
85
|
+
*/
|
|
86
|
+
decodeObservations(data) {
|
|
87
|
+
if (!data.id || !data.size || !data.dimension)
|
|
88
|
+
return [];
|
|
89
|
+
const dims = data.id;
|
|
90
|
+
const sizes = data.size;
|
|
91
|
+
const value = data.value ?? {};
|
|
92
|
+
const status = data.status ?? {};
|
|
93
|
+
const statusLabels = data.extension?.status?.label ?? {};
|
|
94
|
+
// Compute strides: stride[i] = product of sizes[i+1..n-1]
|
|
95
|
+
const strides = new Array(dims.length).fill(1);
|
|
96
|
+
for (let i = dims.length - 2; i >= 0; i--) {
|
|
97
|
+
// noUncheckedIndexedAccess: bounds are [0..dims.length-2], so i+1 and sizes[i+1] are safe
|
|
98
|
+
strides[i] = (strides[i + 1] ?? 1) * (sizes[i + 1] ?? 1);
|
|
99
|
+
}
|
|
100
|
+
// Build dimension value arrays: index position → {code, label}
|
|
101
|
+
const dimValues = dims.map((dim) => {
|
|
102
|
+
const cat = data.dimension?.[dim]?.category;
|
|
103
|
+
if (!cat?.index)
|
|
104
|
+
return [];
|
|
105
|
+
return Object.entries(cat.index)
|
|
106
|
+
.sort(([, a], [, b]) => a - b)
|
|
107
|
+
.map(([code]) => ({
|
|
108
|
+
code,
|
|
109
|
+
label: cat.label?.[code] ?? code,
|
|
110
|
+
}));
|
|
111
|
+
});
|
|
112
|
+
const observations = [];
|
|
113
|
+
const totalCells = sizes.reduce((a, b) => a * b, 1);
|
|
114
|
+
for (let linearIdx = 0; linearIdx < totalCells; linearIdx++) {
|
|
115
|
+
const keyStr = String(linearIdx);
|
|
116
|
+
if (!(keyStr in value) && !(keyStr in status))
|
|
117
|
+
continue;
|
|
118
|
+
const dimensions = {};
|
|
119
|
+
let remaining = linearIdx;
|
|
120
|
+
for (let d = 0; d < dims.length; d++) {
|
|
121
|
+
const stride = strides[d] ?? 1;
|
|
122
|
+
const pos = Math.floor(remaining / stride);
|
|
123
|
+
remaining = remaining % stride;
|
|
124
|
+
const dimName = dims[d];
|
|
125
|
+
const dimArr = dimValues[d];
|
|
126
|
+
const dimVal = dimArr?.[pos];
|
|
127
|
+
if (dimName) {
|
|
128
|
+
dimensions[dimName] = dimVal ?? { code: String(pos), label: String(pos) };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const rawValue = value[keyStr] ?? null;
|
|
132
|
+
const statusCode = status[keyStr];
|
|
133
|
+
const obs = { dimensions, value: rawValue };
|
|
134
|
+
if (statusCode) {
|
|
135
|
+
obs.status = {
|
|
136
|
+
code: statusCode,
|
|
137
|
+
label: statusLabels[statusCode] ?? statusCode,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
observations.push(obs);
|
|
141
|
+
}
|
|
142
|
+
return observations;
|
|
143
|
+
}
|
|
144
|
+
extractAnnotation(data, type, field) {
|
|
145
|
+
return data.extension?.annotation?.find((a) => a.type === type)?.[field];
|
|
146
|
+
}
|
|
147
|
+
extractMetadata(data, datasetCode) {
|
|
148
|
+
const dims = data.id ?? [];
|
|
149
|
+
const dimension = data.dimension ?? {};
|
|
150
|
+
const dimensions = dims.map((dimCode) => {
|
|
151
|
+
const cat = dimension[dimCode]?.category;
|
|
152
|
+
const allValues = cat?.index ? Object.entries(cat.index).sort(([, a], [, b]) => a - b) : [];
|
|
153
|
+
return {
|
|
154
|
+
code: dimCode,
|
|
155
|
+
label: dimension[dimCode]?.label ?? dimCode,
|
|
156
|
+
valuesCount: allValues.length,
|
|
157
|
+
sampleValues: allValues.slice(0, 10).map(([code]) => ({
|
|
158
|
+
code,
|
|
159
|
+
label: cat?.label?.[code] ?? code,
|
|
160
|
+
})),
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
const obsCountRaw = this.extractAnnotation(data, 'OBS_COUNT', 'title');
|
|
164
|
+
const obsCount = obsCountRaw ? parseInt(obsCountRaw, 10) : 0;
|
|
165
|
+
const oldest = this.extractAnnotation(data, 'OBS_PERIOD_OVERALL_OLDEST', 'title') ?? '';
|
|
166
|
+
const latest = this.extractAnnotation(data, 'OBS_PERIOD_OVERALL_LATEST', 'title') ?? '';
|
|
167
|
+
const lastUpdated = this.extractAnnotation(data, 'UPDATE_DATA', 'date') ?? '';
|
|
168
|
+
const metadataUrl = this.extractAnnotation(data, 'ESMS_HTML', 'href');
|
|
169
|
+
return {
|
|
170
|
+
code: datasetCode,
|
|
171
|
+
label: data.label ?? datasetCode,
|
|
172
|
+
dimensions,
|
|
173
|
+
timeRange: { start: oldest, end: latest },
|
|
174
|
+
obsCount: isNaN(obsCount) ? 0 : obsCount,
|
|
175
|
+
lastUpdated,
|
|
176
|
+
...(metadataUrl && { metadataUrl }),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** Fetch dataset metadata using a minimal lastTimePeriod=1 query. */
|
|
180
|
+
async getDatasetInfo(datasetCode, ctx) {
|
|
181
|
+
ctx.log.info('Fetching dataset info', { datasetCode });
|
|
182
|
+
const url = this.buildUrl(datasetCode, { lastTimePeriod: '1' });
|
|
183
|
+
const data = await this.fetchJson(url, ctx);
|
|
184
|
+
return this.extractMetadata(data, datasetCode);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get all valid values for a specific dimension.
|
|
188
|
+
* For 'geo', applies geoLevel filter; for others, uses lastTimePeriod=1.
|
|
189
|
+
*/
|
|
190
|
+
async getDimensionValues(datasetCode, dimension, geoLevel, ctx) {
|
|
191
|
+
ctx.log.info('Fetching dimension values', { datasetCode, dimension, geoLevel });
|
|
192
|
+
const params = {};
|
|
193
|
+
if (dimension === 'geo' && geoLevel) {
|
|
194
|
+
params['geoLevel'] = geoLevel;
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
params['lastTimePeriod'] = '1';
|
|
198
|
+
}
|
|
199
|
+
const url = this.buildUrl(datasetCode, params);
|
|
200
|
+
const data = await this.fetchJson(url, ctx);
|
|
201
|
+
const dimDef = data.dimension?.[dimension];
|
|
202
|
+
if (!dimDef) {
|
|
203
|
+
throw notFound(`Dimension "${dimension}" not found in dataset "${datasetCode}". ` +
|
|
204
|
+
`Use eurostat_get_dataset_info to see valid dimensions.`, { reason: 'not_found', datasetCode, dimension });
|
|
205
|
+
}
|
|
206
|
+
const cat = dimDef.category;
|
|
207
|
+
const allValues = cat?.index
|
|
208
|
+
? Object.entries(cat.index)
|
|
209
|
+
.sort(([, a], [, b]) => a - b)
|
|
210
|
+
.map(([code]) => ({ code, label: cat.label?.[code] ?? code }))
|
|
211
|
+
: [];
|
|
212
|
+
return {
|
|
213
|
+
dimensionCode: dimension,
|
|
214
|
+
dimensionLabel: dimDef.label ?? dimension,
|
|
215
|
+
values: allValues,
|
|
216
|
+
totalCount: allValues.length,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Query dataset observations with dimension filters.
|
|
221
|
+
* Returns decoded observations.
|
|
222
|
+
*/
|
|
223
|
+
async queryDataset(datasetCode, filters, geoLevel, sinceP, untilP, lastN, lang, ctx) {
|
|
224
|
+
ctx.log.info('Querying dataset', { datasetCode, filters, geoLevel, sinceP, untilP, lastN });
|
|
225
|
+
// Validate mutually exclusive params
|
|
226
|
+
if (filters['geo'] && geoLevel) {
|
|
227
|
+
throw invalidParams(`"geo" filter and "geo_level" cannot be used together. ` +
|
|
228
|
+
`Use one or the other: "geo" for specific country/region codes, "geo_level" for filtering by NUTS hierarchy level.`, { reason: 'conflicting_params' });
|
|
229
|
+
}
|
|
230
|
+
const params = { lang };
|
|
231
|
+
// Apply dimension filters
|
|
232
|
+
for (const [dim, values] of Object.entries(filters)) {
|
|
233
|
+
if (values.length > 0)
|
|
234
|
+
params[dim] = values;
|
|
235
|
+
}
|
|
236
|
+
if (geoLevel)
|
|
237
|
+
params['geoLevel'] = geoLevel;
|
|
238
|
+
if (sinceP && !lastN)
|
|
239
|
+
params['sinceTimePeriod'] = sinceP;
|
|
240
|
+
if (untilP && !lastN)
|
|
241
|
+
params['untilTimePeriod'] = untilP;
|
|
242
|
+
if (lastN)
|
|
243
|
+
params['lastTimePeriod'] = String(lastN);
|
|
244
|
+
const url = this.buildUrl(datasetCode, params);
|
|
245
|
+
const data = await this.fetchJson(url, ctx);
|
|
246
|
+
// Detect no-results case (empty value object, no error)
|
|
247
|
+
if (data.id && data.value !== undefined && Object.keys(data.value).length === 0) {
|
|
248
|
+
throw notFound(`Query returned no observations for dataset "${datasetCode}". ` +
|
|
249
|
+
`The dimension filter combination may not exist in the data. ` +
|
|
250
|
+
`Verify dimension values with eurostat_get_dimension_values first.`, { reason: 'no_results', datasetCode, filters });
|
|
251
|
+
}
|
|
252
|
+
const observations = this.decodeObservations(data);
|
|
253
|
+
const missingObsCount = observations.filter((o) => o.value === null).length;
|
|
254
|
+
const oldest = this.extractAnnotation(data, 'OBS_PERIOD_OVERALL_OLDEST', 'title') ?? '';
|
|
255
|
+
const latest = this.extractAnnotation(data, 'OBS_PERIOD_OVERALL_LATEST', 'title') ?? '';
|
|
256
|
+
return {
|
|
257
|
+
datasetCode,
|
|
258
|
+
datasetLabel: data.label ?? datasetCode,
|
|
259
|
+
dimensionsUsed: data.id ?? [],
|
|
260
|
+
observations,
|
|
261
|
+
obsCount: observations.length,
|
|
262
|
+
timeRange: { start: oldest, end: latest },
|
|
263
|
+
missingObsCount,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// --- Init/accessor pattern ---
|
|
268
|
+
let _service;
|
|
269
|
+
export function initEurostatDataService(config, storage) {
|
|
270
|
+
_service = new EurostatDataService(config, storage);
|
|
271
|
+
}
|
|
272
|
+
export function getEurostatDataService() {
|
|
273
|
+
if (!_service) {
|
|
274
|
+
throw new Error('EurostatDataService not initialized — call initEurostatDataService() in setup()');
|
|
275
|
+
}
|
|
276
|
+
return _service;
|
|
277
|
+
}
|
|
278
|
+
//# sourceMappingURL=eurostat-data-service.js.map
|