@1mcp/agent 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,67 +4,207 @@ A unified Model Context Protocol server implementation that aggregates multiple
4
4
 
5
5
  ## Overview
6
6
 
7
- 1MCP (One MCP) is designed to simplify the way you work with AI coding assistants. Instead of configuring multiple MCP servers for different clients (Cursor, Roo Code, Claude, etc.), 1MCP provides a single, unified server that:
7
+ 1MCP (One MCP) is designed to simplify the way you work with AI assistants. Instead of configuring multiple MCP servers for different clients (Claude Desktop, Cherry Studio, Cursor, Roo Code, Claude, etc.), 1MCP provides a single, unified server that:
8
8
 
9
9
  - Aggregates multiple MCP servers into one unified interface
10
10
  - Reduces system resource usage by eliminating redundant server instances
11
- - Simplifies configuration management across different AI coding assistants
11
+ - Simplifies configuration management across different AI assistants
12
12
  - Provides a standardized way for AI models to interact with external tools and resources
13
+ - Supports dynamic configuration reloading without server restart
14
+ - Handles graceful shutdown and resource cleanup
13
15
 
14
- The implementation includes:
16
+ ## Quick Start
15
17
 
16
- - Express-based HTTP server with SSE (Server-Sent Events) support
17
- - Winston-based logging system with MCP protocol integration
18
- - Support for MCP capabilities and notifications
19
- - Smart request routing and capability aggregation
18
+ To enable Cursor to use existing MCP servers already configured in Claude Desktop, follow these steps:
20
19
 
21
- ## Server Implementation
22
-
23
- The server is implemented using:
24
-
25
- - Express.js for HTTP handling
26
- - Server-Sent Events (SSE) for real-time communication
27
- - MCP SDK for protocol implementation
28
-
29
- ### Server Configuration
30
-
31
- The server runs on port 3050 by default and exposes the following endpoints:
32
-
33
- - `/sse` - SSE endpoint for establishing connections
34
- - `/messages` - Endpoint for receiving messages from clients
35
-
36
- ## MCP Logging Implementation
37
-
38
- This project includes a Winston transport for the Model Context Protocol (MCP) logging protocol. The implementation allows logs from the agent to be sent to MCP clients using the standard MCP logging notification format.
20
+ 1. Run the 1MCP server with the Claude Desktop config file:
21
+ ```bash
22
+ npx -y @1mcp/agent --config ~/Library/Application\ Support/Claude/claude_desktop_config.json
23
+ ```
39
24
 
40
- ### Features
25
+ 2. Add the 1MCP server to your Cursor config file (`~/.cursor/mcp.json`):
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "1mcp": {
30
+ "type": "http",
31
+ "url": "http://localhost:3050/sse"
32
+ }
33
+ }
34
+ }
35
+ ```
41
36
 
42
- - Seamless integration with Winston logger
43
- - Automatic mapping of Winston log levels to MCP log levels
44
- - Support for structured logging data
45
- - Configurable logger name
46
- - Connection-aware logging (only sends logs when clients are connected)
47
- - Sensitive data sanitization
37
+ 3. Enjoy it!
48
38
 
49
39
  ## Usage
50
40
 
51
41
  You can run the server directly using `npx`:
52
42
 
53
43
  ```bash
54
- # Run with HTTP/SSE transport (default)
44
+ # Basic usage (starts server with SSE transport)
55
45
  npx -y @1mcp/agent
56
46
 
57
- # Run with stdio transport
47
+ # Use existing Claude Desktop config
48
+ npx -y @1mcp/agent --config ~/Library/Application\ Support/Claude/claude_desktop_config.json
49
+
50
+ # Use stdio transport instead of SSE
58
51
  npx -y @1mcp/agent --transport stdio
59
52
 
60
- # Show help
53
+ # Show all available options
61
54
  npx -y @1mcp/agent --help
62
55
  ```
63
56
 
64
57
  Available options:
65
- - `--transport, -t`: Transport type to use (choices: "stdio", "http", default: "http")
58
+ - `--transport, -t`: Choose transport type ("stdio" or "sse", default: "sse")
59
+ - `--config, -c`: Use a specific config file
60
+ - `--port, -P`: Change SSE port (default: 3050)
61
+ - `--host, -H`: Change SSE host (default: localhost)
62
+ - `--tags, -g`: Filter servers by tags (see Tags section below)
66
63
  - `--help, -h`: Show help
67
64
 
65
+ ### Understanding Tags
66
+
67
+ Tags help you control which MCP servers are available to different clients. Think of tags as labels that describe what each server can do.
68
+
69
+ #### How to Use Tags
70
+
71
+ 1. **In your server config**: Add tags to each server to describe its capabilities
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "web-server": {
76
+ "command": "uvx",
77
+ "args": ["mcp-server-fetch"],
78
+ "tags": ["network", "web"],
79
+ "disabled": false
80
+ },
81
+ "file-server": {
82
+ "command": "npx",
83
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "~/Downloads"],
84
+ "tags": ["filesystem"],
85
+ "disabled": false
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ 2. **When starting 1MCP in stdio mode**: You can filter servers by tags
92
+ ```bash
93
+ # Only start servers with the "network" tag
94
+ npx -y @1mcp/agent --transport stdio --tags "network"
95
+
96
+ # Start servers with either "network" or "filesystem" tags
97
+ npx -y @1mcp/agent --transport stdio --tags "network,filesystem"
98
+ ```
99
+
100
+ 3. **When using SSE transport**: Clients can request servers with specific tags
101
+ ```json
102
+ {
103
+ "mcpServers": {
104
+ "1mcp": {
105
+ "type": "http",
106
+ "url": "http://localhost:3050/sse?tags=network" // Only connect to network-capable servers
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ Example tags:
113
+ - `network`: For servers that make web requests
114
+ - `filesystem`: For servers that handle file operations
115
+ - `memory`: For servers that provide memory/storage
116
+ - `shell`: For servers that run shell commands
117
+ - `db`: For servers that handle database operations
118
+
119
+ ## Configuration
120
+
121
+ ### Global Configuration
122
+
123
+ The server automatically manages configuration in a global location:
124
+
125
+ - macOS/Linux: `~/.config/1mcp/mcp.json`
126
+ - Windows: `%APPDATA%/1mcp/mcp.json`
127
+
128
+ ### Configuration File Format
129
+
130
+ ```json
131
+ {
132
+ "mcpServers": {
133
+ "mcp-server-fetch": {
134
+ "command": "uvx",
135
+ "args": [
136
+ "mcp-server-fetch"
137
+ ],
138
+ "disabled": false
139
+ },
140
+ "server-memory": {
141
+ "command": "npx",
142
+ "args": [
143
+ "-y",
144
+ "@modelcontextprotocol/server-memory"
145
+ ],
146
+ "disabled": false
147
+ }
148
+ }
149
+ }
150
+ ```
151
+
152
+ ## How It Works
153
+
154
+ ### System Architecture
155
+
156
+ ```mermaid
157
+ graph TB
158
+ subgraph "AI Assistants"
159
+ A1[Claude Desktop]
160
+ A2[Cursor]
161
+ A3[Cherry Studio]
162
+ A4[Roo Code]
163
+ end
164
+
165
+ subgraph "1MCP Server"
166
+ MCP[1MCP Agent]
167
+ end
168
+
169
+ subgraph "MCP Servers"
170
+ S1[Server 1]
171
+ S2[Server 2]
172
+ S3[Server 3]
173
+ end
174
+
175
+ A1 -->|sse| MCP
176
+ A2 -->|sse| MCP
177
+ A3 -->|sse| MCP
178
+ A4 -->|sse| MCP
179
+
180
+ MCP --> |sse| S1
181
+ MCP --> |stdio| S2
182
+ MCP --> |stdio| S3
183
+ ```
184
+
185
+ ### Request Flow
186
+
187
+ ```mermaid
188
+ sequenceDiagram
189
+ participant Client as AI Assistant
190
+ participant 1MCP as 1MCP Server
191
+ participant MCP as MCP Servers
192
+
193
+ Client->>1MCP: Send MCP Request
194
+ activate 1MCP
195
+
196
+ 1MCP->>1MCP: Validate Request
197
+ 1MCP->>1MCP: Load Config
198
+ 1MCP->>MCP: Forward Request
199
+ activate MCP
200
+
201
+ MCP-->>1MCP: Response
202
+ deactivate MCP
203
+
204
+ 1MCP-->>Client: Forward Response
205
+ deactivate 1MCP
206
+ ```
207
+
68
208
  ## Development
69
209
 
70
210
  Install dependencies:
@@ -87,23 +227,6 @@ Run the server:
87
227
  pnpm dev
88
228
  ```
89
229
 
90
- ## Installation
91
-
92
- To use with Claude Desktop, add the server config:
93
-
94
- On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
95
- On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
96
-
97
- ```json
98
- {
99
- "mcpServers": {
100
- "agent": {
101
- "command": "/path/to/agent/build/index.js"
102
- }
103
- }
104
- }
105
- ```
106
-
107
230
  ### Debugging
108
231
 
109
232
  Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script:
@@ -1,23 +1,21 @@
1
- import logger from '../logger.js';
1
+ import logger from '../logger/logger.js';
2
2
  import { setupClientToServerNotifications, setupServerToClientNotifications, } from '../handlers/notificationHandlers.js';
3
3
  import { registerRequestHandlers } from '../handlers/requestHandlers.js';
4
4
  /**
5
5
  * Collects capabilities from all clients and registers them with the server
6
6
  * @param clients Record of client instances
7
7
  * @param server The MCP server instance
8
+ * @param tags Array of tags to filter clients by
8
9
  * @returns The combined server capabilities
9
10
  */
10
- export async function collectAndRegisterCapabilities(clients, server) {
11
+ export async function setupCapabilities(clients, serverInfo) {
11
12
  // Collect capabilities from all clients
12
13
  const capabilities = collectCapabilities(clients);
13
14
  // Set up notification handlers
14
- setupClientToServerNotifications(clients, server);
15
- setupServerToClientNotifications(clients, server);
16
- // Register capabilities with the server
17
- logger.info(`Registering capabilities: ${JSON.stringify(capabilities)}`);
18
- server.registerCapabilities(capabilities);
15
+ setupClientToServerNotifications(clients, serverInfo);
16
+ setupServerToClientNotifications(clients, serverInfo);
19
17
  // Register request handlers based on capabilities
20
- registerRequestHandlers(clients, server, capabilities);
18
+ registerRequestHandlers(clients, serverInfo, capabilities);
21
19
  return capabilities;
22
20
  }
23
21
  /**
@@ -27,9 +25,9 @@ export async function collectAndRegisterCapabilities(clients, server) {
27
25
  */
28
26
  function collectCapabilities(clients) {
29
27
  let capabilities = {};
30
- for (const [name, client] of Object.entries(clients)) {
28
+ for (const [name, clientInfo] of Object.entries(clients)) {
31
29
  try {
32
- const clientCapabilities = client.getServerCapabilities() || {};
30
+ const clientCapabilities = clientInfo.client.getServerCapabilities() || {};
33
31
  logger.debug(`Capabilities from ${name}: ${JSON.stringify(clientCapabilities)}`);
34
32
  capabilities = { ...capabilities, ...clientCapabilities };
35
33
  }
package/build/client.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
- export default async function createClient(transport) {
2
+ import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from './constants.js';
3
+ export default async function createClient() {
3
4
  const client = new Client({
4
- name: '1mcp',
5
- version: '1.0.0',
5
+ name: MCP_SERVER_NAME,
6
+ version: MCP_SERVER_VERSION,
6
7
  }, {
7
8
  capabilities: {
8
9
  experimental: {},
@@ -1,7 +1,9 @@
1
1
  import createClient from '../client.js';
2
- import logger from '../logger.js';
3
- import { CONNECTION_RETRY } from '../constants.js';
4
- import { ClientConnectionError, ClientNotFoundError, withErrorHandling } from '../utils/errorHandling.js';
2
+ import logger from '../logger/logger.js';
3
+ import { CONNECTION_RETRY, MCP_SERVER_NAME } from '../constants.js';
4
+ import { withErrorHandling } from '../utils/errorHandling.js';
5
+ import { ClientConnectionError, ClientNotFoundError } from '../utils/errorTypes.js';
6
+ import { ClientStatus } from '../types.js';
5
7
  /**
6
8
  * Creates client instances for all transports with retry logic
7
9
  * @param transports Record of transport instances
@@ -12,18 +14,30 @@ export async function createClients(transports) {
12
14
  for (const [name, transport] of Object.entries(transports)) {
13
15
  logger.info(`Creating client for ${name}`);
14
16
  try {
15
- const client = await createClient(transport);
17
+ const client = await createClient();
16
18
  // Connect with retry logic
17
19
  await connectWithRetry(client, transport, name);
18
- clients[name] = client;
20
+ clients[name] = {
21
+ name,
22
+ transport,
23
+ client,
24
+ status: ClientStatus.Connected,
25
+ lastConnected: new Date(),
26
+ };
19
27
  logger.info(`Client created for ${name}`);
20
28
  }
21
29
  catch (error) {
22
30
  logger.error(`Failed to create client for ${name}: ${error}`);
23
- // We continue with other clients even if one fails
31
+ clients[name] = {
32
+ name,
33
+ transport,
34
+ client: await createClient(),
35
+ status: ClientStatus.Error,
36
+ lastError: error instanceof Error ? error : new Error(String(error)),
37
+ };
24
38
  }
25
39
  }
26
- return clients;
40
+ return Object.freeze(clients);
27
41
  }
28
42
  /**
29
43
  * Connects a client to its transport with retry logic
@@ -36,7 +50,11 @@ async function connectWithRetry(client, transport, name) {
36
50
  for (let i = 0; i < CONNECTION_RETRY.MAX_ATTEMPTS; i++) {
37
51
  try {
38
52
  await client.connect(transport);
39
- logger.info(`Successfully connected to ${name}`);
53
+ const sv = await client.getServerVersion();
54
+ if (sv?.name === MCP_SERVER_NAME) {
55
+ throw new ClientConnectionError(name, new Error('Aborted to prevent circular dependency'));
56
+ }
57
+ logger.info(`Successfully connected to ${name} with server ${sv?.name} version ${sv?.version}`);
40
58
  return;
41
59
  }
42
60
  catch (error) {
@@ -67,13 +85,30 @@ export function getClient(clients, clientName) {
67
85
  return client;
68
86
  }
69
87
  /**
70
- * Executes a client operation with error handling
88
+ * Executes a client operation with error handling and timeout
71
89
  * @param clients Record of client instances
72
90
  * @param clientName The name of the client to use
73
91
  * @param operation The operation to execute
92
+ * @param options Operation options including timeout and retry settings
74
93
  * @returns The result of the operation
75
94
  */
76
- export async function executeClientOperation(clients, clientName, operation) {
77
- const client = getClient(clients, clientName);
78
- return withErrorHandling(async () => operation(client), `Error executing operation on client ${clientName}`)();
95
+ export async function executeClientOperation(clients, clientName, operation, options = {}) {
96
+ const clientInfo = getClient(clients, clientName);
97
+ const { retryCount = 0, retryDelay = 1000 } = options;
98
+ return withErrorHandling(async () => {
99
+ let lastError;
100
+ for (let i = 0; i <= retryCount; i++) {
101
+ try {
102
+ return await operation(clientInfo);
103
+ }
104
+ catch (error) {
105
+ lastError = error instanceof Error ? error : new Error(String(error));
106
+ if (i < retryCount) {
107
+ logger.info(`Retrying operation ${operation.name} on client ${clientName} after ${retryDelay}ms`);
108
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
109
+ }
110
+ }
111
+ }
112
+ throw lastError;
113
+ }, `Error executing operation on client ${clientName}`)();
79
114
  }
@@ -1,8 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { EventEmitter } from 'events';
4
- import { MCP_CONFIG_FILE } from '../constants.js';
5
- import logger from '../logger.js';
4
+ import { getGlobalConfigPath, getGlobalConfigDir, DEFAULT_CONFIG } from '../constants.js';
5
+ import logger from '../logger/logger.js';
6
6
  /**
7
7
  * Configuration change event types
8
8
  */
@@ -20,22 +20,44 @@ export class ConfigManager extends EventEmitter {
20
20
  configFilePath;
21
21
  /**
22
22
  * Private constructor to enforce singleton pattern
23
+ * @param configFilePath - Optional path to the config file. If not provided, uses global config path
23
24
  */
24
- constructor() {
25
+ constructor(configFilePath) {
25
26
  super();
26
- this.configFilePath = path.resolve(process.cwd(), MCP_CONFIG_FILE);
27
+ this.configFilePath = configFilePath || getGlobalConfigPath();
28
+ this.ensureConfigExists();
27
29
  this.loadConfig();
28
30
  }
29
31
  /**
30
- * Get the singleton instance of the ConfigManager
31
- * @returns The ConfigManager instance
32
+ * Get the singleton instance of ConfigManager
33
+ * @param configFilePath - Optional path to the config file
32
34
  */
33
- static getInstance() {
35
+ static getInstance(configFilePath) {
34
36
  if (!ConfigManager.instance) {
35
- ConfigManager.instance = new ConfigManager();
37
+ ConfigManager.instance = new ConfigManager(configFilePath);
36
38
  }
37
39
  return ConfigManager.instance;
38
40
  }
41
+ /**
42
+ * Ensure the config directory and file exist
43
+ */
44
+ ensureConfigExists() {
45
+ try {
46
+ const configDir = getGlobalConfigDir();
47
+ if (!fs.existsSync(configDir)) {
48
+ fs.mkdirSync(configDir, { recursive: true });
49
+ logger.info(`Created config directory: ${configDir}`);
50
+ }
51
+ if (!fs.existsSync(this.configFilePath)) {
52
+ fs.writeFileSync(this.configFilePath, JSON.stringify(DEFAULT_CONFIG, null, 2));
53
+ logger.info(`Created default config file: ${this.configFilePath}`);
54
+ }
55
+ }
56
+ catch (error) {
57
+ logger.error(`Failed to ensure config exists: ${error}`);
58
+ throw error;
59
+ }
60
+ }
39
61
  /**
40
62
  * Load the configuration from the config file
41
63
  */
@@ -105,4 +127,4 @@ export class ConfigManager extends EventEmitter {
105
127
  return { ...this.transportConfig };
106
128
  }
107
129
  }
108
- export default ConfigManager.getInstance();
130
+ export default ConfigManager;
@@ -0,0 +1,106 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ // Mock modules before importing ConfigManager
4
+ jest.mock('fs', () => ({
5
+ existsSync: jest.fn(),
6
+ mkdirSync: jest.fn(),
7
+ writeFileSync: jest.fn(),
8
+ readFileSync: jest.fn(),
9
+ watch: jest.fn(() => ({ close: jest.fn() })),
10
+ }));
11
+ const mockLogger = {
12
+ error: jest.fn(),
13
+ warn: jest.fn(),
14
+ info: jest.fn(),
15
+ debug: jest.fn(),
16
+ level: 'info',
17
+ transports: [],
18
+ };
19
+ jest.doMock('../logger/logger.js', () => ({
20
+ __esModule: true,
21
+ default: mockLogger,
22
+ }));
23
+ // Mock constants
24
+ jest.doMock('../constants.js', () => ({
25
+ __esModule: true,
26
+ DEFAULT_CONFIG: { mcpServers: {} },
27
+ getGlobalConfigPath: jest.fn(),
28
+ getGlobalConfigDir: jest.fn().mockReturnValue('/test'),
29
+ }));
30
+ // Import after mocks are set up
31
+ import { ConfigManager, ConfigChangeEvent } from './configManager.js';
32
+ import { DEFAULT_CONFIG } from '../constants.js';
33
+ describe('ConfigManager', () => {
34
+ const testConfigPath = '/test/config.json';
35
+ beforeEach(() => {
36
+ // Reset all mocks
37
+ jest.clearAllMocks();
38
+ // Reset singleton instance
39
+ ConfigManager.instance = undefined;
40
+ // Default mock implementations
41
+ fs.existsSync.mockReturnValue(true);
42
+ fs.readFileSync.mockReturnValue(JSON.stringify({ mcpServers: {} }));
43
+ });
44
+ describe('getInstance', () => {
45
+ it('should create singleton instance', () => {
46
+ const instance1 = ConfigManager.getInstance(testConfigPath);
47
+ const instance2 = ConfigManager.getInstance(testConfigPath);
48
+ expect(instance1).toBe(instance2);
49
+ });
50
+ it('should use provided config path', () => {
51
+ const instance = ConfigManager.getInstance(testConfigPath);
52
+ expect(instance.configFilePath).toBe(testConfigPath);
53
+ });
54
+ });
55
+ describe('ensureConfigExists', () => {
56
+ it('should create config directory and file if they do not exist', () => {
57
+ fs.existsSync
58
+ .mockReturnValueOnce(false) // Directory doesn't exist
59
+ .mockReturnValueOnce(false); // File doesn't exist
60
+ ConfigManager.getInstance(testConfigPath);
61
+ expect(fs.mkdirSync).toHaveBeenCalledWith(path.dirname(testConfigPath), { recursive: true });
62
+ expect(fs.writeFileSync).toHaveBeenCalledWith(testConfigPath, JSON.stringify(DEFAULT_CONFIG, null, 2));
63
+ });
64
+ });
65
+ describe('config watching', () => {
66
+ it('should start and stop watching config file', () => {
67
+ const instance = ConfigManager.getInstance(testConfigPath);
68
+ instance.startWatching();
69
+ expect(fs.watch).toHaveBeenCalledWith(testConfigPath, expect.any(Function));
70
+ instance.stopWatching();
71
+ expect(instance['configWatcher']).toBeNull();
72
+ });
73
+ it('should reload config on file change', () => {
74
+ const instance = ConfigManager.getInstance(testConfigPath);
75
+ const mockWatcher = { close: jest.fn() };
76
+ let watchCallback = () => { };
77
+ fs.watch.mockImplementation((path, callback) => {
78
+ watchCallback = callback;
79
+ return mockWatcher;
80
+ });
81
+ instance.startWatching();
82
+ // Setup spy for event emission
83
+ const emitSpy = jest.spyOn(instance, 'emit');
84
+ // Mock new config data
85
+ const newConfig = { mcpServers: { test: { url: 'test-url' } } };
86
+ fs.readFileSync.mockReturnValueOnce(JSON.stringify(newConfig));
87
+ // Simulate file change
88
+ watchCallback('change', path.basename(testConfigPath));
89
+ expect(emitSpy).toHaveBeenCalledWith(ConfigChangeEvent.TRANSPORT_CONFIG_CHANGED, newConfig.mcpServers);
90
+ });
91
+ });
92
+ describe('getTransportConfig', () => {
93
+ it('should return copy of transport config', () => {
94
+ const testConfig = {
95
+ mcpServers: {
96
+ test: { url: 'test-url' },
97
+ },
98
+ };
99
+ fs.readFileSync.mockReturnValueOnce(JSON.stringify(testConfig));
100
+ const instance = ConfigManager.getInstance(testConfigPath);
101
+ const config = instance.getTransportConfig();
102
+ expect(config).toEqual(testConfig.mcpServers);
103
+ expect(config).not.toBe(instance['transportConfig']); // Should be a copy
104
+ });
105
+ });
106
+ });
@@ -1,38 +1,53 @@
1
1
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
2
- import logger from '../logger.js';
3
- import configManager from './configManager.js';
4
- /**
5
- * Loads transport configurations from the MCP config file
6
- * @returns Record of transport configurations
7
- */
8
- export function loadTransportConfig() {
9
- return configManager.getTransportConfig();
10
- }
2
+ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
3
+ import { ZodError } from 'zod';
4
+ import logger from '../logger/logger.js';
5
+ import { transportConfigSchema } from '../types.js';
11
6
  /**
12
7
  * Creates transport instances from configuration
13
8
  * @returns Record of transport instances
14
9
  */
15
- export function createTransports() {
10
+ export function createTransports(config) {
16
11
  const transports = {};
17
- const mcpConfig = loadTransportConfig();
18
- for (const [name, transport] of Object.entries(mcpConfig)) {
19
- if (transport.disabled) {
12
+ for (const [name, params] of Object.entries(config)) {
13
+ if (params.disabled) {
20
14
  logger.debug(`Skipping disabled transport: ${name}`);
21
15
  continue;
22
16
  }
23
17
  try {
24
- // Merge environment variables
25
- transport.env = {
26
- ...Object.fromEntries(Object.entries(process.env)
27
- .filter(([_, v]) => v !== undefined)
28
- .map(([k, v]) => [k, String(v)])),
29
- ...transport.env,
30
- };
31
- transports[name] = new StdioClientTransport(transport);
32
- logger.debug(`Created transport for ${name}`);
18
+ const validatedTransport = transportConfigSchema.parse(params);
19
+ if (validatedTransport.type === 'sse' || validatedTransport.type === 'http') {
20
+ if (!validatedTransport.url) {
21
+ throw new Error(`URL is required for SSE transport: ${name}`);
22
+ }
23
+ const transport = new SSEClientTransport(new URL(validatedTransport.url), {
24
+ requestInit: {
25
+ headers: validatedTransport.headers,
26
+ },
27
+ });
28
+ transport.timeout = validatedTransport.timeout;
29
+ transport.tags = validatedTransport.tags;
30
+ transports[name] = transport;
31
+ }
32
+ else {
33
+ if (!validatedTransport.command) {
34
+ throw new Error(`Command is required for stdio transport: ${name}`);
35
+ }
36
+ const transport = new StdioClientTransport(validatedTransport);
37
+ transport.timeout = validatedTransport.timeout;
38
+ transport.tags = validatedTransport.tags;
39
+ transports[name] = transport;
40
+ }
41
+ logger.debug(`Created transport: ${name}`);
33
42
  }
34
43
  catch (error) {
35
- logger.error(`Failed to create transport for ${name}: ${error}`);
44
+ if (error instanceof ZodError) {
45
+ logger.error(`Invalid transport configuration for ${name}:`, error.errors);
46
+ }
47
+ else {
48
+ logger.error(`Error creating transport ${name}:`, error);
49
+ }
50
+ throw error;
36
51
  }
37
52
  }
38
53
  return transports;