break: simplify interface, drop file IO, accept common mcp server config (#38)

This commit is contained in:
Ben Burns
2025-04-03 21:56:54 +13:00
committed by GitHub
parent b4313142e0
commit 09f511a57d
26 changed files with 1559 additions and 3758 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,
-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"
}
+44 -347
View File
@@ -3,7 +3,7 @@
[![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 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).
## Features
@@ -25,6 +25,7 @@ This library provides a lightweight wrapper that makes[Anthropic Model Context P
- Optimized for OpenAI, Anthropic, and Google models
- 🛠️ **Development Features**
- Uses `debug` package for debug logging
- Flexible configuration options
- Robust error handling
@@ -48,16 +49,9 @@ For enhanced SSE header support:
npm install extended-eventsource
```
## Prerequisites
# Example: Manage the MCP Client yourself
- 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.
This example shows how you can manage your own MCP client and use it to get tools that you can pass to a LangGraph prebuilt ReAcT agent.
```bash
npm install @langchain/mcp-adapters @langchain/langgraph @langchain/core @langchain/openai
@@ -65,52 +59,28 @@ npm install @langchain/mcp-adapters @langchain/langgraph @langchain/core @langch
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
## 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';
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' });
const model = new ChatOpenAI({ modelName: "gpt-4" });
// Create transport for stdio connection
// Automatically starts and connects to a MCP reference server
const transport = new StdioClientTransport({
command: 'python',
args: ['math_server.py'],
command: "npx",
args: ["-y", "@modelcontextprotocol/server-math"],
});
// Initialize the client
const client = new Client({
name: 'math-client',
version: '1.0.0',
name: "math-client",
version: "1.0.0",
});
try {
@@ -123,7 +93,7 @@ try {
// 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?" }],
messages: [{ role: "user", content: "what's (3 + 5) x 12?" }],
});
console.log(agentResponse);
} catch (e) {
@@ -134,59 +104,40 @@ try {
}
```
## Multiple MCP Servers
# Example: Connect to one or more servers via config
The library also allows you to connect to multiple MCP servers and load tools from them:
### 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
## Client
```ts
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
import { ChatOpenAI } from '@langchain/openai';
import { createReactAgent } from '@langchain/langgraph/prebuilt';
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();
const client = new MultiServerMCPClient({
// adds a STDIO connection to a server named "math"
math: {
transport: "stdio",
command: "npx",
args: ["-y", "@modelcontextprotocol/server-math"],
},
// add additional servers by adding more keys to the config
// here's a filesystem server
filesystem: {
transport: "stdio",
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem"],
},
});
const tools = await client.getTools();
// Create an OpenAI model
const model = new ChatOpenAI({
modelName: 'gpt-4o',
modelName: "gpt-4o",
temperature: 0,
});
@@ -198,269 +149,16 @@ const agent = createReactAgent({
// Run the agent
const mathResponse = await agent.invoke({
messages: [{ role: 'user', content: "what's (3 + 5) x 12?" }],
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?' }],
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.
For more detailed examples, see the [examples](./examples) directory.
## Browser Environments
@@ -513,7 +211,6 @@ To output debug logs only from the `tools` module:
DEBUG='@langchain/mcp-adapters:tools'
```
## License
MIT
+107 -426
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,21 @@ const { StdioClientTransport } = await import(
const { SSEClientTransport } = await import(
"@modelcontextprotocol/sdk/client/sse.js"
);
const fs = await import("fs");
const path = await import("path");
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", () => {
@@ -273,38 +72,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({
@@ -324,8 +96,8 @@ describe("MultiServerMCPClient", () => {
});
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 +112,12 @@ 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 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 +138,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 +157,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 +259,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 +279,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 +294,7 @@ describe("MultiServerMCPClient", () => {
});
});
// 6. Cleanup Handling tests
// Cleanup Handling tests
describe("close", () => {
test("should close all connections properly", async () => {
const client = new MultiServerMCPClient({
@@ -529,16 +313,17 @@ describe("MultiServerMCPClient", () => {
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();
});
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 +338,7 @@ describe("MultiServerMCPClient", () => {
await client.initializeConnections();
await client.close();
// Verify that the client handled the error gracefully
});
});
// 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",
]);
expect(StdioClientTransport).toHaveBeenCalledWith({
command: "python",
args: ["./script.py"],
env: undefined,
});
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"
);
expect(SSEClientTransport).toHaveBeenCalled();
expect(Client).toHaveBeenCalled();
expect(mockClientConnect).toHaveBeenCalled();
expect(mockClientListTools).toHaveBeenCalled();
});
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
);
// Verify that headers were set correctly
});
test("should connect with useNodeEventSource option", async () => {
const client = new MultiServerMCPClient();
await client.connectToServerViaSSE(
"test-server",
"http://localhost:8000/sse",
undefined,
true
);
// Verify that useNodeEventSource was set correctly
});
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
);
// Verify that reconnect configuration was set correctly
expect(closeMock).toHaveBeenCalledOnce();
});
});
});
File diff suppressed because it is too large Load Diff
+72
View File
@@ -0,0 +1,72 @@
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,
};
});
+78 -64
View File
@@ -1,16 +1,21 @@
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 type { StructuredTool } 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 +27,39 @@ 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 handle empty tool list", async () => {
// Set up mock response
mockClient.listTools.mockReturnValueOnce(
Promise.resolve({
@@ -61,7 +69,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 +77,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 +103,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 +135,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 +203,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"]
}
}
}
+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
+4 -14
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 Math 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
@@ -54,23 +54,13 @@ async function runExample() {
client = new MultiServerMCPClient({
math: {
transport: "stdio",
command: "python",
args: ["./examples/math_server.py"],
command: "npx",
args: ["-y", "@modelcontextprotocol/server-math"],
},
});
// 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");
-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")
+12 -11
View File
@@ -19,13 +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": "yarn build",
"format": "prettier --config .prettierrc --write \"src/**/*.ts\" \"examples/**/*.ts\"",
"format:check": "prettier --config .prettierrc --check \"src\" \"examples/**/*.ts\"",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest"
@@ -48,7 +48,8 @@
"dependencies": {
"@dmitryrechkin/json-schema-to-zod": "^1.0.1",
"@modelcontextprotocol/sdk": "^1.7.0",
"debug": "^4.4.0"
"debug": "^4.4.0",
"zod": "^3.24.2"
},
"peerDependencies": {
"@langchain/core": "^0.3.40"
@@ -58,16 +59,16 @@
},
"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.43",
"@langchain/langgraph": "^0.2.62",
"@langchain/openai": "^0.5.2",
"@langchain/scripts": "^0.1.3",
"@tsconfig/recommended": "^1.0.8",
"@types/debug": "^4.1.12",
"@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",
@@ -77,17 +78,17 @@
"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",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"npm-run-all": "^4.1.5",
"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"
+351 -806
View File
File diff suppressed because it is too large Load Diff
+55 -2
View File
@@ -200,6 +200,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.
*
@@ -210,12 +248,27 @@ 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 (
await Promise.all(
@@ -224,7 +277,7 @@ export async function loadMcpTools(
.map(async (tool: MCPTool) => {
try {
const dst = new DynamicStructuredTool({
name: tool.name,
name: `${toolNamePrefix}${tool.name}`,
description: tool.description || "",
schema: await convertSchema(
(tool.inputSchema ?? {
+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"]
}
+521 -546
View File
File diff suppressed because it is too large Load Diff