@wardby/cli 0.7.0 → 0.8.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 (304) hide show
  1. package/.env.example +5 -5
  2. package/README.md +6 -1
  3. package/deploy/local/docker-compose.quickstart-coding.yml +6 -0
  4. package/dist/claude-coding-worker/driver.d.ts +12 -0
  5. package/dist/claude-coding-worker/driver.js +29 -7
  6. package/dist/claude-coding-worker/main.js +5 -1
  7. package/dist/claude-coding-worker/sdk.js +17 -1
  8. package/dist/claude-plugin/command.d.ts +61 -0
  9. package/dist/claude-plugin/command.js +376 -0
  10. package/dist/cli-help.d.ts +1 -1
  11. package/dist/cli-help.js +2 -1
  12. package/dist/cli.js +47 -3
  13. package/dist/coding/code-host.d.ts +16 -0
  14. package/dist/coding/code-host.js +60 -0
  15. package/dist/coding/mcp-token.d.ts +12 -0
  16. package/dist/coding/mcp-token.js +26 -0
  17. package/dist/coding/protocol.js +2 -0
  18. package/dist/coding/redact.js +6 -1
  19. package/dist/coding/transcript/recorder.d.ts +5 -4
  20. package/dist/coding/transcript/recorder.js +1 -1
  21. package/dist/coding-worker/debug-trace.d.ts +3 -1
  22. package/dist/coding-worker/debug-trace.js +4 -2
  23. package/dist/coding-worker/driver.js +8 -1
  24. package/dist/coding-worker/main.js +5 -1
  25. package/dist/coding-worker/mcp-config.d.ts +27 -0
  26. package/dist/coding-worker/mcp-config.js +32 -0
  27. package/dist/coding-worker/sdk.d.ts +36 -0
  28. package/dist/coding-worker/sdk.js +28 -1
  29. package/dist/coding-worker/types.d.ts +4 -0
  30. package/dist/config/mcp-call-limit.d.ts +8 -0
  31. package/dist/config/mcp-call-limit.js +18 -0
  32. package/dist/config/mcp-servers.d.ts +8 -0
  33. package/dist/config/mcp-servers.js +12 -0
  34. package/dist/config/test-simulators.d.ts +10 -0
  35. package/dist/config/test-simulators.js +28 -0
  36. package/dist/config/tool-timeout.d.ts +1 -0
  37. package/dist/config/tool-timeout.js +18 -0
  38. package/dist/core/coding-mcp.d.ts +36 -0
  39. package/dist/core/coding-mcp.js +253 -0
  40. package/dist/core/coding-repository.d.ts +46 -0
  41. package/dist/core/coding-repository.js +92 -0
  42. package/dist/core/delegation-slots.d.ts +27 -0
  43. package/dist/core/delegation-slots.js +44 -0
  44. package/dist/core/dispatch.d.ts +6 -2
  45. package/dist/core/dispatch.js +36 -11
  46. package/dist/core/error-code.js +1 -0
  47. package/dist/core/grants.d.ts +28 -3
  48. package/dist/core/grants.js +46 -4
  49. package/dist/core/host-events.js +7 -3
  50. package/dist/core/host-status.js +2 -2
  51. package/dist/core/issue-status.js +4 -3
  52. package/dist/core/mark-ready.d.ts +1 -1
  53. package/dist/core/mark-ready.js +1 -1
  54. package/dist/core/mcp-client/client.d.ts +47 -0
  55. package/dist/core/mcp-client/client.js +141 -0
  56. package/dist/core/mcp-client/jsonrpc.d.ts +15 -0
  57. package/dist/core/mcp-client/jsonrpc.js +54 -0
  58. package/dist/core/mcp-client/results.d.ts +13 -0
  59. package/dist/core/mcp-client/results.js +42 -0
  60. package/dist/core/mcp-servers.d.ts +90 -0
  61. package/dist/core/mcp-servers.js +230 -0
  62. package/dist/core/mcp-tools.d.ts +108 -0
  63. package/dist/core/mcp-tools.js +287 -0
  64. package/dist/core/merge-order-check.js +38 -19
  65. package/dist/core/notification-dispatcher.js +2 -1
  66. package/dist/core/related-pull-requests.js +26 -10
  67. package/dist/core/review-fix.js +2 -2
  68. package/dist/core/review-host-tools.js +3 -1
  69. package/dist/core/runner.d.ts +66 -16
  70. package/dist/core/runner.js +281 -73
  71. package/dist/core/secret-broker-config.d.ts +2 -0
  72. package/dist/core/secret-broker-config.js +1 -1
  73. package/dist/core/secret-guard.d.ts +5 -2
  74. package/dist/core/secret-guard.js +22 -7
  75. package/dist/core/secrets.d.ts +11 -1
  76. package/dist/core/secrets.js +27 -1
  77. package/dist/core/serializable-retry.d.ts +18 -0
  78. package/dist/core/serializable-retry.js +43 -0
  79. package/dist/core/tool-admin.d.ts +5 -1
  80. package/dist/core/tool-admin.js +7 -2
  81. package/dist/core/tool-names.d.ts +13 -0
  82. package/dist/core/tool-names.js +20 -0
  83. package/dist/core/webhooks.d.ts +4 -0
  84. package/dist/core/webhooks.js +68 -49
  85. package/dist/core/workflow-run-events.js +2 -1
  86. package/dist/generated/prisma/browser.d.ts +31 -0
  87. package/dist/generated/prisma/client.d.ts +31 -0
  88. package/dist/generated/prisma/commonInputTypes.d.ts +50 -50
  89. package/dist/generated/prisma/internal/class.d.ts +66 -0
  90. package/dist/generated/prisma/internal/class.js +4 -4
  91. package/dist/generated/prisma/internal/prismaNamespace.d.ts +530 -1
  92. package/dist/generated/prisma/internal/prismaNamespace.js +75 -2
  93. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +79 -0
  94. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +75 -2
  95. package/dist/generated/prisma/models/Agent.d.ts +271 -0
  96. package/dist/generated/prisma/models/AgentMcpServer.d.ts +1412 -0
  97. package/dist/generated/prisma/models/AgentMcpServer.js +1 -0
  98. package/dist/generated/prisma/models/AgentRepository.d.ts +40 -1
  99. package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
  100. package/dist/generated/prisma/models/CodingAgentProfile.d.ts +0 -7
  101. package/dist/generated/prisma/models/CodingProxySession.d.ts +119 -2
  102. package/dist/generated/prisma/models/CodingRun.d.ts +76 -1
  103. package/dist/generated/prisma/models/HostProvider.d.ts +2176 -0
  104. package/dist/generated/prisma/models/HostProvider.js +1 -0
  105. package/dist/generated/prisma/models/HostProviderCredential.d.ts +1285 -0
  106. package/dist/generated/prisma/models/HostProviderCredential.js +1 -0
  107. package/dist/generated/prisma/models/HostProviderOperation.d.ts +1470 -0
  108. package/dist/generated/prisma/models/HostProviderOperation.js +1 -0
  109. package/dist/generated/prisma/models/HostProviderTrustedAccount.d.ts +1145 -0
  110. package/dist/generated/prisma/models/HostProviderTrustedAccount.js +1 -0
  111. package/dist/generated/prisma/models/McpServer.d.ts +1500 -0
  112. package/dist/generated/prisma/models/McpServer.js +1 -0
  113. package/dist/generated/prisma/models/NativeGatewayCall.d.ts +39 -1
  114. package/dist/generated/prisma/models/Principal.d.ts +167 -0
  115. package/dist/generated/prisma/models/ResourceGrant.d.ts +1 -1
  116. package/dist/generated/prisma/models/Secret.d.ts +387 -0
  117. package/dist/generated/prisma/models/Tool.d.ts +223 -1
  118. package/dist/generated/prisma/models.d.ts +6 -0
  119. package/dist/help/search.js +12 -3
  120. package/dist/help-index.json +719 -47
  121. package/dist/import/create.js +16 -1
  122. package/dist/import/neutral-schema.d.ts +1 -0
  123. package/dist/import/neutral-schema.js +2 -0
  124. package/dist/mcp/auth/grants-cli.d.ts +6 -4
  125. package/dist/mcp/auth/grants-cli.js +42 -3
  126. package/dist/mcp/auth/resource-server.d.ts +3 -1
  127. package/dist/mcp/auth/resource-server.js +17 -2
  128. package/dist/mcp/context.d.ts +3 -0
  129. package/dist/mcp/index.js +11 -0
  130. package/dist/mcp/tools/agents.js +100 -15
  131. package/dist/mcp/tools/grants.js +66 -28
  132. package/dist/mcp/tools/host-providers.d.ts +2 -0
  133. package/dist/mcp/tools/host-providers.js +215 -0
  134. package/dist/mcp/tools/mcp-servers.d.ts +26 -0
  135. package/dist/mcp/tools/mcp-servers.js +481 -0
  136. package/dist/mcp/tools/repositories.js +50 -10
  137. package/dist/mcp/tools/runs.js +178 -18
  138. package/dist/mcp/tools/secret-elicitation.d.ts +2 -0
  139. package/dist/mcp/tools/secret-elicitation.js +25 -2
  140. package/dist/mcp/tools/secrets.js +38 -4
  141. package/dist/mcp/tools/tools.d.ts +6 -4
  142. package/dist/mcp/tools/tools.js +40 -8
  143. package/dist/mcp/tools/trigger.js +28 -6
  144. package/dist/mcp/transport/http-limits.d.ts +2 -0
  145. package/dist/mcp/transport/http-limits.js +6 -2
  146. package/dist/mcp/webhooks/ingress.js +10 -0
  147. package/dist/native-worker/gateway-server.d.ts +14 -0
  148. package/dist/native-worker/gateway-server.js +28 -7
  149. package/dist/native-worker/gateway.d.ts +2 -0
  150. package/dist/native-worker/gateway.js +5 -1
  151. package/dist/native-worker/http-server.d.ts +4 -0
  152. package/dist/native-worker/http-server.js +51 -0
  153. package/dist/native-worker/ledger.d.ts +6 -2
  154. package/dist/native-worker/ledger.js +5 -4
  155. package/dist/native-worker/protocol.d.ts +1 -0
  156. package/dist/native-worker/protocol.js +4 -1
  157. package/dist/providers/coding-proxy/capability-hash.d.ts +2 -0
  158. package/dist/providers/coding-proxy/capability-hash.js +5 -0
  159. package/dist/providers/coding-proxy/mcp-route.d.ts +34 -0
  160. package/dist/providers/coding-proxy/mcp-route.js +166 -0
  161. package/dist/providers/coding-proxy/memory-ledger.d.ts +2 -0
  162. package/dist/providers/coding-proxy/memory-ledger.js +15 -0
  163. package/dist/providers/coding-proxy/prisma-ledger.d.ts +1 -0
  164. package/dist/providers/coding-proxy/prisma-ledger.js +30 -2
  165. package/dist/providers/coding-proxy/proxy.d.ts +7 -1
  166. package/dist/providers/coding-proxy/proxy.js +134 -34
  167. package/dist/providers/coding-proxy/runtime.d.ts +6 -0
  168. package/dist/providers/coding-proxy/runtime.js +32 -1
  169. package/dist/providers/coding-proxy/server.d.ts +3 -0
  170. package/dist/providers/coding-proxy/server.js +10 -0
  171. package/dist/providers/coding-proxy/types.d.ts +13 -0
  172. package/dist/providers/executor/composition.js +5 -0
  173. package/dist/providers/executor/container.d.ts +33 -1
  174. package/dist/providers/executor/container.js +76 -7
  175. package/dist/providers/host-tools/invoke.d.ts +37 -0
  176. package/dist/providers/host-tools/invoke.js +117 -0
  177. package/dist/providers/host-tools/operations.d.ts +65 -0
  178. package/dist/providers/host-tools/operations.js +149 -0
  179. package/dist/providers/host-tools/registry.d.ts +79 -0
  180. package/dist/providers/host-tools/registry.js +362 -0
  181. package/dist/providers/host-tools/request-guard.d.ts +17 -0
  182. package/dist/providers/host-tools/request-guard.js +99 -0
  183. package/dist/providers/host-tools/self-test.d.ts +64 -0
  184. package/dist/providers/host-tools/self-test.js +142 -0
  185. package/dist/providers/jobs/docker-isolation.d.ts +2 -2
  186. package/dist/providers/jobs/docker-isolation.js +30 -8
  187. package/dist/providers/jobs/docker.d.ts +8 -0
  188. package/dist/providers/jobs/docker.js +33 -9
  189. package/dist/providers/jobs/kubernetes-isolation.d.ts +3 -1
  190. package/dist/providers/jobs/kubernetes-isolation.js +15 -2
  191. package/dist/providers/jobs/kubernetes.d.ts +5 -0
  192. package/dist/providers/jobs/kubernetes.js +6 -1
  193. package/dist/providers/jobs/types.d.ts +6 -0
  194. package/dist/providers/review-host/types.d.ts +6 -4
  195. package/dist/providers/review-host/types.js +1 -1
  196. package/dist/providers/vcs/git.d.ts +6 -2
  197. package/dist/providers/vcs/git.js +15 -4
  198. package/dist/providers/vcs/github.d.ts +2 -71
  199. package/dist/providers/vcs/github.js +8 -11
  200. package/dist/providers/vcs/index.d.ts +1 -1
  201. package/dist/providers/vcs/index.js +1 -1
  202. package/dist/providers/vcs/remote.d.ts +4 -0
  203. package/dist/providers/vcs/remote.js +7 -1
  204. package/dist/providers/vcs/routing.d.ts +12 -6
  205. package/dist/providers/vcs/routing.js +21 -6
  206. package/dist/providers/vcs/types.d.ts +71 -3
  207. package/dist/quickstart/index.d.ts +5 -0
  208. package/dist/quickstart/index.js +21 -0
  209. package/dist/quickstart-images.json +1 -1
  210. package/dist/sandbox/eval-core.d.ts +12 -2
  211. package/dist/sandbox/eval-core.js +20 -4
  212. package/dist/sandbox/host-functions.d.ts +4 -0
  213. package/dist/sandbox/host-functions.js +7 -2
  214. package/dist/sandbox/limits.d.ts +4 -1
  215. package/dist/sandbox/limits.js +4 -1
  216. package/dist/sandbox/run-in-sandbox.d.ts +6 -0
  217. package/dist/sandbox/run-in-sandbox.js +14 -2
  218. package/dist/sandbox/safe-fetch.d.ts +2 -0
  219. package/dist/sandbox/safe-fetch.js +1 -1
  220. package/dist/sandbox/secret-broker.d.ts +1 -0
  221. package/dist/sandbox/secret-broker.js +1 -1
  222. package/dist/sandbox/tool-timeout.d.ts +32 -0
  223. package/dist/sandbox/tool-timeout.js +52 -0
  224. package/dist/sandbox/user-tool.d.ts +8 -1
  225. package/dist/sandbox/user-tool.js +9 -3
  226. package/dist/viewer/graph.js +2 -1
  227. package/dist/wardby-bin.js +5 -0
  228. package/docs/README.md +11 -1
  229. package/docs/agent-recipes.md +37 -4
  230. package/docs/architecture-runtime.md +1 -1
  231. package/docs/claude-code-plugin.md +164 -0
  232. package/docs/code-review-agents.md +6 -0
  233. package/docs/coding-agent-setup.md +77 -1
  234. package/docs/coding-worker-byo-images.md +4 -2
  235. package/docs/coding-worker-isolation.md +78 -7
  236. package/docs/debugging-agent-runs.md +242 -0
  237. package/docs/getting-started-gke.md +5 -4
  238. package/docs/getting-started-identity-provider.md +43 -37
  239. package/docs/getting-started.md +8 -0
  240. package/docs/host-providers.md +294 -0
  241. package/docs/mcp-servers.md +254 -0
  242. package/docs/native-sandbox.md +25 -9
  243. package/docs/security-deployment.md +110 -31
  244. package/docs/tool-secrets.md +3 -0
  245. package/docs/viewer-api.md +6 -0
  246. package/docs/viewer-api.openapi.yml +1 -1
  247. package/help/admin-viewer.md +4 -0
  248. package/help/agent-recipes.md +33 -2
  249. package/help/brokered-secrets.md +3 -0
  250. package/help/build-claude-code-image.md +1 -1
  251. package/help/claude-code-run-pane.md +40 -0
  252. package/help/errors/coding-mcp-pins-unavailable.md +35 -0
  253. package/help/errors/database-auth-invalid.md +1 -1
  254. package/help/errors/host-provider-error.md +40 -0
  255. package/help/errors/host-tool-failed.md +36 -0
  256. package/help/errors/mcp-call-limit.md +22 -0
  257. package/help/errors/mcp-call-timeout.md +22 -0
  258. package/help/errors/mcp-server-auth-failed.md +22 -0
  259. package/help/errors/mcp-server-credentials-unavailable.md +45 -0
  260. package/help/errors/mcp-server-error.md +28 -0
  261. package/help/errors/mcp-server-unreachable.md +22 -0
  262. package/help/errors/mcp-servers-off.md +21 -0
  263. package/help/errors/mcp-tool-not-allowed.md +26 -0
  264. package/help/errors/mcp-tool-removed.md +22 -0
  265. package/help/errors/mcp-unavailable.md +25 -0
  266. package/help/errors/native-sandbox-capacity.md +1 -1
  267. package/help/errors/native-sandbox-deadline-exceeded.md +1 -1
  268. package/help/errors/native-sandbox-docker-failed.md +1 -1
  269. package/help/errors/native-sandbox-gateway-error.md +1 -1
  270. package/help/errors/native-sandbox-gateway-unavailable.md +1 -1
  271. package/help/errors/native-sandbox-image-not-pinned.md +1 -1
  272. package/help/errors/native-sandbox-isolation-mismatch.md +1 -1
  273. package/help/errors/native-sandbox-network-unenforced.md +1 -1
  274. package/help/errors/native-sandbox-requires-catalog.md +1 -1
  275. package/help/errors/native-sandbox-unavailable.md +1 -1
  276. package/help/errors/native-sandbox-warm-delivery-failed.md +1 -1
  277. package/help/errors/native-sandbox-worker-exited.md +1 -1
  278. package/help/errors/native-sandbox-worker-lost.md +1 -1
  279. package/help/errors/native-sandbox-worker-unready.md +1 -1
  280. package/help/errors/repository-not-listed.md +36 -0
  281. package/help/errors/repository-required.md +36 -0
  282. package/help/errors/tool-timeout.md +38 -0
  283. package/help/host-providers.md +40 -0
  284. package/help/identity-and-access.md +4 -1
  285. package/help/mcp-servers.md +73 -0
  286. package/help/mcp.md +7 -2
  287. package/help/native-capabilities.md +27 -1
  288. package/help/native-sandbox.md +1 -1
  289. package/help/operating-agents.md +16 -0
  290. package/help/related-pull-requests.md +7 -3
  291. package/help/repo-instructions-and-skills.md +3 -1
  292. package/help/resume-runs-locally.md +3 -0
  293. package/help/stop-run.md +1 -1
  294. package/help/troubleshooting/coding-workers.md +9 -1
  295. package/help/troubleshooting/debugging-agent-runs.md +29 -0
  296. package/package.json +2 -1
  297. package/prisma/migrations/20261102000000_host_providers/migration.sql +56 -0
  298. package/prisma/migrations/20261104000000_coding_agent_repository_list/migration.sql +5 -0
  299. package/prisma/migrations/20261106000000_tool_timeout/migration.sql +4 -0
  300. package/prisma/migrations/20261107000000_mcp_servers/migration.sql +33 -0
  301. package/prisma/migrations/20261108000000_coding_mcp/migration.sql +5 -0
  302. package/prisma/migrations/20261109000000_task_run_index/migration.sql +3 -0
  303. package/prisma/schema.prisma +159 -14
  304. package/scripts/git-askpass.sh +1 -1
@@ -14,6 +14,7 @@ export declare const LEVELS: {
14
14
  readonly agent: readonly ["read", "execute", "write"];
15
15
  readonly tool: readonly ["read", "use", "write"];
16
16
  readonly budget_group: readonly ["read", "use", "write"];
17
+ readonly mcp_server: readonly ["read", "use", "write"];
17
18
  };
18
19
  export type ResourceType = keyof typeof LEVELS;
19
20
  export type Level<T extends ResourceType = ResourceType> = (typeof LEVELS)[T][number];
@@ -29,8 +30,9 @@ export declare const EVERYONE_MAX: {
29
30
  readonly agent: "write";
30
31
  readonly tool: "use";
31
32
  readonly budget_group: "use";
33
+ readonly mcp_server: "use";
32
34
  };
33
- /** Types grant_access/revoke_access accept today (Phase 1: agents only). */
35
+ /** Types grant_access/revoke_access accept today: agents and MCP servers. */
34
36
  export declare const SHAREABLE_TYPES: readonly ResourceType[];
35
37
  export declare const EVERYONE_KEY = "everyone";
36
38
  export declare function principalGranteeKey(principalId: string): string;
@@ -74,7 +76,7 @@ export declare function canDelegate(db: GrantDb, parent: {
74
76
  }): Promise<boolean>;
75
77
  /** The create-data for an everyone grant (migration, import, CLI --public). */
76
78
  export declare function everyoneGrantData(type: ResourceType, resourceId: string, level: string, source: string): {
77
- resourceType: "tool" | "agent" | "budget_group";
79
+ resourceType: "tool" | "agent" | "budget_group" | "mcp_server";
78
80
  resourceId: string;
79
81
  granteeKind: string;
80
82
  granteePrincipalId: null;
@@ -82,9 +84,32 @@ export declare function everyoneGrantData(type: ResourceType, resourceId: string
82
84
  level: string;
83
85
  source: string;
84
86
  };
87
+ /**
88
+ * True when `ownerId` owns the MCP server or holds use (or higher) on it through a grant. An agent's MCP
89
+ * attachment is legitimate only while its owner can use the server: runs check it on load and on every
90
+ * call, and the cleanup paths (make_owner, adopt-public, prune-bindings, revoke_access and grant_access
91
+ * lowering a level) drop the attachments that fail it.
92
+ */
93
+ export declare function ownerCanUseMcpServer(db: GrantDb, ownerId: string | null, server: {
94
+ id: string;
95
+ ownerId: string | null;
96
+ }): Promise<boolean>;
97
+ /** Keeps the items whose server the owner cannot use (see ownerCanUseMcpServer). */
98
+ export declare function filterUnusableMcpServers<T extends {
99
+ server: {
100
+ id: string;
101
+ ownerId: string | null;
102
+ };
103
+ }>(db: GrantDb, ownerOf: (item: T) => string | null, items: T[]): Promise<T[]>;
104
+ /**
105
+ * After a grant on an MCP server is revoked or lowered: deletes the server's attachments whose agent owner
106
+ * can no longer use it (see ownerCanUseMcpServer), in the caller's transaction. The server owner's own
107
+ * agents are never touched. Returns how many were removed.
108
+ */
109
+ export declare function pruneUnusableMcpAttachments(db: GrantDb & Pick<PrismaClient, "agentMcpServer">, serverId: string): Promise<number>;
85
110
  /** Every resource delete path calls this in the same transaction (no FK on resourceId). */
86
111
  export declare function deleteGrantsFor(db: GrantDb, type: ResourceType, resourceId: string): Promise<number>;
87
- type BindingDb = Pick<PrismaClient, "agentSecret" | "agentDatastore" | "agentRepository" | "agentSubAgent" | "agentTool" | "codingAgentProfile">;
112
+ type BindingDb = Pick<PrismaClient, "agentSecret" | "agentDatastore" | "agentRepository" | "agentSubAgent" | "agentTool" | "codingAgentProfile" | "agentMcpServer">;
88
113
  /**
89
114
  * The owner-only bindings an agent holds, by kind (empty = none). An agent
90
115
  * may be public-write only while this is empty; binding a secret, datastore,
@@ -2,6 +2,7 @@ export const LEVELS = {
2
2
  agent: ["read", "execute", "write"],
3
3
  tool: ["read", "use", "write"],
4
4
  budget_group: ["read", "use", "write"],
5
+ mcp_server: ["read", "use", "write"],
5
6
  };
6
7
  /**
7
8
  * Highest level an "everyone" grant may carry. Agents may be public-write
@@ -10,9 +11,9 @@ export const LEVELS = {
10
11
  * the binding tools. Otherwise anyone could rewrite the prompt of an agent
11
12
  * that holds its owner's secrets.
12
13
  */
13
- export const EVERYONE_MAX = { agent: "write", tool: "use", budget_group: "use" };
14
- /** Types grant_access/revoke_access accept today (Phase 1: agents only). */
15
- export const SHAREABLE_TYPES = ["agent"];
14
+ export const EVERYONE_MAX = { agent: "write", tool: "use", budget_group: "use", mcp_server: "use" };
15
+ /** Types grant_access/revoke_access accept today: agents and MCP servers. */
16
+ export const SHAREABLE_TYPES = ["agent", "mcp_server"];
16
17
  export const EVERYONE_KEY = "everyone";
17
18
  export function principalGranteeKey(principalId) {
18
19
  return `principal:${principalId}`;
@@ -119,6 +120,44 @@ export function everyoneGrantData(type, resourceId, level, source) {
119
120
  source,
120
121
  };
121
122
  }
123
+ /**
124
+ * True when `ownerId` owns the MCP server or holds use (or higher) on it through a grant. An agent's MCP
125
+ * attachment is legitimate only while its owner can use the server: runs check it on load and on every
126
+ * call, and the cleanup paths (make_owner, adopt-public, prune-bindings, revoke_access and grant_access
127
+ * lowering a level) drop the attachments that fail it.
128
+ */
129
+ export async function ownerCanUseMcpServer(db, ownerId, server) {
130
+ if (ownerId === null)
131
+ return false;
132
+ return atLeast("mcp_server", await effectiveAccess(db, "mcp_server", server, ownerId), "use");
133
+ }
134
+ /** Keeps the items whose server the owner cannot use (see ownerCanUseMcpServer). */
135
+ export async function filterUnusableMcpServers(db, ownerOf, items) {
136
+ const unusable = [];
137
+ for (const item of items) {
138
+ if (!(await ownerCanUseMcpServer(db, ownerOf(item), item.server)))
139
+ unusable.push(item);
140
+ }
141
+ return unusable;
142
+ }
143
+ /**
144
+ * After a grant on an MCP server is revoked or lowered: deletes the server's attachments whose agent owner
145
+ * can no longer use it (see ownerCanUseMcpServer), in the caller's transaction. The server owner's own
146
+ * agents are never touched. Returns how many were removed.
147
+ */
148
+ export async function pruneUnusableMcpAttachments(db, serverId) {
149
+ const rows = await db.agentMcpServer.findMany({
150
+ where: { serverId },
151
+ include: { server: { select: { id: true, ownerId: true } }, agent: { select: { ownerId: true } } },
152
+ });
153
+ const unusable = await filterUnusableMcpServers(db, (row) => row.agent.ownerId, rows);
154
+ if (unusable.length === 0)
155
+ return 0;
156
+ const { count } = await db.agentMcpServer.deleteMany({
157
+ where: { serverId, agentId: { in: unusable.map((row) => row.agentId) } },
158
+ });
159
+ return count;
160
+ }
122
161
  /** Every resource delete path calls this in the same transaction (no FK on resourceId). */
123
162
  export async function deleteGrantsFor(db, type, resourceId) {
124
163
  const { count } = await db.resourceGrant.deleteMany({ where: { resourceType: type, resourceId } });
@@ -133,7 +172,7 @@ export async function deleteGrantsFor(db, type, resourceId) {
133
172
  * coding task.
134
173
  */
135
174
  export async function agentOwnerBindings(db, agentId) {
136
- const [secrets, datastores, repositories, subagents, capabilityTools, codingProfiles] = await Promise.all([
175
+ const [secrets, datastores, repositories, subagents, capabilityTools, codingProfiles, mcpServers] = await Promise.all([
137
176
  db.agentSecret.count({ where: { agentId } }),
138
177
  db.agentDatastore.count({ where: { agentId } }),
139
178
  db.agentRepository.count({ where: { agentId } }),
@@ -153,6 +192,8 @@ export async function agentOwnerBindings(db, agentId) {
153
192
  },
154
193
  }),
155
194
  db.codingAgentProfile.count({ where: { agentId } }),
195
+ // A server's brokered secrets act for its owner on every call.
196
+ db.agentMcpServer.count({ where: { agentId } }),
156
197
  ]);
157
198
  return Object.entries({
158
199
  secrets,
@@ -161,6 +202,7 @@ export async function agentOwnerBindings(db, agentId) {
161
202
  "sub-agents": subagents,
162
203
  "tool capabilities": capabilityTools,
163
204
  "a coding repository": codingProfiles,
205
+ "MCP servers": mcpServers,
164
206
  })
165
207
  .filter(([, count]) => count > 0)
166
208
  .map(([kind]) => kind);
@@ -6,6 +6,7 @@
6
6
  * docs/private/2026-09-25-code-review-host-design.md §6.3.
7
7
  */
8
8
  import { Prisma } from "#prisma";
9
+ import { codeProviderOf, pullRequestWebUrl } from "../coding/code-host.js";
9
10
  import { linkedPullRequestAttribution, openingRunAttribution, resolveWorkItem, RESPONSE_PATH_SNAPSHOT_BUDGET, } from "./attribution.js";
10
11
  import { headStoppedAtBudget, pushingRunInFlight } from "./budget-stopped-head.js";
11
12
  import { checkContinuation, dispatchRun } from "./dispatch.js";
@@ -290,7 +291,7 @@ export function continuationHint(prNumber, runId) {
290
291
  `continued instead of opening a new one.]`);
291
292
  }
292
293
  const HINT_RUN_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
293
- const HINT_REPOSITORY = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
294
+ const HINT_REPOSITORY = /^(?:[a-z][a-z0-9-]{1,30}:)?[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
294
295
  /** One hint per open sibling (stored rows only; links built, ids re-validated), then the shared guidance. */
295
296
  export function siblingContinuationHints(siblings) {
296
297
  const lines = siblings
@@ -299,8 +300,11 @@ export function siblingContinuationHints(siblings) {
299
300
  Number.isSafeInteger(s.number) &&
300
301
  s.number > 0)
301
302
  .slice(0, MAX_SIBLING_HINTS)
302
- .map((s) => `- ${s.repository}#${s.number} (https://github.com/${s.repository}/pull/${s.number}): to change it, ` +
303
- `delegate to that repository's coding agent and ${continuePriorRunPhrase(s.openedByRunId)}.`);
303
+ .map((s) => {
304
+ const url = pullRequestWebUrl(codeProviderOf(s.repository), s.repository, s.number);
305
+ return (`- ${s.repository}#${s.number}${url ? ` (${url})` : ""}: to change it, ` +
306
+ `delegate to that repository's coding agent and ${continuePriorRunPhrase(s.openedByRunId)}.`);
307
+ });
304
308
  if (lines.length === 0)
305
309
  return undefined;
306
310
  return [
@@ -1,7 +1,7 @@
1
1
  import { summarizeRegistryFetches, warningRefusals } from "../coding/registry/report.js";
2
2
  import { packageRefusalWording } from "../coding/registry/refusal-wording.js";
3
3
  import { isPackageAuditMode, packageAuditWording, summarizePackageAudit, } from "../coding/registry/audit-mode.js";
4
- import { CODING_CODE_PROVIDER } from "../coding/protocol.js";
4
+ import { codeProviderOf } from "../coding/code-host.js";
5
5
  import { storedServiceNames } from "../coding/services/catalog.js";
6
6
  import { SERVICE_UNREADY_CATEGORY, serviceRefusalSentence, serviceUnreadySentence, } from "../coding/services/wording.js";
7
7
  import { PROTECTED_PATH_CATEGORY, PROTECTED_PATH_HOST_LINE } from "../coding/protected-path-wording.js";
@@ -71,7 +71,7 @@ export function pullRequestOutcome(result) {
71
71
  outcome: r.outcome,
72
72
  repository: r.repository,
73
73
  pullRequestNumber: r.pullRequestNumber,
74
- codeProvider: CODING_CODE_PROVIDER,
74
+ codeProvider: codeProviderOf(r.repository),
75
75
  ...(typeof r.pullRequestUrl === "string" ? { pullRequestUrl: r.pullRequestUrl } : {}),
76
76
  ...failedCheckCount(r.tests),
77
77
  };
@@ -5,6 +5,7 @@
5
5
  * text. Best effort throughout: nothing here throws.
6
6
  */
7
7
  import { Prisma } from "#prisma";
8
+ import { pullRequestWebUrl } from "../coding/code-host.js";
8
9
  import { projectOf, } from "../providers/issue-tracker/types.js";
9
10
  import { collectRunOutcome, fullReplyComments, fullReplyOf, isBlockedByRefusals, JIRA_REPLY_MAX_CHARS, outcomeBody, REPLY_MAX_PARTS, runLine, workingBody, } from "./host-status.js";
10
11
  import { continuedForAnotherIssue, recordPullRequests } from "./issue-bridge.js";
@@ -15,9 +16,9 @@ const ORPHAN_GRACE_MS = 2 * 60 * 1000;
15
16
  const ORPHAN_MAX_AGE_MS = 24 * 60 * 60 * 1000;
16
17
  const ORPHAN_BATCH = 20;
17
18
  export function toJiraMarkdown(text) {
18
- return text
19
- .replace(/<sub>(.*?)<\/sub>/g, "_$1_")
20
- .replace(/\b([\w.-]+\/[\w.-]+)#(\d+)\b/g, (_m, repo, n) => `[${repo}#${n}](https://github.com/${repo}/pull/${n})`);
19
+ return text.replace(/<sub>(.*?)<\/sub>/g, "_$1_").replace(
20
+ // With a `<provider>:` prefix it is a hosted repository, not a GitHub one: left as written.
21
+ /(\b[a-z][a-z0-9-]{1,30}:)?\b([\w.-]+\/[\w.-]+)#(\d+)\b/g, (match, prefix, repo, n) => prefix ? match : `[${repo}#${n}](${pullRequestWebUrl("github", repo, Number(n))})`);
21
22
  }
22
23
  export function issueStatusRow(event, runId, visibilityRole) {
23
24
  return { runId, provider: "jira", issueKey: event.issueKey, visibilityRole };
@@ -29,6 +29,6 @@ export interface MarkReadyDeps {
29
29
  hosts: ReviewHostRegistry | undefined;
30
30
  log?: Logger;
31
31
  }
32
- export declare function maybeMarkReady(deps: MarkReadyDeps, repository: string, prNumber: number, provider?: string): Promise<MarkReadyOutcome>;
32
+ export declare function maybeMarkReady(deps: MarkReadyDeps, repository: string, prNumber: number, provider: string): Promise<MarkReadyOutcome>;
33
33
  /** After a reviewer run ends: evaluate the pull request it reviewed. Null when it reviewed none. */
34
34
  export declare function markReadyAfterRun(deps: MarkReadyDeps, runId: string): Promise<MarkReadyOutcome | null>;
@@ -13,7 +13,7 @@ const CODING_RUN_SELECT = {
13
13
  run: { select: { agentId: true, parentRunId: true, status: true } },
14
14
  };
15
15
  const passed = (c) => c !== undefined && c.status === "completed" && c.conclusion === "success";
16
- export async function maybeMarkReady(deps, repository, prNumber, provider = "github") {
16
+ export async function maybeMarkReady(deps, repository, prNumber, provider) {
17
17
  try {
18
18
  const host = deps.hosts?.[provider];
19
19
  if (!host?.pullRequestOrigin || !host.readOwnChecks || !host.readCi || !host.markReadyForReview)
@@ -0,0 +1,47 @@
1
+ export interface McpHttpRequest {
2
+ method: "POST" | "DELETE";
3
+ headers: Record<string, string>;
4
+ body?: string;
5
+ }
6
+ export interface McpHttpResponse {
7
+ status: number;
8
+ /** Header names are lower-case. */
9
+ headers: Record<string, string>;
10
+ body: string;
11
+ }
12
+ /** The transport: in production brokeredFetch + safeFetch; in tests a fake. Throws only for network-level failures. */
13
+ export type McpFetch = (request: McpHttpRequest, signal: AbortSignal) => Promise<McpHttpResponse>;
14
+ export interface McpToolEntry {
15
+ name: string;
16
+ description: string;
17
+ inputSchema: Record<string, unknown>;
18
+ annotations?: Record<string, unknown>;
19
+ }
20
+ export interface McpServerInfo {
21
+ name?: string;
22
+ version?: string;
23
+ protocolVersion: string;
24
+ }
25
+ export declare class McpClientError extends Error {
26
+ readonly code: "mcp_server_unreachable" | "mcp_server_auth_failed" | "mcp_server_error" | "mcp_call_timeout";
27
+ constructor(code: "mcp_server_unreachable" | "mcp_server_auth_failed" | "mcp_server_error" | "mcp_call_timeout", message: string);
28
+ }
29
+ export declare class McpHttpClient {
30
+ private readonly fetchImpl;
31
+ private readonly signal;
32
+ private nextId;
33
+ private sessionId;
34
+ private protocolVersion;
35
+ constructor(options: {
36
+ fetch: McpFetch;
37
+ signal: AbortSignal;
38
+ });
39
+ private headers;
40
+ /** One POST. Maps transport failures and HTTP errors to McpClientError; never puts a body or header value in a message. */
41
+ private post;
42
+ private request;
43
+ initialize(): Promise<McpServerInfo>;
44
+ listTools(): Promise<McpToolEntry[]>;
45
+ callTool(name: string, args: Record<string, unknown>): Promise<unknown>;
46
+ close(): Promise<void>;
47
+ }
@@ -0,0 +1,141 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { MCP_PROTOCOL_VERSION, findResponse, hasServerRequest } from "./jsonrpc.js";
3
+ /** The package version, sent as the client version (package.json sits three levels up from src/core/mcp-client and dist/core/mcp-client). */
4
+ const CLIENT_VERSION = JSON.parse(readFileSync(new URL("../../../package.json", import.meta.url), "utf8")).version;
5
+ const MAX_PAGES = 20;
6
+ const MAX_TOOLS = 500;
7
+ const MAX_ERROR_MESSAGE_CHARS = 500;
8
+ export class McpClientError extends Error {
9
+ code;
10
+ constructor(code, message) {
11
+ super(message);
12
+ this.code = code;
13
+ this.name = "McpClientError";
14
+ }
15
+ }
16
+ function isRecord(value) {
17
+ return value !== null && typeof value === "object" && !Array.isArray(value);
18
+ }
19
+ export class McpHttpClient {
20
+ fetchImpl;
21
+ signal;
22
+ nextId = 1;
23
+ sessionId;
24
+ protocolVersion;
25
+ constructor(options) {
26
+ this.fetchImpl = options.fetch;
27
+ this.signal = options.signal;
28
+ }
29
+ headers() {
30
+ const headers = {
31
+ Accept: "application/json, text/event-stream",
32
+ "Content-Type": "application/json",
33
+ };
34
+ if (this.sessionId !== undefined)
35
+ headers["Mcp-Session-Id"] = this.sessionId;
36
+ if (this.protocolVersion !== undefined)
37
+ headers["MCP-Protocol-Version"] = this.protocolVersion;
38
+ return headers;
39
+ }
40
+ /** One POST. Maps transport failures and HTTP errors to McpClientError; never puts a body or header value in a message. */
41
+ async post(payload) {
42
+ let response;
43
+ try {
44
+ response = await this.fetchImpl({ method: "POST", headers: this.headers(), body: JSON.stringify({ jsonrpc: "2.0", ...payload }) }, this.signal);
45
+ }
46
+ catch (error) {
47
+ const message = error instanceof Error ? error.message : "";
48
+ if (this.signal.aborted || message.startsWith("fetch_cancelled_or_timeout")) {
49
+ throw new McpClientError("mcp_call_timeout", "The MCP server call timed out or was cancelled.");
50
+ }
51
+ throw new McpClientError("mcp_server_unreachable", "The MCP server could not be reached.");
52
+ }
53
+ if (response.status === 401 || response.status === 403) {
54
+ throw new McpClientError("mcp_server_auth_failed", `The MCP server refused the credentials (HTTP ${response.status}).`);
55
+ }
56
+ if (response.status < 200 || response.status >= 300) {
57
+ throw new McpClientError("mcp_server_error", `The MCP server answered with HTTP ${response.status}.`);
58
+ }
59
+ return response;
60
+ }
61
+ async request(method, params) {
62
+ const id = this.nextId++;
63
+ const response = await this.post({ id, method, params });
64
+ const contentType = response.headers["content-type"];
65
+ const found = findResponse(response.body, contentType, id);
66
+ if (found === undefined) {
67
+ if (hasServerRequest(response.body, contentType)) {
68
+ throw new McpClientError("mcp_server_error", "The MCP server asked for something Wardby doesn't support (sampling, elicitation or roots).");
69
+ }
70
+ throw new McpClientError("mcp_server_unreachable", "The MCP server sent no response to the request.");
71
+ }
72
+ // Any error member, even a malformed one ("error": null, a string), fails the call: never a TypeError.
73
+ const replyError = found.error;
74
+ if (replyError !== undefined) {
75
+ const text = isRecord(replyError) && typeof replyError.message === "string" ? replyError.message : "unknown error";
76
+ throw new McpClientError("mcp_server_error", `The MCP server returned an error: ${text.slice(0, MAX_ERROR_MESSAGE_CHARS)}`);
77
+ }
78
+ return { result: found.result, response };
79
+ }
80
+ async initialize() {
81
+ const { result, response } = await this.request("initialize", {
82
+ protocolVersion: MCP_PROTOCOL_VERSION,
83
+ capabilities: {},
84
+ clientInfo: { name: "wardby", version: CLIENT_VERSION },
85
+ });
86
+ const sessionId = response.headers["mcp-session-id"];
87
+ if (sessionId !== undefined && sessionId !== "")
88
+ this.sessionId = sessionId;
89
+ const body = isRecord(result) ? result : {};
90
+ const protocolVersion = typeof body.protocolVersion === "string" ? body.protocolVersion : MCP_PROTOCOL_VERSION;
91
+ this.protocolVersion = protocolVersion;
92
+ await this.post({ method: "notifications/initialized" });
93
+ const serverInfo = isRecord(body.serverInfo) ? body.serverInfo : {};
94
+ const info = { protocolVersion };
95
+ if (typeof serverInfo.name === "string")
96
+ info.name = serverInfo.name;
97
+ if (typeof serverInfo.version === "string")
98
+ info.version = serverInfo.version;
99
+ return info;
100
+ }
101
+ async listTools() {
102
+ const tools = [];
103
+ let cursor;
104
+ for (let page = 0; page < MAX_PAGES; page++) {
105
+ const { result } = await this.request("tools/list", cursor === undefined ? {} : { cursor });
106
+ const body = isRecord(result) ? result : {};
107
+ for (const entry of Array.isArray(body.tools) ? body.tools : []) {
108
+ if (!isRecord(entry) || typeof entry.name !== "string" || !isRecord(entry.inputSchema))
109
+ continue;
110
+ const tool = {
111
+ name: entry.name,
112
+ description: typeof entry.description === "string" ? entry.description : "",
113
+ inputSchema: entry.inputSchema,
114
+ };
115
+ if (isRecord(entry.annotations))
116
+ tool.annotations = entry.annotations;
117
+ tools.push(tool);
118
+ }
119
+ if (tools.length >= MAX_TOOLS)
120
+ return tools.slice(0, MAX_TOOLS);
121
+ cursor = typeof body.nextCursor === "string" && body.nextCursor !== "" ? body.nextCursor : undefined;
122
+ if (cursor === undefined)
123
+ break;
124
+ }
125
+ return tools;
126
+ }
127
+ async callTool(name, args) {
128
+ const { result } = await this.request("tools/call", { name, arguments: args });
129
+ return result;
130
+ }
131
+ async close() {
132
+ if (this.sessionId === undefined)
133
+ return;
134
+ try {
135
+ await this.fetchImpl({ method: "DELETE", headers: this.headers() }, this.signal);
136
+ }
137
+ catch {
138
+ // Closing is best effort.
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,15 @@
1
+ /** JSON-RPC framing for MCP's Streamable HTTP transport: reads a JSON or SSE response body. */
2
+ export declare const MCP_PROTOCOL_VERSION = "2025-06-18";
3
+ export interface JsonRpcResponse {
4
+ id: number | string | null;
5
+ result?: unknown;
6
+ error?: {
7
+ code: number;
8
+ message: string;
9
+ data?: unknown;
10
+ };
11
+ }
12
+ /** Finds the response to `id` in a JSON body or an SSE body. Ignores notifications and server requests. */
13
+ export declare function findResponse(body: string, contentType: string | undefined, id: number): JsonRpcResponse | undefined;
14
+ /** True when the body carried a server-to-client request (has `method` and `id`): we answer none of them. */
15
+ export declare function hasServerRequest(body: string, contentType: string | undefined): boolean;
@@ -0,0 +1,54 @@
1
+ /** JSON-RPC framing for MCP's Streamable HTTP transport: reads a JSON or SSE response body. */
2
+ export const MCP_PROTOCOL_VERSION = "2025-06-18";
3
+ /** Every JSON-RPC message in a body: a JSON value (object or batch array) or the `data:` frames of an SSE stream. */
4
+ function messages(body, contentType) {
5
+ const parsed = [];
6
+ const isSse = (contentType ?? "").toLowerCase().includes("text/event-stream");
7
+ if (isSse) {
8
+ for (const frame of body.split(/\r?\n\r?\n/)) {
9
+ const data = frame
10
+ .split(/\r?\n/)
11
+ .filter((line) => line.startsWith("data:"))
12
+ .map((line) => {
13
+ const value = line.slice(5);
14
+ return value.startsWith(" ") ? value.slice(1) : value;
15
+ });
16
+ if (data.length === 0)
17
+ continue;
18
+ try {
19
+ parsed.push(JSON.parse(data.join("\n")));
20
+ }
21
+ catch {
22
+ // A malformed frame is skipped.
23
+ }
24
+ }
25
+ }
26
+ else {
27
+ try {
28
+ parsed.push(JSON.parse(body));
29
+ }
30
+ catch {
31
+ // Not JSON: no messages.
32
+ }
33
+ }
34
+ const out = [];
35
+ for (const value of parsed) {
36
+ for (const item of Array.isArray(value) ? value : [value]) {
37
+ if (item !== null && typeof item === "object" && !Array.isArray(item))
38
+ out.push(item);
39
+ }
40
+ }
41
+ return out;
42
+ }
43
+ /** Finds the response to `id` in a JSON body or an SSE body. Ignores notifications and server requests. */
44
+ export function findResponse(body, contentType, id) {
45
+ for (const message of messages(body, contentType)) {
46
+ if (message.id === id && !("method" in message))
47
+ return message;
48
+ }
49
+ return undefined;
50
+ }
51
+ /** True when the body carried a server-to-client request (has `method` and `id`): we answer none of them. */
52
+ export function hasServerRequest(body, contentType) {
53
+ return messages(body, contentType).some((message) => typeof message.method === "string" && message.id !== undefined && message.id !== null);
54
+ }
@@ -0,0 +1,13 @@
1
+ /** Turns an MCP CallToolResult into the string the model sees. */
2
+ export declare const MCP_RESULT_MAX_CHARS = 100000;
3
+ export interface ToolResultText {
4
+ /** The text the model sees: redacted, then capped at MCP_RESULT_MAX_CHARS. */
5
+ text: string;
6
+ /** True when the server reported an error or the result was unreadable. */
7
+ isError: boolean;
8
+ }
9
+ /**
10
+ * The model-facing text of a CallToolResult. `redact` runs on the assembled text before it is
11
+ * truncated, so a secret that straddles the cut is still matched whole.
12
+ */
13
+ export declare function toolResultText(result: unknown, redact?: (text: string) => string): ToolResultText;
@@ -0,0 +1,42 @@
1
+ /** Turns an MCP CallToolResult into the string the model sees. */
2
+ export const MCP_RESULT_MAX_CHARS = 100_000;
3
+ function errorText(message) {
4
+ return JSON.stringify({ error: "mcp_server_error", message });
5
+ }
6
+ function truncate(text) {
7
+ if (text.length <= MCP_RESULT_MAX_CHARS)
8
+ return text;
9
+ return `${text.slice(0, MCP_RESULT_MAX_CHARS)}\n[truncated: ${text.length - MCP_RESULT_MAX_CHARS} characters omitted]`;
10
+ }
11
+ /**
12
+ * The model-facing text of a CallToolResult. `redact` runs on the assembled text before it is
13
+ * truncated, so a secret that straddles the cut is still matched whole.
14
+ */
15
+ export function toolResultText(result, redact = (t) => t) {
16
+ if (result === null || typeof result !== "object" || Array.isArray(result)) {
17
+ return { text: errorText("The MCP server returned an unreadable result."), isError: true };
18
+ }
19
+ const r = result;
20
+ const parts = [];
21
+ let hasText = false;
22
+ for (const part of Array.isArray(r.content) ? r.content : []) {
23
+ if (part === null || typeof part !== "object")
24
+ continue;
25
+ const p = part;
26
+ if (p.type === "text" && typeof p.text === "string") {
27
+ hasText = true;
28
+ parts.push(p.text);
29
+ }
30
+ else if (typeof p.type === "string") {
31
+ parts.push(`[${p.type} omitted]`);
32
+ }
33
+ }
34
+ let text = parts.join("\n");
35
+ if (!hasText && r.structuredContent !== undefined && r.structuredContent !== null) {
36
+ text = JSON.stringify(r.structuredContent);
37
+ }
38
+ const safe = truncate(redact(text));
39
+ if (r.isError === true)
40
+ return { text: errorText(safe), isError: true };
41
+ return { text: safe, isError: false };
42
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Remote MCP servers: the owner-side domain. Validates a server's URL and name, loads the owner's
3
+ * brokered secrets for it, builds the McpFetch that sends them (brokeredFetch over safeFetch), takes
4
+ * the redacted tool snapshot, and diffs snapshots. Secret values never leave this file in any output:
5
+ * snapshots are redacted by exact value (every encoded form) plus token shapes before they are returned.
6
+ */
7
+ import type { PrismaClient } from "#prisma";
8
+ import { type McpFetch, type McpServerInfo, type McpToolEntry } from "./mcp-client/client.js";
9
+ import type { SecretsAccessor } from "./secrets.js";
10
+ import type { Logger } from "./logger.js";
11
+ import type { SecretCipher } from "../providers/secrets/types.js";
12
+ import { safeFetch } from "../sandbox/safe-fetch.js";
13
+ export declare class McpServerInputError extends Error {
14
+ constructor(message: string);
15
+ }
16
+ /** What the server's owner (or an admin) sees when a stored secret of the server can't be decrypted. */
17
+ export declare const CREDENTIALS_UNAVAILABLE_MESSAGE = "The MCP server's credentials can't be decrypted on this server. Check that every Wardby process (control plane, scheduler, native gateway) uses the same SECRET_APP_KEY, or set the secret again.";
18
+ /**
19
+ * A server's brokered secret exists but its stored value can't be decrypted (another
20
+ * SECRET_APP_KEY, a key-id mismatch, a corrupt ciphertext). Not an input problem: the message is
21
+ * fixed and never carries the cipher's error, the ciphertext or a value.
22
+ */
23
+ export declare class McpCredentialsUnavailableError extends Error {
24
+ /** Empty while a server is being registered (it has no id yet). */
25
+ readonly serverId: string;
26
+ readonly serverName: string;
27
+ readonly secretName: string;
28
+ readonly code = "mcp_server_credentials_unavailable";
29
+ constructor(
30
+ /** Empty while a server is being registered (it has no id yet). */
31
+ serverId: string, serverName: string, secretName: string);
32
+ }
33
+ /** The one operator line for an undecryptable server secret: ids and names only, never the cipher's error text. */
34
+ export declare function logCredentialsUnavailable(log: Pick<Logger, "warn">, error: McpCredentialsUnavailableError): void;
35
+ export interface McpServerRow {
36
+ id: string;
37
+ ownerId: string;
38
+ name: string;
39
+ url: string;
40
+ secretNames: string[];
41
+ }
42
+ /** Validates a server URL: https, no userinfo, no fragment, default port; returns the normalised string. */
43
+ export declare function normaliseServerUrl(raw: string): string;
44
+ export declare function assertServerName(name: string): void;
45
+ /** The tool name the model sees: `<server>__<tool>` with any other character mapped to `_`. */
46
+ export declare function exposedToolName(serverName: string, toolName: string): string;
47
+ /** A server's saved tool list (toolSnapshot); anything but an array reads as empty. */
48
+ export declare function snapshotOf(server: {
49
+ toolSnapshot: unknown;
50
+ }): McpToolEntry[];
51
+ /** Exposed names of allowed tools; a name too long to expose is left out (refused at attach anyway). */
52
+ export declare function exposedNames(serverName: string, allowedTools: readonly string[]): string[];
53
+ /** Added, removed and changed (description or schema) tool names between two snapshots; order-independent. */
54
+ export declare function diffSnapshots(before: McpToolEntry[], after: McpToolEntry[]): {
55
+ added: string[];
56
+ removed: string[];
57
+ changed: string[];
58
+ };
59
+ /** Redacts every secret value of this server (exact, every encoded and escaped form) plus token shapes. */
60
+ export declare function redactForServer(text: string, values: string[]): string;
61
+ /** The same error with its message redacted; an McpClientError keeps its code, any other keeps its name. */
62
+ export declare function redactError(error: unknown, values: string[]): Error;
63
+ type SecretDb = Pick<PrismaClient, "secret">;
64
+ /**
65
+ * Loads the owner's secrets for the server; each must exist, be owned by ownerId, be brokered, and list
66
+ * the URL's host (McpServerInputError otherwise). A stored value that can't be decrypted throws
67
+ * McpCredentialsUnavailableError.
68
+ */
69
+ export declare function loadServerSecrets(db: SecretDb, cipher: Pick<SecretCipher, "decrypt">, server: McpServerRow): Promise<{
70
+ accessor: SecretsAccessor;
71
+ values: string[];
72
+ }>;
73
+ /** The McpFetch for one server: brokeredFetch over safeFetch with the timeout and signal; text body, lower-case headers. */
74
+ export declare function serverFetch(server: McpServerRow, accessor: SecretsAccessor, options: {
75
+ timeoutMs: number;
76
+ fetchImpl?: typeof safeFetch;
77
+ }): McpFetch;
78
+ /** initialize + listTools + close; the result is redacted (as JSON text) before it is returned. */
79
+ export declare function fetchSnapshot(server: McpServerRow, deps: {
80
+ db: SecretDb;
81
+ cipher: Pick<SecretCipher, "decrypt">;
82
+ fetchImpl?: typeof safeFetch;
83
+ timeoutMs?: number;
84
+ }): Promise<{
85
+ serverInfo: McpServerInfo;
86
+ tools: McpToolEntry[];
87
+ }>;
88
+ /** For the secret delete guard: names of the owner's MCP servers whose secretNames include the secret. */
89
+ export declare function mcpServersUsingSecret(db: Pick<PrismaClient, "mcpServer">, ownerId: string, secretName: string): Promise<string[]>;
90
+ export {};