@zackbart/connecta 0.34.0 → 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 +393 -0
- package/README.md +62 -105
- 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 +15 -7
- package/dist/connectors/api-connector.js +147 -57
- 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/option-shapes.js +1 -1
- package/dist/connectors/remote-mcp.d.ts +12 -30
- package/dist/connectors/remote-mcp.js +960 -564
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +5 -1
- package/dist/describe-config.js +3 -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 +221 -87
- 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 +534 -408
- 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 +8 -4
- package/dist/operator-ui/connection-map.js +9 -6
- package/dist/operator-ui/contract.d.ts +7 -3
- package/dist/operator-ui/endpoints.js +5 -14
- 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/health.js +52 -0
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/operator-ui/overview.js +11 -19
- package/dist/operator-ui/setup-commands.js +0 -4
- package/dist/operator-ui/view.d.ts +5 -29
- package/dist/operator-ui/view.js +3 -59
- 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 +71 -32
- package/dist/provider.js +134 -40
- 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 +8 -209
- package/dist/providers/_shared/google/workspace.js +7 -426
- 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 +3 -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 +17 -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 +7 -22
- package/dist/providers/cloudflare/index.js +80 -194
- 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 +3299 -3208
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +538 -18
- 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 +3 -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 +6 -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 +7 -27
- package/dist/providers/stripe/index.js +75 -191
- 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 -10
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +19 -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 +11 -25
- package/dist/providers/vercel/index.js +67 -261
- 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 +398 -376
- package/dist/providers/vercel/{value-safety.js → safety.js} +261 -42
- 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 +30 -28
- package/dist/registry.js +446 -413
- 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 +29 -22
- 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 +330 -224
- 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 +11 -99
- 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 +2550 -186
- package/documentation/auth.md +821 -98
- package/documentation/code-mode.md +56 -21
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +144 -105
- package/documentation/meta-tools.md +411 -34
- package/documentation/operating.md +28 -4
- package/documentation/operator-tests.md +11 -2
- package/documentation/operator-ui.md +103 -33
- package/documentation/provider-auth-migration.md +38 -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 -42
- 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 -134
- package/dist/catalog-drift.js +0 -493
- 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 -43
- 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 -1078
- 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/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -352
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -57
- package/dist/providers/mixpanel/index.js +0 -468
- 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 -65
- 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 -49
- package/dist/providers/revenuecat/index.js +0 -557
- 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,55 +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
|
-
import { 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
|
-
}
|
|
40
|
+
import { READ_ONLY, CALL_ADMISSION, REMOTE_MCP_AUTH, USAGE_GUIDE } from "./option-shapes.js";
|
|
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"),
|
|
44
|
+
readOnly: READ_ONLY,
|
|
49
45
|
callAdmission: CALL_ADMISSION,
|
|
50
46
|
usageGuide: USAGE_GUIDE,
|
|
51
47
|
auth: REMOTE_MCP_AUTH,
|
|
48
|
+
headers: strings(),
|
|
52
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
|
+
}
|
|
53
83
|
/**
|
|
54
84
|
* How long a downstream gets to answer the session-termination DELETE before
|
|
55
85
|
* teardown stops waiting. This is a network round-trip budget, deliberately
|
|
@@ -454,10 +484,11 @@ function isCursorShapeError(err) {
|
|
|
454
484
|
* passed the MCP and OAuth boundaries, and the origin and errno connecta
|
|
455
485
|
* derived for it, which a message alone would drop.
|
|
456
486
|
*/
|
|
487
|
+
/** An authorization start's failure for authorize_connector: no write's, so its advice stays. */
|
|
457
488
|
function startMessage(err) {
|
|
458
489
|
const details = err instanceof ConnectorCallError ? err.details : undefined;
|
|
459
490
|
const facts = [details?.host, details?.code].filter(Boolean).join(", ");
|
|
460
|
-
return facts ? `${msg(err)} (${facts})` : msg(err);
|
|
491
|
+
return withResendAdvice(facts ? `${msg(err)} (${facts})` : msg(err), resendAdviceOf(err));
|
|
461
492
|
}
|
|
462
493
|
/**
|
|
463
494
|
* End the downstream's session before the connection is torn down.
|
|
@@ -498,7 +529,7 @@ function terminateSession(transport, logger, connectorId) {
|
|
|
498
529
|
return Effect.raceAllFirst([
|
|
499
530
|
// The SDK issues no request at all when no `mcp-session-id` was captured,
|
|
500
531
|
// so a stateless downstream never sees a spurious DELETE.
|
|
501
|
-
promised(() =>
|
|
532
|
+
promised(authMethod(() => authInvoke(Reflect, "apply", [terminate, transport, []]))).pipe(Effect.catch((error) =>
|
|
502
533
|
// The SDK's error quotes the downstream's status text, and its `data`
|
|
503
534
|
// the body, so the record keeps only the status it answered with.
|
|
504
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))))),
|
|
@@ -519,7 +550,7 @@ function closeLocally(client, transport) {
|
|
|
519
550
|
if (!client && !transport)
|
|
520
551
|
return Effect.void;
|
|
521
552
|
return Effect.raceAllFirst([
|
|
522
|
-
promised(() =>
|
|
553
|
+
promised(authMethod(() => (client ? authInvoke(client, "close", []) : authValue(() => transport?.close())))).pipe(Effect.ignore),
|
|
523
554
|
Effect.sleep(Duration.millis(LOCAL_CLOSE_BUDGET_MS)),
|
|
524
555
|
]);
|
|
525
556
|
}
|
|
@@ -530,7 +561,7 @@ function closeLocally(client, transport) {
|
|
|
530
561
|
* allocates no AbortController.
|
|
531
562
|
*/
|
|
532
563
|
function promised(evaluate) {
|
|
533
|
-
return
|
|
564
|
+
return authCall(evaluate, []);
|
|
534
565
|
}
|
|
535
566
|
/** A retained cause chain must not smuggle an SDK payload past the boundary. */
|
|
536
567
|
function hasSdkPayload(error) {
|
|
@@ -544,14 +575,14 @@ function hasSdkPayload(error) {
|
|
|
544
575
|
}
|
|
545
576
|
/** Cancellation can bypass an inner SDK catch and return the signal's reason. */
|
|
546
577
|
function payloadFree(run) {
|
|
547
|
-
return
|
|
578
|
+
return authMethod((...args) => authFlow(function* () {
|
|
548
579
|
try {
|
|
549
|
-
return
|
|
580
|
+
return (yield authCall(run, [...args]));
|
|
550
581
|
}
|
|
551
582
|
catch (error) {
|
|
552
583
|
throw hasSdkPayload(error) ? carryFailureFacts(error, downstreamCallError(error)) : error;
|
|
553
584
|
}
|
|
554
|
-
};
|
|
585
|
+
}));
|
|
555
586
|
}
|
|
556
587
|
/** Both the SDK's auth signal and its classified form invalidate a live client. */
|
|
557
588
|
function requiresAuthorization(error) {
|
|
@@ -651,7 +682,7 @@ async function digestOf(value) {
|
|
|
651
682
|
*
|
|
652
683
|
* A scheme ending in `Basic` names HTTP Basic credentials wherever a provider
|
|
653
684
|
* nests it, so the `user:secret` it frames is base64-encoded — that is what
|
|
654
|
-
* makes plain `Basic` and
|
|
685
|
+
* makes plain `Basic` and nested `Bearer Basic` one rule instead of two.
|
|
655
686
|
*/
|
|
656
687
|
function credentialHeaderValue(scheme, value) {
|
|
657
688
|
if (scheme === null)
|
|
@@ -725,7 +756,7 @@ function redirectedInit(init, status) {
|
|
|
725
756
|
* arbitrary static header names receive the same protection as Authorization.
|
|
726
757
|
*/
|
|
727
758
|
export function redirectSafeFetch(connectorId, policy = "none", baseFetch = fetch) {
|
|
728
|
-
return
|
|
759
|
+
return authMethod((input, initialInit = {}) => authFlow(function* () {
|
|
729
760
|
let current = new URL(input);
|
|
730
761
|
let init = initialInit;
|
|
731
762
|
const seen = new Set([current.href]);
|
|
@@ -733,22 +764,23 @@ export function redirectSafeFetch(connectorId, policy = "none", baseFetch = fetc
|
|
|
733
764
|
while (true) {
|
|
734
765
|
let response;
|
|
735
766
|
try {
|
|
736
|
-
response =
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
767
|
+
response = (yield authCall(baseFetch, [
|
|
768
|
+
current,
|
|
769
|
+
{
|
|
770
|
+
...init,
|
|
771
|
+
redirect: "manual",
|
|
772
|
+
},
|
|
773
|
+
]));
|
|
740
774
|
}
|
|
741
775
|
catch (cause) {
|
|
742
776
|
if (cause instanceof ConnectorCallError)
|
|
743
777
|
throw downstreamCallError(cause);
|
|
744
778
|
throw unavailableCallError(cause, current.href, undefined, hasSdkPayload(cause) ? undefined : (init.signal ?? undefined));
|
|
745
779
|
}
|
|
746
|
-
// Every MCP and OAuth exchange reads its answer through here, and the
|
|
747
|
-
// SDK reads bodies with `.text()` and `.json()`: as bytes, never text.
|
|
748
780
|
if (!REDIRECT_STATUSES.has(response.status))
|
|
749
781
|
return byteReadResponse(response);
|
|
750
782
|
const location = response.headers.get("location");
|
|
751
|
-
|
|
783
|
+
(yield authCatch(authValue(() => response.body?.cancel()), () => { }));
|
|
752
784
|
if (!location) {
|
|
753
785
|
throw new RemoteMcpRedirectError(connectorId, `HTTP ${response.status} carried no Location header`);
|
|
754
786
|
}
|
|
@@ -782,7 +814,7 @@ export function redirectSafeFetch(connectorId, policy = "none", baseFetch = fetc
|
|
|
782
814
|
init = redirectedInit(init, response.status);
|
|
783
815
|
current = next;
|
|
784
816
|
}
|
|
785
|
-
};
|
|
817
|
+
}));
|
|
786
818
|
}
|
|
787
819
|
class RemoteMcpDestinationError extends OAuthRequestNotSentError {
|
|
788
820
|
constructor(connectorId, reason) {
|
|
@@ -835,13 +867,13 @@ for (const [ctor, label] of [
|
|
|
835
867
|
* failures after an HTTP dispatch keep their permanent ambiguous verdict.
|
|
836
868
|
*/
|
|
837
869
|
function learnedUrlSafeFetch(connectorId, configured, baseFetch) {
|
|
838
|
-
return
|
|
870
|
+
return authMethod((input, init) => authFlow(function* () {
|
|
839
871
|
const reason = learnedUrlRefusal(configured, new URL(input));
|
|
840
872
|
if (reason !== undefined) {
|
|
841
873
|
throw new RemoteMcpDestinationError(connectorId, reason);
|
|
842
874
|
}
|
|
843
|
-
return byteReadResponse(
|
|
844
|
-
};
|
|
875
|
+
return byteReadResponse((yield authCall(baseFetch, [input, init])));
|
|
876
|
+
}));
|
|
845
877
|
}
|
|
846
878
|
/** The JSON-RPC error answers each transport delivered, as `code\0message`. */
|
|
847
879
|
const wireErrors = new WeakMap();
|
|
@@ -965,6 +997,7 @@ function withheldAs(message, verdict) {
|
|
|
965
997
|
...(verdict.details ? { details: verdict.details } : {}),
|
|
966
998
|
...(verdict.validation ? { validation: verdict.validation } : {}),
|
|
967
999
|
...(verdict.current ? { current: verdict.current } : {}),
|
|
1000
|
+
...(verdict.writeOutcome ? { writeOutcome: verdict.writeOutcome } : {}),
|
|
968
1001
|
});
|
|
969
1002
|
}
|
|
970
1003
|
return new WithheldTextError(message, verdict);
|
|
@@ -976,7 +1009,7 @@ function withheldAs(message, verdict) {
|
|
|
976
1009
|
* `grant_type`), or the JSON POST of dynamic client registration.
|
|
977
1010
|
*/
|
|
978
1011
|
function tracedOAuthFetch(trail, endpoint, baseFetch) {
|
|
979
|
-
return
|
|
1012
|
+
return authMethod((input, init) => authFlow(function* () {
|
|
980
1013
|
const url = new URL(input);
|
|
981
1014
|
if (url.href === endpoint.href) {
|
|
982
1015
|
trail.last = undefined;
|
|
@@ -985,7 +1018,8 @@ function tracedOAuthFetch(trail, endpoint, baseFetch) {
|
|
|
985
1018
|
const body = init?.body;
|
|
986
1019
|
const step = (init?.method ?? "GET").toUpperCase() === "GET"
|
|
987
1020
|
? "discovery"
|
|
988
|
-
: body instanceof URLSearchParams ||
|
|
1021
|
+
: body instanceof URLSearchParams ||
|
|
1022
|
+
(typeof body === "string" && new URLSearchParams(body).has("grant_type"))
|
|
989
1023
|
? "token request"
|
|
990
1024
|
: "client registration";
|
|
991
1025
|
trail.last = { step, host: url.origin };
|
|
@@ -993,7 +1027,7 @@ function tracedOAuthFetch(trail, endpoint, baseFetch) {
|
|
|
993
1027
|
trail.registration = url.origin;
|
|
994
1028
|
}
|
|
995
1029
|
const leg = trail.last;
|
|
996
|
-
const response =
|
|
1030
|
+
const response = (yield authCall(baseFetch, [input, init]));
|
|
997
1031
|
if (leg) {
|
|
998
1032
|
leg.httpStatus = response.status;
|
|
999
1033
|
const wait = retryAfterMs(response.headers);
|
|
@@ -1001,7 +1035,7 @@ function tracedOAuthFetch(trail, endpoint, baseFetch) {
|
|
|
1001
1035
|
leg.retryAfterMs = wait;
|
|
1002
1036
|
}
|
|
1003
1037
|
return byteReadResponse(response);
|
|
1004
|
-
};
|
|
1038
|
+
}));
|
|
1005
1039
|
}
|
|
1006
1040
|
function registrationErrorCode(body) {
|
|
1007
1041
|
try {
|
|
@@ -1012,6 +1046,37 @@ function registrationErrorCode(body) {
|
|
|
1012
1046
|
return undefined;
|
|
1013
1047
|
}
|
|
1014
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
|
+
}
|
|
1015
1080
|
/**
|
|
1016
1081
|
* Proxy a downstream remote MCP server. SDK clients and transports are scoped
|
|
1017
1082
|
* to one inbound request: reused by calls within a batch/execute_code run, but
|
|
@@ -1024,13 +1089,15 @@ function registrationErrorCode(body) {
|
|
|
1024
1089
|
*/
|
|
1025
1090
|
export function remoteMcp(id, opts) {
|
|
1026
1091
|
opts = assertKnownOptions(opts, `remoteMcp(${JSON.stringify(id)})`, REMOTE_MCP_OPTIONS);
|
|
1092
|
+
if (opts.readOnly !== undefined)
|
|
1093
|
+
READ_ONLY.parse(opts.readOnly, `Connector(${JSON.stringify(id)}).readOnly`);
|
|
1027
1094
|
if (opts.callAdmission !== undefined)
|
|
1028
1095
|
CALL_ADMISSION.parse(opts.callAdmission, `remoteMcp(${JSON.stringify(id)}).callAdmission`);
|
|
1096
|
+
const extraHeaders = extraResourceHeaders(id, opts);
|
|
1029
1097
|
const skillsEnabled = opts.skills;
|
|
1030
1098
|
if (skillsEnabled !== undefined && typeof skillsEnabled !== "boolean") {
|
|
1031
1099
|
throw new Error(`[connecta] connector "${id}" skills must be a boolean.`);
|
|
1032
1100
|
}
|
|
1033
|
-
const classification = opts.classify === undefined ? undefined : reviewedClassification(opts.classify, `connector "${id}"`);
|
|
1034
1101
|
const clientMetadataUrl = opts.auth?.type === "oauth" ? opts.auth.clientMetadataUrl : undefined;
|
|
1035
1102
|
const oauthScope = opts.auth?.type === "oauth" ? opts.auth.scope : undefined;
|
|
1036
1103
|
assertOAuthScope(id, oauthScope);
|
|
@@ -1183,40 +1250,41 @@ export function remoteMcp(id, opts) {
|
|
|
1183
1250
|
* Anything not already in connecta's words is withheld, including a store's
|
|
1184
1251
|
* error while the flow saves what it discovered, which can quote it.
|
|
1185
1252
|
*/
|
|
1186
|
-
const withoutAuthorizationServerText =
|
|
1187
|
-
const safeOrigin =
|
|
1253
|
+
const withoutAuthorizationServerText = authMethod((ctx, err, trail, signals) => authFlow(function* () {
|
|
1254
|
+
const safeOrigin = authMethod((host) => authFlow(function* () {
|
|
1188
1255
|
if (!host || sentSecretsFor(ctx).contains(host, true))
|
|
1189
1256
|
return undefined;
|
|
1190
1257
|
if (host === destination.origin || (staticClient && host === new URL(staticClient.issuer).origin))
|
|
1191
1258
|
return host;
|
|
1192
1259
|
try {
|
|
1193
|
-
return host === (
|
|
1260
|
+
return host === (yield authInvoke(newProvider(ctx), "validatedIssuerOrigin", []))
|
|
1261
|
+
? host
|
|
1262
|
+
: undefined;
|
|
1194
1263
|
}
|
|
1195
1264
|
catch {
|
|
1196
1265
|
return undefined;
|
|
1197
1266
|
}
|
|
1198
|
-
};
|
|
1267
|
+
}));
|
|
1199
1268
|
if (ownAbortReason(err, signals) && !hasSdkPayload(err))
|
|
1200
1269
|
return err;
|
|
1201
1270
|
if (err instanceof RegistrationRejectedError) {
|
|
1202
1271
|
const code = registrationErrorCode(err.body);
|
|
1203
|
-
const origin =
|
|
1272
|
+
const origin = (yield authCall(safeOrigin, [trail.registration]));
|
|
1204
1273
|
const facts = {
|
|
1205
1274
|
step: "OAuth client registration",
|
|
1206
1275
|
...(origin ? { origin } : {}),
|
|
1207
1276
|
httpStatus: err.status,
|
|
1208
1277
|
...(code ? { oauthError: code } : {}),
|
|
1209
1278
|
};
|
|
1210
|
-
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 ` +
|
|
1211
1280
|
`${origin ?? "its authorization server"}: the ` +
|
|
1212
1281
|
`registration endpoint answered HTTP ${err.status}` +
|
|
1213
1282
|
`${code ? ` with OAuth error ${code}` : ""}. Its response is ` +
|
|
1214
1283
|
"withheld because it can quote the request or anything the server " +
|
|
1215
|
-
"chose to add. Check the server's client registration policy, then "
|
|
1216
|
-
"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);
|
|
1217
1285
|
}
|
|
1218
1286
|
const leg = trail.last;
|
|
1219
|
-
const origin =
|
|
1287
|
+
const origin = (yield authCall(safeOrigin, [leg?.host]));
|
|
1220
1288
|
const facts = leg
|
|
1221
1289
|
? {
|
|
1222
1290
|
step: `OAuth ${leg.step}`,
|
|
@@ -1232,11 +1300,10 @@ export function remoteMcp(id, opts) {
|
|
|
1232
1300
|
(classified instanceof ConnectorCallError && classified.code === "provider_permission_denied")) {
|
|
1233
1301
|
return attachFailureFacts(carryFailureFacts(err, classified), facts);
|
|
1234
1302
|
}
|
|
1235
|
-
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"} ` +
|
|
1236
1304
|
`failed${errorKind(err)}. The error is withheld because its text can ` +
|
|
1237
|
-
"quote what the server sent. Check the server's OAuth metadata, then "
|
|
1238
|
-
|
|
1239
|
-
};
|
|
1305
|
+
"quote what the server sent. Check the server's OAuth metadata.", classified), ", then retry authorization")), facts);
|
|
1306
|
+
}));
|
|
1240
1307
|
const endpointOrigin = new URL(opts.url).origin;
|
|
1241
1308
|
/**
|
|
1242
1309
|
* The MCP boundary, where an error leaves the SDK at the handshake, a
|
|
@@ -1258,7 +1325,7 @@ export function remoteMcp(id, opts) {
|
|
|
1258
1325
|
origin: endpointOrigin,
|
|
1259
1326
|
...(httpStatus ? { httpStatus } : {}),
|
|
1260
1327
|
};
|
|
1261
|
-
const classified = downstreamCallError(redactSentSecrets(ctx, err), undefined, undefined, undefined,
|
|
1328
|
+
const classified = downstreamCallError(redactSentSecrets(ctx, err), undefined, undefined, undefined, requestSentSecrets(ctx));
|
|
1262
1329
|
const verdict = isOauth && classified instanceof ConnectorCallError && classified.code === "auth_required"
|
|
1263
1330
|
? carryFailureFacts(err, authRequiredError())
|
|
1264
1331
|
: classified;
|
|
@@ -1305,7 +1372,7 @@ export function remoteMcp(id, opts) {
|
|
|
1305
1372
|
*/
|
|
1306
1373
|
const boundOAuthFlows = (ctx, transport, endpoint,
|
|
1307
1374
|
/** The request's and connection's signals: only their reasons pass as written. */
|
|
1308
|
-
signals) => {
|
|
1375
|
+
signals, renewalSignal) => {
|
|
1309
1376
|
const internals = transport;
|
|
1310
1377
|
const adapted = internals._authProvider;
|
|
1311
1378
|
const onUnauthorized = adapted?.onUnauthorized;
|
|
@@ -1317,29 +1384,46 @@ export function remoteMcp(id, opts) {
|
|
|
1317
1384
|
"flows cannot be bounded. Pin @modelcontextprotocol/client to the " +
|
|
1318
1385
|
"version this release ships with.");
|
|
1319
1386
|
}
|
|
1320
|
-
const flow =
|
|
1387
|
+
const flow = authMethod((start, signal) => authFlow(function* () {
|
|
1321
1388
|
const trail = {};
|
|
1322
1389
|
try {
|
|
1323
|
-
return
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
+
]));
|
|
1327
1404
|
}
|
|
1328
1405
|
catch (err) {
|
|
1329
|
-
throw
|
|
1330
|
-
}
|
|
1331
|
-
};
|
|
1332
|
-
adapted.onUnauthorized = (ctx) => flow
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
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
|
+
]));
|
|
1343
1427
|
};
|
|
1344
1428
|
class OperatorDisconnectedError extends ConnectorCallError {
|
|
1345
1429
|
constructor() {
|
|
@@ -1364,11 +1448,11 @@ export function remoteMcp(id, opts) {
|
|
|
1364
1448
|
* trusting any cached client, so an operator's replacement is picked up
|
|
1365
1449
|
* without a redeploy. The value stays in the caller's local scope.
|
|
1366
1450
|
*/
|
|
1367
|
-
const readCredential =
|
|
1451
|
+
const readCredential = authMethod((ctx) => authFlow(function* () {
|
|
1368
1452
|
if (requestAuth) {
|
|
1369
1453
|
let value;
|
|
1370
1454
|
try {
|
|
1371
|
-
value =
|
|
1455
|
+
value = (yield authInvoke(requestAuth, "token", [ctx]));
|
|
1372
1456
|
}
|
|
1373
1457
|
catch (error) {
|
|
1374
1458
|
if (error instanceof ConnectorCallError)
|
|
@@ -1390,19 +1474,12 @@ export function remoteMcp(id, opts) {
|
|
|
1390
1474
|
throw new CredentialRequiredError(`Connector "${id}" needs an operator-managed credential, but ` +
|
|
1391
1475
|
"credential storage is not configured. Configure vault in deployment code and redeploy. Call authorize_connector for recovery options.");
|
|
1392
1476
|
}
|
|
1393
|
-
|
|
1394
|
-
// the registry's accessor; nothing to reclassify here.
|
|
1395
|
-
const value = (await ctx.credential.get())?.trim();
|
|
1477
|
+
const value = (yield authInvoke(ctx.credential, "get", []))?.trim();
|
|
1396
1478
|
if (!value) {
|
|
1397
1479
|
throw new CredentialRequiredError(`Connector "${id}" has no stored credential — call ` +
|
|
1398
1480
|
`authorize_connector({ connector: "${id}" }) and follow the ` +
|
|
1399
1481
|
"operator handoff it returns.");
|
|
1400
1482
|
}
|
|
1401
|
-
// Refuse a value a header cannot carry BEFORE anything frames it. A
|
|
1402
|
-
// wrapped newline in a pasted key is the ordinary way this happens, and
|
|
1403
|
-
// the runtime that rejects the header quotes the whole value back in its
|
|
1404
|
-
// TypeError — a message that travels to status, to the activity log, and
|
|
1405
|
-
// to the agent. So the check lives here, and says only what is wrong.
|
|
1406
1483
|
if (carriesIllegalHeaderChar(value)) {
|
|
1407
1484
|
throw new CredentialRequiredError(`Connector "${id}"'s stored credential contains a character a header ` +
|
|
1408
1485
|
"cannot carry (a line break or other control character). Call " +
|
|
@@ -1410,7 +1487,7 @@ export function remoteMcp(id, opts) {
|
|
|
1410
1487
|
"it in this connection in the operator UI. The value is not shown or logged.");
|
|
1411
1488
|
}
|
|
1412
1489
|
return value;
|
|
1413
|
-
};
|
|
1490
|
+
}));
|
|
1414
1491
|
/**
|
|
1415
1492
|
* Replace, never edit.
|
|
1416
1493
|
*
|
|
@@ -1468,48 +1545,59 @@ export function remoteMcp(id, opts) {
|
|
|
1468
1545
|
});
|
|
1469
1546
|
};
|
|
1470
1547
|
const listTools = client.listTools.bind(client);
|
|
1471
|
-
client.listTools = (params, options) => cache
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
result
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
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
|
+
}
|
|
1496
1578
|
}
|
|
1497
1579
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1580
|
+
return result;
|
|
1581
|
+
})),
|
|
1582
|
+
]));
|
|
1501
1583
|
const listResources = client.listResources.bind(client);
|
|
1502
|
-
client.listResources = (params, options) => cache
|
|
1584
|
+
client.listResources = authMethod((params, options) => authInvoke(cache, "withListing", [
|
|
1585
|
+
listingContext(options),
|
|
1586
|
+
authMethod(() => authCall(listResources, [params, options])),
|
|
1587
|
+
]));
|
|
1503
1588
|
const listResourceTemplates = client.listResourceTemplates.bind(client);
|
|
1504
|
-
client.listResourceTemplates = (params, options) => cache
|
|
1589
|
+
client.listResourceTemplates = authMethod((params, options) => authInvoke(cache, "withListing", [
|
|
1590
|
+
listingContext(options),
|
|
1591
|
+
authMethod(() => authCall(listResourceTemplates, [params, options])),
|
|
1592
|
+
]));
|
|
1505
1593
|
const request = client.request.bind(client);
|
|
1506
1594
|
const walks = new WeakMap();
|
|
1507
|
-
client.request = (
|
|
1595
|
+
client.request = authMethod((...args) => authFlow(function* () {
|
|
1508
1596
|
const message = args[0];
|
|
1509
1597
|
if (message.method !== "tools/list" &&
|
|
1510
1598
|
message.method !== "resources/list" &&
|
|
1511
1599
|
message.method !== "resources/templates/list")
|
|
1512
|
-
return
|
|
1600
|
+
return yield authCall(request, args, client);
|
|
1513
1601
|
const method = message.method;
|
|
1514
1602
|
const ctx = cache.currentContext();
|
|
1515
1603
|
const options = (args.length === 3 ? args[2] : args[1]);
|
|
@@ -1525,11 +1613,15 @@ export function remoteMcp(id, opts) {
|
|
|
1525
1613
|
}
|
|
1526
1614
|
let page;
|
|
1527
1615
|
try {
|
|
1528
|
-
page =
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
?
|
|
1532
|
-
:
|
|
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
|
+
]));
|
|
1533
1625
|
}
|
|
1534
1626
|
catch (error) {
|
|
1535
1627
|
if (isCursorShapeError(error))
|
|
@@ -1575,19 +1667,18 @@ export function remoteMcp(id, opts) {
|
|
|
1575
1667
|
if (!walk.first)
|
|
1576
1668
|
walk.first = result;
|
|
1577
1669
|
else {
|
|
1578
|
-
// SDK aggregation retains page-one metadata. A later page may only
|
|
1579
|
-
// narrow its reuse grant, never broaden the complete catalog's scope.
|
|
1580
1670
|
walk.first.ttlMs = Math.min(walk.first.ttlMs, clean.ttlMs);
|
|
1581
1671
|
if (clean.cacheScope !== "public")
|
|
1582
1672
|
walk.first.cacheScope = "private";
|
|
1583
1673
|
}
|
|
1584
1674
|
observeCatalogExpiry(ctx, method, walk.first.ttlMs);
|
|
1585
1675
|
return result;
|
|
1586
|
-
});
|
|
1676
|
+
}));
|
|
1587
1677
|
};
|
|
1588
1678
|
/** This request scope's entry, open or closed, created on first sight. */
|
|
1589
1679
|
const entryFor = (ctx) => {
|
|
1590
|
-
const
|
|
1680
|
+
const owner = Context.get(viewServices(ctx), RequestScope);
|
|
1681
|
+
const key = owner;
|
|
1591
1682
|
let state = states.get(key);
|
|
1592
1683
|
if (!state) {
|
|
1593
1684
|
state = {
|
|
@@ -1596,6 +1687,7 @@ export function remoteMcp(id, opts) {
|
|
|
1596
1687
|
client: null,
|
|
1597
1688
|
transport: null,
|
|
1598
1689
|
toolDefinitions: new Map(),
|
|
1690
|
+
dispatches: new Map(),
|
|
1599
1691
|
connecting: null,
|
|
1600
1692
|
authRequired: false,
|
|
1601
1693
|
provider: null,
|
|
@@ -1604,6 +1696,13 @@ export function remoteMcp(id, opts) {
|
|
|
1604
1696
|
credentialDigest: null,
|
|
1605
1697
|
};
|
|
1606
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
|
+
});
|
|
1607
1706
|
}
|
|
1608
1707
|
return state;
|
|
1609
1708
|
};
|
|
@@ -1614,7 +1713,7 @@ export function remoteMcp(id, opts) {
|
|
|
1614
1713
|
throw scopeEndedError();
|
|
1615
1714
|
return state;
|
|
1616
1715
|
};
|
|
1617
|
-
const newProvider = (ctx, state, signal = ctx
|
|
1716
|
+
const newProvider = (ctx, state, signal = viewSignal(ctx, true)) => {
|
|
1618
1717
|
if (state?.provider)
|
|
1619
1718
|
return state.provider;
|
|
1620
1719
|
const metadataUrl = staticClient ? undefined : (clientMetadataUrl ?? selfHostedClientUrl(ctx.publicUrl, id));
|
|
@@ -1647,76 +1746,250 @@ export function remoteMcp(id, opts) {
|
|
|
1647
1746
|
* by the caller so the raw secret is not passed around twice. Never
|
|
1648
1747
|
* retained past the transport it configures.
|
|
1649
1748
|
*/
|
|
1650
|
-
credentialFramed = null, signal = ctx
|
|
1749
|
+
credentialFramed = null, signal = viewSignal(ctx, true),
|
|
1651
1750
|
/** The raw value behind `credentialFramed`, under the same terms. */
|
|
1652
|
-
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 } : {});
|
|
1653
1768
|
if (opts._transportFactory)
|
|
1654
1769
|
return opts._transportFactory(ctx);
|
|
1655
1770
|
const url = new URL(opts.url);
|
|
1656
|
-
|
|
1657
|
-
|
|
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* () {
|
|
1658
1790
|
let rpc;
|
|
1659
1791
|
if (typeof init?.body === "string") {
|
|
1660
1792
|
try {
|
|
1661
1793
|
rpc = JSON.parse(init.body);
|
|
1662
1794
|
}
|
|
1663
|
-
catch {
|
|
1664
|
-
|
|
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);
|
|
1665
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 });
|
|
1666
1862
|
}
|
|
1667
|
-
const writeContinuation = downstreamWriteContinuation(ctx.requestScope ?? ctx) &&
|
|
1668
|
-
init?.method === "POST" &&
|
|
1669
|
-
skillObject(rpc) &&
|
|
1670
|
-
rpc.method === "tools/call";
|
|
1671
|
-
const response = writeContinuation
|
|
1672
|
-
? await fetch(input, { ...init, redirect: "manual" })
|
|
1673
|
-
: await fetch(input, init);
|
|
1674
1863
|
if (writeContinuation && (response.status === 401 || (response.status >= 300 && response.status < 400))) {
|
|
1675
|
-
|
|
1676
|
-
// Stop before the SDK can refresh auth or follow a redirect and resend
|
|
1677
|
-
// this write. The host's one-use continuation nonce remains spent.
|
|
1864
|
+
(yield authCatch(authValue(() => response.body?.cancel()), () => { }));
|
|
1678
1865
|
if (response.status === 401)
|
|
1679
1866
|
throw authRequiredError();
|
|
1680
|
-
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" });
|
|
1681
1872
|
}
|
|
1682
1873
|
if (skillsEnabled && skillObject(rpc) && (rpc.method === "skills/list" || rpc.method === "resources/read")) {
|
|
1683
|
-
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
|
+
]));
|
|
1684
1880
|
}
|
|
1685
1881
|
return response;
|
|
1686
|
-
};
|
|
1882
|
+
}));
|
|
1687
1883
|
// A runtime refusing the assembled header quotes it, and the transport
|
|
1688
1884
|
// error below keeps none of what the runtime said. So whether the
|
|
1689
1885
|
// rejection quoted the credential is decided here, first, and survives
|
|
1690
1886
|
// as the class of the error rather than as its text.
|
|
1691
1887
|
const guardedFetch = redirectSafeFetch(id, opts.redirects, credentialFramed === null
|
|
1692
1888
|
? trackedFetch
|
|
1693
|
-
:
|
|
1889
|
+
: authMethod((input, init) => authFlow(function* () {
|
|
1694
1890
|
try {
|
|
1695
|
-
return
|
|
1891
|
+
return (yield authCall(trackedFetch, [input, init]));
|
|
1696
1892
|
}
|
|
1697
1893
|
catch (err) {
|
|
1698
1894
|
throw withoutCredential(err, credentialValue, credentialFramed);
|
|
1699
1895
|
}
|
|
1700
|
-
});
|
|
1896
|
+
})));
|
|
1701
1897
|
if (opts.auth?.type === "oauth") {
|
|
1702
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
|
+
});
|
|
1703
1923
|
const transport = new StreamableHTTPClientTransport(url, {
|
|
1704
1924
|
authProvider: oauthProvider,
|
|
1705
1925
|
onInsufficientScope: "throw",
|
|
1706
|
-
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
|
+
})),
|
|
1707
1948
|
});
|
|
1708
|
-
boundOAuthFlows(ctx, transport, url, [signal, ctx.signal]);
|
|
1709
|
-
return transport;
|
|
1949
|
+
boundOAuthFlows(ctx, transport, url, [signal, ctx.signal], (response) => renewalSignals.get(response));
|
|
1950
|
+
return sendingEachWriteOnce(transport, flights, untrackedRead);
|
|
1710
1951
|
}
|
|
1711
1952
|
const headers = opts.auth?.type === "headers"
|
|
1712
1953
|
? opts.auth.headers
|
|
1713
1954
|
: (credentialAuth || requestAuth) && credentialFramed !== null
|
|
1714
1955
|
? { ...requestAuth?.headers, [credentialHeader]: credentialFramed }
|
|
1715
1956
|
: undefined;
|
|
1716
|
-
return new StreamableHTTPClientTransport(url, {
|
|
1957
|
+
return sendingEachWriteOnce(new StreamableHTTPClientTransport(url, {
|
|
1717
1958
|
...(headers ? { requestInit: { headers } } : {}),
|
|
1718
|
-
fetch: guardedFetch,
|
|
1719
|
-
});
|
|
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;
|
|
1720
1993
|
};
|
|
1721
1994
|
const reset = (state) => {
|
|
1722
1995
|
state.lease = null;
|
|
@@ -1758,101 +2031,85 @@ export function remoteMcp(id, opts) {
|
|
|
1758
2031
|
if (release)
|
|
1759
2032
|
detach(release);
|
|
1760
2033
|
};
|
|
1761
|
-
const
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
if (provider.isOperatorDisconnectedEpoch(oauthGeneration)) {
|
|
1777
|
-
closeHalf(state);
|
|
1778
|
-
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);
|
|
1779
2049
|
}
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
if (state.client && oauthGeneration !== undefined && state.connectedGeneration !== null) {
|
|
1785
|
-
if (isClosed(state))
|
|
1786
|
-
throw scopeEndedError();
|
|
1787
|
-
if (oauthGeneration !== state.connectedGeneration) {
|
|
1788
|
-
closeHalf(state);
|
|
2050
|
+
};
|
|
2051
|
+
try {
|
|
2052
|
+
if (state.authRequired) {
|
|
2053
|
+
throw authRequiredError();
|
|
1789
2054
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
credentialValue = await readCredential(ctx);
|
|
1801
|
-
credentialFramed = credentialHeaderValue(credentialScheme, credentialValue);
|
|
1802
|
-
credentialDigest = await digestOf(credentialValue);
|
|
1803
|
-
// Gated on a connect in flight as well as a cached client, exactly like
|
|
1804
|
-
// the epoch read above: a rotation that lands while the first caller is
|
|
1805
|
-
// still connecting must not let the second one ride the key the vault
|
|
1806
|
-
// has already replaced.
|
|
1807
|
-
if ((state.client || state.connecting) &&
|
|
1808
|
-
state.credentialDigest !== null &&
|
|
1809
|
-
state.credentialDigest !== credentialDigest) {
|
|
1810
|
-
// A slow vault/token read or digest can resume after a newer caller
|
|
1811
|
-
// connected with the replacement. Never let that stale read tear down
|
|
1812
|
-
// the newer connection and install the rotated-away credential.
|
|
1813
|
-
const observedConnection = state.client ?? state.connecting;
|
|
1814
|
-
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) {
|
|
1815
2065
|
if (isClosed(state))
|
|
1816
2066
|
throw scopeEndedError();
|
|
1817
|
-
if (
|
|
1818
|
-
|
|
2067
|
+
if (oauthGeneration !== state.connectedGeneration) {
|
|
2068
|
+
closeHalf(state);
|
|
1819
2069
|
}
|
|
1820
|
-
closeHalf(state);
|
|
1821
2070
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
else {
|
|
1842
|
-
connectingWaiters.delete(attempt);
|
|
1843
|
-
// A cancelled waiter leaves its siblings' handshake alone. Once every
|
|
1844
|
-
// waiter has left, abandon the attempt and its OAuth refresh too.
|
|
1845
|
-
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
|
+
}
|
|
1846
2090
|
closeHalf(state);
|
|
2091
|
+
}
|
|
1847
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;
|
|
1848
2108
|
}
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
throw scopeEndedError();
|
|
1854
|
-
return client;
|
|
1855
|
-
};
|
|
2109
|
+
finally {
|
|
2110
|
+
leave();
|
|
2111
|
+
}
|
|
2112
|
+
}));
|
|
1856
2113
|
/**
|
|
1857
2114
|
* Start this scope's connect attempt, publishing it before any of it runs.
|
|
1858
2115
|
* The attempt completes its Deferred itself, and every caller — the one that
|
|
@@ -1866,7 +2123,10 @@ export function remoteMcp(id, opts) {
|
|
|
1866
2123
|
// while this connect is in flight has to be visible to the next caller,
|
|
1867
2124
|
// which would otherwise wait on a client bound to the older key.
|
|
1868
2125
|
state.credentialDigest = credentialDigest;
|
|
1869
|
-
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(() => {
|
|
1870
2130
|
// Force reset may have abandoned this attempt and installed a new
|
|
1871
2131
|
// one in the same request scope. An old completion must not erase
|
|
1872
2132
|
// the new attempt and allow a third concurrent connect.
|
|
@@ -1885,8 +2145,8 @@ export function remoteMcp(id, opts) {
|
|
|
1885
2145
|
// The transport and its OAuth provider are shared by every call in this
|
|
1886
2146
|
// scope. A call's deadline signal is aborted even on success, so retaining
|
|
1887
2147
|
// the first caller's signal here would abort its siblings and later calls.
|
|
1888
|
-
//
|
|
1889
|
-
//
|
|
2148
|
+
// The provider uses the connection's lifetime. Each challenged response
|
|
2149
|
+
// separately binds renewal preparation to the call that initiated it.
|
|
1890
2150
|
const connectionAbort = new AbortController();
|
|
1891
2151
|
// Own cancellation before beginFlow's storage reads.
|
|
1892
2152
|
// The lease first owns just the signal, then the transport it creates.
|
|
@@ -1914,7 +2174,9 @@ export function remoteMcp(id, opts) {
|
|
|
1914
2174
|
// step-up — happens inside the SDK, bound to the epoch live now: a
|
|
1915
2175
|
// reset meanwhile fails the attempt rather than handing it the next
|
|
1916
2176
|
// epoch's grant.
|
|
1917
|
-
const genAtStart = provider
|
|
2177
|
+
const genAtStart = provider
|
|
2178
|
+
? yield* promised(authMethod(() => authInvoke(provider, "beginFlow", [])))
|
|
2179
|
+
: "";
|
|
1918
2180
|
if (!owned())
|
|
1919
2181
|
return yield* Effect.fail(scopeEndedError());
|
|
1920
2182
|
if (provider)
|
|
@@ -1935,39 +2197,57 @@ export function remoteMcp(id, opts) {
|
|
|
1935
2197
|
clientName: ctx.oauthClientName,
|
|
1936
2198
|
}
|
|
1937
2199
|
: undefined;
|
|
1938
|
-
const negotiationDigest = yield* promised(() => digestOf
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
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
|
+
]),
|
|
1953
2218
|
])));
|
|
1954
2219
|
const prior = opts.versionNegotiation === "legacy"
|
|
1955
2220
|
? undefined
|
|
1956
|
-
: yield* promised(() => readNegotiation
|
|
2221
|
+
: yield* promised(authMethod(() => authCall(readNegotiation, [ctx, negotiationDigest])));
|
|
1957
2222
|
if (!owned())
|
|
1958
2223
|
return yield* Effect.fail(scopeEndedError());
|
|
1959
|
-
const cacheOptions = yield* promised(() => catalogClientOptions
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
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
|
+
])));
|
|
1971
2251
|
if (!owned())
|
|
1972
2252
|
return yield* Effect.fail(scopeEndedError());
|
|
1973
2253
|
const makeClient = () => {
|
|
@@ -1997,14 +2277,14 @@ export function remoteMcp(id, opts) {
|
|
|
1997
2277
|
state.transport = t;
|
|
1998
2278
|
held.transport = t;
|
|
1999
2279
|
recordWireErrors(t, () => observeCatalogChange(ctx, id, connectionAbort.signal, cacheOwner));
|
|
2000
|
-
yield* promised(
|
|
2280
|
+
yield* promised(authMethod(() => authFlow(function* () {
|
|
2001
2281
|
try {
|
|
2002
|
-
|
|
2282
|
+
(yield authInvoke(c, "connect", [
|
|
2283
|
+
t,
|
|
2284
|
+
{ signal: handshakeAbort.signal, ...(prior ? { prior } : {}) },
|
|
2285
|
+
]));
|
|
2003
2286
|
}
|
|
2004
2287
|
catch (error) {
|
|
2005
|
-
// Some legacy servers crash on an unknown pre-initialize method.
|
|
2006
|
-
// Only an auto probe's typed HTTP 5xx permits a fresh legacy
|
|
2007
|
-
// handshake. An auth failure, timeout or initialize failure does not.
|
|
2008
2288
|
if (prior ||
|
|
2009
2289
|
opts.versionNegotiation === "legacy" ||
|
|
2010
2290
|
!(error instanceof SdkHttpError) ||
|
|
@@ -2014,15 +2294,16 @@ export function remoteMcp(id, opts) {
|
|
|
2014
2294
|
!owned() ||
|
|
2015
2295
|
handshakeAbort.signal.aborted)
|
|
2016
2296
|
throw error;
|
|
2017
|
-
// The SDK closed the failed probe transport. Its replacement owns
|
|
2018
|
-
// a new connection, with the same credential and OAuth generation.
|
|
2019
2297
|
c = makeClient();
|
|
2020
2298
|
t = buildTransport(ctx, provider, credentialFramed, connectionAbort.signal, credentialValue);
|
|
2021
2299
|
state.transport = held.transport = t;
|
|
2022
2300
|
recordWireErrors(t, () => observeCatalogChange(ctx, id, connectionAbort.signal, cacheOwner));
|
|
2023
|
-
|
|
2301
|
+
(yield authInvoke(c, "connect", [
|
|
2302
|
+
t,
|
|
2303
|
+
{ signal: handshakeAbort.signal, prior: { kind: "legacy" } },
|
|
2304
|
+
]));
|
|
2024
2305
|
}
|
|
2025
|
-
}).pipe(Effect.mapError((err) => atMcpBoundary(ctx,
|
|
2306
|
+
}))).pipe(Effect.mapError((err) => atMcpBoundary(ctx,
|
|
2026
2307
|
// First, while the runtime's text can still be read for it.
|
|
2027
2308
|
withoutCredential(err, credentialValue, credentialFramed), "MCP handshake", t, [ctx.signal, connectionAbort.signal, handshakeAbort.signal])));
|
|
2028
2309
|
held.client = c;
|
|
@@ -2036,7 +2317,7 @@ export function remoteMcp(id, opts) {
|
|
|
2036
2317
|
// credentials this client just bound to. Discard it rather than cache
|
|
2037
2318
|
// a stale-isolate connection.
|
|
2038
2319
|
if (provider) {
|
|
2039
|
-
const generation = yield* promised(() => provider.
|
|
2320
|
+
const generation = yield* promised(authMethod(() => authInvoke(provider, "liveEpoch", []))).pipe(Effect.mapError((error) => (owned() ? error : scopeEndedError())));
|
|
2040
2321
|
// closeScope can land while the epoch read is pending, after
|
|
2041
2322
|
// connect succeeded but before this client is cached. Discard the
|
|
2042
2323
|
// client on that side of the await too.
|
|
@@ -2051,11 +2332,15 @@ export function remoteMcp(id, opts) {
|
|
|
2051
2332
|
state.authRequired = false;
|
|
2052
2333
|
const discover = c.getDiscoverResult();
|
|
2053
2334
|
if (!prior)
|
|
2054
|
-
yield* promised(() => storeNegotiation
|
|
2335
|
+
yield* promised(authMethod(() => authCall(storeNegotiation, [
|
|
2336
|
+
ctx,
|
|
2337
|
+
negotiationDigest,
|
|
2338
|
+
discover ? { kind: "modern", discover } : { kind: "legacy" },
|
|
2339
|
+
])));
|
|
2055
2340
|
if (!owned())
|
|
2056
2341
|
return yield* Effect.fail(scopeEndedError());
|
|
2057
2342
|
return c;
|
|
2058
|
-
}).pipe(Effect.catch((err) => {
|
|
2343
|
+
}).pipe(Effect.catchDefect(Effect.fail), Effect.catch((err) => {
|
|
2059
2344
|
// Close the failed connection through its client, without waiting.
|
|
2060
2345
|
if (owned()) {
|
|
2061
2346
|
state.lease = null;
|
|
@@ -2087,27 +2372,23 @@ export function remoteMcp(id, opts) {
|
|
|
2087
2372
|
}));
|
|
2088
2373
|
});
|
|
2089
2374
|
};
|
|
2090
|
-
const disconnectAuthorization =
|
|
2375
|
+
const disconnectAuthorization = authMethod((ctx, state, operatorDisconnected = false, preserveClient = false) => authFlow(function* () {
|
|
2091
2376
|
const provider = newProvider(ctx, state);
|
|
2092
|
-
// Publish the replacement epoch before waiting on or closing any
|
|
2093
|
-
// request-local transport. A hung connect therefore cannot delay the
|
|
2094
|
-
// fence, and every late OAuth write stays in the older namespace.
|
|
2095
2377
|
const reset = operatorDisconnected
|
|
2096
|
-
? provider
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
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]);
|
|
2101
2385
|
try {
|
|
2102
|
-
|
|
2386
|
+
yield reset;
|
|
2103
2387
|
}
|
|
2104
2388
|
finally {
|
|
2105
|
-
// Abandon any connect in flight and close whichever half of the
|
|
2106
|
-
// client/transport exists. Reset is unconditional because storage may already
|
|
2107
|
-
// be fenced behind a newer epoch after a cleanup error.
|
|
2108
2389
|
closeHalf(state);
|
|
2109
2390
|
}
|
|
2110
|
-
};
|
|
2391
|
+
}));
|
|
2111
2392
|
// Built once from construction-time values: names, framing, and public
|
|
2112
2393
|
// URLs only. A static header's value and a stored credential never reach
|
|
2113
2394
|
// it, and a URL keeps origin and path: a CIMD document's query may carry a
|
|
@@ -2131,6 +2412,8 @@ export function remoteMcp(id, opts) {
|
|
|
2131
2412
|
...(describedClientMetadataUrl !== undefined ? { clientMetadataUrl: describedClientMetadataUrl } : {}),
|
|
2132
2413
|
}
|
|
2133
2414
|
: { mode: "none" };
|
|
2415
|
+
if (extraHeaders)
|
|
2416
|
+
authDescription.headerNames = [...new Set([...(authDescription.headerNames ?? []), ...Object.keys(extraHeaders)])];
|
|
2134
2417
|
const endpoint = describedEndpoint(opts.url);
|
|
2135
2418
|
const optionSources = Object.freeze({
|
|
2136
2419
|
"source.kind": "default",
|
|
@@ -2150,9 +2433,9 @@ export function remoteMcp(id, opts) {
|
|
|
2150
2433
|
if (ctx.signal?.aborted)
|
|
2151
2434
|
throw ctx.signal.reason instanceof Error ? ctx.signal.reason : scopeEndedError();
|
|
2152
2435
|
};
|
|
2153
|
-
const withSkillsClient =
|
|
2436
|
+
const withSkillsClient = authMethod((ctx, method, run) => authFlow(function* () {
|
|
2154
2437
|
const state = stateFor(ctx);
|
|
2155
|
-
const client =
|
|
2438
|
+
const client = (yield authCall(ensureConnected, [ctx, state]));
|
|
2156
2439
|
try {
|
|
2157
2440
|
skillScopeOpen(ctx, state);
|
|
2158
2441
|
const capabilities = client.getServerCapabilities();
|
|
@@ -2163,7 +2446,7 @@ export function remoteMcp(id, opts) {
|
|
|
2163
2446
|
(extension.directoryRead !== undefined && typeof extension.directoryRead !== "boolean")) {
|
|
2164
2447
|
throw new ConnectorCallError("connector_call_failed", "The downstream did not declare a valid Skills extension and resources capability.");
|
|
2165
2448
|
}
|
|
2166
|
-
const result =
|
|
2449
|
+
const result = (yield authCall(run, [client, state]));
|
|
2167
2450
|
skillScopeOpen(ctx, state);
|
|
2168
2451
|
return result;
|
|
2169
2452
|
}
|
|
@@ -2173,11 +2456,9 @@ export function remoteMcp(id, opts) {
|
|
|
2173
2456
|
const classified = atMcpBoundary(ctx, error, method, client.transport, [ctx.signal]);
|
|
2174
2457
|
if (requiresAuthorization(classified) && state.client === client)
|
|
2175
2458
|
state.authRequired = true;
|
|
2176
|
-
// The SDK and downstream can quote whole manifests or bodies in errors.
|
|
2177
|
-
// Keep their typed verdict and operator facts, never the quoted payload.
|
|
2178
2459
|
throw carryFailureFacts(classified, withheldAs(`Connector "${id}" downstream ${method} failed.`, classified));
|
|
2179
2460
|
}
|
|
2180
|
-
};
|
|
2461
|
+
}));
|
|
2181
2462
|
const connector = {
|
|
2182
2463
|
id,
|
|
2183
2464
|
...(opts.title !== undefined ? { title: opts.title } : {}),
|
|
@@ -2194,6 +2475,7 @@ export function remoteMcp(id, opts) {
|
|
|
2194
2475
|
},
|
|
2195
2476
|
}),
|
|
2196
2477
|
...(opts.description !== undefined ? { description: opts.description } : {}),
|
|
2478
|
+
...(opts.readOnly !== undefined ? { readOnly: opts.readOnly } : {}),
|
|
2197
2479
|
...(opts.authScope !== undefined ? { authScope: opts.authScope } : {}),
|
|
2198
2480
|
...(opts.maxResultBytes !== undefined ? { maxResultBytes: opts.maxResultBytes } : {}),
|
|
2199
2481
|
...(opts.callAdmission !== undefined ? { callAdmission: opts.callAdmission } : {}),
|
|
@@ -2201,47 +2483,55 @@ export function remoteMcp(id, opts) {
|
|
|
2201
2483
|
...(skillsEnabled
|
|
2202
2484
|
? {
|
|
2203
2485
|
downstreamSkills: {
|
|
2204
|
-
list: (ctx) => withSkillsClient
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
listed.
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
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;
|
|
2227
2519
|
}
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
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
|
+
])),
|
|
2239
2532
|
},
|
|
2240
2533
|
}
|
|
2241
2534
|
: {}),
|
|
2242
|
-
// Data the registry classifies every read with; listTools below returns
|
|
2243
|
-
// the downstream's listing unclassified.
|
|
2244
|
-
...(classification !== undefined ? { classification } : {}),
|
|
2245
2535
|
// Declaring the slot is what makes the rest of the operator surface work:
|
|
2246
2536
|
// the connection's credential form renders it, the shape check compares
|
|
2247
2537
|
// against it, and authorize_connector returns the operator handoff rather than
|
|
@@ -2255,17 +2545,9 @@ export function remoteMcp(id, opts) {
|
|
|
2255
2545
|
* here is connecta's to verify — the credential's scope, project,
|
|
2256
2546
|
* and mode are the provider's answer, not ours.
|
|
2257
2547
|
*/
|
|
2258
|
-
testCredential:
|
|
2548
|
+
testCredential: authMethod((value, ctx) => authFlow(function* () {
|
|
2259
2549
|
try {
|
|
2260
|
-
|
|
2261
|
-
// assembled deep inside `ensureConnected`, and handing a
|
|
2262
|
-
// candidate down that path would mean threading a second secret
|
|
2263
|
-
// through the whole connection state. `/ui/credentials/<id>/test`
|
|
2264
|
-
// reads the stored value and passes it here, so today the two are
|
|
2265
|
-
// the same string. Check rather than assume: a route that later
|
|
2266
|
-
// tested an unsaved candidate would otherwise silently report on
|
|
2267
|
-
// the old value, which is the one answer worse than refusing.
|
|
2268
|
-
const stored = (await ctx.credential?.get())?.trim();
|
|
2550
|
+
const stored = (yield authCall(ctx.credential?.get, [], ctx.credential))?.trim();
|
|
2269
2551
|
if (stored !== value.trim()) {
|
|
2270
2552
|
return {
|
|
2271
2553
|
ok: false,
|
|
@@ -2273,7 +2555,7 @@ export function remoteMcp(id, opts) {
|
|
|
2273
2555
|
"saved. Save the value first, then test it.",
|
|
2274
2556
|
};
|
|
2275
2557
|
}
|
|
2276
|
-
const tools =
|
|
2558
|
+
const tools = (yield authInvoke(connector, "listTools", [ctx]));
|
|
2277
2559
|
return {
|
|
2278
2560
|
ok: true,
|
|
2279
2561
|
message: `Connected — the downstream served ${tools.length} tool${tools.length === 1 ? "" : "s"}.`,
|
|
@@ -2283,24 +2565,22 @@ export function remoteMcp(id, opts) {
|
|
|
2283
2565
|
return { ok: false, message: msg(err) };
|
|
2284
2566
|
}
|
|
2285
2567
|
finally {
|
|
2286
|
-
// Standalone tests own their scope; an operator route owns
|
|
2287
|
-
// bounded teardown for the context it supplied to this hook.
|
|
2288
2568
|
if (!connectorScopeCleanupClaimed(ctx, id))
|
|
2289
|
-
|
|
2569
|
+
(yield authCall(connector.closeScope, [ctx], connector));
|
|
2290
2570
|
}
|
|
2291
|
-
},
|
|
2571
|
+
})),
|
|
2292
2572
|
}
|
|
2293
2573
|
: {}),
|
|
2294
2574
|
// The SDK walks the whole chain and writes one complete result. Intake
|
|
2295
2575
|
// runs on its public request seam before any page can enter that cache.
|
|
2296
|
-
|
|
2576
|
+
listTools: authMethod((ctx) => authFlow(function* () {
|
|
2297
2577
|
const state = stateFor(ctx);
|
|
2298
|
-
const client =
|
|
2578
|
+
const client = (yield authCall(ensureConnected, [ctx, state]));
|
|
2299
2579
|
let clean;
|
|
2300
2580
|
const options = requestOptions(ctx) ?? {};
|
|
2301
2581
|
listingContexts.set(options, ctx);
|
|
2302
2582
|
try {
|
|
2303
|
-
const result =
|
|
2583
|
+
const result = (yield authInvoke(client, "listTools", [undefined, options]));
|
|
2304
2584
|
clean = redactCatalog(ctx, result.tools);
|
|
2305
2585
|
}
|
|
2306
2586
|
catch (err) {
|
|
@@ -2324,15 +2604,10 @@ export function remoteMcp(id, opts) {
|
|
|
2324
2604
|
...(t.title !== undefined ? { title: t.title } : {}),
|
|
2325
2605
|
...(t.icons !== undefined
|
|
2326
2606
|
? {
|
|
2327
|
-
// Inline images can consume the catalog ceiling and storage chunks.
|
|
2328
|
-
// The SDK needs no icons for validation or parameter mirroring.
|
|
2329
2607
|
icons: t.icons.filter((icon) => !/^data:/i.test(icon.src)),
|
|
2330
2608
|
}
|
|
2331
2609
|
: {}),
|
|
2332
2610
|
...(t.execution !== undefined ? { execution: t.execution } : {}),
|
|
2333
|
-
// SEP-2243 declarations live in inputSchema. SDK validation and header
|
|
2334
|
-
// mirroring need no _meta keys, so arbitrary downstream metadata stays
|
|
2335
|
-
// out of the persisted catalog.
|
|
2336
2611
|
...(t.description !== undefined ? { description: t.description } : {}),
|
|
2337
2612
|
...(t.inputSchema !== undefined
|
|
2338
2613
|
? {
|
|
@@ -2350,88 +2625,157 @@ export function remoteMcp(id, opts) {
|
|
|
2350
2625
|
}
|
|
2351
2626
|
: {}),
|
|
2352
2627
|
}));
|
|
2353
|
-
},
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
}
|
|
2358
|
-
const state = stateFor(ctx);
|
|
2359
|
-
const client = await ensureConnected(ctx, state);
|
|
2628
|
+
})),
|
|
2629
|
+
callTool: authMethod((name, args, ctx, options) => authFlow(function* () {
|
|
2630
|
+
enterProviderBoundary(ctx, connector);
|
|
2631
|
+
let active;
|
|
2360
2632
|
try {
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
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.`);
|
|
2367
2664
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
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
|
+
}
|
|
2377
2674
|
}
|
|
2378
|
-
|
|
2379
|
-
|
|
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");
|
|
2380
2706
|
}
|
|
2381
|
-
}
|
|
2382
|
-
const result = await client
|
|
2383
|
-
.callTool({
|
|
2384
|
-
name,
|
|
2385
|
-
arguments: (args ?? {}),
|
|
2386
|
-
...options?.input,
|
|
2387
|
-
}, {
|
|
2388
|
-
...requestOptions(ctx),
|
|
2389
|
-
allowInputRequired: true,
|
|
2390
|
-
// SDK 2.3.1's callTool checks outputSchema even for a suspension.
|
|
2391
|
-
// Retain its header mirroring, then validate only final results.
|
|
2392
|
-
...(toolDefinition ? { toolDefinition: { ...toolDefinition, outputSchema: undefined } } : {}),
|
|
2393
|
-
})
|
|
2394
|
-
.catch((err) => {
|
|
2395
|
-
assertDownstreamOutputSafe(ctx.requestScope ?? ctx, err);
|
|
2396
|
-
throw atMcpBoundary(ctx, err, "tools/call", client.transport, [ctx.signal]);
|
|
2397
|
-
});
|
|
2398
|
-
if (isInputRequiredResult(result)) {
|
|
2399
2707
|
return result;
|
|
2400
|
-
}
|
|
2401
|
-
assertDownstreamOutputSafe(ctx.requestScope ?? ctx, result);
|
|
2402
|
-
if (output && !result.isError) {
|
|
2403
|
-
if (result.structuredContent === undefined)
|
|
2404
|
-
throw outputError(-32600, `Tool ${name} has an output schema but did not return structured content`);
|
|
2405
|
-
const validation = await output["~standard"].validate(result.structuredContent);
|
|
2406
|
-
if (validation.issues)
|
|
2407
|
-
throw outputError(-32602, "Structured content does not match the tool's output schema: validation failed");
|
|
2408
|
-
}
|
|
2409
|
-
return result;
|
|
2708
|
+
}))));
|
|
2410
2709
|
}
|
|
2411
2710
|
catch (err) {
|
|
2412
|
-
|
|
2413
|
-
|
|
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)
|
|
2414
2722
|
throw err;
|
|
2415
|
-
if (requiresAuthorization(err))
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
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);
|
|
2422
2759
|
}
|
|
2423
|
-
|
|
2424
|
-
|
|
2760
|
+
finally {
|
|
2761
|
+
if (active)
|
|
2762
|
+
active.state.dispatches.delete(active.dispatch);
|
|
2763
|
+
}
|
|
2764
|
+
})),
|
|
2765
|
+
readResource: authMethod((uri, ctx) => authFlow(function* () {
|
|
2425
2766
|
const state = stateFor(ctx);
|
|
2426
|
-
const client =
|
|
2767
|
+
const client = (yield authCall(ensureConnected, [ctx, state]));
|
|
2427
2768
|
try {
|
|
2428
2769
|
const options = requestOptions(ctx) ?? {};
|
|
2429
2770
|
listingContexts.set(options, ctx);
|
|
2430
2771
|
try {
|
|
2431
2772
|
if (!client.getServerCapabilities()?.resources)
|
|
2432
2773
|
throw unadvertisedResource();
|
|
2433
|
-
const resources =
|
|
2434
|
-
const templates =
|
|
2774
|
+
const resources = (yield authInvoke(client, "listResources", [undefined, options]));
|
|
2775
|
+
const templates = (yield authInvoke(client, "listResourceTemplates", [
|
|
2776
|
+
undefined,
|
|
2777
|
+
options,
|
|
2778
|
+
]));
|
|
2435
2779
|
const exact = resources.resources.some((resource) => resource.uri === uri);
|
|
2436
2780
|
if (!exact) {
|
|
2437
2781
|
const match = resourceUriMatchesTemplates(uri, templates.resourceTemplates, (code) => resourceTemplateRefusals.add(code));
|
|
@@ -2450,18 +2794,19 @@ export function remoteMcp(id, opts) {
|
|
|
2450
2794
|
finally {
|
|
2451
2795
|
listingContexts.delete(options);
|
|
2452
2796
|
}
|
|
2453
|
-
const result =
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2797
|
+
const result = (yield authCatch(authInvoke(client, "readResource", [
|
|
2798
|
+
{ uri },
|
|
2799
|
+
{
|
|
2800
|
+
...requestOptions(ctx),
|
|
2801
|
+
cacheMode: "bypass",
|
|
2802
|
+
allowInputRequired: true,
|
|
2803
|
+
},
|
|
2804
|
+
]), (err) => {
|
|
2460
2805
|
if (err instanceof ResourceNotFoundError) {
|
|
2461
2806
|
throw new ConnectorCallError("not_found", "The downstream resource does not exist.");
|
|
2462
2807
|
}
|
|
2463
2808
|
throw atMcpBoundary(ctx, err, "resources/read", client.transport, [ctx.signal]);
|
|
2464
|
-
});
|
|
2809
|
+
}));
|
|
2465
2810
|
if (isInputRequiredResult(result)) {
|
|
2466
2811
|
throw new ConnectorCallError("input_required_unsupported", "Resource reads inside programs cannot request mid-call input.");
|
|
2467
2812
|
}
|
|
@@ -2476,170 +2821,161 @@ export function remoteMcp(id, opts) {
|
|
|
2476
2821
|
if (err instanceof UnauthorizedError)
|
|
2477
2822
|
throw carryFailureFacts(err, authRequiredError());
|
|
2478
2823
|
}
|
|
2479
|
-
throw downstreamCallError(err, undefined, undefined, undefined,
|
|
2824
|
+
throw downstreamCallError(err, undefined, undefined, undefined, requestSentSecrets(ctx));
|
|
2480
2825
|
}
|
|
2481
|
-
},
|
|
2482
|
-
|
|
2483
|
-
//
|
|
2484
|
-
//
|
|
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.
|
|
2485
2830
|
const state = entryFor(ctx);
|
|
2486
|
-
// Closed before any await, and before the finalizers run: every check
|
|
2487
|
-
// from here on sees the scope ended. A duplicate teardown finds it
|
|
2488
|
-
// closed and has nothing to run.
|
|
2489
2831
|
const release = Scope.closeUnsafe(state.scope, Exit.void);
|
|
2490
2832
|
const cacheClosing = closeCatalogCacheScope(ctx, id, cacheOwner);
|
|
2491
|
-
// Storage cannot cancel an already-started write. Bound the join in
|
|
2492
|
-
// parallel with transport cleanup so it adds no unbounded teardown tail.
|
|
2493
|
-
const cacheClosed = cacheClosing &&
|
|
2494
|
-
runEdge(Effect.raceAllFirst([
|
|
2495
|
-
Effect.promise(() => cacheClosing),
|
|
2496
|
-
Effect.sleep(Duration.millis(LOCAL_CLOSE_BUDGET_MS)),
|
|
2497
|
-
]));
|
|
2498
2833
|
reset(state);
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
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* () {
|
|
2505
2849
|
const state = stateFor(ctx);
|
|
2506
|
-
const report =
|
|
2507
|
-
const path = isOauth
|
|
2850
|
+
const report = authMethod((status) => authFlow(function* () {
|
|
2851
|
+
const path = isOauth
|
|
2852
|
+
? (yield authInvoke(newProvider(ctx, state), "registrationPath", []))
|
|
2853
|
+
: undefined;
|
|
2508
2854
|
return ownStatus({
|
|
2509
2855
|
...status,
|
|
2510
2856
|
...(path ? { registrationPath: path } : {}),
|
|
2511
2857
|
...(resourceTemplateRefusals.size ? { resourceTemplateRefusals: [...resourceTemplateRefusals] } : {}),
|
|
2512
2858
|
});
|
|
2513
|
-
};
|
|
2859
|
+
}));
|
|
2514
2860
|
try {
|
|
2515
|
-
|
|
2516
|
-
return report
|
|
2861
|
+
(yield authCall(ensureConnected, [ctx, state]));
|
|
2862
|
+
return (yield authCall(report, [{ state: "ok" }]));
|
|
2517
2863
|
}
|
|
2518
2864
|
catch (err) {
|
|
2519
|
-
// An empty slot, or one with no vault behind it, is reported the way a
|
|
2520
|
-
// missing grant is: present, unauthenticated, and repairable — never a
|
|
2521
|
-
// boot failure and never a silently absent connector.
|
|
2522
2865
|
if (err instanceof CredentialRequiredError) {
|
|
2523
|
-
return report
|
|
2866
|
+
return (yield authCall(report, [{ state: "auth_required", message: err.message }]));
|
|
2524
2867
|
}
|
|
2525
2868
|
if (err instanceof OperatorDisconnectedError) {
|
|
2526
|
-
return report
|
|
2869
|
+
return (yield authCall(report, [{ state: "auth_required", message: err.message }]));
|
|
2527
2870
|
}
|
|
2528
2871
|
if (state.authRequired) {
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
});
|
|
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
|
+
]));
|
|
2538
2880
|
}
|
|
2539
|
-
// An operator surface: the record, never the error's own text.
|
|
2540
2881
|
return failureStatus(id, err);
|
|
2541
2882
|
}
|
|
2542
|
-
},
|
|
2543
|
-
|
|
2883
|
+
})),
|
|
2884
|
+
finishAuth: authMethod((code, ctx, callbackParams) => authView(ctx, authMethod((signal) => authFlow(function* () {
|
|
2544
2885
|
const state = stateFor(ctx);
|
|
2545
2886
|
const provider = newProvider(ctx, state);
|
|
2546
|
-
// verifyState ran on this request-scoped provider first and found the
|
|
2547
|
-
// consent; a programmatic exchange names it by its callback's state.
|
|
2548
2887
|
const consent = callbackParams?.get("state") ?? null;
|
|
2549
|
-
if (!consent || !(
|
|
2888
|
+
if (!consent || !(yield authInvoke(provider, "verifyCallbackState", [consent]))) {
|
|
2550
2889
|
throw new ConnectorCallError("connector_call_failed", `Connector "${id}" authorization callback matches no pending consent; nothing was exchanged.`);
|
|
2551
2890
|
}
|
|
2552
|
-
// The exchange reads and writes only the epoch its consent was written
|
|
2553
|
-
// in; it decides nothing about the grant there.
|
|
2554
2891
|
provider.validateCallbackIssuer(callbackParams?.get("iss") ?? null);
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
// fence before the token request is its to cross. A connection this
|
|
2559
|
-
// scope opened earlier speaks for its connect attempt, not this callback.
|
|
2560
|
-
const t = buildTransport(ctx, provider);
|
|
2892
|
+
(yield authInvoke(provider, "bindFlow", []));
|
|
2893
|
+
signal.throwIfAborted();
|
|
2894
|
+
const t = buildTransport(ctx, provider, null, signal, null, true);
|
|
2561
2895
|
const trail = {};
|
|
2562
2896
|
const internals = t;
|
|
2563
2897
|
const traced = tracedOAuthFetch(trail, new URL(opts.url), internals._fetchWithInit ?? fetch);
|
|
2564
2898
|
const exchange = new Proxy(t, {
|
|
2565
|
-
get: (target, key) =>
|
|
2899
|
+
get: (target, key) => key === "_fetchWithInit" ? traced : Reflect.get(target, key, target),
|
|
2566
2900
|
set: (target, key, value) => Reflect.set(target, key, value, target),
|
|
2567
2901
|
});
|
|
2568
2902
|
try {
|
|
2569
|
-
|
|
2570
|
-
// Reset so the next use reconnects with the freshly stored tokens.
|
|
2903
|
+
(yield authInvoke(exchange, "finishAuth", [callbackParams]));
|
|
2571
2904
|
closeHalf(state);
|
|
2572
2905
|
}
|
|
2573
2906
|
catch (err) {
|
|
2574
|
-
throw
|
|
2907
|
+
throw (yield authCall(withoutAuthorizationServerText, [
|
|
2908
|
+
ctx,
|
|
2909
|
+
err,
|
|
2910
|
+
trail,
|
|
2911
|
+
[signal, ctx.signal],
|
|
2912
|
+
]));
|
|
2575
2913
|
}
|
|
2576
2914
|
finally {
|
|
2577
|
-
// This exchange-only transport has no lease in the request scope.
|
|
2578
2915
|
detach(closeConnection(null, t, ctx.logger));
|
|
2579
2916
|
}
|
|
2580
|
-
},
|
|
2917
|
+
})), true)),
|
|
2581
2918
|
};
|
|
2582
2919
|
if (opts.auth?.type === "oauth") {
|
|
2583
|
-
connector.verifyState =
|
|
2920
|
+
connector.verifyState = authMethod((oauthState, ctx) => authFlow(function* () {
|
|
2584
2921
|
const state = stateFor(ctx);
|
|
2585
|
-
return newProvider(ctx, state)
|
|
2586
|
-
};
|
|
2587
|
-
connector.verifyCallbackIssuer =
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
};
|
|
2594
|
-
connector.
|
|
2595
|
-
|
|
2596
|
-
};
|
|
2597
|
-
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* () {
|
|
2598
2938
|
ctx = authorizingContext(ctx);
|
|
2599
2939
|
const state = stateFor(ctx);
|
|
2600
2940
|
state.provider = null;
|
|
2601
2941
|
const p = newProvider(ctx, state);
|
|
2602
2942
|
if (ctx.signal?.aborted)
|
|
2603
2943
|
throw ctx.signal.reason;
|
|
2604
|
-
const disconnected = startOpts?.force
|
|
2944
|
+
const disconnected = startOpts?.force
|
|
2945
|
+
? false
|
|
2946
|
+
: (yield authInvoke(p, "operatorDisconnected", []));
|
|
2605
2947
|
if (ctx.signal?.aborted)
|
|
2606
2948
|
throw ctx.signal.reason;
|
|
2607
2949
|
if (startOpts?.force || disconnected) {
|
|
2608
|
-
|
|
2950
|
+
(yield authCall(disconnectAuthorization, [ctx, state, false, startOpts?.force === true]));
|
|
2609
2951
|
}
|
|
2610
2952
|
else {
|
|
2611
|
-
|
|
2612
|
-
// the SDK flow, which would overwrite the PKCE verifier and invalidate
|
|
2613
|
-
// the URL the operator may be mid-consent on. Only a recent one: an
|
|
2614
|
-
// old or untimed URL falls through to a fresh flow in this same
|
|
2615
|
-
// epoch, which keeps the stored client registration and discovery.
|
|
2616
|
-
const pending = await p.reusablePendingAuthorizationUrl();
|
|
2953
|
+
const pending = (yield authInvoke(p, "reusablePendingAuthorizationUrl", []));
|
|
2617
2954
|
if (pending) {
|
|
2618
2955
|
return {
|
|
2619
2956
|
state: "auth_required",
|
|
2620
2957
|
authorizationUrl: pending,
|
|
2621
2958
|
authorizationReused: true,
|
|
2622
|
-
message: "Authorization required
|
|
2959
|
+
message: "Authorization required \u2014 open the URL to connect.",
|
|
2623
2960
|
};
|
|
2624
2961
|
}
|
|
2625
2962
|
}
|
|
2626
2963
|
if (ctx.signal?.aborted)
|
|
2627
2964
|
throw ctx.signal.reason;
|
|
2628
2965
|
try {
|
|
2629
|
-
|
|
2966
|
+
(yield authCall(ensureConnected, [ctx, state]));
|
|
2630
2967
|
return {
|
|
2631
2968
|
state: "ok",
|
|
2632
|
-
message: "Already authorized
|
|
2969
|
+
message: "Already authorized \u2014 connection is healthy.",
|
|
2633
2970
|
};
|
|
2634
2971
|
}
|
|
2635
2972
|
catch (err) {
|
|
2636
2973
|
if (state.authRequired) {
|
|
2637
|
-
// The consent this start's attempt stored, while its epoch is live:
|
|
2638
|
-
// one a later reset published belongs to another flow, and this
|
|
2639
|
-
// start fails rather than hand it out.
|
|
2640
2974
|
let authorizationUrl;
|
|
2641
2975
|
try {
|
|
2642
|
-
authorizationUrl =
|
|
2976
|
+
authorizationUrl = (yield state.attemptProvider
|
|
2977
|
+
? authInvoke(state.attemptProvider, "consentUrl", [viewSignal(ctx, true)])
|
|
2978
|
+
: Effect.succeed(undefined));
|
|
2643
2979
|
}
|
|
2644
2980
|
catch (readErr) {
|
|
2645
2981
|
return { state: "error", message: startMessage(readErr) };
|
|
@@ -2647,20 +2983,31 @@ export function remoteMcp(id, opts) {
|
|
|
2647
2983
|
return {
|
|
2648
2984
|
state: "auth_required",
|
|
2649
2985
|
...(authorizationUrl !== undefined ? { authorizationUrl } : {}),
|
|
2650
|
-
message: "Authorization required
|
|
2986
|
+
message: "Authorization required \u2014 open the URL to connect.",
|
|
2651
2987
|
};
|
|
2652
2988
|
}
|
|
2653
2989
|
return { state: "error", message: startMessage(err) };
|
|
2654
2990
|
}
|
|
2655
|
-
};
|
|
2991
|
+
}));
|
|
2656
2992
|
}
|
|
2657
|
-
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* () {
|
|
2658
3003
|
trackCredentialReads(ctx);
|
|
2659
3004
|
const secrets = sentSecretsFor(ctx);
|
|
2660
3005
|
if (opts.auth?.type === "headers") {
|
|
2661
3006
|
for (const value of Object.values(opts.auth.headers))
|
|
2662
3007
|
secrets.header(value);
|
|
2663
3008
|
}
|
|
3009
|
+
for (const value of Object.values(extraHeaders ?? {}))
|
|
3010
|
+
secrets.header(value);
|
|
2664
3011
|
const state = entryFor(ctx);
|
|
2665
3012
|
let active = activeContexts.get(state);
|
|
2666
3013
|
if (!active) {
|
|
@@ -2669,9 +3016,7 @@ export function remoteMcp(id, opts) {
|
|
|
2669
3016
|
}
|
|
2670
3017
|
active.add(ctx);
|
|
2671
3018
|
try {
|
|
2672
|
-
const result =
|
|
2673
|
-
// Opaque state must reach the sealing handler byte-exact. Invocation
|
|
2674
|
-
// intercepts it before ordinary result processing or guest exposure.
|
|
3019
|
+
const result = (yield authCall(run, []));
|
|
2675
3020
|
if (!preserveInput || !isInputRequiredResult(result))
|
|
2676
3021
|
assertDownstreamOutputSafe(ctx.requestScope ?? ctx, result);
|
|
2677
3022
|
return (preserveInput && isInputRequiredResult(result)) || !redactResult
|
|
@@ -2680,35 +3025,82 @@ export function remoteMcp(id, opts) {
|
|
|
2680
3025
|
}
|
|
2681
3026
|
catch (error) {
|
|
2682
3027
|
assertDownstreamOutputSafe(ctx.requestScope ?? ctx, error);
|
|
2683
|
-
throw
|
|
3028
|
+
throw requestSentSecrets(ctx).redact(error);
|
|
2684
3029
|
}
|
|
2685
3030
|
finally {
|
|
2686
3031
|
active.delete(ctx);
|
|
2687
3032
|
}
|
|
2688
|
-
};
|
|
3033
|
+
}));
|
|
2689
3034
|
const callTool = connector.callTool;
|
|
2690
|
-
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
|
+
]));
|
|
2691
3041
|
const listTools = connector.listTools;
|
|
2692
|
-
connector.listTools = (ctx) => withActiveSecrets
|
|
3042
|
+
connector.listTools = authMethod((ctx) => authCall(withActiveSecrets, [
|
|
3043
|
+
ctx,
|
|
3044
|
+
authMethod(() => authCall(listTools, [ctx])),
|
|
3045
|
+
]));
|
|
2693
3046
|
const readResource = connector.readResource;
|
|
2694
|
-
connector.readResource = (uri, ctx) => withActiveSecrets
|
|
3047
|
+
connector.readResource = authMethod((uri, ctx) => authCall(withActiveSecrets, [
|
|
3048
|
+
ctx,
|
|
3049
|
+
authMethod(() => authCall(readResource, [uri, ctx])),
|
|
3050
|
+
]));
|
|
2695
3051
|
if (connector.downstreamSkills) {
|
|
2696
3052
|
const { list, read } = connector.downstreamSkills;
|
|
2697
|
-
connector.downstreamSkills.list = (
|
|
2698
|
-
|
|
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
|
+
]));
|
|
2699
3063
|
}
|
|
2700
3064
|
if (isOauth && !staticClient && clientMetadataUrl === undefined)
|
|
2701
3065
|
declareSelfHostedClient(connector, oauthScope);
|
|
2702
3066
|
if (isOauth) {
|
|
2703
3067
|
const start = connector.startAuth;
|
|
2704
|
-
connector.startAuth =
|
|
2705
|
-
const status =
|
|
2706
|
-
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", []));
|
|
2707
3071
|
return { ...status, ...(path ? { registrationPath: path } : {}) };
|
|
2708
|
-
};
|
|
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);
|
|
2709
3101
|
// Pin before the first asynchronous storage/discovery read, not only once
|
|
2710
3102
|
// a refresh flight exists. These operations do not take call admission.
|
|
2711
|
-
const retain =
|
|
3103
|
+
const retain = retainOperation;
|
|
2712
3104
|
connector.listTools = retain(connector.listTools, 0);
|
|
2713
3105
|
connector.callTool = retain(connector.callTool, 2);
|
|
2714
3106
|
connector.readResource = retain(connector.readResource, 1);
|
|
@@ -2735,15 +3127,19 @@ export function remoteMcp(id, opts) {
|
|
|
2735
3127
|
connector.startAuth = payloadFree(connector.startAuth);
|
|
2736
3128
|
if (connector.finishAuth)
|
|
2737
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]));
|
|
2738
3136
|
if (isOauth)
|
|
2739
|
-
registerInvocationAuth(connector,
|
|
2740
|
-
// Only inspect local grant state here. Discovery, refresh, and MCP
|
|
2741
|
-
// transports remain inside callTool and cannot establish retry safety.
|
|
3137
|
+
registerInvocationAuth(connector, retainOperation(settlingOperation(authMethod((ctx) => authFlow(function* () {
|
|
2742
3138
|
const provider = newProvider(ctx, stateFor(ctx));
|
|
2743
|
-
if (
|
|
3139
|
+
if ((yield authInvoke(provider, "operatorDisconnected", [])))
|
|
2744
3140
|
throw operatorDisconnectedError();
|
|
2745
|
-
if (!(
|
|
3141
|
+
if (!(yield authInvoke(provider, "tokens", [])))
|
|
2746
3142
|
throw authRequiredError();
|
|
2747
|
-
}, 0));
|
|
3143
|
+
})), 0), 0));
|
|
2748
3144
|
return connector;
|
|
2749
3145
|
}
|