> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mcpjam.com/llms.txt
> Use this file to discover all available pages before exploring further.

# MCPClientManager

> Technical documentation for the MCPClientManager orchestration class

## 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

```typescript theme={"theme":"css-variables"}
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

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

#### HTTP/SSE Configuration

```typescript theme={"theme":"css-variables"}
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

```typescript theme={"theme":"css-variables"}
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 `getConnectionStatus()` 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:

```typescript theme={"theme":"css-variables"}
// 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):

```typescript theme={"theme":"css-variables"}
// 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

<CodeGroup>
  ```typescript server/app.ts theme={"theme":"css-variables"}
  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();
  });

  ```
</CodeGroup>

### 2. Chat Endpoint

<CodeGroup>
  ```typescript server/routes/mcp/chat.ts theme={"theme":"css-variables"}
  // 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();
  ```
</CodeGroup>

### 3. HTTP Bridge

<CodeGroup>
  ```typescript server/services/mcp-http-bridge.ts theme={"theme":"css-variables"}
  // 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 });
  }

  ```
</CodeGroup>

### 4. Managing Server Connections

<CodeGroup>
  ```typescript Connection Management theme={"theme":"css-variables"}
  // 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.getConnectionStatus("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();
  ```
</CodeGroup>

## API Reference

### Connection Management

<ResponseField name="connectToServer" type="(serverId: string, config: MCPServerConfig) => Promise<Client>">
  Connect to an MCP server. Throws if server ID already exists. Returns the
  connected Client instance.
</ResponseField>

<ResponseField name="disconnectServer" type="(serverId: string) => Promise<void>">
  Disconnect from a server and clean up resources.
</ResponseField>

<ResponseField name="disconnectAllServers" type="() => Promise<void>">
  Disconnect from all servers and reset state.
</ResponseField>

<ResponseField name="removeServer" type="(serverId: string) => void">
  Remove server from state without attempting disconnection.
</ResponseField>

<ResponseField name="listServers" type="() => string[]">
  Get array of all server IDs.
</ResponseField>

<ResponseField name="hasServer" type="(serverId: string) => boolean">
  Check if a server is registered.
</ResponseField>

<ResponseField name="getServerSummaries" type="() => ServerSummary[]">
  Get status and config for all servers.
</ResponseField>

<ResponseField name="getConnectionStatus" type="(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.
</ResponseField>

<ResponseField name="getServerConfig" type="(serverId: string) => MCPServerConfig | undefined">
  Get configuration for a server.
</ResponseField>

### Tools

<ResponseField name="listTools" type="(serverId: string, params?, options?) => Promise<ListToolsResult>">
  List tools for a single server. Caches metadata. Returns empty list if
  unsupported.
</ResponseField>

<ResponseField name="getTools" type="(serverIds?: string[]) => Promise<ListToolsResult>">
  Get tools from multiple servers (or all if not specified). Returns flattened
  list.
</ResponseField>

<ResponseField name="executeTool" type="(serverId: string, toolName: string, args: Record<string, unknown>, options?) => Promise<CallToolResult>">
  Execute a tool on a specific server.
</ResponseField>

<ResponseField name="getToolsForAiSdk" type="(serverIds?: string[] | string, options?) => Promise<ToolSet>">
  Get tools in Vercel AI SDK format. Automatically wires up tool execution. Each
  tool has `_serverId` metadata attached. App-only tools (`_meta.ui.visibility =   ["app"]`) are excluded by default per SEP-1865.
</ResponseField>

Options:

<ResponseField name="schemas" type="ToolSchemaOverrides | 'automatic'">
  * `schemas?: ToolSchemaOverrides | "automatic"` - Control schema conversion
</ResponseField>

<ResponseField name="includeAppOnly" type="boolean">
  * `includeAppOnly?: boolean` - When `true`, includes SEP-1865 app-only tools
    (`_meta.ui.visibility = ["app"]`) in the returned tool set. Defaults to
    `false`. Use only when intentionally mirroring a host that does not
    implement visibility filtering.
</ResponseField>

<ResponseField name="getAllToolsMetadata" type="(serverId: string) => Record<string, Record<string, any>>">
  Get all tool `_meta` fields for OpenAI Apps SDK.
</ResponseField>

<ResponseField name="pingServer" type="(serverId: string, options?) => void">
  Send ping to server.
</ResponseField>

### Resources

<ResponseField name="listResources" type="(serverId: string, params?, options?) => Promise<ResourceListResult>">
  List available resources. Returns empty if unsupported.
</ResponseField>

<ResponseField name="readResource" type="(serverId: string, params: { uri: string }, options?) => Promise<ReadResourceResult>">
  Read a resource by URI.
</ResponseField>

<ResponseField name="subscribeResource" type="(serverId: string, params: { uri: string }, options?) => Promise<void>">
  Subscribe to resource updates.
</ResponseField>

<ResponseField name="unsubscribeResource" type="(serverId: string, params: { uri: string }, options?) => Promise<void>">
  Unsubscribe from resource updates.
</ResponseField>

<ResponseField name="listResourceTemplates" type="(serverId: string, params?, options?) => Promise<ResourceTemplateListResult>">
  List resource templates.
</ResponseField>

### Prompts

<ResponseField name="listPrompts" type="(serverId: string, params?, options?) => Promise<PromptListResult>">
  List available prompts. Returns empty if unsupported.
</ResponseField>

<ResponseField name="getPrompt" type="(serverId: string, params: { name: string, arguments?: Record<string, string> }, options?) => Promise<GetPromptResult>">
  Get a prompt with optional arguments.
</ResponseField>

### Notifications

<ResponseField name="addNotificationHandler" type="(serverId: string, schema: NotificationSchema, handler: NotificationHandler) => void">
  Add a notification handler for a server.
</ResponseField>

<ResponseField name="onResourceListChanged" type="(serverId: string, handler: NotificationHandler) => void">
  Handle `resources/list_changed` notifications.
</ResponseField>

<ResponseField name="onResourceUpdated" type="(serverId: string, handler: NotificationHandler) => void">
  Handle `resources/updated` notifications.
</ResponseField>

<ResponseField name="onPromptListChanged" type="(serverId: string, handler: NotificationHandler) => void">
  Handle `prompts/list_changed` notifications.
</ResponseField>

### Elicitation

<ResponseField name="setElicitationHandler" type="(serverId: string, handler: ElicitationHandler) => void">
  Set server-specific elicitation handler.
</ResponseField>

<ResponseField name="clearElicitationHandler" type="(serverId: string) => void">
  Remove server-specific handler.
</ResponseField>

<ResponseField name="setElicitationCallback" type="(callback: (request: {...}) => Promise<ElicitResult>) => void">
  Set global elicitation callback (used if no server-specific handler).
</ResponseField>

<ResponseField name="clearElicitationCallback" type="() => void">
  Remove global callback.
</ResponseField>

<ResponseField name="getPendingElicitations" type="() => Map<string, { resolve, reject }>">
  Get map of pending elicitation promise resolvers.
</ResponseField>

<ResponseField name="respondToElicitation" type="(requestId: string, response: ElicitResult) => boolean">
  Resolve a pending elicitation. Returns `true` if found.
</ResponseField>

### Advanced

<ResponseField name="getClient" type="(serverId: string) => Client | undefined">
  Get raw MCP SDK Client instance for advanced usage.
</ResponseField>

<ResponseField name="getSessionIdByServer" type="(serverId: string) => string | undefined">
  Get session ID for Streamable HTTP servers.
</ResponseField>

## Examples

### Example 1: Basic Setup with Multiple Servers

<CodeGroup>
  ```typescript Basic Setup theme={"theme":"css-variables"}
  import { MCPClientManager } from "@mcpjam/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",
  });

  ```
</CodeGroup>

### Example 2: Vercel AI SDK Integration

<CodeGroup>
  ```typescript AI SDK Integration theme={"theme":"css-variables"}
  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);
  ```
</CodeGroup>

### Example 3: Dynamic Server Management

<CodeGroup>
  ```typescript Dynamic Management theme={"theme":"css-variables"}
  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.getConnectionStatus("weather") === "connected") {
  const { resources } = await manager.listResources("weather");
  console.log(resources);
  }

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

  ```
</CodeGroup>

### Example 4: Resource Subscriptions

<CodeGroup>
  ```typescript Resource Subscriptions theme={"theme":"css-variables"}
  // 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);
  ```
</CodeGroup>

### Example 5: Custom RPC Logging

<CodeGroup>
  ```typescript Custom Logging theme={"theme":"css-variables"}
  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 });
      },

  }
  );

  ```
</CodeGroup>

## Best Practices

<CardGroup cols={2}>
  <Card title="Connection Management" icon="plug">
    **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
  </Card>

  <Card title="Tool Execution" icon="wrench">
    **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
  </Card>

  <Card title="Elicitation Handling" icon="message-circle">
    **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
  </Card>

  <Card title="Performance" icon="gauge">
    **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
  </Card>
</CardGroup>

## Troubleshooting

<AccordionGroup>
  <Accordion title="&#x22;MCP server is already connected&#x22;">
    **Cause**: Attempting to connect with a server ID that's already in use.

    **Solution**: Disconnect first or use a different ID.
  </Accordion>

  <Accordion title="&#x22;MCP server is not connected&#x22;">
    **Cause**: Attempting operations on a disconnected server.

    **Solution**: Check `getConnectionStatus()` and connect if needed.
  </Accordion>

  <Accordion title="&#x22;Method not found&#x22; or &#x22;Method not implemented&#x22;">
    **Cause**: Server doesn't support the requested capability.

    **Solution**: The manager returns empty results for unsupported methods (tools/list, resources/list, prompts/list).
  </Accordion>

  <Accordion title="Transport connection failures">
    **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
  </Accordion>
</AccordionGroup>

## Related Documentation

* [MCP Specification](https://spec.modelcontextprotocol.io/)
* [MCPJam Inspector](https://github.com/MCPJam/inspector)
* [Vercel AI SDK](https://sdk.vercel.ai/)
* [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/sdk)

## 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

```
```
