mirror of
https://github.com/langchain-ai/langchainjs-mcp-adapters.git
synced 2026-07-01 12:27:48 -04:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3f16633d1 | |||
| 01b70d18c9 | |||
| 72818340c1 | |||
| 373b08e090 | |||
| 831359db3a | |||
| 68f130e1d0 | |||
| 03172e3fcd | |||
| 8cfec54c29 | |||
| c1b0f3d8ad |
@@ -5,3 +5,5 @@ index.d.cts
|
||||
node_modules
|
||||
dist
|
||||
.yarn
|
||||
.env
|
||||
.eslintcache
|
||||
|
||||
@@ -1,421 +1,11 @@
|
||||
# LangChain.js MCP Adapters
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **This package has been migrated into [the LangChainJS monorepo](https://github.com/langchain-ai/langchainjs/tree/main/libs/langchain-mcp-adapters).**
|
||||
|
||||
[](https://www.npmjs.com/package/@langchain/mcp-adapters)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
This library provides a lightweight wrapper that makes [Anthropic Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) tools compatible with [LangChain.js](https://github.com/langchain-ai/langchainjs) and [LangGraph.js](https://github.com/langchain-ai/langgraphjs).
|
||||
This library provides a lightweight wrapper to allow [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) services to be used with [LangChain.js](https://github.com/langchain-ai/langchainjs).
|
||||
|
||||
## Features
|
||||
|
||||
- 🔌 **Transport Options**
|
||||
|
||||
- Connect to MCP servers via stdio (local) or SSE (remote)
|
||||
- Support for custom headers in SSE connections for authentication
|
||||
- Configurable reconnection strategies for both transport types
|
||||
|
||||
- 🔄 **Multi-Server Management**
|
||||
|
||||
- Connect to multiple MCP servers simultaneously
|
||||
- Auto-organize tools by server or access them as a flattened collection
|
||||
- Convenient configuration via JSON file
|
||||
|
||||
- 🧩 **Agent Integration**
|
||||
|
||||
- Compatible with LangChain.js and LangGraph.js
|
||||
- Optimized for OpenAI, Anthropic, and Google models
|
||||
- Supports rich content responses including text, images, and embedded resources
|
||||
|
||||
- 🛠️ **Development Features**
|
||||
- Uses `debug` package for debug logging
|
||||
- Flexible configuration options
|
||||
- Robust error handling
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @langchain/mcp-adapters
|
||||
```
|
||||
|
||||
### Optional Dependencies
|
||||
|
||||
For SSE connections with custom headers in Node.js:
|
||||
|
||||
```bash
|
||||
npm install eventsource
|
||||
```
|
||||
|
||||
For enhanced SSE header support:
|
||||
|
||||
```bash
|
||||
npm install extended-eventsource
|
||||
```
|
||||
|
||||
# Example: Manage the MCP Client yourself
|
||||
|
||||
This example shows how you can manage your own MCP client and use it to get tools that you can pass to a LangGraph prebuilt ReAcT agent.
|
||||
|
||||
```bash
|
||||
npm install @langchain/mcp-adapters @langchain/langgraph @langchain/core @langchain/openai
|
||||
|
||||
export OPENAI_API_KEY=<your_api_key>
|
||||
```
|
||||
|
||||
## Client
|
||||
|
||||
```ts
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { loadMcpTools } from "@langchain/mcp-adapters";
|
||||
|
||||
// Initialize the ChatOpenAI model
|
||||
const model = new ChatOpenAI({ modelName: "gpt-4" });
|
||||
|
||||
// Automatically starts and connects to a MCP reference server
|
||||
const transport = new StdioClientTransport({
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-math"],
|
||||
});
|
||||
|
||||
// Initialize the client
|
||||
const client = new Client({
|
||||
name: "math-client",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
try {
|
||||
// Connect to the transport
|
||||
await client.connect(transport);
|
||||
|
||||
// Get tools with custom configuration
|
||||
const tools = await loadMcpTools("math", client, {
|
||||
// Whether to throw errors if a tool fails to load (optional, default: true)
|
||||
throwOnLoadError: true,
|
||||
// Whether to prefix tool names with the server name (optional, default: false)
|
||||
prefixToolNameWithServerName: false,
|
||||
// Optional additional prefix for tool names (optional, default: "")
|
||||
additionalToolNamePrefix: "",
|
||||
});
|
||||
|
||||
// Create and run the agent
|
||||
const agent = createReactAgent({ llm: model, tools });
|
||||
const agentResponse = await agent.invoke({
|
||||
messages: [{ role: "user", content: "what's (3 + 5) x 12?" }],
|
||||
});
|
||||
console.log(agentResponse);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
// Clean up connection
|
||||
await client.close();
|
||||
}
|
||||
```
|
||||
|
||||
# Example: Connect to one or more servers via config
|
||||
|
||||
The library also allows you to connect to multiple MCP servers and load tools from them:
|
||||
|
||||
## Client
|
||||
|
||||
```ts
|
||||
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
// Create client and connect to server
|
||||
const client = new MultiServerMCPClient({
|
||||
// Global tool configuration options
|
||||
// Whether to throw on errors if a tool fails to load (optional, default: true)
|
||||
throwOnLoadError: true,
|
||||
// Whether to prefix tool names with the server name (optional, default: true)
|
||||
prefixToolNameWithServerName: true,
|
||||
// Optional additional prefix for tool names (optional, default: "mcp")
|
||||
additionalToolNamePrefix: "mcp",
|
||||
|
||||
// Server configuration
|
||||
mcpServers: {
|
||||
// adds a STDIO connection to a server named "math"
|
||||
math: {
|
||||
transport: "stdio",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-math"],
|
||||
// Restart configuration for stdio transport
|
||||
restart: {
|
||||
enabled: true,
|
||||
maxAttempts: 3,
|
||||
delayMs: 1000,
|
||||
},
|
||||
},
|
||||
|
||||
// here's a filesystem server
|
||||
filesystem: {
|
||||
transport: "stdio",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-filesystem"],
|
||||
},
|
||||
|
||||
// SSE transport example with reconnection configuration
|
||||
weather: {
|
||||
transport: "sse",
|
||||
url: "https://example.com/mcp-weather",
|
||||
headers: {
|
||||
Authorization: "Bearer token123",
|
||||
},
|
||||
useNodeEventSource: true,
|
||||
reconnect: {
|
||||
enabled: true,
|
||||
maxAttempts: 5,
|
||||
delayMs: 2000,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const tools = await client.getTools();
|
||||
|
||||
// Create an OpenAI model
|
||||
const model = new ChatOpenAI({
|
||||
modelName: "gpt-4o",
|
||||
temperature: 0,
|
||||
});
|
||||
|
||||
// Create the React agent
|
||||
const agent = createReactAgent({
|
||||
llm: model,
|
||||
tools,
|
||||
});
|
||||
|
||||
// Run the agent
|
||||
try {
|
||||
const mathResponse = await agent.invoke({
|
||||
messages: [{ role: "user", content: "what's (3 + 5) x 12?" }],
|
||||
});
|
||||
console.log(mathResponse);
|
||||
} catch (error) {
|
||||
console.error("Error during agent execution:", error);
|
||||
// Tools throw ToolException for tool-specific errors
|
||||
if (error.name === "ToolException") {
|
||||
console.error("Tool execution failed:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
await client.close();
|
||||
```
|
||||
|
||||
For more detailed examples, see the [examples](./examples) directory.
|
||||
|
||||
## Tool Configuration Options
|
||||
|
||||
When loading MCP tools either directly through `loadMcpTools` or via `MultiServerMCPClient`, you can configure the following options:
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ------------------------------ | ------- | ------- | ------------------------------------------------------------------------------------ |
|
||||
| `throwOnLoadError` | boolean | `true` | Whether to throw an error if a tool fails to load |
|
||||
| `prefixToolNameWithServerName` | boolean | `false` | If true, prefixes all tool names with the server name (e.g., `serverName__toolName`) |
|
||||
| `additionalToolNamePrefix` | string | `""` | Additional prefix to add to tool names (e.g., `prefix__serverName__toolName`) |
|
||||
|
||||
## Response Handling
|
||||
|
||||
MCP tools return results in the `content_and_artifact` format which can include:
|
||||
|
||||
- **Text content**: Plain text responses
|
||||
- **Image content**: Base64-encoded images with MIME type
|
||||
- **Embedded resources**: Files, structured data, or other resources
|
||||
|
||||
Example for handling different content types:
|
||||
|
||||
```ts
|
||||
const tool = tools.find((t) => t.name === "mcp__math__calculate");
|
||||
const result = await tool.invoke({ expression: "(3 + 5) * 12" });
|
||||
|
||||
// Result format: [content, artifacts]
|
||||
// - content: string | MessageContentComplex[]
|
||||
// - artifacts: EmbeddedResource[]
|
||||
|
||||
const [textContent, artifacts] = result;
|
||||
|
||||
// Handle text content
|
||||
if (typeof textContent === "string") {
|
||||
console.log("Result:", textContent);
|
||||
} else {
|
||||
// Handle complex content (text + images)
|
||||
textContent.forEach((item) => {
|
||||
if (item.type === "text") {
|
||||
console.log("Text:", item.text);
|
||||
} else if (item.type === "image_url") {
|
||||
console.log("Image URL:", item.image_url.url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle artifacts if needed
|
||||
if (artifacts.length > 0) {
|
||||
console.log("Received artifacts:", artifacts);
|
||||
}
|
||||
```
|
||||
|
||||
## Reconnection Strategies
|
||||
|
||||
Both transport types support automatic reconnection:
|
||||
|
||||
### Stdio Transport Restart
|
||||
|
||||
```ts
|
||||
{
|
||||
transport: "stdio",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-math"],
|
||||
restart: {
|
||||
enabled: true, // Enable automatic restart
|
||||
maxAttempts: 3, // Maximum restart attempts
|
||||
delayMs: 1000 // Delay between attempts in ms
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SSE Transport Reconnect
|
||||
|
||||
```ts
|
||||
{
|
||||
transport: "sse",
|
||||
url: "https://example.com/mcp-server",
|
||||
headers: { "Authorization": "Bearer token123" },
|
||||
useNodeEventSource: true,
|
||||
reconnect: {
|
||||
enabled: true, // Enable automatic reconnection
|
||||
maxAttempts: 5, // Maximum reconnection attempts
|
||||
delayMs: 2000 // Delay between attempts in ms
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The library provides different error types to help with debugging:
|
||||
|
||||
- **MCPClientError**: For client connection and initialization issues
|
||||
- **ToolException**: For errors during tool execution
|
||||
- **ZodError**: For configuration validation errors (invalid connection settings, etc.)
|
||||
|
||||
Example error handling:
|
||||
|
||||
```ts
|
||||
try {
|
||||
const client = new MultiServerMCPClient({
|
||||
math: {
|
||||
transport: "stdio",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-math"],
|
||||
},
|
||||
});
|
||||
|
||||
const tools = await client.getTools();
|
||||
const result = await tools[0].invoke({ expression: "1 + 2" });
|
||||
} catch (error) {
|
||||
if (error.name === "MCPClientError") {
|
||||
// Handle connection issues
|
||||
console.error(`Connection error (${error.serverName}):`, error.message);
|
||||
} else if (error.name === "ToolException") {
|
||||
// Handle tool execution errors
|
||||
console.error("Tool execution failed:", error.message);
|
||||
} else if (error.name === "ZodError") {
|
||||
// Handle configuration validation errors
|
||||
console.error("Configuration error:", error.issues);
|
||||
// Zod errors contain detailed information about what went wrong
|
||||
error.issues.forEach((issue) => {
|
||||
console.error(`- Path: ${issue.path.join(".")}, Error: ${issue.message}`);
|
||||
});
|
||||
} else {
|
||||
// Handle other errors
|
||||
console.error("Unexpected error:", error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Common Zod Validation Errors
|
||||
|
||||
The library uses Zod for validating configuration. Here are some common validation errors:
|
||||
|
||||
- **Missing required parameters**: For example, omitting `command` for stdio transport or `url` for SSE transport
|
||||
- **Invalid parameter types**: For example, providing a number where a string is expected
|
||||
- **Invalid connection configuration**: For example, using an invalid URL format for SSE transport
|
||||
|
||||
Example Zod error for an invalid SSE URL:
|
||||
|
||||
```json
|
||||
{
|
||||
"issues": [
|
||||
{
|
||||
"code": "invalid_string",
|
||||
"validation": "url",
|
||||
"path": ["mcpServers", "weather", "url"],
|
||||
"message": "Invalid url"
|
||||
}
|
||||
],
|
||||
"name": "ZodError"
|
||||
}
|
||||
```
|
||||
|
||||
## Browser Environments
|
||||
|
||||
When using in browsers:
|
||||
|
||||
- Native EventSource API doesn't support custom headers
|
||||
- Consider using a proxy or pass authentication via query parameters
|
||||
- May require CORS configuration on the server side
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Connection Failures**:
|
||||
|
||||
- Verify the MCP server is running
|
||||
- Check command paths and network connectivity
|
||||
|
||||
2. **Tool Execution Errors**:
|
||||
|
||||
- Examine server logs for error messages
|
||||
- Ensure input parameters match the expected schema
|
||||
|
||||
3. **Headers Not Applied**:
|
||||
- Install the recommended `extended-eventsource` package
|
||||
- Set `useNodeEventSource: true` in SSE connections
|
||||
|
||||
### Debug Logging
|
||||
|
||||
This package makes use of the [debug](https://www.npmjs.com/package/debug) package for debug logging.
|
||||
|
||||
Logging is disabled by default, and can be enabled by setting the `DEBUG` environment variable as per
|
||||
the instructions in the debug package.
|
||||
|
||||
To output all debug logs from this package:
|
||||
|
||||
```bash
|
||||
DEBUG='@langchain/mcp-adapters:*'
|
||||
```
|
||||
|
||||
To output debug logs only from the `client` module:
|
||||
|
||||
```bash
|
||||
DEBUG='@langchain/mcp-adapters:client'
|
||||
```
|
||||
|
||||
To output debug logs only from the `tools` module:
|
||||
|
||||
```bash
|
||||
DEBUG='@langchain/mcp-adapters:tools'
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Big thanks to [@vrknetha](https://github.com/vrknetha), [@cawstudios](https://caw.tech) for the initial implementation!
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please check out our [contributing guidelines](CONTRIBUTING.md) for more information.
|
||||
This project has moved. For a current description of this project, please see the [up-to-date README](https://github.com/langchain-ai/langchainjs/tree/main/libs/langchain-mcp-adapters#readme) at the project's new location.
|
||||
|
||||
@@ -21,6 +21,9 @@ const { StdioClientTransport } = await import(
|
||||
const { SSEClientTransport } = await import(
|
||||
"@modelcontextprotocol/sdk/client/sse.js"
|
||||
);
|
||||
const { StreamableHTTPClientTransport } = await import(
|
||||
"@modelcontextprotocol/sdk/client/streamableHttp.js"
|
||||
);
|
||||
|
||||
describe("MultiServerMCPClient", () => {
|
||||
// Setup and teardown
|
||||
@@ -63,6 +66,17 @@ describe("MultiServerMCPClient", () => {
|
||||
// Additional assertions to verify the connection was processed correctly
|
||||
});
|
||||
|
||||
test("should process valid streamable HTTP connection config", () => {
|
||||
const client = new MultiServerMCPClient({
|
||||
"test-server": {
|
||||
transport: "http",
|
||||
url: "http://localhost:8000/mcp",
|
||||
},
|
||||
});
|
||||
expect(client).toBeDefined();
|
||||
// Additional assertions to verify the connection was processed correctly
|
||||
});
|
||||
|
||||
test("should have a compile time error and a runtime error when the config is invalid", () => {
|
||||
expect(() => {
|
||||
// eslint-disable-next-line no-new
|
||||
@@ -93,6 +107,7 @@ describe("MultiServerMCPClient", () => {
|
||||
command: "python",
|
||||
args: ["./script.py"],
|
||||
env: undefined,
|
||||
stderr: "inherit",
|
||||
});
|
||||
|
||||
expect(Client).toHaveBeenCalled();
|
||||
@@ -116,6 +131,24 @@ describe("MultiServerMCPClient", () => {
|
||||
expect(Client.prototype.listTools).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should initialize streamable HTTP connections correctly", async () => {
|
||||
const client = new MultiServerMCPClient({
|
||||
"test-server": {
|
||||
transport: "http",
|
||||
url: "http://localhost:8000/mcp",
|
||||
},
|
||||
});
|
||||
|
||||
await client.initializeConnections();
|
||||
|
||||
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(
|
||||
new URL("http://localhost:8000/mcp")
|
||||
);
|
||||
expect(Client).toHaveBeenCalled();
|
||||
expect(Client.prototype.connect).toHaveBeenCalled();
|
||||
expect(Client.prototype.listTools).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should throw on connection failure", async () => {
|
||||
(Client as Mock).mockImplementationOnce(() => ({
|
||||
connect: vi
|
||||
@@ -307,6 +340,10 @@ describe("MultiServerMCPClient", () => {
|
||||
transport: "sse",
|
||||
url: "http://localhost:8000/sse",
|
||||
},
|
||||
server3: {
|
||||
transport: "http",
|
||||
url: "http://localhost:8000/mcp",
|
||||
},
|
||||
});
|
||||
|
||||
await client.initializeConnections();
|
||||
@@ -315,6 +352,7 @@ describe("MultiServerMCPClient", () => {
|
||||
// Verify that all transports were closed using the mock functions directly
|
||||
expect(StdioClientTransport.prototype.close).toHaveBeenCalled();
|
||||
expect(SSEClientTransport.prototype.close).toHaveBeenCalled();
|
||||
expect(StreamableHTTPClientTransport.prototype.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should handle errors during cleanup gracefully", async () => {
|
||||
@@ -341,4 +379,105 @@ describe("MultiServerMCPClient", () => {
|
||||
expect(closeMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
// Streamable HTTP specific tests
|
||||
describe("streamable HTTP transport", () => {
|
||||
test("should throw when streamable HTTP config is missing required fields", () => {
|
||||
expect(() => {
|
||||
// eslint-disable-next-line no-new
|
||||
new MultiServerMCPClient({
|
||||
// @ts-expect-error missing url field
|
||||
"test-server": {
|
||||
transport: "http",
|
||||
// Missing url field
|
||||
},
|
||||
});
|
||||
}).toThrow(ZodError);
|
||||
});
|
||||
|
||||
test("should throw when streamable HTTP URL is invalid", () => {
|
||||
expect(() => {
|
||||
// eslint-disable-next-line no-new
|
||||
new MultiServerMCPClient({
|
||||
"test-server": {
|
||||
transport: "http",
|
||||
url: "invalid-url", // Invalid URL format
|
||||
},
|
||||
});
|
||||
}).toThrow(ZodError);
|
||||
});
|
||||
|
||||
test("should handle mixed transport types including streamable HTTP", async () => {
|
||||
const client = new MultiServerMCPClient({
|
||||
"stdio-server": {
|
||||
transport: "stdio",
|
||||
command: "python",
|
||||
args: ["./script.py"],
|
||||
},
|
||||
"sse-server": {
|
||||
transport: "sse",
|
||||
url: "http://localhost:8000/sse",
|
||||
},
|
||||
"streamable-server": {
|
||||
transport: "http",
|
||||
url: "http://localhost:8000/mcp",
|
||||
},
|
||||
});
|
||||
|
||||
await client.initializeConnections();
|
||||
|
||||
// Verify all transports were initialized
|
||||
expect(StreamableHTTPClientTransport).toHaveBeenCalled();
|
||||
expect(SSEClientTransport).toHaveBeenCalled();
|
||||
expect(StdioClientTransport).toHaveBeenCalled();
|
||||
|
||||
// Get tools from all servers
|
||||
const tools = await client.getTools();
|
||||
expect(tools.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("should throw on streamable HTTP connection failure", 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({
|
||||
"test-server": {
|
||||
transport: "http",
|
||||
url: "http://localhost:8000/mcp",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(() => client.initializeConnections()).rejects.toThrow(
|
||||
MCPClientError
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle errors during streamable HTTP cleanup gracefully", async () => {
|
||||
const closeMock = vi
|
||||
.fn()
|
||||
.mockReturnValue(Promise.reject(new Error("Close failed")));
|
||||
|
||||
// Mock close to throw an error
|
||||
(StreamableHTTPClientTransport as Mock).mockImplementationOnce(() => ({
|
||||
close: closeMock,
|
||||
connect: vi.fn().mockReturnValue(Promise.resolve()),
|
||||
}));
|
||||
|
||||
const client = new MultiServerMCPClient({
|
||||
"test-server": {
|
||||
transport: "http",
|
||||
url: "http://localhost:8000/mcp",
|
||||
},
|
||||
});
|
||||
|
||||
await client.initializeConnections();
|
||||
await client.close();
|
||||
|
||||
expect(closeMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { vi, describe, test, expect, beforeEach, type Mock } from "vitest";
|
||||
import { ZodError } from "zod";
|
||||
import { Connection } from "../src/client.js";
|
||||
import type {
|
||||
ClientConfig,
|
||||
Connection,
|
||||
StdioConnection,
|
||||
} from "../src/client.js";
|
||||
|
||||
import "./mocks.js";
|
||||
|
||||
@@ -11,6 +15,9 @@ const { StdioClientTransport } = await import(
|
||||
const { SSEClientTransport } = await import(
|
||||
"@modelcontextprotocol/sdk/client/sse.js"
|
||||
);
|
||||
const { StreamableHTTPClientTransport } = await import(
|
||||
"@modelcontextprotocol/sdk/client/streamableHttp.js"
|
||||
);
|
||||
const { MultiServerMCPClient, MCPClientError } = await import(
|
||||
"../src/client.js"
|
||||
);
|
||||
@@ -44,6 +51,23 @@ describe("MultiServerMCPClient", () => {
|
||||
expect(Client).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should process valid streamable HTTP connection config", async () => {
|
||||
const config = {
|
||||
"test-server": {
|
||||
transport: "http" as const,
|
||||
url: "http://localhost:8000/mcp",
|
||||
},
|
||||
};
|
||||
|
||||
const client = new MultiServerMCPClient(config);
|
||||
expect(client).toBeDefined();
|
||||
|
||||
// Initialize connections and verify
|
||||
await client.initializeConnections();
|
||||
expect(StreamableHTTPClientTransport).toHaveBeenCalled();
|
||||
expect(Client).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should process valid SSE connection config", async () => {
|
||||
const config = {
|
||||
"test-server": {
|
||||
@@ -59,7 +83,15 @@ describe("MultiServerMCPClient", () => {
|
||||
|
||||
// Initialize connections and verify
|
||||
await client.initializeConnections();
|
||||
expect(SSEClientTransport).toHaveBeenCalled();
|
||||
expect(SSEClientTransport).toHaveBeenCalledWith(
|
||||
new URL(config["test-server"].url),
|
||||
{
|
||||
eventSourceInit: {},
|
||||
requestInit: {
|
||||
headers: config["test-server"].headers,
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(Client).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -316,6 +348,10 @@ describe("MultiServerMCPClient", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const conf = client.config;
|
||||
expect(conf.additionalToolNamePrefix).toBe("mcp");
|
||||
expect(conf.prefixToolNameWithServerName).toBe(true);
|
||||
|
||||
await client.initializeConnections();
|
||||
const tools = await client.getTools();
|
||||
|
||||
@@ -514,5 +550,30 @@ describe("MultiServerMCPClient", () => {
|
||||
// Should not have created a client
|
||||
expect(Client).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should throw on streamable HTTP transport creation errors", async () => {
|
||||
// Force an error when creating transport
|
||||
(StreamableHTTPClientTransport as Mock).mockImplementationOnce(() => {
|
||||
throw new Error("Streamable HTTP transport creation failed");
|
||||
});
|
||||
|
||||
const client = new MultiServerMCPClient({
|
||||
"test-server": {
|
||||
transport: "http" as const,
|
||||
url: "http://localhost:8000/mcp",
|
||||
},
|
||||
});
|
||||
|
||||
// Should throw error when connecting
|
||||
await expect(
|
||||
async () => await client.initializeConnections()
|
||||
).rejects.toThrow();
|
||||
|
||||
// Should have attempted to create transport
|
||||
expect(StreamableHTTPClientTransport).toHaveBeenCalled();
|
||||
|
||||
// Should not have created a client
|
||||
expect(Client).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,3 +70,21 @@ vi.mock("@modelcontextprotocol/sdk/client/sse.js", () => {
|
||||
SSEClientTransport,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => {
|
||||
const streamableHTTPClientTransportPrototype = {
|
||||
connect: vi.fn().mockReturnValue(Promise.resolve()),
|
||||
send: vi.fn().mockReturnValue(Promise.resolve()),
|
||||
close: vi.fn().mockReturnValue(Promise.resolve()),
|
||||
};
|
||||
const StreamableHTTPClientTransport = vi.fn().mockImplementation((config) => {
|
||||
return {
|
||||
...streamableHTTPClientTransportPrototype,
|
||||
config,
|
||||
};
|
||||
});
|
||||
StreamableHTTPClientTransport.prototype = streamableHTTPClientTransportPrototype;
|
||||
return {
|
||||
StreamableHTTPClientTransport,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import express from "express";
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export async function main() {
|
||||
const server = new McpServer({
|
||||
name: "backwards-compatible-server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
const calcSchema = { a: z.number(), b: z.number() };
|
||||
|
||||
server.tool(
|
||||
"add",
|
||||
"Adds two numbers together",
|
||||
calcSchema,
|
||||
async ({ a, b }: { a: number; b: number }, extra) => {
|
||||
return {
|
||||
content: [{ type: "text", text: `${a + b}` }],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"subtract",
|
||||
"Subtracts two numbers",
|
||||
calcSchema,
|
||||
async ({ a, b }: { a: number; b: number }, extra) => {
|
||||
return { content: [{ type: "text", text: `${a - b}` }] };
|
||||
}
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"multiply",
|
||||
"Multiplies two numbers",
|
||||
calcSchema,
|
||||
async ({ a, b }: { a: number; b: number }, extra) => {
|
||||
return { content: [{ type: "text", text: `${a * b}` }] };
|
||||
}
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"divide",
|
||||
"Divides two numbers",
|
||||
calcSchema,
|
||||
async ({ a, b }: { a: number; b: number }, extra) => {
|
||||
return { content: [{ type: "text", text: `${a / b}` }] };
|
||||
}
|
||||
);
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Store transports for each session type
|
||||
const transports = {
|
||||
streamable: {} as Record<string, StreamableHTTPServerTransport>,
|
||||
sse: {} as Record<string, SSEServerTransport>,
|
||||
};
|
||||
|
||||
// Modern Streamable HTTP endpoint
|
||||
app.post("/mcp", async (req, res) => {
|
||||
// Check for existing session ID
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
|
||||
if (sessionId && transports.streamable[sessionId]) {
|
||||
// Reuse existing transport
|
||||
transport = transports.streamable[sessionId];
|
||||
} else if (!sessionId && isInitializeRequest(req.body)) {
|
||||
// New initialization request
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
onsessioninitialized: (sessionId) => {
|
||||
// Store the transport by session ID
|
||||
transports.streamable[sessionId] = transport;
|
||||
},
|
||||
});
|
||||
|
||||
// Clean up transport when closed
|
||||
transport.onclose = () => {
|
||||
if (transport.sessionId) {
|
||||
delete transports.streamable[transport.sessionId];
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to the MCP server
|
||||
await server.connect(transport);
|
||||
} else {
|
||||
// Invalid request
|
||||
console.error(
|
||||
"Invalid Streamable HTTP request: ",
|
||||
JSON.stringify(req.body, null, 2)
|
||||
);
|
||||
res.status(400).json({
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32000,
|
||||
message: "Bad Request: No valid session ID provided",
|
||||
},
|
||||
id: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the request
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
});
|
||||
|
||||
// Reusable handler for GET and DELETE requests
|
||||
const handleSessionRequest = async (
|
||||
req: express.Request,
|
||||
res: express.Response
|
||||
) => {
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
if (!sessionId || !transports.streamable[sessionId]) {
|
||||
console.error(
|
||||
"Invalid Streamable HTTP request (invalid/missing session ID): ",
|
||||
JSON.stringify(req.body, null, 2)
|
||||
);
|
||||
res.status(400).send("Invalid or missing session ID");
|
||||
return;
|
||||
}
|
||||
|
||||
const transport = transports.streamable[sessionId];
|
||||
await transport.handleRequest(req, res);
|
||||
};
|
||||
|
||||
app.get("/mcp", handleSessionRequest);
|
||||
app.delete("/mcp", handleSessionRequest);
|
||||
|
||||
// Legacy SSE endpoint for older clients
|
||||
app.get("/sse", async (req, res) => {
|
||||
// Create SSE transport for legacy clients
|
||||
const transport = new SSEServerTransport("/messages", res);
|
||||
transports.sse[transport.sessionId] = transport;
|
||||
|
||||
res.on("close", () => {
|
||||
delete transports.sse[transport.sessionId];
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
});
|
||||
|
||||
// Legacy message endpoint for older clients
|
||||
app.post("/messages", async (req, res) => {
|
||||
const sessionId = req.query.sessionId as string;
|
||||
const transport = transports.sse[sessionId];
|
||||
if (transport) {
|
||||
await transport.handlePostMessage(req, res, req.body);
|
||||
} else {
|
||||
console.error("No transport found for sessionId", sessionId);
|
||||
res.status(400).send("No transport found for sessionId");
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
}
|
||||
|
||||
if (typeof require !== "undefined" && require.main === module) {
|
||||
main().catch(console.error);
|
||||
}
|
||||
|
||||
if (
|
||||
import.meta.url === process.argv[1] ||
|
||||
import.meta.url === `file://${process.argv[1]}`
|
||||
) {
|
||||
main().catch(console.error);
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Calculator MCP Server with LangGraph Example
|
||||
*
|
||||
* This example demonstrates how to use the Calculator MCP server with LangGraph
|
||||
* to create a structured workflow for simple calculations.
|
||||
*
|
||||
* The graph-based approach allows:
|
||||
* 1. Clear separation of responsibilities (reasoning vs execution)
|
||||
* 2. Conditional routing based on tool calls
|
||||
* 3. Structured handling of complex multi-tool operations
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import {
|
||||
StateGraph,
|
||||
END,
|
||||
START,
|
||||
MessagesAnnotation,
|
||||
} from "@langchain/langgraph";
|
||||
import { ToolNode } from "@langchain/langgraph/prebuilt";
|
||||
import {
|
||||
HumanMessage,
|
||||
AIMessage,
|
||||
SystemMessage,
|
||||
isHumanMessage,
|
||||
} from "@langchain/core/messages";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
import { main as calculatorServerMain } from "./calculator_server_shttp_sse.js";
|
||||
|
||||
// MCP client imports
|
||||
import { MultiServerMCPClient } from "../src/index.js";
|
||||
|
||||
// Load environment variables from .env file
|
||||
dotenv.config();
|
||||
|
||||
const transportType = process.env.MCP_TRANSPORT_TYPE === "sse" ? "sse" : "http";
|
||||
|
||||
export async function runExample(client?: MultiServerMCPClient) {
|
||||
try {
|
||||
console.log("Initializing MCP client...");
|
||||
|
||||
void calculatorServerMain();
|
||||
|
||||
// Wait for the server to start
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
// Create a client with configurations for the calculator server
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
client =
|
||||
client ??
|
||||
new MultiServerMCPClient({
|
||||
calculator: {
|
||||
url: `http://localhost:3000/${
|
||||
transportType === "sse" ? "sse" : "mcp"
|
||||
}`,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Connected to server");
|
||||
|
||||
// Get all tools (flattened array is the default now)
|
||||
const mcpTools = await client.getTools();
|
||||
|
||||
if (mcpTools.length === 0) {
|
||||
throw new Error("No tools found");
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Loaded ${mcpTools.length} MCP tools: ${mcpTools
|
||||
.map((tool) => tool.name)
|
||||
.join(", ")}`
|
||||
);
|
||||
|
||||
// Create an OpenAI model with tools attached
|
||||
const systemMessage = `You are an assistant that helps users with calculations.
|
||||
You have access to tools that can add, subtract, multiply, and divide numbers. Use
|
||||
these tools to answer the user's questions.`;
|
||||
|
||||
const model = new ChatOpenAI({
|
||||
modelName: process.env.OPENAI_MODEL_NAME || "gpt-4o-mini",
|
||||
temperature: 0.7,
|
||||
}).bindTools(mcpTools);
|
||||
|
||||
// Create a tool node for the LangGraph
|
||||
const toolNode = new ToolNode(mcpTools);
|
||||
|
||||
// ================================================
|
||||
// Create a LangGraph agent flow
|
||||
// ================================================
|
||||
console.log("\n=== CREATING LANGGRAPH AGENT FLOW ===");
|
||||
|
||||
// Define the function that calls the model
|
||||
const llmNode = async (state: typeof MessagesAnnotation.State) => {
|
||||
console.log(`Calling LLM with ${state.messages.length} messages`);
|
||||
|
||||
// Add system message if it's the first call
|
||||
let { messages } = state;
|
||||
if (messages.length === 1 && isHumanMessage(messages[0])) {
|
||||
messages = [new SystemMessage(systemMessage), ...messages];
|
||||
}
|
||||
|
||||
const response = await model.invoke(messages);
|
||||
return { messages: [response] };
|
||||
};
|
||||
|
||||
// Create a new graph with MessagesAnnotation
|
||||
const workflow = new StateGraph(MessagesAnnotation)
|
||||
|
||||
// Add the nodes to the graph
|
||||
.addNode("llm", llmNode)
|
||||
.addNode("tools", toolNode)
|
||||
|
||||
// Add edges - these define how nodes are connected
|
||||
.addEdge(START, "llm")
|
||||
.addEdge("tools", "llm")
|
||||
|
||||
// Conditional routing to end or continue the tool loop
|
||||
.addConditionalEdges("llm", (state) => {
|
||||
const lastMessage = state.messages[state.messages.length - 1];
|
||||
|
||||
// Cast to AIMessage to access tool_calls property
|
||||
const aiMessage = lastMessage as AIMessage;
|
||||
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) {
|
||||
console.log("Tool calls detected, routing to tools node");
|
||||
|
||||
// Log what tools are being called
|
||||
const toolNames = aiMessage.tool_calls
|
||||
.map((tc) => tc.name)
|
||||
.join(", ");
|
||||
console.log(`Tools being called: ${toolNames}`);
|
||||
|
||||
return "tools";
|
||||
}
|
||||
|
||||
// If there are no tool calls, we're done
|
||||
console.log("No tool calls, ending the workflow");
|
||||
return END;
|
||||
});
|
||||
|
||||
// Compile the graph
|
||||
const app = workflow.compile();
|
||||
|
||||
// Define examples to run
|
||||
const examples = [
|
||||
{
|
||||
name: "Add 1 and 2",
|
||||
query: "What is 1 + 2?",
|
||||
},
|
||||
{
|
||||
name: "Subtract 1 from 2",
|
||||
query: "What is 2 - 1?",
|
||||
},
|
||||
{
|
||||
name: "Multiply 1 and 2",
|
||||
query: "What is 1 * 2?",
|
||||
},
|
||||
{
|
||||
name: "Divide 1 by 2",
|
||||
query: "What is 1 / 2?",
|
||||
},
|
||||
];
|
||||
|
||||
// Run the examples
|
||||
console.log("\n=== RUNNING LANGGRAPH AGENT ===");
|
||||
|
||||
for (const example of examples) {
|
||||
console.log(`\n--- Example: ${example.name} ---`);
|
||||
console.log(`Query: ${example.query}`);
|
||||
|
||||
// Run the LangGraph agent
|
||||
const result = await app.invoke({
|
||||
messages: [new HumanMessage(example.query)],
|
||||
});
|
||||
|
||||
// Display the final answer
|
||||
const finalMessage = result.messages[result.messages.length - 1];
|
||||
console.log(`\nResult: ${finalMessage.content}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
process.exit(1); // Exit with error code
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close();
|
||||
console.log("Closed all MCP connections");
|
||||
}
|
||||
|
||||
// Exit process after a short delay to allow for cleanup
|
||||
setTimeout(() => {
|
||||
console.log("Example completed, exiting process.");
|
||||
process.exit(0);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
|
||||
if (isMainModule) {
|
||||
runExample().catch((error) => console.error("Setup error:", error));
|
||||
}
|
||||
@@ -18,5 +18,6 @@ export const config = {
|
||||
tsConfigPath: resolve("./tsconfig.json"),
|
||||
cjsSource: "./dist-cjs",
|
||||
cjsDestination: "./dist",
|
||||
additionalGitignorePaths: [".env", ".eslintcache"],
|
||||
abs,
|
||||
};
|
||||
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@langchain/mcp-adapters",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.5",
|
||||
"description": "LangChain.js adapters for Model Context Protocol (MCP)",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
@@ -46,7 +46,7 @@
|
||||
"author": "Ravi Kiran Vemula",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.7.0",
|
||||
"@modelcontextprotocol/sdk": "^1.11.2",
|
||||
"debug": "^4.4.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
@@ -64,6 +64,7 @@
|
||||
"@langchain/scripts": "^0.1.3",
|
||||
"@tsconfig/recommended": "^1.0.8",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/express": "^5",
|
||||
"@types/node": "^22.13.10",
|
||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||
"@typescript-eslint/parser": "^6.12.0",
|
||||
@@ -78,6 +79,7 @@
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vitest": "^0.5.4",
|
||||
"eventsource": "^3.0.6",
|
||||
"express": "^5.1.0",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^15.2.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
|
||||
+389
-71
@@ -1,10 +1,17 @@
|
||||
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 {
|
||||
SSEClientTransport,
|
||||
type SseError,
|
||||
} from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
import {
|
||||
StreamableHTTPClientTransport,
|
||||
StreamableHTTPError,
|
||||
type StreamableHTTPReconnectionOptions,
|
||||
} from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||
import type { StructuredToolInterface } from "@langchain/core/tools";
|
||||
import debug from "debug";
|
||||
import { z } from "zod";
|
||||
import Stream from "node:stream";
|
||||
import { loadMcpTools, LoadMcpToolsOptions } from "./tools.js";
|
||||
|
||||
// Read package name from package.json
|
||||
@@ -17,7 +24,7 @@ function getDebugLog() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create schema for stdio transport restart configuration
|
||||
* Stdio transport restart configuration
|
||||
*/
|
||||
export function createStdioRestartSchema() {
|
||||
return z
|
||||
@@ -48,37 +55,67 @@ export function createStdioRestartSchema() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create schema for stdio transport connection
|
||||
* Stdio transport connection
|
||||
*/
|
||||
export function createStdioConnectionSchema() {
|
||||
return z
|
||||
.object({
|
||||
/**
|
||||
* Optional transport type, inferred from the structure of the config if not provided. Included
|
||||
* for compatibility with common MCP client config file formats.
|
||||
*/
|
||||
transport: z.literal("stdio").optional(),
|
||||
/**
|
||||
* Optional transport type, inferred from the structure of the config if not provided. Included
|
||||
* for compatibility with common MCP client config file formats.
|
||||
*/
|
||||
type: z.literal("stdio").optional(),
|
||||
/**
|
||||
* The executable to run the server (e.g. `node`, `npx`, etc)
|
||||
*/
|
||||
command: z.string().describe("The executable to run the server"),
|
||||
/**
|
||||
* Array of command line arguments to pass to the executable
|
||||
*/
|
||||
args: z
|
||||
.array(z.string())
|
||||
.describe("Command line arguments to pass to the executable"),
|
||||
/**
|
||||
* Environment variables to set when spawning the process.
|
||||
*/
|
||||
env: z
|
||||
.record(z.string())
|
||||
.describe("The environment to use when spawning the process")
|
||||
.optional(),
|
||||
/**
|
||||
* The encoding to use when reading from the process
|
||||
*/
|
||||
encoding: z
|
||||
.string()
|
||||
.describe("The encoding to use when reading from the process")
|
||||
.optional(),
|
||||
/**
|
||||
* How to handle stderr of the child process. This matches the semantics of Node's `child_process.spawn`
|
||||
*
|
||||
* The default is "inherit", meaning messages to stderr will be printed to the parent process's stderr.
|
||||
*
|
||||
* @default "inherit"
|
||||
*/
|
||||
stderr: z
|
||||
.union([
|
||||
z.literal("overlapped"),
|
||||
z.literal("pipe"),
|
||||
z.literal("ignore"),
|
||||
z.literal("inherit"),
|
||||
z.instanceof(Stream),
|
||||
])
|
||||
.describe(
|
||||
"How to handle stderr of the child process. This matches the semantics of Node's `child_process.spawn`"
|
||||
)
|
||||
.optional(),
|
||||
.optional()
|
||||
.default("inherit"),
|
||||
/**
|
||||
* The working directory to use when spawning the process.
|
||||
*/
|
||||
cwd: z
|
||||
.string()
|
||||
.describe("The working directory to use when spawning the process")
|
||||
@@ -94,9 +131,9 @@ export function createStdioConnectionSchema() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create schema for SSE transport reconnection configuration
|
||||
* Streamable HTTP transport reconnection configuration
|
||||
*/
|
||||
export function createSseReconnectSchema() {
|
||||
export function createStreamableReconnectSchema() {
|
||||
return z
|
||||
.object({
|
||||
/**
|
||||
@@ -123,30 +160,51 @@ export function createSseReconnectSchema() {
|
||||
.describe("The delay in milliseconds between reconnection attempts")
|
||||
.optional(),
|
||||
})
|
||||
.describe("Configuration for SSE transport reconnection");
|
||||
.describe("Configuration for streamable HTTP transport reconnection");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create schema for SSE transport connection
|
||||
* Streamable HTTP transport connection
|
||||
*/
|
||||
export function createSseConnectionSchema() {
|
||||
return z.intersection(
|
||||
z
|
||||
.object({
|
||||
url: z.string().url(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
useNodeEventSource: z.boolean().optional(),
|
||||
/**
|
||||
* Additional reconnection settings
|
||||
*/
|
||||
reconnect: createSseReconnectSchema().optional(),
|
||||
})
|
||||
.describe("Configuration for SSE transport connection"),
|
||||
z.union([
|
||||
z.object({ transport: z.literal("sse") }),
|
||||
z.object({ type: z.literal("sse") }),
|
||||
])
|
||||
);
|
||||
export function createStreamableHTTPConnectionSchema() {
|
||||
return z
|
||||
.object({
|
||||
/**
|
||||
* Optional transport type, inferred from the structure of the config. If "sse", will not attempt
|
||||
* to connect using streamable HTTP.
|
||||
*/
|
||||
transport: z.union([z.literal("http"), z.literal("sse")]).optional(),
|
||||
/**
|
||||
* Optional transport type, inferred from the structure of the config. If "sse", will not attempt
|
||||
* to connect using streamable HTTP.
|
||||
*/
|
||||
type: z.union([z.literal("http"), z.literal("sse")]).optional(),
|
||||
/**
|
||||
* The URL to connect to
|
||||
*/
|
||||
url: z.string().url(),
|
||||
/**
|
||||
* Additional headers to send with the request, useful for authentication
|
||||
*/
|
||||
headers: z.record(z.string()).optional(),
|
||||
/**
|
||||
* Whether to use Node's EventSource for SSE connections (not applicable to streamable HTTP)
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
useNodeEventSource: z.boolean().optional().default(false),
|
||||
/**
|
||||
* Additional reconnection settings.
|
||||
*/
|
||||
reconnect: createStreamableReconnectSchema().optional(),
|
||||
/**
|
||||
* Whether to automatically fallback to SSE if Streamable HTTP is not available or not supported
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
automaticSSEFallback: z.boolean().optional().default(true),
|
||||
})
|
||||
.describe("Configuration for streamable HTTP transport connection");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,29 +212,52 @@ export function createSseConnectionSchema() {
|
||||
*/
|
||||
export function createConnectionSchema() {
|
||||
return z
|
||||
.union([createStdioConnectionSchema(), createSseConnectionSchema()])
|
||||
.union([
|
||||
createStdioConnectionSchema(),
|
||||
createStreamableHTTPConnectionSchema(),
|
||||
])
|
||||
.describe("Configuration for a single MCP server");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create schema for {@link MultiServerMCPClient} configuration
|
||||
* {@link MultiServerMCPClient} configuration
|
||||
*/
|
||||
export function createClientConfigSchema() {
|
||||
return z
|
||||
.object({
|
||||
/**
|
||||
* A map of server names to their configuration
|
||||
*/
|
||||
mcpServers: z
|
||||
.record(createConnectionSchema())
|
||||
.describe("A map of server names to their configuration"),
|
||||
/**
|
||||
* Whether to throw an error if a tool fails to load
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
throwOnLoadError: z
|
||||
.boolean()
|
||||
.describe("Whether to throw an error if a tool fails to load")
|
||||
.optional()
|
||||
.default(true),
|
||||
/**
|
||||
* Whether to prefix tool names with the server name. Prefixes are separated by double
|
||||
* underscores (example: `calculator_server_1__add`).
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
prefixToolNameWithServerName: z
|
||||
.boolean()
|
||||
.describe("Whether to prefix tool names with the server name")
|
||||
.optional()
|
||||
.default(true),
|
||||
/**
|
||||
* An additional prefix to add to the tool name Prefixes are separated by double underscores
|
||||
* (example: `mcp__add`).
|
||||
*
|
||||
* @default "mcp"
|
||||
*/
|
||||
additionalToolNamePrefix: z
|
||||
.string()
|
||||
.describe("An additional prefix to add to the tool name")
|
||||
@@ -189,26 +270,54 @@ export function createClientConfigSchema() {
|
||||
/**
|
||||
* Configuration for stdio transport connection
|
||||
*/
|
||||
export type StdioConnection = z.infer<
|
||||
export type StdioConnection = z.input<
|
||||
ReturnType<typeof createStdioConnectionSchema>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Configuration for SSE transport connection
|
||||
* Type for {@link StdioConnection} with default values applied.
|
||||
*/
|
||||
export type SSEConnection = z.infer<
|
||||
ReturnType<typeof createSseConnectionSchema>
|
||||
export type ResolvedStdioConnection = z.infer<
|
||||
ReturnType<typeof createStdioConnectionSchema>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Configuration for streamable HTTP transport connection
|
||||
*/
|
||||
export type StreamableHTTPConnection = z.input<
|
||||
ReturnType<typeof createStreamableHTTPConnectionSchema>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Type for {@link StreamableHTTPConnection} with default values applied.
|
||||
*/
|
||||
export type ResolvedStreamableHTTPConnection = z.infer<
|
||||
ReturnType<typeof createStreamableHTTPConnectionSchema>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Union type for all transport connection types
|
||||
*/
|
||||
export type Connection = z.infer<ReturnType<typeof createConnectionSchema>>;
|
||||
export type Connection = z.input<ReturnType<typeof createConnectionSchema>>;
|
||||
|
||||
/**
|
||||
* Type for {@link MultiServerMCPClient} configuration
|
||||
*/
|
||||
export type ClientConfig = z.infer<ReturnType<typeof createClientConfigSchema>>;
|
||||
export type ClientConfig = z.input<ReturnType<typeof createClientConfigSchema>>;
|
||||
|
||||
/**
|
||||
* Type for {@link Connection} with default values applied.
|
||||
*/
|
||||
export type ResolvedConnection = z.infer<
|
||||
ReturnType<typeof createConnectionSchema>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Type for {@link MultiServerMCPClient} configuration, with default values applied.
|
||||
*/
|
||||
export type ResolvedClientConfig = z.infer<
|
||||
ReturnType<typeof createClientConfigSchema>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Error class for MCP client operations
|
||||
@@ -220,9 +329,17 @@ export class MCPClientError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function isStdioConnection(
|
||||
connection: Connection
|
||||
): connection is StdioConnection {
|
||||
function isResolvedStdioConnection(
|
||||
connection: unknown
|
||||
): connection is ResolvedStdioConnection {
|
||||
if (
|
||||
typeof connection !== "object" ||
|
||||
connection === null ||
|
||||
Array.isArray(connection)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("transport" in connection && connection.transport === "stdio") {
|
||||
return true;
|
||||
}
|
||||
@@ -238,17 +355,36 @@ function isStdioConnection(
|
||||
return false;
|
||||
}
|
||||
|
||||
function isSSEConnection(connection: Connection): connection is SSEConnection {
|
||||
if ("transport" in connection && connection.transport === "sse") {
|
||||
return true;
|
||||
function isResolvedStreamableHTTPConnection(
|
||||
connection: unknown
|
||||
): connection is ResolvedStreamableHTTPConnection {
|
||||
if (
|
||||
typeof connection !== "object" ||
|
||||
connection === null ||
|
||||
Array.isArray(connection)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("type" in connection && connection.type === "sse") {
|
||||
if (
|
||||
("transport" in connection &&
|
||||
typeof connection.transport === "string" &&
|
||||
["http", "sse"].includes(connection.transport)) ||
|
||||
("type" in connection &&
|
||||
typeof connection.type === "string" &&
|
||||
["http", "sse"].includes(connection.type))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("url" in connection && typeof connection.url === "string") {
|
||||
return true;
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(connection.url);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -262,7 +398,7 @@ export class MultiServerMCPClient {
|
||||
|
||||
private _serverNameToTools: Record<string, StructuredToolInterface[]> = {};
|
||||
|
||||
private _connections?: Record<string, Connection>;
|
||||
private _connections?: Record<string, ResolvedConnection>;
|
||||
|
||||
private _loadToolsOptions: LoadMcpToolsOptions;
|
||||
|
||||
@@ -270,18 +406,31 @@ export class MultiServerMCPClient {
|
||||
|
||||
private _transportInstances: Record<
|
||||
string,
|
||||
StdioClientTransport | SSEClientTransport
|
||||
StdioClientTransport | SSEClientTransport | StreamableHTTPClientTransport
|
||||
> = {};
|
||||
|
||||
private _config: ResolvedClientConfig;
|
||||
|
||||
/**
|
||||
* Returns clone of server config for inspection purposes.
|
||||
*
|
||||
* Client does not support config modifications.
|
||||
*/
|
||||
get config(): ClientConfig {
|
||||
// clone config so it can't be mutated
|
||||
return JSON.parse(JSON.stringify(this._config));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new MultiServerMCPClient.
|
||||
*
|
||||
* @param connections - Optional connections to initialize
|
||||
*/
|
||||
constructor(config: ClientConfig | Record<string, Connection>) {
|
||||
let parsedServerConfig: ClientConfig;
|
||||
let parsedServerConfig: ResolvedClientConfig;
|
||||
|
||||
const configSchema = createClientConfigSchema();
|
||||
|
||||
if ("mcpServers" in config) {
|
||||
parsedServerConfig = configSchema.parse(config);
|
||||
} else {
|
||||
@@ -302,6 +451,7 @@ export class MultiServerMCPClient {
|
||||
additionalToolNamePrefix: parsedServerConfig.additionalToolNamePrefix,
|
||||
};
|
||||
|
||||
this._config = parsedServerConfig;
|
||||
this._connections = parsedServerConfig.mcpServers;
|
||||
}
|
||||
|
||||
@@ -320,7 +470,7 @@ export class MultiServerMCPClient {
|
||||
throw new MCPClientError("No connections to initialize");
|
||||
}
|
||||
|
||||
const connectionsToInit: [string, Connection][] = Array.from(
|
||||
const connectionsToInit: [string, ResolvedConnection][] = Array.from(
|
||||
Object.entries(this._connections).filter(
|
||||
([serverName]) => this._clients[serverName] === undefined
|
||||
)
|
||||
@@ -331,10 +481,17 @@ export class MultiServerMCPClient {
|
||||
`INFO: Initializing connection to server "${serverName}"...`
|
||||
);
|
||||
|
||||
if (isStdioConnection(connection)) {
|
||||
if (isResolvedStdioConnection(connection)) {
|
||||
await this._initializeStdioConnection(serverName, connection);
|
||||
} else if (isSSEConnection(connection)) {
|
||||
await this._initializeSSEConnection(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(
|
||||
@@ -400,9 +557,9 @@ export class MultiServerMCPClient {
|
||||
*/
|
||||
private async _initializeStdioConnection(
|
||||
serverName: string,
|
||||
connection: StdioConnection
|
||||
connection: ResolvedStdioConnection
|
||||
): Promise<void> {
|
||||
const { command, args, env, restart } = connection;
|
||||
const { command, args, env, restart, stderr } = connection;
|
||||
|
||||
getDebugLog()(
|
||||
`DEBUG: Creating stdio transport for server "${serverName}" with command: ${command} ${args.join(
|
||||
@@ -414,6 +571,7 @@ export class MultiServerMCPClient {
|
||||
command,
|
||||
args,
|
||||
env,
|
||||
stderr,
|
||||
});
|
||||
|
||||
this._transportInstances[serverName] = transport;
|
||||
@@ -458,8 +616,8 @@ export class MultiServerMCPClient {
|
||||
private _setupStdioRestart(
|
||||
serverName: string,
|
||||
transport: StdioClientTransport,
|
||||
connection: StdioConnection,
|
||||
restart: NonNullable<StdioConnection["restart"]>
|
||||
connection: ResolvedStdioConnection,
|
||||
restart: NonNullable<ResolvedStdioConnection["restart"]>
|
||||
): void {
|
||||
const originalOnClose = transport.onclose;
|
||||
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-misused-promises
|
||||
@@ -483,19 +641,135 @@ export class MultiServerMCPClient {
|
||||
};
|
||||
}
|
||||
|
||||
private _getHttpErrorCode(error: unknown): number | undefined {
|
||||
const streamableError = error as StreamableHTTPError | SseError;
|
||||
let { code } = streamableError;
|
||||
// try parsing from error message if code is not set
|
||||
if (code == null) {
|
||||
const m = streamableError.message.match(/\(HTTP (\d\d\d)\)/);
|
||||
if (m && m.length > 1) {
|
||||
code = parseInt(m[1], 10);
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private _toSSEConnectionURL(url: string): string {
|
||||
const urlObj = new URL(url);
|
||||
const pathnameParts = urlObj.pathname.split("/");
|
||||
const lastPart = pathnameParts.at(-1);
|
||||
if (lastPart && lastPart === "mcp") {
|
||||
pathnameParts[pathnameParts.length - 1] = "sse";
|
||||
}
|
||||
urlObj.pathname = pathnameParts.join("/");
|
||||
return urlObj.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize an SSE connection
|
||||
* Initialize a streamable HTTP connection
|
||||
*/
|
||||
private async _initializeSSEConnection(
|
||||
private async _initializeStreamableHTTPConnection(
|
||||
serverName: string,
|
||||
connection: SSEConnection
|
||||
connection: ResolvedStreamableHTTPConnection
|
||||
): Promise<void> {
|
||||
const { url, headers, useNodeEventSource, reconnect } = connection;
|
||||
const {
|
||||
url,
|
||||
headers,
|
||||
reconnect,
|
||||
type: typeField,
|
||||
transport: transportField,
|
||||
} = connection;
|
||||
|
||||
const automaticSSEFallback = connection.automaticSSEFallback ?? true;
|
||||
|
||||
const transportType = typeField || transportField;
|
||||
|
||||
getDebugLog()(
|
||||
`DEBUG: Creating SSE transport for server "${serverName}" with URL: ${url}`
|
||||
);
|
||||
|
||||
if (transportType === "http" || transportType == null) {
|
||||
const transport = await this._createStreamableHTTPTransport(
|
||||
serverName,
|
||||
url,
|
||||
headers,
|
||||
reconnect
|
||||
);
|
||||
this._transportInstances[serverName] = transport;
|
||||
|
||||
const client = new Client({
|
||||
name: "langchain-mcp-adapter",
|
||||
version: "0.1.0",
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect(transport);
|
||||
|
||||
this._clients[serverName] = client;
|
||||
|
||||
const cleanup = async () => {
|
||||
getDebugLog()(
|
||||
`DEBUG: Closing streamable HTTP transport for server "${serverName}"`
|
||||
);
|
||||
await transport.close();
|
||||
};
|
||||
|
||||
this._cleanupFunctions.push(cleanup);
|
||||
|
||||
// Load tools for this server
|
||||
await this._loadToolsForServer(serverName, client);
|
||||
} catch (error) {
|
||||
const code = this._getHttpErrorCode(error);
|
||||
if (automaticSSEFallback && code != null && code >= 400 && code < 500) {
|
||||
// Streamable HTTP error is a 4xx, so fall back to SSE
|
||||
try {
|
||||
await this._initializeSSEConnection(serverName, connection);
|
||||
} catch (firstSSEError) {
|
||||
// try one more time, but modify the URL to end with `/sse`
|
||||
const sseUrl = this._toSSEConnectionURL(url);
|
||||
|
||||
if (sseUrl !== url) {
|
||||
try {
|
||||
await this._initializeSSEConnection(serverName, {
|
||||
...connection,
|
||||
url: sseUrl,
|
||||
});
|
||||
} catch (secondSSEError) {
|
||||
throw new MCPClientError(
|
||||
`Failed to connect to streamable HTTP server "${serverName}, url: ${url}": ${error}. Additionally, tried falling back to SSE at ${url} and ${sseUrl}, but this also failed: ${secondSSEError}`,
|
||||
serverName
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new MCPClientError(
|
||||
`Failed to connect to streamable HTTP server after trying to fall back to SSE: "${serverName}, url: ${url}": ${error} (SSE fallback failed with error ${firstSSEError})`,
|
||||
serverName
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new MCPClientError(
|
||||
`Failed to connect to streamable HTTP server "${serverName}, url: ${url}": ${error}`,
|
||||
serverName
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize an SSE connection
|
||||
*
|
||||
* Don't call this directly unless SSE transport is explicitly requested. Otherwise,
|
||||
* use _initializeStreamableHTTPConnection and it'll fall back to SSE if needed for
|
||||
* backwards compatibility.
|
||||
*/
|
||||
private async _initializeSSEConnection(
|
||||
serverName: string,
|
||||
connection: ResolvedStreamableHTTPConnection // used for both SSE and streamable HTTP
|
||||
): Promise<void> {
|
||||
const { url, headers, useNodeEventSource, reconnect } = connection;
|
||||
|
||||
try {
|
||||
const transport = await this._createSSETransport(
|
||||
serverName,
|
||||
@@ -539,7 +813,7 @@ export class MultiServerMCPClient {
|
||||
await this._loadToolsForServer(serverName, client);
|
||||
} catch (error) {
|
||||
throw new MCPClientError(
|
||||
`Failed to create SSE transport for server "${serverName}": ${error}`,
|
||||
`Failed to create SSE transport for server "${serverName}, url: ${url}": ${error}`,
|
||||
serverName
|
||||
);
|
||||
}
|
||||
@@ -585,6 +859,44 @@ export class MultiServerMCPClient {
|
||||
});
|
||||
}
|
||||
|
||||
private async _createStreamableHTTPTransport(
|
||||
serverName: string,
|
||||
url: string,
|
||||
headers?: Record<string, string>,
|
||||
reconnect?: ResolvedStreamableHTTPConnection["reconnect"]
|
||||
): Promise<StreamableHTTPClientTransport> {
|
||||
if (!headers) {
|
||||
// Simple case - no headers, use default transport
|
||||
return new StreamableHTTPClientTransport(new URL(url));
|
||||
} else {
|
||||
getDebugLog()(
|
||||
`DEBUG: Using custom headers for SSE transport to server "${serverName}"`
|
||||
);
|
||||
|
||||
// partial options object for setting up reconnections
|
||||
const r: {
|
||||
reconnectionOptions: StreamableHTTPReconnectionOptions;
|
||||
} = {
|
||||
reconnectionOptions: {
|
||||
initialReconnectionDelay: reconnect?.delayMs ?? 1000, // MCP default
|
||||
maxReconnectionDelay: reconnect?.delayMs ?? 30000, // MCP default
|
||||
maxRetries: reconnect?.maxAttempts ?? 2, // MCP default
|
||||
reconnectionDelayGrowFactor: 1.5, // MCP default
|
||||
},
|
||||
};
|
||||
|
||||
if (reconnect != null && reconnect.enabled === false) {
|
||||
r.reconnectionOptions.maxRetries = 0;
|
||||
}
|
||||
|
||||
return new StreamableHTTPClientTransport(new URL(url), {
|
||||
requestInit: { headers },
|
||||
// don't set if reconnect is null so we rely on SDK defaults
|
||||
...(reconnect == null ? {} : r),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an EventSource transport for Node.js environments
|
||||
*/
|
||||
@@ -613,9 +925,8 @@ export class MultiServerMCPClient {
|
||||
|
||||
// For Extended EventSource, create the SSE transport
|
||||
return new SSEClientTransport(new URL(url), {
|
||||
// Pass empty options for test compatibility
|
||||
eventSourceInit: {},
|
||||
requestInit: {},
|
||||
requestInit: { headers },
|
||||
});
|
||||
} catch (extendedError) {
|
||||
// Fall back to standard eventsource if extended-eventsource is not available
|
||||
@@ -662,13 +973,13 @@ export class MultiServerMCPClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up SSE reconnect handling
|
||||
* Set up reconnect handling for SSE (Streamable HTTP reconnects are more complex and are handled internally by the SDK)
|
||||
*/
|
||||
private _setupSSEReconnect(
|
||||
serverName: string,
|
||||
transport: SSEClientTransport,
|
||||
connection: SSEConnection,
|
||||
reconnect: NonNullable<SSEConnection["reconnect"]>
|
||||
transport: SSEClientTransport | StreamableHTTPClientTransport,
|
||||
connection: ResolvedStreamableHTTPConnection,
|
||||
reconnect: NonNullable<ResolvedStreamableHTTPConnection["reconnect"]>
|
||||
): void {
|
||||
const originalOnClose = transport.onclose;
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises, no-param-reassign
|
||||
@@ -680,7 +991,7 @@ export class MultiServerMCPClient {
|
||||
// Only attempt reconnect if we haven't cleaned up
|
||||
if (this._clients[serverName]) {
|
||||
getDebugLog()(
|
||||
`INFO: SSE connection for server "${serverName}" closed, attempting to reconnect...`
|
||||
`INFO: HTTP connection for server "${serverName}" closed, attempting to reconnect...`
|
||||
);
|
||||
await this._attemptReconnect(
|
||||
serverName,
|
||||
@@ -728,7 +1039,7 @@ export class MultiServerMCPClient {
|
||||
*/
|
||||
private async _attemptReconnect(
|
||||
serverName: string,
|
||||
connection: Connection,
|
||||
connection: ResolvedConnection,
|
||||
maxAttempts = 3,
|
||||
delayMs = 1000
|
||||
): Promise<void> {
|
||||
@@ -758,10 +1069,17 @@ export class MultiServerMCPClient {
|
||||
}
|
||||
|
||||
// Initialize just this connection based on its type
|
||||
if (isStdioConnection(connection)) {
|
||||
if (isResolvedStdioConnection(connection)) {
|
||||
await this._initializeStdioConnection(serverName, connection);
|
||||
} else if (isSSEConnection(connection)) {
|
||||
await this._initializeSSEConnection(serverName, connection);
|
||||
} else if (isResolvedStreamableHTTPConnection(connection)) {
|
||||
if (connection.type === "sse" || connection.transport === "sse") {
|
||||
await this._initializeSSEConnection(serverName, connection);
|
||||
} else {
|
||||
await this._initializeStreamableHTTPConnection(
|
||||
serverName,
|
||||
connection
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if connected
|
||||
|
||||
+8
-1
@@ -1,8 +1,15 @@
|
||||
import { type StreamableHTTPConnection } from "./client.js";
|
||||
|
||||
export {
|
||||
MultiServerMCPClient,
|
||||
type Connection,
|
||||
type StdioConnection,
|
||||
type SSEConnection,
|
||||
type StreamableHTTPConnection,
|
||||
} from "./client.js";
|
||||
|
||||
/**
|
||||
* Type alias for backward compatibility with previous versions of the package.
|
||||
*/
|
||||
export type SSEConnection = StreamableHTTPConnection;
|
||||
|
||||
export { loadMcpTools } from "./tools.js";
|
||||
|
||||
+3
-4
@@ -95,15 +95,14 @@ async function _convertCallToolResult(
|
||||
if (result.isError) {
|
||||
throw new ToolException(
|
||||
`MCP tool '${toolName}' on server '${serverName}' returned an error: ${result.content
|
||||
.map((content: CallToolResultContent) => content.text)
|
||||
.map((content) => content.text)
|
||||
.join("\n")}`
|
||||
);
|
||||
}
|
||||
|
||||
const mcpTextAndImageContent: MessageContentComplex[] = (
|
||||
result.content.filter(
|
||||
(content: CallToolResultContent) =>
|
||||
content.type === "text" || content.type === "image"
|
||||
(content) => content.type === "text" || content.type === "image"
|
||||
) as (TextContent | ImageContent)[]
|
||||
).map((content: TextContent | ImageContent) => {
|
||||
switch (content.type) {
|
||||
@@ -134,7 +133,7 @@ async function _convertCallToolResult(
|
||||
await Promise.all(
|
||||
(
|
||||
result.content.filter(
|
||||
(content: CallToolResultContent) => content.type === "resource"
|
||||
(content) => content.type === "resource"
|
||||
) as EmbeddedResource[]
|
||||
).map((content: EmbeddedResource) =>
|
||||
_embeddedResourceToArtifact(content, client)
|
||||
|
||||
Reference in New Issue
Block a user