@1mcp/agent 0.7.0 → 0.8.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 +55 -0
- package/build/capabilities/capabilityManager.d.ts +34 -0
- package/build/capabilities/capabilityManager.d.ts.map +1 -0
- package/build/capabilities/capabilityManager.js +1 -0
- package/build/capabilities/capabilityManager.js.map +1 -0
- package/build/client.d.ts +25 -0
- package/build/client.d.ts.map +1 -0
- package/build/client.js +3 -10
- package/build/client.js.map +1 -0
- package/build/clients/clientManager.d.ts +40 -0
- package/build/clients/clientManager.d.ts.map +1 -0
- package/build/clients/clientManager.js +1 -0
- package/build/clients/clientManager.js.map +1 -0
- package/build/config/configManager.d.ts +54 -0
- package/build/config/configManager.d.ts.map +1 -0
- package/build/config/configManager.js +1 -0
- package/build/config/configManager.js.map +1 -0
- package/build/config/transportConfig.d.ts +7 -0
- package/build/config/transportConfig.d.ts.map +1 -0
- package/build/config/transportConfig.js +1 -0
- package/build/config/transportConfig.js.map +1 -0
- package/build/constants.d.ts +43 -0
- package/build/constants.d.ts.map +1 -0
- package/build/constants.js +19 -4
- package/build/constants.js.map +1 -0
- package/build/handlers/notificationHandlers.d.ts +14 -0
- package/build/handlers/notificationHandlers.d.ts.map +1 -0
- package/build/handlers/notificationHandlers.js +1 -0
- package/build/handlers/notificationHandlers.js.map +1 -0
- package/build/handlers/requestHandlers.d.ts +10 -0
- package/build/handlers/requestHandlers.d.ts.map +1 -0
- package/build/handlers/requestHandlers.js +1 -0
- package/build/handlers/requestHandlers.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +6 -1
- package/build/index.js.map +1 -0
- package/build/logger/logger.d.ts +13 -0
- package/build/logger/logger.d.ts.map +1 -0
- package/build/logger/logger.js +5 -35
- package/build/logger/logger.js.map +1 -0
- package/build/middleware/loggingMiddleware.d.ts +6 -0
- package/build/middleware/loggingMiddleware.d.ts.map +1 -0
- package/build/middleware/loggingMiddleware.js +1 -0
- package/build/middleware/loggingMiddleware.js.map +1 -0
- package/build/server/expressServer.d.ts +10 -0
- package/build/server/expressServer.d.ts.map +1 -0
- package/build/server/expressServer.js +2 -11
- package/build/server/expressServer.js.map +1 -0
- package/build/server.d.ts +7 -0
- package/build/server.d.ts.map +1 -0
- package/build/server.js +1 -0
- package/build/server.js.map +1 -0
- package/build/serverManager.d.ts +25 -0
- package/build/serverManager.d.ts.map +1 -0
- package/build/serverManager.js +4 -2
- package/build/serverManager.js.map +1 -0
- package/build/services/configReloadService.d.ts +37 -0
- package/build/services/configReloadService.d.ts.map +1 -0
- package/build/services/configReloadService.js +1 -0
- package/build/services/configReloadService.js.map +1 -0
- package/build/types.d.ts +129 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +1 -0
- package/build/types.js.map +1 -0
- package/build/utils/clientFiltering.d.ts +37 -0
- package/build/utils/clientFiltering.d.ts.map +1 -0
- package/build/utils/clientFiltering.js +1 -0
- package/build/utils/clientFiltering.js.map +1 -0
- package/build/utils/errorHandling.d.ts +62 -0
- package/build/utils/errorHandling.d.ts.map +1 -0
- package/build/utils/errorHandling.js +1 -0
- package/build/utils/errorHandling.js.map +1 -0
- package/build/utils/errorTypes.d.ts +25 -0
- package/build/utils/errorTypes.d.ts.map +1 -0
- package/build/utils/errorTypes.js +1 -0
- package/build/utils/errorTypes.js.map +1 -0
- package/package.json +8 -9
- package/build/clients/clientManager.test.js +0 -172
- package/build/config/configManager.test.js +0 -92
- package/build/logger/mcpTransport.js +0 -150
- package/build/serverManager.test.js +0 -158
- package/build/test-utils/fixtures/commonFixtures.js +0 -44
- package/build/test-utils/helpers/testHelpers.js +0 -23
- package/build/test-utils/mocks/commonMocks.js +0 -65
- package/build/utils/clientFiltering.test.js +0 -124
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1mcp/agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "One MCP server to aggregate them all - A unified Model Context Protocol server implementation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@modelcontextprotocol/sdk": "^1.7.0",
|
|
52
|
-
"express": "^
|
|
52
|
+
"express": "^5.1.0",
|
|
53
53
|
"uuid": "^11.1.0",
|
|
54
54
|
"winston": "^3.17.0",
|
|
55
55
|
"winston-transport": "^4.9.0",
|
|
@@ -61,25 +61,24 @@
|
|
|
61
61
|
"@eslint/js": "^9.22.0",
|
|
62
62
|
"@testing-library/jest-dom": "^6.6.3",
|
|
63
63
|
"@types/express": "^5.0.0",
|
|
64
|
-
"@types/jest": "^29.5.14",
|
|
65
64
|
"@types/node": "^22.13.10",
|
|
66
65
|
"@types/uuid": "^10.0.0",
|
|
67
66
|
"@types/yargs": "^17.0.33",
|
|
68
67
|
"@typescript-eslint/eslint-plugin": "^8.27.0",
|
|
69
68
|
"@typescript-eslint/parser": "^8.27.0",
|
|
69
|
+
"@vitest/coverage-v8": "^1.4.0",
|
|
70
70
|
"eslint": "^9.22.0",
|
|
71
71
|
"eslint-config-prettier": "^10.1.1",
|
|
72
72
|
"eslint-plugin-prettier": "^5.2.3",
|
|
73
73
|
"globals": "^16.0.0",
|
|
74
74
|
"husky": "^9.1.7",
|
|
75
|
-
"jest": "^29.7.0",
|
|
76
75
|
"jiti": "^2.4.2",
|
|
77
76
|
"lint-staged": "^15.5.0",
|
|
78
77
|
"prettier": "^3.5.3",
|
|
79
|
-
"ts-jest": "^29.2.6",
|
|
80
78
|
"ts-node": "^10.9.2",
|
|
81
79
|
"tsc-watch": "^6.2.1",
|
|
82
|
-
"typescript": "^5.3.3"
|
|
80
|
+
"typescript": "^5.3.3",
|
|
81
|
+
"vitest": "^1.4.0"
|
|
83
82
|
},
|
|
84
83
|
"scripts": {
|
|
85
84
|
"dev": "tsc-watch --onSuccess \"node build/index.js --config ./mcp.json\"",
|
|
@@ -88,8 +87,8 @@
|
|
|
88
87
|
"inspector": "npx -y @modelcontextprotocol/inspector",
|
|
89
88
|
"lint": "eslint . --ext .ts",
|
|
90
89
|
"lint:fix": "eslint . --ext .ts --fix",
|
|
91
|
-
"test": "
|
|
92
|
-
"test:watch": "
|
|
93
|
-
"test:coverage": "
|
|
90
|
+
"test": "vitest run",
|
|
91
|
+
"test:watch": "vitest",
|
|
92
|
+
"test:coverage": "vitest run --coverage"
|
|
94
93
|
}
|
|
95
94
|
}
|
|
@@ -1,172 +0,0 @@
|
|
|
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,92 +0,0 @@
|
|
|
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,150 +0,0 @@
|
|
|
1
|
-
import Transport from 'winston-transport';
|
|
2
|
-
// Map Winston log levels to MCP log levels
|
|
3
|
-
const LOG_LEVEL_MAP = {
|
|
4
|
-
error: 'error',
|
|
5
|
-
warn: 'warning',
|
|
6
|
-
info: 'info',
|
|
7
|
-
verbose: 'debug',
|
|
8
|
-
debug: 'debug',
|
|
9
|
-
silly: 'debug',
|
|
10
|
-
};
|
|
11
|
-
/**
|
|
12
|
-
* Winston transport for sending logs to MCP clients
|
|
13
|
-
*/
|
|
14
|
-
export class MCPTransport extends Transport {
|
|
15
|
-
server;
|
|
16
|
-
loggerName;
|
|
17
|
-
connected = false;
|
|
18
|
-
constructor(options) {
|
|
19
|
-
super(options);
|
|
20
|
-
this.server = options.server;
|
|
21
|
-
this.loggerName = options.loggerName || '1mcp';
|
|
22
|
-
// Set up connection status handlers
|
|
23
|
-
this.setupConnectionHandlers();
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Set up handlers for connection status changes
|
|
27
|
-
*/
|
|
28
|
-
setupConnectionHandlers() {
|
|
29
|
-
// Handle connection close
|
|
30
|
-
this.server.onclose = () => {
|
|
31
|
-
this.setConnected(false);
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Set the connection status of the transport
|
|
36
|
-
* @param connected Whether the server is connected
|
|
37
|
-
*/
|
|
38
|
-
setConnected(connected) {
|
|
39
|
-
const previousState = this.connected;
|
|
40
|
-
this.connected = connected;
|
|
41
|
-
// Log connection state changes
|
|
42
|
-
if (previousState !== connected) {
|
|
43
|
-
console.debug(`MCP Transport connection state changed to: ${connected ? 'connected' : 'disconnected'}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Send a log notification to the MCP client
|
|
48
|
-
* @param info The log info object
|
|
49
|
-
* @returns true if the notification was sent, false otherwise
|
|
50
|
-
*/
|
|
51
|
-
sendLogNotification(info) {
|
|
52
|
-
if (!this.server || !this.connected) {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
// Map Winston log level to MCP log level
|
|
56
|
-
const level = LOG_LEVEL_MAP[info.level] || 'info';
|
|
57
|
-
const sanitizedData = this.sanitizeData(info);
|
|
58
|
-
// Extract message and metadata
|
|
59
|
-
const { message, level: _, ...meta } = sanitizedData;
|
|
60
|
-
// Create MCP logging notification
|
|
61
|
-
const notification = {
|
|
62
|
-
method: 'notifications/message',
|
|
63
|
-
params: {
|
|
64
|
-
level,
|
|
65
|
-
message: typeof message === 'string' ? message : JSON.stringify(message),
|
|
66
|
-
logger: this.loggerName,
|
|
67
|
-
data: Object.keys(meta).length > 0 ? meta : undefined,
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
// Send notification to MCP clients
|
|
71
|
-
try {
|
|
72
|
-
this.server.notification(notification);
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
console.error('Failed to send log notification to MCP client:', error);
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Sanitize log data to remove sensitive information
|
|
82
|
-
* @param data The data to sanitize
|
|
83
|
-
* @returns Sanitized data
|
|
84
|
-
*/
|
|
85
|
-
sanitizeData(data) {
|
|
86
|
-
if (typeof data !== 'object' || data === null) {
|
|
87
|
-
return data;
|
|
88
|
-
}
|
|
89
|
-
// List of potentially sensitive field names
|
|
90
|
-
const sensitiveFields = [
|
|
91
|
-
'password',
|
|
92
|
-
'token',
|
|
93
|
-
'secret',
|
|
94
|
-
'key',
|
|
95
|
-
'credential',
|
|
96
|
-
'auth',
|
|
97
|
-
'apiKey',
|
|
98
|
-
'api_key',
|
|
99
|
-
'accessToken',
|
|
100
|
-
'access_token',
|
|
101
|
-
'jwt',
|
|
102
|
-
'ssn',
|
|
103
|
-
'creditCard',
|
|
104
|
-
'credit_card',
|
|
105
|
-
'cvv',
|
|
106
|
-
'pin',
|
|
107
|
-
];
|
|
108
|
-
if (Array.isArray(data)) {
|
|
109
|
-
return data.map((item) => this.sanitizeData(item));
|
|
110
|
-
}
|
|
111
|
-
const sanitized = {};
|
|
112
|
-
for (const [key, value] of Object.entries(data)) {
|
|
113
|
-
// Check if this is a sensitive field
|
|
114
|
-
const isSensitive = sensitiveFields.some((field) => key.toLowerCase().includes(field.toLowerCase()));
|
|
115
|
-
if (isSensitive) {
|
|
116
|
-
sanitized[key] = '[REDACTED]';
|
|
117
|
-
}
|
|
118
|
-
else if (typeof value === 'object' && value !== null) {
|
|
119
|
-
sanitized[key] = this.sanitizeData(value);
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
sanitized[key] = value;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return sanitized;
|
|
126
|
-
}
|
|
127
|
-
log(info, callback) {
|
|
128
|
-
setImmediate(() => {
|
|
129
|
-
this.emit('logged', info);
|
|
130
|
-
});
|
|
131
|
-
// Only try to send if connected
|
|
132
|
-
if (this.connected) {
|
|
133
|
-
try {
|
|
134
|
-
this.sendLogNotification(info);
|
|
135
|
-
}
|
|
136
|
-
catch (_) {
|
|
137
|
-
// Ignore any errors during log sending
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
// Always call the callback to prevent blocking
|
|
141
|
-
callback();
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Clean up resources when the transport is closed
|
|
145
|
-
*/
|
|
146
|
-
close() {
|
|
147
|
-
this.setConnected(false);
|
|
148
|
-
this.server.onclose = undefined;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
-
import { ServerManager } from './serverManager.js';
|
|
3
|
-
import logger from './logger/logger.js';
|
|
4
|
-
import configReloadService from './services/configReloadService.js';
|
|
5
|
-
import { setupCapabilities } from './capabilities/capabilityManager.js';
|
|
6
|
-
import { enhanceServerWithLogging } from './middleware/loggingMiddleware.js';
|
|
7
|
-
// Mock dependencies
|
|
8
|
-
jest.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
|
|
9
|
-
Server: jest.fn(),
|
|
10
|
-
}));
|
|
11
|
-
jest.mock('@modelcontextprotocol/sdk/shared/transport.js', () => ({
|
|
12
|
-
Transport: jest.fn(),
|
|
13
|
-
}));
|
|
14
|
-
jest.mock('./logger/logger.js', () => {
|
|
15
|
-
const mockLogger = {
|
|
16
|
-
info: jest.fn(),
|
|
17
|
-
error: jest.fn(),
|
|
18
|
-
};
|
|
19
|
-
return {
|
|
20
|
-
__esModule: true,
|
|
21
|
-
default: mockLogger,
|
|
22
|
-
};
|
|
23
|
-
});
|
|
24
|
-
jest.mock('./services/configReloadService.js', () => ({
|
|
25
|
-
__esModule: true,
|
|
26
|
-
default: {
|
|
27
|
-
initialize: jest.fn(),
|
|
28
|
-
},
|
|
29
|
-
}));
|
|
30
|
-
jest.mock('./capabilities/capabilityManager.js', () => ({
|
|
31
|
-
setupCapabilities: jest.fn(),
|
|
32
|
-
}));
|
|
33
|
-
jest.mock('./middleware/loggingMiddleware.js', () => ({
|
|
34
|
-
enhanceServerWithLogging: jest.fn(),
|
|
35
|
-
}));
|
|
36
|
-
describe('ServerManager', () => {
|
|
37
|
-
let mockConfig;
|
|
38
|
-
let mockCapabilities;
|
|
39
|
-
let mockClients;
|
|
40
|
-
let mockTransports;
|
|
41
|
-
let mockTransport;
|
|
42
|
-
let mockServer;
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
// Reset all mocks
|
|
45
|
-
jest.clearAllMocks();
|
|
46
|
-
// Setup test data
|
|
47
|
-
mockConfig = { name: 'test-server', version: '1.0.0' };
|
|
48
|
-
mockCapabilities = { capabilities: { test: true } };
|
|
49
|
-
mockClients = {};
|
|
50
|
-
mockTransports = {};
|
|
51
|
-
mockTransport = {
|
|
52
|
-
// Add any required Transport properties here
|
|
53
|
-
};
|
|
54
|
-
mockServer = {
|
|
55
|
-
connect: jest.fn().mockResolvedValue(undefined),
|
|
56
|
-
transport: mockTransport,
|
|
57
|
-
};
|
|
58
|
-
// Setup mocks
|
|
59
|
-
Server.mockImplementation(() => mockServer);
|
|
60
|
-
setupCapabilities.mockResolvedValue(undefined);
|
|
61
|
-
enhanceServerWithLogging.mockReturnValue(undefined);
|
|
62
|
-
configReloadService.initialize.mockImplementation(() => undefined);
|
|
63
|
-
});
|
|
64
|
-
describe('getInstance', () => {
|
|
65
|
-
it('should create a singleton instance', () => {
|
|
66
|
-
const instance1 = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
|
|
67
|
-
const instance2 = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
|
|
68
|
-
expect(instance1).toBe(instance2);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
describe('connectTransport', () => {
|
|
72
|
-
let serverManager;
|
|
73
|
-
const sessionId = 'test-session';
|
|
74
|
-
const tags = ['tag1', 'tag2'];
|
|
75
|
-
beforeEach(() => {
|
|
76
|
-
serverManager = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
|
|
77
|
-
});
|
|
78
|
-
it('should successfully connect a transport', async () => {
|
|
79
|
-
await serverManager.connectTransport(mockTransport, sessionId, tags);
|
|
80
|
-
expect(Server).toHaveBeenCalledWith(mockConfig, mockCapabilities);
|
|
81
|
-
expect(enhanceServerWithLogging).toHaveBeenCalledWith(mockServer);
|
|
82
|
-
expect(setupCapabilities).toHaveBeenCalled();
|
|
83
|
-
expect(configReloadService.initialize).toHaveBeenCalled();
|
|
84
|
-
expect(mockServer.connect).toHaveBeenCalledWith(mockTransport);
|
|
85
|
-
expect(logger.info).toHaveBeenCalledWith(`Connected transport for session ${sessionId}`);
|
|
86
|
-
});
|
|
87
|
-
it('should handle connection errors', async () => {
|
|
88
|
-
const error = new Error('Connection failed');
|
|
89
|
-
mockServer.connect.mockRejectedValueOnce(error);
|
|
90
|
-
await expect(serverManager.connectTransport(mockTransport, sessionId, tags)).rejects.toThrow('Connection failed');
|
|
91
|
-
expect(logger.error).toHaveBeenCalled();
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
describe('disconnectTransport', () => {
|
|
95
|
-
let serverManager;
|
|
96
|
-
const sessionId = 'test-session';
|
|
97
|
-
beforeEach(() => {
|
|
98
|
-
serverManager = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
|
|
99
|
-
});
|
|
100
|
-
it('should successfully disconnect a transport', async () => {
|
|
101
|
-
await serverManager.connectTransport(mockTransport, sessionId);
|
|
102
|
-
jest.clearAllMocks(); // Clear the logs from connectTransport
|
|
103
|
-
serverManager.disconnectTransport(sessionId);
|
|
104
|
-
expect(logger.info).toHaveBeenCalledWith(`Disconnected transport for session ${sessionId}`);
|
|
105
|
-
});
|
|
106
|
-
it('should handle non-existent session gracefully', () => {
|
|
107
|
-
serverManager.disconnectTransport('non-existent');
|
|
108
|
-
expect(logger.info).not.toHaveBeenCalled();
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
describe('transport management methods', () => {
|
|
112
|
-
let serverManager;
|
|
113
|
-
const sessionId = 'test-session';
|
|
114
|
-
beforeEach(async () => {
|
|
115
|
-
serverManager = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
|
|
116
|
-
await serverManager.connectTransport(mockTransport, sessionId);
|
|
117
|
-
});
|
|
118
|
-
it('should get transport by session id', () => {
|
|
119
|
-
const transport = serverManager.getTransport(sessionId);
|
|
120
|
-
expect(transport).toBe(mockTransport);
|
|
121
|
-
});
|
|
122
|
-
it('should return undefined for non-existent session', () => {
|
|
123
|
-
const transport = serverManager.getTransport('non-existent');
|
|
124
|
-
expect(transport).toBeUndefined();
|
|
125
|
-
});
|
|
126
|
-
it('should get all transports', () => {
|
|
127
|
-
const transports = serverManager.getTransports();
|
|
128
|
-
expect(transports.size).toBe(1);
|
|
129
|
-
expect(transports.get(sessionId)).toBe(mockTransport);
|
|
130
|
-
});
|
|
131
|
-
it('should get client transports', () => {
|
|
132
|
-
const clientTransports = serverManager.getClientTransports();
|
|
133
|
-
expect(clientTransports).toEqual(mockTransports);
|
|
134
|
-
});
|
|
135
|
-
it('should get active transports count', () => {
|
|
136
|
-
expect(serverManager.getActiveTransportsCount()).toBe(1);
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
describe('getServer', () => {
|
|
140
|
-
let serverManager;
|
|
141
|
-
const sessionId = 'test-session';
|
|
142
|
-
const tags = ['tag1', 'tag2'];
|
|
143
|
-
beforeEach(async () => {
|
|
144
|
-
serverManager = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
|
|
145
|
-
await serverManager.connectTransport(mockTransport, sessionId, tags);
|
|
146
|
-
});
|
|
147
|
-
it('should return server info for existing session', () => {
|
|
148
|
-
const serverInfo = serverManager.getServer(sessionId);
|
|
149
|
-
expect(serverInfo).toBeDefined();
|
|
150
|
-
expect(serverInfo?.server).toBe(mockServer);
|
|
151
|
-
expect(serverInfo?.tags).toEqual(tags);
|
|
152
|
-
});
|
|
153
|
-
it('should return undefined for non-existent session', () => {
|
|
154
|
-
const serverInfo = serverManager.getServer('non-existent');
|
|
155
|
-
expect(serverInfo).toBeUndefined();
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
});
|
|
@@ -1,44 +0,0 @@
|
|
|
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
|
-
};
|