@desplega.ai/agent-swarm 1.113.0 → 1.115.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 (237) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-q4n7cz6e.js → actions-17ya5wde.js} +8 -9
  3. package/dist/{app-es4nzc71.js → app-4f718yk0.js} +6 -6
  4. package/dist/{assistant-0x8ey0vs.js → assistant-ah7csnjz.js} +19 -18
  5. package/dist/{boot-reembed-69agkhv6.js → boot-reembed-4jpxhb25.js} +6 -6
  6. package/dist/{boot-reembed-p0ny05t8.js → boot-reembed-qata34xa.js} +5 -5
  7. package/dist/{boot-scrub-logs-yybrkmvr.js → boot-scrub-logs-tv48nn3z.js} +4 -4
  8. package/dist/{claude-adapter-mma9a1yr.js → claude-adapter-3csn5bks.js} +3 -3
  9. package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-kysmkz89.js} +2 -2
  10. package/dist/cli-0pygq9r1.js +429 -0
  11. package/dist/{cli-hsetkkd3.js → cli-12vb0xg6.js} +33 -9
  12. package/dist/{cli-ncfgsqbd.js → cli-15y7qhzp.js} +262 -33
  13. package/dist/{cli-7ga43spr.js → cli-1yjrbg1f.js} +1 -1
  14. package/dist/{cli-36ymmpmp.js → cli-3samkyap.js} +2 -2
  15. package/dist/cli-52fg7s7x.js +269 -0
  16. package/dist/{cli-hjhvekf4.js → cli-65ekje01.js} +11 -8
  17. package/dist/{cli-h3k622d0.js → cli-6aq8xhvm.js} +90 -3
  18. package/dist/{cli-f3qa069v.js → cli-7xje5amz.js} +1 -1
  19. package/dist/cli-86fdnerz.js +405 -0
  20. package/dist/{cli-9cy6nc5m.js → cli-8fjwym53.js} +1 -1
  21. package/dist/{cli-c3frqf65.js → cli-b23axgz4.js} +3 -3
  22. package/dist/{cli-a0t7vapf.js → cli-bc89my0z.js} +1 -1
  23. package/dist/{cli-bnzbn8j8.js → cli-beea4a3y.js} +73 -24
  24. package/dist/{cli-q21d49ac.js → cli-bw80ck94.js} +1 -1
  25. package/dist/{cli-pwc5e83c.js → cli-cchksf5q.js} +12 -7
  26. package/dist/{cli-30bbaveh.js → cli-cmw9rzy5.js} +2 -2
  27. package/dist/{cli-y6mrptcn.js → cli-cnwssmsr.js} +1 -1
  28. package/dist/{cli-r5g6ngtn.js → cli-dcca6j8m.js} +46 -5
  29. package/dist/{cli-tkmm5nf9.js → cli-e61fs2hp.js} +59 -2
  30. package/dist/{cli-6wscj9z0.js → cli-e7306swv.js} +14 -14
  31. package/dist/{cli-dcfyyh3t.js → cli-ftyf58xj.js} +2 -2
  32. package/dist/{cli-4xyj9jtq.js → cli-hrhye2rm.js} +1062 -478
  33. package/dist/cli-jkhwmmgg.js +136 -0
  34. package/dist/{cli-y4ycrnjc.js → cli-k3y4z3h5.js} +1 -1
  35. package/dist/{cli-6xd0bvf8.js → cli-ma04faw5.js} +1 -1
  36. package/dist/{cli-2fc97xm5.js → cli-mnbgv3w7.js} +4 -4
  37. package/dist/{cli-trmapn9k.js → cli-pet3aev6.js} +95 -10
  38. package/dist/{cli-z46pnksz.js → cli-q1rx7g0d.js} +175 -28
  39. package/dist/{cli-qwgtacd6.js → cli-r9f6ngrf.js} +1244 -336
  40. package/dist/{cli-gtkqc144.js → cli-shvaxw27.js} +13 -7
  41. package/dist/{cli-ywez6yps.js → cli-sq7f72d6.js} +1 -1
  42. package/dist/cli-t0vqevnm.js +393 -0
  43. package/dist/{cli-tbw13sx8.js → cli-tece6zgm.js} +1 -1
  44. package/dist/{cli-bgvskydy.js → cli-tffvrh6j.js} +1 -1
  45. package/dist/cli-wh91zby6.js +187222 -0
  46. package/dist/{cli-anrj584m.js → cli-xz9aq0rf.js} +1 -1
  47. package/dist/{cli-mq580e06.js → cli-zsrdty74.js} +18 -18
  48. package/dist/cli.js +32 -15
  49. package/dist/{codex-adapter-5cmy858w.js → codex-adapter-de3w0nch.js} +10 -10
  50. package/dist/{codex-session-runner-46q8mrba.js → codex-session-runner-bnw8cysk.js} +10 -10
  51. package/dist/{commands-z5dbwta3.js → commands-hch4e2k1.js} +4 -4
  52. package/dist/{db-bbgahh4z.js → db-p3h2j06x.js} +14 -4
  53. package/dist/{handlers-6b44317z.js → handlers-9er4zx9q.js} +17 -31
  54. package/dist/{hook-sbp5fmps.js → hook-4axs756z.js} +7 -7
  55. package/dist/{http-2xz43gz3.js → http-809fvtz2.js} +1934 -886
  56. package/dist/{index-wz1ea15p.js → index-2dgfj9av.js} +3 -3
  57. package/dist/{index-nhat35em.js → index-47em3d2n.js} +14 -11
  58. package/dist/{index-5ghxn8s6.js → index-9fqaktjr.js} +12 -9
  59. package/dist/{index-0gzmqj4k.js → index-9ht8gr56.js} +13 -10
  60. package/dist/{index-vfedgdw6.js → index-rm85cfdx.js} +368 -40
  61. package/dist/{index-ykvs43z4.js → index-t2snazez.js} +4 -4
  62. package/dist/{keepalive-62gfj91d.js → keepalive-3cps6e3e.js} +7 -6
  63. package/dist/{lead-ag0akn0v.js → lead-fkk95wgq.js} +36 -32
  64. package/dist/{maintenance-xxrrzk5x.js → maintenance-fweqh0yj.js} +6 -6
  65. package/dist/{mistral-conversations-26yrnbf1.js → mistral-conversations-7gqnz2wt.js} +7 -7
  66. package/dist/oauth-refresh-sweep-sycqtbph.js +114 -0
  67. package/dist/{onboard-64zyfcbs.js → onboard-cy0rkgvx.js} +2 -2
  68. package/dist/{opencode-adapter-mqsxr170.js → opencode-adapter-p6bxnbm4.js} +2 -2
  69. package/dist/{otel-zjd9zaxs.js → otel-g299k41b.js} +2 -2
  70. package/dist/{otel-impl-b86qseaa.js → otel-impl-wbns8hf7.js} +1 -1
  71. package/dist/{pi-mono-adapter-kxjxpfgy.js → pi-mono-adapter-05sck1ds.js} +17 -109
  72. package/dist/{pricing-refresh-cgtkffpa.js → pricing-refresh-0hvazmkf.js} +6 -6
  73. package/dist/rbac-roles-kfesjy9y.js +28 -0
  74. package/dist/rbac-roles-mwcwhbxc.js +283 -0
  75. package/dist/{seed-pricing-1r1y1xkq.js → seed-pricing-h2kdq4cq.js} +5 -5
  76. package/dist/{setup-mkcgkjf7.js → setup-8506q97w.js} +2 -2
  77. package/dist/{worker-351sze63.js → worker-4r5c6zyb.js} +36 -32
  78. package/openapi.json +1327 -20
  79. package/package.json +3 -3
  80. package/src/agentmail/handlers.ts +16 -6
  81. package/src/agentmail/templates.ts +25 -5
  82. package/src/be/audit-user.ts +28 -4
  83. package/src/be/db-queries/oauth.ts +42 -0
  84. package/src/be/db.ts +314 -28
  85. package/src/be/identity.ts +79 -0
  86. package/src/be/mcp-proxy.ts +132 -0
  87. package/src/be/migrations/109_rbac_roles.sql +79 -0
  88. package/src/be/migrations/111_oauth_credential_bindings.sql +6 -0
  89. package/src/be/migrations/112_script_connections_graphql.sql +96 -0
  90. package/src/be/migrations/113_task_routing_affinity.sql +9 -0
  91. package/src/be/migrations/114_backfill_gpt_5_6_pricing.sql +15 -0
  92. package/src/be/modelsdev-cache.json +135 -0
  93. package/src/be/oauth-credential-bindings.ts +35 -0
  94. package/src/be/oauth-refresh-sweep.ts +159 -0
  95. package/src/be/rbac-audit.ts +52 -13
  96. package/src/be/rbac-roles.ts +424 -0
  97. package/src/be/script-connections.ts +459 -40
  98. package/src/be/script-credential-broker.ts +29 -3
  99. package/src/be/scripts/typecheck.ts +51 -7
  100. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +325 -24
  101. package/src/be/seed-scripts/catalog/catalog-report.ts +325 -24
  102. package/src/be/users.ts +24 -0
  103. package/src/cli.tsx +17 -0
  104. package/src/github/handlers.ts +10 -9
  105. package/src/github/templates.ts +45 -9
  106. package/src/gitlab/handlers.ts +20 -8
  107. package/src/gitlab/templates.ts +18 -6
  108. package/src/heartbeat/heartbeat.ts +143 -21
  109. package/src/http/all-routes.ts +3 -0
  110. package/src/http/approval-requests.ts +12 -0
  111. package/src/http/config.ts +44 -10
  112. package/src/http/core.ts +30 -3
  113. package/src/http/favorites.ts +1 -5
  114. package/src/http/index.ts +25 -1
  115. package/src/http/mcp-oauth.ts +2 -0
  116. package/src/http/mcp-servers.ts +44 -3
  117. package/src/http/oauth-generic.ts +105 -0
  118. package/src/http/poll.ts +20 -3
  119. package/src/http/script-connection-proxy.ts +116 -0
  120. package/src/http/script-connections.ts +1657 -0
  121. package/src/http/scripts.ts +27 -7
  122. package/src/http/skills.ts +11 -0
  123. package/src/http/tasks.ts +27 -13
  124. package/src/http/x.ts +6 -2
  125. package/src/integrations/kapso/inbound.ts +23 -6
  126. package/src/jira/sync.ts +111 -17
  127. package/src/jira/templates.ts +10 -2
  128. package/src/mcp-client/http-client.ts +194 -0
  129. package/src/oauth/app-validation.ts +16 -0
  130. package/src/oauth/ensure-token.ts +69 -14
  131. package/src/oauth/mcp-wrapper.ts +15 -0
  132. package/src/oauth/wrapper.ts +48 -20
  133. package/src/prompts/session-templates.ts +3 -3
  134. package/src/providers/codex-models.ts +100 -30
  135. package/src/providers/modelsdev-reasoning.json +51 -0
  136. package/src/providers/pi-mono-mcp-client.ts +2 -133
  137. package/src/providers/reasoning-effort.ts +11 -5
  138. package/src/rbac/admission.ts +68 -0
  139. package/src/rbac/index.ts +2 -0
  140. package/src/rbac/legacy-policy.ts +12 -0
  141. package/src/rbac/permissions.ts +48 -0
  142. package/src/scheduler/scheduler.ts +9 -1
  143. package/src/scripts-runtime/api-client.ts +151 -19
  144. package/src/scripts-runtime/api-types.ts +38 -6
  145. package/src/scripts-runtime/credential-broker/default-bindings.ts +1 -0
  146. package/src/scripts-runtime/credential-broker/types.ts +8 -0
  147. package/src/scripts-runtime/ctx.ts +11 -1
  148. package/src/scripts-runtime/eval-harness.ts +5 -1
  149. package/src/scripts-runtime/executors/types.ts +2 -1
  150. package/src/scripts-runtime/loader.ts +3 -1
  151. package/src/scripts-runtime/mcp-client.ts +87 -0
  152. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  153. package/src/scripts-runtime/types/stdlib.d.ts +6 -0
  154. package/src/scripts-runtime/types/swarm-sdk.d.ts +19 -0
  155. package/src/server-user.ts +114 -59
  156. package/src/server.ts +12 -0
  157. package/src/slack/assistant.ts +14 -6
  158. package/src/slack/enrich.ts +17 -0
  159. package/src/slack/handlers.ts +8 -22
  160. package/src/slack/responses.ts +91 -1
  161. package/src/slack/thread-buffer.ts +6 -3
  162. package/src/slack/watcher.ts +9 -6
  163. package/src/tasks/worker-follow-up.ts +100 -3
  164. package/src/tests/agentmail-handlers.test.ts +32 -3
  165. package/src/tests/approval-requests.test.ts +61 -0
  166. package/src/tests/audit-user.test.ts +47 -1
  167. package/src/tests/codex-adapter.test.ts +46 -23
  168. package/src/tests/credential-broker.test.ts +5 -2
  169. package/src/tests/delete-page-tool.test.ts +197 -0
  170. package/src/tests/github-handlers.test.ts +28 -2
  171. package/src/tests/gitlab-handlers.test.ts +125 -0
  172. package/src/tests/heartbeat.test.ts +48 -3
  173. package/src/tests/identity.test.ts +117 -0
  174. package/src/tests/jira-sync.test.ts +99 -1
  175. package/src/tests/kapso-inbound.test.ts +7 -0
  176. package/src/tests/mcp-tools-user.test.ts +60 -6
  177. package/src/tests/model-control.test.ts +8 -2
  178. package/src/tests/oauth-credential-bindings.test.ts +490 -0
  179. package/src/tests/oauth-refresh-sweep.test.ts +160 -0
  180. package/src/tests/pool-affinity.test.ts +401 -0
  181. package/src/tests/prompt-template-remaining.test.ts +2 -2
  182. package/src/tests/rbac-admission-e2e.test.ts +386 -0
  183. package/src/tests/rbac-admission.test.ts +492 -0
  184. package/src/tests/rbac-e2e-helpers.ts +3 -0
  185. package/src/tests/rbac-engine.test.ts +50 -0
  186. package/src/tests/rbac-mcp-admission-e2e.test.ts +362 -0
  187. package/src/tests/rbac-roles.test.ts +328 -0
  188. package/src/tests/reasoning-effort.test.ts +15 -2
  189. package/src/tests/schedule-target-type.test.ts +50 -0
  190. package/src/tests/scheduled-tasks.test.ts +40 -0
  191. package/src/tests/script-apis-mcp.test.ts +37 -13
  192. package/src/tests/script-connections-http.test.ts +1158 -0
  193. package/src/tests/script-connections-mcp.test.ts +894 -0
  194. package/src/tests/script-connections.test.ts +754 -6
  195. package/src/tests/scripts-external-api.test.ts +49 -1
  196. package/src/tests/scripts-runtime-secret-egress.test.ts +1 -0
  197. package/src/tests/scripts-runtime.test.ts +291 -0
  198. package/src/tests/seed-scripts.test.ts +36 -0
  199. package/src/tests/slack-identity-resolution.test.ts +37 -3
  200. package/src/tests/slack-inline-output.test.ts +65 -0
  201. package/src/tests/slack-thread-buffer.test.ts +28 -0
  202. package/src/tests/tool-annotations.test.ts +1 -0
  203. package/src/tests/user-token-rest-auth.test.ts +140 -1
  204. package/src/tests/workflow-swarm-script.test.ts +44 -0
  205. package/src/tests/workflow-triggers-v2.test.ts +300 -1
  206. package/src/tools/credential-bindings/tool.ts +277 -18
  207. package/src/tools/delete-page.ts +145 -0
  208. package/src/tools/mcp-servers/mcp-server-delete.ts +7 -4
  209. package/src/tools/request-human-input.ts +2 -0
  210. package/src/tools/resolve-user.ts +77 -27
  211. package/src/tools/script-apis.ts +62 -3
  212. package/src/tools/script-connections/tool.ts +275 -31
  213. package/src/tools/send-task.ts +15 -1
  214. package/src/tools/task-action.ts +34 -2
  215. package/src/tools/templates.ts +53 -1
  216. package/src/tools/tool-config.ts +2 -1
  217. package/src/tools/utils.ts +2 -0
  218. package/src/tools/workflows/create-workflow.ts +8 -1
  219. package/src/tools/workflows/update-workflow.ts +11 -2
  220. package/src/types.ts +90 -11
  221. package/src/utils/scoped-resource.ts +25 -0
  222. package/src/workflows/executors/human-in-the-loop.ts +1 -0
  223. package/src/workflows/executors/swarm-script.ts +7 -1
  224. package/src/workflows/triggers.ts +146 -18
  225. package/templates/skills/swarm-scripts/content.md +33 -8
  226. package/dist/cli-bgef578c.js +0 -771
  227. package/dist/cli-c9mtm09b.js +0 -43
  228. package/dist/cli-ceewqajw.js +0 -1698
  229. package/dist/{anthropic-messages-8nc6r2dw.js → anthropic-messages-kq3pjhbp.js} +6 -6
  230. package/dist/{azure-openai-responses-6yfzw3qw.js → azure-openai-responses-gm1ejtmn.js} +7 -7
  231. package/dist/{cli-vbraamxr.js → cli-6b4wwmm8.js} +9 -9
  232. package/dist/{cli-2r7s8jhp.js → cli-sznberje.js} +3 -3
  233. package/dist/{google-generative-ai-vfq6ymz9.js → google-generative-ai-hyrra2jm.js} +3 -3
  234. package/dist/{google-vertex-mj5y5rfj.js → google-vertex-8bs2pzgn.js} +3 -3
  235. package/dist/{openai-codex-responses-8h6kvfv8.js → openai-codex-responses-3nkvje79.js} +4 -4
  236. package/dist/{openai-completions-x6kjnqjd.js → openai-completions-0asftkwg.js} +11 -11
  237. package/dist/{openai-responses-7jwyz3ds.js → openai-responses-pmatek45.js} +10 -10
@@ -1,18 +1,22 @@
1
1
  import { getDb, getSwarmConfigs } from "@/be/db";
2
+ import { assertUrlSafe, publicEndpointSsrfOptions } from "@/oauth/mcp-wrapper";
2
3
  import type {
3
4
  ScriptApiConnectionDescriptor,
4
5
  ScriptApiJsonSchema,
5
6
  ScriptApiJsonValue,
6
7
  ScriptApiOperationDescriptor,
8
+ ScriptMcpConnectionDescriptor,
9
+ ScriptMcpToolDescriptor,
7
10
  } from "@/scripts-runtime/api-types";
8
11
  import {
9
12
  CREDENTIAL_BINDINGS_CONFIG_KEY,
10
13
  type CredentialBinding,
11
14
  normalizeCredentialBindingsDocument,
12
15
  } from "@/scripts-runtime/credential-broker";
16
+ import { listMcpServerTools } from "./mcp-proxy";
13
17
 
14
18
  export type ScriptConnectionScope = "global" | "agent" | "repo";
15
- export type ScriptConnectionKind = "raw" | "openapi" | "mcp";
19
+ export type ScriptConnectionKind = "raw" | "openapi" | "mcp" | "graphql";
16
20
 
17
21
  export type ScriptCredentialBindingRecord = CredentialBinding & {
18
22
  id: string;
@@ -60,6 +64,8 @@ type BindingRow = {
60
64
  scope: string;
61
65
  scope_id: string | null;
62
66
  active: number;
67
+ auth_kind: string;
68
+ oauth_provider: string | null;
63
69
  source: string;
64
70
  created_at: string;
65
71
  updated_at: string;
@@ -114,6 +120,8 @@ function bindingFromRow(row: BindingRow): ScriptCredentialBindingRecord {
114
120
  scope: row.scope as ScriptConnectionScope,
115
121
  scopeId: row.scope_id,
116
122
  active: row.active === 1,
123
+ authKind: row.auth_kind === "oauth" ? "oauth" : "config",
124
+ oauthProvider: row.oauth_provider ?? undefined,
117
125
  source: row.source as ScriptCredentialBindingRecord["source"],
118
126
  createdAt: row.created_at,
119
127
  updatedAt: row.updated_at,
@@ -234,6 +242,8 @@ export function upsertCredentialBinding(data: {
234
242
  scope?: ScriptConnectionScope;
235
243
  scopeId?: string | null;
236
244
  active?: boolean;
245
+ authKind?: CredentialBinding["authKind"];
246
+ oauthProvider?: string | null;
237
247
  source?: "default" | "user" | "migration";
238
248
  userId?: string | null;
239
249
  }): ScriptCredentialBindingRecord {
@@ -242,6 +252,11 @@ export function upsertCredentialBinding(data: {
242
252
  const scope = data.scope ?? "global";
243
253
  const scopeId = scope === "global" ? null : (data.scopeId ?? null);
244
254
  const active = data.active === false ? 0 : 1;
255
+ const authKind = data.authKind ?? "config";
256
+ if (authKind === "oauth" && !data.oauthProvider) {
257
+ throw new Error("oauthProvider is required for oauth credential bindings");
258
+ }
259
+ const oauthProvider = data.oauthProvider ?? null;
245
260
  const source = data.source ?? "user";
246
261
  const existing =
247
262
  (data.id ? getCredentialBindingById(data.id) : null) ??
@@ -269,12 +284,15 @@ export function upsertCredentialBinding(data: {
269
284
  string,
270
285
  string | null,
271
286
  string,
287
+ string | null,
288
+ string,
272
289
  string,
273
290
  ]
274
291
  >(
275
292
  `UPDATE script_credential_bindings
276
293
  SET config_key = ?, allowed_hosts_json = ?, header_template = ?, query_template = ?,
277
- scope = ?, scope_id = ?, active = ?, source = ?, updated_by = ?, updated_at = ?
294
+ scope = ?, scope_id = ?, active = ?, auth_kind = ?, oauth_provider = ?,
295
+ source = ?, updated_by = ?, updated_at = ?
278
296
  WHERE id = ? RETURNING *`,
279
297
  )
280
298
  .get(
@@ -285,6 +303,8 @@ export function upsertCredentialBinding(data: {
285
303
  scope,
286
304
  scopeId,
287
305
  active,
306
+ authKind,
307
+ oauthProvider,
288
308
  source,
289
309
  data.userId ?? null,
290
310
  now,
@@ -307,6 +327,8 @@ export function upsertCredentialBinding(data: {
307
327
  string | null,
308
328
  number,
309
329
  string,
330
+ string | null,
331
+ string,
310
332
  string,
311
333
  string,
312
334
  string | null,
@@ -315,8 +337,8 @@ export function upsertCredentialBinding(data: {
315
337
  >(
316
338
  `INSERT INTO script_credential_bindings
317
339
  (id, config_key, allowed_hosts_json, header_template, query_template, scope, scope_id,
318
- active, source, created_at, updated_at, created_by, updated_by)
319
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
340
+ active, auth_kind, oauth_provider, source, created_at, updated_at, created_by, updated_by)
341
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
320
342
  )
321
343
  .get(
322
344
  id,
@@ -327,6 +349,8 @@ export function upsertCredentialBinding(data: {
327
349
  scope,
328
350
  scopeId,
329
351
  active,
352
+ authKind,
353
+ oauthProvider,
330
354
  source,
331
355
  now,
332
356
  now,
@@ -382,6 +406,7 @@ export function listScriptConnections(context?: {
382
406
  repoId?: string;
383
407
  kind?: ScriptConnectionKind;
384
408
  includeDisabled?: boolean;
409
+ allScopes?: boolean;
385
410
  }): ScriptConnectionRecord[] {
386
411
  const rows = getDb()
387
412
  .prepare<ConnectionRow, []>("SELECT * FROM script_connections ORDER BY slug ASC")
@@ -390,7 +415,19 @@ export function listScriptConnections(context?: {
390
415
  .map(connectionFromRow)
391
416
  .filter((connection) => !context?.kind || connection.kind === context.kind)
392
417
  .filter((connection) => context?.includeDisabled || connection.enabled)
393
- .filter((connection) => !context || applies(connection.scope, connection.scopeId, context));
418
+ .filter(
419
+ (connection) =>
420
+ !context ||
421
+ context.allScopes === true ||
422
+ applies(connection.scope, connection.scopeId, context),
423
+ );
424
+ }
425
+
426
+ export function getScriptConnectionById(id: string): ScriptConnectionRecord | null {
427
+ const row = getDb()
428
+ .prepare<ConnectionRow, [string]>("SELECT * FROM script_connections WHERE id = ?")
429
+ .get(id);
430
+ return row ? connectionFromRow(row) : null;
394
431
  }
395
432
 
396
433
  function schemaToTs(schema: unknown): string {
@@ -529,7 +566,10 @@ function extractOperations(
529
566
  (param) => `${JSON.stringify(param.name)}${param.required ? "" : "?"}: ${param.type}`,
530
567
  )
531
568
  .join("; ");
532
- const hasBody = Boolean(jsonContent?.schema || requestBody);
569
+ // Some generated specs (e.g. readme.io exports) declare a requestBody on
570
+ // GET operations; fetch() rejects GET/HEAD bodies, so never generate one.
571
+ const methodAllowsBody = method !== "get" && method !== "head";
572
+ const hasBody = methodAllowsBody && Boolean(jsonContent?.schema || requestBody);
533
573
  const requestType = `export type ${typeBase}Args = {${paramsByPlace("path") ? ` path: { ${paramsByPlace("path")} };` : ""}${paramsByPlace("query") ? ` query?: { ${paramsByPlace("query")} };` : ""}${paramsByPlace("header") ? ` header?: { ${paramsByPlace("header")} };` : ""}${hasBody ? ` body: ${bodyType};` : ""}};`;
534
574
  const responseDecl = `export type ${typeBase}Response = ${responseType};`;
535
575
  typeBlocks.push(requestType, responseDecl);
@@ -565,13 +605,7 @@ export function buildGeneratedArtifacts(input: {
565
605
  }): { generatedTypes: string; generatedRuntimeJson: string } {
566
606
  const slug = normalizeSlug(input.slug);
567
607
  const { operations, types } = extractOperations(input.openapiSpec, slug);
568
- const credential = input.credentialBinding
569
- ? {
570
- configKey: input.credentialBinding.configKey,
571
- headerTemplate: input.credentialBinding.headerTemplate,
572
- queryTemplate: input.credentialBinding.queryTemplate,
573
- }
574
- : null;
608
+ const credential = credentialDescriptor(input.credentialBinding);
575
609
  const descriptor: ScriptApiConnectionDescriptor = {
576
610
  slug,
577
611
  baseUrl: input.baseUrl,
@@ -581,16 +615,214 @@ export function buildGeneratedArtifacts(input: {
581
615
  const generatedTypes = [
582
616
  types,
583
617
  `export interface ${pascal(slug)}Api {`,
584
- ...operations.map(
585
- (operation) =>
586
- ` ${operation.name}(args: ${operation.requestType}): Promise<${operation.responseType}>;`,
618
+ ...operations.flatMap((operation) => [
619
+ ` ${operation.name}(args: ${operation.requestType}): Promise<${operation.responseType}>;`,
620
+ ` ${operation.name}(args: ${operation.requestType}, options: ScriptApiRawOptions): Promise<ScriptApiRawResult>;`,
621
+ ]),
622
+ "}",
623
+ ].join("\n");
624
+ return { generatedTypes, generatedRuntimeJson: JSON.stringify(descriptor) };
625
+ }
626
+
627
+ function credentialDescriptor(binding: ScriptCredentialBindingRecord | null) {
628
+ return binding
629
+ ? {
630
+ configKey: binding.configKey,
631
+ headerTemplate: binding.headerTemplate,
632
+ queryTemplate: binding.queryTemplate,
633
+ }
634
+ : null;
635
+ }
636
+
637
+ export function buildGraphqlGeneratedArtifacts(input: {
638
+ slug: string;
639
+ baseUrl: string;
640
+ credentialBinding: ScriptCredentialBindingRecord | null;
641
+ }): { generatedTypes: string; generatedRuntimeJson: string } {
642
+ const slug = normalizeSlug(input.slug);
643
+ const descriptor: ScriptApiConnectionDescriptor = {
644
+ slug,
645
+ kind: "graphql",
646
+ baseUrl: input.baseUrl,
647
+ credential: credentialDescriptor(input.credentialBinding),
648
+ };
649
+ const generatedTypes = [
650
+ `export interface ${pascal(slug)}Api {`,
651
+ " graphql<T = JsonValue>(query: string, variables?: Record<string, JsonValue>): Promise<T>;",
652
+ "}",
653
+ ].join("\n");
654
+ return { generatedTypes, generatedRuntimeJson: JSON.stringify(descriptor) };
655
+ }
656
+
657
+ type OpenapiSpecFetchResult =
658
+ | {
659
+ status: "fetched";
660
+ spec: unknown;
661
+ specJson: string;
662
+ etag: string | null;
663
+ fetchedAt: string;
664
+ }
665
+ | {
666
+ status: "not_modified";
667
+ etag: string | null;
668
+ fetchedAt: string;
669
+ };
670
+
671
+ let openapiSpecFetchForTesting: typeof fetch | null = null;
672
+
673
+ export function setOpenapiSpecFetchForTesting(fetchImpl: typeof fetch | null): void {
674
+ openapiSpecFetchForTesting = fetchImpl;
675
+ }
676
+
677
+ function openapiSpecUrlOptions() {
678
+ return publicEndpointSsrfOptions();
679
+ }
680
+
681
+ function isRedirectStatus(status: number): boolean {
682
+ return status >= 300 && status < 400 && status !== 304;
683
+ }
684
+
685
+ function mcpToolMethodName(name: string): string {
686
+ return normalizeSlug(name);
687
+ }
688
+
689
+ function mcpToolMethodNames<T extends { name: string }>(
690
+ tools: T[],
691
+ ): Array<{ tool: T; methodName: string }> {
692
+ const used = new Set<string>();
693
+ const baseCounts = new Map<string, number>();
694
+ return tools.map((tool) => {
695
+ const base = mcpToolMethodName(tool.name);
696
+ const count = baseCounts.get(base) ?? 0;
697
+ baseCounts.set(base, count + 1);
698
+ let methodName = count === 0 ? base : `${base}${count + 1}`;
699
+ let suffix = count + 2;
700
+ while (used.has(methodName)) {
701
+ methodName = `${base}${suffix}`;
702
+ suffix += 1;
703
+ }
704
+ used.add(methodName);
705
+ return { tool, methodName };
706
+ });
707
+ }
708
+
709
+ function mcpToolArgsType(inputSchema: unknown): string {
710
+ if (!inputSchema || typeof inputSchema !== "object") return "Record<string, JsonValue>";
711
+ const schema = inputSchema as Record<string, unknown>;
712
+ if (Object.keys(schema).length === 0) return "Record<string, JsonValue>";
713
+ const type = schemaToTs(schema);
714
+ return type === "JsonValue" ? "Record<string, JsonValue>" : type;
715
+ }
716
+
717
+ export function buildMcpGeneratedArtifacts(input: {
718
+ slug: string;
719
+ connectionId: string;
720
+ tools: Array<{ name: string; description?: string; inputSchema?: Record<string, unknown> }>;
721
+ }): { generatedTypes: string; generatedRuntimeJson: string } {
722
+ const slug = normalizeSlug(input.slug);
723
+ const methodNames = mcpToolMethodNames(input.tools);
724
+ const tools: ScriptMcpToolDescriptor[] = input.tools.map((tool) => ({
725
+ name: tool.name,
726
+ description: tool.description,
727
+ inputSchema: jsonSchema(tool.inputSchema ?? {}),
728
+ }));
729
+ const generatedTypes = [
730
+ `export interface ${pascal(slug)}Mcp {`,
731
+ ...methodNames.map(
732
+ ({ tool, methodName }) =>
733
+ ` ${methodName}(args: ${mcpToolArgsType(tool.inputSchema)}): Promise<JsonValue>;`,
587
734
  ),
588
735
  "}",
589
736
  ].join("\n");
737
+ const descriptor: ScriptMcpConnectionDescriptor = {
738
+ slug,
739
+ kind: "mcp",
740
+ connectionId: input.connectionId,
741
+ tools,
742
+ };
590
743
  return { generatedTypes, generatedRuntimeJson: JSON.stringify(descriptor) };
591
744
  }
592
745
 
593
- export function upsertScriptConnection(data: {
746
+ function parseOpenapiSpecBody(body: string): unknown {
747
+ try {
748
+ return JSON.parse(body);
749
+ } catch (jsonErr) {
750
+ const yaml = (Bun as unknown as { YAML?: { parse(input: string): unknown } }).YAML;
751
+ if (!yaml) {
752
+ throw new Error(
753
+ `OpenAPI spec response was not valid JSON. JSON specs only unless Bun.YAML.parse is available: ${
754
+ jsonErr instanceof Error ? jsonErr.message : String(jsonErr)
755
+ }`,
756
+ );
757
+ }
758
+ try {
759
+ return yaml.parse(body);
760
+ } catch (yamlErr) {
761
+ throw new Error(
762
+ `OpenAPI spec response was neither valid JSON nor valid YAML: ${
763
+ yamlErr instanceof Error ? yamlErr.message : String(yamlErr)
764
+ }`,
765
+ );
766
+ }
767
+ }
768
+ }
769
+
770
+ export async function fetchOpenapiSpec(
771
+ url: string,
772
+ opts: { etag?: string | null } = {},
773
+ ): Promise<OpenapiSpecFetchResult> {
774
+ let parsed = assertUrlSafe(url, openapiSpecUrlOptions());
775
+ const controller = new AbortController();
776
+ const timeout = setTimeout(() => controller.abort(), 15_000);
777
+ try {
778
+ const headers = new Headers({
779
+ Accept: "application/json, application/yaml;q=0.9, text/yaml;q=0.8, */*;q=0.5",
780
+ });
781
+ if (opts.etag) headers.set("If-None-Match", opts.etag);
782
+ const fetchImpl = openapiSpecFetchForTesting ?? Bun.fetch;
783
+ let response: Response | null = null;
784
+ for (let hop = 0; hop <= 5; hop += 1) {
785
+ response = await fetchImpl(parsed, {
786
+ headers,
787
+ signal: controller.signal,
788
+ redirect: "manual",
789
+ });
790
+ if (!isRedirectStatus(response.status)) break;
791
+ const location = response.headers.get("location");
792
+ if (!location) {
793
+ throw new Error(`OpenAPI spec redirect missing Location header: HTTP ${response.status}`);
794
+ }
795
+ parsed = assertUrlSafe(new URL(location, parsed).toString(), openapiSpecUrlOptions());
796
+ if (hop === 5) throw new Error("OpenAPI spec fetch exceeded 5 redirects.");
797
+ }
798
+ if (!response) throw new Error("OpenAPI spec fetch failed before receiving a response.");
799
+ const fetchedAt = new Date().toISOString();
800
+ const etag = response.headers.get("etag");
801
+ if (response.status === 304) {
802
+ return { status: "not_modified", etag, fetchedAt };
803
+ }
804
+ if (!response.ok) {
805
+ throw new Error(`Failed to fetch OpenAPI spec: HTTP ${response.status}`);
806
+ }
807
+ const spec = parseOpenapiSpecBody(await response.text());
808
+ return {
809
+ status: "fetched",
810
+ spec,
811
+ specJson: JSON.stringify(spec),
812
+ etag,
813
+ fetchedAt,
814
+ };
815
+ } catch (err) {
816
+ if (err instanceof DOMException && err.name === "AbortError") {
817
+ throw new Error("Timed out fetching OpenAPI spec after 15s");
818
+ }
819
+ throw err;
820
+ } finally {
821
+ clearTimeout(timeout);
822
+ }
823
+ }
824
+
825
+ export async function upsertScriptConnection(data: {
594
826
  id?: string;
595
827
  slug: string;
596
828
  displayName?: string | null;
@@ -602,23 +834,67 @@ export function upsertScriptConnection(data: {
602
834
  credentialBindingId?: string | null;
603
835
  openapiSpecSourceKind?: "url" | "inline" | "agent_fs" | null;
604
836
  openapiSpecSource?: string | null;
837
+ openapiSpecUrl?: string | null;
605
838
  openapiSpecJson?: string | null;
839
+ openapiSpecEtag?: string | null;
840
+ openapiSpecFetchedAt?: string | null;
606
841
  mcpServerId?: string | null;
607
842
  enabled?: boolean;
843
+ agentId?: string;
608
844
  userId?: string | null;
609
- }): ScriptConnectionRecord {
845
+ }): Promise<ScriptConnectionRecord> {
610
846
  const now = new Date().toISOString();
611
847
  const id = data.id ?? crypto.randomUUID();
612
848
  const scope = data.scope ?? "global";
613
849
  const scopeId = scope === "global" ? null : (data.scopeId ?? null);
850
+ const normalizedSlug = normalizeSlug(data.slug);
614
851
  let generatedTypes: string | null = null;
615
852
  let generatedRuntimeJson: string | null = null;
616
853
  let generationError: string | null = null;
617
854
  let generatedAt: string | null = null;
855
+ let openapiSpecJson = data.openapiSpecJson ?? null;
856
+ let openapiSpecSourceKind =
857
+ data.openapiSpecSourceKind ??
858
+ (data.kind === "openapi" ? (data.openapiSpecUrl ? "url" : "inline") : null);
859
+ let openapiSpecSource = data.openapiSpecSource ?? data.openapiSpecUrl ?? null;
860
+ let openapiSpecEtag = data.openapiSpecEtag ?? null;
861
+ let openapiSpecFetchedAt = data.openapiSpecFetchedAt ?? null;
862
+ let openapiSpec: unknown;
863
+
864
+ const existing = data.id
865
+ ? getDb()
866
+ .prepare<{ id: string; version: number }, [string]>(
867
+ "SELECT id, version FROM script_connections WHERE id = ?",
868
+ )
869
+ .get(data.id)
870
+ : getDb()
871
+ .prepare<{ id: string; version: number }, [string, string, string | null]>(
872
+ "SELECT id, version FROM script_connections WHERE slug = ? AND scope = ? AND COALESCE(scope_id, '') = COALESCE(?, '')",
873
+ )
874
+ .get(normalizedSlug, scope, scopeId);
875
+ const connectionId = existing?.id ?? id;
618
876
 
619
877
  if (data.kind === "openapi") {
878
+ if (!openapiSpecJson && data.openapiSpecUrl) {
879
+ const fetched = await fetchOpenapiSpec(data.openapiSpecUrl);
880
+ if (fetched.status === "not_modified") {
881
+ throw new Error("OpenAPI spec URL returned 304 without an existing cached spec.");
882
+ }
883
+ openapiSpec = fetched.spec;
884
+ openapiSpecJson = fetched.specJson;
885
+ openapiSpecSourceKind = "url";
886
+ openapiSpecSource = data.openapiSpecUrl;
887
+ openapiSpecEtag = fetched.etag;
888
+ openapiSpecFetchedAt = fetched.fetchedAt;
889
+ }
620
890
  try {
621
- const spec = JSON.parse(data.openapiSpecJson ?? "{}");
891
+ // Inline specs may be pasted as YAML too — parse with the same
892
+ // JSON-then-YAML fallback as fetched specs and store canonical JSON.
893
+ let spec = openapiSpec;
894
+ if (!spec) {
895
+ spec = parseOpenapiSpecBody(openapiSpecJson ?? "{}");
896
+ openapiSpecJson = JSON.stringify(spec);
897
+ }
622
898
  const binding = data.credentialBindingId
623
899
  ? getCredentialBindingById(data.credentialBindingId)
624
900
  : null;
@@ -636,20 +912,53 @@ export function upsertScriptConnection(data: {
636
912
  }
637
913
  }
638
914
 
639
- const existing = data.id
640
- ? getDb()
641
- .prepare<{ id: string; version: number }, [string]>(
642
- "SELECT id, version FROM script_connections WHERE id = ?",
643
- )
644
- .get(data.id)
645
- : getDb()
646
- .prepare<{ id: string; version: number }, [string, string, string | null]>(
647
- "SELECT id, version FROM script_connections WHERE slug = ? AND scope = ? AND COALESCE(scope_id, '') = COALESCE(?, '')",
648
- )
649
- .get(normalizeSlug(data.slug), scope, scopeId);
915
+ if (data.kind === "mcp") {
916
+ try {
917
+ if (!data.mcpServerId) throw new Error("mcpServerId is required for MCP connections");
918
+ // Resolve MCP auth config in the scope the connection will RUN under:
919
+ // an agent-scoped connection may rely on that agent's scoped secrets,
920
+ // which the (lead) caller's own context cannot see.
921
+ const discoveryContext =
922
+ scope === "agent" && scopeId
923
+ ? { agentId: scopeId }
924
+ : scope === "repo" && scopeId
925
+ ? { agentId: data.agentId, repoId: scopeId }
926
+ : { agentId: data.agentId };
927
+ const tools = await listMcpServerTools(data.mcpServerId, discoveryContext);
928
+ const artifacts = buildMcpGeneratedArtifacts({
929
+ slug: data.slug,
930
+ connectionId,
931
+ tools,
932
+ });
933
+ generatedTypes = artifacts.generatedTypes;
934
+ generatedRuntimeJson = artifacts.generatedRuntimeJson;
935
+ generatedAt = now;
936
+ } catch (err) {
937
+ generationError = err instanceof Error ? err.message : String(err);
938
+ }
939
+ }
940
+
941
+ if (data.kind === "graphql") {
942
+ if (!data.baseUrl) throw new Error("baseUrl is required for GraphQL connections");
943
+ if (!data.allowedHosts?.length) {
944
+ throw new Error("allowedHosts is required for GraphQL connections");
945
+ }
946
+ new URL(data.baseUrl);
947
+ const binding = data.credentialBindingId
948
+ ? getCredentialBindingById(data.credentialBindingId)
949
+ : null;
950
+ const artifacts = buildGraphqlGeneratedArtifacts({
951
+ slug: data.slug,
952
+ baseUrl: data.baseUrl,
953
+ credentialBinding: binding,
954
+ });
955
+ generatedTypes = artifacts.generatedTypes;
956
+ generatedRuntimeJson = artifacts.generatedRuntimeJson;
957
+ generatedAt = now;
958
+ }
650
959
 
651
960
  const params = [
652
- normalizeSlug(data.slug),
961
+ normalizedSlug,
653
962
  data.displayName ?? null,
654
963
  data.kind,
655
964
  scope,
@@ -657,9 +966,11 @@ export function upsertScriptConnection(data: {
657
966
  data.baseUrl ?? null,
658
967
  JSON.stringify(data.allowedHosts ?? []),
659
968
  data.credentialBindingId ?? null,
660
- data.openapiSpecSourceKind ?? (data.kind === "openapi" ? "inline" : null),
661
- data.openapiSpecSource ?? null,
662
- data.openapiSpecJson ?? null,
969
+ openapiSpecSourceKind,
970
+ openapiSpecSource,
971
+ openapiSpecJson,
972
+ openapiSpecEtag,
973
+ openapiSpecFetchedAt,
663
974
  data.mcpServerId ?? null,
664
975
  generatedTypes,
665
976
  generatedRuntimeJson,
@@ -674,7 +985,8 @@ export function upsertScriptConnection(data: {
674
985
  `UPDATE script_connections SET
675
986
  slug = ?, display_name = ?, kind = ?, scope = ?, scope_id = ?, base_url = ?,
676
987
  allowed_hosts_json = ?, credential_binding_id = ?, openapi_spec_source_kind = ?,
677
- openapi_spec_source = ?, openapi_spec_json = ?, mcp_server_id = ?, generated_types = ?,
988
+ openapi_spec_source = ?, openapi_spec_json = ?, openapi_spec_etag = ?,
989
+ openapi_spec_fetched_at = ?, mcp_server_id = ?, generated_types = ?,
678
990
  generated_runtime_json = ?, generated_at = ?, generation_error = ?, enabled = ?,
679
991
  updated_at = ?, updated_by = ?, version = ?
680
992
  WHERE id = ? RETURNING *`,
@@ -692,15 +1004,92 @@ export function upsertScriptConnection(data: {
692
1004
  `INSERT INTO script_connections
693
1005
  (id, slug, display_name, kind, scope, scope_id, base_url, allowed_hosts_json,
694
1006
  credential_binding_id, openapi_spec_source_kind, openapi_spec_source, openapi_spec_json,
695
- mcp_server_id, generated_types, generated_runtime_json, generated_at, generation_error,
696
- enabled, created_at, updated_at, created_by, updated_by)
697
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
1007
+ openapi_spec_etag, openapi_spec_fetched_at, mcp_server_id, generated_types,
1008
+ generated_runtime_json, generated_at, generation_error, enabled, created_at, updated_at,
1009
+ created_by, updated_by)
1010
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
698
1011
  )
699
1012
  .get(id, ...params, now, now, data.userId ?? null, data.userId ?? null);
700
1013
  if (!row) throw new Error("Failed to create script connection");
701
1014
  return connectionFromRow(row);
702
1015
  }
703
1016
 
1017
+ export async function refreshScriptConnection(
1018
+ id: string,
1019
+ userId?: string | null,
1020
+ callerAgentId?: string,
1021
+ ): Promise<ScriptConnectionRecord | null> {
1022
+ const row = getDb()
1023
+ .prepare<ConnectionRow, [string]>("SELECT * FROM script_connections WHERE id = ?")
1024
+ .get(id);
1025
+ if (!row) return null;
1026
+ const connection = connectionFromRow(row);
1027
+
1028
+ // MCP refresh = re-run tool discovery (servers add/remove tools over time).
1029
+ // Route through the upsert path so discovery scoping, versioning, and
1030
+ // generation-error handling stay identical to registration.
1031
+ if (connection.kind === "mcp") {
1032
+ if (!connection.mcpServerId) {
1033
+ throw new Error("MCP connection has no mcpServerId to refresh from.");
1034
+ }
1035
+ return upsertScriptConnection({
1036
+ id: connection.id,
1037
+ slug: connection.slug,
1038
+ displayName: connection.displayName,
1039
+ kind: "mcp",
1040
+ scope: connection.scope,
1041
+ scopeId: connection.scopeId,
1042
+ allowedHosts: connection.allowedHosts,
1043
+ credentialBindingId: connection.credentialBindingId,
1044
+ mcpServerId: connection.mcpServerId,
1045
+ enabled: connection.enabled,
1046
+ agentId: callerAgentId,
1047
+ userId,
1048
+ });
1049
+ }
1050
+
1051
+ if (connection.kind !== "openapi") {
1052
+ throw new Error("Only OpenAPI and MCP script connections can be refreshed.");
1053
+ }
1054
+ if (connection.openapiSpecSourceKind !== "url" || !connection.openapiSpecSource) {
1055
+ throw new Error("Only OpenAPI script connections registered by URL can be refreshed.");
1056
+ }
1057
+
1058
+ const fetched = await fetchOpenapiSpec(connection.openapiSpecSource, {
1059
+ etag: connection.openapiSpecEtag,
1060
+ });
1061
+ if (fetched.status === "not_modified") {
1062
+ const refreshed = getDb()
1063
+ .prepare<ConnectionRow, [string, string]>(
1064
+ `UPDATE script_connections
1065
+ SET openapi_spec_fetched_at = ?
1066
+ WHERE id = ? RETURNING *`,
1067
+ )
1068
+ .get(fetched.fetchedAt, id);
1069
+ return refreshed ? connectionFromRow(refreshed) : null;
1070
+ }
1071
+
1072
+ return upsertScriptConnection({
1073
+ id: connection.id,
1074
+ slug: connection.slug,
1075
+ displayName: connection.displayName,
1076
+ kind: "openapi",
1077
+ scope: connection.scope,
1078
+ scopeId: connection.scopeId,
1079
+ baseUrl: connection.baseUrl,
1080
+ allowedHosts: connection.allowedHosts,
1081
+ credentialBindingId: connection.credentialBindingId,
1082
+ openapiSpecSourceKind: "url",
1083
+ openapiSpecSource: connection.openapiSpecSource,
1084
+ openapiSpecJson: fetched.specJson,
1085
+ openapiSpecEtag: fetched.etag,
1086
+ openapiSpecFetchedAt: fetched.fetchedAt,
1087
+ mcpServerId: connection.mcpServerId,
1088
+ enabled: connection.enabled,
1089
+ userId,
1090
+ });
1091
+ }
1092
+
704
1093
  export function setScriptConnectionEnabled(
705
1094
  id: string,
706
1095
  enabled: boolean,
@@ -719,7 +1108,8 @@ export function setScriptConnectionEnabled(
719
1108
  export function getScriptApiConnectionDescriptors(
720
1109
  context: { agentId?: string; repoId?: string } = {},
721
1110
  ): ScriptApiConnectionDescriptor[] {
722
- return listScriptConnections({ ...context, kind: "openapi" })
1111
+ return listScriptConnections(context)
1112
+ .filter((connection) => connection.kind === "openapi" || connection.kind === "graphql")
723
1113
  .map((connection) => {
724
1114
  if (!connection.generatedRuntimeJson) return null;
725
1115
  try {
@@ -731,8 +1121,25 @@ export function getScriptApiConnectionDescriptors(
731
1121
  .filter((descriptor): descriptor is ScriptApiConnectionDescriptor => Boolean(descriptor));
732
1122
  }
733
1123
 
1124
+ export function getScriptMcpConnectionDescriptors(
1125
+ context: { agentId?: string; repoId?: string } = {},
1126
+ ): ScriptMcpConnectionDescriptor[] {
1127
+ return listScriptConnections({ ...context, kind: "mcp" })
1128
+ .map((connection) => {
1129
+ if (!connection.generatedRuntimeJson) return null;
1130
+ try {
1131
+ return JSON.parse(connection.generatedRuntimeJson) as ScriptMcpConnectionDescriptor;
1132
+ } catch {
1133
+ return null;
1134
+ }
1135
+ })
1136
+ .filter((descriptor): descriptor is ScriptMcpConnectionDescriptor => Boolean(descriptor));
1137
+ }
1138
+
734
1139
  export function getScriptApiTypes(context: { agentId?: string; repoId?: string } = {}): string {
735
- const connections = listScriptConnections({ ...context, kind: "openapi" });
1140
+ const connections = listScriptConnections(context).filter(
1141
+ (connection) => connection.kind === "openapi" || connection.kind === "graphql",
1142
+ );
736
1143
  const blocks = connections
737
1144
  .filter((connection) => connection.generatedTypes)
738
1145
  .map((connection) => connection.generatedTypes as string);
@@ -742,3 +1149,15 @@ export function getScriptApiTypes(context: { agentId?: string; repoId?: string }
742
1149
  .map((connection) => ` ${connection.slug}: ${pascal(connection.slug)}Api;`);
743
1150
  return `${blocks.join("\n\n")}\n\nexport interface ScriptApiRegistry {\n${members.join("\n")}\n}\n`;
744
1151
  }
1152
+
1153
+ export function getScriptMcpTypes(context: { agentId?: string; repoId?: string } = {}): string {
1154
+ const connections = listScriptConnections({ ...context, kind: "mcp" });
1155
+ const blocks = connections
1156
+ .filter((connection) => connection.generatedTypes)
1157
+ .map((connection) => connection.generatedTypes as string);
1158
+ if (blocks.length === 0) return "export interface ScriptMcpRegistry {}\n";
1159
+ const members = connections
1160
+ .filter((connection) => connection.generatedTypes)
1161
+ .map((connection) => ` ${connection.slug}: ${pascal(connection.slug)}Mcp;`);
1162
+ return `${blocks.join("\n\n")}\n\nexport interface ScriptMcpRegistry {\n${members.join("\n")}\n}\n`;
1163
+ }