chore: prepare for npm publishing, add .npmignore, CHANGELOG and CONTRIBUTING files

This commit is contained in:
vrknetha
2025-03-04 01:38:30 +05:30
commit 35d335f11b
40 changed files with 11080 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
---
name: Bug report
about: Create a report to help us improve
title: '[BUG] '
labels: bug
assignees: ''
---
## Bug Description
A clear and concise description of what the bug is.
## Reproduction Steps
Steps to reproduce the behavior:
1. Create a client with '...'
2. Connect to server using '...'
3. Call method '...'
4. See error
## Expected Behavior
A clear and concise description of what you expected to happen.
## Actual Behavior
What actually happened, including any error messages, stack traces, or unexpected output.
## Environment
- OS: [e.g. macOS, Windows, Linux]
- Node.js version: [e.g. 18.15.0]
- Package version: [e.g. 0.1.0]
- MCP SDK version: [e.g. 1.6.1]
## Additional Context
Add any other context about the problem here, such as:
- Server implementation details
- Transport type (stdio, SSE)
- Any relevant logs
## Possible Solution
If you have suggestions on how to fix the issue, please describe them here.
+27
View File
@@ -0,0 +1,27 @@
---
name: Feature request
about: Suggest an idea for this project
title: '[FEATURE] '
labels: enhancement
assignees: ''
---
## Feature Description
A clear and concise description of the feature you'd like to see implemented.
## Use Case
Describe the use case or problem this feature would solve. Ex. I'm always frustrated when [...]
## Proposed Solution
A clear and concise description of what you want to happen.
## Alternatives Considered
A description of any alternative solutions or features you've considered.
## Additional Context
Add any other context, code examples, or references about the feature request here.
+27
View File
@@ -0,0 +1,27 @@
## Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.
Fixes # (issue)
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update
- [ ] Refactoring (no functional changes)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce.
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
+57
View File
@@ -0,0 +1,57 @@
name: CI
on:
push:
branches: [main]
workflow_dispatch:
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Build
run: npm run build
- name: Run tests
run: npm test
coverage:
name: Coverage
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Generate coverage report
run: npm test -- --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
+68
View File
@@ -0,0 +1,68 @@
name: Publish to npm
on:
release:
types: [created]
workflow_dispatch:
inputs:
version:
description: 'Version to publish (patch, minor, major, or specific version)'
required: true
default: 'patch'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
- name: Configure Git
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
- name: Version bump (automatic)
if: github.event_name == 'release'
run: |
VERSION=$(echo "${{ github.ref }}" | sed -e 's/refs\/tags\/v//')
npm version $VERSION --no-git-tag-version
- name: Version bump (manual)
if: github.event_name == 'workflow_dispatch'
run: npm version ${{ github.event.inputs.version }} --no-git-tag-version
- name: Publish to npm
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Push version changes
if: github.event_name == 'workflow_dispatch'
run: |
NEW_VERSION=$(node -p "require('./package.json').version")
git add package.json package-lock.json
git commit -m "chore: bump version to v${NEW_VERSION} [skip ci]"
git tag -a "v${NEW_VERSION}" -m "Release v${NEW_VERSION}"
git push origin main
git push origin "v${NEW_VERSION}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+51
View File
@@ -0,0 +1,51 @@
name: PR Validation
on:
pull_request:
branches: [main]
workflow_dispatch:
jobs:
validate:
name: Validate
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Type check
run: npm run build --noEmit
- name: Run tests
run: npm test
format-check:
name: Format Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Check formatting
run: npx prettier --check "src/**/*.ts" "examples/**/*.ts"
+52
View File
@@ -0,0 +1,52 @@
# Dependency directories
node_modules/
.pnp/
.pnp.js
# Build outputs
dist/
build/
*.tsbuildinfo
.env
# 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/
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
+49
View File
@@ -0,0 +1,49 @@
# Source files
src/
# Examples
examples/
# Tests
__tests__/
*.test.ts
jest.config.js
# Development configs
.eslintrc.js
.eslintignore
.prettierrc
.prettierrc.json
.prettierignore
tsconfig.json
.github/
.husky/
# Git files
.git/
.gitignore
# Editor files
.vscode/
.idea/
*.swp
*.swo
# Logs
logs/
*.log
npm-debug.log*
# Dependency directories
node_modules/
# Coverage directory
coverage/
# Misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
+11
View File
@@ -0,0 +1,11 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}
+27
View File
@@ -0,0 +1,27 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- GitHub Actions workflows for PR validation, CI, and npm publishing
- Husky for Git hooks
- lint-staged for running linters on staged files
- Issue and PR templates
- CHANGELOG.md and CONTRIBUTING.md
## [0.1.0] - 2023-03-03
### Added
- Initial release
- Support for stdio and SSE transports
- MultiServerMCPClient for connecting to multiple MCP servers
- Configuration file support
- Examples for various use cases
- Integration with LangChain.js agents
+88
View File
@@ -0,0 +1,88 @@
# Contributing to LangChain.js MCP Adapters
Thank you for considering contributing to LangChain.js MCP Adapters! This document provides guidelines and instructions for contributing to this project.
## Code of Conduct
By participating in this project, you agree to abide by our code of conduct. Please be respectful and considerate of others.
## How Can I Contribute?
### Reporting Bugs
Before creating bug reports, please check the existing issues to see if the problem has already been reported. When you are creating a bug report, please include as many details as possible:
- Use a clear and descriptive title
- Describe the exact steps to reproduce the problem
- Provide specific examples to demonstrate the steps
- Describe the behavior you observed and what you expected to see
- Include screenshots if applicable
- Include details about your environment (OS, Node.js version, package version)
### Suggesting Enhancements
Enhancement suggestions are welcome! When suggesting an enhancement:
- Use a clear and descriptive title
- Provide a detailed description of the suggested enhancement
- Explain why this enhancement would be useful to most users
- List some examples of how this enhancement would be used
### Pull Requests
- Fill in the required template
- Follow the TypeScript coding style
- Include tests for new features or bug fixes
- Update documentation as needed
- End all files with a newline
- Make sure your code passes all tests and linting
## Development Workflow
1. Fork the repository
2. Clone your fork: `git clone https://github.com/your-username/langchainjs-mcp-adapters.git`
3. Create a new branch: `git checkout -b feature/your-feature-name`
4. Make your changes
5. Run tests: `npm test`
6. Run linting: `npm run lint`
7. Commit your changes: `git commit -m "Add some feature"`
8. Push to the branch: `git push origin feature/your-feature-name`
9. Submit a pull request
## Setting Up Development Environment
1. Install dependencies: `npm install`
2. Build the project: `npm run build`
3. Run tests: `npm test`
## Testing
- Write tests for all new features and bug fixes
- Run tests before submitting a pull request: `npm test`
- Ensure code coverage remains high
## Coding Style
- Follow the ESLint and Prettier configurations
- Use meaningful variable and function names
- Write clear comments for complex logic
- Document public APIs using JSDoc comments
## Commit Messages
- Use the present tense ("Add feature" not "Added feature")
- Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
- Limit the first line to 72 characters or less
- Reference issues and pull requests after the first line
## Versioning
This project follows [Semantic Versioning](https://semver.org/). When contributing, consider the impact of your changes:
- PATCH version for backwards-compatible bug fixes
- MINOR version for backwards-compatible new features
- MAJOR version for incompatible API changes
## License
By contributing to this project, you agree that your contributions will be licensed under the project's MIT license.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Ravi Kiran Vemula
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+328
View File
@@ -0,0 +1,328 @@
# LangChain.js MCP Adapters
This package provides adapters for using [Model Context Protocol (MCP)](https://github.com/model-context-protocol/model-context-protocol) tools with LangChain.js.
## Installation
```bash
npm install langchainjs-mcp-adapters
```
## Usage
### Connecting to an MCP Server
You can connect to an MCP server using either stdio or SSE transport:
```typescript
import { MultiServerMCPClient } from 'langchainjs-mcp-adapters';
// Create a client
const client = new MultiServerMCPClient();
// Connect to a server using stdio
await client.connectToServerViaStdio(
'math-server', // A name to identify this server
'python', // Command to run
['./math_server.py'] // Arguments for the command
);
// Connect to a server using SSE
await client.connectToServerViaSSE(
'weather-server', // A name to identify this server
'http://localhost:8000/sse' // URL of the SSE server
);
// Get all tools from all connected servers
const tools = client.getTools();
// Use the tools
const result = await tools[0].invoke({ param1: 'value1', param2: 'value2' });
// Close the client when done
await client.close();
```
### Initializing Multiple Connections
You can also initialize multiple connections at once:
```typescript
import { MultiServerMCPClient } from 'langchainjs-mcp-adapters';
const client = new MultiServerMCPClient({
'math-server': {
transport: 'stdio',
command: 'python',
args: ['./math_server.py'],
},
'weather-server': {
transport: 'sse',
url: 'http://localhost:8000/sse',
},
});
// Initialize all connections
await client.initialize();
// Get all tools
const tools = client.getTools();
// Close all connections when done
await client.close();
```
### Using Configuration File
You can define your MCP server configurations in a JSON file (`mcp.json`) and load them:
```typescript
import { MultiServerMCPClient } from 'langchainjs-mcp-adapters';
// Create a client from the config file
const client = MultiServerMCPClient.fromConfigFile();
// Or specify a custom path: MultiServerMCPClient.fromConfigFile("./config/mcp.json");
// Initialize all connections
await client.initialize();
// Get all tools
const tools = client.getTools();
// Close all connections when done
await client.close();
```
Example `mcp.json` file:
```json
{
"servers": {
"math": {
"transport": "stdio",
"command": "python",
"args": ["./examples/math_server.py"]
},
"weather": {
"transport": "sse",
"url": "http://localhost:8000/sse"
}
}
}
```
You can also omit the `transport` field for stdio servers, as it's the default transport:
```json
{
"servers": {
"math": {
"command": "python",
"args": ["./examples/math_server.py"]
},
"weather": {
"transport": "sse",
"url": "http://localhost:8000/sse"
}
}
}
```
The client will attempt to connect to all servers defined in the configuration file. If a server is not available, it will log an error and continue with the available servers. If no servers are available, it will throw an error.
```typescript
// Error handling when initializing connections
try {
const client = MultiServerMCPClient.fromConfigFile();
await client.initialize();
// Use the client...
} catch (error) {
console.error('Failed to connect to any servers:', error.message);
}
```
### Using with LangChain Agents
You can use MCP tools with LangChain agents:
```typescript
import { MultiServerMCPClient } from 'langchainjs-mcp-adapters';
import { ChatOpenAI } from '@langchain/openai';
import { createOpenAIFunctionsAgent, AgentExecutor } from 'langchain/agents';
import { ChatPromptTemplate } from '@langchain/core/prompts';
// Create a client and connect to servers
const client = new MultiServerMCPClient();
await client.connectToServerViaStdio('math-server', 'python', ['./math_server.py']);
// Get tools
const tools = client.getTools();
// Create an agent
const model = new ChatOpenAI({ temperature: 0 });
const prompt = ChatPromptTemplate.fromMessages([
['system', 'You are a helpful assistant that can use tools to solve problems.'],
['human', '{input}'],
]);
const agent = createOpenAIFunctionsAgent({
llm: model,
tools,
prompt,
});
const agentExecutor = new AgentExecutor({
agent,
tools,
});
// Run the agent
const result = await agentExecutor.invoke({
input: 'What is 5 + 3?',
});
console.log(result.output);
// Close the client when done
await client.close();
```
## Example MCP Servers
### Math Server (stdio transport)
Here's an example of a simple MCP server in Python using stdio transport:
```python
from mcp.server.fastmcp import FastMCP
# Create a server
mcp = FastMCP(name="Math")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two integers and return the result."""
return a + b
@mcp.tool()
def multiply(a: int, b: int) -> int:
"""Multiply two integers and return the result."""
return a * b
# Run the server with stdio transport
if __name__ == "__main__":
mcp.run(transport="stdio")
```
### Weather Server (SSE transport)
Here's an example of an MCP server using SSE transport:
```python
from mcp.server.fastmcp import FastMCP
# Create a server
mcp = FastMCP(name="Weather")
@mcp.tool()
def get_temperature(city: str) -> str:
"""Get the current temperature for a city."""
# Mock implementation
temperatures = {
"new york": "72°F",
"london": "65°F",
"tokyo": "25 degrees Celsius",
}
city_lower = city.lower()
if city_lower in temperatures:
return f"The current temperature in {city} is {temperatures[city_lower]}."
else:
return "Temperature data not available for this city"
# Run the server with SSE transport
if __name__ == "__main__":
mcp.run(transport="sse")
```
## Running the Examples
The package includes several example files that demonstrate how to use MCP adapters:
1. `math_example.ts` - Basic example using a math server with stdio transport
2. `sse_example.ts` - Example using a weather server with SSE transport
3. `multi_sse_example.ts` - Example connecting to multiple servers with different transport types
4. `config_example.ts` - Example using server configurations from an `mcp.json` file
To run the examples:
```bash
# Start the weather server with SSE transport
python examples/weather_server.py
# In another terminal, run the SSE example
node --loader ts-node/esm examples/sse_example.ts
# Or run the multi-server example
node --loader ts-node/esm examples/multi_sse_example.ts
# Or run the config-based example (requires mcp.json in the project root)
node --loader ts-node/esm examples/config_example.ts
```
## Development
### GitHub Actions Workflows
This project uses GitHub Actions for continuous integration and deployment:
#### PR Validation
The PR validation workflow runs automatically on all pull requests to the `main` branch. It performs:
- Code linting with ESLint
- Type checking with TypeScript
- Unit tests with Jest
- Format checking with Prettier
#### Continuous Integration
The CI workflow runs on the `main` branch after merges and:
- Runs linting and tests
- Builds the package
- Generates and uploads test coverage reports
#### Publishing to npm
The package can be published to npm in two ways:
1. **Automatic publishing on GitHub Release**:
- Create a new release in GitHub
- The workflow will automatically publish the package with the release version
2. **Manual publishing**:
- Go to the "Actions" tab in GitHub
- Select the "Publish to npm" workflow
- Click "Run workflow"
- Choose the version bump type (patch, minor, major) or specify a version
### Setting up npm publishing
To enable npm publishing, you need to:
1. Create an npm access token with publish permissions
2. Add the token as a GitHub repository secret named `NPM_TOKEN`
### Contributing
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to this project.
### Changelog
For a detailed list of changes between versions, see the [CHANGELOG.md](CHANGELOG.md) file.
## License
MIT
+258
View File
@@ -0,0 +1,258 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { StructuredTool } from '@langchain/core/tools';
import { MultiServerMCPClient } from '../src/client';
import * as toolsModule from '../src/tools';
// Mock the Client class
jest.mock('@modelcontextprotocol/sdk/client/index.js', () => {
return {
Client: jest.fn().mockImplementation(() => {
return {
connect: jest.fn().mockResolvedValue(undefined),
listTools: jest.fn().mockResolvedValue({ tools: [] }),
callTool: jest.fn().mockResolvedValue({ content: [{ type: 'text', text: 'result' }] }),
};
}),
};
});
// Mock the transports
jest.mock('@modelcontextprotocol/sdk/client/stdio.js', () => {
return {
StdioClientTransport: jest.fn().mockImplementation(() => ({
close: jest.fn().mockResolvedValue(undefined),
})),
};
});
jest.mock('@modelcontextprotocol/sdk/client/sse.js', () => {
return {
SSEClientTransport: jest.fn().mockImplementation(() => ({
close: jest.fn().mockResolvedValue(undefined),
})),
};
});
// Mock the tools module
jest.mock('../src/tools', () => {
return {
loadMcpTools: jest.fn().mockResolvedValue([
{
name: 'test-tool',
description: 'A test tool',
invoke: jest.fn().mockResolvedValue('test result'),
},
]),
};
});
describe('MultiServerMCPClient', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('constructor with stdio config', () => {
it('should set up a server with stdio transport', async () => {
const client = new MultiServerMCPClient({
'test-server': {
transport: 'stdio',
command: 'python',
args: ['script.py'],
env: { ENV_VAR: 'value' },
},
});
await client.initializeConnections();
// Verify StdioClientTransport was created with the correct parameters
expect(StdioClientTransport).toHaveBeenCalledWith({
command: 'python',
args: ['script.py'],
env: { ENV_VAR: 'value' },
});
// Verify Client was created and connected
expect(Client).toHaveBeenCalled();
const mockClientInstance = (Client as jest.Mock).mock.results[0].value;
expect(mockClientInstance.connect).toHaveBeenCalled();
// Verify tools were loaded
expect(toolsModule.loadMcpTools).toHaveBeenCalled();
});
});
describe('constructor with SSE config', () => {
it('should set up a server with SSE transport', async () => {
const client = new MultiServerMCPClient({
'test-server': {
transport: 'sse',
url: 'https://example.com/sse',
},
});
await client.initializeConnections();
// Verify SSEClientTransport was created with the correct URL
expect(SSEClientTransport).toHaveBeenCalledWith(new URL('https://example.com/sse'));
// Verify Client was created and connected
expect(Client).toHaveBeenCalled();
const mockClientInstance = (Client as jest.Mock).mock.results[0].value;
expect(mockClientInstance.connect).toHaveBeenCalled();
// Verify tools were loaded
expect(toolsModule.loadMcpTools).toHaveBeenCalled();
});
});
describe('constructor validation', () => {
it('should skip servers with unsupported transport', async () => {
const client = new MultiServerMCPClient({
'test-server': {
transport: 'unsupported' as any,
},
});
// Should not throw, just log a warning and return empty Map
const result = await client.initializeConnections();
expect(result.size).toBe(0);
});
it('should skip servers with missing required parameters', async () => {
const clientWithMissingCommand = new MultiServerMCPClient({
'test-server': {
transport: 'stdio',
} as any,
});
// Should not throw, just log a warning and return empty Map
const result1 = await clientWithMissingCommand.initializeConnections();
expect(result1.size).toBe(0);
const clientWithMissingArgs = new MultiServerMCPClient({
'test-server': {
transport: 'stdio',
command: 'python',
} as any,
});
// Should not throw, just log a warning and return empty Map
const result2 = await clientWithMissingArgs.initializeConnections();
expect(result2.size).toBe(0);
const clientWithMissingUrl = new MultiServerMCPClient({
'test-server': {
transport: 'sse',
} as any,
});
// Should not throw, just log a warning and return empty Map
const result3 = await clientWithMissingUrl.initializeConnections();
expect(result3.size).toBe(0);
});
});
describe('getTools', () => {
it('should return all tools from all servers', async () => {
const client = new MultiServerMCPClient({
server1: {
transport: 'stdio',
command: 'python',
args: ['script1.py'],
},
server2: {
transport: 'stdio',
command: 'python',
args: ['script2.py'],
},
});
await client.initializeConnections();
const serverTools = client.getTools();
expect(serverTools.size).toBe(2); // Two servers
const server1Tools = serverTools.get('server1');
const server2Tools = serverTools.get('server2');
expect(server1Tools).toBeDefined();
expect(server2Tools).toBeDefined();
expect(server1Tools![0].name).toBe('test-tool');
expect(server2Tools![0].name).toBe('test-tool');
});
});
describe('initializeConnections', () => {
it('should initialize connections from constructor', async () => {
const client = new MultiServerMCPClient({
server1: {
transport: 'stdio',
command: 'python',
args: ['script1.py'],
},
server2: {
transport: 'sse',
url: 'https://example.com/sse',
},
});
await client.initializeConnections();
// Verify both connections were established
expect(StdioClientTransport).toHaveBeenCalledWith({
command: 'python',
args: ['script1.py'],
env: undefined,
});
expect(SSEClientTransport).toHaveBeenCalledWith(new URL('https://example.com/sse'));
// Verify Client was created and connected twice
expect(Client).toHaveBeenCalledTimes(2);
// Verify tools were loaded twice
expect(toolsModule.loadMcpTools).toHaveBeenCalledTimes(2);
});
it('should do nothing if no connections are provided', async () => {
const client = new MultiServerMCPClient();
await client.initializeConnections();
// Verify no connections were established
expect(StdioClientTransport).not.toHaveBeenCalled();
expect(SSEClientTransport).not.toHaveBeenCalled();
expect(Client).not.toHaveBeenCalled();
expect(toolsModule.loadMcpTools).not.toHaveBeenCalled();
});
});
describe('close', () => {
it('should close all connections', async () => {
const client = new MultiServerMCPClient({
server1: {
transport: 'stdio',
command: 'python',
args: ['script1.py'],
},
server2: {
transport: 'stdio',
command: 'python',
args: ['script2.py'],
},
});
await client.initializeConnections();
await client.close();
// Verify all transports were closed
const mockTransportInstances = [
(StdioClientTransport as jest.Mock).mock.results[0].value,
(StdioClientTransport as jest.Mock).mock.results[1].value,
];
expect(mockTransportInstances[0].close).toHaveBeenCalled();
expect(mockTransportInstances[1].close).toHaveBeenCalled();
});
});
});
+194
View File
@@ -0,0 +1,194 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StructuredTool } from '@langchain/core/tools';
import { convertMcpToolToLangchainTool, loadMcpTools } from '../src/tools';
// Mock Client
const mockClient: jest.Mocked<Client> = {
listTools: jest.fn(),
callTool: jest.fn(),
close: jest.fn(),
} as unknown as jest.Mocked<Client>;
describe('tools', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('convertMcpToolToLangchainTool', () => {
it('should convert an MCP tool to a LangChain tool', async () => {
// Mock tool call result
const mockToolResult = {
content: [
{
type: 'text',
text: '42',
},
],
};
mockClient.callTool.mockResolvedValue(mockToolResult);
// Create a LangChain tool from an MCP tool
const tool = convertMcpToolToLangchainTool(mockClient, 'calculator', 'A calculator tool', {
type: 'object',
properties: {
a: { type: 'number' },
b: { type: 'number' },
},
required: ['a', 'b'],
});
// Verify the tool properties
expect(tool).toBeInstanceOf(StructuredTool);
expect(tool.name).toBe('calculator');
expect(tool.description).toBe('A calculator tool');
// Invoke the tool
const result = await tool.invoke({ a: 2, b: 3 });
// Verify the tool was called with the correct parameters
expect(mockClient.callTool).toHaveBeenCalledWith({
name: 'calculator',
arguments: { a: 2, b: 3 },
});
// Verify the result
expect(result).toBe('42');
});
it('should handle errors from the MCP tool', async () => {
// Mock tool call result with an error
const mockToolResult = {
isError: true,
content: [
{
type: 'text',
text: 'Invalid input',
},
],
};
mockClient.callTool.mockResolvedValue(mockToolResult);
// Create a LangChain tool from an MCP tool
const tool = convertMcpToolToLangchainTool(mockClient, 'calculator', 'A calculator tool', {
type: 'object',
properties: {
a: { type: 'number' },
b: { type: 'number' },
},
required: ['a', 'b'],
});
// Invoke the tool and expect it to throw
await expect(tool.invoke({ a: -1, b: 3 })).rejects.toThrow('Invalid input');
});
it('should handle non-text content from the MCP tool', async () => {
// Mock tool call result with non-text content
const mockContent = {
type: 'image',
data: 'base64-encoded-data',
mimeType: 'image/png',
};
const mockToolResult = {
content: [mockContent],
};
mockClient.callTool.mockResolvedValue(mockToolResult);
// Create a LangChain tool from an MCP tool
const tool = convertMcpToolToLangchainTool(
mockClient,
'image-generator',
'An image generator tool',
{
type: 'object',
properties: {
prompt: { type: 'string' },
},
required: ['prompt'],
}
);
// Invoke the tool
const result = await tool.invoke({ prompt: 'A cat' });
// Verify the result is the content object
expect(result).toBe('[object Object]'); // String(_convertCallToolResult) converts objects to string
});
});
describe('loadMcpTools', () => {
it('should load all tools from an MCP client', async () => {
// Mock listTools response
mockClient.listTools.mockResolvedValue({
tools: [
{
name: 'add',
description: 'Add two numbers',
inputSchema: {
type: 'object',
properties: {
a: { type: 'number' },
b: { type: 'number' },
},
required: ['a', 'b'],
},
},
{
name: 'subtract',
description: 'Subtract two numbers',
inputSchema: {
type: 'object',
properties: {
a: { type: 'number' },
b: { type: 'number' },
},
required: ['a', 'b'],
},
},
],
});
// Load tools
const tools = await loadMcpTools(mockClient);
// Verify listTools was called
expect(mockClient.listTools).toHaveBeenCalled();
// Verify the tools were loaded correctly
expect(tools).toHaveLength(2);
expect(tools[0].name).toBe('add');
expect(tools[1].name).toBe('subtract');
});
it('should handle empty tool descriptions', async () => {
// Mock listTools response with a tool missing a description
mockClient.listTools.mockResolvedValue({
tools: [
{
name: 'add',
inputSchema: {
type: 'object',
properties: {
a: { type: 'number' },
b: { type: 'number' },
},
required: ['a', 'b'],
},
},
],
});
// Load tools
const tools = await loadMcpTools(mockClient);
// Verify the tool was loaded with an empty description
expect(tools).toHaveLength(1);
expect(tools[0].name).toBe('add');
expect(tools[0].description).toBe('');
});
});
});
+66
View File
@@ -0,0 +1,66 @@
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import prettierConfig from "eslint-config-prettier";
export default tseslint.config(
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,
},
],
},
ignores: ["node_modules/", "dist/", "coverage/", "**/*.js", "**/*.cjs"],
},
{
files: ["**/*.test.ts", "**/*.spec.ts", "**/__tests__/**/*.ts"],
rules: {
// Relaxed rules for test files
"@typescript-eslint/no-explicit-any": "off",
"no-console": "off",
},
}
);
+187
View File
@@ -0,0 +1,187 @@
# MCP Adapter Examples
This directory contains examples demonstrating how to use the LangChain.js MCP (Machine Communication Protocol) adapter with various configurations and use cases.
## Basic Examples
### Math Server Example
`math_example.ts` - A simple example demonstrating how to use the MCP adapter with a math server via stdio transport.
```bash
node --loader ts-node/esm examples/math_example.ts
```
### SSE Example
`sse_example.ts` - Demonstrates how to use the MCP adapter with a server using SSE (Server-Sent Events) transport.
```bash
# First start the weather server
python examples/weather_server.py
# Then run the example
node --loader ts-node/esm examples/sse_example.ts
```
### Logging Example
`logging_example.ts` - Demonstrates how to use the built-in Winston logger with the MCP adapter for detailed logging of client operations.
```bash
# First start both servers
python examples/math_server.py &
python examples/weather_server.py &
# Then run the example
node --loader ts-node/esm examples/logging_example.ts
```
## Configuration Examples
### Transport Configuration Example
`transport_config_example.ts` - Shows how to create an MCP client with a custom configuration that specifies different transport types for different servers.
```bash
# First start the weather server
python examples/weather_server.py
# Then run the example
node --loader ts-node/esm examples/transport_config_example.ts
```
### JSON Configuration Example
`json_config_example.ts` - Demonstrates how to load server configurations from JSON files.
```bash
# First start the weather server
python examples/weather_server.py
# Then run the example
node --loader ts-node/esm examples/json_config_example.ts
```
## Multi-Server Examples
### Multi-Transport Example
`multi_transport_example.ts` - Shows how to connect to multiple servers using different transport methods (stdio and SSE) and how to use tools from different servers.
```bash
# First start the weather server
python examples/weather_server.py
# Then run the example
node --loader ts-node/esm examples/multi_transport_example.ts
```
## LLM Integration Examples
### Agent Example
`agent_example.ts` - Demonstrates how to use MCP tools with a LangChain agent using OpenAI.
```bash
# Set your OpenAI API key in a .env file
echo "OPENAI_API_KEY=your-api-key" > .env
# Start the servers
python examples/math_server.py &
python examples/weather_server.py &
# Run the example
node --loader ts-node/esm examples/agent_example.ts
```
### Gemini Example
`gemini_example.ts` - Shows how to use MCP tools with Google's Gemini model.
```bash
# Set your Google API key in a .env file
echo "GOOGLE_API_KEY=your-api-key" > .env
# Start the servers
python examples/math_server.py &
python examples/weather_server.py &
# Run the example
node --loader ts-node/esm examples/gemini_example.ts
```
### Gemini Agent Example
`gemini_agent_example.ts` - Demonstrates how to use MCP tools with a LangChain agent using Google's Gemini model.
```bash
# Set your Google API key in a .env file
echo "GOOGLE_API_KEY=your-api-key" > .env
# Start the servers
python examples/math_server.py &
python examples/weather_server.py &
# Run the example
node --loader ts-node/esm examples/gemini_agent_example.ts
```
## Server Examples
### Math Server
`math_server.py` - A simple Python server that provides math operations (add, multiply) via the MCP protocol.
### Weather Server
`weather_server.py` - A Python server that provides weather information (temperature, forecast) via the MCP protocol with SSE transport.
```bash
# Run the weather server
python examples/weather_server.py
# By default, it runs on port 8000
# You can specify a different port using command line arguments:
python examples/weather_server.py --sse-port 8001
```
## Configuration Files
### Simple MCP Configuration
`simple_mcp.json` - A simple configuration file for the MCP client that specifies a math server and a weather server.
### Complex MCP Configuration
`complex_mcp.json` - A more complex configuration file that includes environment variables and additional server configurations.
## Logging
The MCP adapter includes a built-in logging system using Winston. The logger provides the following features:
- Different log levels (error, warn, info, http, debug)
- Colorized console output
- File logging for errors and all logs
- Environment-aware log levels (more verbose in development, less in production)
To use the logger in your own code:
```typescript
import { MultiServerMCPClient } from "../src/client.js";
import logger from "../src/logger.js";
// Use the logger
logger.info("Starting MCP client");
logger.debug("Detailed debug information");
logger.warn("Warning message");
logger.error("Error message");
// The client uses the logger internally
const client = new MultiServerMCPClient({...});
```
Log files are stored in the `logs` directory:
- `logs/error.log`: Contains only error-level logs
- `logs/all.log`: Contains all logs
+103
View File
@@ -0,0 +1,103 @@
import { MultiServerMCPClient } from '../src/client.js';
import { ChatOpenAI } from '@langchain/openai';
import { createOpenAIFunctionsAgent, AgentExecutor } from 'langchain/agents';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import dotenv from 'dotenv';
// Load environment variables from .env file
dotenv.config();
/**
* This example demonstrates how to use MCP tools with a LangChain agent.
*
* It connects to both a math server and a weather server, retrieves the available tools,
* and creates an agent that can use these tools to solve problems.
*
* Note: You need to set the OPENAI_API_KEY environment variable to run this example.
*/
async function main() {
if (!process.env.OPENAI_API_KEY) {
console.error('Please set the OPENAI_API_KEY environment variable in the .env file');
process.exit(1);
}
// Create a client with configurations for both servers
const client = new MultiServerMCPClient({
math: {
transport: 'stdio',
command: 'python',
args: ['./examples/math_server.py'],
},
weather: {
transport: 'stdio',
command: 'python',
args: ['./examples/weather_server.py'],
},
});
try {
// Initialize connections to both servers
console.log('Initializing connections to servers...');
await client.initializeConnections();
console.log('Connected to servers');
// Get all tools from all servers
const serverTools = client.getTools();
// Flatten all tools for use with the agent
const allTools = Array.from(serverTools.values()).flat();
console.log(`Available tools: ${allTools.map(tool => tool.name).join(', ')}`);
// Create an agent
console.log('\nCreating agent...');
const model = new ChatOpenAI({
temperature: 0,
modelName: 'gpt-4o', // or any other model that supports function calling
});
const prompt = ChatPromptTemplate.fromMessages([
[
'system',
"You are a helpful assistant that can perform calculations and get weather information. Use the tools available to you to answer the user's questions.",
],
['human', '{input}'],
]);
const agent = await createOpenAIFunctionsAgent({
llm: model,
tools: allTools,
prompt,
});
const agentExecutor = new AgentExecutor({
agent,
tools: allTools,
});
// Run the agent with different queries
const queries = [
'What is 5 + 3?',
'What is 7 * 9?',
"What's the current temperature in Tokyo?",
"What's the 3-day forecast for London?",
"If it's 72°F in New York, what is that in Celsius?",
];
for (const query of queries) {
console.log(`\n--- Query: "${query}" ---`);
const result = await agentExecutor.invoke({
input: query,
});
console.log(`Answer: ${result.output}`);
}
} catch (error) {
console.error('Error:', error);
} finally {
// Close the client
console.log('\nClosing client...');
await client.close();
console.log('Client closed');
}
}
main().catch(console.error);
+21
View File
@@ -0,0 +1,21 @@
{
"servers": {
"math": {
"command": "python",
"args": ["./examples/math_server.py"],
"env": {
"DEBUG": "true",
"PYTHONPATH": "./examples"
}
},
"weather": {
"transport": "sse",
"url": "http://localhost:8000/sse"
},
"custom-server": {
"transport": "stdio",
"command": "node",
"args": ["./examples/custom_server.js"]
}
}
}
+103
View File
@@ -0,0 +1,103 @@
import { MultiServerMCPClient } from '../src/client.js';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { initializeAgentExecutorWithOptions } from 'langchain/agents';
import dotenv from 'dotenv';
// Load environment variables from .env file
dotenv.config();
/**
* This example demonstrates how to use MCP tools with a LangChain agent using Google's Gemini model.
*
* It connects to both a math server and a weather server, retrieves the available tools,
* and creates an agent that can use these tools to solve problems.
*
* Note: You need to set the GOOGLE_API_KEY environment variable in the .env file to run this example.
*/
async function main() {
if (!process.env.GOOGLE_API_KEY) {
console.error('Please set the GOOGLE_API_KEY environment variable in the .env file');
process.exit(1);
}
// Create a client with configurations for both servers
const client = new MultiServerMCPClient({
math: {
transport: 'stdio',
command: 'python',
args: ['./examples/math_server.py'],
},
weather: {
transport: 'stdio',
command: 'python',
args: ['./examples/weather_server.py'],
},
});
try {
// Initialize connections to both servers
console.log('Initializing connections to servers...');
await client.initializeConnections();
console.log('Connected to servers');
// Get all tools from all servers
const serverTools = client.getTools();
// Flatten all tools for use with the agent
const allTools = Array.from(serverTools.values()).flat();
console.log(`Available tools: ${allTools.map(tool => tool.name).join(', ')}`);
// Print tool descriptions
console.log('Tool descriptions:');
for (const [serverName, tools] of serverTools.entries()) {
console.log(`\nServer: ${serverName}`);
for (const tool of tools) {
console.log(`- ${tool.name}: ${tool.description}`);
}
}
// Create an agent
console.log('\nCreating agent...');
const model = new ChatGoogleGenerativeAI({
apiKey: process.env.GOOGLE_API_KEY,
modelName: 'gemini-2.0-flash',
temperature: 0,
});
// Initialize the agent executor with a simple agent type
const executor = await initializeAgentExecutorWithOptions(allTools, model, {
agentType: 'chat-zero-shot-react-description',
verbose: true,
});
// Run the agent with different queries
const queries = [
'What is 5 + 3?',
'What is 7 * 9?',
"What's the current temperature in Tokyo?",
"What's the 3-day forecast for London?",
"If it's 72°F in New York, what is that in Celsius?",
];
for (const query of queries) {
console.log(`\n--- Query: "${query}" ---`);
try {
const result = await executor.invoke({
input: query,
});
console.log(`Answer: ${result.output}`);
} catch (error) {
console.error(`Error processing query "${query}":`, error);
}
}
} catch (error) {
console.error('Error:', error);
} finally {
// Close the client
console.log('\nClosing client...');
await client.close();
console.log('Client closed');
}
}
main().catch(console.error);
+155
View File
@@ -0,0 +1,155 @@
import { MultiServerMCPClient } from '../src/client.js';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import dotenv from 'dotenv';
// Load environment variables from .env file
dotenv.config();
/**
* This example demonstrates how to use MCP tools with Google's Gemini model.
*
* It connects to both a math server and a weather server, retrieves the available tools,
* and uses them directly with the model.
*
* Note: You need to set the GOOGLE_API_KEY environment variable in the .env file to run this example.
*/
async function main() {
if (!process.env.GOOGLE_API_KEY) {
console.error('Please set the GOOGLE_API_KEY environment variable in the .env file');
process.exit(1);
}
// Create a client with configurations for both servers
const client = new MultiServerMCPClient({
math: {
transport: 'stdio',
command: 'python',
args: ['./examples/math_server.py'],
},
weather: {
transport: 'stdio',
command: 'python',
args: ['./examples/weather_server.py'],
},
});
try {
// Initialize connections to both servers
console.log('Initializing connections to servers...');
await client.initializeConnections();
console.log('Connected to servers');
// Get all tools from all servers
const serverTools = client.getTools();
// Flatten all tools for display purposes
const allTools = Array.from(serverTools.values()).flat();
console.log(`Available tools: ${allTools.map(tool => tool.name).join(', ')}`);
// Print tool descriptions
console.log('Tool descriptions:');
for (const [serverName, tools] of serverTools.entries()) {
console.log(`\nServer: ${serverName}`);
for (const tool of tools) {
console.log(`- ${tool.name}: ${tool.description}`);
}
}
// Create the model
console.log('\nCreating model...');
const model = new ChatGoogleGenerativeAI({
apiKey: process.env.GOOGLE_API_KEY,
modelName: 'gemini-2.0-flash',
temperature: 0,
});
// Define some example operations
console.log('\n--- Math Operations ---');
// Get math tools
const mathTools = serverTools.get('math') || [];
// Add two numbers
const addTool = mathTools.find(t => t.name === 'add');
if (addTool) {
const addResult = await addTool.invoke({
a: 5,
b: 3,
});
console.log(`5 + 3 = ${addResult}`);
} else {
console.log('Add tool not available');
}
// Multiply two numbers
const multiplyTool = mathTools.find(t => t.name === 'multiply');
if (multiplyTool) {
const multiplyResult = await multiplyTool.invoke({
a: 4,
b: 7,
});
console.log(`4 * 7 = ${multiplyResult}`);
} else {
console.log('Multiply tool not available');
}
// Get weather information
console.log('\n--- Weather Information ---');
// Get weather tools
const weatherTools = serverTools.get('weather') || [];
// Get temperature for a city
const temperatureTool = weatherTools.find(t => t.name === 'get_temperature');
let temperatureResult;
if (temperatureTool) {
temperatureResult = await temperatureTool.invoke({
city: 'New York',
});
console.log(`Temperature in New York: ${temperatureResult}`);
} else {
console.log('Temperature tool not available');
}
// Get forecast for a city
const forecastTool = weatherTools.find(t => t.name === 'get_forecast');
let forecastResult;
if (forecastTool) {
forecastResult = await forecastTool.invoke({
city: 'London',
days: 3,
});
console.log(`Forecast for London: ${forecastResult}`);
} else {
console.log('Forecast tool not available');
}
// Use Gemini to interpret the results
console.log('\n--- Gemini Interpretations ---');
// Ask Gemini to convert Fahrenheit to Celsius
if (temperatureResult) {
const tempConversionPrompt = `If the temperature in New York is ${temperatureResult}, what is that in Celsius?`;
console.log(`Query: "${tempConversionPrompt}"`);
const tempConversionResponse = await model.invoke(tempConversionPrompt);
console.log(`Gemini's answer: ${tempConversionResponse.content}`);
}
// Ask Gemini to summarize the forecast
if (forecastResult) {
const forecastSummaryPrompt = `Summarize this forecast: ${forecastResult}`;
console.log(`\nQuery: "${forecastSummaryPrompt}"`);
const forecastSummaryResponse = await model.invoke(forecastSummaryPrompt);
console.log(`Gemini's answer: ${forecastSummaryResponse.content}`);
}
} catch (error) {
console.error('Error:', error);
} finally {
// Close the client
console.log('\nClosing client...');
await client.close();
console.log('Client closed');
}
}
main().catch(console.error);
+147
View File
@@ -0,0 +1,147 @@
import { MultiServerMCPClient } from '../src/client.js';
import * as fs from 'fs';
import * as path from 'path';
/**
* This example demonstrates how to use the MCP adapter with server configurations
* loaded from JSON configuration files.
*
* It shows:
* 1. How to load configurations from a specific JSON file
* 2. How to load configurations from the default mcp.json file
* 3. How to handle connection errors gracefully
* 4. How to use tools from different servers
*
* Before running this example:
* 1. Make sure you have the simple_mcp.json file in the examples directory
* 2. Start the weather server with SSE transport:
* python examples/weather_server.py
*
* 3. Then run this example with:
* node --loader ts-node/esm examples/json_config_example.ts
*/
async function main() {
// Determine which config file to use
const simpleMcpPath = './examples/simple_mcp.json';
const defaultMcpPath = './mcp.json';
let configPath;
if (fs.existsSync(simpleMcpPath)) {
configPath = simpleMcpPath;
console.log(`Loading MCP server configurations from ${path.basename(configPath)}...`);
} else if (fs.existsSync(defaultMcpPath)) {
configPath = defaultMcpPath;
console.log(`Loading MCP server configurations from ${path.basename(configPath)}...`);
} else {
console.error(`Neither ${simpleMcpPath} nor ${defaultMcpPath} exists.`);
console.log('Creating a client with default configuration instead...');
configPath = null;
}
try {
// Create a client from the config file or with default configuration
const client = configPath
? MultiServerMCPClient.fromConfigFile(configPath)
: new MultiServerMCPClient({
math: {
transport: 'stdio',
command: 'python',
args: ['./examples/math_server.py'],
},
weather: {
transport: 'sse',
url: 'http://localhost:8000/sse',
},
});
// Initialize all connections
console.log('Initializing connections to all servers...');
try {
await client.initializeConnections();
console.log('Connected to all servers');
} catch (error) {
console.error(
'Error connecting to some servers:',
error instanceof Error ? error.message : String(error)
);
console.log('Continuing with available servers...');
}
// Get all tools from all servers
const serverTools = client.getTools();
if (serverTools.size === 0) {
console.log('No tools available. Make sure at least one server is running.');
return;
}
// Flatten all tools for display purposes
const allTools = Array.from(serverTools.values()).flat();
console.log(`Available tools: ${allTools.map(tool => tool.name).join(', ')}`);
console.log(`Tool descriptions:`);
for (const [serverName, tools] of serverTools.entries()) {
console.log(`\nServer: ${serverName}`);
for (const tool of tools) {
console.log(`- ${tool.name}: ${tool.description}`);
}
}
// Use the add tool from math server if available
const mathTools = serverTools.get('math') || [];
const addTool = mathTools.find(tool => tool.name === 'add');
if (addTool) {
console.log('\nUsing add tool from math server...');
const addResult = await addTool.invoke({ a: 10, b: 5 });
console.log(`10 + 5 = ${addResult}`);
} else {
console.log('\nAdd tool not available. Make sure the math server is running.');
}
// Use the multiply tool from math server if available
const multiplyTool = mathTools.find(tool => tool.name === 'multiply');
if (multiplyTool) {
console.log('\nUsing multiply tool from math server...');
const multiplyResult = await multiplyTool.invoke({ a: 7, b: 8 });
console.log(`7 * 8 = ${multiplyResult}`);
} else {
console.log('\nMultiply tool not available. Make sure the math server is running.');
}
// Get temperature from weather server if available
const weatherTools = serverTools.get('weather') || [];
const getTempTool = weatherTools.find(tool => tool.name === 'get_temperature');
if (getTempTool) {
console.log('\nGetting temperature from weather server...');
const tempResult = await getTempTool.invoke({ city: 'Tokyo' });
console.log(tempResult);
} else {
console.log('\nTemperature tool not available. Make sure the weather server is running.');
}
// Get forecast from weather server if available
const getForecastTool = weatherTools.find(tool => tool.name === 'get_forecast');
if (getForecastTool) {
console.log('\nGetting forecast from weather server...');
const forecastResult = await getForecastTool.invoke({
city: 'London',
days: 3,
});
console.log(forecastResult);
} else {
console.log('\nForecast tool not available. Make sure the weather server is running.');
}
// Close the client when done
console.log('\nClosing client...');
await client.close();
console.log('Client closed');
} catch (error) {
console.error('Error:', error instanceof Error ? error.message : String(error));
}
}
main().catch(error =>
console.error('Unhandled error:', error instanceof Error ? error.message : String(error))
);
+100
View File
@@ -0,0 +1,100 @@
/**
* This example demonstrates how to use the built-in Winston logger with the MCP adapter.
*
* To run this example:
* 1. Start the math server: python examples/math_server.py
* 2. Start the weather server: python examples/weather_server.py
* 3. Run this example: node --loader ts-node/esm examples/logging_example.ts
*/
import { MultiServerMCPClient, logger } from '../src/index.js';
// Set log level to debug for more verbose output
// This is the default in development mode
// In production mode, the default is 'info'
process.env.NODE_ENV = 'development';
async function main() {
logger.info('Starting logging example');
// Create a client with connections to both the math and weather servers
logger.info('Creating MCP client with connections to math and weather servers');
const client = new MultiServerMCPClient({
math: {
command: 'python',
args: ['examples/math_server.py'],
},
weather: {
transport: 'sse',
url: 'http://localhost:8000/sse',
},
});
try {
// Initialize connections to all servers
logger.info('Initializing connections to servers');
const serverTools = await client.initializeConnections();
// Log the available tools from each server
// Use Array.from to convert the Map entries to an array
Array.from(serverTools.keys()).forEach(serverName => {
const tools = serverTools.get(serverName) || [];
logger.info(`Server "${serverName}" has ${tools.length} tools available:`);
tools.forEach(tool => {
logger.info(` - ${tool.name}: ${tool.description}`);
});
});
// Get the math tools
const mathTools = serverTools.get('math') || [];
if (mathTools.length > 0) {
// Find the add tool
const addTool = mathTools.find(tool => tool.name === 'add');
if (addTool) {
logger.debug('Calling add tool with numbers 5 and 7');
const result = await addTool.invoke({ a: 5, b: 7 });
logger.info(`Result of 5 + 7 = ${result}`);
} else {
logger.warn('Add tool not found in math server');
}
} else {
logger.error('No tools found for math server');
}
// Get the weather tools
const weatherTools = serverTools.get('weather') || [];
if (weatherTools.length > 0) {
// Find the temperature tool
const tempTool = weatherTools.find(tool => tool.name === 'get_temperature');
if (tempTool) {
logger.debug('Calling temperature tool for San Francisco');
const result = await tempTool.invoke({ city: 'San Francisco' });
logger.info(`Temperature in San Francisco: ${result}`);
} else {
logger.warn('Temperature tool not found in weather server');
}
} else {
logger.error('No tools found for weather server');
}
// Demonstrate different log levels
logger.error('This is an error message - will appear in error.log and all.log');
logger.warn('This is a warning message');
logger.info('This is an info message');
logger.http('This is an HTTP message');
logger.debug('This is a debug message - only visible in development mode');
// Close all connections
logger.info('Closing all connections');
await client.close();
logger.info('Logging example completed successfully');
} catch (error) {
logger.error(`Error in logging example: ${error}`);
}
}
main().catch(error => {
logger.error(`Unhandled error in main: ${error}`);
process.exit(1);
});
+61
View File
@@ -0,0 +1,61 @@
/**
* This example demonstrates how to use the MCP adapter with a math server.
*
* To run this example:
* 1. Run this example: node --loader ts-node/esm examples/math_example.ts
*
* The math server will be started automatically via stdio transport.
*/
import { MultiServerMCPClient } from '../src/client.js';
async function main() {
try {
// Create a client
const client = new MultiServerMCPClient({
math: {
// No transport specified - will default to stdio
command: 'python',
args: ['./examples/math_server.py'],
},
});
// Connect to the math server via stdio
console.log('Connecting to math server via stdio...');
const serverTools = await client.initializeConnections();
console.log('Connected to math server');
// Get the math tools
const mathTools = serverTools.get('math') || [];
console.log(`Available tools: ${mathTools.map(tool => tool.name).join(', ')}`);
console.log(`Tool descriptions:`);
for (const tool of mathTools) {
console.log(`- ${tool.name}: ${tool.description}`);
}
// Add two numbers
console.log('\nAdding 5 + 3...');
const addTool = mathTools.find(tool => tool.name === 'add');
if (!addTool) {
throw new Error('add tool not found');
}
const addResult = await addTool.invoke({ a: 5, b: 3 });
console.log(`Result: ${addResult}`);
// Multiply two numbers
console.log('\nMultiplying 4 * 7...');
const multiplyTool = mathTools.find(tool => tool.name === 'multiply');
if (!multiplyTool) {
throw new Error('multiply tool not found');
}
const multiplyResult = await multiplyTool.invoke({ a: 4, b: 7 });
console.log(`Result: ${multiplyResult}`);
// Close the client
await client.close();
} catch (error) {
console.error('Error:', error);
}
}
main();
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env python
from mcp.server.fastmcp import FastMCP
# Create a server
mcp = FastMCP(name="Math")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two integers and return the result."""
return a + b
@mcp.tool()
def multiply(a: int, b: int) -> int:
"""Multiply two integers and return the result."""
return a * b
# Run the server
if __name__ == "__main__":
mcp.run(transport="stdio")
+245
View File
@@ -0,0 +1,245 @@
import { MultiServerMCPClient } from '../src/client.js';
/**
* This example demonstrates how to use the MCP adapter with multiple servers
* using different transport methods.
*
* It shows:
* 1. How to connect to servers using different transport methods (stdio and SSE)
* 2. Two different approaches to configure multiple servers:
* - Using a configuration object at initialization
* - Using individual connect methods after initialization
* 3. How to use tools from different servers
* 4. How to perform complex operations using tools from multiple servers
*
* Before running this example:
* 1. Start the weather server with SSE transport:
* python examples/weather_server.py
*
* 2. Then run this example with:
* node --loader ts-node/esm examples/multi_transport_example.ts
*/
async function main() {
console.log('=== Method 1: Using Configuration Object ===');
await runWithConfigObject();
console.log('\n\n=== Method 2: Using Individual Connect Methods ===');
await runWithConnectMethods();
}
/**
* Demonstrates using a configuration object to connect to multiple servers
*/
async function runWithConfigObject() {
// Create a client with multiple server connections using a configuration object
const client = new MultiServerMCPClient({
math: {
transport: 'stdio',
command: 'python',
args: ['./examples/math_server.py'],
},
weather: {
transport: 'sse',
url: 'http://localhost:8000/sse',
},
});
try {
// Initialize all connections
console.log('Initializing connections to all servers...');
try {
await client.initializeConnections();
console.log('Connected to all servers');
} catch (error) {
console.error(
'Error connecting to some servers:',
error instanceof Error ? error.message : String(error)
);
console.log('Continuing with available servers...');
}
await demonstrateTools(client);
} finally {
// Close the client
console.log('\nClosing client...');
await client.close();
console.log('Client closed');
}
}
/**
* Demonstrates using individual connect methods to connect to multiple servers
*/
async function runWithConnectMethods() {
// Create a client with math server configuration
const mathClient = new MultiServerMCPClient({
math: {
transport: 'stdio',
command: 'python',
args: ['./examples/math_server.py'],
},
});
// Create a client with weather server configuration
const weatherClient = new MultiServerMCPClient({
weather: {
transport: 'sse',
url: 'http://localhost:8000/sse',
},
});
// Create a combined client for demonstration
const combinedClient = new MultiServerMCPClient({
math: {
transport: 'stdio',
command: 'python',
args: ['./examples/math_server.py'],
},
weather: {
transport: 'sse',
url: 'http://localhost:8000/sse',
},
});
try {
// Initialize math server connection
console.log('Connecting to math server...');
try {
await mathClient.initializeConnections();
console.log('Connected to math server');
} catch (error) {
console.error(
'Error connecting to math server:',
error instanceof Error ? error.message : String(error)
);
}
// Initialize weather server connection
console.log('\nConnecting to weather server...');
try {
await weatherClient.initializeConnections();
console.log('Connected to weather server');
} catch (error) {
console.error(
'Error connecting to weather server:',
error instanceof Error ? error.message : String(error)
);
}
// For demonstration purposes, we'll use the combined client
// In a real application, you might want to merge the tools from both clients
console.log('\nInitializing combined client for demonstration...');
await combinedClient.initializeConnections();
await demonstrateTools(combinedClient);
} finally {
// Close all clients
console.log('\nClosing clients...');
await mathClient.close();
await weatherClient.close();
await combinedClient.close();
console.log('Clients closed');
}
}
/**
* Demonstrates using tools from multiple servers
*/
async function demonstrateTools(client: MultiServerMCPClient) {
// Get all tools from all servers
const serverTools = client.getTools();
if (serverTools.size === 0) {
console.log('No tools available. Make sure at least one server is running.');
return;
}
// Flatten all tools for display purposes
const allTools = Array.from(serverTools.values()).flat();
console.log(`\nAvailable tools: ${allTools.map(tool => tool.name).join(', ')}`);
console.log(`Tool descriptions:`);
for (const [serverName, tools] of serverTools.entries()) {
console.log(`\nServer: ${serverName}`);
for (const tool of tools) {
console.log(`- ${tool.name}: ${tool.description}`);
}
}
// Use the math tools if available
console.log('\n--- Math Operations ---');
const mathTools = serverTools.get('math') || [];
const addTool = mathTools.find(tool => tool.name === 'add');
if (addTool) {
const addResult = await addTool.invoke({ a: 5, b: 3 });
console.log(`5 + 3 = ${addResult}`);
} else {
console.log('Add tool not available');
}
const multiplyTool = mathTools.find(tool => tool.name === 'multiply');
if (multiplyTool) {
const multiplyResult = await multiplyTool.invoke({ a: 4, b: 7 });
console.log(`4 * 7 = ${multiplyResult}`);
} else {
console.log('Multiply tool not available');
}
// Use the weather tools if available
console.log('\n--- Weather Information ---');
const weatherTools = serverTools.get('weather') || [];
const temperatureTool = weatherTools.find(tool => tool.name === 'get_temperature');
if (temperatureTool) {
const temperatureResult = await temperatureTool.invoke({
city: 'New York',
});
console.log(`Temperature in New York: ${temperatureResult}`);
} else {
console.log('Temperature tool not available');
}
const forecastTool = weatherTools.find(tool => tool.name === 'get_forecast');
if (forecastTool) {
const forecastResult = await forecastTool.invoke({
city: 'London',
days: 3,
});
console.log(`Forecast for London: ${forecastResult}`);
} else {
console.log('Forecast tool not available');
}
// Perform complex operations if all required tools are available
if (addTool && multiplyTool && temperatureTool) {
console.log('\n--- Complex Operations ---');
// Example 1: Simple math operation
const sum = await addTool.invoke({ a: 5, b: 3 });
const product = await multiplyTool.invoke({ a: sum, b: 2 });
console.log(`(5 + 3) * 2 = ${product}`);
// Example 2: Convert temperature from Fahrenheit to Celsius
const temperatureResult = await temperatureTool.invoke({
city: 'New York',
});
const tempStr = temperatureResult.toString();
const tempMatch = tempStr.match(/(\d+)°F/);
if (tempMatch && tempMatch[1]) {
const tempF = parseInt(tempMatch[1]);
const tempMinusThirtyTwo = await addTool.invoke({ a: tempF, b: -32 });
const tempCelsius = await multiplyTool.invoke({
a: tempMinusThirtyTwo,
b: 5 / 9,
});
console.log(`Temperature in New York converted to Celsius: ${Math.round(tempCelsius)}°C`);
} else {
console.log('Could not extract temperature value for conversion');
}
}
}
main().catch(error =>
console.error('Unhandled error:', error instanceof Error ? error.message : String(error))
);
+12
View File
@@ -0,0 +1,12 @@
{
"servers": {
"math": {
"command": "python",
"args": ["./examples/math_server.py"]
},
"weather-sse": {
"transport": "sse",
"url": "http://localhost:8000/sse"
}
}
}
+67
View File
@@ -0,0 +1,67 @@
import { MultiServerMCPClient } from '../src/client.js';
/**
* This example demonstrates how to use the MCP adapter with an SSE server.
*
* Before running this example, you need to:
*
* 1. Start the weather server with SSE transport:
* python examples/weather_server.py
*
* 2. Then run this example with:
* node --loader ts-node/esm examples/sse_example.ts
*/
async function main() {
try {
// Create a client with a connection to the weather server
const client = new MultiServerMCPClient({
weather: {
transport: 'sse',
url: 'http://localhost:8000/sse',
},
});
// Initialize the connection
console.log('Connecting to weather server via SSE...');
const serverTools = await client.initializeConnections();
console.log('Connected to weather server');
// Get the weather tools
const weatherTools = serverTools.get('weather') || [];
console.log(`Available tools: ${weatherTools.map(tool => tool.name).join(', ')}`);
console.log(`Tool descriptions:`);
for (const tool of weatherTools) {
console.log(`- ${tool.name}: ${tool.description}`);
}
// Get temperature for a city
console.log('\nGetting temperature for New York...');
const getTempTool = weatherTools.find(tool => tool.name === 'get_temperature');
if (!getTempTool) {
throw new Error('get_temperature tool not found');
}
const tempResult = await getTempTool.invoke({ city: 'New York' });
console.log(tempResult);
// Get forecast for a city
console.log('\nGetting forecast for London...');
const getForecastTool = weatherTools.find(tool => tool.name === 'get_forecast');
if (!getForecastTool) {
throw new Error('get_forecast tool not found');
}
const forecastResult = await getForecastTool.invoke({
city: 'London',
days: 3,
});
console.log(forecastResult);
// Close the client
console.log('\nClosing client...');
await client.close();
console.log('Client closed');
} catch (error) {
console.error('Error:', error);
}
}
main();
+129
View File
@@ -0,0 +1,129 @@
import { MultiServerMCPClient } from '../src/client.js';
/**
* This example demonstrates how to use the MCP adapter with a custom configuration
* that specifies different transport types for different servers.
*
* It shows:
* 1. How to create a client with a custom configuration
* 2. How to use the default stdio transport implicitly
* 3. How to specify the SSE transport explicitly
* 4. How to handle connection errors gracefully
* 5. How to use tools from different servers
*
* Before running this example:
* 1. Start the weather server with SSE transport:
* python examples/weather_server.py
*
* 2. Then run this example with:
* node --loader ts-node/esm examples/transport_config_example.ts
*/
async function main() {
console.log('Creating MCP client with custom transport configuration...');
try {
// Create a client with a custom configuration
const client = new MultiServerMCPClient({
math: {
// No transport specified - will default to stdio
command: 'python',
args: ['./examples/math_server.py'],
},
weather: {
// Explicitly specify SSE transport
transport: 'sse',
url: 'http://localhost:8000/sse',
},
});
// Initialize all connections
console.log('Initializing connections to all servers...');
try {
await client.initializeConnections();
console.log('Connected to all servers');
} catch (error) {
console.error(
'Error connecting to some servers:',
error instanceof Error ? error.message : String(error)
);
console.log('Continuing with available servers...');
}
// Get all tools from all servers
const serverTools = client.getTools();
if (serverTools.size === 0) {
console.log('No tools available. Make sure at least one server is running.');
return;
}
// Flatten all tools for display purposes
const allTools = Array.from(serverTools.values()).flat();
console.log(`Available tools: ${allTools.map(tool => tool.name).join(', ')}`);
console.log(`Tool descriptions:`);
for (const [serverName, tools] of serverTools.entries()) {
console.log(`\nServer: ${serverName}`);
for (const tool of tools) {
console.log(`- ${tool.name}: ${tool.description}`);
}
}
// Use the add tool from math server if available
const mathTools = serverTools.get('math') || [];
const addTool = mathTools.find(tool => tool.name === 'add');
if (addTool) {
console.log('\nUsing add tool from math server...');
const addResult = await addTool.invoke({ a: 10, b: 5 });
console.log(`10 + 5 = ${addResult}`);
} else {
console.log('\nAdd tool not available. Make sure the math server is running.');
}
// Use the multiply tool from math server if available
const multiplyTool = mathTools.find(tool => tool.name === 'multiply');
if (multiplyTool) {
console.log('\nUsing multiply tool from math server...');
const multiplyResult = await multiplyTool.invoke({ a: 7, b: 8 });
console.log(`7 * 8 = ${multiplyResult}`);
} else {
console.log('\nMultiply tool not available. Make sure the math server is running.');
}
// Get temperature from weather server if available
const weatherTools = serverTools.get('weather') || [];
const getTempTool = weatherTools.find(tool => tool.name === 'get_temperature');
if (getTempTool) {
console.log('\nGetting temperature from weather server...');
const tempResult = await getTempTool.invoke({ city: 'Tokyo' });
console.log(tempResult);
} else {
console.log('\nTemperature tool not available. Make sure the weather server is running.');
}
// Get forecast from weather server if available
const getForecastTool = weatherTools.find(tool => tool.name === 'get_forecast');
if (getForecastTool) {
console.log('\nGetting forecast from weather server...');
const forecastResult = await getForecastTool.invoke({
city: 'London',
days: 3,
});
console.log(forecastResult);
} else {
console.log('\nForecast tool not available. Make sure the weather server is running.');
}
// Close the client when done
console.log('\nClosing client...');
await client.close();
console.log('Client closed');
} catch (error) {
console.error('Error:', error instanceof Error ? error.message : String(error));
}
}
main().catch(error =>
console.error('Unhandled error:', error instanceof Error ? error.message : String(error))
);
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python
from mcp.server.fastmcp import FastMCP
import sys
# Create a server
mcp = FastMCP(name="Weather")
@mcp.tool()
def get_temperature(city: str) -> str:
"""Get the current temperature for a city.
This is a mock implementation that returns fake data.
In a real application, this would call a weather API.
"""
# Mock implementation - in a real app, this would call a weather API
temperatures = {
"new york": "72°F",
"london": "65°F",
"tokyo": "25 degrees Celsius",
"paris": "70°F",
"sydney": "80°F",
}
city_lower = city.lower()
if city_lower in temperatures:
return f"The current temperature in {city} is {temperatures[city_lower]}."
else:
return f"Temperature data not available for {city}."
@mcp.tool()
def get_forecast(city: str, days: int = 3) -> str:
"""Get the weather forecast for a city.
Args:
city: The name of the city to get the forecast for.
days: The number of days to forecast (default: 3).
Returns:
A string containing the weather forecast.
"""
# Mock implementation - in a real app, this would call a weather API
forecasts = {
"new york": "Sunny with a chance of rain",
"london": "Cloudy with occasional showers",
"tokyo": "Clear skies",
"paris": "Partly cloudy",
"sydney": "Warm and sunny",
}
city_lower = city.lower()
if city_lower in forecasts:
return f"The {days}-day forecast for {city} is: {forecasts[city_lower]}."
else:
return f"Forecast data not available for {city}."
# Run the server
if __name__ == "__main__":
# Set the port using command line arguments
# The FastMCP server will read these arguments
if len(sys.argv) == 1: # No arguments provided
# Add command line arguments for the port
sys.argv.extend(["--sse-port", "8000"])
port = 8000
else:
# Check if --sse-port is already in the arguments
if "--sse-port" in sys.argv:
port_index = sys.argv.index("--sse-port") + 1
if port_index < len(sys.argv):
port = int(sys.argv[port_index])
else:
port = 8000
else:
# Add the port argument
sys.argv.extend(["--sse-port", "8000"])
port = 8000
# Run with SSE transport
print(f"Starting weather server with SSE transport on port {port}...")
mcp.run(transport="sse")
+22
View File
@@ -0,0 +1,22 @@
export default {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.ts'],
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.test.ts'],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],
moduleFileExtensions: ['ts', 'js', 'json'],
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
};
+13
View File
@@ -0,0 +1,13 @@
{
"servers": {
"math": {
"transport": "stdio",
"command": "python",
"args": ["./examples/math_server.py"]
},
"weather": {
"transport": "sse",
"url": "http://localhost:8000/sse"
}
}
}
+7610
View File
File diff suppressed because it is too large Load Diff
+68
View File
@@ -0,0 +1,68 @@
{
"name": "langchainjs-mcp-adapters",
"version": "0.1.0",
"description": "LangChain.js adapters for Model Context Protocol (MCP)",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"scripts": {
"build": "tsc",
"test": "jest",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write \"src/**/*.ts\" \"examples/**/*.ts\"",
"prepare": "husky"
},
"lint-staged": {
"*.{js,ts}": [
"eslint --fix",
"prettier --write"
]
},
"keywords": [
"langchain",
"mcp",
"model-context-protocol",
"ai",
"tools"
],
"author": "Ravi Kiran Vemula",
"license": "MIT",
"dependencies": {
"@langchain/core": "^0.3.40",
"@langchain/google-genai": "^0.1.10",
"@langchain/openai": "^0.4.4",
"@modelcontextprotocol/sdk": "^1.6.1",
"dotenv": "^16.4.7",
"langchain": "^0.3.19",
"winston": "^3.17.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.30",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^10.0.2",
"husky": "^9.0.11",
"jest": "^29.7.0",
"lint-staged": "^15.2.2",
"prettier": "^3.2.5",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"typescript": "^5.4.2",
"typescript-eslint": "^8.26.0"
},
"engines": {
"node": ">=18"
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"directories": {
"example": "examples"
}
}
+291
View File
@@ -0,0 +1,291 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { StructuredTool } from '@langchain/core/tools';
import { loadMcpTools } from './tools.js';
import * as fs from 'fs';
import * as path from 'path';
import logger from './logger.js';
type StdioConnection = {
transport: 'stdio';
command: string;
args: string[];
env?: Record<string, string>;
encoding?: string;
encodingErrorHandler?: 'strict' | 'ignore' | 'replace';
};
type SSEConnection = {
transport: 'sse';
url: string;
};
type Connection = StdioConnection | SSEConnection;
type MCPConfig = {
servers: Record<string, Connection>;
};
/**
* Client for connecting to multiple MCP servers and loading LangChain-compatible tools.
*/
export class MultiServerMCPClient {
private clients: Map<string, Client> = new Map();
private serverNameToTools: Map<string, StructuredTool[]> = new Map();
private connections?: Record<string, Connection>;
private cleanupFunctions: Array<() => Promise<void>> = [];
/**
* Create a new MultiServerMCPClient.
*
* @param connections - Optional connections to initialize
*/
constructor(connections?: Record<string, any>) {
if (connections) {
// Process connections to ensure they have the correct format
const processedConnections: Record<string, Connection> = {};
for (const [serverName, config] of Object.entries(connections)) {
if (typeof config !== 'object' || config === null) {
logger.warn(`Invalid configuration for server "${serverName}". Skipping.`);
continue;
}
// If transport is explicitly set
if (config.transport) {
if (config.transport === 'stdio') {
if (!config.command || !Array.isArray(config.args)) {
logger.warn(
`Server "${serverName}" is missing required properties for stdio transport. Skipping.`
);
continue;
}
const stdioConfig: StdioConnection = {
transport: 'stdio',
command: config.command,
args: config.args,
};
// Add optional properties if they exist
if (config.env && typeof config.env === 'object') {
stdioConfig.env = config.env;
}
if (typeof config.encoding === 'string') {
stdioConfig.encoding = config.encoding;
}
if (['strict', 'ignore', 'replace'].includes(config.encodingErrorHandler)) {
stdioConfig.encodingErrorHandler = config.encodingErrorHandler as
| 'strict'
| 'ignore'
| 'replace';
}
processedConnections[serverName] = stdioConfig;
} else if (config.transport === 'sse') {
if (!config.url) {
logger.warn(
`Server "${serverName}" is missing required URL for SSE transport. Skipping.`
);
continue;
}
processedConnections[serverName] = {
transport: 'sse',
url: config.url,
};
} else {
logger.warn(
`Server "${serverName}" has unsupported transport type: ${config.transport}. Skipping.`
);
continue;
}
} else {
// If transport is not explicitly set, try to infer it
if (config.command && Array.isArray(config.args)) {
// Looks like stdio
const stdioConfig: StdioConnection = {
transport: 'stdio',
command: config.command,
args: config.args,
};
// Add optional properties if they exist
if (config.env && typeof config.env === 'object') {
stdioConfig.env = config.env;
}
if (typeof config.encoding === 'string') {
stdioConfig.encoding = config.encoding;
}
if (['strict', 'ignore', 'replace'].includes(config.encodingErrorHandler)) {
stdioConfig.encodingErrorHandler = config.encodingErrorHandler as
| 'strict'
| 'ignore'
| 'replace';
}
processedConnections[serverName] = stdioConfig;
} else if (config.url) {
// Looks like SSE
processedConnections[serverName] = {
transport: 'sse',
url: config.url,
};
} else {
logger.warn(`Server "${serverName}" has invalid configuration. Skipping.`);
continue;
}
}
}
this.connections = processedConnections;
}
}
/**
* Load a configuration from a JSON file.
*
* @param configPath - Path to the configuration file
* @returns A new MultiServerMCPClient
*/
static fromConfigFile(configPath: string): MultiServerMCPClient {
try {
const configData = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(configData) as MCPConfig;
logger.info(`Loaded MCP configuration from ${configPath}`);
return new MultiServerMCPClient(config.servers);
} catch (error) {
logger.error(`Failed to load MCP configuration from ${configPath}: ${error}`);
throw new Error(`Failed to load MCP configuration: ${error}`);
}
}
/**
* Initialize connections to all servers.
*
* @returns A map of server names to arrays of tools
*/
async initializeConnections(): Promise<Map<string, StructuredTool[]>> {
if (!this.connections) {
logger.warn('No connections to initialize');
return new Map();
}
for (const [serverName, connection] of Object.entries(this.connections)) {
try {
logger.info(`Initializing connection to server "${serverName}"...`);
let client: Client;
let cleanup: () => Promise<void>;
if (connection.transport === 'stdio') {
const { command, args, env } = connection;
logger.debug(
`Creating stdio transport for server "${serverName}" with command: ${command} ${args.join(' ')}`
);
const transport = new StdioClientTransport({
command,
args,
env,
});
client = new Client({
name: 'langchain-mcp-adapter',
version: '0.1.0',
});
await client.connect(transport);
cleanup = async () => {
logger.debug(`Closing stdio transport for server "${serverName}"`);
await transport.close();
};
} else if (connection.transport === 'sse') {
const { url } = connection;
logger.debug(`Creating SSE transport for server "${serverName}" with URL: ${url}`);
const transport = new SSEClientTransport(new URL(url));
client = new Client({
name: 'langchain-mcp-adapter',
version: '0.1.0',
});
await client.connect(transport);
cleanup = async () => {
logger.debug(`Closing SSE transport for server "${serverName}"`);
await transport.close();
};
} else {
// This should never happen due to the validation in the constructor
logger.error(`Unsupported transport type for server "${serverName}"`);
continue;
}
this.clients.set(serverName, client);
this.cleanupFunctions.push(cleanup);
// Load tools for this server
try {
logger.debug(`Loading tools for server "${serverName}"...`);
const tools = await loadMcpTools(client);
this.serverNameToTools.set(serverName, tools);
logger.info(`Successfully loaded ${tools.length} tools from server "${serverName}"`);
} catch (error) {
logger.error(`Failed to load tools from server "${serverName}": ${error}`);
}
} catch (error) {
logger.error(`Failed to connect to server "${serverName}": ${error}`);
}
}
return this.serverNameToTools;
}
/**
* Get all tools from all servers.
*
* @returns A map of server names to arrays of tools
*/
getTools(): Map<string, StructuredTool[]> {
return this.serverNameToTools;
}
/**
* Get a client for a specific server.
*
* @param serverName - The name of the server
* @returns The client for the server, or undefined if the server is not connected
*/
getClient(serverName: string): Client | undefined {
return this.clients.get(serverName);
}
/**
* Close all connections.
*/
async close(): Promise<void> {
logger.info('Closing all MCP connections...');
for (const cleanup of this.cleanupFunctions) {
try {
await cleanup();
} catch (error) {
logger.error(`Error during cleanup: ${error}`);
}
}
this.cleanupFunctions = [];
this.clients.clear();
this.serverNameToTools.clear();
logger.info('All MCP connections closed');
}
}
+3
View File
@@ -0,0 +1,3 @@
export { MultiServerMCPClient } from './client.js';
export { convertMcpToolToLangchainTool, loadMcpTools } from './tools.js';
export { default as logger } from './logger.js';
+84
View File
@@ -0,0 +1,84 @@
import * as winston from 'winston';
/**
* Logging levels:
* error: 0 - Severe errors that cause the application to crash or malfunction
* warn: 1 - Warnings that don't stop the application but should be addressed
* info: 2 - General information about application operation
* http: 3 - HTTP request/response information
* debug: 4 - Detailed debugging information
*/
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4,
};
/**
* Determine the appropriate log level based on the environment.
* In development, we want to see all logs.
* In production, we only want to see warnings and errors.
*/
const level = () => {
const env = process.env.NODE_ENV || 'development';
const isDevelopment = env === 'development';
return isDevelopment ? 'debug' : 'warn';
};
/**
* Define colors for each log level to improve readability in the console.
*/
const colors = {
error: 'red',
warn: 'yellow',
info: 'green',
http: 'magenta',
debug: 'white',
};
// Add colors to Winston
winston.addColors(colors);
/**
* Define the format for log messages.
* We include a timestamp, colorize the output, and format the message.
*/
const format = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
winston.format.colorize({ all: true }),
winston.format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`)
);
/**
* Define the transports for log messages.
* We log to the console and to files.
*/
const transports = [
// Console transport
new winston.transports.Console(),
// File transport for errors
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
}),
// File transport for all logs
new winston.transports.File({
filename: 'logs/all.log',
}),
];
/**
* Create the logger instance with our configuration.
*/
const logger = winston.createLogger({
level: level(),
levels,
format,
transports,
});
export default logger;
+163
View File
@@ -0,0 +1,163 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import logger from './logger.js';
// Define the types that were previously imported from the SDK
interface ContentItem {
type: string;
text?: string;
[key: string]: any;
}
// Custom error class for tool exceptions
class ToolException extends Error {
constructor(message: string) {
super(message);
this.name = 'ToolException';
}
}
/**
* Process the result from calling an MCP tool.
*
* @param result - The result from the MCP tool call
* @returns The processed result
*/
function _convertCallToolResult(result: any): any {
// Check for error in the response
if (result.isError) {
// Find the first text content for error message
if (Array.isArray(result.content)) {
const textContent = result.content.find((item: ContentItem) => item.type === 'text');
if (textContent && textContent.text) {
throw new ToolException(textContent.text);
}
}
throw new ToolException('Tool execution failed');
}
// Handle content array from the new SDK format
if (Array.isArray(result.content)) {
// Find the first text content
const textContent = result.content.find((item: ContentItem) => item.type === 'text');
if (textContent) {
return textContent.text;
}
// If there's only one content item, return it
if (result.content.length === 1) {
return result.content[0];
}
// Return the whole content array if no text found
return result.content;
}
// Handle old format or other formats
return result;
}
/**
* Convert an MCP tool to a LangChain tool.
*
* @param client - The MCP client
* @param toolName - The name of the tool
* @param toolDescription - The description of the tool
* @param toolSchema - The schema of the tool
* @returns A LangChain tool
*/
export function convertMcpToolToLangchainTool(
client: Client,
toolName: string,
toolDescription: string,
toolSchema: any
): StructuredTool {
// Convert the JSON schema to a Zod schema
let zodSchema: z.ZodObject<any>;
try {
// Create a Zod schema based on the tool's schema
if (toolSchema && toolSchema.type === 'object' && toolSchema.properties) {
const schemaShape: Record<string, z.ZodType> = {};
// Convert each property to a Zod type
Object.entries(toolSchema.properties).forEach(([key, value]: [string, any]) => {
if (value.type === 'string') {
schemaShape[key] = z.string();
} else if (value.type === 'number') {
schemaShape[key] = z.number();
} else if (value.type === 'boolean') {
schemaShape[key] = z.boolean();
} else if (value.type === 'array') {
schemaShape[key] = z.array(z.any());
} else {
schemaShape[key] = z.any();
}
});
zodSchema = z.object(schemaShape);
} else {
zodSchema = z.object({});
}
} catch (error) {
logger.warn(`Error creating Zod schema for tool ${toolName}:`, error);
zodSchema = z.object({});
}
// Create a class that extends StructuredTool
class MCPTool extends StructuredTool {
name = toolName;
description = toolDescription;
schema = zodSchema;
constructor() {
super();
}
protected async _call(input: Record<string, any>): Promise<string> {
try {
logger.debug(`Calling MCP tool ${toolName} with input:`, input);
// Use the new SDK format for calling tools
const result = await client.callTool({
name: toolName,
arguments: input,
});
const processedResult = _convertCallToolResult(result);
logger.debug(`MCP tool ${toolName} returned:`, processedResult);
return String(processedResult);
} catch (error) {
logger.error(`Error calling tool ${toolName}:`, error);
throw new ToolException(`Error calling tool ${toolName}: ${error}`);
}
}
}
return new MCPTool();
}
/**
* Load all tools from an MCP client.
*
* @param client - The MCP client
* @returns A list of LangChain tools
*/
export async function loadMcpTools(client: Client): Promise<StructuredTool[]> {
const tools: StructuredTool[] = [];
logger.debug('Listing available MCP tools...');
const toolsResponse = await client.listTools();
const toolsInfo = toolsResponse.tools;
logger.info(`Found ${toolsInfo.length} MCP tools`);
for (const toolInfo of toolsInfo) {
logger.debug(`Converting MCP tool "${toolInfo.name}" to LangChain tool`);
const tool = convertMcpToolToLangchainTool(
client,
toolInfo.name,
toolInfo.description || '',
toolInfo.inputSchema
);
tools.push(tool);
}
return tools;
}
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"sourceMap": true,
"downlevelIteration": true
},
"include": ["src/**/*", "examples/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}