@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,16 +1,21 @@
1
+ import { bindMcpClient } from "./mcp-client-context.js";
2
+ import { CONFIG_DEFAULTS } from "./config-defaults.js";
1
3
  import { z } from "zod";
2
4
  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";
5
+ import { boundedDiscoveryText, CatalogService, DEFAULT_SEARCH_LIMIT, DiscoveryPolicyError, flatSearchResult, MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT, } from "./catalog-service.js";
4
6
  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";
7
+ import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
8
+ import { failureRecord, logFailure } from "./operator-record.js";
9
+ import { MAX_EXECUTE_RESULT_CHARS, serializeResultText } from "./executor-result.js";
10
+ import { InvocationService } from "./invocation.js";
8
11
  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;
12
+ import { hasConnectorGuides, SkillsRegistry } from "./skills.js";
13
+ import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs } from "./timeout.js";
14
+ import { AUTHORIZE_OUTPUT, CALL_OUTPUT, SEARCH_OUTPUT, SKILLS_OUTPUT } from "./meta-output.js";
15
+ import { RESULT_TTL_SECONDS, resultKeys } from "./storage/keys.js";
16
+ import { agentOutputOperations, sentSecretsForRequest } from "./sent-secrets.js";
17
+ import { captureDownstreamInput } from "./downstream-input.js";
18
+ export { MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT };
14
19
  const enc = new TextEncoder();
15
20
  const dec = new TextDecoder();
16
21
  export function jsonResult(obj, text = JSON.stringify(obj)) {
@@ -21,7 +26,7 @@ export function jsonResult(obj, text = JSON.stringify(obj)) {
21
26
  : {}),
22
27
  };
23
28
  }
24
- export function errorResult(message) {
29
+ function errorResult(message) {
25
30
  return { content: [{ type: "text", text: message }], isError: true };
26
31
  }
27
32
  function discoveryErrorResult(error) {
@@ -57,7 +62,7 @@ async function discoveryResult(operation, hint) {
57
62
  function isContinuationByte(b) {
58
63
  return b !== undefined && (b & 0xc0) === 0x80;
59
64
  }
60
- /** Smallest accepted `get_result` byte offset. */
65
+ /** Smallest accepted `connecta.result` byte offset. */
61
66
  const MIN_RESULT_OFFSET = 0;
62
67
  /** Whole-byte offset accepted by the result representation documented in
63
68
  * documentation/meta-tools.md#result-representation. */
@@ -101,17 +106,9 @@ export function alignEndToCharBoundary(bytes, offset, end, total) {
101
106
  }
102
107
  return e;
103
108
  }
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+):`);
109
+ /** Every readable stash carries host-admitted bindings; older entries fail closed. */
110
+ const RESULT_ENVELOPE_V4 = "connecta-result-v4:";
111
+ const RESULT_ENVELOPE_V4_HEADER = /^connecta-result-v4:([A-Za-z0-9+/=]+):(\d+):(\d+):(\d+):/;
115
112
  /**
116
113
  * Smallest chunk of result text stored under one key. A multiple of three so
117
114
  * every chunk's base64 stands alone and a byte offset inside it lands on a
@@ -122,7 +119,7 @@ const RESULT_CHUNK_BYTES = 49_152;
122
119
  /**
123
120
  * Keys one stashed result may occupy. Chunking trades write count for read
124
121
  * 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
122
+ * a SQL store commits one row per write. Above roughly 1.5 MB the
126
123
  * chunks widen instead of multiplying, so a result costs a bounded number of
127
124
  * writes and a page still reads a small fraction of it.
128
125
  */
@@ -161,13 +158,13 @@ function pagingHint(results, totalBytes, preview) {
161
158
  : preview.kind === "text-of-envelope"
162
159
  ? ["Its text follows.", `to read the full ${totalBytes}-byte content array as JSON, page it`]
163
160
  : [`The result is ${totalBytes} bytes.`, "to read it in full, page it"];
164
- return `${shown} ${READ_REDUCE_FIRST}; ${full} with get_result using nextAction.`;
161
+ return `${shown} ${READ_REDUCE_FIRST}; ${full} with connecta.result using nextAction.`;
165
162
  }
166
163
  const body = preview.kind === "prefix"
167
- ? `Bytes 0-${preview.bytes} of ${totalBytes} follow; page the rest with get_result using nextAction.`
164
+ ? `Bytes 0-${preview.bytes} of ${totalBytes} follow; page the rest with connecta.result using nextAction.`
168
165
  : 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.`;
166
+ ? `Its text follows; page the full ${totalBytes}-byte content array as JSON with connecta.result using nextAction.`
167
+ : `Page all ${totalBytes} bytes with connecta.result using nextAction.`;
171
168
  return `${WRITE_ALREADY_RAN} ${body}`;
172
169
  }
173
170
  /**
@@ -182,14 +179,21 @@ function pagingHint(results, totalBytes, preview) {
182
179
  */
183
180
  async function stashResult(bytes, results, preview) {
184
181
  const totalBytes = bytes.length;
182
+ if (!results.pageable)
183
+ return {
184
+ truncated: true,
185
+ totalBytes,
186
+ hint: `${WRITE_ALREADY_RAN} Paging is unavailable for write results on read-only pools.`,
187
+ };
185
188
  const id = crypto.randomUUID();
186
189
  try {
187
190
  // Base64 permits byte-range decoding, and splitting the envelope across
188
191
  // 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
192
+ // the whole result (issue #540). Chunk 0 carries the header; the connecta.result
190
193
  // reader below maps a byte offset back to chunk index and base64 quad.
191
194
  const chunkBytes = resultChunkBytes(totalBytes);
192
- const chunks = [`${RESULT_ENVELOPE_V3}${totalBytes}:${chunkBytes}:${results.cap}:`];
195
+ const binding = base64Of(enc.encode(JSON.stringify(results.binding)));
196
+ const chunks = [`${RESULT_ENVELOPE_V4}${binding}:${totalBytes}:${chunkBytes}:${results.cap}:`];
193
197
  for (let offset = 0; offset < bytes.length; offset += chunkBytes) {
194
198
  const chunk = base64Of(bytes.subarray(offset, offset + chunkBytes));
195
199
  if (offset === 0)
@@ -197,7 +201,7 @@ async function stashResult(bytes, results, preview) {
197
201
  else
198
202
  chunks.push(chunk);
199
203
  }
200
- if (!await results.set(`result:${id}`, chunks, RESULT_TTL_SECONDS)) {
204
+ if (!(await results.set(id, chunks, RESULT_TTL_SECONDS))) {
201
205
  throw new Error("Result stash capacity exhausted");
202
206
  }
203
207
  }
@@ -223,9 +227,14 @@ async function stashResult(bytes, results, preview) {
223
227
  resultId: id,
224
228
  totalBytes,
225
229
  hint: pagingHint(results, totalBytes, preview),
230
+ nextOffset: preview.kind === "prefix" ? preview.bytes : 0,
226
231
  nextAction: {
227
- tool: "get_result",
228
- arguments: { id, offset: preview.kind === "prefix" ? preview.bytes : 0 },
232
+ tool: "execute_code",
233
+ // A byte may cost six JSON characters when escaped. Leave room for the
234
+ // result id, page counters, and continuation metadata in the returned page.
235
+ arguments: {
236
+ code: `async () => await connecta.result(${JSON.stringify(id)}, { offset: ${preview.kind === "prefix" ? preview.bytes : 0}, maxBytes: ${Math.floor((MAX_EXECUTE_RESULT_CHARS - 1024) / 6)} })`,
237
+ },
229
238
  },
230
239
  };
231
240
  }
@@ -235,11 +244,14 @@ function headOf(bytes, cap) {
235
244
  }
236
245
  /** One text block: the notice line, then the preview. */
237
246
  function noticeFirst(notice, preview) {
238
- return { content: [{ type: "text", text: `${JSON.stringify(notice)}\n${preview}` }] };
247
+ return {
248
+ content: [{ type: "text", text: `${JSON.stringify(notice)}\n${preview}` }],
249
+ structuredContent: { ...notice, format: "text" },
250
+ };
239
251
  }
240
252
  /**
241
253
  * 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,
254
+ * full text and return a JSON truncation notice pointing at connecta.result,
243
255
  * followed by the first `cap` bytes. The preview is a byte prefix of what
244
256
  * pages, so the notice's next action continues where it stops. `bytes` is
245
257
  * `text` already encoded, so a caller that had to measure it to make this
@@ -266,21 +278,25 @@ async function guardText(text, results, cap) {
266
278
  // Normalizing at the door means the size check below always measures exactly
267
279
  // the text that is emitted, and no future caller can launder a non-string
268
280
  // through it the way issue #42 describes.
269
- const body = typeof text === "string" ? text : serializeResultText(text);
281
+ const serialized = typeof text === "string" ? text : serializeResultText(text);
282
+ const body = results.secrets?.text(serialized) ?? serialized;
270
283
  return guardEncoded(body, enc.encode(body), results, cap);
271
284
  }
272
285
  /** Store an oversized JSON value and replace it with a page handle. */
273
286
  async function guardValue(value, results, cap) {
274
- const text = serializeResultText(value);
287
+ const serialized = serializeResultText(value);
288
+ const text = results.secrets?.text(serialized) ?? serialized;
275
289
  const bytes = enc.encode(text);
276
290
  if (bytes.length <= cap)
277
- return { result: value, truncated: false };
291
+ return { result: results.secrets?.redact(value) ?? value, truncated: false };
278
292
  const notice = await stashResult(bytes, results, { kind: "none" });
279
293
  return {
280
- result: notice.resultId ? notice : {
281
- ...notice,
282
- preview: escapedHeadOf(bytes, cap),
283
- },
294
+ result: notice.resultId
295
+ ? notice
296
+ : {
297
+ ...notice,
298
+ preview: escapedHeadOf(bytes, cap),
299
+ },
284
300
  truncated: true,
285
301
  };
286
302
  }
@@ -317,12 +333,13 @@ function escapedHeadOf(bytes, cap) {
317
333
  *
318
334
  * Over the cap, what a client gets depends on whether a preview is usable.
319
335
  * Several text blocks preview as their text joined by newlines, while
320
- * get_result pages the envelope from offset 0, where the block boundaries
336
+ * connecta.result pages the envelope from offset 0, where the block boundaries
321
337
  * live. An envelope carrying non-text blocks is replaced by the notice alone:
322
338
  * 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`.
339
+ * the full envelope is stashed and pages through `connecta.result`.
324
340
  */
325
341
  async function guardContent(content, results, cap) {
342
+ content = results.secrets?.redact({ content }).content ?? content;
326
343
  const only = content.length === 1 ? content[0] : undefined;
327
344
  if (only?.type === "text" && typeof only.text === "string") {
328
345
  const bytes = enc.encode(only.text);
@@ -333,7 +350,8 @@ async function guardContent(content, results, cap) {
333
350
  }
334
351
  let text;
335
352
  try {
336
- text = JSON.stringify(content);
353
+ const serialized = JSON.stringify(content);
354
+ text = results.secrets?.text(serialized) ?? serialized;
337
355
  }
338
356
  catch {
339
357
  // A block carrying a BigInt or a cycle cannot be serialized, so it cannot
@@ -385,40 +403,53 @@ function oauthFollowUp(connectorId) {
385
403
  * override each have one runtime source of truth.
386
404
  */
387
405
  export function createMetaTools(registry, baseUrl, opts = {}) {
406
+ return agentOutputOperations((requestScope) => metaToolsForRequest(registry, baseUrl, { ...opts, requestScope }), opts.requestScope);
407
+ }
408
+ function metaToolsForRequest(registry, baseUrl, opts = {}) {
388
409
  // Already normalized and warned about at registry construction.
389
410
  const globalCap = registry.maxResultBytes;
390
- const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs);
411
+ const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs) ?? CONFIG_DEFAULTS.execute.hostCallTimeoutMs;
391
412
  const probeTimeoutMs = normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
392
413
  const discoveryConcurrency = resolveDiscoveryConcurrency(opts.discoveryConcurrency);
393
414
  // createMetaTools() is called once per inbound MCP request. Sharing this
394
415
  // identity lets remote connectors reuse one downstream client inside that
395
416
  // request without leaking request-bound I/O into the next one.
396
- const requestScope = {};
417
+ const requestScope = opts.requestScope ?? {};
418
+ const sentSecrets = sentSecretsForRequest(requestScope);
419
+ const skills = new SkillsRegistry(registry, baseUrl, {
420
+ requestScope,
421
+ requestSignal: opts.requestSignal,
422
+ probeTimeoutMs,
423
+ defer: opts.defer,
424
+ });
397
425
  const catalog = new CatalogService(registry, baseUrl, {
398
426
  requestScope,
399
427
  probeTimeoutMs,
400
428
  concurrency: discoveryConcurrency,
401
429
  defer: opts.defer,
402
430
  requestSignal: opts.requestSignal,
403
- approval: opts.approval,
404
431
  // searchRoute keeps its top-level default. In-program callers use a
405
432
  // separate CatalogService configured for connecta.search.
406
433
  });
407
434
  const invocation = new InvocationService(registry, catalog, opts.activity);
408
435
  /** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
409
- async function runCall(call, source, options = {}) {
436
+ async function runCall(call, source) {
410
437
  const timeoutMs = normalizeTimeoutMs(call.timeoutMs) ?? defaultToolTimeoutMs;
411
438
  const outcome = await invocation.invoke(call.address, call.args ?? {}, {
412
439
  source,
413
- ...(options.allowDestructive !== undefined
414
- ? { allowDestructive: options.allowDestructive }
415
- : {}),
440
+ sentSecrets,
416
441
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
417
- ...(opts.requestSignal !== undefined
418
- ? { requestSignal: opts.requestSignal }
419
- : {}),
442
+ ...(opts.requestSignal !== undefined ? { requestSignal: opts.requestSignal } : {}),
420
443
  unwrapResult: call.resultMode === "value",
421
- processResult: async (result, resolved) => {
444
+ ...(opts.downstreamInput
445
+ ? {
446
+ processInputRequired: async (result, resolved, secrets) => {
447
+ await captureDownstreamInput(requestScope, resolved.connector.id, `${resolved.connector.id}.${resolved.toolName}`, result, secrets);
448
+ return { toolResult: { content: [] } };
449
+ },
450
+ }
451
+ : {}),
452
+ processResult: async (result, resolved, secrets, format) => {
422
453
  // Result-size cap for THIS call: the connector's own override wins,
423
454
  // then the deployment-wide value, then the built-in default (already
424
455
  // folded into `globalCap`). Resolved per call so one request can
@@ -427,13 +458,21 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
427
458
  // here, so the connector simply inherits `globalCap`.
428
459
  const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, globalCap);
429
460
  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", {
461
+ secrets,
462
+ pageable: resolved.definition.classification === "read" || opts.trust === "trusted",
463
+ binding: {
464
+ identity: registry.resultIdentity(),
465
+ baseUrl,
434
466
  connector: resolved.connector.id,
435
- tool: resolved.toolName,
436
- }),
467
+ tool: resolved.definition.name,
468
+ classification: resolved.definition.classification === "read" ? "read" : "write",
469
+ },
470
+ write: resolved.definition.classification !== "read",
471
+ cap,
472
+ set: (id, value, ttlSeconds) => registry.stashResult(id, value, ttlSeconds),
473
+ // The catalog entry, never `toolName`: a record names a tool
474
+ // only through the grammar check (src/operator-record.ts).
475
+ warn: () => logFailure(registry.contextFor(resolved.connector.id, baseUrl, requestScope).logger, "result paging unavailable", failureRecord({ connector: resolved.connector.id, tool: resolved.definition })),
437
476
  };
438
477
  const processed = (toolResult, truncated, value) => ({
439
478
  toolResult,
@@ -444,23 +483,27 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
444
483
  let value = result;
445
484
  const guarded = await guardValue(value, results, cap);
446
485
  value = guarded.result;
447
- return processed(jsonResult({ ok: true, data: value }), guarded.truncated, { value });
486
+ return processed(jsonResult({ ok: true, data: value, format }), guarded.truncated, { value });
448
487
  }
449
488
  if (resolved.connector.kind === "mcp") {
450
489
  const mcpResult = result;
451
490
  let content = mcpResult?.content ?? [];
452
- if (!content.some((block) => block.type === "text") &&
453
- mcpResult?.structuredContent !== undefined) {
454
- content = [...content, {
491
+ if (!content.some((block) => block.type === "text") && mcpResult?.structuredContent !== undefined) {
492
+ content = [
493
+ ...content,
494
+ {
455
495
  type: "text",
456
496
  text: JSON.stringify(mcpResult.structuredContent),
457
- }];
497
+ },
498
+ ];
458
499
  }
459
500
  const guarded = await guardContent(content, results, cap);
501
+ guarded.result.structuredContent = { ...guarded.result.structuredContent, format };
460
502
  return processed(guarded.result, guarded.truncated);
461
503
  }
462
504
  const value = result;
463
505
  const guarded = await guardText(serializeResultText(value), results, cap);
506
+ guarded.result.structuredContent = { ...guarded.result.structuredContent, format };
464
507
  return processed(guarded.result, guarded.truncated, { value });
465
508
  },
466
509
  activityFriction: (processed) => processed.friction,
@@ -473,23 +516,17 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
473
516
  outcome.error.details !== undefined ||
474
517
  // So does a conflict's `current`: where things stand is the retry.
475
518
  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);
519
+ ["auth_required", "invalid_args", "input_required_unsupported"].includes(outcome.error.code);
520
+ // Every mode carries the complete repair envelope in both MCP forms.
521
+ const makeFailedResult = () => jsonResult({
522
+ ok: false,
523
+ error: outcome.error,
524
+ durationMs: outcome.durationMs,
525
+ attempts: outcome.attempts,
526
+ ...(call.diagnostics ? { timing: outcome.timing } : {}),
527
+ });
491
528
  let failedResult = makeFailedResult();
492
- // Value mode repeats the error in text and structuredContent. Account for
529
+ // Errors repeat in text and structuredContent. Account for
493
530
  // both copies and JSON escaping when the bounded provider reason is large.
494
531
  if (!recoveryRequired) {
495
532
  const cap = resolveMaxResultBytes(outcome.resolved?.connector.maxResultBytes, globalCap);
@@ -500,9 +537,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
500
537
  failedResult = makeFailedResult();
501
538
  }
502
539
  }
503
- if (recoveryRequired) {
504
- failedResult.isError = true;
505
- }
540
+ failedResult.isError = true;
506
541
  return {
507
542
  toolResult: failedResult,
508
543
  durationMs: outcome.durationMs,
@@ -515,6 +550,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
515
550
  ? jsonResult({
516
551
  ok: true,
517
552
  data: outcome.value.value,
553
+ format: outcome.format,
518
554
  durationMs: outcome.durationMs,
519
555
  attempts: outcome.attempts,
520
556
  ...(call.diagnostics ? { timing: outcome.timing } : {}),
@@ -525,37 +561,37 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
525
561
  durationMs: outcome.durationMs,
526
562
  attempts: outcome.attempts,
527
563
  timing: outcome.timing,
528
- ...(Object.prototype.hasOwnProperty.call(outcome.value, "value")
529
- ? { value: outcome.value.value }
530
- : {}),
564
+ ...(Object.prototype.hasOwnProperty.call(outcome.value, "value") ? { value: outcome.value.value } : {}),
531
565
  };
532
566
  }
533
567
  return {
534
568
  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
- };
569
+ try {
570
+ if (!args.name) {
571
+ const listing = await skills.summaries();
572
+ return {
573
+ structuredContent: { skills: listing },
574
+ content: [
575
+ {
576
+ type: "text",
577
+ text: 'Available skills. Fetch one with skills({ name: "<name>" }).\n\n' +
578
+ listing.map((skill) => `- \`${skill.name}\` — ${skill.description}`).join("\n"),
579
+ },
580
+ ],
581
+ };
582
+ }
583
+ const text = await skills.text(args.name);
584
+ return { content: [{ type: "text", text }], structuredContent: { name: args.name, format: "text", text } };
585
+ }
586
+ catch (error) {
587
+ return errorResult(error instanceof ConnectorCallError ? error.message : "Skills are unavailable.");
548
588
  }
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
589
  },
554
590
  async searchTools(args) {
555
591
  if (args.connector !== undefined && enc.encode(args.connector).length > 512) {
556
592
  return discoveryErrorResult(new DiscoveryPolicyError("invalid_args", "connector must be at most 512 UTF-8 bytes."));
557
593
  }
558
- return discoveryResult(async () => groupedSearchResult(await catalog.search({
594
+ return discoveryResult(async () => flatSearchResult(await catalog.search({
559
595
  ...args,
560
596
  includeSchemaKeys: args.includeSchemas !== undefined,
561
597
  })), "Request a smaller limit, omit fullDescriptions, or use compact schemas.");
@@ -566,16 +602,12 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
566
602
  async callDestructiveTool(args) {
567
603
  // `reason` is read by the host's approval view and stops there — runCall
568
604
  // forwards only the call arguments, so it never reaches the connector.
569
- return (await runCall(args, "call_destructive_tool", { allowDestructive: true })).toolResult;
605
+ return (await runCall(args, "call_destructive_tool")).toolResult;
570
606
  },
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)) {
607
+ async readResult(args, options = {}) {
608
+ // Defense for in-process callers too: guest paging validates these domains
609
+ // before calling here, while internal consumers may call readResult directly.
610
+ if (args.maxBytes !== undefined && !isValidMaxResultBytes(args.maxBytes)) {
579
611
  return errorResult(`Invalid maxBytes ${args.maxBytes}: must be a whole number of bytes ` +
580
612
  `>= ${MIN_MAX_RESULT_BYTES}. Omit it to use the deployment default.`);
581
613
  }
@@ -598,52 +630,90 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
598
630
  // is simply gone. Every key a page touches answers the same way, so a
599
631
  // backend that dies halfway through a multi-chunk page says so.
600
632
  const read = async (key) => {
633
+ options.signal?.throwIfAborted();
634
+ let value;
601
635
  try {
602
- return await results.get(key) ?? null;
636
+ value = (await results.get(key)) ?? null;
603
637
  }
604
638
  catch {
605
- return false;
639
+ value = false;
606
640
  }
641
+ // A storage driver may finish its pending read after cancellation. No
642
+ // subsequent chunk read or decode may start when that happens (INV-7).
643
+ options.signal?.throwIfAborted();
644
+ return value;
607
645
  };
608
- const stored = await read(`result:${args.id}`);
646
+ const stored = await read(resultKeys.chunk(args.id, 0));
609
647
  if (stored === false)
610
648
  return unavailableResult();
611
649
  if (stored === null) {
612
650
  return errorResult(`Unknown or expired result id "${boundedEchoText(args.id)}"`);
613
651
  }
652
+ const denied = () => errorResult(`Unknown or expired result id "${boundedEchoText(args.id)}"`);
653
+ const header = RESULT_ENVELOPE_V4_HEADER.exec(stored);
654
+ if (!header)
655
+ return denied();
656
+ let binding;
657
+ try {
658
+ binding = JSON.parse(dec.decode(Uint8Array.from(atob(header[1]), (char) => char.charCodeAt(0))));
659
+ }
660
+ catch {
661
+ return denied();
662
+ }
663
+ const identity = registry.resultIdentity();
664
+ if (!binding ||
665
+ binding.baseUrl !== baseUrl ||
666
+ !binding.identity ||
667
+ binding.identity.subject !== identity.subject ||
668
+ binding.identity.principal !== identity.principal ||
669
+ binding.identity.endpoint !== identity.endpoint ||
670
+ binding.identity.origin !== identity.origin ||
671
+ typeof binding.connector !== "string" ||
672
+ typeof binding.tool !== "string" ||
673
+ !["read", "write"].includes(binding.classification))
674
+ return denied();
675
+ const allowed = async () => {
676
+ options.signal?.throwIfAborted();
677
+ try {
678
+ const address = `${binding.connector}.${binding.tool}`;
679
+ if (!(await registry.recheckResultAccess(address, binding.classification, options.signal)))
680
+ return false;
681
+ if (!registry.getConnector(binding.connector))
682
+ return false;
683
+ const tools = await registry.getTools(binding.connector, baseUrl, requestScope, options.signal ? { signal: options.signal } : {});
684
+ const tool = tools.find((tool) => tool.name === binding.tool);
685
+ return Boolean(tool && ((binding.classification === "read" && tool.classification === "read") || opts.trust === "trusted"));
686
+ }
687
+ catch {
688
+ options.signal?.throwIfAborted();
689
+ return false;
690
+ }
691
+ };
692
+ if (!(await allowed()))
693
+ return denied();
614
694
  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;
695
+ const cap = Number(header[4]);
696
+ if (!isValidMaxResultBytes(cap))
697
+ return denied();
632
698
  const maxBytes = Math.min(args.maxBytes ?? cap, cap);
633
699
  let bytes;
634
700
  let total;
635
701
  let start = 0;
636
- if (header) {
637
- total = Number(header[1]);
638
- const chunkBytes = chunked ? Number(chunked[2]) : Math.max(total, 1);
702
+ {
703
+ total = Number(header[2]);
704
+ const chunkBytes = Number(header[3]);
705
+ if (!Number.isSafeInteger(total) ||
706
+ total < 0 ||
707
+ !Number.isSafeInteger(chunkBytes) ||
708
+ chunkBytes < 3 ||
709
+ chunkBytes % 3 !== 0)
710
+ return denied();
639
711
  start = Math.floor(Math.max(0, Math.min(requestedOffset, total) - 3) / 3) * 3;
640
712
  const end = Math.min(total, requestedOffset + maxBytes + 4);
641
713
  bytes = new Uint8Array(Math.max(0, end - start));
642
714
  const lastChunk = Math.floor(Math.max(end - 1, start) / chunkBytes);
643
715
  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}`);
716
+ const encoded = index === 0 ? stored.slice(header[0].length) : await read(resultKeys.chunk(args.id, index));
647
717
  if (encoded === false)
648
718
  return unavailableResult();
649
719
  if (encoded === null) {
@@ -656,16 +726,12 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
656
726
  // alignment because every chunk boundary is a multiple of three.
657
727
  const from = Math.max(start, chunkStart) - chunkStart;
658
728
  const to = Math.min(end, chunkStart + chunkBytes, total) - chunkStart;
659
- const binary = atob(encoded.slice(from / 3 * 4, Math.ceil(to / 3) * 4));
729
+ const binary = atob(encoded.slice((from / 3) * 4, Math.ceil(to / 3) * 4));
660
730
  for (let at = from; at < to; at++) {
661
731
  bytes[chunkStart + at - start] = binary.charCodeAt(at - from);
662
732
  }
663
733
  }
664
734
  }
665
- else {
666
- bytes = enc.encode(stored);
667
- total = bytes.length;
668
- }
669
735
  // Validated above, so no coercion is needed here — only alignment. A
670
736
  // client that computes its own offsets can land inside a multi-byte
671
737
  // character, which would decode as U+FFFD; the offset actually served is
@@ -685,16 +751,19 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
685
751
  // then the page as raw text. A JSON `text` field escaped every quote and
686
752
  // newline in the page — a second layer over JSON payloads — which
687
753
  // inflated pages past the size clients accept and made them hard to read.
688
- return noticeFirst({
754
+ const pageHeader = {
689
755
  resultId: args.id,
690
756
  offset,
691
757
  bytes: Math.max(0, end - offset),
692
758
  totalBytes: total,
693
759
  hasMore,
694
- ...(hasMore
695
- ? { nextAction: { tool: "get_result", arguments: { id: args.id, offset: end } } }
696
- : {}),
697
- }, slice);
760
+ ...(hasMore ? { nextOffset: end } : {}),
761
+ };
762
+ if (!(await allowed()))
763
+ return denied();
764
+ const page = noticeFirst(pageHeader, slice);
765
+ page.structuredContent = { ...pageHeader, text: slice, format: "text" };
766
+ return page;
698
767
  },
699
768
  async authorizeConnector(args) {
700
769
  const connector = registry.getConnector(args.connector);
@@ -727,8 +796,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
727
796
  })) ?? [
728
797
  {
729
798
  name: "value",
730
- guidance: connector.credential.description ??
731
- connector.credential.label,
799
+ guidance: connector.credential.description ?? connector.credential.label,
732
800
  },
733
801
  ];
734
802
  return jsonResult({
@@ -746,49 +814,40 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
746
814
  : "Shared credential mutation requires a signed-in human with access to this connector."),
747
815
  });
748
816
  }
817
+ if (opts.oauthConnectUnavailable || !opts.oauthConnectUrl)
818
+ return jsonResult({
819
+ connector: connector.id,
820
+ recovery: "unavailable",
821
+ message: opts.oauthConnectUnavailable ??
822
+ "OAuth connection requires an interactive provider and a credential vault with a handoff signing key.",
823
+ });
749
824
  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);
825
+ return jsonResult({
826
+ connector: connector.id,
827
+ recovery: "unavailable",
828
+ message: "Your identity is not permitted to manage authentication for this connection.",
829
+ });
752
830
  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
831
  return jsonResult({
762
832
  connector: connector.id,
763
833
  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 } : {}),
834
+ status: "auth_required",
835
+ authorizationUrl: await opts.oauthConnectUrl(connector.id, args.force),
836
+ instructions: "Open authorizationUrl in a browser and sign in as the user who requested this connection. Connecta verifies your identity and permission before starting consent. " +
837
+ oauthFollowUp(connector.id),
773
838
  });
774
839
  }
775
840
  catch (err) {
776
841
  return errorResult(msg(err));
777
842
  }
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
843
  },
784
844
  };
785
845
  }
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.';
846
+ 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".`;
847
+ 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
848
  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.';
849
+ 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.";
850
+ 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
851
  /**
793
852
  * Sentences appended to a meta-tool description only when this connection
794
853
  * actually has connector guides. Tool descriptions are always-loaded context,
@@ -804,9 +863,7 @@ const GUIDE_NOTES = {
804
863
  };
805
864
  /** `base`, plus its guide note when any VISIBLE connector carries a guide. */
806
865
  function describedFor(registry, base, note) {
807
- return hasConnectorGuides(registry.listConnectors())
808
- ? base + GUIDE_NOTES[note]
809
- : base;
866
+ return hasConnectorGuides(registry.listConnectors()) ? base + GUIDE_NOTES[note] : base;
810
867
  }
811
868
  /**
812
869
  * Connecta refuses downstream tools that are not explicitly annotated
@@ -827,7 +884,7 @@ const READ_ONLY_LOCAL = {
827
884
  openWorldHint: false,
828
885
  };
829
886
  const CALL_INPUT_SCHEMA = {
830
- address: z.string(),
887
+ address: z.string().meta({ "x-mcp-header": "Address" }),
831
888
  args: z.record(z.string(), z.unknown()).optional(),
832
889
  resultMode: z.enum(["mcp", "value"]).optional(),
833
890
  timeoutMs: z.number().int().positive().optional(),
@@ -860,18 +917,9 @@ const AUTHORIZE_INPUT = advertisedSchema(z.object({
860
917
  connector: z.string(),
861
918
  force: z.boolean().optional(),
862
919
  }));
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
920
  /**
873
- * Register the six explicit meta-tools onto an McpServer instance.
874
- * `registerExecuteTool` adds the seventh, `execute_code`. Broad discovery and
921
+ * Register the five explicit meta-tools onto an McpServer instance.
922
+ * `registerExecuteTool` adds the sixth, `execute_code`. Broad discovery and
875
923
  * multi-call work uses discovery and ordinary JavaScript promises inside a
876
924
  * program, which `execute_code` builds over the same
877
925
  * `CatalogService` and `InvocationService` these handlers use — one shared
@@ -879,53 +927,81 @@ const GET_RESULT_INPUT = advertisedSchema(z.object({
879
927
  */
880
928
  export function registerMetaTools(server, registry, ctx) {
881
929
  const mt = createMetaTools(registry, ctx.baseUrl, {
930
+ downstreamInput: Boolean(ctx.authElicitation),
931
+ requestScope: ctx.requestScope,
932
+ trust: ctx.trust,
882
933
  defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
883
934
  probeTimeoutMs: ctx.probeTimeoutMs,
884
935
  discoveryConcurrency: ctx.discoveryConcurrency,
885
936
  activity: ctx.activity,
937
+ client: ctx.client,
886
938
  canManageAuth: ctx.canManageAuth,
887
939
  credentialHandoffUrl: ctx.credentialHandoffUrl,
940
+ oauthConnectUrl: ctx.oauthConnectUrl,
941
+ oauthConnectUnavailable: ctx.oauthConnectUnavailable,
888
942
  requestSignal: ctx.requestSignal,
889
943
  defer: ctx.defer,
890
- approval: ctx.approval,
891
944
  });
945
+ const bindRequest = (request) => {
946
+ if (ctx.client)
947
+ bindMcpClient(request, ctx.client, ctx.activity);
948
+ };
892
949
  server.registerTool("skills", {
893
950
  description: describedFor(registry, SKILLS_DESC, "skills"),
894
951
  inputSchema: SKILLS_INPUT,
952
+ outputSchema: SKILLS_OUTPUT,
895
953
  annotations: READ_ONLY_LOCAL,
896
- }, async (args) => mt.skills(args));
954
+ }, async (args, request) => {
955
+ bindRequest(request);
956
+ return mt.skills(args);
957
+ });
897
958
  server.registerTool("search_tools", {
898
959
  description: describedFor(registry, SEARCH_DESC, "search"),
899
960
  inputSchema: SEARCH_INPUT,
961
+ outputSchema: SEARCH_OUTPUT,
900
962
  annotations: READ_ONLY_REMOTE,
901
- }, async (args) => mt.searchTools(args));
963
+ }, async (args, request) => {
964
+ bindRequest(request);
965
+ return mt.searchTools(args);
966
+ });
902
967
  server.registerTool("call_tool", {
903
968
  description: CALL_DESC,
904
969
  inputSchema: CALL_INPUT,
970
+ outputSchema: CALL_OUTPUT,
905
971
  // call_tool admits only tools that are themselves explicitly read-only;
906
972
  // anything else is refused and routed to call_destructive_tool.
907
973
  annotations: READ_ONLY_REMOTE,
908
- }, async (args) => mt.callTool(args));
974
+ }, async (args, request) => {
975
+ bindRequest(request);
976
+ return ctx.authElicitation
977
+ ? ctx.authElicitation.run("call_tool", args, request, () => mt.callTool(args))
978
+ : mt.callTool(args);
979
+ });
909
980
  server.registerTool("call_destructive_tool", {
910
981
  description: describedFor(registry, CALL_DESTRUCTIVE_DESC, "destructive"),
911
982
  inputSchema: CALL_DESTRUCTIVE_INPUT,
983
+ outputSchema: CALL_OUTPUT,
912
984
  annotations: {
913
985
  destructiveHint: true,
914
986
  readOnlyHint: false,
915
987
  openWorldHint: true,
916
988
  },
917
- }, async (args) => {
989
+ }, async (args, request) => {
990
+ bindRequest(request);
918
991
  // `reason` is the host's to display and connecta's to keep out of the
919
992
  // downstream call, so this destructuring is the whole of its handling:
920
993
  // nothing below reads it. Dropping it is also what makes an empty or
921
994
  // whitespace-only one "absent" rather than a validation failure — there
922
995
  // is no field left for it to be absent from.
923
996
  const { reason: _hostContext, ...call } = args;
924
- return mt.callDestructiveTool(call);
997
+ return ctx.authElicitation
998
+ ? ctx.authElicitation.run("call_destructive_tool", call, request, () => mt.callDestructiveTool(call))
999
+ : mt.callDestructiveTool(call);
925
1000
  });
926
1001
  server.registerTool("authorize_connector", {
927
1002
  description: AUTHORIZE_DESC,
928
1003
  inputSchema: AUTHORIZE_INPUT,
1004
+ outputSchema: AUTHORIZE_OUTPUT,
929
1005
  // Starts (or with force, resets) a downstream OAuth flow — it changes
930
1006
  // stored connector auth state, so it is deliberately not read-only.
931
1007
  annotations: {
@@ -933,10 +1009,10 @@ export function registerMetaTools(server, registry, ctx) {
933
1009
  destructiveHint: false,
934
1010
  openWorldHint: true,
935
1011
  },
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));
1012
+ }, async (args, request) => {
1013
+ bindRequest(request);
1014
+ return ctx.authElicitation
1015
+ ? ctx.authElicitation.run("authorize_connector", args, request, () => mt.authorizeConnector(args))
1016
+ : mt.authorizeConnector(args);
1017
+ });
942
1018
  }