Skip to main content

Overview

MCPClientManager is the central orchestration class for managing multiple MCP (Model Context Protocol) server connections within MCPJam Inspector. It provides a high-level abstraction over the @modelcontextprotocol/sdk Client class, handling connection lifecycle, transport selection, and unified access to MCP capabilities across multiple servers. Location: sdk/src/mcp-client-manager/index.ts Key Responsibilities:
  • Managing multiple MCP server connections with unique identifiers
  • Auto-detecting and configuring appropriate transports (STDIO, SSE, Streamable HTTP)
  • Providing unified APIs for tools, resources, prompts, and elicitations
  • Handling connection state, reconnection, and error recovery
  • Integrating with AI frameworks (Vercel AI SDK)
  • Supporting elicitation (interactive prompts from MCP servers)

Architecture

Class Structure

export class MCPClientManager {
  // State management
  private readonly clientStates: Map<string, ManagedClientState>
  private readonly notificationHandlers: Map<string, Map<NotificationSchema, Set<NotificationHandler>>>
  private readonly elicitationHandlers: Map<string, ElicitationHandler>
  private readonly toolsMetadataCache: Map<string, Map<string, any>>

  // Configuration
  private readonly defaultClientVersion: string
  private readonly defaultCapabilities: ClientCapabilityOptions
  private readonly defaultTimeout: number

  // Logging
  private defaultLogJsonRpc: boolean
  private defaultRpcLogger?: (event: {...}) => void

  // Elicitation support
  private elicitationCallback?: (request: {...}) => Promise<ElicitResult>
  private readonly pendingElicitations: Map<string, {...}>
}

Server Configuration Types

The manager supports two transport types, automatically selected based on configuration:

STDIO Configuration

type StdioServerConfig = BaseServerConfig & {
  command: string; // Command to execute (e.g., "npx")
  args?: string[]; // Command arguments
  env?: Record<string, string>; // Environment variables
};

HTTP/SSE Configuration

type HttpServerConfig = BaseServerConfig & {
  url: URL; // Server endpoint
  requestInit?: RequestInit; // Fetch options (headers, etc.)
  eventSourceInit?: EventSourceInit; // SSE options
  authProvider?: AuthProvider; // OAuth provider
  reconnectionOptions?: ReconnectionOptions;
  sessionId?: string;
  preferSSE?: boolean; // Force SSE over Streamable HTTP
};

Base Configuration

type BaseServerConfig = {
  capabilities?: ClientCapabilityOptions;  // MCP capabilities to advertise
  timeout?: number;         // Request timeout (default: 120s)
  version?: string;         // Client version string
  onError?: (error: unknown) => void;  // Error callback
  logJsonRpc?: boolean;     // Enable console JSON-RPC logging
  rpcLogger?: (event: {...}) => void;  // Custom RPC logger
}

Core Concepts

1. Connection Lifecycle

The manager maintains three connection states:
  • disconnected: No client exists, no connection attempt in progress
  • connecting: Connection attempt in progress (tracked via state.promise)
  • connected: Client successfully connected and ready
State transitions:
disconnected → connecting (via connectToServer)
connecting → connected (on successful connect)
connecting → disconnected (on connection failure)
connected → disconnected (on close/disconnect)
Connection Status Verification: The manager verifies connection status by attempting a ping to the server each time getConnectionStatusByAttemptingPing() is called. This ensures the status reflects the actual server availability rather than cached state.

2. Transport Selection

The manager automatically selects the appropriate transport:
  1. STDIO Transport: Used when config has command property
    • Spawns subprocess with StdioClientTransport
    • Manages stdin/stdout/stderr streams
    • Includes default environment variables
  2. HTTP Transport: Used when config has url property
    • Streamable HTTP (default): Bidirectional streaming over HTTP
    • SSE (fallback): Server-Sent Events for unidirectional streaming
    • Auto-fallback: Tries Streamable HTTP first, falls back to SSE on failure
    • Force SSE: Set preferSSE: true or use URL ending in /sse

3. Tool Metadata Caching

The manager caches tool _meta fields for OpenAI Apps SDK compatibility:
// During listTools, metadata is extracted and cached
for (const tool of result.tools) {
  if (tool._meta) {
    metadataMap.set(tool.name, tool._meta);
  }
}
this.toolsMetadataCache.set(serverId, metadataMap);
Access via getAllToolsMetadata(serverId) to get all tool metadata for a server.

4. Elicitation Support

Elicitation allows MCP servers to request interactive input during tool execution: Two modes:
  1. Server-specific handler: Set per-server via setElicitationHandler(serverId, handler)
  2. Global callback: Set globally via setElicitationCallback(callback)
Pending elicitation pattern (used in chat endpoint):
// Set global callback that emits SSE event and waits for response
manager.setElicitationCallback(async (request) => {
  emitSSE({ type: 'elicitation_request', requestId: request.requestId, ... });

  return new Promise((resolve, reject) => {
    manager.getPendingElicitations().set(request.requestId, { resolve, reject });
    setTimeout(() => reject(new Error('Timeout')), 300000);
  });
});

// Later, when user responds via API:
manager.respondToElicitation(requestId, userResponse);

5. JSON-RPC Logging

RPC logging can be enabled at three levels:
  1. Global default: new MCPClientManager({}, { defaultLogJsonRpc: true })
  2. Global custom logger: new MCPClientManager({}, { rpcLogger: (event) => {...} })
  3. Per-server: { serverId: { ..., logJsonRpc: true } } or rpcLogger: (event) => {...}
The manager wraps transports with a LoggingTransport that intercepts all JSON-RPC messages.

Usage Patterns in MCPJam Inspector

1. App Initialization

const mcpClientManager = new MCPClientManager(
  {},  // Start with no servers
  {
    // Wire RPC logging to SSE bus for real-time inspection
    rpcLogger: ({ direction, message, serverId }) => {
      rpcLogBus.publish({
        serverId,
        direction,
        timestamp: new Date().toISOString(),
        message,
      });
    },
  }
);

// Inject into Hono context for all routes
app.use("\*", async (c, next) => {
c.mcpClientManager = mcpClientManager;
await next();
});

2. Chat Endpoint

// Get tools with server metadata attached
const toolsets = await mcpClientManager.getToolsForAiSdk(
  requestData.selectedServers  // Optional: specific servers only
);

// Set up elicitation handling
mcpClientManager.setElicitationCallback(async (request) => {
  // Emit SSE event to client
  sendSseEvent(controller, encoder, {
    type: 'elicitation_request',
    requestId: request.requestId,
    message: request.message,
    schema: request.schema,
  });

  // Return promise resolved when user responds
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => reject(new Error('Timeout')), 300000);
    mcpClientManager.getPendingElicitations().set(request.requestId, {
      resolve: (response) => { clearTimeout(timeout); resolve(response); },
      reject: (error) => { clearTimeout(timeout); reject(error); },
    });
  });
});

// Stream text with tools
const streamResult = await streamText({
  model,
  tools: toolsets,
  messages,
  // Tool calls handled automatically by AI SDK using mcpClientManager.executeTool
});

// Clean up
mcpClientManager.clearElicitationCallback();

3. HTTP Bridge

// Handle JSON-RPC tool call
case "tools/call": {
  // Support prefixed tool names (serverId:toolName)
  let targetServerId = serverId;
  let toolName = params?.name;

if (toolName?.includes(":")) {
const [prefix, actualName] = toolName.split(":", 2);
if (clientManager.hasServer(prefix)) {
targetServerId = prefix;
}
toolName = actualName;
}

const result = await clientManager.executeTool(
targetServerId,
toolName,
params?.arguments ?? {}
);

return respond({ result });
}

4. Managing Server Connections

// Add server dynamically
await mcpClientManager.connectToServer("filesystem", {
  command: "npx",
  args: ["-y", "@modelcontextprotocol/server-filesystem", "/path"],
  env: { DEBUG: "1" },
});

// Check status (verifies with ping)
const status = mcpClientManager.getConnectionStatusByAttemptingPing("filesystem");
// Returns: "connected" | "connecting" | "disconnected"

// Get all servers
const summaries = mcpClientManager.getServerSummaries();
// Returns: { id: string, status: MCPConnectionStatus, config: MCPServerConfig }[]

// Disconnect
await mcpClientManager.disconnectServer("filesystem");

// Disconnect all
await mcpClientManager.disconnectAllServers();

API Reference

Connection Management

connectToServer
(serverId: string, config: MCPServerConfig) => Promise<Client>
Connect to an MCP server. Throws if server ID already exists. Returns the connected Client instance.
disconnectServer
(serverId: string) => Promise<void>
Disconnect from a server and clean up resources.
disconnectAllServers
() => Promise<void>
Disconnect from all servers and reset state.
removeServer
(serverId: string) => void
Remove server from state without attempting disconnection.
listServers
() => string[]
Get array of all server IDs.
hasServer
(serverId: string) => boolean
Check if a server is registered.
getServerSummaries
() => ServerSummary[]
Get status and config for all servers.
getConnectionStatusByAttemptingPing
(serverId: string) => MCPConnectionStatus
Get connection status by attempting a ping to verify server availability: "connected" | "connecting" | "disconnected". This method actively checks the connection rather than relying on cached state.
getServerConfig
(serverId: string) => MCPServerConfig | undefined
Get configuration for a server.

Tools

listTools
(serverId: string, params?, options?) => Promise<ListToolsResult>
List tools for a single server. Caches metadata. Returns empty list if unsupported.
getTools
(serverIds?: string[]) => Promise<ListToolsResult>
Get tools from multiple servers (or all if not specified). Returns flattened list.
executeTool
(serverId: string, toolName: string, args: Record<string, unknown>, options?) => Promise<CallToolResult>
Execute a tool on a specific server.
getToolsForAiSdk
(serverIds?: string[] | string, options?) => Promise<ToolSet>
Get tools in Vercel AI SDK format. Automatically wires up tool execution. Each tool has _serverId metadata attached.
Options:
schemas
ToolSchemaOverrides | 'automatic'
  • schemas?: ToolSchemaOverrides | "automatic" - Control schema conversion
getAllToolsMetadata
(serverId: string) => Record<string, Record<string, any>>
Get all tool _meta fields for OpenAI Apps SDK.
pingServer
(serverId: string, options?) => void
Send ping to server.

Resources

listResources
(serverId: string, params?, options?) => Promise<ResourceListResult>
List available resources. Returns empty if unsupported.
readResource
(serverId: string, params: { uri: string }, options?) => Promise<ReadResourceResult>
Read a resource by URI.
subscribeResource
(serverId: string, params: { uri: string }, options?) => Promise<void>
Subscribe to resource updates.
unsubscribeResource
(serverId: string, params: { uri: string }, options?) => Promise<void>
Unsubscribe from resource updates.
listResourceTemplates
(serverId: string, params?, options?) => Promise<ResourceTemplateListResult>
List resource templates.

Prompts

listPrompts
(serverId: string, params?, options?) => Promise<PromptListResult>
List available prompts. Returns empty if unsupported.
getPrompt
(serverId: string, params: { name: string, arguments?: Record<string, string> }, options?) => Promise<GetPromptResult>
Get a prompt with optional arguments.

Notifications

addNotificationHandler
(serverId: string, schema: NotificationSchema, handler: NotificationHandler) => void
Add a notification handler for a server.
onResourceListChanged
(serverId: string, handler: NotificationHandler) => void
Handle resources/list_changed notifications.
onResourceUpdated
(serverId: string, handler: NotificationHandler) => void
Handle resources/updated notifications.
onPromptListChanged
(serverId: string, handler: NotificationHandler) => void
Handle prompts/list_changed notifications.

Elicitation

setElicitationHandler
(serverId: string, handler: ElicitationHandler) => void
Set server-specific elicitation handler.
clearElicitationHandler
(serverId: string) => void
Remove server-specific handler.
setElicitationCallback
(callback: (request: {...}) => Promise<ElicitResult>) => void
Set global elicitation callback (used if no server-specific handler).
clearElicitationCallback
() => void
Remove global callback.
getPendingElicitations
() => Map<string, { resolve, reject }>
Get map of pending elicitation promise resolvers.
respondToElicitation
(requestId: string, response: ElicitResult) => boolean
Resolve a pending elicitation. Returns true if found.

Advanced

getClient
(serverId: string) => Client | undefined
Get raw MCP SDK Client instance for advanced usage.
getSessionIdByServer
(serverId: string) => string | undefined
Get session ID for Streamable HTTP servers.

Examples

Example 1: Basic Setup with Multiple Servers

import { MCPClientManager } from "@/sdk";

const manager = new MCPClientManager({
// STDIO server
filesystem: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
},

// HTTP server with auth
asana: {
url: new URL("https://mcp.asana.com/sse"),
requestInit: {
headers: {
Authorization: `Bearer ${process.env.ASANA_TOKEN}`,
},
},
},
});

// List all tools
const { tools } = await manager.getTools();
console.log(`Total tools: ${tools.length}`);

// Execute tool
const result = await manager.executeTool("filesystem", "read_file", {
path: "/tmp/test.txt",
});

Example 2: Vercel AI SDK Integration

import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";

const manager = new MCPClientManager({
  everything: {
    command: "npx",
    args: ["-y", "@modelcontextprotocol/server-everything"],
  },
});

const response = await generateText({
  model: openai("gpt-4o"),
  tools: await manager.getToolsForAiSdk(),
  messages: [{ role: "user", content: "Add 5 and 7" }],
});

console.log(response.text);

Example 3: Dynamic Server Management

const manager = new MCPClientManager();

// Add server on demand
await manager.connectToServer("weather", {
url: new URL("http://localhost:3000/mcp"),
});

// Check status (verifies with ping)
if (manager.getConnectionStatusByAttemptingPing("weather") === "connected") {
const { resources } = await manager.listResources("weather");
console.log(resources);
}

// Remove when done
await manager.disconnectServer("weather");

Example 4: Resource Subscriptions

// Subscribe to resource updates
manager.onResourceUpdated("docs", (notification) => {
  console.log("Resource updated:", notification.params.uri);
});

await manager.subscribeResource("docs", {
  uri: "file:///README.md",
});

// Read resource
const resource = await manager.readResource("docs", {
  uri: "file:///README.md",
});
console.log(resource.contents[0].text);

Example 5: Custom RPC Logging

const manager = new MCPClientManager(
  {
    server1: { command: "mcp-server", args: [] },
  },
  {
    rpcLogger: ({ direction, message, serverId }) => {
      const timestamp = new Date().toISOString();
      console.log(`[${timestamp}][${serverId}][${direction}]`, message);

      // Save to database or monitoring system
      logToDatabase({ timestamp, serverId, direction, message });
    },

}
);

Best Practices

Connection Management

DO:
  • Use unique, descriptive server IDs
  • Check connection status with ping verification before operations
  • Handle connection errors gracefully
  • Clean up connections when no longer needed
DON’T:
  • Reuse server IDs without disconnecting first
  • Assume connections are always ready without verification
  • Leave connections open indefinitely
  • Ignore connection state changes

Tool Execution

DO:
  • Validate tool arguments before execution
  • Set appropriate timeouts for long-running tools
  • Handle tool errors with meaningful messages
  • Use getToolsForAiSdk for AI framework integration
DON’T:
  • Execute tools without checking server status
  • Use hardcoded tool names without verification
  • Ignore tool execution errors
  • Mix manual tool calling with AI SDK integration

Elicitation Handling

DO:
  • Set timeouts for elicitation responses
  • Clean up pending elicitations on errors
  • Use server-specific handlers for custom logic
  • Clear callbacks when done to prevent leaks
DON’T:
  • Leave elicitations pending indefinitely
  • Forget to respond to elicitation requests
  • Mix server-specific and global handlers unexpectedly
  • Ignore elicitation errors

Performance

DO:
  • Cache tool metadata when possible
  • Reuse connections across requests
  • Use parallel operations with getTools()
  • Monitor connection health
DON’T:
  • Create new connections for each request
  • Poll for updates without subscriptions
  • Ignore connection pool limits
  • Skip cleanup on shutdown

Troubleshooting

Cause: Attempting to connect with a server ID that’s already in use.Solution: Disconnect first or use a different ID.
Cause: Attempting operations on a disconnected server.Solution: Check getConnectionStatus() and connect if needed.
Cause: Server doesn’t support the requested capability.Solution: The manager returns empty results for unsupported methods (tools/list, resources/list, prompts/list).
Cause: Network issues, server not running, or incorrect configuration.Solution:
  • Verify server is accessible
  • Check configuration (URL, command, args)
  • Review server logs for errors
  • Use RPC logging to debug protocol issues

See Also

  • sdk/mcp-client-manager/README.md - Public-facing documentation
  • sdk/mcp-client-manager/goal.md - Original design goals
  • sdk/mcp-client-manager/tool-converters.ts - AI SDK conversion logic
  • server/routes/mcp/chat.ts - Chat endpoint usage
  • server/services/mcp-http-bridge.ts - HTTP bridge implementation