diff --git a/package-lock.json b/package-lock.json index 56fb93a..bbc6c80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -880,6 +880,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -993,6 +994,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -3051,6 +3053,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3709,6 +3712,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -4018,6 +4022,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -4657,6 +4662,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5370,6 +5376,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -5705,6 +5712,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/test/test-utils.ts b/test/test-utils.ts index a76b4a0..ebed907 100644 --- a/test/test-utils.ts +++ b/test/test-utils.ts @@ -1,5 +1,41 @@ import { spawn, ChildProcess } from "child_process"; +/** + * A single MCP tool description returned by the tools/list method. + * This is a minimal structural type to avoid using `any` while allowing + * the response to include additional properties. + */ +export interface McpToolDescription { + name: string; + description?: string; + // Allow additional properties without losing type safety. + [key: string]: unknown; +} + +/** + * JSON-RPC 2.0 response for the tools/list method, + * when returned as a standard JSON HTTP response. + */ +export interface ListMcpToolsResponse { + jsonrpc: "2.0"; + id: number | string | null; + result?: { + tools?: McpToolDescription[]; + [key: string]: unknown; + }; + error?: { + code: number; + message: string; + data?: unknown; + }; +} + +/** + * Response shape when the tools/list call is delivered via + * Server-Sent Events and parsed by `parseSSEResponse`. + */ +export type ListMcpToolsSseResponse = SSEResponseData; + /** * Wait for server to be ready by polling health endpoint */ @@ -41,7 +77,9 @@ export async function isServerRunning(port: number): Promise { /** * Parse SSE (Server-Sent Events) response */ -export function parseSSEResponse(sseText: string): any { +export type SSEResponseData = Record; + +export function parseSSEResponse(sseText: string): SSEResponseData { const lines = sseText.trim().split("\n"); for (const line of lines) { if (line.startsWith("data: ")) { @@ -158,6 +196,41 @@ export async function initializeMCPSession( return sessionId; } +/** + * JSON-RPC response types for MCP tool calls + */ +interface McpToolError { + code: number; + message: string; + data?: unknown; +} + +interface McpToolSuccessResult { + jsonrpc: "2.0"; + id: number | string | null; + result: unknown; +} + +interface McpToolErrorResult { + jsonrpc: "2.0"; + id: number | string | null; + error: McpToolError; +} + +type McpToolResponse = McpToolSuccessResult | McpToolErrorResult; + +/** + * Counter for generating sequential JSON-RPC IDs for deterministic testing + */ +let nextJsonRpcId = 1; + +/** + * Reset the JSON-RPC ID counter. Call this in test setup/teardown to ensure test isolation. + */ +export function resetJsonRpcId(): void { + nextJsonRpcId = 1; +} + /** * Call MCP tool */ @@ -165,8 +238,8 @@ export async function callMCPTool( baseUrl: string, sessionId: string, toolName: string, - args: any -): Promise { + args: unknown +): Promise { const response = await fetch(baseUrl, { method: "POST", headers: { @@ -181,7 +254,7 @@ export async function callMCPTool( name: toolName, arguments: args, }, - id: Math.floor(Math.random() * 10000), + id: nextJsonRpcId++, }), }); @@ -195,7 +268,8 @@ export async function callMCPTool( const text = await response.text(); return parseSSEResponse(text); } else { - return response.json(); + const jsonResponse = await response.json(); + return jsonResponse as McpToolResponse; } } @@ -205,7 +279,7 @@ export async function callMCPTool( export async function listMCPTools( baseUrl: string, sessionId: string -): Promise { +): Promise { const response = await fetch(baseUrl, { method: "POST", headers: { @@ -230,6 +304,7 @@ export async function listMCPTools( const text = await response.text(); return parseSSEResponse(text); } else { - return response.json(); + const jsonResponse = await response.json(); + return jsonResponse as ListMcpToolsResponse; } }