mirror of
https://github.com/langchain-ai/langchainjs-mcp-adapters.git
synced 2026-07-01 12:27:48 -04:00
@@ -0,0 +1,68 @@
|
||||
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/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", "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: |
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
echo "Final version to publish: $FINAL_VERSION"
|
||||
|
||||
- name: Publish to npm
|
||||
run: npm publish --access public
|
||||
run: yarn npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
+7
-54
@@ -1,54 +1,7 @@
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
.pnp/
|
||||
.pnp.js
|
||||
.cursor
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
*.tsbuildinfo
|
||||
.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/
|
||||
index.cjs
|
||||
index.js
|
||||
index.d.ts
|
||||
index.d.cts
|
||||
node_modules
|
||||
dist
|
||||
.yarn
|
||||
|
||||
+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"
|
||||
}
|
||||
Binary file not shown.
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
|
||||
+221
-170
@@ -1,6 +1,14 @@
|
||||
import { vi, describe, test, expect, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
vi,
|
||||
describe,
|
||||
test,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
type Mock,
|
||||
} from "vitest";
|
||||
// Mock the problematic dependencies using vi.mock
|
||||
vi.mock('@modelcontextprotocol/sdk/client/sse.js', () => {
|
||||
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());
|
||||
@@ -16,7 +24,7 @@ vi.mock('@modelcontextprotocol/sdk/client/sse.js', () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@modelcontextprotocol/sdk/client/stdio.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());
|
||||
@@ -32,21 +40,21 @@ vi.mock('@modelcontextprotocol/sdk/client/stdio.js', () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@modelcontextprotocol/sdk/client/index.js', () => {
|
||||
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',
|
||||
name: "testTool",
|
||||
description: "A test tool",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
input: { type: 'string' },
|
||||
input: { type: "string" },
|
||||
},
|
||||
required: ['input'],
|
||||
required: ["input"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -54,7 +62,7 @@ vi.mock('@modelcontextprotocol/sdk/client/index.js', () => {
|
||||
);
|
||||
const callToolMock = vi.fn().mockReturnValue(
|
||||
Promise.resolve({
|
||||
content: [{ type: 'text', text: 'result' }],
|
||||
content: [{ type: "text", text: "result" }],
|
||||
})
|
||||
);
|
||||
const closeMock = vi.fn().mockReturnValue(Promise.resolve());
|
||||
@@ -69,15 +77,17 @@ vi.mock('@modelcontextprotocol/sdk/client/index.js', () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('fs', () => ({
|
||||
vi.mock("fs", () => ({
|
||||
readFileSync: vi.fn(),
|
||||
existsSync: vi.fn().mockImplementation(() => false),
|
||||
}));
|
||||
vi.mock('path', () => ({
|
||||
vi.mock("path", async () => ({
|
||||
resolve: vi.fn(),
|
||||
join: (await vi.importActual("path")).join,
|
||||
}));
|
||||
|
||||
// Mock the logger
|
||||
vi.mock('../src/logger.js', () => {
|
||||
vi.mock("../src/logger.js", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: {
|
||||
@@ -90,41 +100,47 @@ vi.mock('../src/logger.js', () => {
|
||||
});
|
||||
|
||||
// Create placeholder mocks that will be replaced in beforeEach
|
||||
vi.mock('@modelcontextprotocol/sdk/client/sse.js', () => ({
|
||||
vi.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({
|
||||
SSEClientTransport: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({
|
||||
vi.mock("@modelcontextprotocol/sdk/client/stdio.js", () => ({
|
||||
StdioClientTransport: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
|
||||
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');
|
||||
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', () => {
|
||||
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 mockClientConnect: Mock;
|
||||
let mockClientListTools: Mock;
|
||||
let mockClientCallTool: Mock;
|
||||
let mockClientClose: Mock;
|
||||
|
||||
let mockStdioTransportClose: vi.Mock;
|
||||
let mockStdioTransportConnect: vi.Mock;
|
||||
let mockStdioTransportSend: vi.Mock;
|
||||
let mockStdioTransportClose: Mock;
|
||||
let mockStdioTransportConnect: Mock;
|
||||
let mockStdioTransportSend: Mock;
|
||||
// Define specific function type for onclose handlers
|
||||
let mockStdioOnClose: (() => void) | null;
|
||||
|
||||
let mockSSETransportClose: vi.Mock;
|
||||
let mockSSETransportConnect: vi.Mock;
|
||||
let mockSSETransportSend: vi.Mock;
|
||||
let mockSSETransportClose: Mock;
|
||||
let mockSSETransportConnect: Mock;
|
||||
let mockSSETransportSend: Mock;
|
||||
// Define specific function type for onclose handlers
|
||||
let mockSSEOnClose: (() => void) | null;
|
||||
|
||||
@@ -134,13 +150,17 @@ describe('MultiServerMCPClient', () => {
|
||||
|
||||
// Set up mock implementations for Client
|
||||
mockClientConnect = vi.fn().mockReturnValue(Promise.resolve());
|
||||
mockClientListTools = vi.fn().mockReturnValue(Promise.resolve({ tools: [] }));
|
||||
mockClientListTools = vi
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ tools: [] }));
|
||||
mockClientCallTool = vi
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ content: [{ type: 'text', text: 'result' }] }));
|
||||
.mockReturnValue(
|
||||
Promise.resolve({ content: [{ type: "text", text: "result" }] })
|
||||
);
|
||||
mockClientClose = vi.fn().mockReturnValue(Promise.resolve());
|
||||
|
||||
(Client as vi.Mock).mockImplementation(() => ({
|
||||
(Client as Mock).mockImplementation(() => ({
|
||||
connect: mockClientConnect,
|
||||
listTools: mockClientListTools,
|
||||
callTool: mockClientCallTool,
|
||||
@@ -153,7 +173,7 @@ describe('MultiServerMCPClient', () => {
|
||||
mockStdioTransportSend = vi.fn().mockReturnValue(Promise.resolve());
|
||||
mockStdioOnClose = null;
|
||||
|
||||
(StdioClientTransport as vi.Mock).mockImplementation(() => {
|
||||
(StdioClientTransport as Mock).mockImplementation(() => {
|
||||
const transport = {
|
||||
close: mockStdioTransportClose,
|
||||
connect: mockStdioTransportConnect,
|
||||
@@ -161,7 +181,7 @@ describe('MultiServerMCPClient', () => {
|
||||
onclose: null as (() => void) | null,
|
||||
};
|
||||
// Capture the onclose handler when it's set
|
||||
Object.defineProperty(transport, 'onclose', {
|
||||
Object.defineProperty(transport, "onclose", {
|
||||
get: () => mockStdioOnClose,
|
||||
set: (handler: () => void) => {
|
||||
mockStdioOnClose = handler;
|
||||
@@ -176,7 +196,7 @@ describe('MultiServerMCPClient', () => {
|
||||
mockSSETransportSend = vi.fn().mockReturnValue(Promise.resolve());
|
||||
mockSSEOnClose = null;
|
||||
|
||||
(SSEClientTransport as vi.Mock).mockImplementation(() => {
|
||||
(SSEClientTransport as Mock).mockImplementation(() => {
|
||||
const transport = {
|
||||
close: mockSSETransportClose,
|
||||
connect: mockSSETransportConnect,
|
||||
@@ -184,7 +204,7 @@ describe('MultiServerMCPClient', () => {
|
||||
onclose: null as (() => void) | null,
|
||||
};
|
||||
// Capture the onclose handler when it's set
|
||||
Object.defineProperty(transport, 'onclose', {
|
||||
Object.defineProperty(transport, "onclose", {
|
||||
get: () => mockSSEOnClose,
|
||||
set: (handler: () => void) => {
|
||||
mockSSEOnClose = handler;
|
||||
@@ -193,19 +213,19 @@ describe('MultiServerMCPClient', () => {
|
||||
return transport;
|
||||
});
|
||||
|
||||
(fs.readFileSync as vi.Mock).mockImplementation(() =>
|
||||
(fs.readFileSync as Mock).mockImplementation(() =>
|
||||
JSON.stringify({
|
||||
servers: {
|
||||
'test-server': {
|
||||
transport: 'stdio',
|
||||
command: 'python',
|
||||
args: ['./script.py'],
|
||||
"test-server": {
|
||||
transport: "stdio",
|
||||
command: "python",
|
||||
args: ["./script.py"],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
(path.resolve as vi.Mock).mockImplementation(p => p);
|
||||
(path.resolve as Mock).mockImplementation((p) => p);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -213,30 +233,30 @@ describe('MultiServerMCPClient', () => {
|
||||
});
|
||||
|
||||
// 1. Constructor functionality tests
|
||||
describe('constructor', () => {
|
||||
test('should initialize with empty connections', () => {
|
||||
describe("constructor", () => {
|
||||
test("should initialize with empty connections", () => {
|
||||
const client = new MultiServerMCPClient();
|
||||
expect(client).toBeDefined();
|
||||
});
|
||||
|
||||
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,12 +264,13 @@ 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 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);
|
||||
@@ -257,48 +278,48 @@ describe('MultiServerMCPClient', () => {
|
||||
});
|
||||
|
||||
// 2. Configuration Loading tests
|
||||
describe('fromConfigFile', () => {
|
||||
test('should load config from a valid file', () => {
|
||||
const client = MultiServerMCPClient.fromConfigFile('./mcp.json');
|
||||
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');
|
||||
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');
|
||||
test("should throw error for invalid config file", () => {
|
||||
(fs.readFileSync as Mock).mockImplementation(() => {
|
||||
throw new Error("File not found");
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
MultiServerMCPClient.fromConfigFile('./invalid.json');
|
||||
MultiServerMCPClient.fromConfigFile("./invalid.json");
|
||||
}).toThrow(MCPClientError);
|
||||
});
|
||||
|
||||
test('should throw error for invalid JSON in config file', () => {
|
||||
(fs.readFileSync as vi.Mock).mockImplementation(() => 'invalid json');
|
||||
test("should throw error for invalid JSON in config file", () => {
|
||||
(fs.readFileSync as Mock).mockImplementation(() => "invalid json");
|
||||
|
||||
expect(() => {
|
||||
MultiServerMCPClient.fromConfigFile('./invalid.json');
|
||||
MultiServerMCPClient.fromConfigFile("./invalid.json");
|
||||
}).toThrow(MCPClientError);
|
||||
});
|
||||
});
|
||||
|
||||
// 3. Connection Management tests
|
||||
describe('initializeConnections', () => {
|
||||
test('should initialize stdio connections correctly', async () => {
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -307,11 +328,11 @@ describe('MultiServerMCPClient', () => {
|
||||
expect(mockClientListTools).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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -323,49 +344,57 @@ describe('MultiServerMCPClient', () => {
|
||||
expect(mockClientListTools).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 throw on connection failure", async () => {
|
||||
(Client as Mock).mockImplementation(() => ({
|
||||
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).mockImplementation(() => ({
|
||||
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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// 4. Reconnection Logic tests
|
||||
describe('reconnection', () => {
|
||||
test('should attempt to reconnect stdio transport when enabled', async () => {
|
||||
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'],
|
||||
"test-server": {
|
||||
transport: "stdio",
|
||||
command: "python",
|
||||
args: ["./script.py"],
|
||||
restart: {
|
||||
enabled: true,
|
||||
maxAttempts: 3,
|
||||
@@ -377,7 +406,7 @@ describe('MultiServerMCPClient', () => {
|
||||
await client.initializeConnections();
|
||||
|
||||
// Reset the call counts to focus on reconnection
|
||||
(StdioClientTransport as vi.Mock).mockClear();
|
||||
(StdioClientTransport as Mock).mockClear();
|
||||
|
||||
// Trigger the onclose handler if it exists
|
||||
if (mockStdioOnClose) {
|
||||
@@ -385,17 +414,19 @@ describe('MultiServerMCPClient', () => {
|
||||
}
|
||||
|
||||
// Expect a new transport to be created after a delay (for reconnection)
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
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 () => {
|
||||
test("should attempt to reconnect SSE transport when enabled", async () => {
|
||||
const client = new MultiServerMCPClient({
|
||||
'test-server': {
|
||||
transport: 'sse',
|
||||
url: 'http://localhost:8000/sse',
|
||||
"test-server": {
|
||||
transport: "sse",
|
||||
url: "http://localhost:8000/sse",
|
||||
reconnect: {
|
||||
enabled: true,
|
||||
maxAttempts: 3,
|
||||
@@ -407,7 +438,7 @@ describe('MultiServerMCPClient', () => {
|
||||
await client.initializeConnections();
|
||||
|
||||
// Reset the call counts to focus on reconnection
|
||||
(SSEClientTransport as vi.Mock).mockClear();
|
||||
(SSEClientTransport as Mock).mockClear();
|
||||
|
||||
// Trigger the onclose handler if it exists
|
||||
if (mockSSEOnClose) {
|
||||
@@ -415,47 +446,51 @@ describe('MultiServerMCPClient', () => {
|
||||
}
|
||||
|
||||
// Expect a new transport to be created after a delay (for reconnection)
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
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 () => {
|
||||
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 () => {
|
||||
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 () => {
|
||||
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).mockImplementation(() => ({
|
||||
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"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -466,27 +501,27 @@ describe('MultiServerMCPClient', () => {
|
||||
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 () => {
|
||||
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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -498,18 +533,20 @@ describe('MultiServerMCPClient', () => {
|
||||
expect(mockSSETransportClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should handle errors during cleanup gracefully', async () => {
|
||||
test("should handle errors during cleanup gracefully", async () => {
|
||||
// Mock close to throw an error
|
||||
(StdioClientTransport as vi.Mock).mockImplementation(() => ({
|
||||
close: vi.fn().mockReturnValue(Promise.reject(new Error('Close failed'))),
|
||||
(StdioClientTransport as Mock).mockImplementation(() => ({
|
||||
close: vi
|
||||
.fn()
|
||||
.mockReturnValue(Promise.reject(new Error("Close failed"))),
|
||||
onclose: null,
|
||||
}));
|
||||
|
||||
const client = new MultiServerMCPClient({
|
||||
'test-server': {
|
||||
transport: 'stdio',
|
||||
command: 'python',
|
||||
args: ['./script.py'],
|
||||
"test-server": {
|
||||
transport: "stdio",
|
||||
command: "python",
|
||||
args: ["./script.py"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -521,14 +558,16 @@ describe('MultiServerMCPClient', () => {
|
||||
});
|
||||
|
||||
// 7. Specific Connection Method tests
|
||||
describe('connectToServerViaStdio', () => {
|
||||
test('should connect to a stdio server correctly', async () => {
|
||||
describe("connectToServerViaStdio", () => {
|
||||
test("should connect to a stdio server correctly", async () => {
|
||||
const client = new MultiServerMCPClient();
|
||||
await client.connectToServerViaStdio('test-server', 'python', ['./script.py']);
|
||||
await client.connectToServerViaStdio("test-server", "python", [
|
||||
"./script.py",
|
||||
]);
|
||||
|
||||
expect(StdioClientTransport).toHaveBeenCalledWith({
|
||||
command: 'python',
|
||||
args: ['./script.py'],
|
||||
command: "python",
|
||||
args: ["./script.py"],
|
||||
env: undefined,
|
||||
});
|
||||
|
||||
@@ -537,25 +576,30 @@ describe('MultiServerMCPClient', () => {
|
||||
expect(mockClientListTools).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should connect with environment variables', async () => {
|
||||
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);
|
||||
const env = { NODE_ENV: "test" };
|
||||
await client.connectToServerViaStdio(
|
||||
"test-server",
|
||||
"python",
|
||||
["./script.py"],
|
||||
env
|
||||
);
|
||||
|
||||
expect(StdioClientTransport).toHaveBeenCalledWith({
|
||||
command: 'python',
|
||||
args: ['./script.py'],
|
||||
command: "python",
|
||||
args: ["./script.py"],
|
||||
env,
|
||||
});
|
||||
});
|
||||
|
||||
test('should connect with restart configuration', async () => {
|
||||
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'],
|
||||
"test-server",
|
||||
"python",
|
||||
["./script.py"],
|
||||
undefined,
|
||||
restart
|
||||
);
|
||||
@@ -564,10 +608,13 @@ describe('MultiServerMCPClient', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('connectToServerViaSSE', () => {
|
||||
test('should connect to an SSE server correctly', async () => {
|
||||
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.connectToServerViaSSE(
|
||||
"test-server",
|
||||
"http://localhost:8000/sse"
|
||||
);
|
||||
|
||||
expect(SSEClientTransport).toHaveBeenCalled();
|
||||
expect(Client).toHaveBeenCalled();
|
||||
@@ -575,19 +622,23 @@ describe('MultiServerMCPClient', () => {
|
||||
expect(mockClientListTools).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should connect with headers', async () => {
|
||||
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);
|
||||
const headers = { Authorization: "Bearer token" };
|
||||
await client.connectToServerViaSSE(
|
||||
"test-server",
|
||||
"http://localhost:8000/sse",
|
||||
headers
|
||||
);
|
||||
|
||||
// Verify that headers were set correctly
|
||||
});
|
||||
|
||||
test('should connect with useNodeEventSource option', async () => {
|
||||
test("should connect with useNodeEventSource option", async () => {
|
||||
const client = new MultiServerMCPClient();
|
||||
await client.connectToServerViaSSE(
|
||||
'test-server',
|
||||
'http://localhost:8000/sse',
|
||||
"test-server",
|
||||
"http://localhost:8000/sse",
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
@@ -595,12 +646,12 @@ describe('MultiServerMCPClient', () => {
|
||||
// Verify that useNodeEventSource was set correctly
|
||||
});
|
||||
|
||||
test('should connect with reconnect configuration', async () => {
|
||||
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',
|
||||
"test-server",
|
||||
"http://localhost:8000/sse",
|
||||
undefined,
|
||||
undefined,
|
||||
reconnect
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -6,16 +6,25 @@
|
||||
*/
|
||||
|
||||
/* 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';
|
||||
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';
|
||||
import { MultiServerMCPClient } from "../src/index.js";
|
||||
|
||||
// Load environment variables from .env file
|
||||
dotenv.config();
|
||||
@@ -27,60 +36,74 @@ dotenv.config();
|
||||
async function runConfigTest() {
|
||||
try {
|
||||
// Log when we start
|
||||
console.log('Starting test with configuration files...');
|
||||
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');
|
||||
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));
|
||||
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 &&
|
||||
typeof serverConfig === "object" &&
|
||||
"headers" in serverConfig &&
|
||||
serverConfig.headers
|
||||
) {
|
||||
console.log(
|
||||
`Server ${serverName} has headers:`,
|
||||
Object.keys(serverConfig.headers).map(key => `${key}: ***`)
|
||||
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');
|
||||
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));
|
||||
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...');
|
||||
console.log("Connecting to math server directly...");
|
||||
|
||||
// Define the python executable (use 'python3' on systems where 'python' might not be in PATH)
|
||||
const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
|
||||
const pythonCmd = process.platform === "win32" ? "python" : "python3";
|
||||
|
||||
// Create a client with the math server only
|
||||
const client = new MultiServerMCPClient({
|
||||
math: {
|
||||
transport: 'stdio',
|
||||
transport: "stdio",
|
||||
command: pythonCmd,
|
||||
args: [path.join(process.cwd(), 'examples', 'math_server.py')],
|
||||
args: [path.join(process.cwd(), "examples", "math_server.py")],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -92,12 +115,12 @@ async function runConfigTest() {
|
||||
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(', '));
|
||||
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',
|
||||
modelName: process.env.OPENAI_MODEL_NAME || "gpt-4o",
|
||||
temperature: 0,
|
||||
}).bindTools(mcpTools);
|
||||
|
||||
@@ -106,7 +129,7 @@ async function runConfigTest() {
|
||||
|
||||
// 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] };
|
||||
};
|
||||
@@ -115,26 +138,26 @@ async function runConfigTest() {
|
||||
const workflow = new StateGraph(MessagesAnnotation)
|
||||
|
||||
// Add the nodes to the graph
|
||||
.addNode('llm', llmNode)
|
||||
.addNode('tools', toolNode)
|
||||
.addNode("llm", llmNode)
|
||||
.addNode("tools", toolNode)
|
||||
|
||||
// Add edges - need to cast to any to fix TypeScript errors
|
||||
.addEdge(START, 'llm')
|
||||
.addEdge('tools', 'llm')
|
||||
.addEdge(START, "llm")
|
||||
.addEdge("tools", "llm")
|
||||
|
||||
// Add conditional logic to determine the next step
|
||||
.addConditionalEdges('llm', state => {
|
||||
.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';
|
||||
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');
|
||||
console.log("No tool calls, ending the workflow");
|
||||
return END;
|
||||
});
|
||||
|
||||
@@ -144,9 +167,9 @@ async function runConfigTest() {
|
||||
// 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?',
|
||||
"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
|
||||
@@ -157,7 +180,7 @@ async function runConfigTest() {
|
||||
// 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.'
|
||||
"You are a helpful assistant that can use tools to solve math problems."
|
||||
),
|
||||
new HumanMessage(query),
|
||||
];
|
||||
@@ -166,7 +189,9 @@ async function runConfigTest() {
|
||||
const result = await app.invoke({ messages });
|
||||
|
||||
// Get the last AI message as the response
|
||||
const lastMessage = result.messages.filter(message => message._getType() === 'ai').pop();
|
||||
const lastMessage = result.messages
|
||||
.filter((message) => message._getType() === "ai")
|
||||
.pop();
|
||||
|
||||
console.log(`\nFinal Answer: ${lastMessage?.content}`);
|
||||
} catch (error) {
|
||||
@@ -175,22 +200,22 @@ async function runConfigTest() {
|
||||
}
|
||||
|
||||
// Close all connections
|
||||
console.log('\nClosing connections...');
|
||||
console.log("\nClosing connections...");
|
||||
await client.close();
|
||||
|
||||
console.log('Test completed successfully');
|
||||
console.log("Test completed successfully");
|
||||
} catch (error) {
|
||||
console.error('Error running test:', error);
|
||||
console.error("Error running test:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test
|
||||
runConfigTest()
|
||||
.then(() => {
|
||||
console.log('Configuration test completed successfully');
|
||||
console.log("Configuration test completed successfully");
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error running configuration test:', error);
|
||||
.catch((error) => {
|
||||
console.error("Error running configuration test:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -11,16 +11,26 @@
|
||||
*/
|
||||
|
||||
/* 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 } from '@langchain/core/messages';
|
||||
import dotenv from 'dotenv';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
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,36 +41,39 @@ dotenv.config();
|
||||
*/
|
||||
export async function runExample(client?: MultiServerMCPClient) {
|
||||
try {
|
||||
console.log('Initializing MCP client...');
|
||||
console.log("Initializing MCP client...");
|
||||
|
||||
// Create a client with configurations for the filesystem server
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
client =
|
||||
client ??
|
||||
new MultiServerMCPClient({
|
||||
filesystem: {
|
||||
transport: 'stdio',
|
||||
command: 'npx',
|
||||
transport: "stdio",
|
||||
command: "npx",
|
||||
args: [
|
||||
'-y',
|
||||
'@modelcontextprotocol/server-filesystem',
|
||||
'./examples/filesystem_test', // This directory needs to exist
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-filesystem",
|
||||
"./examples/filesystem_test", // This directory needs to exist
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// Initialize connections to the server
|
||||
await client.initializeConnections();
|
||||
console.log('Connected to server');
|
||||
console.log("Connected to server");
|
||||
|
||||
// Get all tools (flattened array is the default now)
|
||||
const mcpTools = client.getTools();
|
||||
|
||||
if (mcpTools.length === 0) {
|
||||
throw new Error('No tools found');
|
||||
throw new Error("No tools found");
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Loaded ${mcpTools.length} MCP tools: ${mcpTools.map(tool => tool.name).join(', ')}`
|
||||
`Loaded ${mcpTools.length} MCP tools: ${mcpTools
|
||||
.map((tool) => tool.name)
|
||||
.join(", ")}`
|
||||
);
|
||||
|
||||
// Create an OpenAI model with tools attached
|
||||
@@ -74,7 +87,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-4o-mini',
|
||||
modelName: process.env.OPENAI_MODEL_NAME || "gpt-4o-mini",
|
||||
temperature: 0,
|
||||
}).bindTools(mcpTools);
|
||||
|
||||
@@ -84,15 +97,15 @@ For reading multiple files, you can use the read_multiple_files tool.`;
|
||||
// ================================================
|
||||
// Create a LangGraph agent flow
|
||||
// ================================================
|
||||
console.log('\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) => {
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -104,31 +117,33 @@ For reading multiple files, you can use the read_multiple_files tool.`;
|
||||
const workflow = new StateGraph(MessagesAnnotation)
|
||||
|
||||
// Add the nodes to the graph
|
||||
.addNode('llm', llmNode)
|
||||
.addNode('tools', toolNode)
|
||||
.addNode("llm", llmNode)
|
||||
.addNode("tools", toolNode)
|
||||
|
||||
// Add edges - these define how nodes are connected
|
||||
.addEdge(START, 'llm')
|
||||
.addEdge('tools', 'llm')
|
||||
.addEdge(START, "llm")
|
||||
.addEdge("tools", "llm")
|
||||
|
||||
// Conditional routing to end or continue the tool loop
|
||||
.addConditionalEdges('llm', state => {
|
||||
.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');
|
||||
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(', ');
|
||||
const toolNames = aiMessage.tool_calls
|
||||
.map((tc) => tc.name)
|
||||
.join(", ");
|
||||
console.log(`Tools being called: ${toolNames}`);
|
||||
|
||||
return 'tools';
|
||||
return "tools";
|
||||
}
|
||||
|
||||
// If there are no tool calls, we're done
|
||||
console.log('No tool calls, ending the workflow');
|
||||
console.log("No tool calls, ending the workflow");
|
||||
return END;
|
||||
});
|
||||
|
||||
@@ -138,29 +153,29 @@ 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) {
|
||||
console.log(`\n--- Example: ${example.name} ---`);
|
||||
@@ -176,33 +191,33 @@ For reading multiple files, you can use the read_multiple_files tool.`;
|
||||
console.log(`\nResult: ${finalMessage.content}`);
|
||||
|
||||
// Let's list the directory to see the changes
|
||||
console.log('\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];
|
||||
console.log(listMessage.content);
|
||||
} catch (error) {
|
||||
console.error('Error listing directory:', error);
|
||||
console.error("Error listing directory:", error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
console.error("Error:", error);
|
||||
process.exit(1); // Exit with error code
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close();
|
||||
console.log('Closed all MCP connections');
|
||||
console.log("Closed 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);
|
||||
}
|
||||
@@ -212,7 +227,7 @@ 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 });
|
||||
@@ -224,5 +239,5 @@ const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isMainModule) {
|
||||
setupTestDirectory()
|
||||
.then(() => runExample())
|
||||
.catch(error => console.error('Setup error:', error));
|
||||
.catch((error) => console.error("Setup error:", error));
|
||||
}
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
import { HumanMessage } from '@langchain/core/messages';
|
||||
import dotenv from 'dotenv';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { HumanMessage } from "@langchain/core/messages";
|
||||
import dotenv from "dotenv";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
// MCP client imports
|
||||
import { MultiServerMCPClient } from '../src/index.js';
|
||||
import { MultiServerMCPClient } from "../src/index.js";
|
||||
|
||||
// Load environment variables from .env file
|
||||
dotenv.config();
|
||||
@@ -23,16 +23,20 @@ dotenv.config();
|
||||
* Create a custom configuration file for Firecrawl
|
||||
*/
|
||||
function createConfigFile(): string {
|
||||
const configPath = path.join(process.cwd(), 'examples', 'firecrawl_config.json');
|
||||
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',
|
||||
transport: "sse",
|
||||
url: process.env.FIRECRAWL_SERVER_URL || "http://localhost:8000/v1/mcp",
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.FIRECRAWL_API_KEY || 'demo'}`,
|
||||
Authorization: `Bearer ${process.env.FIRECRAWL_API_KEY || "demo"}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -52,7 +56,7 @@ 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);
|
||||
|
||||
@@ -62,28 +66,28 @@ async function runExample() {
|
||||
console.log(`Created custom configuration file at: ${configPath}`);
|
||||
|
||||
// Initialize the MCP client with the custom configuration
|
||||
console.log('Initializing MCP client from custom configuration file...');
|
||||
console.log("Initializing MCP client from custom configuration file...");
|
||||
client = MultiServerMCPClient.fromConfigFile(configPath);
|
||||
|
||||
// Connect to the servers
|
||||
await client.initializeConnections();
|
||||
console.log('Connected to servers from custom configuration');
|
||||
console.log("Connected to servers from custom configuration");
|
||||
|
||||
// Get Firecrawl tools specifically
|
||||
const mcpTools = client.getTools();
|
||||
const firecrawlTools = mcpTools.filter(
|
||||
tool => client!.getServerForTool(tool.name) === 'firecrawl'
|
||||
(tool) => client!.getServerForTool(tool.name) === "firecrawl"
|
||||
);
|
||||
|
||||
if (firecrawlTools.length === 0) {
|
||||
throw new Error('No Firecrawl tools found');
|
||||
throw new Error("No Firecrawl tools found");
|
||||
}
|
||||
|
||||
console.log(`Found ${firecrawlTools.length} Firecrawl tools`);
|
||||
|
||||
// Initialize the LLM
|
||||
const model = new ChatOpenAI({
|
||||
modelName: process.env.OPENAI_MODEL_NAME || 'gpt-3.5-turbo',
|
||||
modelName: process.env.OPENAI_MODEL_NAME || "gpt-3.5-turbo",
|
||||
temperature: 0,
|
||||
});
|
||||
|
||||
@@ -95,7 +99,7 @@ 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";
|
||||
|
||||
console.log(`Running agent with query: ${query}`);
|
||||
|
||||
@@ -104,8 +108,8 @@ async function runExample() {
|
||||
messages: [new HumanMessage(query)],
|
||||
});
|
||||
|
||||
console.log('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
|
||||
@@ -113,25 +117,25 @@ async function runExample() {
|
||||
|
||||
// Clean up the temporary configuration file
|
||||
fs.unlinkSync(configPath);
|
||||
console.log('Removed temporary configuration file');
|
||||
console.log("Removed temporary configuration file");
|
||||
} catch (error) {
|
||||
console.error('Error in example:', error);
|
||||
console.error("Error in example:", error);
|
||||
} finally {
|
||||
// Close all MCP connections
|
||||
if (client) {
|
||||
console.log('Closing all MCP connections...');
|
||||
console.log("Closing all MCP connections...");
|
||||
await client.close();
|
||||
console.log('All MCP connections closed');
|
||||
console.log("All MCP connections closed");
|
||||
}
|
||||
|
||||
// Clear the timeout if it hasn't fired yet
|
||||
clearTimeout(timeout);
|
||||
|
||||
// Complete the example
|
||||
console.log('Example execution completed');
|
||||
console.log("Example execution completed");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the example
|
||||
runExample();
|
||||
runExample().catch(console.error);
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
*/
|
||||
|
||||
/* 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';
|
||||
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 { MultiServerMCPClient } from "../src/index.js";
|
||||
|
||||
// Load environment variables from .env file
|
||||
dotenv.config();
|
||||
@@ -25,33 +25,33 @@ 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 {
|
||||
console.log('Initializing MCP client from default configuration file...');
|
||||
console.log("Initializing MCP client from default configuration file...");
|
||||
|
||||
// The client will automatically look for and load mcp.json from the current directory
|
||||
client = new MultiServerMCPClient();
|
||||
await client.initializeConnections();
|
||||
console.log('Connected to servers from default configuration');
|
||||
console.log("Connected to servers from default configuration");
|
||||
|
||||
// Get Firecrawl tools specifically
|
||||
const mcpTools = client.getTools();
|
||||
const firecrawlTools = mcpTools.filter(
|
||||
tool => client!.getServerForTool(tool.name) === 'firecrawl'
|
||||
(tool) => client!.getServerForTool(tool.name) === "firecrawl"
|
||||
);
|
||||
|
||||
if (firecrawlTools.length === 0) {
|
||||
throw new Error('No Firecrawl tools found');
|
||||
throw new Error("No Firecrawl tools found");
|
||||
}
|
||||
|
||||
console.log(`Found ${firecrawlTools.length} Firecrawl tools`);
|
||||
|
||||
// Initialize the LLM
|
||||
const model = new ChatOpenAI({
|
||||
modelName: process.env.OPENAI_MODEL_NAME || 'gpt-3.5-turbo',
|
||||
modelName: process.env.OPENAI_MODEL_NAME || "gpt-3.5-turbo",
|
||||
temperature: 0,
|
||||
});
|
||||
|
||||
@@ -62,7 +62,8 @@ async function runExample() {
|
||||
});
|
||||
|
||||
// Define a query for testing Firecrawl
|
||||
const query = 'Scrape the content from https://example.com and summarize it in bullet points';
|
||||
const query =
|
||||
"Scrape the content from https://example.com and summarize it in bullet points";
|
||||
|
||||
console.log(`Running agent with query: ${query}`);
|
||||
|
||||
@@ -71,30 +72,30 @@ async function runExample() {
|
||||
messages: [new HumanMessage(query)],
|
||||
});
|
||||
|
||||
console.log('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);
|
||||
} catch (error) {
|
||||
console.log('Error in example:', error);
|
||||
console.log("Error in example:", error);
|
||||
} finally {
|
||||
// Close all MCP connections
|
||||
if (client) {
|
||||
console.log('Closing all MCP connections...');
|
||||
console.log("Closing all MCP connections...");
|
||||
await client.close();
|
||||
console.log('All MCP connections closed');
|
||||
console.log("All MCP connections closed");
|
||||
}
|
||||
|
||||
// Clear the timeout if it hasn't fired yet
|
||||
clearTimeout(timeout);
|
||||
|
||||
// Complete the example
|
||||
console.log('Example execution completed');
|
||||
console.log("Example execution completed");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the example
|
||||
runExample();
|
||||
runExample().catch(console.error);
|
||||
|
||||
@@ -8,22 +8,31 @@
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
import { StateGraph, END, START, MessagesAnnotation } from '@langchain/langgraph';
|
||||
import { ToolNode } from '@langchain/langgraph/prebuilt';
|
||||
import { HumanMessage, AIMessage } from '@langchain/core/messages';
|
||||
import dotenv from 'dotenv';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import {
|
||||
StateGraph,
|
||||
END,
|
||||
START,
|
||||
MessagesAnnotation,
|
||||
} from "@langchain/langgraph";
|
||||
import { ToolNode } from "@langchain/langgraph/prebuilt";
|
||||
import { HumanMessage, AIMessage } from "@langchain/core/messages";
|
||||
import dotenv from "dotenv";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
// MCP client imports
|
||||
import { MultiServerMCPClient } from '../src/index.js';
|
||||
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');
|
||||
const additionalConfigPath = path.join(
|
||||
process.cwd(),
|
||||
"examples",
|
||||
"additional_servers.json"
|
||||
);
|
||||
|
||||
/**
|
||||
* Create an additional configuration file with extra servers
|
||||
@@ -32,16 +41,21 @@ 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')],
|
||||
"custom-server": {
|
||||
transport: "stdio",
|
||||
command: "python",
|
||||
args: [path.join(process.cwd(), "examples", "weather_server.py")],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(additionalConfigPath, JSON.stringify(configContent, null, 2));
|
||||
console.log(`Created additional configuration file at ${additionalConfigPath}`);
|
||||
fs.writeFileSync(
|
||||
additionalConfigPath,
|
||||
JSON.stringify(configContent, null, 2)
|
||||
);
|
||||
console.log(
|
||||
`Created additional configuration file at ${additionalConfigPath}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,7 +66,7 @@ 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);
|
||||
|
||||
@@ -60,7 +74,9 @@ async function runExample() {
|
||||
// Create the additional configuration file
|
||||
createAdditionalConfigFile();
|
||||
|
||||
console.log('Initializing MCP client with enhanced configuration loading...');
|
||||
console.log(
|
||||
"Initializing MCP client with enhanced configuration loading..."
|
||||
);
|
||||
|
||||
// Create a client - it will automatically load from mcp.json if it exists
|
||||
client = new MultiServerMCPClient();
|
||||
@@ -71,56 +87,62 @@ async function runExample() {
|
||||
// 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");'],
|
||||
"inline-server": {
|
||||
transport: "stdio",
|
||||
command: "node",
|
||||
args: ["-e", 'console.log("This is an inline server example");'],
|
||||
},
|
||||
});
|
||||
|
||||
// Initialize all connections from the merged configurations
|
||||
await client.initializeConnections();
|
||||
console.log('Connected to servers from all configurations');
|
||||
console.log("Connected to servers from all configurations");
|
||||
|
||||
// Get all tools from all servers
|
||||
const mcpTools = client.getTools();
|
||||
|
||||
if (mcpTools.length === 0) {
|
||||
throw new Error('No tools found');
|
||||
throw new Error("No tools found");
|
||||
}
|
||||
|
||||
console.log(`Loaded ${mcpTools.length} MCP tools in total`);
|
||||
|
||||
// Filter tools from different servers
|
||||
const firecrawlTools = mcpTools.filter(
|
||||
tool => client!.getServerForTool(tool.name) === 'firecrawl'
|
||||
(tool) => client!.getServerForTool(tool.name) === "firecrawl"
|
||||
);
|
||||
|
||||
if (firecrawlTools.length === 0) {
|
||||
console.log('No Firecrawl tools found, using math tools for the example');
|
||||
console.log("No Firecrawl tools found, using math tools for the example");
|
||||
// In this case, use math tools as a fallback
|
||||
const mathTools = mcpTools.filter(tool => client!.getServerForTool(tool.name) === 'math');
|
||||
const mathTools = mcpTools.filter(
|
||||
(tool) => client!.getServerForTool(tool.name) === "math"
|
||||
);
|
||||
|
||||
if (mathTools.length > 0) {
|
||||
console.log(
|
||||
`Using ${mathTools.length} math tools: ${mathTools.map(tool => tool.name).join(', ')}`
|
||||
`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');
|
||||
console.log("\n=== MATH TOOLS EXAMPLE ===");
|
||||
console.log("Math example completed successfully");
|
||||
return;
|
||||
} else {
|
||||
throw new Error('No suitable tools found for the example');
|
||||
throw new Error("No suitable tools found for the example");
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Loaded ${firecrawlTools.length} Firecrawl tools: ${firecrawlTools.map(tool => tool.name).join(', ')}`
|
||||
`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',
|
||||
modelName: process.env.OPENAI_MODEL_NAME || "gpt-4o",
|
||||
temperature: 0,
|
||||
}).bindTools(firecrawlTools);
|
||||
|
||||
@@ -130,11 +152,11 @@ async function runExample() {
|
||||
// ================================================
|
||||
// Create a LangGraph agent flow
|
||||
// ================================================
|
||||
console.log('\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) => {
|
||||
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] };
|
||||
};
|
||||
@@ -143,24 +165,24 @@ async function runExample() {
|
||||
const workflow = new StateGraph(MessagesAnnotation)
|
||||
|
||||
// Add the nodes to the graph
|
||||
.addNode('llm', llmNode)
|
||||
.addNode('tools', toolNode)
|
||||
.addNode("llm", llmNode)
|
||||
.addNode("tools", toolNode)
|
||||
|
||||
// Add edges - these define how nodes are connected
|
||||
.addEdge(START, 'llm')
|
||||
.addEdge('tools', 'llm')
|
||||
.addEdge(START, "llm")
|
||||
.addEdge("tools", "llm")
|
||||
|
||||
// Conditional routing to end or continue the tool loop
|
||||
.addConditionalEdges('llm', state => {
|
||||
.addConditionalEdges("llm", (state) => {
|
||||
const lastMessage = state.messages[state.messages.length - 1];
|
||||
const aiMessage = lastMessage as AIMessage;
|
||||
|
||||
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) {
|
||||
console.log('Tool calls detected, routing to tools node');
|
||||
return 'tools';
|
||||
console.log("Tool calls detected, routing to tools node");
|
||||
return "tools";
|
||||
}
|
||||
|
||||
console.log('No tool calls, ending the workflow');
|
||||
console.log("No tool calls, ending the workflow");
|
||||
return END;
|
||||
});
|
||||
|
||||
@@ -172,7 +194,7 @@ async function runExample() {
|
||||
'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("\n=== RUNNING LANGGRAPH AGENT ===");
|
||||
console.log(`\nQuery: ${query}`);
|
||||
|
||||
try {
|
||||
@@ -184,7 +206,8 @@ async function runExample() {
|
||||
// Run with a 20-second timeout
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(
|
||||
() => reject(new Error('LangGraph execution timed out after 20 seconds')),
|
||||
() =>
|
||||
reject(new Error("LangGraph execution timed out after 20 seconds")),
|
||||
20000
|
||||
);
|
||||
});
|
||||
@@ -196,11 +219,11 @@ async function runExample() {
|
||||
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...');
|
||||
console.error("LangGraph execution error:", error);
|
||||
console.log("Continuing with cleanup...");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
console.error("Error:", error);
|
||||
process.exit(1); // Exit with error code
|
||||
} finally {
|
||||
// Clear the global timeout
|
||||
@@ -209,22 +232,24 @@ async function runExample() {
|
||||
// Close all client connections
|
||||
if (client) {
|
||||
await client.close();
|
||||
console.log('\nClosed all connections');
|
||||
console.log("\nClosed all connections");
|
||||
}
|
||||
|
||||
// Clean up our additional config file
|
||||
if (fs.existsSync(additionalConfigPath)) {
|
||||
fs.unlinkSync(additionalConfigPath);
|
||||
console.log(`Cleaned up additional configuration file at ${additionalConfigPath}`);
|
||||
console.log(
|
||||
`Cleaned up additional configuration file at ${additionalConfigPath}`
|
||||
);
|
||||
}
|
||||
|
||||
// Exit process after a short delay to allow for cleanup
|
||||
setTimeout(() => {
|
||||
console.log('Example completed, exiting process.');
|
||||
console.log("Example completed, exiting process.");
|
||||
process.exit(0);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the example
|
||||
runExample();
|
||||
runExample().catch(console.error);
|
||||
|
||||
@@ -7,22 +7,31 @@
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
import { StateGraph, END, START, MessagesAnnotation } from '@langchain/langgraph';
|
||||
import { ToolNode } from '@langchain/langgraph/prebuilt';
|
||||
import { HumanMessage, AIMessage, BaseMessage } from '@langchain/core/messages';
|
||||
import dotenv from 'dotenv';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import {
|
||||
StateGraph,
|
||||
END,
|
||||
START,
|
||||
MessagesAnnotation,
|
||||
} from "@langchain/langgraph";
|
||||
import { ToolNode } from "@langchain/langgraph/prebuilt";
|
||||
import { HumanMessage, AIMessage, BaseMessage } from "@langchain/core/messages";
|
||||
import dotenv from "dotenv";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
// MCP client imports
|
||||
import { MultiServerMCPClient } from '../src/index.js';
|
||||
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');
|
||||
const partialConfigPath = path.join(
|
||||
process.cwd(),
|
||||
"examples",
|
||||
"math_server_config.json"
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a configuration file for just the math server
|
||||
@@ -31,9 +40,9 @@ function createMathServerConfigFile() {
|
||||
const configContent = {
|
||||
servers: {
|
||||
math: {
|
||||
transport: 'stdio',
|
||||
command: 'python',
|
||||
args: [path.join(process.cwd(), 'examples', 'math_server.py')],
|
||||
transport: "stdio",
|
||||
command: "python",
|
||||
args: [path.join(process.cwd(), "examples", "math_server.py")],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -53,50 +62,63 @@ async function runExample() {
|
||||
// Create the math server configuration file
|
||||
createMathServerConfigFile();
|
||||
|
||||
console.log('Initializing MCP client from math server configuration file...');
|
||||
console.log(
|
||||
"Initializing MCP client from math server configuration file..."
|
||||
);
|
||||
|
||||
// Create a client from the configuration file
|
||||
client = MultiServerMCPClient.fromConfigFile(partialConfigPath);
|
||||
|
||||
// Initialize connections to the math server
|
||||
await client.initializeConnections();
|
||||
console.log('Connected to math server from configuration');
|
||||
console.log("Connected to math server from configuration");
|
||||
|
||||
// Now add the firecrawl server directly in code
|
||||
console.log('Adding firecrawl server directly in code...');
|
||||
await client.connectToServerViaStdio('firecrawl', 'npx', ['-y', 'firecrawl-mcp'], {
|
||||
// Adding the API key from environment
|
||||
FIRECRAWL_API_KEY: process.env.FIRECRAWL_API_KEY || '',
|
||||
// Optional configurations
|
||||
FIRECRAWL_RETRY_MAX_ATTEMPTS: '3',
|
||||
});
|
||||
console.log("Adding firecrawl server directly in code...");
|
||||
await client.connectToServerViaStdio(
|
||||
"firecrawl",
|
||||
"npx",
|
||||
["-y", "firecrawl-mcp"],
|
||||
{
|
||||
// Adding the API key from environment
|
||||
FIRECRAWL_API_KEY: process.env.FIRECRAWL_API_KEY || "",
|
||||
// Optional configurations
|
||||
FIRECRAWL_RETRY_MAX_ATTEMPTS: "3",
|
||||
}
|
||||
);
|
||||
|
||||
console.log('Connected to firecrawl server directly');
|
||||
console.log("Connected to firecrawl server directly");
|
||||
|
||||
// Get all tools from all servers
|
||||
const mcpTools = client.getTools();
|
||||
|
||||
if (mcpTools.length === 0) {
|
||||
throw new Error('No tools found');
|
||||
throw new Error("No tools found");
|
||||
}
|
||||
|
||||
// Filter tools from different servers
|
||||
const mathTools = mcpTools.filter(tool => client!.getServerForTool(tool.name) === 'math');
|
||||
const mathTools = mcpTools.filter(
|
||||
(tool) => client!.getServerForTool(tool.name) === "math"
|
||||
);
|
||||
const firecrawlTools = mcpTools.filter(
|
||||
tool => client!.getServerForTool(tool.name) === 'firecrawl'
|
||||
(tool) => client!.getServerForTool(tool.name) === "firecrawl"
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Loaded ${mathTools.length} math tools: ${mathTools.map(tool => tool.name).join(', ')}`
|
||||
`Loaded ${mathTools.length} math tools: ${mathTools
|
||||
.map((tool) => tool.name)
|
||||
.join(", ")}`
|
||||
);
|
||||
console.log(
|
||||
`Loaded ${firecrawlTools.length} firecrawl tools: ${firecrawlTools.map(tool => tool.name).join(', ')}`
|
||||
`Loaded ${firecrawlTools.length} firecrawl tools: ${firecrawlTools
|
||||
.map((tool) => tool.name)
|
||||
.join(", ")}`
|
||||
);
|
||||
console.log(`Loaded ${mcpTools.length} tools in total`);
|
||||
|
||||
// Create an OpenAI model and bind the tools
|
||||
const model = new ChatOpenAI({
|
||||
modelName: process.env.OPENAI_MODEL_NAME || 'gpt-4o',
|
||||
modelName: process.env.OPENAI_MODEL_NAME || "gpt-4o",
|
||||
temperature: 0,
|
||||
}).bindTools(mcpTools);
|
||||
|
||||
@@ -106,11 +128,11 @@ async function runExample() {
|
||||
// ================================================
|
||||
// Create a LangGraph agent flow
|
||||
// ================================================
|
||||
console.log('\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) => {
|
||||
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] };
|
||||
};
|
||||
@@ -119,24 +141,24 @@ async function runExample() {
|
||||
const workflow = new StateGraph(MessagesAnnotation)
|
||||
|
||||
// Add the nodes to the graph
|
||||
.addNode('llm', llmNode)
|
||||
.addNode('tools', toolNode)
|
||||
.addNode("llm", llmNode)
|
||||
.addNode("tools", toolNode)
|
||||
|
||||
// Add edges - these define how nodes are connected
|
||||
.addEdge(START, 'llm')
|
||||
.addEdge('tools', 'llm')
|
||||
.addEdge(START, "llm")
|
||||
.addEdge("tools", "llm")
|
||||
|
||||
// Conditional routing to end or continue the tool loop
|
||||
.addConditionalEdges('llm', state => {
|
||||
.addConditionalEdges("llm", (state) => {
|
||||
const lastMessage = state.messages[state.messages.length - 1];
|
||||
const aiMessage = lastMessage as AIMessage;
|
||||
|
||||
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) {
|
||||
console.log('Tool calls detected, routing to tools node');
|
||||
return 'tools';
|
||||
console.log("Tool calls detected, routing to tools node");
|
||||
return "tools";
|
||||
}
|
||||
|
||||
console.log('No tool calls, ending the workflow');
|
||||
console.log("No tool calls, ending the workflow");
|
||||
return END;
|
||||
});
|
||||
|
||||
@@ -145,10 +167,10 @@ async function runExample() {
|
||||
|
||||
// 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.';
|
||||
"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("\n=== RUNNING LANGGRAPH AGENT ===");
|
||||
console.log(`\nQuery: ${query}`);
|
||||
|
||||
// Run the LangGraph agent with the query
|
||||
@@ -159,37 +181,43 @@ async function runExample() {
|
||||
// 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';
|
||||
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)
|
||||
}`
|
||||
);
|
||||
});
|
||||
|
||||
const finalMessage = result.messages[result.messages.length - 1];
|
||||
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(partialConfigPath)) {
|
||||
fs.unlinkSync(partialConfigPath);
|
||||
console.log(`Cleaned up math server configuration file at ${partialConfigPath}`);
|
||||
console.log(
|
||||
`Cleaned up math server configuration file at ${partialConfigPath}`
|
||||
);
|
||||
}
|
||||
|
||||
// Exit process after a short delay to allow for cleanup
|
||||
setTimeout(() => {
|
||||
console.log('Example completed, exiting process.');
|
||||
console.log("Example completed, exiting process.");
|
||||
process.exit(0);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the example
|
||||
runExample();
|
||||
runExample().catch(console.error);
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
*/
|
||||
|
||||
/* 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';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { HumanMessage } from "@langchain/core/messages";
|
||||
import dotenv from "dotenv";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
// MCP client imports
|
||||
import { MultiServerMCPClient } from '../src/index.js';
|
||||
import { MultiServerMCPClient } from "../src/index.js";
|
||||
|
||||
// Load environment variables from .env file
|
||||
dotenv.config();
|
||||
@@ -22,8 +22,8 @@ dotenv.config();
|
||||
// Path for our multiple servers config file
|
||||
const multipleServersConfigPath = path.join(
|
||||
process.cwd(),
|
||||
'examples',
|
||||
'multiple_servers_config.json'
|
||||
"examples",
|
||||
"multiple_servers_config.json"
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -34,25 +34,30 @@ function createMultipleServersConfigFile() {
|
||||
servers: {
|
||||
// Firecrawl server configuration
|
||||
firecrawl: {
|
||||
transport: 'stdio',
|
||||
command: 'npx',
|
||||
args: ['-y', 'firecrawl-mcp'],
|
||||
transport: "stdio",
|
||||
command: "npx",
|
||||
args: ["-y", "firecrawl-mcp"],
|
||||
env: {
|
||||
FIRECRAWL_API_KEY: process.env.FIRECRAWL_API_KEY || '',
|
||||
FIRECRAWL_RETRY_MAX_ATTEMPTS: '3',
|
||||
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')],
|
||||
transport: "stdio",
|
||||
command: "python",
|
||||
args: [path.join(process.cwd(), "examples", "math_server.py")],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(multipleServersConfigPath, JSON.stringify(configContent, null, 2));
|
||||
console.log(`Created multiple servers configuration file at ${multipleServersConfigPath}`);
|
||||
fs.writeFileSync(
|
||||
multipleServersConfigPath,
|
||||
JSON.stringify(configContent, null, 2)
|
||||
);
|
||||
console.log(
|
||||
`Created multiple servers configuration file at ${multipleServersConfigPath}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,36 +71,40 @@ async function runExample() {
|
||||
// Create the multiple servers configuration file
|
||||
createMultipleServersConfigFile();
|
||||
|
||||
console.log('Initializing MCP client from multiple servers configuration file...');
|
||||
console.log(
|
||||
"Initializing MCP client from multiple servers configuration file..."
|
||||
);
|
||||
|
||||
// Create a client from the configuration file
|
||||
client = MultiServerMCPClient.fromConfigFile(multipleServersConfigPath);
|
||||
|
||||
// Initialize connections to all servers in the configuration
|
||||
await client.initializeConnections();
|
||||
console.log('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();
|
||||
|
||||
if (mcpTools.length === 0) {
|
||||
throw new Error('No tools found');
|
||||
throw new Error("No tools found");
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Loaded ${mcpTools.length} MCP tools: ${mcpTools.map(tool => tool.name).join(', ')}`
|
||||
`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({
|
||||
@@ -105,13 +114,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}`);
|
||||
@@ -126,28 +135,30 @@ 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);
|
||||
console.log(`Cleaned up multiple servers configuration file at ${multipleServersConfigPath}`);
|
||||
console.log(
|
||||
`Cleaned up multiple servers configuration file at ${multipleServersConfigPath}`
|
||||
);
|
||||
}
|
||||
|
||||
// Exit process after a short delay to allow for cleanup
|
||||
setTimeout(() => {
|
||||
console.log('Example completed, exiting process.');
|
||||
console.log("Example completed, exiting process.");
|
||||
process.exit(0);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the example
|
||||
runExample();
|
||||
runExample().catch(console.error);
|
||||
|
||||
@@ -23,14 +23,19 @@
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
import { StateGraph, END, START, MessagesAnnotation } from '@langchain/langgraph';
|
||||
import { ToolNode } from '@langchain/langgraph/prebuilt';
|
||||
import { HumanMessage, AIMessage, BaseMessage } from '@langchain/core/messages';
|
||||
import dotenv from 'dotenv';
|
||||
import { 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();
|
||||
@@ -43,41 +48,43 @@ async function runExample() {
|
||||
let client: MultiServerMCPClient | null = null;
|
||||
|
||||
try {
|
||||
console.log('Initializing MCP client...');
|
||||
console.log("Initializing MCP client...");
|
||||
|
||||
// Create a client with configurations for the math server only
|
||||
client = new MultiServerMCPClient({
|
||||
math: {
|
||||
transport: 'stdio',
|
||||
command: 'python',
|
||||
args: ['./examples/math_server.py'],
|
||||
transport: "stdio",
|
||||
command: "python",
|
||||
args: ["./examples/math_server.py"],
|
||||
},
|
||||
});
|
||||
|
||||
// Initialize connections to the server
|
||||
await client.initializeConnections();
|
||||
console.log('Connected to server');
|
||||
console.log("Connected to server");
|
||||
|
||||
// Connect to the math server
|
||||
await client.connectToServerViaStdio('math', 'npx', [
|
||||
'-y',
|
||||
'@modelcontextprotocol/server-math',
|
||||
await client.connectToServerViaStdio("math", "npx", [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-math",
|
||||
]);
|
||||
|
||||
// Get the tools (flattened array is the default now)
|
||||
const mcpTools = client.getTools();
|
||||
|
||||
if (mcpTools.length === 0) {
|
||||
throw new Error('No tools found');
|
||||
throw new Error("No tools found");
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Loaded ${mcpTools.length} MCP tools: ${mcpTools.map(tool => tool.name).join(', ')}`
|
||||
`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);
|
||||
|
||||
@@ -87,7 +94,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.
|
||||
@@ -99,7 +106,7 @@ 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] };
|
||||
};
|
||||
@@ -108,30 +115,30 @@ async function runExample() {
|
||||
const workflow = new StateGraph(MessagesAnnotation)
|
||||
|
||||
// Add the nodes to the graph
|
||||
.addNode('llm', llmNode)
|
||||
.addNode('tools', toolNode)
|
||||
.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
|
||||
.addEdge(START, 'llm')
|
||||
.addEdge('tools', 'llm')
|
||||
.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
|
||||
.addConditionalEdges('llm', state => {
|
||||
.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';
|
||||
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');
|
||||
console.log("No tool calls, ending the workflow");
|
||||
return END;
|
||||
});
|
||||
|
||||
@@ -140,10 +147,10 @@ async function runExample() {
|
||||
const app = workflow.compile();
|
||||
|
||||
// Define queries for testing
|
||||
const queries = ['What is 5 + 3?', 'What is 7 * 9?'];
|
||||
const queries = ["What is 5 + 3?", "What is 7 * 9?"];
|
||||
|
||||
// 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}`);
|
||||
|
||||
@@ -156,9 +163,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)
|
||||
}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -166,22 +177,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);
|
||||
|
||||
@@ -11,22 +11,22 @@
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
import { MultiServerMCPClient } from '../src/index.js';
|
||||
import { runExample as runFileSystemExample } from './filesystem_langgraph_example.js';
|
||||
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',
|
||||
transport: "stdio",
|
||||
command: "docker",
|
||||
args: [
|
||||
'run',
|
||||
'-i',
|
||||
'--rm',
|
||||
'-v',
|
||||
'mcp-filesystem-data:/projects',
|
||||
'mcp/filesystem',
|
||||
'/projects',
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-v",
|
||||
"mcp-filesystem-data:/projects",
|
||||
"mcp/filesystem",
|
||||
"/projects",
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -36,5 +36,5 @@ async function runExample() {
|
||||
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isMainModule) {
|
||||
runExample().catch(error => console.error('Setup error:', error));
|
||||
runExample().catch((error) => console.error("Setup error:", error));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
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",
|
||||
abs,
|
||||
};
|
||||
Generated
-8443
File diff suppressed because it is too large
Load Diff
+56
-20
@@ -5,6 +5,7 @@
|
||||
"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,22 @@
|
||||
"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/",
|
||||
"lint": "yarn lint:eslint && yarn lint:dpdm",
|
||||
"lint:dpdm": "dpdm --exit-code circular:1 --no-warning --no-tree src/**/*.ts examples/**/*.ts",
|
||||
"lint:eslint": "NODE_OPTIONS=--max-old-space-size=4096 eslint --cache --ext .ts,.js src/ examples/",
|
||||
"lint:fix": "yarn lint:eslint --fix && yarn lint:dpdm",
|
||||
"prepack": "pinst --disable && yarn build",
|
||||
"postinstall": "husky",
|
||||
"postpack": "pinst --enable",
|
||||
"format": "prettier --config .prettierrc --write \"src/**/*.ts\" \"examples/**/*.ts\"",
|
||||
"format:check": "prettier --config .prettierrc --check \"src\" \"examples/**/*.ts\"",
|
||||
"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}": [
|
||||
@@ -53,35 +60,64 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@langchain/core": "^0.3.40",
|
||||
"@langchain/langgraph": "^0.2.56",
|
||||
"@langchain/openai": "^0.4.4",
|
||||
"@langchain/scripts": "^0.1.3",
|
||||
"@tsconfig/recommended": "^1.0.8",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@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.0.9",
|
||||
"dotenv": "^16.4.7",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^10.0.2",
|
||||
"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.5",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^15.2.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.2.5",
|
||||
"pinst": "^3.0.0",
|
||||
"prettier": "^2.8.3",
|
||||
"release-it": "^17.6.0",
|
||||
"rollup": "^4.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.2",
|
||||
"typescript": "^4.9.5 || ^5.4.5",
|
||||
"typescript-eslint": "^8.26.0",
|
||||
"vitest": "^3.0.9"
|
||||
},
|
||||
"resolutions": {
|
||||
"typescript": "4.9.5"
|
||||
},
|
||||
"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"
|
||||
]
|
||||
}
|
||||
|
||||
+338
-191
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -3,6 +3,6 @@ export {
|
||||
type Connection,
|
||||
type StdioConnection,
|
||||
type SSEConnection,
|
||||
} from './client.js';
|
||||
} from "./client.js";
|
||||
|
||||
export { loadMcpTools } from './tools.js';
|
||||
export { loadMcpTools } from "./tools.js";
|
||||
|
||||
+87
-56
@@ -1,36 +1,42 @@
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import type {
|
||||
CallToolResult,
|
||||
TextContent,
|
||||
ImageContent,
|
||||
EmbeddedResource,
|
||||
ReadResourceResult,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
Tool as MCPTool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import {
|
||||
DynamicStructuredTool,
|
||||
type DynamicStructuredToolInput,
|
||||
type StructuredToolInterface,
|
||||
} from '@langchain/core/tools';
|
||||
} from "@langchain/core/tools";
|
||||
import {
|
||||
MessageContent,
|
||||
MessageContentComplex,
|
||||
MessageContentImageUrl,
|
||||
MessageContentText,
|
||||
} from '@langchain/core/messages';
|
||||
import { JSONSchema, JSONSchemaToZod } from '@dmitryrechkin/json-schema-to-zod';
|
||||
} from "@langchain/core/messages";
|
||||
import { JSONSchema, JSONSchemaToZod } from "@dmitryrechkin/json-schema-to-zod";
|
||||
|
||||
import debug from 'debug';
|
||||
import { z } from 'zod';
|
||||
import debug from "debug";
|
||||
|
||||
const {
|
||||
default: { name: packageName },
|
||||
} = await import('../package.json');
|
||||
const moduleName = 'tools';
|
||||
// Replace direct initialization with lazy initialization
|
||||
let debugLog: debug.Debugger;
|
||||
function getDebugLog() {
|
||||
if (!debugLog) {
|
||||
debugLog = debug("@langchain/mcp-adapters:tools");
|
||||
}
|
||||
return debugLog;
|
||||
}
|
||||
|
||||
const debugLog = debug(`${packageName}:${moduleName}`);
|
||||
|
||||
export type CallToolResultContentType = CallToolResult['content'][number]['type'];
|
||||
export type CallToolResultContent = TextContent | ImageContent | EmbeddedResource;
|
||||
export type CallToolResultContentType =
|
||||
CallToolResult["content"][number]["type"];
|
||||
export type CallToolResultContent =
|
||||
| TextContent
|
||||
| ImageContent
|
||||
| EmbeddedResource;
|
||||
|
||||
async function _embeddedResourceToArtifact(
|
||||
resource: EmbeddedResource,
|
||||
@@ -41,12 +47,14 @@ async function _embeddedResourceToArtifact(
|
||||
uri: resource.resource.uri,
|
||||
});
|
||||
|
||||
return response.contents.map(content => ({
|
||||
type: 'resource',
|
||||
resource: {
|
||||
...content,
|
||||
},
|
||||
}));
|
||||
return response.contents.map(
|
||||
(content: ReadResourceResult["contents"][number]) => ({
|
||||
type: "resource",
|
||||
resource: {
|
||||
...content,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
return [resource];
|
||||
}
|
||||
@@ -57,7 +65,7 @@ async function _embeddedResourceToArtifact(
|
||||
export class ToolException extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ToolException';
|
||||
this.name = "ToolException";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,39 +96,58 @@ async function _convertCallToolResult(
|
||||
|
||||
if (result.isError) {
|
||||
throw new ToolException(
|
||||
`MCP tool '${toolName}' on server '${serverName}' returned an error: ${result.content.map(content => content.text).join('\n')}`
|
||||
`MCP tool '${toolName}' on server '${serverName}' returned an error: ${result.content
|
||||
.map((content: CallToolResultContent) => content.text)
|
||||
.join("\n")}`
|
||||
);
|
||||
}
|
||||
|
||||
const mcpTextAndImageContent: MessageContentComplex[] = result.content
|
||||
.filter(content => content.type === 'text' || content.type === 'image')
|
||||
.map(content => {
|
||||
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;
|
||||
}
|
||||
});
|
||||
const mcpTextAndImageContent: MessageContentComplex[] = (
|
||||
result.content.filter(
|
||||
(content: CallToolResultContent) =>
|
||||
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')
|
||||
.map(content => _embeddedResourceToArtifact(content, client))
|
||||
(
|
||||
result.content.filter(
|
||||
(content: CallToolResultContent) => content.type === "resource"
|
||||
) as EmbeddedResource[]
|
||||
).map((content: EmbeddedResource) =>
|
||||
_embeddedResourceToArtifact(content, client)
|
||||
)
|
||||
)
|
||||
).flat();
|
||||
|
||||
if (mcpTextAndImageContent.length === 1 && mcpTextAndImageContent[0].type === 'text') {
|
||||
if (
|
||||
mcpTextAndImageContent.length === 1 &&
|
||||
mcpTextAndImageContent[0].type === "text"
|
||||
) {
|
||||
return [mcpTextAndImageContent[0].text, artifacts];
|
||||
}
|
||||
|
||||
@@ -147,13 +174,14 @@ async function _callTool(
|
||||
): Promise<[MessageContent, EmbeddedResource[]]> {
|
||||
let result: CallToolResult;
|
||||
try {
|
||||
debugLog(`INFO: Calling tool ${toolName}(${JSON.stringify(args)})`);
|
||||
getDebugLog()(`INFO: Calling tool ${toolName}(${JSON.stringify(args)})`);
|
||||
result = (await client.callTool({
|
||||
name: toolName,
|
||||
arguments: args,
|
||||
})) as CallToolResult;
|
||||
} catch (error) {
|
||||
debugLog(`Error calling tool ${toolName}: ${String(error)}`);
|
||||
getDebugLog()(`Error calling tool ${toolName}: ${String(error)}`);
|
||||
// eslint-disable-next-line no-instanceof/no-instanceof
|
||||
if (error instanceof ToolException) {
|
||||
throw error;
|
||||
}
|
||||
@@ -177,31 +205,34 @@ export async function loadMcpTools(
|
||||
): Promise<StructuredToolInterface[]> {
|
||||
// Get tools in a single operation
|
||||
const toolsResponse = await client.listTools();
|
||||
debugLog(`INFO: Found ${toolsResponse.tools?.length || 0} MCP tools`);
|
||||
getDebugLog()(`INFO: Found ${toolsResponse.tools?.length || 0} MCP tools`);
|
||||
|
||||
// Filter out tools without names and convert in a single map operation
|
||||
return (toolsResponse.tools || [])
|
||||
.filter(tool => !!tool.name)
|
||||
.map(tool => {
|
||||
.filter((tool: MCPTool) => !!tool.name)
|
||||
.map((tool: MCPTool) => {
|
||||
try {
|
||||
const dst = new DynamicStructuredTool({
|
||||
name: tool.name,
|
||||
description: tool.description || '',
|
||||
description: tool.description || "",
|
||||
schema: JSONSchemaToZod.convert(
|
||||
(tool.inputSchema ?? { type: 'object', properties: {} }) as JSONSchema
|
||||
(tool.inputSchema ?? {
|
||||
type: "object",
|
||||
properties: {},
|
||||
}) as JSONSchema
|
||||
),
|
||||
responseFormat: 'content_and_artifact',
|
||||
responseFormat: "content_and_artifact",
|
||||
func: _callTool.bind(
|
||||
null,
|
||||
serverName,
|
||||
tool.name,
|
||||
client
|
||||
) as DynamicStructuredToolInput<z.AnyZodObject>['func'],
|
||||
) as DynamicStructuredToolInput["func"],
|
||||
});
|
||||
debugLog(`INFO: Successfully loaded tool: ${dst.name}`);
|
||||
getDebugLog()(`INFO: Successfully loaded tool: ${dst.name}`);
|
||||
return dst;
|
||||
} catch (error) {
|
||||
debugLog(`ERROR: Failed to load tool "${tool.name}":`, error);
|
||||
getDebugLog()(`ERROR: Failed to load tool "${tool.name}":`, error);
|
||||
if (throwOnLoadError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": false
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "docs", "**/tests"]
|
||||
}
|
||||
+17
-12
@@ -1,18 +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,
|
||||
"resolveJsonModule": 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/**/*", "__tests__/**/*"],
|
||||
"exclude": ["node_modules", "dist", "docs"]
|
||||
}
|
||||
Reference in New Issue
Block a user