@zackbart/connecta 0.28.1 → 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 (319) hide show
  1. package/CHANGELOG.md +788 -5
  2. package/README.md +62 -42
  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 +1584 -1580
  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 +93 -71
  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 +59 -7
  52. package/dist/connectors/api-connector.js +106 -19
  53. package/dist/connectors/api.js +4 -1
  54. package/dist/connectors/guarded-fetch.d.ts +144 -0
  55. package/dist/connectors/guarded-fetch.js +84 -35
  56. package/dist/connectors/negotiation-cache.js +46 -0
  57. package/dist/connectors/option-shapes.js +27 -0
  58. package/dist/connectors/remote-mcp.d.ts +30 -4
  59. package/dist/connectors/remote-mcp.js +1774 -320
  60. package/dist/connectors/resource-uri.js +335 -0
  61. package/dist/credential-contract.d.ts +10 -0
  62. package/dist/credential-rules.js +1 -3
  63. package/dist/credential-url.js +108 -0
  64. package/dist/credentials.d.ts +6 -1
  65. package/dist/credentials.js +27 -18
  66. package/dist/d1.d.ts +130 -0
  67. package/dist/d1.js +290 -0
  68. package/dist/describe-config.d.ts +165 -0
  69. package/dist/describe-config.js +315 -0
  70. package/dist/described.js +106 -0
  71. package/dist/downstream-input-context.js +119 -0
  72. package/dist/downstream-input.js +323 -0
  73. package/dist/errors.d.ts +97 -20
  74. package/dist/errors.js +158 -80
  75. package/dist/execute.js +453 -305
  76. package/dist/executor-admission.d.ts +1 -0
  77. package/dist/executor-admission.js +11 -15
  78. package/dist/executor-contract.d.ts +20 -0
  79. package/dist/executor-contract.js +48 -0
  80. package/dist/executor-result.js +24 -9
  81. package/dist/executors/quickjs-child.js +3 -4
  82. package/dist/executors/quickjs-runtime.js +147 -59
  83. package/dist/executors/quickjs.js +29 -35
  84. package/dist/favicon.js +1 -2
  85. package/dist/fix-prompt.js +1 -3
  86. package/dist/guest-runtime.js +188 -0
  87. package/dist/guest-types.d.ts +176 -0
  88. package/dist/guest-types.js +1 -0
  89. package/dist/html-security.js +24 -0
  90. package/dist/identity.js +1 -3
  91. package/dist/inbound-credential.js +52 -0
  92. package/dist/index.d.ts +24 -256
  93. package/dist/index.js +65 -478
  94. package/dist/invocation-auth.js +69 -0
  95. package/dist/invocation.js +230 -73
  96. package/dist/json-schema.d.ts +1 -1
  97. package/dist/mcp-client-context.js +35 -0
  98. package/dist/mcp-result.js +11 -7
  99. package/dist/meta-output.js +76 -0
  100. package/dist/meta-tool-names.d.ts +2 -0
  101. package/dist/meta-tool-names.js +3 -0
  102. package/dist/meta-tools.js +285 -209
  103. package/dist/module-contracts.d.ts +8 -23
  104. package/dist/node.d.ts +0 -2
  105. package/dist/node.js +2 -2
  106. package/dist/oauth-callback-outcome.js +1 -3
  107. package/dist/oauth-handoff.js +117 -0
  108. package/dist/oauth-sealing.js +20 -0
  109. package/dist/operator-record.d.ts +137 -0
  110. package/dist/operator-record.js +420 -0
  111. package/dist/operator-ui/assets.js +31 -0
  112. package/dist/operator-ui/contract.d.ts +72 -0
  113. package/dist/operator-ui/contract.js +1 -0
  114. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  115. package/dist/operator-ui/generated.js +5 -4
  116. package/dist/operator-ui/model.d.ts +12 -87
  117. package/dist/operator-ui/model.js +2 -9
  118. package/dist/operator-ui/setup-commands.js +1 -3
  119. package/dist/operator-ui/view.d.ts +323 -0
  120. package/dist/operator-ui/view.js +62 -189
  121. package/dist/page-styles.js +2 -3
  122. package/dist/program-source.js +14 -2
  123. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  124. package/dist/provider.d.ts +142 -0
  125. package/dist/provider.js +187 -0
  126. package/dist/providers/_shared/google/result-size.js +57 -0
  127. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  128. package/dist/providers/_shared/google/service-account.js +500 -0
  129. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  130. package/dist/providers/_shared/google/workspace.js +518 -0
  131. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  132. package/dist/providers/basecamp/index.js +997 -0
  133. package/dist/providers/basecamp/skill.generated.js +9 -0
  134. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  135. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  136. package/dist/providers/breeze/skill.generated.js +9 -0
  137. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  138. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  139. package/dist/providers/ccb/skill.generated.js +9 -0
  140. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  141. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  142. package/dist/providers/cloudflare/skill.generated.js +10 -0
  143. package/dist/providers/docs/index.d.ts +54 -0
  144. package/dist/providers/docs/index.js +1237 -0
  145. package/dist/providers/docs/skill.generated.js +9 -0
  146. package/dist/providers/drive/index.d.ts +53 -0
  147. package/dist/providers/drive/index.js +1523 -0
  148. package/dist/providers/drive/skill.generated.js +9 -0
  149. package/dist/providers/forms/index.d.ts +54 -0
  150. package/dist/providers/forms/index.js +1217 -0
  151. package/dist/providers/forms/skill.generated.js +11 -0
  152. package/dist/providers/github/auth.d.ts +32 -0
  153. package/dist/providers/github/auth.js +361 -0
  154. package/dist/providers/github/hosted.js +218 -0
  155. package/dist/providers/github/index.d.ts +13 -0
  156. package/dist/providers/github/index.js +140 -0
  157. package/dist/providers/github/key.js +100 -0
  158. package/dist/providers/github/rest.js +321 -0
  159. package/dist/providers/github/scope.d.ts +29 -0
  160. package/dist/providers/github/scope.js +124 -0
  161. package/dist/providers/github/skill.generated.js +8 -0
  162. package/dist/providers/gmail/index.d.ts +51 -0
  163. package/dist/providers/gmail/index.js +1436 -0
  164. package/dist/providers/gmail/skill.generated.js +9 -0
  165. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  166. package/dist/providers/linear/index.js +352 -0
  167. package/dist/providers/linear/skill.generated.js +8 -0
  168. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  169. package/dist/providers/mixpanel/index.js +416 -0
  170. package/dist/providers/mixpanel/skill.generated.js +9 -0
  171. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  172. package/dist/providers/{notion.js → notion/index.js} +226 -239
  173. package/dist/providers/notion/skill.generated.js +10 -0
  174. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  175. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  176. package/dist/providers/overflow/skill.generated.js +8 -0
  177. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  178. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  179. package/dist/providers/planning-center/skill.generated.js +10 -0
  180. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  181. package/dist/providers/revenuecat/index.js +557 -0
  182. package/dist/providers/revenuecat/skill.generated.js +11 -0
  183. package/dist/providers/sheets/index.d.ts +54 -0
  184. package/dist/providers/sheets/index.js +1237 -0
  185. package/dist/providers/sheets/skill.generated.js +10 -0
  186. package/dist/providers/slides/index.d.ts +53 -0
  187. package/dist/providers/slides/index.js +2645 -0
  188. package/dist/providers/slides/skill.generated.js +9 -0
  189. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  190. package/dist/providers/stripe/index.js +266 -0
  191. package/dist/providers/stripe/skill.generated.js +11 -0
  192. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  193. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  194. package/dist/providers/tithely/skill.generated.js +8 -0
  195. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  196. package/dist/providers/vercel/index.js +1038 -0
  197. package/dist/providers/vercel/mcp-ownership.js +212 -0
  198. package/dist/providers/vercel/skill.generated.js +11 -0
  199. package/dist/registry.d.ts +108 -171
  200. package/dist/registry.js +513 -925
  201. package/dist/request-state.js +21 -0
  202. package/dist/resource-read.js +100 -0
  203. package/dist/result-shapes.js +17 -49
  204. package/dist/retired-toolkits.js +1 -1
  205. package/dist/routes/access-tokens.js +3 -6
  206. package/dist/routes/activity-context.js +21 -0
  207. package/dist/routes/activity-disclosure.js +40 -0
  208. package/dist/routes/activity.js +59 -21
  209. package/dist/routes/actor-labels.js +3 -50
  210. package/dist/routes/connect.js +221 -0
  211. package/dist/routes/credentials.js +15 -27
  212. package/dist/routes/mcp.js +356 -130
  213. package/dist/routes/oauth-client-metadata.js +15 -0
  214. package/dist/routes/oauth-management.js +45 -107
  215. package/dist/routes/oauth.js +110 -57
  216. package/dist/routes/operator.js +3 -3
  217. package/dist/routes/shared.d.ts +25 -64
  218. package/dist/routes/shared.js +160 -80
  219. package/dist/routes/skills.js +30 -0
  220. package/dist/routes/ui-config.js +225 -0
  221. package/dist/routes/ui-data.js +40 -35
  222. package/dist/routes/ui.js +66 -56
  223. package/dist/runtime/admission.js +4 -2
  224. package/dist/runtime/call-admission.js +3 -1
  225. package/dist/runtime/connector-scope.js +1 -4
  226. package/dist/runtime/run.js +10 -39
  227. package/dist/runtime/services.js +12 -117
  228. package/dist/runtime/shared-read.js +1 -3
  229. package/dist/runtime/storage.js +8 -1
  230. package/dist/sent-secrets.d.ts +52 -0
  231. package/dist/sent-secrets.js +474 -0
  232. package/dist/server.js +28 -71
  233. package/dist/skills.js +406 -174
  234. package/dist/sqlite.d.ts +48 -0
  235. package/dist/sqlite.js +166 -0
  236. package/dist/storage/keys.js +428 -0
  237. package/dist/storage/memory.js +7 -0
  238. package/dist/storage/sql.d.ts +66 -0
  239. package/dist/storage/sql.js +534 -0
  240. package/dist/timeout.js +4 -2
  241. package/dist/tool-name.js +17 -0
  242. package/dist/tool-safety.d.ts +9 -35
  243. package/dist/tool-safety.js +20 -40
  244. package/dist/types.d.ts +320 -54
  245. package/dist/types.js +0 -1
  246. package/dist/ui.d.ts +7 -19
  247. package/dist/ui.js +76 -90
  248. package/dist/url-safety.js +3 -6
  249. package/dist/usage-guide.js +137 -0
  250. package/dist/validate.d.ts +7 -0
  251. package/dist/validate.js +309 -47
  252. package/dist/version.d.ts +1 -7
  253. package/dist/version.js +3 -7
  254. package/dist/worker.d.ts +11 -0
  255. package/dist/worker.js +434 -0
  256. package/documentation/architecture.md +664 -231
  257. package/documentation/auth.md +936 -546
  258. package/documentation/code-mode.md +501 -312
  259. package/documentation/meta-tools.md +449 -200
  260. package/documentation/operator-tests.md +93 -0
  261. package/documentation/provider-migration-0.29.md +60 -0
  262. package/examples/worker/AGENTS.md +34 -12
  263. package/examples/worker/README.md +286 -255
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  266. package/examples/worker/src/connecta.config.ts +151 -0
  267. package/examples/worker/src/index.ts +14 -197
  268. package/examples/worker/wrangler.jsonc +15 -35
  269. package/package.json +120 -54
  270. package/templates/node/.dockerignore +1 -2
  271. package/templates/node/.env.example +20 -21
  272. package/templates/node/AGENTS.md +21 -17
  273. package/templates/node/Dockerfile +3 -5
  274. package/templates/node/README.md +133 -58
  275. package/templates/node/docker-compose.yml +11 -12
  276. package/templates/node/package.json +4 -2
  277. package/templates/node/src/connecta.config.ts +119 -0
  278. package/templates/node/src/index.ts +9 -154
  279. package/templates/node/src/provision-token.ts +17 -0
  280. package/AGENTS.md +0 -170
  281. package/dist/artifacts/connector.d.ts +0 -58
  282. package/dist/artifacts/connector.js +0 -730
  283. package/dist/artifacts/document.js +0 -86
  284. package/dist/artifacts/guide.js +0 -140
  285. package/dist/artifacts/html-scan.js +0 -329
  286. package/dist/artifacts/json.js +0 -78
  287. package/dist/artifacts/kv-store.d.ts +0 -20
  288. package/dist/artifacts/kv-store.js +0 -140
  289. package/dist/artifacts/markdown.js +0 -478
  290. package/dist/artifacts/module.d.ts +0 -37
  291. package/dist/artifacts/module.js +0 -66
  292. package/dist/artifacts/operations.d.ts +0 -197
  293. package/dist/artifacts/operations.js +0 -1002
  294. package/dist/artifacts/refresh.d.ts +0 -55
  295. package/dist/artifacts/refresh.js +0 -236
  296. package/dist/artifacts/routes.js +0 -206
  297. package/dist/artifacts/types.d.ts +0 -214
  298. package/dist/artifacts/types.js +0 -22
  299. package/dist/artifacts/validate.d.ts +0 -62
  300. package/dist/artifacts/validate.js +0 -651
  301. package/dist/artifacts.d.ts +0 -6
  302. package/dist/artifacts.js +0 -9
  303. package/dist/auth/bearer.d.ts +0 -11
  304. package/dist/auth/bearer.js +0 -50
  305. package/dist/providers/basecamp.js +0 -445
  306. package/dist/providers/linear.js +0 -217
  307. package/dist/providers/mixpanel.js +0 -247
  308. package/dist/providers/revenuecat.js +0 -317
  309. package/dist/providers/stripe.js +0 -253
  310. package/dist/providers/vercel.js +0 -1253
  311. package/dist/storage/file.d.ts +0 -15
  312. package/dist/storage/file.js +0 -455
  313. package/ethos.md +0 -93
  314. package/examples/worker/src/cloudflare-kv.ts +0 -43
  315. package/examples/worker/src/d1-activity-row.ts +0 -155
  316. package/examples/worker/src/d1-activity.ts +0 -147
  317. package/examples/worker/src/d1-storage.ts +0 -138
  318. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  319. package/templates/node/src/file-activity.ts +0 -182
@@ -0,0 +1,69 @@
1
+ import { vettedSchemaDigest } from "./catalog-drift.js";
2
+ // Only built-in factories register local credential checks. This hook is not
3
+ // part of Connector, and a replacement callTool cannot inherit its proof.
4
+ const checks = new WeakMap();
5
+ export function registerInvocationAuth(connector, check) {
6
+ checks.set(connector, { callTool: connector.callTool, check });
7
+ }
8
+ /** Local credential resolution only, before entering any connector call. */
9
+ export async function resolveInvocationAuth(connector, ctx) {
10
+ const entry = checks.get(connector);
11
+ if (entry?.callTool === connector.callTool)
12
+ await entry.check(ctx);
13
+ }
14
+ const requests = new WeakMap();
15
+ function requestFor(scope) {
16
+ let facts = requests.get(scope);
17
+ if (!facts) {
18
+ facts = { entered: [], authFailures: new Map(), retryReads: new Map() };
19
+ requests.set(scope, facts);
20
+ }
21
+ return facts;
22
+ }
23
+ /** Bind the verdict, annotation facts and schemas used for this call. */
24
+ export async function classificationDigest(definition) {
25
+ try {
26
+ const schema = await vettedSchemaDigest(definition);
27
+ const annotations = Object.entries(definition.annotations ?? {}).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
28
+ const bytes = new TextEncoder().encode(JSON.stringify({ classification: definition.classification, annotations, schema }));
29
+ return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
30
+ }
31
+ catch {
32
+ // A digest refusal blocks recovery, not an ordinary call.
33
+ return "";
34
+ }
35
+ }
36
+ /** Called by the host immediately before callTool, using its private scope. */
37
+ export function recordCallEntry(scope, call, definition) {
38
+ // Snapshot synchronously. Recovery hashes this private copy only when it
39
+ // needs a replay binding, without changing ordinary admission or completion.
40
+ requestFor(scope).entered.push({ ...call, definition: structuredClone(definition) });
41
+ }
42
+ export function bindReplayReads(scope, reads) {
43
+ requestFor(scope).retryReads = new Map(reads.map((read) => [read.address, read.digest]));
44
+ }
45
+ export function replayClassificationDigest(scope, address) {
46
+ return requestFor(scope).retryReads.get(address);
47
+ }
48
+ /** Eligibility comes from invocation control flow, never error fields. */
49
+ export function recordAuthFailure(scope, connector, eligible) {
50
+ const failures = requestFor(scope).authFailures;
51
+ failures.set(connector, eligible && failures.get(connector) !== false);
52
+ }
53
+ export async function authRecoveryFacts(scope, connector) {
54
+ const facts = requestFor(scope);
55
+ const writeEntered = facts.entered.some((call) => call.classification === "write");
56
+ const reads = new Map();
57
+ let unsafeEntered = writeEntered;
58
+ for (const call of facts.entered) {
59
+ const digest = await classificationDigest(call.definition);
60
+ unsafeEntered ||= !call.fresh || !digest || (reads.has(call.address) && reads.get(call.address) !== digest);
61
+ reads.set(call.address, digest);
62
+ }
63
+ return {
64
+ writeEntered,
65
+ unsafeEntered,
66
+ eligible: !unsafeEntered && facts.authFailures.get(connector) === true,
67
+ reads: [...reads].map(([address, digest]) => ({ address, digest })),
68
+ };
69
+ }
@@ -1,11 +1,16 @@
1
+ import { hasControlCharacters } from "./tool-name.js";
2
+ import { surfaceAllowsTool } from "./tool-safety.js";
1
3
  import { Cause, Effect, Exit } from "effect";
2
- import { isCallAdmissionError, } from "./call-admission.js";
3
- import { boundedEchoText, classifyCallError, ConnectorCallError, echoedCallArgs, framingError, } from "./errors.js";
4
- import { unwrapMcpResult } from "./mcp-result.js";
4
+ import { isCallAdmissionError } from "./call-admission.js";
5
+ import { boundedEchoText, classificationCode, classifyCallError, ConnectorCallError, echoedCallArgs, framingError, } from "./errors.js";
6
+ import { downstreamValue } from "./mcp-result.js";
7
+ import { carryFailureFacts, classifiedFailure, failureRecord, logFailure, recordedToolName, } from "./operator-record.js";
5
8
  import { splitAddress } from "./registry.js";
6
9
  import { runEdge, withDeadlineEffect } from "./runtime/run.js";
7
- import { isExplicitlyReadOnly } from "./tool-safety.js";
8
- import { validateToolInput } from "./validate.js";
10
+ import { validateCatalogToolInput } from "./validate.js";
11
+ import { sentSecretsFor, sentSecretsForRequest, trackCredentialReads } from "./sent-secrets.js";
12
+ import { classificationDigest, recordAuthFailure, recordCallEntry, replayClassificationDigest, resolveInvocationAuth, } from "./invocation-auth.js";
13
+ import { downstreamContinuation, isDownstreamInputResult, assertDownstreamOutputSafe, } from "./downstream-input-context.js";
9
14
  function defined(values) {
10
15
  return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
11
16
  }
@@ -38,8 +43,7 @@ function admitted(registry, target, args, signal) {
38
43
  args: args ?? {},
39
44
  ...defined({ signal }),
40
45
  });
41
- return Effect.tryPromise({ try: () => pending, catch: (error) => error })
42
- .pipe(Effect.onInterrupt(() => Effect.sync(() => {
46
+ return Effect.tryPromise({ try: () => pending, catch: (error) => error }).pipe(Effect.onInterrupt(() => Effect.sync(() => {
43
47
  pending.then((permit) => permit.release(), () => { });
44
48
  })));
45
49
  }), (permit) => Effect.sync(() => permit.release()), { interruptible: true });
@@ -54,23 +58,29 @@ function callerCancelledDetails() {
54
58
  function recoveryMode(registry, connector, baseUrl) {
55
59
  if (connector.startAuth)
56
60
  return "oauth";
57
- if (registry.credentialUiAvailable() && connector.credential &&
61
+ if (registry.credentialUiAvailable() &&
62
+ connector.credential &&
58
63
  registry.contextFor(connector.id, baseUrl).credential) {
59
64
  return "operator_config";
60
65
  }
61
66
  return "unavailable";
62
67
  }
63
68
  function isCallerCancellation(error, signal) {
64
- return (signal?.aborted === true ||
65
- (isCallAdmissionError(error) && error.admissionKind === "cancelled"));
69
+ return signal?.aborted === true || (isCallAdmissionError(error) && error.admissionKind === "cancelled");
66
70
  }
67
71
  /**
68
72
  * A downstream MCP tool answered with `isError`. Classified exactly as the
69
- * plain `Error` it used to be — same message, same heuristics — and marked so
73
+ * plain `Error`: untyped, non-retryable, and marked so
70
74
  * a write's outcome can tell "the service said no" from "nobody answered".
71
75
  */
72
76
  class DownstreamToolError extends Error {
73
77
  }
78
+ function isTimeoutFailure(error) {
79
+ const code = error.details?.code;
80
+ return (error.code === "timeout" ||
81
+ (error.code === "unavailable" &&
82
+ (code === "timeout" || code === "ETIMEDOUT" || code?.endsWith("_TIMEOUT") === true)));
83
+ }
74
84
  function assertRawMcpSuccess(kind, result) {
75
85
  if (kind !== "mcp" || result == null || typeof result !== "object")
76
86
  return;
@@ -145,13 +155,18 @@ export class InvocationService {
145
155
  pipeline(address, args, context) {
146
156
  return Effect.gen({ self: this }, function* () {
147
157
  const started = Date.now();
158
+ const argumentEcho = echoedCallArgs(args ?? {});
148
159
  let catalogMs = 0;
149
160
  let admissionMs = 0;
150
161
  let connectorMs = 0;
151
162
  let resultProcessingMs = 0;
152
163
  let attempts = 0;
164
+ let resultBytes;
153
165
  let dispatchedToConnector = false;
166
+ let preInvocationAuthFailure = false;
154
167
  let answered = false;
168
+ const sentSecrets = sentSecretsForRequest(this.catalog.requestScope);
169
+ context.sentSecrets?.include(sentSecrets);
155
170
  // A write gate's refusal that is no attempt; see WriteGateDecision.
156
171
  let unrecorded = false;
157
172
  let resolved;
@@ -171,20 +186,26 @@ export class InvocationService {
171
186
  const identity = activityTarget
172
187
  ? {
173
188
  connectorId: activityTarget.connector.id,
174
- toolName: activityTarget.toolName,
189
+ // A resolved tool's name is the downstream's choice, recorded
190
+ // only when it fits the tool-name grammar (INV-6).
191
+ toolName: resolved ? recordedToolName(resolved.definition) : activityTarget.toolName,
175
192
  }
176
193
  : attempted;
177
194
  if (!identity)
178
195
  return;
196
+ const toolName = hasControlCharacters(identity.toolName) ? "<unlisted>" : identity.toolName;
179
197
  this.activity?.recordTool?.(this.activity, {
180
198
  connectorId: identity.connectorId,
181
- toolName: identity.toolName,
182
- address: `${identity.connectorId}.${identity.toolName}`,
199
+ toolName,
200
+ address: `${identity.connectorId}.${toolName}`,
183
201
  source: context.source,
184
202
  outcome,
203
+ ...(activityTarget?.connector.authScope === "personal" ? { personal: true } : {}),
185
204
  durationMs: Date.now() - started,
186
205
  attempts,
187
206
  ...defined({
207
+ classification: resolved?.definition.classification,
208
+ resultBytes,
188
209
  errorCode: classification.errorCode,
189
210
  friction: classification.friction,
190
211
  }),
@@ -193,7 +214,40 @@ export class InvocationService {
193
214
  const enrich = (error, target) => {
194
215
  if (!target)
195
216
  return error;
217
+ if (isTimeoutFailure(error) && dispatchedToConnector && resolved?.definition.classification === "write") {
218
+ const echoed = argumentEcho;
219
+ return {
220
+ ...error,
221
+ code: "write_outcome_unknown",
222
+ message: `${error.message} Its outcome is unknown. Check the target before repeating this call.`,
223
+ retryable: false,
224
+ connector: target.connector.id,
225
+ operation: `${target.connector.id}.${target.toolName}`,
226
+ uncertainCall: {
227
+ address: `${target.connector.id}.${target.toolName}`,
228
+ ...echoed,
229
+ ...("args" in echoed ? {} : { argsOmitted: true }),
230
+ },
231
+ retry: "Do not retry automatically. Check whether the write took effect first." +
232
+ ("args" in echoed ? "" : " The arguments exceed the echo budget; use the exact arguments you sent."),
233
+ };
234
+ }
235
+ if (error.code === "auth_required" && target.connector.startAuth) {
236
+ error = { ...error, code: "downstream_oauth_required" };
237
+ }
238
+ const enteredWrite = resolved?.definition.classification === "write" && dispatchedToConnector;
196
239
  switch (error.code) {
240
+ case "input_required_unsupported":
241
+ if (context.processInputRequired)
242
+ return error;
243
+ return {
244
+ ...error,
245
+ nextAction: {
246
+ tool: resolved?.definition.classification === "read" ? "call_tool" : "call_destructive_tool",
247
+ arguments: { address: `${target.connector.id}.${target.toolName}`, ...echoedCallArgs(args) },
248
+ purpose: "Use this direct call so the host can fulfill the downstream input request.",
249
+ },
250
+ };
197
251
  case "destructive_tool_requires_approval": {
198
252
  const echoed = echoedCallArgs(args);
199
253
  return {
@@ -212,18 +266,28 @@ export class InvocationService {
212
266
  };
213
267
  }
214
268
  case "auth_required":
269
+ case "downstream_oauth_required":
215
270
  return {
216
271
  ...error,
217
272
  connector: target.connector.id,
218
273
  operation: `${target.connector.id}.${target.toolName}`,
219
274
  recovery: recoveryMode(this.registry, target.connector, this.catalog.baseUrl),
275
+ ...(enteredWrite ? { reconciliationRequired: true, retryable: false } : {}),
220
276
  nextAction: {
221
277
  tool: "authorize_connector",
222
278
  arguments: { connector: target.connector.id },
223
279
  operatorHandoff: "Give the URL and instructions it returns to the operator.",
224
280
  },
225
- retry: `Retry ${target.connector.id}.${target.toolName} after ` +
226
- "the operator completes recovery.",
281
+ retry: enteredWrite
282
+ ? "This write may have partially run. Reconcile its target before retrying after the operator completes recovery."
283
+ : `Retry ${target.connector.id}.${target.toolName} after the operator completes recovery.`,
284
+ };
285
+ case "provider_permission_denied":
286
+ return {
287
+ ...error,
288
+ connector: target.connector.id,
289
+ operation: `${target.connector.id}.${target.toolName}`,
290
+ retry: "Ask the provider's resource owner or administrator to grant the required permission or scope, then retry. Repeating the call or reconnecting alone will not grant access.",
227
291
  };
228
292
  case "invalid_args":
229
293
  if (!error.validation)
@@ -236,17 +300,32 @@ export class InvocationService {
236
300
  query: target.toolName,
237
301
  connector: target.connector.id,
238
302
  }, "Inspect the current input shape if the validation findings are not sufficient."),
239
- retry: `Correct the listed arguments and retry ` +
240
- `${target.connector.id}.${target.toolName}.`,
303
+ retry: `Correct the listed arguments and retry ` + `${target.connector.id}.${target.toolName}.`,
241
304
  };
242
305
  default:
243
306
  return error;
244
307
  }
245
308
  };
309
+ let stage = "catalog";
246
310
  const failed = (error) => {
247
311
  const diagnostics = timing();
248
312
  const target = resolved ?? activityTarget;
249
- const details = enrich(error, target);
313
+ if (target && (error.code === "auth_required" || error.code === "downstream_oauth_required")) {
314
+ recordAuthFailure(this.catalog.requestScope, target.connector.id, (resolved?.definition.classification === "read" && resolved.classificationFresh === true) ||
315
+ preInvocationAuthFailure ||
316
+ (!resolved && context.source === "call_tool"));
317
+ }
318
+ // Attach host deadline facts before write recovery changes the code.
319
+ if (isTimeoutFailure(error)) {
320
+ const elapsedMs = Date.now() - started;
321
+ const operation = boundedEchoText(address, 512);
322
+ error = {
323
+ ...error,
324
+ message: `Operation "${operation}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${context.timeoutMs}ms).`,
325
+ details: { ...error.details, operation, stage, elapsedMs, ...defined({ deadlineMs: context.timeoutMs }) },
326
+ };
327
+ }
328
+ const details = sentSecrets.redact(enrich(error, target));
250
329
  const outcome = () => ({
251
330
  ok: false,
252
331
  durationMs: Date.now() - started,
@@ -261,47 +340,49 @@ export class InvocationService {
261
340
  // leaves no event and nothing in the operator's log.
262
341
  if (unrecorded)
263
342
  return outcome();
264
- // Activity rows stay payload-free by construction; the operator's log is
265
- // where the downstream reason goes, bounded and without arguments.
343
+ // Activity rows and this log line are both payload-free by
344
+ // construction: the line is the failure's typed record, never its
345
+ // message, which may be the downstream's own words (INV-6).
266
346
  if (target && details.code !== "destructive_tool_requires_approval") {
267
- this.registry
268
- .contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope)
269
- .logger.warn("[connecta] call failed", {
347
+ logFailure(this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope).logger, "call failed", failureRecord({
270
348
  connector: target.connector.id,
271
- tool: target.toolName,
349
+ // Named only when the catalog listed it (src/operator-record.ts).
350
+ tool: resolved?.definition,
272
351
  source: context.source,
273
- code: details.code,
274
- // Sanitized transport diagnostics (origin and errno only, #539).
275
- ...(details.details ? { details: details.details } : {}),
276
352
  attempts,
277
353
  durationMs: Date.now() - started,
278
- message: String(details.message ?? "").slice(0, 300),
279
- });
354
+ // Every refusal reaching `failed` is one connecta built: a
355
+ // thrown value's classification, a framing refusal, or a
356
+ // cancellation. Nothing else is read as a classification.
357
+ }, classifiedFailure(error)));
280
358
  }
281
- record(details.code === "timeout"
282
- ? "timeout"
283
- : details.code === "cancelled"
284
- ? "cancelled"
285
- : "error", { errorCode: details.code });
359
+ record(details.code === "timeout" ? "timeout" : details.code === "cancelled" ? "cancelled" : "error", defined({ errorCode: classificationCode(details.code) }));
286
360
  return outcome();
287
361
  };
288
- if (context.requestSignal?.aborted) {
362
+ if (context.requestSignal?.aborted || context.dispatchSignal?.aborted) {
289
363
  // Preserve the cancelled admission-attempt count without starting discovery.
290
364
  attempts = 1;
291
365
  return failed(callerCancelledDetails());
292
366
  }
293
367
  let result;
368
+ let inputRequiredValue;
294
369
  let observedResult;
370
+ let valueFormat = "json";
295
371
  // Everything up to the downstream result: resolution, the safety and
296
372
  // schema refusals, admission, and the one attempt. A refusal is its
297
373
  // success value. Its failure is an abort reason or something nobody
298
374
  // expected, and a throw anywhere in it — a defect, to Effect — is
299
375
  // classified like a failure, as the async body this replaced did.
300
376
  const dispatch = (callSignal) => Effect.gen({ self: this }, function* () {
301
- const resolution = yield* this.catalog.resolve(address, defined({ signal: callSignal }));
377
+ const admissionSignal = context.dispatchSignal
378
+ ? AbortSignal.any([context.dispatchSignal, ...(callSignal ? [callSignal] : [])])
379
+ : callSignal;
380
+ const resolution = yield* this.catalog.resolve(address, defined({ signal: admissionSignal }));
302
381
  catalogMs += resolution.catalogMs;
303
382
  if (callSignal?.aborted)
304
383
  return yield* Effect.fail(callSignal.reason);
384
+ if (admissionSignal?.aborted)
385
+ return callerCancelledDetails();
305
386
  if (!resolution.ok) {
306
387
  if (resolution.connector && resolution.toolName) {
307
388
  activityTarget = {
@@ -317,30 +398,40 @@ export class InvocationService {
317
398
  const target = resolution.resolved;
318
399
  resolved = target;
319
400
  activityTarget = target;
320
- const consequential = !isExplicitlyReadOnly(target.definition) && !context.allowDestructive;
321
- const gate = consequential && (context.gates?.(target) ?? true)
322
- ? context.writeGate
401
+ const write = target.definition.classification !== "read";
402
+ const canonicalAddress = `${target.connector.id}.${target.toolName}`;
403
+ const input = context.source !== "execute_code"
404
+ ? downstreamContinuation(this.catalog.requestScope, target.connector.id, canonicalAddress)
323
405
  : undefined;
324
- if (consequential && !gate) {
406
+ const expectedDigest = replayClassificationDigest(this.catalog.requestScope, canonicalAddress);
407
+ if (expectedDigest !== undefined &&
408
+ (target.classificationFresh !== true ||
409
+ (yield* Effect.promise(() => classificationDigest(target.definition))) !== expectedDigest)) {
410
+ return {
411
+ ...framingError("auth_replay_refused", "An entered read no longer has the same fresh classification. Reconcile its target before starting a new request."),
412
+ reconciliationRequired: true,
413
+ };
414
+ }
415
+ if (!surfaceAllowsTool(target.definition.classification, context.source, context.trust)) {
325
416
  const canonicalAddress = `${target.connector.id}.${target.toolName}`;
326
- return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is not explicitly read-only. Invoke it through call_destructive_tool so the MCP host can request explicit approval.`);
417
+ return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is a write. Invoke it through call_destructive_tool so the MCP host can request approval.`);
327
418
  }
328
419
  // Remote MCP tools advertise their input schema in the catalog. Validate
329
420
  // against that same request-local definition before admission or provider
330
421
  // dispatch, so a predictable mismatch stays structured instead of being
331
422
  // flattened into provider-specific error prose. Unsupported schemas retain
332
423
  // validateToolInput's fail-open behavior and reach the downstream normally.
333
- if (target.connector.kind === "mcp" &&
334
- target.definition.inputSchema) {
335
- const invalid = validateToolInput(target.definition.inputSchema, args ?? {}, {
424
+ if (target.connector.kind === "mcp" && target.definition.inputSchema) {
425
+ const invalid = validateCatalogToolInput(target.definition.inputSchema, args ?? {}, {
336
426
  address: `${target.connector.id}.${target.toolName}`,
337
- logger: this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope).logger,
338
- });
427
+ logger: this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope)
428
+ .logger,
429
+ }, { connector: target.connector.id, tool: target.definition });
339
430
  if (invalid)
340
431
  return classifyCallError(invalid);
341
432
  }
342
- if (gate) {
343
- const decision = yield* gate(target, args ?? {});
433
+ if (write && context.beforeWrite) {
434
+ const decision = yield* context.beforeWrite(target, args ?? {});
344
435
  if (decision.kind === "refuse") {
345
436
  unrecorded = decision.unrecorded === true;
346
437
  return decision.error;
@@ -349,45 +440,102 @@ export class InvocationService {
349
440
  if (callSignal?.aborted)
350
441
  return yield* Effect.fail(callSignal.reason);
351
442
  attempts = 1;
352
- const call = () => {
353
- const connectorContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs }));
354
- if (target.connector.credential &&
355
- !connectorContext.credential) {
356
- throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. Call " +
357
- `authorize_connector({ connector: "${target.connector.id}" }).`);
443
+ const call = async () => {
444
+ const connectorContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs, defer: this.catalog.defer }));
445
+ trackCredentialReads(connectorContext);
446
+ sentSecrets.include(sentSecretsFor(connectorContext));
447
+ try {
448
+ if (target.connector.credential) {
449
+ if (!connectorContext.credential) {
450
+ throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. Call " +
451
+ `authorize_connector({ connector: "${target.connector.id}" }).`);
452
+ }
453
+ if (!(await connectorContext.credential.getAll())) {
454
+ throw new ConnectorCallError("auth_required", `Connector "${target.connector.id}" has no stored credential. Call authorize_connector.`);
455
+ }
456
+ }
457
+ await resolveInvocationAuth(target.connector, connectorContext);
458
+ }
459
+ catch (error) {
460
+ const code = classifyCallError(error).code;
461
+ preInvocationAuthFailure = code === "auth_required" || code === "downstream_oauth_required";
462
+ throw error;
358
463
  }
359
464
  // Cancellation can arrive during admission or context construction.
360
- if (callSignal?.aborted)
361
- throw callSignal.reason;
465
+ if (admissionSignal?.aborted)
466
+ throw admissionSignal.reason;
362
467
  dispatchedToConnector = true;
363
- return target.connector.callTool(target.toolName, args ?? {}, connectorContext);
468
+ recordCallEntry(this.catalog.requestScope, {
469
+ address: canonicalAddress,
470
+ classification: write ? "write" : "read",
471
+ fresh: target.classificationFresh === true,
472
+ }, target.definition);
473
+ return await target.connector.callTool(target.toolName, args ?? {}, connectorContext,
474
+ // The connector may retain or mutate its definition. Keep the
475
+ // invocation's classification and schema private, even during
476
+ // this call, and give every dispatch its own deep copy.
477
+ { definition: structuredClone(target.definition), ...defined({ input }) });
364
478
  };
365
479
  // The permit belongs to this scope, so success, failure, and the
366
480
  // deadline's interruption all release it. Interruption stops the
367
481
  // wait instead of waiting for the connector, so an uncooperative one
368
482
  // cannot hold its permit past the deadline.
369
483
  const attempt = yield* Effect.exit(Effect.scoped(Effect.gen({ self: this }, function* () {
370
- yield* timed((elapsed) => { admissionMs += elapsed; }, admitted(this.registry, target, args, callSignal));
371
- const raw = yield* timed((elapsed) => { connectorMs += elapsed; }, Effect.tryPromise({
484
+ stage = "admission";
485
+ yield* timed((elapsed) => {
486
+ admissionMs += elapsed;
487
+ }, admitted(this.registry, target, args, admissionSignal));
488
+ stage = "downstream";
489
+ const reply = yield* timed((elapsed) => {
490
+ connectorMs += elapsed;
491
+ }, Effect.tryPromise({
372
492
  try: () => Promise.resolve(call()),
373
493
  catch: (error) => error,
374
494
  }));
495
+ if (target.connector.kind === "mcp" && isDownstreamInputResult(reply)) {
496
+ if (!context.processInputRequired)
497
+ throw new ConnectorCallError("input_required_unsupported", "The downstream returned input_required. Use the equivalent direct MCP call to provide input.");
498
+ const value = yield* Effect.tryPromise({
499
+ try: () => Promise.resolve(context.processInputRequired(reply, target, sentSecrets)),
500
+ catch: (error) => error,
501
+ });
502
+ return { inputRequired: true, value };
503
+ }
504
+ assertDownstreamOutputSafe(this.catalog.requestScope, reply);
505
+ const raw = sentSecrets.redact(reply);
375
506
  // isError is checked here for BOTH result shapes so every adapter
376
507
  // reports the same downstream-failure wording, and the throw lands
377
508
  // inside the attempt where it feeds health.
378
509
  assertRawMcpSuccess(target.connector.kind, raw);
379
- return { raw, observed: unwrapMcpResult(target.connector.kind, raw) };
510
+ return { raw, observed: sentSecrets.redact(downstreamValue(target.connector.kind, raw)) };
380
511
  })));
381
512
  if (Exit.isFailure(attempt)) {
382
513
  if (callSignal?.aborted)
383
514
  return yield* Effect.fail(callSignal.reason);
515
+ if (!dispatchedToConnector && admissionSignal?.aborted)
516
+ return callerCancelledDetails();
384
517
  const attemptError = Cause.squash(attempt.cause);
385
518
  answered = answeredFailure(attemptError);
386
519
  return isCallerCancellation(attemptError, context.requestSignal)
387
520
  ? callerCancelledDetails()
388
- : classifyCallError(attemptError);
521
+ : carryFailureFacts(attemptError, classifyCallError(sentSecrets.redact(attemptError)));
522
+ }
523
+ if ("inputRequired" in attempt.value) {
524
+ inputRequiredValue = { value: attempt.value.value };
525
+ return undefined;
389
526
  }
390
- observedResult = attempt.value.observed;
527
+ observedResult = attempt.value.observed.data;
528
+ try {
529
+ const serialized = attempt.value.observed.format === "text" && typeof observedResult === "string"
530
+ ? observedResult
531
+ : JSON.stringify(observedResult);
532
+ if (serialized !== undefined)
533
+ resultBytes = new TextEncoder().encode(serialized).byteLength;
534
+ }
535
+ catch {
536
+ /* Unserializable values have no measurable byte count. */
537
+ }
538
+ valueFormat = attempt.value.observed.format;
391
539
  result = context.unwrapResult ? observedResult : attempt.value.raw;
392
540
  return undefined;
393
541
  });
@@ -405,9 +553,11 @@ export class InvocationService {
405
553
  })
406
554
  : dispatch());
407
555
  if (Exit.isFailure(dispatched)) {
408
- return failed(context.requestSignal?.aborted
556
+ const failure = Cause.squash(dispatched.cause);
557
+ const details = context.requestSignal?.aborted
409
558
  ? callerCancelledDetails()
410
- : classifyCallError(Cause.squash(dispatched.cause)));
559
+ : carryFailureFacts(failure, classifyCallError(sentSecrets.redact(failure)));
560
+ return failed(details);
411
561
  }
412
562
  if (dispatched.value)
413
563
  return failed(dispatched.value);
@@ -417,13 +567,19 @@ export class InvocationService {
417
567
  return yield* Effect.die(new Error("Invocation completed without a resolved tool"));
418
568
  }
419
569
  const processResult = context.processResult;
420
- const processing = processResult
421
- ? Effect.tryPromise({
422
- try: () => Promise.resolve(processResult(result, completed)),
423
- catch: (error) => error,
424
- })
425
- : Effect.succeed(result);
426
- const processed = yield* Effect.exit(timed((elapsed) => { resultProcessingMs += elapsed; }, Effect.tap(processing, () => Effect.sync(() => {
570
+ const processing = inputRequiredValue
571
+ ? Effect.succeed(inputRequiredValue.value)
572
+ : processResult
573
+ ? Effect.tryPromise({
574
+ try: () => Promise.resolve(processResult(sentSecrets.redact(result), completed, sentSecrets, valueFormat)),
575
+ catch: (error) => error,
576
+ })
577
+ : Effect.succeed(result);
578
+ const processed = yield* Effect.exit(timed((elapsed) => {
579
+ resultProcessingMs += elapsed;
580
+ }, Effect.tap(processing, () => Effect.sync(() => {
581
+ if (inputRequiredValue)
582
+ return;
427
583
  try {
428
584
  this.registry.observeOutputShape(completed.connector.id, completed.definition, observedResult);
429
585
  }
@@ -436,13 +592,14 @@ export class InvocationService {
436
592
  if (Exit.isFailure(processed))
437
593
  return unprocessable();
438
594
  try {
439
- const value = processed.value;
595
+ const value = sentSecrets.redact(processed.value);
440
596
  const diagnostics = timing();
441
597
  const friction = context.activityFriction?.(value);
442
598
  record("success", friction ? { friction } : {});
443
599
  return {
444
600
  ok: true,
445
601
  value,
602
+ format: valueFormat,
446
603
  resolved: completed,
447
604
  durationMs: Date.now() - started,
448
605
  attempts,
@@ -1,2 +1,2 @@
1
1
  export { Validator } from "@cfworker/json-schema";
2
- export type { OutputUnit, Schema, SchemaDraft, ValidationResult, } from "@cfworker/json-schema";
2
+ export type { OutputUnit, Schema, SchemaDraft, ValidationResult } from "@cfworker/json-schema";
@@ -0,0 +1,35 @@
1
+ import { CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY } from "@modelcontextprotocol/server";
2
+ const RESERVED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
3
+ /** A null-prototype copy, so a declared key can never shadow an inherited one. */
4
+ function ownCapabilities(capabilities) {
5
+ const copy = Object.create(null);
6
+ for (const [key, value] of Object.entries(capabilities)) {
7
+ if (!RESERVED_KEYS.has(key))
8
+ copy[key] = value;
9
+ }
10
+ return copy;
11
+ }
12
+ /** Lift only the SDK-validated envelope, without retaining unrelated metadata. */
13
+ export function bindMcpClient(context, client, activity) {
14
+ const envelope = context.mcpReq.envelope;
15
+ const capabilities = envelope?.[CLIENT_CAPABILITIES_META_KEY];
16
+ const info = envelope?.[CLIENT_INFO_META_KEY];
17
+ delete client.clientCapabilities;
18
+ delete client.clientInfo;
19
+ if (capabilities && typeof capabilities === "object" && !Array.isArray(capabilities)) {
20
+ client.clientCapabilities = ownCapabilities(capabilities);
21
+ }
22
+ if (info &&
23
+ typeof info === "object" &&
24
+ "name" in info &&
25
+ "version" in info &&
26
+ typeof info.name === "string" &&
27
+ typeof info.version === "string") {
28
+ client.clientInfo = { name: info.name, version: info.version };
29
+ }
30
+ if (activity) {
31
+ delete activity.clientInfo;
32
+ if (client.clientInfo)
33
+ activity.clientInfo = client.clientInfo;
34
+ }
35
+ }