mirror of
https://github.com/langchain-ai/langchainjs-mcp-adapters.git
synced 2026-07-01 12:27:48 -04:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3f16633d1 | |||
| 01b70d18c9 | |||
| 72818340c1 | |||
| 373b08e090 | |||
| 831359db3a | |||
| 68f130e1d0 | |||
| 03172e3fcd | |||
| 8cfec54c29 | |||
| c1b0f3d8ad | |||
| 66f6eb0455 | |||
| b8ccb243a0 | |||
| a67182573d | |||
| 2930bf5124 | |||
| 60458b9926 | |||
| 6093a1c9ba | |||
| 09f511a57d | |||
| b4313142e0 | |||
| 18daa5b83f | |||
| 2fc9bfa79e | |||
| e38ff20113 | |||
| 6eb9d6cf7a |
+10
-1
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,3 +5,5 @@ index.d.cts
|
||||
node_modules
|
||||
dist
|
||||
.yarn
|
||||
.env
|
||||
.eslintcache
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
Binary file not shown.
@@ -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).**
|
||||
|
||||
[](https://www.npmjs.com/package/@langchain/mcp-adapters)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
This library provides a lightweight wrapper that makes[Anthropic Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) tools compatible with [LangChain.js](https://github.com/langchain-ai/langchainjs) and [LangGraph.js](https://github.com/langchain-ai/langgraphjs).
|
||||
This library provides a lightweight wrapper to allow [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) services to be used with [LangChain.js](https://github.com/langchain-ai/langchainjs).
|
||||
|
||||
## Features
|
||||
|
||||
- 🔌 **Transport Options**
|
||||
|
||||
- Connect to MCP servers via stdio (local) or SSE (remote)
|
||||
- Support for custom headers in SSE connections for authentication
|
||||
- Configurable reconnection strategies for both transport types
|
||||
|
||||
- 🔄 **Multi-Server Management**
|
||||
|
||||
- Connect to multiple MCP servers simultaneously
|
||||
- Auto-organize tools by server or access them as a flattened collection
|
||||
- Convenient configuration via JSON file
|
||||
|
||||
- 🧩 **Agent Integration**
|
||||
|
||||
- Compatible with LangChain.js and LangGraph.js
|
||||
- Optimized for OpenAI, Anthropic, and Google models
|
||||
|
||||
- 🛠️ **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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
},
|
||||
"math-server": {
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["./math_server.py"]
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-math"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import express from "express";
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export async function main() {
|
||||
const server = new McpServer({
|
||||
name: "backwards-compatible-server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
const calcSchema = { a: z.number(), b: z.number() };
|
||||
|
||||
server.tool(
|
||||
"add",
|
||||
"Adds two numbers together",
|
||||
calcSchema,
|
||||
async ({ a, b }: { a: number; b: number }, extra) => {
|
||||
return {
|
||||
content: [{ type: "text", text: `${a + b}` }],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"subtract",
|
||||
"Subtracts two numbers",
|
||||
calcSchema,
|
||||
async ({ a, b }: { a: number; b: number }, extra) => {
|
||||
return { content: [{ type: "text", text: `${a - b}` }] };
|
||||
}
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"multiply",
|
||||
"Multiplies two numbers",
|
||||
calcSchema,
|
||||
async ({ a, b }: { a: number; b: number }, extra) => {
|
||||
return { content: [{ type: "text", text: `${a * b}` }] };
|
||||
}
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"divide",
|
||||
"Divides two numbers",
|
||||
calcSchema,
|
||||
async ({ a, b }: { a: number; b: number }, extra) => {
|
||||
return { content: [{ type: "text", text: `${a / b}` }] };
|
||||
}
|
||||
);
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Store transports for each session type
|
||||
const transports = {
|
||||
streamable: {} as Record<string, StreamableHTTPServerTransport>,
|
||||
sse: {} as Record<string, SSEServerTransport>,
|
||||
};
|
||||
|
||||
// Modern Streamable HTTP endpoint
|
||||
app.post("/mcp", async (req, res) => {
|
||||
// Check for existing session ID
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
|
||||
if (sessionId && transports.streamable[sessionId]) {
|
||||
// Reuse existing transport
|
||||
transport = transports.streamable[sessionId];
|
||||
} else if (!sessionId && isInitializeRequest(req.body)) {
|
||||
// New initialization request
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
onsessioninitialized: (sessionId) => {
|
||||
// Store the transport by session ID
|
||||
transports.streamable[sessionId] = transport;
|
||||
},
|
||||
});
|
||||
|
||||
// Clean up transport when closed
|
||||
transport.onclose = () => {
|
||||
if (transport.sessionId) {
|
||||
delete transports.streamable[transport.sessionId];
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to the MCP server
|
||||
await server.connect(transport);
|
||||
} else {
|
||||
// Invalid request
|
||||
console.error(
|
||||
"Invalid Streamable HTTP request: ",
|
||||
JSON.stringify(req.body, null, 2)
|
||||
);
|
||||
res.status(400).json({
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32000,
|
||||
message: "Bad Request: No valid session ID provided",
|
||||
},
|
||||
id: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the request
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
});
|
||||
|
||||
// Reusable handler for GET and DELETE requests
|
||||
const handleSessionRequest = async (
|
||||
req: express.Request,
|
||||
res: express.Response
|
||||
) => {
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
if (!sessionId || !transports.streamable[sessionId]) {
|
||||
console.error(
|
||||
"Invalid Streamable HTTP request (invalid/missing session ID): ",
|
||||
JSON.stringify(req.body, null, 2)
|
||||
);
|
||||
res.status(400).send("Invalid or missing session ID");
|
||||
return;
|
||||
}
|
||||
|
||||
const transport = transports.streamable[sessionId];
|
||||
await transport.handleRequest(req, res);
|
||||
};
|
||||
|
||||
app.get("/mcp", handleSessionRequest);
|
||||
app.delete("/mcp", handleSessionRequest);
|
||||
|
||||
// Legacy SSE endpoint for older clients
|
||||
app.get("/sse", async (req, res) => {
|
||||
// Create SSE transport for legacy clients
|
||||
const transport = new SSEServerTransport("/messages", res);
|
||||
transports.sse[transport.sessionId] = transport;
|
||||
|
||||
res.on("close", () => {
|
||||
delete transports.sse[transport.sessionId];
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
});
|
||||
|
||||
// Legacy message endpoint for older clients
|
||||
app.post("/messages", async (req, res) => {
|
||||
const sessionId = req.query.sessionId as string;
|
||||
const transport = transports.sse[sessionId];
|
||||
if (transport) {
|
||||
await transport.handlePostMessage(req, res, req.body);
|
||||
} else {
|
||||
console.error("No transport found for sessionId", sessionId);
|
||||
res.status(400).send("No transport found for sessionId");
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
}
|
||||
|
||||
if (typeof require !== "undefined" && require.main === module) {
|
||||
main().catch(console.error);
|
||||
}
|
||||
|
||||
if (
|
||||
import.meta.url === process.argv[1] ||
|
||||
import.meta.url === `file://${process.argv[1]}`
|
||||
) {
|
||||
main().catch(console.error);
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Calculator MCP Server with LangGraph Example
|
||||
*
|
||||
* This example demonstrates how to use the Calculator MCP server with LangGraph
|
||||
* to create a structured workflow for simple calculations.
|
||||
*
|
||||
* The graph-based approach allows:
|
||||
* 1. Clear separation of responsibilities (reasoning vs execution)
|
||||
* 2. Conditional routing based on tool calls
|
||||
* 3. Structured handling of complex multi-tool operations
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import {
|
||||
StateGraph,
|
||||
END,
|
||||
START,
|
||||
MessagesAnnotation,
|
||||
} from "@langchain/langgraph";
|
||||
import { ToolNode } from "@langchain/langgraph/prebuilt";
|
||||
import {
|
||||
HumanMessage,
|
||||
AIMessage,
|
||||
SystemMessage,
|
||||
isHumanMessage,
|
||||
} from "@langchain/core/messages";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
import { main as calculatorServerMain } from "./calculator_server_shttp_sse.js";
|
||||
|
||||
// MCP client imports
|
||||
import { MultiServerMCPClient } from "../src/index.js";
|
||||
|
||||
// Load environment variables from .env file
|
||||
dotenv.config();
|
||||
|
||||
const transportType = process.env.MCP_TRANSPORT_TYPE === "sse" ? "sse" : "http";
|
||||
|
||||
export async function runExample(client?: MultiServerMCPClient) {
|
||||
try {
|
||||
console.log("Initializing MCP client...");
|
||||
|
||||
void calculatorServerMain();
|
||||
|
||||
// Wait for the server to start
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
// Create a client with configurations for the calculator server
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
client =
|
||||
client ??
|
||||
new MultiServerMCPClient({
|
||||
calculator: {
|
||||
url: `http://localhost:3000/${
|
||||
transportType === "sse" ? "sse" : "mcp"
|
||||
}`,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Connected to server");
|
||||
|
||||
// Get all tools (flattened array is the default now)
|
||||
const mcpTools = await client.getTools();
|
||||
|
||||
if (mcpTools.length === 0) {
|
||||
throw new Error("No tools found");
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Loaded ${mcpTools.length} MCP tools: ${mcpTools
|
||||
.map((tool) => tool.name)
|
||||
.join(", ")}`
|
||||
);
|
||||
|
||||
// Create an OpenAI model with tools attached
|
||||
const systemMessage = `You are an assistant that helps users with calculations.
|
||||
You have access to tools that can add, subtract, multiply, and divide numbers. Use
|
||||
these tools to answer the user's questions.`;
|
||||
|
||||
const model = new ChatOpenAI({
|
||||
modelName: process.env.OPENAI_MODEL_NAME || "gpt-4o-mini",
|
||||
temperature: 0.7,
|
||||
}).bindTools(mcpTools);
|
||||
|
||||
// Create a tool node for the LangGraph
|
||||
const toolNode = new ToolNode(mcpTools);
|
||||
|
||||
// ================================================
|
||||
// Create a LangGraph agent flow
|
||||
// ================================================
|
||||
console.log("\n=== CREATING LANGGRAPH AGENT FLOW ===");
|
||||
|
||||
// Define the function that calls the model
|
||||
const llmNode = async (state: typeof MessagesAnnotation.State) => {
|
||||
console.log(`Calling LLM with ${state.messages.length} messages`);
|
||||
|
||||
// Add system message if it's the first call
|
||||
let { messages } = state;
|
||||
if (messages.length === 1 && isHumanMessage(messages[0])) {
|
||||
messages = [new SystemMessage(systemMessage), ...messages];
|
||||
}
|
||||
|
||||
const response = await model.invoke(messages);
|
||||
return { messages: [response] };
|
||||
};
|
||||
|
||||
// Create a new graph with MessagesAnnotation
|
||||
const workflow = new StateGraph(MessagesAnnotation)
|
||||
|
||||
// Add the nodes to the graph
|
||||
.addNode("llm", llmNode)
|
||||
.addNode("tools", toolNode)
|
||||
|
||||
// Add edges - these define how nodes are connected
|
||||
.addEdge(START, "llm")
|
||||
.addEdge("tools", "llm")
|
||||
|
||||
// Conditional routing to end or continue the tool loop
|
||||
.addConditionalEdges("llm", (state) => {
|
||||
const lastMessage = state.messages[state.messages.length - 1];
|
||||
|
||||
// Cast to AIMessage to access tool_calls property
|
||||
const aiMessage = lastMessage as AIMessage;
|
||||
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) {
|
||||
console.log("Tool calls detected, routing to tools node");
|
||||
|
||||
// Log what tools are being called
|
||||
const toolNames = aiMessage.tool_calls
|
||||
.map((tc) => tc.name)
|
||||
.join(", ");
|
||||
console.log(`Tools being called: ${toolNames}`);
|
||||
|
||||
return "tools";
|
||||
}
|
||||
|
||||
// If there are no tool calls, we're done
|
||||
console.log("No tool calls, ending the workflow");
|
||||
return END;
|
||||
});
|
||||
|
||||
// Compile the graph
|
||||
const app = workflow.compile();
|
||||
|
||||
// Define examples to run
|
||||
const examples = [
|
||||
{
|
||||
name: "Add 1 and 2",
|
||||
query: "What is 1 + 2?",
|
||||
},
|
||||
{
|
||||
name: "Subtract 1 from 2",
|
||||
query: "What is 2 - 1?",
|
||||
},
|
||||
{
|
||||
name: "Multiply 1 and 2",
|
||||
query: "What is 1 * 2?",
|
||||
},
|
||||
{
|
||||
name: "Divide 1 by 2",
|
||||
query: "What is 1 / 2?",
|
||||
},
|
||||
];
|
||||
|
||||
// Run the examples
|
||||
console.log("\n=== RUNNING LANGGRAPH AGENT ===");
|
||||
|
||||
for (const example of examples) {
|
||||
console.log(`\n--- Example: ${example.name} ---`);
|
||||
console.log(`Query: ${example.query}`);
|
||||
|
||||
// Run the LangGraph agent
|
||||
const result = await app.invoke({
|
||||
messages: [new HumanMessage(example.query)],
|
||||
});
|
||||
|
||||
// Display the final answer
|
||||
const finalMessage = result.messages[result.messages.length - 1];
|
||||
console.log(`\nResult: ${finalMessage.content}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
process.exit(1); // Exit with error code
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close();
|
||||
console.log("Closed all MCP connections");
|
||||
}
|
||||
|
||||
// Exit process after a short delay to allow for cleanup
|
||||
setTimeout(() => {
|
||||
console.log("Example completed, exiting process.");
|
||||
process.exit(0);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
|
||||
if (isMainModule) {
|
||||
runExample().catch((error) => console.error("Setup error:", error));
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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,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 ===");
|
||||
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -18,5 +18,6 @@ export const config = {
|
||||
tsConfigPath: resolve("./tsconfig.json"),
|
||||
cjsSource: "./dist-cjs",
|
||||
cjsDestination: "./dist",
|
||||
additionalGitignorePaths: [".env", ".eslintcache"],
|
||||
abs,
|
||||
};
|
||||
|
||||
+20
-20
@@ -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
File diff suppressed because it is too large
Load Diff
+8
-1
@@ -1,8 +1,15 @@
|
||||
import { type StreamableHTTPConnection } from "./client.js";
|
||||
|
||||
export {
|
||||
MultiServerMCPClient,
|
||||
type Connection,
|
||||
type StdioConnection,
|
||||
type SSEConnection,
|
||||
type StreamableHTTPConnection,
|
||||
} from "./client.js";
|
||||
|
||||
/**
|
||||
* Type alias for backward compatibility with previous versions of the package.
|
||||
*/
|
||||
export type SSEConnection = StreamableHTTPConnection;
|
||||
|
||||
export { loadMcpTools } from "./tools.js";
|
||||
|
||||
+87
-39
@@ -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
@@ -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"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user