Error classes

@frontmcp/sdk exports an error class for most things that can go wrong, from a model sending bad arguments to a provider that can't be built. This page groups them by area; a few other exported classes aren't listed. Each one has a string code, which clients receive, and is either public, so its message reaches the client in production, or internal, so production replaces it with an error ID. A handful are for you to fail with. Most are what FrontMCP throws itself, and knowing them tells you what a client saw and why. For how to fail a call, see this.fail(); for every code a client can receive, see Error codes.

this.fail(new PublicMcpError(message, code?, statusCode?))
throw new InvalidInputError(message?, details?)

Reference

McpError

Every FrontMCP error extends the abstract McpError, itself an Error, except the guard's errors and @frontmcp/auth's internal ones (see below).

MemberTypeDescription
codestringWhat kind of error it is, like "INVALID_INPUT". Clients get it as a tool result's _meta.code, or as data.code in a JSON-RPC error.
isPublicbooleanWhether the message may reach clients in production.
statusCodenumberAn HTTP-style status. It picks the JSON-RPC code for resources and prompts; a tool result is always HTTP 200.
errorIdstringerr_ and 16 hex digits, new for every error. Clients get it in _meta.errorId or data.errorId, and FrontMCP logs it with the error, so a user's report can be found in the log.
messagestringThe message as written, for your logs.
getPublicMessage()stringWhat a production client reads.
getInternalMessage()stringWhat a development client reads in a tool result.
toMcpError(isDevelopment?)CallToolResultThe isError tool result for this error.

PublicMcpError(message, code?, statusCode?, wwwAuthenticate?)

The error to fail with when the model should read the message. code defaults to "PUBLIC_ERROR", and statusCode to 400. wwwAuthenticate is only used on FrontMCP's own HTTP endpoints. Subclass it for errors of your own:

export class TicketArchivedError extends PublicMcpError {
  constructor(id: string) {
    super(`Ticket ${id} is archived. Ask a lead to restore it.`, "TICKET_ARCHIVED", 409);
  }
}

InternalMcpError(message, code?)

An error whose message must not leave the server. code defaults to "INTERNAL_ERROR", and statusCode is 500. Production clients get Internal FrontMCP error. Please contact support with error ID: err_…; the message goes to the log.

What the client receives

From a tool

A tool that fails, with this.fail() or by throwing, answers with a normal result: isError: true, the text, and _meta with errorId, code, timestamp, and in development, stack.

  • _meta.code is the error's code. this.fail() passes every McpError through as it is. A thrown error keeps its code too if it's public.
  • A thrown error that isn't public is wrapped as ToolExecutionError: code TOOL_EXECUTION_ERROR, text Tool "…" execution failed: and the message. Passed to this.fail(), the same InternalMcpError keeps its code, and a plain Error becomes SERVER_ERROR. See it run.
  • The text is getInternalMessage() in development and getPublicMessage() in production. For most public errors they're the same; the few that differ are marked below.
  • A few refusals are JSON-RPC errors instead, all before execute() runs: a thrown ToolCredentialsRequiredError (-32001, with data.authUrl); for clients before MCP 2026-07-28, TaskAugmentationNotSupportedError and TaskAugmentationRequiredError (-32601); and under 2026-07-28, a call to a tool with execution.taskSupport: "required" from a client that didn't declare the tasks extension (-32602, Tool "…" runs as a task; declare the io.modelcontextprotocol/tasks extension in clientCapabilities).

From a resource or a prompt

resources/read and prompts/get answer with a JSON-RPC error. Its message is getPublicMessage(), except that an internal error's message is kept in development, and data holds errorId and code. The JSON-RPC code comes from the class:

ErrorJSON-RPC code
A class with its own JSON-RPC mapping (marked "own" below)Its own code, and its own data, without code
Public, statusCode 401-32001
Public, statusCode 403-32003
Public, statusCode below 500-32602
Public, statusCode 500 or more-32603, with the message kept
Internal-32603

A thrown error that isn't public is wrapped, as ResourceReadError (Resource "…" read failed: ) or PromptExecutionError (Prompt execution failed: ). See it run.

Before any handler

A few refusals happen before FrontMCP knows what the request is for. They're HTTP errors with a JSON-RPC body, and no error class of yours is involved: throttle.global gives HTTP 429 and -32029, throttle.ipFilter HTTP 403 and -32001 (see Guard options), and, on the Node HTTP server, a body over http.bodyLimit HTTP 413 and -32600, Payload Too Large.

In production

With NODE_ENV=production, a public error's message is sent, and an internal error's is replaced with Internal FrontMCP error. Please contact support with error ID: err_…; InvalidOutputError says Output validation failed. Please contact support. instead. No stack is sent. The Playground runs in development, so the examples below format errors the way production does with createErrorHandler({ isDevelopment: false }).

Errors that aren't McpErrors

  • Any other Error, like a TypeError or a driver's error, is internal. Passed to this.fail(), it becomes a GenericServerError, code SERVER_ERROR; in development the text adds Original error: and the stack.
  • The guard's errors (ExecutionTimeoutError, ConcurrencyLimitError and the rest) extend GuardError, not McpError. Passed to this.fail(), they keep their code and message, as public errors. Thrown, only ExecutionTimeoutError keeps its code in a tool; the others are wrapped. In a resource or prompt, all of them are wrapped when thrown, like any error that isn't public.
  • @frontmcp/auth's internal errors (JwtSecretRequiredError, SessionSecretRequiredError and the rest) extend AuthInternalError. Their own code is lost: clients get SERVER_ERROR.
  • AuthorityDeniedError, from @frontmcp/auth, is how authorities refuses a call. Clients get code AUTHORITY_DENIED, and its message is kept in production.

Helper functions

FunctionWhat it does
isPublicError(error)true for an McpError with isPublic. false for guard errors and AuthorityDeniedError, which FrontMCP still sends as public.
isClientFacingError(error)isPublicError(), or an AuthorityDeniedError.
toMcpError(error)The McpError FrontMCP would send for error: an McpError as it is, a guard error as a public GuardLimitMcpError with the same code, anything else as a GenericServerError.
formatMcpErrorResponse(error, isDevelopment?)The isError tool result for error. isDevelopment defaults to NODE_ENV !== "production".
createErrorHandler({ isDevelopment?, logger?, errorTransformer? })An ErrorHandler: handle(error, context?) formats a tool result as tools/call does (and logs it with logger); wrap(fn) makes a function throw McpErrors only.
extractPublicMessage(error)The most specific message in an error's cause chain. FrontMCP doesn't use it.
shouldStopExecution(error)Whether an error ends a flow: true except for a FlowControl that responds or continues.
MCP_ERROR_CODESThe JSON-RPC codes: UNAUTHORIZED -32001, RESOURCE_NOT_FOUND -32002, FORBIDDEN -32003, INVALID_REQUEST -32600, METHOD_NOT_FOUND -32601, INVALID_PARAMS -32602, INTERNAL_ERROR -32603, PARSE_ERROR -32700.

The classes

The tables give each class's code, the JSON-RPC code a resource or prompt gets for it (a tool gets the code in _meta.code, as above), whether its message is kept in production, and who throws it. Unless a table says otherwise, a class's constructor takes what its message needs, in the order the message uses it.

Base classes

ClasscodeJSON-RPCProductionWho throws it
PublicMcpError(message, code?, statusCode?)PUBLIC_ERROR, or codeby statusCode (-32602 by default)KeptYou, for anything the model should read. FrontMCP, for skill and channel requests it can't serve.
InternalMcpError(message, code?)INTERNAL_ERROR, or code-32603HiddenYou, for failures to keep in the log. FrontMCP, for its own failures.
GenericServerError(message, originalError?)SERVER_ERROR-32603HiddenFrontMCP, for any other Error passed to this.fail().

Tools and prompts

ClasscodeJSON-RPCProductionWho throws it
ToolNotFoundError(name)TOOL_NOT_FOUND-32602Kept: Tool "x" not foundFrontMCP, for a name no tool has, or an internal tool called by a client.
ToolExecutionError(name, cause?)TOOL_EXECUTION_ERROR-32603HiddenFrontMCP, around an error thrown from execute() that isn't public.
EntryUnavailableError(type, name, …)ENTRY_UNAVAILABLE-32003 (own)Kept. It describes the server's environment: OS, runtime, deployment.FrontMCP, for a tool, resource, resource template or prompt whose availableWhen doesn't match where the server runs. One whose surface leaves out "mcp" gets the not-found error instead.
PromptNotFoundError(name)PROMPT_NOT_FOUND-32602Kept: Prompt not found: xFrontMCP, for an unknown prompt.
PromptExecutionError(name, cause?)PROMPT_EXECUTION_FAILED-32603HiddenFrontMCP, around an error thrown from a prompt that isn't public.
MissingPromptArgumentError(name)MISSING_PROMPT_ARGUMENT-32602Kept: Missing required argument: idFrontMCP, for a required prompt argument that wasn't sent.
ToolCallError(toolName, result)TOOL_CALL_ERRORKeptFrontMCP's in-process clients with an LLM format, like connectClaude(): their callTool() rejects with it when the tool fails. message is the tool's error text, and result the MCP result, isError and _meta.code included. It never reaches a client over MCP. New in 1.9.2.

Resources

ClasscodeJSON-RPCProductionWho throws it
ResourceNotFoundError(uri)RESOURCE_NOT_FOUND-32602 (own, data: { uri }). -32002 for clients before MCP 2026-07-28.Kept: Resource not found: tickets://T-9FrontMCP, for a URI nothing serves. You, for a template whose item doesn't exist, so the client gets the same error as for an unknown URI.
ResourceReadError(uri, cause?)RESOURCE_READ_ERROR-32603HiddenFrontMCP, around an error thrown from a resource that isn't public.
InvalidResourceUriError(uri, reason?)INVALID_RESOURCE_URI-32602KeptYou. FrontMCP never throws it.

Validation

ClasscodeJSON-RPCProductionWho throws it
InvalidInputError(message?, details?)INVALID_INPUT-32602Kept, with details appended as Details: and JSONFrontMCP, for arguments that don't match inputSchema (Invalid tool input, then the Zod issues). You, for rules a schema can't express; see this.fail.
InvalidOutputError(details?)INVALID_OUTPUT-32603Hidden: Output validation failed. Please contact support.FrontMCP, for a result that doesn't match outputSchema, or holds NaN, and since 1.8.7 for an agent's answer that doesn't match. In development the text is Tool output validation failed (output does not match outputSchema at status), naming the first field that didn't match.
InvalidMethodError(method, expected)INVALID_METHOD-32602KeptFrontMCP, inside its own request handling. Clients don't normally see it.

Auth

ClasscodeJSON-RPCProductionWho throws it
UnauthorizedError(message?, wwwAuthenticate?)UNAUTHORIZED-32001Kept (default Unauthorized)You, when the caller must sign in again. FrontMCP, for an older client's session that can't be restored from its token.
AuthorizationRequiredError({ appId, toolId, authUrl?, requiredScopes?, … })AUTHORIZATION_REQUIRED-32003Kept; production adds To authorize, click: and authUrlFrontMCP, when the caller hasn't authorized the tool's app yet. See Progressive auth.
ToolNotConsentedError(name)TOOL_NOT_CONSENTED-32003 (own)KeptFrontMCP, for a tool the user didn't select when consenting. See Tools: consent.
ToolCredentialsRequiredError({ toolId, providers, authUrl? })TOOL_CREDENTIALS_REQUIRED-32001 (own), also in tools/callKeptFrontMCP, for a tool whose required authProviders credential isn't connected. See Progressive auth.
AuthConfigurationError(message, { errors?, suggestion? })AUTH_CONFIGURATION_ERROR-32603Kept; production adds To fix this issue: and suggestionFrontMCP, at startup, for an auth or authorities configuration that can't work, including an authorities rule that checks nothing.
UnsupportedClientVersionError(version)UNSUPPORTED_CLIENT_VERSIONKeptFrontMCP, for an older client's initialize whose protocol version isn't a date.
SessionMissingError(message?)SESSION_MISSING-32001KeptNobody: FrontMCP 1.9.3 never throws it.
PublicAccessDeniedError(kind, name)PUBLIC_ACCESS_DENIED-32003 (own)Kept: The tool "help-desk:close_ticket" is not available to anonymous callersFrontMCP, for an anonymous caller's call to a tool or prompt that the server's publicAccess doesn't list. New in 1.9.2.
SessionIdentityRequiredError(what?)SESSION_IDENTITY_REQUIRED-32003Kept: The secure store's session scope needs a verified session or a signed-in caller. Authenticate the request, or use a session transport (an MCP revision before 2026-07-28).FrontMCP, when this.secureStore with scope: "session" is used by a caller with neither a verified session nor a sign-in, like an anonymous MCP 2026-07-28 caller. New in 1.8.5.
AuthorityDeniedError (@frontmcp/auth)AUTHORITY_DENIED-32003 (own, data: { entryType, entryName, deniedBy, denial })Kept: Access denied to Tool "help-desk:close_ticket": …FrontMCP, when a caller fails an authorities rule. Not exported by @frontmcp/sdk.

@frontmcp/sdk also re-exports @frontmcp/auth's internal errors, which FrontMCP throws while starting up or handling tokens. Clients get SERVER_ERROR for all of them, hidden in production; the server log has the real code. The ones you're most likely to meet: JwtSecretRequiredError (JWT_SECRET_REQUIRED: production with auth.mode: "local" and no JWT_SECRET), JwtSecretWeakError (JWT_SECRET_INVALID: a secret under 32 bytes), SessionSecretRequiredError (SESSION_SECRET_REQUIRED), ElicitationSecretRequiredError, TokenNotAvailableError, TokenStoreRequiredError, TokenLeakDetectedError, EncryptionKeyNotConfiguredError, EncryptionContextNotSetError, VaultLoadError, VaultNotFoundError, SessionIdEmptyError, CredentialProviderAlreadyRegisteredError, OrchestratedAuthNotAvailableError, OrchestratorJwksNotAvailableError and NoProviderIdError. AuthProvidersNotConfiguredError, ScopeDeniedError and InMemoryStoreRequiredError are never thrown.

Rate limits and the guard

ClasscodeJSON-RPCProductionWho throws it
RateLimitError(retryAfter?)RATE_LIMIT_EXCEEDED-32602Kept: Rate limit exceeded. Retry after 30 secondsFrontMCP, for a tool's rateLimit. You, for limits of your own.
QuotaExceededError(quotaType?)QUOTA_EXCEEDED-32602Kept: export quota exceeded (usage by default)You. FrontMCP never throws it.
ExecutionTimeoutError(name, ms)EXECUTION_TIMEOUT-32602 through this.fail()KeptFrontMCP, for a tool's timeout.
ConcurrencyLimitError(name, max)CONCURRENCY_LIMIT-32602 through this.fail()KeptFrontMCP, for concurrency and globalConcurrency.
QueueTimeoutError(name, ms)QUEUE_TIMEOUT-32602 through this.fail()KeptFrontMCP, when a queued call waits longer than queueTimeoutMs.
IpBlockedError(ip), IpNotAllowedError(ip)IP_BLOCKED, IP_NOT_ALLOWED-32003 through this.fail()KeptNobody: ipFilter answers the request itself.
GuardStorageUnavailableError(storageType, cause?)GUARD_STORAGE_UNAVAILABLEReplaced, since 1.9: Service temporarily unavailable: the rate-limit store cannot be reached. Retry shortly., with status 503FrontMCP, when throttle.storage can't be reached and its fallback isn't "memory": while the server starts, where it stops the server (new in 1.8.6), and since 1.8.7 on a call whose limit check or concurrency slot the store can't serve, where the client gets it. See Guard troubleshooting and while the server runs.
GuardError(message, code, statusCode)as givenThe base of the six above.
GuardLimitMcpError(guardError)the guard error'sby statusKeptFrontMCP, when it turns a guard error into an McpError.

See Guard options for when each limit trips.

Agents

ClasscodeProductionWho throws it
AgentNotFoundError(id)AGENT_NOT_FOUNDKeptFrontMCP, for an unknown agent, like a name this.invokeAgent() doesn't know: Agent not found: ….
AgentVisibilityErrorAGENT_VISIBILITY_DENIEDKeptFrontMCP, when invokeAgent() names an agent this one can't see, like one with swarm.isVisible: false. New in 1.9.
AgentCallDepthExceededErrorAGENT_CALL_DEPTH_EXCEEDEDKeptFrontMCP, for a call from one agent to another past swarm.maxCallDepth. New in 1.9.
AgentConfigurationErrorAGENT_CONFIGURATION_ERRORFrontMCP, at startup, for an agent's exports that names something the agent doesn't have. New in 1.9.
AgentExecutionError(id, cause?)AGENT_EXECUTION_FAILEDHiddenFrontMCP, around an agent's failures.
AgentNotConfiguredError(name), AgentToolNotFoundError(agent, tool, available)AGENT_NOT_CONFIGURED, AGENT_TOOL_NOT_FOUNDHiddenFrontMCP, for an agent without an LLM adapter, or asking for a tool it doesn't have.
AgentLoopExceededError, AgentTimeoutError, AgentLlmErrorAGENT_LOOP_EXCEEDED, AGENT_TIMEOUT, AGENT_LLM_ERRORNobody: FrontMCP 1.9.3 never throws them.

A client calling an agent gets TOOL_EXECUTION_ERROR when its loop runs out of turns or time, or its model fails; @Agent lists the texts. The exceptions are an answer that doesn't match the agent's outputSchema, which is InvalidOutputError, code INVALID_OUTPUT, as for a tool (in 1.8.5 and 1.8.6 it was TOOL_EXECUTION_ERROR too, with a stack trace in development), and the three agent-to-agent errors above, which keep their codes.

Elicitation and multi-round-trip requests

ClasscodeJSON-RPCProductionWho throws it
ElicitationDisabledError()ELICITATION_DISABLED-32602Kept, shorter: Elicitation is disabled on this server. Development: Elicitation is disabled in server configuration. Enable it via @FrontMcp({ elicitation: { enabled: true } })FrontMCP, for this.elicit() on a server without elicitation.enabled.
ElicitationNotSupportedError(message?)ELICITATION_NOT_SUPPORTED-32602KeptFrontMCP, when an older client has no session or transport to ask through.
ElicitationNotOwnedError(elicitId)ELICITATION_NOT_OWNED-32003 (own, data: { elicitId })Kept: Elicitation "elicit-…" was not requested by this callerFrontMCP, when sendElicitationResult answers a question that another caller was asked. See this.elicit().
ElicitationTimeoutError(id, ttl)ELICITATION_TIMEOUT-32602Kept, reworded: … The user did not respond within the allowed time (300 seconds).FrontMCP, when an older client's user doesn't answer in time.
ElicitationFallbackRequired(…)ELICITATION_FALLBACKFrontMCP, internally, for clients that can't show forms. The call answers with instructions instead of an error.
ElicitationStoreNotInitializedError, ElicitationEncryptionError, ElicitationSubscriptionErrorELICITATION_STORE_NOT_INITIALIZED, ELICITATION_ENCRYPTION_ERROR, ELICITATION_SUBSCRIPTION_ERROR-32603HiddenFrontMCP, for failures of its elicitation store.
SamplingNotAvailableError(feature?)MRTR_REQUIRED-32602KeptFrontMCP, for sampling or roots/list from a client older than MCP 2026-07-28.
InputRequiredSignal, MissingClientCapabilityErrorINPUT_REQUIRED, MISSING_REQUIRED_CLIENT_CAPABILITYFrontMCP, under MCP 2026-07-28: the first becomes an input_required result, the second HTTP 400 with -32021. isMrtrSignal(error) recognizes both.

Tasks

ClasscodeJSON-RPCWho throws it
TaskNotFoundError(taskId)TASK_NOT_FOUND-32602 (own)FrontMCP, for an unknown task id: Failed to retrieve task: Task not found.
TaskAlreadyTerminalError(status)TASK_ALREADY_TERMINAL-32602 (own)FrontMCP, when cancelling a task that has completed, failed or been cancelled.
TaskAugmentationNotSupportedError(tool), TaskAugmentationRequiredError(tool)TASK_AUGMENTATION_NOT_SUPPORTED, TASK_AUGMENTATION_REQUIRED-32601 (own), also in tools/callFrontMCP, for a client before MCP 2026-07-28 that asks for a task on a tool with taskSupport: "forbidden", or doesn't on one with "required". Under 2026-07-28 the second is a -32602.
TaskStoreNotInitializedError()TASK_STORE_NOT_INITIALIZED-32603FrontMCP, for its own task store. Hidden in production.

All but the last are public.

Jobs and workflows

ClasscodeProductionWho throws it
JobNotAuthorizedError(name)JOB_NOT_AUTHORIZEDKept: Job or workflow "x" not found or not permittedFrontMCP's job tools, for a job that doesn't exist or that the caller may not run. See @Job.
DynamicJobRegistrationDisabledError(kind?)DYNAMIC_REGISTRATION_DISABLEDKeptFrontMCP's register_job and register_workflow, without jobs.allowDynamicRegistration.
WorkflowTimeoutError, WorkflowJobTimeoutError, WorkflowStepNotFoundError, WorkflowDagValidationErrorWORKFLOW_TIMEOUT, WORKFLOW_JOB_TIMEOUT, WORKFLOW_STEP_NOT_FOUND, WORKFLOW_DAG_VALIDATIONHiddenFrontMCP, while running a workflow. Clients get TOOL_EXECUTION_ERROR from execute_workflow; see @Workflow.
DynamicJobDirectExecutionError(name?)DYNAMIC_JOB_DIRECT_EXECUTIONHiddenFrontMCP, for a dynamic job run outside its sandbox.

Remote apps

Remote servers covers how a remote app fails, and what each failure looks like.

For App.remote(). The public ones tell the model which remote server failed.

ClasscodeProductionWho throws it
RemoteTimeoutError(app, operation, ms)REMOTE_TIMEOUT_ERRORKeptFrontMCP, when a remote tool call takes too long (30 seconds by default) on every try.
RemoteToolNotFoundError, RemoteResourceNotFoundError, RemotePromptNotFoundErrorREMOTE_TOOL_NOT_FOUND, REMOTE_RESOURCE_NOT_FOUND, REMOTE_PROMPT_NOT_FOUNDKeptFrontMCP, when the remote server doesn't have it.
RemoteToolExecutionError, RemoteResourceReadError, RemotePromptGetErrorREMOTE_TOOL_EXECUTION_ERROR, REMOTE_RESOURCE_READ_ERROR, REMOTE_PROMPT_GET_ERRORKept, with the remote error's messageFrontMCP, when the remote call fails.
RemoteAuthError(app, details?)REMOTE_AUTH_ERRORKept: Authentication failed for remote server "status": the remote server refused the credentials (HTTP 401)FrontMCP, when the remote answers a tool call, resource read or prompt request with 401, or a remoteAuth: { mode: "mapped" } mapper throws. A remote that refuses the connection while the server starts is a RemoteConnectionError instead. (Changed in 1.9.3: before, it was effectively never thrown.)
RemoteNotConnectedError(app)REMOTE_NOT_CONNECTEDKeptFrontMCP, before the remote app has connected.
RemoteConnectionError, RemoteCapabilityDiscoveryErrorREMOTE_CONNECTION_ERROR, REMOTE_CAPABILITY_DISCOVERY_ERRORHiddenFrontMCP, when connecting or listing the remote server's capabilities fails.
InvalidRetryOptionsErrorINVALID_RETRY_OPTIONSHiddenFrontMCP, for bad retry options.
RemoteDisconnectError, RemoteAuthorizationError, RemoteTransportError, RemoteUnsupportedTransportError, RemoteCapabilityNotSupportedError, RemoteConfigurationErrorNobody: never thrown in 1.9.3.

The remote errors also have an mcpErrorCode field. FrontMCP doesn't use it for tools/call, resources/read or prompts/get: the JSON-RPC code follows statusCode like any other public error, so a remote "not found" is -32602.

Providers

All internal (-32603, hidden in production), thrown by FrontMCP while building or looking up providers: ProviderNotAvailableError (PROVIDER_NOT_AVAILABLE: what this.get() throws for a provider it can't find), ProviderNotRegisteredError, ProviderNotInstantiatedError, ProviderScopeMismatchError, ProviderScopedAccessError, ProviderConstructionError (Failed to construct provider "…"), ProviderDependencyError, DependencyCycleError (Circular dependency detected: A -> B -> A), InvalidDependencyScopeError, PluginDependencyError, InvalidPluginScopeError and DependencyNotFoundError.

Registries, decorators and normalization

All internal, thrown by FrontMCP, most of them while the server starts:

  • Registries: DuplicateAppIdError (DUPLICATE_APP_ID, Two apps share the id "status": …, for two App.remote() or App.esm() apps with one id; new in 1.9.3), RegistryDefinitionNotFoundError, RegistryGraphEntryNotFoundError, RegistryDependencyNotRegisteredError (Tool "X" depends on "Db", which is not registered), InvalidRegistryKindError, NameDisambiguationError, EntryValidationError, FlowNotRegisteredError, UnsupportedHookOwnerKindError, InvalidHookFlowError and RegistryNotInitializedError.
  • Decorators: InvalidDecoratorMetadataError (@FrontMcp invalid metadata for "apps": …, see @FrontMcp) and HookTargetNotDefinedError.
  • Normalization, for an entry in a providers, plugins, tools or other list that isn't what FrontMCP expects: MissingProvideError, InvalidUseClassError, InvalidUseFactoryError, InvalidUseValueError and InvalidEntityError (Invalid Tool 'X'. Expected a class or function.).

Transport

Internal, thrown by FrontMCP's transports: MethodNotImplementedError, UnsupportedTransportTypeError, InvalidTransportSessionError, TransportNotConnectedError, TransportAlreadyStartedError, TransportServiceNotAvailableError and SessionClaimConflictError. TransportBusRequiredError is never thrown. Two are public: UnsupportedContentTypeError (UNSUPPORTED_CONTENT_TYPE, for an older client's SSE message with the wrong content type) and PayloadTooLargeError (PAYLOAD_TOO_LARGE, HTTP 413).

ESM packages

Loading apps from npm covers how a package fails to load. For an App.esm() package, FrontMCP throws these while the server starts, so each one stops it:

ClasscodeWhen
EsmVersionResolutionErrorESM_VERSION_RESOLUTION_ERRORNo version matches the range, or the registry doesn't have the package: Failed to resolve version for "@acme/nope@1.0.0": Package "@acme/nope" not found in registry at ….
EsmRegistryAuthErrorESM_REGISTRY_AUTH_ERRORThe registry answered 401 or 403.
EsmPackageLoadErrorESM_PACKAGE_LOAD_ERRORThe package's bundle couldn't be fetched or run: Failed to load ESM package "@acme/status-tools"@1.0.0: ….
EsmManifestInvalidErrorESM_MANIFEST_INVALIDThe package loaded, but doesn't export a FrontMCP package: Invalid manifest in ESM package "…": ESM module does not export a valid FrontMcpPackageManifest. …
EsmCacheErrorESM_CACHE_ERRORThe bundle couldn't be cached.
EsmInvalidSpecifierErrorESM_INVALID_SPECIFIERThrown by App.esm() itself, as soon as it is called, for a specifier that isn't a package name with an optional range.

Changed in 1.9.2: before, these were exported but never thrown, and a package that failed to load failed with a plain Error.

Since 1.9.1, for a single tool, resource or prompt loaded with Tool.esm(), Tool.remote() and the like: ExternalEntryLoadError, when its package doesn't load or its server can't be reached, ExternalEntryNotFoundError, when the package or server has no entry by that name, and ExternalEntryNotSupportedError, for an agent, skill or job loaded that way, which FrontMCP refuses. All three stop the server from starting.

Everything else

Internal, thrown by FrontMCP: FlowExitedWithoutOutputError (Flow exited without producing output: a tool's execute() returned undefined), RequestContextNotAvailableError (this.context used outside a request), ContextExtensionNotAvailableError, ServerNotFoundError, ConfigNotFoundError, RequiredConfigUndefinedError, SessionVerificationFailedError, ScopeConfigurationError, HaConfigurationError, InvokeStateMissingKeyError, FlowInputMissingError, DynamicAdapterNameError, ServerlessHandlerNotInitializedError, VercelKvNotSupportedError, the skill errors InvalidSkillError, SkillInstructionFetchError, InvalidInstructionSourceError, SkillSessionError and SkillContextNotImplementedError, the agent errors AgentConfigKeyNotFoundError, AgentToolExecutionError and AgentMethodNotAvailableError, and ToolNameConflictError, for a tool added with registerTool() under a name the server already has (new in 1.9). SkillValidationError is public: it stops the server from starting when a skill with toolValidation: "strict" names a tool the server doesn't have (Skill '…' failed tool validation: missing tools […], since 1.9; see @Skill). GlobalConfigNotFoundError (Plugin "cache" requires global "redis" configuration) is public, and so is UnenforcedMetadataError (UNENFORCED_METADATA), which stops the server from starting when an entry declares a plugin's option, like approval, and no plugin that enforces it covers the entry: see @Plugin. VercelKvAsyncInitRequiredError and EnclaveExecutionError are never thrown.

FlowControl isn't an error to report: it's how this.fail() and this.respond() end a call, and its message is empty. See A tool call succeeds after this.fail().

Caveats

  • Fail with a public class for anything the model should read. PublicMcpError, or one of InvalidInputError, RateLimitError, QuotaExceededError, UnauthorizedError and ResourceNotFoundError, or a subclass of your own.
  • Throwing and failing differ only for errors that aren't public: TOOL_EXECUTION_ERROR when thrown, the error's own code through this.fail(). Production hides both.
  • In a resource or prompt, statusCode picks the JSON-RPC code. A PublicMcpError with status 404 is -32602, like any other client error; 401 and 403 are the ones that change it.
  • A development client reads more than a production one: the stack, an internal error's message, and for a few classes, a longer developer message. Test what production sends with createErrorHandler({ isDevelopment: false }).
  • Many exported classes are never thrown in 1.9.3, as the tables say. Don't wait for them in a client.

Usage

Failing with a built-in class

Each sample below is a class you might fail a tool with. fail_with fails with the one you name; the test checks what a client reads for each, in development. Try other samples in the Call tab.

Open
import {
  ConcurrencyLimitError,
  ElicitationDisabledError,
  InternalMcpError,
  InvalidInputError,
  PublicMcpError,
  QuotaExceededError,
  RateLimitError,
  SessionSecretRequiredError,
  UnauthorizedError,
} from "@frontmcp/sdk";

export const samples: Record<string, Error> = {
  public: new PublicMcpError("There's no ticket T-9."),
  publicWithCode: new PublicMcpError("Ticket T-2 is archived. Ask a lead to restore it.", "TICKET_ARCHIVED", 409),
  invalidInput: new InvalidInputError("`due` must be in the future.", { field: "due" }),
  rateLimit: new RateLimitError(3600),
  quota: new QuotaExceededError("export"),
  unauthorized: new UnauthorizedError("Sign in again to export tickets."),
  internal: new InternalMcpError("replica lag 12s on tickets-db-2", "DB_LAG"),
  plain: new Error("connect ECONNREFUSED 10.0.4.12:5432"),
  guard: new ConcurrencyLimitError("export_tickets", 1),
  elicitationOff: new ElicitationDisabledError(),
  authInternal: new SessionSecretRequiredError("sessions"),
};

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

What production sends

The same samples, formatted by an ErrorHandler with isDevelopment: false, which is what tools/call uses when NODE_ENV=production. Public errors keep their message; internal ones, plain errors and @frontmcp/auth's errors become an error ID; ElicitationDisabledError switches to its shorter message:

Open
import { test, expect } from "@frontmcp/testing";
import { createErrorHandler, isPublicError, toMcpError } from "@frontmcp/sdk";
import { samples } from "./samples";

const production = createErrorHandler({ isDevelopment: false });
const hidden = /^Internal FrontMCP error\. Please contact support with error ID: err_[0-9a-f]{16}$/;

const expected: Record<string, [code: string, text: string | RegExp]> = {
  public: ["PUBLIC_ERROR", "There's no ticket T-9."],
  invalidInput: ["INVALID_INPUT", '`due` must be in the future.\nDetails: {\n  "field": "due"\n}'],
  rateLimit: ["RATE_LIMIT_EXCEEDED", "Rate limit exceeded. Retry after 3600 seconds"],
  guard: ["CONCURRENCY_LIMIT", 'Concurrency limit reached for "export_tickets" (max: 1)'],
  elicitationOff: ["ELICITATION_DISABLED", "Elicitation is disabled on this server."],
  internal: ["DB_LAG", hidden],
  plain: ["SERVER_ERROR", hidden],
  authInternal: ["SERVER_ERROR", hidden],
};

for (const [sample, [code, text]] of Object.entries(expected)) {
  test(`${sample} in production`, () => {
    const result = production.handle(samples[sample]);
    expect(result._meta?.code).toBe(code);
    if (typeof text === "string") expect(result.content[0].text).toBe(text);
    else expect(result.content[0].text).toMatch(text);
    expect(result._meta).not.toHaveProperty("stack");
  });
}

test("the error ID is the one in the server's log", () => {
  const result = production.handle(samples.internal);
  expect(result._meta?.errorId).toBe((samples.internal as { errorId?: string }).errorId);
});

test("guard errors aren't McpErrors, but are sent as public ones", () => {
  expect(isPublicError(samples.guard)).toBe(false);
  expect(toMcpError(samples.guard)).toMatchObject({ code: "CONCURRENCY_LIMIT", isPublic: true });
});

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Failing vs throwing

A public error reads the same thrown or failed. The others don't: thrown from a tool's execute(), an InternalMcpError or a guard error other than a timeout is wrapped as TOOL_EXECUTION_ERROR, and a ToolCredentialsRequiredError becomes a JSON-RPC error rather than a tool result. Thrown from a resource, every guard error is wrapped, the timeout too:

Open
import {
  ConcurrencyLimitError,
  ExecutionTimeoutError,
  InternalMcpError,
  PublicMcpError,
  ResourceContext,
  ResourceTemplate,
  Tool,
  ToolContext,
  ToolCredentialsRequiredError,
  z,
} from "@frontmcp/sdk";

const make: Record<string, () => Error> = {
  public: () => new PublicMcpError("There's no ticket T-9.", "TICKET_NOT_FOUND", 404),
  internal: () => new InternalMcpError("replica lag 12s", "DB_LAG"),
  concurrency: () => new ConcurrencyLimitError("export", 1),
  timeout: () => new ExecutionTimeoutError("export", 500),
  credentials: () => new ToolCredentialsRequiredError({ toolId: "sync_crm", providers: ["crm"], authUrl: "https://desk.example.com/connect/crm" }),
};

const input = { sample: z.enum(["public", "internal", "concurrency", "timeout", "credentials"]) };
type Input = { sample: "public" | "internal" | "concurrency" | "timeout" | "credentials" };

@Tool({ name: "throw_it", description: "Throw a sample error", inputSchema: input })
export class ThrowIt extends ToolContext {
  async execute({ sample }: Input): Promise<{ ok: boolean }> {
    throw make[sample]();
  }
}

@Tool({ name: "fail_it", description: "Fail with a sample error", inputSchema: input })
export class FailIt extends ToolContext {
  async execute({ sample }: Input): Promise<{ ok: boolean }> {
    this.fail(make[sample]());
  }
}

@ResourceTemplate({ name: "thrown", uriTemplate: "thrown://{sample}", mimeType: "text/plain" })
export class Thrown extends ResourceContext<{ sample: string }> {
  async execute(uri: string, { sample }: { sample: string }): Promise<{ contents: { uri: string; text: string }[] }> {
    throw make[sample]();
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Failing in a resource or a prompt

In a resource or a prompt, the class's statusCode picks the JSON-RPC code. TicketLockedError is a public error of this server's own, with status 403:

Open
import { InternalMcpError, PublicMcpError, ResourceContext, ResourceNotFoundError, ResourceTemplate, UnauthorizedError } from "@frontmcp/sdk";

export class TicketLockedError extends PublicMcpError {
  constructor(id: string) {
    super(`Ticket ${id} is under legal hold.`, "TICKET_LOCKED", 403);
  }
}

@ResourceTemplate({ name: "ticket", uriTemplate: "tickets://{id}", mimeType: "application/json" })
export class Ticket extends ResourceContext<{ id: string }> {
  async execute(uri: string, { id }: { id: string }) {
    if (id === "T-3") this.fail(new TicketLockedError(id));
    if (id === "T-4") this.fail(new UnauthorizedError("Sign in to read archived tickets."));
    if (id === "T-5") this.fail(new PublicMcpError("The ticket store is down for maintenance.", "STORE_DOWN", 503));
    if (id === "T-6") this.fail(new InternalMcpError("replica lag 12s", "DB_LAG"));
    if (id !== "T-1") this.fail(new ResourceNotFoundError(uri));
    return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ id, title: "Cannot log in" }) }] };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Errors FrontMCP raises for you

Most errors a client sees come from FrontMCP, before or around your code. Here each test makes FrontMCP raise one:

Open
import { App, FrontMcp, Job, JobContext, Prompt, PromptContext, Resource, ResourceContext, Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "search_tickets", description: "Search tickets by title", inputSchema: { query: z.string().min(1) } })
class SearchTickets extends ToolContext {
  async execute({ query }: { query: string }) {
    return { query, tickets: [] };
  }
}

@Tool({ name: "close_ticket", description: "Close a ticket", inputSchema: { id: z.string() }, authorities: "admin" })
class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    return { id, closed: true };
  }
}

@Tool({ name: "run_on_deno", description: "Only on Deno", inputSchema: {}, availableWhen: { runtime: ["deno"] } })
class RunOnDeno extends ToolContext {
  async execute() {
    return { ok: true };
  }
}

@Tool({ name: "export_archive", description: "Export the ticket archive", inputSchema: {}, execution: { taskSupport: "required" } })
class ExportArchive extends ToolContext {
  async execute() {
    return { rows: 18_000 };
  }
}

@Prompt({ name: "summarize_ticket", description: "Summarize a ticket", arguments: [{ name: "id", required: true }] })
class SummarizeTicket extends PromptContext {
  async execute({ id }: Record<string, string>) {
    return { messages: [{ role: "user" as const, content: { type: "text" as const, text: `Summarize ticket ${id}.` } }] };
  }
}

@Resource({ name: "audit-log", uri: "desk://audit-log", mimeType: "text/plain", authorities: "admin" })
class AuditLog extends ResourceContext {
  async execute(uri: string) {
    return { contents: [{ uri, text: "T-1 closed by nour" }] };
  }
}

@Resource({ name: "deploy-notes", uri: "desk://deploy-notes", mimeType: "text/plain", availableWhen: { runtime: ["deno"] } })
class DeployNotes extends ResourceContext {
  async execute(uri: string) {
    return { contents: [{ uri, text: "Deploy with deployctl." }] };
  }
}

@Job({ name: "nightly_cleanup", description: "Close stale tickets", inputSchema: {}, outputSchema: {} })
class NightlyCleanup extends JobContext {
  async execute() {
    return {};
  }
}

@App({
  id: "help-desk",
  name: "Help Desk",
  tools: [SearchTickets, CloseTicket, RunOnDeno, ExportArchive],
  resources: [AuditLog, DeployNotes],
  prompts: [SummarizeTicket],
  jobs: [NightlyCleanup],
})
class HelpDeskApp {}

@FrontMcp({
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDeskApp],
  authorities: { profiles: { admin: { roles: { any: ["admin"] } } } },
})
export default class Server {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

ENTRY_UNAVAILABLE's message lists the server's OS, runtime and deployment, in production too, for tools, resources and prompts alike. If that's more than callers should know, leave the entry out of the server instead of relying on availableWhen.


Troubleshooting

Internal FrontMCP error. Please contact support with error ID: err_…

The server runs with NODE_ENV=production, and the call failed with an internal error: an InternalMcpError or one of FrontMCP's, a plain Error, or one of @frontmcp/auth's. Search the server's log for the error ID: FrontMCP logs the original message and stack with it. If the model should read the message, fail with a public class. See this.fail.

My error's code arrives as TOOL_EXECUTION_ERROR

It was thrown from execute() and isn't public: an InternalMcpError, a guard error, or an Error with a code property of its own. Throw a PublicMcpError subclass, or pass the error to this.fail(), which keeps an McpError's code. See Failing vs throwing.

My error's code arrives as SERVER_ERROR

It isn't an McpError: a plain Error, or one of @frontmcp/auth's internal errors, passed to this.fail(). Only McpErrors and guard errors keep their code. Wrap it: this.fail(new InternalMcpError(error.message, "CRM_SYNC_FAILED")) keeps the message out of production, with a code clients can tell apart.

A resource error arrives as -32602 when I meant "not found"

Under MCP 2026-07-28, every client error from a resource is -32602, including ResourceNotFoundError; -32002 is only for older clients. Tell errors apart by data.code, which is your class's code. ResourceNotFoundError is the exception: its data is { uri }, so check the message.

The client sees a long developer message, like Elicitation is disabled in server configuration. Enable it via …

In development, a tool's error text is the internal message, written for you. Production sends the public one: here, Elicitation is disabled on this server. Resources and prompts send the public one in both. See What production sends.

I'm waiting for AgentLoopExceededError (or QuotaExceededError, SessionMissingError, an ESM error…) and it never comes

FrontMCP 1.9.3 exports these classes but never throws them. The tables say which, and what's thrown instead, like TOOL_EXECUTION_ERROR for an agent that runs out of turns.