# Errors

A **tool error** is a successful JSON-RPC result with `isError: true` that the model reads and recovers from. A **protocol error** is a JSON-RPC error response the model never sees.

## Return a tool error with `isError`

Return `isError: true` from a tool handler to report a failure the model should see.

```ts
import { McpServer, ProtocolError, ProtocolErrorCode, ResourceNotFoundError, ResourceTemplate } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';

const notes = new Map([['welcome', 'Read tools.md first.']]);

const server = new McpServer({ name: 'notes', version: '1.0.0' });

server.registerTool(
    'read-note',
    {
        description: 'Read a note by its id',
        inputSchema: z.object({ id: z.string() })
    },
    async ({ id }) => {
        const note = notes.get(id);
        if (!note) {
            return {
                content: [{ type: 'text', text: `No note with id "${id}". Known ids: ${[...notes.keys()].join(', ')}` }],
                isError: true
            };
        }
        return { content: [{ type: 'text', text: note }] };
    }
);
```

Every call on this page comes from an in-memory `Client` connected to the server above — [Test a server](https://ts.sdk.modelcontextprotocol.io/v2/testing.md) shows that wiring. Call `read-note` with an id that does not exist.

```ts
const missing = await client.callTool({ name: 'read-note', arguments: { id: 'drafts' } });
console.log(missing);
```

The `tools/call` response is an ordinary result:

```
{
  content: [
    {
      type: 'text',
      text: 'No note with id "drafts". Known ids: welcome'
    }
  ],
  isError: true
}
```

The model reads the message, sees `welcome` in it, and retries with an id that exists. Put the recovery hint in `text` — it is the only thing the model has to work with.

## Let a thrown exception become a tool error

Throw instead: the SDK catches anything a tool handler throws and converts it to the same `isError: true` shape.

```ts
server.registerTool(
    'delete-note',
    {
        description: 'Delete a note by its id',
        inputSchema: z.object({ id: z.string() })
    },
    async ({ id }) => {
        if (!notes.delete(id)) {
            throw new Error(`Cannot delete "${id}": no such note`);
        }
        return { content: [{ type: 'text', text: `Deleted "${id}"` }] };
    }
);
```

Call `delete-note` with the same missing id.

```ts
const thrown = await client.callTool({ name: 'delete-note', arguments: { id: 'drafts' } });
console.log(thrown);
```

The exception's `message` becomes the result's `content` text:

```
{
  content: [ { type: 'text', text: 'Cannot delete "drafts": no such note' } ],
  isError: true
}
```

A throw and an explicit `isError: true` produce the same shape; returning explicitly gives you control over `content`. The SDK skips `outputSchema` validation on any `isError` result.

## Throw a protocol error

Resource, prompt, and completion callbacks have no `isError` channel. Throw `ProtocolError(code, message, data?)` when the request itself is wrong.

```ts
server.registerResource(
    'note',
    new ResourceTemplate('note://{id}', { list: undefined }),
    { description: 'A note by its id' },
    async (uri, { id }) => {
        const noteId = String(id);
        if (!/^[a-z]+$/.test(noteId)) {
            throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Note ids are lowercase letters, got "${noteId}"`);
        }
        const note = notes.get(noteId);
        if (!note) throw new ResourceNotFoundError(uri.href);
        return { contents: [{ uri: uri.href, text: note }] };
    }
);
```

::: info Coming from v1?
`ProtocolError` and `ProtocolErrorCode` replace v1's `McpError` and `ErrorCode` — run the codemod, then see the [upgrade guide](https://ts.sdk.modelcontextprotocol.io/v2/migration/upgrade-to-v2.md).
:::

Read the resource with an id the callback rejects.

```ts
try {
    await client.readResource({ uri: 'note://42' });
} catch (error) {
    const { code, message } = error as ProtocolError;
    console.log({ code, message });
}
```

`readResource` rejects with a `ProtocolError` carrying the wire fields:

```
{ code: -32602, message: 'Note ids are lowercase letters, got "42"' }
```

On the wire this is a JSON-RPC error response — `{ code, message, data? }` instead of a `result` — and the host's MCP client handles it; the model never sees it. A non-`ProtocolError` exception thrown from one of these callbacks surfaces as `-32603` Internal Error with the exception's message.

## Choose between tool error and protocol error

Pick by audience. The model drives `tools/call`, so a failure it can recover from — a missing record, a bad argument, a transient upstream fault — belongs in `isError: true` with a message that names the fix. The host application drives `resources/read`, `prompts/get`, and `completion/complete`, so failures there are protocol errors addressed to the caller's code.

The handler decides which channel exists:

- A tool handler produces only tool errors. The SDK converts every exception it throws — including a thrown `ProtocolError` — into an `isError: true` result. `UrlElicitationRequiredError` is the one exception; it propagates as a JSON-RPC error so the host can open the URL — see [Elicitation](https://ts.sdk.modelcontextprotocol.io/v2/servers/elicitation.md).
- A resource, prompt, or completion callback produces only protocol errors. Throw a `ProtocolError`.

## Use the typed error subclasses

Each subclass picks the right `ProtocolErrorCode` and packs structured `data` for you. `ResourceNotFoundError` takes the missing URI — the read callback above already throws it for a well-formed id with no note.

```ts
try {
    await client.readResource({ uri: 'note://archived' });
} catch (error) {
    const { code, message, data } = error as ResourceNotFoundError;
    console.log({ code, message, data });
}
```

The error carries the requested URI in `data` and the code the spec mandates for a `resources/read` miss:

```
{
  code: -32602,
  message: 'Resource not found: note://archived',
  data: { uri: 'note://archived' }
}
```

Three more subclasses cover the other structured protocol errors:

- `UrlElicitationRequiredError(elicitations)` — `-32042`; the only error a tool handler can propagate. See [Elicitation](https://ts.sdk.modelcontextprotocol.io/v2/servers/elicitation.md).
- `UnsupportedProtocolVersionError({ supported, requested })` — `-32022`; `data.supported` lets the peer pick a version and retry.
- `MissingRequiredClientCapabilityError({ requiredCapabilities })` — `-32021`; `data.requiredCapabilities` names exactly what the client must declare.

Match these by `code` and `data` shape when peers may run pre-brand SDK copies or hand you plain wire shapes; on brand-aware releases `instanceof` also matches across separately bundled copies of the SDK. The same check is available as an explicit static guard — `ProtocolError.isInstance(err)`, `ResourceNotFoundError.isInstance(err)` — which narrows in TypeScript and reads the same brand.

## Look up a protocol error code

`ProtocolErrorCode` is the complete vocabulary of wire codes the SDK sends and recognizes.

| Member | Code | Meaning |
| --- | --- | --- |
| `ParseError` | `-32700` | The message was not valid JSON. |
| `InvalidRequest` | `-32600` | The message was not a valid JSON-RPC request. |
| `MethodNotFound` | `-32601` | No handler is registered for the method. |
| `InvalidParams` | `-32602` | The params are wrong — also the code for a `resources/read` miss. |
| `InternalError` | `-32603` | The handler threw something other than a `ProtocolError`. |
| `ResourceNotFound` | `-32002` | Receive-tolerated only: the SDK answers a `resources/read` miss with `-32602` and never emits `-32002`. Throw `ResourceNotFoundError` instead. |
| `MissingRequiredClientCapability` | `-32021` | The request needs a capability the client did not declare. |
| `UnsupportedProtocolVersion` | `-32022` | The requested protocol version is unknown to the receiver or unsupported by it. |
| `UrlElicitationRequired` | `-32042` | The tool needs the user to visit a URL before it can complete. |

`-32021` and `-32022` are new in protocol revision 2026-07-28 — see [Protocol versions](https://ts.sdk.modelcontextprotocol.io/v2/protocol-versions.md).

## Recap

- `isError: true` is a successful JSON-RPC result carrying a tool failure the model reads and acts on.
- A tool handler that throws produces the same `isError: true` result; the exception's `message` becomes the `content` text.
- A tool handler cannot produce a protocol error — only `UrlElicitationRequiredError` escapes.
- `ProtocolError` and its subclasses, thrown from resource, prompt, and completion callbacks, become JSON-RPC error responses the model never sees.
- `ResourceNotFoundError` and the other subclasses pick the code and pack structured `data`; match them by `code` and `data` — or, on brand-aware releases, by `instanceof`.
- The table above lists every `ProtocolErrorCode` member.
