amalgm 0.1.72 → 0.1.73

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 (64) hide show
  1. package/package.json +1 -2
  2. package/runtime/scripts/amalgm-mcp/agents/rest.js +29 -22
  3. package/runtime/scripts/amalgm-mcp/agents/store.js +90 -24
  4. package/runtime/scripts/amalgm-mcp/agents/talk.js +66 -37
  5. package/runtime/scripts/amalgm-mcp/agents/tools.js +0 -4
  6. package/runtime/scripts/amalgm-mcp/automations/runner.js +4 -5
  7. package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +54 -57
  8. package/runtime/scripts/amalgm-mcp/automations/tools.js +4 -4
  9. package/runtime/scripts/amalgm-mcp/email/inbound.js +21 -0
  10. package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +3 -3
  11. package/runtime/scripts/amalgm-mcp/index.js +3 -1
  12. package/runtime/scripts/amalgm-mcp/lib/prefs.js +15 -0
  13. package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +5 -3
  14. package/runtime/scripts/amalgm-mcp/runtime-worker.js +47 -0
  15. package/runtime/scripts/amalgm-mcp/server/combined-service.js +27 -0
  16. package/runtime/scripts/amalgm-mcp/server/control-plane-service.js +24 -0
  17. package/runtime/scripts/amalgm-mcp/server/core-tools.js +52 -7
  18. package/runtime/scripts/amalgm-mcp/server/http-context.js +44 -0
  19. package/runtime/scripts/amalgm-mcp/server/http-service.js +34 -0
  20. package/runtime/scripts/amalgm-mcp/server/http.js +31 -465
  21. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +57 -0
  22. package/runtime/scripts/amalgm-mcp/server/mcp-adapter.js +22 -0
  23. package/runtime/scripts/amalgm-mcp/server/mcp-http-service.js +24 -0
  24. package/runtime/scripts/amalgm-mcp/server/mcp-request-context.js +32 -0
  25. package/runtime/scripts/amalgm-mcp/server/mcp.js +14 -73
  26. package/runtime/scripts/amalgm-mcp/server/routes/agents.js +29 -0
  27. package/runtime/scripts/amalgm-mcp/server/routes/apps.js +59 -0
  28. package/runtime/scripts/amalgm-mcp/server/routes/automations.js +45 -0
  29. package/runtime/scripts/amalgm-mcp/server/routes/browser.js +49 -0
  30. package/runtime/scripts/amalgm-mcp/server/routes/credentials.js +30 -0
  31. package/runtime/scripts/amalgm-mcp/server/routes/events.js +18 -0
  32. package/runtime/scripts/amalgm-mcp/server/routes/files.js +33 -0
  33. package/runtime/scripts/amalgm-mcp/server/routes/health.js +24 -0
  34. package/runtime/scripts/amalgm-mcp/server/routes/inbound.js +18 -0
  35. package/runtime/scripts/amalgm-mcp/server/routes/local.js +21 -0
  36. package/runtime/scripts/amalgm-mcp/server/routes/preferences.js +18 -0
  37. package/runtime/scripts/amalgm-mcp/server/routes/state.js +23 -0
  38. package/runtime/scripts/amalgm-mcp/server/routes/toolbox.js +59 -0
  39. package/runtime/scripts/amalgm-mcp/server/routes/workspace.js +57 -0
  40. package/runtime/scripts/amalgm-mcp/server/service-lifecycle.js +54 -0
  41. package/runtime/scripts/amalgm-mcp/services/control-plane.js +19 -0
  42. package/runtime/scripts/amalgm-mcp/services/mcp-adapter.js +12 -0
  43. package/runtime/scripts/amalgm-mcp/services/runtime-worker.js +18 -0
  44. package/runtime/scripts/amalgm-mcp/slack/inbound.js +21 -0
  45. package/runtime/scripts/amalgm-mcp/state/snapshot.js +3 -3
  46. package/runtime/scripts/amalgm-mcp/tests/agents-source-of-truth.test.js +82 -0
  47. package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +5 -5
  48. package/runtime/scripts/amalgm-mcp/tests/core-tools.test.js +37 -0
  49. package/runtime/scripts/amalgm-mcp/tests/mcp-surface.test.js +158 -0
  50. package/runtime/scripts/amalgm-mcp/tests/server-routing.test.js +81 -0
  51. package/runtime/scripts/amalgm-mcp/tests/server-services.test.js +156 -0
  52. package/runtime/scripts/amalgm-mcp/tests/system-catalog.test.js +81 -0
  53. package/runtime/scripts/amalgm-mcp/tests/toolbox-service.test.js +250 -0
  54. package/runtime/scripts/amalgm-mcp/toolbox/mcp-surface.js +106 -0
  55. package/runtime/scripts/amalgm-mcp/toolbox/names.js +41 -0
  56. package/runtime/scripts/amalgm-mcp/toolbox/rest.js +107 -53
  57. package/runtime/scripts/amalgm-mcp/toolbox/runner.js +9 -75
  58. package/runtime/scripts/amalgm-mcp/toolbox/selection.js +22 -0
  59. package/runtime/scripts/amalgm-mcp/toolbox/service.js +227 -0
  60. package/runtime/scripts/amalgm-mcp/toolbox/store.js +9 -78
  61. package/runtime/scripts/amalgm-mcp/toolbox/system-catalog.js +112 -0
  62. package/runtime/scripts/amalgm-mcp/toolbox/tools.js +11 -74
  63. package/runtime/scripts/amalgm-mcp/workflows/compiler.js +1 -1
  64. package/runtime/scripts/amalgm-mcp/workflows/runner.js +4 -5
@@ -1,479 +1,45 @@
1
1
  /**
2
- * HTTP server routes + lifecycle.
2
+ * HTTP server lifecycle and auth boundary.
3
3
  *
4
- * Route table:
5
- * GET /healthz → stats snapshot
6
- * * /mcp → MCP streamable-HTTP (delegated to SDK transport)
7
- * POST /events → webhook ingress (signature match + workflow run)
8
- * GET /events → recent events (in-memory ring buffer)
9
- * GET /automations/list → list automations/triggers/workflows
10
- * POST /automations/{get,create,update,delete,run-now,validate}
11
- * POST /automations/triggers/{update,delete}
12
- * GET /workspace/projects → list projects
13
- * POST /workspace/import → copy a local folder into workspaces
14
- * POST /github/clone → clone repository onto the computer
15
- * GET /workspace/branches → list git branches
16
- * POST /workspace/checkout → checkout git branch
17
- * GET /workspace/diff → git diff summary + patch
18
- * GET /workspace/worktree → list worktrees
19
- * POST /workspace/worktree → create worktree
20
- * DELETE /workspace/worktree → remove worktree
21
- * GET /fs/list → list files
22
- * GET /fs/read → read file
23
- * POST /fs/{write,delete,mkdir,rename}
24
- * GET /mcp-connections → list sanitized MCP connections
25
- * POST /mcp-connections → save/update MCP connection
26
- * DELETE /mcp-connections → remove MCP connection
27
- * GET /toolbox → list local ToolboxDB tools/actions
28
- * GET /toolbox/tools?id=... → get one toolbox tool
29
- * POST /toolbox/tools → save/update a toolbox tool
30
- * PATCH /toolbox/tools → partially update a toolbox tool
31
- * DELETE /toolbox/tools → remove a toolbox tool
32
- * GET /toolbox/actions?id=... → get one toolbox action
33
- * POST /toolbox/actions → save/update a toolbox action
34
- * PATCH /toolbox/actions → partially update a toolbox action
35
- * DELETE /toolbox/actions → remove a toolbox action
36
- * GET /state/snapshot → current Local Live Store snapshot
37
- * GET /state/events → replay Local Live Store events
38
- * GET /state/stream → SSE Local Live Store stream
39
- * GET /browser/profiles → list durable Amalgm browser profiles
40
- * POST /browser/profiles → save/update browser profile metadata
41
- * GET /browser/auth-bundles → list encrypted browser auth bundles
42
- * POST /browser/auth-bundles/export → save cookies/storage into encrypted bundle
43
- * POST /browser/auth-bundles/import → hydrate cookies/storage from encrypted bundle
44
- * DELETE /browser/auth-bundles → remove encrypted auth bundle metadata/blob
45
- * GET /browser/login-sessions → list or token-read temporary browser login sessions
46
- * POST /browser/login-sessions → create a temporary browser auth link
47
- * POST /browser/login-sessions/complete → export auth bundle and mark login complete
48
- * POST /browser/login-sessions/cancel → cancel a temporary browser auth link
49
- * GET /user-preferences → current local user preferences
50
- * PUT /user-preferences → update local user preferences
51
- * POST /local/init → init local computer state
52
- * GET /local/credentials → discover local agent auth
53
- * POST /local/credentials/resolve → resolve spawn env for agent auth
54
- * POST /email/inbound → run a reply turn for inbound email
55
- * POST /slack/inbound → run a reply turn for inbound Slack
56
- * GET /credentials → list local Amalgm credential profiles
57
- * POST /credentials → save local Amalgm credential profile
58
- * DELETE /credentials → remove local Amalgm credential profile
59
- * POST /user-api-keys → legacy save BYOK credential file
60
- * DELETE /user-api-keys → legacy remove BYOK credential file
61
- * GET /apps/list → list apps
62
- * GET /apps/routes → app DNS routes for tunnel adverts
63
- * POST /apps/{register,redeploy,start,stop,connect-dns,disconnect-dns}
64
- * Legacy /artifacts/* aliases remain for older clients during the rename.
65
- * GET /agents/list → list agents
66
- * POST /agents/{get,create,update,delete}
4
+ * This file should stay small: MCP transport handling lives in mcp-adapter.js,
5
+ * local product APIs live under server/routes/*, and background work starts via
6
+ * runtime-worker.js.
67
7
  */
68
8
 
69
- const http = require('http');
70
9
  const { PORT, STORAGE_DIR } = require('../config');
71
10
  const { hasSupabase } = require('../lib/supabase');
72
- const { authorizeRuntimeHttp } = require('../../runtime-auth');
11
+ const { startRuntimeWorker, stopRuntimeWorker } = require('../runtime-worker');
12
+ const { createCombinedServer } = require('./combined-service');
13
+ const { startHttpServer } = require('./service-lifecycle');
73
14
 
74
- const { loadAgents } = require('../agents/store');
75
- const { loadApps } = require('../apps/store');
76
- const { activeAgentConversations } = require('../agents/talk');
77
- const { listAutomations } = require('../automations/store');
78
- const { startAutomationScheduler, stopAutomationScheduler } = require('../automations/scheduler');
79
- const { createTransport } = require('./mcp');
80
- const { handleEventsPost } = require('../events/ingress');
81
- const eventsRing = require('../events/ring-buffer');
82
- const automationsRest = require('../automations/rest');
83
- const agentsRest = require('../agents/rest');
84
- const appsRest = require('../apps/rest');
85
- const emailInbound = require('../email/inbound');
86
- const slackInbound = require('../slack/inbound');
87
- const fsRest = require('../fs/rest');
88
- const localRest = require('../local/rest');
89
- const mcpConnectionsRest = require('../mcp-connections/rest');
90
- const toolboxRest = require('../toolbox/rest');
91
- const stateRest = require('../state/rest');
92
- const browserRest = require('../browser/rest');
93
- const prefsStore = require('../lib/prefs');
94
- const userApiKeysRest = require('../user-api-keys/rest');
95
- const credentialsRest = require('../credentials/rest');
96
- const workspaceRest = require('../workspace/rest');
97
-
98
- function createServer() {
99
- return http.createServer(async (req, res) => {
100
- if (!authorizeRuntimeHttp(req, res, { exposeHeaders: 'Mcp-Session-Id' })) return;
101
-
102
- async function readJsonBody() {
103
- const chunks = [];
104
- for await (const chunk of req) chunks.push(chunk);
105
- return JSON.parse(Buffer.concat(chunks).toString());
106
- }
107
- function sendJson(statusCode, data) {
108
- res.writeHead(statusCode, { 'Content-Type': 'application/json' });
109
- res.end(JSON.stringify(data));
110
- }
111
- function getQuery() {
112
- return Object.fromEntries(new URL(req.url, 'http://127.0.0.1').searchParams.entries());
113
- }
114
-
115
- try {
116
- // ── MCP ─────────────────────────────────────────────────────────────────
117
- // Delegate anything under /mcp directly to the SDK transport. A fresh
118
- // transport + Server pair is created per request: the stateless
119
- // StreamableHTTPServerTransport is single-shot in this mode, so reusing
120
- // one across requests would fail on the second call with HTTP 500.
121
- if (req.url === '/mcp' || req.url === '/mcp/' || req.url.startsWith('/mcp?')) {
122
- const transport = await createTransport();
123
- await transport.handleRequest(req, res);
124
- return;
125
- }
126
-
127
- // ── Health ──────────────────────────────────────────────────────────────
128
- if (req.url === '/healthz') {
129
- const agents = loadAgents();
130
- const automations = listAutomations();
131
- return sendJson(200, {
132
- status: 'ok',
133
- automations: automations.length,
134
- enabledAutomations: automations.filter((automation) => automation.enabled !== false).length,
135
- apps: loadApps().apps.length,
136
- customAgents: agents.agents.length,
137
- activeAgentConversations: activeAgentConversations.size,
138
- });
139
- }
140
-
141
- // ── /state — Local Live Store snapshot/replay/stream ─────────────────
142
- if (req.url.startsWith('/state/snapshot') && req.method === 'GET') {
143
- return stateRest.handleSnapshot(getQuery(), sendJson);
144
- }
145
- if (req.url.startsWith('/state/events') && req.method === 'GET') {
146
- return stateRest.handleEvents(getQuery(), sendJson);
147
- }
148
- if (req.url.startsWith('/state/stream') && req.method === 'GET') {
149
- return stateRest.handleStream(req, res, getQuery());
150
- }
151
-
152
- // ── /browser REST ───────────────────────────────────────────────────
153
- if (req.url === '/browser/profiles' && req.method === 'GET') {
154
- return browserRest.handleProfilesList(sendJson);
155
- }
156
- if (req.url === '/browser/profiles' && req.method === 'POST') {
157
- return browserRest.handleProfilesUpsert(await readJsonBody(), sendJson);
158
- }
159
- if (req.url === '/browser/auth-bundles' && req.method === 'GET') {
160
- return browserRest.handleAuthBundlesList(sendJson);
161
- }
162
- if (req.url === '/browser/auth-bundles/export' && req.method === 'POST') {
163
- return browserRest.handleAuthBundleExport(await readJsonBody(), sendJson);
164
- }
165
- if (req.url === '/browser/auth-bundles/import' && req.method === 'POST') {
166
- return browserRest.handleAuthBundleImport(await readJsonBody(), sendJson);
167
- }
168
- if (req.url === '/browser/auth-bundles' && req.method === 'DELETE') {
169
- return browserRest.handleAuthBundleDelete(await readJsonBody(), sendJson);
170
- }
171
- if (req.url.startsWith('/browser/login-sessions') && req.method === 'GET') {
172
- return browserRest.handleLoginSessionsList(getQuery(), sendJson);
173
- }
174
- if (req.url === '/browser/login-sessions' && req.method === 'POST') {
175
- return browserRest.handleLoginSessionCreate(await readJsonBody(), sendJson);
176
- }
177
- if (req.url === '/browser/login-sessions/complete' && req.method === 'POST') {
178
- return browserRest.handleLoginSessionComplete(await readJsonBody(), sendJson);
179
- }
180
- if (req.url === '/browser/login-sessions/cancel' && req.method === 'POST') {
181
- return browserRest.handleLoginSessionCancel(await readJsonBody(), sendJson);
182
- }
183
-
184
- // ── /user-preferences REST ───────────────────────────────────────────
185
- if (req.url === '/user-preferences' && req.method === 'GET') {
186
- return sendJson(200, prefsStore.readUserPreferences());
187
- }
188
- if (req.url === '/user-preferences' && req.method === 'PUT') {
189
- const preferences = prefsStore.updateUserPreferences(await readJsonBody());
190
- return sendJson(200, { success: true, preferences });
191
- }
192
-
193
- // ── /agents (REST, not MCP) ─────────────────────────────────────────────
194
- if (req.url === '/agents/list' && req.method === 'GET') {
195
- return agentsRest.handleList(sendJson);
196
- }
197
- if (req.url === '/agents/get' && req.method === 'POST') {
198
- return agentsRest.handleGet(await readJsonBody(), sendJson);
199
- }
200
- if (req.url === '/agents/create' && req.method === 'POST') {
201
- return agentsRest.handleCreate(await readJsonBody(), sendJson);
202
- }
203
- if (req.url === '/agents/update' && req.method === 'POST') {
204
- return agentsRest.handleUpdate(await readJsonBody(), sendJson);
205
- }
206
- if (req.url === '/agents/delete' && req.method === 'POST') {
207
- return agentsRest.handleDelete(await readJsonBody(), sendJson);
208
- }
209
-
210
- // ── /events — webhook ingress ───────────────────────────────────────────
211
- if (req.url === '/events' && req.method === 'POST') {
212
- return handleEventsPost(req, sendJson);
213
- }
214
- if (req.url === '/events' && req.method === 'GET') {
215
- return sendJson(200, { events: eventsRing.recent(50) });
216
- }
217
-
218
- // ── /automations REST ─────────────────────────────────────────────────
219
- if (req.url === '/automations/list' && req.method === 'GET') {
220
- return automationsRest.handleList(req, sendJson);
221
- }
222
- if (req.url === '/automations/get' && req.method === 'POST') {
223
- return automationsRest.handleGet(await readJsonBody(), sendJson);
224
- }
225
- if (req.url === '/automations/create' && req.method === 'POST') {
226
- return automationsRest.handleCreate(await readJsonBody(), sendJson);
227
- }
228
- if (req.url === '/automations/update' && req.method === 'POST') {
229
- return automationsRest.handleUpdate(await readJsonBody(), sendJson);
230
- }
231
- if (req.url === '/automations/delete' && req.method === 'POST') {
232
- return automationsRest.handleDelete(await readJsonBody(), sendJson);
233
- }
234
- if (req.url === '/automations/run-now' && req.method === 'POST') {
235
- return automationsRest.handleRunNow(await readJsonBody(), sendJson);
236
- }
237
- if (req.url === '/automations/validate' && req.method === 'POST') {
238
- return automationsRest.handleValidate(await readJsonBody(), sendJson);
239
- }
240
- if (req.url === '/automations/triggers/update' && req.method === 'POST') {
241
- return automationsRest.handleTriggerUpdate(await readJsonBody(), sendJson);
242
- }
243
- if (req.url === '/automations/triggers/delete' && req.method === 'POST') {
244
- return automationsRest.handleTriggerDelete(await readJsonBody(), sendJson);
245
- }
246
-
247
- // ── /workspace REST ─────────────────────────────────────────────────────
248
- if (req.url === '/workspace/projects' && req.method === 'GET') {
249
- return workspaceRest.handleProjects(sendJson);
250
- }
251
- if (req.url === '/workspace/projects' && req.method === 'POST') {
252
- return workspaceRest.handleProjectRegister(await readJsonBody(), sendJson);
253
- }
254
- if (req.url === '/workspace/projects' && req.method === 'PATCH') {
255
- return workspaceRest.handleProjectUpdate(await readJsonBody(), sendJson);
256
- }
257
- if (req.url === '/workspace/projects' && req.method === 'DELETE') {
258
- return workspaceRest.handleProjectDelete(await readJsonBody(), sendJson);
259
- }
260
- if (req.url === '/workspace/import' && req.method === 'POST') {
261
- return workspaceRest.handleImport(await readJsonBody(), sendJson);
262
- }
263
- if (req.url === '/github/clone' && req.method === 'POST') {
264
- return workspaceRest.handleClone(await readJsonBody(), sendJson);
265
- }
266
- if (req.url.startsWith('/workspace/branches') && req.method === 'GET') {
267
- return workspaceRest.handleBranches(getQuery(), sendJson);
268
- }
269
- if (req.url === '/workspace/checkout' && req.method === 'POST') {
270
- return workspaceRest.handleCheckout(await readJsonBody(), sendJson);
271
- }
272
- if (req.url.startsWith('/workspace/diff') && req.method === 'GET') {
273
- return workspaceRest.handleDiff(getQuery(), sendJson);
274
- }
275
- if (req.url.startsWith('/workspace/worktree') && req.method === 'GET') {
276
- return workspaceRest.handleWorktreeList(getQuery(), sendJson);
277
- }
278
- if (req.url === '/workspace/worktree' && req.method === 'POST') {
279
- return workspaceRest.handleWorktreeCreate(await readJsonBody(), sendJson);
280
- }
281
- if (req.url === '/workspace/worktree' && req.method === 'DELETE') {
282
- return workspaceRest.handleWorktreeDelete(await readJsonBody(), sendJson);
283
- }
284
-
285
- // ── /fs REST ────────────────────────────────────────────────────────────
286
- if (req.url.startsWith('/fs/list') && req.method === 'GET') {
287
- return fsRest.handleList(getQuery(), sendJson);
288
- }
289
- if (req.url.startsWith('/fs/read') && req.method === 'GET') {
290
- return fsRest.handleRead(getQuery(), sendJson);
291
- }
292
- if (req.url === '/fs/write' && req.method === 'POST') {
293
- return fsRest.handleWrite(await readJsonBody(), sendJson);
294
- }
295
- if (req.url === '/fs/delete' && req.method === 'POST') {
296
- return fsRest.handleDelete(await readJsonBody(), sendJson);
297
- }
298
- if (req.url === '/fs/mkdir' && req.method === 'POST') {
299
- return fsRest.handleMkdir(await readJsonBody(), sendJson);
300
- }
301
- if (req.url === '/fs/rename' && req.method === 'POST') {
302
- return fsRest.handleRename(await readJsonBody(), sendJson);
303
- }
304
-
305
- // ── /mcp-connections REST ──────────────────────────────────────────────
306
- if (req.url === '/mcp-connections' && req.method === 'GET') {
307
- return mcpConnectionsRest.handleList(sendJson);
308
- }
309
- if (req.url === '/mcp-connections' && req.method === 'POST') {
310
- return mcpConnectionsRest.handleUpsert(await readJsonBody(), sendJson);
311
- }
312
- if (req.url === '/mcp-connections' && req.method === 'DELETE') {
313
- return mcpConnectionsRest.handleDelete(await readJsonBody(), sendJson);
314
- }
315
-
316
- // ── /toolbox REST ─────────────────────────────────────────────────────
317
- if (req.url === '/toolbox' && req.method === 'GET') {
318
- return toolboxRest.handleList(sendJson);
319
- }
320
- if ((req.url === '/toolbox/tools' || req.url.startsWith('/toolbox/tools?')) && req.method === 'GET') {
321
- return toolboxRest.handleGetTool(getQuery(), sendJson);
322
- }
323
- if (req.url === '/toolbox/tools' && req.method === 'POST') {
324
- return toolboxRest.handleUpsertTool(await readJsonBody(), sendJson);
325
- }
326
- if (req.url === '/toolbox/tools' && req.method === 'PATCH') {
327
- return toolboxRest.handleUpdateTool(await readJsonBody(), sendJson);
328
- }
329
- if (req.url === '/toolbox/tools' && req.method === 'DELETE') {
330
- return toolboxRest.handleDeleteTool(await readJsonBody(), sendJson);
331
- }
332
- if ((req.url === '/toolbox/actions' || req.url.startsWith('/toolbox/actions?')) && req.method === 'GET') {
333
- return toolboxRest.handleGetAction(getQuery(), sendJson);
334
- }
335
- if (req.url === '/toolbox/actions' && req.method === 'POST') {
336
- return toolboxRest.handleUpsertAction(await readJsonBody(), sendJson);
337
- }
338
- if (req.url === '/toolbox/actions' && req.method === 'PATCH') {
339
- return toolboxRest.handleUpdateAction(await readJsonBody(), sendJson);
340
- }
341
- if (req.url === '/toolbox/actions' && req.method === 'DELETE') {
342
- return toolboxRest.handleDeleteAction(await readJsonBody(), sendJson);
343
- }
344
-
345
- // ── /local REST ────────────────────────────────────────────────────────
346
- if (req.url === '/local/init' && req.method === 'POST') {
347
- return localRest.handleInit(sendJson);
348
- }
349
- if (req.url === '/local/credentials' && req.method === 'GET') {
350
- return localRest.handleCredentials(sendJson);
351
- }
352
- if (req.url === '/local/credentials/resolve' && req.method === 'POST') {
353
- return localRest.handleResolve(await readJsonBody(), sendJson);
354
- }
355
-
356
- // ── /credentials REST ─────────────────────────────────────────────────
357
- if (req.url === '/credentials' && req.method === 'GET') {
358
- return credentialsRest.handleList(sendJson);
359
- }
360
- if (req.url === '/credentials' && req.method === 'POST') {
361
- return credentialsRest.handleSave(await readJsonBody(), sendJson);
362
- }
363
- if (req.url === '/credentials' && req.method === 'DELETE') {
364
- return credentialsRest.handleDelete(await readJsonBody(), sendJson);
365
- }
366
-
367
- // ── /email REST ────────────────────────────────────────────────────────
368
- if (req.url === '/email/inbound' && req.method === 'POST') {
369
- return emailInbound.handleEmailInbound(await readJsonBody(), sendJson);
370
- }
371
- if (req.url === '/slack/inbound' && req.method === 'POST') {
372
- return slackInbound.handleSlackInbound(await readJsonBody(), sendJson);
373
- }
374
-
375
- // ── /user-api-keys REST ───────────────────────────────────────────────
376
- if (req.url === '/user-api-keys' && req.method === 'POST') {
377
- return userApiKeysRest.handleSave(await readJsonBody(), sendJson);
378
- }
379
- if (req.url === '/user-api-keys' && req.method === 'DELETE') {
380
- return userApiKeysRest.handleDelete(sendJson);
381
- }
382
-
383
- // ── /apps REST ────────────────────────────────────────────────────
384
- if (req.url === '/apps/list' && req.method === 'GET') {
385
- return appsRest.handleList(sendJson);
386
- }
387
- if (req.url === '/apps/routes' && req.method === 'GET') {
388
- return appsRest.handleRoutes(sendJson);
389
- }
390
- if (req.url === '/apps/register' && req.method === 'POST') {
391
- return appsRest.handleRegister(await readJsonBody(), sendJson);
392
- }
393
- if (req.url === '/apps/redeploy' && req.method === 'POST') {
394
- return appsRest.handleRedeploy(await readJsonBody(), sendJson);
395
- }
396
- if (req.url === '/apps/start' && req.method === 'POST') {
397
- return appsRest.handleStart(await readJsonBody(), sendJson);
398
- }
399
- if (req.url === '/apps/stop' && req.method === 'POST') {
400
- return appsRest.handleStop(await readJsonBody(), sendJson);
401
- }
402
- if (req.url === '/apps/delete' && req.method === 'POST') {
403
- return appsRest.handleDelete(await readJsonBody(), sendJson);
404
- }
405
- if (req.url === '/apps/connect-dns' && req.method === 'POST') {
406
- return appsRest.handleConnectDns(await readJsonBody(), sendJson);
407
- }
408
- if (req.url === '/apps/disconnect-dns' && req.method === 'POST') {
409
- return appsRest.handleDisconnectDns(await readJsonBody(), sendJson);
410
- }
411
-
412
- // ── legacy /artifacts REST aliases ────────────────────────────────
413
- if (req.url === '/artifacts/list' && req.method === 'GET') {
414
- return appsRest.handleList(sendJson, { legacy: true });
415
- }
416
- if (req.url === '/artifacts/routes' && req.method === 'GET') {
417
- return appsRest.handleRoutes(sendJson, { legacy: true });
418
- }
419
- if (req.url === '/artifacts/register' && req.method === 'POST') {
420
- return appsRest.handleRegister(await readJsonBody(), sendJson, { legacy: true });
421
- }
422
- if (req.url === '/artifacts/redeploy' && req.method === 'POST') {
423
- return appsRest.handleRedeploy(await readJsonBody(), sendJson, { legacy: true });
424
- }
425
- if (req.url === '/artifacts/start' && req.method === 'POST') {
426
- return appsRest.handleStart(await readJsonBody(), sendJson, { legacy: true });
427
- }
428
- if (req.url === '/artifacts/stop' && req.method === 'POST') {
429
- return appsRest.handleStop(await readJsonBody(), sendJson, { legacy: true });
430
- }
431
- if (req.url === '/artifacts/delete' && req.method === 'POST') {
432
- return appsRest.handleDelete(await readJsonBody(), sendJson, { legacy: true });
433
- }
434
- if (req.url === '/artifacts/connect-dns' && req.method === 'POST') {
435
- return appsRest.handleConnectDns(await readJsonBody(), sendJson, { legacy: true });
436
- }
437
- if (req.url === '/artifacts/disconnect-dns' && req.method === 'POST') {
438
- return appsRest.handleDisconnectDns(await readJsonBody(), sendJson, { legacy: true });
439
- }
440
-
441
- res.writeHead(404, { 'Content-Type': 'text/plain' });
442
- res.end('Not found');
443
- } catch (err) {
444
- console.error('[AmalgmMCP] Unhandled error:', err);
445
- if (!res.headersSent) sendJson(500, { error: 'Internal server error' });
446
- }
447
- });
15
+ function createServer(options = {}) {
16
+ return createCombinedServer(options);
448
17
  }
449
18
 
450
- async function listen() {
451
- const httpServer = createServer();
452
-
453
- httpServer.listen(PORT, '127.0.0.1', async () => {
454
- console.log(`[AmalgmMCP] Listening on 127.0.0.1:${PORT}`);
455
- console.log(`[AmalgmMCP] Storage: ${STORAGE_DIR}`);
456
- console.log(
457
- `[AmalgmMCP] Supabase: ${hasSupabase() ? 'connected' : 'not configured (runs are local-only)'}`,
458
- );
459
- // Hydrate user model prefs non-blocking from local SQLite.
460
- require('../lib/prefs').hydrateModelPreferences().catch(() => {});
461
- startAutomationScheduler();
19
+ async function listen(options = {}) {
20
+ const httpServer = createServer(options);
21
+ const runtimeWorker = options.runtimeWorker || {
22
+ start: startRuntimeWorker,
23
+ stop: stopRuntimeWorker,
24
+ };
25
+
26
+ startHttpServer(httpServer, {
27
+ port: options.port || PORT,
28
+ serviceName: 'AmalgmMCP',
29
+ onListening() {
30
+ console.log(`[AmalgmMCP] Storage: ${STORAGE_DIR}`);
31
+ console.log(
32
+ `[AmalgmMCP] Supabase: ${hasSupabase() ? 'connected' : 'not configured (runs are local-only)'}`,
33
+ );
34
+ // Hydrate user model prefs non-blocking from local SQLite.
35
+ require('../lib/prefs').hydrateModelPreferences().catch(() => {});
36
+ runtimeWorker.start();
37
+ },
38
+ onShutdown() {
39
+ runtimeWorker.stop();
40
+ },
462
41
  });
463
42
 
464
- // Graceful shutdown. Running tasks get aborted via SIGTERM → process exit;
465
- // we don't try to individually abort them here since chat-server owns its own
466
- // runtime teardown paths.
467
- function shutdown() {
468
- console.log('[AmalgmMCP] Shutting down...');
469
- stopAutomationScheduler();
470
- httpServer.close(() => process.exit(0));
471
- setTimeout(() => process.exit(0), 5000);
472
- }
473
-
474
- process.on('SIGTERM', shutdown);
475
- process.on('SIGINT', shutdown);
476
-
477
43
  return httpServer;
478
44
  }
479
45
 
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Local product/control-plane API dispatcher.
5
+ *
6
+ * These route groups are not the MCP surface. They are the local HTTP APIs the
7
+ * app uses to manage resources: agents, apps, automations, toolbox records,
8
+ * browser auth, workspace/files, credentials, state, and inbound event helpers.
9
+ */
10
+
11
+ const { handleHealthRoutes } = require('./routes/health');
12
+ const { handleStateRoutes } = require('./routes/state');
13
+ const { handleBrowserRoutes } = require('./routes/browser');
14
+ const { handlePreferenceRoutes } = require('./routes/preferences');
15
+ const { handleAgentRoutes } = require('./routes/agents');
16
+ const { handleEventRoutes } = require('./routes/events');
17
+ const { handleAutomationRoutes } = require('./routes/automations');
18
+ const { handleWorkspaceRoutes } = require('./routes/workspace');
19
+ const { handleFileRoutes } = require('./routes/files');
20
+ const { handleToolboxRoutes } = require('./routes/toolbox');
21
+ const { handleLocalRoutes } = require('./routes/local');
22
+ const { handleCredentialRoutes } = require('./routes/credentials');
23
+ const { handleInboundRoutes } = require('./routes/inbound');
24
+ const { handleAppRoutes } = require('./routes/apps');
25
+
26
+ const DEFAULT_ROUTE_GROUPS = [
27
+ handleHealthRoutes,
28
+ handleStateRoutes,
29
+ handleBrowserRoutes,
30
+ handlePreferenceRoutes,
31
+ handleAgentRoutes,
32
+ handleEventRoutes,
33
+ handleAutomationRoutes,
34
+ handleWorkspaceRoutes,
35
+ handleFileRoutes,
36
+ handleToolboxRoutes,
37
+ handleLocalRoutes,
38
+ handleCredentialRoutes,
39
+ handleInboundRoutes,
40
+ handleAppRoutes,
41
+ ];
42
+
43
+ function createLocalServiceRouter(routeGroups = DEFAULT_ROUTE_GROUPS) {
44
+ return {
45
+ async handle(ctx) {
46
+ for (const routeGroup of routeGroups) {
47
+ if (await routeGroup(ctx)) return true;
48
+ }
49
+ return false;
50
+ },
51
+ };
52
+ }
53
+
54
+ module.exports = {
55
+ DEFAULT_ROUTE_GROUPS,
56
+ createLocalServiceRouter,
57
+ };
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ const { createTransport } = require('./mcp');
4
+
5
+ function isMcpRequest(pathname) {
6
+ return pathname === '/mcp' || pathname === '/mcp/';
7
+ }
8
+
9
+ async function handleMcpRequest(ctx) {
10
+ if (!isMcpRequest(ctx.pathname)) return false;
11
+
12
+ // The SDK transport is single-shot in stateless streamable-HTTP mode.
13
+ // Reusing one across requests fails on the second call with HTTP 500.
14
+ const transport = await createTransport();
15
+ await transport.handleRequest(ctx.req, ctx.res);
16
+ return true;
17
+ }
18
+
19
+ module.exports = {
20
+ handleMcpRequest,
21
+ isMcpRequest,
22
+ };
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const { createRuntimeHttpServer } = require('./http-service');
4
+ const { handleMcpRequest } = require('./mcp-adapter');
5
+
6
+ function createMcpHttpHandler(options = {}) {
7
+ const mcpRequestHandler = options.mcpRequestHandler || handleMcpRequest;
8
+
9
+ return async function handleMcpHttpRequest(ctx) {
10
+ return mcpRequestHandler(ctx);
11
+ };
12
+ }
13
+
14
+ function createMcpHttpServer(options = {}) {
15
+ return createRuntimeHttpServer(createMcpHttpHandler(options), {
16
+ serviceName: options.serviceName || 'AmalgmMCPAdapter',
17
+ exposeHeaders: options.exposeHeaders,
18
+ });
19
+ }
20
+
21
+ module.exports = {
22
+ createMcpHttpHandler,
23
+ createMcpHttpServer,
24
+ };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const { AMALGM_USER_ID } = require('../config');
4
+ const { hasSupabase, supabaseSelect } = require('../lib/supabase');
5
+
6
+ /**
7
+ * Look up the caller's session to tag DB rows with origin metadata. Reads
8
+ * x-amalgm-session-id from the transport's passed-through HTTP headers.
9
+ */
10
+ async function buildMcpRequestContext(extra) {
11
+ const headers = extra?.requestInfo?.headers || {};
12
+ const h = headers['x-amalgm-session-id'];
13
+ const callerSessionId = Array.isArray(h) ? h[0] : h || null;
14
+ const ctx = { callerSessionId };
15
+
16
+ if (callerSessionId && hasSupabase()) {
17
+ const rows = await supabaseSelect(
18
+ 'sessions',
19
+ `id=eq.${callerSessionId}&user_id=eq.${AMALGM_USER_ID}&select=id,title,harness,metadata`,
20
+ );
21
+ const parent = Array.isArray(rows) ? rows[0] : null;
22
+ if (parent) {
23
+ ctx.sessionMetadata = parent.metadata || {};
24
+ ctx.originName = parent.title || undefined;
25
+ ctx.originHarnessId = parent.harness || undefined;
26
+ ctx.originBaseHarnessId = parent.metadata?.baseHarnessId || undefined;
27
+ }
28
+ }
29
+ return ctx;
30
+ }
31
+
32
+ module.exports = { buildMcpRequestContext };