diff --git a/.changeset/swift-pens-sniff.md b/.changeset/swift-pens-sniff.md new file mode 100644 index 000000000000..b746479a44c0 --- /dev/null +++ b/.changeset/swift-pens-sniff.md @@ -0,0 +1,5 @@ +--- +"@langchain/mcp-adapters": minor +--- + +Added optional param handleConnectionErrorsGracefully to MultiServerMCPClient to skip connecting to MCPs that don't initially connect diff --git a/libs/langchain-mcp-adapters/README.md b/libs/langchain-mcp-adapters/README.md index 9c72844af603..859770cda69c 100644 --- a/libs/langchain-mcp-adapters/README.md +++ b/libs/langchain-mcp-adapters/README.md @@ -58,6 +58,9 @@ const client = new MultiServerMCPClient({ // Use standardized content block format in tool outputs useStandardContentBlocks: true, + // Whether to skip connecting to MCP servers that fail to connect (optional, default: false) + handleConnectionErrorsGracefully: true, + // Server configuration mcpServers: { // adds a STDIO connection to a server named "math" @@ -232,6 +235,7 @@ When loading MCP tools either directly through `loadMcpTools` or via `MultiServe | `useStandardContentBlocks` | `boolean` | `false` | See [Tool Output Mapping](#tool-output-mapping); set true for new applications | | `outputHandling` | `"content"`, `"artifact"`, or `object` | `resource` -> `"artifact"`, all others -> `"content"` | See [Tool Output Mapping](#tool-output-mapping) | | `defaultToolTimeout` | `number` | `0` | Default timeout for all tools (overridable on a per-tool basis) | +| `handleConnectionErrorsGracefully` | `boolean` | `false` | Whether to skip servers that fail to connect instead of throwing an error | ## Tool Output Mapping @@ -531,6 +535,40 @@ The library provides different error types to help with debugging: - **ToolException**: For errors during tool execution - **ZodError**: For configuration validation errors (invalid connection settings, etc.) +### Graceful Connection Error Handling + +By default, the `MultiServerMCPClient` will throw an error if any server fails to connect. You can change this behavior by setting `handleConnectionErrorsGracefully: true` in the client configuration. When enabled: + +- Servers that fail to connect are skipped and logged as warnings +- The client continues to work with only the servers that successfully connected +- Failed servers are removed from the connection list and won't be retried +- If no servers successfully connect, a warning is logged but no error is thrown + +```ts +const client = new MultiServerMCPClient({ + mcpServers: { + "working-server": { + transport: "stdio", + command: "npx", + args: ["-y", "@modelcontextprotocol/server-math"], + }, + "broken-server": { + transport: "http", + url: "http://localhost:9999/mcp", // This server doesn't exist + }, + }, + handleConnectionErrorsGracefully: true, // Skip failed connections + useStandardContentBlocks: true, +}); + +// This won't throw even though "broken-server" fails to connect +const tools = await client.getTools(); // Only tools from "working-server" + +// You can check which servers are actually connected +const workingClient = await client.getClient("working-server"); // Returns client +const brokenClient = await client.getClient("broken-server"); // Returns undefined +``` + Example error handling: ```ts diff --git a/libs/langchain-mcp-adapters/__tests__/client.basic.test.ts b/libs/langchain-mcp-adapters/__tests__/client.basic.test.ts index d27b863c4320..500b6e6f95cc 100644 --- a/libs/langchain-mcp-adapters/__tests__/client.basic.test.ts +++ b/libs/langchain-mcp-adapters/__tests__/client.basic.test.ts @@ -547,5 +547,80 @@ describe("MultiServerMCPClient", () => { expect(closeMock).toHaveBeenCalledOnce(); }); + + test("should handle connection errors gracefully when handleConnectionErrorsGracefully is true", async () => { + // Mock one successful and one failing connection + let clientCallCount = 0; + (Client as Mock).mockImplementation(() => { + clientCallCount += 1; + if (clientCallCount === 1) { + // First server fails + return { + connect: vi + .fn() + .mockReturnValue(Promise.reject(new Error("Connection failed"))), + listTools: vi.fn().mockReturnValue(Promise.resolve({ tools: [] })), + }; + } else { + // Second server succeeds + return { + connect: vi.fn().mockReturnValue(Promise.resolve()), + listTools: vi.fn().mockReturnValue(Promise.resolve({ tools: [] })), + }; + } + }); + + const client = new MultiServerMCPClient({ + mcpServers: { + "failing-server": { + transport: "http", + url: "http://localhost:8000/mcp", + }, + "working-server": { + transport: "http", + url: "http://localhost:8001/mcp", + }, + }, + handleConnectionErrorsGracefully: true, + }); + + // Should not throw, even though one server fails + const tools = await client.initializeConnections(); + + // Should have tools from the working server only + expect(tools).toBeDefined(); + + // Working server should be accessible + const workingClient = await client.getClient("working-server"); + expect(workingClient).toBeDefined(); + + // Failing server should not be accessible + const failingClient = await client.getClient("failing-server"); + expect(failingClient).toBeUndefined(); + }); + + test("should throw on connection failure when handleConnectionErrorsGracefully is false", async () => { + (Client as Mock).mockImplementationOnce(() => ({ + connect: vi + .fn() + .mockReturnValue(Promise.reject(new Error("Connection failed"))), + listTools: vi.fn().mockReturnValue(Promise.resolve({ tools: [] })), + })); + + const client = new MultiServerMCPClient({ + mcpServers: { + "failing-server": { + transport: "http", + url: "http://localhost:8000/mcp", + }, + }, + handleConnectionErrorsGracefully: false, + }); + + // Should throw when handleConnectionErrorsGracefully is false (default behavior) + await expect(() => client.initializeConnections()).rejects.toThrow( + MCPClientError + ); + }); }); }); diff --git a/libs/langchain-mcp-adapters/examples/filesystem_langgraph_example.ts b/libs/langchain-mcp-adapters/examples/filesystem_langgraph_example.ts index 91a4ac6ba914..d58e9143396c 100644 --- a/libs/langchain-mcp-adapters/examples/filesystem_langgraph_example.ts +++ b/libs/langchain-mcp-adapters/examples/filesystem_langgraph_example.ts @@ -1,5 +1,5 @@ /** - * Filesystem MCP Server with LangGraph Example + * Filesystem MCP Server with LangGraph Example and Graceful Error Handling * * This example demonstrates how to use the Filesystem MCP server with LangGraph * to create a structured workflow for complex file operations. @@ -8,6 +8,10 @@ * 1. Clear separation of responsibilities (reasoning vs execution) * 2. Conditional routing based on file operation types * 3. Structured handling of complex multi-file operations + * + * It also demonstrates the handleConnectionErrorsGracefully feature by including an + * optional server that might not be available - the client will continue working with + * the servers that successfully connect. */ /* eslint-disable no-console */ @@ -58,11 +62,18 @@ export async function runExample(client?: MultiServerMCPClient) { "./examples/filesystem_test", // This directory needs to exist ], }, + // This server is not available - demonstrate graceful error handling + "optional-math-server": { + transport: "http", + url: "http://localhost:9999/mcp", // This server likely doesn't exist + }, }, useStandardContentBlocks: true, + // Handle connection errors gracefully - continue with available servers + handleConnectionErrorsGracefully: true, }); - console.log("Connected to server"); + console.log("Connected to servers"); // Get all tools (flattened array is the default now) const mcpTools = await client.getTools(); @@ -77,6 +88,18 @@ export async function runExample(client?: MultiServerMCPClient) { .join(", ")}` ); + // Check which servers are actually connected + console.log("\n=== Server Connection Status ==="); + const serverNames = ["filesystem", "optional-math-server"]; + for (const serverName of serverNames) { + const serverClient = await client.getClient(serverName); + if (serverClient) { + console.log(`✅ ${serverName}: Connected`); + } else { + console.log(`❌ ${serverName}: Failed to connect (skipped gracefully)`); + } + } + // Create an OpenAI model with tools attached const systemMessage = `You are an assistant that helps users with file operations. You have access to tools that can read and write files, create directories, diff --git a/libs/langchain-mcp-adapters/src/client.ts b/libs/langchain-mcp-adapters/src/client.ts index 50edb7a63b0f..bcc1523dca0c 100644 --- a/libs/langchain-mcp-adapters/src/client.ts +++ b/libs/langchain-mcp-adapters/src/client.ts @@ -130,6 +130,8 @@ export class MultiServerMCPClient { private _config: ResolvedClientConfig; + private _handleConnectionErrorsGracefully: boolean; + /** * Returns clone of server config for inspection purposes. * @@ -140,6 +142,13 @@ export class MultiServerMCPClient { return JSON.parse(JSON.stringify(this._config)); } + /** + * Returns whether connection errors should be handled gracefully. + */ + get handleConnectionErrorsGracefully(): boolean { + return this._handleConnectionErrorsGracefully; + } + /** * Create a new MultiServerMCPClient. * @@ -180,6 +189,8 @@ export class MultiServerMCPClient { parsedServerConfig.prefixToolNameWithServerName, additionalToolNamePrefix: parsedServerConfig.additionalToolNamePrefix, useStandardContentBlocks: parsedServerConfig.useStandardContentBlocks, + handleConnectionErrorsGracefully: + parsedServerConfig.handleConnectionErrorsGracefully, ...(Object.keys(outputHandling).length > 0 ? { outputHandling } : {}), ...(defaultToolTimeout ? { defaultToolTimeout } : {}), }; @@ -187,6 +198,8 @@ export class MultiServerMCPClient { this._config = parsedServerConfig; this._connections = parsedServerConfig.mcpServers; + this._handleConnectionErrorsGracefully = + parsedServerConfig.handleConnectionErrorsGracefully; } /** @@ -194,8 +207,11 @@ export class MultiServerMCPClient { * methods requiring an active connection (like {@link getTools} or {@link getClient}) are called, * but you can call it directly to ensure all connections are established before using the tools. * - * @returns A map of server names to arrays of tools - * @throws {MCPClientError} If initialization fails + * When handleConnectionErrorsGracefully is true, servers that fail to connect are skipped + * and removed from the connection list. Otherwise, any connection failure will throw an error. + * + * @returns A map of server names to arrays of tools (only includes successfully connected servers) + * @throws {MCPClientError} If initialization fails and handleConnectionErrorsGracefully is false */ async initializeConnections(): Promise< Record @@ -210,28 +226,45 @@ export class MultiServerMCPClient { ) ); - for (const [serverName, connection] of connectionsToInit) { - getDebugLog()( - `INFO: Initializing connection to server "${serverName}"...` - ); + if (this._handleConnectionErrorsGracefully) { + const successfulConnections: [string, ResolvedConnection][] = []; - if (isResolvedStdioConnection(connection)) { - await this._initializeStdioConnection(serverName, connection); - } else if (isResolvedStreamableHTTPConnection(connection)) { - if (connection.type === "sse" || connection.transport === "sse") { - await this._initializeSSEConnection(serverName, connection); - } else { - await this._initializeStreamableHTTPConnection( - serverName, - connection + for (const [serverName, connection] of connectionsToInit) { + try { + getDebugLog()( + `INFO: Testing connection to server "${serverName}"...` ); + + await this._testAndInitializeConnection(serverName, connection); + successfulConnections.push([serverName, connection]); + + getDebugLog()( + `INFO: Successfully connected to server "${serverName}"` + ); + } catch (error) { + getDebugLog()( + `WARN: Failed to connect to server "${serverName}": ${error}. Skipping this server.` + ); + if (this._connections) { + delete this._connections[serverName]; + } } - } else { - // This should never happen due to the validation in the constructor - throw new MCPClientError( - `Unsupported transport type for server "${serverName}"`, - serverName + } + + // If no connections succeeded and we had connections to try, log a warning + if (successfulConnections.length === 0 && connectionsToInit.length > 0) { + getDebugLog()( + `WARN: Failed to connect to any of the ${connectionsToInit.length} configured servers` + ); + } + } else { + // Original behavior: throw on any connection failure + for (const [serverName, connection] of connectionsToInit) { + getDebugLog()( + `INFO: Initializing connection to server "${serverName}"...` ); + + await this._testAndInitializeConnection(serverName, connection); } } @@ -286,6 +319,30 @@ export class MultiServerMCPClient { getDebugLog()(`INFO: All MCP connections closed`); } + /** + * Test and initialize a connection based on its type + */ + private async _testAndInitializeConnection( + serverName: string, + connection: ResolvedConnection + ): Promise { + if (isResolvedStdioConnection(connection)) { + await this._initializeStdioConnection(serverName, connection); + } else if (isResolvedStreamableHTTPConnection(connection)) { + if (connection.type === "sse" || connection.transport === "sse") { + await this._initializeSSEConnection(serverName, connection); + } else { + await this._initializeStreamableHTTPConnection(serverName, connection); + } + } else { + // This should never happen due to the validation in the constructor + throw new MCPClientError( + `Unsupported transport type for server "${serverName}"`, + serverName + ); + } + } + /** * Initialize a stdio connection */ diff --git a/libs/langchain-mcp-adapters/src/types.ts b/libs/langchain-mcp-adapters/src/types.ts index c98ddf7af553..7893ef4c7141 100644 --- a/libs/langchain-mcp-adapters/src/types.ts +++ b/libs/langchain-mcp-adapters/src/types.ts @@ -480,6 +480,18 @@ export const clientConfigSchema = z ) .optional() .default(false), + /** + * Whether to handle connection errors gracefully without throwing exceptions + * + * @default false + */ + handleConnectionErrorsGracefully: z + .boolean() + .describe( + "Whether to handle connection errors gracefully without throwing exceptions" + ) + .optional() + .default(false), }) .and(baseConfigSchema) .describe("Configuration for the MCP client"); @@ -587,6 +599,13 @@ export type LoadMcpToolsOptions = { * If not specified, tools will use their own configured timeout values. */ defaultToolTimeout?: number; + + /** + * Whether to handle connection errors gracefully without throwing exceptions + * + * @default false + */ + handleConnectionErrorsGracefully?: boolean; }; /**