# Matchers

> The MCP and widget matchers @frontmcp/testing adds to expect, and the Jest matchers you can use with them in the Playground.

Source: https://frontmcp.dev/reference/testing/matchers

`@frontmcp/testing` adds matchers to Jest's `expect` that understand MCP: lists of tools and resources, tool results, resource contents, prompts, raw JSON-RPC responses, and the widget a tool with a `ui` sends. Their failure messages say what they found instead, like the names of the tools that were listed. Every matcher works with `.not`.

```ts
expect(await mcp.tools.list()).toContainTool("search_tickets");
expect(await mcp.tools.call("search_tickets", { query: "log" })).toBeSuccessful();
```

---

## Reference

### MCP matchers

`expect` comes from `@frontmcp/testing`, with the MCP matchers already added:

```ts
import { test, expect } from "@frontmcp/testing";
```

[See examples of each below.](#usage)

For lists:

| Matcher | Use it on | Passes when |
| --- | --- | --- |
| `toContainTool(name)` | `mcp.tools.list()` | A tool has this `name`. |
| `toContainResource(uri)` | `mcp.resources.list()` | A resource has this `uri`. |
| `toContainResourceTemplate(uriTemplate)` | `mcp.resources.listTemplates()` | A template has this `uriTemplate`, written the same way. |
| `toContainPrompt(name)` | `mcp.prompts.list()` | A prompt has this `name`. |

For results:

| Matcher | Use it on | Passes when |
| --- | --- | --- |
| `toBeSuccessful()` | Any result wrapper | `isSuccess` is `true`. |
| `toBeError(code?)` | Any result wrapper | `isError` is `true`. With a number, the JSON-RPC error's `code` must also equal it. With a string, like `"INVALID_INPUT"`, the tool error's `_meta.code` must. |
| `toHaveTextContent(text?)` | `mcp.tools.call()`, `mcp.resources.read()` | The result has text. With `text`, the first text block contains it. |
| `toHaveImageContent()` | `mcp.tools.call()` | A content block has type `"image"`. |
| `toHaveResourceContent()` | `mcp.tools.call()` | A content block has type `"resource"`, an embedded resource. |
| `toHaveMimeType(type)` | `mcp.resources.read()` | The first content block's `mimeType` is `type`. |
| `toHaveMessages(count)` | `mcp.prompts.get()` | The prompt has exactly `count` messages. |

For raw responses:

| Matcher | Use it on | Passes when |
| --- | --- | --- |
| `toBeValidJsonRpc()` | `mcp.raw.request()` | The response has `jsonrpc: "2.0"`, an `id`, and exactly one of `result` or `error`. |
| `toHaveResult()` | `mcp.raw.request()` | The response has a `result`. |
| `toHaveError()` | `mcp.raw.request()` | The response has an `error`. |
| `toHaveErrorCode(code)` | `mcp.raw.request()` | The response's `error.code` is `code`. |

### UI matchers

A tool with a [`ui`](https://frontmcp.dev/reference/ui) option, served inline (the default), sends its page in the result's `_meta["ui/html"]`, next to `ui/type` and `ui/mimeType`. These matchers check that page, or the result's `_meta`. Pass them what `mcp.tools.call()` returns, or `result.raw`; the page matchers also take the HTML as a string. They're in `expect` under `frontmcp test`. The Playground's Tests tab doesn't have them, so the [example below](#checking-a-tools-widget) runs in a project.

For the page:

| Matcher | Passes when |
| --- | --- |
| `toHaveRenderedHtml()` | `_meta["ui/html"]` is a non-empty string that doesn't contain the text `mdx-fallback`, a marker no FrontMCP 1.9.4 package writes. |
| `toContainHtmlElement(tag)` | The page has an opening tag `<tag>` or `<tag …>`, in any case. |
| `toContainBoundValue(value)` | The page contains `String(value)`. `value` is a string or a number. |
| `toBeXssSafe()` | The page has no `<script` tag, no event handler like `onclick=` after a space (anywhere, text included), and no `javascript:`. It also passes when there's no page. A FrontMCP page always fails it: see [Caveats](#caveats). |
| `toHaveWidgetMetadata()` | `_meta` has `ui/html`, `ui/mimeType` or a `ui` object. |
| `toHaveCssClass(className)` | A `class` (or `className`) attribute contains `className` between word boundaries, so `"ticket"` matches `class="ticket-card"` too. |
| `toNotContainRawContent(text)` | The page doesn't contain `text`. It also passes when there's no page. |
| `toHaveProperHtmlStructure()` | The page has at least one tag, and doesn't contain both `&lt;` and `&gt;`. |

For `_meta`:

| Matcher | Passes when |
| --- | --- |
| `toHaveMetaKey(key)` | `_meta` has `key`, like `"ui/html"`. |
| `toNotHaveMetaKey(key)` | `_meta` doesn't have `key`. It also passes when there's no `_meta`. |
| `toHaveMetaValue(key, value)` | `_meta[key]` has the same `JSON.stringify()` as `value`, so the order of an object's keys counts. |
| `toHaveOnlyNamespacedMeta(prefix)` | Every key of `_meta` starts with `prefix`, like `"ui/"`, and there's at least one. |
| `toHavePlatformMeta(platform)` | A key of `_meta` starts with `ui/`, and none starts with `openai/` or `frontmcp/`. |
| `toHavePlatformMimeType(platform)` | `_meta["ui/mimeType"]` is `"text/html;profile=mcp-app"`. |
| `toHavePlatformHtml(platform)` | `_meta["ui/html"]` is a non-empty string. |

`platform` is a client platform name, like `"openai"`, `"claude"` or `"generic-mcp"`. In 1.9.4 the three platform matchers expect the same of every platform, so the name you pass changes nothing.

`@frontmcp/testing` also exports the matchers as `uiMatchers`, and `UIAssertions`: similar checks as functions that throw, like `UIAssertions.assertRenderedUI(result)`, which returns the page.

### Jest matchers in the Playground

Under `frontmcp test`, `expect` is Jest's, so every Jest matcher works. The Playground's Tests tab implements these:

| Matcher | Passes when |
| --- | --- |
| `toBe(value)` | The value is `value`, compared with `Object.is`. |
| `toEqual(value)` | The value has the same structure. Properties set to `undefined` are ignored. |
| `toStrictEqual(value)` | Like `toEqual`, but `undefined` properties and classes count. |
| `toMatchObject(object)` | The value has at least the properties in `object`, recursively. |
| `toHaveProperty(path, value?)` | The property at `path` (`"a.b"` or `["a", "b"]`) exists, and equals `value` if given. |
| `toContain(item)` | An array contains `item` (compared with `===`), or a string contains the substring. |
| `toContainEqual(item)` | An array contains an item equal to `item`. |
| `toHaveLength(n)` | The value's `length` is `n`. |
| `toBeDefined()`, `toBeUndefined()`, `toBeNull()` | The value is not `undefined`, is `undefined`, or is `null`. |
| `toBeTruthy()`, `toBeFalsy()` | The value is truthy, or falsy. |
| `toBeNaN()` | The value is `NaN`. |
| `toBeGreaterThan(n)`, `toBeGreaterThanOrEqual(n)`, `toBeLessThan(n)`, `toBeLessThanOrEqual(n)` | The number compares that way with `n`. |
| `toBeCloseTo(n, digits?)` | The number is `n`, to `digits` decimal places (default 2). |
| `toMatch(pattern)` | The string matches a regular expression, or contains a string. |
| `toBeInstanceOf(Class)` | The value is an instance of `Class`. |
| `toThrow(expected?)` | The function throws. `expected` can be part of the message, a regular expression, or an error class. |

And these modifiers and helpers:

| Modifier or helper | Description |
| --- | --- |
| `.not` | Inverts the matcher that follows. |
| `.resolves`, `.rejects` | Waits for a promise, then matches its value or its error. `await` the `expect`. |
| `expect.any(Class)` | Matches any value of that type, like `expect.any(String)`. |
| `expect.anything()` | Matches anything except `null` and `undefined`. |
| `expect.objectContaining(object)` | Matches an object with at least these properties. |
| `expect.arrayContaining(array)` | Matches an array containing these items, in any order. |
| `expect.stringContaining(text)` | Matches a string containing `text`. |
| `expect.stringMatching(pattern)` | Matches a string that matches `pattern`. |

#### Caveats

- **A numeric `toBeError(code)` only matches JSON-RPC errors.** A tool that fails returns a result with `isError: true` and no JSON-RPC code, so `toBeError()` passes and `toBeError(-32602)` doesn't. Its kind is in `result.raw._meta?.code`, and `toBeError("INVALID_INPUT")`, with a string, checks it.
- Some error codes depend on the protocol version. Reading an unknown resource fails with `-32602` under 2026-07-28, which the Playground speaks, and `-32002` under 2025-06-18, which `frontmcp test`'s client speaks.
- `toHaveTextContent(text)` and `toHaveMimeType()` look at the first text block, or the first content block. Check `result.raw` for the others.
- The list matchers compare names and URIs exactly, and don't expand templates: `toContainResourceTemplate("ticket://{ticketId}")` doesn't match `ticket://{id}`.
- **The UI matchers read the whole page, not only your template.** FrontMCP's page carries its [bridge script](https://frontmcp.dev/reference/ui/hosts#the-bridge) and the call's input and output as JSON. So `toBeXssSafe()` fails for every tool served inline, `toContainHtmlElement("script")` always passes, and `toContainBoundValue("T-1")` passes even when the template never shows the id. To check that a value was escaped, look for its escaped form with `toContainBoundValue("&lt;…")`, and make sure the tag didn't render with `not.toContainHtmlElement()`.
- **Escaped values fail `toHaveProperHtmlStructure()`.** A value like `<img …>`, which the template's `html` helper escapes, puts both `&lt;` and `&gt;` in the page.
- **A widget that isn't served inline has no page in the result.** For `servingMode: "static"`, the result's `_meta` is empty, so `toHaveRenderedHtml()`, `toHaveWidgetMetadata()` and the matchers that look for a `ui/*` key fail. Read its page with `mcp.resources.read("ui://widget/<tool>.html")` instead, and check the text with Jest's matchers ([The widget resource](https://frontmcp.dev/reference/ui#the-widget-resource)).
- **TypeScript only knows the eight page matchers.** The seven `_meta` matchers run under `frontmcp test`, which compiles tests without type-checking them, but your editor and `tsc` reject them ([Troubleshooting](#property-tohaveplatformmeta-does-not-exist-on-type-mcpexpectmatchersvoid-toolresultwrapper)). Check `_meta` with Jest's matchers where that matters, like `expect(result.raw._meta?.["ui/type"]).toBe("html")`.

---

## Usage

### Checking what the server offers

The list matchers take the arrays that `list()` returns.

```ts help-desk.ts
import { Prompt, PromptContext, Resource, ResourceContext, ResourceTemplate, Tool, ToolContext, z } from "@frontmcp/sdk";

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

@Resource({ name: "sla", uri: "policy://sla", description: "Reply times by priority" })
export class SlaPolicy extends ResourceContext {
  async execute() {
    return { high: "4 hours" };
  }
}

@ResourceTemplate({ name: "ticket", uriTemplate: "ticket://{id}", description: "One ticket by id" })
export class TicketResource extends ResourceContext<{ id: string }> {
  async execute(uri: string, { id }: { id: string }) {
    return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ id }) }] };
  }
}

@Prompt({ name: "triage", description: "Triage a ticket", arguments: [{ name: "id", required: true }] })
export class TriagePrompt extends PromptContext {
  async execute(args: Record<string, string>) {
    return { messages: [{ role: "user", content: { type: "text", text: `Triage ticket ${args.id}.` } }] };
  }
}
```

```ts lists.test.ts active
import { test, expect } from "@frontmcp/testing";

test("toContainTool", async ({ mcp }) => {
  const tools = await mcp.tools.list();
  expect(tools).toContainTool("search_tickets");
  expect(tools).not.toContainTool("delete_ticket");
});

test("toContainResource", async ({ mcp }) => {
  expect(await mcp.resources.list()).toContainResource("policy://sla");
});

test("toContainResourceTemplate", async ({ mcp }) => {
  const templates = await mcp.resources.listTemplates();
  expect(templates).toContainResourceTemplate("ticket://{id}");
  expect(templates).not.toContainResourceTemplate("ticket://{ticketId}");
});

test("toContainPrompt", async ({ mcp }) => {
  expect(await mcp.prompts.list()).toContainPrompt("triage");
});
```

### Checking a tool's result

The result matchers take what `mcp.tools.call()` returns.

```ts tools.ts
import { PublicMcpError, Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "get_ticket", description: "Get one ticket by id", inputSchema: { id: z.string() } })
export class GetTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    if (id !== "T-1") this.fail(new PublicMcpError(`There's no ticket ${id}.`));
    return { id, title: "Cannot log in" };
  }
}

@Tool({ name: "ticket_chart", description: "A chart of open tickets per day", inputSchema: {}, outputSchema: "image" })
export class TicketChart extends ToolContext {
  async execute() {
    return { type: "image", data: "iVBORw0KGgo=", mimeType: "image/png" };
  }
}

@Tool({ name: "attach_log", description: "Attach a ticket's error log", inputSchema: { id: z.string() }, outputSchema: "resource" })
export class AttachLog extends ToolContext {
  async execute({ id }: { id: string }) {
    return { type: "resource", resource: { uri: `logs://${id}`, mimeType: "text/plain", text: "Error: session expired" } };
  }
}
```

```ts results.test.ts active
import { test, expect } from "@frontmcp/testing";

test("toBeSuccessful and toHaveTextContent", async ({ mcp }) => {
  const result = await mcp.tools.call("get_ticket", { id: "T-1" });
  expect(result).toBeSuccessful();
  expect(result).toHaveTextContent("Cannot log in");
});

test("toBeError, for a tool error", async ({ mcp }) => {
  const result = await mcp.tools.call("get_ticket", { id: "T-9" });
  expect(result).toBeError();
  expect(result).not.toBeSuccessful();
  expect(result).toHaveTextContent("no ticket T-9");
});

test("toHaveImageContent", async ({ mcp }) => {
  const result = await mcp.tools.call("ticket_chart");
  expect(result).toHaveImageContent();
  expect(result).not.toHaveTextContent();
});

test("toHaveResourceContent", async ({ mcp }) => {
  expect(await mcp.tools.call("attach_log", { id: "T-1" })).toHaveResourceContent();
});
```

### Checking a tool's widget

The UI matchers take what `mcp.tools.call()` returns for a tool with a `ui`. The Playground doesn't have them, so these files are a project's, run with `npx frontmcp test`. `get_ticket` shows a ticket as a card, and `T-2` is a ticket a customer filed with markup in its title:

```ts src/tickets.tool.ts
import { Tool, ToolContext, z, type TemplateContext } from "@frontmcp/sdk";

type Ticket = { id: string; title: string; status: "open" | "closed" };

const tickets: Record<string, Ticket> = {
  "T-1": { id: "T-1", title: "Cannot log in", status: "open" },
  "T-2": { id: "T-2", title: "<img src=x onerror=alert(1)>", status: "open" },
};

const card = (ctx: TemplateContext<{ id: string }, Ticket>) =>
  ctx.helpers.html`<article class="ticket-card ${ctx.output.status}"><h2>${ctx.output.title}</h2></article>`;

@Tool({
  name: "get_ticket",
  description: "Get one support ticket by its id, shown as a card",
  inputSchema: { id: z.string() },
  ui: { template: card },
})
export class GetTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    return tickets[id];
  }
}
```

```ts src/main.ts
import { App, FrontMcp } from "@frontmcp/sdk";
import { GetTicket } from "./tickets.tool";

@App({ id: "help-desk", name: "Help Desk", tools: [GetTicket] })
class HelpDeskApp {}

@FrontMcp({ info: { name: "help-desk", version: "1.0.0" }, apps: [HelpDeskApp], http: { port: Number(process.env.PORT ?? 3000) } })
export default class Server {}
```

```ts e2e/widget.e2e.spec.ts
import { test, expect } from "@frontmcp/testing";

test.use({ server: "./src/main.ts" });

test("get_ticket shows the ticket as a card", async ({ mcp }) => {
  const result = await mcp.tools.call("get_ticket", { id: "T-1" });
  expect(result).toHaveRenderedHtml();
  expect(result).toContainHtmlElement("article");
  expect(result).toHaveCssClass("ticket-card");
  expect(result).toContainBoundValue("Cannot log in");
});

test("the page travels in the result's _meta", async ({ mcp }) => {
  const result = await mcp.tools.call("get_ticket", { id: "T-1" });
  expect(result).toHaveWidgetMetadata();
  expect(Object.keys(result.raw._meta ?? {})).toEqual(["ui/html", "ui/type", "ui/mimeType"]);
});

test("a title with markup in it is escaped, not rendered", async ({ mcp }) => {
  const result = await mcp.tools.call("get_ticket", { id: "T-2" });
  expect(result).toContainBoundValue("&lt;img src=x onerror=alert(1)&gt;");
  expect(result).not.toContainHtmlElement("img");
});
```

The last test is the check `toBeXssSafe()` can't make here: the page has FrontMCP's own `<script>`, so that matcher fails for every inline widget ([Caveats](#caveats)). What matters is that the customer's markup arrived as text. To see the page these tests check, call the tool in a Playground with a **Widget** tab, as on [Tools with a UI](https://frontmcp.dev/learn/tools-with-a-ui).

### Checking a resource read

`mcp.resources.read()` returns a wrapper that works with `toHaveMimeType()`, `toHaveTextContent()` and the result matchers. A read that fails is a JSON-RPC error, so `toBeError(code)` can check its code: `-32602` here, and `-32002` under `frontmcp test` (see [Caveats](#caveats)).

```ts sla.resource.ts
import { Resource, ResourceContext } from "@frontmcp/sdk";

@Resource({ name: "sla", uri: "policy://sla", description: "Reply times by priority" })
export class SlaPolicy extends ResourceContext {
  async execute() {
    return { high: "4 hours", normal: "1 business day" };
  }
}
```

```ts read.test.ts active
import { test, expect } from "@frontmcp/testing";

test("toHaveMimeType and toHaveTextContent", async ({ mcp }) => {
  const content = await mcp.resources.read("policy://sla");
  expect(content).toBeSuccessful();
  expect(content).toHaveMimeType("application/json");
  expect(content).toHaveTextContent("4 hours");
});

test("toBeError with a code", async ({ mcp }) => {
  const content = await mcp.resources.read("policy://refunds");
  expect(content).toBeError(-32602);
});
```

### Checking a prompt

`toHaveMessages()` counts a prompt's messages. Check their contents with Jest's matchers.

```ts triage.prompt.ts
import { Prompt, PromptContext } from "@frontmcp/sdk";

@Prompt({ name: "triage", description: "Triage a ticket", arguments: [{ name: "id", required: true }] })
export class TriagePrompt extends PromptContext {
  async execute(args: Record<string, string>) {
    return {
      messages: [
        { role: "user", content: { type: "text", text: `Read ticket ${args.id}.` } },
        { role: "assistant", content: { type: "text", text: "Which priority rules apply?" } },
        { role: "user", content: { type: "text", text: "Use the SLA policy at policy://sla." } },
      ],
    };
  }
}
```

```ts prompt.test.ts active
import { test, expect } from "@frontmcp/testing";

test("toHaveMessages", async ({ mcp }) => {
  const prompt = await mcp.prompts.get("triage", { id: "T-1" });
  expect(prompt).toHaveMessages(3);
  expect(prompt.messages.map((m) => m.role)).toEqual(["user", "assistant", "user"]);
  expect(prompt.messages[0].content).toMatchObject({ text: "Read ticket T-1." });
});
```

### Checking a raw response

The JSON-RPC matchers take the response from `mcp.raw.request()`, which includes the error when there is one.

```ts ping.tool.ts
import { Tool, ToolContext } from "@frontmcp/sdk";

@Tool({ name: "ping", description: "Reply with pong", inputSchema: {} })
export class Ping extends ToolContext {
  async execute() {
    return { reply: "pong" };
  }
}
```

```ts raw.test.ts active
import { test, expect } from "@frontmcp/testing";

test("toBeValidJsonRpc and toHaveResult", async ({ mcp }) => {
  const response = await mcp.raw.request({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} });
  expect(response).toBeValidJsonRpc();
  expect(response).toHaveResult();
  expect(response).not.toHaveError();
});

test("toHaveError and toHaveErrorCode", async ({ mcp }) => {
  const response = await mcp.raw.request({ jsonrpc: "2.0", id: 2, method: "tickets/list", params: {} });
  expect(response).toBeValidJsonRpc();
  expect(response).toHaveError();
  expect(response).toHaveErrorCode(-32601);
});
```

### Using Jest's matchers

Every Jest matcher the Playground implements, on a real tool result:

```ts get-ticket.tool.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "get_ticket", description: "Get one ticket by id", inputSchema: { id: z.string() } })
export class GetTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    return { id, title: "Cannot log in", hoursOpen: 26.5, tags: ["login", "web"], assignee: null, customer: { name: "Acme", plan: "pro" } };
  }
}
```

```ts jest.test.ts active
import { test, expect } from "@frontmcp/testing";

const getT1 = async (mcp: any) => (await mcp.tools.call("get_ticket", { id: "T-1" })).json();

test.describe("comparing values", () => {
  test("toBe, toEqual, toStrictEqual", async ({ mcp }) => {
    const ticket = await getT1(mcp);
    expect(ticket.id).toBe("T-1");
    expect(ticket.tags).toEqual(["login", "web"]);
    expect({ ...ticket.customer }).toStrictEqual({ name: "Acme", plan: "pro" });
    expect({ id: "T-1", note: undefined }).toEqual({ id: "T-1" });
    expect({ id: "T-1", note: undefined }).not.toStrictEqual({ id: "T-1" });
  });

  test("toMatchObject and toHaveProperty", async ({ mcp }) => {
    const ticket = await getT1(mcp);
    expect(ticket).toMatchObject({ id: "T-1", customer: { plan: "pro" } });
    expect(ticket).toHaveProperty("customer.name", "Acme");
    expect(ticket).toHaveProperty(["tags", "0"], "login");
    expect(ticket).not.toHaveProperty("closedAt");
  });
});

test.describe("checking kinds of value", () => {
  test("toBeDefined, toBeUndefined, toBeNull, toBeTruthy, toBeFalsy, toBeNaN", async ({ mcp }) => {
    const ticket = await getT1(mcp);
    expect(ticket.title).toBeDefined();
    expect(ticket.closedAt).toBeUndefined();
    expect(ticket.assignee).toBeNull();
    expect(ticket.tags.length).toBeTruthy();
    expect(ticket.assignee).toBeFalsy();
    expect(Number(ticket.title)).toBeNaN();
  });

  test("toBeInstanceOf", async ({ mcp }) => {
    const ticket = await getT1(mcp);
    expect(ticket.tags).toBeInstanceOf(Array);
  });
});

test.describe("numbers, strings and arrays", () => {
  test("number comparisons and toBeCloseTo", async ({ mcp }) => {
    const { hoursOpen } = await getT1(mcp);
    expect(hoursOpen).toBeGreaterThan(24);
    expect(hoursOpen).toBeGreaterThanOrEqual(26.5);
    expect(hoursOpen).toBeLessThan(48);
    expect(hoursOpen).toBeLessThanOrEqual(26.5);
    expect(hoursOpen / 3).toBeCloseTo(8.83);
  });

  test("toMatch, toContain, toContainEqual, toHaveLength", async ({ mcp }) => {
    const ticket = await getT1(mcp);
    expect(ticket.id).toMatch(/^T-\d+$/);
    expect(ticket.title).toContain("log in");
    expect(ticket.tags).toContain("web");
    expect([ticket.customer]).toContainEqual({ name: "Acme", plan: "pro" });
    expect(ticket.tags).toHaveLength(2);
  });
});

test.describe("errors, promises and asymmetric matchers", () => {
  test("toThrow", () => {
    expect(() => JSON.parse("{")).toThrow(SyntaxError);
    expect(() => {
      throw new Error("No ticket T-9");
    }).toThrow("T-9");
  });

  test(".resolves and .rejects", async ({ mcp }) => {
    await expect(getT1(mcp)).resolves.toMatchObject({ id: "T-1" });
    await expect(Promise.reject(new Error("timed out"))).rejects.toThrow(/timed/);
  });

  test("expect.any, anything, objectContaining, arrayContaining, stringContaining, stringMatching", async ({ mcp }) => {
    const ticket = await getT1(mcp);
    expect(ticket).toEqual({
      id: expect.stringMatching(/^T-/),
      title: expect.stringContaining("log"),
      hoursOpen: expect.any(Number),
      tags: expect.arrayContaining(["web"]),
      assignee: null,
      customer: expect.objectContaining({ plan: "pro" }),
    });
    expect(ticket.customer).toEqual({ name: expect.anything(), plan: "pro" });
  });
});
```

---

## Troubleshooting

### "Expected tools to contain "close_ticket", but got: [search_tickets, get_ticket]"

The server doesn't list a tool with that exact name. Compare with the names after `got:`. Names are case-sensitive, and `close-ticket` isn't `close_ticket`. The resource, template and prompt matchers print the same kind of list.

### "Expected result to be successful, but got error: unknown error"

The tool failed with a tool error, which has no JSON-RPC error message to print. Read `result.text()` to see why. The Playground prints the tool's message here instead of "unknown error".

### "Expected error code -32602, but got undefined"

The result is a tool error, not a JSON-RPC error, so it has no numeric code. Pass its FrontMCP code as a string instead, like `toBeError("INVALID_INPUT")` or `toBeError("PUBLIC_ERROR")`, which compares `result.raw._meta?.code`.

### "Expected a result wrapper object with isSuccess property"

`toBeSuccessful()` and `toBeError()` need the object `mcp.tools.call()`, `mcp.resources.read()` or `mcp.prompts.get()` returns. This message means they got something else, such as `result.json()` or a response from `mcp.raw.request()`. For raw responses, use `toHaveResult()` and `toHaveError()`.

### "Expected MIME type "application/json", but got "text/plain""

The first content block has a different `mimeType`. Set `mimeType` on the resource, or on the block it returns. See [`@Resource`](https://frontmcp.dev/reference/sdk/resource#executeuri-params) for how FrontMCP picks one.

### "Expected an array of tools, but received object"

The value is a promise, not the list. Put `await` before `mcp.tools.list()`.

### `Expected HTML to be XSS safe, but found: <script> tag`

Every page FrontMCP serves inline includes its bridge script, so `toBeXssSafe()` can't pass for a tool's widget. Check that the values your template shows are escaped instead: see [Checking a tool's widget](#checking-a-tools-widget).

### "Expected _meta to have ui/html property with rendered HTML"

The result has no page. The tool has no `ui`, its widget isn't served inline (`servingMode: "static"` or `"hybrid"`), or the matcher got `result.json()` rather than the result. Pass what `mcp.tools.call()` returns, or `result.raw`.

### "Expected proper HTML structure, but found escaped HTML entities - content may not have been rendered"

The page contains both `&lt;` and `&gt;`, which is what escaping a value like `<img …>` produces. The page was rendered; this matcher can't tell escaped data from a template that wasn't. Check the elements you expect with `toContainHtmlElement()` instead.

### `Property 'toHavePlatformMeta' does not exist on type 'McpExpectMatchers<void, ToolResultWrapper>'`

`@frontmcp/testing` 1.9.4 declares types for the eight page matchers only. `toHavePlatformMeta`, `toHaveMetaKey`, `toHaveMetaValue`, `toNotHaveMetaKey`, `toHaveOnlyNamespacedMeta`, `toHavePlatformMimeType` and `toHavePlatformHtml` run under `frontmcp test`, which doesn't type-check tests, but `tsc` and your editor report this error. Check `result.raw._meta` with Jest's matchers instead.
