# Running FrontMCP Anywhere

> The entry points under @FrontMcp's built-in server. createFetchHandler() serves MCP from Workers and other web-standard runtimes, and connect() and createDirect() call your tools in-process.

Source: https://frontmcp.dev/learn/running-frontmcp-anywhere

So far, `@FrontMcp` and the `frontmcp` CLI have decided how your server runs: `frontmcp dev` starts an HTTP server, and `frontmcp build` packages the server for a target like Node or Cloudflare Workers. Underneath are a few functions you can call yourself, for when you need to own a part FrontMCP usually owns: the HTTP layer, or the client. Most servers never need them.

**You will learn**
- What `@FrontMcp` does for you, and which entry points sit under it
- How to serve MCP from any runtime that speaks `Request` and `Response`
- How to call your tools in-process with `connect()` and `createDirect()`
- What changes when you skip the transport: identity, errors and formatting

## What `@FrontMcp` does for you

`@FrontMcp({ info, apps })` records your server's configuration and, on Node, starts an HTTP server as soon as the decorated class is defined. The CLI's serverless builds switch that off and wrap the same configuration in the entry point their target needs. When you need something else, these are the pieces to build from:

| You want to | Use | You get |
| --- | --- | --- |
| Serve MCP over HTTP from Node | `@FrontMcp`, with `frontmcp dev` and `frontmcp build` | A running server |
| Serve MCP from a runtime that hands you a web `Request`: Cloudflare Workers, Deno, Bun, or a route in another framework | [`FrontMcpInstance.createFetchHandler(config)`](https://frontmcp.dev/reference/sdk/create-fetch-handler) | `(request: Request) => Promise<Response>` |
| Call your tools from your own code, through an MCP client | [`connect(config, options)`](https://frontmcp.dev/reference/sdk/connect), or `connectClaude()`, `connectOpenAI()`, `connectLangChain()`, `connectVercelAI()` | A `DirectClient` |
| Call your tools from your own code, without a client | [`FrontMcpInstance.createDirect(config)`](https://frontmcp.dev/reference/sdk/create), or `create()` for a config without apps | A `DirectMcpServer` |

`FrontMcpInstance.runStdio()` and `runUnixSocket()` serve local clients over stdio or a Unix socket. They need Node and aren't covered here.

All of them take the configuration you'd give `@FrontMcp`. Keep it in its own module, and decorate a class with it only in the file the CLI runs, because importing a decorated class starts a server:

```ts config.ts
import { HelpDesk } from "./help-desk.app";

export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
  http: { entryPath: "/mcp" },
};
```

```ts main.ts
import "reflect-metadata";
import { FrontMcp } from "@frontmcp/sdk";
import { config } from "./config";

@FrontMcp(config)
export default class HelpDeskServer {}
```

A worker, a script or an agent imports `config.ts` and never touches `main.ts`.

## Serving MCP from any runtime

`FrontMcpInstance.createFetchHandler(config)` returns a function that takes a web `Request` and returns a `Response`, and knows nothing else about where it runs. This is a complete Cloudflare Worker, and the test sends it the same requests Cloudflare would:

```ts worker.ts active
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./config";

const handler = FrontMcpInstance.createFetchHandler(config);

export default {
  async fetch(request: Request): Promise<Response> {
    return (await handler)(request);
  },
};
```

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

// What an MCP client sends under 2026-07-28: the method, and for calls the
// tool's name, repeated in headers, and the protocol version in two places.
function mcp(method: string, params: Record<string, unknown> = {}) {
  return new Request("https://desk.example.com/mcp", {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "mcp-protocol-version": "2026-07-28",
      "mcp-method": method,
      ...(method === "tools/call" ? { "mcp-name": String(params.name) } : {}),
    },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: 1,
      method,
      params: { ...params, _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } },
    }),
  });
}

test("lists tools at /mcp", async () => {
  const body = await (await worker.fetch(mcp("tools/list"))).json();
  expect(body.result.tools.map((t: { name: string }) => t.name)).toEqual(["get_ticket"]);
});

test("calls a tool", async () => {
  const body = await (await worker.fetch(mcp("tools/call", { name: "get_ticket", arguments: { id: "T-1" } }))).json();
  expect(body.result.structuredContent).toEqual({ id: "T-1", title: "Cannot log in", status: "open" });
});

test("answers health checks on /healthz", async () => {
  const res = await worker.fetch(new Request("https://desk.example.com/healthz"));
  expect(await res.json()).toMatchObject({ status: "ok", transport: "web-fetch" });
});

test("answers 404 anywhere else", async () => {
  const res = await worker.fetch(new Request("https://desk.example.com/", { method: "POST", body: "{}" }));
  expect(res.status).toBe(404);
});
```

```ts config.ts
import { HelpDesk } from "./help-desk.app";

export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
  http: { entryPath: "/mcp" },
};
```

```ts help-desk.app.ts
import { App, 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: "open" },
];

@Tool({
  name: "get_ticket",
  description: "Get one support ticket by its id.",
  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}.`, "TICKET_NOT_FOUND"));
    return ticket;
  }
}

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

`createFetchHandler()` takes the same configuration as `@FrontMcp`. The handler serves MCP at `http.entryPath`, or at `/` if you don't set one, answers `/healthz` and `/readyz` everywhere, and returns 404 for anything else. It returns a promise of the handler, and builds the server when you call it, so a server that can't start fails there. On an edge isolate like a Worker, which can't build a server while the module loads, it builds on the first request instead: see [`createFetchHandler()`](https://frontmcp.dev/reference/sdk/create-fetch-handler#returns).

This site's Playground runs on the same function. It builds a fetch handler from each example and sends it requests from a Web Worker in your browser, and the Wire tab shows those requests. In a project, deploy the Worker like any other; FrontMCP's CLI also has a Cloudflare build target that writes this entry file for you. Any other runtime or framework that gives you a `Request` and takes a `Response` back can call `handler(request)` the same way.

Behind `createFetchHandler()`, your tools see the request much as they would behind FrontMCP's own server: a `traceparent` header continues the client's trace, and `this.context.metadata` has the user agent and the `x-frontmcp-*` headers. A deep dive in [Reading the Request](https://frontmcp.dev/learn/reading-the-request) tests it.

## Calling your tools through a client: `connect()`

Sometimes your own code is the MCP client: an agent loop that calls a model's API directly, a command-line tool, a job. `connect(config, options)` builds the server in your process and connects a client to it in memory. It goes through the same handshake and requests as a client over HTTP, without a network.

The `connectClaude()`, `connectOpenAI()`, `connectLangChain()` and `connectVercelAI()` variants also convert tools and results to the shape each API or library expects:

```ts agent.test.ts active
import { test, expect } from "@frontmcp/testing";
import { ToolCallError, connectClaude } from "@frontmcp/sdk";
import { config } from "./config";

test("tools come in the shape Claude's Messages API takes", async () => {
  const client = await connectClaude(config);
  const tools = await client.listTools();
  await client.close();
  expect(tools).toEqual([
    {
      name: "get_ticket",
      description: "Get one support ticket by its id.",
      input_schema: expect.objectContaining({ type: "object", required: ["id"] }),
    },
  ]);
});

test("results come back as content blocks", async () => {
  const client = await connectClaude(config);
  const result = await client.callTool("get_ticket", { id: "T-1" });
  await client.close();
  expect(result).toEqual([{ type: "text", text: '{"id":"T-1","title":"Cannot log in","status":"open"}' }]);
});

test("a failed call rejects with a ToolCallError", async () => {
  const client = await connectClaude(config);
  const error = await client.callTool("get_ticket", { id: "T-9" }).catch((err: unknown) => err);
  await client.close();
  expect(error).toBeInstanceOf(ToolCallError);
  expect((error as ToolCallError).message).toBe("There's no ticket T-9.");
  expect((error as ToolCallError).result.content).toEqual([{ type: "text", text: "There's no ticket T-9." }]);
});
```

```ts config.ts
import { HelpDesk } from "./help-desk.app";

export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
};
```

```ts help-desk.app.ts
import { App, 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: "open" },
];

@Tool({
  name: "get_ticket",
  description: "Get one support ticket by its id.",
  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}.`, "TICKET_NOT_FOUND"));
    return ticket;
  }
}

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

`listTools()` returns tools with `input_schema`, ready to pass to Claude, and `callTool()` returns the result's content blocks, ready for a `tool_result`. Claude's format has no place for MCP's `isError`, so a failed call doesn't return at all: it rejects with a `ToolCallError`, whose `message` is the tool's error text and whose `result` is the full MCP result. An agent that doesn't catch it stops at the first failed call, when the model could have tried another id. The second challenge catches it and sends the failure back as a `tool_result` with `is_error: true`. Plain `connect()` returns the full MCP result instead, `isError` included, for code that wants to convert it itself.

`connect()` also takes who the client is and who the user is:

```ts
const client = await connect(config, {
  clientInfo: { name: "desk-agent", version: "1.0.0" },
  authToken: userToken,
  session: { user: { sub: "nour" }, scopes: ["tickets:write"] },
});
```

`clientInfo` becomes [`this.clientInfo`](https://frontmcp.dev/learn/reading-the-request) in your tools, `session.user` becomes the caller in `this.auth`, `session.scopes` the caller's scopes, so `this.auth.hasScope("tickets:write")` is `true` (since 1.9.3), and `authToken` becomes the request's token, which [`this.fetch()` keeps to itself](https://frontmcp.dev/learn/calling-other-services#sending-your-own-credentials). (In FrontMCP 1.9.4, `connect()` leaves `this.context.authInfo.clientId` empty, so read the caller from `this.auth`, as this course's tools do.) Nothing checks the user, the scopes or the token: in-process, your code is the authentication, so only pass a user your own code has verified.

## Calling your tools without a client: `createDirect()`

A script or a job often doesn't need a client at all, just a way to run a tool. `FrontMcpInstance.createDirect(config)` returns a `DirectMcpServer` with `listTools()`, `callTool()`, `listResources()`, `readResource()`, `listPrompts()`, `getPrompt()` and `dispose()`. `create()` does the same from a flat configuration with `tools` and `providers` at the top level and no `@App`, which is what the tests in [Reading the Request](https://frontmcp.dev/learn/reading-the-request) use.

Its results are plain MCP results, and each call can say who the user is with `authContext`: `authContext.user` stands in for the claims of a verified token, so its `sub` becomes `this.auth.user.sub` and its `roles` become `this.auth.roles`. Here a nightly job closes tickets as the user `nightly-cleanup`:

```ts nightly.test.ts active
import { test, expect } from "@frontmcp/testing";
import { FrontMcpInstance, connect } from "@frontmcp/sdk";
import { config } from "./config";

const asJob = { authContext: { user: { sub: "nightly-cleanup" } } };

test("a job can call tools as a named user", async () => {
  const server = await FrontMcpInstance.createDirect(config);
  const result = await server.callTool("close_ticket", { id: "T-1" }, asJob);
  await server.dispose();
  expect(result.structuredContent).toEqual({ id: "T-1", status: "closed", closed_by: "nightly-cleanup" });
});

test("a failed call throws, instead of returning isError", async () => {
  const server = await FrontMcpInstance.createDirect(config);
  await expect(server.callTool("close_ticket", { id: "T-9" }, asJob)).rejects.toThrow("There's no ticket T-9.");
  await expect(server.callTool("close_ticket", { id: 9 }, asJob)).rejects.toThrow("Invalid tool input");
  await server.dispose();
});

test("🚩 without authContext, the caller is `direct`, not anonymous", async () => {
  const server = await FrontMcpInstance.createDirect(config);
  const result = await server.callTool("close_ticket", { id: "T-2" });
  await server.dispose();
  expect(result.structuredContent).toEqual({ id: "T-2", status: "closed", closed_by: "direct" });
});

test("one server, several users: each call runs as the user it names", async () => {
  const server = await FrontMcpInstance.createDirect(config);
  const ana = await server.callTool("close_ticket", { id: "T-1" }, { authContext: { user: { sub: "ana" } } });
  const bo = await server.callTool("close_ticket", { id: "T-2" }, { authContext: { user: { sub: "bo" } } });
  const job = await server.callTool("close_ticket", { id: "T-1" }, asJob);
  await server.dispose();
  expect(ana.structuredContent).toMatchObject({ closed_by: "ana" });
  expect(bo.structuredContent).toMatchObject({ closed_by: "bo" });
  expect(job.structuredContent).toMatchObject({ closed_by: "nightly-cleanup" });
});

test("connect() takes the user from `session.user`", async () => {
  const client = await connect(config, { session: { user: { sub: "nightly-cleanup" } } });
  const result = await client.callTool("close_ticket", { id: "T-2" });
  await client.close();
  expect(result).toMatchObject({ structuredContent: { closed_by: "nightly-cleanup" } });
});
```

```ts config.ts
import { HelpDesk } from "./help-desk.app";

export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
};
```

```ts help-desk.app.ts
import { App, 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: "open" },
];

@Tool({
  name: "close_ticket",
  description: "Close a support ticket. Signed-in agents only.",
  inputSchema: { id: z.string().regex(/^T-\d+$/).describe("Ticket id, like T-1") },
})
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    if (this.auth.isAnonymous) this.fail(new PublicMcpError("Sign in to close tickets.", "SIGN_IN_REQUIRED"));
    const ticket = tickets.find((t) => t.id === id);
    if (!ticket) this.fail(new PublicMcpError(`There's no ticket ${id}.`, "TICKET_NOT_FOUND"));
    ticket.status = "closed";
    return { id, status: ticket.status, closed_by: this.auth.user.sub };
  }
}

@App({ id: "help-desk", name: "Help Desk", tools: [CloseTicket] })
export class HelpDesk {}
```

Two things work differently from a client, and the tests show both:

- **Failures are exceptions.** Over MCP, a tool that fails returns a result with `isError: true`. `callTool()` here rejects instead, with the `PublicMcpError` the tool failed with, `code` and all, or an `InvalidInputError` for bad arguments. Wrap calls in `try` when one failure shouldn't stop the job.
- **No `authContext` isn't anonymous.** Without it, the caller is a signed-in user called `direct`: `this.auth.user.sub` is `"direct"` and `isAnonymous` is `false`, so a tool's check for anonymous callers lets the call through. Always name the user, even for jobs.

One server can serve many users. Each call runs as the user its `authContext` names, and gets a `this.context` of its own, with its own `requestId` and `authInfo`. The fourth test closes tickets as Ana, Bo and the job on one server, and each ticket records the right user. (In FrontMCP 1.8.0, calls without `authContext.sessionId` shared one context, so Bo's call could run as Ana. On 1.8.0, give each user their own `sessionId`.)

A server you build this way inside a Cloudflare Worker, in a Durable Object or a queue consumer, gets no request from a fetch handler, so it doesn't see the Worker's `env` by itself. Pass it as `workerEnv`, in `createDirect()`'s configuration, on one call, or in `connect()`'s options, and your tools read it as `this.workerEnv` (since 1.9.4). [Passing a Worker's bindings](https://frontmcp.dev/reference/sdk/create#passing-a-workers-bindings) shows it.

## Recap

- `@FrontMcp` records your configuration and starts a server on import. Keep the configuration in its own module, so other entry points can use it without starting one.
- `FrontMcpInstance.createFetchHandler(config)` turns a server into `(request) => Promise<Response>`, for Workers and any other web-standard runtime. It serves `http.entryPath`, `/healthz` and `/readyz`.
- `connect()` connects an MCP client in memory. The platform variants convert tools and results for an LLM API, and reject a failed call with a `ToolCallError` that carries the result.
- `createDirect()` and `create()` run tools with no client: results are MCP results, and failures are thrown.
- In-process callers say who the user is and nothing checks it. Pass only users your code has verified, and always pass one.

## Try some challenges

### Challenge: Serve MCP at /mcp
The help desk's MCP clients are set up with `https://desk.example.com/mcp`, and this worker answers every one of their requests with a 404. Open the Tests tab, then fix the worker so that MCP requests to `/mcp` work.

```ts worker.ts active
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./config";

const handler = FrontMcpInstance.createFetchHandler(config);

export default {
  async fetch(request: Request): Promise<Response> {
    return (await handler)(request);
  },
};
```

```ts config.ts
import { HelpDesk } from "./help-desk.app";

export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
};
```

```ts config.ts solution
import { HelpDesk } from "./help-desk.app";

export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
  http: { entryPath: "/mcp" },
};
```

```ts help-desk.app.ts
import { App, 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: "open" },
];

@Tool({
  name: "get_ticket",
  description: "Get one support ticket by its id.",
  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}.`, "TICKET_NOT_FOUND"));
    return ticket;
  }
}

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

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

function listTools() {
  return worker.fetch(
    new Request("https://desk.example.com/mcp", {
      method: "POST",
      headers: { "content-type": "application/json", "mcp-protocol-version": "2026-07-28", "mcp-method": "tools/list" },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: { _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } } }),
    }),
  );
}

test("`tools/list` at `/mcp` answers 200", async () => {
  const res = await listTools();
  expect({ status: res.status, body: await res.text() }).toMatchObject({ status: 200 });
});

test("it lists `get_ticket`", async () => {
  const body = await (await listTools()).json();
  expect(body.result?.tools).toContainTool("get_ticket");
});

test("health checks still answer at `/healthz`", async () => {
  const res = await worker.fetch(new Request("https://desk.example.com/healthz"));
  expect(res.status).toBe(200);
});
```

**Hint:**
The handler serves MCP at one path and answers 404 everywhere else. Where is that path when the configuration doesn't name one?

**Solution:**
Without `http.entryPath`, the handler serves MCP at `/`, so a request to `/mcp` got the same 404 as any other unknown path. `http: { entryPath: "/mcp" }` moves the MCP endpoint there, and `/healthz` and `/readyz` keep answering where they were. The fix belongs in the configuration rather than the worker, so every entry point that uses `config.ts` agrees on the path.

### Challenge: Keep failures visible to Claude
`toolResult()` in `agent.ts` runs a tool for an agent that talks to Claude's Messages API, and returns the `tool_result` block to send back. When the tool fails, `connectClaude()`'s `callTool()` rejects, and so does `toolResult()`, so the agent never tells the model. Return a `tool_result` for a failed call too, with `is_error: true` and the tool's content blocks.

```ts agent.ts active
import { connectClaude } from "@frontmcp/sdk";
import { config } from "./config";

export async function toolResult(toolUseId: string, name: string, input: Record<string, unknown>) {
  const client = await connectClaude(config);
  try {
    const content = await client.callTool(name, input);
    return { type: "tool_result", tool_use_id: toolUseId, content, is_error: false };
  } finally {
    await client.close();
  }
}
```

```ts agent.ts solution
import { ToolCallError, connectClaude } from "@frontmcp/sdk";
import { config } from "./config";

export async function toolResult(toolUseId: string, name: string, input: Record<string, unknown>) {
  const client = await connectClaude(config);
  try {
    const content = await client.callTool(name, input);
    return { type: "tool_result", tool_use_id: toolUseId, content, is_error: false };
  } catch (err) {
    if (!(err instanceof ToolCallError)) throw err;
    return { type: "tool_result", tool_use_id: toolUseId, content: err.result.content, is_error: true };
  } finally {
    await client.close();
  }
}
```

```ts config.ts
import { HelpDesk } from "./help-desk.app";

export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
};
```

```ts help-desk.app.ts
import { App, 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: "open" },
];

@Tool({
  name: "get_ticket",
  description: "Get one support ticket by its id.",
  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}.`, "TICKET_NOT_FOUND"));
    return ticket;
  }
}

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

```ts agent.test.ts hidden
import { test, expect } from "@frontmcp/testing";
import { toolResult } from "./agent";

test("a failed call has `is_error: true`", async () => {
  expect(await toolResult("toolu_1", "get_ticket", { id: "T-9" })).toEqual({
    type: "tool_result",
    tool_use_id: "toolu_1",
    content: [{ type: "text", text: "There's no ticket T-9." }],
    is_error: true,
  });
});

test("a good call has `is_error: false` and the same content blocks", async () => {
  expect(await toolResult("toolu_2", "get_ticket", { id: "T-1" })).toEqual({
    type: "tool_result",
    tool_use_id: "toolu_2",
    content: [{ type: "text", text: '{"id":"T-1","title":"Cannot log in","status":"open"}' }],
    is_error: false,
  });
});
```

**Hint:**
Wrap the call in `try`. What does the error `callTool()` rejects with carry?

**Solution:**
A failed call rejects with a `ToolCallError`, and its `result` is the MCP result the tool sent, content blocks and all. Those blocks are text blocks Claude accepts, so the `catch` sends them as they are, with `is_error: true`. The model reads "There's no ticket T-9." as a failure and can try another id. Any other error is rethrown: it isn't the tool's answer, so it shouldn't reach the model as one. Plain `connect()` works too: it returns the failure as a result with `isError`, which you'd copy into `is_error`.

### Challenge: Don't let one failure stop the job
`closeAll()` in `nightly.ts` closes a list of tickets for the nightly job. One id in tonight's list doesn't exist, and the whole job stops there. Make it close every ticket it can, and report the ones it couldn't with their error `code`. Call the tools as `nightly-cleanup` while you're at it: right now the job runs as `direct`.

```ts nightly.ts active
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./config";

export async function closeAll(ids: string[]) {
  const server = await FrontMcpInstance.createDirect(config);
  const closed: unknown[] = [];
  for (const id of ids) {
    const result = await server.callTool("close_ticket", { id });
    closed.push(result.structuredContent);
  }
  await server.dispose();
  return { closed, failed: [] as { id: string; code: string }[] };
}
```

```ts nightly.ts solution
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./config";

export async function closeAll(ids: string[]) {
  const server = await FrontMcpInstance.createDirect(config);
  const closed: unknown[] = [];
  const failed: { id: string; code: string }[] = [];
  try {
    for (const id of ids) {
      try {
        const result = await server.callTool("close_ticket", { id }, { authContext: { user: { sub: "nightly-cleanup" } } });
        closed.push(result.structuredContent);
      } catch (err) {
        failed.push({ id, code: (err as { code?: string }).code ?? "UNKNOWN" });
      }
    }
  } finally {
    await server.dispose();
  }
  return { closed, failed };
}
```

```ts config.ts
import { HelpDesk } from "./help-desk.app";

export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
};
```

```ts help-desk.app.ts
import { App, 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: "open" },
];

@Tool({
  name: "close_ticket",
  description: "Close a support ticket. Signed-in agents only.",
  inputSchema: { id: z.string().regex(/^T-\d+$/).describe("Ticket id, like T-1") },
})
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    if (this.auth.isAnonymous) this.fail(new PublicMcpError("Sign in to close tickets.", "SIGN_IN_REQUIRED"));
    const ticket = tickets.find((t) => t.id === id);
    if (!ticket) this.fail(new PublicMcpError(`There's no ticket ${id}.`, "TICKET_NOT_FOUND"));
    ticket.status = "closed";
    return { id, status: ticket.status, closed_by: this.auth.user.sub };
  }
}

@App({ id: "help-desk", name: "Help Desk", tools: [CloseTicket] })
export class HelpDesk {}
```

```ts nightly.test.ts hidden
import { test, expect } from "@frontmcp/testing";
import { closeAll } from "./nightly";

test("tickets after a missing one still get closed", async () => {
  const report = await closeAll(["T-1", "T-9", "T-2"]);
  expect(report.closed).toHaveLength(2);
});

test("the missing ticket is reported with its code", async () => {
  const report = await closeAll(["T-1", "T-9", "T-2"]);
  expect(report.failed).toEqual([{ id: "T-9", code: "TICKET_NOT_FOUND" }]);
});

test("tickets are closed by `nightly-cleanup`", async () => {
  const report = await closeAll(["T-1"]);
  expect(report.closed).toEqual([{ id: "T-1", status: "closed", closed_by: "nightly-cleanup" }]);
});
```

**Hint:**
`callTool()` rejects when a tool fails, and the error it rejects with has the tool's `code`. Pass the user as the third argument: `{ authContext: { user: { sub: "nightly-cleanup" } } }`.

**Solution:**
Each call gets its own `try`, so a failure is recorded and the loop moves on. The error is the `PublicMcpError` the tool failed with, so its `code` says why. The outer `try` makes sure the server is disposed even if something unexpected escapes. And the `authContext` puts the job's name on every ticket it closes, instead of the `direct` user FrontMCP fills in when you don't name one.
