@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 +2 -2
- package/build/capabilities/capabilityManager.js +12 -5
- package/build/clients/clientManager.js +44 -21
- package/build/clients/clientManager.test.js +172 -0
- package/build/config/configManager.test.js +14 -28
- package/build/constants.js +1 -1
- package/build/handlers/requestHandlers.js +44 -30
- package/build/logger/logger.js +12 -8
- package/build/test-utils/fixtures/commonFixtures.js +44 -0
- package/build/test-utils/helpers/testHelpers.js +23 -0
- package/build/test-utils/mocks/commonMocks.js +65 -0
- package/build/utils/clientFiltering.js +72 -0
- package/build/utils/clientFiltering.test.js +124 -0
- package/package.json +21 -3
package/README.md
CHANGED
|
@@ -229,10 +229,10 @@ pnpm dev
|
|
|
229
229
|
|
|
230
230
|
### Debugging
|
|
231
231
|
|
|
232
|
-
|
|
232
|
+
Using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script:
|
|
233
233
|
|
|
234
234
|
```bash
|
|
235
|
-
pnpm
|
|
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
|
|
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
|
-
|
|
27
|
+
const capabilities = {};
|
|
28
28
|
for (const [name, clientInfo] of Object.entries(clients)) {
|
|
29
29
|
try {
|
|
30
|
-
const
|
|
31
|
-
logger.debug(`Capabilities from ${name}: ${JSON.stringify(
|
|
32
|
-
|
|
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 {
|
|
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
|
|
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
|
|
96
|
-
const clientInfo = getClient(clients, clientName);
|
|
93
|
+
export async function executeOperation(operation, contextName, options = {}) {
|
|
97
94
|
const { retryCount = 0, retryDelay = 1000 } = options;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
75
|
+
const emitSpy = createEventEmitterSpy(instance);
|
|
84
76
|
// Mock new config data
|
|
85
|
-
|
|
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,
|
|
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();
|
package/build/constants.js
CHANGED
|
@@ -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.
|
|
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 {
|
|
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
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// Register
|
|
50
|
-
|
|
51
|
-
|
|
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 =
|
|
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
|
|
104
|
-
for (const [name, clientInfo] of Object.entries(
|
|
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(
|
|
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
|
|
167
|
-
for (const [name, clientInfo] of Object.entries(
|
|
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(
|
|
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
|
|
215
|
-
for (const [name, clientInfo] of Object.entries(
|
|
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(
|
|
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
|
});
|
package/build/logger/logger.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "One MCP server to aggregate them all - A unified Model Context Protocol server implementation",
|
|
5
|
-
"
|
|
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
|
|
88
|
+
"inspector": "npx -y @modelcontextprotocol/inspector",
|
|
71
89
|
"lint": "eslint . --ext .ts",
|
|
72
90
|
"lint:fix": "eslint . --ext .ts --fix",
|
|
73
91
|
"test": "jest",
|