euparliamentmonitor 0.9.19 → 0.9.21
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 +2 -2
- package/package.json +4 -3
- package/scripts/aggregator/editorial-brief-resolver.d.ts +38 -0
- package/scripts/aggregator/editorial-brief-resolver.js +32 -0
- package/scripts/aggregator/generator/render-one.js +35 -0
- package/scripts/aggregator/html/localize-body.d.ts +32 -0
- package/scripts/aggregator/html/localize-body.js +69 -0
- package/scripts/aggregator/html/shell.d.ts +10 -0
- package/scripts/aggregator/html/shell.js +11 -1
- package/scripts/aggregator/markdown-renderer.d.ts +23 -24
- package/scripts/aggregator/markdown-renderer.js +39 -25
- package/scripts/aggregator/metadata/artifact-highlight.d.ts +15 -22
- package/scripts/aggregator/metadata/artifact-highlight.js +14 -230
- package/scripts/aggregator/metadata/artifact-walker.d.ts +34 -0
- package/scripts/aggregator/metadata/artifact-walker.js +177 -0
- package/scripts/aggregator/metadata/editorial-highlight.d.ts +15 -0
- package/scripts/aggregator/metadata/editorial-highlight.js +53 -0
- package/scripts/aggregator/metadata/priority-finding-highlight.js +7 -2
- package/scripts/aggregator/metadata/resolve-helpers.js +9 -3
- package/scripts/aggregator/metadata/text-utils.js +7 -0
- package/scripts/aggregator/metadata/translated-sibling.d.ts +23 -0
- package/scripts/aggregator/metadata/translated-sibling.js +39 -0
- package/scripts/aggregator/reader-guide/builder.js +3 -1
- package/scripts/aggregator/reader-guide/labels.d.ts +7 -0
- package/scripts/aggregator/reader-guide/labels.js +22 -0
- package/scripts/aggregator/reader-intelligence-guide.d.ts +1 -1
- package/scripts/aggregator/reader-intelligence-guide.js +1 -1
- package/scripts/aggregator/seo-entity-extractor.d.ts +45 -0
- package/scripts/aggregator/seo-entity-extractor.js +211 -0
- package/scripts/constants/articles/breaking-strings-central.d.ts +8 -0
- package/scripts/constants/articles/breaking-strings-central.js +105 -0
- package/scripts/constants/articles/breaking-strings-east.d.ts +8 -0
- package/scripts/constants/articles/breaking-strings-east.js +203 -0
- package/scripts/constants/articles/breaking-strings-nordic.d.ts +8 -0
- package/scripts/constants/articles/breaking-strings-nordic.js +252 -0
- package/scripts/constants/articles/breaking-strings-west.d.ts +8 -0
- package/scripts/constants/articles/breaking-strings-west.js +154 -0
- package/scripts/constants/articles/breaking.d.ts +0 -1
- package/scripts/constants/articles/breaking.js +9 -6
- package/scripts/constants/articles/dashboard/ar.d.ts +8 -0
- package/scripts/constants/articles/dashboard/ar.js +71 -0
- package/scripts/constants/articles/dashboard/da.d.ts +8 -0
- package/scripts/constants/articles/dashboard/da.js +71 -0
- package/scripts/constants/articles/dashboard/de.d.ts +8 -0
- package/scripts/constants/articles/dashboard/de.js +71 -0
- package/scripts/constants/articles/dashboard/en.d.ts +8 -0
- package/scripts/constants/articles/dashboard/en.js +71 -0
- package/scripts/constants/articles/dashboard/es.d.ts +8 -0
- package/scripts/constants/articles/dashboard/es.js +71 -0
- package/scripts/constants/articles/dashboard/fi.d.ts +8 -0
- package/scripts/constants/articles/dashboard/fi.js +71 -0
- package/scripts/constants/articles/dashboard/fr.d.ts +8 -0
- package/scripts/constants/articles/dashboard/fr.js +71 -0
- package/scripts/constants/articles/dashboard/he.d.ts +8 -0
- package/scripts/constants/articles/dashboard/he.js +71 -0
- package/scripts/constants/articles/dashboard/index.d.ts +7 -0
- package/scripts/constants/articles/dashboard/index.js +33 -0
- package/scripts/constants/articles/dashboard/ja.d.ts +8 -0
- package/scripts/constants/articles/dashboard/ja.js +71 -0
- package/scripts/constants/articles/dashboard/ko.d.ts +8 -0
- package/scripts/constants/articles/dashboard/ko.js +71 -0
- package/scripts/constants/articles/dashboard/nl.d.ts +8 -0
- package/scripts/constants/articles/dashboard/nl.js +71 -0
- package/scripts/constants/articles/dashboard/no.d.ts +8 -0
- package/scripts/constants/articles/dashboard/no.js +71 -0
- package/scripts/constants/articles/dashboard/sv.d.ts +8 -0
- package/scripts/constants/articles/dashboard/sv.js +71 -0
- package/scripts/constants/articles/dashboard/zh.d.ts +8 -0
- package/scripts/constants/articles/dashboard/zh.js +71 -0
- package/scripts/constants/articles/dashboard.d.ts +7 -2
- package/scripts/constants/articles/dashboard.js +4 -8
- package/scripts/constants/articles/deep-analysis/ar.d.ts +8 -0
- package/scripts/constants/articles/deep-analysis/ar.js +75 -0
- package/scripts/constants/articles/deep-analysis/da.d.ts +8 -0
- package/scripts/constants/articles/deep-analysis/da.js +75 -0
- package/scripts/constants/articles/deep-analysis/de.d.ts +8 -0
- package/scripts/constants/articles/deep-analysis/de.js +75 -0
- package/scripts/constants/articles/deep-analysis/en.d.ts +8 -0
- package/scripts/constants/articles/deep-analysis/en.js +75 -0
- package/scripts/constants/articles/deep-analysis/es.d.ts +8 -0
- package/scripts/constants/articles/deep-analysis/es.js +75 -0
- package/scripts/constants/articles/deep-analysis/fi.d.ts +8 -0
- package/scripts/constants/articles/deep-analysis/fi.js +75 -0
- package/scripts/constants/articles/deep-analysis/fr.d.ts +8 -0
- package/scripts/constants/articles/deep-analysis/fr.js +75 -0
- package/scripts/constants/articles/deep-analysis/he.d.ts +8 -0
- package/scripts/constants/articles/deep-analysis/he.js +75 -0
- package/scripts/constants/articles/deep-analysis/index.d.ts +7 -0
- package/scripts/constants/articles/deep-analysis/index.js +33 -0
- package/scripts/constants/articles/deep-analysis/ja.d.ts +8 -0
- package/scripts/constants/articles/deep-analysis/ja.js +75 -0
- package/scripts/constants/articles/deep-analysis/ko.d.ts +8 -0
- package/scripts/constants/articles/deep-analysis/ko.js +75 -0
- package/scripts/constants/articles/deep-analysis/nl.d.ts +8 -0
- package/scripts/constants/articles/deep-analysis/nl.js +75 -0
- package/scripts/constants/articles/deep-analysis/no.d.ts +8 -0
- package/scripts/constants/articles/deep-analysis/no.js +75 -0
- package/scripts/constants/articles/deep-analysis/sv.d.ts +8 -0
- package/scripts/constants/articles/deep-analysis/sv.js +75 -0
- package/scripts/constants/articles/deep-analysis/zh.d.ts +8 -0
- package/scripts/constants/articles/deep-analysis/zh.js +75 -0
- package/scripts/constants/articles/deep-analysis.d.ts +4 -3
- package/scripts/constants/articles/deep-analysis.js +3 -7
- package/scripts/constants/articles/localized-keywords-central.d.ts +8 -0
- package/scripts/constants/articles/localized-keywords-central.js +118 -0
- package/scripts/constants/articles/localized-keywords-nordic.d.ts +8 -0
- package/scripts/constants/articles/localized-keywords-nordic.js +303 -0
- package/scripts/constants/articles/localized-keywords.js +4 -2
- package/scripts/constants/articles/swot-builder-central.d.ts +8 -0
- package/scripts/constants/articles/swot-builder-central.js +90 -0
- package/scripts/constants/articles/swot-builder-nordic.d.ts +8 -0
- package/scripts/constants/articles/swot-builder-nordic.js +216 -0
- package/scripts/constants/articles/swot.js +4 -2
- package/scripts/constants/articles/week-ahead-eu.d.ts +12 -0
- package/scripts/constants/articles/week-ahead-eu.js +278 -0
- package/scripts/constants/articles/week-ahead-global.d.ts +12 -0
- package/scripts/constants/articles/week-ahead-global.js +278 -0
- package/scripts/constants/articles/week-ahead.d.ts +4 -7
- package/scripts/constants/articles/week-ahead.js +11 -535
- package/scripts/constants/world-bank/category-map-analysis.d.ts +9 -0
- package/scripts/constants/world-bank/category-map-analysis.js +204 -0
- package/scripts/constants/world-bank/category-map-legislative.d.ts +9 -0
- package/scripts/constants/world-bank/category-map-legislative.js +130 -0
- package/scripts/constants/world-bank/category-map-periodic.d.ts +9 -0
- package/scripts/constants/world-bank/category-map-periodic.js +176 -0
- package/scripts/constants/world-bank/category-map.d.ts +3 -26
- package/scripts/constants/world-bank/category-map.js +8 -501
- package/scripts/discover-untranslated-briefs.js +123 -4
- package/scripts/generators/news-indexes/per-language.js +21 -7
- package/scripts/generators/political-intelligence/html.js +39 -8
- package/scripts/generators/sitemap/html.js +25 -7
- package/scripts/mcp/ep/client.d.ts +0 -1
- package/scripts/mcp/ep/client.js +0 -65
- package/scripts/mcp/ep/error-classifier.d.ts +2 -2
- package/scripts/mcp/ep/error-classifier.js +2 -2
- package/scripts/mcp/ep/tools-list.d.ts +13 -0
- package/scripts/mcp/ep/tools-list.js +79 -0
- package/scripts/mcp/ep-mcp-client.d.ts +1 -0
- package/scripts/mcp/ep-mcp-client.js +1 -0
- package/scripts/mcp/imf/client.d.ts +3 -64
- package/scripts/mcp/imf/client.js +18 -207
- package/scripts/mcp/imf/http-transport.d.ts +92 -0
- package/scripts/mcp/imf/http-transport.js +232 -0
- package/scripts/mcp/transport/connection.d.ts +25 -53
- package/scripts/mcp/transport/connection.js +90 -250
- package/scripts/mcp/transport/process.d.ts +62 -0
- package/scripts/mcp/transport/process.js +147 -0
- package/scripts/mcp/transport/reconnect.d.ts +73 -0
- package/scripts/mcp/transport/reconnect.js +96 -0
- package/scripts/validate-brief-translations.js +122 -6
- package/scripts/constants/articles/breaking-strings-eu.d.ts +0 -7
- package/scripts/constants/articles/breaking-strings-global.d.ts +0 -7
- package/scripts/constants/articles/dashboard-builder-eu.d.ts +0 -7
- package/scripts/constants/articles/dashboard-builder-global.d.ts +0 -7
- package/scripts/constants/articles/deep-analysis-strings-eu.d.ts +0 -7
- package/scripts/constants/articles/deep-analysis-strings-global.d.ts +0 -7
- package/scripts/constants/articles/localized-keywords-eu.d.ts +0 -7
- package/scripts/constants/articles/swot-builder-eu.d.ts +0 -7
|
@@ -150,72 +150,11 @@ export declare class IMFMCPClient {
|
|
|
150
150
|
agencyId?: string;
|
|
151
151
|
}): Promise<MCPToolResult>;
|
|
152
152
|
/**
|
|
153
|
-
* Build
|
|
154
|
-
* Tries the IMF-only MCP fetch-proxy gateway first (bypasses AWF Squid
|
|
155
|
-
* proxy in agentic workflow sandbox), then falls back to direct fetch.
|
|
156
|
-
*
|
|
157
|
-
* @param path - Path (already URL-encoded) to append to the base URL.
|
|
158
|
-
* @returns Response body (`text/*` or `application/*`) as a string.
|
|
159
|
-
* @throws When the HTTP status is not 2xx, the request times out, or
|
|
160
|
-
* the network layer raises.
|
|
161
|
-
* @internal
|
|
162
|
-
*/
|
|
163
|
-
private _getText;
|
|
164
|
-
/**
|
|
165
|
-
* Direct-fetch strategy with subscription-key rotation.
|
|
166
|
-
*
|
|
167
|
-
* Iterates configured `IMF_API_PRIMARY_KEY` → `IMF_API_SECONDARY_KEY`,
|
|
168
|
-
* retrying only on `401`/`403`. Network errors short-circuit immediately.
|
|
169
|
-
*
|
|
170
|
-
* @param url - Fully-qualified IMF SDMX URL.
|
|
171
|
-
* @returns Response body text on success.
|
|
172
|
-
* @throws The last HTTP/network error when all configured keys are exhausted.
|
|
173
|
-
* @internal
|
|
174
|
-
*/
|
|
175
|
-
private _fetchDirectWithKeyRotation;
|
|
176
|
-
/**
|
|
177
|
-
* Single direct-fetch attempt with one subscription key. Classifies the
|
|
178
|
-
* outcome so {@link _fetchDirectWithKeyRotation} can decide whether to
|
|
179
|
-
* rotate keys or surface the error.
|
|
180
|
-
*
|
|
181
|
-
* @param url - Fully-qualified IMF SDMX URL.
|
|
182
|
-
* @param key - Subscription key for this attempt, or `undefined` to send unauthenticated.
|
|
183
|
-
* @returns `'ok'` with body text, `'auth'` with the 401/403 error, or `'error'` for everything else.
|
|
184
|
-
* @internal
|
|
185
|
-
*/
|
|
186
|
-
private _fetchOnceWithKey;
|
|
187
|
-
/**
|
|
188
|
-
* Remove any configured IMF subscription keys from an Error's message and
|
|
189
|
-
* stack so that downstream `console.warn` / fallback envelopes cannot leak
|
|
190
|
-
* the secret even if the underlying fetch implementation (or proxy) embeds
|
|
191
|
-
* request headers in its thrown error.
|
|
192
|
-
*
|
|
193
|
-
* @param error - Error returned by `_fetchImpl` or constructed from a non-Error throw.
|
|
194
|
-
* @returns A new {@link Error} whose `message` (and `stack` when present) have
|
|
195
|
-
* each configured subscription key replaced with `[REDACTED]`. Returns the
|
|
196
|
-
* original error untouched when no keys are configured.
|
|
197
|
-
* @internal
|
|
198
|
-
*/
|
|
199
|
-
private _redactSubscriptionKeys;
|
|
200
|
-
/**
|
|
201
|
-
* Fetch a URL via the MCP fetch-proxy gateway (JSON-RPC 2.0 over HTTP).
|
|
202
|
-
* The fetch-proxy server runs in a container that bypasses the AWF Squid proxy.
|
|
203
|
-
*
|
|
204
|
-
* @param url - Fully-qualified URL to fetch.
|
|
205
|
-
* @returns Response text, or null if the gateway call fails.
|
|
206
|
-
* @internal
|
|
207
|
-
*/
|
|
208
|
-
private _fetchViaGateway;
|
|
209
|
-
/**
|
|
210
|
-
* GET a URL and parse the response body as JSON.
|
|
153
|
+
* Build an {@link IMFHttpContext} adapter for http-transport.ts helpers.
|
|
211
154
|
*
|
|
212
|
-
* @
|
|
213
|
-
* @param path - Path to append to the base URL.
|
|
214
|
-
* @returns Parsed JSON value.
|
|
215
|
-
* @throws When the response is not JSON, not 2xx, or the request fails.
|
|
216
|
-
* @internal
|
|
155
|
+
* @returns Context adapter for IMF HTTP transport helpers
|
|
217
156
|
*/
|
|
218
|
-
private
|
|
157
|
+
private _httpCtx;
|
|
219
158
|
}
|
|
220
159
|
/**
|
|
221
160
|
* Forward-looking alias for `IMFMCPClient`. New code should prefer
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
import { IMF_FALLBACK
|
|
3
|
+
import { IMF_FALLBACK } from './config.js';
|
|
4
4
|
import { resolveAgency, resolveCodelistCodes, defaultDimensionOrder, buildSDMXKey, withDefaultFrequency, } from './sdmx.js';
|
|
5
5
|
import { unwrapLocalisedLabel, wrapAsMCPResult } from './observations.js';
|
|
6
6
|
import { readBaseAndTimeout, stripTrailingSlashes, readImfSubscriptionKeysFromEnv, } from './utils.js';
|
|
7
|
+
import { getText, getJSON } from './http-transport.js';
|
|
7
8
|
export class IMFMCPClient {
|
|
8
9
|
_apiBaseUrl;
|
|
9
10
|
_timeoutMs;
|
|
@@ -99,7 +100,7 @@ export class IMFMCPClient {
|
|
|
99
100
|
*/
|
|
100
101
|
async listDatabases() {
|
|
101
102
|
try {
|
|
102
|
-
const json = await
|
|
103
|
+
const json = await getJSON('/structure/dataflow', this._httpCtx());
|
|
103
104
|
const flows = json?.data?.dataflows ?? [];
|
|
104
105
|
const rows = flows.map((f) => ({
|
|
105
106
|
id: f.id ?? '',
|
|
@@ -133,7 +134,7 @@ export class IMFMCPClient {
|
|
|
133
134
|
return IMF_FALLBACK;
|
|
134
135
|
}
|
|
135
136
|
try {
|
|
136
|
-
const json = await
|
|
137
|
+
const json = await getJSON('/structure/dataflow', this._httpCtx());
|
|
137
138
|
const flows = json?.data?.dataflows ?? [];
|
|
138
139
|
const needle = keyword.toLowerCase();
|
|
139
140
|
const rows = flows
|
|
@@ -179,7 +180,7 @@ export class IMFMCPClient {
|
|
|
179
180
|
}
|
|
180
181
|
try {
|
|
181
182
|
const agency = agencyId ?? resolveAgency(databaseId);
|
|
182
|
-
const json = await
|
|
183
|
+
const json = await getJSON(`/structure/dataflow/${encodeURIComponent(agency)}/${encodeURIComponent(databaseId)}/+?references=datastructure`, this._httpCtx());
|
|
183
184
|
const ds = json?.data?.dataStructures?.[0];
|
|
184
185
|
const dims = ds?.dataStructureComponents?.dimensionList?.dimensions ?? [];
|
|
185
186
|
const rows = dims.map((d) => ({ id: d.id, name: unwrapLocalisedLabel(d.name) }));
|
|
@@ -216,7 +217,7 @@ export class IMFMCPClient {
|
|
|
216
217
|
}
|
|
217
218
|
try {
|
|
218
219
|
const agency = agencyId ?? resolveAgency(databaseId);
|
|
219
|
-
const structure = await
|
|
220
|
+
const structure = await getJSON(`/structure/dataflow/${encodeURIComponent(agency)}/${encodeURIComponent(databaseId)}/+?references=all`, this._httpCtx());
|
|
220
221
|
const ds = structure?.data?.dataStructures?.[0];
|
|
221
222
|
const dims = ds?.dataStructureComponents?.dimensionList?.dimensions ?? [];
|
|
222
223
|
const dim = dims.find((d) => d.id.toLowerCase() === parameter.toLowerCase());
|
|
@@ -289,7 +290,7 @@ export class IMFMCPClient {
|
|
|
289
290
|
format: 'jsondata',
|
|
290
291
|
});
|
|
291
292
|
const url = `/data/dataflow/${encodeURIComponent(agency)}/${encodeURIComponent(databaseId)}/+/${key}?${qs.toString()}`;
|
|
292
|
-
const text = await this.
|
|
293
|
+
const text = await getText(url, this._httpCtx());
|
|
293
294
|
return wrapAsMCPResult(text);
|
|
294
295
|
}
|
|
295
296
|
catch (error) {
|
|
@@ -298,211 +299,21 @@ export class IMFMCPClient {
|
|
|
298
299
|
return IMF_FALLBACK;
|
|
299
300
|
}
|
|
300
301
|
}
|
|
301
|
-
// ─── private
|
|
302
|
+
// ─── private HTTP context factory ─────────────────────────────────────────
|
|
302
303
|
/**
|
|
303
|
-
* Build
|
|
304
|
-
* Tries the IMF-only MCP fetch-proxy gateway first (bypasses AWF Squid
|
|
305
|
-
* proxy in agentic workflow sandbox), then falls back to direct fetch.
|
|
304
|
+
* Build an {@link IMFHttpContext} adapter for http-transport.ts helpers.
|
|
306
305
|
*
|
|
307
|
-
* @
|
|
308
|
-
* @returns Response body (`text/*` or `application/*`) as a string.
|
|
309
|
-
* @throws When the HTTP status is not 2xx, the request times out, or
|
|
310
|
-
* the network layer raises.
|
|
311
|
-
* @internal
|
|
306
|
+
* @returns Context adapter for IMF HTTP transport helpers
|
|
312
307
|
*/
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
catch {
|
|
322
|
-
// Gateway unavailable — fall through to direct fetch
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
return this._fetchDirectWithKeyRotation(url);
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* Direct-fetch strategy with subscription-key rotation.
|
|
329
|
-
*
|
|
330
|
-
* Iterates configured `IMF_API_PRIMARY_KEY` → `IMF_API_SECONDARY_KEY`,
|
|
331
|
-
* retrying only on `401`/`403`. Network errors short-circuit immediately.
|
|
332
|
-
*
|
|
333
|
-
* @param url - Fully-qualified IMF SDMX URL.
|
|
334
|
-
* @returns Response body text on success.
|
|
335
|
-
* @throws The last HTTP/network error when all configured keys are exhausted.
|
|
336
|
-
* @internal
|
|
337
|
-
*/
|
|
338
|
-
async _fetchDirectWithKeyRotation(url) {
|
|
339
|
-
const attempts = this._imfSubscriptionKeys.length > 0 ? [...this._imfSubscriptionKeys] : [undefined];
|
|
340
|
-
let lastError;
|
|
341
|
-
for (let i = 0; i < attempts.length; i += 1) {
|
|
342
|
-
const isLast = i + 1 >= attempts.length;
|
|
343
|
-
const outcome = await this._fetchOnceWithKey(url, attempts[i]);
|
|
344
|
-
if (outcome.kind === 'ok')
|
|
345
|
-
return outcome.text;
|
|
346
|
-
lastError = outcome.error;
|
|
347
|
-
if (outcome.kind === 'auth' && !isLast)
|
|
348
|
-
continue;
|
|
349
|
-
throw outcome.error;
|
|
350
|
-
}
|
|
351
|
-
if (lastError !== undefined)
|
|
352
|
-
throw lastError;
|
|
353
|
-
throw new Error(`IMF request to ${url} failed without producing a response`);
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Single direct-fetch attempt with one subscription key. Classifies the
|
|
357
|
-
* outcome so {@link _fetchDirectWithKeyRotation} can decide whether to
|
|
358
|
-
* rotate keys or surface the error.
|
|
359
|
-
*
|
|
360
|
-
* @param url - Fully-qualified IMF SDMX URL.
|
|
361
|
-
* @param key - Subscription key for this attempt, or `undefined` to send unauthenticated.
|
|
362
|
-
* @returns `'ok'` with body text, `'auth'` with the 401/403 error, or `'error'` for everything else.
|
|
363
|
-
* @internal
|
|
364
|
-
*/
|
|
365
|
-
async _fetchOnceWithKey(url, key) {
|
|
366
|
-
const headers = { ...IMF_REQUEST_HEADERS };
|
|
367
|
-
if (key !== undefined && key.length > 0) {
|
|
368
|
-
headers[IMF_SUBSCRIPTION_KEY_HEADER] = key;
|
|
369
|
-
}
|
|
370
|
-
const controller = new AbortController();
|
|
371
|
-
const timer = setTimeout(() => controller.abort(), this._timeoutMs);
|
|
372
|
-
try {
|
|
373
|
-
const response = await this._fetchImpl(url, {
|
|
374
|
-
method: 'GET',
|
|
375
|
-
headers,
|
|
376
|
-
signal: controller.signal,
|
|
377
|
-
});
|
|
378
|
-
if (response.ok) {
|
|
379
|
-
if (response.status === 204) {
|
|
380
|
-
return {
|
|
381
|
-
kind: 'error',
|
|
382
|
-
error: new Error(`HTTP 204 No Content for ${url} — likely missing or invalid ${IMF_SUBSCRIPTION_KEY_HEADER} (set IMF_API_PRIMARY_KEY)`),
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
return { kind: 'ok', text: await response.text() };
|
|
386
|
-
}
|
|
387
|
-
const error = this._redactSubscriptionKeys(new Error(`HTTP ${response.status} ${response.statusText} for ${url}`));
|
|
388
|
-
const isAuthFailure = response.status === 401 || response.status === 403;
|
|
389
|
-
return { kind: isAuthFailure ? 'auth' : 'error', error };
|
|
390
|
-
}
|
|
391
|
-
catch (err) {
|
|
392
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
393
|
-
return { kind: 'error', error: this._redactSubscriptionKeys(error) };
|
|
394
|
-
}
|
|
395
|
-
finally {
|
|
396
|
-
clearTimeout(timer);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
/**
|
|
400
|
-
* Remove any configured IMF subscription keys from an Error's message and
|
|
401
|
-
* stack so that downstream `console.warn` / fallback envelopes cannot leak
|
|
402
|
-
* the secret even if the underlying fetch implementation (or proxy) embeds
|
|
403
|
-
* request headers in its thrown error.
|
|
404
|
-
*
|
|
405
|
-
* @param error - Error returned by `_fetchImpl` or constructed from a non-Error throw.
|
|
406
|
-
* @returns A new {@link Error} whose `message` (and `stack` when present) have
|
|
407
|
-
* each configured subscription key replaced with `[REDACTED]`. Returns the
|
|
408
|
-
* original error untouched when no keys are configured.
|
|
409
|
-
* @internal
|
|
410
|
-
*/
|
|
411
|
-
_redactSubscriptionKeys(error) {
|
|
412
|
-
if (this._imfSubscriptionKeys.length === 0)
|
|
413
|
-
return error;
|
|
414
|
-
const redact = (s) => {
|
|
415
|
-
let out = s;
|
|
416
|
-
for (const key of this._imfSubscriptionKeys) {
|
|
417
|
-
if (!key)
|
|
418
|
-
continue;
|
|
419
|
-
// Escape regex metacharacters in the key
|
|
420
|
-
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
421
|
-
out = out.replace(new RegExp(escaped, 'g'), '[REDACTED]');
|
|
422
|
-
}
|
|
423
|
-
return out;
|
|
424
|
-
};
|
|
425
|
-
const redacted = new Error(redact(error.message));
|
|
426
|
-
if (error.stack) {
|
|
427
|
-
redacted.stack = redact(error.stack);
|
|
428
|
-
}
|
|
429
|
-
return redacted;
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Fetch a URL via the MCP fetch-proxy gateway (JSON-RPC 2.0 over HTTP).
|
|
433
|
-
* The fetch-proxy server runs in a container that bypasses the AWF Squid proxy.
|
|
434
|
-
*
|
|
435
|
-
* @param url - Fully-qualified URL to fetch.
|
|
436
|
-
* @returns Response text, or null if the gateway call fails.
|
|
437
|
-
* @internal
|
|
438
|
-
*/
|
|
439
|
-
async _fetchViaGateway(url) {
|
|
440
|
-
const gatewayUrl = this._fetchProxyGatewayUrl;
|
|
441
|
-
if (!gatewayUrl)
|
|
442
|
-
return null;
|
|
443
|
-
const rpcRequest = {
|
|
444
|
-
jsonrpc: '2.0',
|
|
445
|
-
id: Date.now(),
|
|
446
|
-
method: 'tools/call',
|
|
447
|
-
params: {
|
|
448
|
-
name: 'fetch_url',
|
|
449
|
-
arguments: { url },
|
|
450
|
-
},
|
|
451
|
-
};
|
|
452
|
-
const headers = {
|
|
453
|
-
'Content-Type': 'application/json',
|
|
454
|
-
Accept: 'application/json, text/event-stream',
|
|
308
|
+
_httpCtx() {
|
|
309
|
+
return {
|
|
310
|
+
apiBaseUrl: this._apiBaseUrl,
|
|
311
|
+
timeoutMs: this._timeoutMs,
|
|
312
|
+
fetchImpl: this._fetchImpl,
|
|
313
|
+
fetchProxyGatewayUrl: this._fetchProxyGatewayUrl,
|
|
314
|
+
fetchProxyApiKey: this._fetchProxyApiKey,
|
|
315
|
+
imfSubscriptionKeys: this._imfSubscriptionKeys,
|
|
455
316
|
};
|
|
456
|
-
if (this._fetchProxyApiKey) {
|
|
457
|
-
headers['Authorization'] = `Bearer ${this._fetchProxyApiKey}`;
|
|
458
|
-
}
|
|
459
|
-
const controller = new AbortController();
|
|
460
|
-
const timer = setTimeout(() => controller.abort(), this._timeoutMs);
|
|
461
|
-
try {
|
|
462
|
-
const response = await this._fetchImpl(gatewayUrl, {
|
|
463
|
-
method: 'POST',
|
|
464
|
-
headers,
|
|
465
|
-
body: JSON.stringify(rpcRequest),
|
|
466
|
-
signal: controller.signal,
|
|
467
|
-
});
|
|
468
|
-
if (!response.ok)
|
|
469
|
-
return null;
|
|
470
|
-
let body = await response.text();
|
|
471
|
-
if (body.trimStart().startsWith('data:')) {
|
|
472
|
-
const lines = body.split('\n').filter((l) => l.startsWith('data:'));
|
|
473
|
-
body = lines.map((l) => l.slice(5).trim()).join('');
|
|
474
|
-
}
|
|
475
|
-
const parsed = JSON.parse(body);
|
|
476
|
-
if (parsed.error)
|
|
477
|
-
return null;
|
|
478
|
-
const text = parsed.result?.content?.[0]?.text;
|
|
479
|
-
return text && text.length > 0 ? text : null;
|
|
480
|
-
}
|
|
481
|
-
catch {
|
|
482
|
-
return null;
|
|
483
|
-
}
|
|
484
|
-
finally {
|
|
485
|
-
clearTimeout(timer);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
/**
|
|
489
|
-
* GET a URL and parse the response body as JSON.
|
|
490
|
-
*
|
|
491
|
-
* @template T - Narrow response type declared by the caller.
|
|
492
|
-
* @param path - Path to append to the base URL.
|
|
493
|
-
* @returns Parsed JSON value.
|
|
494
|
-
* @throws When the response is not JSON, not 2xx, or the request fails.
|
|
495
|
-
* @internal
|
|
496
|
-
*/
|
|
497
|
-
async _getJSON(path) {
|
|
498
|
-
const raw = await this._getText(path);
|
|
499
|
-
try {
|
|
500
|
-
return JSON.parse(raw);
|
|
501
|
-
}
|
|
502
|
-
catch (error) {
|
|
503
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
504
|
-
throw new Error(`Failed to parse IMF response as JSON: ${message}`, { cause: error });
|
|
505
|
-
}
|
|
506
317
|
}
|
|
507
318
|
}
|
|
508
319
|
/**
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter passed by {@link IMFMCPClient} to HTTP transport helpers.
|
|
3
|
+
* Bundles the fields required for authenticated IMF SDMX 3.0 requests
|
|
4
|
+
* without requiring helpers to reference the client class directly.
|
|
5
|
+
*/
|
|
6
|
+
export interface IMFHttpContext {
|
|
7
|
+
/** Fully-qualified IMF SDMX base URL (no trailing slash) */
|
|
8
|
+
readonly apiBaseUrl: string;
|
|
9
|
+
/** Per-request timeout in milliseconds */
|
|
10
|
+
readonly timeoutMs: number;
|
|
11
|
+
/** Fetch implementation (allows injection in tests) */
|
|
12
|
+
readonly fetchImpl: typeof fetch;
|
|
13
|
+
/** Optional fetch-proxy gateway URL (bypasses AWF Squid proxy) */
|
|
14
|
+
readonly fetchProxyGatewayUrl: string | undefined;
|
|
15
|
+
/** Optional API key for the fetch-proxy gateway */
|
|
16
|
+
readonly fetchProxyApiKey: string | undefined;
|
|
17
|
+
/** Configured Azure-APIM subscription keys (primary / secondary) */
|
|
18
|
+
readonly imfSubscriptionKeys: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build a full URL from `path` and GET it as text, applying the configured
|
|
22
|
+
* timeout. Tries the IMF-only MCP fetch-proxy gateway first when configured
|
|
23
|
+
* (bypasses AWF Squid proxy in agentic workflow sandbox), then falls back to
|
|
24
|
+
* direct fetch with subscription-key rotation.
|
|
25
|
+
*
|
|
26
|
+
* @param path - Path (already URL-encoded) to append to the base URL.
|
|
27
|
+
* @param ctx - HTTP context adapter from the client instance.
|
|
28
|
+
* @returns Response body as a string.
|
|
29
|
+
* @throws When the HTTP status is not 2xx, the request times out, or
|
|
30
|
+
* the network layer raises.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getText(path: string, ctx: IMFHttpContext): Promise<string>;
|
|
33
|
+
/**
|
|
34
|
+
* GET a URL, parse the response body as JSON, and return the typed value.
|
|
35
|
+
*
|
|
36
|
+
* @template T - Narrow response type declared by the caller.
|
|
37
|
+
* @param path - Path to append to the base URL.
|
|
38
|
+
* @param ctx - HTTP context adapter from the client instance.
|
|
39
|
+
* @returns Parsed JSON value.
|
|
40
|
+
* @throws When the response is not JSON, not 2xx, or the request fails.
|
|
41
|
+
*/
|
|
42
|
+
export declare function getJSON<T>(path: string, ctx: IMFHttpContext): Promise<T>;
|
|
43
|
+
/**
|
|
44
|
+
* Direct-fetch strategy with subscription-key rotation.
|
|
45
|
+
*
|
|
46
|
+
* Iterates configured `IMF_API_PRIMARY_KEY` → `IMF_API_SECONDARY_KEY`,
|
|
47
|
+
* retrying only on `401`/`403`. Network errors short-circuit immediately.
|
|
48
|
+
*
|
|
49
|
+
* @param url - Fully-qualified IMF SDMX URL.
|
|
50
|
+
* @param ctx - HTTP context adapter.
|
|
51
|
+
* @returns Response body text on success.
|
|
52
|
+
* @throws The last HTTP/network error when all configured keys are exhausted.
|
|
53
|
+
*/
|
|
54
|
+
export declare function fetchDirectWithKeyRotation(url: string, ctx: IMFHttpContext): Promise<string>;
|
|
55
|
+
/**
|
|
56
|
+
* Single direct-fetch attempt with one subscription key.
|
|
57
|
+
*
|
|
58
|
+
* @param url - Fully-qualified IMF SDMX URL.
|
|
59
|
+
* @param key - Subscription key for this attempt, or `undefined` to send unauthenticated.
|
|
60
|
+
* @param ctx - HTTP context adapter.
|
|
61
|
+
* @returns `'ok'` with body text, `'auth'` with the 401/403 error, or `'error'` for everything else.
|
|
62
|
+
*/
|
|
63
|
+
export declare function fetchOnceWithKey(url: string, key: string | undefined, ctx: IMFHttpContext): Promise<{
|
|
64
|
+
kind: 'ok';
|
|
65
|
+
text: string;
|
|
66
|
+
} | {
|
|
67
|
+
kind: 'auth' | 'error';
|
|
68
|
+
error: Error;
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* Remove any configured IMF subscription keys from an Error's message and
|
|
72
|
+
* stack so that downstream `console.warn` / fallback envelopes cannot leak
|
|
73
|
+
* the secret even if the underlying fetch implementation embeds request headers
|
|
74
|
+
* in its thrown error.
|
|
75
|
+
*
|
|
76
|
+
* @param error - Error returned by `fetchImpl` or constructed from a non-Error throw.
|
|
77
|
+
* @param keys - IMF subscription keys to redact.
|
|
78
|
+
* @returns A new {@link Error} whose `message` (and `stack` when present) have
|
|
79
|
+
* each configured subscription key replaced with `[REDACTED]`. Returns the
|
|
80
|
+
* original error untouched when no keys are configured.
|
|
81
|
+
*/
|
|
82
|
+
export declare function redactSubscriptionKeys(error: Error, keys: readonly string[]): Error;
|
|
83
|
+
/**
|
|
84
|
+
* Fetch a URL via the MCP fetch-proxy gateway (JSON-RPC 2.0 over HTTP).
|
|
85
|
+
* The fetch-proxy server runs in a container that bypasses the AWF Squid proxy.
|
|
86
|
+
*
|
|
87
|
+
* @param url - Fully-qualified URL to fetch.
|
|
88
|
+
* @param ctx - HTTP context adapter.
|
|
89
|
+
* @returns Response text, or null if the gateway call fails.
|
|
90
|
+
*/
|
|
91
|
+
export declare function fetchViaGateway(url: string, ctx: IMFHttpContext): Promise<string | null>;
|
|
92
|
+
//# sourceMappingURL=http-transport.d.ts.map
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* @module MCP/imf/http-transport
|
|
5
|
+
* @description HTTP fetch helpers for IMF SDMX 3.0 client: direct fetch with
|
|
6
|
+
* subscription-key rotation, gateway proxy fallback, and key redaction.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from `client.ts` to keep individual file sizes under 400 LOC.
|
|
9
|
+
* Operates on an explicit {@link IMFHttpContext} adapter rather than `this`.
|
|
10
|
+
*/
|
|
11
|
+
import { IMF_REQUEST_HEADERS, IMF_SUBSCRIPTION_KEY_HEADER } from './config.js';
|
|
12
|
+
import { parseSSEResponse } from '../transport/sse-parser.js';
|
|
13
|
+
// ─── Public helpers ────────────────────────────────────────────────────────────
|
|
14
|
+
/**
|
|
15
|
+
* Build a full URL from `path` and GET it as text, applying the configured
|
|
16
|
+
* timeout. Tries the IMF-only MCP fetch-proxy gateway first when configured
|
|
17
|
+
* (bypasses AWF Squid proxy in agentic workflow sandbox), then falls back to
|
|
18
|
+
* direct fetch with subscription-key rotation.
|
|
19
|
+
*
|
|
20
|
+
* @param path - Path (already URL-encoded) to append to the base URL.
|
|
21
|
+
* @param ctx - HTTP context adapter from the client instance.
|
|
22
|
+
* @returns Response body as a string.
|
|
23
|
+
* @throws When the HTTP status is not 2xx, the request times out, or
|
|
24
|
+
* the network layer raises.
|
|
25
|
+
*/
|
|
26
|
+
export async function getText(path, ctx) {
|
|
27
|
+
const url = `${ctx.apiBaseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
|
28
|
+
if (ctx.fetchProxyGatewayUrl) {
|
|
29
|
+
try {
|
|
30
|
+
const result = await fetchViaGateway(url, ctx);
|
|
31
|
+
if (result !== null)
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Gateway unavailable — fall through to direct fetch
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return fetchDirectWithKeyRotation(url, ctx);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* GET a URL, parse the response body as JSON, and return the typed value.
|
|
42
|
+
*
|
|
43
|
+
* @template T - Narrow response type declared by the caller.
|
|
44
|
+
* @param path - Path to append to the base URL.
|
|
45
|
+
* @param ctx - HTTP context adapter from the client instance.
|
|
46
|
+
* @returns Parsed JSON value.
|
|
47
|
+
* @throws When the response is not JSON, not 2xx, or the request fails.
|
|
48
|
+
*/
|
|
49
|
+
export async function getJSON(path, ctx) {
|
|
50
|
+
const raw = await getText(path, ctx);
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(raw);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
+
throw new Error(`Failed to parse IMF response as JSON: ${message}`, { cause: error });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
60
|
+
/**
|
|
61
|
+
* Direct-fetch strategy with subscription-key rotation.
|
|
62
|
+
*
|
|
63
|
+
* Iterates configured `IMF_API_PRIMARY_KEY` → `IMF_API_SECONDARY_KEY`,
|
|
64
|
+
* retrying only on `401`/`403`. Network errors short-circuit immediately.
|
|
65
|
+
*
|
|
66
|
+
* @param url - Fully-qualified IMF SDMX URL.
|
|
67
|
+
* @param ctx - HTTP context adapter.
|
|
68
|
+
* @returns Response body text on success.
|
|
69
|
+
* @throws The last HTTP/network error when all configured keys are exhausted.
|
|
70
|
+
*/
|
|
71
|
+
export async function fetchDirectWithKeyRotation(url, ctx) {
|
|
72
|
+
const attempts = ctx.imfSubscriptionKeys.length > 0 ? [...ctx.imfSubscriptionKeys] : [undefined];
|
|
73
|
+
let lastError;
|
|
74
|
+
for (let i = 0; i < attempts.length; i += 1) {
|
|
75
|
+
const isLast = i + 1 >= attempts.length;
|
|
76
|
+
const outcome = await fetchOnceWithKey(url, attempts[i], ctx);
|
|
77
|
+
if (outcome.kind === 'ok')
|
|
78
|
+
return outcome.text;
|
|
79
|
+
lastError = outcome.error;
|
|
80
|
+
if (outcome.kind === 'auth' && !isLast)
|
|
81
|
+
continue;
|
|
82
|
+
throw outcome.error;
|
|
83
|
+
}
|
|
84
|
+
if (lastError !== undefined)
|
|
85
|
+
throw lastError;
|
|
86
|
+
throw new Error(`IMF request to ${url} failed without producing a response`);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Single direct-fetch attempt with one subscription key.
|
|
90
|
+
*
|
|
91
|
+
* @param url - Fully-qualified IMF SDMX URL.
|
|
92
|
+
* @param key - Subscription key for this attempt, or `undefined` to send unauthenticated.
|
|
93
|
+
* @param ctx - HTTP context adapter.
|
|
94
|
+
* @returns `'ok'` with body text, `'auth'` with the 401/403 error, or `'error'` for everything else.
|
|
95
|
+
*/
|
|
96
|
+
export async function fetchOnceWithKey(url, key, ctx) {
|
|
97
|
+
const headers = { ...IMF_REQUEST_HEADERS };
|
|
98
|
+
if (key !== undefined && key.length > 0) {
|
|
99
|
+
headers[IMF_SUBSCRIPTION_KEY_HEADER] = key;
|
|
100
|
+
}
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const timer = setTimeout(() => controller.abort(), ctx.timeoutMs);
|
|
103
|
+
try {
|
|
104
|
+
const response = await ctx.fetchImpl(url, {
|
|
105
|
+
method: 'GET',
|
|
106
|
+
headers,
|
|
107
|
+
signal: controller.signal,
|
|
108
|
+
});
|
|
109
|
+
if (response.ok) {
|
|
110
|
+
if (response.status === 204) {
|
|
111
|
+
return {
|
|
112
|
+
kind: 'error',
|
|
113
|
+
error: new Error(`HTTP 204 No Content for ${url} — likely missing or invalid ${IMF_SUBSCRIPTION_KEY_HEADER} (set IMF_API_PRIMARY_KEY)`),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { kind: 'ok', text: await response.text() };
|
|
117
|
+
}
|
|
118
|
+
const error = redactSubscriptionKeys(new Error(`HTTP ${response.status} ${response.statusText} for ${url}`), ctx.imfSubscriptionKeys);
|
|
119
|
+
const isAuthFailure = response.status === 401 || response.status === 403;
|
|
120
|
+
return { kind: isAuthFailure ? 'auth' : 'error', error };
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
124
|
+
return { kind: 'error', error: redactSubscriptionKeys(error, ctx.imfSubscriptionKeys) };
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Remove any configured IMF subscription keys from an Error's message and
|
|
132
|
+
* stack so that downstream `console.warn` / fallback envelopes cannot leak
|
|
133
|
+
* the secret even if the underlying fetch implementation embeds request headers
|
|
134
|
+
* in its thrown error.
|
|
135
|
+
*
|
|
136
|
+
* @param error - Error returned by `fetchImpl` or constructed from a non-Error throw.
|
|
137
|
+
* @param keys - IMF subscription keys to redact.
|
|
138
|
+
* @returns A new {@link Error} whose `message` (and `stack` when present) have
|
|
139
|
+
* each configured subscription key replaced with `[REDACTED]`. Returns the
|
|
140
|
+
* original error untouched when no keys are configured.
|
|
141
|
+
*/
|
|
142
|
+
export function redactSubscriptionKeys(error, keys) {
|
|
143
|
+
if (keys.length === 0)
|
|
144
|
+
return error;
|
|
145
|
+
const redact = (s) => {
|
|
146
|
+
let out = s;
|
|
147
|
+
for (const key of keys) {
|
|
148
|
+
if (!key)
|
|
149
|
+
continue;
|
|
150
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
151
|
+
out = out.replace(new RegExp(escaped, 'g'), '[REDACTED]');
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
};
|
|
155
|
+
const redacted = new Error(redact(error.message));
|
|
156
|
+
if (error.stack) {
|
|
157
|
+
redacted.stack = redact(error.stack);
|
|
158
|
+
}
|
|
159
|
+
return redacted;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Fetch a URL via the MCP fetch-proxy gateway (JSON-RPC 2.0 over HTTP).
|
|
163
|
+
* The fetch-proxy server runs in a container that bypasses the AWF Squid proxy.
|
|
164
|
+
*
|
|
165
|
+
* @param url - Fully-qualified URL to fetch.
|
|
166
|
+
* @param ctx - HTTP context adapter.
|
|
167
|
+
* @returns Response text, or null if the gateway call fails.
|
|
168
|
+
*/
|
|
169
|
+
export async function fetchViaGateway(url, ctx) {
|
|
170
|
+
const gatewayUrl = ctx.fetchProxyGatewayUrl;
|
|
171
|
+
if (!gatewayUrl)
|
|
172
|
+
return null;
|
|
173
|
+
const rpcRequest = {
|
|
174
|
+
jsonrpc: '2.0',
|
|
175
|
+
id: Date.now(),
|
|
176
|
+
method: 'tools/call',
|
|
177
|
+
params: {
|
|
178
|
+
name: 'fetch_url',
|
|
179
|
+
arguments: { url },
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
const headers = {
|
|
183
|
+
'Content-Type': 'application/json',
|
|
184
|
+
Accept: 'application/json, text/event-stream',
|
|
185
|
+
};
|
|
186
|
+
if (ctx.fetchProxyApiKey) {
|
|
187
|
+
// Reject CR/LF in the API key to prevent HTTP header injection (mirrors
|
|
188
|
+
// buildAuthorizationHeader in transport/gateway.ts).
|
|
189
|
+
if (/[\r\n]/.test(ctx.fetchProxyApiKey)) {
|
|
190
|
+
console.warn('Invalid IMF fetch-proxy API key: control characters (CR/LF) are not allowed; skipping gateway fallback.');
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
headers['Authorization'] = `Bearer ${ctx.fetchProxyApiKey}`;
|
|
194
|
+
}
|
|
195
|
+
const controller = new AbortController();
|
|
196
|
+
const timer = setTimeout(() => controller.abort(), ctx.timeoutMs);
|
|
197
|
+
try {
|
|
198
|
+
const response = await ctx.fetchImpl(gatewayUrl, {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
headers,
|
|
201
|
+
body: JSON.stringify(rpcRequest),
|
|
202
|
+
signal: controller.signal,
|
|
203
|
+
});
|
|
204
|
+
if (!response.ok)
|
|
205
|
+
return null;
|
|
206
|
+
const body = await response.text();
|
|
207
|
+
const trimmed = body.trimStart();
|
|
208
|
+
let parsed = null;
|
|
209
|
+
if (trimmed.startsWith('data:') || trimmed.startsWith('event:')) {
|
|
210
|
+
parsed = parseSSEResponse(body);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
try {
|
|
214
|
+
parsed = JSON.parse(body);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (!parsed || parsed.error)
|
|
221
|
+
return null;
|
|
222
|
+
const text = parsed.result?.content?.[0]?.text;
|
|
223
|
+
return text && text.length > 0 ? text : null;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
clearTimeout(timer);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=http-transport.js.map
|