repoburg 1.3.163 → 1.3.165
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/backend/.env +1 -0
- package/backend/dist/src/llm-orchestration/llm-orchestration.interfaces.d.ts +2 -1
- package/backend/dist/src/llm-orchestration/llm-orchestration.interfaces.js.map +1 -1
- package/backend/dist/src/mcp-server/mcp-internal.controller.d.ts +3 -11
- package/backend/dist/src/mcp-server/mcp-internal.controller.js.map +1 -1
- package/backend/dist/src/mcp-server/mcp-server.bootstrap.js +9 -7
- package/backend/dist/src/mcp-server/mcp-server.bootstrap.js.map +1 -1
- package/backend/dist/src/mcp-server/mcp-server.constants.d.ts +1 -0
- package/backend/dist/src/mcp-server/mcp-server.constants.js.map +1 -1
- package/backend/dist/src/mcp-server/mcp-server.controller.d.ts +1 -9
- package/backend/dist/src/mcp-server/mcp-server.module.js +1 -5
- package/backend/dist/src/mcp-server/mcp-server.module.js.map +1 -1
- package/backend/dist/src/mcp-server/mcp-server.process.service.js.map +1 -1
- package/backend/dist/src/mcp-server/mcp-server.service.d.ts +9 -13
- package/backend/dist/src/mcp-server/mcp-server.service.js +27 -24
- package/backend/dist/src/mcp-server/mcp-server.service.js.map +1 -1
- package/backend/dist/src/tool-hooks/hook-context.interface.d.ts +2 -1
- package/backend/dist/src/utils/tool-schema-converter.d.ts +8 -3
- package/backend/dist/src/utils/tool-schema-converter.js.map +1 -1
- package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
- package/daemon/dist/mcp-gateway.js +38 -18
- package/daemon/dist/mcp-gateway.spec.js +295 -0
- package/daemon/package.json +15 -2
- package/package.json +1 -1
- package/platform-cli.js +264 -1
|
@@ -25,6 +25,11 @@ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
|
25
25
|
const crypto_1 = require("crypto");
|
|
26
26
|
const stateManager_1 = require("./stateManager");
|
|
27
27
|
const serviceManager_1 = require("./serviceManager");
|
|
28
|
+
/**
|
|
29
|
+
* Daemon-level tool names exposed by the gateway (always available, no
|
|
30
|
+
* instance routing needed).
|
|
31
|
+
*/
|
|
32
|
+
const DAEMON_TOOL_NAMES = ['list_instances', 'start_instance', 'stop_instance'];
|
|
28
33
|
// ---------------------------------------------------------------------------
|
|
29
34
|
// Gateway
|
|
30
35
|
// ---------------------------------------------------------------------------
|
|
@@ -33,6 +38,10 @@ const INSTANCE_NAME_PARAM = {
|
|
|
33
38
|
type: 'string',
|
|
34
39
|
description: 'Name of the Repoburg instance to manage. Use the list_instances tool to see available instances.',
|
|
35
40
|
};
|
|
41
|
+
/** Extracts an error message from an unknown catch value. */
|
|
42
|
+
function errorMessage(err) {
|
|
43
|
+
return err instanceof Error ? err.message : String(err);
|
|
44
|
+
}
|
|
36
45
|
class McpGateway {
|
|
37
46
|
constructor() {
|
|
38
47
|
this.sessions = new Map();
|
|
@@ -101,7 +110,7 @@ class McpGateway {
|
|
|
101
110
|
res.status(405).send('Method not allowed');
|
|
102
111
|
}
|
|
103
112
|
catch (err) {
|
|
104
|
-
console.error(`[McpGateway] Error handling MCP request: ${err
|
|
113
|
+
console.error(`[McpGateway] Error handling MCP request: ${errorMessage(err)}`);
|
|
105
114
|
if (!res.headersSent) {
|
|
106
115
|
res.status(500).send('Internal server error');
|
|
107
116
|
}
|
|
@@ -127,17 +136,23 @@ class McpGateway {
|
|
|
127
136
|
const { name, arguments: args } = request.params;
|
|
128
137
|
const toolArgs = args ?? {};
|
|
129
138
|
// Daemon-level tools
|
|
139
|
+
let result;
|
|
130
140
|
if (name === 'list_instances') {
|
|
131
|
-
|
|
141
|
+
result = this.handleListInstances();
|
|
142
|
+
}
|
|
143
|
+
else if (name === 'start_instance') {
|
|
144
|
+
result = await this.handleStartInstance(toolArgs);
|
|
132
145
|
}
|
|
133
|
-
if (name === '
|
|
134
|
-
|
|
146
|
+
else if (name === 'stop_instance') {
|
|
147
|
+
result = await this.handleStopInstance(toolArgs);
|
|
135
148
|
}
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
else {
|
|
150
|
+
// manage_* tools — proxy to the target instance
|
|
151
|
+
result = await this.handleManageTool(name, toolArgs);
|
|
138
152
|
}
|
|
139
|
-
//
|
|
140
|
-
|
|
153
|
+
// Cast to the SDK's expected ServerResult union — structurally compatible
|
|
154
|
+
// but the Zod-inferred type is too narrow for direct assignment.
|
|
155
|
+
return result;
|
|
141
156
|
});
|
|
142
157
|
return server;
|
|
143
158
|
}
|
|
@@ -165,12 +180,16 @@ class McpGateway {
|
|
|
165
180
|
console.warn(`[McpGateway] Failed to fetch tools from instance '${runningInstance.name}' (${resp.status}).`);
|
|
166
181
|
return;
|
|
167
182
|
}
|
|
168
|
-
const
|
|
169
|
-
this.toolCache =
|
|
183
|
+
const rawTools = (await resp.json());
|
|
184
|
+
this.toolCache = rawTools.map((raw) => this.augmentWithInstanceName({
|
|
185
|
+
name: raw.tool_name,
|
|
186
|
+
description: raw.description,
|
|
187
|
+
inputSchema: raw.input_schema,
|
|
188
|
+
}));
|
|
170
189
|
console.log(`[McpGateway] Cached ${this.toolCache.length} manage_* tools from instance '${runningInstance.name}'.`);
|
|
171
190
|
}
|
|
172
191
|
catch (err) {
|
|
173
|
-
console.warn(`[McpGateway] Could not refresh tool cache: ${err
|
|
192
|
+
console.warn(`[McpGateway] Could not refresh tool cache: ${errorMessage(err)}`);
|
|
174
193
|
}
|
|
175
194
|
finally {
|
|
176
195
|
this.isRefreshing = false;
|
|
@@ -256,21 +275,22 @@ class McpGateway {
|
|
|
256
275
|
}
|
|
257
276
|
async handleStartInstance(args) {
|
|
258
277
|
const projectPath = args.path;
|
|
259
|
-
if (
|
|
278
|
+
if (typeof projectPath !== 'string') {
|
|
260
279
|
return this.errorResult('Missing required parameter: path');
|
|
261
280
|
}
|
|
281
|
+
const name = typeof args.name === 'string' ? args.name : undefined;
|
|
262
282
|
try {
|
|
263
|
-
const service = await serviceManager_1.serviceManager.start(projectPath,
|
|
283
|
+
const service = await serviceManager_1.serviceManager.start(projectPath, name);
|
|
264
284
|
this.invalidateToolCache();
|
|
265
285
|
return this.textResult(`Instance '${service.name}' started on port ${service.port} (path: ${service.path}).`);
|
|
266
286
|
}
|
|
267
287
|
catch (err) {
|
|
268
|
-
return this.errorResult(`Failed to start instance: ${err
|
|
288
|
+
return this.errorResult(`Failed to start instance: ${errorMessage(err)}`);
|
|
269
289
|
}
|
|
270
290
|
}
|
|
271
291
|
async handleStopInstance(args) {
|
|
272
292
|
const name = args.name;
|
|
273
|
-
if (
|
|
293
|
+
if (typeof name !== 'string') {
|
|
274
294
|
return this.errorResult('Missing required parameter: name');
|
|
275
295
|
}
|
|
276
296
|
try {
|
|
@@ -278,7 +298,7 @@ class McpGateway {
|
|
|
278
298
|
return this.textResult(`Instance '${name}' stopped.`);
|
|
279
299
|
}
|
|
280
300
|
catch (err) {
|
|
281
|
-
return this.errorResult(`Failed to stop instance: ${err
|
|
301
|
+
return this.errorResult(`Failed to stop instance: ${errorMessage(err)}`);
|
|
282
302
|
}
|
|
283
303
|
}
|
|
284
304
|
// -------------------------------------------------------------------------
|
|
@@ -286,7 +306,7 @@ class McpGateway {
|
|
|
286
306
|
// -------------------------------------------------------------------------
|
|
287
307
|
async handleManageTool(toolName, args) {
|
|
288
308
|
const instanceName = args.instance_name;
|
|
289
|
-
if (
|
|
309
|
+
if (typeof instanceName !== 'string') {
|
|
290
310
|
return this.errorResult(`Missing required parameter: instance_name. Use the list_instances tool to see available instances.`);
|
|
291
311
|
}
|
|
292
312
|
const service = stateManager_1.stateManager.getService(instanceName);
|
|
@@ -314,7 +334,7 @@ class McpGateway {
|
|
|
314
334
|
return this.toToolResult(result);
|
|
315
335
|
}
|
|
316
336
|
catch (err) {
|
|
317
|
-
return this.errorResult(`Failed to reach instance '${instanceName}': ${err
|
|
337
|
+
return this.errorResult(`Failed to reach instance '${instanceName}': ${errorMessage(err)}`);
|
|
318
338
|
}
|
|
319
339
|
}
|
|
320
340
|
// -------------------------------------------------------------------------
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for the MCP gateway — the daemon-hosted single MCP entry point that
|
|
4
|
+
* manages all Repoburg instances.
|
|
5
|
+
*
|
|
6
|
+
* Tests focus on:
|
|
7
|
+
* - Tool cache refresh (fetch from running instance, augment with instance_name)
|
|
8
|
+
* - Cache invalidation
|
|
9
|
+
* - getGatewayInfo() shape
|
|
10
|
+
* - Daemon-level tool routing (list_instances, start_instance, stop_instance)
|
|
11
|
+
* - manage_* proxy routing (missing instance_name, unknown instance, not running,
|
|
12
|
+
* valid POST, fetch failure, result mapping)
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
// Mock dependencies before importing the gateway.
|
|
16
|
+
jest.mock('./stateManager', () => ({
|
|
17
|
+
stateManager: {
|
|
18
|
+
getServices: jest.fn(),
|
|
19
|
+
getService: jest.fn(),
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
jest.mock('./serviceManager', () => ({
|
|
23
|
+
serviceManager: {
|
|
24
|
+
start: jest.fn(),
|
|
25
|
+
stop: jest.fn(),
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
// Mock MCP SDK — we test the gateway's logic, not the SDK.
|
|
29
|
+
jest.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
|
|
30
|
+
Server: jest.fn().mockImplementation(() => ({
|
|
31
|
+
setRequestHandler: jest.fn(),
|
|
32
|
+
connect: jest.fn(),
|
|
33
|
+
close: jest.fn(),
|
|
34
|
+
})),
|
|
35
|
+
}));
|
|
36
|
+
jest.mock('@modelcontextprotocol/sdk/server/streamableHttp.js', () => ({
|
|
37
|
+
StreamableHTTPServerTransport: jest.fn(),
|
|
38
|
+
}));
|
|
39
|
+
jest.mock('@modelcontextprotocol/sdk/types.js', () => ({
|
|
40
|
+
CallToolRequestSchema: 'callTool',
|
|
41
|
+
ListToolsRequestSchema: 'listTools',
|
|
42
|
+
isInitializeRequest: jest.fn(),
|
|
43
|
+
}));
|
|
44
|
+
const mcp_gateway_1 = require("./mcp-gateway");
|
|
45
|
+
const stateManager_1 = require("./stateManager");
|
|
46
|
+
const serviceManager_1 = require("./serviceManager");
|
|
47
|
+
const mockStateManager = stateManager_1.stateManager;
|
|
48
|
+
const mockServiceManager = serviceManager_1.serviceManager;
|
|
49
|
+
// Helper: create a mock Service object.
|
|
50
|
+
function mockService(name, status = 'running') {
|
|
51
|
+
return { name, path: `/projects/${name}`, port: 3400, status, pid: 12345 };
|
|
52
|
+
}
|
|
53
|
+
// Helper: raw tool definition as returned by backend's GET /mcp-internal/tools.
|
|
54
|
+
function rawToolDef(name, description, required = []) {
|
|
55
|
+
return {
|
|
56
|
+
tool_name: name,
|
|
57
|
+
description,
|
|
58
|
+
input_schema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: required.reduce((acc, r) => {
|
|
61
|
+
acc[r] = { type: 'string', description: `${r} param` };
|
|
62
|
+
return acc;
|
|
63
|
+
}, {}),
|
|
64
|
+
required: required.length > 0 ? required : undefined,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
describe('McpGateway', () => {
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
jest.clearAllMocks();
|
|
71
|
+
// Reset gateway state between tests.
|
|
72
|
+
mcp_gateway_1.mcpGateway.invalidateToolCache();
|
|
73
|
+
// Default: no instances.
|
|
74
|
+
mockStateManager.getServices.mockReturnValue([]);
|
|
75
|
+
mockStateManager.getService.mockReturnValue(undefined);
|
|
76
|
+
mockServiceManager.start.mockReset();
|
|
77
|
+
mockServiceManager.stop.mockReset();
|
|
78
|
+
});
|
|
79
|
+
describe('refreshToolCache', () => {
|
|
80
|
+
it('should fetch tools from a running instance and augment with instance_name', async () => {
|
|
81
|
+
mockStateManager.getServices.mockReturnValue([mockService('alpha', 'running')]);
|
|
82
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
83
|
+
ok: true,
|
|
84
|
+
json: async () => [rawToolDef('manage_agents', 'Manage agents', ['action'])],
|
|
85
|
+
});
|
|
86
|
+
await mcp_gateway_1.mcpGateway.refreshToolCache();
|
|
87
|
+
const info = await mcp_gateway_1.mcpGateway.getGatewayInfo();
|
|
88
|
+
const manageTools = info.tools.filter((t) => t.name.startsWith('manage_'));
|
|
89
|
+
expect(manageTools).toHaveLength(1);
|
|
90
|
+
expect(manageTools[0].name).toBe('manage_agents');
|
|
91
|
+
expect(manageTools[0].inputSchema.properties).toHaveProperty('instance_name');
|
|
92
|
+
expect(manageTools[0].inputSchema.required).toContain('instance_name');
|
|
93
|
+
});
|
|
94
|
+
it('should leave cache null when no instance is running', async () => {
|
|
95
|
+
mockStateManager.getServices.mockReturnValue([mockService('alpha', 'stopped')]);
|
|
96
|
+
await mcp_gateway_1.mcpGateway.refreshToolCache();
|
|
97
|
+
const info = await mcp_gateway_1.mcpGateway.getGatewayInfo();
|
|
98
|
+
// Only daemon-level tools, no manage_* tools.
|
|
99
|
+
const manageTools = info.tools.filter((t) => t.name.startsWith('manage_'));
|
|
100
|
+
expect(manageTools).toHaveLength(0);
|
|
101
|
+
});
|
|
102
|
+
it('should skip refresh when cache is already populated', async () => {
|
|
103
|
+
mockStateManager.getServices.mockReturnValue([mockService('alpha', 'running')]);
|
|
104
|
+
const fetchMock = jest.fn().mockResolvedValue({
|
|
105
|
+
ok: true,
|
|
106
|
+
json: async () => [rawToolDef('manage_agents', 'Manage agents')],
|
|
107
|
+
});
|
|
108
|
+
global.fetch = fetchMock;
|
|
109
|
+
await mcp_gateway_1.mcpGateway.refreshToolCache();
|
|
110
|
+
await mcp_gateway_1.mcpGateway.refreshToolCache(); // second call
|
|
111
|
+
// fetch should only be called once (cache hit on second call).
|
|
112
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
113
|
+
});
|
|
114
|
+
it('should handle fetch failure gracefully (cache stays null)', async () => {
|
|
115
|
+
mockStateManager.getServices.mockReturnValue([mockService('alpha', 'running')]);
|
|
116
|
+
global.fetch = jest.fn().mockResolvedValue({ ok: false, status: 500 });
|
|
117
|
+
await mcp_gateway_1.mcpGateway.refreshToolCache();
|
|
118
|
+
const info = await mcp_gateway_1.mcpGateway.getGatewayInfo();
|
|
119
|
+
const manageTools = info.tools.filter((t) => t.name.startsWith('manage_'));
|
|
120
|
+
expect(manageTools).toHaveLength(0);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('invalidateToolCache', () => {
|
|
124
|
+
it('should clear cache so next refresh re-fetches', async () => {
|
|
125
|
+
mockStateManager.getServices.mockReturnValue([mockService('alpha', 'running')]);
|
|
126
|
+
const fetchMock = jest.fn().mockResolvedValue({
|
|
127
|
+
ok: true,
|
|
128
|
+
json: async () => [rawToolDef('manage_agents', 'Manage agents')],
|
|
129
|
+
});
|
|
130
|
+
global.fetch = fetchMock;
|
|
131
|
+
await mcp_gateway_1.mcpGateway.refreshToolCache();
|
|
132
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
133
|
+
mcp_gateway_1.mcpGateway.invalidateToolCache();
|
|
134
|
+
await mcp_gateway_1.mcpGateway.refreshToolCache();
|
|
135
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('getGatewayInfo', () => {
|
|
139
|
+
it('should return descriptor with server_name, url, instances, tools, client_config', async () => {
|
|
140
|
+
mockStateManager.getServices.mockReturnValue([
|
|
141
|
+
mockService('alpha', 'running'),
|
|
142
|
+
mockService('beta', 'stopped'),
|
|
143
|
+
]);
|
|
144
|
+
const info = await mcp_gateway_1.mcpGateway.getGatewayInfo();
|
|
145
|
+
expect(info.server_name).toBe('repoburg-gateway');
|
|
146
|
+
expect(info.url).toBe('http://localhost:9998/mcp');
|
|
147
|
+
expect(info.instances).toHaveLength(2);
|
|
148
|
+
expect(info.instances[0].name).toBe('alpha');
|
|
149
|
+
expect(info.instances[0].status).toBe('running');
|
|
150
|
+
expect(info.client_config).toContain('http://localhost:9998/mcp');
|
|
151
|
+
// Should always include daemon-level tools.
|
|
152
|
+
const toolNames = info.tools.map((t) => t.name);
|
|
153
|
+
expect(toolNames).toContain('list_instances');
|
|
154
|
+
expect(toolNames).toContain('start_instance');
|
|
155
|
+
expect(toolNames).toContain('stop_instance');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe('daemon-level tool routing', () => {
|
|
159
|
+
it('list_instances should return text with instance info', () => {
|
|
160
|
+
mockStateManager.getServices.mockReturnValue([
|
|
161
|
+
mockService('alpha', 'running'),
|
|
162
|
+
mockService('beta', 'stopped'),
|
|
163
|
+
]);
|
|
164
|
+
const result = mcp_gateway_1.mcpGateway.handleListInstances();
|
|
165
|
+
const text = result.content[0].text;
|
|
166
|
+
expect(text).toContain('alpha');
|
|
167
|
+
expect(text).toContain('running');
|
|
168
|
+
expect(text).toContain('beta');
|
|
169
|
+
expect(text).toContain('stopped');
|
|
170
|
+
expect(result.isError).toBeUndefined();
|
|
171
|
+
});
|
|
172
|
+
it('list_instances should show message when no instances', () => {
|
|
173
|
+
mockStateManager.getServices.mockReturnValue([]);
|
|
174
|
+
const result = mcp_gateway_1.mcpGateway.handleListInstances();
|
|
175
|
+
expect(result.content[0].text).toContain('No Repoburg instances');
|
|
176
|
+
});
|
|
177
|
+
it('start_instance should call serviceManager.start and invalidate cache', async () => {
|
|
178
|
+
mockServiceManager.start.mockResolvedValue(mockService('gamma', 'running'));
|
|
179
|
+
const result = await mcp_gateway_1.mcpGateway.handleStartInstance({
|
|
180
|
+
path: '/projects/gamma',
|
|
181
|
+
name: 'gamma',
|
|
182
|
+
});
|
|
183
|
+
expect(mockServiceManager.start).toHaveBeenCalledWith('/projects/gamma', 'gamma');
|
|
184
|
+
expect(result.content[0].text).toContain('gamma');
|
|
185
|
+
expect(result.isError).toBeUndefined();
|
|
186
|
+
});
|
|
187
|
+
it('start_instance should error when path is missing', async () => {
|
|
188
|
+
const result = await mcp_gateway_1.mcpGateway.handleStartInstance({});
|
|
189
|
+
expect(result.isError).toBe(true);
|
|
190
|
+
expect(result.content[0].text).toContain('Missing required parameter: path');
|
|
191
|
+
});
|
|
192
|
+
it('stop_instance should call serviceManager.stop', async () => {
|
|
193
|
+
const result = await mcp_gateway_1.mcpGateway.handleStopInstance({ name: 'alpha' });
|
|
194
|
+
expect(mockServiceManager.stop).toHaveBeenCalledWith('alpha');
|
|
195
|
+
expect(result.content[0].text).toContain('alpha');
|
|
196
|
+
expect(result.isError).toBeUndefined();
|
|
197
|
+
});
|
|
198
|
+
it('stop_instance should error when name is missing', async () => {
|
|
199
|
+
const result = await mcp_gateway_1.mcpGateway.handleStopInstance({});
|
|
200
|
+
expect(result.isError).toBe(true);
|
|
201
|
+
expect(result.content[0].text).toContain('Missing required parameter: name');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
describe('manage_* proxy routing', () => {
|
|
205
|
+
it('should error when instance_name is missing', async () => {
|
|
206
|
+
const result = await mcp_gateway_1.mcpGateway.handleManageTool('manage_agents', {
|
|
207
|
+
action: 'create',
|
|
208
|
+
});
|
|
209
|
+
expect(result.isError).toBe(true);
|
|
210
|
+
expect(result.content[0].text).toContain('instance_name');
|
|
211
|
+
});
|
|
212
|
+
it('should error when instance is not found', async () => {
|
|
213
|
+
mockStateManager.getService.mockReturnValue(undefined);
|
|
214
|
+
const result = await mcp_gateway_1.mcpGateway.handleManageTool('manage_agents', {
|
|
215
|
+
instance_name: 'unknown',
|
|
216
|
+
action: 'create',
|
|
217
|
+
});
|
|
218
|
+
expect(result.isError).toBe(true);
|
|
219
|
+
expect(result.content[0].text).toContain("not found");
|
|
220
|
+
});
|
|
221
|
+
it('should error when instance is not running', async () => {
|
|
222
|
+
mockStateManager.getService.mockReturnValue(mockService('alpha', 'stopped'));
|
|
223
|
+
const result = await mcp_gateway_1.mcpGateway.handleManageTool('manage_agents', {
|
|
224
|
+
instance_name: 'alpha',
|
|
225
|
+
action: 'create',
|
|
226
|
+
});
|
|
227
|
+
expect(result.isError).toBe(true);
|
|
228
|
+
expect(result.content[0].text).toContain('not running');
|
|
229
|
+
});
|
|
230
|
+
it('should strip instance_name and POST to the instance backend', async () => {
|
|
231
|
+
mockStateManager.getService.mockReturnValue(mockService('alpha', 'running'));
|
|
232
|
+
const fetchMock = jest.fn().mockResolvedValue({
|
|
233
|
+
ok: true,
|
|
234
|
+
json: async () => ({
|
|
235
|
+
status: 'SUCCESS',
|
|
236
|
+
summary: 'Agent created',
|
|
237
|
+
execution_log: { output: 'Created agent "test"' },
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
global.fetch = fetchMock;
|
|
241
|
+
const result = await mcp_gateway_1.mcpGateway.handleManageTool('manage_agents', {
|
|
242
|
+
instance_name: 'alpha',
|
|
243
|
+
action: 'create',
|
|
244
|
+
name: 'test',
|
|
245
|
+
});
|
|
246
|
+
expect(fetchMock).toHaveBeenCalledWith('http://localhost:3400/mcp-internal/execute-tool', expect.objectContaining({
|
|
247
|
+
method: 'POST',
|
|
248
|
+
body: JSON.stringify({
|
|
249
|
+
tool_name: 'manage_agents',
|
|
250
|
+
arguments: { action: 'create', name: 'test' }, // instance_name stripped
|
|
251
|
+
}),
|
|
252
|
+
}));
|
|
253
|
+
expect(result.isError).toBeUndefined();
|
|
254
|
+
expect(result.content[0].text).toContain('Created agent "test"');
|
|
255
|
+
});
|
|
256
|
+
it('should map FAILURE result to isError: true', async () => {
|
|
257
|
+
mockStateManager.getService.mockReturnValue(mockService('alpha', 'running'));
|
|
258
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
259
|
+
ok: true,
|
|
260
|
+
json: async () => ({
|
|
261
|
+
status: 'FAILURE',
|
|
262
|
+
summary: 'Validation error',
|
|
263
|
+
error_message: 'Name is required',
|
|
264
|
+
execution_log: { output: '', error_message: 'Name is required' },
|
|
265
|
+
}),
|
|
266
|
+
});
|
|
267
|
+
const result = await mcp_gateway_1.mcpGateway.handleManageTool('manage_agents', {
|
|
268
|
+
instance_name: 'alpha',
|
|
269
|
+
action: 'create',
|
|
270
|
+
});
|
|
271
|
+
expect(result.isError).toBe(true);
|
|
272
|
+
expect(result.content[0].text).toContain('Name is required');
|
|
273
|
+
});
|
|
274
|
+
it('should error when fetch to backend fails', async () => {
|
|
275
|
+
mockStateManager.getService.mockReturnValue(mockService('alpha', 'running'));
|
|
276
|
+
global.fetch = jest.fn().mockRejectedValue(new Error('ECONNREFUSED'));
|
|
277
|
+
const result = await mcp_gateway_1.mcpGateway.handleManageTool('manage_agents', {
|
|
278
|
+
instance_name: 'alpha',
|
|
279
|
+
action: 'create',
|
|
280
|
+
});
|
|
281
|
+
expect(result.isError).toBe(true);
|
|
282
|
+
expect(result.content[0].text).toContain('ECONNREFUSED');
|
|
283
|
+
});
|
|
284
|
+
it('should error when backend returns non-ok HTTP status', async () => {
|
|
285
|
+
mockStateManager.getService.mockReturnValue(mockService('alpha', 'running'));
|
|
286
|
+
global.fetch = jest.fn().mockResolvedValue({ ok: false, status: 500 });
|
|
287
|
+
const result = await mcp_gateway_1.mcpGateway.handleManageTool('manage_agents', {
|
|
288
|
+
instance_name: 'alpha',
|
|
289
|
+
action: 'create',
|
|
290
|
+
});
|
|
291
|
+
expect(result.isError).toBe(true);
|
|
292
|
+
expect(result.content[0].text).toContain('500');
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|
package/daemon/package.json
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"start": "cross-env NODE_ENV=production node dist/index.js",
|
|
10
|
-
"dev": "NODE_ENV=development ts-node-dev --respawn --transpile-only src/index.ts"
|
|
10
|
+
"dev": "NODE_ENV=development ts-node-dev --respawn --transpile-only src/index.ts",
|
|
11
|
+
"test": "jest"
|
|
11
12
|
},
|
|
12
13
|
"keywords": [],
|
|
13
14
|
"author": "Celal Ertug",
|
|
@@ -16,9 +17,21 @@
|
|
|
16
17
|
"@types/cors": "^2.8.17",
|
|
17
18
|
"@types/express": "^4.17.21",
|
|
18
19
|
"@types/fs-extra": "^11.0.4",
|
|
20
|
+
"@types/jest": "^29.5.14",
|
|
19
21
|
"@types/node": "^20.14.9",
|
|
20
22
|
"@types/ws": "^8.5.10",
|
|
23
|
+
"jest": "^29.7.0",
|
|
24
|
+
"ts-jest": "^29.4.11",
|
|
21
25
|
"ts-node-dev": "^2.0.0",
|
|
22
26
|
"typescript": "^5.5.2"
|
|
27
|
+
},
|
|
28
|
+
"jest": {
|
|
29
|
+
"moduleFileExtensions": ["js", "json", "ts"],
|
|
30
|
+
"rootDir": "src",
|
|
31
|
+
"testRegex": ".*\\.spec\\.ts$",
|
|
32
|
+
"transform": {
|
|
33
|
+
"^.+\\.(t|j)s$": ["ts-jest", { "isolatedModules": true }]
|
|
34
|
+
},
|
|
35
|
+
"testEnvironment": "node"
|
|
23
36
|
}
|
|
24
|
-
}
|
|
37
|
+
}
|