Compare commits

...

21 Commits

Author SHA1 Message Date
Ben Burns c3f16633d1 chore: update README with migration notice 2025-05-16 15:18:45 -07:00
github-actions[bot] 01b70d18c9 chore: bump version to v0.4.5 [skip ci] 2025-05-13 21:07:33 +00:00
Ben Burns 72818340c1 fix: drop useless import from node:stream package (#75)
fixes #62
2025-05-13 13:49:06 -07:00
Ben Burns 373b08e090 Merge pull request #74 from langchain-ai/release
chore: bump version to v0.4.4 [skip ci]
2025-05-13 13:21:09 -07:00
Ben Burns 831359db3a chore: bump version to v0.4.4 [skip ci] 2025-05-13 13:16:00 -07:00
Abhilash Panigrahi 68f130e1d0 feat: support StreamableHTTPClientTransport (#64)
Co-authored-by: Abhilash Panigrahi <pabhila@amazon.com>
Co-authored-by: Ben Burns <803016+benjamincburns@users.noreply.github.com>
2025-05-13 13:00:39 -07:00
github-actions[bot] 03172e3fcd chore: bump version to v0.4.3 [skip ci] 2025-05-12 17:54:44 +00:00
Niko Nummi 8cfec54c29 feat: send headers when using node event source transport (#58) 2025-05-12 08:49:34 -07:00
Ravi Kiran Vemula c1b0f3d8ad changes company name in readme (#72) 2025-05-12 08:14:33 -07:00
github-actions[bot] 66f6eb0455 chore: bump version to v0.4.2 [skip ci] 2025-04-09 06:48:21 +00:00
Ben Burns b8ccb243a0 fix: drop unnecessary dependency (#57) 2025-04-09 18:47:18 +12:00
github-actions[bot] a67182573d chore: bump version to v0.4.1 [skip ci] 2025-04-09 06:43:02 +00:00
Ben Burns 2930bf5124 fix: pass tool schema without conversion (#56) 2025-04-09 18:40:43 +12:00
github-actions[bot] 60458b9926 chore: bump version to v0.4.0 [skip ci] 2025-04-04 03:23:07 +00:00
Ben Burns 6093a1c9ba chore: update README to reflect recent changes (#53) 2025-04-04 16:17:51 +13:00
Ben Burns 09f511a57d break: simplify interface, drop file IO, accept common mcp server config (#38) 2025-04-03 21:56:54 +13:00
github-actions[bot] b4313142e0 chore: bump version to v0.3.4 [skip ci] 2025-03-24 02:43:50 +00:00
Ben Burns 18daa5b83f fix: fix CJS by dynamically importing json-schema-to-zod (#37)
fixes #36
2025-03-24 15:42:25 +13:00
github-actions[bot] 2fc9bfa79e chore: bump version to v0.3.3 [skip ci] 2025-03-22 04:22:31 +00:00
Ben Burns e38ff20113 fix: don't autorun husky on npm install (#35) 2025-03-22 17:21:20 +13:00
Ben Burns 6eb9d6cf7a fix: fix post-release commit (#34) 2025-03-22 17:15:22 +13:00
33 changed files with 2765 additions and 4103 deletions
+10 -1
View File
@@ -24,6 +24,15 @@ module.exports = {
],
rules: {
"no-instanceof/no-instanceof": 2,
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "memberLike",
"modifiers": ["private"],
"format": ["camelCase"],
"leadingUnderscore": "require"
}
],
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/no-shadow": 0,
@@ -38,7 +47,7 @@ module.exports = {
"import/extensions": [2, "ignorePackages"],
"import/no-extraneous-dependencies": [
"error",
{ devDependencies: ["**/*.test.ts", "examples/**/*.ts"] },
{ devDependencies: ["**/*.test.ts", "__tests__/**/*.ts", "examples/**/*.ts"] },
],
"import/no-unresolved": 0,
"import/prefer-default-export": 0,
+1 -1
View File
@@ -86,7 +86,7 @@ jobs:
if: github.event_name == 'workflow_dispatch'
run: |
NEW_VERSION=$(node -p "require('./package.json').version")
git add package.json package-lock.json
git add package.json yarn.lock
git commit -m "chore: bump version to v${NEW_VERSION} [skip ci]"
git tag -a "v${NEW_VERSION}" -m "Release v${NEW_VERSION}"
git push origin main
+2
View File
@@ -5,3 +5,5 @@ index.d.cts
node_modules
dist
.yarn
.env
.eslintcache
-11
View File
@@ -1,11 +0,0 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}
+5
View File
@@ -0,0 +1,5 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"typescript.tsdk": "node_modules/typescript/lib"
}
Binary file not shown.
+5 -521
View File
@@ -1,527 +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).**
[![npm version](https://img.shields.io/npm/v/@langchain/mcp-adapters.svg)](https://www.npmjs.com/package/@langchain/mcp-adapters)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- 🛠️ **Development Features**
- 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
```
## Prerequisites
- Node.js >= 18
- For stdio transport: Python MCP servers require Python 3.8+
- For SSE transport: A running MCP server with SSE endpoint
- For SSE with headers in Node.js: The `eventsource` package
# Quickstart
Here is a simple example of using the MCP tools with a LangGraph agent.
```bash
npm install @langchain/mcp-adapters @langchain/langgraph @langchain/core @langchain/openai
export OPENAI_API_KEY=<your_api_key>
```
### Server
First, let's create an MCP server that can add and multiply numbers.
```python
# math_server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Math")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
@mcp.tool()
def multiply(a: int, b: int) -> int:
"""Multiply two numbers"""
return a * b
if __name__ == "__main__":
mcp.run(transport="stdio")
```
### 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' });
// Create transport for stdio connection
const transport = new StdioClientTransport({
command: 'python',
args: ['math_server.py'],
});
// 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
const tools = await loadMcpTools("math", client);
// 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();
}
```
## Multiple MCP Servers
The library also allows you to connect to multiple MCP servers and load tools from them:
### Server
```python
# math_server.py
...
# weather_server.py
from mcp.server.fastmcp import FastMCP
# Create a server
mcp = FastMCP(name="Weather")
@mcp.tool()
def get_temperature(city: str) -> str:
"""Get the current temperature for a city."""
# Mock implementation
temperatures = {
"new york": "72°F",
"london": "65°F",
"tokyo": "25°C",
}
city_lower = city.lower()
if city_lower in temperatures:
return f"The current temperature in {city} is {temperatures[city_lower]}."
else:
return "Temperature data not available for this city"
# Run the server with SSE transport
if __name__ == "__main__":
mcp.run(transport="sse")
```
### 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();
await client.connectToServerViaStdio('math-server', 'python', ['math_server.py']);
await client.connectToServerViaSSE('weather-server', 'http://localhost:8000/sse');
const tools = 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
const mathResponse = await agent.invoke({
messages: [{ role: 'user', content: "what's (3 + 5) x 12?" }],
});
const weatherResponse = await agent.invoke({
messages: [{ role: 'user', content: 'what is the weather in nyc?' }],
});
await client.close();
```
Below are more detailed examples of how to configure `MultiServerMCPClient`.
### Basic Connection
```typescript
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
// Create a client
const client = new MultiServerMCPClient();
// Connect to a local server via stdio
await client.connectToServerViaStdio(
'math-server', // Server name
'python', // Command to run
['./math_server.py'] // Command arguments
);
// Connect to a remote server via SSE
await client.connectToServerViaSSE(
'weather-server', // Server name
'http://localhost:8000/sse' // SSE endpoint URL
);
// Get all tools from all servers as a flattened array
const tools = client.getTools();
// Get tools from specific servers
const mathTools = client.getTools(['math-server']);
// Get tools grouped by server name
const toolsByServer = client.getToolsByServer();
// Close all connections when done
await client.close();
```
> [!NOTE]
> For stdio connections, the `transport` field is optional. If not specified, it defaults to 'stdio'.
### With Authentication Headers
```typescript
// Connect to a server with authentication
await client.connectToServerViaSSE(
'auth-server',
'https://api.example.com/mcp/sse',
{
Authorization: 'Bearer token',
'X-API-Key': 'your-api-key',
},
true // Use Node.js EventSource for header support
);
```
### Configuration via JSON
Define your server connections in a JSON file:
```json
{
"servers": {
"math": {
"command": "python",
"args": ["./math_server.py"]
},
"weather": {
"transport": "sse",
"url": "http://localhost:8000/sse",
"headers": {
"Authorization": "Bearer token"
},
"useNodeEventSource": true
}
}
}
```
Then load it in your code:
```typescript
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
// Load from default location (./mcp.json)
const client = MultiServerMCPClient.fromConfigFile();
// Or specify a custom path
// const client = MultiServerMCPClient.fromConfigFile('./config/mcp.json');
await client.initializeConnections();
const tools = client.getTools();
```
## Enhanced Configuration Management
LangChainJS-MCP-Adapters provides flexible and powerful configuration management capabilities:
### Automatic Default Configuration
The client automatically looks for and loads a `mcp.json` file from the current working directory if no explicit configuration is provided:
```typescript
// This will automatically load from ./mcp.json if it exists
const client = new MultiServerMCPClient();
await client.initializeConnections();
```
### Configuration Loading Options
There are multiple ways to load configurations:
```typescript
// Method 1: Automatic default loading
const client1 = new MultiServerMCPClient(); // Automatically checks for mcp.json
// Method 2: From specified config file
const client2 = MultiServerMCPClient.fromConfigFile('./config/custom-mcp.json');
```
### Combining Multiple Configuration Sources
You can combine configurations from multiple sources - they will be merged rather than replaced:
```typescript
// Start with default configuration or empty if no mcp.json exists
const client = new MultiServerMCPClient();
// Add another configuration file
client.addConfigFromFile('./team1-servers.json');
// Add yet another configuration file
client.addConfigFromFile('./team2-servers.json');
// Add configurations directly in code
client.addConnections({
'custom-server': {
transport: 'stdio',
command: 'python',
args: ['./special_server.py'],
},
});
// Initialize all connections from all sources
await client.initializeConnections();
```
### Configuration Processing Order
Configurations are processed in the order they are added:
1. Constructor argument or automatic `mcp.json` (if present)
2. Each `addConfigFromFile()` call in sequence
3. Each `addConnections()` call in sequence
If the same server name appears in multiple configurations, **the later configuration takes precedence**, allowing for overriding settings.
### Direct Connection Methods
For simple use cases, you can bypass configuration files entirely and connect to servers directly using the provided connection methods:
```typescript
const client = new MultiServerMCPClient();
// Add a stdio connection
await client.connectToServerViaStdio(
'math-server',
'python',
['./math_server.py'],
// Optional environment variables
{ PYTHONPATH: './lib' },
// Optional restart configuration
{ enabled: true, maxAttempts: 3, delayMs: 2000 }
);
// Add an SSE connection
await client.connectToServerViaSSE(
'remote-server',
'https://api.example.com/mcp/sse',
// Optional headers
{ Authorization: 'Bearer token' },
// Optional Node.js EventSource flag
true,
// Optional reconnection configuration
{ enabled: true, maxAttempts: 5, delayMs: 1000 }
);
```
### Environment Variable Substitution
Configuration files support environment variable substitution using `${ENV_VAR}` syntax in both string values and environment variable objects:
```json
{
"servers": {
"api-server": {
"transport": "sse",
"url": "https://${API_DOMAIN}/sse",
"headers": {
"Authorization": "Bearer ${API_TOKEN}"
}
},
"local-server": {
"transport": "stdio",
"command": "python",
"args": ["./server.py"],
"env": {
"OPENAI_API_KEY": "${OPENAI_API_KEY}",
"DEBUG_LEVEL": "info"
}
}
}
}
```
### Configuration File Structure
Below is the complete schema for the configuration file:
```json
{
"servers": {
"server-name": {
// For stdio transport (transport field is optional for stdio)
"transport": "stdio", // Optional for stdio, defaults to "stdio" if command and args are present
"command": "python",
"args": ["./server.py"],
"env": {
"ENV_VAR": "value"
},
"encoding": "utf-8",
"encodingErrorHandler": "strict",
"restart": {
"enabled": true,
"maxAttempts": 3,
"delayMs": 1000
},
// For SSE transport (transport field is required)
"transport": "sse",
"url": "http://localhost:8000/sse",
"headers": {
"Authorization": "Bearer token"
},
"useNodeEventSource": true,
"reconnect": {
"enabled": true,
"maxAttempts": 3,
"delayMs": 1000
}
}
}
}
```
> [!NOTE]
> For stdio connections, the `transport` field is optional. If not specified, it defaults to 'stdio' when `command` and `args` are present.
## 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.
+230 -410
View File
@@ -7,110 +7,9 @@ import {
afterEach,
type Mock,
} from "vitest";
// Mock the problematic dependencies using vi.mock
vi.mock("@modelcontextprotocol/sdk/client/sse.js", () => {
// Create mock functions for all methods
const connectMock = vi.fn().mockReturnValue(Promise.resolve());
const sendMock = vi.fn().mockReturnValue(Promise.resolve());
const closeMock = vi.fn().mockReturnValue(Promise.resolve());
import { ZodError } from "zod";
return {
SSEClientTransport: vi.fn().mockImplementation(() => ({
connect: connectMock,
send: sendMock,
close: closeMock,
onclose: null,
})),
};
});
vi.mock("@modelcontextprotocol/sdk/client/stdio.js", () => {
// Create mock functions for all methods
const connectMock = vi.fn().mockReturnValue(Promise.resolve());
const sendMock = vi.fn().mockReturnValue(Promise.resolve());
const closeMock = vi.fn().mockReturnValue(Promise.resolve());
return {
StdioClientTransport: vi.fn().mockImplementation(() => ({
connect: connectMock,
send: sendMock,
close: closeMock,
onclose: null,
})),
};
});
vi.mock("@modelcontextprotocol/sdk/client/index.js", () => {
// Create mock functions for all methods
const connectMock = vi.fn().mockReturnValue(Promise.resolve());
const listToolsMock = vi.fn().mockReturnValue(
Promise.resolve({
tools: [
{
name: "testTool",
description: "A test tool",
inputSchema: {
type: "object",
properties: {
input: { type: "string" },
},
required: ["input"],
},
},
],
})
);
const callToolMock = vi.fn().mockReturnValue(
Promise.resolve({
content: [{ type: "text", text: "result" }],
})
);
const closeMock = vi.fn().mockReturnValue(Promise.resolve());
return {
Client: vi.fn().mockImplementation(() => ({
connect: connectMock,
listTools: listToolsMock,
callTool: callToolMock,
close: closeMock,
})),
};
});
vi.mock("fs", () => ({
readFileSync: vi.fn(),
existsSync: vi.fn().mockImplementation(() => false),
}));
vi.mock("path", async () => ({
resolve: vi.fn(),
join: (await vi.importActual("path")).join,
}));
// Mock the logger
vi.mock("../src/logger.js", () => {
return {
__esModule: true,
default: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
};
});
// Create placeholder mocks that will be replaced in beforeEach
vi.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({
SSEClientTransport: vi.fn(),
}));
vi.mock("@modelcontextprotocol/sdk/client/stdio.js", () => ({
StdioClientTransport: vi.fn(),
}));
vi.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
Client: vi.fn(),
}));
import "./mocks.js";
const { MultiServerMCPClient, MCPClientError } = await import(
"../src/client.js"
@@ -122,121 +21,24 @@ const { StdioClientTransport } = await import(
const { SSEClientTransport } = await import(
"@modelcontextprotocol/sdk/client/sse.js"
);
const fs = await import("fs");
const path = await import("path");
const { StreamableHTTPClientTransport } = await import(
"@modelcontextprotocol/sdk/client/streamableHttp.js"
);
describe("MultiServerMCPClient", () => {
// Create mock implementations that will be used throughout the tests
let mockClientConnect: Mock;
let mockClientListTools: Mock;
let mockClientCallTool: Mock;
let mockClientClose: Mock;
let mockStdioTransportClose: Mock;
let mockStdioTransportConnect: Mock;
let mockStdioTransportSend: Mock;
// Define specific function type for onclose handlers
let mockStdioOnClose: (() => void) | null;
let mockSSETransportClose: Mock;
let mockSSETransportConnect: Mock;
let mockSSETransportSend: Mock;
// Define specific function type for onclose handlers
let mockSSEOnClose: (() => void) | null;
// Setup and teardown
beforeEach(() => {
vi.clearAllMocks();
// Set up mock implementations for Client
mockClientConnect = vi.fn().mockReturnValue(Promise.resolve());
mockClientListTools = vi
.fn()
.mockReturnValue(Promise.resolve({ tools: [] }));
mockClientCallTool = vi
.fn()
.mockReturnValue(
Promise.resolve({ content: [{ type: "text", text: "result" }] })
);
mockClientClose = vi.fn().mockReturnValue(Promise.resolve());
(Client as Mock).mockImplementation(() => ({
connect: mockClientConnect,
listTools: mockClientListTools,
callTool: mockClientCallTool,
close: mockClientClose,
}));
// Set up mock implementations for StdioClientTransport
mockStdioTransportClose = vi.fn().mockReturnValue(Promise.resolve());
mockStdioTransportConnect = vi.fn().mockReturnValue(Promise.resolve());
mockStdioTransportSend = vi.fn().mockReturnValue(Promise.resolve());
mockStdioOnClose = null;
(StdioClientTransport as Mock).mockImplementation(() => {
const transport = {
close: mockStdioTransportClose,
connect: mockStdioTransportConnect,
send: mockStdioTransportSend,
onclose: null as (() => void) | null,
};
// Capture the onclose handler when it's set
Object.defineProperty(transport, "onclose", {
get: () => mockStdioOnClose,
set: (handler: () => void) => {
mockStdioOnClose = handler;
},
});
return transport;
});
// Set up mock implementations for SSEClientTransport
mockSSETransportClose = vi.fn().mockReturnValue(Promise.resolve());
mockSSETransportConnect = vi.fn().mockReturnValue(Promise.resolve());
mockSSETransportSend = vi.fn().mockReturnValue(Promise.resolve());
mockSSEOnClose = null;
(SSEClientTransport as Mock).mockImplementation(() => {
const transport = {
close: mockSSETransportClose,
connect: mockSSETransportConnect,
send: mockSSETransportSend,
onclose: null as (() => void) | null,
};
// Capture the onclose handler when it's set
Object.defineProperty(transport, "onclose", {
get: () => mockSSEOnClose,
set: (handler: () => void) => {
mockSSEOnClose = handler;
},
});
return transport;
});
(fs.readFileSync as Mock).mockImplementation(() =>
JSON.stringify({
servers: {
"test-server": {
transport: "stdio",
command: "python",
args: ["./script.py"],
},
},
})
);
(path.resolve as Mock).mockImplementation((p) => p);
});
afterEach(() => {
vi.resetAllMocks();
vi.clearAllMocks();
});
// 1. Constructor functionality tests
// Constructor functionality tests
describe("constructor", () => {
test("should initialize with empty connections", () => {
const client = new MultiServerMCPClient();
expect(client).toBeDefined();
test("should throw if initialized with empty connections", () => {
expect(() => new MultiServerMCPClient({})).toThrow(MCPClientError);
});
test("should process valid stdio connection config", () => {
@@ -264,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
@@ -273,38 +86,11 @@ describe("MultiServerMCPClient", () => {
transport: "invalid",
},
});
}).toThrow(MCPClientError);
}).toThrow(ZodError);
});
});
// 2. Configuration Loading tests
describe("fromConfigFile", () => {
test("should load config from a valid file", () => {
const client = MultiServerMCPClient.fromConfigFile("./mcp.json");
expect(client).toBeDefined();
expect(fs.readFileSync).toHaveBeenCalledWith("./mcp.json", "utf8");
});
test("should throw error for invalid config file", () => {
(fs.readFileSync as Mock).mockImplementation(() => {
throw new Error("File not found");
});
expect(() => {
MultiServerMCPClient.fromConfigFile("./invalid.json");
}).toThrow(MCPClientError);
});
test("should throw error for invalid JSON in config file", () => {
(fs.readFileSync as Mock).mockImplementation(() => "invalid json");
expect(() => {
MultiServerMCPClient.fromConfigFile("./invalid.json");
}).toThrow(MCPClientError);
});
});
// 3. Connection Management tests
// Connection Management tests
describe("initializeConnections", () => {
test("should initialize stdio connections correctly", async () => {
const client = new MultiServerMCPClient({
@@ -321,11 +107,12 @@ describe("MultiServerMCPClient", () => {
command: "python",
args: ["./script.py"],
env: undefined,
stderr: "inherit",
});
expect(Client).toHaveBeenCalled();
expect(mockClientConnect).toHaveBeenCalled();
expect(mockClientListTools).toHaveBeenCalled();
expect(Client.prototype.connect).toHaveBeenCalled();
expect(Client.prototype.listTools).toHaveBeenCalled();
});
test("should initialize SSE connections correctly", async () => {
@@ -340,12 +127,30 @@ describe("MultiServerMCPClient", () => {
expect(SSEClientTransport).toHaveBeenCalled();
expect(Client).toHaveBeenCalled();
expect(mockClientConnect).toHaveBeenCalled();
expect(mockClientListTools).toHaveBeenCalled();
expect(Client.prototype.connect).toHaveBeenCalled();
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).mockImplementation(() => ({
(Client as Mock).mockImplementationOnce(() => ({
connect: vi
.fn()
.mockReturnValue(Promise.reject(new Error("Connection failed"))),
@@ -366,7 +171,7 @@ describe("MultiServerMCPClient", () => {
});
test("should throw on tool loading failures", async () => {
(Client as Mock).mockImplementation(() => ({
(Client as Mock).mockImplementationOnce(() => ({
connect: vi.fn().mockReturnValue(Promise.resolve()),
listTools: vi
.fn()
@@ -385,87 +190,100 @@ describe("MultiServerMCPClient", () => {
MCPClientError
);
});
});
// 4. Reconnection Logic tests
describe("reconnection", () => {
test("should attempt to reconnect stdio transport when enabled", async () => {
const client = new MultiServerMCPClient({
"test-server": {
transport: "stdio",
command: "python",
args: ["./script.py"],
restart: {
enabled: true,
maxAttempts: 3,
delayMs: 100,
// Reconnection Logic tests
describe("reconnection", () => {
test("should attempt to reconnect stdio transport when enabled", async () => {
const client = new MultiServerMCPClient({
"test-server": {
transport: "stdio",
command: "python",
args: ["./script.py"],
restart: {
enabled: true,
maxAttempts: 3,
delayMs: 100,
},
},
},
});
await client.initializeConnections();
expect(StdioClientTransport).toHaveBeenCalledTimes(1);
// Reset the call counts to focus on reconnection
(StdioClientTransport as Mock).mockClear();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const anyClient = client as any;
const transportInstances = anyClient._transportInstances;
const transportInstance = transportInstances["test-server"];
expect(transportInstance).toBeDefined();
const { onclose } = transportInstance;
expect(onclose).toBeDefined();
onclose();
// Expect a new transport to be created after a delay (for reconnection)
await new Promise((resolve) => {
setTimeout(resolve, 150);
});
// Verify reconnection was attempted by checking if the constructor was called again
expect(StdioClientTransport).toHaveBeenCalledTimes(1);
});
await client.initializeConnections();
// Reset the call counts to focus on reconnection
(StdioClientTransport as Mock).mockClear();
// Trigger the onclose handler if it exists
if (mockStdioOnClose) {
await mockStdioOnClose();
}
// Expect a new transport to be created after a delay (for reconnection)
await new Promise((resolve) => {
setTimeout(resolve, 150);
});
// Verify reconnection was attempted by checking if the constructor was called again
expect(StdioClientTransport).toHaveBeenCalledTimes(1);
});
test("should attempt to reconnect SSE transport when enabled", async () => {
const client = new MultiServerMCPClient({
"test-server": {
transport: "sse",
url: "http://localhost:8000/sse",
reconnect: {
enabled: true,
maxAttempts: 3,
delayMs: 100,
test("should attempt to reconnect SSE transport when enabled", async () => {
const client = new MultiServerMCPClient({
"test-server": {
transport: "sse",
url: "http://localhost:8000/sse",
reconnect: {
enabled: true,
maxAttempts: 3,
delayMs: 100,
},
},
},
});
await client.initializeConnections();
// Reset the call counts to focus on reconnection
expect(SSEClientTransport).toHaveBeenCalledTimes(1);
(SSEClientTransport as Mock).mockClear();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const anyClient = client as any;
const transportInstances = anyClient._transportInstances;
const transportInstance = transportInstances["test-server"];
expect(transportInstance).toBeDefined();
const { onclose } = transportInstance;
expect(onclose).toBeDefined();
onclose();
// Expect a new transport to be created after a delay (for reconnection)
await new Promise((resolve) => {
setTimeout(resolve, 150);
});
// Verify reconnection was attempted by checking if the constructor was called again
expect(SSEClientTransport).toHaveBeenCalledTimes(1);
});
await client.initializeConnections();
// Reset the call counts to focus on reconnection
(SSEClientTransport as Mock).mockClear();
// Trigger the onclose handler if it exists
if (mockSSEOnClose) {
await mockSSEOnClose();
}
// Expect a new transport to be created after a delay (for reconnection)
await new Promise((resolve) => {
setTimeout(resolve, 150);
test("should respect maxAttempts setting for reconnection", async () => {
// For this test, we'll modify the test to be simpler
expect(true).toBe(true);
});
// Verify reconnection was attempted by checking if the constructor was called again
expect(SSEClientTransport).toHaveBeenCalledTimes(1);
});
test("should respect maxAttempts setting for reconnection", async () => {
// For this test, we'll modify the test to be simpler
expect(true).toBe(true);
});
test("should not attempt reconnection when not enabled", async () => {
// For this test, we'll modify the test to be simpler
expect(true).toBe(true);
test("should not attempt reconnection when not enabled", async () => {
// For this test, we'll modify the test to be simpler
expect(true).toBe(true);
});
});
});
// 5. Tool Management tests
// Tool Management tests
describe("getTools", () => {
test("should get all tools as a flattened array", async () => {
// Mock tool response
@@ -474,7 +292,7 @@ describe("MultiServerMCPClient", () => {
{ name: "tool2", description: "Tool 2", inputSchema: {} },
];
(Client as Mock).mockImplementation(() => ({
(Client as Mock).mockImplementationOnce(() => ({
connect: vi.fn().mockReturnValue(Promise.resolve()),
listTools: vi
.fn()
@@ -494,8 +312,7 @@ describe("MultiServerMCPClient", () => {
},
});
await client.initializeConnections();
const tools = client.getTools();
const tools = await client.getTools();
// Expect tools from both servers in a flat array
expect(tools.length).toBeGreaterThan(0);
@@ -510,7 +327,7 @@ describe("MultiServerMCPClient", () => {
});
});
// 6. Cleanup Handling tests
// Cleanup Handling tests
describe("close", () => {
test("should close all connections properly", async () => {
const client = new MultiServerMCPClient({
@@ -523,22 +340,28 @@ describe("MultiServerMCPClient", () => {
transport: "sse",
url: "http://localhost:8000/sse",
},
server3: {
transport: "http",
url: "http://localhost:8000/mcp",
},
});
await client.initializeConnections();
await client.close();
// Verify that all transports were closed using the mock functions directly
expect(mockStdioTransportClose).toHaveBeenCalled();
expect(mockSSETransportClose).toHaveBeenCalled();
expect(StdioClientTransport.prototype.close).toHaveBeenCalled();
expect(SSEClientTransport.prototype.close).toHaveBeenCalled();
expect(StreamableHTTPClientTransport.prototype.close).toHaveBeenCalled();
});
test("should handle errors during cleanup gracefully", async () => {
const closeMock = vi
.fn()
.mockReturnValue(Promise.reject(new Error("Close failed")));
// Mock close to throw an error
(StdioClientTransport as Mock).mockImplementation(() => ({
close: vi
.fn()
.mockReturnValue(Promise.reject(new Error("Close failed"))),
(StdioClientTransport as Mock).mockImplementationOnce(() => ({
close: closeMock,
onclose: null,
}));
@@ -553,111 +376,108 @@ describe("MultiServerMCPClient", () => {
await client.initializeConnections();
await client.close();
// Verify that the client handled the error gracefully
expect(closeMock).toHaveBeenCalledOnce();
});
});
// 7. Specific Connection Method tests
describe("connectToServerViaStdio", () => {
test("should connect to a stdio server correctly", async () => {
const client = new MultiServerMCPClient();
await client.connectToServerViaStdio("test-server", "python", [
"./script.py",
]);
// 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);
});
expect(StdioClientTransport).toHaveBeenCalledWith({
command: "python",
args: ["./script.py"],
env: undefined,
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",
},
});
expect(Client).toHaveBeenCalled();
expect(mockClientConnect).toHaveBeenCalled();
expect(mockClientListTools).toHaveBeenCalled();
});
test("should connect with environment variables", async () => {
const client = new MultiServerMCPClient();
const env = { NODE_ENV: "test" };
await client.connectToServerViaStdio(
"test-server",
"python",
["./script.py"],
env
);
expect(StdioClientTransport).toHaveBeenCalledWith({
command: "python",
args: ["./script.py"],
env,
});
});
test("should connect with restart configuration", async () => {
const client = new MultiServerMCPClient();
const restart = { enabled: true, maxAttempts: 3, delayMs: 100 };
await client.connectToServerViaStdio(
"test-server",
"python",
["./script.py"],
undefined,
restart
);
// Verify that restart configuration was set correctly
});
});
describe("connectToServerViaSSE", () => {
test("should connect to an SSE server correctly", async () => {
const client = new MultiServerMCPClient();
await client.connectToServerViaSSE(
"test-server",
"http://localhost:8000/sse"
);
await client.initializeConnections();
// Verify all transports were initialized
expect(StreamableHTTPClientTransport).toHaveBeenCalled();
expect(SSEClientTransport).toHaveBeenCalled();
expect(Client).toHaveBeenCalled();
expect(mockClientConnect).toHaveBeenCalled();
expect(mockClientListTools).toHaveBeenCalled();
expect(StdioClientTransport).toHaveBeenCalled();
// Get tools from all servers
const tools = await client.getTools();
expect(tools.length).toBeGreaterThan(0);
});
test("should connect with headers", async () => {
const client = new MultiServerMCPClient();
const headers = { Authorization: "Bearer token" };
await client.connectToServerViaSSE(
"test-server",
"http://localhost:8000/sse",
headers
);
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: [] })),
}));
// Verify that headers were set correctly
const client = new MultiServerMCPClient({
"test-server": {
transport: "http",
url: "http://localhost:8000/mcp",
},
});
await expect(() => client.initializeConnections()).rejects.toThrow(
MCPClientError
);
});
test("should connect with useNodeEventSource option", async () => {
const client = new MultiServerMCPClient();
await client.connectToServerViaSSE(
"test-server",
"http://localhost:8000/sse",
undefined,
true
);
test("should handle errors during streamable HTTP cleanup gracefully", async () => {
const closeMock = vi
.fn()
.mockReturnValue(Promise.reject(new Error("Close failed")));
// Verify that useNodeEventSource was set correctly
});
// Mock close to throw an error
(StreamableHTTPClientTransport as Mock).mockImplementationOnce(() => ({
close: closeMock,
connect: vi.fn().mockReturnValue(Promise.resolve()),
}));
test("should connect with reconnect configuration", async () => {
const client = new MultiServerMCPClient();
const reconnect = { enabled: true, maxAttempts: 3, delayMs: 100 };
await client.connectToServerViaSSE(
"test-server",
"http://localhost:8000/sse",
undefined,
undefined,
reconnect
);
const client = new MultiServerMCPClient({
"test-server": {
transport: "http",
url: "http://localhost:8000/mcp",
},
});
// Verify that reconnect configuration was set correctly
await client.initializeConnections();
await client.close();
expect(closeMock).toHaveBeenCalledOnce();
});
});
});
File diff suppressed because it is too large Load Diff
+90
View File
@@ -0,0 +1,90 @@
import { vi } from "vitest";
// Set up mocks for external modules
vi.mock("@modelcontextprotocol/sdk/client/index.js", () => {
const clientPrototype = {
connect: vi.fn().mockReturnValue(Promise.resolve()),
listTools: vi.fn().mockReturnValue(
Promise.resolve({
tools: [
{
name: "tool1",
description: "Test tool 1",
inputSchema: { type: "object", properties: {} },
},
{
name: "tool2",
description: "Test tool 2",
inputSchema: { type: "object", properties: {} },
},
],
})
),
callTool: vi
.fn()
.mockReturnValue(
Promise.resolve({ content: [{ type: "text", text: "result" }] })
),
close: vi.fn().mockImplementation(() => Promise.resolve()),
tools: [], // Add the tools property
};
const Client = vi.fn().mockImplementation(() => clientPrototype);
Client.prototype = clientPrototype;
return {
Client,
};
});
vi.mock("@modelcontextprotocol/sdk/client/stdio.js", () => {
const stdioClientTransportPrototype = {
connect: vi.fn().mockReturnValue(Promise.resolve()),
send: vi.fn().mockReturnValue(Promise.resolve()),
close: vi.fn().mockReturnValue(Promise.resolve()),
};
const StdioClientTransport = vi.fn().mockImplementation((config) => {
return {
...stdioClientTransportPrototype,
config,
};
});
StdioClientTransport.prototype = stdioClientTransportPrototype;
return {
StdioClientTransport,
};
});
vi.mock("@modelcontextprotocol/sdk/client/sse.js", () => {
const sseClientTransportPrototype = {
connect: vi.fn().mockReturnValue(Promise.resolve()),
send: vi.fn().mockReturnValue(Promise.resolve()),
close: vi.fn().mockReturnValue(Promise.resolve()),
};
const SSEClientTransport = vi.fn().mockImplementation((config) => {
return {
...sseClientTransportPrototype,
config,
};
});
SSEClientTransport.prototype = sseClientTransportPrototype;
return {
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,
};
});
+149 -64
View File
@@ -1,16 +1,24 @@
import { describe, test, expect, beforeEach, vi, MockedObject } from 'vitest';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
import type { StructuredTool } from '@langchain/core/tools';
import { describe, test, expect, beforeEach, vi, MockedObject } from "vitest";
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
import {
StructuredTool,
ToolInputParsingException,
} from "@langchain/core/tools";
import type {
EmbeddedResource,
ImageContent,
TextContent,
} from '@modelcontextprotocol/sdk/types.js';
import type { AIMessage, MessageContentComplex, ToolMessage } from '@langchain/core/messages';
const { loadMcpTools } = await import('../src/tools.js');
} from "@modelcontextprotocol/sdk/types.js";
import type {
AIMessage,
MessageContentComplex,
ToolMessage,
} from "@langchain/core/messages";
const { loadMcpTools } = await import("../src/tools.js");
// Create a mock client
describe('Simplified Tool Adapter Tests', () => {
describe("Simplified Tool Adapter Tests", () => {
let mockClient: MockedObject<Client>;
beforeEach(() => {
@@ -22,36 +30,107 @@ describe('Simplified Tool Adapter Tests', () => {
vi.clearAllMocks();
});
describe('loadMcpTools', () => {
test('should load all tools from client', async () => {
describe("loadMcpTools", () => {
test("should load all tools from client", async () => {
// Set up mock response
mockClient.listTools.mockReturnValueOnce(
Promise.resolve({
tools: [
{
name: 'tool1',
description: 'Tool 1',
inputSchema: { type: 'object', properties: {}, required: [] },
name: "tool1",
description: "Tool 1",
inputSchema: { type: "object", properties: {}, required: [] },
},
{
name: 'tool2',
description: 'Tool 2',
inputSchema: { type: 'object', properties: {}, required: [] },
name: "tool2",
description: "Tool 2",
inputSchema: { type: "object", properties: {}, required: [] },
},
],
})
);
// Load tools
const tools = await loadMcpTools('mockServer(should load all tools)', mockClient as Client);
const tools = await loadMcpTools(
"mockServer(should load all tools)",
mockClient as Client
);
// Verify results
expect(tools.length).toBe(2);
expect(tools[0].name).toBe('tool1');
expect(tools[1].name).toBe('tool2');
expect(tools[0].name).toBe("tool1");
expect(tools[1].name).toBe("tool2");
});
test('should handle empty tool list', async () => {
test("should validate tool input against input schema", async () => {
// Set up mock response
mockClient.listTools.mockReturnValueOnce(
Promise.resolve({
tools: [
{
name: "weather",
description: "Get the weather for a given city",
inputSchema: {
type: "object",
properties: {
city: { type: "string" },
},
required: ["city"],
},
},
],
})
);
mockClient.callTool.mockImplementation((params) => {
// should not be called if input is invalid
const args = params.arguments as { city: string };
expect(args.city).toBeDefined();
expect(typeof args.city).toBe("string");
return Promise.resolve({
content: [
{
type: "text",
text: `It is currently 70 degrees and cloudy in ${args.city}.`,
},
],
});
});
// Load tools
const tools = await loadMcpTools(
"mockServer(should validate tool input against input schema)",
mockClient as Client
);
// Verify results
expect(tools.length).toBe(1);
expect(tools[0].name).toBe("weather");
const weatherTool = tools[0];
// should not invoke the tool when input is invalid
await expect(
weatherTool.invoke({ location: "New York" })
).rejects.toThrow(ToolInputParsingException);
expect(mockClient.callTool).not.toHaveBeenCalled();
// should invoke the tool when input is valid
await expect(weatherTool.invoke({ city: "New York" })).resolves.toEqual(
"It is currently 70 degrees and cloudy in New York."
);
expect(mockClient.callTool).toHaveBeenCalledWith({
arguments: {
city: "New York",
},
name: "weather",
});
});
test("should handle empty tool list", async () => {
// Set up mock response
mockClient.listTools.mockReturnValueOnce(
Promise.resolve({
@@ -61,7 +140,7 @@ describe('Simplified Tool Adapter Tests', () => {
// Load tools
const tools = await loadMcpTools(
'mockServer(should handle empty tool list)',
"mockServer(should handle empty tool list)",
mockClient as Client
);
@@ -69,25 +148,25 @@ describe('Simplified Tool Adapter Tests', () => {
expect(tools.length).toBe(0);
});
test('should filter out tools without names', async () => {
test("should filter out tools without names", async () => {
// Set up mock response
mockClient.listTools.mockReturnValueOnce(
// @ts-expect-error - Purposefully dropped name field on one of the tools, should be type error.
Promise.resolve({
tools: [
{
name: 'tool1',
description: 'Tool 1',
inputSchema: { type: 'object', properties: {}, required: [] },
name: "tool1",
description: "Tool 1",
inputSchema: { type: "object", properties: {}, required: [] },
},
{
description: 'No name tool',
inputSchema: { type: 'object', properties: {}, required: [] },
description: "No name tool",
inputSchema: { type: "object", properties: {}, required: [] },
},
{
name: 'tool2',
description: 'Tool 2',
inputSchema: { type: 'object', properties: {}, required: [] },
name: "tool2",
description: "Tool 2",
inputSchema: { type: "object", properties: {}, required: [] },
},
],
})
@@ -95,30 +174,30 @@ describe('Simplified Tool Adapter Tests', () => {
// Load tools
const tools = await loadMcpTools(
'mockServer(should filter out tools without names)',
"mockServer(should filter out tools without names)",
mockClient as Client
);
// Verify results
expect(tools.length).toBe(2);
expect(tools[0].name).toBe('tool1');
expect(tools[1].name).toBe('tool2');
expect(tools[0].name).toBe("tool1");
expect(tools[1].name).toBe("tool2");
});
test('should load tools with specified response format', async () => {
test("should load tools with specified response format", async () => {
// Set up mock response with input schema
mockClient.listTools.mockReturnValueOnce(
Promise.resolve({
tools: [
{
name: 'tool1',
description: 'Tool 1',
name: "tool1",
description: "Tool 1",
inputSchema: {
type: 'object',
type: "object",
properties: {
input: { type: 'string' },
input: { type: "string" },
},
required: ['input'],
required: ["input"],
},
},
],
@@ -127,57 +206,63 @@ describe('Simplified Tool Adapter Tests', () => {
// Load tools with content_and_artifact response format
const tools = await loadMcpTools(
'mockServer(should load tools with specified response format)',
"mockServer(should load tools with specified response format)",
mockClient as Client
);
// Verify tool was loaded
expect(tools.length).toBe(1);
expect((tools[0] as StructuredTool).responseFormat).toBe('content_and_artifact');
expect((tools[0] as StructuredTool).responseFormat).toBe(
"content_and_artifact"
);
// Mock the call result to check response format handling
const mockImageContent: ImageContent = {
type: 'image',
data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', // valid grayscale PNG image
mimeType: 'image/png',
type: "image",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", // valid grayscale PNG image
mimeType: "image/png",
};
const mockTextContent: TextContent = {
type: 'text',
text: 'Here is your image',
type: "text",
text: "Here is your image",
};
const mockEmbeddedResourceContent: EmbeddedResource = {
type: 'resource',
type: "resource",
resource: {
text: 'Here is your image',
uri: 'test-data://test-artifact',
mimeType: 'text/plain',
text: "Here is your image",
uri: "test-data://test-artifact",
mimeType: "text/plain",
},
};
const mockContent = [mockTextContent, mockImageContent, mockEmbeddedResourceContent];
const mockContent = [
mockTextContent,
mockImageContent,
mockEmbeddedResourceContent,
];
const expectedContentBlocks: MessageContentComplex[] = [
{
type: 'text',
text: 'Here is your image',
type: "text",
text: "Here is your image",
},
{
type: 'image_url',
type: "image_url",
image_url: {
url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
},
},
];
const expectedArtifacts = [
{
type: 'resource',
type: "resource",
resource: {
text: 'Here is your image',
uri: 'test-data://test-artifact',
mimeType: 'text/plain',
text: "Here is your image",
uri: "test-data://test-artifact",
mimeType: "text/plain",
},
},
];
@@ -189,16 +274,16 @@ describe('Simplified Tool Adapter Tests', () => {
);
// Invoke the tool with proper input matching the schema
const result = await tools[0].invoke({ input: 'test input' });
const result = await tools[0].invoke({ input: "test input" });
// Verify the result
expect(result).toEqual(expectedContentBlocks);
const toolCall: NonNullable<AIMessage['tool_calls']>[number] = {
args: { input: 'test input' },
name: 'tool1',
id: 'tool_call_id_123',
type: 'tool_call',
const toolCall: NonNullable<AIMessage["tool_calls"]>[number] = {
args: { input: "test input" },
name: "mcp__mockServer(should load tools with specified response format)__tool1",
id: "tool_call_id_123",
type: "tool_call",
};
// call the tool directly via invoke
+16 -50
View File
@@ -1,70 +1,36 @@
# LangChainJS-MCP-Adapters Examples
This directory contains examples demonstrating how to use the LangChainJS-MCP-Adapters library with various MCP servers, with a focus on the Firecrawl MCP server.
This directory contains examples demonstrating how to use the `@langchain/mcp-adapters` library with various MCP servers
## Running the Examples
We've added dedicated npm scripts to make it easy to run the examples:
```bash
# Build all examples
npm run build:examples
yarn build:examples
# Run specific examples
npm run example:default # Uses automatic loading from mcp.json
npm run example:custom # Uses a custom configuration file
npm run example:multiple # Uses multiple MCP servers from a config file
npm run example:mixed # Loads one server from config and another directly in code
npm run example:enhanced # Demonstrates all enhanced configuration features
# Run specific example
cd examples && npx -y tsx firecrawl_custom_config_example.ts
```
## Example Descriptions
### 1. Default Configuration (`firecrawl_default_config_example.ts`)
### Filesystem LangGraph Example (`filesystem_langgraph_example.ts`)
Demonstrates using the Filesystem MCP server with LangGraph to create a structured workflow for complex file operations. The example creates a graph-based agent that can perform various file operations like creating multiple files, reading files, creating directory structures, and organizing files.
Demonstrates the automatic loading of MCP server configurations from the default `mcp.json` file in the root directory. This example showcases how to:
### Firecrawl - Custom Configuration (`firecrawl_custom_config_example.ts`)
Shows how to initialize the Firecrawl MCP server with a custom configuration. The example sets up a connection to Firecrawl using SSE transport, loads tools from the server, and creates a React agent to perform web scraping tasks and find news about artificial intelligence.
- Create a client that automatically loads from `mcp.json`
- Initialize connections to all servers in the configuration
- Filter tools by server name
- Use tools with LangGraph
### Firecrawl - Multiple Servers (`firecrawl_multiple_servers_example.ts`)
Demonstrates how to use multiple MCP servers simultaneously by configuring both Firecrawl for web scraping and a Math server for calculations. The example creates a React agent that can use tools from both servers to answer queries involving both math calculations and web content retrieval.
### 2. Custom Configuration (`firecrawl_custom_config_example.ts`)
### LangGraph - Complex Config (`langgraph_complex_config_example.ts`)
Illustrates using different configuration files to set up connections to MCP servers, with a focus on the Math server. This example shows how to parse JSON configuration files, connect to a Math server directly, and create a LangGraph workflow that can perform mathematical operations using MCP tools.
Shows how to create and use a custom configuration file specifically for the Firecrawl server. This example:
### LangGraph - Simple Config (`langgraph_example.ts`)
Shows a straightforward integration of LangGraph with MCP tools, creating a flexible agent workflow. The example demonstrates how to set up a graph-based structure with separate nodes for LLM reasoning and tool execution, with conditional routing between nodes based on whether tool calls are needed.
- Creates a custom configuration file
- Initializes the client from this file
- Shows environment variable configuration
### 3. Multiple Servers (`firecrawl_multiple_servers_example.ts`)
Demonstrates loading multiple MCP servers (Firecrawl and Math) from a single configuration file. This example:
- Creates a configuration with multiple servers
- Loads and uses tools from different servers in the same agent
### 4. Mixed Loading (`firecrawl_mixed_loading_example.ts`)
Shows a mixed approach to loading MCP servers - one from a configuration file and another directly in code. This example:
- Loads the math server from a config file
- Adds the Firecrawl server via direct code
- Works with tools from both servers
### 5. Enhanced Configuration (`firecrawl_enhanced_config_example.ts`)
Showcases all the enhanced configuration features, including:
- Automatic loading from the default configuration
- Adding multiple configuration sources
- Environment variable substitution
- Adding servers directly in code
## Configuration Files
- `mcp.json` - Default configuration file in the root directory
- Custom configuration files created in the examples
### Launching a Containerized MCP Server (`mcp_over_docker_example.ts`)
Shows how to run an MCP server inside a Docker container. This example configures a connection to a containerized Filesystem MCP server with appropriate volume mounting, demonstrating how to use Docker to isolate and run MCP servers while still allowing file operations.
## Requirements
+2 -2
View File
@@ -11,8 +11,8 @@
},
"math-server": {
"transport": "stdio",
"command": "python",
"args": ["./math_server.py"]
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-math"]
}
}
}
+173
View File
@@ -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);
}
+204
View File
@@ -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));
}
+2 -6
View File
@@ -1,12 +1,8 @@
{
"servers": {
"math": {
"command": "python",
"args": ["./examples/math_server.py"],
"env": {
"DEBUG": "true",
"PYTHONPATH": "./examples"
}
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-math"],
},
"weather": {
"transport": "sse",
+1 -3
View File
@@ -59,12 +59,10 @@ export async function runExample(client?: MultiServerMCPClient) {
},
});
// Initialize connections to the server
await client.initializeConnections();
console.log("Connected to server");
// Get all tools (flattened array is the default now)
const mcpTools = client.getTools();
const mcpTools = await client.getTools();
if (mcpTools.length === 0) {
throw new Error("No tools found");
+13 -46
View File
@@ -9,44 +9,26 @@
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage } from "@langchain/core/messages";
import dotenv from "dotenv";
import * as fs from "fs";
import * as path from "path";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
// MCP client imports
import { MultiServerMCPClient } from "../src/index.js";
import { Connection, MultiServerMCPClient } from "../src/index.js";
// Load environment variables from .env file
dotenv.config();
/**
* Create a custom configuration file for Firecrawl
* A custom configuration for Firecrawl
*/
function createConfigFile(): string {
const configPath = path.join(
process.cwd(),
"examples",
"firecrawl_config.json"
);
// Configuration for the Firecrawl server
const config = {
servers: {
firecrawl: {
transport: "sse",
url: process.env.FIRECRAWL_SERVER_URL || "http://localhost:8000/v1/mcp",
headers: {
Authorization: `Bearer ${process.env.FIRECRAWL_API_KEY || "demo"}`,
},
},
const config: Record<string, Connection> = {
firecrawl: {
transport: "sse",
url: process.env.FIRECRAWL_SERVER_URL || "http://localhost:8000/v1/mcp",
headers: {
Authorization: `Bearer ${process.env.FIRECRAWL_API_KEY || "demo"}`,
},
};
// Write the configuration to a file
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
return configPath;
}
},
};
/**
* Example demonstrating loading from custom configuration
@@ -61,23 +43,12 @@ async function runExample() {
}, 30000);
try {
// Create a custom configuration file
const configPath = createConfigFile();
console.log(`Created custom configuration file at: ${configPath}`);
// Initialize the MCP client with the custom configuration
console.log("Initializing MCP client from custom configuration file...");
client = MultiServerMCPClient.fromConfigFile(configPath);
// Connect to the servers
await client.initializeConnections();
console.log("Connected to servers from custom configuration");
console.log("Initializing MCP client from custom configuration...");
client = new MultiServerMCPClient(config);
// Get Firecrawl tools specifically
const mcpTools = client.getTools();
const firecrawlTools = mcpTools.filter(
(tool) => client!.getServerForTool(tool.name) === "firecrawl"
);
const firecrawlTools = await client.getTools("firecrawl");
if (firecrawlTools.length === 0) {
throw new Error("No Firecrawl tools found");
@@ -114,10 +85,6 @@ async function runExample() {
// Clear the timeout since the example completed successfully
clearTimeout(timeout);
// Clean up the temporary configuration file
fs.unlinkSync(configPath);
console.log("Removed temporary configuration file");
} catch (error) {
console.error("Error in example:", error);
} finally {
@@ -1,101 +0,0 @@
/**
* Firecrawl MCP Server Example - Default Configuration
*
* This example demonstrates loading from default configuration file (mcp.json)
* And getting tools from the Firecrawl server with automatic initialization
*/
/* eslint-disable no-console */
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage } from "@langchain/core/messages";
import dotenv from "dotenv";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
// MCP client imports
import { MultiServerMCPClient } from "../src/index.js";
// Load environment variables from .env file
dotenv.config();
/**
* Example demonstrating loading from default configuration
*/
async function runExample() {
let client: MultiServerMCPClient | null = null;
// Add a timeout to prevent the process from hanging indefinitely
const timeout = setTimeout(() => {
console.error("Example timed out after 30 seconds");
process.exit(1);
}, 30000);
try {
console.log("Initializing MCP client from default configuration file...");
// The client will automatically look for and load mcp.json from the current directory
client = new MultiServerMCPClient();
await client.initializeConnections();
console.log("Connected to servers from default configuration");
// Get Firecrawl tools specifically
const mcpTools = client.getTools();
const firecrawlTools = mcpTools.filter(
(tool) => client!.getServerForTool(tool.name) === "firecrawl"
);
if (firecrawlTools.length === 0) {
throw new Error("No Firecrawl tools found");
}
console.log(`Found ${firecrawlTools.length} Firecrawl tools`);
// Initialize the LLM
const model = new ChatOpenAI({
modelName: process.env.OPENAI_MODEL_NAME || "gpt-3.5-turbo",
temperature: 0,
});
// Create a React agent using LangGraph's createReactAgent
const agent = createReactAgent({
llm: model,
tools: firecrawlTools,
});
// Define a query for testing Firecrawl
const query =
"Scrape the content from https://example.com and summarize it in bullet points";
console.log(`Running agent with query: ${query}`);
// Run the agent
const result = await agent.invoke({
messages: [new HumanMessage(query)],
});
console.log("Agent execution completed");
console.log("\nFinal output:");
console.log(result);
// Clear the timeout since the example completed successfully
clearTimeout(timeout);
} catch (error) {
console.log("Error in example:", error);
} finally {
// Close all MCP connections
if (client) {
console.log("Closing all MCP connections...");
await client.close();
console.log("All MCP connections closed");
}
// Clear the timeout if it hasn't fired yet
clearTimeout(timeout);
// Complete the example
console.log("Example execution completed");
process.exit(0);
}
}
// Run the example
runExample().catch(console.error);
@@ -1,255 +0,0 @@
/**
* Firecrawl MCP Server Example - Enhanced Configuration Loading
*
* This example demonstrates the enhanced configuration loading capabilities:
* 1. Automatic loading from default mcp.json
* 2. Adding configurations from multiple sources
* 3. Environment variable substitution
*/
/* 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 } from "@langchain/core/messages";
import dotenv from "dotenv";
import fs from "fs";
import path from "path";
// MCP client imports
import { MultiServerMCPClient } from "../src/index.js";
// Load environment variables from .env file
dotenv.config();
// Path for an additional configuration file
const additionalConfigPath = path.join(
process.cwd(),
"examples",
"additional_servers.json"
);
/**
* Create an additional configuration file with extra servers
*/
function createAdditionalConfigFile() {
const configContent = {
servers: {
// Example of another server that could be used
"custom-server": {
transport: "stdio",
command: "python",
args: [path.join(process.cwd(), "examples", "weather_server.py")],
},
},
};
fs.writeFileSync(
additionalConfigPath,
JSON.stringify(configContent, null, 2)
);
console.log(
`Created additional configuration file at ${additionalConfigPath}`
);
}
/**
* Example demonstrating the enhanced configuration loading features
*/
async function runExample() {
let client: MultiServerMCPClient | null = null;
// Add a timeout to prevent the process from hanging indefinitely
const timeout = setTimeout(() => {
console.error("Example timed out after 30 seconds");
process.exit(1);
}, 30000);
try {
// Create the additional configuration file
createAdditionalConfigFile();
console.log(
"Initializing MCP client with enhanced configuration loading..."
);
// Create a client - it will automatically load from mcp.json if it exists
client = new MultiServerMCPClient();
// Add configuration from another file
client.addConfigFromFile(additionalConfigPath);
// Add an additional server configuration directly
client.addConnections({
// Direct configuration for an additional server
"inline-server": {
transport: "stdio",
command: "node",
args: ["-e", 'console.log("This is an inline server example");'],
},
});
// Initialize all connections from the merged configurations
await client.initializeConnections();
console.log("Connected to servers from all configurations");
// Get all tools from all servers
const mcpTools = client.getTools();
if (mcpTools.length === 0) {
throw new Error("No tools found");
}
console.log(`Loaded ${mcpTools.length} MCP tools in total`);
// Filter tools from different servers
const firecrawlTools = mcpTools.filter(
(tool) => client!.getServerForTool(tool.name) === "firecrawl"
);
if (firecrawlTools.length === 0) {
console.log("No Firecrawl tools found, using math tools for the example");
// In this case, use math tools as a fallback
const mathTools = mcpTools.filter(
(tool) => client!.getServerForTool(tool.name) === "math"
);
if (mathTools.length > 0) {
console.log(
`Using ${mathTools.length} math tools: ${mathTools
.map((tool) => tool.name)
.join(", ")}`
);
// Run a simple math example and exit
console.log("\n=== MATH TOOLS EXAMPLE ===");
console.log("Math example completed successfully");
return;
} else {
throw new Error("No suitable tools found for the example");
}
}
console.log(
`Loaded ${firecrawlTools.length} Firecrawl tools: ${firecrawlTools
.map((tool) => tool.name)
.join(", ")}`
);
// Create an OpenAI model and bind just the Firecrawl tools for this example
const model = new ChatOpenAI({
modelName: process.env.OPENAI_MODEL_NAME || "gpt-4o",
temperature: 0,
}).bindTools(firecrawlTools);
// Create a tool node for the LangGraph
const toolNode = new ToolNode(firecrawlTools);
// ================================================
// 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 messages:", state.messages.length);
const response = await model.invoke(state.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];
const aiMessage = lastMessage as AIMessage;
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) {
console.log("Tool calls detected, routing to tools node");
return "tools";
}
console.log("No tool calls, ending the workflow");
return END;
});
// Compile the graph
const app = workflow.compile();
// Define a query for testing Firecrawl
const query =
'Search the web for information about "programming in TypeScript" and give me a summary of the top results';
// Test the LangGraph agent with the query
console.log("\n=== RUNNING LANGGRAPH AGENT ===");
console.log(`\nQuery: ${query}`);
try {
// Set a timeout for the langgraph invocation
const langgraphPromise = app.invoke({
messages: [new HumanMessage(query)],
});
// Run with a 20-second timeout
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(
() =>
reject(new Error("LangGraph execution timed out after 20 seconds")),
20000
);
});
// Race between the LangGraph execution and the timeout
const result = await Promise.race([langgraphPromise, timeoutPromise]);
// Display the final response
const finalMessage = result.messages[result.messages.length - 1];
console.log(`\nResult: ${finalMessage.content}`);
} catch (error) {
console.error("LangGraph execution error:", error);
console.log("Continuing with cleanup...");
}
} catch (error) {
console.error("Error:", error);
process.exit(1); // Exit with error code
} finally {
// Clear the global timeout
clearTimeout(timeout);
// Close all client connections
if (client) {
await client.close();
console.log("\nClosed all connections");
}
// Clean up our additional config file
if (fs.existsSync(additionalConfigPath)) {
fs.unlinkSync(additionalConfigPath);
console.log(
`Cleaned up additional configuration file at ${additionalConfigPath}`
);
}
// Exit process after a short delay to allow for cleanup
setTimeout(() => {
console.log("Example completed, exiting process.");
process.exit(0);
}, 500);
}
}
// Run the example
runExample().catch(console.error);
-223
View File
@@ -1,223 +0,0 @@
/**
* Mixed Loading MCP Servers Example - Firecrawl and Math
*
* This example demonstrates a mixed approach to loading MCP servers:
* 1. Loading the math server from a configuration file
* 2. Adding the firecrawl server directly in code
*/
/* 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, BaseMessage } from "@langchain/core/messages";
import dotenv from "dotenv";
import fs from "fs";
import path from "path";
// MCP client imports
import { MultiServerMCPClient } from "../src/index.js";
// Load environment variables from .env file
dotenv.config();
// Path for our partial config file (only containing math server)
const partialConfigPath = path.join(
process.cwd(),
"examples",
"math_server_config.json"
);
/**
* Create a configuration file for just the math server
*/
function createMathServerConfigFile() {
const configContent = {
servers: {
math: {
transport: "stdio",
command: "python",
args: [path.join(process.cwd(), "examples", "math_server.py")],
},
},
};
fs.writeFileSync(partialConfigPath, JSON.stringify(configContent, null, 2));
console.log(`Created math server configuration file at ${partialConfigPath}`);
}
/**
* Example demonstrating how to use a mixed approach to loading MCP servers
* This example loads one server from config and adds another directly in code
*/
async function runExample() {
let client: MultiServerMCPClient | null = null;
try {
// Create the math server configuration file
createMathServerConfigFile();
console.log(
"Initializing MCP client from math server configuration file..."
);
// Create a client from the configuration file
client = MultiServerMCPClient.fromConfigFile(partialConfigPath);
// Initialize connections to the math server
await client.initializeConnections();
console.log("Connected to math server from configuration");
// Now add the firecrawl server directly in code
console.log("Adding firecrawl server directly in code...");
await client.connectToServerViaStdio(
"firecrawl",
"npx",
["-y", "firecrawl-mcp"],
{
// Adding the API key from environment
FIRECRAWL_API_KEY: process.env.FIRECRAWL_API_KEY || "",
// Optional configurations
FIRECRAWL_RETRY_MAX_ATTEMPTS: "3",
}
);
console.log("Connected to firecrawl server directly");
// Get all tools from all servers
const mcpTools = client.getTools();
if (mcpTools.length === 0) {
throw new Error("No tools found");
}
// Filter tools from different servers
const mathTools = mcpTools.filter(
(tool) => client!.getServerForTool(tool.name) === "math"
);
const firecrawlTools = mcpTools.filter(
(tool) => client!.getServerForTool(tool.name) === "firecrawl"
);
console.log(
`Loaded ${mathTools.length} math tools: ${mathTools
.map((tool) => tool.name)
.join(", ")}`
);
console.log(
`Loaded ${firecrawlTools.length} firecrawl tools: ${firecrawlTools
.map((tool) => tool.name)
.join(", ")}`
);
console.log(`Loaded ${mcpTools.length} tools in total`);
// Create an OpenAI model and bind the tools
const model = new ChatOpenAI({
modelName: process.env.OPENAI_MODEL_NAME || "gpt-4o",
temperature: 0,
}).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 messages:", state.messages.length);
const response = await model.invoke(state.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];
const aiMessage = lastMessage as AIMessage;
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) {
console.log("Tool calls detected, routing to tools node");
return "tools";
}
console.log("No tool calls, ending the workflow");
return END;
});
// Compile the graph
const app = workflow.compile();
// Define a query that will require both servers
const query =
"First, scrape https://example.com and count how many paragraphs are there. Then, multiply that number by 5.";
// Test the LangGraph agent with the query
console.log("\n=== RUNNING LANGGRAPH AGENT ===");
console.log(`\nQuery: ${query}`);
// Run the LangGraph agent with the query
const result = await app.invoke({
messages: [new HumanMessage(query)],
});
// Display the full conversation
console.log(`\nFinal Messages (${result.messages.length}):`);
result.messages.forEach((msg: BaseMessage, i: number) => {
const msgType = "type" in msg ? msg.type : "unknown";
console.log(
`[${i}] ${msgType}: ${
typeof msg.content === "string"
? msg.content
: JSON.stringify(msg.content)
}`
);
});
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 {
// Close all client connections
if (client) {
await client.close();
console.log("\nClosed all connections");
}
// Clean up our config file
if (fs.existsSync(partialConfigPath)) {
fs.unlinkSync(partialConfigPath);
console.log(
`Cleaned up math server configuration file at ${partialConfigPath}`
);
}
// Exit process after a short delay to allow for cleanup
setTimeout(() => {
console.log("Example completed, exiting process.");
process.exit(0);
}, 500);
}
}
// Run the example
runExample().catch(console.error);
+21 -63
View File
@@ -10,55 +10,32 @@ import { ChatOpenAI } from "@langchain/openai";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { HumanMessage } from "@langchain/core/messages";
import dotenv from "dotenv";
import fs from "fs";
import path from "path";
// MCP client imports
import { MultiServerMCPClient } from "../src/index.js";
import { Connection, MultiServerMCPClient } from "../src/index.js";
// Load environment variables from .env file
dotenv.config();
// Path for our multiple servers config file
const multipleServersConfigPath = path.join(
process.cwd(),
"examples",
"multiple_servers_config.json"
);
/**
* Create a configuration file for multiple MCP servers
* Configuration for multiple MCP servers
*/
function createMultipleServersConfigFile() {
const configContent = {
servers: {
// Firecrawl server configuration
firecrawl: {
transport: "stdio",
command: "npx",
args: ["-y", "firecrawl-mcp"],
env: {
FIRECRAWL_API_KEY: process.env.FIRECRAWL_API_KEY || "",
FIRECRAWL_RETRY_MAX_ATTEMPTS: "3",
},
},
// Math server configuration
math: {
transport: "stdio",
command: "python",
args: [path.join(process.cwd(), "examples", "math_server.py")],
},
const multipleServersConfig: Record<string, Connection> = {
firecrawl: {
transport: "stdio",
command: "npx",
args: ["-y", "firecrawl-mcp"],
env: {
FIRECRAWL_API_KEY: process.env.FIRECRAWL_API_KEY || "",
FIRECRAWL_RETRY_MAX_ATTEMPTS: "3",
},
};
fs.writeFileSync(
multipleServersConfigPath,
JSON.stringify(configContent, null, 2)
);
console.log(
`Created multiple servers configuration file at ${multipleServersConfigPath}`
);
}
},
// Math server configuration
math: {
transport: "stdio",
command: "npx",
args: ["-y", "@modelcontextprotocol/server-math"],
},
};
/**
* Example demonstrating how to use multiple MCP servers with React agent
@@ -68,22 +45,17 @@ async function runExample() {
let client: MultiServerMCPClient | null = null;
try {
// Create the multiple servers configuration file
createMultipleServersConfigFile();
console.log(
"Initializing MCP client from multiple servers configuration file..."
"Initializing MCP client from multiple servers configuration..."
);
// Create a client from the configuration file
client = MultiServerMCPClient.fromConfigFile(multipleServersConfigPath);
client = new MultiServerMCPClient(multipleServersConfig);
// Initialize connections to all servers in the configuration
await client.initializeConnections();
console.log("Connected to servers from multiple servers configuration");
// Get all tools from all servers
const mcpTools = client.getTools();
const mcpTools = await client.getTools();
if (mcpTools.length === 0) {
throw new Error("No tools found");
@@ -143,20 +115,6 @@ async function runExample() {
await client.close();
console.log("\nClosed all connections");
}
// Clean up our config file
if (fs.existsSync(multipleServersConfigPath)) {
fs.unlinkSync(multipleServersConfigPath);
console.log(
`Cleaned up multiple servers configuration file at ${multipleServersConfigPath}`
);
}
// Exit process after a short delay to allow for cleanup
setTimeout(() => {
console.log("Example completed, exiting process.");
process.exit(0);
}, 500);
}
}
@@ -95,23 +95,17 @@ async function runConfigTest() {
// Step 3: Connect directly to the math server using explicit path
console.log("Connecting to math server directly...");
// Define the python executable (use 'python3' on systems where 'python' might not be in PATH)
const pythonCmd = process.platform === "win32" ? "python" : "python3";
// Create a client with the math server only
const client = new MultiServerMCPClient({
math: {
transport: "stdio",
command: pythonCmd,
args: [path.join(process.cwd(), "examples", "math_server.py")],
command: "npx",
args: ["-y", "@modelcontextprotocol/server-math"],
},
});
// Initialize connection to the math server
await client.initializeConnections();
// Get tools from the math server
const mcpTools = client.getTools();
const mcpTools = await client.getTools();
console.log(`Loaded ${mcpTools.length} tools from math server`);
// Log the names of available tools
+10 -18
View File
@@ -10,7 +10,7 @@
* - Built-in persistence capabilities
*
* In this example, we:
* 1. Set up an MCP client to connect to a Python-based math server
* 1. Set up an MCP client to connect to the MCP everything server reference example
* 2. Create a LangGraph workflow with two nodes: one for the LLM and one for tools
* 3. Define the edges and conditional routing between the nodes
* 4. Execute the workflow with example queries
@@ -42,7 +42,7 @@ dotenv.config();
/**
* Example demonstrating how to use MCP tools with LangGraph agent flows
* This example connects to a math server and uses its tools
* This example connects to a everything server and uses its tools
*/
async function runExample() {
let client: MultiServerMCPClient | null = null;
@@ -50,27 +50,17 @@ async function runExample() {
try {
console.log("Initializing MCP client...");
// Create a client with configurations for the math server only
// Create a client with configurations for the everything server only
client = new MultiServerMCPClient({
math: {
everything: {
transport: "stdio",
command: "python",
args: ["./examples/math_server.py"],
command: "npx",
args: ["-y", "@modelcontextprotocol/server-everything"],
},
});
// Initialize connections to the server
await client.initializeConnections();
console.log("Connected to server");
// Connect to the math server
await client.connectToServerViaStdio("math", "npx", [
"-y",
"@modelcontextprotocol/server-math",
]);
// Get the tools (flattened array is the default now)
const mcpTools = client.getTools();
const mcpTools = await client.getTools();
if (mcpTools.length === 0) {
throw new Error("No tools found");
@@ -147,7 +137,9 @@ async function runExample() {
const app = workflow.compile();
// Define queries for testing
const queries = ["What is 5 + 3?", "What is 7 * 9?"];
const queries = [
"If Sally has 420324 apples and mark steals 7824 of them, how many does she have left?",
];
// Test the LangGraph agent with the queries
console.log("\n=== RUNNING LANGGRAPH AGENT ===");
-22
View File
@@ -1,22 +0,0 @@
#!/usr/bin/env python
from mcp.server.fastmcp import FastMCP
# Create a server
mcp = FastMCP(name="Math")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two integers and return the result."""
return a + b
@mcp.tool()
def multiply(a: int, b: int) -> int:
"""Multiply two integers and return the result."""
return a * b
# Run the server
if __name__ == "__main__":
mcp.run(transport="stdio")
-82
View File
@@ -1,82 +0,0 @@
#!/usr/bin/env python
from mcp.server.fastmcp import FastMCP
import sys
# Create a server
mcp = FastMCP(name="Weather")
@mcp.tool()
def get_temperature(city: str) -> str:
"""Get the current temperature for a city.
This is a mock implementation that returns fake data.
In a real application, this would call a weather API.
"""
# Mock implementation - in a real app, this would call a weather API
temperatures = {
"new york": "72°F",
"london": "65°F",
"tokyo": "25 degrees Celsius",
"paris": "70°F",
"sydney": "80°F",
}
city_lower = city.lower()
if city_lower in temperatures:
return f"The current temperature in {city} is {temperatures[city_lower]}."
else:
return f"Temperature data not available for {city}."
@mcp.tool()
def get_forecast(city: str, days: int = 3) -> str:
"""Get the weather forecast for a city.
Args:
city: The name of the city to get the forecast for.
days: The number of days to forecast (default: 3).
Returns:
A string containing the weather forecast.
"""
# Mock implementation - in a real app, this would call a weather API
forecasts = {
"new york": "Sunny with a chance of rain",
"london": "Cloudy with occasional showers",
"tokyo": "Clear skies",
"paris": "Partly cloudy",
"sydney": "Warm and sunny",
}
city_lower = city.lower()
if city_lower in forecasts:
return f"The {days}-day forecast for {city} is: {forecasts[city_lower]}."
else:
return f"Forecast data not available for {city}."
# Run the server
if __name__ == "__main__":
# Set the port using command line arguments
# The FastMCP server will read these arguments
if len(sys.argv) == 1: # No arguments provided
# Add command line arguments for the port
sys.argv.extend(["--sse-port", "8000"])
port = 8000
else:
# Check if --sse-port is already in the arguments
if "--sse-port" in sys.argv:
port_index = sys.argv.index("--sse-port") + 1
if port_index < len(sys.argv):
port = int(sys.argv[port_index])
else:
port = 8000
else:
# Add the port argument
sys.argv.extend(["--sse-port", "8000"])
port = 8000
# Run with SSE transport
print(f"Starting weather server with SSE transport on port {port}...")
mcp.run(transport="sse")
+1
View File
@@ -18,5 +18,6 @@ export const config = {
tsConfigPath: resolve("./tsconfig.json"),
cjsSource: "./dist-cjs",
cjsDestination: "./dist",
additionalGitignorePaths: [".env", ".eslintcache"],
abs,
};
+20 -20
View File
@@ -1,6 +1,6 @@
{
"name": "@langchain/mcp-adapters",
"version": "0.3.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",
@@ -19,15 +19,13 @@
"build:main": "yarn lc_build --create-entrypoints --pre --tree-shaking",
"build:examples": "tsc -p tsconfig.examples.json",
"clean": "rm -rf dist/ dist-cjs/ .turbo/",
"format": "prettier --config .prettierrc --write \"src/**/*.ts\" \"examples/**/*.ts\"",
"format:check": "prettier --config .prettierrc --check \"src\" \"examples/**/*.ts\"",
"lint": "yarn lint:eslint && yarn lint:dpdm",
"lint:dpdm": "dpdm --exit-code circular:1 --no-warning --no-tree src/**/*.ts examples/**/*.ts",
"lint:eslint": "NODE_OPTIONS=--max-old-space-size=4096 eslint --cache --ext .ts,.js src/ examples/",
"lint:fix": "yarn lint:eslint --fix && yarn lint:dpdm",
"prepack": "pinst --disable && yarn build",
"postinstall": "husky",
"postpack": "pinst --enable",
"format": "prettier --config .prettierrc --write \"src/**/*.ts\" \"examples/**/*.ts\"",
"format:check": "prettier --config .prettierrc --check \"src\" \"examples/**/*.ts\"",
"prepack": "yarn build",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest"
@@ -48,28 +46,29 @@
"author": "Ravi Kiran Vemula",
"license": "MIT",
"dependencies": {
"@dmitryrechkin/json-schema-to-zod": "^1.0.1",
"@modelcontextprotocol/sdk": "^1.7.0",
"debug": "^4.4.0"
"@modelcontextprotocol/sdk": "^1.11.2",
"debug": "^4.4.0",
"zod": "^3.24.2"
},
"peerDependencies": {
"@langchain/core": "^0.3.40"
"@langchain/core": "^0.3.44"
},
"optionalDependencies": {
"extended-eventsource": "^1.x"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@langchain/core": "^0.3.40",
"@langchain/langgraph": "^0.2.56",
"@langchain/openai": "^0.4.4",
"@langchain/core": "^0.3.44",
"@langchain/langgraph": "^0.2.62",
"@langchain/openai": "^0.5.5",
"@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",
"@vitest/coverage-v8": "^3.0.9",
"@vitest/coverage-v8": "^3.1.1",
"dotenv": "^16.4.7",
"dpdm": "^3.12.0",
"eslint": "^8.33.0",
@@ -79,21 +78,22 @@
"eslint-plugin-no-instanceof": "^1.0.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vitest": "^0.5.4",
"eventsource": "^3.0.5",
"eventsource": "^3.0.6",
"express": "^5.1.0",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"npm-run-all": "^4.1.5",
"pinst": "^3.0.0",
"prettier": "^2.8.3",
"release-it": "^17.6.0",
"rollup": "^4.5.2",
"rollup": "^4.39.0",
"ts-node": "^10.9.2",
"typescript": "^4.9.5 || ^5.4.5",
"typescript-eslint": "^8.26.0",
"vitest": "^3.0.9"
"typescript-eslint": "^8.29.0",
"vitest": "^3.1.1"
},
"resolutions": {
"typescript": "4.9.5"
"typescript": "4.9.5",
"uuid": "^11.0.0"
},
"engines": {
"node": ">=18"
+687 -824
View File
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -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";
+87 -39
View File
@@ -18,8 +18,6 @@ import {
MessageContentImageUrl,
MessageContentText,
} from "@langchain/core/messages";
import { JSONSchema, JSONSchemaToZod } from "@dmitryrechkin/json-schema-to-zod";
import debug from "debug";
// Replace direct initialization with lazy initialization
@@ -97,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) {
@@ -136,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)
@@ -191,6 +188,44 @@ async function _callTool(
return _convertCallToolResult(serverName, toolName, result, client);
}
export type LoadMcpToolsOptions = {
/**
* If true, throw an error if a tool fails to load.
*
* @default true
*/
throwOnLoadError?: boolean;
/**
* If true, the tool name will be prefixed with the server name followed by a double underscore.
* This is useful if you want to avoid tool name collisions across servers.
*
* @default false
*/
prefixToolNameWithServerName?: boolean;
/**
* An additional prefix to add to the tool name. Will be added at the very beginning of the tool
* name, separated by a double underscore.
*
* For example, if `additionalToolNamePrefix` is `"mcp"`, and `prefixToolNameWithServerName` is
* `true`, the tool name `"my-tool"` provided by server `"my-server"` will become
* `"mcp__my-server__my-tool"`.
*
* Similarly, if `additionalToolNamePrefix` is `mcp` and `prefixToolNameWithServerName` is false,
* the tool name would be `"mcp__my-tool"`.
*
* @default ""
*/
additionalToolNamePrefix?: string;
};
const defaultLoadMcpToolsOptions: LoadMcpToolsOptions = {
throwOnLoadError: true,
prefixToolNameWithServerName: false,
additionalToolNamePrefix: "",
};
/**
* Load all tools from an MCP client.
*
@@ -201,43 +236,56 @@ async function _callTool(
export async function loadMcpTools(
serverName: string,
client: Client,
throwOnLoadError: boolean = true
options?: LoadMcpToolsOptions
): Promise<StructuredToolInterface[]> {
const {
throwOnLoadError,
prefixToolNameWithServerName,
additionalToolNamePrefix,
} = {
...defaultLoadMcpToolsOptions,
...(options ?? {}),
};
// Get tools in a single operation
const toolsResponse = await client.listTools();
getDebugLog()(`INFO: Found ${toolsResponse.tools?.length || 0} MCP tools`);
const initialPrefix = additionalToolNamePrefix
? `${additionalToolNamePrefix}__`
: "";
const serverPrefix = prefixToolNameWithServerName ? `${serverName}__` : "";
const toolNamePrefix = `${initialPrefix}${serverPrefix}`;
// Filter out tools without names and convert in a single map operation
return (toolsResponse.tools || [])
.filter((tool: MCPTool) => !!tool.name)
.map((tool: MCPTool) => {
try {
const dst = new DynamicStructuredTool({
name: tool.name,
description: tool.description || "",
schema: JSONSchemaToZod.convert(
(tool.inputSchema ?? {
type: "object",
properties: {},
}) as JSONSchema
),
responseFormat: "content_and_artifact",
func: _callTool.bind(
null,
serverName,
tool.name,
client
) as DynamicStructuredToolInput["func"],
});
getDebugLog()(`INFO: Successfully loaded tool: ${dst.name}`);
return dst;
} catch (error) {
getDebugLog()(`ERROR: Failed to load tool "${tool.name}":`, error);
if (throwOnLoadError) {
throw error;
}
return null;
}
})
.filter(Boolean) as StructuredToolInterface[];
return (
await Promise.all(
(toolsResponse.tools || [])
.filter((tool: MCPTool) => !!tool.name)
.map(async (tool: MCPTool) => {
try {
const dst = new DynamicStructuredTool({
name: `${toolNamePrefix}${tool.name}`,
description: tool.description || "",
schema: tool.inputSchema,
responseFormat: "content_and_artifact",
func: _callTool.bind(
null,
serverName,
tool.name,
client
) as DynamicStructuredToolInput["func"],
});
getDebugLog()(`INFO: Successfully loaded tool: ${dst.name}`);
return dst;
} catch (error) {
getDebugLog()(`ERROR: Failed to load tool "${tool.name}":`, error);
if (throwOnLoadError) {
throw error;
}
return null;
}
})
)
).filter(Boolean) as StructuredToolInterface[];
}
+2 -2
View File
@@ -19,6 +19,6 @@
"strict": true,
"noEmit": true
},
"include": ["src/**/*", "__tests__/**/*"],
"exclude": ["node_modules", "dist", "docs"]
"include": ["src/**/*.ts", "__tests__/**/*.ts"],
"exclude": ["node_modules", "dist", "docs", "coverage"]
}
+721 -662
View File
File diff suppressed because it is too large Load Diff