@zackbart/connecta 0.34.1 → 0.35.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.
- package/CHANGELOG.md +363 -0
- package/README.md +62 -106
- package/SECURITY.md +3 -2
- package/bin/connecta.mjs +41 -16
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +2 -1
- package/dist/access-tokens.js +66 -30
- package/dist/activity.d.ts +1 -1
- package/dist/activity.js +2 -0
- package/dist/agent-config.d.ts +23 -0
- package/dist/agent-config.js +59 -0
- package/dist/agent-description.js +16 -0
- package/dist/agent-identity.js +4 -4
- package/dist/agent-observation.js +39 -0
- package/dist/auth/clerk-sdk/client.d.ts +2 -0
- package/dist/auth/clerk-sdk/generated.js +554 -563
- package/dist/auth/clerk-transport.js +70 -3
- package/dist/auth/clerk.d.ts +13 -0
- package/dist/auth/clerk.js +653 -246
- package/dist/auth/cloudflare-access.js +46 -42
- package/dist/auth/downstream-client-metadata.js +2 -2
- package/dist/auth/downstream-oauth.js +2051 -1160
- package/dist/auth/downstream-revocation.js +88 -88
- package/dist/auth/native-call.js +20 -0
- package/dist/auth/native.js +184 -0
- package/dist/auth/oauth-start-reset.js +11 -5
- package/dist/auth/oauth-token-response.js +121 -102
- package/dist/auth/oauth-v2-migration.js +97 -78
- package/dist/auth/renewable-grant.js +10 -0
- package/dist/auth/static-oauth.js +663 -271
- package/dist/auth/vercel-agent.js +266 -0
- package/dist/auth-admission.js +204 -0
- package/dist/auth-elicitation.js +350 -241
- package/dist/authorization-handoff.js +164 -90
- package/dist/call-admission-context.js +3 -10
- package/dist/call-admission.js +2 -0
- package/dist/call-classification.js +9 -0
- package/dist/call-recovery.js +32 -13
- package/dist/canonical-json.js +8 -0
- package/dist/catalog-cache.js +369 -213
- package/dist/catalog-service.js +659 -530
- package/dist/catalog.js +5 -1
- package/dist/concurrency.js +0 -25
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +3 -1
- package/dist/config-schema.js +13 -3
- package/dist/config.d.ts +40 -6
- package/dist/config.js +40 -26
- package/dist/connector-caller.d.ts +4 -0
- package/dist/connector-caller.js +17 -4
- package/dist/connectors/api-connector.d.ts +12 -7
- package/dist/connectors/api-connector.js +142 -56
- package/dist/connectors/guarded-fetch.d.ts +51 -17
- package/dist/connectors/guarded-fetch.js +215 -121
- package/dist/connectors/negotiation-cache.js +20 -10
- package/dist/connectors/remote-mcp.d.ts +10 -30
- package/dist/connectors/remote-mcp.js +955 -563
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +3 -1
- package/dist/describe-config.js +2 -0
- package/dist/downstream-input.js +347 -260
- package/dist/errors.d.ts +87 -6
- package/dist/errors.js +114 -4
- package/dist/execute.js +87 -50
- package/dist/executor-admission.js +3 -0
- package/dist/executors/quickjs.js +163 -112
- package/dist/guarded-transport.js +50 -0
- package/dist/guest-runtime.js +1 -1
- package/dist/http/client.js +439 -0
- package/dist/http/errors.js +558 -0
- package/dist/http/fields.js +49 -0
- package/dist/http/hatch-paging.js +318 -0
- package/dist/http/hatch.js +1279 -0
- package/dist/http/json-schema.js +267 -0
- package/dist/http/node-transport.js +241 -0
- package/dist/http/oauth.js +162 -0
- package/dist/http/paging.js +228 -0
- package/dist/http/provider.js +221 -0
- package/dist/http/safety.js +82 -0
- package/dist/http/services.js +79 -0
- package/dist/http/tools.js +383 -0
- package/dist/http/vendor-index.js +138 -0
- package/dist/http/vendor.js +448 -0
- package/dist/http/write-request.js +66 -0
- package/dist/http-policy.d.ts +11 -0
- package/dist/http-policy.js +45 -0
- package/dist/inbound-credential.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -0
- package/dist/invocation-auth.js +30 -19
- package/dist/invocation.js +218 -85
- package/dist/mcp-result.js +0 -4
- package/dist/membership.d.ts +4 -0
- package/dist/meta-output.js +1 -1
- package/dist/meta-tools.js +531 -406
- package/dist/native-recovery.js +52 -0
- package/dist/native-tools.js +382 -0
- package/dist/node.d.ts +4 -3
- package/dist/node.js +45 -8
- package/dist/oauth-handoff.js +177 -104
- package/dist/oauth-partition.js +6 -20
- package/dist/oauth-sealing.js +6 -7
- package/dist/operation-vocabulary.js +101 -0
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +17 -4
- package/dist/operator-ui/agents.d.ts +5 -4
- package/dist/operator-ui/agents.js +17 -7
- package/dist/operator-ui/config-summary.js +5 -4
- package/dist/operator-ui/connection-map.js +7 -5
- package/dist/operator-ui/contract.d.ts +7 -3
- package/dist/operator-ui/endpoints.js +4 -13
- package/dist/operator-ui/fix-prompts.d.ts +1 -1
- package/dist/operator-ui/fix-prompts.js +0 -8
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/operator-ui/overview.js +5 -2
- package/dist/operator-ui/setup-commands.js +0 -4
- package/dist/operator-ui/view.d.ts +2 -25
- package/dist/operator-ui/view.js +2 -76
- package/dist/program-writes.js +47 -26
- package/dist/provider-call-facts.d.ts +115 -0
- package/dist/provider-call-facts.js +182 -0
- package/dist/provider-call.d.ts +112 -0
- package/dist/provider-call.js +271 -0
- package/dist/provider.d.ts +65 -31
- package/dist/provider.js +116 -39
- package/dist/providers/_shared/google/hatch.js +91 -0
- package/dist/providers/_shared/google/kit.js +537 -0
- package/dist/providers/_shared/google/result-size.js +15 -2
- package/dist/providers/_shared/google/service-account.d.ts +2 -1
- package/dist/providers/_shared/google/service-account.js +8 -4
- package/dist/providers/_shared/google/workspace.d.ts +6 -210
- package/dist/providers/_shared/google/workspace.js +6 -425
- package/dist/providers/_shared/rest/compact-index.js +86 -0
- package/dist/providers/_shared/rest/dispatch.js +3 -23
- package/dist/providers/_shared/rest/kit.js +546 -0
- package/dist/providers/_shared/rest/operation-index.js +230 -14
- package/dist/providers/_shared/rest/response-media.js +11 -0
- package/dist/providers/_shared/rest/tools.js +101 -78
- package/dist/providers/_shared/rest/value-safety.js +353 -195
- package/dist/providers/_shared/sensitive-fields.js +27 -0
- package/dist/providers/breeze/api.js +584 -0
- package/dist/providers/breeze/index.d.ts +1 -3
- package/dist/providers/breeze/index.js +67 -1420
- package/dist/providers/breeze/openapi.generated.js +60 -0
- package/dist/providers/breeze/skill.generated.js +2 -6
- package/dist/providers/breeze/tools.js +696 -0
- package/dist/providers/ccb/api.js +496 -0
- package/dist/providers/ccb/index.d.ts +15 -37
- package/dist/providers/ccb/index.js +77 -1702
- package/dist/providers/ccb/lib/constants.d.ts +34 -0
- package/dist/providers/ccb/lib/constants.js +74 -0
- package/dist/providers/ccb/lib/guide.js +9 -0
- package/dist/providers/ccb/lib/verdicts.js +387 -0
- package/dist/providers/ccb/openapi.generated.js +397 -0
- package/dist/providers/ccb/safety.js +295 -0
- package/dist/providers/ccb/skill.generated.js +2 -6
- package/dist/providers/ccb/tools.js +853 -0
- package/dist/providers/cloudflare/api.js +1066 -0
- package/dist/providers/cloudflare/index.d.ts +5 -22
- package/dist/providers/cloudflare/index.js +79 -197
- package/dist/providers/cloudflare/lib/config.d.ts +16 -0
- package/dist/providers/cloudflare/lib/config.js +2 -0
- package/dist/providers/cloudflare/lib/discovery.js +29 -0
- package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
- package/dist/providers/cloudflare/lib/hatch.js +103 -0
- package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
- package/dist/providers/cloudflare/lib/response-media.js +75 -0
- package/dist/providers/cloudflare/openapi.generated.js +4 -3
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +487 -0
- package/dist/providers/cloudflare/skill.generated.js +2 -10
- package/dist/providers/cloudflare/tools.js +360 -0
- package/dist/providers/docs/api.js +51 -0
- package/dist/providers/docs/index.d.ts +1 -12
- package/dist/providers/docs/index.js +53 -1165
- package/dist/providers/docs/lib/constants.d.ts +18 -0
- package/dist/providers/docs/lib/constants.js +18 -0
- package/dist/providers/docs/lib/discovery.js +26 -0
- package/dist/providers/docs/lib/requests.js +48 -0
- package/dist/providers/docs/lib/schemas.js +168 -0
- package/dist/providers/docs/openapi.generated.js +16 -0
- package/dist/providers/docs/safety.js +54 -0
- package/dist/providers/docs/skill.generated.js +2 -6
- package/dist/providers/docs/tools.js +802 -0
- package/dist/providers/drive/api.js +177 -0
- package/dist/providers/drive/index.d.ts +1 -12
- package/dist/providers/drive/index.js +38 -1448
- package/dist/providers/drive/lib/constants.d.ts +6 -0
- package/dist/providers/drive/lib/constants.js +6 -0
- package/dist/providers/drive/lib/contracts.js +532 -0
- package/dist/providers/drive/lib/hatch.js +197 -0
- package/dist/providers/drive/openapi.generated.js +77 -0
- package/dist/providers/drive/safety.js +90 -0
- package/dist/providers/drive/skill.generated.js +2 -6
- package/dist/providers/drive/tools.js +1290 -0
- package/dist/providers/forms/api.js +71 -0
- package/dist/providers/forms/index.d.ts +1 -12
- package/dist/providers/forms/index.js +49 -1145
- package/dist/providers/forms/lib/constants.d.ts +14 -0
- package/dist/providers/forms/lib/constants.js +19 -0
- package/dist/providers/forms/lib/hatch.js +72 -0
- package/dist/providers/forms/openapi.generated.js +23 -0
- package/dist/providers/forms/safety.js +34 -0
- package/dist/providers/forms/skill.generated.js +2 -8
- package/dist/providers/forms/tools.js +1024 -0
- package/dist/providers/github/api.js +150 -0
- package/dist/providers/github/index.d.ts +4 -5
- package/dist/providers/github/index.js +30 -87
- package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
- package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
- package/dist/providers/github/lib/discovery.js +132 -0
- package/dist/providers/github/lib/operations.js +58 -0
- package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
- package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
- package/dist/providers/github/openapi.generated.js +1216 -0
- package/dist/providers/github/safety.js +216 -0
- package/dist/providers/github/skill.generated.js +2 -5
- package/dist/providers/github/tools.js +506 -0
- package/dist/providers/gmail/api.js +114 -0
- package/dist/providers/gmail/index.d.ts +1 -12
- package/dist/providers/gmail/index.js +73 -1621
- package/dist/providers/gmail/lib/constants.d.ts +13 -0
- package/dist/providers/gmail/lib/constants.js +16 -0
- package/dist/providers/gmail/lib/index.js +173 -0
- package/dist/providers/gmail/lib/schemas.js +176 -0
- package/dist/providers/gmail/lib/threading.js +120 -0
- package/dist/providers/gmail/openapi.generated.js +92 -0
- package/dist/providers/gmail/safety.js +21 -0
- package/dist/providers/gmail/skill.generated.js +2 -6
- package/dist/providers/gmail/tools.js +1435 -0
- package/dist/providers/overflow/api.js +1008 -0
- package/dist/providers/overflow/index.d.ts +1 -17
- package/dist/providers/overflow/index.js +60 -1488
- package/dist/providers/overflow/skill.generated.js +2 -5
- package/dist/providers/overflow/tools.js +873 -0
- package/dist/providers/planning-center/api.js +467 -0
- package/dist/providers/planning-center/index.d.ts +4 -35
- package/dist/providers/planning-center/index.js +69 -2445
- package/dist/providers/planning-center/lib/constants.d.ts +58 -0
- package/dist/providers/planning-center/lib/constants.js +83 -0
- package/dist/providers/planning-center/lib/discovery.js +40 -0
- package/dist/providers/planning-center/lib/guide.js +39 -0
- package/dist/providers/planning-center/lib/json-api.js +83 -0
- package/dist/providers/planning-center/lib/verdicts.js +1758 -0
- package/dist/providers/planning-center/openapi.generated.js +1764 -0
- package/dist/providers/planning-center/safety.js +188 -0
- package/dist/providers/planning-center/skill.generated.js +2 -27
- package/dist/providers/planning-center/tools.js +1010 -0
- package/dist/providers/sheets/api.js +133 -0
- package/dist/providers/sheets/index.d.ts +4 -16
- package/dist/providers/sheets/index.js +61 -1172
- package/dist/providers/sheets/lib/constants.d.ts +14 -0
- package/dist/providers/sheets/lib/constants.js +14 -0
- package/dist/providers/sheets/lib/hatch.js +163 -0
- package/dist/providers/sheets/lib/requests.js +105 -0
- package/dist/providers/sheets/lib/schemas.js +138 -0
- package/dist/providers/sheets/openapi.generated.js +30 -0
- package/dist/providers/sheets/safety.js +27 -0
- package/dist/providers/sheets/skill.generated.js +2 -7
- package/dist/providers/sheets/tools.js +734 -0
- package/dist/providers/slides/api.js +147 -0
- package/dist/providers/slides/index.d.ts +2 -10
- package/dist/providers/slides/index.js +48 -2555
- package/dist/providers/slides/lib/constants.d.ts +11 -0
- package/dist/providers/slides/lib/constants.js +11 -0
- package/dist/providers/slides/lib/requests.js +60 -0
- package/dist/providers/slides/lib/schemas.js +565 -0
- package/dist/providers/slides/openapi.generated.js +18 -0
- package/dist/providers/slides/skill.generated.js +2 -6
- package/dist/providers/slides/tools.js +1968 -0
- package/dist/providers/stripe/api.js +401 -0
- package/dist/providers/stripe/index.d.ts +5 -27
- package/dist/providers/stripe/index.js +74 -210
- package/dist/providers/stripe/lib/config.d.ts +4 -0
- package/dist/providers/stripe/lib/config.js +2 -0
- package/dist/providers/stripe/openapi.generated.js +4 -3
- package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
- package/dist/providers/stripe/skill.generated.js +2 -11
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +17 -0
- package/dist/providers/tithely/index.js +86 -965
- package/dist/providers/tithely/skill.generated.js +2 -5
- package/dist/providers/tithely/tools.js +426 -0
- package/dist/providers/vercel/api.js +551 -0
- package/dist/providers/vercel/index.d.ts +9 -25
- package/dist/providers/vercel/index.js +66 -260
- package/dist/providers/vercel/lib/config.d.ts +2 -0
- package/dist/providers/vercel/lib/config.js +2 -0
- package/dist/providers/vercel/lib/index.js +117 -0
- package/dist/providers/vercel/lib/logs.js +100 -0
- package/dist/providers/vercel/lib/pin.js +270 -0
- package/dist/providers/vercel/openapi.generated.js +10 -5
- package/dist/providers/vercel/{value-safety.js → safety.js} +247 -38
- package/dist/providers/vercel/skill.generated.js +2 -7
- package/dist/providers/vercel/tools.js +642 -0
- package/dist/redaction-identity.js +47 -0
- package/dist/redaction-marker.js +125 -0
- package/dist/registry.d.ts +21 -15
- package/dist/registry.js +409 -376
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/actor-labels.js +1 -1
- package/dist/routes/connect.js +1 -1
- package/dist/routes/dispatch.js +103 -0
- package/dist/routes/mcp.js +170 -79
- package/dist/routes/oauth.js +3 -1
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +5 -4
- package/dist/routes/shared.js +244 -177
- package/dist/routes/skills.js +10 -8
- package/dist/routes/ui-config.js +27 -3
- package/dist/routes/ui-data.js +21 -20
- package/dist/routes/ui.js +1 -1
- package/dist/runtime/call-admission.js +0 -26
- package/dist/runtime/call-entry.js +30 -0
- package/dist/runtime/call.js +556 -0
- package/dist/runtime/connector-scope.js +7 -0
- package/dist/runtime/credential-probe.js +34 -0
- package/dist/runtime/executor.js +40 -0
- package/dist/runtime/native-storage.js +63 -0
- package/dist/runtime/registry.js +27 -0
- package/dist/runtime/request-scope.js +72 -0
- package/dist/runtime/run.js +15 -7
- package/dist/runtime/shared-read.js +34 -5
- package/dist/runtime/storage.js +34 -21
- package/dist/sent-secrets.d.ts +194 -37
- package/dist/sent-secrets.js +1743 -340
- package/dist/server.js +5 -56
- package/dist/skills.js +322 -222
- package/dist/sqlite.js +15 -14
- package/dist/storage/d1-copy.js +255 -0
- package/dist/storage/d1-driver.js +15 -0
- package/dist/storage/keys.js +28 -0
- package/dist/storage/sql-driver.js +88 -0
- package/dist/storage/sql-native.js +544 -0
- package/dist/storage/sql-schema.d.ts +6 -0
- package/dist/storage/sql-schema.js +15 -0
- package/dist/storage/sql.d.ts +14 -20
- package/dist/storage/sql.js +29 -535
- package/dist/timeout.js +0 -26
- package/dist/tool-safety.js +20 -10
- package/dist/types.d.ts +7 -109
- package/dist/upward.js +3 -0
- package/dist/usage-guide.js +3 -1
- package/dist/validate.d.ts +4 -2
- package/dist/validate.js +86 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +312 -292
- package/dist/workspace-subject.js +5 -1
- package/dist/write-outcome.js +454 -34
- package/documentation/README.md +27 -7
- package/documentation/architecture.md +2547 -226
- package/documentation/auth.md +821 -98
- package/documentation/code-mode.md +49 -16
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +129 -144
- package/documentation/meta-tools.md +409 -32
- package/documentation/operating.md +27 -3
- package/documentation/operator-tests.md +3 -0
- package/documentation/operator-ui.md +68 -45
- package/documentation/provider-auth-migration.md +16 -63
- package/documentation/provider-migration-0.29.md +10 -5
- package/documentation/provider-migration-0.35.md +975 -0
- package/documentation/providers.md +965 -0
- package/examples/worker/AGENTS.md +23 -9
- package/examples/worker/README.md +254 -135
- package/examples/worker/alchemy.run.ts +116 -0
- package/examples/worker/gitignore +10 -0
- package/examples/worker/src/connecta.config.ts +23 -21
- package/examples/worker/wrangler.jsonc +14 -5
- package/guides/connecting-agents.md +157 -0
- package/guides/how-it-works.md +156 -0
- package/guides/safety-and-trust.md +153 -0
- package/package.json +27 -46
- package/templates/node/.env.example +3 -0
- package/templates/node/README.md +22 -2
- package/templates/node/docker-compose.yml +1 -0
- package/templates/node/package.json +3 -2
- package/templates/node/src/connecta.config.ts +8 -0
- package/dist/catalog-drift.d.ts +0 -138
- package/dist/catalog-drift.js +0 -578
- package/dist/providers/_shared/request-token.js +0 -83
- package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
- package/dist/providers/_shared/rest/tools.d.ts +0 -141
- package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
- package/dist/providers/basecamp/index.d.ts +0 -45
- package/dist/providers/basecamp/index.js +0 -997
- package/dist/providers/basecamp/skill.generated.js +0 -9
- package/dist/providers/cloudflare/rest.d.ts +0 -37
- package/dist/providers/cloudflare/rest.js +0 -1097
- package/dist/providers/github/hosted.js +0 -218
- package/dist/providers/github/rest.js +0 -321
- package/dist/providers/infisical/index.d.ts +0 -7
- package/dist/providers/infisical/index.js +0 -713
- package/dist/providers/infisical/skill.generated.js +0 -8
- package/dist/providers/intercom/index.d.ts +0 -24
- package/dist/providers/intercom/index.js +0 -121
- package/dist/providers/intercom/skill.generated.js +0 -8
- package/dist/providers/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -537
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -59
- package/dist/providers/mixpanel/index.js +0 -472
- package/dist/providers/mixpanel/operations.js +0 -20
- package/dist/providers/mixpanel/rest.js +0 -603
- package/dist/providers/mixpanel/skill.generated.js +0 -10
- package/dist/providers/mixpanel/value-safety.js +0 -16
- package/dist/providers/notion/index.d.ts +0 -67
- package/dist/providers/notion/index.js +0 -368
- package/dist/providers/notion/openapi.generated.js +0 -75
- package/dist/providers/notion/rest.d.ts +0 -24
- package/dist/providers/notion/rest.js +0 -1226
- package/dist/providers/notion/skill.generated.js +0 -11
- package/dist/providers/notion/value-safety.js +0 -56
- package/dist/providers/revenuecat/index.d.ts +0 -51
- package/dist/providers/revenuecat/index.js +0 -697
- package/dist/providers/revenuecat/skill.generated.js +0 -11
- package/dist/providers/stripe/rest.d.ts +0 -14
- package/dist/providers/stripe/rest.js +0 -402
- package/dist/providers/vercel/rest.d.ts +0 -20
- package/dist/providers/vercel/rest.js +0 -1020
- package/dist/providers/wispr-flow/index.d.ts +0 -14
- package/dist/providers/wispr-flow/index.js +0 -117
- package/dist/providers/wispr-flow/skill.generated.js +0 -8
- /package/dist/providers/github/{key.js → lib/key.js} +0 -0
|
@@ -1,56 +1,85 @@
|
|
|
1
|
+
import { authView } from "../auth/native-call.js";
|
|
2
|
+
import { authFlow, authCall, authInvoke, authValue, authMethod, authCatch } from "../auth/native.js";
|
|
3
|
+
import { isOAuthCredentialRequest } from "../auth/oauth-token-response.js";
|
|
4
|
+
import { deriveContext } from "../derived-context.js";
|
|
5
|
+
import { operationContext, openRequest, viewServices, viewSignal } from "../runtime/call.js";
|
|
1
6
|
import { assertRemoteOAuthClient, declareSelfHostedClient, downstreamClientMetadata, downstreamRedirectUri, remoteClientAuthMethod, selfHostedClientUrl, trackRemoteClientRequest, } from "../auth/downstream-client-metadata.js";
|
|
2
7
|
import { AuthorizationServerMismatchError, Client, InsecureTokenEndpointError, InsufficientScopeError, IssuerMismatchError, MissingRequiredClientCapabilityError, OAuthClientFlowError, OAuthError, ProtocolError, RegistrationRejectedError, ResourceNotFoundError, SdkError, SdkErrorCode, SdkHttpError, SseError, UnsupportedProtocolVersionError, UrlElicitationRequiredError, isInputRequiredResult, fromJsonSchema, isJSONRPCErrorResponse, isJSONRPCNotification, specTypeSchemas, StreamableHTTPClientTransport, UnauthorizedError, } from "@modelcontextprotocol/client";
|
|
3
8
|
import { Deferred, Duration, Effect, Exit, Scope } from "effect";
|
|
4
|
-
import { assertOAuthScope, authorizingContext, KvOAuthProvider, OAuthRequestNotSentError, refreshCoordinatorsByPartition, } from "../auth/downstream-oauth.js";
|
|
9
|
+
import { assertOAuthScope, authorizingContext, codeExchangeResponse, KvOAuthProvider, OAuthRequestNotSentError, refreshCoordinatorsByPartition, } from "../auth/downstream-oauth.js";
|
|
5
10
|
import { trackOAuthStartReset } from "../auth/oauth-start-reset.js";
|
|
6
11
|
import { byteReadResponse } from "../byte-read-response.js";
|
|
7
|
-
import { redactCatalog, redactSentSecrets, sentSecretsFor, shortSecretWarning, trackCredentialReads, } from "../sent-secrets.js";
|
|
12
|
+
import { redactCatalog, redactSentSecrets, requestSentSecrets, sentSecretsFor, shortSecretWarning, trackCredentialReads, } from "../sent-secrets.js";
|
|
8
13
|
import { MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES } from "../catalog-limits.js";
|
|
9
14
|
import { catalogClientOptions, catalogItems, observeCompletedCatalogRefresh, closeCatalogCacheScope, observeCatalogChange, observeCatalogFetch, observeCatalogExpiry, } from "../catalog-cache.js";
|
|
10
|
-
import { reviewedClassification } from "../catalog-drift.js";
|
|
11
15
|
import { connectorScopeCleanupClaimed } from "../connector-scope.js";
|
|
12
|
-
import { boundedEchoText, ConnectorCallError, msg, networkErrorCode, unavailableCallError, WithheldTextError, } from "../errors.js";
|
|
16
|
+
import { boundedEchoText, ConnectorCallError, msg, networkErrorCode, resendAdvice, resendAdviceOf, unavailableCallError, WithheldTextError, withResendAdvice, } from "../errors.js";
|
|
13
17
|
import { attachFailureFacts, carryFailureFacts, errorLabel, failureRecord, failureStatus, labelErrorClass, logFailure, OAUTH_ERROR_CODES, ownStatus, } from "../operator-record.js";
|
|
14
18
|
import { CONNECTA_VERSION } from "../version.js";
|
|
15
19
|
import { learnedUrlRefusal } from "../url-safety.js";
|
|
16
20
|
import { oauthSealerFor } from "../oauth-sealing.js";
|
|
17
|
-
import {
|
|
21
|
+
import { oauthPartitionFor, retainOAuthPartition } from "../oauth-partition.js";
|
|
22
|
+
import { RequestScope } from "../runtime/request-scope.js";
|
|
23
|
+
import { registerCallEntry } from "../runtime/call-entry.js";
|
|
24
|
+
import { Context } from "effect";
|
|
18
25
|
import { registerInvocationAuth } from "../invocation-auth.js";
|
|
19
26
|
import { downstreamInputCapabilities, assertDownstreamOutputSafe, downstreamWriteContinuation, } from "../downstream-input-context.js";
|
|
20
27
|
import { detach, runEdge } from "../runtime/run.js";
|
|
21
|
-
import { assertKnownOptions, keys, optionsOf } from "../config-schema.js";
|
|
28
|
+
import { assertKnownOptions, keys, optionsOf, strings } from "../config-schema.js";
|
|
22
29
|
import { describedEndpoint, describedUrl } from "../described.js";
|
|
23
|
-
import { retryAfterMs } from "
|
|
30
|
+
import { retryAfterMs } from "../http-policy.js";
|
|
24
31
|
import { resourceUriMatchesTemplates } from "./resource-uri.js";
|
|
25
32
|
import { callerOf } from "../connector-caller.js";
|
|
33
|
+
import { callClassification } from "../call-classification.js";
|
|
34
|
+
import { guardedTransport, sendGuarded, sendCredentialPost } from "../guarded-transport.js";
|
|
35
|
+
import { sourceClassification } from "../tool-safety.js";
|
|
36
|
+
import { dispatchProviderRequest, recordProviderResponse, enterProviderBoundary, knownProviderFailure, providerBoundary, providerRequest, renewedAfterUnauthorized, vendorRefusal, } from "../provider-call.js";
|
|
37
|
+
import { providerCallFailure, unsentAfterUnauthorized } from "../write-outcome.js";
|
|
38
|
+
import { recordRequestField } from "../provider-call-facts.js";
|
|
26
39
|
import { readNegotiation, storeNegotiation } from "./negotiation-cache.js";
|
|
27
40
|
import { READ_ONLY, CALL_ADMISSION, REMOTE_MCP_AUTH, USAGE_GUIDE } from "./option-shapes.js";
|
|
28
|
-
/**
|
|
29
|
-
* Apply a maintained provider's slot copy and header framing to credential
|
|
30
|
-
* auth the deployment left bare.
|
|
31
|
-
*
|
|
32
|
-
* A provider knows what its own key is called and how the endpoint expects it
|
|
33
|
-
* framed; a deployment that states either one keeps its answer. Every other
|
|
34
|
-
* auth shape passes through untouched, so a provider can hand this its whole
|
|
35
|
-
* `auth` option without branching first.
|
|
36
|
-
*/
|
|
37
|
-
export function withCredentialDefaults(auth, defaults) {
|
|
38
|
-
if (auth.type !== "credential")
|
|
39
|
-
return auth;
|
|
40
|
-
return {
|
|
41
|
-
...auth,
|
|
42
|
-
credential: auth.credential ?? defaults.credential,
|
|
43
|
-
...(auth.scheme === undefined && defaults.scheme !== undefined ? { scheme: defaults.scheme } : {}),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
41
|
/** The closed options remoteMcp() accepts; see `assertKnownOptions`. */
|
|
47
42
|
const REMOTE_MCP_OPTIONS = optionsOf()({
|
|
48
|
-
...keys("url", "title", "description", "authScope", "maxResultBytes", "versionNegotiation", "redirects", "requireHttps", "logger", "_transportFactory", "
|
|
43
|
+
...keys("url", "title", "description", "authScope", "maxResultBytes", "versionNegotiation", "redirects", "requireHttps", "logger", "_transportFactory", "skills"),
|
|
49
44
|
readOnly: READ_ONLY,
|
|
50
45
|
callAdmission: CALL_ADMISSION,
|
|
51
46
|
usageGuide: USAGE_GUIDE,
|
|
52
47
|
auth: REMOTE_MCP_AUTH,
|
|
48
|
+
headers: strings(),
|
|
53
49
|
});
|
|
50
|
+
/** Extra headers are immutable code configuration, not another auth channel. */
|
|
51
|
+
function extraResourceHeaders(id, opts) {
|
|
52
|
+
if (opts.headers === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
const refuse = () => new Error(`remoteMcp(${JSON.stringify(id)}).headers requires non-secret resource headers without reserved or auth-owned names; use auth for credentials.`);
|
|
55
|
+
if (opts.auth?.type === "headers")
|
|
56
|
+
throw refuse();
|
|
57
|
+
const authNames = new Set([
|
|
58
|
+
"authorization",
|
|
59
|
+
...(opts.auth?.type === "credential" ? [opts.auth.header?.trim().toLowerCase() || "authorization"] : []),
|
|
60
|
+
...(opts.auth?.type === "request" ? Object.keys(opts.auth.headers ?? {}).map((name) => name.toLowerCase()) : []),
|
|
61
|
+
]);
|
|
62
|
+
const normalized = Object.create(null);
|
|
63
|
+
for (const [name, value] of Object.entries(opts.headers)) {
|
|
64
|
+
const lower = name.toLowerCase();
|
|
65
|
+
if (!HEADER_NAME_TOKEN.test(name) ||
|
|
66
|
+
typeof value !== "string" ||
|
|
67
|
+
/[\r\n]/.test(value) ||
|
|
68
|
+
Object.hasOwn(normalized, lower) ||
|
|
69
|
+
authNames.has(lower) ||
|
|
70
|
+
/^(?:accept|cookie|host|origin|connection|keep-alive|proxy-authenticate|proxy-authorization|te|trailer|transfer-encoding|upgrade|last-event-id|dpop|www-authenticate)$/.test(lower) ||
|
|
71
|
+
/^(?:content-|mcp-)/.test(lower) ||
|
|
72
|
+
/(?:api[-_]?key|token|secret|password|credential|authorization|signature|session)/.test(lower))
|
|
73
|
+
throw refuse();
|
|
74
|
+
try {
|
|
75
|
+
normalized[lower] = new Headers({ [lower]: value }).get(lower);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw refuse();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return Object.freeze(Object.fromEntries(Object.entries(normalized).sort(([a], [b]) => a.localeCompare(b))));
|
|
82
|
+
}
|
|
54
83
|
/**
|
|
55
84
|
* How long a downstream gets to answer the session-termination DELETE before
|
|
56
85
|
* teardown stops waiting. This is a network round-trip budget, deliberately
|
|
@@ -455,10 +484,11 @@ function isCursorShapeError(err) {
|
|
|
455
484
|
* passed the MCP and OAuth boundaries, and the origin and errno connecta
|
|
456
485
|
* derived for it, which a message alone would drop.
|
|
457
486
|
*/
|
|
487
|
+
/** An authorization start's failure for authorize_connector: no write's, so its advice stays. */
|
|
458
488
|
function startMessage(err) {
|
|
459
489
|
const details = err instanceof ConnectorCallError ? err.details : undefined;
|
|
460
490
|
const facts = [details?.host, details?.code].filter(Boolean).join(", ");
|
|
461
|
-
return facts ? `${msg(err)} (${facts})` : msg(err);
|
|
491
|
+
return withResendAdvice(facts ? `${msg(err)} (${facts})` : msg(err), resendAdviceOf(err));
|
|
462
492
|
}
|
|
463
493
|
/**
|
|
464
494
|
* End the downstream's session before the connection is torn down.
|
|
@@ -499,7 +529,7 @@ function terminateSession(transport, logger, connectorId) {
|
|
|
499
529
|
return Effect.raceAllFirst([
|
|
500
530
|
// The SDK issues no request at all when no `mcp-session-id` was captured,
|
|
501
531
|
// so a stateless downstream never sees a spurious DELETE.
|
|
502
|
-
promised(() =>
|
|
532
|
+
promised(authMethod(() => authInvoke(Reflect, "apply", [terminate, transport, []]))).pipe(Effect.catch((error) =>
|
|
503
533
|
// The SDK's error quotes the downstream's status text, and its `data`
|
|
504
534
|
// the body, so the record keeps only the status it answered with.
|
|
505
535
|
warn(() => logFailure(logger, "session termination refused or failed; the downstream session may remain until its provider timeout", failureRecord({ connector: connectorId }, error instanceof SdkHttpError ? attachFailureFacts(error, { httpStatus: error.status }) : error))))),
|
|
@@ -520,7 +550,7 @@ function closeLocally(client, transport) {
|
|
|
520
550
|
if (!client && !transport)
|
|
521
551
|
return Effect.void;
|
|
522
552
|
return Effect.raceAllFirst([
|
|
523
|
-
promised(() =>
|
|
553
|
+
promised(authMethod(() => (client ? authInvoke(client, "close", []) : authValue(() => transport?.close())))).pipe(Effect.ignore),
|
|
524
554
|
Effect.sleep(Duration.millis(LOCAL_CLOSE_BUDGET_MS)),
|
|
525
555
|
]);
|
|
526
556
|
}
|
|
@@ -531,7 +561,7 @@ function closeLocally(client, transport) {
|
|
|
531
561
|
* allocates no AbortController.
|
|
532
562
|
*/
|
|
533
563
|
function promised(evaluate) {
|
|
534
|
-
return
|
|
564
|
+
return authCall(evaluate, []);
|
|
535
565
|
}
|
|
536
566
|
/** A retained cause chain must not smuggle an SDK payload past the boundary. */
|
|
537
567
|
function hasSdkPayload(error) {
|
|
@@ -545,14 +575,14 @@ function hasSdkPayload(error) {
|
|
|
545
575
|
}
|
|
546
576
|
/** Cancellation can bypass an inner SDK catch and return the signal's reason. */
|
|
547
577
|
function payloadFree(run) {
|
|
548
|
-
return
|
|
578
|
+
return authMethod((...args) => authFlow(function* () {
|
|
549
579
|
try {
|
|
550
|
-
return
|
|
580
|
+
return (yield authCall(run, [...args]));
|
|
551
581
|
}
|
|
552
582
|
catch (error) {
|
|
553
583
|
throw hasSdkPayload(error) ? carryFailureFacts(error, downstreamCallError(error)) : error;
|
|
554
584
|
}
|
|
555
|
-
};
|
|
585
|
+
}));
|
|
556
586
|
}
|
|
557
587
|
/** Both the SDK's auth signal and its classified form invalidate a live client. */
|
|
558
588
|
function requiresAuthorization(error) {
|
|
@@ -652,7 +682,7 @@ async function digestOf(value) {
|
|
|
652
682
|
*
|
|
653
683
|
* A scheme ending in `Basic` names HTTP Basic credentials wherever a provider
|
|
654
684
|
* nests it, so the `user:secret` it frames is base64-encoded — that is what
|
|
655
|
-
* makes plain `Basic` and
|
|
685
|
+
* makes plain `Basic` and nested `Bearer Basic` one rule instead of two.
|
|
656
686
|
*/
|
|
657
687
|
function credentialHeaderValue(scheme, value) {
|
|
658
688
|
if (scheme === null)
|
|
@@ -726,7 +756,7 @@ function redirectedInit(init, status) {
|
|
|
726
756
|
* arbitrary static header names receive the same protection as Authorization.
|
|
727
757
|
*/
|
|
728
758
|
export function redirectSafeFetch(connectorId, policy = "none", baseFetch = fetch) {
|
|
729
|
-
return
|
|
759
|
+
return authMethod((input, initialInit = {}) => authFlow(function* () {
|
|
730
760
|
let current = new URL(input);
|
|
731
761
|
let init = initialInit;
|
|
732
762
|
const seen = new Set([current.href]);
|
|
@@ -734,22 +764,23 @@ export function redirectSafeFetch(connectorId, policy = "none", baseFetch = fetc
|
|
|
734
764
|
while (true) {
|
|
735
765
|
let response;
|
|
736
766
|
try {
|
|
737
|
-
response =
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
767
|
+
response = (yield authCall(baseFetch, [
|
|
768
|
+
current,
|
|
769
|
+
{
|
|
770
|
+
...init,
|
|
771
|
+
redirect: "manual",
|
|
772
|
+
},
|
|
773
|
+
]));
|
|
741
774
|
}
|
|
742
775
|
catch (cause) {
|
|
743
776
|
if (cause instanceof ConnectorCallError)
|
|
744
777
|
throw downstreamCallError(cause);
|
|
745
778
|
throw unavailableCallError(cause, current.href, undefined, hasSdkPayload(cause) ? undefined : (init.signal ?? undefined));
|
|
746
779
|
}
|
|
747
|
-
// Every MCP and OAuth exchange reads its answer through here, and the
|
|
748
|
-
// SDK reads bodies with `.text()` and `.json()`: as bytes, never text.
|
|
749
780
|
if (!REDIRECT_STATUSES.has(response.status))
|
|
750
781
|
return byteReadResponse(response);
|
|
751
782
|
const location = response.headers.get("location");
|
|
752
|
-
|
|
783
|
+
(yield authCatch(authValue(() => response.body?.cancel()), () => { }));
|
|
753
784
|
if (!location) {
|
|
754
785
|
throw new RemoteMcpRedirectError(connectorId, `HTTP ${response.status} carried no Location header`);
|
|
755
786
|
}
|
|
@@ -783,7 +814,7 @@ export function redirectSafeFetch(connectorId, policy = "none", baseFetch = fetc
|
|
|
783
814
|
init = redirectedInit(init, response.status);
|
|
784
815
|
current = next;
|
|
785
816
|
}
|
|
786
|
-
};
|
|
817
|
+
}));
|
|
787
818
|
}
|
|
788
819
|
class RemoteMcpDestinationError extends OAuthRequestNotSentError {
|
|
789
820
|
constructor(connectorId, reason) {
|
|
@@ -836,13 +867,13 @@ for (const [ctor, label] of [
|
|
|
836
867
|
* failures after an HTTP dispatch keep their permanent ambiguous verdict.
|
|
837
868
|
*/
|
|
838
869
|
function learnedUrlSafeFetch(connectorId, configured, baseFetch) {
|
|
839
|
-
return
|
|
870
|
+
return authMethod((input, init) => authFlow(function* () {
|
|
840
871
|
const reason = learnedUrlRefusal(configured, new URL(input));
|
|
841
872
|
if (reason !== undefined) {
|
|
842
873
|
throw new RemoteMcpDestinationError(connectorId, reason);
|
|
843
874
|
}
|
|
844
|
-
return byteReadResponse(
|
|
845
|
-
};
|
|
875
|
+
return byteReadResponse((yield authCall(baseFetch, [input, init])));
|
|
876
|
+
}));
|
|
846
877
|
}
|
|
847
878
|
/** The JSON-RPC error answers each transport delivered, as `code\0message`. */
|
|
848
879
|
const wireErrors = new WeakMap();
|
|
@@ -966,6 +997,7 @@ function withheldAs(message, verdict) {
|
|
|
966
997
|
...(verdict.details ? { details: verdict.details } : {}),
|
|
967
998
|
...(verdict.validation ? { validation: verdict.validation } : {}),
|
|
968
999
|
...(verdict.current ? { current: verdict.current } : {}),
|
|
1000
|
+
...(verdict.writeOutcome ? { writeOutcome: verdict.writeOutcome } : {}),
|
|
969
1001
|
});
|
|
970
1002
|
}
|
|
971
1003
|
return new WithheldTextError(message, verdict);
|
|
@@ -977,7 +1009,7 @@ function withheldAs(message, verdict) {
|
|
|
977
1009
|
* `grant_type`), or the JSON POST of dynamic client registration.
|
|
978
1010
|
*/
|
|
979
1011
|
function tracedOAuthFetch(trail, endpoint, baseFetch) {
|
|
980
|
-
return
|
|
1012
|
+
return authMethod((input, init) => authFlow(function* () {
|
|
981
1013
|
const url = new URL(input);
|
|
982
1014
|
if (url.href === endpoint.href) {
|
|
983
1015
|
trail.last = undefined;
|
|
@@ -986,7 +1018,8 @@ function tracedOAuthFetch(trail, endpoint, baseFetch) {
|
|
|
986
1018
|
const body = init?.body;
|
|
987
1019
|
const step = (init?.method ?? "GET").toUpperCase() === "GET"
|
|
988
1020
|
? "discovery"
|
|
989
|
-
: body instanceof URLSearchParams ||
|
|
1021
|
+
: body instanceof URLSearchParams ||
|
|
1022
|
+
(typeof body === "string" && new URLSearchParams(body).has("grant_type"))
|
|
990
1023
|
? "token request"
|
|
991
1024
|
: "client registration";
|
|
992
1025
|
trail.last = { step, host: url.origin };
|
|
@@ -994,7 +1027,7 @@ function tracedOAuthFetch(trail, endpoint, baseFetch) {
|
|
|
994
1027
|
trail.registration = url.origin;
|
|
995
1028
|
}
|
|
996
1029
|
const leg = trail.last;
|
|
997
|
-
const response =
|
|
1030
|
+
const response = (yield authCall(baseFetch, [input, init]));
|
|
998
1031
|
if (leg) {
|
|
999
1032
|
leg.httpStatus = response.status;
|
|
1000
1033
|
const wait = retryAfterMs(response.headers);
|
|
@@ -1002,7 +1035,7 @@ function tracedOAuthFetch(trail, endpoint, baseFetch) {
|
|
|
1002
1035
|
leg.retryAfterMs = wait;
|
|
1003
1036
|
}
|
|
1004
1037
|
return byteReadResponse(response);
|
|
1005
|
-
};
|
|
1038
|
+
}));
|
|
1006
1039
|
}
|
|
1007
1040
|
function registrationErrorCode(body) {
|
|
1008
1041
|
try {
|
|
@@ -1013,6 +1046,37 @@ function registrationErrorCode(body) {
|
|
|
1013
1046
|
return undefined;
|
|
1014
1047
|
}
|
|
1015
1048
|
}
|
|
1049
|
+
/**
|
|
1050
|
+
* The key a `tools/call`'s params carry its dispatch under. Object spread
|
|
1051
|
+
* copies a symbol-keyed property, and the pinned SDK only spreads a request's
|
|
1052
|
+
* params (to add a progress token or its `_meta` envelope); `JSON.stringify`
|
|
1053
|
+
* skips symbol keys, so nothing of it reaches the wire.
|
|
1054
|
+
*/
|
|
1055
|
+
const DISPATCH = Symbol("connecta.dispatch");
|
|
1056
|
+
function newDispatch(write, sourceWrite) {
|
|
1057
|
+
return { write, sourceWrite, sent: false, unauthorized: false, redirected: false, resendRefused: false };
|
|
1058
|
+
}
|
|
1059
|
+
function withDispatch(params, dispatch) {
|
|
1060
|
+
Object.defineProperty(params, DISPATCH, { value: dispatch, enumerable: true });
|
|
1061
|
+
return params;
|
|
1062
|
+
}
|
|
1063
|
+
function dispatchOf(params) {
|
|
1064
|
+
return params !== null && typeof params === "object"
|
|
1065
|
+
? params[DISPATCH]
|
|
1066
|
+
: undefined;
|
|
1067
|
+
}
|
|
1068
|
+
/** A further request for a write that already left, stopped before it does. */
|
|
1069
|
+
function resendRefused(dispatch) {
|
|
1070
|
+
dispatch.resendRefused = true;
|
|
1071
|
+
return new ConnectorCallError("connector_call_failed", "This write was already sent once; it was not sent again.", {
|
|
1072
|
+
retryable: false,
|
|
1073
|
+
writeOutcome: "unknown",
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
/** Lost tracking cannot prove whether an earlier message already sent a write. */
|
|
1077
|
+
function missingDispatch() {
|
|
1078
|
+
return new ConnectorCallError("connector_call_failed", "The downstream call lost its dispatch record and cannot be proven read-only. It was not sent; whether an earlier write took effect is unknown. Check the target before repeating it.", { retryable: false, writeOutcome: "unknown" });
|
|
1079
|
+
}
|
|
1016
1080
|
/**
|
|
1017
1081
|
* Proxy a downstream remote MCP server. SDK clients and transports are scoped
|
|
1018
1082
|
* to one inbound request: reused by calls within a batch/execute_code run, but
|
|
@@ -1029,11 +1093,11 @@ export function remoteMcp(id, opts) {
|
|
|
1029
1093
|
READ_ONLY.parse(opts.readOnly, `Connector(${JSON.stringify(id)}).readOnly`);
|
|
1030
1094
|
if (opts.callAdmission !== undefined)
|
|
1031
1095
|
CALL_ADMISSION.parse(opts.callAdmission, `remoteMcp(${JSON.stringify(id)}).callAdmission`);
|
|
1096
|
+
const extraHeaders = extraResourceHeaders(id, opts);
|
|
1032
1097
|
const skillsEnabled = opts.skills;
|
|
1033
1098
|
if (skillsEnabled !== undefined && typeof skillsEnabled !== "boolean") {
|
|
1034
1099
|
throw new Error(`[connecta] connector "${id}" skills must be a boolean.`);
|
|
1035
1100
|
}
|
|
1036
|
-
const classification = opts.classify === undefined ? undefined : reviewedClassification(opts.classify, `connector "${id}"`);
|
|
1037
1101
|
const clientMetadataUrl = opts.auth?.type === "oauth" ? opts.auth.clientMetadataUrl : undefined;
|
|
1038
1102
|
const oauthScope = opts.auth?.type === "oauth" ? opts.auth.scope : undefined;
|
|
1039
1103
|
assertOAuthScope(id, oauthScope);
|
|
@@ -1186,40 +1250,41 @@ export function remoteMcp(id, opts) {
|
|
|
1186
1250
|
* Anything not already in connecta's words is withheld, including a store's
|
|
1187
1251
|
* error while the flow saves what it discovered, which can quote it.
|
|
1188
1252
|
*/
|
|
1189
|
-
const withoutAuthorizationServerText =
|
|
1190
|
-
const safeOrigin =
|
|
1253
|
+
const withoutAuthorizationServerText = authMethod((ctx, err, trail, signals) => authFlow(function* () {
|
|
1254
|
+
const safeOrigin = authMethod((host) => authFlow(function* () {
|
|
1191
1255
|
if (!host || sentSecretsFor(ctx).contains(host, true))
|
|
1192
1256
|
return undefined;
|
|
1193
1257
|
if (host === destination.origin || (staticClient && host === new URL(staticClient.issuer).origin))
|
|
1194
1258
|
return host;
|
|
1195
1259
|
try {
|
|
1196
|
-
return host === (
|
|
1260
|
+
return host === (yield authInvoke(newProvider(ctx), "validatedIssuerOrigin", []))
|
|
1261
|
+
? host
|
|
1262
|
+
: undefined;
|
|
1197
1263
|
}
|
|
1198
1264
|
catch {
|
|
1199
1265
|
return undefined;
|
|
1200
1266
|
}
|
|
1201
|
-
};
|
|
1267
|
+
}));
|
|
1202
1268
|
if (ownAbortReason(err, signals) && !hasSdkPayload(err))
|
|
1203
1269
|
return err;
|
|
1204
1270
|
if (err instanceof RegistrationRejectedError) {
|
|
1205
1271
|
const code = registrationErrorCode(err.body);
|
|
1206
|
-
const origin =
|
|
1272
|
+
const origin = (yield authCall(safeOrigin, [trail.registration]));
|
|
1207
1273
|
const facts = {
|
|
1208
1274
|
step: "OAuth client registration",
|
|
1209
1275
|
...(origin ? { origin } : {}),
|
|
1210
1276
|
httpStatus: err.status,
|
|
1211
1277
|
...(code ? { oauthError: code } : {}),
|
|
1212
1278
|
};
|
|
1213
|
-
return attachFailureFacts(carryFailureFacts(err, withheldAs(`Connector "${id}" could not register an OAuth client with ` +
|
|
1279
|
+
return attachFailureFacts(carryFailureFacts(err, resendAdvice(withheldAs(`Connector "${id}" could not register an OAuth client with ` +
|
|
1214
1280
|
`${origin ?? "its authorization server"}: the ` +
|
|
1215
1281
|
`registration endpoint answered HTTP ${err.status}` +
|
|
1216
1282
|
`${code ? ` with OAuth error ${code}` : ""}. Its response is ` +
|
|
1217
1283
|
"withheld because it can quote the request or anything the server " +
|
|
1218
|
-
"chose to add. Check the server's client registration policy, then "
|
|
1219
|
-
"retry authorization.", downstreamCallError(err, undefined, trail.last?.retryAfterMs))), facts);
|
|
1284
|
+
"chose to add. Check the server's client registration policy.", downstreamCallError(err, undefined, trail.last?.retryAfterMs)), ", then retry authorization")), facts);
|
|
1220
1285
|
}
|
|
1221
1286
|
const leg = trail.last;
|
|
1222
|
-
const origin =
|
|
1287
|
+
const origin = (yield authCall(safeOrigin, [leg?.host]));
|
|
1223
1288
|
const facts = leg
|
|
1224
1289
|
? {
|
|
1225
1290
|
step: `OAuth ${leg.step}`,
|
|
@@ -1235,11 +1300,10 @@ export function remoteMcp(id, opts) {
|
|
|
1235
1300
|
(classified instanceof ConnectorCallError && classified.code === "provider_permission_denied")) {
|
|
1236
1301
|
return attachFailureFacts(carryFailureFacts(err, classified), facts);
|
|
1237
1302
|
}
|
|
1238
|
-
return attachFailureFacts(carryFailureFacts(err, withheldAs(`Connector "${id}" OAuth ${leg ? `${leg.step}${origin ? ` with ${origin}` : ""}` : "flow"} ` +
|
|
1303
|
+
return attachFailureFacts(carryFailureFacts(err, resendAdvice(withheldAs(`Connector "${id}" OAuth ${leg ? `${leg.step}${origin ? ` with ${origin}` : ""}` : "flow"} ` +
|
|
1239
1304
|
`failed${errorKind(err)}. The error is withheld because its text can ` +
|
|
1240
|
-
"quote what the server sent. Check the server's OAuth metadata, then "
|
|
1241
|
-
|
|
1242
|
-
};
|
|
1305
|
+
"quote what the server sent. Check the server's OAuth metadata.", classified), ", then retry authorization")), facts);
|
|
1306
|
+
}));
|
|
1243
1307
|
const endpointOrigin = new URL(opts.url).origin;
|
|
1244
1308
|
/**
|
|
1245
1309
|
* The MCP boundary, where an error leaves the SDK at the handshake, a
|
|
@@ -1261,7 +1325,7 @@ export function remoteMcp(id, opts) {
|
|
|
1261
1325
|
origin: endpointOrigin,
|
|
1262
1326
|
...(httpStatus ? { httpStatus } : {}),
|
|
1263
1327
|
};
|
|
1264
|
-
const classified = downstreamCallError(redactSentSecrets(ctx, err), undefined, undefined, undefined,
|
|
1328
|
+
const classified = downstreamCallError(redactSentSecrets(ctx, err), undefined, undefined, undefined, requestSentSecrets(ctx));
|
|
1265
1329
|
const verdict = isOauth && classified instanceof ConnectorCallError && classified.code === "auth_required"
|
|
1266
1330
|
? carryFailureFacts(err, authRequiredError())
|
|
1267
1331
|
: classified;
|
|
@@ -1308,7 +1372,7 @@ export function remoteMcp(id, opts) {
|
|
|
1308
1372
|
*/
|
|
1309
1373
|
const boundOAuthFlows = (ctx, transport, endpoint,
|
|
1310
1374
|
/** The request's and connection's signals: only their reasons pass as written. */
|
|
1311
|
-
signals) => {
|
|
1375
|
+
signals, renewalSignal) => {
|
|
1312
1376
|
const internals = transport;
|
|
1313
1377
|
const adapted = internals._authProvider;
|
|
1314
1378
|
const onUnauthorized = adapted?.onUnauthorized;
|
|
@@ -1320,29 +1384,46 @@ export function remoteMcp(id, opts) {
|
|
|
1320
1384
|
"flows cannot be bounded. Pin @modelcontextprotocol/client to the " +
|
|
1321
1385
|
"version this release ships with.");
|
|
1322
1386
|
}
|
|
1323
|
-
const flow =
|
|
1387
|
+
const flow = authMethod((start, signal) => authFlow(function* () {
|
|
1324
1388
|
const trail = {};
|
|
1325
1389
|
try {
|
|
1326
|
-
return
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1390
|
+
return (yield authCall(start, [
|
|
1391
|
+
(fetchFn) => tracedOAuthFetch(trail, endpoint, authMethod((input, init) => authFlow(function* () {
|
|
1392
|
+
trackSentRequest(ctx, input, init);
|
|
1393
|
+
return (yield authCall(fetchFn, [
|
|
1394
|
+
input,
|
|
1395
|
+
signal
|
|
1396
|
+
? {
|
|
1397
|
+
...init,
|
|
1398
|
+
signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal,
|
|
1399
|
+
}
|
|
1400
|
+
: init,
|
|
1401
|
+
]));
|
|
1402
|
+
}))),
|
|
1403
|
+
]));
|
|
1330
1404
|
}
|
|
1331
1405
|
catch (err) {
|
|
1332
|
-
throw
|
|
1333
|
-
}
|
|
1334
|
-
};
|
|
1335
|
-
adapted.onUnauthorized = (ctx) => flow
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1406
|
+
throw (yield authCall(withoutAuthorizationServerText, [ctx, err, trail, [...signals, signal]]));
|
|
1407
|
+
}
|
|
1408
|
+
}));
|
|
1409
|
+
adapted.onUnauthorized = authMethod((ctx) => authCall(flow, [
|
|
1410
|
+
authMethod((trace) => authInvoke(Reflect, "apply", [
|
|
1411
|
+
onUnauthorized,
|
|
1412
|
+
adapted,
|
|
1413
|
+
[{ ...ctx, fetchFn: trace(ctx.fetchFn ?? fetch) }],
|
|
1414
|
+
])),
|
|
1415
|
+
renewalSignal(ctx.response),
|
|
1416
|
+
]));
|
|
1417
|
+
internals._stepUpAuthorize = authMethod((challenge, retries) => authCall(flow, [
|
|
1418
|
+
authMethod((trace) => authFlow(function* () {
|
|
1419
|
+
const traced = trace(internals._fetchWithInit ?? fetch);
|
|
1420
|
+
const self = new Proxy(transport, {
|
|
1421
|
+
get: (target, key) => key === "_fetchWithInit" ? traced : Reflect.get(target, key, target),
|
|
1422
|
+
set: (target, key, value) => Reflect.set(target, key, value, target),
|
|
1423
|
+
});
|
|
1424
|
+
return (yield authInvoke(Reflect, "apply", [stepUp, self, [challenge, retries]]));
|
|
1425
|
+
})),
|
|
1426
|
+
]));
|
|
1346
1427
|
};
|
|
1347
1428
|
class OperatorDisconnectedError extends ConnectorCallError {
|
|
1348
1429
|
constructor() {
|
|
@@ -1367,11 +1448,11 @@ export function remoteMcp(id, opts) {
|
|
|
1367
1448
|
* trusting any cached client, so an operator's replacement is picked up
|
|
1368
1449
|
* without a redeploy. The value stays in the caller's local scope.
|
|
1369
1450
|
*/
|
|
1370
|
-
const readCredential =
|
|
1451
|
+
const readCredential = authMethod((ctx) => authFlow(function* () {
|
|
1371
1452
|
if (requestAuth) {
|
|
1372
1453
|
let value;
|
|
1373
1454
|
try {
|
|
1374
|
-
value =
|
|
1455
|
+
value = (yield authInvoke(requestAuth, "token", [ctx]));
|
|
1375
1456
|
}
|
|
1376
1457
|
catch (error) {
|
|
1377
1458
|
if (error instanceof ConnectorCallError)
|
|
@@ -1393,19 +1474,12 @@ export function remoteMcp(id, opts) {
|
|
|
1393
1474
|
throw new CredentialRequiredError(`Connector "${id}" needs an operator-managed credential, but ` +
|
|
1394
1475
|
"credential storage is not configured. Configure vault in deployment code and redeploy. Call authorize_connector for recovery options.");
|
|
1395
1476
|
}
|
|
1396
|
-
|
|
1397
|
-
// the registry's accessor; nothing to reclassify here.
|
|
1398
|
-
const value = (await ctx.credential.get())?.trim();
|
|
1477
|
+
const value = (yield authInvoke(ctx.credential, "get", []))?.trim();
|
|
1399
1478
|
if (!value) {
|
|
1400
1479
|
throw new CredentialRequiredError(`Connector "${id}" has no stored credential — call ` +
|
|
1401
1480
|
`authorize_connector({ connector: "${id}" }) and follow the ` +
|
|
1402
1481
|
"operator handoff it returns.");
|
|
1403
1482
|
}
|
|
1404
|
-
// Refuse a value a header cannot carry BEFORE anything frames it. A
|
|
1405
|
-
// wrapped newline in a pasted key is the ordinary way this happens, and
|
|
1406
|
-
// the runtime that rejects the header quotes the whole value back in its
|
|
1407
|
-
// TypeError — a message that travels to status, to the activity log, and
|
|
1408
|
-
// to the agent. So the check lives here, and says only what is wrong.
|
|
1409
1483
|
if (carriesIllegalHeaderChar(value)) {
|
|
1410
1484
|
throw new CredentialRequiredError(`Connector "${id}"'s stored credential contains a character a header ` +
|
|
1411
1485
|
"cannot carry (a line break or other control character). Call " +
|
|
@@ -1413,7 +1487,7 @@ export function remoteMcp(id, opts) {
|
|
|
1413
1487
|
"it in this connection in the operator UI. The value is not shown or logged.");
|
|
1414
1488
|
}
|
|
1415
1489
|
return value;
|
|
1416
|
-
};
|
|
1490
|
+
}));
|
|
1417
1491
|
/**
|
|
1418
1492
|
* Replace, never edit.
|
|
1419
1493
|
*
|
|
@@ -1471,48 +1545,59 @@ export function remoteMcp(id, opts) {
|
|
|
1471
1545
|
});
|
|
1472
1546
|
};
|
|
1473
1547
|
const listTools = client.listTools.bind(client);
|
|
1474
|
-
client.listTools = (params, options) => cache
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
result
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1548
|
+
client.listTools = authMethod((params, options) => authInvoke(cache, "withListing", [
|
|
1549
|
+
listingContext(options),
|
|
1550
|
+
authMethod(() => authFlow(function* () {
|
|
1551
|
+
const before = toolPages;
|
|
1552
|
+
let result;
|
|
1553
|
+
try {
|
|
1554
|
+
result = (yield authCall(listTools, [params, options]));
|
|
1555
|
+
}
|
|
1556
|
+
catch (error) {
|
|
1557
|
+
const bounded = atMcpBoundary(cache.currentContext(), error, "tools/list", client.transport, [
|
|
1558
|
+
cache.currentContext().signal,
|
|
1559
|
+
]);
|
|
1560
|
+
const transient = (error instanceof SdkHttpError && [502, 503, 504].includes(error.status)) ||
|
|
1561
|
+
(bounded instanceof ConnectorCallError && bounded.code === "unavailable");
|
|
1562
|
+
const fallback = transient && toolPages === before
|
|
1563
|
+
? (yield authInvoke(cache, "fallbackTools", []))
|
|
1564
|
+
: undefined;
|
|
1565
|
+
if (!fallback)
|
|
1566
|
+
throw error;
|
|
1567
|
+
return fallback;
|
|
1568
|
+
}
|
|
1569
|
+
if (toolPages !== before && params?.cursor === undefined) {
|
|
1570
|
+
const refresh = (yield authInvoke(cache, "completeCatalogRefresh", [result]));
|
|
1571
|
+
if (refresh) {
|
|
1572
|
+
try {
|
|
1573
|
+
(yield authCall(observeCompletedCatalogRefresh, [cache.currentContext(), refresh]));
|
|
1574
|
+
}
|
|
1575
|
+
catch (error) {
|
|
1576
|
+
logFailure(cache.currentContext().logger, "catalog refresh observation failed", failureRecord({ connector: id }, error));
|
|
1577
|
+
}
|
|
1499
1578
|
}
|
|
1500
1579
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1580
|
+
return result;
|
|
1581
|
+
})),
|
|
1582
|
+
]));
|
|
1504
1583
|
const listResources = client.listResources.bind(client);
|
|
1505
|
-
client.listResources = (params, options) => cache
|
|
1584
|
+
client.listResources = authMethod((params, options) => authInvoke(cache, "withListing", [
|
|
1585
|
+
listingContext(options),
|
|
1586
|
+
authMethod(() => authCall(listResources, [params, options])),
|
|
1587
|
+
]));
|
|
1506
1588
|
const listResourceTemplates = client.listResourceTemplates.bind(client);
|
|
1507
|
-
client.listResourceTemplates = (params, options) => cache
|
|
1589
|
+
client.listResourceTemplates = authMethod((params, options) => authInvoke(cache, "withListing", [
|
|
1590
|
+
listingContext(options),
|
|
1591
|
+
authMethod(() => authCall(listResourceTemplates, [params, options])),
|
|
1592
|
+
]));
|
|
1508
1593
|
const request = client.request.bind(client);
|
|
1509
1594
|
const walks = new WeakMap();
|
|
1510
|
-
client.request = (
|
|
1595
|
+
client.request = authMethod((...args) => authFlow(function* () {
|
|
1511
1596
|
const message = args[0];
|
|
1512
1597
|
if (message.method !== "tools/list" &&
|
|
1513
1598
|
message.method !== "resources/list" &&
|
|
1514
1599
|
message.method !== "resources/templates/list")
|
|
1515
|
-
return
|
|
1600
|
+
return yield authCall(request, args, client);
|
|
1516
1601
|
const method = message.method;
|
|
1517
1602
|
const ctx = cache.currentContext();
|
|
1518
1603
|
const options = (args.length === 3 ? args[2] : args[1]);
|
|
@@ -1528,11 +1613,15 @@ export function remoteMcp(id, opts) {
|
|
|
1528
1613
|
}
|
|
1529
1614
|
let page;
|
|
1530
1615
|
try {
|
|
1531
|
-
page =
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
?
|
|
1535
|
-
:
|
|
1616
|
+
page = (yield authCall(request, [
|
|
1617
|
+
message,
|
|
1618
|
+
method === "tools/list"
|
|
1619
|
+
? CompatibleListToolsResultSchema
|
|
1620
|
+
: method === "resources/list"
|
|
1621
|
+
? specTypeSchemas.ListResourcesResult
|
|
1622
|
+
: specTypeSchemas.ListResourceTemplatesResult,
|
|
1623
|
+
options,
|
|
1624
|
+
]));
|
|
1536
1625
|
}
|
|
1537
1626
|
catch (error) {
|
|
1538
1627
|
if (isCursorShapeError(error))
|
|
@@ -1578,19 +1667,18 @@ export function remoteMcp(id, opts) {
|
|
|
1578
1667
|
if (!walk.first)
|
|
1579
1668
|
walk.first = result;
|
|
1580
1669
|
else {
|
|
1581
|
-
// SDK aggregation retains page-one metadata. A later page may only
|
|
1582
|
-
// narrow its reuse grant, never broaden the complete catalog's scope.
|
|
1583
1670
|
walk.first.ttlMs = Math.min(walk.first.ttlMs, clean.ttlMs);
|
|
1584
1671
|
if (clean.cacheScope !== "public")
|
|
1585
1672
|
walk.first.cacheScope = "private";
|
|
1586
1673
|
}
|
|
1587
1674
|
observeCatalogExpiry(ctx, method, walk.first.ttlMs);
|
|
1588
1675
|
return result;
|
|
1589
|
-
});
|
|
1676
|
+
}));
|
|
1590
1677
|
};
|
|
1591
1678
|
/** This request scope's entry, open or closed, created on first sight. */
|
|
1592
1679
|
const entryFor = (ctx) => {
|
|
1593
|
-
const
|
|
1680
|
+
const owner = Context.get(viewServices(ctx), RequestScope);
|
|
1681
|
+
const key = owner;
|
|
1594
1682
|
let state = states.get(key);
|
|
1595
1683
|
if (!state) {
|
|
1596
1684
|
state = {
|
|
@@ -1599,6 +1687,7 @@ export function remoteMcp(id, opts) {
|
|
|
1599
1687
|
client: null,
|
|
1600
1688
|
transport: null,
|
|
1601
1689
|
toolDefinitions: new Map(),
|
|
1690
|
+
dispatches: new Map(),
|
|
1602
1691
|
connecting: null,
|
|
1603
1692
|
authRequired: false,
|
|
1604
1693
|
provider: null,
|
|
@@ -1607,6 +1696,13 @@ export function remoteMcp(id, opts) {
|
|
|
1607
1696
|
credentialDigest: null,
|
|
1608
1697
|
};
|
|
1609
1698
|
states.set(key, state);
|
|
1699
|
+
const owned = state;
|
|
1700
|
+
owner.own(() => {
|
|
1701
|
+
const closing = Scope.closeUnsafe(owned.scope, Exit.void);
|
|
1702
|
+
reset(owned);
|
|
1703
|
+
if (closing)
|
|
1704
|
+
detach(closing);
|
|
1705
|
+
});
|
|
1610
1706
|
}
|
|
1611
1707
|
return state;
|
|
1612
1708
|
};
|
|
@@ -1617,7 +1713,7 @@ export function remoteMcp(id, opts) {
|
|
|
1617
1713
|
throw scopeEndedError();
|
|
1618
1714
|
return state;
|
|
1619
1715
|
};
|
|
1620
|
-
const newProvider = (ctx, state, signal = ctx
|
|
1716
|
+
const newProvider = (ctx, state, signal = viewSignal(ctx, true)) => {
|
|
1621
1717
|
if (state?.provider)
|
|
1622
1718
|
return state.provider;
|
|
1623
1719
|
const metadataUrl = staticClient ? undefined : (clientMetadataUrl ?? selfHostedClientUrl(ctx.publicUrl, id));
|
|
@@ -1650,76 +1746,250 @@ export function remoteMcp(id, opts) {
|
|
|
1650
1746
|
* by the caller so the raw secret is not passed around twice. Never
|
|
1651
1747
|
* retained past the transport it configures.
|
|
1652
1748
|
*/
|
|
1653
|
-
credentialFramed = null, signal = ctx
|
|
1749
|
+
credentialFramed = null, signal = viewSignal(ctx, true),
|
|
1654
1750
|
/** The raw value behind `credentialFramed`, under the same terms. */
|
|
1655
|
-
credentialValue = null
|
|
1751
|
+
credentialValue = null,
|
|
1752
|
+
/** A code exchange never transfers ownership to the refresh coordinator. */
|
|
1753
|
+
exchangeOnly = false) => {
|
|
1754
|
+
const resourceInit = (input, init) => {
|
|
1755
|
+
if (!extraHeaders)
|
|
1756
|
+
return init;
|
|
1757
|
+
const target = new URL(input instanceof Request ? input.url : String(input));
|
|
1758
|
+
const resource = new URL(opts.url);
|
|
1759
|
+
if (target.origin !== resource.origin || target.pathname !== resource.pathname)
|
|
1760
|
+
return init;
|
|
1761
|
+
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
|
|
1762
|
+
for (const [name, value] of Object.entries(extraHeaders))
|
|
1763
|
+
headers.set(name, value);
|
|
1764
|
+
return { ...init, headers };
|
|
1765
|
+
};
|
|
1766
|
+
const initiatingSignal = ctx.signal;
|
|
1767
|
+
ctx = deriveContext(operationContext(ctx), signal ? { signal } : {});
|
|
1656
1768
|
if (opts._transportFactory)
|
|
1657
1769
|
return opts._transportFactory(ctx);
|
|
1658
1770
|
const url = new URL(opts.url);
|
|
1659
|
-
|
|
1660
|
-
|
|
1771
|
+
// Missing records never acquire a fresh write attempt. Only active calls
|
|
1772
|
+
// for this exact tool, all classified read, can prove a recordless message
|
|
1773
|
+
// safe. Catalog hints alone cannot prove the invocation's served verdict.
|
|
1774
|
+
const untrackedRead = (params) => {
|
|
1775
|
+
if (!skillObject(params) || typeof params.name !== "string")
|
|
1776
|
+
return false;
|
|
1777
|
+
let read = false;
|
|
1778
|
+
for (const [dispatch, name] of entryFor(ctx).dispatches) {
|
|
1779
|
+
if (name !== params.name)
|
|
1780
|
+
continue;
|
|
1781
|
+
if (dispatch.write)
|
|
1782
|
+
return false;
|
|
1783
|
+
read = true;
|
|
1784
|
+
}
|
|
1785
|
+
return read;
|
|
1786
|
+
};
|
|
1787
|
+
// This transport's calls on the wire, by JSON-RPC id, while each is sent.
|
|
1788
|
+
const flights = new Map();
|
|
1789
|
+
const trackedFetch = authMethod((input, init) => authFlow(function* () {
|
|
1661
1790
|
let rpc;
|
|
1662
1791
|
if (typeof init?.body === "string") {
|
|
1663
1792
|
try {
|
|
1664
1793
|
rpc = JSON.parse(init.body);
|
|
1665
1794
|
}
|
|
1666
|
-
catch {
|
|
1667
|
-
|
|
1795
|
+
catch { }
|
|
1796
|
+
}
|
|
1797
|
+
const toolCall = init?.method === "POST" && skillObject(rpc) && rpc.method === "tools/call" ? rpc : undefined;
|
|
1798
|
+
const writeContinuation = downstreamWriteContinuation(ctx.requestScope ?? ctx) && toolCall !== undefined;
|
|
1799
|
+
const flight = toolCall && (typeof toolCall.id === "string" || typeof toolCall.id === "number")
|
|
1800
|
+
? flights.get(toolCall.id)
|
|
1801
|
+
: undefined;
|
|
1802
|
+
if (toolCall && !flight && !untrackedRead(toolCall.params))
|
|
1803
|
+
throw missingDispatch();
|
|
1804
|
+
const credentialPost = isOauth && (init?.method ?? "GET").toUpperCase() === "POST" && (!skillObject(rpc) || rpc.jsonrpc !== "2.0");
|
|
1805
|
+
const sender = flight?.write ? guardedTransport(true) : undefined;
|
|
1806
|
+
if (flight?.write) {
|
|
1807
|
+
if (flight.sent) {
|
|
1808
|
+
if (flight.request && flight.authorization)
|
|
1809
|
+
renewedAfterUnauthorized(flight.request.entry, flight.authorization, new Headers(init?.headers).get("authorization") ?? flight.authorization);
|
|
1810
|
+
throw resendRefused(flight);
|
|
1668
1811
|
}
|
|
1812
|
+
flight.authorization = new Headers(init?.headers).get("authorization");
|
|
1813
|
+
}
|
|
1814
|
+
const request = flight?.request;
|
|
1815
|
+
if (request?.entry.dispatched)
|
|
1816
|
+
request.entry = providerRequest(request.ctx, !flight?.write, request.entry.provider, {
|
|
1817
|
+
method: init?.method ?? "GET",
|
|
1818
|
+
});
|
|
1819
|
+
const entry = request?.entry;
|
|
1820
|
+
const dispatch = () => {
|
|
1821
|
+
if (flight?.write) {
|
|
1822
|
+
if (flight.sent)
|
|
1823
|
+
throw resendRefused(flight);
|
|
1824
|
+
flight.sent = true;
|
|
1825
|
+
}
|
|
1826
|
+
if (request && entry) {
|
|
1827
|
+
if (flight?.sourceWrite)
|
|
1828
|
+
recordRequestField(entry, "sourceWrite", true);
|
|
1829
|
+
dispatchProviderRequest(request.ctx, entry, init?.headers, init?.method ?? "GET");
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1832
|
+
trackSentRequest(ctx, input, init);
|
|
1833
|
+
// Attach retained settlement facts at the physical Promise leaf.
|
|
1834
|
+
// Cancellation may end the native waiter before it resumes; neither
|
|
1835
|
+
// the status nor a 401 verdict may disappear with that live authority.
|
|
1836
|
+
const response = (yield authValue(() => {
|
|
1837
|
+
const pending = credentialPost
|
|
1838
|
+
? sendCredentialPost(input, init ?? {}, dispatch)
|
|
1839
|
+
: sender
|
|
1840
|
+
? sendGuarded(sender, new URL(input instanceof Request ? input.url : String(input)), { ...init, redirect: "manual" }, dispatch)
|
|
1841
|
+
: (() => {
|
|
1842
|
+
dispatch();
|
|
1843
|
+
return writeContinuation ? fetch(input, { ...init, redirect: "manual" }) : fetch(input, init);
|
|
1844
|
+
})();
|
|
1845
|
+
return pending.then((response) => {
|
|
1846
|
+
if (entry)
|
|
1847
|
+
recordProviderResponse(entry, response.status);
|
|
1848
|
+
if (flight?.write) {
|
|
1849
|
+
if (response.status === 401)
|
|
1850
|
+
flight.unauthorized = true;
|
|
1851
|
+
else if (REDIRECT_STATUSES.has(response.status))
|
|
1852
|
+
flight.redirected = true;
|
|
1853
|
+
}
|
|
1854
|
+
if (init?.signal?.aborted || request?.ctx.signal?.aborted)
|
|
1855
|
+
void response.body?.cancel().catch(() => { });
|
|
1856
|
+
return response;
|
|
1857
|
+
});
|
|
1858
|
+
}));
|
|
1859
|
+
if (credentialPost && !isOAuthCredentialRequest(init) && REDIRECT_STATUSES.has(response.status)) {
|
|
1860
|
+
(yield authCatch(authValue(() => response.body?.cancel()), () => { }));
|
|
1861
|
+
throw new ConnectorCallError("connector_call_failed", "OAuth registration endpoint redirected; the request was not repeated.", { retryable: false });
|
|
1669
1862
|
}
|
|
1670
|
-
const writeContinuation = downstreamWriteContinuation(ctx.requestScope ?? ctx) &&
|
|
1671
|
-
init?.method === "POST" &&
|
|
1672
|
-
skillObject(rpc) &&
|
|
1673
|
-
rpc.method === "tools/call";
|
|
1674
|
-
const response = writeContinuation
|
|
1675
|
-
? await fetch(input, { ...init, redirect: "manual" })
|
|
1676
|
-
: await fetch(input, init);
|
|
1677
1863
|
if (writeContinuation && (response.status === 401 || (response.status >= 300 && response.status < 400))) {
|
|
1678
|
-
|
|
1679
|
-
// Stop before the SDK can refresh auth or follow a redirect and resend
|
|
1680
|
-
// this write. The host's one-use continuation nonce remains spent.
|
|
1864
|
+
(yield authCatch(authValue(() => response.body?.cancel()), () => { }));
|
|
1681
1865
|
if (response.status === 401)
|
|
1682
1866
|
throw authRequiredError();
|
|
1683
|
-
throw new ConnectorCallError("connector_call_failed", "The downstream redirected a write continuation; it was not repeated.", { retryable: false });
|
|
1867
|
+
throw new ConnectorCallError("connector_call_failed", "The downstream redirected a write continuation; it was not repeated.", { retryable: false, writeOutcome: "unknown" });
|
|
1868
|
+
}
|
|
1869
|
+
if (flight?.redirected) {
|
|
1870
|
+
(yield authCatch(authValue(() => response.body?.cancel()), () => { }));
|
|
1871
|
+
throw new ConnectorCallError("connector_call_failed", "The downstream answered this write with a redirect; it was not sent again.", { retryable: false, writeOutcome: "unknown" });
|
|
1684
1872
|
}
|
|
1685
1873
|
if (skillsEnabled && skillObject(rpc) && (rpc.method === "skills/list" || rpc.method === "resources/read")) {
|
|
1686
|
-
return
|
|
1874
|
+
return (yield authCall(boundedSkillResponse, [
|
|
1875
|
+
response,
|
|
1876
|
+
rpc.method === "skills/list" ? MAX_SERIALIZED_CATALOG_BYTES : MAX_SKILL_READ_RPC_BYTES,
|
|
1877
|
+
rpc.id,
|
|
1878
|
+
init?.signal,
|
|
1879
|
+
]));
|
|
1687
1880
|
}
|
|
1688
1881
|
return response;
|
|
1689
|
-
};
|
|
1882
|
+
}));
|
|
1690
1883
|
// A runtime refusing the assembled header quotes it, and the transport
|
|
1691
1884
|
// error below keeps none of what the runtime said. So whether the
|
|
1692
1885
|
// rejection quoted the credential is decided here, first, and survives
|
|
1693
1886
|
// as the class of the error rather than as its text.
|
|
1694
1887
|
const guardedFetch = redirectSafeFetch(id, opts.redirects, credentialFramed === null
|
|
1695
1888
|
? trackedFetch
|
|
1696
|
-
:
|
|
1889
|
+
: authMethod((input, init) => authFlow(function* () {
|
|
1697
1890
|
try {
|
|
1698
|
-
return
|
|
1891
|
+
return (yield authCall(trackedFetch, [input, init]));
|
|
1699
1892
|
}
|
|
1700
1893
|
catch (err) {
|
|
1701
1894
|
throw withoutCredential(err, credentialValue, credentialFramed);
|
|
1702
1895
|
}
|
|
1703
|
-
});
|
|
1896
|
+
})));
|
|
1704
1897
|
if (opts.auth?.type === "oauth") {
|
|
1705
1898
|
const oauthProvider = provider ?? newProvider(ctx, undefined, signal);
|
|
1899
|
+
// The session/provider belong to the request. A renewal belongs to the
|
|
1900
|
+
// exact call whose HTTP response challenged it, until claim submission.
|
|
1901
|
+
// Carry that ownership on the response the SDK passes to onUnauthorized;
|
|
1902
|
+
// a shared mutable "current call" would borrow a concurrent call's lease.
|
|
1903
|
+
const renewalSignals = new WeakMap();
|
|
1904
|
+
const exchangeFetch = (send) => exchangeOnly
|
|
1905
|
+
? authMethod((input, init) => authView(ctx, authMethod((requestSignal) => authFlow(function* () {
|
|
1906
|
+
const response = (yield authCall(send, [
|
|
1907
|
+
input,
|
|
1908
|
+
{
|
|
1909
|
+
...init,
|
|
1910
|
+
signal: init?.signal ? AbortSignal.any([requestSignal, init.signal]) : requestSignal,
|
|
1911
|
+
},
|
|
1912
|
+
]));
|
|
1913
|
+
if (requestSignal.aborted) {
|
|
1914
|
+
void response.body?.cancel().catch(() => { });
|
|
1915
|
+
throw requestSignal.reason;
|
|
1916
|
+
}
|
|
1917
|
+
return codeExchangeResponse(response, requestSignal);
|
|
1918
|
+
})), true))
|
|
1919
|
+
: send;
|
|
1920
|
+
const coordinatedFetch = refreshCoordinatorFor(ctx).coordinatedFetch(oauthProvider, exchangeFetch(learnedUrlSafeFetch(id, url, trackedFetch)), signal, ctx.defer, exchangeFetch(learnedUrlSafeFetch(id, url, authMethod((input, init) => authCall(guardedFetch, [input, resourceInit(input, init)])))), () => {
|
|
1921
|
+
guardedTransport(true);
|
|
1922
|
+
});
|
|
1706
1923
|
const transport = new StreamableHTTPClientTransport(url, {
|
|
1707
1924
|
authProvider: oauthProvider,
|
|
1708
1925
|
onInsufficientScope: "throw",
|
|
1709
|
-
fetch:
|
|
1926
|
+
fetch: authMethod((input, init) => authFlow(function* () {
|
|
1927
|
+
const response = (yield authCall(coordinatedFetch, [input, init]));
|
|
1928
|
+
if (response.status === 401) {
|
|
1929
|
+
let owner;
|
|
1930
|
+
if (typeof init?.body === "string") {
|
|
1931
|
+
try {
|
|
1932
|
+
const rpc = JSON.parse(init.body);
|
|
1933
|
+
if (skillObject(rpc)) {
|
|
1934
|
+
if (rpc.method === "initialize" || rpc.method === "server/discover")
|
|
1935
|
+
owner = initiatingSignal;
|
|
1936
|
+
else if (typeof rpc.id === "string" || typeof rpc.id === "number")
|
|
1937
|
+
owner = flights.get(rpc.id)?.request?.ctx.signal;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
catch { }
|
|
1941
|
+
}
|
|
1942
|
+
const ownedSignal = owner && init?.signal ? AbortSignal.any([owner, init.signal]) : (owner ?? init?.signal);
|
|
1943
|
+
if (ownedSignal)
|
|
1944
|
+
renewalSignals.set(response, ownedSignal);
|
|
1945
|
+
}
|
|
1946
|
+
return response;
|
|
1947
|
+
})),
|
|
1710
1948
|
});
|
|
1711
|
-
boundOAuthFlows(ctx, transport, url, [signal, ctx.signal]);
|
|
1712
|
-
return transport;
|
|
1949
|
+
boundOAuthFlows(ctx, transport, url, [signal, ctx.signal], (response) => renewalSignals.get(response));
|
|
1950
|
+
return sendingEachWriteOnce(transport, flights, untrackedRead);
|
|
1713
1951
|
}
|
|
1714
1952
|
const headers = opts.auth?.type === "headers"
|
|
1715
1953
|
? opts.auth.headers
|
|
1716
1954
|
: (credentialAuth || requestAuth) && credentialFramed !== null
|
|
1717
1955
|
? { ...requestAuth?.headers, [credentialHeader]: credentialFramed }
|
|
1718
1956
|
: undefined;
|
|
1719
|
-
return new StreamableHTTPClientTransport(url, {
|
|
1957
|
+
return sendingEachWriteOnce(new StreamableHTTPClientTransport(url, {
|
|
1720
1958
|
...(headers ? { requestInit: { headers } } : {}),
|
|
1721
|
-
fetch: guardedFetch,
|
|
1722
|
-
});
|
|
1959
|
+
fetch: authMethod((input, init) => authCall(guardedFetch, [input, resourceInit(input, init)])),
|
|
1960
|
+
}), flights, untrackedRead);
|
|
1961
|
+
};
|
|
1962
|
+
/**
|
|
1963
|
+
* Admit a write's call to the wire once, whatever JSON-RPC id a later
|
|
1964
|
+
* message for it carries, and show `trackedFetch` which call each message
|
|
1965
|
+
* belongs to while it is sent. A missing record fails closed unless active
|
|
1966
|
+
* calls prove the tool is read (INV-1, INV-9).
|
|
1967
|
+
*/
|
|
1968
|
+
const sendingEachWriteOnce = (transport, flights, untrackedRead) => {
|
|
1969
|
+
const send = transport.send.bind(transport);
|
|
1970
|
+
transport.send = authMethod((message, options) => authFlow(function* () {
|
|
1971
|
+
const request = !Array.isArray(message) && "method" in message && message.method === "tools/call" && "id" in message
|
|
1972
|
+
? message
|
|
1973
|
+
: undefined;
|
|
1974
|
+
if (!request)
|
|
1975
|
+
return (yield authCall(send, [message, options]));
|
|
1976
|
+
const dispatch = dispatchOf(request.params);
|
|
1977
|
+
if (!dispatch) {
|
|
1978
|
+
if (!untrackedRead(request.params))
|
|
1979
|
+
throw missingDispatch();
|
|
1980
|
+
return (yield authCall(send, [message, options]));
|
|
1981
|
+
}
|
|
1982
|
+
if (dispatch.write && dispatch.sent)
|
|
1983
|
+
throw resendRefused(dispatch);
|
|
1984
|
+
flights.set(request.id, dispatch);
|
|
1985
|
+
try {
|
|
1986
|
+
return (yield authCall(send, [message, options]));
|
|
1987
|
+
}
|
|
1988
|
+
finally {
|
|
1989
|
+
flights.delete(request.id);
|
|
1990
|
+
}
|
|
1991
|
+
}));
|
|
1992
|
+
return transport;
|
|
1723
1993
|
};
|
|
1724
1994
|
const reset = (state) => {
|
|
1725
1995
|
state.lease = null;
|
|
@@ -1761,101 +2031,85 @@ export function remoteMcp(id, opts) {
|
|
|
1761
2031
|
if (release)
|
|
1762
2032
|
detach(release);
|
|
1763
2033
|
};
|
|
1764
|
-
const
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
if (provider.isOperatorDisconnectedEpoch(oauthGeneration)) {
|
|
1780
|
-
closeHalf(state);
|
|
1781
|
-
throw operatorDisconnectedError();
|
|
2034
|
+
const waitForConnection = (attempt, signal) => runEdge(Deferred.await(attempt), signal ? { signal } : {});
|
|
2035
|
+
const ensureConnected = authMethod((ctx, state) => authFlow(function* () {
|
|
2036
|
+
let joined = state.connecting;
|
|
2037
|
+
if (joined)
|
|
2038
|
+
connectingWaiters.set(joined, (connectingWaiters.get(joined) ?? 0) + 1);
|
|
2039
|
+
const leave = () => {
|
|
2040
|
+
if (!joined)
|
|
2041
|
+
return;
|
|
2042
|
+
const remaining = (connectingWaiters.get(joined) ?? 1) - 1;
|
|
2043
|
+
if (remaining > 0)
|
|
2044
|
+
connectingWaiters.set(joined, remaining);
|
|
2045
|
+
else {
|
|
2046
|
+
connectingWaiters.delete(joined);
|
|
2047
|
+
if (ctx.signal?.aborted && state.connecting === joined)
|
|
2048
|
+
closeHalf(state);
|
|
1782
2049
|
}
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
if (state.client && oauthGeneration !== undefined && state.connectedGeneration !== null) {
|
|
1788
|
-
if (isClosed(state))
|
|
1789
|
-
throw scopeEndedError();
|
|
1790
|
-
if (oauthGeneration !== state.connectedGeneration) {
|
|
1791
|
-
closeHalf(state);
|
|
2050
|
+
};
|
|
2051
|
+
try {
|
|
2052
|
+
if (state.authRequired) {
|
|
2053
|
+
throw authRequiredError();
|
|
1792
2054
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
credentialValue = await readCredential(ctx);
|
|
1804
|
-
credentialFramed = credentialHeaderValue(credentialScheme, credentialValue);
|
|
1805
|
-
credentialDigest = await digestOf(credentialValue);
|
|
1806
|
-
// Gated on a connect in flight as well as a cached client, exactly like
|
|
1807
|
-
// the epoch read above: a rotation that lands while the first caller is
|
|
1808
|
-
// still connecting must not let the second one ride the key the vault
|
|
1809
|
-
// has already replaced.
|
|
1810
|
-
if ((state.client || state.connecting) &&
|
|
1811
|
-
state.credentialDigest !== null &&
|
|
1812
|
-
state.credentialDigest !== credentialDigest) {
|
|
1813
|
-
// A slow vault/token read or digest can resume after a newer caller
|
|
1814
|
-
// connected with the replacement. Never let that stale read tear down
|
|
1815
|
-
// the newer connection and install the rotated-away credential.
|
|
1816
|
-
const observedConnection = state.client ?? state.connecting;
|
|
1817
|
-
const currentCredential = await readCredential(ctx);
|
|
2055
|
+
let oauthGeneration;
|
|
2056
|
+
if (isOauth && (state.client || state.connecting)) {
|
|
2057
|
+
const provider = newProvider(ctx, state, openRequest(ctx.requestScope ?? ctx).signal);
|
|
2058
|
+
oauthGeneration = (yield authView(ctx, authMethod(() => authInvoke(provider, "liveEpoch", []))));
|
|
2059
|
+
if (provider.isOperatorDisconnectedEpoch(oauthGeneration)) {
|
|
2060
|
+
closeHalf(state);
|
|
2061
|
+
throw operatorDisconnectedError();
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
if (state.client && oauthGeneration !== undefined && state.connectedGeneration !== null) {
|
|
1818
2065
|
if (isClosed(state))
|
|
1819
2066
|
throw scopeEndedError();
|
|
1820
|
-
if (
|
|
1821
|
-
|
|
2067
|
+
if (oauthGeneration !== state.connectedGeneration) {
|
|
2068
|
+
closeHalf(state);
|
|
1822
2069
|
}
|
|
1823
|
-
closeHalf(state);
|
|
1824
2070
|
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
else {
|
|
1845
|
-
connectingWaiters.delete(attempt);
|
|
1846
|
-
// A cancelled waiter leaves its siblings' handshake alone. Once every
|
|
1847
|
-
// waiter has left, abandon the attempt and its OAuth refresh too.
|
|
1848
|
-
if (ctx.signal?.aborted && state.connecting === attempt)
|
|
2071
|
+
let credentialValue = null;
|
|
2072
|
+
let credentialFramed = null;
|
|
2073
|
+
let credentialDigest = null;
|
|
2074
|
+
if (credentialAuth || requestAuth) {
|
|
2075
|
+
credentialValue = (yield authCall(readCredential, [ctx]));
|
|
2076
|
+
credentialFramed = credentialHeaderValue(credentialScheme, credentialValue);
|
|
2077
|
+
credentialDigest = (yield authCall(digestOf, [credentialValue]));
|
|
2078
|
+
if ((state.client || state.connecting) &&
|
|
2079
|
+
state.credentialDigest !== null &&
|
|
2080
|
+
state.credentialDigest !== credentialDigest) {
|
|
2081
|
+
const observedConnection = state.client ?? state.connecting;
|
|
2082
|
+
const currentCredential = (yield authCall(readCredential, [ctx]));
|
|
2083
|
+
if (isClosed(state))
|
|
2084
|
+
throw scopeEndedError();
|
|
2085
|
+
if (currentCredential !== credentialValue || (state.client ?? state.connecting) !== observedConnection) {
|
|
2086
|
+
throw resendAdvice(new ConnectorCallError("connector_call_failed", "The downstream credential changed while connecting.", {
|
|
2087
|
+
retryable: true,
|
|
2088
|
+
}), "; retry the operation");
|
|
2089
|
+
}
|
|
1849
2090
|
closeHalf(state);
|
|
2091
|
+
}
|
|
1850
2092
|
}
|
|
2093
|
+
viewSignal(ctx).throwIfAborted();
|
|
2094
|
+
if (isClosed(state))
|
|
2095
|
+
throw scopeEndedError();
|
|
2096
|
+
if (state.client)
|
|
2097
|
+
return state.client;
|
|
2098
|
+
const attempt = state.connecting ?? startConnect(ctx, state, credentialValue, credentialFramed, credentialDigest);
|
|
2099
|
+
if (joined !== attempt) {
|
|
2100
|
+
leave();
|
|
2101
|
+
joined = attempt;
|
|
2102
|
+
connectingWaiters.set(attempt, (connectingWaiters.get(attempt) ?? 0) + 1);
|
|
2103
|
+
}
|
|
2104
|
+
const client = (yield authCall(waitForConnection, [attempt, ctx.signal]));
|
|
2105
|
+
if (isClosed(state))
|
|
2106
|
+
throw scopeEndedError();
|
|
2107
|
+
return client;
|
|
1851
2108
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
throw scopeEndedError();
|
|
1857
|
-
return client;
|
|
1858
|
-
};
|
|
2109
|
+
finally {
|
|
2110
|
+
leave();
|
|
2111
|
+
}
|
|
2112
|
+
}));
|
|
1859
2113
|
/**
|
|
1860
2114
|
* Start this scope's connect attempt, publishing it before any of it runs.
|
|
1861
2115
|
* The attempt completes its Deferred itself, and every caller — the one that
|
|
@@ -1869,7 +2123,10 @@ export function remoteMcp(id, opts) {
|
|
|
1869
2123
|
// while this connect is in flight has to be visible to the next caller,
|
|
1870
2124
|
// which would otherwise wait on a client bound to the older key.
|
|
1871
2125
|
state.credentialDigest = credentialDigest;
|
|
1872
|
-
detach(connect(ctx, state, attempt, credentialValue, credentialFramed).pipe(
|
|
2126
|
+
detach(connect(ctx, state, attempt, credentialValue, credentialFramed).pipe(
|
|
2127
|
+
// The connection belongs to this request, without the first waiter's
|
|
2128
|
+
// call/credential/budget authority. Refresh coordinators stay separate.
|
|
2129
|
+
Effect.provideService(RequestScope, Context.get(viewServices(ctx), RequestScope)), Effect.onExit((exit) => Effect.sync(() => {
|
|
1873
2130
|
// Force reset may have abandoned this attempt and installed a new
|
|
1874
2131
|
// one in the same request scope. An old completion must not erase
|
|
1875
2132
|
// the new attempt and allow a third concurrent connect.
|
|
@@ -1888,8 +2145,8 @@ export function remoteMcp(id, opts) {
|
|
|
1888
2145
|
// The transport and its OAuth provider are shared by every call in this
|
|
1889
2146
|
// scope. A call's deadline signal is aborted even on success, so retaining
|
|
1890
2147
|
// the first caller's signal here would abort its siblings and later calls.
|
|
1891
|
-
//
|
|
1892
|
-
//
|
|
2148
|
+
// The provider uses the connection's lifetime. Each challenged response
|
|
2149
|
+
// separately binds renewal preparation to the call that initiated it.
|
|
1893
2150
|
const connectionAbort = new AbortController();
|
|
1894
2151
|
// Own cancellation before beginFlow's storage reads.
|
|
1895
2152
|
// The lease first owns just the signal, then the transport it creates.
|
|
@@ -1917,7 +2174,9 @@ export function remoteMcp(id, opts) {
|
|
|
1917
2174
|
// step-up — happens inside the SDK, bound to the epoch live now: a
|
|
1918
2175
|
// reset meanwhile fails the attempt rather than handing it the next
|
|
1919
2176
|
// epoch's grant.
|
|
1920
|
-
const genAtStart = provider
|
|
2177
|
+
const genAtStart = provider
|
|
2178
|
+
? yield* promised(authMethod(() => authInvoke(provider, "beginFlow", [])))
|
|
2179
|
+
: "";
|
|
1921
2180
|
if (!owned())
|
|
1922
2181
|
return yield* Effect.fail(scopeEndedError());
|
|
1923
2182
|
if (provider)
|
|
@@ -1938,39 +2197,57 @@ export function remoteMcp(id, opts) {
|
|
|
1938
2197
|
clientName: ctx.oauthClientName,
|
|
1939
2198
|
}
|
|
1940
2199
|
: undefined;
|
|
1941
|
-
const negotiationDigest = yield* promised(() => digestOf
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2200
|
+
const negotiationDigest = yield* promised(authMethod(() => authCall(digestOf, [
|
|
2201
|
+
JSON.stringify([
|
|
2202
|
+
opts.url,
|
|
2203
|
+
opts.versionNegotiation ?? "auto",
|
|
2204
|
+
opts.auth?.type,
|
|
2205
|
+
opts.auth?.type === "headers" ? opts.auth.headers : requestAuth?.headers,
|
|
2206
|
+
...(extraHeaders ? [extraHeaders] : []),
|
|
2207
|
+
credentialHeader,
|
|
2208
|
+
credentialScheme,
|
|
2209
|
+
opts.authScope ?? "shared",
|
|
2210
|
+
opts.redirects ?? "none",
|
|
2211
|
+
oauthConfig,
|
|
2212
|
+
oauthScope,
|
|
2213
|
+
state.credentialDigest,
|
|
2214
|
+
genAtStart,
|
|
2215
|
+
callerOf(ctx),
|
|
2216
|
+
skillsEnabled === true,
|
|
2217
|
+
]),
|
|
1956
2218
|
])));
|
|
1957
2219
|
const prior = opts.versionNegotiation === "legacy"
|
|
1958
2220
|
? undefined
|
|
1959
|
-
: yield* promised(() => readNegotiation
|
|
2221
|
+
: yield* promised(authMethod(() => authCall(readNegotiation, [ctx, negotiationDigest])));
|
|
1960
2222
|
if (!owned())
|
|
1961
2223
|
return yield* Effect.fail(scopeEndedError());
|
|
1962
|
-
const cacheOptions = yield* promised(() => catalogClientOptions
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
2224
|
+
const cacheOptions = yield* promised(authMethod(() => authCall(catalogClientOptions, [
|
|
2225
|
+
ctx,
|
|
2226
|
+
id,
|
|
2227
|
+
JSON.stringify({
|
|
2228
|
+
url: new URL(opts.url).href,
|
|
2229
|
+
auth: opts.auth?.type ?? null,
|
|
2230
|
+
headers: opts.auth?.type === "headers" ? opts.auth.headers : requestAuth?.headers,
|
|
2231
|
+
...(extraHeaders ? { extraHeaders } : {}),
|
|
2232
|
+
credentialHeader,
|
|
2233
|
+
credentialScheme,
|
|
2234
|
+
authScope: opts.authScope ?? "shared",
|
|
2235
|
+
redirects: opts.redirects ?? "none",
|
|
2236
|
+
versionNegotiation: opts.versionNegotiation ?? "auto",
|
|
2237
|
+
oauthConfig,
|
|
2238
|
+
oauthScope,
|
|
2239
|
+
}),
|
|
2240
|
+
JSON.stringify([genAtStart, state.credentialDigest]),
|
|
2241
|
+
connectionAbort.signal,
|
|
2242
|
+
cacheOwner,
|
|
2243
|
+
requestAuth || opts.authScope === "personal" ? "private" : "shared",
|
|
2244
|
+
provider
|
|
2245
|
+
? authMethod(() => authFlow(function* () {
|
|
2246
|
+
const tokens = yield authInvoke(provider, "tokens", []);
|
|
2247
|
+
return yield authCall(digestOf, [JSON.stringify(tokens)]);
|
|
2248
|
+
}))
|
|
2249
|
+
: undefined,
|
|
2250
|
+
])));
|
|
1974
2251
|
if (!owned())
|
|
1975
2252
|
return yield* Effect.fail(scopeEndedError());
|
|
1976
2253
|
const makeClient = () => {
|
|
@@ -2000,14 +2277,14 @@ export function remoteMcp(id, opts) {
|
|
|
2000
2277
|
state.transport = t;
|
|
2001
2278
|
held.transport = t;
|
|
2002
2279
|
recordWireErrors(t, () => observeCatalogChange(ctx, id, connectionAbort.signal, cacheOwner));
|
|
2003
|
-
yield* promised(
|
|
2280
|
+
yield* promised(authMethod(() => authFlow(function* () {
|
|
2004
2281
|
try {
|
|
2005
|
-
|
|
2282
|
+
(yield authInvoke(c, "connect", [
|
|
2283
|
+
t,
|
|
2284
|
+
{ signal: handshakeAbort.signal, ...(prior ? { prior } : {}) },
|
|
2285
|
+
]));
|
|
2006
2286
|
}
|
|
2007
2287
|
catch (error) {
|
|
2008
|
-
// Some legacy servers crash on an unknown pre-initialize method.
|
|
2009
|
-
// Only an auto probe's typed HTTP 5xx permits a fresh legacy
|
|
2010
|
-
// handshake. An auth failure, timeout or initialize failure does not.
|
|
2011
2288
|
if (prior ||
|
|
2012
2289
|
opts.versionNegotiation === "legacy" ||
|
|
2013
2290
|
!(error instanceof SdkHttpError) ||
|
|
@@ -2017,15 +2294,16 @@ export function remoteMcp(id, opts) {
|
|
|
2017
2294
|
!owned() ||
|
|
2018
2295
|
handshakeAbort.signal.aborted)
|
|
2019
2296
|
throw error;
|
|
2020
|
-
// The SDK closed the failed probe transport. Its replacement owns
|
|
2021
|
-
// a new connection, with the same credential and OAuth generation.
|
|
2022
2297
|
c = makeClient();
|
|
2023
2298
|
t = buildTransport(ctx, provider, credentialFramed, connectionAbort.signal, credentialValue);
|
|
2024
2299
|
state.transport = held.transport = t;
|
|
2025
2300
|
recordWireErrors(t, () => observeCatalogChange(ctx, id, connectionAbort.signal, cacheOwner));
|
|
2026
|
-
|
|
2301
|
+
(yield authInvoke(c, "connect", [
|
|
2302
|
+
t,
|
|
2303
|
+
{ signal: handshakeAbort.signal, prior: { kind: "legacy" } },
|
|
2304
|
+
]));
|
|
2027
2305
|
}
|
|
2028
|
-
}).pipe(Effect.mapError((err) => atMcpBoundary(ctx,
|
|
2306
|
+
}))).pipe(Effect.mapError((err) => atMcpBoundary(ctx,
|
|
2029
2307
|
// First, while the runtime's text can still be read for it.
|
|
2030
2308
|
withoutCredential(err, credentialValue, credentialFramed), "MCP handshake", t, [ctx.signal, connectionAbort.signal, handshakeAbort.signal])));
|
|
2031
2309
|
held.client = c;
|
|
@@ -2039,7 +2317,7 @@ export function remoteMcp(id, opts) {
|
|
|
2039
2317
|
// credentials this client just bound to. Discard it rather than cache
|
|
2040
2318
|
// a stale-isolate connection.
|
|
2041
2319
|
if (provider) {
|
|
2042
|
-
const generation = yield* promised(() => provider.
|
|
2320
|
+
const generation = yield* promised(authMethod(() => authInvoke(provider, "liveEpoch", []))).pipe(Effect.mapError((error) => (owned() ? error : scopeEndedError())));
|
|
2043
2321
|
// closeScope can land while the epoch read is pending, after
|
|
2044
2322
|
// connect succeeded but before this client is cached. Discard the
|
|
2045
2323
|
// client on that side of the await too.
|
|
@@ -2054,11 +2332,15 @@ export function remoteMcp(id, opts) {
|
|
|
2054
2332
|
state.authRequired = false;
|
|
2055
2333
|
const discover = c.getDiscoverResult();
|
|
2056
2334
|
if (!prior)
|
|
2057
|
-
yield* promised(() => storeNegotiation
|
|
2335
|
+
yield* promised(authMethod(() => authCall(storeNegotiation, [
|
|
2336
|
+
ctx,
|
|
2337
|
+
negotiationDigest,
|
|
2338
|
+
discover ? { kind: "modern", discover } : { kind: "legacy" },
|
|
2339
|
+
])));
|
|
2058
2340
|
if (!owned())
|
|
2059
2341
|
return yield* Effect.fail(scopeEndedError());
|
|
2060
2342
|
return c;
|
|
2061
|
-
}).pipe(Effect.catch((err) => {
|
|
2343
|
+
}).pipe(Effect.catchDefect(Effect.fail), Effect.catch((err) => {
|
|
2062
2344
|
// Close the failed connection through its client, without waiting.
|
|
2063
2345
|
if (owned()) {
|
|
2064
2346
|
state.lease = null;
|
|
@@ -2090,27 +2372,23 @@ export function remoteMcp(id, opts) {
|
|
|
2090
2372
|
}));
|
|
2091
2373
|
});
|
|
2092
2374
|
};
|
|
2093
|
-
const disconnectAuthorization =
|
|
2375
|
+
const disconnectAuthorization = authMethod((ctx, state, operatorDisconnected = false, preserveClient = false) => authFlow(function* () {
|
|
2094
2376
|
const provider = newProvider(ctx, state);
|
|
2095
|
-
// Publish the replacement epoch before waiting on or closing any
|
|
2096
|
-
// request-local transport. A hung connect therefore cannot delay the
|
|
2097
|
-
// fence, and every late OAuth write stays in the older namespace.
|
|
2098
2377
|
const reset = operatorDisconnected
|
|
2099
|
-
? provider
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2378
|
+
? authInvoke(provider, "disconnectAuthorization", [
|
|
2379
|
+
learnedUrlSafeFetch(id, new URL(opts.url), authMethod((input, init) => authFlow(function* () {
|
|
2380
|
+
trackSentRequest(ctx, input, init);
|
|
2381
|
+
return (yield authCall(sendCredentialPost, [input, init ?? {}]));
|
|
2382
|
+
}))),
|
|
2383
|
+
])
|
|
2384
|
+
: authInvoke(provider, "resetAuthorization", [false, preserveClient]);
|
|
2104
2385
|
try {
|
|
2105
|
-
|
|
2386
|
+
yield reset;
|
|
2106
2387
|
}
|
|
2107
2388
|
finally {
|
|
2108
|
-
// Abandon any connect in flight and close whichever half of the
|
|
2109
|
-
// client/transport exists. Reset is unconditional because storage may already
|
|
2110
|
-
// be fenced behind a newer epoch after a cleanup error.
|
|
2111
2389
|
closeHalf(state);
|
|
2112
2390
|
}
|
|
2113
|
-
};
|
|
2391
|
+
}));
|
|
2114
2392
|
// Built once from construction-time values: names, framing, and public
|
|
2115
2393
|
// URLs only. A static header's value and a stored credential never reach
|
|
2116
2394
|
// it, and a URL keeps origin and path: a CIMD document's query may carry a
|
|
@@ -2134,6 +2412,8 @@ export function remoteMcp(id, opts) {
|
|
|
2134
2412
|
...(describedClientMetadataUrl !== undefined ? { clientMetadataUrl: describedClientMetadataUrl } : {}),
|
|
2135
2413
|
}
|
|
2136
2414
|
: { mode: "none" };
|
|
2415
|
+
if (extraHeaders)
|
|
2416
|
+
authDescription.headerNames = [...new Set([...(authDescription.headerNames ?? []), ...Object.keys(extraHeaders)])];
|
|
2137
2417
|
const endpoint = describedEndpoint(opts.url);
|
|
2138
2418
|
const optionSources = Object.freeze({
|
|
2139
2419
|
"source.kind": "default",
|
|
@@ -2153,9 +2433,9 @@ export function remoteMcp(id, opts) {
|
|
|
2153
2433
|
if (ctx.signal?.aborted)
|
|
2154
2434
|
throw ctx.signal.reason instanceof Error ? ctx.signal.reason : scopeEndedError();
|
|
2155
2435
|
};
|
|
2156
|
-
const withSkillsClient =
|
|
2436
|
+
const withSkillsClient = authMethod((ctx, method, run) => authFlow(function* () {
|
|
2157
2437
|
const state = stateFor(ctx);
|
|
2158
|
-
const client =
|
|
2438
|
+
const client = (yield authCall(ensureConnected, [ctx, state]));
|
|
2159
2439
|
try {
|
|
2160
2440
|
skillScopeOpen(ctx, state);
|
|
2161
2441
|
const capabilities = client.getServerCapabilities();
|
|
@@ -2166,7 +2446,7 @@ export function remoteMcp(id, opts) {
|
|
|
2166
2446
|
(extension.directoryRead !== undefined && typeof extension.directoryRead !== "boolean")) {
|
|
2167
2447
|
throw new ConnectorCallError("connector_call_failed", "The downstream did not declare a valid Skills extension and resources capability.");
|
|
2168
2448
|
}
|
|
2169
|
-
const result =
|
|
2449
|
+
const result = (yield authCall(run, [client, state]));
|
|
2170
2450
|
skillScopeOpen(ctx, state);
|
|
2171
2451
|
return result;
|
|
2172
2452
|
}
|
|
@@ -2176,11 +2456,9 @@ export function remoteMcp(id, opts) {
|
|
|
2176
2456
|
const classified = atMcpBoundary(ctx, error, method, client.transport, [ctx.signal]);
|
|
2177
2457
|
if (requiresAuthorization(classified) && state.client === client)
|
|
2178
2458
|
state.authRequired = true;
|
|
2179
|
-
// The SDK and downstream can quote whole manifests or bodies in errors.
|
|
2180
|
-
// Keep their typed verdict and operator facts, never the quoted payload.
|
|
2181
2459
|
throw carryFailureFacts(classified, withheldAs(`Connector "${id}" downstream ${method} failed.`, classified));
|
|
2182
2460
|
}
|
|
2183
|
-
};
|
|
2461
|
+
}));
|
|
2184
2462
|
const connector = {
|
|
2185
2463
|
id,
|
|
2186
2464
|
...(opts.title !== undefined ? { title: opts.title } : {}),
|
|
@@ -2205,47 +2483,55 @@ export function remoteMcp(id, opts) {
|
|
|
2205
2483
|
...(skillsEnabled
|
|
2206
2484
|
? {
|
|
2207
2485
|
downstreamSkills: {
|
|
2208
|
-
list: (ctx) => withSkillsClient
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
listed.
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2486
|
+
list: authMethod((ctx) => authCall(withSkillsClient, [
|
|
2487
|
+
ctx,
|
|
2488
|
+
"skills/list",
|
|
2489
|
+
authMethod((client, state) => authFlow(function* () {
|
|
2490
|
+
const listed = [];
|
|
2491
|
+
const uris = new Set();
|
|
2492
|
+
const cursors = new Set();
|
|
2493
|
+
let cursor;
|
|
2494
|
+
let bytes = 0;
|
|
2495
|
+
for (let page = 0; page < MAX_SKILLS; page++) {
|
|
2496
|
+
skillScopeOpen(ctx, state);
|
|
2497
|
+
const result = (yield authInvoke(client, "request", [
|
|
2498
|
+
{ method: "skills/list", ...(cursor === undefined ? {} : { params: { cursor } }) },
|
|
2499
|
+
SkillPageSchema,
|
|
2500
|
+
requestOptions(ctx),
|
|
2501
|
+
]));
|
|
2502
|
+
bytes += result.bytes;
|
|
2503
|
+
if (bytes > MAX_SERIALIZED_CATALOG_BYTES || listed.length + result.skills.length > MAX_SKILLS) {
|
|
2504
|
+
throw new ConnectorCallError("connector_call_failed", "Downstream Skills listing exceeds the aggregate limit.");
|
|
2505
|
+
}
|
|
2506
|
+
for (const skill of result.skills) {
|
|
2507
|
+
if (uris.has(skill.uri))
|
|
2508
|
+
throw new ConnectorCallError("connector_call_failed", "Downstream Skills listing contains duplicate skill URIs.");
|
|
2509
|
+
uris.add(skill.uri);
|
|
2510
|
+
listed.push(skill);
|
|
2511
|
+
}
|
|
2512
|
+
if (result.nextCursor === undefined)
|
|
2513
|
+
return listed;
|
|
2514
|
+
if (!result.skills.length || cursors.has(result.nextCursor)) {
|
|
2515
|
+
throw new ConnectorCallError("connector_call_failed", "Downstream Skills pagination loops or makes no progress.");
|
|
2516
|
+
}
|
|
2517
|
+
cursors.add(result.nextCursor);
|
|
2518
|
+
cursor = result.nextCursor;
|
|
2231
2519
|
}
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2520
|
+
throw new ConnectorCallError("connector_call_failed", "Downstream Skills listing exceeds the page limit.");
|
|
2521
|
+
})),
|
|
2522
|
+
])),
|
|
2523
|
+
read: authMethod((uri, ctx) => authCall(withSkillsClient, [
|
|
2524
|
+
ctx,
|
|
2525
|
+
"resources/read",
|
|
2526
|
+
authMethod((client) => authInvoke(client, "request", [
|
|
2527
|
+
{ method: "resources/read", params: { uri } },
|
|
2528
|
+
skillReadSchema(uri),
|
|
2529
|
+
requestOptions(ctx),
|
|
2530
|
+
])),
|
|
2531
|
+
])),
|
|
2243
2532
|
},
|
|
2244
2533
|
}
|
|
2245
2534
|
: {}),
|
|
2246
|
-
// Data the registry classifies every read with; listTools below returns
|
|
2247
|
-
// the downstream's listing unclassified.
|
|
2248
|
-
...(classification !== undefined ? { classification } : {}),
|
|
2249
2535
|
// Declaring the slot is what makes the rest of the operator surface work:
|
|
2250
2536
|
// the connection's credential form renders it, the shape check compares
|
|
2251
2537
|
// against it, and authorize_connector returns the operator handoff rather than
|
|
@@ -2259,17 +2545,9 @@ export function remoteMcp(id, opts) {
|
|
|
2259
2545
|
* here is connecta's to verify — the credential's scope, project,
|
|
2260
2546
|
* and mode are the provider's answer, not ours.
|
|
2261
2547
|
*/
|
|
2262
|
-
testCredential:
|
|
2548
|
+
testCredential: authMethod((value, ctx) => authFlow(function* () {
|
|
2263
2549
|
try {
|
|
2264
|
-
|
|
2265
|
-
// assembled deep inside `ensureConnected`, and handing a
|
|
2266
|
-
// candidate down that path would mean threading a second secret
|
|
2267
|
-
// through the whole connection state. `/ui/credentials/<id>/test`
|
|
2268
|
-
// reads the stored value and passes it here, so today the two are
|
|
2269
|
-
// the same string. Check rather than assume: a route that later
|
|
2270
|
-
// tested an unsaved candidate would otherwise silently report on
|
|
2271
|
-
// the old value, which is the one answer worse than refusing.
|
|
2272
|
-
const stored = (await ctx.credential?.get())?.trim();
|
|
2550
|
+
const stored = (yield authCall(ctx.credential?.get, [], ctx.credential))?.trim();
|
|
2273
2551
|
if (stored !== value.trim()) {
|
|
2274
2552
|
return {
|
|
2275
2553
|
ok: false,
|
|
@@ -2277,7 +2555,7 @@ export function remoteMcp(id, opts) {
|
|
|
2277
2555
|
"saved. Save the value first, then test it.",
|
|
2278
2556
|
};
|
|
2279
2557
|
}
|
|
2280
|
-
const tools =
|
|
2558
|
+
const tools = (yield authInvoke(connector, "listTools", [ctx]));
|
|
2281
2559
|
return {
|
|
2282
2560
|
ok: true,
|
|
2283
2561
|
message: `Connected — the downstream served ${tools.length} tool${tools.length === 1 ? "" : "s"}.`,
|
|
@@ -2287,24 +2565,22 @@ export function remoteMcp(id, opts) {
|
|
|
2287
2565
|
return { ok: false, message: msg(err) };
|
|
2288
2566
|
}
|
|
2289
2567
|
finally {
|
|
2290
|
-
// Standalone tests own their scope; an operator route owns
|
|
2291
|
-
// bounded teardown for the context it supplied to this hook.
|
|
2292
2568
|
if (!connectorScopeCleanupClaimed(ctx, id))
|
|
2293
|
-
|
|
2569
|
+
(yield authCall(connector.closeScope, [ctx], connector));
|
|
2294
2570
|
}
|
|
2295
|
-
},
|
|
2571
|
+
})),
|
|
2296
2572
|
}
|
|
2297
2573
|
: {}),
|
|
2298
2574
|
// The SDK walks the whole chain and writes one complete result. Intake
|
|
2299
2575
|
// runs on its public request seam before any page can enter that cache.
|
|
2300
|
-
|
|
2576
|
+
listTools: authMethod((ctx) => authFlow(function* () {
|
|
2301
2577
|
const state = stateFor(ctx);
|
|
2302
|
-
const client =
|
|
2578
|
+
const client = (yield authCall(ensureConnected, [ctx, state]));
|
|
2303
2579
|
let clean;
|
|
2304
2580
|
const options = requestOptions(ctx) ?? {};
|
|
2305
2581
|
listingContexts.set(options, ctx);
|
|
2306
2582
|
try {
|
|
2307
|
-
const result =
|
|
2583
|
+
const result = (yield authInvoke(client, "listTools", [undefined, options]));
|
|
2308
2584
|
clean = redactCatalog(ctx, result.tools);
|
|
2309
2585
|
}
|
|
2310
2586
|
catch (err) {
|
|
@@ -2328,15 +2604,10 @@ export function remoteMcp(id, opts) {
|
|
|
2328
2604
|
...(t.title !== undefined ? { title: t.title } : {}),
|
|
2329
2605
|
...(t.icons !== undefined
|
|
2330
2606
|
? {
|
|
2331
|
-
// Inline images can consume the catalog ceiling and storage chunks.
|
|
2332
|
-
// The SDK needs no icons for validation or parameter mirroring.
|
|
2333
2607
|
icons: t.icons.filter((icon) => !/^data:/i.test(icon.src)),
|
|
2334
2608
|
}
|
|
2335
2609
|
: {}),
|
|
2336
2610
|
...(t.execution !== undefined ? { execution: t.execution } : {}),
|
|
2337
|
-
// SEP-2243 declarations live in inputSchema. SDK validation and header
|
|
2338
|
-
// mirroring need no _meta keys, so arbitrary downstream metadata stays
|
|
2339
|
-
// out of the persisted catalog.
|
|
2340
2611
|
...(t.description !== undefined ? { description: t.description } : {}),
|
|
2341
2612
|
...(t.inputSchema !== undefined
|
|
2342
2613
|
? {
|
|
@@ -2354,88 +2625,157 @@ export function remoteMcp(id, opts) {
|
|
|
2354
2625
|
}
|
|
2355
2626
|
: {}),
|
|
2356
2627
|
}));
|
|
2357
|
-
},
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
}
|
|
2362
|
-
const state = stateFor(ctx);
|
|
2363
|
-
const client = await ensureConnected(ctx, state);
|
|
2628
|
+
})),
|
|
2629
|
+
callTool: authMethod((name, args, ctx, options) => authFlow(function* () {
|
|
2630
|
+
enterProviderBoundary(ctx, connector);
|
|
2631
|
+
let active;
|
|
2364
2632
|
try {
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2633
|
+
return (yield authView(ctx, authMethod(() => authFlow(function* () {
|
|
2634
|
+
const state = stateFor(ctx);
|
|
2635
|
+
const client = (yield authCatch(authCall(ensureConnected, [ctx, state]), (error) => {
|
|
2636
|
+
throw knownProviderFailure(error);
|
|
2637
|
+
}));
|
|
2638
|
+
const definition = options?.definition;
|
|
2639
|
+
const facts = definition ?? state.toolDefinitions.get(name) ?? { name };
|
|
2640
|
+
const sourceFacts = definition ?? {
|
|
2641
|
+
name,
|
|
2642
|
+
...(facts.annotations
|
|
2643
|
+
? { annotations: facts.annotations }
|
|
2644
|
+
: {}),
|
|
2645
|
+
};
|
|
2646
|
+
const source = sourceClassification(sourceFacts);
|
|
2647
|
+
const serverWrite = source === "write";
|
|
2648
|
+
const write = serverWrite || (callClassification(ctx) ?? definition?.classification ?? source) === "write";
|
|
2649
|
+
const dispatch = newDispatch(write, serverWrite);
|
|
2650
|
+
dispatch.request = {
|
|
2651
|
+
entry: providerRequest(ctx, !write, `Connector "${id}"'s MCP server`, { method: "POST" }),
|
|
2652
|
+
ctx,
|
|
2653
|
+
};
|
|
2654
|
+
active = { state, client, dispatch, write };
|
|
2655
|
+
state.dispatches.set(dispatch, name);
|
|
2656
|
+
const toolDefinition = definition
|
|
2657
|
+
? {
|
|
2658
|
+
...definition,
|
|
2659
|
+
inputSchema: (definition.inputSchema ?? { type: "object" }),
|
|
2660
|
+
}
|
|
2661
|
+
: state.toolDefinitions.get(name);
|
|
2662
|
+
if (toolDefinition?.execution?.taskSupport === "required") {
|
|
2663
|
+
throw new ConnectorCallError("connector_call_failed", `Tool "${name}" requires task-based execution, which Connecta does not support.`);
|
|
2371
2664
|
}
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2665
|
+
let output;
|
|
2666
|
+
const outputError = (code, message) => atMcpBoundary(ctx, new ProtocolError(code, message), "tools/call", client.transport, [ctx.signal]);
|
|
2667
|
+
if (toolDefinition?.outputSchema) {
|
|
2668
|
+
try {
|
|
2669
|
+
output = fromJsonSchema(toolDefinition.outputSchema);
|
|
2670
|
+
}
|
|
2671
|
+
catch {
|
|
2672
|
+
throw outputError(-32602, `Tool '${name}' has an invalid outputSchema: schema could not be compiled`);
|
|
2673
|
+
}
|
|
2381
2674
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2675
|
+
const result = (yield authCatch(authInvoke(client, "callTool", [
|
|
2676
|
+
withDispatch({
|
|
2677
|
+
name,
|
|
2678
|
+
arguments: (args ?? {}),
|
|
2679
|
+
...options?.input,
|
|
2680
|
+
}, dispatch),
|
|
2681
|
+
{
|
|
2682
|
+
...requestOptions(ctx),
|
|
2683
|
+
allowInputRequired: true,
|
|
2684
|
+
...(toolDefinition
|
|
2685
|
+
? { toolDefinition: { ...toolDefinition, outputSchema: undefined } }
|
|
2686
|
+
: write
|
|
2687
|
+
? { toolDefinition: { name, inputSchema: { type: "object" } } }
|
|
2688
|
+
: {}),
|
|
2689
|
+
},
|
|
2690
|
+
]), (err) => {
|
|
2691
|
+
assertDownstreamOutputSafe(ctx.requestScope ?? ctx, err);
|
|
2692
|
+
throw atMcpBoundary(ctx, err, "tools/call", client.transport, [ctx.signal]);
|
|
2693
|
+
}));
|
|
2694
|
+
if (isInputRequiredResult(result)) {
|
|
2695
|
+
return result;
|
|
2696
|
+
}
|
|
2697
|
+
assertDownstreamOutputSafe(ctx.requestScope ?? ctx, result);
|
|
2698
|
+
if (output && !result.isError) {
|
|
2699
|
+
if (result.structuredContent === undefined)
|
|
2700
|
+
throw outputError(-32600, `Tool ${name} has an output schema but did not return structured content`);
|
|
2701
|
+
const validation = (yield authInvoke(output["~standard"], "validate", [
|
|
2702
|
+
result.structuredContent,
|
|
2703
|
+
]));
|
|
2704
|
+
if (validation.issues)
|
|
2705
|
+
throw outputError(-32602, "Structured content does not match the tool's output schema: validation failed");
|
|
2384
2706
|
}
|
|
2385
|
-
}
|
|
2386
|
-
const result = await client
|
|
2387
|
-
.callTool({
|
|
2388
|
-
name,
|
|
2389
|
-
arguments: (args ?? {}),
|
|
2390
|
-
...options?.input,
|
|
2391
|
-
}, {
|
|
2392
|
-
...requestOptions(ctx),
|
|
2393
|
-
allowInputRequired: true,
|
|
2394
|
-
// SDK 2.3.1's callTool checks outputSchema even for a suspension.
|
|
2395
|
-
// Retain its header mirroring, then validate only final results.
|
|
2396
|
-
...(toolDefinition ? { toolDefinition: { ...toolDefinition, outputSchema: undefined } } : {}),
|
|
2397
|
-
})
|
|
2398
|
-
.catch((err) => {
|
|
2399
|
-
assertDownstreamOutputSafe(ctx.requestScope ?? ctx, err);
|
|
2400
|
-
throw atMcpBoundary(ctx, err, "tools/call", client.transport, [ctx.signal]);
|
|
2401
|
-
});
|
|
2402
|
-
if (isInputRequiredResult(result)) {
|
|
2403
2707
|
return result;
|
|
2404
|
-
}
|
|
2405
|
-
assertDownstreamOutputSafe(ctx.requestScope ?? ctx, result);
|
|
2406
|
-
if (output && !result.isError) {
|
|
2407
|
-
if (result.structuredContent === undefined)
|
|
2408
|
-
throw outputError(-32600, `Tool ${name} has an output schema but did not return structured content`);
|
|
2409
|
-
const validation = await output["~standard"].validate(result.structuredContent);
|
|
2410
|
-
if (validation.issues)
|
|
2411
|
-
throw outputError(-32602, "Structured content does not match the tool's output schema: validation failed");
|
|
2412
|
-
}
|
|
2413
|
-
return result;
|
|
2708
|
+
}))));
|
|
2414
2709
|
}
|
|
2415
2710
|
catch (err) {
|
|
2416
|
-
|
|
2417
|
-
|
|
2711
|
+
if (!active)
|
|
2712
|
+
throw knownProviderFailure(err);
|
|
2713
|
+
const { state, client, dispatch, write } = active;
|
|
2714
|
+
const direct = write && callClassification(ctx) === undefined;
|
|
2715
|
+
const ownAbort = ownAbortReason(err, [ctx.signal]) && !hasSdkPayload(err);
|
|
2716
|
+
// The SDK can reject its wait synchronously from the abort event,
|
|
2717
|
+
// before the physical response's Promise observer records its facts.
|
|
2718
|
+
// Yield only settlement; no closed caller regains live authority.
|
|
2719
|
+
if (ownAbort)
|
|
2720
|
+
yield Effect.yieldNow;
|
|
2721
|
+
if (ownAbort && !direct)
|
|
2418
2722
|
throw err;
|
|
2419
|
-
if (requiresAuthorization(err))
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2723
|
+
if (requiresAuthorization(err) && state.client === client)
|
|
2724
|
+
state.authRequired = true;
|
|
2725
|
+
const failure = ownAbort
|
|
2726
|
+
? err
|
|
2727
|
+
: err instanceof UnauthorizedError
|
|
2728
|
+
? carryFailureFacts(err, authRequiredError())
|
|
2729
|
+
: downstreamCallError(err, undefined, undefined, undefined, requestSentSecrets(ctx));
|
|
2730
|
+
if (!write)
|
|
2731
|
+
throw failure;
|
|
2732
|
+
const renewal = dispatch.request?.entry.renewed
|
|
2733
|
+
? undefined
|
|
2734
|
+
: dispatch.resendRefused
|
|
2735
|
+
? authRequiredError()
|
|
2736
|
+
: failure;
|
|
2737
|
+
const unsent = (entry) => {
|
|
2738
|
+
const verdict = unsentAfterUnauthorized(`Connector "${id}"'s MCP server`, renewal, isOauth ? "downstream_oauth_required" : "auth_required");
|
|
2739
|
+
return entry ? vendorRefusal(verdict, entry) : verdict;
|
|
2740
|
+
};
|
|
2741
|
+
const verdict = dispatch.unauthorized
|
|
2742
|
+
? unsent(dispatch.request?.entry)
|
|
2743
|
+
: dispatch.redirected || dispatch.resendRefused
|
|
2744
|
+
? new ConnectorCallError("connector_call_failed", `Connector "${id}"'s MCP server ` +
|
|
2745
|
+
(dispatch.redirected
|
|
2746
|
+
? "answered this write with a redirect, and the write was not sent again, "
|
|
2747
|
+
: "was sent this write, and a second send of it was stopped, ") +
|
|
2748
|
+
"so whether it took effect is unknown: check the target before repeating it.", { retryable: false, writeOutcome: "unknown" })
|
|
2749
|
+
: failure;
|
|
2750
|
+
const vendor = { title: `connector "${id}"` };
|
|
2751
|
+
const status = failureRecord({}, err).httpStatus;
|
|
2752
|
+
const settled = providerCallFailure(ctx, vendor, verdict, {
|
|
2753
|
+
dispatched: dispatch.sent,
|
|
2754
|
+
...(status === undefined ? {} : { status }),
|
|
2755
|
+
fallback: "write",
|
|
2756
|
+
request: dispatch.request?.entry,
|
|
2757
|
+
});
|
|
2758
|
+
throw settled === failure ? failure : carryFailureFacts(failure, settled);
|
|
2426
2759
|
}
|
|
2427
|
-
|
|
2428
|
-
|
|
2760
|
+
finally {
|
|
2761
|
+
if (active)
|
|
2762
|
+
active.state.dispatches.delete(active.dispatch);
|
|
2763
|
+
}
|
|
2764
|
+
})),
|
|
2765
|
+
readResource: authMethod((uri, ctx) => authFlow(function* () {
|
|
2429
2766
|
const state = stateFor(ctx);
|
|
2430
|
-
const client =
|
|
2767
|
+
const client = (yield authCall(ensureConnected, [ctx, state]));
|
|
2431
2768
|
try {
|
|
2432
2769
|
const options = requestOptions(ctx) ?? {};
|
|
2433
2770
|
listingContexts.set(options, ctx);
|
|
2434
2771
|
try {
|
|
2435
2772
|
if (!client.getServerCapabilities()?.resources)
|
|
2436
2773
|
throw unadvertisedResource();
|
|
2437
|
-
const resources =
|
|
2438
|
-
const templates =
|
|
2774
|
+
const resources = (yield authInvoke(client, "listResources", [undefined, options]));
|
|
2775
|
+
const templates = (yield authInvoke(client, "listResourceTemplates", [
|
|
2776
|
+
undefined,
|
|
2777
|
+
options,
|
|
2778
|
+
]));
|
|
2439
2779
|
const exact = resources.resources.some((resource) => resource.uri === uri);
|
|
2440
2780
|
if (!exact) {
|
|
2441
2781
|
const match = resourceUriMatchesTemplates(uri, templates.resourceTemplates, (code) => resourceTemplateRefusals.add(code));
|
|
@@ -2454,18 +2794,19 @@ export function remoteMcp(id, opts) {
|
|
|
2454
2794
|
finally {
|
|
2455
2795
|
listingContexts.delete(options);
|
|
2456
2796
|
}
|
|
2457
|
-
const result =
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2797
|
+
const result = (yield authCatch(authInvoke(client, "readResource", [
|
|
2798
|
+
{ uri },
|
|
2799
|
+
{
|
|
2800
|
+
...requestOptions(ctx),
|
|
2801
|
+
cacheMode: "bypass",
|
|
2802
|
+
allowInputRequired: true,
|
|
2803
|
+
},
|
|
2804
|
+
]), (err) => {
|
|
2464
2805
|
if (err instanceof ResourceNotFoundError) {
|
|
2465
2806
|
throw new ConnectorCallError("not_found", "The downstream resource does not exist.");
|
|
2466
2807
|
}
|
|
2467
2808
|
throw atMcpBoundary(ctx, err, "resources/read", client.transport, [ctx.signal]);
|
|
2468
|
-
});
|
|
2809
|
+
}));
|
|
2469
2810
|
if (isInputRequiredResult(result)) {
|
|
2470
2811
|
throw new ConnectorCallError("input_required_unsupported", "Resource reads inside programs cannot request mid-call input.");
|
|
2471
2812
|
}
|
|
@@ -2480,170 +2821,161 @@ export function remoteMcp(id, opts) {
|
|
|
2480
2821
|
if (err instanceof UnauthorizedError)
|
|
2481
2822
|
throw carryFailureFacts(err, authRequiredError());
|
|
2482
2823
|
}
|
|
2483
|
-
throw downstreamCallError(err, undefined, undefined, undefined,
|
|
2824
|
+
throw downstreamCallError(err, undefined, undefined, undefined, requestSentSecrets(ctx));
|
|
2484
2825
|
}
|
|
2485
|
-
},
|
|
2486
|
-
|
|
2487
|
-
//
|
|
2488
|
-
//
|
|
2826
|
+
})),
|
|
2827
|
+
closeScope: authMethod((ctx) => authFlow(function* () {
|
|
2828
|
+
// Revocation and all finalizers start before any caller guard. Cleanup
|
|
2829
|
+
// retains its own bounded settlement even after the request has closed.
|
|
2489
2830
|
const state = entryFor(ctx);
|
|
2490
|
-
// Closed before any await, and before the finalizers run: every check
|
|
2491
|
-
// from here on sees the scope ended. A duplicate teardown finds it
|
|
2492
|
-
// closed and has nothing to run.
|
|
2493
2831
|
const release = Scope.closeUnsafe(state.scope, Exit.void);
|
|
2494
2832
|
const cacheClosing = closeCatalogCacheScope(ctx, id, cacheOwner);
|
|
2495
|
-
// Storage cannot cancel an already-started write. Bound the join in
|
|
2496
|
-
// parallel with transport cleanup so it adds no unbounded teardown tail.
|
|
2497
|
-
const cacheClosed = cacheClosing &&
|
|
2498
|
-
runEdge(Effect.raceAllFirst([
|
|
2499
|
-
Effect.promise(() => cacheClosing),
|
|
2500
|
-
Effect.sleep(Duration.millis(LOCAL_CLOSE_BUDGET_MS)),
|
|
2501
|
-
]));
|
|
2502
2833
|
reset(state);
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2834
|
+
const closing = runEdge(Effect.all([
|
|
2835
|
+
release ?? Effect.void,
|
|
2836
|
+
cacheClosing
|
|
2837
|
+
? Effect.raceAllFirst([
|
|
2838
|
+
Effect.promise(() => cacheClosing),
|
|
2839
|
+
Effect.sleep(Duration.millis(LOCAL_CLOSE_BUDGET_MS)),
|
|
2840
|
+
])
|
|
2841
|
+
: Effect.void,
|
|
2842
|
+
], { concurrency: "unbounded" }).pipe(Effect.asVoid));
|
|
2843
|
+
void closing.catch(() => { });
|
|
2844
|
+
if (viewSignal(ctx, true).aborted)
|
|
2845
|
+
return yield authValue(() => closing);
|
|
2846
|
+
return yield authView(ctx, authMethod(() => authValue(() => closing)), true);
|
|
2847
|
+
})),
|
|
2848
|
+
status: authMethod((ctx) => authFlow(function* () {
|
|
2509
2849
|
const state = stateFor(ctx);
|
|
2510
|
-
const report =
|
|
2511
|
-
const path = isOauth
|
|
2850
|
+
const report = authMethod((status) => authFlow(function* () {
|
|
2851
|
+
const path = isOauth
|
|
2852
|
+
? (yield authInvoke(newProvider(ctx, state), "registrationPath", []))
|
|
2853
|
+
: undefined;
|
|
2512
2854
|
return ownStatus({
|
|
2513
2855
|
...status,
|
|
2514
2856
|
...(path ? { registrationPath: path } : {}),
|
|
2515
2857
|
...(resourceTemplateRefusals.size ? { resourceTemplateRefusals: [...resourceTemplateRefusals] } : {}),
|
|
2516
2858
|
});
|
|
2517
|
-
};
|
|
2859
|
+
}));
|
|
2518
2860
|
try {
|
|
2519
|
-
|
|
2520
|
-
return report
|
|
2861
|
+
(yield authCall(ensureConnected, [ctx, state]));
|
|
2862
|
+
return (yield authCall(report, [{ state: "ok" }]));
|
|
2521
2863
|
}
|
|
2522
2864
|
catch (err) {
|
|
2523
|
-
// An empty slot, or one with no vault behind it, is reported the way a
|
|
2524
|
-
// missing grant is: present, unauthenticated, and repairable — never a
|
|
2525
|
-
// boot failure and never a silently absent connector.
|
|
2526
2865
|
if (err instanceof CredentialRequiredError) {
|
|
2527
|
-
return report
|
|
2866
|
+
return (yield authCall(report, [{ state: "auth_required", message: err.message }]));
|
|
2528
2867
|
}
|
|
2529
2868
|
if (err instanceof OperatorDisconnectedError) {
|
|
2530
|
-
return report
|
|
2869
|
+
return (yield authCall(report, [{ state: "auth_required", message: err.message }]));
|
|
2531
2870
|
}
|
|
2532
2871
|
if (state.authRequired) {
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
});
|
|
2872
|
+
return (yield authCall(report, [
|
|
2873
|
+
{
|
|
2874
|
+
state: "auth_required",
|
|
2875
|
+
message: credentialAuth
|
|
2876
|
+
? "Authorization required \u2014 the downstream rejected this connector's stored credential."
|
|
2877
|
+
: "Authorization required \u2014 open the URL to connect.",
|
|
2878
|
+
},
|
|
2879
|
+
]));
|
|
2542
2880
|
}
|
|
2543
|
-
// An operator surface: the record, never the error's own text.
|
|
2544
2881
|
return failureStatus(id, err);
|
|
2545
2882
|
}
|
|
2546
|
-
},
|
|
2547
|
-
|
|
2883
|
+
})),
|
|
2884
|
+
finishAuth: authMethod((code, ctx, callbackParams) => authView(ctx, authMethod((signal) => authFlow(function* () {
|
|
2548
2885
|
const state = stateFor(ctx);
|
|
2549
2886
|
const provider = newProvider(ctx, state);
|
|
2550
|
-
// verifyState ran on this request-scoped provider first and found the
|
|
2551
|
-
// consent; a programmatic exchange names it by its callback's state.
|
|
2552
2887
|
const consent = callbackParams?.get("state") ?? null;
|
|
2553
|
-
if (!consent || !(
|
|
2888
|
+
if (!consent || !(yield authInvoke(provider, "verifyCallbackState", [consent]))) {
|
|
2554
2889
|
throw new ConnectorCallError("connector_call_failed", `Connector "${id}" authorization callback matches no pending consent; nothing was exchanged.`);
|
|
2555
2890
|
}
|
|
2556
|
-
// The exchange reads and writes only the epoch its consent was written
|
|
2557
|
-
// in; it decides nothing about the grant there.
|
|
2558
2891
|
provider.validateCallbackIssuer(callbackParams?.get("iss") ?? null);
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
// fence before the token request is its to cross. A connection this
|
|
2563
|
-
// scope opened earlier speaks for its connect attempt, not this callback.
|
|
2564
|
-
const t = buildTransport(ctx, provider);
|
|
2892
|
+
(yield authInvoke(provider, "bindFlow", []));
|
|
2893
|
+
signal.throwIfAborted();
|
|
2894
|
+
const t = buildTransport(ctx, provider, null, signal, null, true);
|
|
2565
2895
|
const trail = {};
|
|
2566
2896
|
const internals = t;
|
|
2567
2897
|
const traced = tracedOAuthFetch(trail, new URL(opts.url), internals._fetchWithInit ?? fetch);
|
|
2568
2898
|
const exchange = new Proxy(t, {
|
|
2569
|
-
get: (target, key) =>
|
|
2899
|
+
get: (target, key) => key === "_fetchWithInit" ? traced : Reflect.get(target, key, target),
|
|
2570
2900
|
set: (target, key, value) => Reflect.set(target, key, value, target),
|
|
2571
2901
|
});
|
|
2572
2902
|
try {
|
|
2573
|
-
|
|
2574
|
-
// Reset so the next use reconnects with the freshly stored tokens.
|
|
2903
|
+
(yield authInvoke(exchange, "finishAuth", [callbackParams]));
|
|
2575
2904
|
closeHalf(state);
|
|
2576
2905
|
}
|
|
2577
2906
|
catch (err) {
|
|
2578
|
-
throw
|
|
2907
|
+
throw (yield authCall(withoutAuthorizationServerText, [
|
|
2908
|
+
ctx,
|
|
2909
|
+
err,
|
|
2910
|
+
trail,
|
|
2911
|
+
[signal, ctx.signal],
|
|
2912
|
+
]));
|
|
2579
2913
|
}
|
|
2580
2914
|
finally {
|
|
2581
|
-
// This exchange-only transport has no lease in the request scope.
|
|
2582
2915
|
detach(closeConnection(null, t, ctx.logger));
|
|
2583
2916
|
}
|
|
2584
|
-
},
|
|
2917
|
+
})), true)),
|
|
2585
2918
|
};
|
|
2586
2919
|
if (opts.auth?.type === "oauth") {
|
|
2587
|
-
connector.verifyState =
|
|
2920
|
+
connector.verifyState = authMethod((oauthState, ctx) => authFlow(function* () {
|
|
2588
2921
|
const state = stateFor(ctx);
|
|
2589
|
-
return newProvider(ctx, state)
|
|
2590
|
-
};
|
|
2591
|
-
connector.verifyCallbackIssuer =
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
};
|
|
2598
|
-
connector.
|
|
2599
|
-
|
|
2600
|
-
};
|
|
2601
|
-
connector.
|
|
2922
|
+
return (yield authInvoke(newProvider(ctx, state), "verifyState", [oauthState]));
|
|
2923
|
+
}));
|
|
2924
|
+
connector.verifyCallbackIssuer = authMethod((issuer, ctx) => Effect.try({
|
|
2925
|
+
try: () => {
|
|
2926
|
+
newProvider(ctx, stateFor(ctx)).validateCallbackIssuer(issuer);
|
|
2927
|
+
return true;
|
|
2928
|
+
},
|
|
2929
|
+
catch: (error) => error,
|
|
2930
|
+
}));
|
|
2931
|
+
connector.consumeAuthError = authMethod((ctx) => authFlow(function* () {
|
|
2932
|
+
(yield authInvoke(newProvider(ctx, stateFor(ctx)), "consumeAuthError", []));
|
|
2933
|
+
}));
|
|
2934
|
+
connector.disconnectAuth = authMethod((ctx) => authFlow(function* () {
|
|
2935
|
+
(yield authCall(disconnectAuthorization, [ctx, stateFor(ctx), true]));
|
|
2936
|
+
}));
|
|
2937
|
+
connector.startAuth = authMethod((ctx, startOpts) => authFlow(function* () {
|
|
2602
2938
|
ctx = authorizingContext(ctx);
|
|
2603
2939
|
const state = stateFor(ctx);
|
|
2604
2940
|
state.provider = null;
|
|
2605
2941
|
const p = newProvider(ctx, state);
|
|
2606
2942
|
if (ctx.signal?.aborted)
|
|
2607
2943
|
throw ctx.signal.reason;
|
|
2608
|
-
const disconnected = startOpts?.force
|
|
2944
|
+
const disconnected = startOpts?.force
|
|
2945
|
+
? false
|
|
2946
|
+
: (yield authInvoke(p, "operatorDisconnected", []));
|
|
2609
2947
|
if (ctx.signal?.aborted)
|
|
2610
2948
|
throw ctx.signal.reason;
|
|
2611
2949
|
if (startOpts?.force || disconnected) {
|
|
2612
|
-
|
|
2950
|
+
(yield authCall(disconnectAuthorization, [ctx, state, false, startOpts?.force === true]));
|
|
2613
2951
|
}
|
|
2614
2952
|
else {
|
|
2615
|
-
|
|
2616
|
-
// the SDK flow, which would overwrite the PKCE verifier and invalidate
|
|
2617
|
-
// the URL the operator may be mid-consent on. Only a recent one: an
|
|
2618
|
-
// old or untimed URL falls through to a fresh flow in this same
|
|
2619
|
-
// epoch, which keeps the stored client registration and discovery.
|
|
2620
|
-
const pending = await p.reusablePendingAuthorizationUrl();
|
|
2953
|
+
const pending = (yield authInvoke(p, "reusablePendingAuthorizationUrl", []));
|
|
2621
2954
|
if (pending) {
|
|
2622
2955
|
return {
|
|
2623
2956
|
state: "auth_required",
|
|
2624
2957
|
authorizationUrl: pending,
|
|
2625
2958
|
authorizationReused: true,
|
|
2626
|
-
message: "Authorization required
|
|
2959
|
+
message: "Authorization required \u2014 open the URL to connect.",
|
|
2627
2960
|
};
|
|
2628
2961
|
}
|
|
2629
2962
|
}
|
|
2630
2963
|
if (ctx.signal?.aborted)
|
|
2631
2964
|
throw ctx.signal.reason;
|
|
2632
2965
|
try {
|
|
2633
|
-
|
|
2966
|
+
(yield authCall(ensureConnected, [ctx, state]));
|
|
2634
2967
|
return {
|
|
2635
2968
|
state: "ok",
|
|
2636
|
-
message: "Already authorized
|
|
2969
|
+
message: "Already authorized \u2014 connection is healthy.",
|
|
2637
2970
|
};
|
|
2638
2971
|
}
|
|
2639
2972
|
catch (err) {
|
|
2640
2973
|
if (state.authRequired) {
|
|
2641
|
-
// The consent this start's attempt stored, while its epoch is live:
|
|
2642
|
-
// one a later reset published belongs to another flow, and this
|
|
2643
|
-
// start fails rather than hand it out.
|
|
2644
2974
|
let authorizationUrl;
|
|
2645
2975
|
try {
|
|
2646
|
-
authorizationUrl =
|
|
2976
|
+
authorizationUrl = (yield state.attemptProvider
|
|
2977
|
+
? authInvoke(state.attemptProvider, "consentUrl", [viewSignal(ctx, true)])
|
|
2978
|
+
: Effect.succeed(undefined));
|
|
2647
2979
|
}
|
|
2648
2980
|
catch (readErr) {
|
|
2649
2981
|
return { state: "error", message: startMessage(readErr) };
|
|
@@ -2651,20 +2983,31 @@ export function remoteMcp(id, opts) {
|
|
|
2651
2983
|
return {
|
|
2652
2984
|
state: "auth_required",
|
|
2653
2985
|
...(authorizationUrl !== undefined ? { authorizationUrl } : {}),
|
|
2654
|
-
message: "Authorization required
|
|
2986
|
+
message: "Authorization required \u2014 open the URL to connect.",
|
|
2655
2987
|
};
|
|
2656
2988
|
}
|
|
2657
2989
|
return { state: "error", message: startMessage(err) };
|
|
2658
2990
|
}
|
|
2659
|
-
};
|
|
2991
|
+
}));
|
|
2660
2992
|
}
|
|
2661
|
-
const
|
|
2993
|
+
const retainOperation = (operation, contextIndex) => authMethod((...args) => authFlow(function* () {
|
|
2994
|
+
const release = retainOAuthPartition(oauthPartitionFor(args[contextIndex]));
|
|
2995
|
+
try {
|
|
2996
|
+
return yield authCall(operation, args);
|
|
2997
|
+
}
|
|
2998
|
+
finally {
|
|
2999
|
+
release();
|
|
3000
|
+
}
|
|
3001
|
+
}));
|
|
3002
|
+
const withActiveSecrets = authMethod((ctx, run, redactResult = true, preserveInput = false) => authFlow(function* () {
|
|
2662
3003
|
trackCredentialReads(ctx);
|
|
2663
3004
|
const secrets = sentSecretsFor(ctx);
|
|
2664
3005
|
if (opts.auth?.type === "headers") {
|
|
2665
3006
|
for (const value of Object.values(opts.auth.headers))
|
|
2666
3007
|
secrets.header(value);
|
|
2667
3008
|
}
|
|
3009
|
+
for (const value of Object.values(extraHeaders ?? {}))
|
|
3010
|
+
secrets.header(value);
|
|
2668
3011
|
const state = entryFor(ctx);
|
|
2669
3012
|
let active = activeContexts.get(state);
|
|
2670
3013
|
if (!active) {
|
|
@@ -2673,9 +3016,7 @@ export function remoteMcp(id, opts) {
|
|
|
2673
3016
|
}
|
|
2674
3017
|
active.add(ctx);
|
|
2675
3018
|
try {
|
|
2676
|
-
const result =
|
|
2677
|
-
// Opaque state must reach the sealing handler byte-exact. Invocation
|
|
2678
|
-
// intercepts it before ordinary result processing or guest exposure.
|
|
3019
|
+
const result = (yield authCall(run, []));
|
|
2679
3020
|
if (!preserveInput || !isInputRequiredResult(result))
|
|
2680
3021
|
assertDownstreamOutputSafe(ctx.requestScope ?? ctx, result);
|
|
2681
3022
|
return (preserveInput && isInputRequiredResult(result)) || !redactResult
|
|
@@ -2684,35 +3025,82 @@ export function remoteMcp(id, opts) {
|
|
|
2684
3025
|
}
|
|
2685
3026
|
catch (error) {
|
|
2686
3027
|
assertDownstreamOutputSafe(ctx.requestScope ?? ctx, error);
|
|
2687
|
-
throw
|
|
3028
|
+
throw requestSentSecrets(ctx).redact(error);
|
|
2688
3029
|
}
|
|
2689
3030
|
finally {
|
|
2690
3031
|
active.delete(ctx);
|
|
2691
3032
|
}
|
|
2692
|
-
};
|
|
3033
|
+
}));
|
|
2693
3034
|
const callTool = connector.callTool;
|
|
2694
|
-
connector.callTool = (name, args, ctx, options) => withActiveSecrets
|
|
3035
|
+
connector.callTool = authMethod((name, args, ctx, options) => authCall(withActiveSecrets, [
|
|
3036
|
+
ctx,
|
|
3037
|
+
authMethod(() => authCall(callTool, [name, args, ctx, options])),
|
|
3038
|
+
true,
|
|
3039
|
+
true,
|
|
3040
|
+
]));
|
|
2695
3041
|
const listTools = connector.listTools;
|
|
2696
|
-
connector.listTools = (ctx) => withActiveSecrets
|
|
3042
|
+
connector.listTools = authMethod((ctx) => authCall(withActiveSecrets, [
|
|
3043
|
+
ctx,
|
|
3044
|
+
authMethod(() => authCall(listTools, [ctx])),
|
|
3045
|
+
]));
|
|
2697
3046
|
const readResource = connector.readResource;
|
|
2698
|
-
connector.readResource = (uri, ctx) => withActiveSecrets
|
|
3047
|
+
connector.readResource = authMethod((uri, ctx) => authCall(withActiveSecrets, [
|
|
3048
|
+
ctx,
|
|
3049
|
+
authMethod(() => authCall(readResource, [uri, ctx])),
|
|
3050
|
+
]));
|
|
2699
3051
|
if (connector.downstreamSkills) {
|
|
2700
3052
|
const { list, read } = connector.downstreamSkills;
|
|
2701
|
-
connector.downstreamSkills.list = (
|
|
2702
|
-
|
|
3053
|
+
connector.downstreamSkills.list = authMethod((ctx) => authCall(withActiveSecrets, [
|
|
3054
|
+
ctx,
|
|
3055
|
+
authMethod(() => authCall(list, [ctx])),
|
|
3056
|
+
false,
|
|
3057
|
+
]));
|
|
3058
|
+
connector.downstreamSkills.read = authMethod((uri, ctx) => authCall(withActiveSecrets, [
|
|
3059
|
+
ctx,
|
|
3060
|
+
authMethod(() => authCall(read, [uri, ctx])),
|
|
3061
|
+
false,
|
|
3062
|
+
]));
|
|
2703
3063
|
}
|
|
2704
3064
|
if (isOauth && !staticClient && clientMetadataUrl === undefined)
|
|
2705
3065
|
declareSelfHostedClient(connector, oauthScope);
|
|
2706
3066
|
if (isOauth) {
|
|
2707
3067
|
const start = connector.startAuth;
|
|
2708
|
-
connector.startAuth =
|
|
2709
|
-
const status =
|
|
2710
|
-
const path =
|
|
3068
|
+
connector.startAuth = authMethod((...args) => authFlow(function* () {
|
|
3069
|
+
const status = (yield authCall(start, [...args]));
|
|
3070
|
+
const path = (yield authInvoke(newProvider(args[0], stateFor(args[0])), "registrationPath", []));
|
|
2711
3071
|
return { ...status, ...(path ? { registrationPath: path } : {}) };
|
|
2712
|
-
};
|
|
3072
|
+
}));
|
|
3073
|
+
}
|
|
3074
|
+
// Every published Promise operation owns its whole wait, including local
|
|
3075
|
+
// storage, digest, registration-path and result processing seams. callTool
|
|
3076
|
+
// settles inside its write decider; closeScope starts cleanup before racing
|
|
3077
|
+
// the join, and finishAuth already owns its request-scoped exchange boundary.
|
|
3078
|
+
const settlingOperation = (operation, contextIndex, requestOwned = false) => authMethod((...args) => authFlow(function* () {
|
|
3079
|
+
const ctx = args[contextIndex];
|
|
3080
|
+
const state = states.get(Context.get(viewServices(ctx), RequestScope));
|
|
3081
|
+
if (state && isClosed(state))
|
|
3082
|
+
return (yield Effect.fail(scopeEndedError()));
|
|
3083
|
+
return (yield authView(ctx, authMethod(() => authCall(operation, [...args])), requestOwned));
|
|
3084
|
+
}));
|
|
3085
|
+
connector.listTools = settlingOperation(connector.listTools, 0);
|
|
3086
|
+
connector.readResource = settlingOperation(connector.readResource, 1);
|
|
3087
|
+
if (connector.downstreamSkills) {
|
|
3088
|
+
connector.downstreamSkills.list = settlingOperation(connector.downstreamSkills.list, 0);
|
|
3089
|
+
connector.downstreamSkills.read = settlingOperation(connector.downstreamSkills.read, 1);
|
|
3090
|
+
}
|
|
3091
|
+
const status = connector.status;
|
|
3092
|
+
connector.status = authMethod((ctx) => authCatch(authView(ctx, authMethod(() => authCall(status, [ctx]))), (error) => failureStatus(id, error)));
|
|
3093
|
+
if (connector.testCredential)
|
|
3094
|
+
connector.testCredential = settlingOperation(connector.testCredential, 1);
|
|
3095
|
+
if (isOauth) {
|
|
3096
|
+
connector.startAuth = settlingOperation(connector.startAuth, 0);
|
|
3097
|
+
connector.disconnectAuth = settlingOperation(connector.disconnectAuth, 0);
|
|
3098
|
+
connector.verifyState = settlingOperation(connector.verifyState, 1, true);
|
|
3099
|
+
connector.verifyCallbackIssuer = settlingOperation(connector.verifyCallbackIssuer, 1, true);
|
|
3100
|
+
connector.consumeAuthError = settlingOperation(connector.consumeAuthError, 0, true);
|
|
2713
3101
|
// Pin before the first asynchronous storage/discovery read, not only once
|
|
2714
3102
|
// a refresh flight exists. These operations do not take call admission.
|
|
2715
|
-
const retain =
|
|
3103
|
+
const retain = retainOperation;
|
|
2716
3104
|
connector.listTools = retain(connector.listTools, 0);
|
|
2717
3105
|
connector.callTool = retain(connector.callTool, 2);
|
|
2718
3106
|
connector.readResource = retain(connector.readResource, 1);
|
|
@@ -2739,15 +3127,19 @@ export function remoteMcp(id, opts) {
|
|
|
2739
3127
|
connector.startAuth = payloadFree(connector.startAuth);
|
|
2740
3128
|
if (connector.finishAuth)
|
|
2741
3129
|
connector.finishAuth = payloadFree(connector.finishAuth);
|
|
3130
|
+
// This boundary's identity in a logical call: this object with its
|
|
3131
|
+
// published, fully wrapped function, fixed now, never what the property or
|
|
3132
|
+
// a copy holds by then.
|
|
3133
|
+
providerBoundary(connector);
|
|
3134
|
+
const ownCall = connector.callTool;
|
|
3135
|
+
registerCallEntry(connector, (name, args, ctx, options) => authCall(ownCall, [name, args, ctx, options]));
|
|
2742
3136
|
if (isOauth)
|
|
2743
|
-
registerInvocationAuth(connector,
|
|
2744
|
-
// Only inspect local grant state here. Discovery, refresh, and MCP
|
|
2745
|
-
// transports remain inside callTool and cannot establish retry safety.
|
|
3137
|
+
registerInvocationAuth(connector, retainOperation(settlingOperation(authMethod((ctx) => authFlow(function* () {
|
|
2746
3138
|
const provider = newProvider(ctx, stateFor(ctx));
|
|
2747
|
-
if (
|
|
3139
|
+
if ((yield authInvoke(provider, "operatorDisconnected", [])))
|
|
2748
3140
|
throw operatorDisconnectedError();
|
|
2749
|
-
if (!(
|
|
3141
|
+
if (!(yield authInvoke(provider, "tokens", [])))
|
|
2750
3142
|
throw authRequiredError();
|
|
2751
|
-
}, 0));
|
|
3143
|
+
})), 0), 0));
|
|
2752
3144
|
return connector;
|
|
2753
3145
|
}
|