@zackbart/connecta 0.28.1 → 0.30.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 (332) hide show
  1. package/CHANGELOG.md +855 -5
  2. package/README.md +125 -116
  3. package/bin/connecta.mjs +121 -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/argument-redaction.js +230 -0
  12. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  13. package/dist/auth/clerk-sdk/client.js +5 -0
  14. package/dist/auth/clerk-sdk/generated.js +1845 -0
  15. package/dist/auth/clerk-transport.js +19 -0
  16. package/dist/auth/clerk.d.ts +17 -7
  17. package/dist/auth/clerk.js +189 -107
  18. package/dist/auth/cloudflare-access.js +25 -26
  19. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  20. package/dist/auth/downstream-client-metadata.js +133 -0
  21. package/dist/auth/downstream-oauth.js +1584 -1580
  22. package/dist/auth/downstream-revocation.js +106 -0
  23. package/dist/auth/oauth-token-response.js +231 -0
  24. package/dist/auth/oauth-v2-migration.js +116 -0
  25. package/dist/auth/static-oauth.js +93 -71
  26. package/dist/auth-elicitation.js +288 -0
  27. package/dist/authorization-handoff.js +111 -0
  28. package/dist/branding.d.ts +8 -16
  29. package/dist/branding.js +26 -59
  30. package/dist/byte-read-response.js +39 -0
  31. package/dist/call-admission.js +12 -16
  32. package/dist/catalog-cache.js +739 -0
  33. package/dist/catalog-drift.d.ts +50 -15
  34. package/dist/catalog-drift.js +365 -128
  35. package/dist/catalog-fingerprint.js +2 -4
  36. package/dist/catalog-freshness.js +13 -0
  37. package/dist/catalog-limits.js +2 -2
  38. package/dist/catalog-service.js +409 -225
  39. package/dist/catalog.js +43 -84
  40. package/dist/concurrency.js +3 -4
  41. package/dist/config-defaults.d.ts +39 -0
  42. package/dist/config-defaults.js +46 -0
  43. package/dist/config-schema.d.ts +181 -0
  44. package/dist/config-schema.js +435 -0
  45. package/dist/config-value-sources.js +102 -0
  46. package/dist/config.d.ts +633 -0
  47. package/dist/config.js +508 -0
  48. package/dist/connector-access.d.ts +1 -0
  49. package/dist/connector-access.js +13 -15
  50. package/dist/connector-caller.d.ts +8 -0
  51. package/dist/connector-caller.js +15 -6
  52. package/dist/connector-scope.js +1 -1
  53. package/dist/connectors/api-connector.d.ts +59 -7
  54. package/dist/connectors/api-connector.js +106 -19
  55. package/dist/connectors/api.js +4 -1
  56. package/dist/connectors/guarded-fetch.d.ts +144 -0
  57. package/dist/connectors/guarded-fetch.js +87 -38
  58. package/dist/connectors/negotiation-cache.js +46 -0
  59. package/dist/connectors/option-shapes.js +27 -0
  60. package/dist/connectors/remote-mcp.d.ts +30 -4
  61. package/dist/connectors/remote-mcp.js +1774 -320
  62. package/dist/connectors/resource-uri.js +335 -0
  63. package/dist/credential-contract.d.ts +10 -0
  64. package/dist/credential-rules.js +1 -3
  65. package/dist/credential-url.js +108 -0
  66. package/dist/credentials.d.ts +6 -1
  67. package/dist/credentials.js +27 -18
  68. package/dist/d1.d.ts +130 -0
  69. package/dist/d1.js +290 -0
  70. package/dist/describe-config.d.ts +165 -0
  71. package/dist/describe-config.js +315 -0
  72. package/dist/described.js +106 -0
  73. package/dist/downstream-input-context.js +128 -0
  74. package/dist/downstream-input.js +325 -0
  75. package/dist/errors.d.ts +132 -31
  76. package/dist/errors.js +175 -87
  77. package/dist/execute.js +798 -318
  78. package/dist/executor-admission.d.ts +1 -0
  79. package/dist/executor-admission.js +11 -15
  80. package/dist/executor-contract.d.ts +20 -0
  81. package/dist/executor-contract.js +48 -0
  82. package/dist/executor-result.js +30 -13
  83. package/dist/executors/quickjs-child.js +15 -9
  84. package/dist/executors/quickjs-protocol.js +5 -6
  85. package/dist/executors/quickjs-runtime.js +152 -59
  86. package/dist/executors/quickjs.js +49 -39
  87. package/dist/favicon.js +1 -2
  88. package/dist/fix-prompt.js +1 -3
  89. package/dist/guest-runtime.js +238 -0
  90. package/dist/guest-types.d.ts +218 -0
  91. package/dist/guest-types.js +1 -0
  92. package/dist/html-security.js +24 -0
  93. package/dist/identity.js +1 -3
  94. package/dist/inbound-credential.js +52 -0
  95. package/dist/index.d.ts +24 -256
  96. package/dist/index.js +65 -478
  97. package/dist/invocation-auth.js +69 -0
  98. package/dist/invocation.js +262 -79
  99. package/dist/json-schema.d.ts +1 -1
  100. package/dist/mcp-client-context.js +35 -0
  101. package/dist/mcp-result.js +11 -7
  102. package/dist/meta-output.js +61 -0
  103. package/dist/meta-tool-names.d.ts +2 -0
  104. package/dist/meta-tool-names.js +3 -0
  105. package/dist/meta-tools.js +419 -272
  106. package/dist/module-contracts.d.ts +8 -23
  107. package/dist/node.d.ts +0 -2
  108. package/dist/node.js +2 -2
  109. package/dist/oauth-callback-outcome.js +1 -3
  110. package/dist/oauth-handoff.js +194 -0
  111. package/dist/oauth-sealing.js +20 -0
  112. package/dist/operator-record.d.ts +137 -0
  113. package/dist/operator-record.js +420 -0
  114. package/dist/operator-ui/assets.js +31 -0
  115. package/dist/operator-ui/contract.d.ts +72 -0
  116. package/dist/operator-ui/contract.js +1 -0
  117. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  118. package/dist/operator-ui/generated.js +5 -4
  119. package/dist/operator-ui/model.d.ts +12 -87
  120. package/dist/operator-ui/model.js +2 -9
  121. package/dist/operator-ui/setup-commands.js +1 -3
  122. package/dist/operator-ui/view.d.ts +323 -0
  123. package/dist/operator-ui/view.js +62 -189
  124. package/dist/page-styles.js +2 -3
  125. package/dist/program-result.js +5 -0
  126. package/dist/program-source.js +14 -2
  127. package/dist/{exempt-writes.js → program-writes.js} +60 -19
  128. package/dist/provider.d.ts +142 -0
  129. package/dist/provider.js +187 -0
  130. package/dist/providers/_shared/google/result-size.js +57 -0
  131. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  132. package/dist/providers/_shared/google/service-account.js +500 -0
  133. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  134. package/dist/providers/_shared/google/workspace.js +518 -0
  135. package/dist/providers/_shared/request-token.js +83 -0
  136. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  137. package/dist/providers/basecamp/index.js +997 -0
  138. package/dist/providers/basecamp/skill.generated.js +9 -0
  139. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  140. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  141. package/dist/providers/breeze/skill.generated.js +9 -0
  142. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  143. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  144. package/dist/providers/ccb/skill.generated.js +9 -0
  145. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  146. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  147. package/dist/providers/cloudflare/skill.generated.js +10 -0
  148. package/dist/providers/docs/index.d.ts +54 -0
  149. package/dist/providers/docs/index.js +1237 -0
  150. package/dist/providers/docs/skill.generated.js +9 -0
  151. package/dist/providers/drive/index.d.ts +53 -0
  152. package/dist/providers/drive/index.js +1523 -0
  153. package/dist/providers/drive/skill.generated.js +9 -0
  154. package/dist/providers/forms/index.d.ts +54 -0
  155. package/dist/providers/forms/index.js +1217 -0
  156. package/dist/providers/forms/skill.generated.js +11 -0
  157. package/dist/providers/github/auth.d.ts +32 -0
  158. package/dist/providers/github/auth.js +361 -0
  159. package/dist/providers/github/hosted.js +218 -0
  160. package/dist/providers/github/index.d.ts +13 -0
  161. package/dist/providers/github/index.js +140 -0
  162. package/dist/providers/github/key.js +100 -0
  163. package/dist/providers/github/rest.js +321 -0
  164. package/dist/providers/github/scope.d.ts +29 -0
  165. package/dist/providers/github/scope.js +124 -0
  166. package/dist/providers/github/skill.generated.js +8 -0
  167. package/dist/providers/gmail/index.d.ts +51 -0
  168. package/dist/providers/gmail/index.js +1436 -0
  169. package/dist/providers/gmail/skill.generated.js +9 -0
  170. package/dist/providers/infisical/index.d.ts +7 -0
  171. package/dist/providers/infisical/index.js +713 -0
  172. package/dist/providers/infisical/skill.generated.js +8 -0
  173. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  174. package/dist/providers/linear/index.js +352 -0
  175. package/dist/providers/linear/skill.generated.js +8 -0
  176. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  177. package/dist/providers/mixpanel/index.js +416 -0
  178. package/dist/providers/mixpanel/skill.generated.js +9 -0
  179. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  180. package/dist/providers/{notion.js → notion/index.js} +226 -239
  181. package/dist/providers/notion/skill.generated.js +10 -0
  182. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  183. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  184. package/dist/providers/overflow/skill.generated.js +8 -0
  185. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  186. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  187. package/dist/providers/planning-center/skill.generated.js +10 -0
  188. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  189. package/dist/providers/revenuecat/index.js +557 -0
  190. package/dist/providers/revenuecat/skill.generated.js +11 -0
  191. package/dist/providers/sheets/index.d.ts +54 -0
  192. package/dist/providers/sheets/index.js +1237 -0
  193. package/dist/providers/sheets/skill.generated.js +10 -0
  194. package/dist/providers/slides/index.d.ts +53 -0
  195. package/dist/providers/slides/index.js +2645 -0
  196. package/dist/providers/slides/skill.generated.js +9 -0
  197. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  198. package/dist/providers/stripe/index.js +266 -0
  199. package/dist/providers/stripe/skill.generated.js +11 -0
  200. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  201. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  202. package/dist/providers/tithely/skill.generated.js +8 -0
  203. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  204. package/dist/providers/vercel/index.js +1038 -0
  205. package/dist/providers/vercel/mcp-ownership.js +212 -0
  206. package/dist/providers/vercel/skill.generated.js +11 -0
  207. package/dist/registry.d.ts +110 -171
  208. package/dist/registry.js +588 -927
  209. package/dist/request-state.js +21 -0
  210. package/dist/resource-read.js +103 -0
  211. package/dist/result-shapes.js +17 -49
  212. package/dist/retired-toolkits.js +1 -1
  213. package/dist/routes/access-tokens.js +3 -6
  214. package/dist/routes/activity-context.js +21 -0
  215. package/dist/routes/activity-disclosure.js +40 -0
  216. package/dist/routes/activity.js +59 -21
  217. package/dist/routes/actor-labels.js +3 -50
  218. package/dist/routes/connect.js +221 -0
  219. package/dist/routes/credentials.js +15 -27
  220. package/dist/routes/mcp.js +365 -130
  221. package/dist/routes/oauth-client-metadata.js +15 -0
  222. package/dist/routes/oauth-management.js +45 -107
  223. package/dist/routes/oauth.js +110 -57
  224. package/dist/routes/operator.js +3 -3
  225. package/dist/routes/shared.d.ts +25 -64
  226. package/dist/routes/shared.js +160 -80
  227. package/dist/routes/skills.js +30 -0
  228. package/dist/routes/ui-config.js +225 -0
  229. package/dist/routes/ui-data.js +40 -35
  230. package/dist/routes/ui.js +66 -56
  231. package/dist/runtime/admission.js +4 -2
  232. package/dist/runtime/call-admission.js +3 -1
  233. package/dist/runtime/connector-scope.js +1 -4
  234. package/dist/runtime/run.js +10 -39
  235. package/dist/runtime/services.js +12 -117
  236. package/dist/runtime/shared-read.js +1 -3
  237. package/dist/runtime/storage.js +8 -1
  238. package/dist/sent-secrets.d.ts +93 -0
  239. package/dist/sent-secrets.js +1013 -0
  240. package/dist/server.js +28 -71
  241. package/dist/skills.js +416 -174
  242. package/dist/sqlite.d.ts +48 -0
  243. package/dist/sqlite.js +181 -0
  244. package/dist/storage/keys.js +443 -0
  245. package/dist/storage/memory.js +16 -4
  246. package/dist/storage/sql.d.ts +66 -0
  247. package/dist/storage/sql.js +543 -0
  248. package/dist/timeout.js +4 -2
  249. package/dist/tool-name.js +17 -0
  250. package/dist/tool-safety.d.ts +9 -35
  251. package/dist/tool-safety.js +20 -40
  252. package/dist/types.d.ts +336 -56
  253. package/dist/types.js +0 -1
  254. package/dist/ui.d.ts +7 -19
  255. package/dist/ui.js +76 -90
  256. package/dist/url-safety.js +3 -6
  257. package/dist/usage-guide.js +146 -0
  258. package/dist/validate.d.ts +7 -0
  259. package/dist/validate.js +309 -47
  260. package/dist/version.d.ts +1 -7
  261. package/dist/version.js +3 -7
  262. package/dist/worker.d.ts +11 -0
  263. package/dist/worker.js +435 -0
  264. package/documentation/README.md +63 -0
  265. package/documentation/architecture.md +696 -231
  266. package/documentation/auth.md +1069 -546
  267. package/documentation/code-mode.md +662 -356
  268. package/documentation/deploying.md +146 -0
  269. package/documentation/integrating.md +159 -0
  270. package/documentation/meta-tools.md +570 -216
  271. package/documentation/operating.md +135 -0
  272. package/documentation/operator-tests.md +97 -0
  273. package/documentation/operator-ui.md +132 -0
  274. package/documentation/provider-migration-0.29.md +65 -0
  275. package/examples/worker/AGENTS.md +34 -12
  276. package/examples/worker/README.md +293 -255
  277. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  278. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  279. package/examples/worker/src/connecta.config.ts +151 -0
  280. package/examples/worker/src/index.ts +14 -197
  281. package/examples/worker/wrangler.jsonc +15 -35
  282. package/package.json +126 -54
  283. package/templates/node/.dockerignore +1 -2
  284. package/templates/node/.env.example +20 -21
  285. package/templates/node/AGENTS.md +21 -17
  286. package/templates/node/Dockerfile +3 -5
  287. package/templates/node/README.md +140 -58
  288. package/templates/node/docker-compose.yml +11 -12
  289. package/templates/node/package.json +4 -2
  290. package/templates/node/src/connecta.config.ts +119 -0
  291. package/templates/node/src/index.ts +9 -154
  292. package/templates/node/src/provision-token.ts +17 -0
  293. package/AGENTS.md +0 -170
  294. package/dist/artifacts/connector.d.ts +0 -58
  295. package/dist/artifacts/connector.js +0 -730
  296. package/dist/artifacts/document.js +0 -86
  297. package/dist/artifacts/guide.js +0 -140
  298. package/dist/artifacts/html-scan.js +0 -329
  299. package/dist/artifacts/json.js +0 -78
  300. package/dist/artifacts/kv-store.d.ts +0 -20
  301. package/dist/artifacts/kv-store.js +0 -140
  302. package/dist/artifacts/markdown.js +0 -478
  303. package/dist/artifacts/module.d.ts +0 -37
  304. package/dist/artifacts/module.js +0 -66
  305. package/dist/artifacts/operations.d.ts +0 -197
  306. package/dist/artifacts/operations.js +0 -1002
  307. package/dist/artifacts/refresh.d.ts +0 -55
  308. package/dist/artifacts/refresh.js +0 -236
  309. package/dist/artifacts/routes.js +0 -206
  310. package/dist/artifacts/types.d.ts +0 -214
  311. package/dist/artifacts/types.js +0 -22
  312. package/dist/artifacts/validate.d.ts +0 -62
  313. package/dist/artifacts/validate.js +0 -651
  314. package/dist/artifacts.d.ts +0 -6
  315. package/dist/artifacts.js +0 -9
  316. package/dist/auth/bearer.d.ts +0 -11
  317. package/dist/auth/bearer.js +0 -50
  318. package/dist/providers/basecamp.js +0 -445
  319. package/dist/providers/linear.js +0 -217
  320. package/dist/providers/mixpanel.js +0 -247
  321. package/dist/providers/revenuecat.js +0 -317
  322. package/dist/providers/stripe.js +0 -253
  323. package/dist/providers/vercel.js +0 -1253
  324. package/dist/storage/file.d.ts +0 -15
  325. package/dist/storage/file.js +0 -455
  326. package/ethos.md +0 -93
  327. package/examples/worker/src/cloudflare-kv.ts +0 -43
  328. package/examples/worker/src/d1-activity-row.ts +0 -155
  329. package/examples/worker/src/d1-activity.ts +0 -147
  330. package/examples/worker/src/d1-storage.ts +0 -138
  331. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  332. package/templates/node/src/file-activity.ts +0 -182
@@ -1,18 +1,26 @@
1
+ import { authorizationHandoff, bindAuthorizationHandoffs } from "./authorization-handoff.js";
2
+ import { bindMcpClient } from "./mcp-client-context.js";
3
+ import { CONFIG_DEFAULTS } from "./config-defaults.js";
1
4
  import { z } from "zod";
2
5
  import { advertisedSchema } from "./advertised-schema.js";
3
- import { boundedDiscoveryText, CatalogService, DEFAULT_SEARCH_LIMIT, DiscoveryPolicyError, groupedSearchResult, MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT, } from "./catalog-service.js";
6
+ import { boundedDiscoveryText, CatalogService, DEFAULT_SEARCH_LIMIT, DiscoveryPolicyError, flatSearchResult, MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT, } from "./catalog-service.js";
4
7
  import { resolveDiscoveryConcurrency } from "./concurrency.js";
5
- import { boundedEchoText, msg } from "./errors.js";
6
- import { serializeResultText } from "./executor-result.js";
7
- import { InvocationService, } from "./invocation.js";
8
+ import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
9
+ import { failureRecord, logFailure } from "./operator-record.js";
10
+ import { guardExecuteResultValue, MAX_EXECUTE_RESULT_CHARS, serializeResultText } from "./executor-result.js";
11
+ import { InvocationService } from "./invocation.js";
8
12
  import { isValidMaxResultBytes, MIN_MAX_RESULT_BYTES, resolveMaxResultBytes, } from "./registry.js";
9
- import { hasConnectorGuides, listSkills, resolveSkill, } from "./skills.js";
10
- import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs, } from "./timeout.js";
11
- import { isExplicitlyReadOnly } from "./tool-safety.js";
12
- export { MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT, };
13
- const RESULT_TTL_SECONDS = 900;
13
+ import { hasConnectorGuides, SkillsRegistry } from "./skills.js";
14
+ import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs } from "./timeout.js";
15
+ import { AUTHORIZE_OUTPUT, SEARCH_OUTPUT, SKILLS_OUTPUT } from "./meta-output.js";
16
+ import { RESULT_TTL_SECONDS, resultKeys } from "./storage/keys.js";
17
+ import { agentOutputOperations, sentSecretsForRequest } from "./sent-secrets.js";
18
+ import { captureDownstreamInput } from "./downstream-input.js";
19
+ import { PROGRAM_RESULT_WIRE_BYTES } from "./program-result.js";
20
+ export { MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT };
14
21
  const enc = new TextEncoder();
15
- const dec = new TextDecoder();
22
+ // Each page is a slice of payload bytes, so a leading U+FEFF is data.
23
+ const dec = new TextDecoder("utf-8", { ignoreBOM: true, fatal: false });
16
24
  export function jsonResult(obj, text = JSON.stringify(obj)) {
17
25
  return {
18
26
  content: [{ type: "text", text }],
@@ -21,7 +29,7 @@ export function jsonResult(obj, text = JSON.stringify(obj)) {
21
29
  : {}),
22
30
  };
23
31
  }
24
- export function errorResult(message) {
32
+ function errorResult(message) {
25
33
  return { content: [{ type: "text", text: message }], isError: true };
26
34
  }
27
35
  function discoveryErrorResult(error) {
@@ -57,7 +65,7 @@ async function discoveryResult(operation, hint) {
57
65
  function isContinuationByte(b) {
58
66
  return b !== undefined && (b & 0xc0) === 0x80;
59
67
  }
60
- /** Smallest accepted `get_result` byte offset. */
68
+ /** Smallest accepted `connecta.result` byte offset. */
61
69
  const MIN_RESULT_OFFSET = 0;
62
70
  /** Whole-byte offset accepted by the result representation documented in
63
71
  * documentation/meta-tools.md#result-representation. */
@@ -101,17 +109,9 @@ export function alignEndToCharBoundary(bytes, offset, end, total) {
101
109
  }
102
110
  return e;
103
111
  }
104
- /** Prefix of the chunked paging envelope; `v1`'s single inline key still reads. */
105
- const RESULT_ENVELOPE_V2 = "connecta-result-v2:";
106
- /**
107
- * The current envelope: v2's header plus the inline cap of the call that
108
- * stashed it, `<total>:<chunk>:<cap>:`, so get_result can clamp a page to the
109
- * connector's own cap without the stash carrying connector identity.
110
- */
111
- const RESULT_ENVELOPE_V3 = "connecta-result-v3:";
112
- const RESULT_ENVELOPE_V3_HEADER = new RegExp(`^${RESULT_ENVELOPE_V3}(\\d+):(\\d+):(\\d+):`);
113
- /** `<total bytes>:<bytes per chunk>:` follow the prefix, then chunk 0's base64. */
114
- const RESULT_ENVELOPE_V2_HEADER = new RegExp(`^${RESULT_ENVELOPE_V2}(\\d+):(\\d+):`);
112
+ /** Every readable stash carries host-admitted bindings; older entries fail closed. */
113
+ const RESULT_ENVELOPE_V4 = "connecta-result-v4:";
114
+ const RESULT_ENVELOPE_V4_HEADER = /^connecta-result-v4:([A-Za-z0-9+/=]+):(\d+):(\d+):(\d+):/;
115
115
  /**
116
116
  * Smallest chunk of result text stored under one key. A multiple of three so
117
117
  * every chunk's base64 stands alone and a byte offset inside it lands on a
@@ -122,7 +122,7 @@ const RESULT_CHUNK_BYTES = 49_152;
122
122
  /**
123
123
  * Keys one stashed result may occupy. Chunking trades write count for read
124
124
  * count, and both are real: every chunk is a storage write at stash time, and
125
- * `fileStorage` rewrites its whole file per write. Above roughly 1.5 MB the
125
+ * a SQL store commits one row per write. Above roughly 1.5 MB the
126
126
  * chunks widen instead of multiplying, so a result costs a bounded number of
127
127
  * writes and a page still reads a small fraction of it.
128
128
  */
@@ -145,6 +145,23 @@ function base64Of(bytes) {
145
145
  }
146
146
  /** Opens a write's notice, ahead of any paging instruction. */
147
147
  const WRITE_ALREADY_RAN = "This write already ran: do not call it again to see its result.";
148
+ /** UTF-8 cannot represent lone UTF-16 surrogates without changing them. */
149
+ function isWellFormedText(text) {
150
+ const native = String.prototype.isWellFormed;
151
+ if (native)
152
+ return native.call(text);
153
+ for (let at = 0; at < text.length; at++) {
154
+ const unit = text.charCodeAt(at);
155
+ if (unit >= 0xd800 && unit <= 0xdbff) {
156
+ const next = text.charCodeAt(++at);
157
+ if (!(next >= 0xdc00 && next <= 0xdfff))
158
+ return false;
159
+ }
160
+ else if (unit >= 0xdc00 && unit <= 0xdfff)
161
+ return false;
162
+ }
163
+ return true;
164
+ }
148
165
  /**
149
166
  * Offers a read the route meta-tools.md prefers for finding something in it:
150
167
  * repeat the call inside a program and reduce it there. Paging comes second,
@@ -156,18 +173,20 @@ const WRITE_ALREADY_RAN = "This write already ran: do not call it again to see i
156
173
  const READ_REDUCE_FIRST = "To find something specific, repeat this read inside execute_code with connecta.call and filter or search the result there, returning only what matters";
157
174
  function pagingHint(results, totalBytes, preview) {
158
175
  if (!results.write) {
176
+ if (results.binding.program)
177
+ return "filter/map/slice data inside execute_code and return only what you need; page this completed return with connecta.result using nextAction.";
159
178
  const [shown, full] = preview.kind === "prefix"
160
179
  ? [`Bytes 0-${preview.bytes} of ${totalBytes} follow.`, "to read it in full, page the rest"]
161
180
  : preview.kind === "text-of-envelope"
162
181
  ? ["Its text follows.", `to read the full ${totalBytes}-byte content array as JSON, page it`]
163
182
  : [`The result is ${totalBytes} bytes.`, "to read it in full, page it"];
164
- return `${shown} ${READ_REDUCE_FIRST}; ${full} with get_result using nextAction.`;
183
+ return `${shown} ${READ_REDUCE_FIRST}; ${full} with connecta.result using nextAction.`;
165
184
  }
166
185
  const body = preview.kind === "prefix"
167
- ? `Bytes 0-${preview.bytes} of ${totalBytes} follow; page the rest with get_result using nextAction.`
186
+ ? `Bytes 0-${preview.bytes} of ${totalBytes} follow; page the rest with connecta.result using nextAction.`
168
187
  : preview.kind === "text-of-envelope"
169
- ? `Its text follows; page the full ${totalBytes}-byte content array as JSON with get_result using nextAction.`
170
- : `Page all ${totalBytes} bytes with get_result using nextAction.`;
188
+ ? `Its text follows; page the full ${totalBytes}-byte content array as JSON with connecta.result using nextAction.`
189
+ : `Page all ${totalBytes} bytes with connecta.result using nextAction.`;
171
190
  return `${WRITE_ALREADY_RAN} ${body}`;
172
191
  }
173
192
  /**
@@ -180,16 +199,32 @@ function pagingHint(results, totalBytes, preview) {
180
199
  * documentation/meta-tools.md#result-representation). It is one compact JSON
181
200
  * line with no raw newline, so the preview starts after the first `\n`.
182
201
  */
183
- async function stashResult(bytes, results, preview) {
202
+ async function stashResult(bytes, results, preview, rawText) {
184
203
  const totalBytes = bytes.length;
204
+ // Direct pages promise offsets into the original UTF-8 text. JSON-escaping
205
+ // raw text would change that contract; never stash the encoder's replacement
206
+ // characters as if they were the completed call's output.
207
+ if (rawText !== undefined && !isWellFormedText(rawText))
208
+ return {
209
+ truncated: true,
210
+ totalBytes,
211
+ hint: `${results.write ? WRITE_ALREADY_RAN + " " : ""}Paging is unavailable: text contains unpaired surrogates and can't be paged as text; request the value as JSON (value mode or a JSON-returning call) to recover it.`,
212
+ };
213
+ if (!results.pageable)
214
+ return {
215
+ truncated: true,
216
+ totalBytes,
217
+ hint: `${WRITE_ALREADY_RAN} Paging is unavailable for write results on read-only pools.`,
218
+ };
185
219
  const id = crypto.randomUUID();
186
220
  try {
187
221
  // Base64 permits byte-range decoding, and splitting the envelope across
188
222
  // keys keeps a page's storage read proportional to the page instead of to
189
- // the whole result (issue #540). Chunk 0 carries the header; the get_result
223
+ // the whole result (issue #540). Chunk 0 carries the header; the connecta.result
190
224
  // reader below maps a byte offset back to chunk index and base64 quad.
191
225
  const chunkBytes = resultChunkBytes(totalBytes);
192
- const chunks = [`${RESULT_ENVELOPE_V3}${totalBytes}:${chunkBytes}:${results.cap}:`];
226
+ const binding = base64Of(enc.encode(JSON.stringify(results.binding)));
227
+ const chunks = [`${RESULT_ENVELOPE_V4}${binding}:${totalBytes}:${chunkBytes}:${results.cap}:`];
193
228
  for (let offset = 0; offset < bytes.length; offset += chunkBytes) {
194
229
  const chunk = base64Of(bytes.subarray(offset, offset + chunkBytes));
195
230
  if (offset === 0)
@@ -197,7 +232,7 @@ async function stashResult(bytes, results, preview) {
197
232
  else
198
233
  chunks.push(chunk);
199
234
  }
200
- if (!await results.set(`result:${id}`, chunks, RESULT_TTL_SECONDS)) {
235
+ if (!(await results.set(id, chunks, RESULT_TTL_SECONDS))) {
201
236
  throw new Error("Result stash capacity exhausted");
202
237
  }
203
238
  }
@@ -223,11 +258,84 @@ async function stashResult(bytes, results, preview) {
223
258
  resultId: id,
224
259
  totalBytes,
225
260
  hint: pagingHint(results, totalBytes, preview),
261
+ nextOffset: preview.kind === "prefix" ? preview.bytes : 0,
226
262
  nextAction: {
227
- tool: "get_result",
228
- arguments: { id, offset: preview.kind === "prefix" ? preview.bytes : 0 },
263
+ tool: "execute_code",
264
+ // A byte may cost six JSON characters when escaped. Leave room for the
265
+ // result id, page counters, and continuation metadata in the returned page.
266
+ arguments: {
267
+ code: `async () => await connecta.result(${JSON.stringify(id)}, { offset: ${preview.kind === "prefix" ? preview.bytes : 0}, maxBytes: ${Math.floor((MAX_EXECUTE_RESULT_CHARS - 1024) / 6)} })`,
268
+ },
269
+ },
270
+ };
271
+ }
272
+ /** Program calls use the direct-call envelope, ledger, expiry and authority. */
273
+ export async function pageProgramResult(registry, baseUrl, resolved, value, format, secrets, trust, requestScope, wireBytes = PROGRAM_RESULT_WIRE_BYTES) {
274
+ const data = secrets.redact(value);
275
+ const wire = JSON.stringify({ data, format });
276
+ if (enc.encode(wire).length <= wireBytes)
277
+ return { data, format };
278
+ const text = secrets.text(format === "text" ? String(data) : serializeResultText(data));
279
+ const bytes = enc.encode(text);
280
+ const write = resolved.definition.classification !== "read";
281
+ const notice = await stashResult(bytes, {
282
+ pageable: !write || trust === "trusted",
283
+ binding: {
284
+ identity: registry.resultIdentity(),
285
+ baseUrl,
286
+ connector: resolved.connector.id,
287
+ tool: resolved.definition.name,
288
+ classification: write ? "write" : "read",
289
+ },
290
+ write,
291
+ cap: resolveMaxResultBytes(resolved.connector.maxResultBytes, registry.maxResultBytes),
292
+ set: (id, chunks, ttl) => registry.stashResult(id, chunks, ttl),
293
+ warn: () => logFailure(registry.contextFor(resolved.connector.id, baseUrl, requestScope).logger, "result paging unavailable", failureRecord({ connector: resolved.connector.id, tool: resolved.definition })),
294
+ }, { kind: "none" }, format === "text" ? text : undefined);
295
+ return {
296
+ format: "paged",
297
+ valueFormat: format,
298
+ ...(notice.resultId ? { resultId: notice.resultId } : {}),
299
+ totalBytes: bytes.length,
300
+ truncated: true,
301
+ hint: notice.resultId
302
+ ? `${write ? WRITE_ALREADY_RAN + " " : ""}Page this result with connecta.result(handle); do not repeat the call for output.`
303
+ : notice.hint,
304
+ };
305
+ }
306
+ /** Program returns share the direct stash, with all host-admitted call dependencies. */
307
+ export async function guardProgramReturn(value, registry, baseUrl, dependencies, secrets, trust, logger, maxChars, completedWrite) {
308
+ const serialized = serializeResultText(value);
309
+ if (serialized.length <= maxChars)
310
+ return value;
311
+ const text = secrets.text(serialized);
312
+ const bytes = enc.encode(text);
313
+ const write = dependencies.some((dependency) => dependency.classification === "write");
314
+ const cap = Math.min(registry.maxResultBytes, ...dependencies.map((dependency) => resolveMaxResultBytes(registry.getConnector(dependency.connector)?.maxResultBytes, registry.maxResultBytes)));
315
+ const results = {
316
+ secrets,
317
+ pageable: !write || trust === "trusted",
318
+ binding: {
319
+ identity: registry.resultIdentity(),
320
+ baseUrl,
321
+ classification: write ? "write" : "read",
322
+ program: dependencies,
229
323
  },
324
+ write: completedWrite,
325
+ cap,
326
+ set: (id, chunks, ttl) => registry.stashResult(id, chunks, ttl),
327
+ warn: () => logFailure(logger, "result paging unavailable", failureRecord({})),
230
328
  };
329
+ const notice = await stashResult(bytes, results, { kind: "none" }, text);
330
+ // Put the entire recovery route ahead of the preview in both response forms.
331
+ // A client retaining only the first 2,000 characters still sees the action.
332
+ if (notice.resultId) {
333
+ const base = { ...notice, totalChars: text.length, preview: "" };
334
+ const budget = Math.max(0, maxChars - JSON.stringify(base).length);
335
+ return { ...base, preview: escapedHeadOf(bytes, Math.min(512, budget)) };
336
+ }
337
+ const fallback = guardExecuteResultValue(secrets.redact(value), maxChars, notice.hint);
338
+ return { truncated: true, totalChars: fallback.totalChars, hint: fallback.hint, preview: fallback.preview };
231
339
  }
232
340
  /** The first `cap` bytes of `bytes`, ending on a character boundary. */
233
341
  function headOf(bytes, cap) {
@@ -235,11 +343,13 @@ function headOf(bytes, cap) {
235
343
  }
236
344
  /** One text block: the notice line, then the preview. */
237
345
  function noticeFirst(notice, preview) {
238
- return { content: [{ type: "text", text: `${JSON.stringify(notice)}\n${preview}` }] };
346
+ return {
347
+ content: [{ type: "text", text: `${JSON.stringify(notice)}\n${preview}` }],
348
+ };
239
349
  }
240
350
  /**
241
351
  * Return `text` as a single content block; if it exceeds `cap` bytes, stash the
242
- * full text and return a JSON truncation notice pointing at get_result,
352
+ * full text and return a JSON truncation notice pointing at connecta.result,
243
353
  * followed by the first `cap` bytes. The preview is a byte prefix of what
244
354
  * pages, so the notice's next action continues where it stops. `bytes` is
245
355
  * `text` already encoded, so a caller that had to measure it to make this
@@ -252,6 +362,10 @@ async function guardEncoded(text, bytes, results, cap) {
252
362
  truncated: false,
253
363
  };
254
364
  }
365
+ if (!isWellFormedText(text)) {
366
+ const notice = await stashResult(bytes, results, { kind: "none" }, text);
367
+ return { result: noticeFirst(notice, ""), truncated: true };
368
+ }
255
369
  const head = headOf(bytes, cap);
256
370
  const notice = await stashResult(bytes, results, {
257
371
  kind: "prefix",
@@ -266,21 +380,25 @@ async function guardText(text, results, cap) {
266
380
  // Normalizing at the door means the size check below always measures exactly
267
381
  // the text that is emitted, and no future caller can launder a non-string
268
382
  // through it the way issue #42 describes.
269
- const body = typeof text === "string" ? text : serializeResultText(text);
383
+ const serialized = typeof text === "string" ? text : serializeResultText(text);
384
+ const body = results.secrets?.text(serialized) ?? serialized;
270
385
  return guardEncoded(body, enc.encode(body), results, cap);
271
386
  }
272
387
  /** Store an oversized JSON value and replace it with a page handle. */
273
388
  async function guardValue(value, results, cap) {
274
- const text = serializeResultText(value);
389
+ const serialized = serializeResultText(value);
390
+ const text = results.secrets?.text(serialized) ?? serialized;
275
391
  const bytes = enc.encode(text);
276
392
  if (bytes.length <= cap)
277
- return { result: value, truncated: false };
393
+ return { result: results.secrets?.redact(value) ?? value, truncated: false };
278
394
  const notice = await stashResult(bytes, results, { kind: "none" });
279
395
  return {
280
- result: notice.resultId ? notice : {
281
- ...notice,
282
- preview: escapedHeadOf(bytes, cap),
283
- },
396
+ result: notice.resultId
397
+ ? notice
398
+ : {
399
+ ...notice,
400
+ preview: escapedHeadOf(bytes, cap),
401
+ },
284
402
  truncated: true,
285
403
  };
286
404
  }
@@ -317,12 +435,13 @@ function escapedHeadOf(bytes, cap) {
317
435
  *
318
436
  * Over the cap, what a client gets depends on whether a preview is usable.
319
437
  * Several text blocks preview as their text joined by newlines, while
320
- * get_result pages the envelope from offset 0, where the block boundaries
438
+ * connecta.result pages the envelope from offset 0, where the block boundaries
321
439
  * live. An envelope carrying non-text blocks is replaced by the notice alone:
322
440
  * the head of a half-written base64 image is of no use to anyone. Either way
323
- * the full envelope is stashed and pages through `get_result`.
441
+ * the full envelope is stashed and pages through `connecta.result`.
324
442
  */
325
443
  async function guardContent(content, results, cap) {
444
+ content = results.secrets?.redact({ content }).content ?? content;
326
445
  const only = content.length === 1 ? content[0] : undefined;
327
446
  if (only?.type === "text" && typeof only.text === "string") {
328
447
  const bytes = enc.encode(only.text);
@@ -333,7 +452,8 @@ async function guardContent(content, results, cap) {
333
452
  }
334
453
  let text;
335
454
  try {
336
- text = JSON.stringify(content);
455
+ const serialized = JSON.stringify(content);
456
+ text = results.secrets?.text(serialized) ?? serialized;
337
457
  }
338
458
  catch {
339
459
  // A block carrying a BigInt or a cycle cannot be serialized, so it cannot
@@ -362,13 +482,6 @@ async function guardContent(content, results, cap) {
362
482
  truncated: true,
363
483
  };
364
484
  }
365
- /**
366
- * The sentence that closes the OAuth handoff, telling the operator's agent how
367
- * to confirm the flow landed through the one surface it can call.
368
- */
369
- function oauthFollowUp(connectorId) {
370
- return `Then retry the original call; connecta.search({ connector: ${JSON.stringify(connectorId)} }) inside execute_code confirms the catalog now loads.`;
371
- }
372
485
  /**
373
486
  * Every meta-tool handler over a registry, one per registered tool. Exported for
374
487
  * direct testing; registerMetaTools() wires the six explicit tools onto an
@@ -385,40 +498,56 @@ function oauthFollowUp(connectorId) {
385
498
  * override each have one runtime source of truth.
386
499
  */
387
500
  export function createMetaTools(registry, baseUrl, opts = {}) {
501
+ return agentOutputOperations((requestScope) => metaToolsForRequest(registry, baseUrl, { ...opts, requestScope }), opts.requestScope);
502
+ }
503
+ function metaToolsForRequest(registry, baseUrl, opts = {}) {
388
504
  // Already normalized and warned about at registry construction.
389
505
  const globalCap = registry.maxResultBytes;
390
- const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs);
506
+ const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs) ?? CONFIG_DEFAULTS.execute.hostCallTimeoutMs;
391
507
  const probeTimeoutMs = normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
392
508
  const discoveryConcurrency = resolveDiscoveryConcurrency(opts.discoveryConcurrency);
393
509
  // createMetaTools() is called once per inbound MCP request. Sharing this
394
510
  // identity lets remote connectors reuse one downstream client inside that
395
511
  // request without leaking request-bound I/O into the next one.
396
- const requestScope = {};
512
+ const requestScope = opts.requestScope ?? {};
513
+ if (opts.canManageAuth || opts.oauthConnectUrl || opts.credentialHandoffUrl || opts.oauthConnectUnavailable) {
514
+ bindAuthorizationHandoffs(requestScope, (connector) => authorizationHandoff(registry, baseUrl, requestScope, opts, { connector }));
515
+ }
516
+ const sentSecrets = sentSecretsForRequest(requestScope);
517
+ const skills = new SkillsRegistry(registry, baseUrl, {
518
+ requestScope,
519
+ requestSignal: opts.requestSignal,
520
+ probeTimeoutMs,
521
+ defer: opts.defer,
522
+ });
397
523
  const catalog = new CatalogService(registry, baseUrl, {
398
524
  requestScope,
399
525
  probeTimeoutMs,
400
526
  concurrency: discoveryConcurrency,
401
527
  defer: opts.defer,
402
528
  requestSignal: opts.requestSignal,
403
- approval: opts.approval,
404
529
  // searchRoute keeps its top-level default. In-program callers use a
405
530
  // separate CatalogService configured for connecta.search.
406
531
  });
407
532
  const invocation = new InvocationService(registry, catalog, opts.activity);
408
533
  /** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
409
- async function runCall(call, source, options = {}) {
534
+ async function runCall(call, source) {
410
535
  const timeoutMs = normalizeTimeoutMs(call.timeoutMs) ?? defaultToolTimeoutMs;
411
536
  const outcome = await invocation.invoke(call.address, call.args ?? {}, {
412
537
  source,
413
- ...(options.allowDestructive !== undefined
414
- ? { allowDestructive: options.allowDestructive }
415
- : {}),
538
+ sentSecrets,
416
539
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
417
- ...(opts.requestSignal !== undefined
418
- ? { requestSignal: opts.requestSignal }
419
- : {}),
540
+ ...(opts.requestSignal !== undefined ? { requestSignal: opts.requestSignal } : {}),
420
541
  unwrapResult: call.resultMode === "value",
421
- processResult: async (result, resolved) => {
542
+ ...(opts.downstreamInput
543
+ ? {
544
+ processInputRequired: async (result, resolved, secrets) => {
545
+ await captureDownstreamInput(requestScope, resolved.connector.id, `${resolved.connector.id}.${resolved.toolName}`, result, secrets);
546
+ return { toolResult: { content: [] } };
547
+ },
548
+ }
549
+ : {}),
550
+ processResult: async (result, resolved, secrets, format) => {
422
551
  // Result-size cap for THIS call: the connector's own override wins,
423
552
  // then the deployment-wide value, then the built-in default (already
424
553
  // folded into `globalCap`). Resolved per call so one request can
@@ -427,13 +556,21 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
427
556
  // here, so the connector simply inherits `globalCap`.
428
557
  const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, globalCap);
429
558
  const results = {
430
- write: !isExplicitlyReadOnly(resolved.definition),
431
- cap,
432
- set: (key, value, ttlSeconds) => registry.stashResult(key, value, ttlSeconds),
433
- warn: () => registry.contextFor(resolved.connector.id, baseUrl, requestScope).logger.warn("[connecta] result paging unavailable", {
559
+ secrets,
560
+ pageable: resolved.definition.classification === "read" || opts.trust === "trusted",
561
+ binding: {
562
+ identity: registry.resultIdentity(),
563
+ baseUrl,
434
564
  connector: resolved.connector.id,
435
- tool: resolved.toolName,
436
- }),
565
+ tool: resolved.definition.name,
566
+ classification: resolved.definition.classification === "read" ? "read" : "write",
567
+ },
568
+ write: resolved.definition.classification !== "read",
569
+ cap,
570
+ set: (id, value, ttlSeconds) => registry.stashResult(id, value, ttlSeconds),
571
+ // The catalog entry, never `toolName`: a record names a tool
572
+ // only through the grammar check (src/operator-record.ts).
573
+ warn: () => logFailure(registry.contextFor(resolved.connector.id, baseUrl, requestScope).logger, "result paging unavailable", failureRecord({ connector: resolved.connector.id, tool: resolved.definition })),
437
574
  };
438
575
  const processed = (toolResult, truncated, value) => ({
439
576
  toolResult,
@@ -444,23 +581,29 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
444
581
  let value = result;
445
582
  const guarded = await guardValue(value, results, cap);
446
583
  value = guarded.result;
447
- return processed(jsonResult({ ok: true, data: value }), guarded.truncated, { value });
584
+ return processed(jsonResult({ ok: true, data: value, format }), guarded.truncated, { value });
448
585
  }
449
586
  if (resolved.connector.kind === "mcp") {
450
587
  const mcpResult = result;
451
588
  let content = mcpResult?.content ?? [];
452
- if (!content.some((block) => block.type === "text") &&
453
- mcpResult?.structuredContent !== undefined) {
454
- content = [...content, {
589
+ if (!content.some((block) => block.type === "text") && mcpResult?.structuredContent !== undefined) {
590
+ content = [
591
+ ...content,
592
+ {
455
593
  type: "text",
456
594
  text: JSON.stringify(mcpResult.structuredContent),
457
- }];
595
+ },
596
+ ];
458
597
  }
459
598
  const guarded = await guardContent(content, results, cap);
599
+ // Native blocks and bounded previews are the result. A format-only
600
+ // structuredContent hides them in clients that prefer that field.
601
+ guarded.result._meta = { "dev.connecta/format": format };
460
602
  return processed(guarded.result, guarded.truncated);
461
603
  }
462
604
  const value = result;
463
605
  const guarded = await guardText(serializeResultText(value), results, cap);
606
+ guarded.result._meta = { "dev.connecta/format": format };
464
607
  return processed(guarded.result, guarded.truncated, { value });
465
608
  },
466
609
  activityFriction: (processed) => processed.friction,
@@ -473,23 +616,17 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
473
616
  outcome.error.details !== undefined ||
474
617
  // So does a conflict's `current`: where things stand is the retry.
475
618
  outcome.error.current !== undefined ||
476
- [
477
- "auth_required",
478
- "invalid_args",
479
- "input_required_unsupported",
480
- ].includes(outcome.error.code);
481
- const makeFailedResult = () => recoveryRequired ||
482
- call.resultMode === "value"
483
- ? jsonResult({
484
- ok: false,
485
- error: outcome.error,
486
- durationMs: outcome.durationMs,
487
- attempts: outcome.attempts,
488
- ...(call.diagnostics ? { timing: outcome.timing } : {}),
489
- })
490
- : errorResult(outcome.error.message);
619
+ ["auth_required", "invalid_args", "input_required_unsupported"].includes(outcome.error.code);
620
+ // Every mode carries the complete repair envelope in both MCP forms.
621
+ const makeFailedResult = () => jsonResult({
622
+ ok: false,
623
+ error: outcome.error,
624
+ durationMs: outcome.durationMs,
625
+ attempts: outcome.attempts,
626
+ ...(call.diagnostics ? { timing: outcome.timing } : {}),
627
+ });
491
628
  let failedResult = makeFailedResult();
492
- // Value mode repeats the error in text and structuredContent. Account for
629
+ // Errors repeat in text and structuredContent. Account for
493
630
  // both copies and JSON escaping when the bounded provider reason is large.
494
631
  if (!recoveryRequired) {
495
632
  const cap = resolveMaxResultBytes(outcome.resolved?.connector.maxResultBytes, globalCap);
@@ -500,9 +637,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
500
637
  failedResult = makeFailedResult();
501
638
  }
502
639
  }
503
- if (recoveryRequired) {
504
- failedResult.isError = true;
505
- }
640
+ failedResult.isError = true;
506
641
  return {
507
642
  toolResult: failedResult,
508
643
  durationMs: outcome.durationMs,
@@ -515,6 +650,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
515
650
  ? jsonResult({
516
651
  ok: true,
517
652
  data: outcome.value.value,
653
+ format: outcome.format,
518
654
  durationMs: outcome.durationMs,
519
655
  attempts: outcome.attempts,
520
656
  ...(call.diagnostics ? { timing: outcome.timing } : {}),
@@ -525,37 +661,37 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
525
661
  durationMs: outcome.durationMs,
526
662
  attempts: outcome.attempts,
527
663
  timing: outcome.timing,
528
- ...(Object.prototype.hasOwnProperty.call(outcome.value, "value")
529
- ? { value: outcome.value.value }
530
- : {}),
664
+ ...(Object.prototype.hasOwnProperty.call(outcome.value, "value") ? { value: outcome.value.value } : {}),
531
665
  };
532
666
  }
533
667
  return {
534
668
  async skills(args = {}) {
535
- const connectors = registry.listConnectors();
536
- if (!args.name) {
537
- return {
538
- content: [
539
- {
540
- type: "text",
541
- text: 'Available skills. Fetch one with skills({ name: "<name>" }).\n\n' +
542
- listSkills(connectors)
543
- .map((skill) => `- \`${skill.name}\` — ${skill.description}`)
544
- .join("\n"),
545
- },
546
- ],
547
- };
669
+ try {
670
+ if (!args.name) {
671
+ const listing = await skills.summaries();
672
+ return {
673
+ structuredContent: { skills: listing },
674
+ content: [
675
+ {
676
+ type: "text",
677
+ text: 'Available skills. Fetch one with skills({ name: "<name>" }).\n\n' +
678
+ listing.map((skill) => `- \`${skill.name}\` — ${skill.description}`).join("\n"),
679
+ },
680
+ ],
681
+ };
682
+ }
683
+ const text = await skills.text(args.name);
684
+ return { content: [{ type: "text", text }], structuredContent: { name: args.name, format: "text", text } };
685
+ }
686
+ catch (error) {
687
+ return errorResult(error instanceof ConnectorCallError ? error.message : "Skills are unavailable.");
548
688
  }
549
- const skill = resolveSkill(args.name, connectors);
550
- if (!skill.found)
551
- return errorResult(skill.message);
552
- return { content: [{ type: "text", text: skill.content }] };
553
689
  },
554
690
  async searchTools(args) {
555
691
  if (args.connector !== undefined && enc.encode(args.connector).length > 512) {
556
692
  return discoveryErrorResult(new DiscoveryPolicyError("invalid_args", "connector must be at most 512 UTF-8 bytes."));
557
693
  }
558
- return discoveryResult(async () => groupedSearchResult(await catalog.search({
694
+ return discoveryResult(async () => flatSearchResult(await catalog.search({
559
695
  ...args,
560
696
  includeSchemaKeys: args.includeSchemas !== undefined,
561
697
  })), "Request a smaller limit, omit fullDescriptions, or use compact schemas.");
@@ -566,16 +702,12 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
566
702
  async callDestructiveTool(args) {
567
703
  // `reason` is read by the host's approval view and stops there — runCall
568
704
  // forwards only the call arguments, so it never reaches the connector.
569
- return (await runCall(args, "call_destructive_tool", { allowDestructive: true })).toolResult;
705
+ return (await runCall(args, "call_destructive_tool")).toolResult;
570
706
  },
571
- async getResult(args) {
572
- // Client-supplied page size and offset: normal input-validation errors,
573
- // not clamps. Callers arriving over MCP are rejected earlier by the
574
- // registered zod schema and never reach these branches, so they exist for
575
- // in-process callers of createMetaTools — which have no schema in front
576
- // of them — and to keep the rules true of the handler on its own terms.
577
- if (args.maxBytes !== undefined &&
578
- !isValidMaxResultBytes(args.maxBytes)) {
707
+ async readResult(args, options = {}) {
708
+ // Defense for in-process callers too: guest paging validates these domains
709
+ // before calling here, while internal consumers may call readResult directly.
710
+ if (args.maxBytes !== undefined && !isValidMaxResultBytes(args.maxBytes)) {
579
711
  return errorResult(`Invalid maxBytes ${args.maxBytes}: must be a whole number of bytes ` +
580
712
  `>= ${MIN_MAX_RESULT_BYTES}. Omit it to use the deployment default.`);
581
713
  }
@@ -583,6 +715,9 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
583
715
  return errorResult(`Invalid offset ${args.offset}: must be a whole number of bytes ` +
584
716
  `>= ${MIN_RESULT_OFFSET}. Omit it to start at the beginning.`);
585
717
  }
718
+ if (args.page !== undefined && (!Number.isSafeInteger(args.page) || args.page < 0 || args.offset !== undefined)) {
719
+ return errorResult("Use a whole page >= 0, without offset.");
720
+ }
586
721
  const results = registry.resultsStorage();
587
722
  const unavailableResult = () => ({
588
723
  ...jsonResult({
@@ -598,52 +733,124 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
598
733
  // is simply gone. Every key a page touches answers the same way, so a
599
734
  // backend that dies halfway through a multi-chunk page says so.
600
735
  const read = async (key) => {
736
+ options.signal?.throwIfAborted();
737
+ let value;
601
738
  try {
602
- return await results.get(key) ?? null;
739
+ value = (await results.get(key)) ?? null;
603
740
  }
604
741
  catch {
605
- return false;
742
+ value = false;
606
743
  }
744
+ // A storage driver may finish its pending read after cancellation. No
745
+ // subsequent chunk read or decode may start when that happens (INV-7).
746
+ options.signal?.throwIfAborted();
747
+ return value;
607
748
  };
608
- const stored = await read(`result:${args.id}`);
749
+ const stored = await read(resultKeys.chunk(args.id, 0));
609
750
  if (stored === false)
610
751
  return unavailableResult();
611
752
  if (stored === null) {
612
753
  return errorResult(`Unknown or expired result id "${boundedEchoText(args.id)}"`);
613
754
  }
614
- const requestedOffset = args.offset ?? 0;
615
- // Read and decode only the chunks this page covers, plus a few bytes of
616
- // UTF-8 boundary lookaround. Pre-upgrade entries stay readable for their
617
- // short TTL: v2 is this format without the recorded cap, v1 inlined the
618
- // whole envelope under one key, which is v2 with a single chunk as wide
619
- // as the result, and raw text before that still pays one full encode
620
- // per page.
621
- const current = RESULT_ENVELOPE_V3_HEADER.exec(stored.slice(0, 96));
622
- const chunked = current ?? RESULT_ENVELOPE_V2_HEADER.exec(stored.slice(0, 80));
623
- const inline = chunked ? null : /^connecta-result-v1:(\d+):/.exec(stored.slice(0, 64));
624
- const header = chunked ?? inline;
625
- // A page never exceeds the inline cap of the call that stashed it — the
626
- // connector's override when it had one — whatever maxBytes asks for.
627
- // Clients cut an oversized page exactly as they cut the original result,
628
- // so honouring a larger request would hand the agent an error in place
629
- // of its data. Entries from before the cap was recorded use the
630
- // deployment-wide one.
631
- const cap = current ? Number(current[3]) : globalCap;
755
+ const denied = () => errorResult(`Unknown or expired result id "${boundedEchoText(args.id)}"`);
756
+ const header = RESULT_ENVELOPE_V4_HEADER.exec(stored);
757
+ if (!header)
758
+ return denied();
759
+ let binding;
760
+ try {
761
+ binding = JSON.parse(dec.decode(Uint8Array.from(atob(header[1]), (char) => char.charCodeAt(0))));
762
+ }
763
+ catch {
764
+ return denied();
765
+ }
766
+ const identity = registry.resultIdentity();
767
+ if (!binding ||
768
+ binding.baseUrl !== baseUrl ||
769
+ !binding.identity ||
770
+ binding.identity.subject !== identity.subject ||
771
+ binding.identity.principal !== identity.principal ||
772
+ binding.identity.endpoint !== identity.endpoint ||
773
+ binding.identity.origin !== identity.origin ||
774
+ (binding.program === undefined
775
+ ? typeof binding.connector !== "string" || typeof binding.tool !== "string"
776
+ : !Array.isArray(binding.program) ||
777
+ binding.program.some((dependency) => !dependency ||
778
+ typeof dependency.connector !== "string" ||
779
+ (dependency.tool !== undefined && typeof dependency.tool !== "string") ||
780
+ (dependency.access !== undefined &&
781
+ (dependency.access !== "catalog" || dependency.classification !== "read")) ||
782
+ !["read", "write"].includes(dependency.classification))) ||
783
+ !["read", "write"].includes(binding.classification))
784
+ return denied();
785
+ const allowed = async () => {
786
+ options.signal?.throwIfAborted();
787
+ try {
788
+ if (binding.program &&
789
+ !(await registry.recheckResultAccess(undefined, binding.classification, options.signal)))
790
+ return false;
791
+ if (binding.classification === "write" && opts.trust !== "trusted")
792
+ return false;
793
+ const dependencies = binding.program ?? [binding];
794
+ for (const dependency of dependencies) {
795
+ const address = dependency.tool === undefined ? dependency.connector : `${dependency.connector}.${dependency.tool}`;
796
+ if (!(await registry.recheckResultAccess(address, dependency.classification, options.signal, dependency.access)))
797
+ return false;
798
+ if (dependency.tool === undefined) {
799
+ if (dependency.access === "catalog"
800
+ ? !registry.getConnector(dependency.connector)
801
+ : !registry.getResourceConnector(dependency.connector))
802
+ return false;
803
+ continue;
804
+ }
805
+ if (!registry.getConnector(dependency.connector))
806
+ return false;
807
+ const tools = await registry.getTools(dependency.connector, baseUrl, requestScope, options.signal ? { signal: options.signal } : {});
808
+ const tool = tools.find((tool) => tool.name === dependency.tool);
809
+ if (!tool ||
810
+ !(dependency.access === "catalog" ||
811
+ (dependency.classification === "read" && tool.classification === "read") ||
812
+ opts.trust === "trusted"))
813
+ return false;
814
+ }
815
+ return true;
816
+ }
817
+ catch {
818
+ options.signal?.throwIfAborted();
819
+ return false;
820
+ }
821
+ };
822
+ if (!(await allowed()))
823
+ return denied();
824
+ opts.onResultRead?.(binding.program ?? [binding]);
825
+ let requestedOffset = args.offset ?? 0;
826
+ const cap = Number(header[4]);
827
+ if (!isValidMaxResultBytes(cap))
828
+ return denied();
632
829
  const maxBytes = Math.min(args.maxBytes ?? cap, cap);
633
830
  let bytes;
634
831
  let total;
635
832
  let start = 0;
636
- if (header) {
637
- total = Number(header[1]);
638
- const chunkBytes = chunked ? Number(chunked[2]) : Math.max(total, 1);
833
+ {
834
+ total = Number(header[2]);
835
+ const chunkBytes = Number(header[3]);
836
+ if (!Number.isSafeInteger(total) ||
837
+ total < 0 ||
838
+ !Number.isSafeInteger(chunkBytes) ||
839
+ chunkBytes < 3 ||
840
+ chunkBytes % 3 !== 0)
841
+ return denied();
842
+ if (args.page !== undefined) {
843
+ // At least one complete UTF-8 codepoint per nominal window. Adjacent
844
+ // pages align both edges backwards, so no character repeats or vanishes.
845
+ const width = Math.max(4, maxBytes);
846
+ requestedOffset = Math.min(total, Math.min(args.page, Math.ceil(total / width)) * width);
847
+ }
639
848
  start = Math.floor(Math.max(0, Math.min(requestedOffset, total) - 3) / 3) * 3;
640
849
  const end = Math.min(total, requestedOffset + maxBytes + 4);
641
850
  bytes = new Uint8Array(Math.max(0, end - start));
642
851
  const lastChunk = Math.floor(Math.max(end - 1, start) / chunkBytes);
643
852
  for (let index = Math.floor(start / chunkBytes); index <= lastChunk; index++) {
644
- const encoded = index === 0
645
- ? stored.slice(header[0].length)
646
- : await read(`result:${args.id}#${index}`);
853
+ const encoded = index === 0 ? stored.slice(header[0].length) : await read(resultKeys.chunk(args.id, index));
647
854
  if (encoded === false)
648
855
  return unavailableResult();
649
856
  if (encoded === null) {
@@ -656,16 +863,12 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
656
863
  // alignment because every chunk boundary is a multiple of three.
657
864
  const from = Math.max(start, chunkStart) - chunkStart;
658
865
  const to = Math.min(end, chunkStart + chunkBytes, total) - chunkStart;
659
- const binary = atob(encoded.slice(from / 3 * 4, Math.ceil(to / 3) * 4));
866
+ const binary = atob(encoded.slice((from / 3) * 4, Math.ceil(to / 3) * 4));
660
867
  for (let at = from; at < to; at++) {
661
868
  bytes[chunkStart + at - start] = binary.charCodeAt(at - from);
662
869
  }
663
870
  }
664
871
  }
665
- else {
666
- bytes = enc.encode(stored);
667
- total = bytes.length;
668
- }
669
872
  // Validated above, so no coercion is needed here — only alignment. A
670
873
  // client that computes its own offsets can land inside a multi-byte
671
874
  // character, which would decode as U+FFFD; the offset actually served is
@@ -678,117 +881,45 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
678
881
  // never split across pages (which would emit U+FFFD on both sides).
679
882
  // `nextOffset` is this aligned end, so it is a valid boundary for the
680
883
  // next call and paging reassembles the original byte-for-byte.
681
- const end = start + alignEndToCharBoundary(bytes, offset - start, offset - start + maxBytes, total - start);
884
+ const end = args.page === undefined
885
+ ? start + alignEndToCharBoundary(bytes, offset - start, offset - start + maxBytes, total - start)
886
+ : start +
887
+ alignStartToCharBoundary(bytes, Math.min(total - start, requestedOffset - start + Math.max(4, maxBytes)));
682
888
  const slice = dec.decode(bytes.subarray(offset - start, end - start));
683
889
  const hasMore = end < total;
684
890
  // The same notice-first shape as a truncated call: one line of header,
685
891
  // then the page as raw text. A JSON `text` field escaped every quote and
686
892
  // newline in the page — a second layer over JSON payloads — which
687
893
  // inflated pages past the size clients accept and made them hard to read.
688
- return noticeFirst({
894
+ const pageHeader = {
689
895
  resultId: args.id,
690
896
  offset,
691
897
  bytes: Math.max(0, end - offset),
692
898
  totalBytes: total,
693
899
  hasMore,
694
- ...(hasMore
695
- ? { nextAction: { tool: "get_result", arguments: { id: args.id, offset: end } } }
696
- : {}),
697
- }, slice);
900
+ ...(hasMore ? { nextOffset: end } : {}),
901
+ };
902
+ if (!(await allowed()))
903
+ return denied();
904
+ const page = noticeFirst(pageHeader, slice);
905
+ page.structuredContent = { ...pageHeader, text: slice, format: "text" };
906
+ return page;
698
907
  },
699
908
  async authorizeConnector(args) {
700
- const connector = registry.getConnector(args.connector);
701
- if (!connector) {
702
- return errorResult(`Unknown connector "${boundedEchoText(args.connector)}"`);
703
- }
704
- if (!connector.startAuth) {
705
- if (!connector.credential) {
706
- return jsonResult({
707
- connector: connector.id,
708
- recovery: "unavailable",
709
- message: `Connector "${connector.id}" declares neither downstream OAuth ` +
710
- "nor an operator-managed credential slot. Update the connector " +
711
- "or deployment configuration before retrying.",
712
- });
713
- }
714
- const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
715
- if (!ctx.credential || !opts.credentialHandoffUrl) {
716
- return jsonResult({
717
- connector: connector.id,
718
- recovery: "unavailable",
719
- message: "Credential recovery needs both a vault and the optional UI. Configure " +
720
- "vault and ui in deployment code, then call " +
721
- "authorize_connector again.",
722
- });
723
- }
724
- const fields = connector.credential.fields?.map((field) => ({
725
- name: field.name,
726
- guidance: field.description ?? field.label,
727
- })) ?? [
728
- {
729
- name: "value",
730
- guidance: connector.credential.description ??
731
- connector.credential.label,
732
- },
733
- ];
734
- return jsonResult({
735
- connector: connector.id,
736
- recovery: "operator_config",
737
- credential: {
738
- label: connector.credential.label,
739
- fields,
740
- },
741
- operatorUrl: opts.credentialHandoffUrl,
742
- instructions: "Have the operator open operatorUrl, set and test the credential, " +
743
- "then retry the original call. No redeploy is needed. " +
744
- (connector.authScope === "personal"
745
- ? "Credential mutation requires the signed-in principal who owns this connection."
746
- : "Shared credential mutation requires a signed-in human with access to this connector."),
747
- });
748
- }
749
- if (!opts.canManageAuth?.(connector.id))
750
- return jsonResult({ connector: connector.id, recovery: "unavailable", message: "Your identity is not permitted to manage authentication for this connection." });
751
- const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
752
909
  try {
753
- const status = await connector.startAuth(ctx, args.force !== undefined ? { force: args.force } : {});
754
- if (status.authorizationUrl) {
755
- await registry.bindOAuthHandoff(connector.id, status.authorizationUrl);
756
- }
757
- if (status.state === "auth_required" && !status.authorizationUrl) {
758
- // auth_required with nothing to open is a dead end for the operator.
759
- return errorResult(`Connector "${connector.id}": authorization required but no URL is available — retry authorize_connector.`);
760
- }
761
- return jsonResult({
762
- connector: connector.id,
763
- recovery: "oauth",
764
- status: status.state,
765
- ...(status.authorizationUrl
766
- ? {
767
- authorizationUrl: status.authorizationUrl,
768
- instructions: "Have the operator open authorizationUrl in a browser and complete the consent flow. The provider then redirects back to this server's /oauth/callback/<connector> route, which finishes the flow automatically. " +
769
- oauthFollowUp(connector.id),
770
- }
771
- : {}),
772
- ...(status.message ? { message: status.message } : {}),
773
- });
910
+ return jsonResult(await authorizationHandoff(registry, baseUrl, requestScope, opts, args));
774
911
  }
775
912
  catch (err) {
776
913
  return errorResult(msg(err));
777
914
  }
778
- finally {
779
- // Auth state may have changed — even on a throw or a half-wiped force —
780
- // so don't serve a stale tool list.
781
- await registry.invalidateStored(connector.id);
782
- }
783
915
  },
784
916
  };
785
917
  }
786
- const SEARCH_DESC = `Use top-level search for catalog inspection or approval-required work before call_destructive_tool. Unknown-address read-only work belongs in one execute_code program that searches, calls, and returns the answer. Use 2–4 action/object terms and includeSchemas="compact"; the default limit is ${DEFAULT_SEARCH_LIMIT}. Set connector when known. safety="readOnly" finds tools that run unasked; "approvalRequired" finds the fail-closed complement. These filters grant no authority. Empty query browses.`;
787
- const CALL_DESC = 'Call one known-address tool explicitly annotated readOnlyHint: true. Use execute_code for unknown-address, multiple, dependent, or reduced read-only work. Unannotated or write-capable tools fail closed to call_destructive_tool. A truncated result carries a get_result action.';
918
+ const SEARCH_DESC = `Use top-level search for catalog inspection or approval-required work before call_destructive_tool. Unknown-address read-only work belongs in one execute_code program that searches, calls, and returns the answer. Use 2–4 action/object terms and includeSchemas="compact"; the default limit is ${DEFAULT_SEARCH_LIMIT}. Set connector when known. safety="readOnly" finds tools that run unasked; "approvalRequired" finds the fail-closed complement. These filters grant no authority. Empty query browses. Returns { catalogErrors, tools, total, offset, limit, hasMore }; read catalogErrors and absence before selecting a tool. Compact schemas have schemaFormat="text".`;
919
+ const CALL_DESC = "Call one known-address tool explicitly annotated readOnlyHint: true. Use execute_code for unknown-address, multiple, dependent, or reduced read-only work. Unannotated or write-capable tools fail closed to call_destructive_tool. A truncated result carries a connecta.result action.";
788
920
  const CALL_DESTRUCTIVE_DESC = "Call any tool not explicitly annotated readOnlyHint: true. Include a short reason for the human reviewer after checking the schema and consequences. The reason grants no authority and is not sent downstream.";
789
- const GET_RESULT_DESC = "Page a truncated direct-call result by id and byte offset. A program result is never paged; reduce it inside execute_code. Returns a one-line JSON header (offset, bytes, totalBytes, hasMore, nextAction) and then the page as raw text. maxBytes is an upper bound, clamped to the result's inline cap.";
790
- const AUTHORIZE_DESC = "Use after auth_required. Returns an OAuth or operator-credential handoff, or reports required deployment configuration. force=true restarts OAuth only; this tool never accepts credentials.";
791
- const SKILLS_DESC = 'List or fetch on-demand guidance. Fetch usage only when the always-loaded instructions are insufficient or a program needs repair.';
921
+ const AUTHORIZE_DESC = "Use after auth_required. Returns an OAuth or operator-credential handoff, or reports required deployment configuration. force=true requests an OAuth restart when the verified user opens the /connect URL; this tool never accepts credentials.";
922
+ const SKILLS_DESC = "List or fetch on-demand guidance. Fetch usage only when the always-loaded instructions are insufficient or a program needs repair.";
792
923
  /**
793
924
  * Sentences appended to a meta-tool description only when this connection
794
925
  * actually has connector guides. Tool descriptions are always-loaded context,
@@ -804,9 +935,7 @@ const GUIDE_NOTES = {
804
935
  };
805
936
  /** `base`, plus its guide note when any VISIBLE connector carries a guide. */
806
937
  function describedFor(registry, base, note) {
807
- return hasConnectorGuides(registry.listConnectors())
808
- ? base + GUIDE_NOTES[note]
809
- : base;
938
+ return hasConnectorGuides(registry.listConnectors()) ? base + GUIDE_NOTES[note] : base;
810
939
  }
811
940
  /**
812
941
  * Connecta refuses downstream tools that are not explicitly annotated
@@ -827,7 +956,7 @@ const READ_ONLY_LOCAL = {
827
956
  openWorldHint: false,
828
957
  };
829
958
  const CALL_INPUT_SCHEMA = {
830
- address: z.string(),
959
+ address: z.string().meta({ "x-mcp-header": "Address" }),
831
960
  args: z.record(z.string(), z.unknown()).optional(),
832
961
  resultMode: z.enum(["mcp", "value"]).optional(),
833
962
  timeoutMs: z.number().int().positive().optional(),
@@ -860,18 +989,9 @@ const AUTHORIZE_INPUT = advertisedSchema(z.object({
860
989
  connector: z.string(),
861
990
  force: z.boolean().optional(),
862
991
  }));
863
- const GET_RESULT_INPUT = advertisedSchema(z.object({
864
- id: z.string(),
865
- // Both bounds are the shared rules (isValidResultOffset,
866
- // isValidMaxResultBytes) expressed for the wire: spelling them against
867
- // the same constants keeps the schema from drifting away from the
868
- // in-handler checks if either floor ever moves.
869
- offset: z.number().int().min(MIN_RESULT_OFFSET).optional(),
870
- maxBytes: z.number().int().min(MIN_MAX_RESULT_BYTES).optional(),
871
- }));
872
992
  /**
873
- * Register the six explicit meta-tools onto an McpServer instance.
874
- * `registerExecuteTool` adds the seventh, `execute_code`. Broad discovery and
993
+ * Register the five explicit meta-tools onto an McpServer instance.
994
+ * `registerExecuteTool` adds the sixth, `execute_code`. Broad discovery and
875
995
  * multi-call work uses discovery and ordinary JavaScript promises inside a
876
996
  * program, which `execute_code` builds over the same
877
997
  * `CatalogService` and `InvocationService` these handlers use — one shared
@@ -879,33 +999,56 @@ const GET_RESULT_INPUT = advertisedSchema(z.object({
879
999
  */
880
1000
  export function registerMetaTools(server, registry, ctx) {
881
1001
  const mt = createMetaTools(registry, ctx.baseUrl, {
1002
+ downstreamInput: Boolean(ctx.authElicitation),
1003
+ requestScope: ctx.requestScope,
1004
+ trust: ctx.trust,
882
1005
  defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
883
1006
  probeTimeoutMs: ctx.probeTimeoutMs,
884
1007
  discoveryConcurrency: ctx.discoveryConcurrency,
885
1008
  activity: ctx.activity,
1009
+ client: ctx.client,
886
1010
  canManageAuth: ctx.canManageAuth,
887
1011
  credentialHandoffUrl: ctx.credentialHandoffUrl,
1012
+ oauthConnectUrl: ctx.oauthConnectUrl,
1013
+ oauthConnectUnavailable: ctx.oauthConnectUnavailable,
888
1014
  requestSignal: ctx.requestSignal,
889
1015
  defer: ctx.defer,
890
- approval: ctx.approval,
891
1016
  });
1017
+ const bindRequest = (request) => {
1018
+ if (ctx.client)
1019
+ bindMcpClient(request, ctx.client, ctx.activity);
1020
+ };
892
1021
  server.registerTool("skills", {
893
1022
  description: describedFor(registry, SKILLS_DESC, "skills"),
894
1023
  inputSchema: SKILLS_INPUT,
1024
+ outputSchema: SKILLS_OUTPUT,
895
1025
  annotations: READ_ONLY_LOCAL,
896
- }, async (args) => mt.skills(args));
1026
+ }, async (args, request) => {
1027
+ bindRequest(request);
1028
+ return mt.skills(args);
1029
+ });
897
1030
  server.registerTool("search_tools", {
898
1031
  description: describedFor(registry, SEARCH_DESC, "search"),
899
1032
  inputSchema: SEARCH_INPUT,
1033
+ outputSchema: SEARCH_OUTPUT,
900
1034
  annotations: READ_ONLY_REMOTE,
901
- }, async (args) => mt.searchTools(args));
1035
+ }, async (args, request) => {
1036
+ bindRequest(request);
1037
+ return mt.searchTools(args);
1038
+ });
902
1039
  server.registerTool("call_tool", {
903
1040
  description: CALL_DESC,
904
1041
  inputSchema: CALL_INPUT,
1042
+ // Default mode returns native content, so no structured result is promised.
905
1043
  // call_tool admits only tools that are themselves explicitly read-only;
906
1044
  // anything else is refused and routed to call_destructive_tool.
907
1045
  annotations: READ_ONLY_REMOTE,
908
- }, async (args) => mt.callTool(args));
1046
+ }, async (args, request) => {
1047
+ bindRequest(request);
1048
+ return ctx.authElicitation
1049
+ ? ctx.authElicitation.run("call_tool", args, request, () => mt.callTool(args))
1050
+ : mt.callTool(args);
1051
+ });
909
1052
  server.registerTool("call_destructive_tool", {
910
1053
  description: describedFor(registry, CALL_DESTRUCTIVE_DESC, "destructive"),
911
1054
  inputSchema: CALL_DESTRUCTIVE_INPUT,
@@ -914,18 +1057,22 @@ export function registerMetaTools(server, registry, ctx) {
914
1057
  readOnlyHint: false,
915
1058
  openWorldHint: true,
916
1059
  },
917
- }, async (args) => {
1060
+ }, async (args, request) => {
1061
+ bindRequest(request);
918
1062
  // `reason` is the host's to display and connecta's to keep out of the
919
1063
  // downstream call, so this destructuring is the whole of its handling:
920
1064
  // nothing below reads it. Dropping it is also what makes an empty or
921
1065
  // whitespace-only one "absent" rather than a validation failure — there
922
1066
  // is no field left for it to be absent from.
923
1067
  const { reason: _hostContext, ...call } = args;
924
- return mt.callDestructiveTool(call);
1068
+ return ctx.authElicitation
1069
+ ? ctx.authElicitation.run("call_destructive_tool", call, request, () => mt.callDestructiveTool(call))
1070
+ : mt.callDestructiveTool(call);
925
1071
  });
926
1072
  server.registerTool("authorize_connector", {
927
1073
  description: AUTHORIZE_DESC,
928
1074
  inputSchema: AUTHORIZE_INPUT,
1075
+ outputSchema: AUTHORIZE_OUTPUT,
929
1076
  // Starts (or with force, resets) a downstream OAuth flow — it changes
930
1077
  // stored connector auth state, so it is deliberately not read-only.
931
1078
  annotations: {
@@ -933,10 +1080,10 @@ export function registerMetaTools(server, registry, ctx) {
933
1080
  destructiveHint: false,
934
1081
  openWorldHint: true,
935
1082
  },
936
- }, async (args) => mt.authorizeConnector(args));
937
- server.registerTool("get_result", {
938
- description: GET_RESULT_DESC,
939
- inputSchema: GET_RESULT_INPUT,
940
- annotations: READ_ONLY_LOCAL,
941
- }, async (args) => mt.getResult(args));
1083
+ }, async (args, request) => {
1084
+ bindRequest(request);
1085
+ return ctx.authElicitation
1086
+ ? ctx.authElicitation.run("authorize_connector", args, request, () => mt.authorizeConnector(args))
1087
+ : mt.authorizeConnector(args);
1088
+ });
942
1089
  }