@zackbart/connecta 0.28.1 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +788 -5
- package/README.md +62 -42
- package/bin/connecta.mjs +110 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1584 -1580
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +93 -71
- package/dist/auth-elicitation.js +283 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +368 -224
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +631 -0
- package/dist/config.js +503 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +59 -7
- package/dist/connectors/api-connector.js +106 -19
- package/dist/connectors/api.js +4 -1
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +84 -35
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1774 -320
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +119 -0
- package/dist/downstream-input.js +323 -0
- package/dist/errors.d.ts +97 -20
- package/dist/errors.js +158 -80
- package/dist/execute.js +453 -305
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +24 -9
- package/dist/executors/quickjs-child.js +3 -4
- package/dist/executors/quickjs-runtime.js +147 -59
- package/dist/executors/quickjs.js +29 -35
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +188 -0
- package/dist/guest-types.d.ts +176 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +24 -256
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +230 -73
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +76 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +285 -209
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +117 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +55 -18
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
- package/dist/providers/{breeze.js → breeze/index.js} +184 -140
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
- package/dist/providers/{ccb.js → ccb/index.js} +271 -116
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -0
- package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
- package/dist/providers/{overflow.js → overflow/index.js} +74 -111
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
- package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
- package/dist/providers/{tithely.js → tithely/index.js} +60 -91
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +108 -171
- package/dist/registry.js +513 -925
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +100 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +356 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +110 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +52 -0
- package/dist/sent-secrets.js +474 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +406 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +166 -0
- package/dist/storage/keys.js +428 -0
- package/dist/storage/memory.js +7 -0
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +534 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +320 -54
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +137 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +434 -0
- package/documentation/architecture.md +664 -231
- package/documentation/auth.md +936 -546
- package/documentation/code-mode.md +501 -312
- package/documentation/meta-tools.md +449 -200
- package/documentation/operator-tests.md +93 -0
- package/documentation/provider-migration-0.29.md +60 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +286 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +120 -54
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +133 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/basecamp.js +0 -445
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { auth, OAuthError, UnauthorizedError } from "@modelcontextprotocol/client";
|
|
2
|
-
import {
|
|
2
|
+
import { byteReadResponse } from "../byte-read-response.js";
|
|
3
|
+
import { sentSecretsFor, sentSecretsFetch, shortSecretWarning } from "../sent-secrets.js";
|
|
3
4
|
import { ConnectorCallError, msg } from "../errors.js";
|
|
4
|
-
import { oauthPartitionFor, retainOAuthPartition, retainingOAuthPartition
|
|
5
|
+
import { oauthPartitionFor, retainOAuthPartition, retainingOAuthPartition } from "../oauth-partition.js";
|
|
5
6
|
import { oauthSealerFor } from "../oauth-sealing.js";
|
|
6
|
-
import {
|
|
7
|
+
import { describeFailure } from "../operator-record.js";
|
|
8
|
+
import { assertOAuthScope, authorizingContext, KvOAuthProvider, refreshCoordinatorsByPartition, } from "./downstream-oauth.js";
|
|
9
|
+
import { trackRemoteClientRequest } from "./downstream-client-metadata.js";
|
|
7
10
|
import { trackOAuthStartReset } from "./oauth-start-reset.js";
|
|
8
11
|
/**
|
|
9
12
|
* Downstream OAuth for a hand-written `api()` connector: the authorization
|
|
@@ -11,8 +14,8 @@ import { trackOAuthStartReset } from "./oauth-start-reset.js";
|
|
|
11
14
|
* declares, rather than ones a server advertises.
|
|
12
15
|
*
|
|
13
16
|
* Everything that makes the `remoteMcp()` grant safe is reused, not copied.
|
|
14
|
-
* `StaticOAuthProvider` is a `KvOAuthProvider` — the same
|
|
15
|
-
* owner partitions,
|
|
17
|
+
* `StaticOAuthProvider` is a `KvOAuthProvider` — the same grant record and
|
|
18
|
+
* consents, sealing, owner partitions, epoch fence, and refresh-failure
|
|
16
19
|
* verdicts — whose discovery state and client are constants, and the SDK's
|
|
17
20
|
* own `auth()` drives it through the same `OAuthRefreshCoordinator`. What a
|
|
18
21
|
* static configuration subtracts is the network learning: no RFC 9728 or 8414
|
|
@@ -40,15 +43,9 @@ const RESERVED_TOKEN_HEADERS = new Set([
|
|
|
40
43
|
]);
|
|
41
44
|
/** An RFC 9110 field-name token. */
|
|
42
45
|
const HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
43
|
-
const CLIENT_AUTHENTICATION = new Set([
|
|
44
|
-
"client_secret_basic",
|
|
45
|
-
"client_secret_post",
|
|
46
|
-
"none",
|
|
47
|
-
]);
|
|
46
|
+
const CLIENT_AUTHENTICATION = new Set(["client_secret_basic", "client_secret_post", "none"]);
|
|
48
47
|
function isLoopback(hostname) {
|
|
49
|
-
return
|
|
50
|
-
hostname === "127.0.0.1" ||
|
|
51
|
-
hostname === "[::1]");
|
|
48
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
52
49
|
}
|
|
53
50
|
/** An absolute HTTPS URL (or HTTP on loopback), without credentials or fragment. */
|
|
54
51
|
function configuredUrl(id, option, value) {
|
|
@@ -82,13 +79,11 @@ function settingsFor(id, config) {
|
|
|
82
79
|
if (typeof config.clientId !== "string" || config.clientId.trim() === "") {
|
|
83
80
|
throw new Error(`[connecta] connector "${id}" oauth.clientId must be a non-empty string.`);
|
|
84
81
|
}
|
|
85
|
-
if (config.clientSecret !== undefined &&
|
|
86
|
-
(typeof config.clientSecret !== "string" || config.clientSecret === "")) {
|
|
82
|
+
if (config.clientSecret !== undefined && (typeof config.clientSecret !== "string" || config.clientSecret === "")) {
|
|
87
83
|
// Usually an unset environment variable read as "". Never echo it.
|
|
88
84
|
throw new Error(`[connecta] connector "${id}" oauth.clientSecret must be a non-empty string when set.`);
|
|
89
85
|
}
|
|
90
|
-
const clientAuthentication = config.tokenEndpointAuthMethod ??
|
|
91
|
-
(config.clientSecret !== undefined ? "client_secret_basic" : "none");
|
|
86
|
+
const clientAuthentication = config.tokenEndpointAuthMethod ?? (config.clientSecret !== undefined ? "client_secret_basic" : "none");
|
|
92
87
|
if (!CLIENT_AUTHENTICATION.has(clientAuthentication)) {
|
|
93
88
|
throw new Error(`[connecta] connector "${id}" oauth.tokenEndpointAuthMethod must be ` +
|
|
94
89
|
'"client_secret_basic", "client_secret_post", or "none".');
|
|
@@ -98,8 +93,7 @@ function settingsFor(id, config) {
|
|
|
98
93
|
'tokenEndpointAuthMethod "none"; a public client sends no secret.');
|
|
99
94
|
}
|
|
100
95
|
if (clientAuthentication !== "none" && config.clientSecret === undefined) {
|
|
101
|
-
throw new Error(`[connecta] connector "${id}" oauth.tokenEndpointAuthMethod ` +
|
|
102
|
-
`"${clientAuthentication}" needs a clientSecret.`);
|
|
96
|
+
throw new Error(`[connecta] connector "${id}" oauth.tokenEndpointAuthMethod ` + `"${clientAuthentication}" needs a clientSecret.`);
|
|
103
97
|
}
|
|
104
98
|
assertOAuthScope(id, config.scope);
|
|
105
99
|
const authorizationParams = [];
|
|
@@ -119,8 +113,7 @@ function settingsFor(id, config) {
|
|
|
119
113
|
throw new Error(`[connecta] connector "${id}" oauth.tokenRequestHeaders has an invalid header name.`);
|
|
120
114
|
}
|
|
121
115
|
if (RESERVED_TOKEN_HEADERS.has(name.toLowerCase())) {
|
|
122
|
-
throw new Error(`[connecta] connector "${id}" oauth.tokenRequestHeaders may not set ` +
|
|
123
|
-
`"${name}"; the token request owns it.`);
|
|
116
|
+
throw new Error(`[connecta] connector "${id}" oauth.tokenRequestHeaders may not set ` + `"${name}"; the token request owns it.`);
|
|
124
117
|
}
|
|
125
118
|
if (typeof value !== "string" || value.includes("\r") || value.includes("\n") || value.includes("\0")) {
|
|
126
119
|
throw new Error(`[connecta] connector "${id}" oauth.tokenRequestHeaders.${name} must be a single-line string.`);
|
|
@@ -170,7 +163,7 @@ class StaticOAuthProvider extends KvOAuthProvider {
|
|
|
170
163
|
super(connectorId, ctx.storage, `${ctx.baseUrl}/oauth/callback/${connectorId}`, coordinator, ctx.allowAuthorization === true, oauthSealerFor(ctx), ctx.signal,
|
|
171
164
|
// No client binding: the client is configuration, so there is no
|
|
172
165
|
// registration for a restart to carry forward or discard.
|
|
173
|
-
undefined, (reset) => trackOAuthStartReset(ctx.requestScope ?? ctx, reset), undefined, settings.scope);
|
|
166
|
+
undefined, (reset) => trackOAuthStartReset(ctx.requestScope ?? ctx, reset), undefined, settings.scope, undefined, { secrets: sentSecretsFor(ctx) });
|
|
174
167
|
this.settings = settings;
|
|
175
168
|
}
|
|
176
169
|
/**
|
|
@@ -182,14 +175,19 @@ class StaticOAuthProvider extends KvOAuthProvider {
|
|
|
182
175
|
const { settings } = this;
|
|
183
176
|
return {
|
|
184
177
|
client_id: settings.clientId,
|
|
185
|
-
...(settings.clientSecret !== undefined
|
|
186
|
-
? { client_secret: settings.clientSecret }
|
|
187
|
-
: {}),
|
|
178
|
+
...(settings.clientSecret !== undefined ? { client_secret: settings.clientSecret } : {}),
|
|
188
179
|
redirect_uris: [this.redirectUrl],
|
|
189
180
|
token_endpoint_auth_method: settings.clientAuthentication,
|
|
190
181
|
issuer: settings.identity,
|
|
191
182
|
};
|
|
192
183
|
}
|
|
184
|
+
configuredAuthorizationBinding() {
|
|
185
|
+
return {
|
|
186
|
+
issuer: this.settings.identity,
|
|
187
|
+
clientId: this.settings.clientId,
|
|
188
|
+
tokenEndpoint: this.settings.tokenEndpoint.href,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
193
191
|
async saveClientInformation() {
|
|
194
192
|
// Nothing to save: the SDK only saves a client it registered or stamped.
|
|
195
193
|
}
|
|
@@ -262,36 +260,45 @@ export function staticOAuth(id, config) {
|
|
|
262
260
|
const callbackProviders = new WeakMap();
|
|
263
261
|
const rejectedScopes = new WeakSet();
|
|
264
262
|
const scopeOf = (ctx) => ctx.requestScope ?? ctx;
|
|
265
|
-
const
|
|
263
|
+
const warnShortSecret = shortSecretWarning();
|
|
264
|
+
const providerFor = (ctx) => {
|
|
265
|
+
warnShortSecret(settings.clientSecret, ctx.logger);
|
|
266
|
+
return new StaticOAuthProvider(id, ctx, coordinatorFor(ctx), settings);
|
|
267
|
+
};
|
|
266
268
|
/**
|
|
267
269
|
* The global `fetch`, read per request, with the configured token-request
|
|
268
270
|
* headers laid over the SDK's own. With discovery and registration answered
|
|
269
271
|
* from configuration the token endpoint is the only URL the SDK fetches here;
|
|
270
272
|
* the check keeps the headers on it even if that ever changes.
|
|
271
273
|
*/
|
|
272
|
-
const tokenEndpointFetch = (input, init = {}) => {
|
|
274
|
+
const tokenEndpointFetch = async (ctx, input, init = {}) => {
|
|
273
275
|
const url = new URL(input);
|
|
274
|
-
if (settings.tokenRequestHeaders.length === 0 ||
|
|
275
|
-
|
|
276
|
-
return
|
|
276
|
+
if (settings.tokenRequestHeaders.length === 0 || `${url.origin}${url.pathname}` !== settings.identity) {
|
|
277
|
+
trackRemoteClientRequest(sentSecretsFor(ctx), input, init);
|
|
278
|
+
return byteReadResponse(await sentSecretsFetch(ctx)(input, { ...init, redirect: "manual" }));
|
|
277
279
|
}
|
|
278
280
|
const headers = new Headers(init.headers);
|
|
279
281
|
for (const [name, value] of settings.tokenRequestHeaders)
|
|
280
282
|
headers.set(name, value);
|
|
281
|
-
|
|
283
|
+
for (const [, value] of settings.tokenRequestHeaders)
|
|
284
|
+
sentSecretsFor(ctx).header(value);
|
|
285
|
+
trackRemoteClientRequest(sentSecretsFor(ctx), input, { ...init, headers });
|
|
286
|
+
return byteReadResponse(await sentSecretsFetch(ctx)(input, { ...init, headers, redirect: "manual" }));
|
|
282
287
|
};
|
|
283
288
|
/**
|
|
284
289
|
* The SDK's `auth()` over this provider. The token endpoint is fetched
|
|
285
|
-
* through the refresh coordinator
|
|
286
|
-
*
|
|
287
|
-
*
|
|
290
|
+
* through the refresh coordinator, with permanent spent fingerprints,
|
|
291
|
+
* rotation commits, and re-consent verdicts. Credential-bearing requests
|
|
292
|
+
* never follow redirects in either OAuth adapter.
|
|
288
293
|
*/
|
|
289
294
|
const runAuth = (provider, ctx, exchange) => auth(provider, {
|
|
290
295
|
serverUrl: settings.serverUrl,
|
|
291
296
|
...exchange,
|
|
292
|
-
fetchFn: coordinatorFor(ctx).coordinatedFetch(provider,
|
|
297
|
+
fetchFn: coordinatorFor(ctx).coordinatedFetch(provider, (input, init) => tokenEndpointFetch(ctx, input, init), ctx.signal, ctx.defer),
|
|
293
298
|
});
|
|
294
|
-
|
|
299
|
+
// No cause, as in remoteMcp(): the `UnauthorizedError` class is the whole
|
|
300
|
+
// verdict, and a logger rendering the chain renders whatever it says.
|
|
301
|
+
const authRequiredError = () => new ConnectorCallError("downstream_oauth_required", `Connector "${id}" requires authorization — call authorize_connector({ connector: "${id}" }) and open the returned URL.`);
|
|
295
302
|
const disconnectedMessage = `Connector "${id}" was disconnected by an operator — explicitly start authorization to reconnect it.`;
|
|
296
303
|
/**
|
|
297
304
|
* One request through the owner's grant. A 401 earns exactly one recovery:
|
|
@@ -314,20 +321,23 @@ export function staticOAuth(id, config) {
|
|
|
314
321
|
throw new ConnectorCallError("connector_call_failed", `Connector "${id}": a request through ctx.oauth.fetch may not set ` +
|
|
315
322
|
"Authorization; the connector's grant owns it.", { retryable: false });
|
|
316
323
|
}
|
|
317
|
-
const signal = init.signal && ctx.signal
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const send = (accessToken) => {
|
|
324
|
+
const signal = init.signal && ctx.signal ? AbortSignal.any([init.signal, ctx.signal]) : (init.signal ?? ctx.signal);
|
|
325
|
+
const send = async (accessToken) => {
|
|
326
|
+
sentSecretsFor(ctx).add(accessToken);
|
|
321
327
|
const sent = new Headers(headers);
|
|
322
328
|
sent.set("Authorization", `Bearer ${accessToken}`);
|
|
323
329
|
// The token rides to a declared origin and no further: a redirect is
|
|
324
330
|
// handed back to the handler unfollowed rather than re-sent anywhere.
|
|
325
|
-
|
|
331
|
+
// The handler reads the answer with `.json()` or `.text()`, which read
|
|
332
|
+
// bytes here: workerd quotes a text read's non-text Content-Type in
|
|
333
|
+
// its own log, out of the handler's reach.
|
|
334
|
+
const response = await sentSecretsFetch(ctx)(url, {
|
|
326
335
|
...init,
|
|
327
336
|
headers: sent,
|
|
328
337
|
redirect: "manual",
|
|
329
338
|
...(signal ? { signal } : {}),
|
|
330
339
|
});
|
|
340
|
+
return byteReadResponse(response);
|
|
331
341
|
};
|
|
332
342
|
if (rejectedScopes.has(scopeOf(ctx)))
|
|
333
343
|
throw authRequiredError();
|
|
@@ -336,13 +346,12 @@ export function staticOAuth(id, config) {
|
|
|
336
346
|
if (signal?.aborted)
|
|
337
347
|
throw signal.reason;
|
|
338
348
|
const provider = providerFor(ctx);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
349
|
+
// Bind this call's reads and writes to the live epoch: a reset landing
|
|
350
|
+
// meanwhile fails the call rather than handing it another flow's grant.
|
|
351
|
+
const epoch = await provider.beginFlow();
|
|
352
|
+
if (provider.isOperatorDisconnectedEpoch(epoch)) {
|
|
353
|
+
throw new ConnectorCallError("downstream_oauth_required", disconnectedMessage);
|
|
342
354
|
}
|
|
343
|
-
// A refresh this call triggers writes into the epoch it started under,
|
|
344
|
-
// so a reset landing meanwhile leaves its rotation unreadable.
|
|
345
|
-
provider.captureGeneration(generation);
|
|
346
355
|
const tokens = await provider.tokens(issuerContext);
|
|
347
356
|
if (!tokens)
|
|
348
357
|
throw authRequiredError();
|
|
@@ -373,14 +382,7 @@ export function staticOAuth(id, config) {
|
|
|
373
382
|
catch (error) {
|
|
374
383
|
if (error instanceof UnauthorizedError) {
|
|
375
384
|
rejectedScopes.add(scopeOf(ctx));
|
|
376
|
-
throw authRequiredError(
|
|
377
|
-
}
|
|
378
|
-
// The coordinator's answer while another request is still committing
|
|
379
|
-
// a rotation it redeemed. The SDK rethrows it untouched; it is a
|
|
380
|
-
// moment, not a verdict on the grant.
|
|
381
|
-
if (error instanceof OAuthError && error.code === "temporarily_unavailable") {
|
|
382
|
-
throw new ConnectorCallError("unavailable", `Connector "${id}" could not refresh its OAuth grant while another ` +
|
|
383
|
-
"request was saving one. The grant is kept; retry.", { cause: error });
|
|
385
|
+
throw authRequiredError();
|
|
384
386
|
}
|
|
385
387
|
throw error;
|
|
386
388
|
}
|
|
@@ -396,13 +398,14 @@ export function staticOAuth(id, config) {
|
|
|
396
398
|
};
|
|
397
399
|
}
|
|
398
400
|
// A status read never starts authorization: this provider cannot, and
|
|
399
|
-
// nothing here calls the SDK. A stored grant
|
|
400
|
-
// says otherwise — the downstream is
|
|
401
|
+
// nothing here calls the SDK. A stored grant for the configured token
|
|
402
|
+
// endpoint is healthy until a call says otherwise — the downstream is
|
|
403
|
+
// asked nothing.
|
|
401
404
|
const provider = providerFor(ctx);
|
|
402
405
|
if (await provider.operatorDisconnected()) {
|
|
403
406
|
return { state: "auth_required", message: disconnectedMessage };
|
|
404
407
|
}
|
|
405
|
-
return (await provider.tokens())
|
|
408
|
+
return (await provider.tokens(issuerContext))
|
|
406
409
|
? { state: "ok" }
|
|
407
410
|
: { state: "auth_required", message: AUTH_REQUIRED_MESSAGE };
|
|
408
411
|
};
|
|
@@ -434,23 +437,16 @@ export function staticOAuth(id, config) {
|
|
|
434
437
|
if (ctx.signal?.aborted)
|
|
435
438
|
throw ctx.signal.reason;
|
|
436
439
|
try {
|
|
437
|
-
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
// past, and a flow started there writes an untimed pending URL that a
|
|
441
|
-
// second Connect could not hand back.
|
|
442
|
-
if (generation === LEGACY_GENERATION)
|
|
443
|
-
generation = await provider.bumpGeneration();
|
|
444
|
-
provider.captureGeneration(generation);
|
|
445
|
-
// An issuer-bound read: a grant from a since-repointed token endpoint
|
|
446
|
-
// is fenced here, before it could be reported healthy.
|
|
440
|
+
// A grant from a since-repointed token endpoint reads as absent here,
|
|
441
|
+
// and the consent this start begins replaces it.
|
|
442
|
+
await provider.beginFlow();
|
|
447
443
|
if (await provider.tokens(issuerContext)) {
|
|
448
444
|
return { state: "ok", message: "Already authorized — connection is healthy." };
|
|
449
445
|
}
|
|
450
446
|
if ((await runAuth(provider, ctx)) === "AUTHORIZED") {
|
|
451
447
|
return { state: "ok", message: "Already authorized — connection is healthy." };
|
|
452
448
|
}
|
|
453
|
-
const authorizationUrl = await provider.
|
|
449
|
+
const authorizationUrl = await provider.consentUrl();
|
|
454
450
|
return {
|
|
455
451
|
state: "auth_required",
|
|
456
452
|
...(authorizationUrl !== undefined ? { authorizationUrl } : {}),
|
|
@@ -461,7 +457,13 @@ export function staticOAuth(id, config) {
|
|
|
461
457
|
if (error instanceof UnauthorizedError) {
|
|
462
458
|
return { state: "auth_required", message: AUTH_REQUIRED_MESSAGE };
|
|
463
459
|
}
|
|
464
|
-
|
|
460
|
+
// Reaches the agent through authorize_connector. Text already in
|
|
461
|
+
// connecta's words passes (token responses are rebuilt before the SDK
|
|
462
|
+
// reads them); anything else is told from its record.
|
|
463
|
+
return {
|
|
464
|
+
state: "error",
|
|
465
|
+
message: error instanceof ConnectorCallError || error instanceof OAuthError ? msg(error) : describeFailure(id, error),
|
|
466
|
+
};
|
|
465
467
|
}
|
|
466
468
|
};
|
|
467
469
|
const disconnectAuth = async (ctx) => {
|
|
@@ -480,7 +482,14 @@ export function staticOAuth(id, config) {
|
|
|
480
482
|
callbackProviders.delete(scopeOf(ctx));
|
|
481
483
|
const authorizationCode = callbackParams?.get("code") ?? code;
|
|
482
484
|
const iss = callbackParams?.get("iss") ?? undefined;
|
|
485
|
+
// Every exchange names and claims its own consent, including without PKCE.
|
|
486
|
+
const consent = callbackParams?.get("state") ?? null;
|
|
487
|
+
if (!consent || !(await provider.verifyCallbackState(consent))) {
|
|
488
|
+
throw new ConnectorCallError("connector_call_failed", `Connector "${id}" authorization callback matches no pending consent; nothing was exchanged.`);
|
|
489
|
+
}
|
|
490
|
+
provider.validateCallbackIssuer(callbackParams?.get("iss") ?? null);
|
|
483
491
|
provider.exchanging(authorizationCode);
|
|
492
|
+
await provider.bindFlow();
|
|
484
493
|
const result = await runAuth(provider, ctx, {
|
|
485
494
|
authorizationCode,
|
|
486
495
|
...(iss !== undefined ? { iss } : {}),
|
|
@@ -488,7 +497,6 @@ export function staticOAuth(id, config) {
|
|
|
488
497
|
if (result !== "AUTHORIZED") {
|
|
489
498
|
throw new UnauthorizedError("Failed to authorize");
|
|
490
499
|
}
|
|
491
|
-
await provider.clearPending();
|
|
492
500
|
rejectedScopes.delete(scopeOf(ctx));
|
|
493
501
|
};
|
|
494
502
|
return {
|
|
@@ -496,6 +504,20 @@ export function staticOAuth(id, config) {
|
|
|
496
504
|
startAuth: retainingOAuthPartition(startAuth, 0),
|
|
497
505
|
disconnectAuth: retainingOAuthPartition(disconnectAuth, 0),
|
|
498
506
|
verifyState: retainingOAuthPartition(verifyState, 1),
|
|
507
|
+
verifyCallbackIssuer: retainingOAuthPartition(async (issuer, ctx) => {
|
|
508
|
+
const provider = callbackProviders.get(scopeOf(ctx));
|
|
509
|
+
if (!provider)
|
|
510
|
+
return false;
|
|
511
|
+
provider.validateCallbackIssuer(issuer);
|
|
512
|
+
return true;
|
|
513
|
+
}, 1),
|
|
514
|
+
consumeAuthError: retainingOAuthPartition(async (ctx) => {
|
|
515
|
+
const provider = callbackProviders.get(scopeOf(ctx));
|
|
516
|
+
callbackProviders.delete(scopeOf(ctx));
|
|
517
|
+
if (!provider)
|
|
518
|
+
throw new ConnectorCallError("connector_call_failed", "OAuth error callback matches no pending consent.");
|
|
519
|
+
await provider.consumeAuthError();
|
|
520
|
+
}, 0),
|
|
499
521
|
finishAuth: retainingOAuthPartition(finishAuth, 1),
|
|
500
522
|
access: (ctx) => ({
|
|
501
523
|
fetch: (input, init) => authorizedFetch(ctx, input, init),
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { createRequestStateCodec, inputRequired, ProtocolError, INVALID_PARAMS, PROTOCOL_VERSION_META_KEY, CLIENT_CAPABILITIES_META_KEY, } from "@modelcontextprotocol/server";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
4
|
+
import { closeScope } from "./runtime/connector-scope.js";
|
|
5
|
+
import { storedCredentialShape } from "./credential-rules.js";
|
|
6
|
+
import { authRecoveryFacts, bindReplayReads, classificationDigest } from "./invocation-auth.js";
|
|
7
|
+
import { CatalogService } from "./catalog-service.js";
|
|
8
|
+
import { DownstreamElicitation, MAX_RELAY_STATE_CHARS, } from "./downstream-input.js";
|
|
9
|
+
import { canonicalState as canonical, requestDigest as digest, stateObject as object, invalidRequestState as invalidState, REQUEST_STATE_TTL_MS as TTL_MS, MAX_INPUT_ROUNDS as MAX_ROUNDS, } from "./request-state.js";
|
|
10
|
+
const INPUT_KEY = "connecta_auth";
|
|
11
|
+
const MESSAGE = "Connect this service in your browser, then retry the request.";
|
|
12
|
+
function failure(code, message) {
|
|
13
|
+
const structuredContent = { ok: false, error: { code, message, retryable: false } };
|
|
14
|
+
return { content: [{ type: "text", text: JSON.stringify(structuredContent) }], structuredContent, isError: true };
|
|
15
|
+
}
|
|
16
|
+
/** Request-local auth recovery at the MCP boundary, never inside a guest provider. */
|
|
17
|
+
export class AuthElicitation {
|
|
18
|
+
options;
|
|
19
|
+
codec;
|
|
20
|
+
downstream;
|
|
21
|
+
stateCodec() {
|
|
22
|
+
const key = this.options.vault?.requestStateKey;
|
|
23
|
+
if (!key)
|
|
24
|
+
return invalidState();
|
|
25
|
+
return (this.codec ??= key.call(this.options.vault).then((bytes) => createRequestStateCodec({
|
|
26
|
+
key: bytes,
|
|
27
|
+
ttlSeconds: TTL_MS / 1000,
|
|
28
|
+
bind: () => canonical({ principal: this.options.principal, endpoint: this.options.endpoint }),
|
|
29
|
+
})));
|
|
30
|
+
}
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.options = options;
|
|
33
|
+
this.downstream = new DownstreamElicitation({
|
|
34
|
+
...options,
|
|
35
|
+
mint: async (state, context) => (await this.stateCodec()).mint(state, context),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** The SDK runs this before any tool handler, including non-auth tools. */
|
|
39
|
+
async verify(wire, context) {
|
|
40
|
+
const { vault, endpoint, principal, registry, canManage } = this.options;
|
|
41
|
+
if (!vault?.requestStateKey || wire.length > MAX_RELAY_STATE_CHARS)
|
|
42
|
+
return invalidState();
|
|
43
|
+
const state = await (await this.stateCodec()).verify(wire, context);
|
|
44
|
+
if (object(state) && state.version === 2 && state.kind === "downstream")
|
|
45
|
+
return this.downstream.verify(state, context);
|
|
46
|
+
if (wire.length > 8192)
|
|
47
|
+
return invalidState();
|
|
48
|
+
if (!object(state) ||
|
|
49
|
+
state.version !== 1 ||
|
|
50
|
+
!principal ||
|
|
51
|
+
state.principal !== principal ||
|
|
52
|
+
state.endpoint !== endpoint ||
|
|
53
|
+
typeof state.connector !== "string" ||
|
|
54
|
+
!registry.getConnector(state.connector) ||
|
|
55
|
+
!canManage(state.connector) ||
|
|
56
|
+
!Number.isSafeInteger(state.expiresAt) ||
|
|
57
|
+
state.expiresAt <= Date.now() ||
|
|
58
|
+
state.expiresAt > Date.now() + TTL_MS ||
|
|
59
|
+
!Number.isInteger(state.round) ||
|
|
60
|
+
state.round < 1 ||
|
|
61
|
+
state.round > MAX_ROUNDS ||
|
|
62
|
+
!["call_tool", "call_destructive_tool", "execute_code", "authorize_connector"].includes(String(state.tool)) ||
|
|
63
|
+
typeof state.digest !== "string" ||
|
|
64
|
+
!/^[a-f0-9]{64}$/.test(state.digest) ||
|
|
65
|
+
!Array.isArray(state.browserNonces) ||
|
|
66
|
+
state.browserNonces.length !== state.round ||
|
|
67
|
+
!state.browserNonces.every((nonce) => typeof nonce === "string" && /^[a-f0-9-]{36}$/.test(nonce)) ||
|
|
68
|
+
!Array.isArray(state.reads) ||
|
|
69
|
+
!state.reads.every((read) => object(read) &&
|
|
70
|
+
typeof read.address === "string" &&
|
|
71
|
+
typeof read.digest === "string" &&
|
|
72
|
+
/^[a-f0-9]{64}$/.test(read.digest)) ||
|
|
73
|
+
(state.address !== undefined && typeof state.address !== "string"))
|
|
74
|
+
return invalidState();
|
|
75
|
+
if (context.mcpReq.method !== "tools/call" || context.http?.req?.headers.get("Mcp-Name") !== state.tool)
|
|
76
|
+
return invalidState();
|
|
77
|
+
return state;
|
|
78
|
+
}
|
|
79
|
+
async browserProgress(state) {
|
|
80
|
+
let started;
|
|
81
|
+
let claimed;
|
|
82
|
+
let completed = false;
|
|
83
|
+
let pending = false;
|
|
84
|
+
for (const nonce of state.browserNonces) {
|
|
85
|
+
const progress = await this.options.linkProgress(state.connector, nonce);
|
|
86
|
+
if (progress === "started")
|
|
87
|
+
started = nonce;
|
|
88
|
+
if (progress === "claimed")
|
|
89
|
+
claimed = nonce;
|
|
90
|
+
completed ||= progress === "started";
|
|
91
|
+
pending ||= progress === "claimed";
|
|
92
|
+
}
|
|
93
|
+
const predecessor = started ?? claimed;
|
|
94
|
+
return { ...(predecessor ? { predecessor } : {}), completed: completed && !pending };
|
|
95
|
+
}
|
|
96
|
+
async connected(id, context) {
|
|
97
|
+
const { registry, publicUrl, vault, principal, defer } = this.options;
|
|
98
|
+
const connector = registry.getConnector(id);
|
|
99
|
+
const scope = {};
|
|
100
|
+
const signal = AbortSignal.any([context.mcpReq.signal, this.options.requestSignal]);
|
|
101
|
+
try {
|
|
102
|
+
return await runEdge(withDeadlineEffect((deadline) => Effect.gen(function* () {
|
|
103
|
+
const ctx = registry.contextFor(id, publicUrl, scope, { signal: deadline });
|
|
104
|
+
yield* Effect.addFinalizer(() => closeScope(connector, ctx, defer));
|
|
105
|
+
if (connector.credential) {
|
|
106
|
+
const metadata = yield* Effect.promise(() => vault.metadata(id, connector.authScope === "personal" ? principal : undefined));
|
|
107
|
+
if (storedCredentialShape(connector.credential, metadata?.fields ?? null).state !== "valid")
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (!connector.status)
|
|
111
|
+
return Boolean(connector.credential);
|
|
112
|
+
return ((yield* Effect.promise(() => registry.statusFor(id, publicUrl, scope, { signal: deadline }))).state ===
|
|
113
|
+
"ok");
|
|
114
|
+
}).pipe(Effect.scoped), { signal, timeoutMs: 5000, timeoutError: new Error("Auth status check timed out") }));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async run(tool, args, context, operation) {
|
|
121
|
+
this.downstream.bind(context);
|
|
122
|
+
const prior = context.mcpReq.requestState();
|
|
123
|
+
if (prior?.version === 2)
|
|
124
|
+
return this.downstream.resume(prior, tool, args, context, operation);
|
|
125
|
+
const previous = prior;
|
|
126
|
+
const requestDigest = previous ? await digest(tool, args) : undefined;
|
|
127
|
+
if (previous && (previous.tool !== tool || previous.digest !== requestDigest || previous.address !== args.address))
|
|
128
|
+
return failure("invalid_request_state", "Invalid or expired requestState");
|
|
129
|
+
const response = context.mcpReq.inputResponses?.[INPUT_KEY];
|
|
130
|
+
if (previous && object(response) && !["accept", "decline", "cancel"].includes(String(response.action))) {
|
|
131
|
+
throw new ProtocolError(INVALID_PARAMS, "Invalid auth elicitation response", {
|
|
132
|
+
reason: "invalid_input_response",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (previous && !(await this.options.claimRetry(previous.connector, previous.browserNonces.at(-1)))) {
|
|
136
|
+
return failure("invalid_request_state", "Invalid or expired requestState");
|
|
137
|
+
}
|
|
138
|
+
if (previous && object(response)) {
|
|
139
|
+
if (response.action === "decline")
|
|
140
|
+
return failure("auth_declined", "Connection was declined. The request was not retried.");
|
|
141
|
+
if (response.action === "cancel")
|
|
142
|
+
return failure("auth_cancelled", "Connection was cancelled. The request was not retried.");
|
|
143
|
+
}
|
|
144
|
+
if (previous?.reads.length) {
|
|
145
|
+
const catalog = new CatalogService(this.options.registry, this.options.publicUrl, {
|
|
146
|
+
requestScope: this.options.requestScope,
|
|
147
|
+
requestSignal: AbortSignal.any([context.mcpReq.signal, this.options.requestSignal]),
|
|
148
|
+
});
|
|
149
|
+
// Recheck every entered read before restarting any part of a program.
|
|
150
|
+
// A trusted pool cannot authorize replay of a newly classified write.
|
|
151
|
+
for (const read of previous.reads) {
|
|
152
|
+
const current = await catalog.resolveTool(read.address, { signal: context.mcpReq.signal });
|
|
153
|
+
if (!current.ok ||
|
|
154
|
+
current.resolved.classificationFresh !== true ||
|
|
155
|
+
current.resolved.definition.classification !== "read" ||
|
|
156
|
+
(await classificationDigest(current.resolved.definition)) !== read.digest) {
|
|
157
|
+
const structuredContent = {
|
|
158
|
+
ok: false,
|
|
159
|
+
error: {
|
|
160
|
+
code: "auth_replay_refused",
|
|
161
|
+
retryable: false,
|
|
162
|
+
reconciliationRequired: true,
|
|
163
|
+
message: "An entered read no longer has the same fresh classification. Reconcile its target before starting a new request.",
|
|
164
|
+
authorizationUrl: (await this.options.connectLink(previous.connector)).url,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
return {
|
|
168
|
+
isError: true,
|
|
169
|
+
structuredContent,
|
|
170
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent) }],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
bindReplayReads(this.options.requestScope, previous.reads);
|
|
175
|
+
}
|
|
176
|
+
const browser = previous && tool === "authorize_connector" ? await this.browserProgress(previous) : undefined;
|
|
177
|
+
if (browser?.completed && previous) {
|
|
178
|
+
// Retire unspent links before the status read. A browser claim racing
|
|
179
|
+
// these CASes either wins and is observed as pending, or is refused.
|
|
180
|
+
await this.options.closeLinks(previous.connector, previous.browserNonces);
|
|
181
|
+
if ((await this.browserProgress(previous)).completed && (await this.connected(previous.connector, context))) {
|
|
182
|
+
const structuredContent = { connector: previous.connector, status: "ok" };
|
|
183
|
+
return { structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
let result = await operation();
|
|
187
|
+
const relayed = await this.downstream.finish(tool, args, context, result, previous);
|
|
188
|
+
if (relayed !== result)
|
|
189
|
+
return relayed;
|
|
190
|
+
const error = result.structuredContent?.error;
|
|
191
|
+
const authFailure = result.isError && object(error) && (error.code === "auth_required" || error.code === "downstream_oauth_required");
|
|
192
|
+
const explicit = tool === "authorize_connector" &&
|
|
193
|
+
["oauth", "operator_config"].includes(String(result.structuredContent?.recovery));
|
|
194
|
+
if (!authFailure && !explicit)
|
|
195
|
+
return result;
|
|
196
|
+
const id = authFailure ? error.connector : explicit ? args.connector : undefined;
|
|
197
|
+
// Every accepted retry owns new request-local facts. Handler-authored
|
|
198
|
+
// errors and program write summaries cannot establish replay eligibility.
|
|
199
|
+
const facts = await authRecoveryFacts(this.options.requestScope, String(id));
|
|
200
|
+
const blocked = !explicit && (!facts.eligible || facts.writeEntered);
|
|
201
|
+
if (authFailure && (facts.unsafeEntered || (tool !== "call_tool" && !facts.eligible))) {
|
|
202
|
+
const structuredContent = {
|
|
203
|
+
...result.structuredContent,
|
|
204
|
+
error: {
|
|
205
|
+
...error,
|
|
206
|
+
retryable: false,
|
|
207
|
+
reconciliationRequired: true,
|
|
208
|
+
retry: "This call may have partially run. Reconcile its target before retrying after the operator completes recovery.",
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
result = { ...result, structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
|
|
212
|
+
}
|
|
213
|
+
const { registry, vault, principal, publicUrl, canManage, unavailable } = this.options;
|
|
214
|
+
const connector = typeof id === "string" ? registry.getConnector(id) : undefined;
|
|
215
|
+
const recoverable = connector?.startAuth || (connector?.credential && this.options.credentialUi);
|
|
216
|
+
if (typeof id !== "string" ||
|
|
217
|
+
!/^[a-z0-9_-]+$/.test(id) ||
|
|
218
|
+
!recoverable ||
|
|
219
|
+
!principal ||
|
|
220
|
+
!publicUrl ||
|
|
221
|
+
unavailable ||
|
|
222
|
+
!canManage(id))
|
|
223
|
+
return result;
|
|
224
|
+
const envelope = context.mcpReq.envelope;
|
|
225
|
+
const capabilities = envelope?.[CLIENT_CAPABILITIES_META_KEY];
|
|
226
|
+
const elicitation = object(capabilities) ? capabilities.elicitation : undefined;
|
|
227
|
+
const capable = envelope?.[PROTOCOL_VERSION_META_KEY] === "2026-07-28" &&
|
|
228
|
+
object(elicitation) &&
|
|
229
|
+
Object.hasOwn(elicitation, "url") &&
|
|
230
|
+
object(elicitation.url);
|
|
231
|
+
if (blocked || !capable || !vault?.requestStateKey || !vault.seal || !vault.open) {
|
|
232
|
+
if (!authFailure)
|
|
233
|
+
return result;
|
|
234
|
+
const structuredContent = {
|
|
235
|
+
...result.structuredContent,
|
|
236
|
+
error: {
|
|
237
|
+
...result.structuredContent?.error,
|
|
238
|
+
authorizationUrl: (await this.options.connectLink(id)).url,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
return { ...result, structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
|
|
242
|
+
}
|
|
243
|
+
if (previous && previous.round >= MAX_ROUNDS)
|
|
244
|
+
return failure("auth_round_limit", "Connection is still required after three prompts. Connect explicitly, then start a new request.");
|
|
245
|
+
// Once a verified browser has started this attempt, retries continue it.
|
|
246
|
+
// A pending consent must never be reset by another force=true prompt.
|
|
247
|
+
const forced = explicit && args.force === true;
|
|
248
|
+
const link = await this.options.connectLink(id, forced && !browser?.predecessor, forced ? browser?.predecessor : undefined);
|
|
249
|
+
const state = {
|
|
250
|
+
version: 1,
|
|
251
|
+
principal,
|
|
252
|
+
endpoint: this.options.endpoint,
|
|
253
|
+
connector: id,
|
|
254
|
+
tool,
|
|
255
|
+
...(typeof args.address === "string" ? { address: args.address } : {}),
|
|
256
|
+
digest: requestDigest ?? (await digest(tool, args)),
|
|
257
|
+
round: (previous?.round ?? 0) + 1,
|
|
258
|
+
expiresAt: previous?.expiresAt ?? Date.now() + TTL_MS,
|
|
259
|
+
browserNonces: [...(previous?.browserNonces ?? []), link.nonce],
|
|
260
|
+
reads: [...new Map([...(previous?.reads ?? []), ...facts.reads].map((read) => [read.address, read])).values()],
|
|
261
|
+
};
|
|
262
|
+
const wire = await (await this.stateCodec()).mint(state, context);
|
|
263
|
+
if (wire.length > 8192) {
|
|
264
|
+
const structuredContent = {
|
|
265
|
+
...result.structuredContent,
|
|
266
|
+
error: {
|
|
267
|
+
...result.structuredContent?.error,
|
|
268
|
+
authorizationUrl: link.url,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
return { ...result, structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
|
|
272
|
+
}
|
|
273
|
+
return inputRequired({
|
|
274
|
+
inputRequests: {
|
|
275
|
+
[INPUT_KEY]: inputRequired.elicitUrl({
|
|
276
|
+
message: MESSAGE,
|
|
277
|
+
url: link.url,
|
|
278
|
+
}),
|
|
279
|
+
},
|
|
280
|
+
requestState: wire,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|