amalgm 0.0.1 → 0.0.33

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 (34) hide show
  1. package/README.md +4 -0
  2. package/lib/cli.js +133 -1
  3. package/lib/supervisor.js +10 -1
  4. package/package.json +2 -1
  5. package/runtime/lib/chatInput.js +9 -0
  6. package/runtime/lib/local/amalgmStore.js +10 -2
  7. package/runtime/scripts/amalgm-mcp/agents/rest.js +60 -81
  8. package/runtime/scripts/amalgm-mcp/agents/store.js +589 -58
  9. package/runtime/scripts/amalgm-mcp/agents/talk.js +10 -4
  10. package/runtime/scripts/amalgm-mcp/agents/tools.js +12 -1
  11. package/runtime/scripts/amalgm-mcp/artifacts/store.js +19 -3
  12. package/runtime/scripts/amalgm-mcp/config.js +2 -0
  13. package/runtime/scripts/amalgm-mcp/events/store.js +16 -1
  14. package/runtime/scripts/amalgm-mcp/lib/prefs.js +85 -37
  15. package/runtime/scripts/amalgm-mcp/local/rest.js +7 -0
  16. package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +83 -0
  17. package/runtime/scripts/amalgm-mcp/server/core-tools.js +20 -0
  18. package/runtime/scripts/amalgm-mcp/server/http.js +42 -0
  19. package/runtime/scripts/amalgm-mcp/server/mcp.js +28 -17
  20. package/runtime/scripts/amalgm-mcp/state/db.js +194 -0
  21. package/runtime/scripts/amalgm-mcp/state/events.js +113 -0
  22. package/runtime/scripts/amalgm-mcp/state/rest.js +64 -0
  23. package/runtime/scripts/amalgm-mcp/state/snapshot.js +76 -0
  24. package/runtime/scripts/amalgm-mcp/tasks/store.js +16 -1
  25. package/runtime/scripts/amalgm-mcp/toolbox/rest.js +75 -0
  26. package/runtime/scripts/amalgm-mcp/toolbox/runner.js +257 -0
  27. package/runtime/scripts/amalgm-mcp/toolbox/store.js +933 -0
  28. package/runtime/scripts/amalgm-mcp/toolbox/tools.js +269 -0
  29. package/runtime/scripts/amalgm-mcp/workspace/rest.js +116 -8
  30. package/runtime/scripts/chat-core/adapters/claude.js +2 -0
  31. package/runtime/scripts/chat-core/contract.js +2 -0
  32. package/runtime/scripts/chat-core/engine.js +77 -19
  33. package/runtime/scripts/credential-adapter.js +4 -2
  34. package/runtime/scripts/local-gateway.js +2 -0
@@ -4,10 +4,10 @@
4
4
  * The SDK handles JSON-RPC framing, protocol-version negotiation, batching,
5
5
  * and streamable-HTTP transport. We just register the two tool handlers.
6
6
  *
7
- * Each domain module (tasks, events, notify, agents, browser) exports a flat
8
- * array of { name, description, inputSchema, handler }. We concatenate them
9
- * into a single TOOLS list, look the tool up by name on each call, and pass
10
- * the result through unchanged.
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
11
  */
12
12
 
13
13
  const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
@@ -22,14 +22,15 @@ const {
22
22
  const { AMALGM_USER_ID } = require('../config');
23
23
  const { hasSupabase, supabaseSelect } = require('../lib/supabase');
24
24
  const { attachActionDescriptor } = require('../lib/tool-result');
25
+ const { CORE_TOOLS } = require('./core-tools');
26
+ const {
27
+ callToolboxMcpTool,
28
+ listToolboxMcpTools,
29
+ } = require('../toolbox/runner');
25
30
 
26
31
  const TOOLS = [
27
- ...require('../tasks/tools'),
28
- ...require('../events/tools'),
29
- ...require('../notify'),
30
- ...require('../agents/tools'),
31
- ...require('../artifacts/tools'),
32
- ...require('../browser/tools'),
32
+ ...CORE_TOOLS,
33
+ ...require('../toolbox/tools'),
33
34
  ];
34
35
 
35
36
  /**
@@ -49,6 +50,7 @@ async function buildRequestContext(extra) {
49
50
  );
50
51
  const parent = Array.isArray(rows) ? rows[0] : null;
51
52
  if (parent) {
53
+ ctx.sessionMetadata = parent.metadata || {};
52
54
  ctx.originName = parent.title || undefined;
53
55
  ctx.originHarnessId = parent.harness || undefined;
54
56
  ctx.originBaseHarnessId = parent.metadata?.baseHarnessId || undefined;
@@ -63,19 +65,28 @@ function createMcpServer() {
63
65
  { capabilities: { tools: {} } },
64
66
  );
65
67
 
66
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
67
- tools: TOOLS.map(({ name, description, inputSchema }) => ({
68
- name,
69
- description,
70
- inputSchema,
71
- })),
72
- }));
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
+ });
73
81
 
74
82
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
75
83
  const toolName = request.params?.name;
76
84
  const toolArgs = request.params?.arguments || {};
77
85
  const tool = TOOLS.find((t) => t.name === toolName);
78
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);
79
90
  return attachActionDescriptor({
80
91
  content: [{ type: 'text', text: `Unknown tool: ${toolName}` }],
81
92
  isError: true,
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Local Live Store database.
5
+ *
6
+ * SQLite owns the monotonic event sequence. Existing resource stores can keep
7
+ * their current files while they migrate table-by-table; the realtime contract
8
+ * is already backed by this WAL database.
9
+ */
10
+
11
+ const path = require('path');
12
+ const { ensureDir } = require('../lib/storage');
13
+ const { LOCAL_DB_FILE, STORAGE_DIR } = require('../config');
14
+
15
+ let db = null;
16
+
17
+ function getBetterSqlite3() {
18
+ try {
19
+ return require('better-sqlite3');
20
+ } catch (error) {
21
+ const message = error instanceof Error ? error.message : String(error);
22
+ throw new Error(`better-sqlite3 is required for the Local Live Store: ${message}`);
23
+ }
24
+ }
25
+
26
+ function openLocalDb() {
27
+ if (db) return db;
28
+
29
+ ensureDir(path.dirname(LOCAL_DB_FILE || STORAGE_DIR));
30
+ const Database = getBetterSqlite3();
31
+ db = new Database(LOCAL_DB_FILE);
32
+ db.pragma('journal_mode = WAL');
33
+ db.pragma('synchronous = NORMAL');
34
+ db.pragma('foreign_keys = ON');
35
+ db.pragma('busy_timeout = 5000');
36
+ migrate(db);
37
+ return db;
38
+ }
39
+
40
+ function migrate(database = openLocalDb()) {
41
+ database.exec(`
42
+ CREATE TABLE IF NOT EXISTS event_log (
43
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
44
+ ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
45
+ resource TEXT NOT NULL,
46
+ op TEXT NOT NULL,
47
+ id TEXT,
48
+ value_json TEXT,
49
+ patch_json TEXT,
50
+ client_mutation_id TEXT,
51
+ source TEXT,
52
+ resource_version INTEGER
53
+ );
54
+
55
+ CREATE INDEX IF NOT EXISTS event_log_resource_seq_idx
56
+ ON event_log(resource, seq);
57
+
58
+ CREATE TABLE IF NOT EXISTS tools (
59
+ id TEXT PRIMARY KEY,
60
+ name TEXT NOT NULL,
61
+ type TEXT NOT NULL,
62
+ owner TEXT NOT NULL,
63
+ origin TEXT NOT NULL,
64
+ status TEXT NOT NULL,
65
+ updated_at TEXT NOT NULL,
66
+ tool_json TEXT NOT NULL
67
+ );
68
+
69
+ CREATE INDEX IF NOT EXISTS tools_status_idx
70
+ ON tools(status);
71
+
72
+ CREATE TABLE IF NOT EXISTS tool_actions (
73
+ id TEXT PRIMARY KEY,
74
+ tool_id TEXT NOT NULL,
75
+ name TEXT NOT NULL,
76
+ status TEXT NOT NULL,
77
+ updated_at TEXT NOT NULL,
78
+ action_json TEXT NOT NULL,
79
+ FOREIGN KEY(tool_id) REFERENCES tools(id) ON DELETE CASCADE
80
+ );
81
+
82
+ CREATE INDEX IF NOT EXISTS tool_actions_tool_id_idx
83
+ ON tool_actions(tool_id);
84
+
85
+ CREATE TABLE IF NOT EXISTS agents (
86
+ id TEXT PRIMARY KEY,
87
+ name TEXT NOT NULL,
88
+ adapter TEXT NOT NULL,
89
+ base_harness_id TEXT NOT NULL,
90
+ location TEXT NOT NULL,
91
+ owner_computer_id TEXT,
92
+ builtin INTEGER NOT NULL DEFAULT 0,
93
+ deletable INTEGER NOT NULL DEFAULT 1,
94
+ status TEXT NOT NULL,
95
+ updated_at TEXT NOT NULL,
96
+ agent_json TEXT NOT NULL
97
+ );
98
+
99
+ CREATE INDEX IF NOT EXISTS agents_base_harness_id_idx
100
+ ON agents(base_harness_id);
101
+
102
+ CREATE INDEX IF NOT EXISTS agents_owner_computer_id_idx
103
+ ON agents(owner_computer_id);
104
+
105
+ CREATE INDEX IF NOT EXISTS agents_builtin_idx
106
+ ON agents(builtin);
107
+ `);
108
+ migrateLegacyToolboxTables(database);
109
+ removeInheritedHarnessTools(database);
110
+ }
111
+
112
+ function tableExists(database, tableName) {
113
+ const row = database.prepare(
114
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?"
115
+ ).get(tableName);
116
+ return Boolean(row);
117
+ }
118
+
119
+ function migrateLegacyToolboxTables(database) {
120
+ if (tableExists(database, 'toolbox_tools')) {
121
+ database.prepare(`
122
+ INSERT OR IGNORE INTO tools (
123
+ id,
124
+ name,
125
+ type,
126
+ owner,
127
+ origin,
128
+ status,
129
+ updated_at,
130
+ tool_json
131
+ )
132
+ SELECT
133
+ id,
134
+ name,
135
+ type,
136
+ owner,
137
+ origin,
138
+ status,
139
+ updated_at,
140
+ tool_json
141
+ FROM toolbox_tools
142
+ `).run();
143
+ }
144
+
145
+ if (tableExists(database, 'toolbox_actions')) {
146
+ database.prepare(`
147
+ INSERT OR IGNORE INTO tool_actions (
148
+ id,
149
+ tool_id,
150
+ name,
151
+ status,
152
+ updated_at,
153
+ action_json
154
+ )
155
+ SELECT
156
+ legacy.id,
157
+ legacy.tool_id,
158
+ legacy.name,
159
+ legacy.status,
160
+ legacy.updated_at,
161
+ legacy.action_json
162
+ FROM toolbox_actions legacy
163
+ INNER JOIN tools ON tools.id = legacy.tool_id
164
+ `).run();
165
+ }
166
+ }
167
+
168
+ function removeInheritedHarnessTools(database) {
169
+ database.prepare(`
170
+ DELETE FROM tool_actions
171
+ WHERE tool_id IN (
172
+ SELECT id FROM tools
173
+ WHERE owner IN ('codex')
174
+ AND origin = 'catalog'
175
+ )
176
+ `).run();
177
+
178
+ database.prepare(`
179
+ DELETE FROM tools
180
+ WHERE owner IN ('codex')
181
+ AND origin = 'catalog'
182
+ `).run();
183
+ }
184
+
185
+ function closeLocalDb() {
186
+ if (!db) return;
187
+ db.close();
188
+ db = null;
189
+ }
190
+
191
+ module.exports = {
192
+ closeLocalDb,
193
+ openLocalDb,
194
+ };
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('events');
4
+ const { openLocalDb } = require('./db');
5
+
6
+ const emitter = new EventEmitter();
7
+ emitter.setMaxListeners(200);
8
+
9
+ function safeJsonStringify(value) {
10
+ if (value === undefined) return null;
11
+ return JSON.stringify(value);
12
+ }
13
+
14
+ function safeJsonParse(value) {
15
+ if (typeof value !== 'string' || !value) return undefined;
16
+ try {
17
+ return JSON.parse(value);
18
+ } catch {
19
+ return undefined;
20
+ }
21
+ }
22
+
23
+ function normalizeEventRow(row) {
24
+ if (!row) return null;
25
+ const event = {
26
+ seq: Number(row.seq),
27
+ ts: row.ts,
28
+ resource: row.resource,
29
+ op: row.op,
30
+ };
31
+ if (row.id != null) event.id = row.id;
32
+ if (row.client_mutation_id != null) event.clientMutationId = row.client_mutation_id;
33
+ if (row.source != null) event.source = row.source;
34
+ if (row.resource_version != null) event.version = Number(row.resource_version);
35
+ const value = safeJsonParse(row.value_json);
36
+ const patch = safeJsonParse(row.patch_json);
37
+ if (value !== undefined) event.value = value;
38
+ if (patch !== undefined) event.patch = patch;
39
+ return event;
40
+ }
41
+
42
+ function insertStateEvent(database, input) {
43
+ if (!input || typeof input.resource !== 'string' || typeof input.op !== 'string') {
44
+ throw new Error('state event requires resource and op');
45
+ }
46
+
47
+ const info = database.prepare(`
48
+ INSERT INTO event_log (
49
+ resource,
50
+ op,
51
+ id,
52
+ value_json,
53
+ patch_json,
54
+ client_mutation_id,
55
+ source,
56
+ resource_version
57
+ )
58
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
59
+ `).run(
60
+ input.resource,
61
+ input.op,
62
+ input.id ?? null,
63
+ safeJsonStringify(input.value),
64
+ safeJsonStringify(input.patch),
65
+ input.clientMutationId ?? input.client_mutation_id ?? null,
66
+ input.source ?? null,
67
+ input.version ?? input.resourceVersion ?? null,
68
+ );
69
+ return normalizeEventRow(
70
+ database.prepare('SELECT * FROM event_log WHERE seq = ?').get(info.lastInsertRowid),
71
+ );
72
+ }
73
+
74
+ function publishStateEvent(event) {
75
+ if (event) emitter.emit('event', event);
76
+ }
77
+
78
+ function currentSeq() {
79
+ const row = openLocalDb()
80
+ .prepare('SELECT COALESCE(MAX(seq), 0) AS seq FROM event_log')
81
+ .get();
82
+ return Number(row?.seq || 0);
83
+ }
84
+
85
+ function appendStateEvent(input) {
86
+ const db = openLocalDb();
87
+ const event = db.transaction(() => insertStateEvent(db, input))();
88
+ publishStateEvent(event);
89
+ return event;
90
+ }
91
+
92
+ function listEventsAfter(afterSeq, options = {}) {
93
+ const after = Number.isFinite(Number(afterSeq)) ? Number(afterSeq) : 0;
94
+ const limit = Math.max(1, Math.min(Number(options.limit) || 1000, 5000));
95
+ const rows = openLocalDb()
96
+ .prepare('SELECT * FROM event_log WHERE seq > ? ORDER BY seq ASC LIMIT ?')
97
+ .all(after, limit);
98
+ return rows.map(normalizeEventRow).filter(Boolean);
99
+ }
100
+
101
+ function subscribeStateEvents(callback) {
102
+ emitter.on('event', callback);
103
+ return () => emitter.off('event', callback);
104
+ }
105
+
106
+ module.exports = {
107
+ appendStateEvent,
108
+ currentSeq,
109
+ insertStateEvent,
110
+ listEventsAfter,
111
+ publishStateEvent,
112
+ subscribeStateEvents,
113
+ };
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ const { buildSnapshot } = require('./snapshot');
4
+ const { listEventsAfter, subscribeStateEvents } = require('./events');
5
+
6
+ function parsePositiveInt(value, fallback) {
7
+ const parsed = Number(value);
8
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
9
+ }
10
+
11
+ function sendSseEvent(res, event) {
12
+ res.write(`id: ${event.seq}\n`);
13
+ res.write('event: state\n');
14
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
15
+ }
16
+
17
+ async function handleSnapshot(query, sendJson) {
18
+ sendJson(200, buildSnapshot(query.resources));
19
+ }
20
+
21
+ async function handleEvents(query, sendJson) {
22
+ const after = parsePositiveInt(query.after, 0);
23
+ const limit = parsePositiveInt(query.limit, 1000);
24
+ const events = listEventsAfter(after, { limit });
25
+ sendJson(200, {
26
+ events,
27
+ seq: events.length > 0 ? events[events.length - 1].seq : after,
28
+ });
29
+ }
30
+
31
+ function handleStream(req, res, query) {
32
+ const after = parsePositiveInt(query.after, 0);
33
+
34
+ res.writeHead(200, {
35
+ 'Content-Type': 'text/event-stream; charset=utf-8',
36
+ 'Cache-Control': 'no-cache, no-transform',
37
+ Connection: 'keep-alive',
38
+ 'X-Accel-Buffering': 'no',
39
+ });
40
+ res.write(': local-live-store connected\n\n');
41
+
42
+ const unsubscribe = subscribeStateEvents((event) => {
43
+ if (event.seq > after) sendSseEvent(res, event);
44
+ });
45
+
46
+ for (const event of listEventsAfter(after, { limit: 5000 })) {
47
+ sendSseEvent(res, event);
48
+ }
49
+
50
+ const heartbeat = setInterval(() => {
51
+ res.write(': heartbeat\n\n');
52
+ }, 25_000);
53
+
54
+ req.on('close', () => {
55
+ clearInterval(heartbeat);
56
+ unsubscribe();
57
+ });
58
+ }
59
+
60
+ module.exports = {
61
+ handleEvents,
62
+ handleSnapshot,
63
+ handleStream,
64
+ };
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ const { currentSeq } = require('./events');
4
+
5
+ const DEFAULT_RESOURCES = [
6
+ 'tasks',
7
+ 'event_triggers',
8
+ 'agents',
9
+ 'artifacts',
10
+ 'toolbox',
11
+ 'tools',
12
+ 'tool_actions',
13
+ ];
14
+
15
+ function normalizeResources(resources) {
16
+ if (!resources) return DEFAULT_RESOURCES;
17
+ const values = Array.isArray(resources)
18
+ ? resources
19
+ : String(resources).split(',');
20
+ const clean = values
21
+ .map((value) => String(value || '').trim())
22
+ .filter(Boolean);
23
+ return clean.length > 0 ? Array.from(new Set(clean)) : DEFAULT_RESOURCES;
24
+ }
25
+
26
+ function readResource(resource, cache) {
27
+ switch (resource) {
28
+ case 'tasks':
29
+ return require('../tasks/store').loadTasks().tasks;
30
+ case 'event_triggers':
31
+ return require('../events/store').loadEventTriggers().triggers;
32
+ case 'agents':
33
+ return require('../agents/store').getAllAgentsWithBuiltins();
34
+ case 'artifacts':
35
+ return require('../artifacts/store').loadArtifacts().artifacts;
36
+ case 'toolbox': {
37
+ cache.toolbox ||= require('../toolbox/store').readToolbox();
38
+ return cache.toolbox;
39
+ }
40
+ case 'tools': {
41
+ cache.toolbox ||= require('../toolbox/store').readToolbox();
42
+ return cache.toolbox.tools;
43
+ }
44
+ case 'tool_actions': {
45
+ cache.toolbox ||= require('../toolbox/store').readToolbox();
46
+ return cache.toolbox.toolActions;
47
+ }
48
+ default:
49
+ return undefined;
50
+ }
51
+ }
52
+
53
+ function buildSnapshot(resourcesInput) {
54
+ const resources = normalizeResources(resourcesInput);
55
+ const beforeSeq = currentSeq();
56
+ const cache = {};
57
+ const data = {};
58
+
59
+ for (const resource of resources) {
60
+ const value = readResource(resource, cache);
61
+ if (value !== undefined) data[resource] = value;
62
+ }
63
+
64
+ const afterSeq = currentSeq();
65
+ return {
66
+ seq: afterSeq,
67
+ stable: beforeSeq === afterSeq,
68
+ resources: data,
69
+ };
70
+ }
71
+
72
+ module.exports = {
73
+ DEFAULT_RESOURCES,
74
+ buildSnapshot,
75
+ normalizeResources,
76
+ };
@@ -22,6 +22,7 @@ const {
22
22
  const { normalizeTaskSchedule } = require('./schedule-normalization');
23
23
  const { DEFAULT_SELECTED_MODELS, getSelectedModel } = require('../lib/prefs');
24
24
  const credentialAdapter = require('../../credential-adapter');
25
+ const { appendStateEvent } = require('../state/events');
25
26
 
26
27
  function normalizeStoredTask(task) {
27
28
  const harness =
@@ -108,8 +109,22 @@ function loadTasks() {
108
109
  return migrated.data;
109
110
  }
110
111
 
111
- function saveTasks(data) {
112
+ function publishTasksChange(data, source = 'tasks') {
113
+ try {
114
+ appendStateEvent({
115
+ resource: 'tasks',
116
+ op: 'replace',
117
+ value: Array.isArray(data?.tasks) ? data.tasks : [],
118
+ source,
119
+ });
120
+ } catch (error) {
121
+ console.warn('[Tasks] Local Live Store publish failed:', error.message);
122
+ }
123
+ }
124
+
125
+ function saveTasks(data, options = {}) {
112
126
  writeJsonAtomic(TASKS_FILE, data);
127
+ publishTasksChange(data, options.source || 'tasks:save');
113
128
  }
114
129
 
115
130
  function updateTaskMeta(taskId, updates) {
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * /toolbox REST routes for the internal UI.
5
+ *
6
+ * Not part of the MCP tool surface. This is the local ToolboxDB API.
7
+ */
8
+
9
+ const {
10
+ deleteTool,
11
+ deleteToolAction,
12
+ readToolbox,
13
+ upsertTool,
14
+ upsertToolAction,
15
+ } = require('./store');
16
+
17
+ async function handleList(sendJson) {
18
+ sendJson(200, readToolbox());
19
+ }
20
+
21
+ async function handleUpsertTool(body, sendJson) {
22
+ try {
23
+ const rawTool = body?.tool && typeof body.tool === 'object' ? body.tool : body;
24
+ const tool = {
25
+ ...rawTool,
26
+ actions: body?.actions ?? rawTool?.actions,
27
+ clientMutationId: body?.clientMutationId ?? rawTool?.clientMutationId,
28
+ };
29
+ const saved = upsertTool(tool);
30
+ sendJson(200, { ok: true, ...saved });
31
+ } catch (error) {
32
+ sendJson(400, { error: error instanceof Error ? error.message : 'Failed to save tool' });
33
+ }
34
+ }
35
+
36
+ async function handleDeleteTool(body, sendJson) {
37
+ try {
38
+ const id = body?.id || body?.toolId;
39
+ if (!id) return sendJson(400, { error: 'id is required' });
40
+ sendJson(200, deleteTool(id, { clientMutationId: body?.clientMutationId }));
41
+ } catch (error) {
42
+ sendJson(400, { error: error instanceof Error ? error.message : 'Failed to delete tool' });
43
+ }
44
+ }
45
+
46
+ async function handleUpsertAction(body, sendJson) {
47
+ try {
48
+ const rawAction = body?.action && typeof body.action === 'object' ? body.action : body;
49
+ const action = upsertToolAction({
50
+ ...rawAction,
51
+ clientMutationId: body?.clientMutationId ?? rawAction?.clientMutationId,
52
+ });
53
+ sendJson(200, { ok: true, action });
54
+ } catch (error) {
55
+ sendJson(400, { error: error instanceof Error ? error.message : 'Failed to save tool action' });
56
+ }
57
+ }
58
+
59
+ async function handleDeleteAction(body, sendJson) {
60
+ try {
61
+ const id = body?.id || body?.actionId;
62
+ if (!id) return sendJson(400, { error: 'id is required' });
63
+ sendJson(200, deleteToolAction(id, { clientMutationId: body?.clientMutationId }));
64
+ } catch (error) {
65
+ sendJson(400, { error: error instanceof Error ? error.message : 'Failed to delete tool action' });
66
+ }
67
+ }
68
+
69
+ module.exports = {
70
+ handleDeleteAction,
71
+ handleDeleteTool,
72
+ handleList,
73
+ handleUpsertAction,
74
+ handleUpsertTool,
75
+ };