import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
export interface MCPClientConfig {
command: string;
args: string[];
env?: Record<string, string>;
cwd?: string;
}
export class MCPTestClient {
private client: Client;
private transport: StdioClientTransport | null = null;
private connected = false;
constructor(private config: MCPClientConfig) {
this.client = new Client(
{
name: "mcp-fetch-test-client",
version: "1.0.0"
},
{
capabilities: {}
}
);
}
async connect(): Promise<void> {
if (this.connected) {
throw new Error("Client is already connected");
}
// Filter out undefined values from process.env
const processEnv = Object.entries(process.env).reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = value;
}
return acc;
}, {} as Record<string, string>);
this.transport = new StdioClientTransport({
command: this.config.command,
args: this.config.args,
env: {
...processEnv,
...this.config.env
}
});
try {
await this.client.connect(this.transport);
this.connected = true;
} catch (error) {
throw error;
}
}
async disconnect(): Promise<void> {
if (!this.connected) {
return;
}
await this.client.close();
this.transport = null;
this.connected = false;
}
async connectWithRetry(maxRetries = 3): Promise<void> {
for (let i = 0; i < maxRetries; i++) {
try {
await this.connect();
return;
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
async discoverCapabilities(): Promise<{
tools: any[];
resources: any[];
prompts: any[];
}> {
if (!this.connected) {
throw new Error("Client is not connected");
}
const [tools, resources, prompts] = await Promise.all([
this.listTools(),
this.listResources(),
this.listPrompts()
]);
return { tools, resources, prompts };
}
async listTools(): Promise<any[]> {
try {
const result = await this.client.listTools();
return result.tools;
} catch (error) {
throw error;
}
}
async listResources(): Promise<any[]> {
const result = await this.client.listResources();
return result.resources;
}
async listPrompts(): Promise<any[]> {
const result = await this.client.listPrompts();
return result.prompts;
}
// Generic tool call with error handling
async callTool(name: string, args: any): Promise<any> {
if (!this.connected) {
throw new Error("Client is not connected");
}
try {
const result = await this.client.callTool({
name,
arguments: args
});
// Parse the result based on the CallToolResultSchema
const validatedResult = CallToolResultSchema.parse(result);
// Extract the actual content from the result
if (validatedResult.content && validatedResult.content.length > 0) {
const firstContent = validatedResult.content[0];
if (firstContent.type === 'text') {
const text = firstContent.text;
// Special handling for get-rules which returns a JSON string
if (name === 'get-rules') {
try {
const parsed = JSON.parse(text);
return parsed;
} catch {
return text;
}
}
// For other tools, try to parse as JSON if it looks like JSON
try {
const parsed = JSON.parse(text);
return parsed;
} catch {
// If not JSON, return as text
return text;
}
}
return firstContent;
}
return validatedResult;
} catch (error: any) {
if (error.code === -32601) {
throw new Error(`Tool '${name}' not found`);
}
if (error.code === -32602) {
throw new Error(`Invalid parameters for tool '${name}': ${error.message}`);
}
throw error;
}
}
// HTTP/Fetch tool
async callFetchTool(params: any): Promise<any> {
return await this.callTool("fetch", params);
}
// WebSocket tools
async callSocketTool(action: any): Promise<any> {
return await this.callTool("socket", action);
}
// Puppeteer consolidated tool
async callPuppeteerTool(action: any): Promise<any> {
return await this.callTool("puppeteer", action);
}
// Puppeteer browser tools
async callBrowserTool(action: any): Promise<any> {
return await this.callTool("puppeteer", action);
}
async callPageTool(action: any): Promise<any> {
return await this.callTool("puppeteer", action);
}
async callExecPageTool(pageId: string, source: string): Promise<any> {
return await this.callTool("puppeteer", { pageId, source });
}
// GraphQL tools
async callGraphQLTool(action: any): Promise<any> {
return await this.callTool("graphql", action);
}
async callGraphQLIntrospectTool(params: any): Promise<any> {
// Convert old introspect params to new merged tool format
const mergedParams = {
action: {
type: "introspect",
...params
}
};
return await this.callTool("graphql", mergedParams);
}
// Documentation tool
async callGetRulesTool(rules?: string[]): Promise<any> {
return await this.callTool("get-rules", { rules: rules || ["quickStart"] });
}
}