# Fixtures

> The fixtures every @frontmcp/testing test receives, mcp, server and auth: how test.use() configures them, what each one offers, and when each is created and torn down.

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

A test in `@frontmcp/testing` is a function that receives three fixtures: `mcp`, a client connected to your server; `server`, the running server, with a way to make more clients; and `auth`, a factory for signed test tokens. `test.use()` says how to start the server. A server starts when the first test that needs it runs, and the tests with the same `test.use()` options share it, by default the whole file. Each test gets a new `mcp` client, so tests share the server's state but not the client's. The Playground's **Tests** tab only has `mcp`, so most examples on this page are code blocks, run with `frontmcp test` and Jest 30 on FrontMCP 1.9.1. `@frontmcp/testing` 1.9.2 changes only how the test server stops (it waits for the process to exit), and `transport: "sse"` was run again on 1.9.2. 1.9.3 changes only `logLevel`, which now reaches the server; that was run with Jest.

```ts
test.use({ server, baseUrl?, port?, project?, entryPath?, transport?, publicMode?, env?, startupTimeout?, logLevel? });

test("name", async ({ mcp, server, auth }) => { /* … */ });
```

---

## Reference

### `test.use(options)`

Say which server the tests use, at the top of the file for all of them, or inside a `test.describe` for the tests in that block:

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

test.use({ server: "./src/main.ts", env: { CRM_URL: "http://localhost:50910" } });

test("search_tickets is listed", async ({ mcp }) => {
  expect(await mcp.tools.list()).toContainTool("search_tickets");
});
```

The server must listen on the port in `process.env.PORT`, which the library sets for it:

```ts src/main.ts
import { FrontMcp } from "@frontmcp/sdk";
import { HelpDesk } from "./help-desk.app";

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

[See more examples below.](#usage)

#### Options

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `server` | `string` | | The entry file, run with `npx tsx <file>` from the directory you run `frontmcp test` in, so paths are relative to it, not to the test file. A value with a space is run as a shell command, like `"node dist/main.js"`. Required, unless you set `baseUrl`. |
| `baseUrl` | `string` | | Test a server that's already running at this URL, instead of starting one. With `server` too, the server is started and the clients connect to `baseUrl`, as through a proxy. |
| `port` | `number` | the first free port from 51000 | The port to start the server on. If it's taken, the library warns and picks another. `0` means the same as leaving it out. The library locks each port it hands out, in a file in the system's temp folder, so Jest workers running files in parallel never get the same one. |
| `project` | `string` | | A name for port allocation. FrontMCP's own E2E projects have their own ranges; any other name gets ports from 51000. |
| `entryPath` | `string` | `"/"` | Where the server serves MCP, if it sets `http.entryPath`. Every client connects there, and `auth`'s tokens are made for it. |
| `transport` | `"streamable-http" \| "sse"` | `"streamable-http"` | `"sse"` connects with the older HTTP+SSE transport: `GET /sse`, then each request as a `POST` to the endpoint the stream names. The server must serve it, which a FrontMCP server does unless its `transport.protocol` turns `legacy` off; otherwise connecting fails with `SSE connection to http://localhost:51000/sse failed: HTTP 404 Not Found` and a hint. Before 1.9.2 it connected only with `publicMode: true`: with the anonymous token, `initialize` got `HTTP 404` `session expired`. Before 1.9.0, `"sse"` threw "SSE transport not yet implemented". |
| `publicMode` | `boolean` | `false` | The `mcp` client doesn't ask the server for an anonymous token, so it sends no `Authorization` header. A client you give a token still sends it: see [`mcp`](#mcp). |
| `env` | `Record<string, string>` | | Environment variables for the server, added to the test process's own. They're read when the server starts, after `beforeAll`, so a value a `beforeAll` hook puts in the object, like a mock's URL, reaches the server. A `JWT_SECRET` here also makes [`auth`](#auth) sign its tokens with it. (Before 1.9.0 they were read when `test.use()` ran.) |
| `startupTimeout` | `number` | `30000` | How long to wait for the server to answer, in milliseconds. It's ready when `GET /health` answers with any `2xx` or `404`. |
| `logLevel` | `"debug" \| "info" \| "warn" \| "error"` | | `"debug"` prints the command, then everything the server prints, as `[SERVER] …`. `DEBUG_SERVER=1` or `DEBUG=1` in the environment does the same. Every value is also the server's [log level](https://frontmcp.dev/reference/server/logging#choosing-a-level), passed to it as `FRONTMCP_LOG_LEVEL`, unless its `logging.level` is set: with `"warn"`, its `info()` lines aren't written. (Changed in 1.9.3: before, the values other than `"debug"` did nothing.) |
| `auth` | `{ mode?, type? }` | | Says what the server's auth is, and doesn't change it: set that in the server's own `@FrontMcp`, and use [`auth`](https://frontmcp.dev/reference/testing/auth) for tokens. `mode: "public"` keeps `mcp` anonymous, as `publicMode` does. `mode` and `type` also reach the server's environment as `FRONTMCP_TEST_AUTH_MODE` and `FRONTMCP_TEST_AUTH_TYPE`, which only your own entry file reads. |

#### How calls combine

- **A call applies to its block.** `test.use()` at the top of the file configures every test in it. Inside a `test.describe`, it configures that block's tests, on top of the file's options: the last value of each option wins, and `env` entries are added. Calling it again in the same place merges the new options in.
- **Each configuration has its own server, and the block that configured it stops it.** Two blocks with different options start two servers, and a block's server stops when the block ends. A block with no `test.use()` of its own uses the server of the block around it. See [Two configurations of a server](#two-configurations-of-a-server).

Before 1.8.7, every `test.use()` in a file merged into one configuration, the last call won for every test, and a `test.use()` inside a `test.describe` stopped the server after that block.

### Lifetimes

| Fixture | Created | Shared by | Ends |
| --- | --- | --- | --- |
| `server` | When the first test that needs it runs, not when the file loads. `beforeAll` hooks run first. | Every test in the file, or in the `describe` block that configured it | After the last test of the file, or of that block, when the library stops the server's process. |
| `auth` | With the server | Every test the server is shared by | With the server |
| `mcp` | Before each test, connected | One test | Disconnected after the test |
| Clients from `server.createClient()` | When you call it | Whoever holds it | When the test ends: the library disconnects them. Calling `disconnect()` yourself is fine. (Before 1.8.7 it didn't close them for you.) |

So everything a tool keeps in memory carries over from one test to the next, and everything you set on a client (mocks, interceptors, an elicitation handler, collected notifications) doesn't. When a test fails, the library prints the server's last 50 log entries under `[TestFixture] === Server Logs (test failed) ===`.

### `mcp`

A new [`McpTestClient`](https://frontmcp.dev/reference/testing#the-mcp-client) for each test, already connected. It speaks MCP 2025-06-18: it sends `initialize`, then uses the session the server gives it.

- **Credentials.** Without `publicMode`, the client first asks the server's `/oauth/token` for an anonymous token (`grant_type: "anonymous"`), and sends it as `Authorization: Bearer …` if it gets one. A public server gives one, so there `this.auth.user.sub` is an `anon:` id. With `publicMode: true`, the client sends no `Authorization` header.
- **On a server that needs a token**, connecting fails with `401`. The fixture prints a warning, `[TestFixture] MCP client could not connect anonymously — the server requires authentication`, with the reason, and gives the test a client that never initialized: `mcp.isConnected()` is `true`, but every request fails with `-32600`, ``Session not initialized — send `initialize` first``. Tests for such a server sign `mcp` in with `await mcp.authenticate(token)`, or use [`server.createClient({ token })`](#server). A `401` or `403` is tried once, so a refused connection doesn't wait. (Before 1.8.7 it was tried three times, half a second apart.)
- **`mcp.authenticate(token)` signs the client in.** It opens a new session as the token's user, and keeps the token, so `mcp.reconnect()` uses it too. When the server refuses the token, it rejects, as `createClient()` does, and the client keeps its old session and identity. See [Testing authentication](https://frontmcp.dev/reference/testing/auth#the-auth-fixture). Before 1.8.7 it changed only the header: the session still belonged to the old token, the next request failed with `HTTP 404: Not Found` (`invalid session id`), and `reconnect()` dropped the token.

Besides the [methods for tools, resources and prompts](https://frontmcp.dev/reference/testing#the-mcp-client), each client keeps a record of its own, for debugging:

| Member | What it holds |
| --- | --- |
| `mcp.logs.all()`, `filter(level)`, `search(text)`, `last()`, `clear()` | The client's own log, like `Connecting to http://localhost:51000...` and `Connected to help-desk`. Not the server's: that's [`server.getLogs()`](#server). |
| `mcp.trace.all()`, `last()`, `clear()` | Every request the client sent and its answer: `{ request: { method, params, id }, response: { result?, error? }, durationMs, timestamp }`. |
| `mcp.transport_info.lastRequestHeaders` | The headers of the last request: `Content-Type`, `Accept`, `User-Agent`, `Authorization` and `mcp-session-id`. |

### `server`

| Member | Description |
| --- | --- |
| `info` | `{ baseUrl, port, pid }`, like `{ baseUrl: "http://localhost:51000", port: 51000, pid: 68493 }`. `pid` is the process listening on `port`, which the library finds with `lsof`; where that finds nothing, such as on Windows, it's the shell that runs the command. `info` is always current: after `restart()` it has the new `pid`. (Before 1.8.7 `pid` was always the shell's.) |
| `createClient(options?)` | Another client, connected: see below. |
| `createClientBuilder()` | A builder for a client with any settings: see below. |
| `restart()` | Stops the server and starts it again on the same port. Everything it kept in memory is gone, sessions included. `mcp` and the clients from `createClient()` reconnect, each with a new session: see [Restarting the server](#restarting-the-server). |
| `getLogs()` | Everything the server printed since it started, as a `string[]` of chunks as they arrived (a chunk can hold several lines, or part of one). Lines from standard error start with `[ERROR] `, even FrontMCP's warnings. It holds FrontMCP's own log lines, at `INFO` by default, and whatever your code prints. |
| `clearLogs()` | Empties `getLogs()`. |

With only `baseUrl`, there's no process: `info` has no `pid`, `getLogs()` is always empty, and `restart()` only waits for the URL to answer, then reconnects the clients.

#### `server.createClient(options?)`

| Option | Type | Description |
| --- | --- | --- |
| `token` | `string` | Sent as `Authorization: Bearer <token>`, with `publicMode: true` too. |
| `clientInfo` | `{ name, version }` | The client's name in `initialize`, and its `User-Agent`, as `name/version`. The default is `@frontmcp/testing` `0.4.0`. |
| `entryPath` | `string` | Overrides `test.use()`'s `entryPath` for this client. |
| `transport` | `"streamable-http" \| "sse"` | As in `test.use()`. |

It resolves to a connected `McpTestClient`, the same kind as `mcp`, and rejects when the server refuses to initialize, with a message like `Failed to initialize MCP connection to http://localhost:51000/ after 1 attempt: HTTP 401: Unauthorized - {"error":"Unauthorized"}.` The library disconnects the client when the test ends.

#### `server.createClientBuilder()`

Returns a builder for the server's URL, with `test.use()`'s `entryPath` and `publicMode`. Each method returns the builder; `build()` returns an unconnected client, and `buildAndConnect()` a connected one.

| Method | Description |
| --- | --- |
| `withToken(token)` | Sends `Authorization: Bearer <token>`. |
| `withHeaders(headers)` | Adds headers to every request, like an API key header of your own. |
| `withPublicMode(enabled?)` | Turns `publicMode` on (the default) or off for this client. |
| `withClientInfo({ name, version })` | As `clientInfo` above. |
| `withProtocolVersion(version)` | The version to ask for in `initialize`. `"2026-07-28"` throws: the client can't speak it. |
| `withTimeout(ms)` | The timeout for each request. Default `30000`. |
| `withQueryParams(params)` | Adds query parameters to the MCP URL. |
| `withCapabilities(capabilities)`, `withPlatform(platform)` | The capabilities the client declares, or those of a known client platform with its `clientInfo`. |
| `withDebug(enabled?)` | Prints the client's log as it goes. |

### `auth`

Makes signed JWTs for your tests. With no `JWT_SECRET` in `test.use({ env })`, it signs with an RSA key of its own, which your server doesn't know; with one, it signs the way a `public`, `local` or `remote` server does, so that server accepts them. [Testing authentication](https://frontmcp.dev/reference/testing/auth) covers each auth mode.

| Member | Description |
| --- | --- |
| `createToken({ sub, scopes?, email?, name?, claims?, expiresIn? })` | A token for that user. `expiresIn` is in seconds, default `3600`: the token lasts at least that long. |
| `createExpiredToken({ sub })` | A token that expired an hour ago. |
| `createInvalidToken({ sub })` | A token whose signature is wrong. |
| `users` | `admin`, `user` and `readOnly`, ready to pass to `createToken()`. |
| `getIssuer()`, `getAudience()`, `getJwks()` | The `iss` and `aud` its tokens carry, and its public key. |

### Hooks

`test.beforeEach(fn)` and `test.afterEach(fn)` with a function that takes a parameter run as part of each test, and receive its fixtures: the same `mcp`, `server` and `auth` the test gets. `beforeEach` runs after the client connects and before the test; `afterEach` runs after the test, even when it failed, and before the clients are disconnected. Hooks in an outer `test.describe` run before an inner block's `beforeEach`, and after its `afterEach`.

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

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

test.beforeEach(async ({ mcp }) => {
  await mcp.tools.call("reset_desk");
});

test("closes a ticket", async ({ mcp }) => {
  expect(await mcp.tools.call("close_ticket", { id: "T-1" })).toBeSuccessful();
});
```

A hook function without a parameter, and every `test.beforeAll` and `test.afterAll`, is Jest's own, and gets no fixtures: use them for what isn't the server, like a [mock API](https://frontmcp.dev/reference/testing/interceptors#mocking-an-api-for-a-server-in-another-process) or a [mock identity provider](https://frontmcp.dev/reference/testing/auth#testing-a-transparent-server). `beforeAll` runs before the server starts, so a mock you start there is up when the server connects to it, and a URL it puts in `test.use()`'s `env` object reaches the server.

Before 1.9.0 every hook was Jest's, so a hook that took a parameter waited for a `done` callback until the test timed out. Jest's `done` style no longer works for `test.beforeEach` and `test.afterEach`.

#### Caveats

- **`test.beforeAll` and `test.afterAll` with a parameter wait for `done`.** Jest treats a hook whose function takes an argument as a callback hook, so `test.beforeAll(async ({ mcp }) => …)` never finishes, and fails after the test timeout, 60 seconds under `frontmcp test`. See [Troubleshooting](#exceeded-timeout-of-60000-ms-for-a-hook-while-waiting-for-done-to-be-called).
- **`test.each` and `test.describe.each` pass each row's values** to the function: see [Running a test for each input](#running-a-test-for-each-input).
- **Tests run one at a time within a file**, in the order they're written, and each file starts its own servers.
- **Files can run in parallel.** Without `--runInBand`, Jest runs files in parallel workers, and each file's servers get different ports. `--runInBand` runs one file at a time, which is the quieter choice for tests that each start a server.

---

## Usage

### Sharing the server between tests

Each test gets a new client, but the server is the same one, so what one test changes, the next one sees. The Playground runs its tests the same way: one server per run, a new `mcp` for each test.

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

const tickets: { id: string; title: string }[] = [];

@Tool({ name: "open_ticket", description: "Open a support ticket", inputSchema: { title: z.string() } })
export class OpenTicket extends ToolContext {
  async execute({ title }: { title: string }) {
    const ticket = { id: `T-${tickets.length + 1}`, title };
    tickets.push(ticket);
    return ticket;
  }
}

@Tool({ name: "list_tickets", description: "List the open tickets", inputSchema: {} })
export class ListTickets extends ToolContext {
  async execute() {
    return { tickets };
  }
}
```

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

test("opens the first ticket", async ({ mcp }) => {
  const result = await mcp.tools.call("open_ticket", { title: "Cannot log in" });
  expect(result.json()).toEqual({ id: "T-1", title: "Cannot log in" });
});

test("the next test sees the same server", async ({ mcp }) => {
  // T-1 was opened by the test above, on another client.
  const result = await mcp.tools.call("open_ticket", { title: "Invoice total is wrong" });
  expect(result.json()).toEqual({ id: "T-2", title: "Invoice total is wrong" });
});

test("a test that doesn't depend on the others", async ({ mcp }) => {
  const before = (await mcp.tools.call("list_tickets")).json().tickets.length;
  await mcp.tools.call("open_ticket", { title: "Login link expired" });
  const after = (await mcp.tools.call("list_tickets")).json().tickets.length;
  expect(after).toBe(before + 1);
});
```

The second test passes only because the first one ran before it. The third checks the change it made, so it passes whatever ran before. Under `frontmcp test`, the same file, named `e2e/tickets.e2e.spec.ts`, needs `test.use({ server: "./src/main.ts" })` at the top; the Playground accepts `test.use()` and ignores it.

### Two configurations of a server

`test.use()` in a `test.describe` gives that block its own options, and its own server. Here the two blocks start a server each, with a different `DESK_NAME`, and each server stops when its block ends:

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

test.describe("north", () => {
  test.use({ server: "./src/main.ts", env: { DESK_NAME: "north" } });

  test("north desk", async ({ mcp }) => {
    expect((await mcp.tools.call("desk_name")).json()).toMatchObject({ name: "north" });
  });
});

test.describe("south", () => {
  test.use({ server: "./src/main.ts", env: { DESK_NAME: "south" } });

  test("south desk", async ({ mcp }) => {
    expect((await mcp.tools.call("desk_name")).json()).toMatchObject({ name: "south" });
  });
});
```

Options you set at the top of the file apply to every block, so a file whose blocks differ in one option can set `server` once at the top and only `env` in each block. Before 1.8.7, both blocks above started a server with `DESK_NAME=south`, and tests needing two configurations of a server went in two files.

### Calling as several clients

`server.createClient()` connects another client to the same server, with its own session, token or client name. The library disconnects it when the test ends, so make it in the test that uses it.

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

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

test("each client has its own session", async ({ mcp, server }) => {
  const cli = await server.createClient({ clientInfo: { name: "desk-cli", version: "2.0.0" } });
  expect(cli.sessionId).not.toBe(mcp.sessionId);
  expect(cli.transport_info.lastRequestHeaders["User-Agent"]).toBe("desk-cli/2.0.0");
  expect(await cli.tools.call("search_tickets", { query: "log" })).toBeSuccessful();
});
```

For clients that call as a signed-in user, see [Testing authentication](https://frontmcp.dev/reference/testing/auth#calling-as-a-signed-in-user).

### Restarting the server

`server.restart()` starts a fresh process on the same port, with nothing in memory, and reconnects `mcp` and every client made with `server.createClient()`, each with a new session:

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

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

test("a restart forgets what the server kept in memory", async ({ mcp, server }) => {
  await mcp.tools.call("search_tickets", { query: "log" });
  expect((await mcp.tools.call("server_calls")).json()).toEqual({ calls: 1 });

  const { pid } = server.info;
  await server.restart();
  expect(server.info.pid).not.toBe(pid);
  expect((await mcp.tools.call("server_calls")).json()).toEqual({ calls: 0 });
});
```

Before 1.8.7 the clients kept the old session, so the next call failed with `HTTP 401: Unauthorized` (a server with no `JWT_SECRET`, whose anonymous tokens died with the process) or `HTTP 404: Not Found` (`invalid session id`), both code `-32000`, until the test called `await mcp.reconnect()`.

### Reading the server's logs

`server.getLogs()` is what the server printed. Clear it first to look at one call:

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

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

test("the call reaches the tool", async ({ mcp, server }) => {
  server.clearLogs();
  await mcp.tools.call("search_tickets", { query: "log" });
  const logs = server.getLogs().join("");
  expect(logs).toContain("tools/call: search_tickets");
});
```

Join the chunks before searching: a line can arrive split across two.

### Running a test for each input

`test.each(table)(name, fn)` registers a test for each row. `fn` receives the fixtures, then the row's values; the name takes Jest's `%s`, `%i` and `%j`:

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

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

test.each(["log", "LOG", "Log"])('search_tickets ignores case: "%s"', async ({ mcp }, query) => {
  const result = await mcp.tools.call("search_tickets", { query });
  expect(result.json().tickets.map((t: { id: string }) => t.id)).toEqual(["T-1", "T-2"]);
});

test.each([
  ["log", ["T-1", "T-2"]],
  ["invoice", ["T-3"]],
])("search_tickets finds %s", async ({ mcp }, query, ids) => {
  const result = await mcp.tools.call("search_tickets", { query });
  expect(result.json().tickets.map((t: { id: string }) => t.id)).toEqual(ids);
});
```

A row that is an array is spread into arguments; any other value is one argument. `test.describe.each(table)(name, body)` passes each row to `body`, and a tagged template works for both. Before 1.8.7 there was no `test.each`, and `test.describe.each` called its function with no arguments, so tests looped over the values instead.

### Testing a server that's already running

With `baseUrl` and no `server`, the library starts nothing and connects to the URL, such as a staging server or one you start yourself:

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

test.use({ baseUrl: process.env.DESK_URL ?? "http://localhost:3000" });

test("search_tickets is listed", async ({ mcp }) => {
  expect(await mcp.tools.list()).toContainTool("search_tickets");
});
```

[Mocking HTTP](https://frontmcp.dev/reference/testing/interceptors#running-the-server-in-the-test-process) starts a server in the test's own process this way, so `httpMock` can see its requests.

---

## Troubleshooting

### "Exceeded timeout of 60000 ms for a hook while waiting for `done()` to be called"

A `test.beforeAll` or `test.afterAll` function takes a parameter, like `test.beforeAll(async ({ mcp }) => …)`. Those hooks are Jest's, get no fixtures, and Jest takes the parameter for a `done` callback that never comes. Work that needs the server goes in `test.beforeEach`, which gets the fixtures:

```ts
// 🚩 Waits for done() until it times out
test.beforeAll(async ({ mcp }) => {
  await mcp.tools.call("reset_desk");
});

// ✅ Runs before each test, with its fixtures
test.beforeEach(async ({ mcp }) => {
  await mcp.tools.call("reset_desk");
});
```

Before 1.9.0, `test.beforeEach` and `test.afterEach` timed out the same way.

### `[TestFixture] MCP client could not connect anonymously`, then ``Session not initialized — send `initialize` first``

The server requires a token, so the `mcp` fixture couldn't connect. Sign it in with `await mcp.authenticate(token)`, or connect a client of your own with a token: `await server.createClient({ token: await auth.createToken({ sub: "agent-1" }) })`. [Testing authentication](https://frontmcp.dev/reference/testing/auth#calling-as-a-signed-in-user) shows which token each auth mode accepts.

### "Server did not become ready within 30000ms"

The server started but never answered on its port. Check that it listens on `process.env.PORT`, run the command from the error yourself, or set `logLevel: "debug"` to see what it prints. A server that's slow to start needs a larger `startupTimeout`.

### `Not connected to MCP server. Call connect() first.` from a client of your own

The client came from `server.createClient()` in an earlier test, and the library disconnected it when that test ended. Make the client inside the test that uses it.

### A test passes alone but fails with the others

The tests share one server, so an earlier test can leave data behind. Make each test set up what it needs, or check the change it made: see [Sharing the server between tests](#sharing-the-server-between-tests).
