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 | 9x 9x 9x 9x 9x 28x 28x 28x 28x 28x 22x 22x 1x 7x 6x 7x 28x 21x 4x 17x 17x 17x 1x 1x 16x 28x 28x 11x 11x 1x 10x 4x 6x 11x 5x 2x 3x 28x 1x | /**
* @module scripts/fetch-calendar/mcp/client
* @description JSON-RPC 2.0 client for the riksdag-regering MCP
* `get_calendar_events` tool.
*
* Throws a typed `CalendarMcpError` on transport, HTTP and protocol errors
* so the orchestrator can distinguish HTML error pages (no retry — fall
* straight back to the web scraper) from network blips (retry).
*
* @author Hack23 AB
* @license Apache-2.0
*/
import type { CalendarFetchConfig } from '../types.js';
import { CalendarMcpError, isDegradedKalenderSentinel, isHtmlErrorResponse } from './errors.js';
export const DEFAULT_MCP_URL =
process.env['MCP_SERVER_URL'] ?? 'https://riksdag-regering-ai.onrender.com/mcp';
export const DEFAULT_TIMEOUT = 15_000;
export const DEFAULT_MAX_RETRIES = 2;
/** Retry base delay (ms); doubled on each subsequent attempt. */
export const RETRY_BASE_DELAY_MS = 1_000;
/** Minimum JSON-RPC 2.0 envelope for a `tools/call` request. */
interface JsonRpcRequest {
jsonrpc: '2.0';
id: number;
method: 'tools/call';
params: { name: string; arguments: Record<string, unknown> };
}
/** Partial shape of a JSON-RPC 2.0 response (only the fields we use). */
interface JsonRpcResponse {
result?: {
content?: Array<{ text?: string }>;
kalender?: unknown[];
events?: unknown[];
[key: string]: unknown;
};
error?: { message?: string; [key: string]: unknown };
[key: string]: unknown;
}
let _rpcId = 1;
/**
* Call the riksdag-regering MCP `get_calendar_events` tool via a single
* JSON-RPC 2.0 POST. Throws a typed `CalendarMcpError` on any transport,
* HTTP, or protocol error so callers can distinguish HTML responses from
* genuine tool failures.
*/
export async function callMcpCalendarEvents(
from: string,
tom: string,
config: Required<Pick<CalendarFetchConfig, 'mcpUrl' | 'timeout' | 'fetchFn'>>,
): Promise<unknown[]> {
const body: JsonRpcRequest = {
jsonrpc: '2.0',
id: _rpcId++,
method: 'tools/call',
params: { name: 'get_calendar_events', arguments: { from, tom } },
};
const controller = new AbortController();
const tid = setTimeout(() => controller.abort(), config.timeout);
let responseText: string;
try {
const response = await config.fetchFn(config.mcpUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream',
},
body: JSON.stringify(body),
signal: controller.signal,
});
responseText = await response.text();
if (!response.ok) {
throw new CalendarMcpError(
`MCP HTTP error: ${response.status} ${response.statusText}`,
isHtmlErrorResponse(responseText) ? 'html' : 'http',
responseText,
);
}
} catch (err) {
if (err instanceof CalendarMcpError) throw err;
const msg = err instanceof Error ? err.message : String(err);
throw new CalendarMcpError(`MCP fetch failed: ${msg}`, 'network');
} finally {
clearTimeout(tid);
}
if (isHtmlErrorResponse(responseText)) {
throw new CalendarMcpError('MCP returned HTML instead of JSON', 'html', responseText);
}
let rpc: JsonRpcResponse;
try {
rpc = JSON.parse(responseText) as JsonRpcResponse;
} catch {
throw new CalendarMcpError(
`MCP response is not valid JSON: ${responseText.slice(0, 120)}`,
'json',
);
}
if (rpc.error) {
const msg = rpc.error.message ?? JSON.stringify(rpc.error);
throw new CalendarMcpError(`MCP tool error: ${msg}`, 'tool');
}
const result = rpc.result ?? {};
const content = result['content'] as Array<{ text?: string }> | undefined;
if (Array.isArray(content) && content[0]?.text) {
let inner: Record<string, unknown>;
try {
inner = JSON.parse(content[0].text) as Record<string, unknown>;
} catch {
throw new CalendarMcpError(
`MCP content text is not valid JSON: ${content[0].text.slice(0, 120)}`,
'json',
);
}
// The server wraps an upstream HTML error in a "successful" envelope with
// an empty events array — detect it so the orchestrator falls back to the
// web scraper instead of trusting a fake zero-event window.
if (isDegradedKalenderSentinel(inner)) {
throw new CalendarMcpError(
`MCP kalender API degraded: ${String(inner['error'] ?? 'upstream HTML error')}`,
'html',
typeof inner['rawHtml'] === 'string' ? inner['rawHtml'] : undefined,
);
}
const events = inner['kalender'] ?? inner['events'];
if (Array.isArray(events)) return events as unknown[];
return [];
}
if (isDegradedKalenderSentinel(result)) {
throw new CalendarMcpError(
`MCP kalender API degraded: ${String(result['error'] ?? 'upstream HTML error')}`,
'html',
typeof result['rawHtml'] === 'string' ? result['rawHtml'] : undefined,
);
}
const direct = result['kalender'] ?? result['events'];
if (Array.isArray(direct)) return direct as unknown[];
return [];
}
|