@zackbart/connecta 0.28.1 → 0.30.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 +855 -5
- package/README.md +125 -116
- package/bin/connecta.mjs +121 -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/argument-redaction.js +230 -0
- 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 +288 -0
- package/dist/authorization-handoff.js +111 -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 +409 -225
- 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 +633 -0
- package/dist/config.js +508 -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 +87 -38
- 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 +128 -0
- package/dist/downstream-input.js +325 -0
- package/dist/errors.d.ts +132 -31
- package/dist/errors.js +175 -87
- package/dist/execute.js +798 -318
- 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 +30 -13
- package/dist/executors/quickjs-child.js +15 -9
- package/dist/executors/quickjs-protocol.js +5 -6
- package/dist/executors/quickjs-runtime.js +152 -59
- package/dist/executors/quickjs.js +49 -39
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +238 -0
- package/dist/guest-types.d.ts +218 -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 +262 -79
- 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 +61 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +419 -272
- 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 +194 -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-result.js +5 -0
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +60 -19
- 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/_shared/request-token.js +83 -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/infisical/index.d.ts +7 -0
- package/dist/providers/infisical/index.js +713 -0
- package/dist/providers/infisical/skill.generated.js +8 -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 +110 -171
- package/dist/registry.js +588 -927
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +103 -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 +365 -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 +93 -0
- package/dist/sent-secrets.js +1013 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +416 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +181 -0
- package/dist/storage/keys.js +443 -0
- package/dist/storage/memory.js +16 -4
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +543 -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 +336 -56
- 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 +146 -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 +435 -0
- package/documentation/README.md +63 -0
- package/documentation/architecture.md +696 -231
- package/documentation/auth.md +1069 -546
- package/documentation/code-mode.md +662 -356
- package/documentation/deploying.md +146 -0
- package/documentation/integrating.md +159 -0
- package/documentation/meta-tools.md +570 -216
- package/documentation/operating.md +135 -0
- package/documentation/operator-tests.md +97 -0
- package/documentation/operator-ui.md +132 -0
- package/documentation/provider-migration-0.29.md +65 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +293 -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 +126 -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 +140 -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
package/dist/routes/mcp.js
CHANGED
|
@@ -1,14 +1,76 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bindActivityRequest } from "../activity-request.js";
|
|
2
|
+
import { closeConnectorScope } from "../connector-scope.js";
|
|
3
|
+
import { executeLimits } from "../config.js";
|
|
4
|
+
import { oauthConnectUrl, oauthConnectLink, oauthConnectLinkProgress, closeOAuthConnectLinks, claimAuthRetry, oauthConnectUnavailable, } from "../oauth-handoff.js";
|
|
5
|
+
import { AuthElicitation } from "../auth-elicitation.js";
|
|
6
|
+
import { classifyInboundRequest, createMcpHandler, isJSONRPCErrorResponse, isLegacyRequest, McpServer, WebStandardStreamableHTTPServerTransport, } from "@modelcontextprotocol/server";
|
|
2
7
|
import { Duration, Effect, Exit, Option, Result, Scope } from "effect";
|
|
3
|
-
import { notFoundResponse } from "../branding.js";
|
|
4
8
|
import { registerExecuteTool } from "../execute.js";
|
|
5
|
-
import { ExecutorAdmissionError
|
|
9
|
+
import { ExecutorAdmissionError } from "../executor-admission.js";
|
|
6
10
|
import { registerMetaTools } from "../meta-tools.js";
|
|
7
11
|
import { intersectAccess } from "../connector-access.js";
|
|
8
12
|
import { CONNECTA_INSTRUCTIONS } from "../skills.js";
|
|
9
|
-
import {
|
|
13
|
+
import { registerSkills } from "./skills.js";
|
|
14
|
+
import { failureRecord, logFailure } from "../operator-record.js";
|
|
15
|
+
import { redactAgentOutput, sentSecretsForRequest } from "../sent-secrets.js";
|
|
10
16
|
import { detach } from "../runtime/run.js";
|
|
11
|
-
import { authorize,
|
|
17
|
+
import { authorize, mayManageConnector, validateAuthPermissions, } from "./shared.js";
|
|
18
|
+
/**
|
|
19
|
+
* The SDK's own bound on the body it reads. Since server 2.1.0 the SDK
|
|
20
|
+
* answers anything over 4 MiB with its own 413, which would quietly shadow
|
|
21
|
+
* the bound the host already enforces: `listen()`'s `maxBodyBytes` on Node,
|
|
22
|
+
* the platform's request limit on Workers. The host owns that decision, as it
|
|
23
|
+
* did before the SDK had an opinion, so the SDK is told to have none. Its
|
|
24
|
+
* option must be finite.
|
|
25
|
+
*/
|
|
26
|
+
const SDK_BODY_BOUND = { maxRequestBodySize: Number.MAX_SAFE_INTEGER };
|
|
27
|
+
// Identity, not a status/header a custom auth response could imitate.
|
|
28
|
+
const deadlineResponses = new WeakSet();
|
|
29
|
+
async function isModernListen(request, signal) {
|
|
30
|
+
const reader = request.clone().body?.getReader();
|
|
31
|
+
if (!reader)
|
|
32
|
+
return false;
|
|
33
|
+
const onAbort = () => {
|
|
34
|
+
void reader.cancel(signal.reason).catch(() => { });
|
|
35
|
+
void request.body?.cancel(signal.reason).catch(() => { });
|
|
36
|
+
};
|
|
37
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
38
|
+
if (signal.aborted)
|
|
39
|
+
onAbort();
|
|
40
|
+
try {
|
|
41
|
+
const decoder = new TextDecoder();
|
|
42
|
+
let text = "";
|
|
43
|
+
let bytes = 0;
|
|
44
|
+
for (;;) {
|
|
45
|
+
const { done, value } = await reader.read();
|
|
46
|
+
if (done)
|
|
47
|
+
break;
|
|
48
|
+
bytes += value.byteLength;
|
|
49
|
+
if (bytes > SDK_BODY_BOUND.maxRequestBodySize)
|
|
50
|
+
return false;
|
|
51
|
+
text += decoder.decode(value, { stream: true });
|
|
52
|
+
}
|
|
53
|
+
if (signal.aborted)
|
|
54
|
+
return false;
|
|
55
|
+
const protocolVersion = request.headers.get("MCP-Protocol-Version");
|
|
56
|
+
const method = request.headers.get("Mcp-Method");
|
|
57
|
+
const route = classifyInboundRequest({
|
|
58
|
+
httpMethod: request.method,
|
|
59
|
+
...(protocolVersion !== null ? { protocolVersionHeader: protocolVersion } : {}),
|
|
60
|
+
...(method !== null ? { mcpMethodHeader: method } : {}),
|
|
61
|
+
body: JSON.parse(text + decoder.decode()),
|
|
62
|
+
});
|
|
63
|
+
return (route.kind === "modern" && route.messageKind === "request" && route.message.method === "subscriptions/listen");
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Leave unreadable, malformed, and legacy bodies to the normal SDK path.
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
signal.removeEventListener("abort", onAbort);
|
|
71
|
+
reader.releaseLock();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
12
74
|
export const MCP_CORS_HEADERS = {
|
|
13
75
|
"Access-Control-Allow-Origin": "*",
|
|
14
76
|
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
@@ -30,28 +92,41 @@ function withMcpCors(response, request, allowedOrigin) {
|
|
|
30
92
|
// Browsers do not interpret a prefix wildcard in Allow-Headers. Echo only
|
|
31
93
|
// valid SEP-2243 field names; unrelated requested headers stay disallowed.
|
|
32
94
|
const paramHeaders = (request.headers.get("Access-Control-Request-Headers") ?? "")
|
|
33
|
-
.toLowerCase()
|
|
34
|
-
.
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.split(",")
|
|
97
|
+
.map((name) => name.trim())
|
|
98
|
+
.filter((name) => /^mcp-param-[!#$%&'*+.^_`|~0-9a-z-]+$/.test(name));
|
|
35
99
|
if (paramHeaders.length) {
|
|
36
100
|
headers.append("Access-Control-Allow-Headers", [...new Set(paramHeaders)].join(", "));
|
|
37
101
|
}
|
|
38
102
|
headers.append("Vary", "Access-Control-Request-Headers");
|
|
39
103
|
}
|
|
40
|
-
headers.set("Access-Control-Expose-Headers", "WWW-Authenticate, Retry-After, mcp-session-id, mcp-protocol-version");
|
|
41
|
-
|
|
104
|
+
headers.set("Access-Control-Expose-Headers", "WWW-Authenticate, Retry-After, mcp-session-id, mcp-protocol-version, Connecta-Error-Code, Connecta-Recovery");
|
|
105
|
+
const wrapped = new Response(response.body, {
|
|
42
106
|
status: response.status,
|
|
43
107
|
statusText: response.statusText,
|
|
44
108
|
headers,
|
|
45
109
|
});
|
|
110
|
+
if (deadlineResponses.has(response))
|
|
111
|
+
deadlineResponses.add(wrapped);
|
|
112
|
+
return wrapped;
|
|
46
113
|
}
|
|
47
|
-
|
|
114
|
+
/** Transport refusals occur before the RPC body is decoded, so its id is unknown. */
|
|
115
|
+
function mcpRefusal(sentSecrets, status, code, message) {
|
|
116
|
+
const response = Response.json(redactAgentOutput(sentSecrets, { jsonrpc: "2.0", error: { code, message } }), {
|
|
117
|
+
status,
|
|
118
|
+
headers: { "Cache-Control": "no-store" },
|
|
119
|
+
});
|
|
120
|
+
if (status === 504 && code === -33003)
|
|
121
|
+
deadlineResponses.add(response);
|
|
122
|
+
return response;
|
|
123
|
+
}
|
|
124
|
+
function requestAdmissionFailure(sentSecrets, error) {
|
|
48
125
|
const overloaded = error.code === "executor_overloaded";
|
|
49
126
|
const data = {
|
|
50
127
|
code: overloaded ? "server_overloaded" : "server_shutting_down",
|
|
51
128
|
retryable: overloaded,
|
|
52
|
-
...(overloaded && error.retryAfterMs !== undefined
|
|
53
|
-
? { retryAfterMs: error.retryAfterMs }
|
|
54
|
-
: {}),
|
|
129
|
+
...(overloaded && error.retryAfterMs !== undefined ? { retryAfterMs: error.retryAfterMs } : {}),
|
|
55
130
|
};
|
|
56
131
|
const headers = new Headers({
|
|
57
132
|
"Content-Type": "application/json",
|
|
@@ -60,20 +135,17 @@ function requestAdmissionFailure(error) {
|
|
|
60
135
|
if (overloaded && error.retryAfterMs !== undefined) {
|
|
61
136
|
headers.set("Retry-After", String(Math.max(1, Math.ceil(error.retryAfterMs / 1_000))));
|
|
62
137
|
}
|
|
63
|
-
return new Response(JSON.stringify({
|
|
138
|
+
return new Response(JSON.stringify(redactAgentOutput(sentSecrets, {
|
|
64
139
|
jsonrpc: "2.0",
|
|
65
|
-
id: null,
|
|
66
140
|
error: {
|
|
67
141
|
// MCP 2026-07-28 basic#error-codes forbids new allocations in the
|
|
68
142
|
// legacy -32000..-32019 range. Use application codes outside the
|
|
69
143
|
// JSON-RPC reserved range, avoiding retired protocol meanings.
|
|
70
|
-
code: overloaded ? -
|
|
71
|
-
message: overloaded
|
|
72
|
-
? "Server capacity is exhausted. Retry later."
|
|
73
|
-
: "Server is shutting down.",
|
|
144
|
+
code: overloaded ? -33001 : -33002,
|
|
145
|
+
message: overloaded ? "Server capacity is exhausted. Retry later." : "Server is shutting down.",
|
|
74
146
|
data,
|
|
75
147
|
},
|
|
76
|
-
}), { status: 503, headers });
|
|
148
|
+
})), { status: 503, headers });
|
|
77
149
|
}
|
|
78
150
|
/**
|
|
79
151
|
* Hand a response the Scope that holds what its request acquired.
|
|
@@ -153,6 +225,12 @@ function scopedToBody(handler, signal) {
|
|
|
153
225
|
yield* Scope.close(scope, exit);
|
|
154
226
|
return yield* exit;
|
|
155
227
|
}
|
|
228
|
+
// A deadline can finish with a fresh error response after aborting the
|
|
229
|
+
// request. Release request resources now, without cancelling that body.
|
|
230
|
+
if (signal.aborted && deadlineResponses.has(exit.value)) {
|
|
231
|
+
yield* Scope.close(scope, Exit.void);
|
|
232
|
+
return exit.value;
|
|
233
|
+
}
|
|
156
234
|
// Every finalizer in the scope is synchronous, so the close has run by
|
|
157
235
|
// the time detach returns: the permit is back, and handed to the next
|
|
158
236
|
// queued request, inside the read or cancel that ended the body.
|
|
@@ -177,8 +255,7 @@ function scopedToBody(handler, signal) {
|
|
|
177
255
|
function admitted(controller, signal) {
|
|
178
256
|
return Effect.acquireRelease(Effect.suspend(() => {
|
|
179
257
|
const pending = controller.acquire({ signal });
|
|
180
|
-
return Effect.tryPromise({ try: () => pending, catch: (error) => error })
|
|
181
|
-
.pipe(Effect.onInterrupt(() => Effect.sync(() => {
|
|
258
|
+
return Effect.tryPromise({ try: () => pending, catch: (error) => error }).pipe(Effect.onInterrupt(() => Effect.sync(() => {
|
|
182
259
|
pending.then((lease) => lease.release(), () => { });
|
|
183
260
|
})));
|
|
184
261
|
}), (lease) => Effect.sync(() => lease.release()), { interruptible: true });
|
|
@@ -194,13 +271,13 @@ function admitted(controller, signal) {
|
|
|
194
271
|
* operator-side log line, which (as ever — issue #47) is the channel that
|
|
195
272
|
* actually reaches a human.
|
|
196
273
|
*/
|
|
197
|
-
function toolkitRetired(logger) {
|
|
274
|
+
function toolkitRetired(logger, sentSecrets) {
|
|
198
275
|
logger.warn("[connecta] rejected an /mcp connection carrying ?toolkit= with 404: " +
|
|
199
|
-
"toolkits were retired in issue #178 (see
|
|
276
|
+
"toolkits were retired in issue #178 (see PRINCIPLES.md) — one deployment " +
|
|
200
277
|
"serves one audience. The client sees a transport-level failure and " +
|
|
201
278
|
"never the reason, so remove the ?toolkit= value from its MCP endpoint " +
|
|
202
279
|
"URL, or point it at the deployment for its audience.");
|
|
203
|
-
return new Response(JSON.stringify({
|
|
280
|
+
return new Response(JSON.stringify(redactAgentOutput(sentSecrets, {
|
|
204
281
|
jsonrpc: "2.0",
|
|
205
282
|
id: null,
|
|
206
283
|
error: {
|
|
@@ -209,7 +286,7 @@ function toolkitRetired(logger) {
|
|
|
209
286
|
"in issue #178 — remove the ?toolkit= value from the MCP endpoint " +
|
|
210
287
|
"URL, or ask the operator for the deployment serving this audience.",
|
|
211
288
|
},
|
|
212
|
-
}), {
|
|
289
|
+
})), {
|
|
213
290
|
status: 404,
|
|
214
291
|
headers: {
|
|
215
292
|
"Content-Type": "application/json",
|
|
@@ -217,89 +294,98 @@ function toolkitRetired(logger) {
|
|
|
217
294
|
},
|
|
218
295
|
});
|
|
219
296
|
}
|
|
220
|
-
function serveMcp(request, requestSignal, opts, baseUrl, actor, registry, canManageAuth, runtimeContext) {
|
|
297
|
+
function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, registry, canManageAuth, principalKey, runtimeContext, trust = "read-only", pool, principalActor) {
|
|
298
|
+
const sentSecrets = sentSecretsForRequest(requestScope);
|
|
221
299
|
// Every McpServer the request builds is fresh and closes with its scope.
|
|
222
300
|
// The modern handler tears its own down after the exchange; the legacy
|
|
223
301
|
// transport never does, and neither may stay wired to an ended request.
|
|
224
302
|
const servers = [];
|
|
225
303
|
const createServer = () => {
|
|
226
|
-
const
|
|
304
|
+
const authElicitation = new AuthElicitation({
|
|
305
|
+
vault: opts.config.vault,
|
|
306
|
+
publicUrl: opts.config.publicUrl,
|
|
307
|
+
endpoint: new URL(new URL(request.url).pathname, baseUrl).href,
|
|
308
|
+
principal: principalKey,
|
|
309
|
+
registry,
|
|
310
|
+
canManage: canManageAuth,
|
|
311
|
+
connectLink: (id, force, after) => oauthConnectLink(opts, opts.config.publicUrl ?? baseUrl, id, principalKey, force, Boolean(opts.config.vault?.seal && opts.config.vault.open), after),
|
|
312
|
+
linkProgress: (id, nonce) => oauthConnectLinkProgress(opts, baseUrl, id, nonce),
|
|
313
|
+
closeLinks: (id, nonces) => closeOAuthConnectLinks(opts, baseUrl, id, nonces),
|
|
314
|
+
claimRetry: (id, nonce) => claimAuthRetry(opts, baseUrl, id, nonce),
|
|
315
|
+
unavailable: oauthConnectUnavailable(opts),
|
|
316
|
+
credentialUi: Boolean(opts.config.ui && opts.config.vault),
|
|
317
|
+
requestSignal,
|
|
318
|
+
requestScope,
|
|
319
|
+
defer: runtimeContext?.waitUntil?.bind(runtimeContext),
|
|
320
|
+
});
|
|
321
|
+
const server = new McpServer(opts.config.serverInfo, {
|
|
322
|
+
// A request-local server cannot publish catalog changes. Set this before
|
|
323
|
+
// tool registration, whose SDK default otherwise advertises listChanged.
|
|
324
|
+
capabilities: { tools: { listChanged: false }, extensions: { "io.modelcontextprotocol/skills": {} } },
|
|
227
325
|
instructions: CONNECTA_INSTRUCTIONS,
|
|
326
|
+
inputRequired: { legacyShim: false },
|
|
327
|
+
requestState: { verify: (state, context) => authElicitation.verify(state, context) },
|
|
228
328
|
cacheHints: {
|
|
329
|
+
"server/discover": { ttlMs: 3_600_000, cacheScope: "private" },
|
|
229
330
|
"tools/list": {
|
|
230
331
|
ttlMs: 3_600_000,
|
|
231
332
|
cacheScope: "private",
|
|
232
333
|
},
|
|
233
334
|
},
|
|
234
335
|
});
|
|
235
|
-
const activity = opts.activity
|
|
336
|
+
const activity = opts.config.activity?.store
|
|
236
337
|
? {
|
|
237
|
-
sink: opts.activity,
|
|
238
|
-
recordTool: opts.
|
|
338
|
+
sink: opts.config.activity?.store,
|
|
339
|
+
recordTool: opts.config.activity?.recordTool,
|
|
239
340
|
actor,
|
|
341
|
+
...(principalActor ? { principalActor } : {}),
|
|
240
342
|
requestId: crypto.randomUUID(),
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
|
|
247
|
-
: {}),
|
|
248
|
-
logger: opts.logger,
|
|
343
|
+
...(pool !== undefined ? { pool } : {}),
|
|
344
|
+
serverInfo: opts.config.serverInfo,
|
|
345
|
+
...(opts.config.activity?.deploymentId ? { deploymentId: opts.config.activity?.deploymentId } : {}),
|
|
346
|
+
...(runtimeContext?.waitUntil ? { defer: runtimeContext.waitUntil.bind(runtimeContext) } : {}),
|
|
347
|
+
logger: opts.config.logger,
|
|
249
348
|
}
|
|
250
349
|
: undefined;
|
|
350
|
+
if (activity)
|
|
351
|
+
bindActivityRequest(requestScope, activity);
|
|
352
|
+
const client = {};
|
|
353
|
+
registerSkills(server, registry, baseUrl, {
|
|
354
|
+
requestScope,
|
|
355
|
+
requestSignal,
|
|
356
|
+
probeTimeoutMs: opts.config.discovery.probeTimeoutMs,
|
|
357
|
+
...(runtimeContext?.waitUntil ? { defer: runtimeContext.waitUntil.bind(runtimeContext) } : {}),
|
|
358
|
+
});
|
|
251
359
|
registerMetaTools(server, registry, {
|
|
360
|
+
authElicitation,
|
|
361
|
+
client,
|
|
252
362
|
baseUrl,
|
|
363
|
+
requestScope,
|
|
364
|
+
trust,
|
|
253
365
|
canManageAuth,
|
|
254
|
-
|
|
366
|
+
oauthConnectUrl: (id, force) => oauthConnectUrl(opts, baseUrl, id, principalKey, force),
|
|
367
|
+
oauthConnectUnavailable: oauthConnectUnavailable(opts),
|
|
368
|
+
credentialHandoffUrl: opts.config.ui?.credentialHandoffUrl(baseUrl),
|
|
255
369
|
...(activity ? { activity } : {}),
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
...(opts.probeTimeoutMs !== undefined
|
|
260
|
-
? { probeTimeoutMs: opts.probeTimeoutMs }
|
|
261
|
-
: {}),
|
|
262
|
-
...(opts.discoveryConcurrency !== undefined
|
|
263
|
-
? { discoveryConcurrency: opts.discoveryConcurrency }
|
|
264
|
-
: {}),
|
|
370
|
+
defaultToolTimeoutMs: opts.config.calls.defaultTimeoutMs ?? opts.config.execute.hostCallTimeoutMs,
|
|
371
|
+
probeTimeoutMs: opts.config.discovery.probeTimeoutMs,
|
|
372
|
+
discoveryConcurrency: opts.config.discovery.concurrency,
|
|
265
373
|
requestSignal,
|
|
266
|
-
...(runtimeContext?.waitUntil
|
|
267
|
-
? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
|
|
268
|
-
: {}),
|
|
269
|
-
approval: opts.approval,
|
|
374
|
+
...(runtimeContext?.waitUntil ? { defer: runtimeContext.waitUntil.bind(runtimeContext) } : {}),
|
|
270
375
|
});
|
|
271
376
|
registerExecuteTool(server, registry, {
|
|
377
|
+
authElicitation,
|
|
378
|
+
client,
|
|
272
379
|
baseUrl,
|
|
380
|
+
requestScope,
|
|
273
381
|
executor: opts.executor,
|
|
274
|
-
|
|
382
|
+
defaultToolTimeoutMs: opts.config.calls.defaultTimeoutMs ?? opts.config.execute.hostCallTimeoutMs,
|
|
383
|
+
logger: opts.config.logger,
|
|
275
384
|
...(activity ? { activity } : {}),
|
|
276
385
|
requestSignal,
|
|
277
|
-
...(runtimeContext?.waitUntil
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
...(opts.discoveryConcurrency !== undefined
|
|
281
|
-
? { discoveryConcurrency: opts.discoveryConcurrency }
|
|
282
|
-
: {}),
|
|
283
|
-
...(opts.probeTimeoutMs !== undefined
|
|
284
|
-
? { probeTimeoutMs: opts.probeTimeoutMs }
|
|
285
|
-
: {}),
|
|
286
|
-
...(opts.maxEmittedBytes !== undefined
|
|
287
|
-
? { maxEmittedBytes: opts.maxEmittedBytes }
|
|
288
|
-
: {}),
|
|
289
|
-
...(opts.maxEmittedBlocks !== undefined
|
|
290
|
-
? { maxEmittedBlocks: opts.maxEmittedBlocks }
|
|
291
|
-
: {}),
|
|
292
|
-
...(opts.maxHostCalls !== undefined
|
|
293
|
-
? { maxHostCalls: opts.maxHostCalls }
|
|
294
|
-
: {}),
|
|
295
|
-
...(opts.hostCallTimeoutMs !== undefined
|
|
296
|
-
? { hostCallTimeoutMs: opts.hostCallTimeoutMs }
|
|
297
|
-
: {}),
|
|
298
|
-
...(opts.watchdogMs !== undefined
|
|
299
|
-
? { watchdogMs: opts.watchdogMs }
|
|
300
|
-
: {}),
|
|
301
|
-
approval: opts.approval,
|
|
302
|
-
maxWrites: opts.maxWrites,
|
|
386
|
+
...(runtimeContext?.waitUntil ? { defer: runtimeContext.waitUntil.bind(runtimeContext) } : {}),
|
|
387
|
+
...executeLimits(opts.config),
|
|
388
|
+
trust,
|
|
303
389
|
});
|
|
304
390
|
servers.push(server);
|
|
305
391
|
return server;
|
|
@@ -309,15 +395,33 @@ function serveMcp(request, requestSignal, opts, baseUrl, actor, registry, canMan
|
|
|
309
395
|
// Connecta's established wire contract is JSON, so retain the documented
|
|
310
396
|
// user-land legacy branch with the same transport setting while the modern
|
|
311
397
|
// branch uses the fetch-native handler.
|
|
312
|
-
if (!(await isLegacyRequest(request))) {
|
|
313
|
-
|
|
398
|
+
if (!(await isLegacyRequest(request, undefined, SDK_BODY_BOUND))) {
|
|
399
|
+
const response = await createMcpHandler(createServer, {
|
|
400
|
+
...SDK_BODY_BOUND,
|
|
314
401
|
legacy: "reject",
|
|
315
|
-
|
|
402
|
+
// Keep the SDK's validation and prevent it from opening an SSE stream.
|
|
403
|
+
// Its capacity error below becomes our permanent unsupported method.
|
|
404
|
+
maxSubscriptions: 0,
|
|
405
|
+
onerror: (error) => logFailure(opts.config.logger, "MCP handler error", failureRecord({}, error), "error"),
|
|
316
406
|
}).fetch(request);
|
|
407
|
+
if (request.headers.get("Mcp-Method") === "subscriptions/listen" && response.status === 200) {
|
|
408
|
+
const body = await response.clone().json();
|
|
409
|
+
if (isJSONRPCErrorResponse(body) &&
|
|
410
|
+
body.error.code === -32603 &&
|
|
411
|
+
body.error.message === "Subscription limit reached") {
|
|
412
|
+
return new Response(JSON.stringify({
|
|
413
|
+
jsonrpc: "2.0",
|
|
414
|
+
id: body.id,
|
|
415
|
+
error: { code: -32601, message: "Method not found: subscriptions/listen" },
|
|
416
|
+
}), { status: 404, headers: response.headers });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return response;
|
|
317
420
|
}
|
|
318
421
|
// Fresh server + transport per legacy request, stateless and JSON-shaped.
|
|
319
422
|
const server = createServer();
|
|
320
423
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
424
|
+
...SDK_BODY_BOUND,
|
|
321
425
|
enableJsonResponse: true,
|
|
322
426
|
});
|
|
323
427
|
await server.connect(transport);
|
|
@@ -326,10 +430,34 @@ function serveMcp(request, requestSignal, opts, baseUrl, actor, registry, canMan
|
|
|
326
430
|
return Effect.addFinalizer(() => Effect.sync(() => {
|
|
327
431
|
for (const server of servers)
|
|
328
432
|
void server.close().catch(() => { });
|
|
329
|
-
})).pipe(Effect.andThen(Effect.promise(
|
|
433
|
+
})).pipe(Effect.andThen(Effect.promise(async () => {
|
|
434
|
+
const response = await exchange();
|
|
435
|
+
// Both SDK transports serialize here. This also covers SDK-generated
|
|
436
|
+
// JSON-RPC errors and allowed HTTP 4xx text, outside tool result shaping.
|
|
437
|
+
if (!response.body)
|
|
438
|
+
return response;
|
|
439
|
+
const body = await response.text();
|
|
440
|
+
let text;
|
|
441
|
+
if (response.headers.get("Content-Type")?.includes("application/json")) {
|
|
442
|
+
// Structured redaction also joins text blocks before a secret split
|
|
443
|
+
// across them can cross the serialization boundary.
|
|
444
|
+
try {
|
|
445
|
+
text = JSON.stringify(redactAgentOutput(sentSecrets, JSON.parse(body)));
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
/* Non-JSON diagnostics still pass through the text boundary. */
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// Parsed JSON already passes every string through the boundary. A second
|
|
452
|
+
// serialized pass would mistake a blob's encoding for credential bytes.
|
|
453
|
+
text ??= redactAgentOutput(sentSecrets, body);
|
|
454
|
+
const headers = new Headers(response.headers);
|
|
455
|
+
headers.delete("Content-Length");
|
|
456
|
+
return new Response(text, { status: response.status, statusText: response.statusText, headers });
|
|
457
|
+
})));
|
|
330
458
|
}
|
|
331
459
|
export function createMcpRoute(opts) {
|
|
332
|
-
const configuredOrigins = opts.allowedOrigins;
|
|
460
|
+
const configuredOrigins = opts.config.allowedOrigins;
|
|
333
461
|
const isExactOrigin = (value) => {
|
|
334
462
|
if (typeof value !== "string")
|
|
335
463
|
return false;
|
|
@@ -341,13 +469,14 @@ export function createMcpRoute(opts) {
|
|
|
341
469
|
return false;
|
|
342
470
|
}
|
|
343
471
|
};
|
|
344
|
-
|
|
345
|
-
(!Array.isArray(configuredOrigins) || !configuredOrigins.every(isExactOrigin))) {
|
|
346
|
-
throw new TypeError('ConnectaConfig.allowedOrigins must be an array of exact HTTP(S) origins or "*".');
|
|
347
|
-
}
|
|
472
|
+
// The schema has already refused anything but exact origins or "*".
|
|
348
473
|
const origins = new Set(configuredOrigins === undefined
|
|
349
|
-
? opts.
|
|
350
|
-
|
|
474
|
+
? opts.config.publicUrl
|
|
475
|
+
? [new URL(opts.config.publicUrl).origin]
|
|
476
|
+
: []
|
|
477
|
+
: configuredOrigins === "*"
|
|
478
|
+
? []
|
|
479
|
+
: configuredOrigins);
|
|
351
480
|
const allowsOrigin = (origin) => {
|
|
352
481
|
if (configuredOrigins === "*")
|
|
353
482
|
return true;
|
|
@@ -367,10 +496,7 @@ export function createMcpRoute(opts) {
|
|
|
367
496
|
const origin = request.headers.get("Origin");
|
|
368
497
|
if (origin === null || allowsOrigin(origin))
|
|
369
498
|
return null;
|
|
370
|
-
return withMcpCors(
|
|
371
|
-
status: 403,
|
|
372
|
-
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" },
|
|
373
|
-
}), request, null);
|
|
499
|
+
return withMcpCors(mcpRefusal(sentSecretsForRequest(request), 403, -33005, "MCP access is forbidden."), request, null);
|
|
374
500
|
};
|
|
375
501
|
let lastAdmissionWarningAt = 0;
|
|
376
502
|
let suppressedAdmissionWarnings = 0;
|
|
@@ -380,7 +506,7 @@ export function createMcpRoute(opts) {
|
|
|
380
506
|
suppressedAdmissionWarnings++;
|
|
381
507
|
return;
|
|
382
508
|
}
|
|
383
|
-
opts.logger.warn("[connecta] MCP request admission rejected", {
|
|
509
|
+
opts.config.logger.warn("[connecta] MCP request admission rejected", {
|
|
384
510
|
retryAfterMs: error.retryAfterMs,
|
|
385
511
|
active: opts.requestAdmission.activeCount,
|
|
386
512
|
queued: opts.requestAdmission.queuedCount,
|
|
@@ -390,9 +516,14 @@ export function createMcpRoute(opts) {
|
|
|
390
516
|
suppressedAdmissionWarnings = 0;
|
|
391
517
|
};
|
|
392
518
|
function routeMcp(context) {
|
|
393
|
-
const { path, request, baseUrl, runtimeContext
|
|
519
|
+
const { path, request, baseUrl, runtimeContext } = context;
|
|
394
520
|
if (path !== "/mcp" && !path.startsWith("/mcp/"))
|
|
395
521
|
return Effect.succeed(null);
|
|
522
|
+
// Admission, refusal bodies, SDK serialization and connector work share
|
|
523
|
+
// one set. Connector contexts still receive an opaque scope, never the
|
|
524
|
+
// inbound Request or its headers.
|
|
525
|
+
const requestScope = {};
|
|
526
|
+
const sentSecrets = sentSecretsForRequest(requestScope, sentSecretsForRequest(request));
|
|
396
527
|
const poolName = path === "/mcp" ? undefined : path.slice("/mcp/".length);
|
|
397
528
|
const origin = request.headers.get("Origin");
|
|
398
529
|
const allowed = origin === null || allowsOrigin(origin);
|
|
@@ -422,7 +553,25 @@ export function createMcpRoute(opts) {
|
|
|
422
553
|
if (deadlineTimer !== undefined)
|
|
423
554
|
clearTimeout(deadlineTimer);
|
|
424
555
|
}));
|
|
425
|
-
|
|
556
|
+
// Only a body-confirmed modern listen may skip admission. Trusting the
|
|
557
|
+
// method header alone would let a legacy tools/call bypass the pool.
|
|
558
|
+
const startedAt = Date.now();
|
|
559
|
+
const maxDurationMs = opts.requestAdmission.maxDurationMs;
|
|
560
|
+
let listen = false;
|
|
561
|
+
if (request.method === "POST" && request.headers.get("Mcp-Method") === "subscriptions/listen") {
|
|
562
|
+
const classify = Effect.promise(() => isModernListen(request, localAbort.signal));
|
|
563
|
+
const prepared = maxDurationMs === undefined
|
|
564
|
+
? Option.some(yield* classify)
|
|
565
|
+
: yield* classify.pipe(Effect.timeoutOption(Duration.millis(maxDurationMs)));
|
|
566
|
+
if (Option.isNone(prepared)) {
|
|
567
|
+
localAbort.abort(new Error("MCP request lifetime exceeded."));
|
|
568
|
+
return cors(mcpRefusal(sentSecrets, 504, -33003, "MCP request lifetime exceeded."));
|
|
569
|
+
}
|
|
570
|
+
listen = prepared.value;
|
|
571
|
+
}
|
|
572
|
+
const admission = listen
|
|
573
|
+
? Result.succeed(undefined)
|
|
574
|
+
: yield* Effect.result(admitted(opts.requestAdmission, request.signal));
|
|
426
575
|
if (Result.isFailure(admission)) {
|
|
427
576
|
const error = admission.failure;
|
|
428
577
|
if (!(error instanceof ExecutorAdmissionError)) {
|
|
@@ -434,12 +583,16 @@ export function createMcpRoute(opts) {
|
|
|
434
583
|
if (error.code === "executor_overloaded") {
|
|
435
584
|
warnAdmissionRejected(error);
|
|
436
585
|
}
|
|
437
|
-
return cors(requestAdmissionFailure(error));
|
|
586
|
+
return cors(requestAdmissionFailure(sentSecrets, error));
|
|
438
587
|
}
|
|
439
|
-
const remainingMs = admission.success
|
|
588
|
+
const remainingMs = admission.success
|
|
589
|
+
? admission.success.remainingMs()
|
|
590
|
+
: maxDurationMs === undefined
|
|
591
|
+
? undefined
|
|
592
|
+
: maxDurationMs - (Date.now() - startedAt);
|
|
440
593
|
if (remainingMs !== undefined && remainingMs <= 0) {
|
|
441
|
-
admission.success
|
|
442
|
-
return cors(
|
|
594
|
+
admission.success?.release();
|
|
595
|
+
return cors(mcpRefusal(sentSecrets, 504, -33003, "MCP request lifetime exceeded."));
|
|
443
596
|
}
|
|
444
597
|
if (remainingMs !== undefined) {
|
|
445
598
|
deadlineTimer = setTimeout(() => {
|
|
@@ -447,8 +600,8 @@ export function createMcpRoute(opts) {
|
|
|
447
600
|
}, remainingMs);
|
|
448
601
|
}
|
|
449
602
|
const localRequest = new Request(request, { signal: localAbort.signal });
|
|
450
|
-
if (admission.success.waitMs > 0) {
|
|
451
|
-
opts.logger.debug("[connecta] MCP request admitted after queue wait", {
|
|
603
|
+
if (admission.success && admission.success.waitMs > 0) {
|
|
604
|
+
opts.config.logger.debug("[connecta] MCP request admitted after queue wait", {
|
|
452
605
|
waitMs: admission.success.waitMs,
|
|
453
606
|
active: opts.requestAdmission.activeCount,
|
|
454
607
|
queued: opts.requestAdmission.queuedCount,
|
|
@@ -459,33 +612,57 @@ export function createMcpRoute(opts) {
|
|
|
459
612
|
// shares it, to save only the lookups an abandoned authorization
|
|
460
613
|
// finishes on its own.
|
|
461
614
|
const handled = Effect.gen(function* () {
|
|
462
|
-
const authz = yield* Effect.promise(() => authorize(localRequest, baseUrl, opts.auth, runtimeContext, opts.identity));
|
|
463
|
-
if (!authz.ok)
|
|
464
|
-
|
|
615
|
+
const authz = yield* Effect.promise(() => authorize(localRequest, baseUrl, opts.config.auth, runtimeContext, opts.config.identity));
|
|
616
|
+
if (!authz.ok) {
|
|
617
|
+
const response = authz.response;
|
|
618
|
+
// Preserve the auth adapter's challenge and status. The MCP host must
|
|
619
|
+
// repair its connection to connecta before any connector can be called.
|
|
620
|
+
if (response.status !== 401 && response.status !== 403)
|
|
621
|
+
return cors(response);
|
|
622
|
+
const headers = new Headers(response.headers);
|
|
623
|
+
headers.set("Connecta-Error-Code", "host_auth_required");
|
|
624
|
+
headers.set("Connecta-Recovery", "host_connection");
|
|
625
|
+
// Custom adapters may own a streaming or non-JSON response. Preserve
|
|
626
|
+
// that body and its request lifetime, and carry recovery in headers.
|
|
627
|
+
if (!response.headers.get("Content-Type")?.includes("application/json")) {
|
|
628
|
+
return cors(new Response(response.body, { status: response.status, headers }));
|
|
629
|
+
}
|
|
630
|
+
headers.set("Content-Type", "application/json");
|
|
631
|
+
headers.delete("Content-Length");
|
|
632
|
+
void response.body?.cancel().catch(() => { });
|
|
633
|
+
return cors(new Response(JSON.stringify(redactAgentOutput(sentSecrets, {
|
|
634
|
+
error: {
|
|
635
|
+
code: "host_auth_required",
|
|
636
|
+
message: "The host is not authorized to connect to this endpoint. Sign in or update the host's connecta access token and endpoint grants.",
|
|
637
|
+
retryable: false,
|
|
638
|
+
recovery: "host_connection",
|
|
639
|
+
},
|
|
640
|
+
})), { status: response.status, headers }));
|
|
641
|
+
}
|
|
465
642
|
// A pool endpoint narrows the identity's own view and nothing else. An
|
|
466
643
|
// undeclared name, a grant that refuses, and a grant that throws are
|
|
467
644
|
// one identical 404 so a credential never enumerates the other pools;
|
|
468
645
|
// the operator log is where the reason lives.
|
|
469
646
|
let access = authz;
|
|
647
|
+
let trust = opts.config.trust;
|
|
470
648
|
if (poolName !== undefined) {
|
|
471
649
|
const pool = opts.pools?.get(poolName);
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
// still reads a plain "Not Found".
|
|
486
|
-
return cors(notFoundResponse(request, opts));
|
|
650
|
+
const denialReason = !pool
|
|
651
|
+
? "pool_not_declared"
|
|
652
|
+
: yield* Effect.promise(async () => {
|
|
653
|
+
try {
|
|
654
|
+
return (await pool.grant(authz.identity)) === true ? undefined : "pool_grant_denied";
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
return "pool_grant_threw";
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
if (!pool || denialReason !== undefined) {
|
|
661
|
+
logFailure(opts.config.logger, "MCP pool request denied", failureRecord({ reason: denialReason ?? "pool_not_declared" }));
|
|
662
|
+
return cors(mcpRefusal(sentSecrets, 404, -33004, "MCP endpoint not found."));
|
|
487
663
|
}
|
|
488
664
|
access = intersectAccess(authz, pool.access);
|
|
665
|
+
trust = pool.trust;
|
|
489
666
|
}
|
|
490
667
|
let scopedRegistry;
|
|
491
668
|
try {
|
|
@@ -496,22 +673,80 @@ export function createMcpRoute(opts) {
|
|
|
496
673
|
...(access.guardedToolAccess ? { guardedToolAccess: access.guardedToolAccess } : {}),
|
|
497
674
|
...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}),
|
|
498
675
|
...(authz.principalKey ? { principalKey: authz.principalKey } : {}),
|
|
676
|
+
endpoint: new URL(request.url).pathname,
|
|
677
|
+
origin: request.headers.get("Origin"),
|
|
678
|
+
currentResultAccess: async (address, classification, signal, access) => {
|
|
679
|
+
signal?.throwIfAborted();
|
|
680
|
+
const current = await authorize(localRequest, baseUrl, opts.config.auth, runtimeContext, opts.config.identity);
|
|
681
|
+
if (!current.ok) {
|
|
682
|
+
await current.response.body?.cancel().catch(() => { });
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
if (current.subjectKey !== authz.subjectKey || current.principalKey !== authz.principalKey)
|
|
686
|
+
return false;
|
|
687
|
+
validateAuthPermissions(current, opts.registry);
|
|
688
|
+
let currentAccess = current;
|
|
689
|
+
let currentTrust = opts.config.trust;
|
|
690
|
+
if (poolName !== undefined) {
|
|
691
|
+
const pool = opts.pools.get(poolName);
|
|
692
|
+
if (!pool || (await pool.grant(current.identity)) !== true)
|
|
693
|
+
return false;
|
|
694
|
+
currentAccess = intersectAccess(current, pool.access);
|
|
695
|
+
currentTrust = pool.trust;
|
|
696
|
+
}
|
|
697
|
+
const view = opts.registry.scoped({
|
|
698
|
+
...currentAccess,
|
|
699
|
+
...(current.subjectKey ? { subjectKey: current.subjectKey } : {}),
|
|
700
|
+
...(current.principalKey ? { principalKey: current.principalKey } : {}),
|
|
701
|
+
});
|
|
702
|
+
if (classification === "write" && currentTrust !== "trusted")
|
|
703
|
+
return false;
|
|
704
|
+
if (address === undefined)
|
|
705
|
+
return true;
|
|
706
|
+
// Skill and resource content requires a whole grant; catalog metadata requires visibility.
|
|
707
|
+
if (!address.includes("."))
|
|
708
|
+
return Boolean(access === "catalog" ? view.getConnector(address) : view.getResourceConnector(address));
|
|
709
|
+
const resolved = view.resolveAddress(address);
|
|
710
|
+
if (!resolved)
|
|
711
|
+
return false;
|
|
712
|
+
const requestScope = {};
|
|
713
|
+
try {
|
|
714
|
+
const tool = (await view.getTools(resolved.connector.id, baseUrl, requestScope, signal ? { signal } : {})).find((tool) => tool.name === resolved.toolName);
|
|
715
|
+
signal?.throwIfAborted();
|
|
716
|
+
return Boolean(tool &&
|
|
717
|
+
(access === "catalog" ||
|
|
718
|
+
(classification === "read" && tool.classification === "read") ||
|
|
719
|
+
currentTrust === "trusted"));
|
|
720
|
+
}
|
|
721
|
+
finally {
|
|
722
|
+
await closeConnectorScope(resolved.connector, view.contextFor(resolved.connector.id, baseUrl, requestScope), runtimeContext?.waitUntil?.bind(runtimeContext));
|
|
723
|
+
}
|
|
724
|
+
},
|
|
499
725
|
caller: {
|
|
500
726
|
identity: authz.identity,
|
|
727
|
+
// `authorize` admits an open deployment's every request as the
|
|
728
|
+
// anonymous actor; only a provider's `ok` is an authentication.
|
|
729
|
+
authenticated: opts.config.auth.length > 0,
|
|
501
730
|
...(poolName !== undefined ? { pool: poolName } : {}),
|
|
502
731
|
},
|
|
503
732
|
});
|
|
504
733
|
}
|
|
505
|
-
catch
|
|
506
|
-
return cors(
|
|
507
|
-
status: 403,
|
|
508
|
-
headers: { "Content-Type": "application/json" },
|
|
509
|
-
}));
|
|
734
|
+
catch {
|
|
735
|
+
return cors(mcpRefusal(sentSecrets, 403, -33005, "MCP access is forbidden."));
|
|
510
736
|
}
|
|
511
737
|
if (new URL(request.url).searchParams.has("toolkit")) {
|
|
512
|
-
return cors(toolkitRetired(opts.logger));
|
|
738
|
+
return cors(toolkitRetired(opts.config.logger, sentSecrets));
|
|
513
739
|
}
|
|
514
|
-
return cors(yield* serveMcp(localRequest, localAbort.signal, opts, baseUrl, authz.actor, scopedRegistry, id => {
|
|
740
|
+
return cors(yield* serveMcp(localRequest, localAbort.signal, requestScope, opts, baseUrl, authz.actor, scopedRegistry, (id) => {
|
|
741
|
+
const connector = scopedRegistry.getConnector(id);
|
|
742
|
+
return Boolean(connector && mayManageConnector(authz, connector));
|
|
743
|
+
}, authz.principalKey, runtimeContext, trust, poolName, authz.identity.principal
|
|
744
|
+
? {
|
|
745
|
+
kind: authz.actor.kind,
|
|
746
|
+
id: authz.identity.principal.id,
|
|
747
|
+
namespace: authz.identity.principal.namespace,
|
|
748
|
+
}
|
|
749
|
+
: undefined));
|
|
515
750
|
});
|
|
516
751
|
if (remainingMs === undefined)
|
|
517
752
|
return yield* handled;
|
|
@@ -520,7 +755,7 @@ export function createMcpRoute(opts) {
|
|
|
520
755
|
localAbort.abort(new Error("MCP request lifetime exceeded."));
|
|
521
756
|
return Option.isSome(bounded)
|
|
522
757
|
? bounded.value
|
|
523
|
-
: cors(
|
|
758
|
+
: cors(mcpRefusal(sentSecrets, 504, -33003, "MCP request lifetime exceeded."));
|
|
524
759
|
}), localAbort.signal);
|
|
525
760
|
}
|
|
526
761
|
return { handle: routeMcp, rejectOrigin };
|