Compare commits

...

34 Commits

Author SHA1 Message Date
Ben Burns c3f16633d1 chore: update README with migration notice 2025-05-16 15:18:45 -07:00
github-actions[bot] 01b70d18c9 chore: bump version to v0.4.5 [skip ci] 2025-05-13 21:07:33 +00:00
Ben Burns 72818340c1 fix: drop useless import from node:stream package (#75)
fixes #62
2025-05-13 13:49:06 -07:00
Ben Burns 373b08e090 Merge pull request #74 from langchain-ai/release
chore: bump version to v0.4.4 [skip ci]
2025-05-13 13:21:09 -07:00
Ben Burns 831359db3a chore: bump version to v0.4.4 [skip ci] 2025-05-13 13:16:00 -07:00
Abhilash Panigrahi 68f130e1d0 feat: support StreamableHTTPClientTransport (#64)
Co-authored-by: Abhilash Panigrahi <pabhila@amazon.com>
Co-authored-by: Ben Burns <803016+benjamincburns@users.noreply.github.com>
2025-05-13 13:00:39 -07:00
github-actions[bot] 03172e3fcd chore: bump version to v0.4.3 [skip ci] 2025-05-12 17:54:44 +00:00
Niko Nummi 8cfec54c29 feat: send headers when using node event source transport (#58) 2025-05-12 08:49:34 -07:00
Ravi Kiran Vemula c1b0f3d8ad changes company name in readme (#72) 2025-05-12 08:14:33 -07:00
github-actions[bot] 66f6eb0455 chore: bump version to v0.4.2 [skip ci] 2025-04-09 06:48:21 +00:00
Ben Burns b8ccb243a0 fix: drop unnecessary dependency (#57) 2025-04-09 18:47:18 +12:00
github-actions[bot] a67182573d chore: bump version to v0.4.1 [skip ci] 2025-04-09 06:43:02 +00:00
Ben Burns 2930bf5124 fix: pass tool schema without conversion (#56) 2025-04-09 18:40:43 +12:00
github-actions[bot] 60458b9926 chore: bump version to v0.4.0 [skip ci] 2025-04-04 03:23:07 +00:00
Ben Burns 6093a1c9ba chore: update README to reflect recent changes (#53) 2025-04-04 16:17:51 +13:00
Ben Burns 09f511a57d break: simplify interface, drop file IO, accept common mcp server config (#38) 2025-04-03 21:56:54 +13:00
github-actions[bot] b4313142e0 chore: bump version to v0.3.4 [skip ci] 2025-03-24 02:43:50 +00:00
Ben Burns 18daa5b83f fix: fix CJS by dynamically importing json-schema-to-zod (#37)
fixes #36
2025-03-24 15:42:25 +13:00
github-actions[bot] 2fc9bfa79e chore: bump version to v0.3.3 [skip ci] 2025-03-22 04:22:31 +00:00
Ben Burns e38ff20113 fix: don't autorun husky on npm install (#35) 2025-03-22 17:21:20 +13:00
Ben Burns 6eb9d6cf7a fix: fix post-release commit (#34) 2025-03-22 17:15:22 +13:00
Ben Burns 7828c19fd8 chore: bump version to v0.3.2 [skip ci] 2025-03-22 17:14:17 +13:00
Ben Burns 98726ec5de fix: don't use yarn for release publish step (#33) 2025-03-22 17:08:18 +13:00
Ben Burns b6e17366be fix: build & import issues (#32)
fixes #30
fixes #31
2025-03-22 16:54:34 +13:00
github-actions[bot] ce7b796ec8 chore: bump version to v0.3.1 [skip ci] 2025-03-20 13:06:47 +00:00
Vadym Barda c3a0dfddc1 export loadMcpTools (#29) 2025-03-20 09:04:36 -04:00
Vadym Barda 304c1847b0 fix package.json path (#28) 2025-03-20 08:55:06 -04:00
github-actions[bot] 40ffbf4080 chore: bump version to v0.3.0 [skip ci] 2025-03-19 21:48:21 +00:00
Ben Burns 99d6be0626 fix build (#26) 2025-03-20 10:44:27 +13:00
Ben Burns 3a59cf313b fix: correctly map MCP tool content to LangChain messages (#23) 2025-03-20 10:40:49 +13:00
Ben Burns 72b740ffcf break: remove winston in favor of the debug package (#25)
* break: remove winston in favor of the debug package
* fix: add missing test coverage dev dependency
2025-03-19 18:36:14 +13:00
Ben Burns 399bf78cd8 fix: lint (#24) 2025-03-19 18:14:36 +13:00
Ben Burns 8cd572316e chore: some minor clean-up (#22) 2025-03-19 10:40:08 +13:00
Ben Burns 6bc0b0703e chore: add example of server running in docker container (#21) 2025-03-19 10:37:17 +13:00
48 changed files with 12364 additions and 12306 deletions
+77
View File
@@ -0,0 +1,77 @@
module.exports = {
extends: [
"airbnb-base",
"eslint:recommended",
"prettier",
"plugin:@typescript-eslint/recommended",
],
parserOptions: {
ecmaVersion: 12,
parser: "@typescript-eslint/parser",
project: ["./tsconfig.json", "./tsconfig.examples.json", "./tsconfig.tests.json"],
sourceType: "module",
},
plugins: ["@typescript-eslint", "no-instanceof", "eslint-plugin-vitest"],
ignorePatterns: [
".eslintrc.cjs",
"scripts",
"node_modules",
"dist",
"dist-cjs",
"*.js",
"*.cjs",
"*.d.ts",
],
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,
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/no-use-before-define": ["error", "nofunc"],
"@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"arrow-body-style": 0,
camelcase: 0,
"class-methods-use-this": 0,
"import/extensions": [2, "ignorePackages"],
"import/no-extraneous-dependencies": [
"error",
{ devDependencies: ["**/*.test.ts", "__tests__/**/*.ts", "examples/**/*.ts"] },
],
"import/no-unresolved": 0,
"import/prefer-default-export": 0,
'vitest/no-focused-tests': 'error',
"keyword-spacing": "error",
"max-classes-per-file": 0,
"max-len": 0,
"no-await-in-loop": 0,
"no-bitwise": 0,
"no-console": 0,
"no-empty-function": 0,
"no-restricted-syntax": 0,
"no-shadow": 0,
"no-continue": 0,
"no-void": 0,
"no-underscore-dangle": 0,
"no-use-before-define": 0,
"no-useless-constructor": 0,
"no-return-await": 0,
"consistent-return": 0,
"no-else-return": 0,
"func-names": 0,
"no-lonely-if": 0,
"prefer-rest-params": 0,
"new-cap": ["error", { properties: false, capIsNew: false }],
},
};
+8 -8
View File
@@ -17,19 +17,19 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache: 'yarn'
- name: Install dependencies
run: npm ci
run: yarn install --immutable
- name: Lint
run: npm run lint
run: yarn lint
- name: Build
run: npm run build
run: yarn build
- name: Run tests
run: npm test
run: yarn test
coverage:
name: Coverage
@@ -42,13 +42,13 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache: 'yarn'
- name: Install dependencies
run: npm ci
run: yarn install --immutable
- name: Generate coverage report
run: npm test -- --coverage
run: yarn test -- --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
+5 -5
View File
@@ -28,16 +28,16 @@ jobs:
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
cache: 'yarn'
- name: Install dependencies
run: npm ci
run: yarn install --immutable
- name: Run tests
run: npm test
run: yarn test
- name: Build
run: npm run build
run: yarn build
- name: Configure Git
run: |
@@ -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
+7 -7
View File
@@ -17,19 +17,19 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache: 'yarn'
- name: Install dependencies
run: npm ci
run: yarn install --immutable
- name: Lint
run: npm run lint
run: yarn lint
- name: Type check
run: npm run build --noEmit
run: yarn build
- name: Run tests
run: npm test
run: yarn test
format-check:
name: Format Check
@@ -42,10 +42,10 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache: 'yarn'
- name: Install dependencies
run: npm ci
run: yarn install --immutable
- name: Check formatting
run: npx prettier --check "src/**/*.ts" "examples/**/*.ts"
+8 -53
View File
@@ -1,54 +1,9 @@
# Dependency directories
node_modules/
.pnp/
.pnp.js
.cursor
# Build outputs
dist/
build/
*.tsbuildinfo
index.cjs
index.js
index.d.ts
index.d.cts
node_modules
dist
.yarn
.env
mcp.json
# Coverage directory
coverage/
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea/
.vscode/
*.swp
*.swo
.DS_Store
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
.env
.venv
.python-version
# Misc
.cache/
.temp/
.tmp/
.eslintcache
+19
View File
@@ -0,0 +1,19 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always",
"requirePragma": false,
"insertPragma": false,
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"vueIndentScriptAndStyle": false,
"endOfLine": "lf"
}
-11
View File
@@ -1,11 +0,0 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}
+5
View File
@@ -0,0 +1,5 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"typescript.tsdk": "node_modules/typescript/lib"
}
File diff suppressed because one or more lines are too long
+873
View File
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: "@yarnpkg/plugin-typescript"
yarnPath: .yarn/releases/yarn-3.5.1.cjs
+5 -525
View File
@@ -1,531 +1,11 @@
# LangChain.js MCP Adapters
> [!IMPORTANT]
> **This package has been migrated into [the LangChainJS monorepo](https://github.com/langchain-ai/langchainjs/tree/main/libs/langchain-mcp-adapters).**
[![npm version](https://img.shields.io/npm/v/@langchain/mcp-adapters.svg)](https://www.npmjs.com/package/@langchain/mcp-adapters)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
This library provides a lightweight wrapper that makes [Anthropic Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) tools compatible with [LangChain.js](https://github.com/langchain-ai/langchainjs) and [LangGraph.js](https://github.com/langchain-ai/langgraphjs).
This library provides a lightweight wrapper to allow [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) services to be used with [LangChain.js](https://github.com/langchain-ai/langchainjs).
## Features
- 🔌 **Transport Options**
- Connect to MCP servers via stdio (local) or SSE (remote)
- Support for custom headers in SSE connections for authentication
- Configurable reconnection strategies for both transport types
- 🔄 **Multi-Server Management**
- Connect to multiple MCP servers simultaneously
- Auto-organize tools by server or access them as a flattened collection
- Convenient configuration via JSON file
- 🧩 **Agent Integration**
- Compatible with LangChain.js and LangGraph.js
- Optimized for OpenAI, Anthropic, and Google models
- 🛠️ **Development Features**
- Comprehensive logging system
- 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(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
Logging is disabled by default for optimal performance. Enable logging when needed for diagnostics:
```typescript
import { enableLogging, disableLogging } from '@langchain/mcp-adapters';
// Enable logging at info level (default)
enableLogging();
// Or specify a specific level
enableLogging('debug'); // Most verbose
enableLogging('info'); // General information
enableLogging('warn'); // Warnings only
enableLogging('error'); // Errors only
// Disable logging when done
disableLogging();
```
You can also access the logger directly:
```typescript
import { logger } from '@langchain/mcp-adapters';
// Advanced logging configuration
logger.level = 'debug';
```
## 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.
+336 -465
View File
@@ -1,242 +1,64 @@
import { vi, describe, test, expect, beforeEach, afterEach } 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 {
vi,
describe,
test,
expect,
beforeEach,
afterEach,
type Mock,
} from "vitest";
import { ZodError } from "zod";
return {
SSEClientTransport: vi.fn().mockImplementation(() => ({
connect: connectMock,
send: sendMock,
close: closeMock,
onclose: null,
})),
};
});
import "./mocks.js";
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(),
}));
vi.mock('path', () => ({
resolve: vi.fn(),
}));
// 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(),
}));
const { MultiServerMCPClient, MCPClientError } = await import('../src/client.js');
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');
const fs = await import('fs');
const path = await import('path');
describe('MultiServerMCPClient', () => {
// Create mock implementations that will be used throughout the tests
let mockClientConnect: vi.Mock;
let mockClientListTools: vi.Mock;
let mockClientCallTool: vi.Mock;
let mockClientClose: vi.Mock;
let mockStdioTransportClose: vi.Mock;
let mockStdioTransportConnect: vi.Mock;
let mockStdioTransportSend: vi.Mock;
// Define specific function type for onclose handlers
let mockStdioOnClose: (() => void) | null;
let mockSSETransportClose: vi.Mock;
let mockSSETransportConnect: vi.Mock;
let mockSSETransportSend: vi.Mock;
// Define specific function type for onclose handlers
let mockSSEOnClose: (() => void) | null;
const { MultiServerMCPClient, MCPClientError } = await import(
"../src/client.js"
);
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
const { StdioClientTransport } = await import(
"@modelcontextprotocol/sdk/client/stdio.js"
);
const { SSEClientTransport } = await import(
"@modelcontextprotocol/sdk/client/sse.js"
);
const { StreamableHTTPClientTransport } = await import(
"@modelcontextprotocol/sdk/client/streamableHttp.js"
);
describe("MultiServerMCPClient", () => {
// 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 vi.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 vi.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 vi.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 vi.Mock).mockImplementation(() =>
JSON.stringify({
servers: {
'test-server': {
transport: 'stdio',
command: 'python',
args: ['./script.py'],
},
},
})
);
(path.resolve as vi.Mock).mockImplementation(p => p);
});
afterEach(() => {
vi.resetAllMocks();
vi.clearAllMocks();
});
// 1. Constructor functionality tests
describe('constructor', () => {
test('should initialize with empty connections', () => {
const client = new MultiServerMCPClient();
expect(client).toBeDefined();
// Constructor functionality tests
describe("constructor", () => {
test("should throw if initialized with empty connections", () => {
expect(() => new MultiServerMCPClient({})).toThrow(MCPClientError);
});
test('should process valid stdio connection config', () => {
test("should process valid stdio connection config", () => {
const client = new MultiServerMCPClient({
'test-server': {
transport: 'stdio',
command: 'python',
args: ['./script.py'],
"test-server": {
transport: "stdio",
command: "python",
args: ["./script.py"],
},
});
expect(client).toBeDefined();
// Additional assertions to verify the connection was processed correctly
});
test('should process valid SSE connection config', () => {
test("should process valid SSE connection config", () => {
const client = new MultiServerMCPClient({
'test-server': {
transport: 'sse',
url: 'http://localhost:8000/sse',
headers: { Authorization: 'Bearer token' },
"test-server": {
transport: "sse",
url: "http://localhost:8000/sse",
headers: { Authorization: "Bearer token" },
useNodeEventSource: true,
},
});
@@ -244,74 +66,60 @@ describe('MultiServerMCPClient', () => {
// 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', () => {
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
new MultiServerMCPClient({
'test-server': {
"test-server": {
// @ts-expect-error shouldn't match type constraints here
transport: 'invalid',
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 vi.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 vi.Mock).mockImplementation(() => 'invalid json');
expect(() => {
MultiServerMCPClient.fromConfigFile('./invalid.json');
}).toThrow(MCPClientError);
});
});
// 3. Connection Management tests
describe('initializeConnections', () => {
test('should initialize stdio connections correctly', async () => {
// Connection Management tests
describe("initializeConnections", () => {
test("should initialize stdio connections correctly", async () => {
const client = new MultiServerMCPClient({
'test-server': {
transport: 'stdio',
command: 'python',
args: ['./script.py'],
"test-server": {
transport: "stdio",
command: "python",
args: ["./script.py"],
},
});
await client.initializeConnections();
expect(StdioClientTransport).toHaveBeenCalledWith({
command: 'python',
args: ['./script.py'],
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 () => {
test("should initialize SSE connections correctly", async () => {
const client = new MultiServerMCPClient({
'test-server': {
transport: 'sse',
url: 'http://localhost:8000/sse',
"test-server": {
transport: "sse",
url: "http://localhost:8000/sse",
},
});
@@ -319,174 +127,222 @@ describe('MultiServerMCPClient', () => {
expect(SSEClientTransport).toHaveBeenCalled();
expect(Client).toHaveBeenCalled();
expect(mockClientConnect).toHaveBeenCalled();
expect(mockClientListTools).toHaveBeenCalled();
expect(Client.prototype.connect).toHaveBeenCalled();
expect(Client.prototype.listTools).toHaveBeenCalled();
});
test('should throw on connection failure', async () => {
(Client as vi.Mock).mockImplementation(() => ({
connect: vi.fn().mockReturnValue(Promise.reject(new Error('Connection failed'))),
test("should initialize streamable HTTP connections correctly", async () => {
const client = new MultiServerMCPClient({
"test-server": {
transport: "http",
url: "http://localhost:8000/mcp",
},
});
await client.initializeConnections();
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(
new URL("http://localhost:8000/mcp")
);
expect(Client).toHaveBeenCalled();
expect(Client.prototype.connect).toHaveBeenCalled();
expect(Client.prototype.listTools).toHaveBeenCalled();
});
test("should throw on connection failure", async () => {
(Client as Mock).mockImplementationOnce(() => ({
connect: vi
.fn()
.mockReturnValue(Promise.reject(new Error("Connection failed"))),
listTools: vi.fn().mockReturnValue(Promise.resolve({ tools: [] })),
}));
const client = new MultiServerMCPClient({
'test-server': {
transport: 'stdio',
command: 'python',
args: ['./script.py'],
"test-server": {
transport: "stdio",
command: "python",
args: ["./script.py"],
},
});
await expect(() => client.initializeConnections()).rejects.toThrow(MCPClientError);
await expect(() => client.initializeConnections()).rejects.toThrow(
MCPClientError
);
});
test('should throw on tool loading failures', async () => {
(Client as vi.Mock).mockImplementation(() => ({
test("should throw on tool loading failures", async () => {
(Client as Mock).mockImplementationOnce(() => ({
connect: vi.fn().mockReturnValue(Promise.resolve()),
listTools: vi.fn().mockReturnValue(Promise.reject(new Error('Failed to list tools'))),
listTools: vi
.fn()
.mockReturnValue(Promise.reject(new Error("Failed to list tools"))),
}));
const client = new MultiServerMCPClient({
'test-server': {
transport: 'stdio',
command: 'python',
args: ['./script.py'],
"test-server": {
transport: "stdio",
command: "python",
args: ["./script.py"],
},
});
await expect(() => client.initializeConnections()).rejects.toThrow(MCPClientError);
await expect(() => client.initializeConnections()).rejects.toThrow(
MCPClientError
);
});
// 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);
});
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);
});
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);
});
});
});
// 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,
},
},
});
await client.initializeConnections();
// Reset the call counts to focus on reconnection
(StdioClientTransport as vi.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,
},
},
});
await client.initializeConnections();
// Reset the call counts to focus on reconnection
(SSEClientTransport as vi.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));
// 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);
});
});
// 5. Tool Management tests
describe('getTools', () => {
test('should get all tools as a flattened array', async () => {
// Tool Management tests
describe("getTools", () => {
test("should get all tools as a flattened array", async () => {
// Mock tool response
const mockTools = [
{ name: 'tool1', description: 'Tool 1', inputSchema: {} },
{ name: 'tool2', description: 'Tool 2', inputSchema: {} },
{ name: "tool1", description: "Tool 1", inputSchema: {} },
{ name: "tool2", description: "Tool 2", inputSchema: {} },
];
(Client as vi.Mock).mockImplementation(() => ({
(Client as Mock).mockImplementationOnce(() => ({
connect: vi.fn().mockReturnValue(Promise.resolve()),
listTools: vi.fn().mockReturnValue(Promise.resolve({ tools: mockTools })),
listTools: vi
.fn()
.mockReturnValue(Promise.resolve({ tools: mockTools })),
}));
const client = new MultiServerMCPClient({
server1: {
transport: 'stdio',
command: 'python',
args: ['./script1.py'],
transport: "stdio",
command: "python",
args: ["./script1.py"],
},
server2: {
transport: 'stdio',
command: 'python',
args: ['./script2.py'],
transport: "stdio",
command: "python",
args: ["./script2.py"],
},
});
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);
});
test('should get tools from specific servers', async () => {
test("should get tools from specific servers", async () => {
// Mock implementation similar to above
});
test('should handle empty tool lists correctly', async () => {
test("should handle empty tool lists correctly", async () => {
// Mock implementation similar to above
});
});
// 6. Cleanup Handling tests
describe('close', () => {
test('should close all connections properly', async () => {
// Cleanup Handling tests
describe("close", () => {
test("should close all connections properly", async () => {
const client = new MultiServerMCPClient({
server1: {
transport: 'stdio',
command: 'python',
args: ['./script1.py'],
transport: "stdio",
command: "python",
args: ["./script1.py"],
},
server2: {
transport: 'sse',
url: 'http://localhost:8000/sse',
transport: "sse",
url: "http://localhost:8000/sse",
},
server3: {
transport: "http",
url: "http://localhost:8000/mcp",
},
});
@@ -494,119 +350,134 @@ describe('MultiServerMCPClient', () => {
await client.close();
// Verify that all transports were closed using the mock functions directly
expect(mockStdioTransportClose).toHaveBeenCalled();
expect(mockSSETransportClose).toHaveBeenCalled();
expect(StdioClientTransport.prototype.close).toHaveBeenCalled();
expect(SSEClientTransport.prototype.close).toHaveBeenCalled();
expect(StreamableHTTPClientTransport.prototype.close).toHaveBeenCalled();
});
test('should handle errors during cleanup gracefully', async () => {
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 vi.Mock).mockImplementation(() => ({
close: vi.fn().mockReturnValue(Promise.reject(new Error('Close failed'))),
(StdioClientTransport as Mock).mockImplementationOnce(() => ({
close: closeMock,
onclose: null,
}));
const client = new MultiServerMCPClient({
'test-server': {
transport: 'stdio',
command: 'python',
args: ['./script.py'],
"test-server": {
transport: "stdio",
command: "python",
args: ["./script.py"],
},
});
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",
},
});
test('should connect with useNodeEventSource option', async () => {
const client = new MultiServerMCPClient();
await client.connectToServerViaSSE(
'test-server',
'http://localhost:8000/sse',
undefined,
true
await expect(() => client.initializeConnections()).rejects.toThrow(
MCPClientError
);
// Verify that useNodeEventSource was set correctly
});
test('should connect with reconnect configuration', async () => {
const client = new MultiServerMCPClient();
const reconnect = { enabled: true, maxAttempts: 3, delayMs: 100 };
await client.connectToServerViaSSE(
'test-server',
'http://localhost:8000/sse',
undefined,
undefined,
reconnect
);
test("should handle errors during streamable HTTP cleanup gracefully", async () => {
const closeMock = vi
.fn()
.mockReturnValue(Promise.reject(new Error("Close failed")));
// Verify that reconnect configuration was set correctly
// Mock close to throw an error
(StreamableHTTPClientTransport as Mock).mockImplementationOnce(() => ({
close: closeMock,
connect: vi.fn().mockReturnValue(Promise.resolve()),
}));
const client = new MultiServerMCPClient({
"test-server": {
transport: "http",
url: "http://localhost:8000/mcp",
},
});
await client.initializeConnections();
await client.close();
expect(closeMock).toHaveBeenCalledOnce();
});
});
});
File diff suppressed because it is too large Load Diff
-109
View File
@@ -1,109 +0,0 @@
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
// Mock fs and path modules
vi.mock('fs', () => ({
existsSync: vi.fn(),
mkdirSync: vi.fn(),
writeFileSync: vi.fn(),
unlinkSync: vi.fn(),
accessSync: vi.fn(),
}));
vi.mock('path', () => ({
join: vi.fn(),
}));
const fs = await import('fs');
const path = await import('path');
const winston = await import('winston');
describe('Logger', () => {
// Store original console.warn implementation
const originalConsoleWarn = console.warn;
let consoleWarnMock: any;
beforeEach(() => {
// Clear module cache to ensure logger is reinitialized
vi.resetModules();
// Mock console.warn to capture warnings
consoleWarnMock = vi.spyOn(console, 'warn').mockImplementation((..._args) => {});
// Configure path.join to return predictable paths
(path.join as any).mockImplementation((...args: string[]) => args.join('/'));
// Reset fs mock implementation
(fs.existsSync as any).mockReset();
(fs.mkdirSync as any).mockReset();
(fs.writeFileSync as any).mockReset();
(fs.unlinkSync as any).mockReset();
});
afterEach(() => {
// Restore console.warn
consoleWarnMock.mockRestore();
console.warn = originalConsoleWarn;
});
test('should fallback to console-only logging when directory creation fails', async () => {
// Mock fs.existsSync to return false (directory doesn't exist)
(fs.existsSync as any).mockReturnValue(false);
// Mock fs.mkdirSync to throw an error
(fs.mkdirSync as any).mockImplementation(() => {
throw new Error('Permission denied');
});
// Import logger (after mocks are set up)
const logger = (await import('../src/logger.js')).default;
// Verify console warning was logged
expect(consoleWarnMock).toHaveBeenCalledWith(
expect.stringContaining('Unable to set up file logging')
);
expect(consoleWarnMock).toHaveBeenCalledWith('Falling back to console logging only');
// Ensure logger was created with only console transport
expect(logger.transports.length).toBe(1);
expect(logger.transports[0]).toBeInstanceOf(winston.transports.Console);
});
test('should fallback to console-only logging when write permission test fails', async () => {
// Mock fs.existsSync to return true (directory exists)
(fs.existsSync as any).mockReturnValue(true);
// Mock fs.writeFileSync to throw an error
(fs.writeFileSync as any).mockImplementation(() => {
throw new Error('Permission denied');
});
// Import logger (after mocks are set up)
const logger = (await import('../src/logger.js')).default;
// Verify console warning was logged
expect(consoleWarnMock).toHaveBeenCalledWith(
expect.stringContaining('Unable to set up file logging')
);
expect(consoleWarnMock).toHaveBeenCalledWith('Falling back to console logging only');
// Ensure logger was created with only console transport
expect(logger.transports.length).toBe(1);
expect(logger.transports[0]).toBeInstanceOf(winston.transports.Console);
});
test('should set up file transports when permissions are available', async () => {
// Mock all the file operations to succeed
(fs.mkdirSync as any).mockImplementation(() => true);
(fs.accessSync as any).mockImplementation(() => true);
(fs.existsSync as any).mockReturnValue(true);
(fs.writeFileSync as any).mockImplementation(() => undefined);
// Import logger directly
const loggerModule = await import('../src/logger.js');
const logger = loggerModule.default;
// Just verify logger was created - don't worry about warnings
expect(logger).toBeDefined();
expect(typeof logger.debug).toBe('function');
expect(typeof logger.info).toBe('function');
expect(typeof logger.warn).toBe('function');
expect(typeof logger.error).toBe('function');
});
});
+90
View File
@@ -0,0 +1,90 @@
import { vi } from "vitest";
// Set up mocks for external modules
vi.mock("@modelcontextprotocol/sdk/client/index.js", () => {
const clientPrototype = {
connect: vi.fn().mockReturnValue(Promise.resolve()),
listTools: vi.fn().mockReturnValue(
Promise.resolve({
tools: [
{
name: "tool1",
description: "Test tool 1",
inputSchema: { type: "object", properties: {} },
},
{
name: "tool2",
description: "Test tool 2",
inputSchema: { type: "object", properties: {} },
},
],
})
),
callTool: vi
.fn()
.mockReturnValue(
Promise.resolve({ content: [{ type: "text", text: "result" }] })
),
close: vi.fn().mockImplementation(() => Promise.resolve()),
tools: [], // Add the tools property
};
const Client = vi.fn().mockImplementation(() => clientPrototype);
Client.prototype = clientPrototype;
return {
Client,
};
});
vi.mock("@modelcontextprotocol/sdk/client/stdio.js", () => {
const stdioClientTransportPrototype = {
connect: vi.fn().mockReturnValue(Promise.resolve()),
send: vi.fn().mockReturnValue(Promise.resolve()),
close: vi.fn().mockReturnValue(Promise.resolve()),
};
const StdioClientTransport = vi.fn().mockImplementation((config) => {
return {
...stdioClientTransportPrototype,
config,
};
});
StdioClientTransport.prototype = stdioClientTransportPrototype;
return {
StdioClientTransport,
};
});
vi.mock("@modelcontextprotocol/sdk/client/sse.js", () => {
const sseClientTransportPrototype = {
connect: vi.fn().mockReturnValue(Promise.resolve()),
send: vi.fn().mockReturnValue(Promise.resolve()),
close: vi.fn().mockReturnValue(Promise.resolve()),
};
const SSEClientTransport = vi.fn().mockImplementation((config) => {
return {
...sseClientTransportPrototype,
config,
};
});
SSEClientTransport.prototype = sseClientTransportPrototype;
return {
SSEClientTransport,
};
});
vi.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => {
const streamableHTTPClientTransportPrototype = {
connect: vi.fn().mockReturnValue(Promise.resolve()),
send: vi.fn().mockReturnValue(Promise.resolve()),
close: vi.fn().mockReturnValue(Promise.resolve()),
};
const StreamableHTTPClientTransport = vi.fn().mockImplementation((config) => {
return {
...streamableHTTPClientTransportPrototype,
config,
};
});
StreamableHTTPClientTransport.prototype = streamableHTTPClientTransportPrototype;
return {
StreamableHTTPClientTransport,
};
});
+214 -56
View File
@@ -1,39 +1,136 @@
import { describe, test, expect, beforeEach, vi } from 'vitest';
const { loadMcpTools } = await import('../src/tools.js');
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");
// Create a mock client
const mockClient = {
callTool: vi.fn(),
listTools: vi.fn(),
};
describe("Simplified Tool Adapter Tests", () => {
let mockClient: MockedObject<Client>;
describe('Simplified Tool Adapter Tests', () => {
beforeEach(() => {
mockClient = {
callTool: vi.fn(),
listTools: vi.fn(),
} as MockedObject<Client>;
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' },
{ name: 'tool2', description: 'Tool 2' },
{
name: "tool1",
description: "Tool 1",
inputSchema: { type: "object", properties: {}, required: [] },
},
{
name: "tool2",
description: "Tool 2",
inputSchema: { type: "object", properties: {}, required: [] },
},
],
})
);
// Load tools
const tools = await loadMcpTools(mockClient as unknown as Parameters<typeof loadMcpTools>[0]);
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({
@@ -42,47 +139,65 @@ describe('Simplified Tool Adapter Tests', () => {
);
// Load tools
const tools = await loadMcpTools(mockClient as unknown as Parameters<typeof loadMcpTools>[0]);
const tools = await loadMcpTools(
"mockServer(should handle empty tool list)",
mockClient as Client
);
// Verify results
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' },
{ description: 'No name tool' }, // Should be filtered out
{ name: 'tool2', description: 'Tool 2' },
{
name: "tool1",
description: "Tool 1",
inputSchema: { type: "object", properties: {}, required: [] },
},
{
description: "No name tool",
inputSchema: { type: "object", properties: {}, required: [] },
},
{
name: "tool2",
description: "Tool 2",
inputSchema: { type: "object", properties: {}, required: [] },
},
],
})
);
// Load tools
const tools = await loadMcpTools(mockClient as unknown as Parameters<typeof loadMcpTools>[0]);
const tools = await loadMcpTools(
"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"],
},
},
],
@@ -91,49 +206,92 @@ describe('Simplified Tool Adapter Tests', () => {
// Load tools with content_and_artifact response format
const tools = await loadMcpTools(
mockClient as unknown as Parameters<typeof loadMcpTools>[0],
'content_and_artifact'
"mockServer(should load tools with specified response format)",
mockClient as Client
);
// Verify tool was loaded
expect(tools.length).toBe(1);
expect((tools[0] as any).responseFormat).toBe('content');
expect((tools[0] as StructuredTool).responseFormat).toBe(
"content_and_artifact"
);
// Mock the call result to check response format handling
const mockImageContent = { type: 'image', url: 'http://example.com/image.jpg' };
mockClient.callTool.mockReturnValueOnce(
const mockImageContent: ImageContent = {
type: "image",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", // valid grayscale PNG image
mimeType: "image/png",
};
const mockTextContent: TextContent = {
type: "text",
text: "Here is your image",
};
const mockEmbeddedResourceContent: EmbeddedResource = {
type: "resource",
resource: {
text: "Here is your image",
uri: "test-data://test-artifact",
mimeType: "text/plain",
},
};
const mockContent = [
mockTextContent,
mockImageContent,
mockEmbeddedResourceContent,
];
const expectedContentBlocks: MessageContentComplex[] = [
{
type: "text",
text: "Here is your image",
},
{
type: "image_url",
image_url: {
url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
},
},
];
const expectedArtifacts = [
{
type: "resource",
resource: {
text: "Here is your image",
uri: "test-data://test-artifact",
mimeType: "text/plain",
},
},
];
mockClient.callTool.mockReturnValue(
Promise.resolve({
content: [{ type: 'text', text: 'Image result' }, mockImageContent],
content: mockContent,
})
);
// 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(Array.isArray(result)).toBe(true);
expect(result[0]).toBe('Image result');
expect(result[1]).toEqual([mockImageContent]);
expect(result).toEqual(expectedContentBlocks);
// Set up a second mock response for the direct call test
mockClient.callTool.mockReturnValueOnce(
Promise.resolve({
content: [{ type: 'text', text: 'Image result' }, mockImageContent],
})
);
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",
};
// Access the tool class implementation through the prototype chain
const toolPrototype = Object.getPrototypeOf(tools[0]);
// Call the _call method directly with bound 'this' context
const directResult = await toolPrototype._call.call(tools[0], { input: 'test input' });
// call the tool directly via invoke
const toolMessageResult: ToolMessage = await tools[0].invoke(toolCall);
expect(Array.isArray(directResult)).toBe(true);
const [textContent, nonTextContent] = directResult as [string, any[]];
expect(textContent).toBe('Image result');
expect(nonTextContent.length).toBe(1);
expect(nonTextContent[0]).toEqual(mockImageContent);
expect(toolMessageResult.tool_call_id).toBe(toolCall.id);
expect(toolMessageResult.content).toEqual(expectedContentBlocks);
expect(toolMessageResult.artifact).toEqual(expectedArtifacts);
});
});
});
-71
View File
@@ -1,71 +0,0 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettierConfig from 'eslint-config-prettier';
export default tseslint.config(
{
// Set global ignores first to ensure they take precedence
ignores: ['node_modules/**', 'dist/**', 'coverage/**'],
},
eslint.configs.recommended,
...tseslint.configs.recommended,
prettierConfig,
{
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'],
rules: {
// TypeScript specific rules
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
// General rules
'no-console': ['warn', { allow: ['warn', 'error', 'info'] }],
'prefer-const': 'warn',
'no-var': 'error',
eqeqeq: ['error', 'always', { null: 'ignore' }],
'no-duplicate-imports': 'error',
'no-unused-expressions': 'error',
'no-shadow': 'off', // TypeScript has better handling with no-shadow
'@typescript-eslint/no-shadow': 'warn',
// Import rules
'import/no-unresolved': 'off', // TypeScript handles this
'import/prefer-default-export': 'off',
'import/extensions': 'off',
// Formatting (Prettier will handle most of this)
'max-len': [
'warn',
{
code: 100,
ignoreUrls: true,
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreRegExpLiterals: true,
ignoreComments: true,
},
],
},
// Additional ignores for safety
ignores: ['**/*.js', '**/*.cjs'],
},
{
files: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**/*.ts'],
rules: {
// Relaxed rules for test files
'@typescript-eslint/no-explicit-any': 'off',
'no-console': 'off',
},
}
);
+16 -50
View File
@@ -1,70 +1,36 @@
# LangChainJS-MCP-Adapters Examples
This directory contains examples demonstrating how to use the LangChainJS-MCP-Adapters library with various MCP servers, with a focus on the Firecrawl MCP server.
This directory contains examples demonstrating how to use the `@langchain/mcp-adapters` library with various MCP servers
## Running the Examples
We've added dedicated npm scripts to make it easy to run the examples:
```bash
# Build all examples
npm run build:examples
yarn build:examples
# Run specific examples
npm run example:default # Uses automatic loading from mcp.json
npm run example:custom # Uses a custom configuration file
npm run example:multiple # Uses multiple MCP servers from a config file
npm run example:mixed # Loads one server from config and another directly in code
npm run example:enhanced # Demonstrates all enhanced configuration features
# Run specific example
cd examples && npx -y tsx firecrawl_custom_config_example.ts
```
## Example Descriptions
### 1. Default Configuration (`firecrawl_default_config_example.ts`)
### Filesystem LangGraph Example (`filesystem_langgraph_example.ts`)
Demonstrates using the Filesystem MCP server with LangGraph to create a structured workflow for complex file operations. The example creates a graph-based agent that can perform various file operations like creating multiple files, reading files, creating directory structures, and organizing files.
Demonstrates the automatic loading of MCP server configurations from the default `mcp.json` file in the root directory. This example showcases how to:
### Firecrawl - Custom Configuration (`firecrawl_custom_config_example.ts`)
Shows how to initialize the Firecrawl MCP server with a custom configuration. The example sets up a connection to Firecrawl using SSE transport, loads tools from the server, and creates a React agent to perform web scraping tasks and find news about artificial intelligence.
- Create a client that automatically loads from `mcp.json`
- Initialize connections to all servers in the configuration
- Filter tools by server name
- Use tools with LangGraph
### Firecrawl - Multiple Servers (`firecrawl_multiple_servers_example.ts`)
Demonstrates how to use multiple MCP servers simultaneously by configuring both Firecrawl for web scraping and a Math server for calculations. The example creates a React agent that can use tools from both servers to answer queries involving both math calculations and web content retrieval.
### 2. Custom Configuration (`firecrawl_custom_config_example.ts`)
### LangGraph - Complex Config (`langgraph_complex_config_example.ts`)
Illustrates using different configuration files to set up connections to MCP servers, with a focus on the Math server. This example shows how to parse JSON configuration files, connect to a Math server directly, and create a LangGraph workflow that can perform mathematical operations using MCP tools.
Shows how to create and use a custom configuration file specifically for the Firecrawl server. This example:
### LangGraph - Simple Config (`langgraph_example.ts`)
Shows a straightforward integration of LangGraph with MCP tools, creating a flexible agent workflow. The example demonstrates how to set up a graph-based structure with separate nodes for LLM reasoning and tool execution, with conditional routing between nodes based on whether tool calls are needed.
- Creates a custom configuration file
- Initializes the client from this file
- Shows environment variable configuration
### 3. Multiple Servers (`firecrawl_multiple_servers_example.ts`)
Demonstrates loading multiple MCP servers (Firecrawl and Math) from a single configuration file. This example:
- Creates a configuration with multiple servers
- Loads and uses tools from different servers in the same agent
### 4. Mixed Loading (`firecrawl_mixed_loading_example.ts`)
Shows a mixed approach to loading MCP servers - one from a configuration file and another directly in code. This example:
- Loads the math server from a config file
- Adds the Firecrawl server via direct code
- Works with tools from both servers
### 5. Enhanced Configuration (`firecrawl_enhanced_config_example.ts`)
Showcases all the enhanced configuration features, including:
- Automatic loading from the default configuration
- Adding multiple configuration sources
- Environment variable substitution
- Adding servers directly in code
## Configuration Files
- `mcp.json` - Default configuration file in the root directory
- Custom configuration files created in the examples
### Launching a Containerized MCP Server (`mcp_over_docker_example.ts`)
Shows how to run an MCP server inside a Docker container. This example configures a connection to a containerized Filesystem MCP server with appropriate volume mounting, demonstrating how to use Docker to isolate and run MCP servers while still allowing file operations.
## Requirements
+2 -2
View File
@@ -11,8 +11,8 @@
},
"math-server": {
"transport": "stdio",
"command": "python",
"args": ["./math_server.py"]
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-math"]
}
}
}
+173
View File
@@ -0,0 +1,173 @@
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { randomUUID } from "node:crypto";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
export async function main() {
const server = new McpServer({
name: "backwards-compatible-server",
version: "1.0.0",
});
const calcSchema = { a: z.number(), b: z.number() };
server.tool(
"add",
"Adds two numbers together",
calcSchema,
async ({ a, b }: { a: number; b: number }, extra) => {
return {
content: [{ type: "text", text: `${a + b}` }],
};
}
);
server.tool(
"subtract",
"Subtracts two numbers",
calcSchema,
async ({ a, b }: { a: number; b: number }, extra) => {
return { content: [{ type: "text", text: `${a - b}` }] };
}
);
server.tool(
"multiply",
"Multiplies two numbers",
calcSchema,
async ({ a, b }: { a: number; b: number }, extra) => {
return { content: [{ type: "text", text: `${a * b}` }] };
}
);
server.tool(
"divide",
"Divides two numbers",
calcSchema,
async ({ a, b }: { a: number; b: number }, extra) => {
return { content: [{ type: "text", text: `${a / b}` }] };
}
);
const app = express();
app.use(express.json());
// Store transports for each session type
const transports = {
streamable: {} as Record<string, StreamableHTTPServerTransport>,
sse: {} as Record<string, SSEServerTransport>,
};
// Modern Streamable HTTP endpoint
app.post("/mcp", async (req, res) => {
// Check for existing session ID
const sessionId = req.headers["mcp-session-id"] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports.streamable[sessionId]) {
// Reuse existing transport
transport = transports.streamable[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
// Store the transport by session ID
transports.streamable[sessionId] = transport;
},
});
// Clean up transport when closed
transport.onclose = () => {
if (transport.sessionId) {
delete transports.streamable[transport.sessionId];
}
};
// Connect to the MCP server
await server.connect(transport);
} else {
// Invalid request
console.error(
"Invalid Streamable HTTP request: ",
JSON.stringify(req.body, null, 2)
);
res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Bad Request: No valid session ID provided",
},
id: null,
});
return;
}
// Handle the request
await transport.handleRequest(req, res, req.body);
});
// Reusable handler for GET and DELETE requests
const handleSessionRequest = async (
req: express.Request,
res: express.Response
) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (!sessionId || !transports.streamable[sessionId]) {
console.error(
"Invalid Streamable HTTP request (invalid/missing session ID): ",
JSON.stringify(req.body, null, 2)
);
res.status(400).send("Invalid or missing session ID");
return;
}
const transport = transports.streamable[sessionId];
await transport.handleRequest(req, res);
};
app.get("/mcp", handleSessionRequest);
app.delete("/mcp", handleSessionRequest);
// Legacy SSE endpoint for older clients
app.get("/sse", async (req, res) => {
// Create SSE transport for legacy clients
const transport = new SSEServerTransport("/messages", res);
transports.sse[transport.sessionId] = transport;
res.on("close", () => {
delete transports.sse[transport.sessionId];
});
await server.connect(transport);
});
// Legacy message endpoint for older clients
app.post("/messages", async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.sse[sessionId];
if (transport) {
await transport.handlePostMessage(req, res, req.body);
} else {
console.error("No transport found for sessionId", sessionId);
res.status(400).send("No transport found for sessionId");
}
});
app.listen(3000);
}
if (typeof require !== "undefined" && require.main === module) {
main().catch(console.error);
}
if (
import.meta.url === process.argv[1] ||
import.meta.url === `file://${process.argv[1]}`
) {
main().catch(console.error);
}
+204
View File
@@ -0,0 +1,204 @@
/**
* Calculator MCP Server with LangGraph Example
*
* This example demonstrates how to use the Calculator MCP server with LangGraph
* to create a structured workflow for simple calculations.
*
* The graph-based approach allows:
* 1. Clear separation of responsibilities (reasoning vs execution)
* 2. Conditional routing based on tool calls
* 3. Structured handling of complex multi-tool operations
*/
/* eslint-disable no-console */
import { ChatOpenAI } from "@langchain/openai";
import {
StateGraph,
END,
START,
MessagesAnnotation,
} from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import {
HumanMessage,
AIMessage,
SystemMessage,
isHumanMessage,
} from "@langchain/core/messages";
import dotenv from "dotenv";
import { main as calculatorServerMain } from "./calculator_server_shttp_sse.js";
// MCP client imports
import { MultiServerMCPClient } from "../src/index.js";
// Load environment variables from .env file
dotenv.config();
const transportType = process.env.MCP_TRANSPORT_TYPE === "sse" ? "sse" : "http";
export async function runExample(client?: MultiServerMCPClient) {
try {
console.log("Initializing MCP client...");
void calculatorServerMain();
// Wait for the server to start
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
// Create a client with configurations for the calculator server
// eslint-disable-next-line no-param-reassign
client =
client ??
new MultiServerMCPClient({
calculator: {
url: `http://localhost:3000/${
transportType === "sse" ? "sse" : "mcp"
}`,
},
});
console.log("Connected to server");
// Get all tools (flattened array is the default now)
const mcpTools = await client.getTools();
if (mcpTools.length === 0) {
throw new Error("No tools found");
}
console.log(
`Loaded ${mcpTools.length} MCP tools: ${mcpTools
.map((tool) => tool.name)
.join(", ")}`
);
// Create an OpenAI model with tools attached
const systemMessage = `You are an assistant that helps users with calculations.
You have access to tools that can add, subtract, multiply, and divide numbers. Use
these tools to answer the user's questions.`;
const model = new ChatOpenAI({
modelName: process.env.OPENAI_MODEL_NAME || "gpt-4o-mini",
temperature: 0.7,
}).bindTools(mcpTools);
// Create a tool node for the LangGraph
const toolNode = new ToolNode(mcpTools);
// ================================================
// Create a LangGraph agent flow
// ================================================
console.log("\n=== CREATING LANGGRAPH AGENT FLOW ===");
// Define the function that calls the model
const llmNode = async (state: typeof MessagesAnnotation.State) => {
console.log(`Calling LLM with ${state.messages.length} messages`);
// Add system message if it's the first call
let { messages } = state;
if (messages.length === 1 && isHumanMessage(messages[0])) {
messages = [new SystemMessage(systemMessage), ...messages];
}
const response = await model.invoke(messages);
return { messages: [response] };
};
// Create a new graph with MessagesAnnotation
const workflow = new StateGraph(MessagesAnnotation)
// Add the nodes to the graph
.addNode("llm", llmNode)
.addNode("tools", toolNode)
// Add edges - these define how nodes are connected
.addEdge(START, "llm")
.addEdge("tools", "llm")
// Conditional routing to end or continue the tool loop
.addConditionalEdges("llm", (state) => {
const lastMessage = state.messages[state.messages.length - 1];
// Cast to AIMessage to access tool_calls property
const aiMessage = lastMessage as AIMessage;
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) {
console.log("Tool calls detected, routing to tools node");
// Log what tools are being called
const toolNames = aiMessage.tool_calls
.map((tc) => tc.name)
.join(", ");
console.log(`Tools being called: ${toolNames}`);
return "tools";
}
// If there are no tool calls, we're done
console.log("No tool calls, ending the workflow");
return END;
});
// Compile the graph
const app = workflow.compile();
// Define examples to run
const examples = [
{
name: "Add 1 and 2",
query: "What is 1 + 2?",
},
{
name: "Subtract 1 from 2",
query: "What is 2 - 1?",
},
{
name: "Multiply 1 and 2",
query: "What is 1 * 2?",
},
{
name: "Divide 1 by 2",
query: "What is 1 / 2?",
},
];
// Run the examples
console.log("\n=== RUNNING LANGGRAPH AGENT ===");
for (const example of examples) {
console.log(`\n--- Example: ${example.name} ---`);
console.log(`Query: ${example.query}`);
// Run the LangGraph agent
const result = await app.invoke({
messages: [new HumanMessage(example.query)],
});
// Display the final answer
const finalMessage = result.messages[result.messages.length - 1];
console.log(`\nResult: ${finalMessage.content}`);
}
} catch (error) {
console.error("Error:", error);
process.exit(1); // Exit with error code
} finally {
if (client) {
await client.close();
console.log("Closed all MCP connections");
}
// Exit process after a short delay to allow for cleanup
setTimeout(() => {
console.log("Example completed, exiting process.");
process.exit(0);
}, 500);
}
}
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
if (isMainModule) {
runExample().catch((error) => console.error("Setup error:", error));
}
+2 -6
View File
@@ -1,12 +1,8 @@
{
"servers": {
"math": {
"command": "python",
"args": ["./examples/math_server.py"],
"env": {
"DEBUG": "true",
"PYTHONPATH": "./examples"
}
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-math"],
},
"weather": {
"transport": "sse",
-193
View File
@@ -1,193 +0,0 @@
/**
* Configuration Test with Math Server using LangGraph
*
* This example demonstrates using configuration files (auth_mcp.json and complex_mcp.json)
* and directly connecting to the local math_server.py script using LangGraph.
*/
import { ChatOpenAI } from '@langchain/openai';
import path from 'path';
import fs from 'fs';
import dotenv from 'dotenv';
import logger from '../src/logger.js';
import { StateGraph, END, START, MessagesAnnotation } from '@langchain/langgraph';
import { ToolNode } from '@langchain/langgraph/prebuilt';
import { HumanMessage, AIMessage, SystemMessage } from '@langchain/core/messages';
import { StructuredToolInterface } from '@langchain/core/tools';
import { z } from 'zod';
// MCP client imports
import { MultiServerMCPClient } from '../src/index.js';
// Load environment variables from .env file
dotenv.config();
/**
* This example demonstrates using multiple configuration files to
* connect to different MCP servers and use their tools with LangGraph
*/
async function runConfigTest() {
try {
// Log when we start
logger.info('Starting test with configuration files...');
// Step 1: Load and verify auth_mcp.json configuration (just testing parsing)
logger.info('Parsing auth_mcp.json configuration...');
const authConfigPath = path.join(process.cwd(), 'examples', 'auth_mcp.json');
if (!fs.existsSync(authConfigPath)) {
throw new Error(`Configuration file not found: ${authConfigPath}`);
}
// Load the auth configuration to verify it parses correctly
const authConfig = JSON.parse(fs.readFileSync(authConfigPath, 'utf-8'));
logger.info('Successfully parsed auth_mcp.json with the following servers:');
logger.info('Servers:', Object.keys(authConfig.servers));
// Print auth headers (redacted for security) to verify they're present
Object.entries(authConfig.servers).forEach(([serverName, serverConfig]: [string, any]) => {
if (serverConfig.headers) {
logger.info(
`Server ${serverName} has headers:`,
Object.keys(serverConfig.headers).map(key => `${key}: ***`)
);
}
});
// Step 2: Load and verify complex_mcp.json configuration
logger.info('Parsing complex_mcp.json configuration...');
const complexConfigPath = path.join(process.cwd(), 'examples', 'complex_mcp.json');
if (!fs.existsSync(complexConfigPath)) {
throw new Error(`Configuration file not found: ${complexConfigPath}`);
}
const complexConfig = JSON.parse(fs.readFileSync(complexConfigPath, 'utf-8'));
logger.info('Successfully parsed complex_mcp.json with the following servers:');
logger.info('Servers:', Object.keys(complexConfig.servers));
// Step 3: Connect directly to the math server using explicit path
logger.info('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')],
},
});
// Initialize connection to the math server
await client.initializeConnections();
// Get tools from the math server
const mcpTools = client.getTools() as StructuredToolInterface<z.ZodObject<any>>[];
logger.info(`Loaded ${mcpTools.length} tools from math server`);
// Log the names of available tools
const toolNames = mcpTools.map(tool => tool.name);
logger.info('Available tools:', toolNames.join(', '));
// Create an OpenAI model for the agent
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);
// Define the function that calls the model
const llmNode = async (state: typeof MessagesAnnotation.State) => {
logger.info('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
workflow.addNode('llm', llmNode);
workflow.addNode('tools', toolNode);
// Add edges - need to cast to any to fix TypeScript errors
workflow.addEdge(START as any, 'llm' as any);
workflow.addEdge('tools' as any, 'llm' as any);
// Add conditional logic to determine the next step
workflow.addConditionalEdges('llm' as any, state => {
const lastMessage = state.messages[state.messages.length - 1];
// If the last message has tool calls, we need to execute the tools
const aiMessage = lastMessage as AIMessage;
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) {
logger.info('Tool calls detected, routing to tools node');
return 'tools' as any;
}
// If there are no tool calls, we're done
logger.info('No tool calls, ending the workflow');
return END as any;
});
// Compile the graph
const app = workflow.compile();
// Define test queries that use math tools
const testQueries = [
// Basic math queries
'What is 5 + 3?',
'What is 7 * 9?',
'If I have 10 and add 15 to it, then multiply the result by 2, what do I get?',
];
// Run each test query
for (const query of testQueries) {
logger.info(`\n=== Running query: "${query}" ===`);
try {
// Create initial messages with a system message and the user query
const messages = [
new SystemMessage(
'You are a helpful assistant that can use tools to solve math problems.'
),
new HumanMessage(query),
];
// Run the LangGraph workflow
const result = await app.invoke({ messages });
// Get the last AI message as the response
const lastMessage = result.messages.filter(message => message._getType() === 'ai').pop();
logger.info(`\nFinal Answer: ${lastMessage?.content}`);
} catch (error) {
logger.error(`Error processing query "${query}":`, error);
}
}
// Close all connections
logger.info('\nClosing connections...');
await client.close();
logger.info('Test completed successfully');
} catch (error) {
logger.error('Error running test:', error);
}
}
// Run the test
runConfigTest()
.then(() => {
logger.info('Configuration test completed successfully');
process.exit(0);
})
.catch(error => {
logger.error('Error running configuration test:', error);
process.exit(1);
});
+95 -82
View File
@@ -10,19 +10,27 @@
* 3. Structured handling of complex multi-file operations
*/
import { ChatOpenAI } from '@langchain/openai';
import { StateGraph, END, START, MessagesAnnotation } from '@langchain/langgraph';
import { ToolNode } from '@langchain/langgraph/prebuilt';
import { HumanMessage, AIMessage, SystemMessage } from '@langchain/core/messages';
import { StructuredToolInterface } from '@langchain/core/tools';
import { z } from 'zod';
import dotenv from 'dotenv';
import logger from '../src/logger.js';
import fs from 'fs';
import path from 'path';
/* 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 fs from "fs";
import path from "path";
// MCP client imports
import { MultiServerMCPClient } from '../src/index.js';
import { MultiServerMCPClient } from "../src/index.js";
// Load environment variables from .env file
dotenv.config();
@@ -31,38 +39,39 @@ dotenv.config();
* Example demonstrating how to use MCP filesystem tools with LangGraph agent flows
* This example focuses on file operations like reading multiple files and writing files
*/
async function runExample() {
let client: MultiServerMCPClient | null = null;
export async function runExample(client?: MultiServerMCPClient) {
try {
logger.info('Initializing MCP client...');
console.log("Initializing MCP client...");
// Create a client with configurations for the filesystem server
client = new MultiServerMCPClient({
filesystem: {
transport: 'stdio',
command: 'npx',
args: [
'-y',
'@modelcontextprotocol/server-filesystem',
'./examples/filesystem_test', // This directory needs to exist
],
},
});
// eslint-disable-next-line no-param-reassign
client =
client ??
new MultiServerMCPClient({
filesystem: {
transport: "stdio",
command: "npx",
args: [
"-y",
"@modelcontextprotocol/server-filesystem",
"./examples/filesystem_test", // This directory needs to exist
],
},
});
// Initialize connections to the server
await client.initializeConnections();
logger.info('Connected to server');
console.log("Connected to server");
// Get all tools (flattened array is the default now)
const mcpTools = client.getTools() as StructuredToolInterface<z.ZodObject<any>>[];
const mcpTools = await client.getTools();
if (mcpTools.length === 0) {
throw new Error('No tools found');
throw new Error("No tools found");
}
logger.info(
`Loaded ${mcpTools.length} MCP tools: ${mcpTools.map(tool => tool.name).join(', ')}`
console.log(
`Loaded ${mcpTools.length} MCP tools: ${mcpTools
.map((tool) => tool.name)
.join(", ")}`
);
// Create an OpenAI model with tools attached
@@ -76,7 +85,7 @@ For file writing operations, format the content properly based on the file type.
For reading multiple files, you can use the read_multiple_files tool.`;
const model = new ChatOpenAI({
modelName: process.env.OPENAI_MODEL_NAME || 'gpt-4-turbo-preview',
modelName: process.env.OPENAI_MODEL_NAME || "gpt-4o-mini",
temperature: 0,
}).bindTools(mcpTools);
@@ -86,15 +95,15 @@ For reading multiple files, you can use the read_multiple_files tool.`;
// ================================================
// Create a LangGraph agent flow
// ================================================
logger.info('\n=== CREATING LANGGRAPH AGENT FLOW ===');
console.log("\n=== CREATING LANGGRAPH AGENT FLOW ===");
// Define the function that calls the model
const llmNode = async (state: typeof MessagesAnnotation.State) => {
logger.info(`Calling LLM with ${state.messages.length} messages`);
console.log(`Calling LLM with ${state.messages.length} messages`);
// Add system message if it's the first call
let messages = state.messages;
if (messages.length === 1 && messages[0] instanceof HumanMessage) {
let { messages } = state;
if (messages.length === 1 && isHumanMessage(messages[0])) {
messages = [new SystemMessage(systemMessage), ...messages];
}
@@ -103,36 +112,38 @@ For reading multiple files, you can use the read_multiple_files tool.`;
};
// Create a new graph with MessagesAnnotation
const workflow = new StateGraph(MessagesAnnotation);
const workflow = new StateGraph(MessagesAnnotation)
// Add the nodes to the graph
workflow.addNode('llm', llmNode);
workflow.addNode('tools', toolNode);
// Add the nodes to the graph
.addNode("llm", llmNode)
.addNode("tools", toolNode)
// Add edges - these define how nodes are connected
workflow.addEdge(START as any, 'llm' as any);
workflow.addEdge('tools' as any, 'llm' as any);
// Add edges - these define how nodes are connected
.addEdge(START, "llm")
.addEdge("tools", "llm")
// Conditional routing to end or continue the tool loop
workflow.addConditionalEdges('llm' as any, state => {
const lastMessage = state.messages[state.messages.length - 1];
// 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) {
logger.info('Tool calls detected, routing to tools node');
// 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(', ');
logger.info(`Tools being called: ${toolNames}`);
// Log what tools are being called
const toolNames = aiMessage.tool_calls
.map((tc) => tc.name)
.join(", ");
console.log(`Tools being called: ${toolNames}`);
return 'tools' as any;
}
return "tools";
}
// If there are no tool calls, we're done
logger.info('No tool calls, ending the workflow');
return END as any;
});
// 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();
@@ -140,33 +151,33 @@ For reading multiple files, you can use the read_multiple_files tool.`;
// Define examples to run
const examples = [
{
name: 'Write multiple files',
name: "Write multiple files",
query:
"Create two files: 'notes.txt' with content 'Important meeting on Thursday' and 'reminder.txt' with content 'Call John about the project'.",
},
{
name: 'Read multiple files',
name: "Read multiple files",
query:
"Read both notes.txt and reminder.txt files and create a summary file called 'summary.txt' that contains information from both files.",
},
{
name: 'Create directory structure',
name: "Create directory structure",
query:
"Create a directory structure for a simple web project. Make a 'project' directory with subdirectories for 'css', 'js', and 'images'. Add an index.html file in the main project directory with a basic HTML5 template.",
},
{
name: 'Search and organize',
name: "Search and organize",
query:
"Search for all .txt files and create a new directory called 'text_files', then list the names of all found text files in a new file called 'text_files/index.txt'.",
},
];
// Run the examples
console.log('\n=== RUNNING LANGGRAPH AGENT ===');
console.log("\n=== RUNNING LANGGRAPH AGENT ===");
for (const example of examples) {
logger.info(`\n--- Example: ${example.name} ---`);
logger.info(`Query: ${example.query}`);
console.log(`\n--- Example: ${example.name} ---`);
console.log(`Query: ${example.query}`);
// Run the LangGraph agent
const result = await app.invoke({
@@ -175,36 +186,36 @@ For reading multiple files, you can use the read_multiple_files tool.`;
// Display the final answer
const finalMessage = result.messages[result.messages.length - 1];
logger.info(`\nResult: ${finalMessage.content}`);
console.log(`\nResult: ${finalMessage.content}`);
// Let's list the directory to see the changes
logger.info('\nDirectory listing after operations:');
console.log("\nDirectory listing after operations:");
try {
const listResult = await app.invoke({
messages: [
new HumanMessage(
'List all files and directories in the current directory and show their structure.'
"List all files and directories in the current directory and show their structure."
),
],
});
const listMessage = listResult.messages[listResult.messages.length - 1];
logger.info(listMessage.content);
console.log(listMessage.content);
} catch (error) {
logger.error('Error listing directory:', error);
console.error("Error listing directory:", error);
}
}
} catch (error) {
logger.error('Error:', error);
console.error("Error:", error);
process.exit(1); // Exit with error code
} finally {
if (client) {
await client.close();
logger.info('Closed all MCP connections');
console.log("Closed all MCP connections");
}
// Exit process after a short delay to allow for cleanup
setTimeout(() => {
logger.info('Example completed, exiting process.');
console.log("Example completed, exiting process.");
process.exit(0);
}, 500);
}
@@ -214,15 +225,17 @@ For reading multiple files, you can use the read_multiple_files tool.`;
* Create a directory for our tests if it doesn't exist yet
*/
async function setupTestDirectory() {
const testDir = path.join('./examples', 'filesystem_test');
const testDir = path.join("./examples", "filesystem_test");
if (!fs.existsSync(testDir)) {
fs.mkdirSync(testDir, { recursive: true });
logger.info(`Created test directory: ${testDir}`);
console.log(`Created test directory: ${testDir}`);
}
}
// Set up test directory and run the example
setupTestDirectory()
.then(() => runExample())
.catch(error => logger.error('Setup error:', error));
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
if (isMainModule) {
setupTestDirectory()
.then(() => runExample())
.catch((error) => console.error("Setup error:", error));
}
+31 -62
View File
@@ -5,46 +5,30 @@
* And getting tools from the Firecrawl server with automatic initialization
*/
import { ChatOpenAI } from '@langchain/openai';
import { HumanMessage } from '@langchain/core/messages';
import { StructuredToolInterface } from '@langchain/core/tools';
import { z } from 'zod';
import dotenv from 'dotenv';
import * as fs from 'fs';
import * as path from 'path';
import logger from '../src/logger.js';
import { createReactAgent } from '@langchain/langgraph/prebuilt';
/* 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';
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
@@ -54,38 +38,27 @@ async function runExample() {
// Add a timeout to prevent the process from hanging indefinitely
const timeout = setTimeout(() => {
console.error('Example timed out after 30 seconds');
console.error("Example timed out after 30 seconds");
process.exit(1);
}, 30000);
try {
// Create a custom configuration file
const configPath = createConfigFile();
logger.info(`Created custom configuration file at: ${configPath}`);
// Initialize the MCP client with the custom configuration
logger.info('Initializing MCP client from custom configuration file...');
client = MultiServerMCPClient.fromConfigFile(configPath);
// Connect to the servers
await client.initializeConnections();
logger.info('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() as StructuredToolInterface<z.ZodObject<any>>[];
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');
throw new Error("No Firecrawl tools found");
}
logger.info(`Found ${firecrawlTools.length} Firecrawl tools`);
console.log(`Found ${firecrawlTools.length} Firecrawl tools`);
// Initialize the LLM
const model = new ChatOpenAI({
modelName: process.env.OPENAI_MODEL_NAME || 'gpt-3.5-turbo',
modelName: process.env.OPENAI_MODEL_NAME || "gpt-3.5-turbo",
temperature: 0,
});
@@ -97,43 +70,39 @@ async function runExample() {
// Define a query for testing Firecrawl
const query =
'Find the latest news about artificial intelligence and summarize the top 3 stories';
"Find the latest news about artificial intelligence and summarize the top 3 stories";
logger.info(`Running agent with query: ${query}`);
console.log(`Running agent with query: ${query}`);
// Run the agent
const result = await agent.invoke({
messages: [new HumanMessage(query)],
});
logger.info('Agent execution completed');
console.log('\nFinal output:');
console.log("Agent execution completed");
console.log("\nFinal output:");
console.log(result);
// Clear the timeout since the example completed successfully
clearTimeout(timeout);
// Clean up the temporary configuration file
fs.unlinkSync(configPath);
logger.info('Removed temporary configuration file');
} catch (error) {
logger.error('Error in example:', error);
console.error("Error in example:", error);
} finally {
// Close all MCP connections
if (client) {
logger.info('Closing all MCP connections...');
console.log("Closing all MCP connections...");
await client.close();
logger.info('All MCP connections closed');
console.log("All MCP connections closed");
}
// Clear the timeout if it hasn't fired yet
clearTimeout(timeout);
// Complete the example
logger.info('Example execution completed');
console.log("Example execution completed");
process.exit(0);
}
}
// Run the example
runExample();
runExample().catch(console.error);
@@ -1,105 +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
*/
import { ChatOpenAI } from '@langchain/openai';
import { HumanMessage } from '@langchain/core/messages';
import { StructuredToolInterface } from '@langchain/core/tools';
import { z } from 'zod';
import dotenv from 'dotenv';
import logger, { enableLogging } from '../src/logger.js';
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() {
// Enable logging for testing
enableLogging('debug');
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 {
logger.info('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();
logger.info('Connected to servers from default configuration');
// Get Firecrawl tools specifically
const mcpTools = client.getTools() as StructuredToolInterface<z.ZodObject<any>>[];
const firecrawlTools = mcpTools.filter(
tool => client!.getServerForTool(tool.name) === 'firecrawl'
);
if (firecrawlTools.length === 0) {
throw new Error('No Firecrawl tools found');
}
logger.info(`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';
logger.info(`Running agent with query: ${query}`);
// Run the agent
const result = await agent.invoke({
messages: [new HumanMessage(query)],
});
logger.info('Agent execution completed');
console.log('\nFinal output:');
console.log(result);
// Clear the timeout since the example completed successfully
clearTimeout(timeout);
} catch (error) {
logger.error('Error in example:', error);
} finally {
// Close all MCP connections
if (client) {
logger.info('Closing all MCP connections...');
await client.close();
logger.info('All MCP connections closed');
}
// Clear the timeout if it hasn't fired yet
clearTimeout(timeout);
// Complete the example
logger.info('Example execution completed');
process.exit(0);
}
}
// Run the example
runExample();
@@ -1,232 +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
*/
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 { StructuredToolInterface } from '@langchain/core/tools';
import { z } from 'zod';
import dotenv from 'dotenv';
import fs from 'fs';
import path from 'path';
import logger from '../src/logger.js';
// 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));
logger.info(`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();
logger.info('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();
logger.info('Connected to servers from all configurations');
// Get all tools from all servers
const mcpTools = client.getTools() as StructuredToolInterface<z.ZodObject<any>>[];
if (mcpTools.length === 0) {
throw new Error('No tools found');
}
logger.info(`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) {
logger.warn('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) {
logger.info(
`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');
}
}
logger.info(
`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
workflow.addNode('llm', llmNode);
workflow.addNode('tools', toolNode);
// Add edges - these define how nodes are connected
workflow.addEdge(START as any, 'llm' as any);
workflow.addEdge('tools' as any, 'llm' as any);
// Conditional routing to end or continue the tool loop
workflow.addConditionalEdges('llm' as any, 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' as any;
}
console.log('No tool calls, ending the workflow');
return END as any;
});
// 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((_, 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])) as any;
// 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);
logger.info(`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();
-197
View File
@@ -1,197 +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
*/
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 { StructuredToolInterface } from '@langchain/core/tools';
import { z } from 'zod';
import dotenv from 'dotenv';
import fs from 'fs';
import path from 'path';
import logger from '../src/logger.js';
// 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));
logger.info(`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();
logger.info('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();
logger.info('Connected to math server from configuration');
// Now add the firecrawl server directly in code
logger.info('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',
});
logger.info('Connected to firecrawl server directly');
// Get all tools from all servers
const mcpTools = client.getTools() as StructuredToolInterface<z.ZodObject<any>>[];
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'
);
logger.info(
`Loaded ${mathTools.length} math tools: ${mathTools.map(tool => tool.name).join(', ')}`
);
logger.info(
`Loaded ${firecrawlTools.length} firecrawl tools: ${firecrawlTools.map(tool => tool.name).join(', ')}`
);
logger.info(`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
workflow.addNode('llm', llmNode);
workflow.addNode('tools', toolNode);
// Add edges - these define how nodes are connected
workflow.addEdge(START as any, 'llm' as any);
workflow.addEdge('tools' as any, 'llm' as any);
// Conditional routing to end or continue the tool loop
workflow.addConditionalEdges('llm' as any, 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' as any;
}
console.log('No tool calls, ending the workflow');
return END as any;
});
// 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);
logger.info(`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();
+43 -79
View File
@@ -5,57 +5,37 @@
* It includes both the Firecrawl server for web scraping and the Math server for calculations.
*/
import { ChatOpenAI } from '@langchain/openai';
import { createReactAgent } from '@langchain/langgraph/prebuilt';
import { HumanMessage } from '@langchain/core/messages';
import { StructuredToolInterface } from '@langchain/core/tools';
import { z } from 'zod';
import dotenv from 'dotenv';
import fs from 'fs';
import path from 'path';
import logger from '../src/logger.js';
/* eslint-disable no-console */
import { ChatOpenAI } from "@langchain/openai";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { HumanMessage } from "@langchain/core/messages";
import dotenv from "dotenv";
// MCP client imports
import { MultiServerMCPClient, enableLogging } 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));
logger.info(`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
@@ -65,42 +45,38 @@ async function runExample() {
let client: MultiServerMCPClient | null = null;
try {
// Enable logging for better visibility
enableLogging('info');
// Create the multiple servers configuration file
createMultipleServersConfigFile();
logger.info('Initializing MCP client from multiple servers configuration file...');
console.log(
"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();
logger.info('Connected to servers from multiple servers configuration');
console.log("Connected to servers from multiple servers configuration");
// Get all tools from all servers
const mcpTools = client.getTools() as StructuredToolInterface<z.ZodObject<any>>[];
const mcpTools = await client.getTools();
if (mcpTools.length === 0) {
throw new Error('No tools found');
throw new Error("No tools found");
}
logger.info(
`Loaded ${mcpTools.length} MCP tools: ${mcpTools.map(tool => tool.name).join(', ')}`
console.log(
`Loaded ${mcpTools.length} MCP tools: ${mcpTools
.map((tool) => tool.name)
.join(", ")}`
);
// Create an OpenAI model
const model = new ChatOpenAI({
modelName: process.env.OPENAI_MODEL_NAME || 'gpt-4o',
modelName: process.env.OPENAI_MODEL_NAME || "gpt-4o",
temperature: 0,
});
// ================================================
// Create a React agent
// ================================================
console.log('\n=== CREATING REACT AGENT ===');
console.log("\n=== CREATING REACT AGENT ===");
// Create the React agent
const agent = createReactAgent({
@@ -110,13 +86,13 @@ async function runExample() {
// Define queries that will use both servers
const queries = [
'What is 25 multiplied by 18?',
'Scrape the content from https://example.com and count how many paragraphs are there',
'If I have 42 items and each costs $7.50, what is the total cost?',
"What is 25 multiplied by 18?",
"Scrape the content from https://example.com and count how many paragraphs are there",
"If I have 42 items and each costs $7.50, what is the total cost?",
];
// Test the React agent with the queries
console.log('\n=== RUNNING REACT AGENT ===');
console.log("\n=== RUNNING REACT AGENT ===");
for (const query of queries) {
console.log(`\nQuery: ${query}`);
@@ -131,28 +107,16 @@ async function runExample() {
console.log(`\nResult: ${finalMessage.content}`);
}
} catch (error) {
console.error('Error:', 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');
console.log("\nClosed all connections");
}
// Clean up our config file
if (fs.existsSync(multipleServersConfigPath)) {
fs.unlinkSync(multipleServersConfigPath);
logger.info(`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);
}
}
// Run the example
runExample();
runExample().catch(console.error);
@@ -0,0 +1,215 @@
/**
* Configuration Test with Math Server using LangGraph
*
* This example demonstrates using configuration files (auth_mcp.json and complex_mcp.json)
* and directly connecting to the local math_server.py script using LangGraph.
*/
/* eslint-disable no-console */
import { ChatOpenAI } from "@langchain/openai";
import path from "path";
import fs from "fs";
import dotenv from "dotenv";
import {
StateGraph,
END,
START,
MessagesAnnotation,
} from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import {
HumanMessage,
AIMessage,
SystemMessage,
} from "@langchain/core/messages";
// MCP client imports
import { MultiServerMCPClient } from "../src/index.js";
// Load environment variables from .env file
dotenv.config();
/**
* This example demonstrates using multiple configuration files to
* connect to different MCP servers and use their tools with LangGraph
*/
async function runConfigTest() {
try {
// Log when we start
console.log("Starting test with configuration files...");
// Step 1: Load and verify auth_mcp.json configuration (just testing parsing)
console.log("Parsing auth_mcp.json configuration...");
const authConfigPath = path.join(
process.cwd(),
"examples",
"auth_mcp.json"
);
if (!fs.existsSync(authConfigPath)) {
throw new Error(`Configuration file not found: ${authConfigPath}`);
}
// Load the auth configuration to verify it parses correctly
const authConfig = JSON.parse(fs.readFileSync(authConfigPath, "utf-8"));
console.log(
"Successfully parsed auth_mcp.json with the following servers:"
);
console.log("Servers:", Object.keys(authConfig.servers));
// Print auth headers (redacted for security) to verify they're present
Object.entries(authConfig.servers).forEach(([serverName, serverConfig]) => {
if (
serverConfig &&
typeof serverConfig === "object" &&
"headers" in serverConfig &&
serverConfig.headers
) {
console.log(
`Server ${serverName} has headers:`,
Object.keys(serverConfig.headers).map((key) => `${key}: ***`)
);
}
});
// Step 2: Load and verify complex_mcp.json configuration
console.log("Parsing complex_mcp.json configuration...");
const complexConfigPath = path.join(
process.cwd(),
"examples",
"complex_mcp.json"
);
if (!fs.existsSync(complexConfigPath)) {
throw new Error(`Configuration file not found: ${complexConfigPath}`);
}
const complexConfig = JSON.parse(
fs.readFileSync(complexConfigPath, "utf-8")
);
console.log(
"Successfully parsed complex_mcp.json with the following servers:"
);
console.log("Servers:", Object.keys(complexConfig.servers));
// Step 3: Connect directly to the math server using explicit path
console.log("Connecting to math server directly...");
// Create a client with the math server only
const client = new MultiServerMCPClient({
math: {
transport: "stdio",
command: "npx",
args: ["-y", "@modelcontextprotocol/server-math"],
},
});
// Get tools from the math server
const mcpTools = await client.getTools();
console.log(`Loaded ${mcpTools.length} tools from math server`);
// Log the names of available tools
const toolNames = mcpTools.map((tool) => tool.name);
console.log("Available tools:", toolNames.join(", "));
// Create an OpenAI model for the agent
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);
// 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 - need to cast to any to fix TypeScript errors
.addEdge(START, "llm")
.addEdge("tools", "llm")
// Add conditional logic to determine the next step
.addConditionalEdges("llm", (state) => {
const lastMessage = state.messages[state.messages.length - 1];
// If the last message has tool calls, we need to execute the tools
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";
}
// 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 test queries that use math tools
const testQueries = [
// Basic math queries
"What is 5 + 3?",
"What is 7 * 9?",
"If I have 10 and add 15 to it, then multiply the result by 2, what do I get?",
];
// Run each test query
for (const query of testQueries) {
console.log(`\n=== Running query: "${query}" ===`);
try {
// Create initial messages with a system message and the user query
const messages = [
new SystemMessage(
"You are a helpful assistant that can use tools to solve math problems."
),
new HumanMessage(query),
];
// Run the LangGraph workflow
const result = await app.invoke({ messages });
// Get the last AI message as the response
const lastMessage = result.messages
.filter((message) => message._getType() === "ai")
.pop();
console.log(`\nFinal Answer: ${lastMessage?.content}`);
} catch (error) {
console.error(`Error processing query "${query}":`, error);
}
}
// Close all connections
console.log("\nClosing connections...");
await client.close();
console.log("Test completed successfully");
} catch (error) {
console.error("Error running test:", error);
}
}
// Run the test
runConfigTest()
.then(() => {
console.log("Configuration test completed successfully");
process.exit(0);
})
.catch((error) => {
console.error("Error running configuration test:", error);
process.exit(1);
});
+67 -66
View File
@@ -10,7 +10,7 @@
* - Built-in persistence capabilities
*
* In this example, we:
* 1. Set up an MCP client to connect to a Python-based math server
* 1. Set up an MCP client to connect to the MCP everything server reference example
* 2. Create a LangGraph workflow with two nodes: one for the LLM and one for tools
* 3. Define the edges and conditional routing between the nodes
* 4. Execute the workflow with example queries
@@ -22,64 +22,59 @@
* - Ability to expand the graph with additional nodes for more complex workflows
*/
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 { StructuredToolInterface } from '@langchain/core/tools';
import { z } from 'zod';
import dotenv from 'dotenv';
import logger from '../src/logger.js';
/* 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";
// MCP client imports
import { MultiServerMCPClient } from '../src/index.js';
import { MultiServerMCPClient } from "../src/index.js";
// Load environment variables from .env file
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;
try {
logger.info('Initializing MCP client...');
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: {
transport: 'stdio',
command: 'python',
args: ['./examples/math_server.py'],
everything: {
transport: "stdio",
command: "npx",
args: ["-y", "@modelcontextprotocol/server-everything"],
},
});
// Initialize connections to the server
await client.initializeConnections();
logger.info('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() as StructuredToolInterface<z.ZodObject<any>>[];
const mcpTools = await client.getTools();
if (mcpTools.length === 0) {
throw new Error('No tools found');
throw new Error("No tools found");
}
logger.info(
`Loaded ${mcpTools.length} MCP tools: ${mcpTools.map(tool => tool.name).join(', ')}`
console.log(
`Loaded ${mcpTools.length} MCP tools: ${mcpTools
.map((tool) => tool.name)
.join(", ")}`
);
// Create an OpenAI model and bind the tools
const model = new ChatOpenAI({
modelName: process.env.OPENAI_MODEL_NAME || 'gpt-4-turbo-preview',
modelName: process.env.OPENAI_MODEL_NAME || "gpt-4-turbo-preview",
temperature: 0,
}).bindTools(mcpTools);
@@ -89,7 +84,7 @@ async function runExample() {
// ================================================
// Create a LangGraph agent flow
// ================================================
console.log('\n=== CREATING LANGGRAPH AGENT FLOW ===');
console.log("\n=== CREATING LANGGRAPH AGENT FLOW ===");
/**
* MessagesAnnotation provides a built-in state schema for handling chat messages.
@@ -101,51 +96,53 @@ async function runExample() {
// Define the function that calls the model
const llmNode = async (state: typeof MessagesAnnotation.State) => {
console.log('Calling LLM with messages:', state.messages.length);
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);
const workflow = new StateGraph(MessagesAnnotation)
// Add the nodes to the graph
workflow.addNode('llm', llmNode);
workflow.addNode('tools', toolNode);
// Add the nodes to the graph
.addNode("llm", llmNode)
.addNode("tools", toolNode)
// Add edges - these define how nodes are connected
// START -> llm: Entry point to the graph
// tools -> llm: After tools are executed, return to LLM for next step
workflow.addEdge(START as any, 'llm' as any);
workflow.addEdge('tools' as any, 'llm' as any);
// Add edges - these define how nodes are connected
// START -> llm: Entry point to the graph
// tools -> llm: After tools are executed, return to LLM for next step
.addEdge(START, "llm")
.addEdge("tools", "llm")
// Conditional routing to end or continue the tool loop
// This is the core of the agent's decision-making process
workflow.addConditionalEdges('llm' as any, state => {
const lastMessage = state.messages[state.messages.length - 1];
// Conditional routing to end or continue the tool loop
// This is the core of the agent's decision-making process
.addConditionalEdges("llm", (state) => {
const lastMessage = state.messages[state.messages.length - 1];
// If the last message has tool calls, we need to execute the tools
// 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');
return 'tools' as any;
}
// If the last message has tool calls, we need to execute the tools
// 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");
return "tools";
}
// If there are no tool calls, we're done
console.log('No tool calls, ending the workflow');
return END as any;
});
// If there are no tool calls, we're done
console.log("No tool calls, ending the workflow");
return END;
});
// Compile the graph
// This creates a runnable LangChain object that we can invoke
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 ===');
console.log("\n=== RUNNING LANGGRAPH AGENT ===");
for (const query of queries) {
console.log(`\nQuery: ${query}`);
@@ -158,9 +155,13 @@ async function runExample() {
// Display the result and all messages in the final state
console.log(`\nFinal Messages (${result.messages.length}):`);
result.messages.forEach((msg: BaseMessage, i: number) => {
const msgType = 'type' in msg ? msg.type : 'unknown';
const msgType = "type" in msg ? msg.type : "unknown";
console.log(
`[${i}] ${msgType}: ${typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)}`
`[${i}] ${msgType}: ${
typeof msg.content === "string"
? msg.content
: JSON.stringify(msg.content)
}`
);
});
@@ -168,22 +169,22 @@ async function runExample() {
console.log(`\nResult: ${finalMessage.content}`);
}
} catch (error) {
console.error('Error:', 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 MCP connections');
console.log("\nClosed all MCP connections");
}
// Exit process after a short delay to allow for cleanup
setTimeout(() => {
console.log('Example completed, exiting process.');
console.log("Example completed, exiting process.");
process.exit(0);
}, 500);
}
}
// Run the example
runExample();
runExample().catch(console.error);
-22
View File
@@ -1,22 +0,0 @@
#!/usr/bin/env python
from mcp.server.fastmcp import FastMCP
# Create a server
mcp = FastMCP(name="Math")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two integers and return the result."""
return a + b
@mcp.tool()
def multiply(a: int, b: int) -> int:
"""Multiply two integers and return the result."""
return a * b
# Run the server
if __name__ == "__main__":
mcp.run(transport="stdio")
+40
View File
@@ -0,0 +1,40 @@
/**
* Filesystem MCP Server with LangGraph Example
*
* This example demonstrates how to use the Filesystem MCP server with LangGraph
* to create a structured workflow for complex file operations.
*
* The graph-based approach allows:
* 1. Clear separation of responsibilities (reasoning vs execution)
* 2. Conditional routing based on file operation types
* 3. Structured handling of complex multi-file operations
*/
/* eslint-disable no-console */
import { MultiServerMCPClient } from "../src/index.js";
import { runExample as runFileSystemExample } from "./filesystem_langgraph_example.js";
async function runExample() {
const client = new MultiServerMCPClient({
filesystem: {
transport: "stdio",
command: "docker",
args: [
"run",
"-i",
"--rm",
"-v",
"mcp-filesystem-data:/projects",
"mcp/filesystem",
"/projects",
],
},
});
await runFileSystemExample(client);
}
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
if (isMainModule) {
runExample().catch((error) => console.error("Setup error:", error));
}
-82
View File
@@ -1,82 +0,0 @@
#!/usr/bin/env python
from mcp.server.fastmcp import FastMCP
import sys
# Create a server
mcp = FastMCP(name="Weather")
@mcp.tool()
def get_temperature(city: str) -> str:
"""Get the current temperature for a city.
This is a mock implementation that returns fake data.
In a real application, this would call a weather API.
"""
# Mock implementation - in a real app, this would call a weather API
temperatures = {
"new york": "72°F",
"london": "65°F",
"tokyo": "25 degrees Celsius",
"paris": "70°F",
"sydney": "80°F",
}
city_lower = city.lower()
if city_lower in temperatures:
return f"The current temperature in {city} is {temperatures[city_lower]}."
else:
return f"Temperature data not available for {city}."
@mcp.tool()
def get_forecast(city: str, days: int = 3) -> str:
"""Get the weather forecast for a city.
Args:
city: The name of the city to get the forecast for.
days: The number of days to forecast (default: 3).
Returns:
A string containing the weather forecast.
"""
# Mock implementation - in a real app, this would call a weather API
forecasts = {
"new york": "Sunny with a chance of rain",
"london": "Cloudy with occasional showers",
"tokyo": "Clear skies",
"paris": "Partly cloudy",
"sydney": "Warm and sunny",
}
city_lower = city.lower()
if city_lower in forecasts:
return f"The {days}-day forecast for {city} is: {forecasts[city_lower]}."
else:
return f"Forecast data not available for {city}."
# Run the server
if __name__ == "__main__":
# Set the port using command line arguments
# The FastMCP server will read these arguments
if len(sys.argv) == 1: # No arguments provided
# Add command line arguments for the port
sys.argv.extend(["--sse-port", "8000"])
port = 8000
else:
# Check if --sse-port is already in the arguments
if "--sse-port" in sys.argv:
port_index = sys.argv.index("--sse-port") + 1
if port_index < len(sys.argv):
port = int(sys.argv[port_index])
else:
port = 8000
else:
# Add the port argument
sys.argv.extend(["--sse-port", "8000"])
port = 8000
# Run with SSE transport
print(f"Starting weather server with SSE transport on port {port}...")
mcp.run(transport="sse")
+23
View File
@@ -0,0 +1,23 @@
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
/**
* @param {string} relativePath
* @returns {string}
*/
function abs(relativePath) {
return resolve(dirname(fileURLToPath(import.meta.url)), relativePath);
}
export const config = {
internals: [/node:/, /@langchain\/core\//, /async_hooks/],
entrypoints: {
index: "index",
},
requiresOptionalDependency: [],
tsConfigPath: resolve("./tsconfig.json"),
cjsSource: "./dist-cjs",
cjsDestination: "./dist",
additionalGitignorePaths: [".env", ".eslintcache"],
abs,
};
-8065
View File
File diff suppressed because it is too large Load Diff
+70 -32
View File
@@ -1,10 +1,11 @@
{
"name": "@langchain/mcp-adapters",
"version": "0.2.4",
"version": "0.4.5",
"description": "LangChain.js adapters for Model Context Protocol (MCP)",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"type": "module",
"packageManager": "yarn@3.5.1",
"repository": {
"type": "git",
"url": "git+https://github.com/langchain-ai/langchainjs-mcp-adapters.git"
@@ -14,16 +15,20 @@
"url": "https://github.com/langchain-ai/langchainjs-mcp-adapters/issues"
},
"scripts": {
"build": "run-s \"build:main -- {@}\" \"build:examples -- {@}\" --",
"build": "run-s \"build:main\" \"build:examples\"",
"build:main": "yarn lc_build --create-entrypoints --pre --tree-shaking",
"build:examples": "tsc -p tsconfig.examples.json",
"clean": "rm -rf dist/ dist-cjs/ .turbo/",
"format": "prettier --config .prettierrc --write \"src/**/*.ts\" \"examples/**/*.ts\"",
"format:check": "prettier --config .prettierrc --check \"src\" \"examples/**/*.ts\"",
"lint": "yarn lint:eslint && yarn lint:dpdm",
"lint:dpdm": "dpdm --exit-code circular:1 --no-warning --no-tree src/**/*.ts examples/**/*.ts",
"lint:eslint": "NODE_OPTIONS=--max-old-space-size=4096 eslint --cache --ext .ts,.js src/ examples/",
"lint:fix": "yarn lint:eslint --fix && yarn lint:dpdm",
"prepack": "yarn build",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest",
"lint": "eslint --ignore-pattern 'dist/**' .",
"lint:fix": "eslint --ignore-pattern 'dist/**' . --fix",
"format": "prettier --write \"src/**/*.ts\" \"examples/**/*.ts\"",
"prepare": "husky",
"build:main": "tsc",
"build:examples": "tsc -p tsconfig.examples.json"
"test:watch": "vitest"
},
"lint-staged": {
"*.{js,ts}": [
@@ -41,45 +46,78 @@
"author": "Ravi Kiran Vemula",
"license": "MIT",
"dependencies": {
"@dmitryrechkin/json-schema-to-zod": "^1.0.1",
"@modelcontextprotocol/sdk": "^1.7.0",
"winston": "^3.17.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/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": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"@vitest/coverage-v8": "^3.1.1",
"dotenv": "^16.4.7",
"eslint": "^8.57.0",
"eslint-config-prettier": "^10.0.2",
"eventsource": "^3.0.5",
"dpdm": "^3.12.0",
"eslint": "^8.33.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-no-instanceof": "^1.0.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vitest": "^0.5.4",
"eventsource": "^3.0.6",
"express": "^5.1.0",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"npm-run-all": "^4.1.5",
"prettier": "^3.2.5",
"prettier": "^2.8.3",
"release-it": "^17.6.0",
"rollup": "^4.39.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.2",
"typescript-eslint": "^8.26.0",
"vitest": "^3.0.9"
"typescript": "^4.9.5 || ^5.4.5",
"typescript-eslint": "^8.29.0",
"vitest": "^3.1.1"
},
"resolutions": {
"typescript": "4.9.5",
"uuid": "^11.0.0"
},
"engines": {
"node": ">=18"
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"directories": {
"example": "examples"
}
},
"exports": {
".": {
"types": {
"import": "./index.d.ts",
"require": "./index.d.cts",
"default": "./index.d.ts"
},
"import": "./index.js",
"require": "./index.cjs"
},
"./package.json": "./package.json"
},
"files": [
"dist/",
"index.cjs",
"index.js",
"index.d.ts",
"index.d.cts"
]
}
+816 -787
View File
File diff suppressed because it is too large Load Diff
+15 -2
View File
@@ -1,2 +1,15 @@
export { MultiServerMCPClient } from './client.js';
export { logger, enableLogging, disableLogging } from './logger.js';
import { type StreamableHTTPConnection } from "./client.js";
export {
MultiServerMCPClient,
type Connection,
type StdioConnection,
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";
-124
View File
@@ -1,124 +0,0 @@
import * as winston from 'winston';
import * as fs from 'fs';
import * as path from 'path';
/**
* Logging levels:
* error: 0 - Severe errors that cause the application to crash or malfunction
* warn: 1 - Warnings that don't stop the application but should be addressed
* info: 2 - General information about application operation
* http: 3 - HTTP request/response information
* debug: 4 - Detailed debugging information
*/
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4,
};
/**
* By default, logging is set to the silent level unless explicitly enabled
* This makes logging opt-in rather than enabled by default
*/
const defaultLevel = 'silent';
/**
* Define colors for each log level to improve readability in the console.
*/
const colors = {
error: 'red',
warn: 'yellow',
info: 'green',
http: 'magenta',
debug: 'white',
};
// Add colors to Winston
winston.addColors(colors);
/**
* Define the format for log messages.
* We include a timestamp, colorize the output, and format the message.
*/
const format = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
winston.format.colorize({ all: true }),
winston.format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`)
);
/**
* Define the transports for log messages.
* Always log to console, and only log to files if we have permission.
*/
// Base transports array (always include console)
const transports: winston.transport[] = [
// Console transport
new winston.transports.Console(),
];
// Attempt to add file transports only if we have write permissions
try {
const logsDir = path.join(process.cwd(), 'logs');
// Create logs directory if it doesn't exist
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
// Test write permissions with a small file
const testFile = path.join(logsDir, '.permissions-test');
fs.writeFileSync(testFile, 'test');
fs.unlinkSync(testFile);
// If we reach here, we have write permissions - add file transports
transports.push(
// File transport for errors
new winston.transports.File({
filename: path.join(logsDir, 'error.log'),
level: 'error',
}),
// File transport for all logs
new winston.transports.File({
filename: path.join(logsDir, 'all.log'),
})
);
} catch (error) {
// If any error occurs during the file operations, log to console only
console.warn(
`Unable to set up file logging: ${error instanceof Error ? error.message : String(error)}`
);
console.warn('Falling back to console logging only');
}
/**
* Create the logger instance with our configuration.
* By default, logging is disabled (silent) but can be enabled by setting the level.
*/
export const logger = winston.createLogger({
level: defaultLevel, // Start with silent logging by default
levels,
format,
transports,
});
/**
* Enable logging at the specified level.
*
* @param level - The log level to enable ('error', 'warn', 'info', 'http', 'debug')
*/
export function enableLogging(level: keyof typeof levels | 'silent' = 'info'): void {
logger.level = level;
logger.debug(`Logging enabled at level: ${level}`);
}
/**
* Disable all logging.
*/
export function disableLogging(): void {
logger.level = 'silent';
}
export default logger;
+224 -103
View File
@@ -1,28 +1,60 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import type {
CallToolResult,
TextContent,
ImageContent,
EmbeddedResource,
ReadResourceResult,
Tool as MCPTool,
} from "@modelcontextprotocol/sdk/types.js";
import {
DynamicStructuredTool,
ResponseFormat,
StructuredToolInterface,
} from '@langchain/core/tools';
import { JSONSchema, JSONSchemaToZod } from '@dmitryrechkin/json-schema-to-zod';
type DynamicStructuredToolInput,
type StructuredToolInterface,
} from "@langchain/core/tools";
import {
MessageContent,
MessageContentComplex,
MessageContentImageUrl,
MessageContentText,
} from "@langchain/core/messages";
import debug from "debug";
import logger from './logger.js';
interface TextContent {
type: 'text';
text: string;
// Replace direct initialization with lazy initialization
let debugLog: debug.Debugger;
function getDebugLog() {
if (!debugLog) {
debugLog = debug("@langchain/mcp-adapters:tools");
}
return debugLog;
}
interface NonTextContent {
type: string;
[key: string]: unknown;
}
export type CallToolResultContentType =
CallToolResult["content"][number]["type"];
export type CallToolResultContent =
| TextContent
| ImageContent
| EmbeddedResource;
interface CallToolResult {
isError?: boolean;
content?: (TextContent | NonTextContent)[] | string | object;
type?: string;
[key: string]: unknown;
async function _embeddedResourceToArtifact(
resource: EmbeddedResource,
client: Client
): Promise<EmbeddedResource[]> {
if (!resource.blob && !resource.text && resource.uri) {
const response: ReadResourceResult = await client.readResource({
uri: resource.resource.uri,
});
return response.contents.map(
(content: ReadResourceResult["contents"][number]) => ({
type: "resource",
resource: {
...content,
},
})
);
}
return [resource];
}
/**
@@ -31,7 +63,7 @@ interface CallToolResult {
export class ToolException extends Error {
constructor(message: string) {
super(message);
this.name = 'ToolException';
this.name = "ToolException";
}
}
@@ -42,38 +74,81 @@ export class ToolException extends Error {
* @param result - The result from the MCP tool call
* @returns A tuple of [textContent, nonTextContent]
*/
function _convertCallToolResult(
result: CallToolResult
): [string | string[], NonTextContent[] | null] {
if (!result || !Array.isArray(result.content)) {
return ['', null];
}
const textContents: TextContent[] = [];
const nonTextContents: NonTextContent[] = [];
// Separate text and non-text content
for (const content of result.content) {
if (content.type === 'text' && 'text' in content) {
textContents.push(content as TextContent);
} else {
nonTextContents.push(content as NonTextContent);
}
}
// Create the text content output
const textOutput = textContents.map(content => content.text);
const finalTextOutput = textOutput.length === 1 ? textOutput[0] : textOutput;
// Check for errors
if (result.isError) {
logger.error('MCP tool returned an error result');
async function _convertCallToolResult(
serverName: string,
toolName: string,
result: CallToolResult,
client: Client
): Promise<[MessageContent, EmbeddedResource[]]> {
if (!result) {
throw new ToolException(
typeof finalTextOutput === 'string' ? finalTextOutput : textOutput.join('\n')
`MCP tool '${toolName}' on server '${serverName}' returned an invalid result - tool call response was undefined`
);
}
return [finalTextOutput, nonTextContents.length > 0 ? nonTextContents : null];
if (!Array.isArray(result.content)) {
throw new ToolException(
`MCP tool '${toolName}' on server '${serverName}' returned an invalid result - expected an array of content, but was ${typeof result.content}`
);
}
if (result.isError) {
throw new ToolException(
`MCP tool '${toolName}' on server '${serverName}' returned an error: ${result.content
.map((content) => content.text)
.join("\n")}`
);
}
const mcpTextAndImageContent: MessageContentComplex[] = (
result.content.filter(
(content) => content.type === "text" || content.type === "image"
) as (TextContent | ImageContent)[]
).map((content: TextContent | ImageContent) => {
switch (content.type) {
case "text":
return {
type: "text",
text: content.text,
} as MessageContentText;
case "image":
return {
type: "image_url",
image_url: {
url: `data:${content.mimeType};base64,${content.data}`,
},
} as MessageContentImageUrl;
default:
throw new ToolException(
`MCP tool '${toolName}' on server '${serverName}' returned an invalid result - expected a text or image content, but was ${
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(content as any).type
}`
);
}
});
// Create the text content output
const artifacts = (
await Promise.all(
(
result.content.filter(
(content) => content.type === "resource"
) as EmbeddedResource[]
).map((content: EmbeddedResource) =>
_embeddedResourceToArtifact(content, client)
)
)
).flat();
if (
mcpTextAndImageContent.length === 1 &&
mcpTextAndImageContent[0].type === "text"
) {
return [mcpTextAndImageContent[0].text, artifacts];
}
return [mcpTextAndImageContent, artifacts];
}
/**
@@ -84,87 +159,133 @@ function _convertCallToolResult(
* @internal
*
* @param client - The MCP client
* @param name - The name of the tool (forwarded to the client)
* @param responseFormat - The response format
* @param toolName - The name of the tool (forwarded to the client)
* @param args - The arguments to pass to the tool
* @returns A tuple of [textContent, nonTextContent]
*/
async function _callTool(
serverName: string,
toolName: string,
client: Client,
name: string,
responseFormat: ResponseFormat,
args: Record<string, unknown>
): Promise<string | [string | string[], NonTextContent[] | null]> {
): Promise<[MessageContent, EmbeddedResource[]]> {
let result: CallToolResult;
try {
logger.info(`Calling tool ${name}(${JSON.stringify(args)})`);
const result = await client.callTool({
name,
getDebugLog()(`INFO: Calling tool ${toolName}(${JSON.stringify(args)})`);
result = (await client.callTool({
name: toolName,
arguments: args,
});
const [textContent, nonTextContent] = _convertCallToolResult({
...result,
isError: result.isError === true,
content: result.content || [],
});
logger.info(`Tool ${name} returned: ${JSON.stringify({ textContent, nonTextContent })}`);
// Return based on the response format
if (responseFormat === 'content_and_artifact') {
return [textContent, nonTextContent];
}
// Default to returning just the text content
return typeof textContent === 'string' ? textContent : textContent.join('\n');
})) as CallToolResult;
} catch (error) {
logger.error(`Error calling tool ${name}: ${String(error)}`);
getDebugLog()(`Error calling tool ${toolName}: ${String(error)}`);
// eslint-disable-next-line no-instanceof/no-instanceof
if (error instanceof ToolException) {
throw error;
}
throw new ToolException(`Error calling tool ${name}: ${String(error)}`);
throw new ToolException(`Error calling tool ${toolName}: ${String(error)}`);
}
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.
*
* @param serverName - The name of the server to load tools from
* @param client - The MCP client
* @param responseFormat - Response format ('text' or 'content_and_artifact')
* @returns A list of LangChain tools
*/
export async function loadMcpTools(
serverName: string,
client: Client,
responseFormat: ResponseFormat = 'content',
throwOnLoadError: boolean = true
options?: LoadMcpToolsOptions
): Promise<StructuredToolInterface[]> {
const {
throwOnLoadError,
prefixToolNameWithServerName,
additionalToolNamePrefix,
} = {
...defaultLoadMcpToolsOptions,
...(options ?? {}),
};
// Get tools in a single operation
const toolsResponse = await client.listTools();
logger.info(`Found ${toolsResponse.tools?.length || 0} MCP tools`);
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 => !!tool.name)
.map(tool => {
try {
const dst = new DynamicStructuredTool({
name: tool.name,
description: tool.description || '',
schema: JSONSchemaToZod.convert(
(tool.inputSchema ?? { type: 'object', properties: {} }) as JSONSchema
),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
func: _callTool.bind(null, client, tool.name, responseFormat) as any,
});
logger.debug(`Successfully loaded tool: ${dst.name}`);
return dst;
} catch (error) {
logger.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[];
}
-26
View File
@@ -1,26 +0,0 @@
declare module 'eventsource' {
export default class EventSource {
constructor(
url: string,
options?: {
headers?: Record<string, string>;
https?: any;
rejectUnauthorized?: boolean;
withCredentials?: boolean;
}
);
onopen: (event: any) => void;
onmessage: (event: any) => void;
onerror: (event: any) => void;
close(): void;
static readonly CONNECTING: number;
static readonly OPEN: number;
static readonly CLOSED: number;
readonly readyState: number;
readonly url: string;
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"declaration": false
},
"exclude": ["node_modules", "dist", "docs", "**/tests"]
}
+2 -1
View File
@@ -10,7 +10,8 @@
"skipLibCheck": true,
"sourceMap": true,
"downlevelIteration": true,
"noEmit": true // we just want type checking - no need to output for inclusion in the published module
"noEmit": true, // we just want type checking - no need to output for inclusion in the published module
"resolveJsonModule": true
},
"include": ["examples/**/*", "src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
+17 -11
View File
@@ -1,17 +1,23 @@
{
"extends": "@tsconfig/recommended",
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"outDir": "./dist",
"outDir": "dist",
"rootDir": "./src",
"target": "ES2021",
"lib": ["ES2021", "ES2022.Object", "DOM"],
"module": "ES2020",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"sourceMap": true,
"downlevelIteration": true
"declaration": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"useDefineForClassFields": true,
"strictPropertyInitialization": false,
"allowJs": true,
"strict": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "examples/**/*"]
"exclude": ["node_modules", "dist", "docs"]
}
+24
View File
@@ -0,0 +1,24 @@
{
"extends": "@tsconfig/recommended",
"compilerOptions": {
"outDir": "dist",
"rootDir": "./src",
"target": "ES2021",
"lib": ["ES2021", "ES2022.Object", "DOM"],
"module": "ES2020",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"declaration": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"useDefineForClassFields": true,
"strictPropertyInitialization": false,
"allowJs": true,
"strict": true,
"noEmit": true
},
"include": ["src/**/*.ts", "__tests__/**/*.ts"],
"exclude": ["node_modules", "dist", "docs", "coverage"]
}
+8312
View File
File diff suppressed because it is too large Load Diff