@zackbart/connecta 0.15.1 → 0.16.1
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/AGENTS.md +40 -20
- package/CHANGELOG.md +653 -0
- package/README.md +25 -5
- package/bin/connecta.mjs +44 -3
- package/dist/access-tokens.d.ts +0 -1
- package/dist/access-tokens.js +0 -1
- package/dist/activity.d.ts +50 -2
- package/dist/activity.js +34 -1
- package/dist/apps-shell.d.ts +0 -1
- package/dist/apps-shell.js +0 -1
- package/dist/auth/bearer.d.ts +0 -1
- package/dist/auth/bearer.js +0 -1
- package/dist/auth/clerk.d.ts +0 -1
- package/dist/auth/clerk.js +45 -9
- package/dist/auth/downstream-oauth.d.ts +0 -1
- package/dist/auth/downstream-oauth.js +0 -1
- package/dist/call-admission.d.ts +0 -1
- package/dist/call-admission.js +0 -1
- package/dist/catalog-drift.d.ts +99 -0
- package/dist/catalog-drift.js +251 -0
- package/dist/catalog-fingerprint.d.ts +0 -1
- package/dist/catalog-fingerprint.js +0 -1
- package/dist/catalog-limits.d.ts +0 -1
- package/dist/catalog-limits.js +0 -1
- package/dist/catalog-service.d.ts +0 -1
- package/dist/catalog-service.js +41 -5
- package/dist/catalog.d.ts +11 -1
- package/dist/catalog.js +20 -1
- package/dist/concurrency.d.ts +0 -1
- package/dist/concurrency.js +0 -1
- package/dist/connector-scope.d.ts +0 -1
- package/dist/connector-scope.js +0 -1
- package/dist/connectors/api.d.ts +29 -21
- package/dist/connectors/api.js +45 -19
- package/dist/connectors/guarded-fetch.d.ts +106 -0
- package/dist/connectors/guarded-fetch.js +289 -0
- package/dist/connectors/remote-mcp.d.ts +0 -1
- package/dist/connectors/remote-mcp.js +0 -1
- package/dist/credentials.d.ts +0 -1
- package/dist/credentials.js +0 -1
- package/dist/errors.d.ts +30 -3
- package/dist/errors.js +1 -1
- package/dist/execute.d.ts +0 -1
- package/dist/execute.js +0 -1
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +28 -1
- package/dist/executor-result.d.ts +0 -1
- package/dist/executor-result.js +0 -1
- package/dist/executors/quickjs-child.d.ts +0 -1
- package/dist/executors/quickjs-child.js +0 -1
- package/dist/executors/quickjs-protocol.d.ts +0 -1
- package/dist/executors/quickjs-protocol.js +0 -1
- package/dist/executors/quickjs-runtime.d.ts +0 -1
- package/dist/executors/quickjs-runtime.js +0 -1
- package/dist/executors/quickjs.d.ts +0 -1
- package/dist/executors/quickjs.js +2 -1
- package/dist/favicon.d.ts +0 -1
- package/dist/favicon.js +0 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +24 -7
- package/dist/invocation.d.ts +0 -1
- package/dist/invocation.js +0 -1
- package/dist/json-schema.d.ts +0 -1
- package/dist/json-schema.js +0 -1
- package/dist/mcp-result.d.ts +0 -1
- package/dist/mcp-result.js +0 -1
- package/dist/meta-tools.d.ts +0 -1
- package/dist/meta-tools.js +0 -1
- package/dist/node.d.ts +0 -1
- package/dist/node.js +0 -1
- package/dist/operator-ui/generated.d.ts +2 -3
- package/dist/operator-ui/generated.js +3 -4
- package/dist/operator-ui/model.d.ts +10 -1
- package/dist/operator-ui/model.js +0 -1
- package/dist/operator-ui/view.d.ts +167 -0
- package/dist/operator-ui/view.js +270 -0
- package/dist/providers/cloudflare.d.ts +0 -1
- package/dist/providers/cloudflare.js +261 -299
- package/dist/providers/linear.d.ts +26 -5
- package/dist/providers/linear.js +32 -61
- package/dist/providers/mixpanel.d.ts +35 -4
- package/dist/providers/mixpanel.js +53 -79
- package/dist/providers/notion.d.ts +0 -1
- package/dist/providers/notion.js +87 -42
- package/dist/providers/stripe.d.ts +13 -1
- package/dist/providers/stripe.js +31 -60
- package/dist/registry.d.ts +34 -2
- package/dist/registry.js +85 -5
- package/dist/retired-toolkits.d.ts +0 -1
- package/dist/retired-toolkits.js +0 -1
- package/dist/routes/access-tokens.d.ts +0 -1
- package/dist/routes/access-tokens.js +0 -1
- package/dist/routes/activity.d.ts +0 -1
- package/dist/routes/activity.js +0 -1
- package/dist/routes/credentials.d.ts +0 -1
- package/dist/routes/credentials.js +0 -1
- package/dist/routes/mcp.d.ts +0 -1
- package/dist/routes/mcp.js +0 -1
- package/dist/routes/oauth.d.ts +0 -1
- package/dist/routes/oauth.js +0 -1
- package/dist/routes/shared.d.ts +2 -1
- package/dist/routes/shared.js +0 -1
- package/dist/routes/ui.d.ts +0 -1
- package/dist/routes/ui.js +0 -1
- package/dist/server.d.ts +0 -1
- package/dist/server.js +11 -1
- package/dist/skills.d.ts +0 -1
- package/dist/skills.js +0 -1
- package/dist/storage/file.d.ts +0 -1
- package/dist/storage/file.js +0 -1
- package/dist/storage/memory.d.ts +0 -1
- package/dist/storage/memory.js +0 -1
- package/dist/timeout.d.ts +0 -1
- package/dist/timeout.js +0 -1
- package/dist/tool-safety.d.ts +0 -1
- package/dist/tool-safety.js +0 -1
- package/dist/types.d.ts +45 -1
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +0 -1
- package/dist/ui.js +13 -137
- package/dist/validate.d.ts +13 -15
- package/dist/validate.js +24 -11
- package/dist/version.d.ts +1 -2
- package/dist/version.js +1 -2
- package/documentation/architecture.md +186 -5
- package/documentation/auth.md +11 -0
- package/documentation/call-admission.md +142 -6
- package/documentation/cloudflare.md +92 -10
- package/documentation/code-first-exploration.md +2 -2
- package/documentation/code-mode.md +14 -14
- package/documentation/connector-guides.md +172 -5
- package/documentation/connectors.md +89 -0
- package/documentation/linear.md +20 -2
- package/documentation/mcp-ui-design.md +2 -2
- package/documentation/meta-tools.md +7 -1
- package/documentation/mixpanel.md +42 -9
- package/documentation/notion.md +23 -2
- package/documentation/operations.md +325 -5
- package/documentation/operator-ui.md +120 -5
- package/documentation/provider-audit.md +160 -0
- package/documentation/provider-conventions.md +667 -0
- package/documentation/request-admission.md +167 -5
- package/documentation/rich-output-design.md +1 -2
- package/documentation/stripe.md +15 -0
- package/documentation/upgrading.md +454 -0
- package/ethos.md +14 -9
- package/examples/worker/README.md +102 -12
- package/examples/worker/src/index.ts +46 -1
- package/examples/worker/wrangler.jsonc +14 -1
- package/package.json +10 -6
- package/templates/node/.dockerignore +10 -0
- package/templates/node/.env.example +40 -3
- package/templates/node/AGENTS.md +16 -1
- package/templates/node/Dockerfile +49 -0
- package/templates/node/README.md +117 -2
- package/templates/node/docker-compose.yml +39 -0
- package/templates/node/package.json +1 -1
- package/templates/node/src/file-activity.ts +182 -0
- package/templates/node/src/index.ts +80 -4
- package/assets/connecta-clay-hero.png +0 -0
- package/dist/access-tokens.d.ts.map +0 -1
- package/dist/access-tokens.js.map +0 -1
- package/dist/activity.d.ts.map +0 -1
- package/dist/activity.js.map +0 -1
- package/dist/apps-shell.d.ts.map +0 -1
- package/dist/apps-shell.js.map +0 -1
- package/dist/auth/bearer.d.ts.map +0 -1
- package/dist/auth/bearer.js.map +0 -1
- package/dist/auth/clerk.d.ts.map +0 -1
- package/dist/auth/clerk.js.map +0 -1
- package/dist/auth/downstream-oauth.d.ts.map +0 -1
- package/dist/auth/downstream-oauth.js.map +0 -1
- package/dist/call-admission.d.ts.map +0 -1
- package/dist/call-admission.js.map +0 -1
- package/dist/catalog-fingerprint.d.ts.map +0 -1
- package/dist/catalog-fingerprint.js.map +0 -1
- package/dist/catalog-limits.d.ts.map +0 -1
- package/dist/catalog-limits.js.map +0 -1
- package/dist/catalog-service.d.ts.map +0 -1
- package/dist/catalog-service.js.map +0 -1
- package/dist/catalog.d.ts.map +0 -1
- package/dist/catalog.js.map +0 -1
- package/dist/concurrency.d.ts.map +0 -1
- package/dist/concurrency.js.map +0 -1
- package/dist/connector-scope.d.ts.map +0 -1
- package/dist/connector-scope.js.map +0 -1
- package/dist/connectors/api.d.ts.map +0 -1
- package/dist/connectors/api.js.map +0 -1
- package/dist/connectors/remote-mcp.d.ts.map +0 -1
- package/dist/connectors/remote-mcp.js.map +0 -1
- package/dist/credentials.d.ts.map +0 -1
- package/dist/credentials.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/execute.d.ts.map +0 -1
- package/dist/execute.js.map +0 -1
- package/dist/executor-admission.d.ts.map +0 -1
- package/dist/executor-admission.js.map +0 -1
- package/dist/executor-result.d.ts.map +0 -1
- package/dist/executor-result.js.map +0 -1
- package/dist/executors/quickjs-child.d.ts.map +0 -1
- package/dist/executors/quickjs-child.js.map +0 -1
- package/dist/executors/quickjs-protocol.d.ts.map +0 -1
- package/dist/executors/quickjs-protocol.js.map +0 -1
- package/dist/executors/quickjs-runtime.d.ts.map +0 -1
- package/dist/executors/quickjs-runtime.js.map +0 -1
- package/dist/executors/quickjs.d.ts.map +0 -1
- package/dist/executors/quickjs.js.map +0 -1
- package/dist/favicon.d.ts.map +0 -1
- package/dist/favicon.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/invocation.d.ts.map +0 -1
- package/dist/invocation.js.map +0 -1
- package/dist/json-schema.d.ts.map +0 -1
- package/dist/json-schema.js.map +0 -1
- package/dist/mcp-result.d.ts.map +0 -1
- package/dist/mcp-result.js.map +0 -1
- package/dist/meta-tools.d.ts.map +0 -1
- package/dist/meta-tools.js.map +0 -1
- package/dist/node.d.ts.map +0 -1
- package/dist/node.js.map +0 -1
- package/dist/operator-ui/generated.d.ts.map +0 -1
- package/dist/operator-ui/generated.js.map +0 -1
- package/dist/operator-ui/model.d.ts.map +0 -1
- package/dist/operator-ui/model.js.map +0 -1
- package/dist/providers/cloudflare.d.ts.map +0 -1
- package/dist/providers/cloudflare.js.map +0 -1
- package/dist/providers/linear.d.ts.map +0 -1
- package/dist/providers/linear.js.map +0 -1
- package/dist/providers/mixpanel.d.ts.map +0 -1
- package/dist/providers/mixpanel.js.map +0 -1
- package/dist/providers/notion.d.ts.map +0 -1
- package/dist/providers/notion.js.map +0 -1
- package/dist/providers/stripe.d.ts.map +0 -1
- package/dist/providers/stripe.js.map +0 -1
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js.map +0 -1
- package/dist/retired-toolkits.d.ts.map +0 -1
- package/dist/retired-toolkits.js.map +0 -1
- package/dist/routes/access-tokens.d.ts.map +0 -1
- package/dist/routes/access-tokens.js.map +0 -1
- package/dist/routes/activity.d.ts.map +0 -1
- package/dist/routes/activity.js.map +0 -1
- package/dist/routes/credentials.d.ts.map +0 -1
- package/dist/routes/credentials.js.map +0 -1
- package/dist/routes/mcp.d.ts.map +0 -1
- package/dist/routes/mcp.js.map +0 -1
- package/dist/routes/oauth.d.ts.map +0 -1
- package/dist/routes/oauth.js.map +0 -1
- package/dist/routes/shared.d.ts.map +0 -1
- package/dist/routes/shared.js.map +0 -1
- package/dist/routes/ui.d.ts.map +0 -1
- package/dist/routes/ui.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/skills.d.ts.map +0 -1
- package/dist/skills.js.map +0 -1
- package/dist/storage/file.d.ts.map +0 -1
- package/dist/storage/file.js.map +0 -1
- package/dist/storage/memory.d.ts.map +0 -1
- package/dist/storage/memory.js.map +0 -1
- package/dist/timeout.d.ts.map +0 -1
- package/dist/timeout.js.map +0 -1
- package/dist/tool-safety.d.ts.map +0 -1
- package/dist/tool-safety.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/ui.d.ts.map +0 -1
- package/dist/ui.js.map +0 -1
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
- package/examples/node/README.md +0 -52
- package/examples/node/src/index.ts +0 -71
- package/src/access-tokens.ts +0 -289
- package/src/activity.ts +0 -234
- package/src/apps-shell.ts +0 -370
- package/src/auth/bearer.ts +0 -61
- package/src/auth/clerk.ts +0 -553
- package/src/auth/downstream-oauth.ts +0 -575
- package/src/call-admission.ts +0 -521
- package/src/catalog-fingerprint.ts +0 -52
- package/src/catalog-limits.ts +0 -11
- package/src/catalog-service.ts +0 -1153
- package/src/catalog.ts +0 -790
- package/src/concurrency.ts +0 -39
- package/src/connector-scope.ts +0 -66
- package/src/connectors/api.ts +0 -159
- package/src/connectors/remote-mcp.ts +0 -1090
- package/src/credentials.ts +0 -428
- package/src/errors.ts +0 -364
- package/src/execute.ts +0 -1406
- package/src/executor-admission.ts +0 -328
- package/src/executor-result.ts +0 -107
- package/src/executors/quickjs-child.ts +0 -172
- package/src/executors/quickjs-protocol.ts +0 -99
- package/src/executors/quickjs-runtime.ts +0 -470
- package/src/executors/quickjs.ts +0 -776
- package/src/favicon.ts +0 -53
- package/src/index.ts +0 -666
- package/src/invocation.ts +0 -655
- package/src/json-schema.ts +0 -11
- package/src/mcp-result.ts +0 -43
- package/src/meta-tools.ts +0 -1607
- package/src/node.ts +0 -206
- package/src/operator-ui/browser.css +0 -423
- package/src/operator-ui/browser.ts +0 -1281
- package/src/operator-ui/generated.ts +0 -4
- package/src/operator-ui/model.ts +0 -111
- package/src/providers/cloudflare.ts +0 -3999
- package/src/providers/linear.ts +0 -302
- package/src/providers/mixpanel.ts +0 -229
- package/src/providers/notion.ts +0 -1879
- package/src/providers/stripe.ts +0 -307
- package/src/registry.ts +0 -1085
- package/src/retired-toolkits.ts +0 -14
- package/src/routes/access-tokens.ts +0 -115
- package/src/routes/activity.ts +0 -215
- package/src/routes/credentials.ts +0 -254
- package/src/routes/mcp.ts +0 -429
- package/src/routes/oauth.ts +0 -322
- package/src/routes/shared.ts +0 -247
- package/src/routes/ui.ts +0 -152
- package/src/server.ts +0 -195
- package/src/skills.ts +0 -271
- package/src/storage/file.ts +0 -123
- package/src/storage/memory.ts +0 -44
- package/src/timeout.ts +0 -84
- package/src/tool-safety.ts +0 -15
- package/src/types.ts +0 -523
- package/src/ui.ts +0 -744
- package/src/validate.ts +0 -387
- package/src/version.ts +0 -7
|
@@ -1,1090 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Client,
|
|
3
|
-
isInputRequiredResult,
|
|
4
|
-
specTypeSchemas,
|
|
5
|
-
StreamableHTTPClientTransport,
|
|
6
|
-
UnauthorizedError,
|
|
7
|
-
} from "@modelcontextprotocol/client";
|
|
8
|
-
import type {
|
|
9
|
-
FetchLike,
|
|
10
|
-
ListToolsResult,
|
|
11
|
-
StandardSchemaV1,
|
|
12
|
-
Tool,
|
|
13
|
-
Transport,
|
|
14
|
-
} from "@modelcontextprotocol/client";
|
|
15
|
-
import { KvOAuthProvider } from "../auth/downstream-oauth.js";
|
|
16
|
-
import { MAX_CATALOG_TOOLS } from "../catalog-limits.js";
|
|
17
|
-
import { ConnectorCallError } from "../errors.js";
|
|
18
|
-
import { CONNECTA_VERSION } from "../version.js";
|
|
19
|
-
import type {
|
|
20
|
-
Connector,
|
|
21
|
-
ConnectorCallAdmissionPolicy,
|
|
22
|
-
ConnectorContext,
|
|
23
|
-
ConnectorStatus,
|
|
24
|
-
ConnectorUsageGuide,
|
|
25
|
-
Logger,
|
|
26
|
-
ToolDef,
|
|
27
|
-
} from "../types.js";
|
|
28
|
-
|
|
29
|
-
export type RemoteMcpAuth =
|
|
30
|
-
| { type: "headers"; headers: Record<string, string> }
|
|
31
|
-
| { type: "oauth" };
|
|
32
|
-
|
|
33
|
-
export type RemoteMcpRedirectPolicy = "none" | "same-origin";
|
|
34
|
-
|
|
35
|
-
export interface RemoteMcpOptions {
|
|
36
|
-
url: string;
|
|
37
|
-
/** Human-readable display name; the connector id remains the address prefix. */
|
|
38
|
-
title?: string;
|
|
39
|
-
description?: string;
|
|
40
|
-
/**
|
|
41
|
-
* Max inline result size (bytes) for this connector's tools before
|
|
42
|
-
* call_tool truncates and stashes the full text for get_result
|
|
43
|
-
* paging. Overrides the deployment's `calls.maxResultBytes`; omit to inherit
|
|
44
|
-
* it. Must be a whole number of bytes >= 1; anything else warns at startup
|
|
45
|
-
* and is ignored.
|
|
46
|
-
*/
|
|
47
|
-
maxResultBytes?: number;
|
|
48
|
-
/** Optional per-runtime downstream call-admission policy. */
|
|
49
|
-
callAdmission?: ConnectorCallAdmissionPolicy;
|
|
50
|
-
/**
|
|
51
|
-
* Optional agent-facing usage guide served by `skills` as
|
|
52
|
-
* `connector:<id>`. A string is markdown; the structured form adds bounded
|
|
53
|
-
* discovery metadata. See `Connector.usageGuide`.
|
|
54
|
-
*/
|
|
55
|
-
usageGuide?: string | ConnectorUsageGuide;
|
|
56
|
-
auth?: RemoteMcpAuth;
|
|
57
|
-
/**
|
|
58
|
-
* Downstream MCP version-negotiation mode. Defaults to `"auto"`, which
|
|
59
|
-
* probes with `server/discover` and falls back to the legacy lifecycle when
|
|
60
|
-
* the response identifies a legacy server. Set `"legacy"` only for a known
|
|
61
|
-
* legacy downstream that cannot safely receive the discovery probe; that
|
|
62
|
-
* path starts directly with the ordinary 2025 `initialize` handshake.
|
|
63
|
-
*/
|
|
64
|
-
versionNegotiation?: "auto" | "legacy";
|
|
65
|
-
/**
|
|
66
|
-
* Downstream HTTP redirect policy. Defaults to `"none"`: every redirect is
|
|
67
|
-
* rejected. `"same-origin"` follows at most five redirects while preserving
|
|
68
|
-
* standard 301/302/303/307/308 method semantics. Cross-origin redirects and
|
|
69
|
-
* HTTPS downgrades are always refused, so credentials never cross the
|
|
70
|
-
* configured request's origin.
|
|
71
|
-
*/
|
|
72
|
-
redirects?: RemoteMcpRedirectPolicy;
|
|
73
|
-
/**
|
|
74
|
-
* Refuse to connect to a non-`https://` `url` at construction (default
|
|
75
|
-
* false). Loopback hosts (`localhost`, `127.0.0.1`, `[::1]`) are always
|
|
76
|
-
* allowed for local development. Off by default, static `headers` credentials
|
|
77
|
-
* over a cleartext connection are warned about but permitted; set this true
|
|
78
|
-
* to make that misconfiguration a hard error instead.
|
|
79
|
-
*/
|
|
80
|
-
requireHttps?: boolean;
|
|
81
|
-
/**
|
|
82
|
-
* Destination for the cleartext-credential warning emitted at construction.
|
|
83
|
-
* Default console.
|
|
84
|
-
*/
|
|
85
|
-
logger?: Logger;
|
|
86
|
-
/**
|
|
87
|
-
* @internal Testing seam. When set, this transport is used instead of the
|
|
88
|
-
* HTTP transport, letting tests point the connector at an in-process MCP
|
|
89
|
-
* server (e.g. via InMemoryTransport). Not part of the public API.
|
|
90
|
-
*/
|
|
91
|
-
_transportFactory?: (ctx: ConnectorContext) => Transport;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* How long a downstream gets to answer the session-termination DELETE before
|
|
96
|
-
* teardown stops waiting. This is a network round-trip budget, deliberately
|
|
97
|
-
* independent of the core's 100 ms caller-facing scope-close window: an
|
|
98
|
-
* already-established cross-internet connection avoids setup, but 50 ms is
|
|
99
|
-
* still too short for an ordinary round trip plus modest provider scheduling.
|
|
100
|
-
* The bounded tail is deferred on runtimes that can keep it alive after the
|
|
101
|
-
* response, while callers continue to wait at most 100 ms.
|
|
102
|
-
*/
|
|
103
|
-
const TERMINATE_SESSION_BUDGET_MS = 1_000;
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Ceiling on the tools one catalog refresh will accumulate while walking
|
|
107
|
-
* `tools/list`.
|
|
108
|
-
*
|
|
109
|
-
* This is the bound that matters, and pages are the wrong dimension to put it
|
|
110
|
-
* in: the *server* picks the page size, so a page ceiling is a tool ceiling
|
|
111
|
-
* multiplied by a number connecta can neither observe in advance nor control.
|
|
112
|
-
* At ten tools a page, 100 pages is 1,000 tools; at a hundred, 10,000 — and
|
|
113
|
-
* connecta's own large-catalog envelope is benchmarked to 100,000 (issue #82).
|
|
114
|
-
* A page ceiling low enough to be a real defense therefore sits *inside* the
|
|
115
|
-
* catalog sizes this product exists to serve. Worse, the common conformant
|
|
116
|
-
* idiom is to advertise a `nextCursor` whenever a page came back full and then
|
|
117
|
-
* serve one empty page to terminate, so a perfectly well-behaved 10,000-tool
|
|
118
|
-
* server paging at 100 spends 101 requests: bound the pages and its entire
|
|
119
|
-
* catalog fails, for doing nothing wrong.
|
|
120
|
-
*
|
|
121
|
-
* So the ceiling goes on accumulated tools — the thing actually held in memory
|
|
122
|
-
* — and it sits at the top of the benchmarked envelope rather than below it.
|
|
123
|
-
* Deliberately the same philosophy as issue #82's discovery-response bounds:
|
|
124
|
-
* cap the bytes a caller can be made to hold, not the number of round trips it
|
|
125
|
-
* took to get them.
|
|
126
|
-
*/
|
|
127
|
-
/**
|
|
128
|
-
* Absolute backstop on `tools/list` pages in one refresh — a runaway guard, not
|
|
129
|
-
* the primary defense.
|
|
130
|
-
*
|
|
131
|
-
* The walk terminates on its own well before this: a cursor handed back twice
|
|
132
|
-
* is a definite loop, two consecutive pages that add no new tools are a server
|
|
133
|
-
* going nowhere, and MAX_CATALOG_TOOLS caps what any of it can accumulate.
|
|
134
|
-
* This exists only so the loop is finite even if a downstream somehow
|
|
135
|
-
* satisfies all three forever on a path with no discovery deadline. Set high
|
|
136
|
-
* enough that no honest server reaches it.
|
|
137
|
-
*/
|
|
138
|
-
const MAX_TOOL_PAGES = 10_000;
|
|
139
|
-
|
|
140
|
-
/** One entry of the SDK's `tools/list` result, before it becomes a ToolDef. */
|
|
141
|
-
type ListedTool = Awaited<ReturnType<Client["listTools"]>>["tools"][number];
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Compatibility concession for hand-rolled servers that serialize
|
|
145
|
-
* end-of-pagination as `null`. Only the cursor is widened; every tool and every
|
|
146
|
-
* other result field still passes through the SDK's pinned schema.
|
|
147
|
-
*/
|
|
148
|
-
const CompatibleListToolsResultSchema: StandardSchemaV1<
|
|
149
|
-
unknown,
|
|
150
|
-
ListToolsResult
|
|
151
|
-
> = {
|
|
152
|
-
"~standard": {
|
|
153
|
-
version: 1,
|
|
154
|
-
vendor: "connecta",
|
|
155
|
-
validate(value) {
|
|
156
|
-
const normalized =
|
|
157
|
-
typeof value === "object" &&
|
|
158
|
-
value !== null &&
|
|
159
|
-
"nextCursor" in value &&
|
|
160
|
-
value.nextCursor === null
|
|
161
|
-
? (() => {
|
|
162
|
-
const copy = { ...value };
|
|
163
|
-
delete copy.nextCursor;
|
|
164
|
-
return copy;
|
|
165
|
-
})()
|
|
166
|
-
: value;
|
|
167
|
-
return specTypeSchemas.ListToolsResult["~standard"].validate(normalized);
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* True for a result-parse failure caused by the page's `nextCursor` itself.
|
|
174
|
-
* `null` is accepted deliberately; other non-string values remain a named
|
|
175
|
-
* downstream nonconformance instead of surfacing as a raw validation dump.
|
|
176
|
-
* Duck-typed rather than `instanceof ZodError`: the SDK may parse with its own
|
|
177
|
-
* zod instance, and cross-instance `instanceof` is a coin flip.
|
|
178
|
-
*/
|
|
179
|
-
function isCursorShapeError(err: unknown): boolean {
|
|
180
|
-
const issues = (err as { issues?: unknown } | null)?.issues;
|
|
181
|
-
if (
|
|
182
|
-
Array.isArray(issues) &&
|
|
183
|
-
issues.some((issue) => {
|
|
184
|
-
const path = (issue as { path?: unknown }).path;
|
|
185
|
-
return Array.isArray(path) && path[0] === "nextCursor";
|
|
186
|
-
})
|
|
187
|
-
) {
|
|
188
|
-
return true;
|
|
189
|
-
}
|
|
190
|
-
// SDK v2 wraps Standard Schema failures in a ProtocolError and preserves the
|
|
191
|
-
// failing path in the message rather than exposing the validator's issues.
|
|
192
|
-
return msg(err).startsWith("Invalid result for tools/list: nextCursor:");
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function msg(err: unknown): string {
|
|
196
|
-
return err instanceof Error ? err.message : String(err);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* End the downstream's session before the connection is torn down.
|
|
201
|
-
*
|
|
202
|
-
* `Client.close()` only unwinds our side — it aborts the transport's controller
|
|
203
|
-
* and fires `onclose`. Spec session termination is a separate DELETE carrying
|
|
204
|
-
* `Mcp-Session-Id`, and without it a stateful provider keeps the session alive
|
|
205
|
-
* until its own (often hour-long) timeout, accumulating abandoned sessions.
|
|
206
|
-
*
|
|
207
|
-
* Ordering is load-bearing: the SDK sends that DELETE on the transport's
|
|
208
|
-
* AbortSignal, so calling this *after* close would abort the request on issue
|
|
209
|
-
* and silently do nothing. Everything else is best-effort — a transport with no
|
|
210
|
-
* `terminateSession` (a custom one, or an older SDK), a downstream that refuses
|
|
211
|
-
* (405 is a legal answer), errors, or never replies all fall through to the
|
|
212
|
-
* close with the session left to age out as it did before.
|
|
213
|
-
*/
|
|
214
|
-
async function terminateSession(
|
|
215
|
-
transport: Transport,
|
|
216
|
-
logger: Logger,
|
|
217
|
-
connectorId: string,
|
|
218
|
-
): Promise<void> {
|
|
219
|
-
const terminate = (
|
|
220
|
-
transport as Transport & { terminateSession?: () => Promise<void> }
|
|
221
|
-
).terminateSession;
|
|
222
|
-
if (typeof terminate !== "function") return;
|
|
223
|
-
// The SDK issues no request at all when no `mcp-session-id` was captured, so
|
|
224
|
-
// a stateless downstream never sees a spurious DELETE.
|
|
225
|
-
const done = Promise.resolve().then(() => terminate.call(transport));
|
|
226
|
-
await new Promise<void>((resolve) => {
|
|
227
|
-
let finished = false;
|
|
228
|
-
const warn = (message: string, error?: unknown) => {
|
|
229
|
-
try {
|
|
230
|
-
if (error === undefined) logger.warn(message);
|
|
231
|
-
else logger.warn(message, error);
|
|
232
|
-
} catch {
|
|
233
|
-
// A diagnostic sink cannot make best-effort teardown observable to the
|
|
234
|
-
// caller in the one way this contract forbids: by replacing its result.
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
const timer = setTimeout(() => {
|
|
238
|
-
if (finished) return;
|
|
239
|
-
finished = true;
|
|
240
|
-
warn(
|
|
241
|
-
`[connecta] connector "${connectorId}" session termination was not ` +
|
|
242
|
-
`acknowledged within ${TERMINATE_SESSION_BUDGET_MS} ms; the ` +
|
|
243
|
-
"downstream may still finish the headers-only DELETE, otherwise " +
|
|
244
|
-
"the session will remain until its provider timeout.",
|
|
245
|
-
);
|
|
246
|
-
resolve();
|
|
247
|
-
}, TERMINATE_SESSION_BUDGET_MS);
|
|
248
|
-
done.then(
|
|
249
|
-
() => {
|
|
250
|
-
if (finished) return;
|
|
251
|
-
finished = true;
|
|
252
|
-
clearTimeout(timer);
|
|
253
|
-
resolve();
|
|
254
|
-
},
|
|
255
|
-
(error) => {
|
|
256
|
-
// The rejection handler stays attached after the timer wins, so an
|
|
257
|
-
// abort or other late failure is consumed without a duplicate warning.
|
|
258
|
-
if (finished) return;
|
|
259
|
-
finished = true;
|
|
260
|
-
clearTimeout(timer);
|
|
261
|
-
warn(
|
|
262
|
-
`[connecta] connector "${connectorId}" session termination was ` +
|
|
263
|
-
"refused or failed; the downstream session may remain until its " +
|
|
264
|
-
"provider timeout.",
|
|
265
|
-
error,
|
|
266
|
-
);
|
|
267
|
-
resolve();
|
|
268
|
-
},
|
|
269
|
-
);
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function isLoopbackHost(hostname: string): boolean {
|
|
274
|
-
return (
|
|
275
|
-
hostname === "localhost" ||
|
|
276
|
-
hostname === "127.0.0.1" ||
|
|
277
|
-
hostname === "[::1]" ||
|
|
278
|
-
hostname === "::1"
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
export const MAX_REMOTE_REDIRECT_HOPS = 5;
|
|
283
|
-
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
284
|
-
const BODY_HEADERS = [
|
|
285
|
-
"content-encoding",
|
|
286
|
-
"content-language",
|
|
287
|
-
"content-length",
|
|
288
|
-
"content-location",
|
|
289
|
-
"content-type",
|
|
290
|
-
"transfer-encoding",
|
|
291
|
-
];
|
|
292
|
-
|
|
293
|
-
export class RemoteMcpRedirectError extends ConnectorCallError {
|
|
294
|
-
constructor(connectorId: string, reason: string) {
|
|
295
|
-
super(
|
|
296
|
-
"connector_call_failed",
|
|
297
|
-
`Connector "${connectorId}" redirect policy rejected the downstream response: ${reason}.`,
|
|
298
|
-
);
|
|
299
|
-
this.name = "RemoteMcpRedirectError";
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function redirectedInit(init: RequestInit, status: number): RequestInit {
|
|
304
|
-
const method = (init.method ?? "GET").toUpperCase();
|
|
305
|
-
const becomesGet =
|
|
306
|
-
(status === 303 && method !== "GET" && method !== "HEAD") ||
|
|
307
|
-
((status === 301 || status === 302) && method === "POST");
|
|
308
|
-
if (!becomesGet) return init;
|
|
309
|
-
const headers = new Headers(init.headers);
|
|
310
|
-
for (const name of BODY_HEADERS) headers.delete(name);
|
|
311
|
-
const redirected = { ...init, method: "GET", headers };
|
|
312
|
-
delete redirected.body;
|
|
313
|
-
return redirected;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Wrap fetch with explicit, bounded redirect handling.
|
|
318
|
-
*
|
|
319
|
-
* The starting URL of each fetch call is trusted by its caller (the configured
|
|
320
|
-
* MCP endpoint, or an OAuth URL discovered by the pinned SDK). Only Location
|
|
321
|
-
* values are policy-controlled here. No rejected target is ever fetched, so
|
|
322
|
-
* arbitrary static header names receive the same protection as Authorization.
|
|
323
|
-
*/
|
|
324
|
-
export function redirectSafeFetch(
|
|
325
|
-
connectorId: string,
|
|
326
|
-
policy: RemoteMcpRedirectPolicy = "none",
|
|
327
|
-
baseFetch: FetchLike = fetch,
|
|
328
|
-
): FetchLike {
|
|
329
|
-
return async (input, initialInit = {}) => {
|
|
330
|
-
let current = new URL(input);
|
|
331
|
-
let init = initialInit;
|
|
332
|
-
const seen = new Set<string>([current.href]);
|
|
333
|
-
let hops = 0;
|
|
334
|
-
|
|
335
|
-
while (true) {
|
|
336
|
-
const response = await baseFetch(current, {
|
|
337
|
-
...init,
|
|
338
|
-
redirect: "manual",
|
|
339
|
-
});
|
|
340
|
-
if (!REDIRECT_STATUSES.has(response.status)) return response;
|
|
341
|
-
|
|
342
|
-
const location = response.headers.get("location");
|
|
343
|
-
await response.body?.cancel().catch(() => {});
|
|
344
|
-
if (!location) {
|
|
345
|
-
throw new RemoteMcpRedirectError(
|
|
346
|
-
connectorId,
|
|
347
|
-
`HTTP ${response.status} carried no Location header`,
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
if (policy === "none") {
|
|
351
|
-
throw new RemoteMcpRedirectError(
|
|
352
|
-
connectorId,
|
|
353
|
-
`HTTP ${response.status} redirects are disabled`,
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
if (hops >= MAX_REMOTE_REDIRECT_HOPS) {
|
|
357
|
-
throw new RemoteMcpRedirectError(
|
|
358
|
-
connectorId,
|
|
359
|
-
`the redirect chain exceeded ${MAX_REMOTE_REDIRECT_HOPS} hops`,
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
let next: URL;
|
|
364
|
-
try {
|
|
365
|
-
next = new URL(location, current);
|
|
366
|
-
} catch {
|
|
367
|
-
throw new RemoteMcpRedirectError(
|
|
368
|
-
connectorId,
|
|
369
|
-
`HTTP ${response.status} carried an invalid Location header`,
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
if (current.protocol === "https:" && next.protocol !== "https:") {
|
|
373
|
-
throw new RemoteMcpRedirectError(
|
|
374
|
-
connectorId,
|
|
375
|
-
"an HTTPS-to-HTTP downgrade is not allowed",
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
if (next.origin !== current.origin) {
|
|
379
|
-
throw new RemoteMcpRedirectError(
|
|
380
|
-
connectorId,
|
|
381
|
-
"a cross-origin redirect is not allowed",
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
if (next.username || next.password) {
|
|
385
|
-
throw new RemoteMcpRedirectError(
|
|
386
|
-
connectorId,
|
|
387
|
-
"a redirect target containing URL credentials is not allowed",
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
if (seen.has(next.href)) {
|
|
391
|
-
throw new RemoteMcpRedirectError(
|
|
392
|
-
connectorId,
|
|
393
|
-
"the redirect chain loops",
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
seen.add(next.href);
|
|
398
|
-
hops++;
|
|
399
|
-
init = redirectedInit(init, response.status);
|
|
400
|
-
current = next;
|
|
401
|
-
}
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
interface ConnectionState {
|
|
406
|
-
client: Client | null;
|
|
407
|
-
transport: Transport | null;
|
|
408
|
-
/**
|
|
409
|
-
* The last complete raw catalog, retained only for this request scope.
|
|
410
|
-
*
|
|
411
|
-
* SDK v2 exposes `toolDefinition` as the public call-time seam for output
|
|
412
|
-
* validation and header mirroring, replacing the v1 private
|
|
413
|
-
* `cacheToolMetadata` reach-through.
|
|
414
|
-
*/
|
|
415
|
-
toolDefinitions: Map<string, Tool>;
|
|
416
|
-
connecting: Promise<void> | null;
|
|
417
|
-
authRequired: boolean;
|
|
418
|
-
provider: KvOAuthProvider | null;
|
|
419
|
-
connectedGeneration: string | null;
|
|
420
|
-
/**
|
|
421
|
-
* One-way latch: set by closeScope and never cleared, so neither a late
|
|
422
|
-
* connect nor a `reset()` can cache a client into a scope that is already
|
|
423
|
-
* gone — that client would have no owner left to close it.
|
|
424
|
-
*/
|
|
425
|
-
closed: boolean;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Proxy a downstream remote MCP server. SDK clients and transports are scoped
|
|
430
|
-
* to one inbound request: reused by calls within a batch/execute_code run, but
|
|
431
|
-
* never carried into a later Cloudflare Worker request. Static-header auth
|
|
432
|
-
* passes headers via requestInit; "oauth" runs the full downstream OAuth flow
|
|
433
|
-
* via KvOAuthProvider.
|
|
434
|
-
*
|
|
435
|
-
* Auth failures degrade the connector to "auth_required" (never crash the
|
|
436
|
-
* server or hide other connectors).
|
|
437
|
-
*/
|
|
438
|
-
export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
439
|
-
// Weak keys ensure a completed request does not leave its SDK client,
|
|
440
|
-
// transport, response bodies, AbortSignals, or connection promise reachable
|
|
441
|
-
// from the isolate singleton. Those are request-bound in Cloudflare Workers.
|
|
442
|
-
const states = new WeakMap<object, ConnectionState>();
|
|
443
|
-
// Closing is terminal even after `states.delete`: a late or future lookup
|
|
444
|
-
// must not recreate an ownerless connection under the ended scope.
|
|
445
|
-
const closedScopes = new WeakSet<object>();
|
|
446
|
-
const isOauth = opts.auth?.type === "oauth";
|
|
447
|
-
const logger = opts.logger ?? console;
|
|
448
|
-
|
|
449
|
-
// Check the destination scheme once at construction: buildTransport (and the
|
|
450
|
-
// SDK's fetch) attach any static credentials to every request, so an http://
|
|
451
|
-
// endpoint sends bearer tokens / API keys in cleartext. Loopback is exempt
|
|
452
|
-
// for local development.
|
|
453
|
-
const destination = new URL(opts.url);
|
|
454
|
-
const insecureDestination =
|
|
455
|
-
destination.protocol !== "https:" && !isLoopbackHost(destination.hostname);
|
|
456
|
-
if (insecureDestination) {
|
|
457
|
-
if (opts.requireHttps) {
|
|
458
|
-
throw new Error(
|
|
459
|
-
`[connecta] connector "${id}" url ${opts.url} is not https:// (and not loopback) — refusing to connect (requireHttps).`,
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
if (opts.auth?.type === "headers") {
|
|
463
|
-
logger.warn(
|
|
464
|
-
`[connecta] connector "${id}" sends static credentials to ${opts.url} over a non-https:// connection — those tokens will be transmitted in cleartext.`,
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/** Typed per-call auth signal; the SDK's UnauthorizedError stays as cause. */
|
|
470
|
-
const authRequiredError = (cause: unknown) =>
|
|
471
|
-
new ConnectorCallError(
|
|
472
|
-
"auth_required",
|
|
473
|
-
`Connector "${id}" requires authorization — call authorize_connector({ connector: "${id}" }) and open the returned URL.`,
|
|
474
|
-
{ cause },
|
|
475
|
-
);
|
|
476
|
-
|
|
477
|
-
class OperatorDisconnectedError extends ConnectorCallError {
|
|
478
|
-
constructor() {
|
|
479
|
-
super(
|
|
480
|
-
"auth_required",
|
|
481
|
-
`Connector "${id}" was disconnected by an operator — explicitly start authorization to reconnect it.`,
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
const operatorDisconnectedError = () => new OperatorDisconnectedError();
|
|
486
|
-
|
|
487
|
-
const scopeEndedError = () =>
|
|
488
|
-
new Error(`Connector "${id}" scope ended during connection.`);
|
|
489
|
-
|
|
490
|
-
const requestOptions = (ctx: ConnectorContext) =>
|
|
491
|
-
ctx.timeoutMs || ctx.signal
|
|
492
|
-
? {
|
|
493
|
-
...(ctx.timeoutMs ? { timeout: ctx.timeoutMs } : {}),
|
|
494
|
-
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
495
|
-
}
|
|
496
|
-
: undefined;
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* One `tools/list` request. The SDK schema is retained wholesale except for
|
|
500
|
-
* accepting `null` as the common, unambiguous end-of-chain spelling. Other
|
|
501
|
-
* cursor shapes still get a useful connector-level diagnosis.
|
|
502
|
-
*/
|
|
503
|
-
const listPage = async (
|
|
504
|
-
client: Client,
|
|
505
|
-
cursor: string | undefined,
|
|
506
|
-
ctx: ConnectorContext,
|
|
507
|
-
) => {
|
|
508
|
-
try {
|
|
509
|
-
return await client.request(
|
|
510
|
-
{
|
|
511
|
-
method: "tools/list",
|
|
512
|
-
...(cursor === undefined ? {} : { params: { cursor } }),
|
|
513
|
-
},
|
|
514
|
-
CompatibleListToolsResultSchema,
|
|
515
|
-
requestOptions(ctx),
|
|
516
|
-
);
|
|
517
|
-
} catch (err) {
|
|
518
|
-
if (!isCursorShapeError(err)) throw err;
|
|
519
|
-
throw new Error(
|
|
520
|
-
`Connector "${id}" returned a tools/list page whose nextCursor is neither a string, null, nor absent — this catalog cannot be walked.`,
|
|
521
|
-
{ cause: err },
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
const stateFor = (ctx: ConnectorContext): ConnectionState => {
|
|
527
|
-
const scope = ctx.requestScope ?? ctx;
|
|
528
|
-
if (closedScopes.has(scope)) throw scopeEndedError();
|
|
529
|
-
let state = states.get(scope);
|
|
530
|
-
if (!state) {
|
|
531
|
-
state = {
|
|
532
|
-
client: null,
|
|
533
|
-
transport: null,
|
|
534
|
-
toolDefinitions: new Map(),
|
|
535
|
-
connecting: null,
|
|
536
|
-
authRequired: false,
|
|
537
|
-
provider: null,
|
|
538
|
-
connectedGeneration: null,
|
|
539
|
-
closed: false,
|
|
540
|
-
};
|
|
541
|
-
states.set(scope, state);
|
|
542
|
-
}
|
|
543
|
-
return state;
|
|
544
|
-
};
|
|
545
|
-
|
|
546
|
-
const getProvider = (
|
|
547
|
-
ctx: ConnectorContext,
|
|
548
|
-
state: ConnectionState,
|
|
549
|
-
): KvOAuthProvider => {
|
|
550
|
-
state.provider ??= new KvOAuthProvider(
|
|
551
|
-
id,
|
|
552
|
-
ctx.storage,
|
|
553
|
-
`${ctx.baseUrl}/oauth/callback/${id}`,
|
|
554
|
-
);
|
|
555
|
-
return state.provider;
|
|
556
|
-
};
|
|
557
|
-
|
|
558
|
-
const newProvider = (ctx: ConnectorContext): KvOAuthProvider =>
|
|
559
|
-
new KvOAuthProvider(
|
|
560
|
-
id,
|
|
561
|
-
ctx.storage,
|
|
562
|
-
`${ctx.baseUrl}/oauth/callback/${id}`,
|
|
563
|
-
);
|
|
564
|
-
|
|
565
|
-
const buildTransport = (
|
|
566
|
-
ctx: ConnectorContext,
|
|
567
|
-
provider: KvOAuthProvider | null,
|
|
568
|
-
): Transport => {
|
|
569
|
-
if (opts._transportFactory) return opts._transportFactory(ctx);
|
|
570
|
-
const url = new URL(opts.url);
|
|
571
|
-
const guardedFetch = redirectSafeFetch(id, opts.redirects);
|
|
572
|
-
if (opts.auth?.type === "oauth") {
|
|
573
|
-
return new StreamableHTTPClientTransport(url, {
|
|
574
|
-
authProvider: provider ?? newProvider(ctx),
|
|
575
|
-
fetch: guardedFetch,
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
const headers =
|
|
579
|
-
opts.auth?.type === "headers" ? opts.auth.headers : undefined;
|
|
580
|
-
return new StreamableHTTPClientTransport(url, {
|
|
581
|
-
...(headers ? { requestInit: { headers } } : {}),
|
|
582
|
-
fetch: guardedFetch,
|
|
583
|
-
});
|
|
584
|
-
};
|
|
585
|
-
|
|
586
|
-
const reset = (state: ConnectionState) => {
|
|
587
|
-
state.client = null;
|
|
588
|
-
state.transport = null;
|
|
589
|
-
state.toolDefinitions.clear();
|
|
590
|
-
state.connecting = null;
|
|
591
|
-
state.authRequired = false;
|
|
592
|
-
state.provider = null;
|
|
593
|
-
state.connectedGeneration = null;
|
|
594
|
-
// `closed` is deliberately not cleared — see ConnectionState.
|
|
595
|
-
};
|
|
596
|
-
|
|
597
|
-
const ensureConnected = async (
|
|
598
|
-
ctx: ConnectorContext,
|
|
599
|
-
state: ConnectionState,
|
|
600
|
-
): Promise<void> => {
|
|
601
|
-
// A 401 after connect is a verdict for the whole request scope, not merely
|
|
602
|
-
// for the one call that observed it. Do not let the still-cached client make
|
|
603
|
-
// a later status or call in the same scope report healthy.
|
|
604
|
-
if (state.authRequired) {
|
|
605
|
-
throw authRequiredError(
|
|
606
|
-
new UnauthorizedError("Downstream authorization is no longer valid."),
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
// Read the OAuth epoch before trusting either a cached client or starting a
|
|
610
|
-
// transport. A disconnected epoch is a durable operator instruction, not
|
|
611
|
-
// merely the absence of credentials: passive status/tool probes must not
|
|
612
|
-
// turn it back into a pending consent flow.
|
|
613
|
-
let oauthGeneration: string | undefined;
|
|
614
|
-
if (isOauth && (state.client || state.connecting)) {
|
|
615
|
-
const provider = getProvider(ctx, state);
|
|
616
|
-
oauthGeneration = await provider.generation();
|
|
617
|
-
if (provider.isOperatorDisconnectedGeneration(oauthGeneration)) {
|
|
618
|
-
const connecting = state.connecting;
|
|
619
|
-
const client = state.client;
|
|
620
|
-
const transport = state.transport;
|
|
621
|
-
reset(state);
|
|
622
|
-
void connecting?.catch(() => {});
|
|
623
|
-
try {
|
|
624
|
-
if (client) await client.close();
|
|
625
|
-
else await transport?.close();
|
|
626
|
-
} catch {
|
|
627
|
-
// The disconnected epoch is authoritative even if local close fails.
|
|
628
|
-
}
|
|
629
|
-
throw operatorDisconnectedError();
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
// Cross-isolate force re-auth: another isolate bumped the KV generation and
|
|
633
|
-
// wiped credentials. This request's cached client still speaks the old
|
|
634
|
-
// token — drop it so the next connect runs against current state.
|
|
635
|
-
if (state.client && oauthGeneration !== undefined && state.connectedGeneration !== null) {
|
|
636
|
-
if (state.closed) throw scopeEndedError();
|
|
637
|
-
if (oauthGeneration !== state.connectedGeneration) {
|
|
638
|
-
reset(state);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
if (state.closed) throw scopeEndedError();
|
|
642
|
-
if (state.client) return;
|
|
643
|
-
if (!state.connecting) {
|
|
644
|
-
let attempt!: Promise<void>;
|
|
645
|
-
attempt = (async () => {
|
|
646
|
-
const ownsAttempt = () =>
|
|
647
|
-
state.connecting === attempt && !state.closed;
|
|
648
|
-
const abandon = async (owner: Client | Transport) => {
|
|
649
|
-
try {
|
|
650
|
-
await owner.close();
|
|
651
|
-
} catch {
|
|
652
|
-
// The attempt is detached either way.
|
|
653
|
-
}
|
|
654
|
-
throw scopeEndedError();
|
|
655
|
-
};
|
|
656
|
-
// Let the assignment immediately below this async IIFE publish
|
|
657
|
-
// `state.connecting = attempt` before ownership is checked. OAuth's
|
|
658
|
-
// generation read naturally yields; unauthenticated transports do not.
|
|
659
|
-
await Promise.resolve();
|
|
660
|
-
// A provider belongs to exactly one connect attempt. A force reset can
|
|
661
|
-
// abandon that attempt while its transport still holds the provider;
|
|
662
|
-
// the replacement must never mutate the abandoned provider's epoch.
|
|
663
|
-
const provider = isOauth ? newProvider(ctx) : null;
|
|
664
|
-
const genAtStart = provider ? await provider.generation() : "";
|
|
665
|
-
if (!ownsAttempt()) throw scopeEndedError();
|
|
666
|
-
if (provider?.isOperatorDisconnectedGeneration(genAtStart)) {
|
|
667
|
-
throw operatorDisconnectedError();
|
|
668
|
-
}
|
|
669
|
-
provider?.captureGeneration(genAtStart);
|
|
670
|
-
// SDK v2 selects its validator by runtime export condition: AJV on
|
|
671
|
-
// Node and @cfworker/json-schema under workerd. The Workers-safe path
|
|
672
|
-
// no longer needs Connecta-specific wiring.
|
|
673
|
-
const c = new Client(
|
|
674
|
-
{ name: "connecta", version: CONNECTA_VERSION },
|
|
675
|
-
{
|
|
676
|
-
versionNegotiation: {
|
|
677
|
-
mode: opts.versionNegotiation ?? "auto",
|
|
678
|
-
},
|
|
679
|
-
// Connecta has no interactive relay. Surface the result manually
|
|
680
|
-
// below as one structured, non-retryable connector failure.
|
|
681
|
-
inputRequired: { autoFulfill: false },
|
|
682
|
-
},
|
|
683
|
-
);
|
|
684
|
-
const t = buildTransport(ctx, provider);
|
|
685
|
-
if (!ownsAttempt()) await abandon(t);
|
|
686
|
-
state.transport = t;
|
|
687
|
-
try {
|
|
688
|
-
await c.connect(t);
|
|
689
|
-
// A probe deadline can end its scope while connect is still in flight.
|
|
690
|
-
// The transport is closed immediately by closeScope; if connect wins
|
|
691
|
-
// that race anyway, close the resulting client rather than
|
|
692
|
-
// resurrecting a session in the detached state object.
|
|
693
|
-
if (!ownsAttempt()) await abandon(c);
|
|
694
|
-
// A force re-auth that landed WHILE we were connecting wiped the
|
|
695
|
-
// credentials this client just bound to. Discard it rather than
|
|
696
|
-
// cache a stale-isolate connection.
|
|
697
|
-
if (provider) {
|
|
698
|
-
const generation = await provider.generation();
|
|
699
|
-
// closeScope can land while the generation read is pending, after
|
|
700
|
-
// connect succeeded but before this client is cached. Discard the
|
|
701
|
-
// client on that side of the await too.
|
|
702
|
-
if (!ownsAttempt()) await abandon(c);
|
|
703
|
-
if (generation !== genAtStart) {
|
|
704
|
-
try {
|
|
705
|
-
await c.close();
|
|
706
|
-
} catch {
|
|
707
|
-
// discarding either way
|
|
708
|
-
}
|
|
709
|
-
throw new UnauthorizedError(
|
|
710
|
-
"Connector was re-authorized during connect; reconnect required.",
|
|
711
|
-
);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
if (!ownsAttempt()) await abandon(c);
|
|
715
|
-
state.client = c;
|
|
716
|
-
state.connectedGeneration = genAtStart;
|
|
717
|
-
state.authRequired = false;
|
|
718
|
-
} catch (err) {
|
|
719
|
-
// Only a real 401/UnauthorizedError means auth is the problem — a
|
|
720
|
-
// network error on an oauth connector must surface as "error", not
|
|
721
|
-
// "auth_required".
|
|
722
|
-
if (err instanceof UnauthorizedError && ownsAttempt()) {
|
|
723
|
-
state.authRequired = true;
|
|
724
|
-
}
|
|
725
|
-
if (err instanceof UnauthorizedError) {
|
|
726
|
-
throw authRequiredError(err);
|
|
727
|
-
}
|
|
728
|
-
throw err;
|
|
729
|
-
} finally {
|
|
730
|
-
// Force reset may have abandoned this attempt and installed a new one
|
|
731
|
-
// in the same request scope. An old completion must not erase the new
|
|
732
|
-
// promise and allow a third concurrent connect.
|
|
733
|
-
if (state.connecting === attempt) state.connecting = null;
|
|
734
|
-
}
|
|
735
|
-
})();
|
|
736
|
-
state.connecting = attempt;
|
|
737
|
-
}
|
|
738
|
-
return state.connecting;
|
|
739
|
-
};
|
|
740
|
-
|
|
741
|
-
const disconnectAuthorization = async (
|
|
742
|
-
ctx: ConnectorContext,
|
|
743
|
-
state: ConnectionState,
|
|
744
|
-
operatorDisconnected = false,
|
|
745
|
-
): Promise<void> => {
|
|
746
|
-
const provider = getProvider(ctx, state);
|
|
747
|
-
// Publish the replacement epoch before waiting on or closing any
|
|
748
|
-
// request-local transport. A hung connect therefore cannot delay the
|
|
749
|
-
// fence, and every late OAuth write stays in the older namespace.
|
|
750
|
-
const connecting = state.connecting;
|
|
751
|
-
try {
|
|
752
|
-
await provider.resetAuthorization(operatorDisconnected);
|
|
753
|
-
} finally {
|
|
754
|
-
// Consume the abandoned connect and close whichever half of the
|
|
755
|
-
// client/transport exists. Reset is unconditional because KV may already
|
|
756
|
-
// be fenced behind a newer epoch after a cleanup error.
|
|
757
|
-
void connecting?.catch(() => {});
|
|
758
|
-
try {
|
|
759
|
-
if (state.client) {
|
|
760
|
-
await state.client.close();
|
|
761
|
-
} else {
|
|
762
|
-
await state.transport?.close();
|
|
763
|
-
}
|
|
764
|
-
} catch {
|
|
765
|
-
// best-effort; the state is discarded either way
|
|
766
|
-
}
|
|
767
|
-
reset(state);
|
|
768
|
-
}
|
|
769
|
-
};
|
|
770
|
-
|
|
771
|
-
const connector: Connector = {
|
|
772
|
-
id,
|
|
773
|
-
...(opts.title !== undefined ? { title: opts.title } : {}),
|
|
774
|
-
kind: "mcp",
|
|
775
|
-
...(opts.description !== undefined
|
|
776
|
-
? { description: opts.description }
|
|
777
|
-
: {}),
|
|
778
|
-
...(opts.maxResultBytes !== undefined
|
|
779
|
-
? { maxResultBytes: opts.maxResultBytes }
|
|
780
|
-
: {}),
|
|
781
|
-
...(opts.callAdmission !== undefined
|
|
782
|
-
? { callAdmission: opts.callAdmission }
|
|
783
|
-
: {}),
|
|
784
|
-
...(opts.usageGuide !== undefined ? { usageGuide: opts.usageGuide } : {}),
|
|
785
|
-
|
|
786
|
-
// `tools/list` is cursor-paginated: the server chooses the page size and
|
|
787
|
-
// signals "there is more" with a `nextCursor`, which the SDK's
|
|
788
|
-
// Client.listTools() returns without following. Collect the whole chain
|
|
789
|
-
// here, because a half-collected catalog is indistinguishable from a small
|
|
790
|
-
// one — later-page tools would simply appear not to exist, unsearchable and
|
|
791
|
-
// unaddressable, with nothing anywhere saying why.
|
|
792
|
-
//
|
|
793
|
-
// All pages ride the one request-scoped client already connected above, and
|
|
794
|
-
// the accumulator is returned rather than stored: a cursor is opaque and
|
|
795
|
-
// session-bound, so nothing here may outlive this call.
|
|
796
|
-
async listTools(ctx) {
|
|
797
|
-
const state = stateFor(ctx);
|
|
798
|
-
await ensureConnected(ctx, state);
|
|
799
|
-
// Bind the client once so the whole walk provably rides one session — a
|
|
800
|
-
// cursor is only meaningful to the connection that issued it, and a
|
|
801
|
-
// re-read could in principle pick up a different one. It is NOT guarding
|
|
802
|
-
// against closeScope nulling state.client mid-loop: closeScope sets
|
|
803
|
-
// `closed` and nulls `client` in one synchronous run, and the loop
|
|
804
|
-
// re-checks `closed` before every page, so the nulled client is
|
|
805
|
-
// unreachable from here.
|
|
806
|
-
const client = state.client!;
|
|
807
|
-
// Raw SDK tools, not ToolDefs: the metadata re-prime below needs fields
|
|
808
|
-
// (task support) that a ToolDef deliberately does not carry.
|
|
809
|
-
const listed: ListedTool[] = [];
|
|
810
|
-
const names = new Set<string>();
|
|
811
|
-
const spent = new Set<string>();
|
|
812
|
-
let cursor: string | undefined;
|
|
813
|
-
/** Consecutive pages that advertised a successor but added nothing. */
|
|
814
|
-
let barren = 0;
|
|
815
|
-
let complete = false;
|
|
816
|
-
try {
|
|
817
|
-
for (let page = 0; page < MAX_TOOL_PAGES; page++) {
|
|
818
|
-
// The scope can end between pages (probe timeout, teardown). Stop
|
|
819
|
-
// rather than keep paging into a transport that is being closed.
|
|
820
|
-
if (state.closed) throw scopeEndedError();
|
|
821
|
-
// A discovery deadline uses the same signal for the whole chain.
|
|
822
|
-
// Check it before issuing each page as well as passing it to the
|
|
823
|
-
// in-flight SDK request, so expiry never starts one more round trip.
|
|
824
|
-
if (ctx.signal?.aborted) {
|
|
825
|
-
throw ctx.signal.reason instanceof Error
|
|
826
|
-
? ctx.signal.reason
|
|
827
|
-
: new Error(`Connector "${id}" catalog deadline expired.`);
|
|
828
|
-
}
|
|
829
|
-
// Page one sends no params at all, so a non-paginated server sees
|
|
830
|
-
// exactly the request it saw before pagination existed.
|
|
831
|
-
const res = await listPage(client, cursor, ctx);
|
|
832
|
-
let added = 0;
|
|
833
|
-
for (const t of res.tools) {
|
|
834
|
-
// First page wins. An unstable cursor can serve the same tool on
|
|
835
|
-
// two pages — a duplicate would inflate `toolCount`, double the
|
|
836
|
-
// `search_tools` row, and churn catalog persistence.
|
|
837
|
-
if (names.has(t.name)) continue;
|
|
838
|
-
names.add(t.name);
|
|
839
|
-
listed.push(t);
|
|
840
|
-
added++;
|
|
841
|
-
}
|
|
842
|
-
// Pagination ends when `nextCursor` is absent or null — never merely
|
|
843
|
-
// falsy. Empty string is present and means "keep going".
|
|
844
|
-
const next = res.nextCursor;
|
|
845
|
-
if (next === undefined || next === null) {
|
|
846
|
-
complete = true;
|
|
847
|
-
break;
|
|
848
|
-
}
|
|
849
|
-
// A page that adds nothing and still claims a successor made no
|
|
850
|
-
// progress. Allow exactly one: the widespread idiom is to advertise
|
|
851
|
-
// a cursor whenever a page came back full and then serve one empty
|
|
852
|
-
// page to terminate. Two in a row is a downstream going nowhere.
|
|
853
|
-
if (added === 0 && ++barren > 1) {
|
|
854
|
-
throw new Error(
|
|
855
|
-
`Connector "${id}" returned two consecutive tools/list pages that added no tools and still advertised another — the catalog is not advancing.`,
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
if (added > 0) barren = 0;
|
|
859
|
-
// A cursor handed back a second time is a loop, not a slow server.
|
|
860
|
-
if (spent.has(next)) {
|
|
861
|
-
throw new Error(
|
|
862
|
-
`Connector "${id}" handed back a tools/list cursor it had already issued — the pagination chain loops.`,
|
|
863
|
-
);
|
|
864
|
-
}
|
|
865
|
-
// Checked here rather than on arrival: this bounds what a *walk* may
|
|
866
|
-
// accumulate; a one-page server was always free to send its page.
|
|
867
|
-
if (listed.length > MAX_CATALOG_TOOLS) {
|
|
868
|
-
throw new Error(
|
|
869
|
-
`Connector "${id}" advertised further tools/list pages past ${listed.length} tools, over the ${MAX_CATALOG_TOOLS}-tool ceiling one catalog refresh will collect.`,
|
|
870
|
-
);
|
|
871
|
-
}
|
|
872
|
-
// Opaque by contract: handed straight back, never parsed, rewritten,
|
|
873
|
-
// or persisted.
|
|
874
|
-
spent.add(next);
|
|
875
|
-
cursor = next;
|
|
876
|
-
}
|
|
877
|
-
} catch (err) {
|
|
878
|
-
// A grant can be revoked after connect and after any earlier page.
|
|
879
|
-
// Classify that exactly like connect-time and call-time authorization
|
|
880
|
-
// failures, and latch it for the rest of this request scope.
|
|
881
|
-
if (err instanceof UnauthorizedError) {
|
|
882
|
-
if (state.client === client) state.authRequired = true;
|
|
883
|
-
throw authRequiredError(err);
|
|
884
|
-
}
|
|
885
|
-
throw err;
|
|
886
|
-
}
|
|
887
|
-
// Fail the refresh outright. Returning what we have would publish a
|
|
888
|
-
// partial catalog that looks complete; throwing lets the registry keep
|
|
889
|
-
// serving the last complete one via its stale fallback.
|
|
890
|
-
if (!complete) {
|
|
891
|
-
throw new Error(
|
|
892
|
-
`Connector "${id}" kept advertising more tools/list pages after ${MAX_TOOL_PAGES} — refusing to page further.`,
|
|
893
|
-
);
|
|
894
|
-
}
|
|
895
|
-
// Publish definitions only after the full walk succeeds. A later-page
|
|
896
|
-
// failure must not leave a partial validation/header view behind.
|
|
897
|
-
state.toolDefinitions = new Map(listed.map((tool) => [tool.name, tool]));
|
|
898
|
-
return listed.map((t) => ({
|
|
899
|
-
name: t.name,
|
|
900
|
-
...(t.description !== undefined ? { description: t.description } : {}),
|
|
901
|
-
...(t.inputSchema !== undefined
|
|
902
|
-
? {
|
|
903
|
-
inputSchema: t.inputSchema as NonNullable<
|
|
904
|
-
ToolDef["inputSchema"]
|
|
905
|
-
>,
|
|
906
|
-
}
|
|
907
|
-
: {}),
|
|
908
|
-
...(t.outputSchema !== undefined
|
|
909
|
-
? {
|
|
910
|
-
outputSchema: t.outputSchema as NonNullable<
|
|
911
|
-
ToolDef["outputSchema"]
|
|
912
|
-
>,
|
|
913
|
-
}
|
|
914
|
-
: {}),
|
|
915
|
-
...(t.annotations !== undefined
|
|
916
|
-
? {
|
|
917
|
-
annotations: t.annotations as NonNullable<
|
|
918
|
-
ToolDef["annotations"]
|
|
919
|
-
>,
|
|
920
|
-
}
|
|
921
|
-
: {}),
|
|
922
|
-
}));
|
|
923
|
-
},
|
|
924
|
-
|
|
925
|
-
async callTool(name, args, ctx) {
|
|
926
|
-
const state = stateFor(ctx);
|
|
927
|
-
await ensureConnected(ctx, state);
|
|
928
|
-
const client = state.client!;
|
|
929
|
-
try {
|
|
930
|
-
const toolDefinition = state.toolDefinitions.get(name);
|
|
931
|
-
if (toolDefinition?.execution?.taskSupport === "required") {
|
|
932
|
-
throw new Error(
|
|
933
|
-
`Tool "${name}" requires task-based execution, which Connecta does not support.`,
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
const result = await client.callTool(
|
|
937
|
-
{
|
|
938
|
-
name,
|
|
939
|
-
arguments: (args ?? {}) as Record<string, unknown>,
|
|
940
|
-
},
|
|
941
|
-
{
|
|
942
|
-
...requestOptions(ctx),
|
|
943
|
-
allowInputRequired: true,
|
|
944
|
-
...(toolDefinition ? { toolDefinition } : {}),
|
|
945
|
-
},
|
|
946
|
-
);
|
|
947
|
-
if (isInputRequiredResult(result)) {
|
|
948
|
-
throw new ConnectorCallError(
|
|
949
|
-
"input_required_unsupported",
|
|
950
|
-
`Connector "${id}" returned input_required for "${name}". ` +
|
|
951
|
-
"Connecta cannot relay multi-round-trip input yet; this " +
|
|
952
|
-
"capability is gated pending real host and downstream adoption.",
|
|
953
|
-
);
|
|
954
|
-
}
|
|
955
|
-
return result;
|
|
956
|
-
} catch (err) {
|
|
957
|
-
// A grant revoked after connect surfaces here, not in ensureConnected.
|
|
958
|
-
if (err instanceof UnauthorizedError) {
|
|
959
|
-
if (state.client === client) state.authRequired = true;
|
|
960
|
-
throw authRequiredError(err);
|
|
961
|
-
}
|
|
962
|
-
throw err;
|
|
963
|
-
}
|
|
964
|
-
},
|
|
965
|
-
|
|
966
|
-
async closeScope(ctx) {
|
|
967
|
-
const scope = ctx.requestScope ?? ctx;
|
|
968
|
-
// Tombstone before any lookup or await. This also makes close-before-use
|
|
969
|
-
// terminal rather than allowing the scope to spring into existence later.
|
|
970
|
-
closedScopes.add(scope);
|
|
971
|
-
const state = states.get(scope);
|
|
972
|
-
if (!state) return;
|
|
973
|
-
|
|
974
|
-
// Delete before awaiting: a duplicate teardown is a no-op.
|
|
975
|
-
states.delete(scope);
|
|
976
|
-
state.closed = true;
|
|
977
|
-
const client = state.client;
|
|
978
|
-
const transport = state.transport;
|
|
979
|
-
state.client = null;
|
|
980
|
-
state.transport = null;
|
|
981
|
-
state.toolDefinitions.clear();
|
|
982
|
-
state.connecting = null;
|
|
983
|
-
state.authRequired = false;
|
|
984
|
-
state.connectedGeneration = null;
|
|
985
|
-
|
|
986
|
-
// Ask the downstream to drop its session first — closing only aborts our
|
|
987
|
-
// side, and the DELETE that frees the server's rides on the very
|
|
988
|
-
// AbortSignal the close is about to trip.
|
|
989
|
-
if (transport) await terminateSession(transport, ctx.logger, id);
|
|
990
|
-
|
|
991
|
-
// Client.close() owns its connected transport. During an unfinished or
|
|
992
|
-
// failed connect there is no cached client yet, so close the transport
|
|
993
|
-
// directly to abort/release that half-open session.
|
|
994
|
-
if (client) {
|
|
995
|
-
await client.close();
|
|
996
|
-
} else {
|
|
997
|
-
await transport?.close();
|
|
998
|
-
}
|
|
999
|
-
},
|
|
1000
|
-
|
|
1001
|
-
async status(ctx): Promise<ConnectorStatus> {
|
|
1002
|
-
const state = stateFor(ctx);
|
|
1003
|
-
try {
|
|
1004
|
-
await ensureConnected(ctx, state);
|
|
1005
|
-
return { state: "ok" };
|
|
1006
|
-
} catch (err) {
|
|
1007
|
-
if (state.authRequired) {
|
|
1008
|
-
const url = await getProvider(ctx, state).pendingAuthorizationUrl();
|
|
1009
|
-
return {
|
|
1010
|
-
state: "auth_required",
|
|
1011
|
-
...(url !== undefined ? { authorizationUrl: url } : {}),
|
|
1012
|
-
message: "Authorization required — open the URL to connect.",
|
|
1013
|
-
};
|
|
1014
|
-
}
|
|
1015
|
-
if (err instanceof OperatorDisconnectedError) {
|
|
1016
|
-
return { state: "auth_required", message: err.message };
|
|
1017
|
-
}
|
|
1018
|
-
return { state: "error", message: msg(err) };
|
|
1019
|
-
}
|
|
1020
|
-
},
|
|
1021
|
-
|
|
1022
|
-
async finishAuth(code, ctx, callbackParams) {
|
|
1023
|
-
const state = stateFor(ctx);
|
|
1024
|
-
const provider = getProvider(ctx, state);
|
|
1025
|
-
// verifyState ran on this request-scoped provider first and captured the
|
|
1026
|
-
// pending flow's generation. If force reset races the exchange, any late
|
|
1027
|
-
// token write remains tagged with that older generation and is unreadable.
|
|
1028
|
-
const t = (state.transport ??
|
|
1029
|
-
buildTransport(ctx, provider)) as StreamableHTTPClientTransport;
|
|
1030
|
-
if (callbackParams !== undefined) {
|
|
1031
|
-
await t.finishAuth(callbackParams);
|
|
1032
|
-
} else {
|
|
1033
|
-
await t.finishAuth(code);
|
|
1034
|
-
}
|
|
1035
|
-
await provider.clearPending();
|
|
1036
|
-
// Reset so the next use reconnects with the freshly stored tokens.
|
|
1037
|
-
reset(state);
|
|
1038
|
-
},
|
|
1039
|
-
};
|
|
1040
|
-
|
|
1041
|
-
if (opts.auth?.type === "oauth") {
|
|
1042
|
-
connector.verifyState = async (oauthState, ctx) => {
|
|
1043
|
-
const state = stateFor(ctx);
|
|
1044
|
-
return getProvider(ctx, state).verifyState(oauthState);
|
|
1045
|
-
};
|
|
1046
|
-
|
|
1047
|
-
connector.disconnectAuth = async (ctx) => {
|
|
1048
|
-
await disconnectAuthorization(ctx, stateFor(ctx), true);
|
|
1049
|
-
};
|
|
1050
|
-
|
|
1051
|
-
connector.startAuth = async (ctx, startOpts) => {
|
|
1052
|
-
const state = stateFor(ctx);
|
|
1053
|
-
const p = getProvider(ctx, state);
|
|
1054
|
-
if (startOpts?.force || (await p.operatorDisconnected())) {
|
|
1055
|
-
await disconnectAuthorization(ctx, state);
|
|
1056
|
-
} else {
|
|
1057
|
-
// A consent URL already outstanding? Re-issue it rather than re-running
|
|
1058
|
-
// the SDK flow, which would overwrite the PKCE verifier and invalidate
|
|
1059
|
-
// the URL the operator may be mid-consent on.
|
|
1060
|
-
const pending = await p.pendingAuthorizationUrl();
|
|
1061
|
-
if (pending) {
|
|
1062
|
-
return {
|
|
1063
|
-
state: "auth_required",
|
|
1064
|
-
authorizationUrl: pending,
|
|
1065
|
-
message: "Authorization required — open the URL to connect.",
|
|
1066
|
-
};
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
try {
|
|
1070
|
-
await ensureConnected(ctx, state);
|
|
1071
|
-
return {
|
|
1072
|
-
state: "ok",
|
|
1073
|
-
message: "Already authorized — connection is healthy.",
|
|
1074
|
-
};
|
|
1075
|
-
} catch (err) {
|
|
1076
|
-
if (state.authRequired) {
|
|
1077
|
-
const authorizationUrl = await p.pendingAuthorizationUrl();
|
|
1078
|
-
return {
|
|
1079
|
-
state: "auth_required",
|
|
1080
|
-
...(authorizationUrl !== undefined ? { authorizationUrl } : {}),
|
|
1081
|
-
message: "Authorization required — open the URL to connect.",
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
|
-
return { state: "error", message: msg(err) };
|
|
1085
|
-
}
|
|
1086
|
-
};
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
return connector;
|
|
1090
|
-
}
|