# Testing and Shipping

> How to test a FrontMCP server end to end with @frontmcp/testing, deploy it somewhere clients can reach, and run several instances of it behind a load balancer.

Source: https://frontmcp.dev/learn/testing-and-shipping

A server that works in the Playground has two more jobs before anyone relies on it: it has to keep working as you change it, and it has to run somewhere clients can reach. This chapter covers both: testing the server end to end, deploying it, and running several copies of it.

**In this chapter**
- [How to test a server end to end](https://frontmcp.dev/learn/testing-your-server)
- [How to build and deploy a server](https://frontmcp.dev/learn/deploying-your-server)
- [How to run several instances behind a load balancer](https://frontmcp.dev/learn/running-several-instances)

## Testing your server

`@frontmcp/testing` starts your server, connects a real MCP client to it, and gives each test that client as `mcp`. You check what a model would see: which tools are listed, what a call returns, what an error looks like. Open the **Tests** tab:

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

test("lists get_ticket", async ({ mcp }) => {
  expect(await mcp.tools.list()).toContainTool("get_ticket");
});

test("returns a ticket by id", async ({ mcp }) => {
  const result = await mcp.tools.call("get_ticket", { id: "T-1" });
  expect(result).toBeSuccessful();
  expect(result.json()).toEqual({ id: "T-1", title: "Cannot log in", status: "open" });
});

test("fails with a readable message for an unknown ticket", async ({ mcp }) => {
  const result = await mcp.tools.call("get_ticket", { id: "T-9" });
  expect(result).toBeError();
  expect(result).toHaveTextContent("There's no ticket T-9");
});
```

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

const tickets = [
  { id: "T-1", title: "Cannot log in", status: "open" },
  { id: "T-2", title: "Invoice total is wrong", status: "closed" },
];

@Tool({
  name: "get_ticket",
  description: "Get one support ticket by its id. Returns the ticket's title and status.",
  inputSchema: { id: z.string().regex(/^T-\d+$/).describe("Ticket id, like T-1") },
})
export class GetTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    const ticket = tickets.find((t) => t.id === id);
    if (!ticket) this.fail(new PublicMcpError(`There's no ticket ${id}.`));
    return ticket;
  }
}
```

The same file runs in your project with `npx frontmcp test`.

Read more: [Testing Your Server](https://frontmcp.dev/learn/testing-your-server)
Learn the `mcp` fixture and the MCP matchers, what `result.json()` really returns, how to tell tool errors from protocol errors, how to test resources, prompts and tools that ask the user, and how to run tests in a project.

## Building and deploying

`frontmcp build` packages your server for a target: a Node bundle, Vercel, AWS Lambda or a Cloudflare Worker. A deployed server runs in production mode, and a few things change there:

- The MCP path in `frontmcp.config.ts` reaches the server only through the `frontmcp` command (`frontmcp dev` and the builds). Started any other way, it answers at `/` until you set `http.entryPath`, so a client pointed at `/mcp` gets `404`.
- FrontMCP hides the message of any plain `Error` a tool throws. Fail with [`PublicMcpError`](https://frontmcp.dev/learn/your-first-tool#returning-results-and-errors) for anything the model should read.
- Clients before MCP 2026-07-28 need `MCP_SESSION_SECRET`. Without it their `initialize` gets a `500` that names the secret (the code is `SESSION_SECRET_REQUIRED`), while clients on MCP 2026-07-28 and `/healthz` still work, so a health check won't notice.

Read more: [Deploying Your Server](https://frontmcp.dev/learn/deploying-your-server)
Learn how to choose a target, set the endpoint path, give production mode the secrets it needs, run the build in a container, check a deployed server and point a client at it.

## Running several instances

Behind a load balancer, each request goes to whichever instance it likes, and each instance has its own memory. Clients on MCP 2026-07-28 keep no session, so they need only the same secrets on every instance. Anything a provider keeps belongs in a store every instance reaches, and older clients' sessions and rate limits are shared through Redis.

Read more: [Running Several Instances](https://frontmcp.dev/learn/running-several-instances)
Learn what instances must share, which secrets every instance needs, how to share sessions and rate limits through Redis, what happens when Redis can't be reached, and the traps in FrontMCP 1.9.3.

[Auth in production](https://frontmcp.dev/reference/auth/production) has the full checklist, and [Deployment](https://frontmcp.dev/reference/deployment/production-build) has every target's options.

## What's next?

Start the chapter with [Testing Your Server](https://frontmcp.dev/learn/testing-your-server). After it, [Escape Hatches](https://frontmcp.dev/learn/escape-hatches) covers what to do when FrontMCP's usual path isn't enough: reading the raw request, calling other services, and running FrontMCP outside a Node server.
