repoburg 1.3.162 → 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.
Files changed (51) hide show
  1. package/backend/.env +1 -27
  2. package/backend/dist/src/app.module.js +2 -0
  3. package/backend/dist/src/app.module.js.map +1 -1
  4. package/backend/dist/src/application-state/application-state.service.d.ts +4 -0
  5. package/backend/dist/src/application-state/application-state.service.js +24 -0
  6. package/backend/dist/src/application-state/application-state.service.js.map +1 -1
  7. package/backend/dist/src/llm-orchestration/llm-orchestration.interfaces.d.ts +2 -1
  8. package/backend/dist/src/llm-orchestration/llm-orchestration.interfaces.js.map +1 -1
  9. package/backend/dist/src/llm-orchestration/tool-schema.service.js +2 -24
  10. package/backend/dist/src/llm-orchestration/tool-schema.service.js.map +1 -1
  11. package/backend/dist/src/mcp-server/mcp-internal.controller.d.ts +12 -0
  12. package/backend/dist/src/mcp-server/mcp-internal.controller.js +50 -0
  13. package/backend/dist/src/mcp-server/mcp-internal.controller.js.map +1 -0
  14. package/backend/dist/src/mcp-server/mcp-server.bootstrap.d.ts +2 -0
  15. package/backend/dist/src/mcp-server/mcp-server.bootstrap.js +162 -0
  16. package/backend/dist/src/mcp-server/mcp-server.bootstrap.js.map +1 -0
  17. package/backend/dist/src/mcp-server/mcp-server.constants.d.ts +8 -0
  18. package/backend/dist/src/mcp-server/mcp-server.constants.js +19 -0
  19. package/backend/dist/src/mcp-server/mcp-server.constants.js.map +1 -0
  20. package/backend/dist/src/mcp-server/mcp-server.controller.d.ts +19 -0
  21. package/backend/dist/src/mcp-server/mcp-server.controller.js +72 -0
  22. package/backend/dist/src/mcp-server/mcp-server.controller.js.map +1 -0
  23. package/backend/dist/src/mcp-server/mcp-server.module.d.ts +2 -0
  24. package/backend/dist/src/mcp-server/mcp-server.module.js +29 -0
  25. package/backend/dist/src/mcp-server/mcp-server.module.js.map +1 -0
  26. package/backend/dist/src/mcp-server/mcp-server.process.service.d.ts +23 -0
  27. package/backend/dist/src/mcp-server/mcp-server.process.service.js +130 -0
  28. package/backend/dist/src/mcp-server/mcp-server.process.service.js.map +1 -0
  29. package/backend/dist/src/mcp-server/mcp-server.service.d.ts +34 -0
  30. package/backend/dist/src/mcp-server/mcp-server.service.js +260 -0
  31. package/backend/dist/src/mcp-server/mcp-server.service.js.map +1 -0
  32. package/backend/dist/src/tool-hooks/hook-context.interface.d.ts +2 -1
  33. package/backend/dist/src/utils/index.d.ts +1 -0
  34. package/backend/dist/src/utils/index.js +1 -0
  35. package/backend/dist/src/utils/index.js.map +1 -1
  36. package/backend/dist/src/utils/tool-schema-converter.d.ts +13 -0
  37. package/backend/dist/src/utils/tool-schema-converter.js +33 -0
  38. package/backend/dist/src/utils/tool-schema-converter.js.map +1 -0
  39. package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
  40. package/backend/package.json +2 -0
  41. package/daemon/dist/index.js +25 -0
  42. package/daemon/dist/mcp-gateway.js +401 -0
  43. package/daemon/dist/mcp-gateway.spec.js +295 -0
  44. package/daemon/package.json +15 -2
  45. package/package.json +1 -1
  46. package/platform-cli.js +264 -1
  47. package/backend/database.sqlite +0 -0
  48. package/backend/section-modified.txt +0 -1
  49. package/daemon/dist/api/system.js +0 -25
  50. package/daemon/dist/daemon.blob +0 -0
  51. package/visual-editor-proxy/.repoburg/data.sqlite +0 -0
@@ -12,6 +12,8 @@
12
12
  "start:dev": "mkdir -p ../.repoburg/logs && NODE_ENV=production script -q ../.repoburg/logs/backend.log nest start --watch",
13
13
  "start:debug": "NODE_ENV=production nest start --debug --watch",
14
14
  "start:prod": "NODE_ENV=production node dist/main",
15
+ "mcp-server": "ts-node -r tsconfig-paths/register src/mcp-server/mcp-server.bootstrap.ts",
16
+ "mcp-server:prod": "NODE_ENV=production node dist/mcp-server/mcp-server.bootstrap.js",
15
17
  "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
16
18
  "test": "NODE_ENV=development NODE_OPTIONS=--experimental-vm-modules jest --bail",
17
19
  "test:watch": "jest --watch",
@@ -12,6 +12,7 @@ const serviceManager_1 = require("./serviceManager");
12
12
  const services_1 = __importDefault(require("./api/services"));
13
13
  const auth_1 = __importDefault(require("./api/auth"));
14
14
  const registry_1 = __importDefault(require("./api/registry"));
15
+ const mcp_gateway_1 = require("./mcp-gateway");
15
16
  const PORT = 9998;
16
17
  async function main() {
17
18
  await serviceManager_1.serviceManager.connect();
@@ -28,6 +29,20 @@ async function main() {
28
29
  app.use('/services', services_1.default);
29
30
  app.use('/auth', (0, auth_1.default)(wss));
30
31
  app.use('/registry', registry_1.default);
32
+ // MCP Gateway — single MCP entry point managing all instances (streamableHttp).
33
+ app.post('/mcp', (req, res) => mcp_gateway_1.mcpGateway.handleExpressRequest(req, res));
34
+ app.get('/mcp', (req, res) => mcp_gateway_1.mcpGateway.handleExpressRequest(req, res));
35
+ app.delete('/mcp', (req, res) => mcp_gateway_1.mcpGateway.handleExpressRequest(req, res));
36
+ // MCP Gateway info endpoint for the management UI.
37
+ app.get('/mcp-gateway-info', async (req, res) => {
38
+ try {
39
+ const info = await mcp_gateway_1.mcpGateway.getGatewayInfo();
40
+ res.json(info);
41
+ }
42
+ catch (err) {
43
+ res.status(500).json({ error: err.message });
44
+ }
45
+ });
31
46
  app.get('/health', (req, res) => {
32
47
  res.status(200).json({ status: 'ok' });
33
48
  });
@@ -38,6 +53,16 @@ async function main() {
38
53
  });
39
54
  server.listen(PORT, () => {
40
55
  console.log(`Repoburg Platform Daemon listening on http://localhost:${PORT}`);
56
+ console.log(`MCP Gateway available at http://localhost:${PORT}/mcp`);
57
+ });
58
+ // Clean up MCP gateway sessions on shutdown.
59
+ process.on('SIGINT', async () => {
60
+ await mcp_gateway_1.mcpGateway.closeAllSessions();
61
+ process.exit(0);
62
+ });
63
+ process.on('SIGTERM', async () => {
64
+ await mcp_gateway_1.mcpGateway.closeAllSessions();
65
+ process.exit(0);
41
66
  });
42
67
  }
43
68
  main().catch(err => {
@@ -0,0 +1,401 @@
1
+ "use strict";
2
+ /**
3
+ * MCP Gateway — a single MCP server entry point hosted in the daemon that
4
+ * manages **all** Repoburg instances.
5
+ *
6
+ * MCP clients (ChatGPT tunnel, Claude Desktop) connect once to
7
+ * `http://localhost:9998/mcp`. Each `manage_*` tool call includes an
8
+ * `instance_name` parameter; the gateway looks up the instance's backend port
9
+ * from `stateManager` and proxies the call to that instance's
10
+ * `POST /mcp-internal/execute-tool` endpoint.
11
+ *
12
+ * The gateway also exposes daemon-level tools (`list_instances`,
13
+ * `start_instance`, `stop_instance`) that operate on the daemon's own PM2
14
+ * process registry — no instance routing needed.
15
+ *
16
+ * Tool definitions for `manage_*` are fetched once from any running instance's
17
+ * `GET /mcp-internal/tools` endpoint, augmented with an `instance_name`
18
+ * required parameter, and cached.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.mcpGateway = void 0;
22
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
23
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
24
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
25
+ const crypto_1 = require("crypto");
26
+ const stateManager_1 = require("./stateManager");
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'];
33
+ // ---------------------------------------------------------------------------
34
+ // Gateway
35
+ // ---------------------------------------------------------------------------
36
+ const GATEWAY_INFO = { name: 'repoburg-mcp-gateway', version: '1.0.0' };
37
+ const INSTANCE_NAME_PARAM = {
38
+ type: 'string',
39
+ description: 'Name of the Repoburg instance to manage. Use the list_instances tool to see available instances.',
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
+ }
45
+ class McpGateway {
46
+ constructor() {
47
+ this.sessions = new Map();
48
+ this.toolCache = null;
49
+ this.isRefreshing = false;
50
+ }
51
+ // -------------------------------------------------------------------------
52
+ // Express route handler — mounted at app.post/get/delete('/mcp', ...)
53
+ // -------------------------------------------------------------------------
54
+ /**
55
+ * Express-compatible handler for the `/mcp` route. Manages stateful
56
+ * streamableHttp sessions: a new transport + Server is created on the
57
+ * `initialize` request and reused for subsequent requests on that session.
58
+ */
59
+ async handleExpressRequest(req, res) {
60
+ try {
61
+ if (req.method === 'POST') {
62
+ const body = req.body;
63
+ const sessionId = req.headers['mcp-session-id'];
64
+ const existing = sessionId ? this.sessions.get(sessionId) : undefined;
65
+ if (existing) {
66
+ await existing.handleRequest(req, res, body);
67
+ return;
68
+ }
69
+ if (body && (0, types_js_1.isInitializeRequest)(body)) {
70
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
71
+ sessionIdGenerator: () => (0, crypto_1.randomUUID)(),
72
+ onsessioninitialized: (sid) => {
73
+ this.sessions.set(sid, transport);
74
+ },
75
+ onsessionclosed: (sid) => {
76
+ this.sessions.delete(sid);
77
+ },
78
+ });
79
+ transport.onclose = () => {
80
+ const sid = transport.sessionId;
81
+ if (sid)
82
+ this.sessions.delete(sid);
83
+ };
84
+ const server = this.createMcpServerInstance();
85
+ await server.connect(transport);
86
+ await transport.handleRequest(req, res, body);
87
+ return;
88
+ }
89
+ // No valid session — reject.
90
+ res.status(400).json({
91
+ jsonrpc: '2.0',
92
+ error: {
93
+ code: -32000,
94
+ message: 'Bad Request: no valid session ID provided',
95
+ },
96
+ id: null,
97
+ });
98
+ return;
99
+ }
100
+ if (req.method === 'GET' || req.method === 'DELETE') {
101
+ const sessionId = req.headers['mcp-session-id'];
102
+ const transport = sessionId ? this.sessions.get(sessionId) : undefined;
103
+ if (!transport) {
104
+ res.status(400).send('Invalid or missing session ID');
105
+ return;
106
+ }
107
+ await transport.handleRequest(req, res);
108
+ return;
109
+ }
110
+ res.status(405).send('Method not allowed');
111
+ }
112
+ catch (err) {
113
+ console.error(`[McpGateway] Error handling MCP request: ${errorMessage(err)}`);
114
+ if (!res.headersSent) {
115
+ res.status(500).send('Internal server error');
116
+ }
117
+ }
118
+ }
119
+ // -------------------------------------------------------------------------
120
+ // MCP Server factory
121
+ // -------------------------------------------------------------------------
122
+ /**
123
+ * Creates a fresh MCP `Server` wired with ListTools + CallTool handlers.
124
+ * One Server per client session.
125
+ */
126
+ createMcpServerInstance() {
127
+ const server = new index_js_1.Server(GATEWAY_INFO, { capabilities: { tools: {} } });
128
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
129
+ // Best-effort cache refresh (non-blocking if already refreshing).
130
+ await this.refreshToolCache();
131
+ const daemonTools = this.buildDaemonTools();
132
+ const manageTools = this.toolCache ?? [];
133
+ return { tools: [...daemonTools, ...manageTools] };
134
+ });
135
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
136
+ const { name, arguments: args } = request.params;
137
+ const toolArgs = args ?? {};
138
+ // Daemon-level tools
139
+ let result;
140
+ if (name === 'list_instances') {
141
+ result = this.handleListInstances();
142
+ }
143
+ else if (name === 'start_instance') {
144
+ result = await this.handleStartInstance(toolArgs);
145
+ }
146
+ else if (name === 'stop_instance') {
147
+ result = await this.handleStopInstance(toolArgs);
148
+ }
149
+ else {
150
+ // manage_* tools — proxy to the target instance
151
+ result = await this.handleManageTool(name, toolArgs);
152
+ }
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;
156
+ });
157
+ return server;
158
+ }
159
+ // -------------------------------------------------------------------------
160
+ // Tool cache management
161
+ // -------------------------------------------------------------------------
162
+ /**
163
+ * Fetches tool definitions from a running instance's
164
+ * `GET /mcp-internal/tools` endpoint, augments each with `instance_name`,
165
+ * and caches the result. Safe to call repeatedly — skips if already
166
+ * refreshing or cache is already populated.
167
+ */
168
+ async refreshToolCache() {
169
+ if (this.isRefreshing || this.toolCache)
170
+ return;
171
+ this.isRefreshing = true;
172
+ try {
173
+ const runningInstance = this.findRunningInstance();
174
+ if (!runningInstance) {
175
+ // No instance running — cache stays null; daemon tools still available.
176
+ return;
177
+ }
178
+ const resp = await fetch(`http://localhost:${runningInstance.port}/mcp-internal/tools`);
179
+ if (!resp.ok) {
180
+ console.warn(`[McpGateway] Failed to fetch tools from instance '${runningInstance.name}' (${resp.status}).`);
181
+ return;
182
+ }
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
+ }));
189
+ console.log(`[McpGateway] Cached ${this.toolCache.length} manage_* tools from instance '${runningInstance.name}'.`);
190
+ }
191
+ catch (err) {
192
+ console.warn(`[McpGateway] Could not refresh tool cache: ${errorMessage(err)}`);
193
+ }
194
+ finally {
195
+ this.isRefreshing = false;
196
+ }
197
+ }
198
+ /**
199
+ * Forces a cache refresh (called after an instance starts). Clears the cache
200
+ * first so the next ListTools re-fetches.
201
+ */
202
+ invalidateToolCache() {
203
+ this.toolCache = null;
204
+ }
205
+ /**
206
+ * Adds `instance_name` as a required string parameter to a tool definition.
207
+ */
208
+ augmentWithInstanceName(tool) {
209
+ const properties = { ...tool.inputSchema.properties, instance_name: INSTANCE_NAME_PARAM };
210
+ const required = [
211
+ ...(tool.inputSchema.required ?? []),
212
+ 'instance_name',
213
+ ];
214
+ return {
215
+ name: tool.name,
216
+ description: tool.description,
217
+ inputSchema: {
218
+ type: 'object',
219
+ properties,
220
+ required,
221
+ },
222
+ };
223
+ }
224
+ // -------------------------------------------------------------------------
225
+ // Daemon-level tool handlers
226
+ // -------------------------------------------------------------------------
227
+ buildDaemonTools() {
228
+ return [
229
+ {
230
+ name: 'list_instances',
231
+ description: 'List all registered Repoburg instances with their name, project path, port, and running status.',
232
+ inputSchema: { type: 'object', properties: {} },
233
+ },
234
+ {
235
+ name: 'start_instance',
236
+ description: 'Start a Repoburg instance for a project path. The instance runs as a PM2-managed backend process with its own SQLite database.',
237
+ inputSchema: {
238
+ type: 'object',
239
+ properties: {
240
+ path: {
241
+ type: 'string',
242
+ description: 'Absolute path to the project directory.',
243
+ },
244
+ name: {
245
+ type: 'string',
246
+ description: 'Optional custom instance name (defaults to the directory basename).',
247
+ },
248
+ },
249
+ required: ['path'],
250
+ },
251
+ },
252
+ {
253
+ name: 'stop_instance',
254
+ description: 'Stop a running Repoburg instance by name.',
255
+ inputSchema: {
256
+ type: 'object',
257
+ properties: {
258
+ name: {
259
+ type: 'string',
260
+ description: 'Name of the instance to stop.',
261
+ },
262
+ },
263
+ required: ['name'],
264
+ },
265
+ },
266
+ ];
267
+ }
268
+ handleListInstances() {
269
+ const services = stateManager_1.stateManager.getServices();
270
+ if (services.length === 0) {
271
+ return this.textResult('No Repoburg instances registered.');
272
+ }
273
+ const lines = services.map((s) => `- ${s.name}: ${s.status} (port ${s.port}, path: ${s.path})`);
274
+ return this.textResult(`Repoburg instances (${services.length}):\n${lines.join('\n')}`);
275
+ }
276
+ async handleStartInstance(args) {
277
+ const projectPath = args.path;
278
+ if (typeof projectPath !== 'string') {
279
+ return this.errorResult('Missing required parameter: path');
280
+ }
281
+ const name = typeof args.name === 'string' ? args.name : undefined;
282
+ try {
283
+ const service = await serviceManager_1.serviceManager.start(projectPath, name);
284
+ this.invalidateToolCache();
285
+ return this.textResult(`Instance '${service.name}' started on port ${service.port} (path: ${service.path}).`);
286
+ }
287
+ catch (err) {
288
+ return this.errorResult(`Failed to start instance: ${errorMessage(err)}`);
289
+ }
290
+ }
291
+ async handleStopInstance(args) {
292
+ const name = args.name;
293
+ if (typeof name !== 'string') {
294
+ return this.errorResult('Missing required parameter: name');
295
+ }
296
+ try {
297
+ await serviceManager_1.serviceManager.stop(name);
298
+ return this.textResult(`Instance '${name}' stopped.`);
299
+ }
300
+ catch (err) {
301
+ return this.errorResult(`Failed to stop instance: ${errorMessage(err)}`);
302
+ }
303
+ }
304
+ // -------------------------------------------------------------------------
305
+ // manage_* tool proxy — routes to the target instance's backend
306
+ // -------------------------------------------------------------------------
307
+ async handleManageTool(toolName, args) {
308
+ const instanceName = args.instance_name;
309
+ if (typeof instanceName !== 'string') {
310
+ return this.errorResult(`Missing required parameter: instance_name. Use the list_instances tool to see available instances.`);
311
+ }
312
+ const service = stateManager_1.stateManager.getService(instanceName);
313
+ if (!service) {
314
+ return this.errorResult(`Instance '${instanceName}' not found. Use the list_instances tool to see available instances.`);
315
+ }
316
+ if (service.status !== 'running') {
317
+ return this.errorResult(`Instance '${instanceName}' is not running (status: ${service.status}). Use the start_instance tool to start it.`);
318
+ }
319
+ // Strip instance_name — the backend doesn't know about it.
320
+ const { instance_name: _unused, ...toolArgs } = args;
321
+ try {
322
+ const resp = await fetch(`http://localhost:${service.port}/mcp-internal/execute-tool`, {
323
+ method: 'POST',
324
+ headers: { 'Content-Type': 'application/json' },
325
+ body: JSON.stringify({
326
+ tool_name: toolName,
327
+ arguments: toolArgs,
328
+ }),
329
+ });
330
+ if (!resp.ok) {
331
+ return this.errorResult(`Instance '${instanceName}' returned HTTP ${resp.status} for tool '${toolName}'.`);
332
+ }
333
+ const result = (await resp.json());
334
+ return this.toToolResult(result);
335
+ }
336
+ catch (err) {
337
+ return this.errorResult(`Failed to reach instance '${instanceName}': ${errorMessage(err)}`);
338
+ }
339
+ }
340
+ // -------------------------------------------------------------------------
341
+ // Gateway info (for the management UI)
342
+ // -------------------------------------------------------------------------
343
+ /**
344
+ * Returns a descriptor of the gateway for the management UI: the gateway
345
+ * URL, available instances, tool list, and a ready-to-paste client config.
346
+ */
347
+ async getGatewayInfo() {
348
+ await this.refreshToolCache();
349
+ const daemonTools = this.buildDaemonTools();
350
+ const manageTools = this.toolCache ?? [];
351
+ const instances = stateManager_1.stateManager.getServices().map((s) => ({
352
+ name: s.name,
353
+ path: s.path,
354
+ port: s.port,
355
+ status: s.status,
356
+ }));
357
+ return {
358
+ server_name: 'repoburg-gateway',
359
+ description: 'Single MCP entry point managing all Repoburg instances. Each manage_* tool requires an instance_name parameter to route to the correct instance.',
360
+ url: `http://localhost:9998/mcp`,
361
+ instances,
362
+ tools: [...daemonTools, ...manageTools],
363
+ client_config: JSON.stringify({ repoburg: { url: 'http://localhost:9998/mcp' } }, null, 2),
364
+ };
365
+ }
366
+ // -------------------------------------------------------------------------
367
+ // Session cleanup
368
+ // -------------------------------------------------------------------------
369
+ async closeAllSessions() {
370
+ await Promise.all(Array.from(this.sessions.values()).map((t) => t.close()));
371
+ this.sessions.clear();
372
+ }
373
+ // -------------------------------------------------------------------------
374
+ // Helpers
375
+ // -------------------------------------------------------------------------
376
+ findRunningInstance() {
377
+ return stateManager_1.stateManager
378
+ .getServices()
379
+ .find((s) => s.status === 'running');
380
+ }
381
+ /**
382
+ * Maps an `ActionExecutionResult` (returned by the backend's
383
+ * `/mcp-internal/execute-tool`) to an MCP `CallToolResult`.
384
+ */
385
+ toToolResult(result) {
386
+ const output = result.execution_log?.output || '';
387
+ const error = result.execution_log?.error_message || result.error_message;
388
+ const text = error ? `${output}\n\nError: ${error}`.trim() : output;
389
+ return {
390
+ content: [{ type: 'text', text }],
391
+ isError: result.status === 'FAILURE' ? true : undefined,
392
+ };
393
+ }
394
+ textResult(text) {
395
+ return { content: [{ type: 'text', text }] };
396
+ }
397
+ errorResult(message) {
398
+ return { content: [{ type: 'text', text: message }], isError: true };
399
+ }
400
+ }
401
+ exports.mcpGateway = new McpGateway();