@zackbart/connecta 0.28.0 → 0.29.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 (320) hide show
  1. package/CHANGELOG.md +961 -5
  2. package/README.md +62 -39
  3. package/bin/connecta.mjs +110 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  12. package/dist/auth/clerk-sdk/client.js +5 -0
  13. package/dist/auth/clerk-sdk/generated.js +1845 -0
  14. package/dist/auth/clerk-transport.js +19 -0
  15. package/dist/auth/clerk.d.ts +17 -7
  16. package/dist/auth/clerk.js +189 -107
  17. package/dist/auth/cloudflare-access.js +25 -26
  18. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  19. package/dist/auth/downstream-client-metadata.js +133 -0
  20. package/dist/auth/downstream-oauth.js +1632 -1509
  21. package/dist/auth/downstream-revocation.js +106 -0
  22. package/dist/auth/oauth-token-response.js +231 -0
  23. package/dist/auth/oauth-v2-migration.js +116 -0
  24. package/dist/auth/static-oauth.js +526 -0
  25. package/dist/auth-elicitation.js +283 -0
  26. package/dist/branding.d.ts +8 -16
  27. package/dist/branding.js +26 -59
  28. package/dist/byte-read-response.js +39 -0
  29. package/dist/call-admission.js +12 -16
  30. package/dist/catalog-cache.js +739 -0
  31. package/dist/catalog-drift.d.ts +50 -15
  32. package/dist/catalog-drift.js +365 -128
  33. package/dist/catalog-fingerprint.js +2 -4
  34. package/dist/catalog-freshness.js +13 -0
  35. package/dist/catalog-limits.js +2 -2
  36. package/dist/catalog-service.js +368 -224
  37. package/dist/catalog.js +43 -84
  38. package/dist/concurrency.js +3 -4
  39. package/dist/config-defaults.d.ts +39 -0
  40. package/dist/config-defaults.js +46 -0
  41. package/dist/config-schema.d.ts +181 -0
  42. package/dist/config-schema.js +435 -0
  43. package/dist/config-value-sources.js +102 -0
  44. package/dist/config.d.ts +631 -0
  45. package/dist/config.js +503 -0
  46. package/dist/connector-access.d.ts +1 -0
  47. package/dist/connector-access.js +13 -15
  48. package/dist/connector-caller.d.ts +8 -0
  49. package/dist/connector-caller.js +15 -6
  50. package/dist/connector-scope.js +1 -1
  51. package/dist/connectors/api-connector.d.ts +220 -0
  52. package/dist/connectors/api-connector.js +214 -0
  53. package/dist/connectors/api.d.ts +8 -71
  54. package/dist/connectors/api.js +13 -99
  55. package/dist/connectors/guarded-fetch.d.ts +144 -0
  56. package/dist/connectors/guarded-fetch.js +108 -25
  57. package/dist/connectors/negotiation-cache.js +46 -0
  58. package/dist/connectors/option-shapes.js +27 -0
  59. package/dist/connectors/remote-mcp.d.ts +30 -4
  60. package/dist/connectors/remote-mcp.js +1780 -351
  61. package/dist/connectors/resource-uri.js +335 -0
  62. package/dist/credential-contract.d.ts +10 -0
  63. package/dist/credential-rules.js +1 -3
  64. package/dist/credential-url.js +108 -0
  65. package/dist/credentials.d.ts +6 -1
  66. package/dist/credentials.js +27 -18
  67. package/dist/d1.d.ts +130 -0
  68. package/dist/d1.js +290 -0
  69. package/dist/describe-config.d.ts +165 -0
  70. package/dist/describe-config.js +315 -0
  71. package/dist/described.js +106 -0
  72. package/dist/downstream-input-context.js +119 -0
  73. package/dist/downstream-input.js +323 -0
  74. package/dist/errors.d.ts +97 -20
  75. package/dist/errors.js +158 -80
  76. package/dist/execute.js +453 -305
  77. package/dist/executor-admission.d.ts +1 -0
  78. package/dist/executor-admission.js +11 -15
  79. package/dist/executor-contract.d.ts +20 -0
  80. package/dist/executor-contract.js +48 -0
  81. package/dist/executor-result.js +24 -9
  82. package/dist/executors/quickjs-child.js +3 -4
  83. package/dist/executors/quickjs-runtime.js +147 -59
  84. package/dist/executors/quickjs.js +29 -35
  85. package/dist/favicon.js +1 -2
  86. package/dist/fix-prompt.js +1 -3
  87. package/dist/guest-runtime.js +188 -0
  88. package/dist/guest-types.d.ts +176 -0
  89. package/dist/guest-types.js +1 -0
  90. package/dist/html-security.js +24 -0
  91. package/dist/identity.js +1 -3
  92. package/dist/inbound-credential.js +52 -0
  93. package/dist/index.d.ts +25 -257
  94. package/dist/index.js +65 -478
  95. package/dist/invocation-auth.js +69 -0
  96. package/dist/invocation.js +230 -73
  97. package/dist/json-schema.d.ts +1 -1
  98. package/dist/mcp-client-context.js +35 -0
  99. package/dist/mcp-result.js +11 -7
  100. package/dist/meta-output.js +76 -0
  101. package/dist/meta-tool-names.d.ts +2 -0
  102. package/dist/meta-tool-names.js +3 -0
  103. package/dist/meta-tools.js +285 -209
  104. package/dist/module-contracts.d.ts +8 -23
  105. package/dist/node.d.ts +0 -2
  106. package/dist/node.js +2 -2
  107. package/dist/oauth-callback-outcome.js +1 -3
  108. package/dist/oauth-handoff.js +117 -0
  109. package/dist/oauth-partition.js +17 -0
  110. package/dist/oauth-sealing.js +20 -0
  111. package/dist/operator-record.d.ts +137 -0
  112. package/dist/operator-record.js +420 -0
  113. package/dist/operator-ui/assets.js +31 -0
  114. package/dist/operator-ui/contract.d.ts +72 -0
  115. package/dist/operator-ui/contract.js +1 -0
  116. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  117. package/dist/operator-ui/generated.js +5 -4
  118. package/dist/operator-ui/model.d.ts +12 -87
  119. package/dist/operator-ui/model.js +2 -9
  120. package/dist/operator-ui/setup-commands.js +1 -3
  121. package/dist/operator-ui/view.d.ts +323 -0
  122. package/dist/operator-ui/view.js +62 -189
  123. package/dist/page-styles.js +2 -3
  124. package/dist/program-source.js +14 -2
  125. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  126. package/dist/provider.d.ts +142 -0
  127. package/dist/provider.js +187 -0
  128. package/dist/providers/_shared/google/result-size.js +57 -0
  129. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  130. package/dist/providers/_shared/google/service-account.js +500 -0
  131. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  132. package/dist/providers/_shared/google/workspace.js +518 -0
  133. package/dist/providers/basecamp/index.d.ts +43 -0
  134. package/dist/providers/basecamp/index.js +997 -0
  135. package/dist/providers/basecamp/skill.generated.js +9 -0
  136. package/dist/providers/breeze/index.d.ts +34 -0
  137. package/dist/providers/breeze/index.js +1446 -0
  138. package/dist/providers/breeze/skill.generated.js +9 -0
  139. package/dist/providers/ccb/index.d.ts +103 -0
  140. package/dist/providers/ccb/index.js +1725 -0
  141. package/dist/providers/ccb/skill.generated.js +9 -0
  142. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  143. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  144. package/dist/providers/cloudflare/skill.generated.js +10 -0
  145. package/dist/providers/docs/index.d.ts +54 -0
  146. package/dist/providers/docs/index.js +1237 -0
  147. package/dist/providers/docs/skill.generated.js +9 -0
  148. package/dist/providers/drive/index.d.ts +53 -0
  149. package/dist/providers/drive/index.js +1523 -0
  150. package/dist/providers/drive/skill.generated.js +9 -0
  151. package/dist/providers/forms/index.d.ts +54 -0
  152. package/dist/providers/forms/index.js +1217 -0
  153. package/dist/providers/forms/skill.generated.js +11 -0
  154. package/dist/providers/github/auth.d.ts +32 -0
  155. package/dist/providers/github/auth.js +361 -0
  156. package/dist/providers/github/hosted.js +218 -0
  157. package/dist/providers/github/index.d.ts +13 -0
  158. package/dist/providers/github/index.js +140 -0
  159. package/dist/providers/github/key.js +100 -0
  160. package/dist/providers/github/rest.js +321 -0
  161. package/dist/providers/github/scope.d.ts +29 -0
  162. package/dist/providers/github/scope.js +124 -0
  163. package/dist/providers/github/skill.generated.js +8 -0
  164. package/dist/providers/gmail/index.d.ts +51 -0
  165. package/dist/providers/gmail/index.js +1436 -0
  166. package/dist/providers/gmail/skill.generated.js +9 -0
  167. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  168. package/dist/providers/linear/index.js +352 -0
  169. package/dist/providers/linear/skill.generated.js +8 -0
  170. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  171. package/dist/providers/mixpanel/index.js +416 -0
  172. package/dist/providers/mixpanel/skill.generated.js +9 -0
  173. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  174. package/dist/providers/{notion.js → notion/index.js} +226 -239
  175. package/dist/providers/notion/skill.generated.js +10 -0
  176. package/dist/providers/overflow/index.d.ts +60 -0
  177. package/dist/providers/overflow/index.js +1535 -0
  178. package/dist/providers/overflow/skill.generated.js +8 -0
  179. package/dist/providers/planning-center/index.d.ts +59 -0
  180. package/dist/providers/planning-center/index.js +2199 -0
  181. package/dist/providers/planning-center/skill.generated.js +10 -0
  182. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  183. package/dist/providers/revenuecat/index.js +557 -0
  184. package/dist/providers/revenuecat/skill.generated.js +11 -0
  185. package/dist/providers/sheets/index.d.ts +54 -0
  186. package/dist/providers/sheets/index.js +1237 -0
  187. package/dist/providers/sheets/skill.generated.js +10 -0
  188. package/dist/providers/slides/index.d.ts +53 -0
  189. package/dist/providers/slides/index.js +2645 -0
  190. package/dist/providers/slides/skill.generated.js +9 -0
  191. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  192. package/dist/providers/stripe/index.js +266 -0
  193. package/dist/providers/stripe/skill.generated.js +11 -0
  194. package/dist/providers/tithely/index.d.ts +49 -0
  195. package/dist/providers/tithely/index.js +1003 -0
  196. package/dist/providers/tithely/skill.generated.js +8 -0
  197. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  198. package/dist/providers/vercel/index.js +1038 -0
  199. package/dist/providers/vercel/mcp-ownership.js +212 -0
  200. package/dist/providers/vercel/skill.generated.js +11 -0
  201. package/dist/registry.d.ts +108 -171
  202. package/dist/registry.js +513 -925
  203. package/dist/request-state.js +21 -0
  204. package/dist/resource-read.js +100 -0
  205. package/dist/result-shapes.js +17 -49
  206. package/dist/retired-toolkits.js +1 -1
  207. package/dist/routes/access-tokens.js +3 -6
  208. package/dist/routes/activity-context.js +21 -0
  209. package/dist/routes/activity-disclosure.js +40 -0
  210. package/dist/routes/activity.js +59 -21
  211. package/dist/routes/actor-labels.js +3 -50
  212. package/dist/routes/connect.js +221 -0
  213. package/dist/routes/credentials.js +15 -27
  214. package/dist/routes/mcp.js +356 -130
  215. package/dist/routes/oauth-client-metadata.js +15 -0
  216. package/dist/routes/oauth-management.js +45 -107
  217. package/dist/routes/oauth.js +136 -57
  218. package/dist/routes/operator.js +3 -3
  219. package/dist/routes/shared.d.ts +25 -64
  220. package/dist/routes/shared.js +160 -80
  221. package/dist/routes/skills.js +30 -0
  222. package/dist/routes/ui-config.js +225 -0
  223. package/dist/routes/ui-data.js +40 -35
  224. package/dist/routes/ui.js +66 -56
  225. package/dist/runtime/admission.js +4 -2
  226. package/dist/runtime/call-admission.js +3 -1
  227. package/dist/runtime/connector-scope.js +1 -4
  228. package/dist/runtime/run.js +10 -39
  229. package/dist/runtime/services.js +12 -117
  230. package/dist/runtime/shared-read.js +1 -3
  231. package/dist/runtime/storage.js +8 -1
  232. package/dist/sent-secrets.d.ts +52 -0
  233. package/dist/sent-secrets.js +474 -0
  234. package/dist/server.js +28 -71
  235. package/dist/skills.js +406 -174
  236. package/dist/sqlite.d.ts +48 -0
  237. package/dist/sqlite.js +166 -0
  238. package/dist/storage/keys.js +428 -0
  239. package/dist/storage/memory.js +7 -0
  240. package/dist/storage/sql.d.ts +66 -0
  241. package/dist/storage/sql.js +534 -0
  242. package/dist/timeout.js +4 -2
  243. package/dist/tool-name.js +17 -0
  244. package/dist/tool-safety.d.ts +9 -35
  245. package/dist/tool-safety.js +20 -40
  246. package/dist/types.d.ts +320 -54
  247. package/dist/types.js +0 -1
  248. package/dist/ui.d.ts +7 -19
  249. package/dist/ui.js +76 -90
  250. package/dist/url-safety.js +3 -6
  251. package/dist/usage-guide.js +137 -0
  252. package/dist/validate.d.ts +7 -0
  253. package/dist/validate.js +309 -47
  254. package/dist/version.d.ts +1 -7
  255. package/dist/version.js +3 -7
  256. package/dist/worker.d.ts +11 -0
  257. package/dist/worker.js +434 -0
  258. package/documentation/architecture.md +665 -230
  259. package/documentation/auth.md +962 -458
  260. package/documentation/code-mode.md +501 -312
  261. package/documentation/meta-tools.md +449 -200
  262. package/documentation/operator-tests.md +93 -0
  263. package/documentation/provider-migration-0.29.md +60 -0
  264. package/examples/worker/AGENTS.md +34 -12
  265. package/examples/worker/README.md +286 -255
  266. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  267. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  268. package/examples/worker/src/connecta.config.ts +151 -0
  269. package/examples/worker/src/index.ts +14 -197
  270. package/examples/worker/wrangler.jsonc +15 -35
  271. package/package.json +125 -35
  272. package/templates/node/.dockerignore +1 -2
  273. package/templates/node/.env.example +20 -21
  274. package/templates/node/AGENTS.md +21 -17
  275. package/templates/node/Dockerfile +3 -5
  276. package/templates/node/README.md +133 -58
  277. package/templates/node/docker-compose.yml +11 -12
  278. package/templates/node/package.json +4 -2
  279. package/templates/node/src/connecta.config.ts +119 -0
  280. package/templates/node/src/index.ts +9 -154
  281. package/templates/node/src/provision-token.ts +17 -0
  282. package/AGENTS.md +0 -170
  283. package/dist/artifacts/connector.d.ts +0 -58
  284. package/dist/artifacts/connector.js +0 -730
  285. package/dist/artifacts/document.js +0 -86
  286. package/dist/artifacts/guide.js +0 -140
  287. package/dist/artifacts/html-scan.js +0 -329
  288. package/dist/artifacts/json.js +0 -78
  289. package/dist/artifacts/kv-store.d.ts +0 -20
  290. package/dist/artifacts/kv-store.js +0 -140
  291. package/dist/artifacts/markdown.js +0 -478
  292. package/dist/artifacts/module.d.ts +0 -37
  293. package/dist/artifacts/module.js +0 -66
  294. package/dist/artifacts/operations.d.ts +0 -197
  295. package/dist/artifacts/operations.js +0 -1002
  296. package/dist/artifacts/refresh.d.ts +0 -55
  297. package/dist/artifacts/refresh.js +0 -236
  298. package/dist/artifacts/routes.js +0 -206
  299. package/dist/artifacts/types.d.ts +0 -214
  300. package/dist/artifacts/types.js +0 -22
  301. package/dist/artifacts/validate.d.ts +0 -62
  302. package/dist/artifacts/validate.js +0 -651
  303. package/dist/artifacts.d.ts +0 -6
  304. package/dist/artifacts.js +0 -9
  305. package/dist/auth/bearer.d.ts +0 -11
  306. package/dist/auth/bearer.js +0 -50
  307. package/dist/providers/linear.js +0 -217
  308. package/dist/providers/mixpanel.js +0 -247
  309. package/dist/providers/revenuecat.js +0 -317
  310. package/dist/providers/stripe.js +0 -253
  311. package/dist/providers/vercel.js +0 -1253
  312. package/dist/storage/file.d.ts +0 -15
  313. package/dist/storage/file.js +0 -455
  314. package/ethos.md +0 -93
  315. package/examples/worker/src/cloudflare-kv.ts +0 -43
  316. package/examples/worker/src/d1-activity-row.ts +0 -155
  317. package/examples/worker/src/d1-activity.ts +0 -147
  318. package/examples/worker/src/d1-storage.ts +0 -138
  319. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  320. package/templates/node/src/file-activity.ts +0 -182
@@ -1,14 +1,76 @@
1
- import { createMcpHandler, isLegacyRequest, McpServer, WebStandardStreamableHTTPServerTransport, } from "@modelcontextprotocol/server";
1
+ import { bindActivityRequest } from "../activity-request.js";
2
+ import { closeConnectorScope } from "../connector-scope.js";
3
+ import { executeLimits } from "../config.js";
4
+ import { oauthConnectUrl, oauthConnectLink, oauthConnectLinkProgress, closeOAuthConnectLinks, claimAuthRetry, oauthConnectUnavailable, } from "../oauth-handoff.js";
5
+ import { AuthElicitation } from "../auth-elicitation.js";
6
+ import { classifyInboundRequest, createMcpHandler, isJSONRPCErrorResponse, isLegacyRequest, McpServer, WebStandardStreamableHTTPServerTransport, } from "@modelcontextprotocol/server";
2
7
  import { Duration, Effect, Exit, Option, Result, Scope } from "effect";
3
- import { notFoundResponse } from "../branding.js";
4
8
  import { registerExecuteTool } from "../execute.js";
5
- import { ExecutorAdmissionError, } from "../executor-admission.js";
9
+ import { ExecutorAdmissionError } from "../executor-admission.js";
6
10
  import { registerMetaTools } from "../meta-tools.js";
7
11
  import { intersectAccess } from "../connector-access.js";
8
12
  import { CONNECTA_INSTRUCTIONS } from "../skills.js";
9
- import { msg } from "../errors.js";
13
+ import { registerSkills } from "./skills.js";
14
+ import { failureRecord, logFailure } from "../operator-record.js";
15
+ import { redactAgentOutput, sentSecretsForRequest } from "../sent-secrets.js";
10
16
  import { detach } from "../runtime/run.js";
11
- import { authorize, loggableValue, mayManageConnector, validateAuthPermissions, } from "./shared.js";
17
+ import { authorize, mayManageConnector, validateAuthPermissions, } from "./shared.js";
18
+ /**
19
+ * The SDK's own bound on the body it reads. Since server 2.1.0 the SDK
20
+ * answers anything over 4 MiB with its own 413, which would quietly shadow
21
+ * the bound the host already enforces: `listen()`'s `maxBodyBytes` on Node,
22
+ * the platform's request limit on Workers. The host owns that decision, as it
23
+ * did before the SDK had an opinion, so the SDK is told to have none. Its
24
+ * option must be finite.
25
+ */
26
+ const SDK_BODY_BOUND = { maxRequestBodySize: Number.MAX_SAFE_INTEGER };
27
+ // Identity, not a status/header a custom auth response could imitate.
28
+ const deadlineResponses = new WeakSet();
29
+ async function isModernListen(request, signal) {
30
+ const reader = request.clone().body?.getReader();
31
+ if (!reader)
32
+ return false;
33
+ const onAbort = () => {
34
+ void reader.cancel(signal.reason).catch(() => { });
35
+ void request.body?.cancel(signal.reason).catch(() => { });
36
+ };
37
+ signal.addEventListener("abort", onAbort, { once: true });
38
+ if (signal.aborted)
39
+ onAbort();
40
+ try {
41
+ const decoder = new TextDecoder();
42
+ let text = "";
43
+ let bytes = 0;
44
+ for (;;) {
45
+ const { done, value } = await reader.read();
46
+ if (done)
47
+ break;
48
+ bytes += value.byteLength;
49
+ if (bytes > SDK_BODY_BOUND.maxRequestBodySize)
50
+ return false;
51
+ text += decoder.decode(value, { stream: true });
52
+ }
53
+ if (signal.aborted)
54
+ return false;
55
+ const protocolVersion = request.headers.get("MCP-Protocol-Version");
56
+ const method = request.headers.get("Mcp-Method");
57
+ const route = classifyInboundRequest({
58
+ httpMethod: request.method,
59
+ ...(protocolVersion !== null ? { protocolVersionHeader: protocolVersion } : {}),
60
+ ...(method !== null ? { mcpMethodHeader: method } : {}),
61
+ body: JSON.parse(text + decoder.decode()),
62
+ });
63
+ return (route.kind === "modern" && route.messageKind === "request" && route.message.method === "subscriptions/listen");
64
+ }
65
+ catch {
66
+ // Leave unreadable, malformed, and legacy bodies to the normal SDK path.
67
+ return false;
68
+ }
69
+ finally {
70
+ signal.removeEventListener("abort", onAbort);
71
+ reader.releaseLock();
72
+ }
73
+ }
12
74
  export const MCP_CORS_HEADERS = {
13
75
  "Access-Control-Allow-Origin": "*",
14
76
  "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
@@ -30,28 +92,41 @@ function withMcpCors(response, request, allowedOrigin) {
30
92
  // Browsers do not interpret a prefix wildcard in Allow-Headers. Echo only
31
93
  // valid SEP-2243 field names; unrelated requested headers stay disallowed.
32
94
  const paramHeaders = (request.headers.get("Access-Control-Request-Headers") ?? "")
33
- .toLowerCase().split(",").map(name => name.trim())
34
- .filter(name => /^mcp-param-[!#$%&'*+.^_`|~0-9a-z-]+$/.test(name));
95
+ .toLowerCase()
96
+ .split(",")
97
+ .map((name) => name.trim())
98
+ .filter((name) => /^mcp-param-[!#$%&'*+.^_`|~0-9a-z-]+$/.test(name));
35
99
  if (paramHeaders.length) {
36
100
  headers.append("Access-Control-Allow-Headers", [...new Set(paramHeaders)].join(", "));
37
101
  }
38
102
  headers.append("Vary", "Access-Control-Request-Headers");
39
103
  }
40
- headers.set("Access-Control-Expose-Headers", "WWW-Authenticate, Retry-After, mcp-session-id, mcp-protocol-version");
41
- return new Response(response.body, {
104
+ headers.set("Access-Control-Expose-Headers", "WWW-Authenticate, Retry-After, mcp-session-id, mcp-protocol-version, Connecta-Error-Code, Connecta-Recovery");
105
+ const wrapped = new Response(response.body, {
42
106
  status: response.status,
43
107
  statusText: response.statusText,
44
108
  headers,
45
109
  });
110
+ if (deadlineResponses.has(response))
111
+ deadlineResponses.add(wrapped);
112
+ return wrapped;
46
113
  }
47
- function requestAdmissionFailure(error) {
114
+ /** Transport refusals occur before the RPC body is decoded, so its id is unknown. */
115
+ function mcpRefusal(sentSecrets, status, code, message) {
116
+ const response = Response.json(redactAgentOutput(sentSecrets, { jsonrpc: "2.0", error: { code, message } }), {
117
+ status,
118
+ headers: { "Cache-Control": "no-store" },
119
+ });
120
+ if (status === 504 && code === -33003)
121
+ deadlineResponses.add(response);
122
+ return response;
123
+ }
124
+ function requestAdmissionFailure(sentSecrets, error) {
48
125
  const overloaded = error.code === "executor_overloaded";
49
126
  const data = {
50
127
  code: overloaded ? "server_overloaded" : "server_shutting_down",
51
128
  retryable: overloaded,
52
- ...(overloaded && error.retryAfterMs !== undefined
53
- ? { retryAfterMs: error.retryAfterMs }
54
- : {}),
129
+ ...(overloaded && error.retryAfterMs !== undefined ? { retryAfterMs: error.retryAfterMs } : {}),
55
130
  };
56
131
  const headers = new Headers({
57
132
  "Content-Type": "application/json",
@@ -60,20 +135,17 @@ function requestAdmissionFailure(error) {
60
135
  if (overloaded && error.retryAfterMs !== undefined) {
61
136
  headers.set("Retry-After", String(Math.max(1, Math.ceil(error.retryAfterMs / 1_000))));
62
137
  }
63
- return new Response(JSON.stringify({
138
+ return new Response(JSON.stringify(redactAgentOutput(sentSecrets, {
64
139
  jsonrpc: "2.0",
65
- id: null,
66
140
  error: {
67
141
  // MCP 2026-07-28 basic#error-codes forbids new allocations in the
68
142
  // legacy -32000..-32019 range. Use application codes outside the
69
143
  // JSON-RPC reserved range, avoiding retired protocol meanings.
70
- code: overloaded ? -31001 : -31002,
71
- message: overloaded
72
- ? "Server capacity is exhausted. Retry later."
73
- : "Server is shutting down.",
144
+ code: overloaded ? -33001 : -33002,
145
+ message: overloaded ? "Server capacity is exhausted. Retry later." : "Server is shutting down.",
74
146
  data,
75
147
  },
76
- }), { status: 503, headers });
148
+ })), { status: 503, headers });
77
149
  }
78
150
  /**
79
151
  * Hand a response the Scope that holds what its request acquired.
@@ -153,6 +225,12 @@ function scopedToBody(handler, signal) {
153
225
  yield* Scope.close(scope, exit);
154
226
  return yield* exit;
155
227
  }
228
+ // A deadline can finish with a fresh error response after aborting the
229
+ // request. Release request resources now, without cancelling that body.
230
+ if (signal.aborted && deadlineResponses.has(exit.value)) {
231
+ yield* Scope.close(scope, Exit.void);
232
+ return exit.value;
233
+ }
156
234
  // Every finalizer in the scope is synchronous, so the close has run by
157
235
  // the time detach returns: the permit is back, and handed to the next
158
236
  // queued request, inside the read or cancel that ended the body.
@@ -177,8 +255,7 @@ function scopedToBody(handler, signal) {
177
255
  function admitted(controller, signal) {
178
256
  return Effect.acquireRelease(Effect.suspend(() => {
179
257
  const pending = controller.acquire({ signal });
180
- return Effect.tryPromise({ try: () => pending, catch: (error) => error })
181
- .pipe(Effect.onInterrupt(() => Effect.sync(() => {
258
+ return Effect.tryPromise({ try: () => pending, catch: (error) => error }).pipe(Effect.onInterrupt(() => Effect.sync(() => {
182
259
  pending.then((lease) => lease.release(), () => { });
183
260
  })));
184
261
  }), (lease) => Effect.sync(() => lease.release()), { interruptible: true });
@@ -194,13 +271,13 @@ function admitted(controller, signal) {
194
271
  * operator-side log line, which (as ever — issue #47) is the channel that
195
272
  * actually reaches a human.
196
273
  */
197
- function toolkitRetired(logger) {
274
+ function toolkitRetired(logger, sentSecrets) {
198
275
  logger.warn("[connecta] rejected an /mcp connection carrying ?toolkit= with 404: " +
199
- "toolkits were retired in issue #178 (see ethos.md) — one deployment " +
276
+ "toolkits were retired in issue #178 (see PRINCIPLES.md) — one deployment " +
200
277
  "serves one audience. The client sees a transport-level failure and " +
201
278
  "never the reason, so remove the ?toolkit= value from its MCP endpoint " +
202
279
  "URL, or point it at the deployment for its audience.");
203
- return new Response(JSON.stringify({
280
+ return new Response(JSON.stringify(redactAgentOutput(sentSecrets, {
204
281
  jsonrpc: "2.0",
205
282
  id: null,
206
283
  error: {
@@ -209,7 +286,7 @@ function toolkitRetired(logger) {
209
286
  "in issue #178 — remove the ?toolkit= value from the MCP endpoint " +
210
287
  "URL, or ask the operator for the deployment serving this audience.",
211
288
  },
212
- }), {
289
+ })), {
213
290
  status: 404,
214
291
  headers: {
215
292
  "Content-Type": "application/json",
@@ -217,89 +294,98 @@ function toolkitRetired(logger) {
217
294
  },
218
295
  });
219
296
  }
220
- function serveMcp(request, requestSignal, opts, baseUrl, actor, registry, canManageAuth, runtimeContext) {
297
+ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, registry, canManageAuth, principalKey, runtimeContext, trust = "read-only", pool, principalActor) {
298
+ const sentSecrets = sentSecretsForRequest(requestScope);
221
299
  // Every McpServer the request builds is fresh and closes with its scope.
222
300
  // The modern handler tears its own down after the exchange; the legacy
223
301
  // transport never does, and neither may stay wired to an ended request.
224
302
  const servers = [];
225
303
  const createServer = () => {
226
- const server = new McpServer(opts.serverInfo, {
304
+ const authElicitation = new AuthElicitation({
305
+ vault: opts.config.vault,
306
+ publicUrl: opts.config.publicUrl,
307
+ endpoint: new URL(new URL(request.url).pathname, baseUrl).href,
308
+ principal: principalKey,
309
+ registry,
310
+ canManage: canManageAuth,
311
+ connectLink: (id, force, after) => oauthConnectLink(opts, opts.config.publicUrl ?? baseUrl, id, principalKey, force, Boolean(opts.config.vault?.seal && opts.config.vault.open), after),
312
+ linkProgress: (id, nonce) => oauthConnectLinkProgress(opts, baseUrl, id, nonce),
313
+ closeLinks: (id, nonces) => closeOAuthConnectLinks(opts, baseUrl, id, nonces),
314
+ claimRetry: (id, nonce) => claimAuthRetry(opts, baseUrl, id, nonce),
315
+ unavailable: oauthConnectUnavailable(opts),
316
+ credentialUi: Boolean(opts.config.ui && opts.config.vault),
317
+ requestSignal,
318
+ requestScope,
319
+ defer: runtimeContext?.waitUntil?.bind(runtimeContext),
320
+ });
321
+ const server = new McpServer(opts.config.serverInfo, {
322
+ // A request-local server cannot publish catalog changes. Set this before
323
+ // tool registration, whose SDK default otherwise advertises listChanged.
324
+ capabilities: { tools: { listChanged: false }, extensions: { "io.modelcontextprotocol/skills": {} } },
227
325
  instructions: CONNECTA_INSTRUCTIONS,
326
+ inputRequired: { legacyShim: false },
327
+ requestState: { verify: (state, context) => authElicitation.verify(state, context) },
228
328
  cacheHints: {
329
+ "server/discover": { ttlMs: 3_600_000, cacheScope: "private" },
229
330
  "tools/list": {
230
331
  ttlMs: 3_600_000,
231
332
  cacheScope: "private",
232
333
  },
233
334
  },
234
335
  });
235
- const activity = opts.activity
336
+ const activity = opts.config.activity?.store
236
337
  ? {
237
- sink: opts.activity,
238
- recordTool: opts.activityModule?.recordTool,
338
+ sink: opts.config.activity?.store,
339
+ recordTool: opts.config.activity?.recordTool,
239
340
  actor,
341
+ ...(principalActor ? { principalActor } : {}),
240
342
  requestId: crypto.randomUUID(),
241
- serverInfo: opts.serverInfo,
242
- ...(opts.activityDeploymentId
243
- ? { deploymentId: opts.activityDeploymentId }
244
- : {}),
245
- ...(runtimeContext?.waitUntil
246
- ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
247
- : {}),
248
- logger: opts.logger,
343
+ ...(pool !== undefined ? { pool } : {}),
344
+ serverInfo: opts.config.serverInfo,
345
+ ...(opts.config.activity?.deploymentId ? { deploymentId: opts.config.activity?.deploymentId } : {}),
346
+ ...(runtimeContext?.waitUntil ? { defer: runtimeContext.waitUntil.bind(runtimeContext) } : {}),
347
+ logger: opts.config.logger,
249
348
  }
250
349
  : undefined;
350
+ if (activity)
351
+ bindActivityRequest(requestScope, activity);
352
+ const client = {};
353
+ registerSkills(server, registry, baseUrl, {
354
+ requestScope,
355
+ requestSignal,
356
+ probeTimeoutMs: opts.config.discovery.probeTimeoutMs,
357
+ ...(runtimeContext?.waitUntil ? { defer: runtimeContext.waitUntil.bind(runtimeContext) } : {}),
358
+ });
251
359
  registerMetaTools(server, registry, {
360
+ authElicitation,
361
+ client,
252
362
  baseUrl,
363
+ requestScope,
364
+ trust,
253
365
  canManageAuth,
254
- credentialHandoffUrl: opts.ui?.credentialHandoffUrl(baseUrl),
366
+ oauthConnectUrl: (id, force) => oauthConnectUrl(opts, baseUrl, id, principalKey, force),
367
+ oauthConnectUnavailable: oauthConnectUnavailable(opts),
368
+ credentialHandoffUrl: opts.config.ui?.credentialHandoffUrl(baseUrl),
255
369
  ...(activity ? { activity } : {}),
256
- ...(opts.defaultToolTimeoutMs !== undefined
257
- ? { defaultToolTimeoutMs: opts.defaultToolTimeoutMs }
258
- : {}),
259
- ...(opts.probeTimeoutMs !== undefined
260
- ? { probeTimeoutMs: opts.probeTimeoutMs }
261
- : {}),
262
- ...(opts.discoveryConcurrency !== undefined
263
- ? { discoveryConcurrency: opts.discoveryConcurrency }
264
- : {}),
370
+ defaultToolTimeoutMs: opts.config.calls.defaultTimeoutMs ?? opts.config.execute.hostCallTimeoutMs,
371
+ probeTimeoutMs: opts.config.discovery.probeTimeoutMs,
372
+ discoveryConcurrency: opts.config.discovery.concurrency,
265
373
  requestSignal,
266
- ...(runtimeContext?.waitUntil
267
- ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
268
- : {}),
269
- approval: opts.approval,
374
+ ...(runtimeContext?.waitUntil ? { defer: runtimeContext.waitUntil.bind(runtimeContext) } : {}),
270
375
  });
271
376
  registerExecuteTool(server, registry, {
377
+ authElicitation,
378
+ client,
272
379
  baseUrl,
380
+ requestScope,
273
381
  executor: opts.executor,
274
- logger: opts.logger,
382
+ defaultToolTimeoutMs: opts.config.calls.defaultTimeoutMs ?? opts.config.execute.hostCallTimeoutMs,
383
+ logger: opts.config.logger,
275
384
  ...(activity ? { activity } : {}),
276
385
  requestSignal,
277
- ...(runtimeContext?.waitUntil
278
- ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
279
- : {}),
280
- ...(opts.discoveryConcurrency !== undefined
281
- ? { discoveryConcurrency: opts.discoveryConcurrency }
282
- : {}),
283
- ...(opts.probeTimeoutMs !== undefined
284
- ? { probeTimeoutMs: opts.probeTimeoutMs }
285
- : {}),
286
- ...(opts.maxEmittedBytes !== undefined
287
- ? { maxEmittedBytes: opts.maxEmittedBytes }
288
- : {}),
289
- ...(opts.maxEmittedBlocks !== undefined
290
- ? { maxEmittedBlocks: opts.maxEmittedBlocks }
291
- : {}),
292
- ...(opts.maxHostCalls !== undefined
293
- ? { maxHostCalls: opts.maxHostCalls }
294
- : {}),
295
- ...(opts.hostCallTimeoutMs !== undefined
296
- ? { hostCallTimeoutMs: opts.hostCallTimeoutMs }
297
- : {}),
298
- ...(opts.watchdogMs !== undefined
299
- ? { watchdogMs: opts.watchdogMs }
300
- : {}),
301
- approval: opts.approval,
302
- maxWrites: opts.maxWrites,
386
+ ...(runtimeContext?.waitUntil ? { defer: runtimeContext.waitUntil.bind(runtimeContext) } : {}),
387
+ ...executeLimits(opts.config),
388
+ trust,
303
389
  });
304
390
  servers.push(server);
305
391
  return server;
@@ -309,15 +395,33 @@ function serveMcp(request, requestSignal, opts, baseUrl, actor, registry, canMan
309
395
  // Connecta's established wire contract is JSON, so retain the documented
310
396
  // user-land legacy branch with the same transport setting while the modern
311
397
  // branch uses the fetch-native handler.
312
- if (!(await isLegacyRequest(request))) {
313
- return createMcpHandler(createServer, {
398
+ if (!(await isLegacyRequest(request, undefined, SDK_BODY_BOUND))) {
399
+ const response = await createMcpHandler(createServer, {
400
+ ...SDK_BODY_BOUND,
314
401
  legacy: "reject",
315
- onerror: (error) => opts.logger.error("[connecta] MCP handler error", error),
402
+ // Keep the SDK's validation and prevent it from opening an SSE stream.
403
+ // Its capacity error below becomes our permanent unsupported method.
404
+ maxSubscriptions: 0,
405
+ onerror: (error) => logFailure(opts.config.logger, "MCP handler error", failureRecord({}, error), "error"),
316
406
  }).fetch(request);
407
+ if (request.headers.get("Mcp-Method") === "subscriptions/listen" && response.status === 200) {
408
+ const body = await response.clone().json();
409
+ if (isJSONRPCErrorResponse(body) &&
410
+ body.error.code === -32603 &&
411
+ body.error.message === "Subscription limit reached") {
412
+ return new Response(JSON.stringify({
413
+ jsonrpc: "2.0",
414
+ id: body.id,
415
+ error: { code: -32601, message: "Method not found: subscriptions/listen" },
416
+ }), { status: 404, headers: response.headers });
417
+ }
418
+ }
419
+ return response;
317
420
  }
318
421
  // Fresh server + transport per legacy request, stateless and JSON-shaped.
319
422
  const server = createServer();
320
423
  const transport = new WebStandardStreamableHTTPServerTransport({
424
+ ...SDK_BODY_BOUND,
321
425
  enableJsonResponse: true,
322
426
  });
323
427
  await server.connect(transport);
@@ -326,10 +430,34 @@ function serveMcp(request, requestSignal, opts, baseUrl, actor, registry, canMan
326
430
  return Effect.addFinalizer(() => Effect.sync(() => {
327
431
  for (const server of servers)
328
432
  void server.close().catch(() => { });
329
- })).pipe(Effect.andThen(Effect.promise(exchange)));
433
+ })).pipe(Effect.andThen(Effect.promise(async () => {
434
+ const response = await exchange();
435
+ // Both SDK transports serialize here. This also covers SDK-generated
436
+ // JSON-RPC errors and allowed HTTP 4xx text, outside tool result shaping.
437
+ if (!response.body)
438
+ return response;
439
+ const body = await response.text();
440
+ let text;
441
+ if (response.headers.get("Content-Type")?.includes("application/json")) {
442
+ // Structured redaction also joins text blocks before a secret split
443
+ // across them can cross the serialization boundary.
444
+ try {
445
+ text = JSON.stringify(redactAgentOutput(sentSecrets, JSON.parse(body)));
446
+ }
447
+ catch {
448
+ /* Non-JSON diagnostics still pass through the text boundary. */
449
+ }
450
+ }
451
+ // Parsed JSON already passes every string through the boundary. A second
452
+ // serialized pass would mistake a blob's encoding for credential bytes.
453
+ text ??= redactAgentOutput(sentSecrets, body);
454
+ const headers = new Headers(response.headers);
455
+ headers.delete("Content-Length");
456
+ return new Response(text, { status: response.status, statusText: response.statusText, headers });
457
+ })));
330
458
  }
331
459
  export function createMcpRoute(opts) {
332
- const configuredOrigins = opts.allowedOrigins;
460
+ const configuredOrigins = opts.config.allowedOrigins;
333
461
  const isExactOrigin = (value) => {
334
462
  if (typeof value !== "string")
335
463
  return false;
@@ -341,13 +469,14 @@ export function createMcpRoute(opts) {
341
469
  return false;
342
470
  }
343
471
  };
344
- if (configuredOrigins !== undefined && configuredOrigins !== "*" &&
345
- (!Array.isArray(configuredOrigins) || !configuredOrigins.every(isExactOrigin))) {
346
- throw new TypeError('ConnectaConfig.allowedOrigins must be an array of exact HTTP(S) origins or "*".');
347
- }
472
+ // The schema has already refused anything but exact origins or "*".
348
473
  const origins = new Set(configuredOrigins === undefined
349
- ? opts.publicUrl ? [new URL(opts.publicUrl).origin] : []
350
- : configuredOrigins === "*" ? [] : configuredOrigins);
474
+ ? opts.config.publicUrl
475
+ ? [new URL(opts.config.publicUrl).origin]
476
+ : []
477
+ : configuredOrigins === "*"
478
+ ? []
479
+ : configuredOrigins);
351
480
  const allowsOrigin = (origin) => {
352
481
  if (configuredOrigins === "*")
353
482
  return true;
@@ -367,10 +496,7 @@ export function createMcpRoute(opts) {
367
496
  const origin = request.headers.get("Origin");
368
497
  if (origin === null || allowsOrigin(origin))
369
498
  return null;
370
- return withMcpCors(new Response('{"error":"origin not allowed"}', {
371
- status: 403,
372
- headers: { "Content-Type": "application/json", "Cache-Control": "no-store" },
373
- }), request, null);
499
+ return withMcpCors(mcpRefusal(sentSecretsForRequest(request), 403, -33005, "MCP access is forbidden."), request, null);
374
500
  };
375
501
  let lastAdmissionWarningAt = 0;
376
502
  let suppressedAdmissionWarnings = 0;
@@ -380,7 +506,7 @@ export function createMcpRoute(opts) {
380
506
  suppressedAdmissionWarnings++;
381
507
  return;
382
508
  }
383
- opts.logger.warn("[connecta] MCP request admission rejected", {
509
+ opts.config.logger.warn("[connecta] MCP request admission rejected", {
384
510
  retryAfterMs: error.retryAfterMs,
385
511
  active: opts.requestAdmission.activeCount,
386
512
  queued: opts.requestAdmission.queuedCount,
@@ -390,9 +516,14 @@ export function createMcpRoute(opts) {
390
516
  suppressedAdmissionWarnings = 0;
391
517
  };
392
518
  function routeMcp(context) {
393
- const { path, request, baseUrl, runtimeContext, } = context;
519
+ const { path, request, baseUrl, runtimeContext } = context;
394
520
  if (path !== "/mcp" && !path.startsWith("/mcp/"))
395
521
  return Effect.succeed(null);
522
+ // Admission, refusal bodies, SDK serialization and connector work share
523
+ // one set. Connector contexts still receive an opaque scope, never the
524
+ // inbound Request or its headers.
525
+ const requestScope = {};
526
+ const sentSecrets = sentSecretsForRequest(requestScope, sentSecretsForRequest(request));
396
527
  const poolName = path === "/mcp" ? undefined : path.slice("/mcp/".length);
397
528
  const origin = request.headers.get("Origin");
398
529
  const allowed = origin === null || allowsOrigin(origin);
@@ -422,7 +553,25 @@ export function createMcpRoute(opts) {
422
553
  if (deadlineTimer !== undefined)
423
554
  clearTimeout(deadlineTimer);
424
555
  }));
425
- const admission = yield* Effect.result(admitted(opts.requestAdmission, request.signal));
556
+ // Only a body-confirmed modern listen may skip admission. Trusting the
557
+ // method header alone would let a legacy tools/call bypass the pool.
558
+ const startedAt = Date.now();
559
+ const maxDurationMs = opts.requestAdmission.maxDurationMs;
560
+ let listen = false;
561
+ if (request.method === "POST" && request.headers.get("Mcp-Method") === "subscriptions/listen") {
562
+ const classify = Effect.promise(() => isModernListen(request, localAbort.signal));
563
+ const prepared = maxDurationMs === undefined
564
+ ? Option.some(yield* classify)
565
+ : yield* classify.pipe(Effect.timeoutOption(Duration.millis(maxDurationMs)));
566
+ if (Option.isNone(prepared)) {
567
+ localAbort.abort(new Error("MCP request lifetime exceeded."));
568
+ return cors(mcpRefusal(sentSecrets, 504, -33003, "MCP request lifetime exceeded."));
569
+ }
570
+ listen = prepared.value;
571
+ }
572
+ const admission = listen
573
+ ? Result.succeed(undefined)
574
+ : yield* Effect.result(admitted(opts.requestAdmission, request.signal));
426
575
  if (Result.isFailure(admission)) {
427
576
  const error = admission.failure;
428
577
  if (!(error instanceof ExecutorAdmissionError)) {
@@ -434,12 +583,16 @@ export function createMcpRoute(opts) {
434
583
  if (error.code === "executor_overloaded") {
435
584
  warnAdmissionRejected(error);
436
585
  }
437
- return cors(requestAdmissionFailure(error));
586
+ return cors(requestAdmissionFailure(sentSecrets, error));
438
587
  }
439
- const remainingMs = admission.success.remainingMs();
588
+ const remainingMs = admission.success
589
+ ? admission.success.remainingMs()
590
+ : maxDurationMs === undefined
591
+ ? undefined
592
+ : maxDurationMs - (Date.now() - startedAt);
440
593
  if (remainingMs !== undefined && remainingMs <= 0) {
441
- admission.success.release();
442
- return cors(new Response("MCP request lifetime exceeded.", { status: 504 }));
594
+ admission.success?.release();
595
+ return cors(mcpRefusal(sentSecrets, 504, -33003, "MCP request lifetime exceeded."));
443
596
  }
444
597
  if (remainingMs !== undefined) {
445
598
  deadlineTimer = setTimeout(() => {
@@ -447,8 +600,8 @@ export function createMcpRoute(opts) {
447
600
  }, remainingMs);
448
601
  }
449
602
  const localRequest = new Request(request, { signal: localAbort.signal });
450
- if (admission.success.waitMs > 0) {
451
- opts.logger.debug("[connecta] MCP request admitted after queue wait", {
603
+ if (admission.success && admission.success.waitMs > 0) {
604
+ opts.config.logger.debug("[connecta] MCP request admitted after queue wait", {
452
605
  waitMs: admission.success.waitMs,
453
606
  active: opts.requestAdmission.activeCount,
454
607
  queued: opts.requestAdmission.queuedCount,
@@ -459,33 +612,57 @@ export function createMcpRoute(opts) {
459
612
  // shares it, to save only the lookups an abandoned authorization
460
613
  // finishes on its own.
461
614
  const handled = Effect.gen(function* () {
462
- const authz = yield* Effect.promise(() => authorize(localRequest, baseUrl, opts.auth, runtimeContext, opts.identity));
463
- if (!authz.ok)
464
- return cors(authz.response);
615
+ const authz = yield* Effect.promise(() => authorize(localRequest, baseUrl, opts.config.auth, runtimeContext, opts.config.identity));
616
+ if (!authz.ok) {
617
+ const response = authz.response;
618
+ // Preserve the auth adapter's challenge and status. The MCP host must
619
+ // repair its connection to connecta before any connector can be called.
620
+ if (response.status !== 401 && response.status !== 403)
621
+ return cors(response);
622
+ const headers = new Headers(response.headers);
623
+ headers.set("Connecta-Error-Code", "host_auth_required");
624
+ headers.set("Connecta-Recovery", "host_connection");
625
+ // Custom adapters may own a streaming or non-JSON response. Preserve
626
+ // that body and its request lifetime, and carry recovery in headers.
627
+ if (!response.headers.get("Content-Type")?.includes("application/json")) {
628
+ return cors(new Response(response.body, { status: response.status, headers }));
629
+ }
630
+ headers.set("Content-Type", "application/json");
631
+ headers.delete("Content-Length");
632
+ void response.body?.cancel().catch(() => { });
633
+ return cors(new Response(JSON.stringify(redactAgentOutput(sentSecrets, {
634
+ error: {
635
+ code: "host_auth_required",
636
+ message: "The host is not authorized to connect to this endpoint. Sign in or update the host's connecta access token and endpoint grants.",
637
+ retryable: false,
638
+ recovery: "host_connection",
639
+ },
640
+ })), { status: response.status, headers }));
641
+ }
465
642
  // A pool endpoint narrows the identity's own view and nothing else. An
466
643
  // undeclared name, a grant that refuses, and a grant that throws are
467
644
  // one identical 404 so a credential never enumerates the other pools;
468
645
  // the operator log is where the reason lives.
469
646
  let access = authz;
647
+ let trust = opts.config.trust;
470
648
  if (poolName !== undefined) {
471
649
  const pool = opts.pools?.get(poolName);
472
- const reason = !pool ? "undeclared" : yield* Effect.promise(async () => {
473
- try {
474
- return (await pool.grant(authz.identity)) === true ? "granted" : "refused";
475
- }
476
- catch {
477
- return "grant threw";
478
- }
479
- });
480
- if (!pool || reason !== "granted") {
481
- opts.logger.warn(`[connecta] refused /mcp/${poolName} with 404: pool ${reason}` +
482
- (authz.actor.id ? ` for ${loggableValue(authz.actor.id)}` : ""));
483
- // The server's own 404, so a browser sees the page every unserved
484
- // path shows and an MCP client (which never asks for text/html)
485
- // still reads a plain "Not Found".
486
- return cors(notFoundResponse(request, opts));
650
+ const denialReason = !pool
651
+ ? "pool_not_declared"
652
+ : yield* Effect.promise(async () => {
653
+ try {
654
+ return (await pool.grant(authz.identity)) === true ? undefined : "pool_grant_denied";
655
+ }
656
+ catch {
657
+ return "pool_grant_threw";
658
+ }
659
+ });
660
+ if (!pool || denialReason !== undefined) {
661
+ logFailure(opts.config.logger, "MCP pool request denied", failureRecord({ reason: denialReason ?? "pool_not_declared" }));
662
+ return cors(mcpRefusal(sentSecrets, 404, -33004, "MCP endpoint not found."));
487
663
  }
488
664
  access = intersectAccess(authz, pool.access);
665
+ trust = pool.trust;
489
666
  }
490
667
  let scopedRegistry;
491
668
  try {
@@ -496,22 +673,71 @@ export function createMcpRoute(opts) {
496
673
  ...(access.guardedToolAccess ? { guardedToolAccess: access.guardedToolAccess } : {}),
497
674
  ...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}),
498
675
  ...(authz.principalKey ? { principalKey: authz.principalKey } : {}),
676
+ endpoint: new URL(request.url).pathname,
677
+ origin: request.headers.get("Origin"),
678
+ currentResultAccess: async (address, classification, signal) => {
679
+ signal?.throwIfAborted();
680
+ const current = await authorize(localRequest, baseUrl, opts.config.auth, runtimeContext, opts.config.identity);
681
+ if (!current.ok) {
682
+ await current.response.body?.cancel().catch(() => { });
683
+ return false;
684
+ }
685
+ if (current.subjectKey !== authz.subjectKey || current.principalKey !== authz.principalKey)
686
+ return false;
687
+ validateAuthPermissions(current, opts.registry);
688
+ let currentAccess = current;
689
+ let currentTrust = opts.config.trust;
690
+ if (poolName !== undefined) {
691
+ const pool = opts.pools.get(poolName);
692
+ if (!pool || (await pool.grant(current.identity)) !== true)
693
+ return false;
694
+ currentAccess = intersectAccess(current, pool.access);
695
+ currentTrust = pool.trust;
696
+ }
697
+ const view = opts.registry.scoped({
698
+ ...currentAccess,
699
+ ...(current.subjectKey ? { subjectKey: current.subjectKey } : {}),
700
+ ...(current.principalKey ? { principalKey: current.principalKey } : {}),
701
+ });
702
+ const resolved = view.resolveAddress(address);
703
+ if (!resolved)
704
+ return false;
705
+ const requestScope = {};
706
+ try {
707
+ const tool = (await view.getTools(resolved.connector.id, baseUrl, requestScope, signal ? { signal } : {})).find((tool) => tool.name === resolved.toolName);
708
+ signal?.throwIfAborted();
709
+ return Boolean(tool &&
710
+ ((classification === "read" && tool.classification === "read") || currentTrust === "trusted"));
711
+ }
712
+ finally {
713
+ await closeConnectorScope(resolved.connector, view.contextFor(resolved.connector.id, baseUrl, requestScope), runtimeContext?.waitUntil?.bind(runtimeContext));
714
+ }
715
+ },
499
716
  caller: {
500
717
  identity: authz.identity,
718
+ // `authorize` admits an open deployment's every request as the
719
+ // anonymous actor; only a provider's `ok` is an authentication.
720
+ authenticated: opts.config.auth.length > 0,
501
721
  ...(poolName !== undefined ? { pool: poolName } : {}),
502
722
  },
503
723
  });
504
724
  }
505
- catch (error) {
506
- return cors(new Response(JSON.stringify({ error: msg(error) }), {
507
- status: 403,
508
- headers: { "Content-Type": "application/json" },
509
- }));
725
+ catch {
726
+ return cors(mcpRefusal(sentSecrets, 403, -33005, "MCP access is forbidden."));
510
727
  }
511
728
  if (new URL(request.url).searchParams.has("toolkit")) {
512
- return cors(toolkitRetired(opts.logger));
729
+ return cors(toolkitRetired(opts.config.logger, sentSecrets));
513
730
  }
514
- return cors(yield* serveMcp(localRequest, localAbort.signal, opts, baseUrl, authz.actor, scopedRegistry, id => { const connector = scopedRegistry.getConnector(id); return Boolean(connector && mayManageConnector(authz, connector)); }, runtimeContext));
731
+ return cors(yield* serveMcp(localRequest, localAbort.signal, requestScope, opts, baseUrl, authz.actor, scopedRegistry, (id) => {
732
+ const connector = scopedRegistry.getConnector(id);
733
+ return Boolean(connector && mayManageConnector(authz, connector));
734
+ }, authz.principalKey, runtimeContext, trust, poolName, authz.identity.principal
735
+ ? {
736
+ kind: authz.actor.kind,
737
+ id: authz.identity.principal.id,
738
+ namespace: authz.identity.principal.namespace,
739
+ }
740
+ : undefined));
515
741
  });
516
742
  if (remainingMs === undefined)
517
743
  return yield* handled;
@@ -520,7 +746,7 @@ export function createMcpRoute(opts) {
520
746
  localAbort.abort(new Error("MCP request lifetime exceeded."));
521
747
  return Option.isSome(bounded)
522
748
  ? bounded.value
523
- : cors(new Response("MCP request lifetime exceeded.", { status: 504 }));
749
+ : cors(mcpRefusal(sentSecrets, 504, -33003, "MCP request lifetime exceeded."));
524
750
  }), localAbort.signal);
525
751
  }
526
752
  return { handle: routeMcp, rejectOrigin };