mcp-server-tester-sse-http-stdio
Version:
MCP Server Tester with SSE support - Test MCP servers using HTTP, SSE, and STDIO transports
165 lines (164 loc) • 5.63 kB
JavaScript
/**
* MCP Client connection management with transport support
*/
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
export class McpClient {
client;
transport = null;
connected = false;
constructor() {
this.client = new Client({
name: 'mcp-tester',
version: '1.4.0',
});
}
async connect(options) {
if (this.connected) {
throw new Error('Client is already connected');
}
try {
this.transport = await this.createTransport(options);
await this.client.connect(this.transport);
this.connected = true;
}
catch (error) {
throw new Error(`Failed to connect to MCP server: ${error instanceof Error ? error.message : String(error)}`);
}
}
async disconnect() {
if (!this.connected) {
return;
}
try {
await this.client.close();
this.connected = false;
}
catch (error) {
console.error('Error disconnecting from MCP server:', error);
}
}
// Expose the raw client for direct SDK usage
get sdk() {
if (!this.connected) {
throw new Error('Client is not connected. Call connect() first.');
}
return this.client;
}
async createTransport(options) {
switch (options.type) {
case 'stdio':
return this.createStdioTransport(options);
case 'http':
return this.createHTTPTransport(options);
case 'sse':
return this.createSSETransport(options);
default:
throw new Error(`Unsupported transport type: ${options.type}`);
}
}
async createStdioTransport(options) {
if (!options.command) {
throw new Error('Command is required for stdio transport');
}
return new StdioClientTransport({
command: options.command,
args: options.args || [],
env: {
...Object.fromEntries(Object.entries(process.env)
.filter(([_, value]) => value !== undefined)
.map(([key, value]) => [key, value])),
...(options.env || {}),
},
});
}
async createHTTPTransport(options) {
if (!options.url) {
throw new Error('URL is required for HTTP transport');
}
const httpOptions = {};
// Add Bearer token to headers if provided
if (options.bearerToken) {
httpOptions.requestInit = {
headers: {
'Authorization': `Bearer ${options.bearerToken}`
}
};
}
return new StreamableHTTPClientTransport(new URL(options.url), httpOptions);
}
async createSSETransport(options) {
if (!options.url) {
throw new Error('URL is required for SSE transport');
}
// SSEClientTransport expects just the URL directly
return new SSEClientTransport(new URL(options.url));
}
}
export function createTransportOptions(serverConfig) {
// Use transport type from config if specified
if (serverConfig.transport) {
// For HTTP/SSE transports, include URL and optional Bearer token
if (serverConfig.transport === 'http' || serverConfig.transport === 'sse') {
return {
type: serverConfig.transport,
url: serverConfig.url,
bearerToken: serverConfig.bearerToken, // Pass Bearer token if provided
};
}
// For stdio, include command and args
return {
type: serverConfig.transport,
command: serverConfig.command,
args: serverConfig.args,
env: serverConfig.env,
};
}
// Default to stdio for command-based servers
return {
type: 'stdio',
command: serverConfig.command,
args: serverConfig.args,
env: serverConfig.env,
};
}
export function createTransportOptionsFromUrl(url) {
const parsedUrl = new URL(url);
// Extract Bearer token from URL query parameters if present
const bearerToken = parsedUrl.searchParams.get('token') || undefined;
// Remove token from URL to keep it clean
if (bearerToken) {
parsedUrl.searchParams.delete('token');
}
// Determine transport type based on URL path
if (parsedUrl.pathname.endsWith('/mcp')) {
return { type: 'http', url: parsedUrl.toString(), bearerToken };
}
else {
// Default to HTTP for URLs
return { type: 'http', url: parsedUrl.toString(), bearerToken };
}
}
export function createServerConfigFromCli(command, args, env) {
const serverConfig = {
command,
};
// Parse comma-separated args
if (args) {
serverConfig.args = args.split(',').map(arg => arg.trim());
}
// Parse key=value,key2=value2 environment variables
if (env) {
serverConfig.env = {};
const envPairs = env.split(',');
for (const pair of envPairs) {
const [key, value] = pair.split('=');
if (key && value) {
serverConfig.env[key.trim()] = value.trim();
}
}
}
return serverConfig;
}