@desplega.ai/agent-swarm 1.90.0 → 1.91.0

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 (52) hide show
  1. package/openapi.json +74 -1
  2. package/package.json +5 -5
  3. package/src/artifact-sdk/server.ts +2 -1
  4. package/src/be/memory/providers/sqlite-store.ts +6 -1
  5. package/src/be/memory/types.ts +1 -0
  6. package/src/be/scripts/typecheck.ts +132 -1
  7. package/src/be/seed-scripts/catalog/compound-insights.ts +188 -0
  8. package/src/be/seed-scripts/catalog/schedule-health.ts +73 -0
  9. package/src/be/seed-scripts/catalog/smart-recall.ts +65 -0
  10. package/src/be/seed-scripts/catalog/tool-usage.ts +56 -0
  11. package/src/be/seed-scripts/index.ts +36 -0
  12. package/src/commands/artifact.ts +3 -2
  13. package/src/commands/profile-sync.ts +310 -0
  14. package/src/commands/runner.ts +91 -1
  15. package/src/hooks/hook.ts +32 -9
  16. package/src/http/index.ts +47 -0
  17. package/src/http/integrations.ts +6 -1
  18. package/src/http/mcp-bridge.ts +117 -0
  19. package/src/http/mcp-oauth.ts +97 -39
  20. package/src/http/memory.ts +5 -2
  21. package/src/http/openapi.ts +2 -2
  22. package/src/http/pages-public.ts +10 -11
  23. package/src/http/pages.ts +7 -11
  24. package/src/http/scripts.ts +24 -1
  25. package/src/http/utils.ts +11 -4
  26. package/src/jira/app.ts +2 -3
  27. package/src/jira/webhook-lifecycle.ts +2 -1
  28. package/src/linear/app.ts +2 -3
  29. package/src/providers/claude-adapter.ts +26 -0
  30. package/src/scripts-runtime/executors/native.ts +1 -0
  31. package/src/scripts-runtime/sdk-allowlist.ts +121 -0
  32. package/src/scripts-runtime/swarm-sdk.ts +198 -3
  33. package/src/scripts-runtime/types/stdlib.d.ts +227 -0
  34. package/src/scripts-runtime/types/swarm-sdk.d.ts +227 -0
  35. package/src/tests/claude-adapter-otel.test.ts +85 -1
  36. package/src/tests/hook-registration-nudge.test.ts +69 -0
  37. package/src/tests/mcp-oauth-manual-client.test.ts +213 -0
  38. package/src/tests/pages-public-html.test.ts +41 -0
  39. package/src/tests/pages-public-json-redirect.test.ts +37 -2
  40. package/src/tests/profile-sync.test.ts +282 -0
  41. package/src/tests/scripts-runtime.test.ts +33 -0
  42. package/src/tests/seed-scripts.test.ts +2 -2
  43. package/src/tools/create-metric.ts +2 -3
  44. package/src/tools/create-page.ts +3 -6
  45. package/src/tools/memory-rate.ts +2 -1
  46. package/src/tools/memory-search.ts +1 -0
  47. package/src/tools/register-kapso-number.ts +2 -4
  48. package/src/tools/request-human-input.ts +2 -1
  49. package/src/tools/script-common.ts +2 -4
  50. package/src/tools/script-run.ts +7 -0
  51. package/src/utils/constants.ts +58 -8
  52. package/templates/skills/swarm-scripts/content.md +46 -7
@@ -1,6 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
- import { getAgentById } from "../be/db";
3
+ import { getAgentById, upsertKv } from "../be/db";
4
4
  import { createEvent } from "../be/events";
5
5
  import { deleteScript, getScript, upsertScriptByName } from "../be/scripts/db";
6
6
  import { searchScripts } from "../be/scripts/embeddings";
@@ -37,6 +37,7 @@ const runBodySchema = z
37
37
  intent: z.string().default(""),
38
38
  scope: ScriptScopeSchema.optional(),
39
39
  fsMode: ScriptFsModeSchema.default("none"),
40
+ idempotencyKey: z.string().max(200).optional(),
40
41
  })
41
42
  .refine((body) => Boolean(body.name) !== Boolean(body.source), {
42
43
  message: "Provide exactly one of name or source",
@@ -289,6 +290,27 @@ export async function handleScripts(
289
290
  agentId: agent.id,
290
291
  });
291
292
 
293
+ // Persist output to KV when idempotencyKey is provided and run succeeded
294
+ let kvSaved: { namespace: string; key: string } | undefined;
295
+ if (parsed.body.idempotencyKey && !output.error && output.exitCode === 0) {
296
+ const kvNamespace = `script:executions`;
297
+ const kvKey = parsed.body.idempotencyKey;
298
+ const kvValue = {
299
+ result: output.result,
300
+ durationMs: output.durationMs,
301
+ scriptName: parsed.body.name ?? null,
302
+ executedAt: new Date().toISOString(),
303
+ };
304
+ upsertKv({
305
+ namespace: kvNamespace,
306
+ key: kvKey,
307
+ value: kvValue,
308
+ valueType: "json",
309
+ expiresAt: null,
310
+ });
311
+ kvSaved = { namespace: kvNamespace, key: kvKey };
312
+ }
313
+
292
314
  let autoSaved: { slug: string; reason: string } | undefined;
293
315
  if (parsed.body.source && !output.error && output.exitCode === 0) {
294
316
  const slug = scratchSlug(parsed.body.intent, parsed.body.source);
@@ -314,6 +336,7 @@ export async function handleScripts(
314
336
  scrubObject({
315
337
  result: output.result,
316
338
  autoSaved,
339
+ kvSaved,
317
340
  truncated: output.truncated,
318
341
  durationMs: output.durationMs,
319
342
  stdout: output.stdout,
package/src/http/utils.ts CHANGED
@@ -157,14 +157,21 @@ export function triggerSchemaErrorResponse(
157
157
  * redirect URIs). Returns a URL with no trailing slash.
158
158
  *
159
159
  * Resolution order:
160
- * 1. `MCP_BASE_URL` env (canonical)
161
- * 2. Inbound request host `X-Forwarded-Proto`/`X-Forwarded-Host` if behind
160
+ * 1. `PUBLIC_MCP_BASE_URL` env — explicit public origin. Wins so split
161
+ * deployments (Helm) can keep `MCP_BASE_URL` pointed at an internal
162
+ * cluster address while outbound URLs use the public ingress.
163
+ * 2. `MCP_BASE_URL` env — canonical when public and internal hosts coincide
164
+ * (e.g. an ngrok tunnel set as `MCP_BASE_URL` in local dev).
165
+ * 3. Inbound request host — `X-Forwarded-Proto`/`X-Forwarded-Host` if behind
162
166
  * a proxy/tunnel (ngrok), else `Host` header. Lets the URL stay correct
163
- * when MCP_BASE_URL is unset and the API is reached via an arbitrary
167
+ * when neither env var is set and the API is reached via an arbitrary
164
168
  * external hostname.
165
- * 3. `http://localhost:<PORT>` fallback
169
+ * 4. `http://localhost:<PORT>` fallback
166
170
  */
167
171
  export function deriveApiBaseUrl(req: IncomingMessage): string {
172
+ const publicBase = process.env.PUBLIC_MCP_BASE_URL?.trim();
173
+ if (publicBase) return publicBase.replace(/\/+$/, "");
174
+
168
175
  const envBase = process.env.MCP_BASE_URL?.trim();
169
176
  if (envBase) return envBase.replace(/\/+$/, "");
170
177
 
package/src/jira/app.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { upsertOAuthApp } from "../be/db-queries/oauth";
2
+ import { getPublicMcpBaseUrl } from "../utils/constants";
2
3
  import { initJiraOutboundSync, teardownJiraOutboundSync } from "./outbound";
3
4
  // Side-effect import: registers all Jira event templates in the in-memory
4
5
  // registry at module load time (mirrors `src/linear/templates.ts`).
@@ -41,9 +42,7 @@ export function initJira(): boolean {
41
42
  // Atlassian. Prefer MCP_BASE_URL over the localhost dev default; in prod
42
43
  // with no JIRA_REDIRECT_URI set, this is what stops Atlassian from sending
43
44
  // the user back to localhost.
44
- const apiBaseUrl =
45
- process.env.MCP_BASE_URL?.trim().replace(/\/+$/, "") ||
46
- `http://localhost:${process.env.PORT || "3013"}`;
45
+ const apiBaseUrl = getPublicMcpBaseUrl();
47
46
  const redirectUri = process.env.JIRA_REDIRECT_URI ?? `${apiBaseUrl}/api/trackers/jira/callback`;
48
47
 
49
48
  upsertOAuthApp("jira", {
@@ -1,3 +1,4 @@
1
+ import { getPublicMcpBaseUrl } from "../utils/constants";
1
2
  import { jiraFetch } from "./client";
2
3
  import { getJiraMetadata, updateJiraMetadata } from "./metadata";
3
4
 
@@ -23,7 +24,7 @@ let keepaliveInterval: ReturnType<typeof setInterval> | null = null;
23
24
  // ─── URL helpers ─────────────────────────────────────────────────────────────
24
25
 
25
26
  function getWebhookBaseUrl(): string {
26
- return process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
27
+ return getPublicMcpBaseUrl();
27
28
  }
28
29
 
29
30
  function getRegisteredWebhookUrl(): string {
package/src/linear/app.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { upsertOAuthApp } from "../be/db-queries/oauth";
2
+ import { getPublicMcpBaseUrl } from "../utils/constants";
2
3
  import { initLinearOutboundSync, teardownLinearOutboundSync } from "./outbound";
3
4
 
4
5
  let initialized = false;
@@ -31,9 +32,7 @@ export function initLinear(): boolean {
31
32
  // verbatim by the OAuth flow. Prefer MCP_BASE_URL over the localhost default
32
33
  // so prod doesn't send users back to localhost when LINEAR_REDIRECT_URI is
33
34
  // unset.
34
- const apiBaseUrl =
35
- process.env.MCP_BASE_URL?.trim().replace(/\/+$/, "") ||
36
- `http://localhost:${process.env.PORT || "3013"}`;
35
+ const apiBaseUrl = getPublicMcpBaseUrl();
37
36
  const redirectUri =
38
37
  process.env.LINEAR_REDIRECT_URI ?? `${apiBaseUrl}/api/trackers/linear/callback`;
39
38
 
@@ -336,6 +336,30 @@ export function buildClaudeCodeOtelEnv(
336
336
  return otelEnv;
337
337
  }
338
338
 
339
+ /**
340
+ * Claude Code runtime defaults for ephemeral swarm harness sessions.
341
+ *
342
+ * These are plain subprocess env vars, not prompt content. They are injected
343
+ * after the resolved swarm config so the worker enforces the memory/privacy
344
+ * guardrails consistently per spawn. Statsig/DNT opt-out is intentionally
345
+ * separate from our Claude Code OTel export path, which is controlled by
346
+ * buildClaudeCodeOtelEnv.
347
+ */
348
+ export function buildClaudeCodeRuntimeEnv(
349
+ _sourceEnv: Record<string, string | undefined>,
350
+ ): Record<string, string> {
351
+ return {
352
+ ENABLE_TOOL_SEARCH: "true",
353
+ CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING: "1",
354
+ CLAUDE_CODE_SKIP_PROMPT_HISTORY: "1",
355
+ CLAUDE_CODE_DISABLE_ATTACHMENTS: "1",
356
+ DISABLE_TELEMETRY: "1",
357
+ DO_NOT_TRACK: "1",
358
+ DISABLE_FEEDBACK_COMMAND: "1",
359
+ DISABLE_BUG_COMMAND: "1",
360
+ };
361
+ }
362
+
339
363
  /**
340
364
  * Resolve the path at which the per-task system prompt is staged on disk.
341
365
  *
@@ -398,11 +422,13 @@ class ClaudeSession implements ProviderSession {
398
422
  // so the freshly-computed TRACEPARENT wins over any stale value the
399
423
  // container env might carry.
400
424
  const otelEnv = buildClaudeCodeOtelEnv(sourceEnv);
425
+ const runtimeEnv = buildClaudeCodeRuntimeEnv(sourceEnv);
401
426
  this.proc = Bun.spawn(cmd, {
402
427
  cwd: this.config.cwd,
403
428
  env: {
404
429
  ENABLE_PROMPT_CACHING_1H: "1",
405
430
  ...sourceEnv,
431
+ ...runtimeEnv,
406
432
  ...otelEnv,
407
433
  TASK_FILE: taskFilePath,
408
434
  // Belt-and-braces: TASK_FILE on disk can disappear mid-session (race
@@ -90,6 +90,7 @@ async function writeBareImportShims(tmpdir: string): Promise<void> {
90
90
  const shims: [string, string][] = [
91
91
  ["stdlib", `${runtimeDir}/stdlib.bundle.js`],
92
92
  ["swarm-sdk", `${runtimeDir}/swarm-sdk.bundle.js`],
93
+ ["zod", `${runtimeDir}/zod.bundle.js`],
93
94
  ];
94
95
  for (const [name, bundlePath] of shims) {
95
96
  const dir = `${tmpdir}/node_modules/${name}`;
@@ -1,19 +1,140 @@
1
1
  export const SDK_TOOL_NAME_MAP = {
2
+ // ── memory ──
2
3
  memory_search: "memory-search",
3
4
  memory_get: "memory-get",
4
5
  memory_rate: "memory_rate",
6
+ memory_delete: "memory-delete", // destructive
7
+ inject_learning: "inject-learning",
8
+
9
+ // ── tasks ──
5
10
  task_list: "get-tasks",
6
11
  task_get: "get-task-details",
7
12
  task_storeProgress: "store-progress",
13
+ task_poll: "poll-task",
14
+ task_send: "send-task",
15
+ task_cancel: "cancel-task", // destructive
16
+ task_action: "task-action",
17
+
18
+ // ── kv ──
8
19
  kv_get: "kv-get",
9
20
  kv_set: "kv-set",
10
21
  kv_del: "kv-delete",
11
22
  kv_incr: "kv-incr",
12
23
  kv_list: "kv-list",
24
+
25
+ // ── repos ──
13
26
  repo_list: "get-repos",
27
+ repo_update: "update-repo",
28
+
29
+ // ── schedules ──
14
30
  schedule_list: "list-schedules",
31
+ schedule_create: "create-schedule",
32
+ schedule_update: "update-schedule",
33
+ schedule_delete: "delete-schedule", // destructive
34
+ schedule_runNow: "run-schedule-now",
35
+
36
+ // ── scripts ──
15
37
  script_search: "script-search",
16
38
  script_run: "script-run",
39
+ script_upsert: "script-upsert",
40
+ script_delete: "script-delete", // destructive
41
+ script_queryTypes: "script-query-types",
42
+
43
+ // ── swarm / agent ──
44
+ swarm_get: "get-swarm",
45
+ agent_info: "my-agent-info",
46
+ agent_join: "join-swarm",
47
+ metrics_get: "get-metrics",
48
+ user_resolve: "resolve-user",
49
+ user_manage: "manage-user",
50
+ db_query: "db-query",
51
+
52
+ // ── config ──
53
+ config_get: "get-config",
54
+ config_list: "list-config",
55
+ config_set: "set-config",
56
+ config_delete: "delete-config", // destructive
57
+
58
+ // ── slack ──
59
+ slack_read: "slack-read",
60
+ slack_listChannels: "slack-list-channels",
61
+ slack_post: "slack-post", // external: sends to Slack
62
+ slack_reply: "slack-reply", // external: sends to Slack
63
+ slack_startThread: "slack-start-thread", // external: sends to Slack
64
+ slack_uploadFile: "slack-upload-file", // external: sends to Slack
65
+ slack_downloadFile: "slack-download-file",
66
+
67
+ // ── messaging (internal) ──
68
+ message_read: "read-messages",
69
+ message_post: "post-message",
70
+
71
+ // ── profiles ──
72
+ profile_update: "update-profile",
73
+
74
+ // ── context / profiles ──
75
+ context_history: "context-history",
76
+ context_diff: "context-diff",
77
+
78
+ // ── services ──
79
+ service_list: "list-services",
80
+ service_register: "register-service",
81
+ service_unregister: "unregister-service", // destructive
82
+ service_updateStatus: "update-service-status",
83
+
84
+ // ── workflows ──
85
+ workflow_list: "list-workflows",
86
+ workflow_get: "get-workflow",
87
+ workflow_create: "create-workflow",
88
+ workflow_update: "update-workflow",
89
+ workflow_patch: "patch-workflow",
90
+ workflow_patchNode: "patch-workflow-node",
91
+ workflow_delete: "delete-workflow", // destructive
92
+ workflow_trigger: "trigger-workflow",
93
+ workflow_listRuns: "list-workflow-runs",
94
+ workflow_getRun: "get-workflow-run",
95
+ workflow_retryRun: "retry-workflow-run",
96
+ workflow_cancelRun: "cancel-workflow-run", // destructive
97
+
98
+ // ── prompt templates ──
99
+ prompt_list: "list-prompt-templates",
100
+ prompt_get: "get-prompt-template",
101
+ prompt_set: "set-prompt-template",
102
+ prompt_delete: "delete-prompt-template", // destructive
103
+ prompt_preview: "preview-prompt-template",
104
+
105
+ // ── tracker ──
106
+ tracker_status: "tracker-status",
107
+ tracker_syncStatus: "tracker-sync-status",
108
+ tracker_linkTask: "tracker-link-task",
109
+ tracker_unlink: "tracker-unlink", // destructive
110
+ tracker_mapAgent: "tracker-map-agent",
111
+
112
+ // ── skills ──
113
+ skill_list: "skill-list",
114
+ skill_get: "skill-get",
115
+ skill_search: "skill-search",
116
+ skill_create: "skill-create",
117
+ skill_update: "skill-update",
118
+ skill_delete: "skill-delete", // destructive
119
+ skill_install: "skill-install",
120
+ skill_uninstall: "skill-uninstall", // destructive
121
+ skill_publish: "skill-publish",
122
+
123
+ // ── mcp servers ──
124
+ mcpServer_list: "mcp-server-list",
125
+ mcpServer_get: "mcp-server-get",
126
+ mcpServer_create: "mcp-server-create",
127
+ mcpServer_update: "mcp-server-update",
128
+ mcpServer_delete: "mcp-server-delete", // destructive
129
+ mcpServer_install: "mcp-server-install",
130
+ mcpServer_uninstall: "mcp-server-uninstall", // destructive
131
+
132
+ // ── pages & metrics ──
133
+ page_create: "create_page",
134
+ metric_create: "create_metric",
135
+
136
+ // ── human input ──
137
+ request_humanInput: "request-human-input",
17
138
  } as const;
18
139
 
19
140
  export const SDK_ALLOWLIST = Object.keys(SDK_TOOL_NAME_MAP) as Array<
@@ -1,6 +1,6 @@
1
1
  import { scrubObject } from "../utils/secret-scrubber";
2
2
  import { Redacted } from "./redacted";
3
- import { isSdkToolAllowed } from "./sdk-allowlist";
3
+ import { isSdkToolAllowed, mcpToolNameForSdkMethod } from "./sdk-allowlist";
4
4
  import type { SwarmConfig } from "./swarm-config";
5
5
 
6
6
  type BridgeRequest = {
@@ -43,9 +43,14 @@ function kvPath(args: Record<string, unknown>, keyRequired = true): string {
43
43
  return key ? `/api/kv/${encodeURIComponent(key)}` : "/api/kv";
44
44
  }
45
45
 
46
- function bridgeRequestFor(name: string, args: unknown): BridgeRequest {
46
+ /**
47
+ * Maps SDK method names to specific REST endpoints where they exist.
48
+ * Returns null for tools that should fall through to the generic MCP bridge.
49
+ */
50
+ function bridgeRequestFor(name: string, args: unknown): BridgeRequest | null {
47
51
  const body = argsRecord(args);
48
52
  switch (name) {
53
+ // ── memory ──
49
54
  case "memory_search":
50
55
  return { method: "POST", path: "/api/memory/search", body };
51
56
  case "memory_get": {
@@ -67,6 +72,13 @@ function bridgeRequestFor(name: string, args: unknown): BridgeRequest {
67
72
  };
68
73
  return { method: "POST", path: "/api/memory/rate", body: { events: [event] } };
69
74
  }
75
+ case "memory_delete": {
76
+ const id = typeof body.id === "string" ? body.id : undefined;
77
+ if (!id) throw new Error("memory_delete requires string `id`");
78
+ return { method: "DELETE", path: `/api/memory/${encodeURIComponent(id)}` };
79
+ }
80
+
81
+ // ── tasks ──
70
82
  case "task_list":
71
83
  return { method: "GET", path: appendQuery("/api/tasks", body) };
72
84
  case "task_get": {
@@ -94,6 +106,13 @@ function bridgeRequestFor(name: string, args: unknown): BridgeRequest {
94
106
  body: { progress: body.progress ?? "" },
95
107
  };
96
108
  }
109
+ case "task_cancel": {
110
+ const taskId = typeof body.taskId === "string" ? body.taskId : undefined;
111
+ if (!taskId) throw new Error("task_cancel requires string `taskId`");
112
+ return { method: "POST", path: `/api/tasks/${encodeURIComponent(taskId)}/cancel` };
113
+ }
114
+
115
+ // ── kv ──
97
116
  case "kv_get":
98
117
  return { method: "GET", path: kvPath(body) };
99
118
  case "kv_set":
@@ -119,11 +138,15 @@ function bridgeRequestFor(name: string, args: unknown): BridgeRequest {
119
138
  offset: body.offset,
120
139
  }),
121
140
  };
141
+
142
+ // ── repos ──
122
143
  case "repo_list":
123
144
  return {
124
145
  method: "GET",
125
146
  path: appendQuery("/api/repos", { autoClone: body.autoClone, name: body.name }),
126
147
  };
148
+
149
+ // ── schedules ──
127
150
  case "schedule_list":
128
151
  return {
129
152
  method: "GET",
@@ -134,12 +157,164 @@ function bridgeRequestFor(name: string, args: unknown): BridgeRequest {
134
157
  hideCompleted: body.hideCompleted,
135
158
  }),
136
159
  };
160
+
161
+ // ── scripts ──
137
162
  case "script_search":
138
163
  return { method: "POST", path: "/api/scripts/search", body };
139
164
  case "script_run":
140
165
  return { method: "POST", path: "/api/scripts/run", body };
166
+
167
+ // ── swarm / agent ──
168
+ case "db_query":
169
+ return { method: "POST", path: "/api/db-query", body };
170
+ case "swarm_get":
171
+ return {
172
+ method: "GET",
173
+ path: appendQuery("/api/agents", {
174
+ fields: body.includeFull ? "full" : "slim",
175
+ }),
176
+ };
177
+ case "agent_info":
178
+ return { method: "GET", path: "/me" };
179
+ case "metrics_get":
180
+ return { method: "GET", path: "/api/metrics" };
181
+ case "task_poll":
182
+ return { method: "GET", path: "/api/poll" };
183
+
184
+ // ── config ──
185
+ case "config_get":
186
+ return {
187
+ method: "GET",
188
+ path: appendQuery("/api/config/resolved", {
189
+ agentId: body.agentId,
190
+ repoId: body.repoId,
191
+ key: body.key,
192
+ includeSecrets: body.includeSecrets,
193
+ }),
194
+ };
195
+ case "config_list":
196
+ return {
197
+ method: "GET",
198
+ path: appendQuery("/api/config", {
199
+ scope: body.scope,
200
+ scopeId: body.scopeId,
201
+ key: body.key,
202
+ includeSecrets: body.includeSecrets,
203
+ }),
204
+ };
205
+ case "config_delete": {
206
+ const id = typeof body.id === "string" ? body.id : undefined;
207
+ if (!id) throw new Error("config_delete requires string `id`");
208
+ return { method: "DELETE", path: `/api/config/${encodeURIComponent(id)}` };
209
+ }
210
+
211
+ // ── services ──
212
+ case "service_list":
213
+ return {
214
+ method: "GET",
215
+ path: appendQuery("/api/services", {
216
+ agentId: body.agentId,
217
+ name: body.name,
218
+ status: body.status,
219
+ }),
220
+ };
221
+
222
+ // ── workflows ──
223
+ case "workflow_list":
224
+ return {
225
+ method: "GET",
226
+ path: appendQuery("/api/workflows", {
227
+ fields: body.includeFull ? "full" : "slim",
228
+ }),
229
+ };
230
+ case "workflow_get": {
231
+ const id = typeof body.id === "string" ? body.id : undefined;
232
+ if (!id) throw new Error("workflow_get requires string `id`");
233
+ return { method: "GET", path: `/api/workflows/${encodeURIComponent(id)}` };
234
+ }
235
+ case "workflow_listRuns": {
236
+ const wfId = typeof body.workflowId === "string" ? body.workflowId : undefined;
237
+ if (!wfId) throw new Error("workflow_listRuns requires string `workflowId`");
238
+ return {
239
+ method: "GET",
240
+ path: appendQuery(`/api/workflows/${encodeURIComponent(wfId)}/runs`, {
241
+ status: body.status,
242
+ }),
243
+ };
244
+ }
245
+ case "workflow_getRun": {
246
+ const id = typeof body.id === "string" ? body.id : undefined;
247
+ if (!id) throw new Error("workflow_getRun requires string `id`");
248
+ return { method: "GET", path: `/api/workflow-runs/${encodeURIComponent(id)}` };
249
+ }
250
+ case "workflow_delete": {
251
+ const id = typeof body.id === "string" ? body.id : undefined;
252
+ if (!id) throw new Error("workflow_delete requires string `id`");
253
+ return { method: "DELETE", path: `/api/workflows/${encodeURIComponent(id)}` };
254
+ }
255
+
256
+ // ── prompt templates ──
257
+ case "prompt_list":
258
+ return {
259
+ method: "GET",
260
+ path: appendQuery("/api/prompt-templates", {
261
+ eventType: body.eventType,
262
+ scope: body.scope,
263
+ scopeId: body.scopeId,
264
+ isDefault: body.isDefault,
265
+ }),
266
+ };
267
+ case "prompt_get": {
268
+ const id = typeof body.id === "string" ? body.id : undefined;
269
+ if (!id) throw new Error("prompt_get requires string `id`");
270
+ return { method: "GET", path: `/api/prompt-templates/${encodeURIComponent(id)}` };
271
+ }
272
+ case "prompt_delete": {
273
+ const id = typeof body.id === "string" ? body.id : undefined;
274
+ if (!id) throw new Error("prompt_delete requires string `id`");
275
+ return { method: "DELETE", path: `/api/prompt-templates/${encodeURIComponent(id)}` };
276
+ }
277
+
278
+ // ── skills ──
279
+ case "skill_list":
280
+ return {
281
+ method: "GET",
282
+ path: appendQuery("/api/skills", {
283
+ scope: body.scope,
284
+ scopeId: body.scopeId,
285
+ includeBuiltin: body.includeBuiltin,
286
+ }),
287
+ };
288
+ case "skill_get": {
289
+ const id = typeof body.id === "string" ? body.id : undefined;
290
+ if (!id) throw new Error("skill_get requires string `id`");
291
+ return { method: "GET", path: `/api/skills/${encodeURIComponent(id)}` };
292
+ }
293
+ case "skill_search":
294
+ return { method: "POST", path: "/api/skills/search", body };
295
+ case "skill_delete": {
296
+ const id = typeof body.id === "string" ? body.id : undefined;
297
+ if (!id) throw new Error("skill_delete requires string `id`");
298
+ return { method: "DELETE", path: `/api/skills/${encodeURIComponent(id)}` };
299
+ }
300
+
301
+ // ── mcp servers ──
302
+ case "mcpServer_list":
303
+ return { method: "GET", path: "/api/mcp-servers" };
304
+ case "mcpServer_get": {
305
+ const id = typeof body.id === "string" ? body.id : undefined;
306
+ if (!id) throw new Error("mcpServer_get requires string `id`");
307
+ return { method: "GET", path: `/api/mcp-servers/${encodeURIComponent(id)}` };
308
+ }
309
+ case "mcpServer_delete": {
310
+ const id = typeof body.id === "string" ? body.id : undefined;
311
+ if (!id) throw new Error("mcpServer_delete requires string `id`");
312
+ return { method: "DELETE", path: `/api/mcp-servers/${encodeURIComponent(id)}` };
313
+ }
314
+
315
+ // ── fallthrough: proxy via generic MCP bridge ──
141
316
  default:
142
- throw new Error(`Tool '${name}' is not exposed through the scripts SDK bridge`);
317
+ return null;
143
318
  }
144
319
  }
145
320
 
@@ -152,6 +327,26 @@ async function callBridgeApi(
152
327
  const baseUrl = Redacted.value(config.mcpBaseUrl).replace(/\/$/, "");
153
328
  const request = bridgeRequestFor(name, args);
154
329
 
330
+ // Tools without a specific REST route go through the generic MCP bridge
331
+ if (!request) {
332
+ const mcpToolName = mcpToolNameForSdkMethod(name);
333
+ const res = await fetch(`${baseUrl}/api/mcp-bridge`, {
334
+ method: "POST",
335
+ headers: headers(config),
336
+ body: JSON.stringify({ tool: mcpToolName, args: args ?? {} }),
337
+ });
338
+ const text = await res.text();
339
+ const data = text ? JSON.parse(text) : {};
340
+ if (!res.ok && options.throwOnError) {
341
+ const message =
342
+ data && typeof data === "object" && "error" in data
343
+ ? String((data as { error: unknown }).error)
344
+ : `api failed with ${res.status}`;
345
+ throw new Error(`swarm-sdk: ${name} failed with ${res.status}: ${message}`);
346
+ }
347
+ return scrubObject({ success: res.ok, status: res.status, data });
348
+ }
349
+
155
350
  const res = await fetch(`${baseUrl}${request.path}`, {
156
351
  method: request.method,
157
352
  headers: headers(config),