amalgm 0.0.0 → 0.0.32

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 (116) hide show
  1. package/README.md +41 -1
  2. package/bin/amalgm.js +8 -2
  3. package/lib/auth-store.js +223 -0
  4. package/lib/cli.js +1132 -0
  5. package/lib/paths.js +30 -0
  6. package/lib/supervisor.js +476 -0
  7. package/lib/tunnel-chat.js +328 -0
  8. package/lib/tunnel-events.js +499 -0
  9. package/package.json +30 -3
  10. package/runtime/README.md +4 -0
  11. package/runtime/lib/chatInput.js +315 -0
  12. package/runtime/lib/harnesses.js +988 -0
  13. package/runtime/lib/local/amalgmStore.js +136 -0
  14. package/runtime/lib/local/credentialResolver.js +425 -0
  15. package/runtime/lib/mcpApps/registry.js +619 -0
  16. package/runtime/package.json +5 -0
  17. package/runtime/scripts/amalgm-mcp/agents/rest.js +144 -0
  18. package/runtime/scripts/amalgm-mcp/agents/store.js +684 -0
  19. package/runtime/scripts/amalgm-mcp/agents/talk.js +1162 -0
  20. package/runtime/scripts/amalgm-mcp/agents/tools.js +221 -0
  21. package/runtime/scripts/amalgm-mcp/artifacts/advertise.js +132 -0
  22. package/runtime/scripts/amalgm-mcp/artifacts/rest.js +103 -0
  23. package/runtime/scripts/amalgm-mcp/artifacts/store.js +157 -0
  24. package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +402 -0
  25. package/runtime/scripts/amalgm-mcp/artifacts/tools.js +176 -0
  26. package/runtime/scripts/amalgm-mcp/browser/page.js +637 -0
  27. package/runtime/scripts/amalgm-mcp/browser/tools.js +688 -0
  28. package/runtime/scripts/amalgm-mcp/config.js +140 -0
  29. package/runtime/scripts/amalgm-mcp/credentials/rest.js +45 -0
  30. package/runtime/scripts/amalgm-mcp/deps.js +40 -0
  31. package/runtime/scripts/amalgm-mcp/email/inbound.js +215 -0
  32. package/runtime/scripts/amalgm-mcp/events/executor.js +179 -0
  33. package/runtime/scripts/amalgm-mcp/events/ingress.js +113 -0
  34. package/runtime/scripts/amalgm-mcp/events/matcher.js +125 -0
  35. package/runtime/scripts/amalgm-mcp/events/rest.js +200 -0
  36. package/runtime/scripts/amalgm-mcp/events/ring-buffer.js +19 -0
  37. package/runtime/scripts/amalgm-mcp/events/store.js +113 -0
  38. package/runtime/scripts/amalgm-mcp/events/tools.js +306 -0
  39. package/runtime/scripts/amalgm-mcp/events/webhook-url.js +28 -0
  40. package/runtime/scripts/amalgm-mcp/fs/rest.js +293 -0
  41. package/runtime/scripts/amalgm-mcp/index.js +100 -0
  42. package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +167 -0
  43. package/runtime/scripts/amalgm-mcp/lib/email-md.js +288 -0
  44. package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +63 -0
  45. package/runtime/scripts/amalgm-mcp/lib/prefs.js +441 -0
  46. package/runtime/scripts/amalgm-mcp/lib/storage.js +92 -0
  47. package/runtime/scripts/amalgm-mcp/lib/supabase.js +118 -0
  48. package/runtime/scripts/amalgm-mcp/lib/tool-result.js +177 -0
  49. package/runtime/scripts/amalgm-mcp/local/rest.js +87 -0
  50. package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +234 -0
  51. package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
  52. package/runtime/scripts/amalgm-mcp/server/core-tools.js +20 -0
  53. package/runtime/scripts/amalgm-mcp/server/http.js +377 -0
  54. package/runtime/scripts/amalgm-mcp/server/mcp.js +127 -0
  55. package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
  56. package/runtime/scripts/amalgm-mcp/state/db.js +194 -0
  57. package/runtime/scripts/amalgm-mcp/state/events.js +113 -0
  58. package/runtime/scripts/amalgm-mcp/state/rest.js +64 -0
  59. package/runtime/scripts/amalgm-mcp/state/snapshot.js +76 -0
  60. package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
  61. package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
  62. package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
  63. package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
  64. package/runtime/scripts/amalgm-mcp/tasks/store.js +154 -0
  65. package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
  66. package/runtime/scripts/amalgm-mcp/toolbox/rest.js +75 -0
  67. package/runtime/scripts/amalgm-mcp/toolbox/runner.js +257 -0
  68. package/runtime/scripts/amalgm-mcp/toolbox/store.js +933 -0
  69. package/runtime/scripts/amalgm-mcp/toolbox/tools.js +269 -0
  70. package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
  71. package/runtime/scripts/amalgm-mcp/workspace/rest.js +497 -0
  72. package/runtime/scripts/chat-core/adapters/claude.js +165 -0
  73. package/runtime/scripts/chat-core/adapters/codex.js +313 -0
  74. package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
  75. package/runtime/scripts/chat-core/auth.js +177 -0
  76. package/runtime/scripts/chat-core/contract.js +328 -0
  77. package/runtime/scripts/chat-core/credentials/store.js +212 -0
  78. package/runtime/scripts/chat-core/egress.js +87 -0
  79. package/runtime/scripts/chat-core/engine.js +253 -0
  80. package/runtime/scripts/chat-core/event-schema.js +231 -0
  81. package/runtime/scripts/chat-core/events.js +190 -0
  82. package/runtime/scripts/chat-core/index.js +11 -0
  83. package/runtime/scripts/chat-core/input.js +50 -0
  84. package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
  85. package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
  86. package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
  87. package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
  88. package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
  89. package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
  90. package/runtime/scripts/chat-core/parts.js +253 -0
  91. package/runtime/scripts/chat-core/recorder.js +65 -0
  92. package/runtime/scripts/chat-core/runtime.js +86 -0
  93. package/runtime/scripts/chat-core/server.js +163 -0
  94. package/runtime/scripts/chat-core/sse.js +196 -0
  95. package/runtime/scripts/chat-core/stores.js +100 -0
  96. package/runtime/scripts/chat-core/tool-display.js +149 -0
  97. package/runtime/scripts/chat-core/tool-shape.js +143 -0
  98. package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
  99. package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
  100. package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
  101. package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
  102. package/runtime/scripts/chat-core/usage.js +343 -0
  103. package/runtime/scripts/chat-server/config.js +110 -0
  104. package/runtime/scripts/chat-server/db.js +529 -0
  105. package/runtime/scripts/chat-server/index.js +33 -0
  106. package/runtime/scripts/chat-server/model-catalog.js +327 -0
  107. package/runtime/scripts/chat-server.js +75 -0
  108. package/runtime/scripts/credential-adapter.js +131 -0
  109. package/runtime/scripts/fs-watcher.js +888 -0
  110. package/runtime/scripts/local-gateway.js +854 -0
  111. package/runtime/scripts/platform-context.txt +246 -0
  112. package/runtime/scripts/port-monitor.js +175 -0
  113. package/runtime/scripts/proxy-token-store.js +162 -0
  114. package/runtime/scripts/runtime-auth.js +163 -0
  115. package/runtime/scripts/test-claude-code-models.js +87 -0
  116. package/runtime/tsconfig.json +15 -0
@@ -0,0 +1,377 @@
1
+ /**
2
+ * HTTP server — routes + lifecycle.
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 + agent run)
8
+ * GET /events → recent events (in-memory ring buffer)
9
+ * GET /tasks/list → list tasks
10
+ * POST /tasks/create → create task
11
+ * POST /tasks/update → update task
12
+ * POST /tasks/delete → delete task
13
+ * POST /tasks/run-now → run task immediately
14
+ * GET /event-triggers/list → list triggers
15
+ * POST /event-triggers/{create,update,delete}
16
+ * GET /workspace/projects → list projects
17
+ * POST /workspace/import → copy a local folder into workspaces
18
+ * POST /github/clone → clone repository onto the computer
19
+ * GET /workspace/branches → list git branches
20
+ * POST /workspace/checkout → checkout git branch
21
+ * GET /workspace/diff → git diff summary + patch
22
+ * GET /workspace/worktree → list worktrees
23
+ * POST /workspace/worktree → create worktree
24
+ * DELETE /workspace/worktree → remove worktree
25
+ * GET /fs/list → list files
26
+ * GET /fs/read → read file
27
+ * POST /fs/{write,delete,mkdir,rename}
28
+ * GET /mcp-connections → list sanitized MCP connections
29
+ * POST /mcp-connections → save/update MCP connection
30
+ * DELETE /mcp-connections → remove MCP connection
31
+ * GET /toolbox → list local ToolboxDB tools/actions
32
+ * POST /toolbox/tools → save/update a toolbox tool
33
+ * DELETE /toolbox/tools → remove a toolbox tool
34
+ * POST /toolbox/actions → save/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
+ * POST /local/init → init local computer state
40
+ * GET /local/credentials → discover local agent auth
41
+ * POST /local/credentials/resolve → resolve spawn env for agent auth
42
+ * POST /email/inbound → run a reply turn for inbound email
43
+ * POST /slack/inbound → run a reply turn for inbound Slack
44
+ * GET /credentials → list local Amalgm credential profiles
45
+ * POST /credentials → save local Amalgm credential profile
46
+ * DELETE /credentials → remove local Amalgm credential profile
47
+ * POST /user-api-keys → legacy save BYOK credential file
48
+ * DELETE /user-api-keys → legacy remove BYOK credential file
49
+ * GET /artifacts/list → list artifacts
50
+ * GET /artifacts/routes → artifact DNS routes for tunnel adverts
51
+ * POST /artifacts/{register,redeploy,start,stop,connect-dns,disconnect-dns}
52
+ * GET /agents/list → list agents
53
+ * POST /agents/{get,create,update,delete}
54
+ */
55
+
56
+ const http = require('http');
57
+ const { PORT, STORAGE_DIR } = require('../config');
58
+ const { hasSupabase } = require('../lib/supabase');
59
+ const { authorizeRuntimeHttp } = require('../../runtime-auth');
60
+
61
+ const { loadTasks } = require('../tasks/store');
62
+ const { loadAgents } = require('../agents/store');
63
+ const { loadArtifacts } = require('../artifacts/store');
64
+ const { activeAgentConversations } = require('../agents/talk');
65
+ const { startScheduler, stopScheduler } = require('../tasks/scheduler');
66
+ const { createTransport } = require('./mcp');
67
+ const { handleEventsPost } = require('../events/ingress');
68
+ const eventsRing = require('../events/ring-buffer');
69
+ const eventsRest = require('../events/rest');
70
+ const agentsRest = require('../agents/rest');
71
+ const artifactsRest = require('../artifacts/rest');
72
+ const emailInbound = require('../email/inbound');
73
+ const slackInbound = require('../slack/inbound');
74
+ const fsRest = require('../fs/rest');
75
+ const localRest = require('../local/rest');
76
+ const mcpConnectionsRest = require('../mcp-connections/rest');
77
+ const toolboxRest = require('../toolbox/rest');
78
+ const stateRest = require('../state/rest');
79
+ const tasksRest = require('../tasks/rest');
80
+ const userApiKeysRest = require('../user-api-keys/rest');
81
+ const credentialsRest = require('../credentials/rest');
82
+ const workspaceRest = require('../workspace/rest');
83
+
84
+ function createServer() {
85
+ return http.createServer(async (req, res) => {
86
+ if (!authorizeRuntimeHttp(req, res, { exposeHeaders: 'Mcp-Session-Id' })) return;
87
+
88
+ async function readJsonBody() {
89
+ const chunks = [];
90
+ for await (const chunk of req) chunks.push(chunk);
91
+ return JSON.parse(Buffer.concat(chunks).toString());
92
+ }
93
+ function sendJson(statusCode, data) {
94
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
95
+ res.end(JSON.stringify(data));
96
+ }
97
+ function getQuery() {
98
+ return Object.fromEntries(new URL(req.url, 'http://127.0.0.1').searchParams.entries());
99
+ }
100
+
101
+ try {
102
+ // ── MCP ─────────────────────────────────────────────────────────────────
103
+ // Delegate anything under /mcp directly to the SDK transport. A fresh
104
+ // transport + Server pair is created per request: the stateless
105
+ // StreamableHTTPServerTransport is single-shot in this mode, so reusing
106
+ // one across requests would fail on the second call with HTTP 500.
107
+ if (req.url === '/mcp' || req.url === '/mcp/' || req.url.startsWith('/mcp?')) {
108
+ const transport = await createTransport();
109
+ await transport.handleRequest(req, res);
110
+ return;
111
+ }
112
+
113
+ // ── Health ──────────────────────────────────────────────────────────────
114
+ if (req.url === '/healthz') {
115
+ const tasks = loadTasks();
116
+ const agents = loadAgents();
117
+ return sendJson(200, {
118
+ status: 'ok',
119
+ tasks: tasks.tasks.length,
120
+ enabledTasks: tasks.tasks.filter((t) => t.enabled).length,
121
+ artifacts: loadArtifacts().artifacts.length,
122
+ customAgents: agents.agents.length,
123
+ activeAgentConversations: activeAgentConversations.size,
124
+ });
125
+ }
126
+
127
+ // ── /state — Local Live Store snapshot/replay/stream ─────────────────
128
+ if (req.url.startsWith('/state/snapshot') && req.method === 'GET') {
129
+ return stateRest.handleSnapshot(getQuery(), sendJson);
130
+ }
131
+ if (req.url.startsWith('/state/events') && req.method === 'GET') {
132
+ return stateRest.handleEvents(getQuery(), sendJson);
133
+ }
134
+ if (req.url.startsWith('/state/stream') && req.method === 'GET') {
135
+ return stateRest.handleStream(req, res, getQuery());
136
+ }
137
+
138
+ // ── /agents (REST, not MCP) ─────────────────────────────────────────────
139
+ if (req.url === '/agents/list' && req.method === 'GET') {
140
+ return agentsRest.handleList(sendJson);
141
+ }
142
+ if (req.url === '/agents/get' && req.method === 'POST') {
143
+ return agentsRest.handleGet(await readJsonBody(), sendJson);
144
+ }
145
+ if (req.url === '/agents/create' && req.method === 'POST') {
146
+ return agentsRest.handleCreate(await readJsonBody(), sendJson);
147
+ }
148
+ if (req.url === '/agents/update' && req.method === 'POST') {
149
+ return agentsRest.handleUpdate(await readJsonBody(), sendJson);
150
+ }
151
+ if (req.url === '/agents/delete' && req.method === 'POST') {
152
+ return agentsRest.handleDelete(await readJsonBody(), sendJson);
153
+ }
154
+
155
+ // ── /events — webhook ingress ───────────────────────────────────────────
156
+ if (req.url === '/events' && req.method === 'POST') {
157
+ return handleEventsPost(req, sendJson);
158
+ }
159
+ if (req.url === '/events' && req.method === 'GET') {
160
+ return sendJson(200, { events: eventsRing.recent(50) });
161
+ }
162
+
163
+ // ── /tasks REST ─────────────────────────────────────────────────────────
164
+ if (req.url === '/tasks/list' && req.method === 'GET') {
165
+ return tasksRest.handleList(sendJson);
166
+ }
167
+ if (req.url === '/tasks/create' && req.method === 'POST') {
168
+ return tasksRest.handleCreate(await readJsonBody(), sendJson);
169
+ }
170
+ if (req.url === '/tasks/update' && req.method === 'POST') {
171
+ return tasksRest.handleUpdate(await readJsonBody(), sendJson);
172
+ }
173
+ if (req.url === '/tasks/delete' && req.method === 'POST') {
174
+ return tasksRest.handleDelete(await readJsonBody(), sendJson);
175
+ }
176
+ if (req.url === '/tasks/run-now' && req.method === 'POST') {
177
+ return tasksRest.handleRunNow(await readJsonBody(), sendJson);
178
+ }
179
+
180
+ // ── /event-triggers REST ────────────────────────────────────────────────
181
+ if (req.url === '/event-triggers/list' && req.method === 'GET') {
182
+ return eventsRest.handleList(req, sendJson);
183
+ }
184
+ if (req.url === '/event-triggers/create' && req.method === 'POST') {
185
+ return eventsRest.handleCreate(await readJsonBody(), sendJson);
186
+ }
187
+ if (req.url === '/event-triggers/update' && req.method === 'POST') {
188
+ return eventsRest.handleUpdate(await readJsonBody(), sendJson);
189
+ }
190
+ if (req.url === '/event-triggers/delete' && req.method === 'POST') {
191
+ return eventsRest.handleDelete(await readJsonBody(), sendJson);
192
+ }
193
+
194
+ // ── /workspace REST ─────────────────────────────────────────────────────
195
+ if (req.url === '/workspace/projects' && req.method === 'GET') {
196
+ return workspaceRest.handleProjects(sendJson);
197
+ }
198
+ if (req.url === '/workspace/import' && req.method === 'POST') {
199
+ return workspaceRest.handleImport(await readJsonBody(), sendJson);
200
+ }
201
+ if (req.url === '/github/clone' && req.method === 'POST') {
202
+ return workspaceRest.handleClone(await readJsonBody(), sendJson);
203
+ }
204
+ if (req.url.startsWith('/workspace/branches') && req.method === 'GET') {
205
+ return workspaceRest.handleBranches(getQuery(), sendJson);
206
+ }
207
+ if (req.url === '/workspace/checkout' && req.method === 'POST') {
208
+ return workspaceRest.handleCheckout(await readJsonBody(), sendJson);
209
+ }
210
+ if (req.url.startsWith('/workspace/diff') && req.method === 'GET') {
211
+ return workspaceRest.handleDiff(getQuery(), sendJson);
212
+ }
213
+ if (req.url.startsWith('/workspace/worktree') && req.method === 'GET') {
214
+ return workspaceRest.handleWorktreeList(getQuery(), sendJson);
215
+ }
216
+ if (req.url === '/workspace/worktree' && req.method === 'POST') {
217
+ return workspaceRest.handleWorktreeCreate(await readJsonBody(), sendJson);
218
+ }
219
+ if (req.url === '/workspace/worktree' && req.method === 'DELETE') {
220
+ return workspaceRest.handleWorktreeDelete(await readJsonBody(), sendJson);
221
+ }
222
+
223
+ // ── /fs REST ────────────────────────────────────────────────────────────
224
+ if (req.url.startsWith('/fs/list') && req.method === 'GET') {
225
+ return fsRest.handleList(getQuery(), sendJson);
226
+ }
227
+ if (req.url.startsWith('/fs/read') && req.method === 'GET') {
228
+ return fsRest.handleRead(getQuery(), sendJson);
229
+ }
230
+ if (req.url === '/fs/write' && req.method === 'POST') {
231
+ return fsRest.handleWrite(await readJsonBody(), sendJson);
232
+ }
233
+ if (req.url === '/fs/delete' && req.method === 'POST') {
234
+ return fsRest.handleDelete(await readJsonBody(), sendJson);
235
+ }
236
+ if (req.url === '/fs/mkdir' && req.method === 'POST') {
237
+ return fsRest.handleMkdir(await readJsonBody(), sendJson);
238
+ }
239
+ if (req.url === '/fs/rename' && req.method === 'POST') {
240
+ return fsRest.handleRename(await readJsonBody(), sendJson);
241
+ }
242
+
243
+ // ── /mcp-connections REST ──────────────────────────────────────────────
244
+ if (req.url === '/mcp-connections' && req.method === 'GET') {
245
+ return mcpConnectionsRest.handleList(sendJson);
246
+ }
247
+ if (req.url === '/mcp-connections' && req.method === 'POST') {
248
+ return mcpConnectionsRest.handleUpsert(await readJsonBody(), sendJson);
249
+ }
250
+ if (req.url === '/mcp-connections' && req.method === 'DELETE') {
251
+ return mcpConnectionsRest.handleDelete(await readJsonBody(), sendJson);
252
+ }
253
+
254
+ // ── /toolbox REST ─────────────────────────────────────────────────────
255
+ if (req.url === '/toolbox' && req.method === 'GET') {
256
+ return toolboxRest.handleList(sendJson);
257
+ }
258
+ if (req.url === '/toolbox/tools' && req.method === 'POST') {
259
+ return toolboxRest.handleUpsertTool(await readJsonBody(), sendJson);
260
+ }
261
+ if (req.url === '/toolbox/tools' && req.method === 'DELETE') {
262
+ return toolboxRest.handleDeleteTool(await readJsonBody(), sendJson);
263
+ }
264
+ if (req.url === '/toolbox/actions' && req.method === 'POST') {
265
+ return toolboxRest.handleUpsertAction(await readJsonBody(), sendJson);
266
+ }
267
+ if (req.url === '/toolbox/actions' && req.method === 'DELETE') {
268
+ return toolboxRest.handleDeleteAction(await readJsonBody(), sendJson);
269
+ }
270
+
271
+ // ── /local REST ────────────────────────────────────────────────────────
272
+ if (req.url === '/local/init' && req.method === 'POST') {
273
+ return localRest.handleInit(sendJson);
274
+ }
275
+ if (req.url === '/local/credentials' && req.method === 'GET') {
276
+ return localRest.handleCredentials(sendJson);
277
+ }
278
+ if (req.url === '/local/credentials/resolve' && req.method === 'POST') {
279
+ return localRest.handleResolve(await readJsonBody(), sendJson);
280
+ }
281
+
282
+ // ── /credentials REST ─────────────────────────────────────────────────
283
+ if (req.url === '/credentials' && req.method === 'GET') {
284
+ return credentialsRest.handleList(sendJson);
285
+ }
286
+ if (req.url === '/credentials' && req.method === 'POST') {
287
+ return credentialsRest.handleSave(await readJsonBody(), sendJson);
288
+ }
289
+ if (req.url === '/credentials' && req.method === 'DELETE') {
290
+ return credentialsRest.handleDelete(await readJsonBody(), sendJson);
291
+ }
292
+
293
+ // ── /email REST ────────────────────────────────────────────────────────
294
+ if (req.url === '/email/inbound' && req.method === 'POST') {
295
+ return emailInbound.handleEmailInbound(await readJsonBody(), sendJson);
296
+ }
297
+ if (req.url === '/slack/inbound' && req.method === 'POST') {
298
+ return slackInbound.handleSlackInbound(await readJsonBody(), sendJson);
299
+ }
300
+
301
+ // ── /user-api-keys REST ───────────────────────────────────────────────
302
+ if (req.url === '/user-api-keys' && req.method === 'POST') {
303
+ return userApiKeysRest.handleSave(await readJsonBody(), sendJson);
304
+ }
305
+ if (req.url === '/user-api-keys' && req.method === 'DELETE') {
306
+ return userApiKeysRest.handleDelete(sendJson);
307
+ }
308
+
309
+ // ── /artifacts REST ────────────────────────────────────────────────────
310
+ if (req.url === '/artifacts/list' && req.method === 'GET') {
311
+ return artifactsRest.handleList(sendJson);
312
+ }
313
+ if (req.url === '/artifacts/routes' && req.method === 'GET') {
314
+ return artifactsRest.handleRoutes(sendJson);
315
+ }
316
+ if (req.url === '/artifacts/register' && req.method === 'POST') {
317
+ return artifactsRest.handleRegister(await readJsonBody(), sendJson);
318
+ }
319
+ if (req.url === '/artifacts/redeploy' && req.method === 'POST') {
320
+ return artifactsRest.handleRedeploy(await readJsonBody(), sendJson);
321
+ }
322
+ if (req.url === '/artifacts/start' && req.method === 'POST') {
323
+ return artifactsRest.handleStart(await readJsonBody(), sendJson);
324
+ }
325
+ if (req.url === '/artifacts/stop' && req.method === 'POST') {
326
+ return artifactsRest.handleStop(await readJsonBody(), sendJson);
327
+ }
328
+ if (req.url === '/artifacts/delete' && req.method === 'POST') {
329
+ return artifactsRest.handleDelete(await readJsonBody(), sendJson);
330
+ }
331
+ if (req.url === '/artifacts/connect-dns' && req.method === 'POST') {
332
+ return artifactsRest.handleConnectDns(await readJsonBody(), sendJson);
333
+ }
334
+ if (req.url === '/artifacts/disconnect-dns' && req.method === 'POST') {
335
+ return artifactsRest.handleDisconnectDns(await readJsonBody(), sendJson);
336
+ }
337
+
338
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
339
+ res.end('Not found');
340
+ } catch (err) {
341
+ console.error('[AmalgmMCP] Unhandled error:', err);
342
+ if (!res.headersSent) sendJson(500, { error: 'Internal server error' });
343
+ }
344
+ });
345
+ }
346
+
347
+ async function listen() {
348
+ const httpServer = createServer();
349
+
350
+ httpServer.listen(PORT, '127.0.0.1', async () => {
351
+ console.log(`[AmalgmMCP] Listening on 127.0.0.1:${PORT}`);
352
+ console.log(`[AmalgmMCP] Storage: ${STORAGE_DIR}`);
353
+ console.log(
354
+ `[AmalgmMCP] Supabase: ${hasSupabase() ? 'connected' : 'not configured (runs are local-only)'}`,
355
+ );
356
+ // Hydrate user model prefs non-blocking (requires Supabase).
357
+ require('../lib/prefs').hydrateModelPreferences().catch(() => {});
358
+ startScheduler();
359
+ });
360
+
361
+ // Graceful shutdown. Running tasks get aborted via SIGTERM → process exit;
362
+ // we don't try to individually abort them here since chat-server owns its own
363
+ // runtime teardown paths.
364
+ function shutdown() {
365
+ console.log('[AmalgmMCP] Shutting down...');
366
+ stopScheduler();
367
+ httpServer.close(() => process.exit(0));
368
+ setTimeout(() => process.exit(0), 5000);
369
+ }
370
+
371
+ process.on('SIGTERM', shutdown);
372
+ process.on('SIGINT', shutdown);
373
+
374
+ return httpServer;
375
+ }
376
+
377
+ module.exports = { createServer, listen };
@@ -0,0 +1,127 @@
1
+ /**
2
+ * MCP server — built on @modelcontextprotocol/sdk.
3
+ *
4
+ * The SDK handles JSON-RPC framing, protocol-version negotiation, batching,
5
+ * and streamable-HTTP transport. We just register the two tool handlers.
6
+ *
7
+ * Each domain module exports a flat array of
8
+ * { name, description, inputSchema, handler }. We concatenate the built-in
9
+ * Amalgm tools with toolbox management tools. Custom CLI/API actions registered
10
+ * in the ToolboxDB are exposed dynamically through this same MCP transport.
11
+ */
12
+
13
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
14
+ const {
15
+ StreamableHTTPServerTransport,
16
+ } = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
17
+ const {
18
+ ListToolsRequestSchema,
19
+ CallToolRequestSchema,
20
+ } = require('@modelcontextprotocol/sdk/types.js');
21
+
22
+ const { AMALGM_USER_ID } = require('../config');
23
+ const { hasSupabase, supabaseSelect } = require('../lib/supabase');
24
+ const { attachActionDescriptor } = require('../lib/tool-result');
25
+ const { CORE_TOOLS } = require('./core-tools');
26
+ const {
27
+ callToolboxMcpTool,
28
+ listToolboxMcpTools,
29
+ } = require('../toolbox/runner');
30
+
31
+ const TOOLS = [
32
+ ...CORE_TOOLS,
33
+ ...require('../toolbox/tools'),
34
+ ];
35
+
36
+ /**
37
+ * Look up the caller's session to tag DB rows with origin metadata. Reads
38
+ * x-amalgm-session-id from the transport's passed-through HTTP headers.
39
+ */
40
+ async function buildRequestContext(extra) {
41
+ const headers = extra?.requestInfo?.headers || {};
42
+ const h = headers['x-amalgm-session-id'];
43
+ const callerSessionId = Array.isArray(h) ? h[0] : h || null;
44
+ const ctx = { callerSessionId };
45
+
46
+ if (callerSessionId && hasSupabase()) {
47
+ const rows = await supabaseSelect(
48
+ 'sessions',
49
+ `id=eq.${callerSessionId}&user_id=eq.${AMALGM_USER_ID}&select=id,title,harness,metadata`,
50
+ );
51
+ const parent = Array.isArray(rows) ? rows[0] : null;
52
+ if (parent) {
53
+ ctx.sessionMetadata = parent.metadata || {};
54
+ ctx.originName = parent.title || undefined;
55
+ ctx.originHarnessId = parent.harness || undefined;
56
+ ctx.originBaseHarnessId = parent.metadata?.baseHarnessId || undefined;
57
+ }
58
+ }
59
+ return ctx;
60
+ }
61
+
62
+ function createMcpServer() {
63
+ const server = new Server(
64
+ { name: 'amalgm', version: '1.0.0' },
65
+ { capabilities: { tools: {} } },
66
+ );
67
+
68
+ server.setRequestHandler(ListToolsRequestSchema, async (_request, extra) => {
69
+ const ctx = await buildRequestContext(extra);
70
+ return {
71
+ tools: [
72
+ ...TOOLS.map(({ name, description, inputSchema }) => ({
73
+ name,
74
+ description,
75
+ inputSchema,
76
+ })),
77
+ ...listToolboxMcpTools(ctx),
78
+ ],
79
+ };
80
+ });
81
+
82
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
83
+ const toolName = request.params?.name;
84
+ const toolArgs = request.params?.arguments || {};
85
+ const tool = TOOLS.find((t) => t.name === toolName);
86
+ if (!tool) {
87
+ const ctx = await buildRequestContext(extra);
88
+ const toolboxResult = await callToolboxMcpTool(toolName, toolArgs, ctx);
89
+ if (toolboxResult) return attachActionDescriptor(toolboxResult, toolName, toolArgs);
90
+ return attachActionDescriptor({
91
+ content: [{ type: 'text', text: `Unknown tool: ${toolName}` }],
92
+ isError: true,
93
+ }, toolName || 'unknown', toolArgs);
94
+ }
95
+ const ctx = await buildRequestContext(extra);
96
+ try {
97
+ const result = await tool.handler(toolArgs, ctx);
98
+ return attachActionDescriptor(result, toolName, toolArgs);
99
+ } catch (err) {
100
+ return attachActionDescriptor({
101
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
102
+ isError: true,
103
+ }, toolName, toolArgs);
104
+ }
105
+ });
106
+
107
+ return server;
108
+ }
109
+
110
+ /**
111
+ * One stateless streamable-HTTP transport + Server pair. The transport is
112
+ * reusable across requests because we don't use session IDs or bidirectional
113
+ * notifications — every tools/list and tools/call is self-contained.
114
+ */
115
+ async function createTransport() {
116
+ const server = createMcpServer();
117
+ const transport = new StreamableHTTPServerTransport({
118
+ sessionIdGenerator: undefined,
119
+ // Return plain JSON responses instead of opening an SSE stream. We don't
120
+ // emit progress notifications, so there's nothing to stream.
121
+ enableJsonResponse: true,
122
+ });
123
+ await server.connect(transport);
124
+ return transport;
125
+ }
126
+
127
+ module.exports = { createTransport, TOOLS };