Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/swift-pens-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@langchain/mcp-adapters": minor
---

Added optional param handleConnectionErrorsGracefully to MultiServerMCPClient to skip connecting to MCPs that don't initially connect
38 changes: 38 additions & 0 deletions libs/langchain-mcp-adapters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
75 changes: 75 additions & 0 deletions libs/langchain-mcp-adapters/__tests__/client.basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
});
});
});
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 */
Expand Down Expand Up @@ -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();
Expand All @@ -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,
Expand Down
97 changes: 77 additions & 20 deletions libs/langchain-mcp-adapters/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ export class MultiServerMCPClient {

private _config: ResolvedClientConfig;

private _handleConnectionErrorsGracefully: boolean;

/**
* Returns clone of server config for inspection purposes.
*
Expand All @@ -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.
*
Expand Down Expand Up @@ -180,22 +189,29 @@ export class MultiServerMCPClient {
parsedServerConfig.prefixToolNameWithServerName,
additionalToolNamePrefix: parsedServerConfig.additionalToolNamePrefix,
useStandardContentBlocks: parsedServerConfig.useStandardContentBlocks,
handleConnectionErrorsGracefully:
parsedServerConfig.handleConnectionErrorsGracefully,
...(Object.keys(outputHandling).length > 0 ? { outputHandling } : {}),
...(defaultToolTimeout ? { defaultToolTimeout } : {}),
};
}

this._config = parsedServerConfig;
this._connections = parsedServerConfig.mcpServers;
this._handleConnectionErrorsGracefully =
parsedServerConfig.handleConnectionErrorsGracefully;
}

/**
* Proactively initialize connections to all servers. This will be called automatically when
* 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<string, DynamicStructuredTool[]>
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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<void> {
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
*/
Expand Down
19 changes: 19 additions & 0 deletions libs/langchain-mcp-adapters/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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;
};

/**
Expand Down
Loading