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,13 +1,27 @@
1
1
  'use strict';
2
2
 
3
- const { readToolbox } = require('../toolbox/store');
4
- const { toolboxMcpToolName } = require('../toolbox/runner');
3
+ const { toolboxMcpToolName } = require('../toolbox/names');
4
+ const {
5
+ defaultToolboxService: toolboxService,
6
+ } = require('../toolbox/service');
5
7
 
6
- function coreTools() {
8
+ function firstPartyToolGroups() {
7
9
  return [
8
- ...require('../notify'),
9
- ...require('../agents/tools'),
10
- ...require('../apps/tools'),
10
+ {
11
+ id: 'amalgm.notifications',
12
+ kind: 'first_party',
13
+ tools: require('../notify'),
14
+ },
15
+ {
16
+ id: 'amalgm.agents',
17
+ kind: 'first_party',
18
+ tools: require('../agents/tools'),
19
+ },
20
+ {
21
+ id: 'amalgm.apps',
22
+ kind: 'first_party',
23
+ tools: require('../apps/tools'),
24
+ },
11
25
  ];
12
26
  }
13
27
 
@@ -19,35 +33,27 @@ function normalizeRef(value) {
19
33
  return String(value || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '');
20
34
  }
21
35
 
22
- function isRunnableToolboxTool(tool) {
23
- return !!tool
24
- && tool.status === 'enabled'
25
- && tool.origin !== 'system'
26
- && (tool.type === 'cli' || tool.type === 'api');
27
- }
28
-
29
- function resolveAmalgmCoreTool(actionName) {
30
- const normalized = String(actionName || '').trim();
31
- return coreTools().find((tool) => (
32
- tool.name === normalized
33
- || tool.name === normalized.replace(/\./g, '_')
34
- )) || null;
36
+ function resolveFirstPartyToolAction(toolId, actionName) {
37
+ const normalizedToolId = String(toolId || '').trim();
38
+ const normalizedActionName = String(actionName || '').trim();
39
+ const group = firstPartyToolGroups().find((candidate) => candidate.id === normalizedToolId);
40
+ if (!group) return null;
41
+ const tool = group.tools.find((candidate) => (
42
+ candidate.name === normalizedActionName
43
+ || candidate.name === normalizedActionName.replace(/\./g, '_')
44
+ ));
45
+ if (!tool) return null;
46
+ return {
47
+ kind: 'first_party',
48
+ ref: actionKey(group.id, tool.name),
49
+ toolId: group.id,
50
+ actionName: tool.name,
51
+ tool,
52
+ };
35
53
  }
36
54
 
37
55
  function resolveToolboxAction(toolId, actionName) {
38
- const normalizedToolId = String(toolId || '').trim();
39
- const normalizedActionName = String(actionName || '').trim();
40
- const toolbox = readToolbox();
41
- const action = Object.values(toolbox.toolActions || {}).find((candidate) => {
42
- if (candidate.toolId !== normalizedToolId) return false;
43
- return candidate.name === normalizedActionName
44
- || candidate.id === normalizedActionName
45
- || candidate.id === `${normalizedToolId}.${normalizedActionName}`
46
- || candidate.id === `${normalizedToolId}:${normalizedActionName}`;
47
- });
48
- if (!action || action.status !== 'enabled') return null;
49
- const tool = toolbox.tools?.[action.toolId];
50
- return isRunnableToolboxTool(tool) ? { tool, action } : null;
56
+ return toolboxService.resolveRunnableAction(toolId, actionName);
51
57
  }
52
58
 
53
59
  function resolveWorkflowToolAction(toolId, actionName) {
@@ -62,17 +68,9 @@ function resolveWorkflowToolAction(toolId, actionName) {
62
68
  description: 'Fetch a URL from a workflow; requires allowlist.network=true at runtime.',
63
69
  };
64
70
  }
65
- if (normalizedToolId === 'amalgm') {
66
- const tool = resolveAmalgmCoreTool(normalizedActionName);
67
- if (!tool) return null;
68
- return {
69
- kind: 'amalgm',
70
- ref: actionKey('amalgm', tool.name),
71
- toolId: 'amalgm',
72
- actionName: tool.name,
73
- tool,
74
- };
75
- }
71
+ const firstParty = resolveFirstPartyToolAction(normalizedToolId, normalizedActionName);
72
+ if (firstParty) return firstParty;
73
+
76
74
  const resolved = resolveToolboxAction(normalizedToolId, normalizedActionName);
77
75
  if (!resolved) return null;
78
76
  return {
@@ -86,7 +84,6 @@ function resolveWorkflowToolAction(toolId, actionName) {
86
84
  }
87
85
 
88
86
  function workflowActionSummaries() {
89
- const toolbox = readToolbox();
90
87
  const actions = [{
91
88
  ref: 'http.fetch',
92
89
  id: 'http.fetch',
@@ -96,20 +93,20 @@ function workflowActionSummaries() {
96
93
  description: 'Fetch a URL; requires allowlist.network=true.',
97
94
  }];
98
95
 
99
- for (const tool of coreTools()) {
100
- actions.push({
101
- ref: actionKey('amalgm', tool.name),
102
- id: actionKey('amalgm', tool.name),
103
- toolId: 'amalgm',
104
- actionName: tool.name,
105
- kind: 'amalgm',
106
- description: tool.description || '',
107
- });
96
+ for (const group of firstPartyToolGroups()) {
97
+ for (const tool of group.tools) {
98
+ actions.push({
99
+ ref: actionKey(group.id, tool.name),
100
+ id: actionKey(group.id, tool.name),
101
+ toolId: group.id,
102
+ actionName: tool.name,
103
+ kind: 'first_party',
104
+ description: tool.description || '',
105
+ });
106
+ }
108
107
  }
109
108
 
110
- for (const action of Object.values(toolbox.toolActions || {})) {
111
- const tool = toolbox.tools?.[action.toolId];
112
- if (!isRunnableToolboxTool(tool) || action.status !== 'enabled') continue;
109
+ for (const { tool, action } of toolboxService.listRunnableActions()) {
113
110
  actions.push({
114
111
  ref: actionKey(tool.id, action.name),
115
112
  id: action.id,
@@ -168,7 +165,7 @@ function validateWorkflowToolActions(ir) {
168
165
  }
169
166
 
170
167
  module.exports = {
171
- resolveAmalgmCoreTool,
168
+ resolveFirstPartyToolAction,
172
169
  resolveToolboxAction,
173
170
  resolveWorkflowToolAction,
174
171
  suggestWorkflowToolActions,
@@ -24,10 +24,10 @@ const WORKFLOW_DESCRIPTION = [
24
24
  'The script is the workflow. A trigger decides when it runs; cells decide what happens.',
25
25
  'Workflow `trigger` refs are `source.event` strings such as `github.push`; `github.*` means any GitHub event; `*.push` means a push from any source; `*.*` means any source and event.',
26
26
  'Cells can call `code(name, fn)`, gated local `cli(name, config)`, gated `http(name, config)`, and `tool(name, "tool.action", args)`.',
27
- 'Agents are tools too: use `agent(name, agentIdOrName, { prompt, run_in_background: true })` or `tool(name, "amalgm.talk_to_agent", args)`.',
27
+ 'Agents are tools too: use `agent(name, agentIdOrName, { prompt, run_in_background: true })` or `tool(name, "amalgm.agents.talk_to_agent", args)`.',
28
28
  'Every function receives one ctx object: `{ automation, trigger, workflow, event, payload, headers, previous, outputs, cells, secrets, stop }`.',
29
29
  'Cell outputs are available as `ctx.cells.cellName.output`, `ctx.cells.cellName.field` for object outputs, `ctx.outputs.cellName`, and top-level `ctx.cellName`.',
30
- 'Example: `tool("notify", "amalgm.notify_user", ({ cells }) => ({ message: cells.greet.output.message }))`.',
30
+ 'Example: `tool("notify", "amalgm.notifications.notify_user", ({ cells }) => ({ message: cells.greet.output.message }))`.',
31
31
  ].join('\n\n');
32
32
 
33
33
  function compactAutomation(automation, includeWorkflowText = true) {
@@ -180,12 +180,12 @@ module.exports = [
180
180
  {
181
181
  name: 'automations_list_actions',
182
182
  description:
183
- 'List workflow-callable tool actions for automation scripts. Use these refs in workflow cells like `tool("name", "tool.ref", args)`. This includes built-in `http.fetch`, Amalgm actions such as `amalgm.notify_user`, and runnable Toolbox CLI/API actions.',
183
+ 'List workflow-callable tool actions for automation scripts. Use these refs in workflow cells like `tool("name", "tool.ref", args)`. This includes built-in `http.fetch`, first-party Toolbox actions such as `amalgm.notifications.notify_user`, and runnable Toolbox CLI/API actions.',
184
184
  inputSchema: {
185
185
  type: 'object',
186
186
  properties: {
187
187
  query: { type: 'string', description: 'Optional case-insensitive filter across ref, id, name, and description.' },
188
- kind: { type: 'string', enum: ['builtin', 'amalgm', 'toolbox'], description: 'Optional action kind filter.' },
188
+ kind: { type: 'string', enum: ['builtin', 'first_party', 'toolbox'], description: 'Optional action kind filter.' },
189
189
  },
190
190
  },
191
191
  async handler({ query, kind } = {}) {
@@ -41,6 +41,24 @@ function asString(value) {
41
41
  return typeof value === 'string' && value.trim() ? value.trim() : null;
42
42
  }
43
43
 
44
+ function normalizeStringList(values) {
45
+ if (!Array.isArray(values)) return [];
46
+ return [...new Set(values
47
+ .filter((value) => typeof value === 'string')
48
+ .map((value) => value.trim())
49
+ .filter(Boolean))];
50
+ }
51
+
52
+ function sessionToolConfig(metadata) {
53
+ const tools = metadata?.tools && typeof metadata.tools === 'object' && !Array.isArray(metadata.tools)
54
+ ? metadata.tools
55
+ : {};
56
+ return {
57
+ mode: tools.mode === 'selected' ? 'selected' : 'all',
58
+ toolIds: normalizeStringList(tools.selectedToolIds),
59
+ };
60
+ }
61
+
44
62
  function normalizeSubject(subject) {
45
63
  const trimmed = asString(subject) || 'Amalgm reply';
46
64
  return /^re:/i.test(trimmed) ? trimmed : `Re: ${trimmed}`;
@@ -124,6 +142,7 @@ async function handleEmailInbound(body, sendJson) {
124
142
  const defaultAuthMethod = asString(metadata.authMethod) || resolveDefaultAuthMethod(baseHarnessId);
125
143
  const cwd = asString(metadata.cwd) || DEFAULT_CWD;
126
144
  const computerId = asString(metadata.computerId);
145
+ const toolConfig = sessionToolConfig(metadata);
127
146
 
128
147
  const normalizedChatInput = normalizeChatInput({
129
148
  parts: [{ type: 'text', text: prompt }],
@@ -136,6 +155,8 @@ async function handleEmailInbound(body, sendJson) {
136
155
  },
137
156
  tools: {
138
157
  mcpAppIds: Array.isArray(metadata.mcpAppIds) ? metadata.mcpAppIds : [],
158
+ mode: toolConfig.mode,
159
+ toolIds: toolConfig.toolIds,
139
160
  },
140
161
  execution: {
141
162
  cwd,
@@ -28,7 +28,7 @@ function engineNpmPublishWorkflow(repoPath) {
28
28
  allowlist: {
29
29
  localCompute: true,
30
30
  secrets: ["NODE_AUTH_TOKEN"],
31
- actions: ["amalgm.notify_user"]
31
+ actions: ["amalgm.notifications.notify_user"]
32
32
  },
33
33
 
34
34
  limits: {
@@ -116,7 +116,7 @@ function engineNpmPublishWorkflow(repoPath) {
116
116
  timeoutMs: 600000
117
117
  }),
118
118
 
119
- tool("notify_user", "amalgm", "notify_user", {
119
+ tool("notify_user", "amalgm.notifications.notify_user", {
120
120
  subject: "amalgm npm publish complete",
121
121
  level: "success",
122
122
  message: ({ outputs }) => {
@@ -153,7 +153,7 @@ function internalWorkflowSeeds() {
153
153
  allowlist: {
154
154
  localCompute: true,
155
155
  secrets: ['NODE_AUTH_TOKEN'],
156
- actions: ['amalgm.notify_user'],
156
+ actions: ['amalgm.notifications.notify_user'],
157
157
  },
158
158
  limits: {
159
159
  maxConcurrentRuns: 1,
@@ -19,7 +19,9 @@
19
19
  * agents/ agents CRUD + talk_to_agent
20
20
  * browser/ visible Electron browser tools + agent-browser fallback
21
21
  * notify/ notify_user (email via proxy)
22
- * server/ MCP JSON-RPC dispatch + HTTP route table
22
+ * server/ HTTP service boundaries + MCP adapter plumbing
23
+ * services/ optional standalone process entrypoints for
24
+ * control-plane, MCP adapter, and worker
23
25
  */
24
26
 
25
27
  async function ensureProxyToken() {
@@ -365,6 +365,20 @@ function reasoningEffortForSelection(harnessId, modelId, normalizedEffort) {
365
365
  return normalizeReasoningEffort(settings.effort || settings.reasoningEffort || settings.reasoning) || 'xhigh';
366
366
  }
367
367
 
368
+ function getModelSettingsForSelection(harnessId, modelId) {
369
+ const normalizedModelId = normalizeModelSelection(harnessId, modelId).modelId || modelId;
370
+ const settingsKey = harnessId === 'codex'
371
+ ? codexModelSettingsKey(normalizedModelId)
372
+ : normalizedModelId;
373
+ const settings =
374
+ (settingsKey && _modelSettings[settingsKey]) ||
375
+ (modelId && _modelSettings[modelId]) ||
376
+ null;
377
+ return isPlainObject(settings) && Object.keys(settings).length > 0
378
+ ? { ...settings }
379
+ : null;
380
+ }
381
+
368
382
  function preferredWorkspaceCwd() {
369
383
  try {
370
384
  const { listWorkspaces } = require('../workspace/store');
@@ -635,6 +649,7 @@ module.exports = {
635
649
  writeUserPreferences,
636
650
  updateUserPreferences,
637
651
  getSelectedModel,
652
+ getModelSettingsForSelection,
638
653
  getPreferredAuthMethod,
639
654
  resolveModelSelection,
640
655
  };
@@ -8,7 +8,9 @@ const os = require('os');
8
8
  const path = require('path');
9
9
  const { AMALGM_DIR } = require('../config');
10
10
  const { appendStateEvent } = require('../state/events');
11
- const { deleteTool, upsertTool } = require('../toolbox/store');
11
+ const {
12
+ defaultToolboxService: toolboxService,
13
+ } = require('../toolbox/service');
12
14
 
13
15
  const CONNECTIONS_FILE = path.join(AMALGM_DIR, 'mcp-connections.json');
14
16
  const CLAUDE_FILE = path.join(os.homedir(), '.claude.json');
@@ -139,7 +141,7 @@ function mirrorConnectionToToolbox(appId, conn) {
139
141
  url: conn?.instanceUrl || conn?.config?.url,
140
142
  };
141
143
 
142
- upsertTool({
144
+ toolboxService.registerTool({
143
145
  id: app?.id || appId,
144
146
  name: app?.name || appId,
145
147
  type: 'mcp',
@@ -165,7 +167,7 @@ function mirrorConnectionToToolbox(appId, conn) {
165
167
 
166
168
  function removeConnectionFromToolbox(appId) {
167
169
  try {
168
- deleteTool(appId);
170
+ toolboxService.deleteTool(appId);
169
171
  } catch {
170
172
  // The toolbox mirror is best-effort; MCP connection removal should still
171
173
  // succeed even if a future system connector cannot be deleted.
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ startAutomationScheduler,
5
+ stopAutomationScheduler,
6
+ } = require('./automations/scheduler');
7
+
8
+ function createRuntimeWorker(scheduler = {
9
+ startAutomationScheduler,
10
+ stopAutomationScheduler,
11
+ }) {
12
+ let running = false;
13
+
14
+ return {
15
+ get running() {
16
+ return running;
17
+ },
18
+ start() {
19
+ if (running) return false;
20
+ scheduler.startAutomationScheduler();
21
+ running = true;
22
+ return true;
23
+ },
24
+ stop() {
25
+ if (!running) return false;
26
+ scheduler.stopAutomationScheduler();
27
+ running = false;
28
+ return true;
29
+ },
30
+ };
31
+ }
32
+
33
+ const defaultRuntimeWorker = createRuntimeWorker();
34
+
35
+ function startRuntimeWorker() {
36
+ return defaultRuntimeWorker.start();
37
+ }
38
+
39
+ function stopRuntimeWorker() {
40
+ return defaultRuntimeWorker.stop();
41
+ }
42
+
43
+ module.exports = {
44
+ createRuntimeWorker,
45
+ startRuntimeWorker,
46
+ stopRuntimeWorker,
47
+ };
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const { createRuntimeHttpServer } = require('./http-service');
4
+ const { createControlPlaneHandler } = require('./control-plane-service');
5
+ const { createMcpHttpHandler } = require('./mcp-http-service');
6
+
7
+ function createCombinedHandler(options = {}) {
8
+ const mcpHandler = options.mcpHandler || createMcpHttpHandler(options);
9
+ const controlPlaneHandler = options.controlPlaneHandler || createControlPlaneHandler(options);
10
+
11
+ return async function handleCombinedRequest(ctx) {
12
+ if (await mcpHandler(ctx)) return true;
13
+ return controlPlaneHandler(ctx);
14
+ };
15
+ }
16
+
17
+ function createCombinedServer(options = {}) {
18
+ return createRuntimeHttpServer(createCombinedHandler(options), {
19
+ serviceName: options.serviceName || 'AmalgmMCP',
20
+ exposeHeaders: options.exposeHeaders,
21
+ });
22
+ }
23
+
24
+ module.exports = {
25
+ createCombinedHandler,
26
+ createCombinedServer,
27
+ };
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const { createRuntimeHttpServer } = require('./http-service');
4
+ const { createLocalServiceRouter } = require('./local-service-router');
5
+
6
+ function createControlPlaneHandler(options = {}) {
7
+ const serviceRouter = options.serviceRouter || createLocalServiceRouter(options.routeGroups);
8
+
9
+ return async function handleControlPlaneRequest(ctx) {
10
+ return serviceRouter.handle(ctx);
11
+ };
12
+ }
13
+
14
+ function createControlPlaneServer(options = {}) {
15
+ return createRuntimeHttpServer(createControlPlaneHandler(options), {
16
+ serviceName: options.serviceName || 'AmalgmControlPlane',
17
+ exposeHeaders: options.exposeHeaders,
18
+ });
19
+ }
20
+
21
+ module.exports = {
22
+ createControlPlaneHandler,
23
+ createControlPlaneServer,
24
+ };
@@ -8,12 +8,57 @@
8
8
  * circular dependency on the MCP server.
9
9
  */
10
10
 
11
- const CORE_TOOLS = [
12
- ...require('../automations/tools'),
13
- ...require('../notify'),
14
- ...require('../agents/tools'),
15
- ...require('../apps/tools'),
16
- ...require('../browser/tools'),
11
+ function group(id, name, description, tools) {
12
+ return {
13
+ id,
14
+ name,
15
+ type: 'mcp',
16
+ owner: 'amalgm',
17
+ origin: 'system',
18
+ status: 'enabled',
19
+ description,
20
+ tools: tools.map((tool) => ({
21
+ ...tool,
22
+ toolboxToolId: id,
23
+ toolboxActionId: `${id}.${tool.name}`,
24
+ toolboxActionName: tool.name,
25
+ })),
26
+ };
27
+ }
28
+
29
+ const CORE_TOOL_GROUPS = [
30
+ group(
31
+ 'amalgm.automations',
32
+ 'Amalgm Automations',
33
+ 'Create, inspect, and run scheduled or event-driven automations.',
34
+ require('../automations/tools'),
35
+ ),
36
+ group(
37
+ 'amalgm.notifications',
38
+ 'Amalgm Notifications',
39
+ 'Notify the user about important results, completions, and failures.',
40
+ require('../notify'),
41
+ ),
42
+ group(
43
+ 'amalgm.agents',
44
+ 'Amalgm Agents',
45
+ 'Discover and talk to other agents on the platform.',
46
+ require('../agents/tools'),
47
+ ),
48
+ group(
49
+ 'amalgm.apps',
50
+ 'Amalgm Apps',
51
+ 'Register, run, and route local-first Amalgm apps.',
52
+ require('../apps/tools'),
53
+ ),
54
+ group(
55
+ 'amalgm.browser',
56
+ 'Amalgm Browser',
57
+ "Use Amalgm's visible browser session for navigation, interaction, and screenshots.",
58
+ require('../browser/tools'),
59
+ ),
17
60
  ];
18
61
 
19
- module.exports = { CORE_TOOLS };
62
+ const CORE_TOOLS = CORE_TOOL_GROUPS.flatMap((toolGroup) => toolGroup.tools);
63
+
64
+ module.exports = { CORE_TOOL_GROUPS, CORE_TOOLS };
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ function requestPathname(req) {
4
+ return new URL(req.url || '/', 'http://127.0.0.1').pathname;
5
+ }
6
+
7
+ function requestQuery(req) {
8
+ return Object.fromEntries(
9
+ new URL(req.url || '/', 'http://127.0.0.1').searchParams.entries(),
10
+ );
11
+ }
12
+
13
+ async function readJsonBody(req) {
14
+ const chunks = [];
15
+ for await (const chunk of req) chunks.push(chunk);
16
+ if (chunks.length === 0) return {};
17
+ return JSON.parse(Buffer.concat(chunks).toString());
18
+ }
19
+
20
+ function createHttpContext(req, res) {
21
+ return {
22
+ req,
23
+ res,
24
+ method: req.method || 'GET',
25
+ pathname: requestPathname(req),
26
+ getQuery: () => requestQuery(req),
27
+ readJsonBody: () => readJsonBody(req),
28
+ sendJson(statusCode, data) {
29
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
30
+ res.end(JSON.stringify(data));
31
+ },
32
+ notFound() {
33
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
34
+ res.end('Not found');
35
+ },
36
+ };
37
+ }
38
+
39
+ module.exports = {
40
+ createHttpContext,
41
+ readJsonBody,
42
+ requestPathname,
43
+ requestQuery,
44
+ };
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const { authorizeRuntimeHttp } = require('../../runtime-auth');
5
+ const { createHttpContext } = require('./http-context');
6
+
7
+ function createRuntimeHttpHandler(routeHandler, options = {}) {
8
+ const serviceName = options.serviceName || 'AmalgmMCP';
9
+ const exposeHeaders = options.exposeHeaders === undefined
10
+ ? 'Mcp-Session-Id'
11
+ : options.exposeHeaders;
12
+
13
+ return async (req, res) => {
14
+ if (!authorizeRuntimeHttp(req, res, { exposeHeaders })) return;
15
+ const ctx = createHttpContext(req, res);
16
+
17
+ try {
18
+ if (await routeHandler(ctx)) return;
19
+ ctx.notFound();
20
+ } catch (err) {
21
+ console.error(`[${serviceName}] Unhandled error:`, err);
22
+ if (!res.headersSent) ctx.sendJson(500, { error: 'Internal server error' });
23
+ }
24
+ };
25
+ }
26
+
27
+ function createRuntimeHttpServer(routeHandler, options = {}) {
28
+ return http.createServer(createRuntimeHttpHandler(routeHandler, options));
29
+ }
30
+
31
+ module.exports = {
32
+ createRuntimeHttpHandler,
33
+ createRuntimeHttpServer,
34
+ };