@zackbart/connecta 0.38.0 → 0.40.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.
Files changed (180) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +3 -2
  3. package/bin/connecta.mjs +4 -8
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +6 -4
  6. package/dist/agent-routes.js +231 -0
  7. package/dist/auth/downstream-oauth/consent.js +519 -0
  8. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  9. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  10. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  11. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  12. package/dist/auth/downstream-oauth.js +66 -2382
  13. package/dist/auth/native.js +37 -0
  14. package/dist/auth/oauth-grant.js +13 -0
  15. package/dist/auth/static-oauth.js +153 -131
  16. package/dist/auth-elicitation.js +7 -1
  17. package/dist/authorization-handoff.js +11 -5
  18. package/dist/catalog-cache.js +219 -59
  19. package/dist/catalog-policy.js +18 -0
  20. package/dist/catalog-service.js +284 -47
  21. package/dist/catalog.js +18 -2
  22. package/dist/config.d.ts +16 -11
  23. package/dist/config.js +9 -6
  24. package/dist/connector-caller.d.ts +1 -1
  25. package/dist/connector-health.js +59 -0
  26. package/dist/connector-status.js +117 -0
  27. package/dist/connectors/api-connector.d.ts +7 -2
  28. package/dist/connectors/api-connector.js +7 -4
  29. package/dist/connectors/negotiation-cache.js +22 -4
  30. package/dist/connectors/remote-mcp/call.js +164 -0
  31. package/dist/connectors/remote-mcp/catalog.js +278 -0
  32. package/dist/connectors/remote-mcp/config.js +263 -0
  33. package/dist/connectors/remote-mcp/connection.js +682 -0
  34. package/dist/connectors/remote-mcp/errors.js +488 -0
  35. package/dist/connectors/remote-mcp/oauth.js +473 -0
  36. package/dist/connectors/remote-mcp/resources.js +473 -0
  37. package/dist/connectors/remote-mcp/runtime.js +1 -0
  38. package/dist/connectors/remote-mcp/transport.js +555 -0
  39. package/dist/connectors/remote-mcp.js +62 -2863
  40. package/dist/credential-contract.d.ts +23 -0
  41. package/dist/credential-reads.js +17 -0
  42. package/dist/credential-verification.js +53 -0
  43. package/dist/credentials.d.ts +7 -0
  44. package/dist/credentials.js +60 -25
  45. package/dist/describe-config.d.ts +12 -2
  46. package/dist/describe-config.js +2 -0
  47. package/dist/discovery-health.js +199 -0
  48. package/dist/downstream-input.js +2 -0
  49. package/dist/errors.d.ts +55 -17
  50. package/dist/errors.js +95 -41
  51. package/dist/execute.js +152 -114
  52. package/dist/executor-result.js +3 -1
  53. package/dist/guest-functions.js +83 -0
  54. package/dist/guest-runtime.js +60 -19
  55. package/dist/guest-types.d.ts +19 -5
  56. package/dist/http/argument-problems.js +35 -0
  57. package/dist/http/errors.js +5 -4
  58. package/dist/http/fields.js +192 -13
  59. package/dist/http/hatch-response.js +73 -0
  60. package/dist/http/hatch.js +88 -23
  61. package/dist/http/oauth.js +3 -2
  62. package/dist/http/provider.js +32 -4
  63. package/dist/http/safety.js +8 -0
  64. package/dist/http/tools.js +44 -23
  65. package/dist/index.d.ts +1 -1
  66. package/dist/index.js +9 -2
  67. package/dist/invocation.js +26 -8
  68. package/dist/membership.d.ts +13 -5
  69. package/dist/membership.js +34 -2
  70. package/dist/meta-tools.js +54 -31
  71. package/dist/native-recovery.js +4 -16
  72. package/dist/native-tools.js +5 -2
  73. package/dist/oauth-sealing.js +10 -0
  74. package/dist/operation-vocabulary.js +135 -13
  75. package/dist/operator-record.d.ts +2 -2
  76. package/dist/operator-record.js +1 -0
  77. package/dist/operator-ui/contract.d.ts +10 -0
  78. package/dist/operator-ui/generated.js +2 -2
  79. package/dist/operator-ui/model.d.ts +22 -1
  80. package/dist/provider-call-facts.js +6 -2
  81. package/dist/providers/_shared/rest/operation-index.js +48 -50
  82. package/dist/providers/_shared/rest/value-safety.js +80 -0
  83. package/dist/providers/breeze/api.js +7 -1
  84. package/dist/providers/breeze/skill.generated.js +1 -1
  85. package/dist/providers/breeze/tools.js +4 -3
  86. package/dist/providers/ccb/api.js +2 -1
  87. package/dist/providers/ccb/index.js +5 -0
  88. package/dist/providers/ccb/skill.generated.js +1 -1
  89. package/dist/providers/cloudflare/api.js +8 -1
  90. package/dist/providers/cloudflare/skill.generated.js +1 -1
  91. package/dist/providers/docs/lib/schemas.js +24 -22
  92. package/dist/providers/docs/safety.js +2 -0
  93. package/dist/providers/docs/skill.generated.js +1 -1
  94. package/dist/providers/docs/tools.js +2 -4
  95. package/dist/providers/drive/api.js +1 -0
  96. package/dist/providers/drive/lib/contracts.js +8 -5
  97. package/dist/providers/drive/skill.generated.js +1 -1
  98. package/dist/providers/drive/tools.js +11 -16
  99. package/dist/providers/forms/api.js +1 -0
  100. package/dist/providers/forms/skill.generated.js +1 -1
  101. package/dist/providers/forms/tools.js +10 -7
  102. package/dist/providers/github/api.js +1 -0
  103. package/dist/providers/github/lib/discovery.js +1 -1
  104. package/dist/providers/github/skill.generated.js +1 -1
  105. package/dist/providers/github/tools.js +61 -17
  106. package/dist/providers/gmail/lib/schemas.js +60 -50
  107. package/dist/providers/gmail/skill.generated.js +1 -1
  108. package/dist/providers/gmail/tools.js +7 -5
  109. package/dist/providers/granola/index.js +2 -0
  110. package/dist/providers/granola/skill.generated.js +1 -1
  111. package/dist/providers/overflow/api.js +7 -1
  112. package/dist/providers/overflow/skill.generated.js +1 -1
  113. package/dist/providers/planning-center/api.js +3 -1
  114. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  115. package/dist/providers/planning-center/skill.generated.js +1 -1
  116. package/dist/providers/quickbooks/safety.js +80 -39
  117. package/dist/providers/quickbooks/skill.generated.js +1 -1
  118. package/dist/providers/sheets/api.js +1 -0
  119. package/dist/providers/sheets/skill.generated.js +1 -1
  120. package/dist/providers/sheets/tools.js +13 -14
  121. package/dist/providers/slides/skill.generated.js +1 -1
  122. package/dist/providers/slides/tools.js +15 -13
  123. package/dist/providers/stripe/skill.generated.js +1 -1
  124. package/dist/providers/tithely/api.js +3 -1
  125. package/dist/providers/tithely/skill.generated.js +1 -1
  126. package/dist/providers/vercel/api.js +1 -1
  127. package/dist/providers/vercel/skill.generated.js +1 -1
  128. package/dist/providers/vercel/tools.js +4 -3
  129. package/dist/registry.d.ts +10 -4
  130. package/dist/registry.js +41 -13
  131. package/dist/routes/credentials.js +129 -60
  132. package/dist/routes/dispatch.js +5 -1
  133. package/dist/routes/mcp.js +30 -14
  134. package/dist/routes/operator.js +2 -2
  135. package/dist/routes/shared.d.ts +6 -1
  136. package/dist/routes/shared.js +7 -1
  137. package/dist/routes/ui-config.js +2 -2
  138. package/dist/routes/ui-data.js +126 -20
  139. package/dist/routes/ui.js +16 -0
  140. package/dist/runtime/call.js +7 -2
  141. package/dist/runtime/native-storage.js +12 -0
  142. package/dist/runtime/request-reads.js +207 -0
  143. package/dist/runtime/request-scope.js +16 -0
  144. package/dist/runtime/storage.js +12 -0
  145. package/dist/skills.js +37 -20
  146. package/dist/storage/keys.js +53 -3
  147. package/dist/storage/memory.js +8 -2
  148. package/dist/storage/sql-native.js +53 -1
  149. package/dist/storage/sql.js +12 -2
  150. package/dist/storage-digest.js +32 -0
  151. package/dist/timeout.js +8 -0
  152. package/dist/types.d.ts +33 -0
  153. package/dist/usage-guide.js +16 -35
  154. package/dist/validate.d.ts +3 -0
  155. package/dist/validate.js +220 -158
  156. package/dist/version.d.ts +1 -1
  157. package/dist/version.js +1 -1
  158. package/dist/write-outcome.js +0 -3
  159. package/documentation/README.md +13 -13
  160. package/documentation/architecture.md +385 -20
  161. package/documentation/auth.md +233 -64
  162. package/documentation/code-mode.md +122 -39
  163. package/documentation/deploying.md +6 -3
  164. package/documentation/integrating.md +16 -2
  165. package/documentation/meta-tools.md +240 -49
  166. package/documentation/migration-0.33.md +5 -1
  167. package/documentation/migration-0.34.md +3 -1
  168. package/documentation/operator-tests.md +1 -1
  169. package/documentation/operator-ui.md +105 -12
  170. package/documentation/pool-membership-upgrade.md +44 -0
  171. package/documentation/providers.md +105 -0
  172. package/examples/worker/AGENTS.md +10 -10
  173. package/examples/worker/README.md +4 -3
  174. package/examples/worker/src/connecta.config.ts +1 -0
  175. package/guides/connecting-agents.md +2 -1
  176. package/guides/how-it-works.md +4 -2
  177. package/package.json +1 -1
  178. package/templates/node/AGENTS.md +13 -12
  179. package/templates/node/package.json +1 -1
  180. package/templates/node/src/connecta.config.ts +1 -0
@@ -0,0 +1,488 @@
1
+ // Error mapping at the MCP boundary: what of an SDK, runtime or downstream
2
+ // error survives, how it is classified, and the connector's own error classes.
3
+ import { routePhrases } from "../../agent-routes.js";
4
+ import { authFlow, authCall, authMethod } from "../../auth/native.js";
5
+ import { AuthorizationServerMismatchError, InsecureTokenEndpointError, InsufficientScopeError, IssuerMismatchError, MissingRequiredClientCapabilityError, OAuthClientFlowError, OAuthError, ProtocolError, RegistrationRejectedError, ResourceNotFoundError, SdkError, SdkErrorCode, SdkHttpError, SseError, UnsupportedProtocolVersionError, UrlElicitationRequiredError, isJSONRPCErrorResponse, isJSONRPCNotification, UnauthorizedError, } from "@modelcontextprotocol/client";
6
+ import { redactSentSecrets, requestSentSecrets } from "../../sent-secrets.js";
7
+ import { boundedEchoText, ConnectorCallError, msg, networkErrorCode, resendAdviceOf, unavailableCallError, WithheldTextError, withResendAdvice, } from "../../errors.js";
8
+ import { attachFailureFacts, carryFailureFacts, errorLabel, failureRecord, labelErrorClass, OAUTH_ERROR_CODES, } from "../../operator-record.js";
9
+ /**
10
+ * An `authorize_connector` start message for a failure: its text, which has
11
+ * passed the MCP and OAuth boundaries, and the origin and errno connecta
12
+ * derived for it, which a message alone would drop.
13
+ */
14
+ /** An authorization start's failure for authorize_connector: no write's, so its advice stays. */
15
+ export function startMessage(err) {
16
+ const details = err instanceof ConnectorCallError ? err.details : undefined;
17
+ const facts = [details?.host, details?.code].filter(Boolean).join(", ");
18
+ return withResendAdvice(facts ? `${msg(err)} (${facts})` : msg(err), resendAdviceOf(err));
19
+ }
20
+ /**
21
+ * One Promise step of an Effect program. It fails with exactly what the
22
+ * promise rejected with, never a wrapper, because callers check the SDK's and
23
+ * connecta's own error classes. `evaluate` takes no signal, so the step
24
+ * allocates no AbortController.
25
+ */
26
+ export function promised(evaluate) {
27
+ return authCall(evaluate, []);
28
+ }
29
+ /** A retained cause chain must not smuggle an SDK payload past the boundary. */
30
+ export function hasSdkPayload(error) {
31
+ const seen = new Set();
32
+ for (let current = error; current instanceof Error && !seen.has(current); current = current.cause) {
33
+ if (current instanceof ProtocolError || current instanceof SdkError)
34
+ return true;
35
+ seen.add(current);
36
+ }
37
+ return false;
38
+ }
39
+ /** Cancellation can bypass an inner SDK catch and return the signal's reason. */
40
+ export function payloadFree(run) {
41
+ return authMethod((...args) => authFlow(function* () {
42
+ try {
43
+ return (yield authCall(run, [...args]));
44
+ }
45
+ catch (error) {
46
+ throw hasSdkPayload(error) ? carryFailureFacts(error, downstreamCallError(error)) : error;
47
+ }
48
+ }));
49
+ }
50
+ /** Both the SDK's auth signal and its classified form invalidate a live client. */
51
+ export function requiresAuthorization(error) {
52
+ return (error instanceof UnauthorizedError ||
53
+ (error instanceof ConnectorCallError &&
54
+ (error.code === "auth_required" || error.code === "downstream_oauth_required")));
55
+ }
56
+ /** Classify SDK/runtime facts, dropping every SDK payload and cause chain. */
57
+ export function downstreamCallError(error, httpStatus, wait, oauthStep, secrets) {
58
+ if (error instanceof ConnectorCallError) {
59
+ return hasSdkPayload(error.cause) ? carryFailureFacts(error, withheldAs(error.message, error)) : error;
60
+ }
61
+ if (error instanceof WithheldTextError || error instanceof UnauthorizedError)
62
+ return error;
63
+ const network = !(error instanceof SdkError) && !(error instanceof OAuthClientFlowError) ? networkErrorCode(error) : undefined;
64
+ if (network) {
65
+ return network === "timeout" || network === "ETIMEDOUT" || network.endsWith("_TIMEOUT")
66
+ ? new ConnectorCallError("timeout", "The downstream request timed out.")
67
+ : unavailableCallError(error);
68
+ }
69
+ const status = error instanceof SdkHttpError || error instanceof RegistrationRejectedError ? error.status : httpStatus;
70
+ if (error instanceof InsufficientScopeError ||
71
+ (status === 403 && !(error instanceof RegistrationRejectedError)) ||
72
+ (error instanceof OAuthError && ["insufficient_scope", "invalid_scope", "access_denied"].includes(error.code))) {
73
+ return new ConnectorCallError("provider_permission_denied", "The provider denied this operation. Check the account's permissions and the tool's required access with the provider or an administrator.");
74
+ }
75
+ // Token endpoints return typed OAuth recovery codes on HTTP 400. Other
76
+ // OAuth steps, especially registration, keep their HTTP status verdict.
77
+ if (error instanceof OAuthError &&
78
+ OAUTH_ERROR_CODES.has(error.code) &&
79
+ (status === undefined || oauthStep === "token request")) {
80
+ return new ConnectorCallError(["invalid_client", "invalid_grant", "invalid_token", "unauthorized_client"].includes(error.code)
81
+ ? "downstream_oauth_required"
82
+ : error.code === "too_many_requests"
83
+ ? "rate_limited"
84
+ : ["server_error", "temporarily_unavailable"].includes(error.code)
85
+ ? "unavailable"
86
+ : "connector_call_failed", `OAuth failed with error ${error.code}.`);
87
+ }
88
+ if (status !== undefined && status >= 400) {
89
+ let message = `The downstream service answered HTTP ${status}.`;
90
+ if (error instanceof SdkHttpError && status < 500 && typeof error.data.text === "string") {
91
+ message = error.message;
92
+ try {
93
+ const body = JSON.parse(error.data.text);
94
+ const detail = body?.message ?? body?.error?.message ?? body?.error_description;
95
+ if (typeof detail === "string")
96
+ message = detail;
97
+ }
98
+ catch {
99
+ // Non-JSON MCP refusals retain only a bounded diagnostic.
100
+ }
101
+ }
102
+ return new ConnectorCallError(status === 401 && !(error instanceof RegistrationRejectedError)
103
+ ? "auth_required"
104
+ : status === 429
105
+ ? "rate_limited"
106
+ : status === 408 && !(error instanceof RegistrationRejectedError)
107
+ ? "timeout"
108
+ : "connector_call_failed", boundedEchoText(secrets?.text(message) ?? message), {
109
+ retryable: [429, 502, 503, 504].includes(status) || (status === 408 && !(error instanceof RegistrationRejectedError)),
110
+ ...(wait !== undefined ? { retryAfterMs: wait } : {}),
111
+ });
112
+ }
113
+ if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) {
114
+ return new ConnectorCallError("timeout", "The downstream request timed out.");
115
+ }
116
+ // ProtocolError.data is server-chosen even when its message is allowed.
117
+ if (error instanceof ProtocolError) {
118
+ return new ConnectorCallError(error.code === -32602 ? "invalid_args" : "connector_call_failed", boundedEchoText(secrets?.text(error.message) ?? error.message));
119
+ }
120
+ return new ConnectorCallError("connector_call_failed", "The downstream operation failed.");
121
+ }
122
+ /** A connection whose request scope ended while it was being made. */
123
+ export class ScopeEndedError extends Error {
124
+ name = "ScopeEndedError";
125
+ }
126
+ labelErrorClass(ScopeEndedError, "ScopeEndedError");
127
+ /** The MCP client's transport lacks the OAuth flow seams this release is pinned against. */
128
+ export class UnboundOAuthFlowsError extends Error {
129
+ name = "UnboundOAuthFlowsError";
130
+ }
131
+ labelErrorClass(UnboundOAuthFlowsError, "UnboundOAuthFlowsError");
132
+ // The pinned SDK's exported errors, labelled by identity for records and
133
+ // connecta's fixed text.
134
+ for (const [ctor, label] of [
135
+ [SdkError, "SdkError"],
136
+ [SdkHttpError, "SdkHttpError"],
137
+ [ProtocolError, "ProtocolError"],
138
+ [MissingRequiredClientCapabilityError, "MissingRequiredClientCapabilityError"],
139
+ [ResourceNotFoundError, "ResourceNotFoundError"],
140
+ [UnsupportedProtocolVersionError, "UnsupportedProtocolVersionError"],
141
+ [UrlElicitationRequiredError, "UrlElicitationRequiredError"],
142
+ [SseError, "SseError"],
143
+ [OAuthError, "OAuthError"],
144
+ [UnauthorizedError, "UnauthorizedError"],
145
+ [OAuthClientFlowError, "OAuthClientFlowError"],
146
+ [AuthorizationServerMismatchError, "AuthorizationServerMismatchError"],
147
+ [InsecureTokenEndpointError, "InsecureTokenEndpointError"],
148
+ [InsufficientScopeError, "InsufficientScopeError"],
149
+ [IssuerMismatchError, "IssuerMismatchError"],
150
+ [RegistrationRejectedError, "RegistrationRejectedError"],
151
+ ]) {
152
+ labelErrorClass(ctor, label);
153
+ }
154
+ /** The JSON-RPC error answers each transport delivered, as `code\0message`. */
155
+ const wireErrors = new WeakMap();
156
+ /** Answers remembered per transport; one request scope rarely sees more. */
157
+ const MAX_WIRE_ERRORS = 64;
158
+ function wireErrorKey(code, message) {
159
+ return `${String(code)}\u0000${String(message)}`;
160
+ }
161
+ /**
162
+ * Remember every JSON-RPC error response `transport` delivers, so that a
163
+ * `ProtocolError` can be told apart as the downstream's own answer. The SDK
164
+ * builds its local refusals (an output schema it cannot compile, a result
165
+ * that fails it) from the same class, with its own text about what the
166
+ * downstream sent; only one whose code and message arrived on the wire is
167
+ * the answer. Installed before `Client.connect`, whose protocol calls the
168
+ * `onmessage` it finds there ahead of its own for every message, and whose
169
+ * version probe restores it when the probe is done.
170
+ */
171
+ /** The JSON-RPC error a response body is, when it is one. */
172
+ export function jsonRpcErrorBody(text) {
173
+ if (typeof text !== "string")
174
+ return undefined;
175
+ try {
176
+ const message = JSON.parse(text);
177
+ if (message?.jsonrpc === "2.0" && Number.isInteger(message.error?.code))
178
+ return message.error;
179
+ }
180
+ catch { }
181
+ return undefined;
182
+ }
183
+ /**
184
+ * Whether a 400's recognized modern error leaves a modern verdict usable.
185
+ * `HeaderMismatch` (-32020) and `MissingRequiredClientCapability` (-32021)
186
+ * concern the request alone. `UnsupportedProtocolVersion` (-32022) keeps it
187
+ * only while its `supported` list, the versions the server now accepts, still
188
+ * names the version the request selected; a missing, malformed or other list
189
+ * ends the verdict. Anything else is a legacy refusal.
190
+ */
191
+ export function keepsModernVerdict(error, selected) {
192
+ if (error?.code === -32020 || error?.code === -32021)
193
+ return true;
194
+ if (error?.code !== -32022 || selected === null)
195
+ return false;
196
+ const supported = error.data?.supported;
197
+ return (Array.isArray(supported) &&
198
+ supported.length <= 64 &&
199
+ supported.every((version) => typeof version === "string") &&
200
+ supported.includes(selected));
201
+ }
202
+ /** HTTP refusals with their own accounting: authorization, scope and throttling. */
203
+ export const AUTH_OR_THROTTLE = [401, 403, 429];
204
+ /**
205
+ * A bounded clone of an error answer's body, read before the SDK reads the
206
+ * original: `""` when longer than the bound, `undefined` for an event stream,
207
+ * an unreadable body, or a read one of `signals` ended. An abort cancels the
208
+ * clone's reader at once, so no read outlives the request or its scope (INV-7).
209
+ */
210
+ export async function errorBodyText(response, signals) {
211
+ if (response.headers.get("content-type")?.toLowerCase().includes("text/event-stream"))
212
+ return undefined;
213
+ const reader = response.clone().body?.getReader();
214
+ const stop = () => void reader?.cancel().catch(() => { });
215
+ const ended = () => signals.some((signal) => signal?.aborted);
216
+ for (const signal of signals)
217
+ signal?.addEventListener("abort", stop, { once: true });
218
+ try {
219
+ const decoder = new TextDecoder();
220
+ let text = "";
221
+ for (let bytes = 0; reader && !ended();) {
222
+ const { done, value } = await reader.read();
223
+ if (done)
224
+ break;
225
+ bytes += value.byteLength;
226
+ if (bytes > MAX_ERROR_BODY_BYTES) {
227
+ stop();
228
+ return "";
229
+ }
230
+ text += decoder.decode(value, { stream: true });
231
+ }
232
+ if (ended()) {
233
+ stop();
234
+ return undefined;
235
+ }
236
+ return text + decoder.decode();
237
+ }
238
+ catch {
239
+ stop();
240
+ return undefined;
241
+ }
242
+ finally {
243
+ for (const signal of signals)
244
+ signal?.removeEventListener("abort", stop);
245
+ }
246
+ }
247
+ const MAX_ERROR_BODY_BYTES = 16_384;
248
+ export function recordWireErrors(transport, catalogChanged) {
249
+ const seen = new Set();
250
+ wireErrors.set(transport, seen);
251
+ transport.onmessage = (message) => {
252
+ // The public transport observer runs before SDK 2.3.1's delete-based
253
+ // eviction. Ordinary opposite-scope cleanup never rotates this fence.
254
+ if (isJSONRPCNotification(message) &&
255
+ (message.method === "notifications/tools/list_changed" ||
256
+ message.method === "notifications/resources/list_changed"))
257
+ catalogChanged();
258
+ if (isJSONRPCErrorResponse(message) && seen.size < MAX_WIRE_ERRORS) {
259
+ seen.add(wireErrorKey(message.error.code, message.error.message));
260
+ }
261
+ };
262
+ }
263
+ /**
264
+ * Whether an error leaving the SDK may keep its own text. The boundary is an
265
+ * allow-list: a JSON-RPC error the downstream answered with, an HTTP 4xx
266
+ * refusal from the MCP endpoint, and errors already in connecta's words (its
267
+ * own classes, an OAuth error the token responses were rebuilt into, and
268
+ * `UnauthorizedError`, whose class is the whole verdict and whose text is
269
+ * never shown). The request's own abort reason is checked by identity before
270
+ * this. Everything else is withheld.
271
+ */
272
+ function keepsItsText(err, transport) {
273
+ if (err instanceof ConnectorCallError ||
274
+ err instanceof WithheldTextError ||
275
+ err instanceof UnauthorizedError ||
276
+ err instanceof OAuthError) {
277
+ return true;
278
+ }
279
+ if (err instanceof SdkHttpError)
280
+ return err.status >= 400 && err.status < 500;
281
+ if (err instanceof ProtocolError) {
282
+ return transport !== undefined && wireErrors.get(transport)?.has(wireErrorKey(err.code, err.message)) === true;
283
+ }
284
+ return false;
285
+ }
286
+ /**
287
+ * The SDK's own checks of a tool's output schema, recognized by how the
288
+ * pinned SDK (2.3.1) begins their messages, and told in connecta's words.
289
+ * Recognition reads the SDK's text only to choose connecta's: none of it, nor
290
+ * the validator's account that follows, is repeated.
291
+ */
292
+ const SDK_OUTPUT_SCHEMA_CHECKS = [
293
+ [
294
+ /^Structured content does not match the tool's output schema/,
295
+ "the result did not match the tool's declared output schema",
296
+ ],
297
+ [
298
+ /^Tool .* has an output schema but did not return structured content/s,
299
+ "the result did not return structured content although the tool declares an output schema",
300
+ ],
301
+ [/^Tool .* has an invalid outputSchema/s, "the tool's declared output schema could not be compiled"],
302
+ ];
303
+ function sdkCheckFailed(err) {
304
+ if (err instanceof SdkError && err.code === SdkErrorCode.RequestTimeout)
305
+ return ": the request timed out";
306
+ if (!(err instanceof ProtocolError))
307
+ return "";
308
+ const check = SDK_OUTPUT_SCHEMA_CHECKS.find(([pattern]) => pattern.test(err.message));
309
+ return check ? `: ${check[1]}` : "";
310
+ }
311
+ /** The first error in `err`'s cause chain already told in connecta's words. */
312
+ function connectaErrorWithin(err) {
313
+ const seen = new Set();
314
+ for (let current = err instanceof Error ? err.cause : undefined; current instanceof Error && !seen.has(current); current = current.cause) {
315
+ seen.add(current);
316
+ if (current instanceof ConnectorCallError || current instanceof WithheldTextError)
317
+ return current;
318
+ }
319
+ return undefined;
320
+ }
321
+ /** The request's own abort reason, by identity, never by name or class. */
322
+ export function ownAbortReason(err, signals) {
323
+ return signals.some((signal) => signal?.aborted === true && err === signal.reason);
324
+ }
325
+ /**
326
+ * The request's own abort reason as the SDK hands it back: a request it
327
+ * cancels rejects with `new SdkError(RequestTimeout, String(reason))` unless
328
+ * the reason already is one (pinned against client 2.3.1). Its text is the
329
+ * caller's, so it passes as the SDK wrote it.
330
+ */
331
+ function ownAbortReasonAsSdkReports(err, signals) {
332
+ return (err instanceof SdkError &&
333
+ err.code === SdkErrorCode.RequestTimeout &&
334
+ signals.some((signal) => signal?.aborted === true && err.message === String(signal.reason)));
335
+ }
336
+ /** An error's class name, when it is a plain identifier worth naming. */
337
+ export function errorKind(err) {
338
+ const label = errorLabel(err);
339
+ return label && label !== "Error" ? ` (${label})` : "";
340
+ }
341
+ /**
342
+ * Replace text while keeping a verdict already derived from structured facts.
343
+ * Never retain the original SDK error as a cause.
344
+ */
345
+ export function withheldAs(message, verdict) {
346
+ if (verdict instanceof ConnectorCallError) {
347
+ return new ConnectorCallError(verdict.code, message, {
348
+ retryable: verdict.retryable,
349
+ ...(verdict.retryAfterMs !== undefined ? { retryAfterMs: verdict.retryAfterMs } : {}),
350
+ ...(verdict.details ? { details: verdict.details } : {}),
351
+ ...(verdict.validation ? { validation: verdict.validation } : {}),
352
+ ...(verdict.current ? { current: verdict.current } : {}),
353
+ ...(verdict.writeOutcome ? { writeOutcome: verdict.writeOutcome } : {}),
354
+ });
355
+ }
356
+ return new WithheldTextError(message, verdict);
357
+ }
358
+ /** One connector's error classes and boundary mapping. */
359
+ export function remoteMcpErrors(rt) {
360
+ const { isOauth, id, endpointOrigin, requestAuth } = rt;
361
+ /**
362
+ * Typed per-call auth signal. The `UnauthorizedError` behind it is not kept
363
+ * as cause: the class is the whole verdict, and a logger rendering the
364
+ * chain would render whatever text the error carried.
365
+ */
366
+ const authRequiredError = (ctx) => new ConnectorCallError(isOauth ? "downstream_oauth_required" : "auth_required", `Connector "${id}" requires authorization — ${routePhrases(ctx).authorizeAndOpen(id)}.`);
367
+ /**
368
+ * The MCP boundary, where an error leaves the SDK at the handshake, a
369
+ * `tools/list` page, or a `tools/call`. The request's own abort reason
370
+ * passes first, by identity, then what `keepsItsText` allows. Anything else
371
+ * (a parser's or validator's account, a non-4xx body, a runtime's message)
372
+ * is told as the step, the endpoint's origin, the HTTP status if there was
373
+ * one, and the error's class, classified as `verdict` would have been.
374
+ */
375
+ const atMcpBoundary = (ctx, err, step, transport, signals) => {
376
+ // An OAuth flow already attached the step and origin it failed at. The
377
+ // outer MCP operation must not replace them with the endpoint's facts.
378
+ if (failureRecord({}, err).step?.startsWith("OAuth "))
379
+ return downstreamCallError(err);
380
+ const httpStatus = err instanceof SdkHttpError ? err.status : undefined;
381
+ // Skills operations retain the existing closed operator-record vocabulary.
382
+ const facts = {
383
+ ...(step === "skills/list" || step === "resources/read" ? {} : { step }),
384
+ origin: endpointOrigin,
385
+ ...(httpStatus ? { httpStatus } : {}),
386
+ };
387
+ const classified = downstreamCallError(redactSentSecrets(ctx, err), undefined, undefined, undefined, requestSentSecrets(ctx));
388
+ const verdict = isOauth && classified instanceof ConnectorCallError && classified.code === "auth_required"
389
+ ? carryFailureFacts(err, authRequiredError(ctx))
390
+ : classified;
391
+ if (ownAbortReasonAsSdkReports(err, signals)) {
392
+ return attachFailureFacts(carryFailureFacts(err, withheldAs(msg(err), verdict)), facts);
393
+ }
394
+ if (err instanceof InsufficientScopeError) {
395
+ return attachFailureFacts(carryFailureFacts(err, verdict), facts);
396
+ }
397
+ if (requestAuth && httpStatus === 401) {
398
+ return attachFailureFacts(new ConnectorCallError("auth_required", "The downstream rejected this request's Bearer token."), facts);
399
+ }
400
+ if (ownAbortReason(err, signals) || keepsItsText(err, transport)) {
401
+ return attachFailureFacts(carryFailureFacts(err, redactSentSecrets(ctx, err instanceof UnauthorizedError || (ownAbortReason(err, signals) && !hasSdkPayload(err)) ? err : verdict)), facts);
402
+ }
403
+ // The SDK wraps a failure of connecta's own fetch (a refused redirect, an
404
+ // unreachable host) in an error of its own, such as the version probe's;
405
+ // that failure's words are connecta's, and they say what happened.
406
+ const ours = connectaErrorWithin(err);
407
+ if (ours) {
408
+ return attachFailureFacts(carryFailureFacts(ours, withheldAs(ours.message, ours)), facts);
409
+ }
410
+ const status = httpStatus ? ` with HTTP ${httpStatus}` : "";
411
+ return attachFailureFacts(carryFailureFacts(err, withheldAs(`Connector "${id}" ${step} with ${endpointOrigin} failed${status}` +
412
+ `${errorKind(err)}${sdkCheckFailed(err)}. The error is withheld because ` +
413
+ "its text can quote what the server sent.", verdict)), facts);
414
+ };
415
+ class OperatorDisconnectedError extends ConnectorCallError {
416
+ constructor() {
417
+ super("downstream_oauth_required", `Connector "${id}" was disconnected by an operator — explicitly start authorization to reconnect it.`);
418
+ }
419
+ }
420
+ const operatorDisconnectedError = () => new OperatorDisconnectedError();
421
+ /**
422
+ * The credential slot is declared but the vault has nothing in it (or has no
423
+ * key to read it with). Deliberately the same `auth_required` code an absent
424
+ * OAuth grant produces: the agent's next move is `authorize_connector`
425
+ * either way, and that tool reads `connector.credential` to hand the
426
+ * operator the connection in the UI when available instead of a consent URL.
427
+ */
428
+ class CredentialRequiredError extends ConnectorCallError {
429
+ constructor(message) {
430
+ super("auth_required", message);
431
+ }
432
+ }
433
+ /**
434
+ * A request resolver threw something untyped. Calls still fail with
435
+ * `auth_required`, but status reports an error: the resolver's own outage
436
+ * is not an authorization the operator can grant.
437
+ */
438
+ class UnresolvedTokenError extends ConnectorCallError {
439
+ constructor() {
440
+ super("auth_required", "The request token could not be resolved.");
441
+ }
442
+ }
443
+ /**
444
+ * Replace, never edit.
445
+ *
446
+ * Any error whose message quotes the credential — the raw value or the
447
+ * framed header it becomes — is discarded whole and replaced with a fixed
448
+ * sentence. Nothing is substringed, masked, or truncated out of the original:
449
+ * a redaction that keeps part of a secret is still a leak, and the original
450
+ * error is not worth one. This is defense in depth behind the validation
451
+ * above, which is what keeps an unsendable value from reaching a transport
452
+ * at all.
453
+ */
454
+ const withoutCredential = (ctx, err, ...secrets) => {
455
+ const quoted = secrets.filter((secret) => typeof secret === "string" && secret !== "");
456
+ if (quoted.length === 0)
457
+ return err;
458
+ const seen = new Set();
459
+ let current = err;
460
+ while (current instanceof Error && !seen.has(current)) {
461
+ seen.add(current);
462
+ // Decided already, beneath the transport, and wrapped by the SDK on
463
+ // the way up (the version probe's error, say).
464
+ if (current instanceof CredentialRequiredError)
465
+ return current;
466
+ if (quoted.some((secret) => current instanceof Error && current.message.includes(secret))) {
467
+ return new CredentialRequiredError(`Connector "${id}" could not send its stored credential as a ` +
468
+ `header. ${routePhrases(ctx).authorizeFor("recovery options")}. When available, ` +
469
+ "re-enter it in this connection in the operator UI. " +
470
+ "The value is not shown or logged.");
471
+ }
472
+ current = current.cause;
473
+ }
474
+ return err;
475
+ };
476
+ // Its own class, so that a status record, which carries no message, still says so.
477
+ const scopeEndedError = () => new ScopeEndedError(`Connector "${id}" scope ended during connection.`);
478
+ return {
479
+ UnresolvedTokenError,
480
+ CredentialRequiredError,
481
+ atMcpBoundary,
482
+ scopeEndedError,
483
+ authRequiredError,
484
+ withoutCredential,
485
+ operatorDisconnectedError,
486
+ OperatorDisconnectedError,
487
+ };
488
+ }