fix: build & import issues (#32)

fixes #30
fixes #31
This commit is contained in:
Ben Burns
2025-03-22 16:54:34 +13:00
committed by GitHub
parent ce7b796ec8
commit b6e17366be
31 changed files with 10467 additions and 9355 deletions
+68
View File
@@ -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 }],
},
};
+8 -8
View File
@@ -17,19 +17,19 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache: 'yarn'
- name: Install dependencies
run: npm ci
run: yarn install --immutable
- name: Lint
run: npm run lint
run: yarn lint
- name: Build
run: npm run build
run: yarn build
- name: Run tests
run: npm test
run: yarn test
coverage:
name: Coverage
@@ -42,13 +42,13 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache: 'yarn'
- name: Install dependencies
run: npm ci
run: yarn install --immutable
- name: Generate coverage report
run: npm test -- --coverage
run: yarn test -- --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
+5 -5
View File
@@ -28,16 +28,16 @@ jobs:
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
cache: 'yarn'
- name: Install dependencies
run: npm ci
run: yarn install --immutable
- name: Run tests
run: npm test
run: yarn test
- name: Build
run: npm run build
run: yarn build
- name: Configure Git
run: |
@@ -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 }}
+7 -7
View File
@@ -17,19 +17,19 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache: 'yarn'
- name: Install dependencies
run: npm ci
run: yarn install --immutable
- name: Lint
run: npm run lint
run: yarn lint
- name: Type check
run: npm run build --noEmit
run: yarn build
- name: Run tests
run: npm test
run: yarn test
format-check:
name: Format Check
@@ -42,10 +42,10 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache: 'yarn'
- name: Install dependencies
run: npm ci
run: yarn install --immutable
- name: Check formatting
run: npx prettier --check "src/**/*.ts" "examples/**/*.ts"
+7 -54
View File
@@ -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
View File
@@ -0,0 +1,19 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always",
"requirePragma": false,
"insertPragma": false,
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"vueIndentScriptAndStyle": false,
"endOfLine": "lf"
}
Binary file not shown.
File diff suppressed because one or more lines are too long
+873
View File
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: "@yarnpkg/plugin-typescript"
yarnPath: .yarn/releases/yarn-3.5.1.cjs
+221 -170
View File
@@ -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
-71
View File
@@ -1,71 +0,0 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettierConfig from 'eslint-config-prettier';
export default tseslint.config(
{
// Set global ignores first to ensure they take precedence
ignores: ['node_modules/**', 'dist/**', 'coverage/**'],
},
eslint.configs.recommended,
...tseslint.configs.recommended,
prettierConfig,
{
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'],
rules: {
// TypeScript specific rules
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
// General rules
'no-console': ['warn', { allow: ['warn', 'error', 'info'] }],
'prefer-const': 'warn',
'no-var': 'error',
eqeqeq: ['error', 'always', { null: 'ignore' }],
'no-duplicate-imports': 'error',
'no-unused-expressions': 'error',
'no-shadow': 'off', // TypeScript has better handling with no-shadow
'@typescript-eslint/no-shadow': 'warn',
// Import rules
'import/no-unresolved': 'off', // TypeScript handles this
'import/prefer-default-export': 'off',
'import/extensions': 'off',
// Formatting (Prettier will handle most of this)
'max-len': [
'warn',
{
code: 100,
ignoreUrls: true,
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreRegExpLiterals: true,
ignoreComments: true,
},
],
},
// Additional ignores for safety
ignores: ['**/*.js', '**/*.cjs'],
},
{
files: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**/*.ts'],
rules: {
// Relaxed rules for test files
'@typescript-eslint/no-explicit-any': 'off',
'no-console': 'off',
},
}
);
+74 -49
View File
@@ -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);
});
+58 -43
View File
@@ -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));
}
+30 -26
View File
@@ -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);
+20 -19
View File
@@ -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);
+76 -51
View File
@@ -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);
+75 -47
View File
@@ -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);
+45 -34
View File
@@ -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);
+46 -35
View File
@@ -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);
+12 -12
View File
@@ -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));
}
+22
View File
@@ -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,
};
-8443
View File
File diff suppressed because it is too large Load Diff
+56 -20
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -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
View File
@@ -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;
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"declaration": false
},
"exclude": ["node_modules", "dist", "docs", "**/tests"]
}
+17 -12
View File
@@ -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"]
}
+24
View File
@@ -0,0 +1,24 @@
{
"extends": "@tsconfig/recommended",
"compilerOptions": {
"outDir": "dist",
"rootDir": "./src",
"target": "ES2021",
"lib": ["ES2021", "ES2022.Object", "DOM"],
"module": "ES2020",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"declaration": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"useDefineForClassFields": true,
"strictPropertyInitialization": false,
"allowJs": true,
"strict": true,
"noEmit": true
},
"include": ["src/**/*", "__tests__/**/*"],
"exclude": ["node_modules", "dist", "docs"]
}
+8253
View File
File diff suppressed because it is too large Load Diff