// MCP plugin tagged errors. API-facing errors carry `HttpApiSchema`// annotations so they can be `.addError(...)` directly on the API group. import { Data, Schema } from "effect"; export const McpConnectionFailureKind = Schema.Literals([ "tls", "dns", "timeout", "connection_refused", "network", "http", "protocol",]);export type McpConnectionFailureKind = typeof McpConnectionFailureKind.Type; export class McpConnectionError extends Schema.TaggedErrorClass<McpConnectionError>()( "McpConnectionError", { transport: Schema.String, message: Schema.String, /** Safe, structural classification of the connection failure. Auto * transport uses this to retry only protocol incompatibilities; callers * can render actionable copy without parsing an external error message. */ failureKind: Schema.optional(McpConnectionFailureKind), /** HTTP status the handshake observed (e.g. 401 on an auth wall), when the * transport surfaced one. Structural, so the liveness classifier and the * auto-transport fallback never string-match the message. */ httpStatus: Schema.optional(Schema.Number), /** The 403 carried an RFC 6750 insufficient_scope challenge (set by the * connection layer when the fetch adapter intercepted one during the * handshake). */ insufficientScope: Schema.optional(Schema.Boolean), }, { httpApiStatus: 400 },) {} export class McpToolDiscoveryError extends Schema.TaggedErrorClass<McpToolDiscoveryError>()( "McpToolDiscoveryError", { stage: Schema.Literals(["connect", "list_tools"]), message: Schema.String, /** HTTP status from the underlying connect or tools/list failure, when * known. */ httpStatus: Schema.optional(Schema.Number), /** The connection negotiated the modern (2026-07-28) era and the server * then broke that revision's response contract — the signature of a * server that echoes whatever protocol version is proposed. Retrying * with legacy negotiation is expected to succeed. */ modernContractViolation: Schema.optional(Schema.Boolean), /** The MCP OAuth provider reached the interactive authorization boundary. * Catalog callers use this structural signal to request reconnect without * parsing or exposing an upstream error message. */ reauthorizationRequired: Schema.optional(Schema.Boolean), /** Discovery hit its deadline (`discoverTools` timeout). Structural, so * the health check can report `probe_timeout` — a slow-but-alive server — * without string-matching the message. */ timedOut: Schema.optional(Schema.Boolean), }, { httpApiStatus: 400 },) {} // Internal only: core wraps non-auth failures as ToolInvocationError.cause, so// this must carry only sanitized invocation metadata. Raw SDK causes can contain// upstream bodies/challenges and should not leave the invoke catch block.export class McpInvocationError extends Data.TaggedError("McpInvocationError")<{ readonly toolName: string; readonly message: string; readonly status?: number; /** The SDK failure came from the HTTP/SSE transport rather than a JSON-RPC * protocol response. Used only to decide whether a leased session is safe * to return to the idle pool. */ readonly transportFailure?: boolean; /** The server rejected the call as an unknown tool (protocol error), which * means the persisted catalog has drifted from the server's live tool set. */ readonly unknownTool?: boolean; /** A 403 whose body named a scope shortfall (RFC 6750 insufficient_scope / * Google's ACCESS_TOKEN_SCOPE_INSUFFICIENT): re-authenticating the same * grant cannot fix it, so the failure must not be labelled * connection_rejected. */ readonly insufficientScope?: boolean; /** The server answered `tools/call` with a JSON-RPC error response (the * spec's protocol error: invalid params, internal error, ...). The call * reached the server and was refused on its merits, so the code and the * server's own message are the failure — not an infrastructure defect. */ readonly protocolError?: { readonly code: number; readonly message: string; }; /** Operator-facing summary of the SDK rejection this error sanitized: * the error's class name and its stable `code` (an `SdkErrorCode`, * `ProtocolErrorCode`, or HTTP status). Never the message — a transport * message can embed an upstream body. Carried so the dispatch defect log * (`tool dispatch failed`, keyed by correlation id) names WHAT the SDK * refused instead of only the tool it refused. */ readonly sdkFailure?: { readonly name: string; readonly code?: string | number; }; /** The server answered `tools/call` with a non-2xx HTTP response whose * body was a JSON object carrying a message (a validation refusal from a * server that answers at the HTTP layer instead of with a JSON-RPC error, * e.g. a 422 naming a missing field). Present only for 4xx statuses other * than the auth walls (401/403), and only when the body parsed as JSON * with a string `message`/`error`/`error.message` — a free-text body is * never copied out of the transport error. */ readonly httpRefusal?: { readonly status: number; readonly message: string; };}> {} export class McpOAuthReauthorizationRequired extends Data.TaggedError( "McpOAuthReauthorizationRequired",)<{ readonly message: string;}> {} /** Thrown by the fetch adapter when a 403 carries an RFC 6750 * `error="insufficient_scope"` challenge. Raised BELOW the MCP SDK on * purpose: with an authProvider present the SDK would otherwise consume the * challenge and re-run auth ("upscoping"), which our static-token provider * can only answer by demanding reauthorization — the exact loop the * oauth_scope_insufficient classification exists to break. */export class McpInsufficientScopeError extends Data.TaggedError("McpInsufficientScopeError")<{ readonly message: string;}> {} export class McpOAuthError extends Schema.TaggedErrorClass<McpOAuthError>()( "McpOAuthError", { message: Schema.String, }, { httpApiStatus: 400 },) {}
// MCP plugin tagged errors. API-facing errors carry `HttpApiSchema`
// annotations so they can be `.addError(...)` directly on the API group.
import { Data, Schema } from "effect";
export const McpConnectionFailureKind = Schema.Literals([
"tls",
"dns",
"timeout",
"connection_refused",
"network",
"http",
"protocol",
]);
export type McpConnectionFailureKind = typeof McpConnectionFailureKind.Type;
export class McpConnectionError extends Schema.TaggedErrorClass<McpConnectionError>()(
"McpConnectionError",
{
transport: Schema.String,
message: Schema.String,
/** Safe, structural classification of the connection failure. Auto
* transport uses this to retry only protocol incompatibilities; callers
* can render actionable copy without parsing an external error message. */
failureKind: Schema.optional(McpConnectionFailureKind),
/** HTTP status the handshake observed (e.g. 401 on an auth wall), when the
* transport surfaced one. Structural, so the liveness classifier and the
* auto-transport fallback never string-match the message. */
httpStatus: Schema.optional(Schema.Number),
/** The 403 carried an RFC 6750 insufficient_scope challenge (set by the
* connection layer when the fetch adapter intercepted one during the
* handshake). */
insufficientScope: Schema.optional(Schema.Boolean),
},
{ httpApiStatus: 400 },
) {}
export class McpToolDiscoveryError extends Schema.TaggedErrorClass<McpToolDiscoveryError>()(
"McpToolDiscoveryError",
{
stage: Schema.Literals(["connect", "list_tools"]),
message: Schema.String,
/** HTTP status from the underlying connect or tools/list failure, when
* known. */
httpStatus: Schema.optional(Schema.Number),
/** The connection negotiated the modern (2026-07-28) era and the server
* then broke that revision's response contract — the signature of a
* server that echoes whatever protocol version is proposed. Retrying
* with legacy negotiation is expected to succeed. */
modernContractViolation: Schema.optional(Schema.Boolean),
/** The MCP OAuth provider reached the interactive authorization boundary.
* Catalog callers use this structural signal to request reconnect without
* parsing or exposing an upstream error message. */
reauthorizationRequired: Schema.optional(Schema.Boolean),
/** Discovery hit its deadline (`discoverTools` timeout). Structural, so
* the health check can report `probe_timeout` — a slow-but-alive server —
* without string-matching the message. */
timedOut: Schema.optional(Schema.Boolean),
},
{ httpApiStatus: 400 },
) {}
// Internal only: core wraps non-auth failures as ToolInvocationError.cause, so
// this must carry only sanitized invocation metadata. Raw SDK causes can contain
// upstream bodies/challenges and should not leave the invoke catch block.
export class McpInvocationError extends Data.TaggedError("McpInvocationError")<{
readonly toolName: string;
readonly message: string;
readonly status?: number;
/** The SDK failure came from the HTTP/SSE transport rather than a JSON-RPC
* protocol response. Used only to decide whether a leased session is safe
* to return to the idle pool. */
readonly transportFailure?: boolean;
/** The server rejected the call as an unknown tool (protocol error), which
* means the persisted catalog has drifted from the server's live tool set. */
readonly unknownTool?: boolean;
/** A 403 whose body named a scope shortfall (RFC 6750 insufficient_scope /
* Google's ACCESS_TOKEN_SCOPE_INSUFFICIENT): re-authenticating the same
* grant cannot fix it, so the failure must not be labelled
* connection_rejected. */
readonly insufficientScope?: boolean;
/** The server answered `tools/call` with a JSON-RPC error response (the
* spec's protocol error: invalid params, internal error, ...). The call
* reached the server and was refused on its merits, so the code and the
* server's own message are the failure — not an infrastructure defect. */
readonly protocolError?: {
readonly code: number;
readonly message: string;
};
/** Operator-facing summary of the SDK rejection this error sanitized:
* the error's class name and its stable `code` (an `SdkErrorCode`,
* `ProtocolErrorCode`, or HTTP status). Never the message — a transport
* message can embed an upstream body. Carried so the dispatch defect log
* (`tool dispatch failed`, keyed by correlation id) names WHAT the SDK
* refused instead of only the tool it refused. */
readonly sdkFailure?: {
readonly name: string;
readonly code?: string | number;
};
/** The server answered `tools/call` with a non-2xx HTTP response whose
* body was a JSON object carrying a message (a validation refusal from a
* server that answers at the HTTP layer instead of with a JSON-RPC error,
* e.g. a 422 naming a missing field). Present only for 4xx statuses other
* than the auth walls (401/403), and only when the body parsed as JSON
* with a string `message`/`error`/`error.message` — a free-text body is
* never copied out of the transport error. */
readonly httpRefusal?: {
readonly status: number;
readonly message: string;
};
}> {}
export class McpOAuthReauthorizationRequired extends Data.TaggedError(
"McpOAuthReauthorizationRequired",
)<{
readonly message: string;
}> {}
/** Thrown by the fetch adapter when a 403 carries an RFC 6750
* `error="insufficient_scope"` challenge. Raised BELOW the MCP SDK on
* purpose: with an authProvider present the SDK would otherwise consume the
* challenge and re-run auth ("upscoping"), which our static-token provider
* can only answer by demanding reauthorization — the exact loop the
* oauth_scope_insufficient classification exists to break. */
export class McpInsufficientScopeError extends Data.TaggedError("McpInsufficientScopeError")<{
readonly message: string;
}> {}
export class McpOAuthError extends Schema.TaggedErrorClass<McpOAuthError>()(
"McpOAuthError",
{
message: Schema.String,
},
{ httpApiStatus: 400 },
) {}