mirror of
https://github.com/langchain-ai/langchainjs-mcp-adapters.git
synced 2026-07-19 21:24:34 -04:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3f16633d1 | |||
| 01b70d18c9 | |||
| 72818340c1 | |||
| 373b08e090 | |||
| 831359db3a | |||
| 68f130e1d0 | |||
| 03172e3fcd | |||
| 8cfec54c29 | |||
| c1b0f3d8ad | |||
| 66f6eb0455 | |||
| b8ccb243a0 | |||
| a67182573d | |||
| 2930bf5124 | |||
| 60458b9926 | |||
| 6093a1c9ba | |||
| 09f511a57d | |||
| b4313142e0 | |||
| 18daa5b83f | |||
| 2fc9bfa79e | |||
| e38ff20113 | |||
| 6eb9d6cf7a | |||
| 7828c19fd8 | |||
| 98726ec5de | |||
| b6e17366be | |||
| ce7b796ec8 | |||
| c3a0dfddc1 | |||
| 304c1847b0 | |||
| 40ffbf4080 | |||
| 99d6be0626 | |||
| 3a59cf313b | |||
| 72b740ffcf | |||
| 399bf78cd8 | |||
| 8cd572316e | |||
| 6bc0b0703e |
@@ -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 }],
|
||||
},
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+873
File diff suppressed because one or more lines are too long
@@ -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
|
||||
@@ -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).**
|
||||
|
||||
[](https://www.npmjs.com/package/@langchain/mcp-adapters)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
This library provides a lightweight wrapper that makes [Anthropic Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) tools compatible with [LangChain.js](https://github.com/langchain-ai/langchainjs) and [LangGraph.js](https://github.com/langchain-ai/langgraphjs).
|
||||
This library provides a lightweight wrapper to allow [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) services to be used with [LangChain.js](https://github.com/langchain-ai/langchainjs).
|
||||
|
||||
## Features
|
||||
|
||||
- 🔌 **Transport Options**
|
||||
|
||||
- Connect to MCP servers via stdio (local) or SSE (remote)
|
||||
- Support for custom headers in SSE connections for authentication
|
||||
- Configurable reconnection strategies for both transport types
|
||||
|
||||
- 🔄 **Multi-Server Management**
|
||||
|
||||
- Connect to multiple MCP servers simultaneously
|
||||
- Auto-organize tools by server or access them as a flattened collection
|
||||
- Convenient configuration via JSON file
|
||||
|
||||
- 🧩 **Agent Integration**
|
||||
|
||||
- Compatible with LangChain.js and LangGraph.js
|
||||
- Optimized for OpenAI, Anthropic, and Google models
|
||||
|
||||
- 🛠️ **Development Features**
|
||||
- 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
@@ -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
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -1,70 +1,36 @@
|
||||
# LangChainJS-MCP-Adapters Examples
|
||||
|
||||
This directory contains examples demonstrating how to use the LangChainJS-MCP-Adapters library with various MCP servers, with a focus on the Firecrawl MCP server.
|
||||
This directory contains examples demonstrating how to use the `@langchain/mcp-adapters` library with various MCP servers
|
||||
|
||||
## Running the Examples
|
||||
|
||||
We've added dedicated npm scripts to make it easy to run the examples:
|
||||
|
||||
```bash
|
||||
# Build all examples
|
||||
npm run build:examples
|
||||
yarn build:examples
|
||||
|
||||
# Run specific examples
|
||||
npm run example:default # Uses automatic loading from mcp.json
|
||||
npm run example:custom # Uses a custom configuration file
|
||||
npm run example:multiple # Uses multiple MCP servers from a config file
|
||||
npm run example:mixed # Loads one server from config and another directly in code
|
||||
npm run example:enhanced # Demonstrates all enhanced configuration features
|
||||
# Run specific example
|
||||
cd examples && npx -y tsx firecrawl_custom_config_example.ts
|
||||
```
|
||||
|
||||
## Example Descriptions
|
||||
|
||||
### 1. Default Configuration (`firecrawl_default_config_example.ts`)
|
||||
### Filesystem LangGraph Example (`filesystem_langgraph_example.ts`)
|
||||
Demonstrates using the Filesystem MCP server with LangGraph to create a structured workflow for complex file operations. The example creates a graph-based agent that can perform various file operations like creating multiple files, reading files, creating directory structures, and organizing files.
|
||||
|
||||
Demonstrates the automatic loading of MCP server configurations from the default `mcp.json` file in the root directory. This example showcases how to:
|
||||
### Firecrawl - Custom Configuration (`firecrawl_custom_config_example.ts`)
|
||||
Shows how to initialize the Firecrawl MCP server with a custom configuration. The example sets up a connection to Firecrawl using SSE transport, loads tools from the server, and creates a React agent to perform web scraping tasks and find news about artificial intelligence.
|
||||
|
||||
- Create a client that automatically loads from `mcp.json`
|
||||
- Initialize connections to all servers in the configuration
|
||||
- Filter tools by server name
|
||||
- Use tools with LangGraph
|
||||
### Firecrawl - Multiple Servers (`firecrawl_multiple_servers_example.ts`)
|
||||
Demonstrates how to use multiple MCP servers simultaneously by configuring both Firecrawl for web scraping and a Math server for calculations. The example creates a React agent that can use tools from both servers to answer queries involving both math calculations and web content retrieval.
|
||||
|
||||
### 2. Custom Configuration (`firecrawl_custom_config_example.ts`)
|
||||
### LangGraph - Complex Config (`langgraph_complex_config_example.ts`)
|
||||
Illustrates using different configuration files to set up connections to MCP servers, with a focus on the Math server. This example shows how to parse JSON configuration files, connect to a Math server directly, and create a LangGraph workflow that can perform mathematical operations using MCP tools.
|
||||
|
||||
Shows how to create and use a custom configuration file specifically for the Firecrawl server. This example:
|
||||
### LangGraph - Simple Config (`langgraph_example.ts`)
|
||||
Shows a straightforward integration of LangGraph with MCP tools, creating a flexible agent workflow. The example demonstrates how to set up a graph-based structure with separate nodes for LLM reasoning and tool execution, with conditional routing between nodes based on whether tool calls are needed.
|
||||
|
||||
- Creates a custom configuration file
|
||||
- Initializes the client from this file
|
||||
- Shows environment variable configuration
|
||||
|
||||
### 3. Multiple Servers (`firecrawl_multiple_servers_example.ts`)
|
||||
|
||||
Demonstrates loading multiple MCP servers (Firecrawl and Math) from a single configuration file. This example:
|
||||
|
||||
- Creates a configuration with multiple servers
|
||||
- Loads and uses tools from different servers in the same agent
|
||||
|
||||
### 4. Mixed Loading (`firecrawl_mixed_loading_example.ts`)
|
||||
|
||||
Shows a mixed approach to loading MCP servers - one from a configuration file and another directly in code. This example:
|
||||
|
||||
- Loads the math server from a config file
|
||||
- Adds the Firecrawl server via direct code
|
||||
- Works with tools from both servers
|
||||
|
||||
### 5. Enhanced Configuration (`firecrawl_enhanced_config_example.ts`)
|
||||
|
||||
Showcases all the enhanced configuration features, including:
|
||||
|
||||
- Automatic loading from the default configuration
|
||||
- Adding multiple configuration sources
|
||||
- Environment variable substitution
|
||||
- Adding servers directly in code
|
||||
|
||||
## Configuration Files
|
||||
|
||||
- `mcp.json` - Default configuration file in the root directory
|
||||
- Custom configuration files created in the examples
|
||||
### Launching a Containerized MCP Server (`mcp_over_docker_example.ts`)
|
||||
Shows how to run an MCP server inside a Docker container. This example configures a connection to a containerized Filesystem MCP server with appropriate volume mounting, demonstrating how to use Docker to isolate and run MCP servers while still allowing file operations.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
},
|
||||
"math-server": {
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["./math_server.py"]
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-math"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import express from "express";
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export async function main() {
|
||||
const server = new McpServer({
|
||||
name: "backwards-compatible-server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
const calcSchema = { a: z.number(), b: z.number() };
|
||||
|
||||
server.tool(
|
||||
"add",
|
||||
"Adds two numbers together",
|
||||
calcSchema,
|
||||
async ({ a, b }: { a: number; b: number }, extra) => {
|
||||
return {
|
||||
content: [{ type: "text", text: `${a + b}` }],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"subtract",
|
||||
"Subtracts two numbers",
|
||||
calcSchema,
|
||||
async ({ a, b }: { a: number; b: number }, extra) => {
|
||||
return { content: [{ type: "text", text: `${a - b}` }] };
|
||||
}
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"multiply",
|
||||
"Multiplies two numbers",
|
||||
calcSchema,
|
||||
async ({ a, b }: { a: number; b: number }, extra) => {
|
||||
return { content: [{ type: "text", text: `${a * b}` }] };
|
||||
}
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"divide",
|
||||
"Divides two numbers",
|
||||
calcSchema,
|
||||
async ({ a, b }: { a: number; b: number }, extra) => {
|
||||
return { content: [{ type: "text", text: `${a / b}` }] };
|
||||
}
|
||||
);
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Store transports for each session type
|
||||
const transports = {
|
||||
streamable: {} as Record<string, StreamableHTTPServerTransport>,
|
||||
sse: {} as Record<string, SSEServerTransport>,
|
||||
};
|
||||
|
||||
// Modern Streamable HTTP endpoint
|
||||
app.post("/mcp", async (req, res) => {
|
||||
// Check for existing session ID
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
|
||||
if (sessionId && transports.streamable[sessionId]) {
|
||||
// Reuse existing transport
|
||||
transport = transports.streamable[sessionId];
|
||||
} else if (!sessionId && isInitializeRequest(req.body)) {
|
||||
// New initialization request
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
onsessioninitialized: (sessionId) => {
|
||||
// Store the transport by session ID
|
||||
transports.streamable[sessionId] = transport;
|
||||
},
|
||||
});
|
||||
|
||||
// Clean up transport when closed
|
||||
transport.onclose = () => {
|
||||
if (transport.sessionId) {
|
||||
delete transports.streamable[transport.sessionId];
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to the MCP server
|
||||
await server.connect(transport);
|
||||
} else {
|
||||
// Invalid request
|
||||
console.error(
|
||||
"Invalid Streamable HTTP request: ",
|
||||
JSON.stringify(req.body, null, 2)
|
||||
);
|
||||
res.status(400).json({
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32000,
|
||||
message: "Bad Request: No valid session ID provided",
|
||||
},
|
||||
id: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the request
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
});
|
||||
|
||||
// Reusable handler for GET and DELETE requests
|
||||
const handleSessionRequest = async (
|
||||
req: express.Request,
|
||||
res: express.Response
|
||||
) => {
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
if (!sessionId || !transports.streamable[sessionId]) {
|
||||
console.error(
|
||||
"Invalid Streamable HTTP request (invalid/missing session ID): ",
|
||||
JSON.stringify(req.body, null, 2)
|
||||
);
|
||||
res.status(400).send("Invalid or missing session ID");
|
||||
return;
|
||||
}
|
||||
|
||||
const transport = transports.streamable[sessionId];
|
||||
await transport.handleRequest(req, res);
|
||||
};
|
||||
|
||||
app.get("/mcp", handleSessionRequest);
|
||||
app.delete("/mcp", handleSessionRequest);
|
||||
|
||||
// Legacy SSE endpoint for older clients
|
||||
app.get("/sse", async (req, res) => {
|
||||
// Create SSE transport for legacy clients
|
||||
const transport = new SSEServerTransport("/messages", res);
|
||||
transports.sse[transport.sessionId] = transport;
|
||||
|
||||
res.on("close", () => {
|
||||
delete transports.sse[transport.sessionId];
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
});
|
||||
|
||||
// Legacy message endpoint for older clients
|
||||
app.post("/messages", async (req, res) => {
|
||||
const sessionId = req.query.sessionId as string;
|
||||
const transport = transports.sse[sessionId];
|
||||
if (transport) {
|
||||
await transport.handlePostMessage(req, res, req.body);
|
||||
} else {
|
||||
console.error("No transport found for sessionId", sessionId);
|
||||
res.status(400).send("No transport found for sessionId");
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
}
|
||||
|
||||
if (typeof require !== "undefined" && require.main === module) {
|
||||
main().catch(console.error);
|
||||
}
|
||||
|
||||
if (
|
||||
import.meta.url === process.argv[1] ||
|
||||
import.meta.url === `file://${process.argv[1]}`
|
||||
) {
|
||||
main().catch(console.error);
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Calculator MCP Server with LangGraph Example
|
||||
*
|
||||
* This example demonstrates how to use the Calculator MCP server with LangGraph
|
||||
* to create a structured workflow for simple calculations.
|
||||
*
|
||||
* The graph-based approach allows:
|
||||
* 1. Clear separation of responsibilities (reasoning vs execution)
|
||||
* 2. Conditional routing based on tool calls
|
||||
* 3. Structured handling of complex multi-tool operations
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import {
|
||||
StateGraph,
|
||||
END,
|
||||
START,
|
||||
MessagesAnnotation,
|
||||
} from "@langchain/langgraph";
|
||||
import { ToolNode } from "@langchain/langgraph/prebuilt";
|
||||
import {
|
||||
HumanMessage,
|
||||
AIMessage,
|
||||
SystemMessage,
|
||||
isHumanMessage,
|
||||
} from "@langchain/core/messages";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
import { main as calculatorServerMain } from "./calculator_server_shttp_sse.js";
|
||||
|
||||
// MCP client imports
|
||||
import { MultiServerMCPClient } from "../src/index.js";
|
||||
|
||||
// Load environment variables from .env file
|
||||
dotenv.config();
|
||||
|
||||
const transportType = process.env.MCP_TRANSPORT_TYPE === "sse" ? "sse" : "http";
|
||||
|
||||
export async function runExample(client?: MultiServerMCPClient) {
|
||||
try {
|
||||
console.log("Initializing MCP client...");
|
||||
|
||||
void calculatorServerMain();
|
||||
|
||||
// Wait for the server to start
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
// Create a client with configurations for the calculator server
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
client =
|
||||
client ??
|
||||
new MultiServerMCPClient({
|
||||
calculator: {
|
||||
url: `http://localhost:3000/${
|
||||
transportType === "sse" ? "sse" : "mcp"
|
||||
}`,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Connected to server");
|
||||
|
||||
// Get all tools (flattened array is the default now)
|
||||
const mcpTools = await client.getTools();
|
||||
|
||||
if (mcpTools.length === 0) {
|
||||
throw new Error("No tools found");
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Loaded ${mcpTools.length} MCP tools: ${mcpTools
|
||||
.map((tool) => tool.name)
|
||||
.join(", ")}`
|
||||
);
|
||||
|
||||
// Create an OpenAI model with tools attached
|
||||
const systemMessage = `You are an assistant that helps users with calculations.
|
||||
You have access to tools that can add, subtract, multiply, and divide numbers. Use
|
||||
these tools to answer the user's questions.`;
|
||||
|
||||
const model = new ChatOpenAI({
|
||||
modelName: process.env.OPENAI_MODEL_NAME || "gpt-4o-mini",
|
||||
temperature: 0.7,
|
||||
}).bindTools(mcpTools);
|
||||
|
||||
// Create a tool node for the LangGraph
|
||||
const toolNode = new ToolNode(mcpTools);
|
||||
|
||||
// ================================================
|
||||
// Create a LangGraph agent flow
|
||||
// ================================================
|
||||
console.log("\n=== CREATING LANGGRAPH AGENT FLOW ===");
|
||||
|
||||
// Define the function that calls the model
|
||||
const llmNode = async (state: typeof MessagesAnnotation.State) => {
|
||||
console.log(`Calling LLM with ${state.messages.length} messages`);
|
||||
|
||||
// Add system message if it's the first call
|
||||
let { messages } = state;
|
||||
if (messages.length === 1 && isHumanMessage(messages[0])) {
|
||||
messages = [new SystemMessage(systemMessage), ...messages];
|
||||
}
|
||||
|
||||
const response = await model.invoke(messages);
|
||||
return { messages: [response] };
|
||||
};
|
||||
|
||||
// Create a new graph with MessagesAnnotation
|
||||
const workflow = new StateGraph(MessagesAnnotation)
|
||||
|
||||
// Add the nodes to the graph
|
||||
.addNode("llm", llmNode)
|
||||
.addNode("tools", toolNode)
|
||||
|
||||
// Add edges - these define how nodes are connected
|
||||
.addEdge(START, "llm")
|
||||
.addEdge("tools", "llm")
|
||||
|
||||
// Conditional routing to end or continue the tool loop
|
||||
.addConditionalEdges("llm", (state) => {
|
||||
const lastMessage = state.messages[state.messages.length - 1];
|
||||
|
||||
// Cast to AIMessage to access tool_calls property
|
||||
const aiMessage = lastMessage as AIMessage;
|
||||
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) {
|
||||
console.log("Tool calls detected, routing to tools node");
|
||||
|
||||
// Log what tools are being called
|
||||
const toolNames = aiMessage.tool_calls
|
||||
.map((tc) => tc.name)
|
||||
.join(", ");
|
||||
console.log(`Tools being called: ${toolNames}`);
|
||||
|
||||
return "tools";
|
||||
}
|
||||
|
||||
// If there are no tool calls, we're done
|
||||
console.log("No tool calls, ending the workflow");
|
||||
return END;
|
||||
});
|
||||
|
||||
// Compile the graph
|
||||
const app = workflow.compile();
|
||||
|
||||
// Define examples to run
|
||||
const examples = [
|
||||
{
|
||||
name: "Add 1 and 2",
|
||||
query: "What is 1 + 2?",
|
||||
},
|
||||
{
|
||||
name: "Subtract 1 from 2",
|
||||
query: "What is 2 - 1?",
|
||||
},
|
||||
{
|
||||
name: "Multiply 1 and 2",
|
||||
query: "What is 1 * 2?",
|
||||
},
|
||||
{
|
||||
name: "Divide 1 by 2",
|
||||
query: "What is 1 / 2?",
|
||||
},
|
||||
];
|
||||
|
||||
// Run the examples
|
||||
console.log("\n=== RUNNING LANGGRAPH AGENT ===");
|
||||
|
||||
for (const example of examples) {
|
||||
console.log(`\n--- Example: ${example.name} ---`);
|
||||
console.log(`Query: ${example.query}`);
|
||||
|
||||
// Run the LangGraph agent
|
||||
const result = await app.invoke({
|
||||
messages: [new HumanMessage(example.query)],
|
||||
});
|
||||
|
||||
// Display the final answer
|
||||
const finalMessage = result.messages[result.messages.length - 1];
|
||||
console.log(`\nResult: ${finalMessage.content}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
process.exit(1); // Exit with error code
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close();
|
||||
console.log("Closed all MCP connections");
|
||||
}
|
||||
|
||||
// Exit process after a short delay to allow for cleanup
|
||||
setTimeout(() => {
|
||||
console.log("Example completed, exiting process.");
|
||||
process.exit(0);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
|
||||
if (isMainModule) {
|
||||
runExample().catch((error) => console.error("Setup error:", error));
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
{
|
||||
"servers": {
|
||||
"math": {
|
||||
"command": "python",
|
||||
"args": ["./examples/math_server.py"],
|
||||
"env": {
|
||||
"DEBUG": "true",
|
||||
"PYTHONPATH": "./examples"
|
||||
}
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-math"],
|
||||
},
|
||||
"weather": {
|
||||
"transport": "sse",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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")
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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,
|
||||
};
|
||||
Generated
-8065
File diff suppressed because it is too large
Load Diff
+70
-32
@@ -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
File diff suppressed because it is too large
Load Diff
+15
-2
@@ -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
@@ -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
@@ -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[];
|
||||
}
|
||||
|
||||
Vendored
-26
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": false
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "docs", "**/tests"]
|
||||
}
|
||||
@@ -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
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user