@1mcp/agent 0.2.0 → 0.7.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
@@ -28,7 +28,7 @@ npx -y @1mcp/agent --config ~/Library/Application\ Support/Claude/claude_desktop
28
28
  "mcpServers": {
29
29
  "1mcp": {
30
30
  "type": "http",
31
- "url": "http://localhost:3050"
31
+ "url": "http://localhost:3050/sse"
32
32
  }
33
33
  }
34
34
  }
@@ -41,24 +41,81 @@ npx -y @1mcp/agent --config ~/Library/Application\ Support/Claude/claude_desktop
41
41
  You can run the server directly using `npx`:
42
42
 
43
43
  ```bash
44
- # Run with HTTP/SSE transport (default)
44
+ # Basic usage (starts server with SSE transport)
45
45
  npx -y @1mcp/agent
46
46
 
47
- # Run with stdio transport
48
- npx -y @1mcp/agent --transport stdio
47
+ # Use existing Claude Desktop config
48
+ npx -y @1mcp/agent --config ~/Library/Application\ Support/Claude/claude_desktop_config.json
49
49
 
50
- # Run with AI assistant config file
51
- npx -y @1mcp/agent --config /path/to/ai/assistant/config.json
50
+ # Use stdio transport instead of SSE
51
+ npx -y @1mcp/agent --transport stdio
52
52
 
53
- # Show help
53
+ # Show all available options
54
54
  npx -y @1mcp/agent --help
55
55
  ```
56
56
 
57
57
  Available options:
58
- - `--transport, -t`: Transport type to use (choices: "stdio", "sse", default: "sse")
59
- - `--config, -c`: Path to the configuration file
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)
60
63
  - `--help, -h`: Show help
61
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
+
62
119
  ## Configuration
63
120
 
64
121
  ### Global Configuration
@@ -172,10 +229,10 @@ pnpm dev
172
229
 
173
230
  ### Debugging
174
231
 
175
- 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:
232
+ Using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script:
176
233
 
177
234
  ```bash
178
- pnpm run inspector
235
+ pnpm inspector
179
236
  ```
180
237
 
181
238
  The Inspector will provide a URL to access debugging tools in your browser.
@@ -5,16 +5,17 @@ import { registerRequestHandlers } from '../handlers/requestHandlers.js';
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 setupCapabilities(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);
15
+ setupClientToServerNotifications(clients, serverInfo);
16
+ setupServerToClientNotifications(clients, serverInfo);
16
17
  // Register request handlers based on capabilities
17
- registerRequestHandlers(clients, server, capabilities);
18
+ registerRequestHandlers(clients, serverInfo);
18
19
  return capabilities;
19
20
  }
20
21
  /**
@@ -23,12 +24,19 @@ export async function setupCapabilities(clients, server) {
23
24
  * @returns The combined server capabilities
24
25
  */
25
26
  function collectCapabilities(clients) {
26
- let capabilities = {};
27
- for (const [name, client] of Object.entries(clients)) {
27
+ const capabilities = {};
28
+ for (const [name, clientInfo] of Object.entries(clients)) {
28
29
  try {
29
- const clientCapabilities = client.getServerCapabilities() || {};
30
- logger.debug(`Capabilities from ${name}: ${JSON.stringify(clientCapabilities)}`);
31
- capabilities = { ...capabilities, ...clientCapabilities };
30
+ const serverCapabilities = clientInfo.client.getServerCapabilities() || {};
31
+ logger.debug(`Capabilities from ${name}: ${JSON.stringify(serverCapabilities)}`);
32
+ // Store capabilities per client
33
+ clientInfo.capabilities = serverCapabilities;
34
+ // Aggregate capabilities
35
+ capabilities.resources = { ...capabilities.resources, ...serverCapabilities.resources };
36
+ capabilities.tools = { ...capabilities.tools, ...serverCapabilities.tools };
37
+ capabilities.prompts = { ...capabilities.prompts, ...serverCapabilities.prompts };
38
+ capabilities.experimental = { ...capabilities.experimental, ...serverCapabilities.experimental };
39
+ capabilities.logging = { ...capabilities.logging, ...serverCapabilities.logging };
32
40
  }
33
41
  catch (error) {
34
42
  logger.error(`Failed to get capabilities from ${name}: ${error}`);
@@ -1,7 +1,8 @@
1
1
  import createClient from '../client.js';
2
2
  import logger from '../logger/logger.js';
3
- import { CONNECTION_RETRY, MCP_SERVER_NAME } from '../constants.js';
4
- import { ClientConnectionError, ClientNotFoundError, withErrorHandling } from '../utils/errorHandling.js';
3
+ import { CONNECTION_RETRY, MCP_SERVER_NAME, ERROR_CODES } from '../constants.js';
4
+ import { ClientConnectionError, ClientNotFoundError, MCPError } from '../utils/errorTypes.js';
5
+ import { ClientStatus } from '../types.js';
5
6
  /**
6
7
  * Creates client instances for all transports with retry logic
7
8
  * @param transports Record of transport instances
@@ -15,15 +16,27 @@ export async function createClients(transports) {
15
16
  const client = await createClient();
16
17
  // Connect with retry logic
17
18
  await connectWithRetry(client, transport, name);
18
- clients[name] = client;
19
+ clients[name] = {
20
+ name,
21
+ transport,
22
+ client,
23
+ status: ClientStatus.Connected,
24
+ lastConnected: new Date(),
25
+ };
19
26
  logger.info(`Client created for ${name}`);
20
27
  }
21
28
  catch (error) {
22
29
  logger.error(`Failed to create client for ${name}: ${error}`);
23
- // We continue with other clients even if one fails
30
+ clients[name] = {
31
+ name,
32
+ transport,
33
+ client: await createClient(),
34
+ status: ClientStatus.Error,
35
+ lastError: error instanceof Error ? error : new Error(String(error)),
36
+ };
24
37
  }
25
38
  }
26
- return clients;
39
+ return Object.freeze(clients);
27
40
  }
28
41
  /**
29
42
  * Connects a client to its transport with retry logic
@@ -71,13 +84,54 @@ export function getClient(clients, clientName) {
71
84
  return client;
72
85
  }
73
86
  /**
74
- * Executes a client operation with error handling
87
+ * Executes an operation with error handling and retry logic
88
+ * @param operation The operation to execute
89
+ * @param context The execution context (client info or server info)
90
+ * @param options Operation options including timeout and retry settings
91
+ * @returns The result of the operation
92
+ */
93
+ export async function executeOperation(operation, contextName, options = {}) {
94
+ const { retryCount = 0, retryDelay = 1000 } = options;
95
+ let lastError;
96
+ for (let i = 0; i <= retryCount; i++) {
97
+ try {
98
+ return await operation();
99
+ }
100
+ catch (error) {
101
+ lastError = error instanceof Error ? error : new Error(String(error));
102
+ if (i < retryCount) {
103
+ logger.info(`Retrying operation ${operation.name} on ${contextName} after ${retryDelay}ms`);
104
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
105
+ }
106
+ }
107
+ }
108
+ // If we get here, we've exhausted all retries
109
+ logger.error(`Operation failed on ${contextName} after ${retryCount + 1} attempts: ${lastError}`);
110
+ if (lastError instanceof MCPError) {
111
+ throw lastError;
112
+ }
113
+ const mcpError = new MCPError(`Error executing operation on ${contextName}`, ERROR_CODES.INTERNAL_SERVER_ERROR, {
114
+ originalError: lastError,
115
+ });
116
+ throw mcpError;
117
+ }
118
+ /**
119
+ * Executes a client operation with error handling and retry logic
75
120
  * @param clients Record of client instances
76
121
  * @param clientName The name of the client to use
77
122
  * @param operation The operation to execute
78
- * @returns The result of the operation
123
+ * @param options Operation options including timeout and retry settings
124
+ */
125
+ export async function executeClientOperation(clients, clientName, operation, options = {}) {
126
+ const clientInfo = getClient(clients, clientName);
127
+ return executeOperation(() => operation(clientInfo), `client ${clientName}`, options);
128
+ }
129
+ /**
130
+ * Executes a server operation with error handling and retry logic
131
+ * @param server The server to execute the operation on
132
+ * @param operation The operation to execute
133
+ * @param options Operation options including timeout and retry settings
79
134
  */
80
- export async function executeClientOperation(clients, clientName, operation) {
81
- const client = getClient(clients, clientName);
82
- return withErrorHandling(async () => operation(client), `Error executing operation on client ${clientName}`)();
135
+ export async function executeServerOperation(server, operation, options = {}) {
136
+ return executeOperation(() => operation(server), 'server', options);
83
137
  }
@@ -0,0 +1,172 @@
1
+ import { createClients, getClient, executeOperation, executeClientOperation, executeServerOperation, } from './clientManager.js';
2
+ import createClientFn from '../client.js';
3
+ import logger from '../logger/logger.js';
4
+ import { ClientStatus } from '../types.js';
5
+ import { ClientConnectionError, ClientNotFoundError, MCPError } from '../utils/errorTypes.js';
6
+ import { MCP_SERVER_NAME, CONNECTION_RETRY } from '../constants.js';
7
+ // Mock dependencies
8
+ jest.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
9
+ Client: jest.fn(),
10
+ }));
11
+ jest.mock('../client.js', () => ({
12
+ __esModule: true,
13
+ default: jest.fn(),
14
+ }));
15
+ jest.mock('../logger/logger.js', () => ({
16
+ __esModule: true,
17
+ default: {
18
+ info: jest.fn(),
19
+ error: jest.fn(),
20
+ },
21
+ }));
22
+ describe('clientManager', () => {
23
+ let mockTransport;
24
+ let mockClient;
25
+ let mockTransports;
26
+ beforeEach(() => {
27
+ jest.clearAllMocks();
28
+ jest.useFakeTimers();
29
+ mockTransport = {
30
+ name: 'test-transport',
31
+ start: jest.fn(),
32
+ send: jest.fn(),
33
+ close: jest.fn(),
34
+ };
35
+ mockClient = {
36
+ connect: jest.fn(),
37
+ getServerVersion: jest.fn(),
38
+ };
39
+ mockTransports = {
40
+ 'test-client': mockTransport,
41
+ };
42
+ createClientFn.mockResolvedValue(mockClient);
43
+ });
44
+ afterEach(() => {
45
+ jest.useRealTimers();
46
+ });
47
+ describe('createClients', () => {
48
+ it('should create clients successfully', async () => {
49
+ mockClient.connect.mockResolvedValue(undefined);
50
+ mockClient.getServerVersion.mockResolvedValue({ name: 'test-server', version: '1.0.0' });
51
+ const clientsPromise = createClients(mockTransports);
52
+ await jest.runAllTimersAsync();
53
+ const clients = await clientsPromise;
54
+ expect(clients['test-client']).toBeDefined();
55
+ expect(clients['test-client'].status).toBe(ClientStatus.Connected);
56
+ expect(clients['test-client'].transport).toBe(mockTransport);
57
+ expect(logger.info).toHaveBeenCalledWith('Client created for test-client');
58
+ });
59
+ it('should handle client connection failure after retries', async () => {
60
+ const error = new Error('Connection failed');
61
+ mockClient.connect.mockRejectedValue(error);
62
+ const clientsPromise = createClients(mockTransports);
63
+ // Run through all retry attempts
64
+ for (let i = 0; i < CONNECTION_RETRY.MAX_ATTEMPTS; i++) {
65
+ await jest.advanceTimersByTimeAsync(CONNECTION_RETRY.INITIAL_DELAY_MS * Math.pow(2, i));
66
+ }
67
+ const clients = await clientsPromise;
68
+ expect(clients['test-client'].status).toBe(ClientStatus.Error);
69
+ expect(clients['test-client'].lastError).toBeInstanceOf(ClientConnectionError);
70
+ expect(clients['test-client'].lastError?.message).toContain('Connection failed');
71
+ expect(mockClient.connect).toHaveBeenCalledTimes(CONNECTION_RETRY.MAX_ATTEMPTS);
72
+ });
73
+ it('should prevent circular dependency with MCP server', async () => {
74
+ mockClient.connect.mockResolvedValue(undefined);
75
+ mockClient.getServerVersion.mockResolvedValue({ name: MCP_SERVER_NAME, version: '1.0.0' });
76
+ const clientsPromise = createClients(mockTransports);
77
+ await jest.runAllTimersAsync();
78
+ const clients = await clientsPromise;
79
+ expect(clients['test-client'].status).toBe(ClientStatus.Error);
80
+ expect(clients['test-client'].lastError).toBeInstanceOf(ClientConnectionError);
81
+ expect(clients['test-client'].lastError?.message).toContain('circular dependency');
82
+ });
83
+ });
84
+ describe('getClient', () => {
85
+ let clients;
86
+ beforeEach(async () => {
87
+ clients = await createClients(mockTransports);
88
+ });
89
+ it('should return client info for existing client', () => {
90
+ const clientInfo = getClient(clients, 'test-client');
91
+ expect(clientInfo).toBeDefined();
92
+ expect(clientInfo.name).toBe('test-client');
93
+ });
94
+ it('should throw ClientNotFoundError for non-existent client', () => {
95
+ expect(() => getClient(clients, 'non-existent')).toThrow(ClientNotFoundError);
96
+ });
97
+ });
98
+ describe('executeOperation', () => {
99
+ it('should execute operation successfully', async () => {
100
+ const operation = jest.fn().mockResolvedValue('result');
101
+ const result = await executeOperation(operation, 'test-context');
102
+ expect(result).toBe('result');
103
+ expect(operation).toHaveBeenCalled();
104
+ });
105
+ it('should retry failed operations', async () => {
106
+ const error = new Error('Operation failed');
107
+ const operation = jest.fn().mockRejectedValueOnce(error).mockResolvedValueOnce('result');
108
+ const operationPromise = executeOperation(operation, 'test-context', { retryCount: 1, retryDelay: 1000 });
109
+ // Advance timer by retry delay
110
+ await jest.advanceTimersByTimeAsync(1000);
111
+ const result = await operationPromise;
112
+ expect(result).toBe('result');
113
+ expect(operation).toHaveBeenCalledTimes(2);
114
+ expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Retrying operation'));
115
+ });
116
+ it('should throw error after max retries', async () => {
117
+ const error = new Error('Operation failed');
118
+ const operation = jest.fn().mockRejectedValue(error);
119
+ const operationPromise = executeOperation(operation, 'test-context', { retryCount: 2, retryDelay: 1000 });
120
+ // Create rejection assertion before advancing timers
121
+ const rejection = expect(operationPromise).rejects.toMatchObject({
122
+ message: 'Error executing operation on test-context',
123
+ data: { originalError: error },
124
+ });
125
+ // Advance timer for each retry
126
+ for (let i = 0; i < 2; i++) {
127
+ await jest.advanceTimersByTimeAsync(1000);
128
+ }
129
+ // Wait for the rejection assertion
130
+ await rejection;
131
+ expect(operation).toHaveBeenCalledTimes(3); // Initial try + 2 retries
132
+ expect(logger.error).toHaveBeenCalled();
133
+ });
134
+ });
135
+ describe('executeClientOperation', () => {
136
+ let clients;
137
+ beforeEach(async () => {
138
+ clients = await createClients(mockTransports);
139
+ });
140
+ it('should execute client operation successfully', async () => {
141
+ const operation = jest.fn().mockResolvedValue('result');
142
+ const result = await executeClientOperation(clients, 'test-client', operation);
143
+ expect(result).toBe('result');
144
+ expect(operation).toHaveBeenCalledWith(clients['test-client']);
145
+ });
146
+ it('should throw error for non-existent client', async () => {
147
+ const operation = jest.fn();
148
+ await expect(executeClientOperation(clients, 'non-existent', operation)).rejects.toThrow(ClientNotFoundError);
149
+ });
150
+ });
151
+ describe('executeServerOperation', () => {
152
+ let mockServer;
153
+ beforeEach(() => {
154
+ mockServer = {
155
+ server: {
156
+ request: jest.fn().mockResolvedValue('result'),
157
+ },
158
+ };
159
+ });
160
+ it('should execute server operation successfully', async () => {
161
+ const operation = jest.fn().mockResolvedValue('result');
162
+ const result = await executeServerOperation(mockServer, operation);
163
+ expect(result).toBe('result');
164
+ expect(operation).toHaveBeenCalledWith(mockServer);
165
+ });
166
+ it('should handle server operation failure', async () => {
167
+ const error = new Error('Server operation failed');
168
+ const operation = jest.fn().mockRejectedValue(error);
169
+ await expect(executeServerOperation(mockServer, operation)).rejects.toThrow(MCPError);
170
+ });
171
+ });
172
+ });
@@ -127,4 +127,4 @@ export class ConfigManager extends EventEmitter {
127
127
  return { ...this.transportConfig };
128
128
  }
129
129
  }
130
- export default ConfigManager.getInstance();
130
+ export default ConfigManager;
@@ -0,0 +1,92 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { ConfigManager, ConfigChangeEvent } from './configManager.js';
4
+ import { DEFAULT_CONFIG } from '../constants.js';
5
+ import { setupCommonJestMocks } from '../test-utils/mocks/commonMocks.js';
6
+ import { testConfig } from '../test-utils/fixtures/commonFixtures.js';
7
+ import { clearAllMocks, createEventEmitterSpy } from '../test-utils/helpers/testHelpers.js';
8
+ // Mock modules
9
+ jest.mock('fs', () => ({
10
+ existsSync: jest.fn(),
11
+ mkdirSync: jest.fn(),
12
+ writeFileSync: jest.fn(),
13
+ readFileSync: jest.fn(),
14
+ watch: jest.fn(() => ({ close: jest.fn() })),
15
+ }));
16
+ // Setup common mocks
17
+ setupCommonJestMocks();
18
+ // Mock constants
19
+ jest.mock('../constants.js', () => ({
20
+ __esModule: true,
21
+ DEFAULT_CONFIG: { mcpServers: {} },
22
+ getGlobalConfigPath: jest.fn(),
23
+ getGlobalConfigDir: jest.fn().mockReturnValue('/test'),
24
+ }));
25
+ describe('ConfigManager', () => {
26
+ const testConfigPath = '/test/config.json';
27
+ beforeEach(() => {
28
+ // Reset all mocks and timers
29
+ clearAllMocks();
30
+ // Reset singleton instance
31
+ ConfigManager.instance = undefined;
32
+ // Default mock implementations
33
+ fs.existsSync.mockReturnValue(true);
34
+ fs.readFileSync.mockReturnValue(JSON.stringify({ mcpServers: {} }));
35
+ });
36
+ describe('getInstance', () => {
37
+ it('should create singleton instance', () => {
38
+ const instance1 = ConfigManager.getInstance(testConfigPath);
39
+ const instance2 = ConfigManager.getInstance(testConfigPath);
40
+ expect(instance1).toBe(instance2);
41
+ });
42
+ it('should use provided config path', () => {
43
+ const instance = ConfigManager.getInstance(testConfigPath);
44
+ expect(instance.configFilePath).toBe(testConfigPath);
45
+ });
46
+ });
47
+ describe('ensureConfigExists', () => {
48
+ it('should create config directory and file if they do not exist', () => {
49
+ fs.existsSync
50
+ .mockReturnValueOnce(false) // Directory doesn't exist
51
+ .mockReturnValueOnce(false); // File doesn't exist
52
+ ConfigManager.getInstance(testConfigPath);
53
+ expect(fs.mkdirSync).toHaveBeenCalledWith(path.dirname(testConfigPath), { recursive: true });
54
+ expect(fs.writeFileSync).toHaveBeenCalledWith(testConfigPath, JSON.stringify(DEFAULT_CONFIG, null, 2));
55
+ });
56
+ });
57
+ describe('config watching', () => {
58
+ it('should start and stop watching config file', () => {
59
+ const instance = ConfigManager.getInstance(testConfigPath);
60
+ instance.startWatching();
61
+ expect(fs.watch).toHaveBeenCalledWith(testConfigPath, expect.any(Function));
62
+ instance.stopWatching();
63
+ expect(instance['configWatcher']).toBeNull();
64
+ });
65
+ it('should reload config on file change', () => {
66
+ const instance = ConfigManager.getInstance(testConfigPath);
67
+ const mockWatcher = { close: jest.fn() };
68
+ let watchCallback = () => { };
69
+ fs.watch.mockImplementation((path, callback) => {
70
+ watchCallback = callback;
71
+ return mockWatcher;
72
+ });
73
+ instance.startWatching();
74
+ // Setup spy for event emission
75
+ const emitSpy = createEventEmitterSpy(instance);
76
+ // Mock new config data
77
+ fs.readFileSync.mockReturnValueOnce(JSON.stringify(testConfig));
78
+ // Simulate file change
79
+ watchCallback('change', path.basename(testConfigPath));
80
+ expect(emitSpy).toHaveBeenCalledWith(ConfigChangeEvent.TRANSPORT_CONFIG_CHANGED, testConfig.mcpServers);
81
+ });
82
+ });
83
+ describe('getTransportConfig', () => {
84
+ it('should return copy of transport config', () => {
85
+ fs.readFileSync.mockReturnValueOnce(JSON.stringify(testConfig));
86
+ const instance = ConfigManager.getInstance(testConfigPath);
87
+ const config = instance.getTransportConfig();
88
+ expect(config).toEqual(testConfig.mcpServers);
89
+ expect(config).not.toBe(instance['transportConfig']); // Should be a copy
90
+ });
91
+ });
92
+ });
@@ -1,74 +1,53 @@
1
1
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
2
2
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
3
- import { z, ZodError } from 'zod';
3
+ import { ZodError } from 'zod';
4
4
  import logger from '../logger/logger.js';
5
- /**
6
- * Zod schema for transport configuration
7
- */
8
- export const transportConfigSchema = z.object({
9
- type: z.enum(['stdio', 'sse', 'http']).optional(),
10
- disabled: z.boolean().optional(),
11
- // SSEServerParameters fields
12
- url: z.string().url().optional(),
13
- headers: z.record(z.string()).optional(),
14
- // StdioServerParameters fields
15
- command: z.string().optional(),
16
- args: z.array(z.string()).optional(),
17
- stderr: z.union([z.string(), z.number()]).optional(),
18
- cwd: z.string().optional(),
19
- env: z.record(z.string()).optional(),
20
- });
5
+ import { transportConfigSchema } from '../types.js';
21
6
  /**
22
7
  * Creates transport instances from configuration
23
8
  * @returns Record of transport instances
24
9
  */
25
- export function createTransports(mcpConfig) {
10
+ export function createTransports(config) {
26
11
  const transports = {};
27
- for (const [name, transport] of Object.entries(mcpConfig)) {
28
- if (transport.disabled) {
12
+ for (const [name, params] of Object.entries(config)) {
13
+ if (params.disabled) {
29
14
  logger.debug(`Skipping disabled transport: ${name}`);
30
15
  continue;
31
16
  }
32
17
  try {
33
- // Validate transport configuration
34
- const validatedTransport = transportConfigSchema.parse(transport);
35
- // Merge environment variables
36
- validatedTransport.env = {
37
- ...Object.fromEntries(Object.entries(process.env)
38
- .filter(([_, v]) => v !== undefined)
39
- .map(([k, v]) => [k, String(v)])),
40
- ...validatedTransport.env,
41
- };
42
- // Create transport based on type
18
+ const validatedTransport = transportConfigSchema.parse(params);
43
19
  if (validatedTransport.type === 'sse' || validatedTransport.type === 'http') {
44
20
  if (!validatedTransport.url) {
45
21
  throw new Error(`URL is required for SSE transport: ${name}`);
46
22
  }
47
- transports[name] = new SSEClientTransport(new URL(validatedTransport.url), {
23
+ const transport = new SSEClientTransport(new URL(validatedTransport.url), {
48
24
  requestInit: {
49
25
  headers: validatedTransport.headers,
50
26
  },
51
27
  });
28
+ transport.timeout = validatedTransport.timeout;
29
+ transport.tags = validatedTransport.tags;
30
+ transports[name] = transport;
52
31
  }
53
32
  else {
54
- // For stdio transport, ensure required fields are present
55
33
  if (!validatedTransport.command) {
56
34
  throw new Error(`Command is required for stdio transport: ${name}`);
57
35
  }
58
- transports[name] = new StdioClientTransport(validatedTransport);
36
+ const transport = new StdioClientTransport(validatedTransport);
37
+ transport.timeout = validatedTransport.timeout;
38
+ transport.tags = validatedTransport.tags;
39
+ transports[name] = transport;
59
40
  }
60
- logger.debug(`Created ${validatedTransport.type} transport for ${name}`);
41
+ logger.debug(`Created transport: ${name}`);
61
42
  }
62
43
  catch (error) {
63
44
  if (error instanceof ZodError) {
64
45
  logger.error(`Invalid transport configuration for ${name}:`, error.errors);
65
46
  }
66
- else if (error instanceof Error) {
67
- logger.error(`Failed to create transport for ${name}: ${error.message}`);
68
- }
69
47
  else {
70
- logger.error(`Failed to create transport for ${name}: ${String(error)}`);
48
+ logger.error(`Error creating transport ${name}:`, error);
71
49
  }
50
+ throw error;
72
51
  }
73
52
  }
74
53
  return transports;
@@ -3,6 +3,7 @@
3
3
  */
4
4
  // Server configuration
5
5
  export const PORT = 3050;
6
+ export const HOST = '127.0.0.1';
6
7
  // API endpoints
7
8
  export const SSE_ENDPOINT = '/sse';
8
9
  export const MESSAGES_ENDPOINT = '/messages';
@@ -22,7 +23,7 @@ export const ERROR_CODES = {
22
23
  // MCP constants
23
24
  export const MCP_CONFIG_FILE = 'mcp.json';
24
25
  export const MCP_SERVER_NAME = '1mcp';
25
- export const MCP_SERVER_VERSION = '0.2.0';
26
+ export const MCP_SERVER_VERSION = '0.7.0';
26
27
  export const MCP_URI_SEPARATOR = '_1mcp_';
27
28
  // Global config paths
28
29
  export const CONFIG_DIR_NAME = '1mcp';