@1mcp/agent 0.4.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
@@ -229,10 +229,10 @@ pnpm dev
229
229
 
230
230
  ### Debugging
231
231
 
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:
232
+ Using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script:
233
233
 
234
234
  ```bash
235
- pnpm run inspector
235
+ pnpm inspector
236
236
  ```
237
237
 
238
238
  The Inspector will provide a URL to access debugging tools in your browser.
@@ -15,7 +15,7 @@ export async function setupCapabilities(clients, serverInfo) {
15
15
  setupClientToServerNotifications(clients, serverInfo);
16
16
  setupServerToClientNotifications(clients, serverInfo);
17
17
  // Register request handlers based on capabilities
18
- registerRequestHandlers(clients, serverInfo, capabilities);
18
+ registerRequestHandlers(clients, serverInfo);
19
19
  return capabilities;
20
20
  }
21
21
  /**
@@ -24,12 +24,19 @@ export async function setupCapabilities(clients, serverInfo) {
24
24
  * @returns The combined server capabilities
25
25
  */
26
26
  function collectCapabilities(clients) {
27
- let capabilities = {};
27
+ const capabilities = {};
28
28
  for (const [name, clientInfo] of Object.entries(clients)) {
29
29
  try {
30
- const clientCapabilities = clientInfo.client.getServerCapabilities() || {};
31
- logger.debug(`Capabilities from ${name}: ${JSON.stringify(clientCapabilities)}`);
32
- 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 };
33
40
  }
34
41
  catch (error) {
35
42
  logger.error(`Failed to get capabilities from ${name}: ${error}`);
@@ -1,8 +1,7 @@
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 { withErrorHandling } from '../utils/errorHandling.js';
5
- import { ClientConnectionError, ClientNotFoundError } from '../utils/errorTypes.js';
3
+ import { CONNECTION_RETRY, MCP_SERVER_NAME, ERROR_CODES } from '../constants.js';
4
+ import { ClientConnectionError, ClientNotFoundError, MCPError } from '../utils/errorTypes.js';
6
5
  import { ClientStatus } from '../types.js';
7
6
  /**
8
7
  * Creates client instances for all transports with retry logic
@@ -85,30 +84,54 @@ export function getClient(clients, clientName) {
85
84
  return client;
86
85
  }
87
86
  /**
88
- * Executes a client operation with error handling and timeout
89
- * @param clients Record of client instances
90
- * @param clientName The name of the client to use
87
+ * Executes an operation with error handling and retry logic
91
88
  * @param operation The operation to execute
89
+ * @param context The execution context (client info or server info)
92
90
  * @param options Operation options including timeout and retry settings
93
91
  * @returns The result of the operation
94
92
  */
95
- export async function executeClientOperation(clients, clientName, operation, options = {}) {
96
- const clientInfo = getClient(clients, clientName);
93
+ export async function executeOperation(operation, contextName, options = {}) {
97
94
  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
- }
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));
110
105
  }
111
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) {
112
111
  throw lastError;
113
- }, `Error executing operation on client ${clientName}`)();
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
120
+ * @param clients Record of client instances
121
+ * @param clientName The name of the client to use
122
+ * @param operation The operation to execute
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
134
+ */
135
+ export async function executeServerOperation(server, operation, options = {}) {
136
+ return executeOperation(() => operation(server), 'server', options);
114
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
+ });
@@ -1,6 +1,11 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- // Mock modules before importing ConfigManager
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
4
9
  jest.mock('fs', () => ({
5
10
  existsSync: jest.fn(),
6
11
  mkdirSync: jest.fn(),
@@ -8,33 +13,20 @@ jest.mock('fs', () => ({
8
13
  readFileSync: jest.fn(),
9
14
  watch: jest.fn(() => ({ close: jest.fn() })),
10
15
  }));
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
- }));
16
+ // Setup common mocks
17
+ setupCommonJestMocks();
23
18
  // Mock constants
24
- jest.doMock('../constants.js', () => ({
19
+ jest.mock('../constants.js', () => ({
25
20
  __esModule: true,
26
21
  DEFAULT_CONFIG: { mcpServers: {} },
27
22
  getGlobalConfigPath: jest.fn(),
28
23
  getGlobalConfigDir: jest.fn().mockReturnValue('/test'),
29
24
  }));
30
- // Import after mocks are set up
31
- import { ConfigManager, ConfigChangeEvent } from './configManager.js';
32
- import { DEFAULT_CONFIG } from '../constants.js';
33
25
  describe('ConfigManager', () => {
34
26
  const testConfigPath = '/test/config.json';
35
27
  beforeEach(() => {
36
- // Reset all mocks
37
- jest.clearAllMocks();
28
+ // Reset all mocks and timers
29
+ clearAllMocks();
38
30
  // Reset singleton instance
39
31
  ConfigManager.instance = undefined;
40
32
  // Default mock implementations
@@ -80,22 +72,16 @@ describe('ConfigManager', () => {
80
72
  });
81
73
  instance.startWatching();
82
74
  // Setup spy for event emission
83
- const emitSpy = jest.spyOn(instance, 'emit');
75
+ const emitSpy = createEventEmitterSpy(instance);
84
76
  // Mock new config data
85
- const newConfig = { mcpServers: { test: { url: 'test-url' } } };
86
- fs.readFileSync.mockReturnValueOnce(JSON.stringify(newConfig));
77
+ fs.readFileSync.mockReturnValueOnce(JSON.stringify(testConfig));
87
78
  // Simulate file change
88
79
  watchCallback('change', path.basename(testConfigPath));
89
- expect(emitSpy).toHaveBeenCalledWith(ConfigChangeEvent.TRANSPORT_CONFIG_CHANGED, newConfig.mcpServers);
80
+ expect(emitSpy).toHaveBeenCalledWith(ConfigChangeEvent.TRANSPORT_CONFIG_CHANGED, testConfig.mcpServers);
90
81
  });
91
82
  });
92
83
  describe('getTransportConfig', () => {
93
84
  it('should return copy of transport config', () => {
94
- const testConfig = {
95
- mcpServers: {
96
- test: { url: 'test-url' },
97
- },
98
- };
99
85
  fs.readFileSync.mockReturnValueOnce(JSON.stringify(testConfig));
100
86
  const instance = ConfigManager.getInstance(testConfigPath);
101
87
  const config = instance.getTransportConfig();
@@ -23,7 +23,7 @@ export const ERROR_CODES = {
23
23
  // MCP constants
24
24
  export const MCP_CONFIG_FILE = 'mcp.json';
25
25
  export const MCP_SERVER_NAME = '1mcp';
26
- export const MCP_SERVER_VERSION = '0.4.0';
26
+ export const MCP_SERVER_VERSION = '0.7.0';
27
27
  export const MCP_URI_SEPARATOR = '_1mcp_';
28
28
  // Global config paths
29
29
  export const CONFIG_DIR_NAME = '1mcp';
@@ -1,11 +1,10 @@
1
- import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourceTemplatesRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, SetLevelRequestSchema, CallToolResultSchema, } from '@modelcontextprotocol/sdk/types.js';
2
- import logger from '../logger/logger.js';
3
- import { setLogLevel } from '../logger/logger.js';
1
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourceTemplatesRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, SetLevelRequestSchema, CallToolResultSchema, CreateMessageRequestSchema, ListRootsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
2
+ import logger, { setLogLevel } from '../logger/logger.js';
4
3
  import { MCP_URI_SEPARATOR, MCP_SERVER_NAME, ERROR_CODES } from '../constants.js';
5
- import { executeClientOperation } from '../clients/clientManager.js';
4
+ import { executeClientOperation, executeServerOperation } from '../clients/clientManager.js';
6
5
  import { parseUri, withErrorHandling } from '../utils/errorHandling.js';
7
6
  import { MCPError } from '../utils/errorTypes.js';
8
- import { filterClientsByTags } from '../utils/clientFiltering.js';
7
+ import { filterClients, byCapabilities, byTags } from '../utils/clientFiltering.js';
9
8
  /**
10
9
  * Sends a partial failure notification to inform clients about backend failures
11
10
  * @param server The MCP server instance
@@ -29,6 +28,25 @@ function sendPartialFailureNotification(serverInfo, operation, failedClients) {
29
28
  },
30
29
  });
31
30
  }
31
+ /**
32
+ * Registers server-specific request handlers
33
+ * @param clients Record of client instances
34
+ * @param serverInfo The MCP server instance
35
+ */
36
+ function registerServerRequestHandlers(clients, serverInfo) {
37
+ Object.entries(clients).forEach(([_, clientInfo]) => {
38
+ clientInfo.client.setRequestHandler(CreateMessageRequestSchema, withErrorHandling(async (request) => {
39
+ return executeServerOperation(serverInfo, (_server) => _server.server.createMessage(request.params, {
40
+ timeout: clientInfo.transport.timeout,
41
+ }));
42
+ }, 'Error creating message'));
43
+ clientInfo.client.setRequestHandler(ListRootsRequestSchema, withErrorHandling(async (request) => {
44
+ return executeServerOperation(serverInfo, (_server) => _server.server.listRoots(request.params, {
45
+ timeout: clientInfo.transport.timeout,
46
+ }));
47
+ }, 'Error listing roots'));
48
+ });
49
+ }
32
50
  /**
33
51
  * Registers all request handlers based on available capabilities
34
52
  * @param clients Record of client instances
@@ -36,24 +54,20 @@ function sendPartialFailureNotification(serverInfo, operation, failedClients) {
36
54
  * @param capabilities The server capabilities
37
55
  * @param tags Array of tags to filter clients by
38
56
  */
39
- export function registerRequestHandlers(clients, serverInfo, capabilities) {
57
+ export function registerRequestHandlers(clients, serverInfo) {
40
58
  // Register logging level handler
41
59
  serverInfo.server.setRequestHandler(SetLevelRequestSchema, async (request) => {
42
60
  setLogLevel(request.params.level);
43
61
  return {};
44
62
  });
45
- // Register resource-related handlers if capability is available
46
- if (capabilities.resources) {
47
- registerResourceHandlers(clients, serverInfo);
48
- }
49
- // Register tool-related handlers if capability is available
50
- if (capabilities.tools) {
51
- registerToolHandlers(clients, serverInfo);
52
- }
53
- // Register prompt-related handlers if capability is available
54
- if (capabilities.prompts) {
55
- registerPromptHandlers(clients, serverInfo);
56
- }
63
+ // Register resource-related handlers
64
+ registerResourceHandlers(clients, serverInfo);
65
+ // Register tool-related handlers
66
+ registerToolHandlers(clients, serverInfo);
67
+ // Register prompt-related handlers
68
+ registerPromptHandlers(clients, serverInfo);
69
+ // Register server-specific request handlers
70
+ registerServerRequestHandlers(clients, serverInfo);
57
71
  }
58
72
  /**
59
73
  * Registers resource-related request handlers
@@ -65,7 +79,7 @@ function registerResourceHandlers(clients, serverInfo) {
65
79
  serverInfo.server.setRequestHandler(ListResourcesRequestSchema, withErrorHandling(async (request) => {
66
80
  const resources = [];
67
81
  const failedClients = [];
68
- const filteredClients = filterClientsByTags(clients, serverInfo.tags);
82
+ const filteredClients = filterClients(byCapabilities({ resources: {} }), byTags(serverInfo.tags))(clients);
69
83
  for (const [name, clientInfo] of Object.entries(filteredClients)) {
70
84
  logger.info(`Listing resources for ${name}`);
71
85
  try {
@@ -84,9 +98,9 @@ function registerResourceHandlers(clients, serverInfo) {
84
98
  failedClients.push({ name, error: error instanceof Error ? error.message : String(error) });
85
99
  }
86
100
  }
87
- // If all clients failed, throw an error
101
+ // If all capable clients failed, throw an error
88
102
  if (failedClients.length === Object.keys(filteredClients).length && Object.keys(filteredClients).length > 0) {
89
- throw new MCPError('Failed to list resources from all clients', ERROR_CODES.INTERNAL_SERVER_ERROR, {
103
+ throw new MCPError('Failed to list resources from all capable clients', ERROR_CODES.INTERNAL_SERVER_ERROR, {
90
104
  failedClients,
91
105
  });
92
106
  }
@@ -100,8 +114,8 @@ function registerResourceHandlers(clients, serverInfo) {
100
114
  serverInfo.server.setRequestHandler(ListResourceTemplatesRequestSchema, withErrorHandling(async (request) => {
101
115
  const resourceTemplates = [];
102
116
  const failedClients = [];
103
- const filteredClients = filterClientsByTags(clients, serverInfo.tags);
104
- for (const [name, clientInfo] of Object.entries(filteredClients)) {
117
+ const capableClients = filterClients(byCapabilities({ resources: {} }), byTags(serverInfo.tags))(clients);
118
+ for (const [name, clientInfo] of Object.entries(capableClients)) {
105
119
  logger.info(`Listing resource templates for ${name}`);
106
120
  try {
107
121
  const result = await clientInfo.client.listResourceTemplates(request.params, {
@@ -120,7 +134,7 @@ function registerResourceHandlers(clients, serverInfo) {
120
134
  }
121
135
  }
122
136
  // If all clients failed, throw an error
123
- if (failedClients.length === Object.keys(filteredClients).length && Object.keys(filteredClients).length > 0) {
137
+ if (failedClients.length === Object.keys(capableClients).length && Object.keys(capableClients).length > 0) {
124
138
  throw new MCPError('Failed to list resource templates from all clients', ERROR_CODES.INTERNAL_SERVER_ERROR, {
125
139
  failedClients,
126
140
  });
@@ -163,8 +177,8 @@ function registerToolHandlers(clients, serverInfo) {
163
177
  serverInfo.server.setRequestHandler(ListToolsRequestSchema, withErrorHandling(async (request) => {
164
178
  const tools = [];
165
179
  const failedClients = [];
166
- const filteredClients = filterClientsByTags(clients, serverInfo.tags);
167
- for (const [name, clientInfo] of Object.entries(filteredClients)) {
180
+ const capableClients = filterClients(byCapabilities({ tools: {} }), byTags(serverInfo.tags))(clients);
181
+ for (const [name, clientInfo] of Object.entries(capableClients)) {
168
182
  logger.info(`Listing tools for ${name}`);
169
183
  try {
170
184
  const result = await clientInfo.client.listTools(request.params, {
@@ -182,7 +196,7 @@ function registerToolHandlers(clients, serverInfo) {
182
196
  }
183
197
  }
184
198
  // If all clients failed, throw an error
185
- if (failedClients.length === Object.keys(filteredClients).length && Object.keys(filteredClients).length > 0) {
199
+ if (failedClients.length === Object.keys(capableClients).length && Object.keys(capableClients).length > 0) {
186
200
  throw new MCPError('Failed to list tools from all clients', ERROR_CODES.INTERNAL_SERVER_ERROR, {
187
201
  failedClients,
188
202
  });
@@ -211,8 +225,8 @@ function registerPromptHandlers(clients, serverInfo) {
211
225
  serverInfo.server.setRequestHandler(ListPromptsRequestSchema, withErrorHandling(async (request) => {
212
226
  const prompts = [];
213
227
  const failedClients = [];
214
- const filteredClients = filterClientsByTags(clients, serverInfo.tags);
215
- for (const [name, clientInfo] of Object.entries(filteredClients)) {
228
+ const capableClients = filterClients(byCapabilities({ prompts: {} }), byTags(serverInfo.tags))(clients);
229
+ for (const [name, clientInfo] of Object.entries(capableClients)) {
216
230
  logger.info(`Listing prompts for ${name}`);
217
231
  try {
218
232
  const result = await clientInfo.client.listPrompts(request.params);
@@ -228,7 +242,7 @@ function registerPromptHandlers(clients, serverInfo) {
228
242
  }
229
243
  }
230
244
  // If all clients failed, throw an error
231
- if (failedClients.length === Object.keys(filteredClients).length && Object.keys(filteredClients).length > 0) {
245
+ if (failedClients.length === Object.keys(capableClients).length && Object.keys(capableClients).length > 0) {
232
246
  throw new MCPError('Failed to list prompts from all clients', ERROR_CODES.INTERNAL_SERVER_ERROR, {
233
247
  failedClients,
234
248
  });
@@ -1,12 +1,6 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
1
  import winston from 'winston';
4
2
  import { MCPTransport } from './mcpTransport.js';
5
- // Ensure logs directory exists
6
- const logsDir = path.join(process.cwd(), 'logs');
7
- if (!fs.existsSync(logsDir)) {
8
- fs.mkdirSync(logsDir, { recursive: true });
9
- }
3
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
10
4
  // Map MCP log levels to Winston log levels
11
5
  const MCP_TO_WINSTON_LEVEL = {
12
6
  debug: 'debug',
@@ -33,7 +27,9 @@ const logger = winston.createLogger({
33
27
  level: 'info',
34
28
  format: customFormat,
35
29
  transports: [
30
+ // Add a silent transport by default to prevent "no transports" warnings
36
31
  new winston.transports.Console({
32
+ silent: true,
37
33
  format: consoleFormat,
38
34
  }),
39
35
  ],
@@ -54,7 +50,15 @@ export function addMCPTransport(server, loggerName) {
54
50
  loggerName: loggerName || '1mcp',
55
51
  level: 'info',
56
52
  });
57
- logger.add(mcpTransport);
53
+ logger.transports = [mcpTransport];
54
+ // Add appropriate transport based on server type
55
+ if (server instanceof SSEServerTransport) {
56
+ // For SSE server, add console transport for visibility
57
+ logger.add(new winston.transports.Console({
58
+ format: consoleFormat,
59
+ level: 'info',
60
+ }));
61
+ }
58
62
  }
59
63
  /**
60
64
  * Set the connection status of the MCP transport
@@ -0,0 +1,44 @@
1
+ import { createMockTransport } from '../mocks/commonMocks.js';
2
+ // Common test clients fixture
3
+ export const createTestClients = () => ({
4
+ client1: {
5
+ capabilities: {
6
+ resources: {},
7
+ tools: {},
8
+ },
9
+ transport: createMockTransport(),
10
+ },
11
+ client2: {
12
+ capabilities: {
13
+ resources: {},
14
+ },
15
+ transport: createMockTransport(),
16
+ },
17
+ client3: {
18
+ capabilities: {
19
+ tools: {},
20
+ },
21
+ transport: createMockTransport(),
22
+ },
23
+ clientNoCapabilities: {
24
+ transport: createMockTransport(),
25
+ },
26
+ clientNoTags: {
27
+ capabilities: {
28
+ resources: {},
29
+ },
30
+ transport: createMockTransport(),
31
+ },
32
+ });
33
+ // Common test config fixture
34
+ export const testConfig = {
35
+ mcpServers: {
36
+ test: { url: 'test-url' },
37
+ },
38
+ };
39
+ // Common test capabilities fixture
40
+ export const testCapabilities = {
41
+ resources: {},
42
+ tools: {},
43
+ test: true,
44
+ };
@@ -0,0 +1,23 @@
1
+ // Helper to clear all mocks in a test
2
+ export const clearAllMocks = () => {
3
+ jest.clearAllMocks();
4
+ jest.useRealTimers();
5
+ };
6
+ // Helper to setup fake timers
7
+ export const setupFakeTimers = () => {
8
+ jest.useFakeTimers();
9
+ };
10
+ // Helper to advance timers by time
11
+ export const advanceTimersByTime = async (ms) => {
12
+ await jest.advanceTimersByTimeAsync(ms);
13
+ };
14
+ // Helper to run all timers
15
+ export const runAllTimers = async () => {
16
+ await jest.runAllTimersAsync();
17
+ };
18
+ // Helper to create a mock event emitter spy
19
+ export const createEventEmitterSpy = (instance) => {
20
+ return jest.spyOn(instance, 'emit');
21
+ };
22
+ // Helper to wait for next tick
23
+ export const waitForNextTick = () => new Promise((resolve) => process.nextTick(resolve));
@@ -0,0 +1,65 @@
1
+ // Mock Logger
2
+ export const mockLogger = {
3
+ info: jest.fn(),
4
+ error: jest.fn(),
5
+ warn: jest.fn(),
6
+ debug: jest.fn(),
7
+ level: 'info',
8
+ transports: [],
9
+ };
10
+ // Mock Transport
11
+ export const createMockTransport = () => ({
12
+ start: jest.fn(),
13
+ send: jest.fn(),
14
+ close: jest.fn(),
15
+ });
16
+ // Mock Server
17
+ export const createMockServer = (overrides = {}) => ({
18
+ connect: jest.fn().mockResolvedValue(undefined),
19
+ transport: createMockTransport(),
20
+ ...overrides,
21
+ });
22
+ // Mock Client
23
+ export const createMockClient = (overrides = {}) => ({
24
+ connect: jest.fn(),
25
+ getServerVersion: jest.fn(),
26
+ ...overrides,
27
+ });
28
+ // Mock File System
29
+ export const createMockFs = () => ({
30
+ existsSync: jest.fn(),
31
+ mkdirSync: jest.fn(),
32
+ writeFileSync: jest.fn(),
33
+ readFileSync: jest.fn(),
34
+ watch: jest.fn(() => ({ close: jest.fn() })),
35
+ });
36
+ // Mock Config
37
+ export const createMockConfig = (overrides = {}) => ({
38
+ name: 'test-server',
39
+ version: '1.0.0',
40
+ ...overrides,
41
+ });
42
+ // Mock Capabilities
43
+ export const createMockCapabilities = (overrides = {}) => ({
44
+ capabilities: {
45
+ resources: {},
46
+ tools: {},
47
+ ...overrides,
48
+ },
49
+ });
50
+ // Jest module mocks setup helper
51
+ export const setupCommonJestMocks = () => {
52
+ jest.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
53
+ Server: jest.fn(),
54
+ }));
55
+ jest.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
56
+ Client: jest.fn(),
57
+ }));
58
+ jest.mock('@modelcontextprotocol/sdk/shared/transport.js', () => ({
59
+ Transport: jest.fn(),
60
+ }));
61
+ jest.mock('../../logger/logger.js', () => ({
62
+ __esModule: true,
63
+ default: mockLogger,
64
+ }));
65
+ };
@@ -27,3 +27,75 @@ export function filterClientsByTags(clients, tags) {
27
27
  }
28
28
  return Object.freeze(filteredClients);
29
29
  }
30
+ /**
31
+ * Filters clients by capabilities
32
+ * @param clients Record of client instances
33
+ * @param capabilities Object containing capabilities to filter by
34
+ * @returns Filtered record of client instances
35
+ */
36
+ export function filterClientsByCapabilities(clients, capabilities) {
37
+ const filteredClients = {};
38
+ let matchedClients = 0;
39
+ for (const [name, clientInfo] of Object.entries(clients)) {
40
+ const clientCapabilities = clientInfo.capabilities || {};
41
+ const hasMatchingCapabilities = Object.keys(capabilities).every((capability) => {
42
+ const clientCapability = clientCapabilities[capability];
43
+ return clientCapability !== undefined;
44
+ });
45
+ if (hasMatchingCapabilities) {
46
+ filteredClients[name] = clientInfo;
47
+ matchedClients++;
48
+ }
49
+ }
50
+ if (matchedClients === 0) {
51
+ logger.warn(`No clients found matching capabilities: ${JSON.stringify(capabilities)}`);
52
+ }
53
+ else {
54
+ logger.debug(`Found ${matchedClients} clients matching capabilities: ${JSON.stringify(capabilities)}`);
55
+ }
56
+ return Object.freeze(filteredClients);
57
+ }
58
+ /**
59
+ * Filters clients by multiple criteria
60
+ * @param filters Array of client filters
61
+ * @returns Filtered record of client instances
62
+ */
63
+ export function filterClients(...filters) {
64
+ return (clients) => {
65
+ return filters.reduce((filteredClients, filter) => filter(filteredClients), clients);
66
+ };
67
+ }
68
+ /**
69
+ * Filters clients by capabilities
70
+ * @param requiredCapabilities Object containing capabilities to filter by
71
+ * @returns Filtered record of client instances
72
+ */
73
+ export function byCapabilities(requiredCapabilities) {
74
+ return (clients) => {
75
+ return Object.entries(clients).reduce((filtered, [name, clientInfo]) => {
76
+ const hasCapabilities = Object.keys(requiredCapabilities).every((cap) => clientInfo.capabilities && cap in clientInfo.capabilities);
77
+ if (hasCapabilities) {
78
+ filtered[name] = clientInfo;
79
+ }
80
+ return filtered;
81
+ }, {});
82
+ };
83
+ }
84
+ /**
85
+ * Filters clients by tags
86
+ * @param tags Array of tags to filter by
87
+ * @returns Filtered record of client instances
88
+ */
89
+ export function byTags(tags) {
90
+ return (clients) => {
91
+ if (!tags || tags.length === 0)
92
+ return clients;
93
+ return Object.entries(clients).reduce((filtered, [name, clientInfo]) => {
94
+ const hasMatchingTags = tags.every((tag) => clientInfo.transport.tags?.includes(tag));
95
+ if (hasMatchingTags) {
96
+ filtered[name] = clientInfo;
97
+ }
98
+ return filtered;
99
+ }, {});
100
+ };
101
+ }
@@ -0,0 +1,124 @@
1
+ import { filterClients, byCapabilities, byTags, filterClientsByCapabilities, filterClientsByTags, } from './clientFiltering.js';
2
+ describe('Client Filtering Utils', () => {
3
+ const mockClients = {
4
+ client1: {
5
+ capabilities: {
6
+ resources: {},
7
+ tools: {},
8
+ },
9
+ transport: {
10
+ tags: ['tag1', 'tag2'],
11
+ },
12
+ },
13
+ client2: {
14
+ capabilities: {
15
+ resources: {},
16
+ },
17
+ transport: {
18
+ tags: ['tag1'],
19
+ },
20
+ },
21
+ client3: {
22
+ capabilities: {
23
+ tools: {},
24
+ },
25
+ transport: {
26
+ tags: ['tag3'],
27
+ },
28
+ },
29
+ clientNoCapabilities: {
30
+ transport: {
31
+ tags: ['tag1'],
32
+ },
33
+ },
34
+ clientNoTags: {
35
+ capabilities: {
36
+ resources: {},
37
+ },
38
+ transport: {},
39
+ },
40
+ };
41
+ describe('filterClientsByCapabilities', () => {
42
+ it('should filter clients by single capability', () => {
43
+ const filtered = filterClientsByCapabilities(mockClients, { resources: {} });
44
+ expect(Object.keys(filtered)).toEqual(['client1', 'client2', 'clientNoTags']);
45
+ });
46
+ it('should filter clients by multiple capabilities', () => {
47
+ const filtered = filterClientsByCapabilities(mockClients, { resources: {}, tools: {} });
48
+ expect(Object.keys(filtered)).toEqual(['client1']);
49
+ });
50
+ it('should handle clients with no capabilities', () => {
51
+ const filtered = filterClientsByCapabilities(mockClients, { resources: {} });
52
+ expect(filtered).not.toHaveProperty('clientNoCapabilities');
53
+ });
54
+ it('should return empty object when no matches found', () => {
55
+ const filtered = filterClientsByCapabilities(mockClients, { nonexistent: {} });
56
+ expect(Object.keys(filtered)).toHaveLength(0);
57
+ });
58
+ });
59
+ describe('filterClientsByTags', () => {
60
+ it('should return all clients when no tags provided', () => {
61
+ const filtered = filterClientsByTags(mockClients, undefined);
62
+ expect(Object.keys(filtered)).toEqual(Object.keys(mockClients));
63
+ });
64
+ it('should filter clients by single tag', () => {
65
+ const filtered = filterClientsByTags(mockClients, ['tag1']);
66
+ expect(Object.keys(filtered)).toEqual(['client1', 'client2', 'clientNoCapabilities']);
67
+ });
68
+ it('should filter clients by multiple tags', () => {
69
+ const filtered = filterClientsByTags(mockClients, ['tag1', 'tag2']);
70
+ expect(Object.keys(filtered)).toEqual(['client1']);
71
+ });
72
+ it('should handle clients with no tags', () => {
73
+ const filtered = filterClientsByTags(mockClients, ['tag1']);
74
+ expect(filtered).not.toHaveProperty('clientNoTags');
75
+ });
76
+ it('should return empty object when no matches found', () => {
77
+ const filtered = filterClientsByTags(mockClients, ['nonexistent']);
78
+ expect(Object.keys(filtered)).toHaveLength(0);
79
+ });
80
+ });
81
+ describe('filterClients (composed filters)', () => {
82
+ it('should chain multiple filters together', () => {
83
+ const filtered = filterClients(byCapabilities({ resources: {} }), byTags(['tag1']))(mockClients);
84
+ expect(Object.keys(filtered)).toEqual(['client1', 'client2']);
85
+ });
86
+ it('should handle empty filters array', () => {
87
+ const filtered = filterClients()(mockClients);
88
+ expect(Object.keys(filtered)).toEqual(Object.keys(mockClients));
89
+ });
90
+ it('should handle no matching results', () => {
91
+ const filtered = filterClients(byCapabilities({ resources: {} }), byTags(['tag3']))(mockClients);
92
+ expect(Object.keys(filtered)).toHaveLength(0);
93
+ });
94
+ });
95
+ describe('byCapabilities', () => {
96
+ it('should create a filter function for capabilities', () => {
97
+ const filter = byCapabilities({ resources: {} });
98
+ const filtered = filter(mockClients);
99
+ expect(Object.keys(filtered)).toEqual(['client1', 'client2', 'clientNoTags']);
100
+ });
101
+ it('should handle undefined capabilities safely', () => {
102
+ const filter = byCapabilities({ resources: {} });
103
+ const filtered = filter({ ...mockClients, unsafeClient: { transport: {} } });
104
+ expect(filtered).not.toHaveProperty('unsafeClient');
105
+ });
106
+ });
107
+ describe('byTags', () => {
108
+ it('should create a filter function for tags', () => {
109
+ const filter = byTags(['tag1']);
110
+ const filtered = filter(mockClients);
111
+ expect(Object.keys(filtered)).toEqual(['client1', 'client2', 'clientNoCapabilities']);
112
+ });
113
+ it('should handle undefined tags array', () => {
114
+ const filter = byTags(undefined);
115
+ const filtered = filter(mockClients);
116
+ expect(Object.keys(filtered)).toEqual(Object.keys(mockClients));
117
+ });
118
+ it('should handle empty tags array', () => {
119
+ const filter = byTags([]);
120
+ const filtered = filter(mockClients);
121
+ expect(Object.keys(filtered)).toEqual(Object.keys(mockClients));
122
+ });
123
+ });
124
+ });
package/package.json CHANGED
@@ -1,8 +1,26 @@
1
1
  {
2
2
  "name": "@1mcp/agent",
3
- "version": "0.4.0",
3
+ "version": "0.7.0",
4
4
  "description": "One MCP server to aggregate them all - A unified Model Context Protocol server implementation",
5
- "homepage": "https://1mcp.app",
5
+ "keywords": [
6
+ "mcp",
7
+ "mcp-server",
8
+ "mcp-client",
9
+ "model-context-protocol",
10
+ "agent",
11
+ "proxy",
12
+ "server",
13
+ "cli",
14
+ "ai",
15
+ "llm",
16
+ "model-aggregation",
17
+ "typescript"
18
+ ],
19
+ "author": "William Xu <xuzp@1mcp.app>",
20
+ "homepage": "https://github.com/1mcp-app/agent",
21
+ "bugs": {
22
+ "url": "https://github.com/1mcp-app/agent/issues"
23
+ },
6
24
  "license": "Apache-2.0",
7
25
  "repository": {
8
26
  "type": "git",
@@ -67,7 +85,7 @@
67
85
  "dev": "tsc-watch --onSuccess \"node build/index.js --config ./mcp.json\"",
68
86
  "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
69
87
  "watch": "tsc --watch",
70
- "inspector": "npx @modelcontextprotocol/inspector build/index.js",
88
+ "inspector": "npx -y @modelcontextprotocol/inspector",
71
89
  "lint": "eslint . --ext .ts",
72
90
  "lint:fix": "eslint . --ext .ts --fix",
73
91
  "test": "jest",