Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 | 3x 3x 3x 3x 3x 3x 37x 9x 5x 5x 4x 4x 3x 3x 3x 4x 1x 1x 3x 3x 3x 3x 81x 79x 3x 81x 81x 2x 2x 2x 2x 79x 79x 79x 7x 3x 3x 3x 3x 79x 79x 79x 79x 2x 77x 79x 79x 72x 72x 72x 77x 3x 8x 7x 7x 7x 7x 6x 1x 1x 7x 17x 6x 6x | // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
/**
* @module MCP/WBMCPClient
* @description World Bank MCP client — domain-specific tool wrappers for
* the World Bank MCP server ({@link https://github.com/anshumax/world_bank_mcp_server}).
*
* Extends {@link MCPConnection} with World Bank-specific tool methods.
* Provides economic indicator data for EU member states to enrich
* European Parliament news articles with macroeconomic context.
*
* Environment variables:
* - `WB_MCP_SERVER_PATH` — Override default server binary path
* - `WB_MCP_GATEWAY_URL` — Use HTTP gateway transport instead of stdio
* - `WB_MCP_GATEWAY_API_KEY` — API key for gateway authentication
*/
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import { MCPConnection } from './mcp-connection.js';
import type { MCPToolResult, MCPClientOptions } from '../types/index.js';
/** npm binary name for the World Bank MCP server */
const WB_BINARY_NAME = 'worldbank-mcp';
/** Platform-specific binary filename (Windows uses .cmd shim) */
const WB_BINARY_FILE = process.platform === 'win32' ? `${WB_BINARY_NAME}.cmd` : WB_BINARY_NAME;
/** Default binary resolved from node_modules/.bin relative to this file's compiled location */
const WB_DEFAULT_SERVER = resolve(
dirname(fileURLToPath(import.meta.url)),
`../../node_modules/.bin/${WB_BINARY_FILE}`
);
/** Fallback payload when indicator data is unavailable (empty CSV) */
const INDICATOR_FALLBACK = '';
/** EU-27 ISO country codes used for aggregate World Bank fallback queries. */
const EU27_ISO2_CODES: readonly string[] = [
'AT',
'BE',
'BG',
'HR',
'CY',
'CZ',
'DK',
'EE',
'FI',
'FR',
'DE',
'GR',
'HU',
'IE',
'IT',
'LV',
'LT',
'LU',
'MT',
'NL',
'PL',
'PT',
'RO',
'SK',
'SI',
'ES',
'SE',
];
/**
* Canonical list of tools exposed by the World Bank MCP gateway. The news
* workflows, probe script, and the integration test suite all reference this
* list so a regression that adds/removes a tool fails a single drift guard
* (`test/integration/mcp/worldbank-mcp.test.js`) instead of silently breaking
* prompt/validator/probe coverage.
*
* Kept in sync with `analysis/methodologies/worldbank-indicator-mapping.md`.
*/
export const WORLD_BANK_MCP_TOOLS: readonly string[] = [
'search-indicators',
'get-countries',
'get-country-info',
'get-economic-data',
'get-social-data',
'get-education-data',
'get-health-data',
];
/**
* MCP Client for World Bank economic data access.
* Extends {@link MCPConnection} with World Bank-specific tool wrapper methods.
*
* Always supplies an explicit World Bank server path so the base class never
* falls back to the European Parliament MCP server defaults.
*/
export class WorldBankMCPClient extends MCPConnection {
/**
* Create a new World Bank MCP client. Always supplies an explicit World
* Bank server path, gateway URL, and label so the base class never falls
* back to European Parliament defaults.
*
* @param options - Connection options. `serverPath` / `gatewayUrl` /
* `gatewayApiKey` / `serverLabel` are filled from `WB_*` environment
* variables when not provided explicitly.
*/
constructor(options: MCPClientOptions = {}) {
super({
...options,
serverPath: options.serverPath ?? process.env['WB_MCP_SERVER_PATH'] ?? WB_DEFAULT_SERVER,
gatewayUrl: options.gatewayUrl ?? process.env['WB_MCP_GATEWAY_URL'] ?? '',
gatewayApiKey: options.gatewayApiKey ?? process.env['WB_MCP_GATEWAY_API_KEY'] ?? '',
serverLabel: options.serverLabel ?? 'World Bank MCP Server',
});
}
/**
* Get economic indicator data for a specific country.
*
* Calls the `get_indicator_for_country` tool on the World Bank MCP server.
*
* @param countryId - World Bank country code (e.g., 'DEU' for Germany, 'FRA' for France)
* @param indicatorId - World Bank indicator ID (e.g., 'NY.GDP.MKTP.CD' for GDP)
* @returns MCP tool result with CSV-formatted indicator data, or empty text on error
*/
async getIndicatorForCountry(countryId: string, indicatorId: string): Promise<MCPToolResult> {
if (!countryId || !indicatorId) {
console.warn('get_indicator_for_country called without required countryId or indicatorId');
return { content: [{ type: 'text', text: INDICATOR_FALLBACK }] };
}
try {
return await this.callTool('get_indicator_for_country', {
country_id: countryId,
indicator_id: indicatorId,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.warn('get_indicator_for_country not available:', message);
return { content: [{ type: 'text', text: INDICATOR_FALLBACK }] };
}
}
/**
* Aggregate a World Bank indicator across EU-27 member states.
*
* This is a client-side fallback for aggregate codes such as `EUU` that are
* rejected by the upstream `worldbank-mcp` server.
*
* **Important:** This method uses simple summation (`aggregation: "sum"`),
* which is only meaningful for **additive** flow/stock metrics (e.g.,
* absolute GDP in USD, total population, total exports). For ratios,
* percentages, rates, or per-capita indicators (e.g., GDP growth %,
* GDP per capita, school enrollment %, health expenditure as % of GDP),
* a plain sum is mathematically incorrect. Callers must verify the
* indicator is additive before using the result.
*
* @param toolName - World Bank MCP tool (`get-economic-data`, `get-social-data`, `get-education-data`, `get-health-data`)
* @param indicator - Indicator key accepted by the selected tool (must be an additive metric — not a ratio or percentage)
* @param years - Number of years to request (default 10)
* @returns MCP-like JSON payload with summed year-series across EU-27 (includes `aggregation: "sum"` field)
*/
async getEU27Aggregate(
toolName: 'get-economic-data' | 'get-social-data' | 'get-education-data' | 'get-health-data',
indicator: string,
years: number = 10
): Promise<MCPToolResult> {
if (!indicator || indicator.trim().length === 0) {
console.warn('getEU27Aggregate called without required indicator');
return { content: [{ type: 'text', text: '{"scope":"EU27","series":[]}' }] };
}
const seriesByYear = new Map<number, number>();
const failedCountries: string[] = [];
const noDataCountries: string[] = [];
const results = await Promise.allSettled(
EU27_ISO2_CODES.map(async (countryCode) => {
const result = await this.callTool(toolName, { countryCode, indicator, years });
return { countryCode, result };
})
);
for (let i = 0; i < results.length; i++) {
const settled = results[i];
if (!settled || settled.status === 'rejected') {
const cc = EU27_ISO2_CODES[i] ?? 'XX';
console.warn(`getEU27Aggregate: ${toolName} failed for ${cc}`);
failedCountries.push(cc);
continue;
}
const { countryCode, result } = settled.value;
const contributed = _accumulateCountryData(result, seriesByYear);
if (!contributed) {
noDataCountries.push(countryCode);
}
}
const series = [...seriesByYear.entries()]
.sort((a, b) => a[0] - b[0])
.map(([year, value]) => ({ year, value }));
const contributingCountries =
EU27_ISO2_CODES.length - failedCountries.length - noDataCountries.length;
return {
content: [
{
type: 'text',
text: JSON.stringify({
scope: 'EU27',
tool: toolName,
indicator,
years,
aggregation: 'sum',
series,
contributingCountries,
failedCountries,
noDataCountries,
}),
},
],
};
}
}
/**
* Parse a single country result and accumulate valid data points into the year map.
*
* @param result - MCP tool result from a single country call
* @param seriesByYear - Accumulator map of year → summed value
* @returns true if at least one valid data point was added
*/
function _accumulateCountryData(result: MCPToolResult, seriesByYear: Map<number, number>): boolean {
const text = result.content?.[0]?.text;
Iif (typeof text !== 'string' || text.length === 0) {
return false;
}
let parsed: { data?: Array<{ year?: number; value?: number | null }> };
try {
parsed = JSON.parse(text) as typeof parsed;
} catch {
return false;
}
const data = Array.isArray(parsed.data) ? parsed.data : [];
let contributed = false;
for (const point of data) {
Eif (
typeof point?.year === 'number' &&
Number.isFinite(point.year) &&
typeof point?.value === 'number' &&
Number.isFinite(point.value)
) {
seriesByYear.set(point.year, (seriesByYear.get(point.year) ?? 0) + point.value);
contributed = true;
}
}
return contributed;
}
/** Singleton World Bank MCP client instance */
let wbClientInstance: WorldBankMCPClient | null = null;
/**
* Get or create singleton World Bank MCP client instance.
*
* Uses `WB_MCP_SERVER_PATH`, `WB_MCP_GATEWAY_URL`, and `WB_MCP_GATEWAY_API_KEY`
* environment variables for configuration. Falls back to stdio transport
* with the `worldbank-mcp` npm binary resolved from `node_modules/.bin`.
*
* @param options - Client options (overrides env vars)
* @returns Connected World Bank MCP client
*/
export async function getWBMCPClient(options: MCPClientOptions = {}): Promise<WorldBankMCPClient> {
if (!wbClientInstance) {
const mergedOptions: MCPClientOptions = {
...options,
maxConnectionAttempts: options.maxConnectionAttempts ?? 2,
connectionRetryDelay: options.connectionRetryDelay ?? 1000,
};
const client = new WorldBankMCPClient(mergedOptions);
try {
await client.connect();
wbClientInstance = client;
} catch (error) {
wbClientInstance = null;
throw error;
}
}
return wbClientInstance;
}
/**
* Close and cleanup singleton World Bank MCP client
*/
export async function closeWBMCPClient(): Promise<void> {
if (wbClientInstance) {
wbClientInstance.disconnect();
wbClientInstance = null;
}
}
|