Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 82 additions & 7 deletions test/test-utils.ts
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -41,7 +77,9 @@ export async function isServerRunning(port: number): Promise<boolean> {
/**
* Parse SSE (Server-Sent Events) response
*/
export function parseSSEResponse(sseText: string): any {
export type SSEResponseData = Record<string, unknown>;

export function parseSSEResponse(sseText: string): SSEResponseData {
const lines = sseText.trim().split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
Expand Down Expand Up @@ -158,15 +196,50 @@ 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
*/
export async function callMCPTool(
baseUrl: string,
sessionId: string,
toolName: string,
args: any
): Promise<any> {
args: unknown
): Promise<McpToolResponse | SSEResponseData> {
const response = await fetch(baseUrl, {
method: "POST",
headers: {
Expand All @@ -181,7 +254,7 @@ export async function callMCPTool(
name: toolName,
arguments: args,
},
id: Math.floor(Math.random() * 10000),
id: nextJsonRpcId++,
}),
});

Expand All @@ -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;
}
}

Expand All @@ -205,7 +279,7 @@ export async function callMCPTool(
export async function listMCPTools(
baseUrl: string,
sessionId: string
): Promise<any> {
): Promise<ListMcpToolsResponse | ListMcpToolsSseResponse> {
const response = await fetch(baseUrl, {
method: "POST",
headers: {
Expand All @@ -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;
}
}