# create() and createDirect()

> Build a server inside your own process and call its tools, resources, prompts and jobs as plain async functions, as a user you name.

Source: https://frontmcp.dev/reference/sdk/create

`create()` and `FrontMcpInstance.createDirect()` build a server inside your process and hand you its tools, resources and prompts as plain async methods. There's no transport, no client and no protocol in between: `callTool()` runs the same checks, hooks and `execute()` as a call from a client, and returns the result, or throws the error. Use them in tests, scripts, scheduled jobs, and in code that embeds a server. `create()` takes a flat configuration with `tools` at the top level; `createDirect()` takes the one you give `@FrontMcp`.

```ts
const server = await create(config)
const server = await FrontMcpInstance.createDirect(config)

const result = await server.callTool(name, args?, { authContext? })
await server.dispose()
```

---

## Reference

### `create(config)`

Pass your server's name and its entries. `create()` puts them in one app and builds a server around it:

```ts nightly.ts
import { create } from "@frontmcp/sdk";
import { CloseTicket } from "./close-ticket.tool";

const server = await create({ info: { name: "help-desk", version: "1.0.0" }, tools: [CloseTicket] });
try {
  const result = await server.callTool("close_ticket", { id: "T-1" }, { authContext: { user: { sub: "nightly-cleanup" } } });
  console.log(result.structuredContent);
} finally {
  await server.dispose();
}
```

[See more examples below.](#usage)

#### Options

| Option | Type | Description |
| --- | --- | --- |
| `info` | `{ name, version, … }` | **Required.** As on [`@FrontMcp`](https://frontmcp.dev/reference/sdk/frontmcp#info). |
| `tools`, `resources`, `prompts` | arrays | The entries to serve: classes, or `tool()`, `resource()` and `prompt()` results. |
| `providers`, `plugins`, `adapters` | arrays | As on [`@App`](https://frontmcp.dev/reference/sdk/app). |
| `agents`, `skills`, `authProviders` | arrays | As on `@App`. |
| `jobDefinitions`, `workflowDefinitions` | arrays | The app's jobs and workflows. Note the names: `@App` calls them `jobs` and `workflows`. |
| `jobs` | `{ enabled, store? }` | The jobs system itself, as on `@FrontMcp`. |
| `auth` | `AuthOptions` | Set on the app. In-process calls skip authentication, so even with `mode: "static"` a call without a token goes through, as `direct`. |
| Every other `@FrontMcp` option | | As on `@FrontMcp`: `fetch`, `authorities`, `instructions`, `ui`, `output`, `throttle`, `elicitation`, `pagination`, `logging`, `redis` and the rest, except `http` and `splitByApp`. [See below.](#keeping-the-servers-options) |
| `appName` | `string` | The name of the app `create()` builds around your entries. Defaults to `info.name`. |
| `cacheKey` | `string` | Return the same server for every `create()` with this key, until it's disposed. [See below.](#reusing-one-server) |
| `machineId` | `string` | Replaces FrontMCP's machine id, which session ids derive from, for the whole process, until this server is disposed. Keeps sessions stored in Redis valid across restarts. |
| `workerEnv` | `Record<string, unknown>` | Platform bindings, like a Cloudflare Worker's `env`, that every call and every client from `server.connect()` hands to the code it runs, as [`this.workerEnv`](https://frontmcp.dev/reference/sdk/contexts#thisworkerenv). A call's or a client's own `workerEnv` replaces it. [See below.](#passing-a-workers-bindings) New in 1.9.4. |

`http` and `splitByApp` are dropped: a server in your process has no HTTP endpoint, and `create()` builds one app. TypeScript rejects them in an object literal. (Changed in 1.9.3: before, `create()` also dropped `fetch`, `authorities`, `instructions`, `ui` and most other server options, without an error. Changed in 1.9.2: before, it dropped `output` and `throttle` too.)

### `FrontMcpInstance.createDirect(config)`

Takes the configuration you give `@FrontMcp`, apps and all, and honours every option except `http`:

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

const server = await FrontMcpInstance.createDirect(config);
const billing = await FrontMcpInstance.createDirect(config, { app: "billing" });
```

`workerEnv` goes in the same object, as for `create()`: `createDirect({ ...config, workerEnv: env })`.

It needs `apps`: a `create()`-style configuration fails with `Invalid input: expected array, received undefined` at `apps`. It needs the configuration object, not the decorated class; pass `getDecoratorConfig(Server)` if all you have is the class.

It serves the server's main endpoint: every app that isn't `standalone: true`, or with `splitByApp: true`, the first app. The second argument, `{ app }`, serves the endpoint an app has of its own instead: each app's with `splitByApp`, a `standalone` app's otherwise. An app without one rejects with `ScopeConfigurationError`, `No endpoint serves app "…" on its own`, naming the apps that have one. [`connect()`](https://frontmcp.dev/reference/sdk/connect#connecting-to-one-app) takes the same option. New in 1.9.3: before, with `splitByApp`, only the first app could be served in-process.

### The `DirectMcpServer`

Both return a `DirectMcpServer`, built and ready:

| Method | Returns |
| --- | --- |
| `listTools(options?)` | `{ tools }`: every tool, however many pages `tools/list` splits them into. `{ paginate: true }` returns one page and its `nextCursor`, and `{ cursor }` the page after it. [See below.](#listing-more-than-one-page-of-tools) |
| `callTool(name, args?, options?)` | The tool's MCP result: `content`, and `structuredContent` for object results. **Throws** when the call fails. |
| `listResources(options?)` | `{ resources }`, every page; `paginate` and `cursor` as for `listTools()`. |
| `listResourceTemplates(options?)` | `{ resourceTemplates }`, the same. |
| `readResource(uri, options?)` | `{ contents }`. |
| `listPrompts(options?)` | `{ prompts }`, the same. |
| `getPrompt(name, args?, options?)` | `{ description, messages }`. |
| `listJobs(options?)`, `listWorkflows(options?)` | The result of the `list_jobs` or `list_workflows` tool: the list is JSON in `content[0].text`. |
| `executeJob(name, input?, options?)`, `executeWorkflow(name, input?, options?)` | The `execute_job` or `execute_workflow` result. `structuredContent` has `runId`, `state` and, once done, `result`. With `options.background: true`, the run goes on [in the background](https://frontmcp.dev/learn/running-jobs-in-the-background). |
| `getJobStatus(runId, options?)`, `getWorkflowStatus(runId, options?)` | The run's status, in `structuredContent`. |
| `connect(sessionIdOrOptions?)` | A [`DirectClient`](https://frontmcp.dev/reference/sdk/connect#the-directclient) connected to this server, for code that wants an MCP client. Closing it leaves the server running, for its other clients too; only `dispose()` ends it. (Changed in 1.9.3: before, closing one disposed part of the server, so `registerTool()` failed and the other clients stopped getting notifications.) |
| `registerTool(definition)` | Adds a tool while the server runs, and returns a function that removes it. New in 1.9: see [`registerTool()`](https://frontmcp.dev/reference/sdk/register-tool). |
| `dispose()` | Releases the server. [See below.](#disposing) |
| `ready` | A promise that has already resolved. |

The job and workflow methods call the built-in tools that run [jobs](https://frontmcp.dev/reference/sdk/job), so they return tool results, not parsed objects.

#### Call options

Every method takes `options` last:

| Option | Description |
| --- | --- |
| `authContext` | Who is calling. [See below.](#who-is-calling) |
| `metadata` | `{ userAgent?, clientIp?, customHeaders? }`, which becomes `this.context.metadata`, as an HTTP request's headers would. Only `x-frontmcp-*` headers are kept in `customHeaders`, and `clientIp` only if it's an IP address. An `x-frontmcp-trace-id` header with a 32-character hex trace id continues that trace. New in 1.9.2: before, tools always saw `{ customHeaders: {} }`. |
| `workerEnv` | Bindings for this call only, read as `this.workerEnv` by the tool, resource, prompt, job or agent it runs. They replace the server's `workerEnv` rather than adding to it. New in 1.9.4. |

#### Who is calling

`authContext` stands in for what authentication would have established. Nothing checks it: in-process, your code is the authentication, so pass only a user your code has verified.

| Field | Becomes |
| --- | --- |
| `user` | The caller's claims. `user.sub` is `this.auth.user.sub`; `roles`, `name` and `email` are read as from a token, so `this.auth.hasRole()` works. The claims also get `iss: "direct"`. |
| `scopes` | `this.auth.scopes` and `this.context.authInfo.scopes`, so `this.auth.hasScope()` works. A `scope` claim in `user` isn't read. New in 1.9.2: before, in-process calls had no scopes. |
| `token` | `this.context.authInfo.token`, which [`this.fetch()`](https://frontmcp.dev/reference/sdk/fetch) sends to the origins in `fetch.forwardCallerTokenTo`. |
| `sessionId` | `this.context.sessionId`. By default each user gets their own, derived from the server's. |
| `extra` | `this.context.authInfo.extra`. |

Two things differ from a real caller:

- **Without `authContext`, the caller is signed in, as `direct`.** `this.auth.user.sub` is `"direct"` and `this.auth.isAnonymous` is `false`, so a check that refuses anonymous callers lets the call through. Always pass a user.
- **Scopes come only from `authContext.scopes`.** A token's `scope` claim becomes the caller's scopes over HTTP, but `user.scope` isn't read here: without `scopes`, `this.auth.scopes` is `[]`.

Each call gets a `this.context` of its own, with its own `requestId` and caller, so one server can serve many users at once.

#### What a tool sees

| On `this` | In-process |
| --- | --- |
| `this.auth` | The `authContext` user, or `direct`. |
| `this.context.traceContext` | A new trace for every call, unless `metadata` names one with `x-frontmcp-trace-id`. |
| `this.context.metadata` | The call's `metadata`, or `{ customHeaders: {} }` without it. |
| `this.workerEnv` | The call's `workerEnv`, else the server's, else `undefined`. |
| `this.clientInfo`, `this.platform` | `undefined` and `"unknown"`: there's no client. |
| `this.elicit()` | Can't ask anyone. With elicitation turned on, the call returns FrontMCP's fallback result, "This tool requires user input to continue", instead of the answer. |
| `this.progress()`, `this.notify()` | Send nothing, and return `false`. |

#### Errors

A failed call **rejects** instead of returning a result with `isError`:

| Failure | Rejects with |
| --- | --- |
| The tool failed with [`this.fail(error)`](https://frontmcp.dev/reference/sdk/fail) or threw a `PublicMcpError` | That error, `code` included. |
| Invalid arguments | `InvalidInputError`, `code` `INVALID_INPUT`, message `Invalid tool input`. `getPublicMessage()` lists the problems. |
| No such tool, resource or prompt | `ToolNotFoundError` (`TOOL_NOT_FOUND`), `ResourceNotFoundError` (`RESOURCE_NOT_FOUND`) or `PromptNotFoundError` (`PROMPT_NOT_FOUND`). |
| The server was disposed | `InternalMcpError`: `DirectMcpServer has been disposed`. |

Wrap calls in `try` when one failure shouldn't stop your code.

#### Disposing

`dispose()` shuts the whole server down: every endpoint is disposed, those it doesn't serve included, and their [`onDispose`](https://frontmcp.dev/reference/sdk/scope) callbacks run. Every call after it rejects with `DirectMcpServer has been disposed`, and a second `dispose()` does nothing. (Changed in 1.9.3: before, it disposed only the endpoint it served.) It also removes the server from the `cacheKey` cache and undoes `machineId`. Call it when you're done, in a `finally`, so a failed call doesn't leave a server behind. `clearCreateCache()` empties the `cacheKey` cache without disposing anything.

#### Caveats

- **`create()` drops `http` and `splitByApp`**, and takes every other server option.
- **Failures throw.** Code that expects MCP results, with `isError`, gets exceptions instead. For MCP results, use a [client](https://frontmcp.dev/reference/sdk/connect).
- **No `authContext` means a signed-in user called `direct`**, not an anonymous caller.
- **`machineId` has no effect in a CommonJS project** (seen with 1.9.4): set the id with `setMachineIdOverride()` before the server starts instead ([Machine ids](https://frontmcp.dev/reference/deployment/high-availability#machine-ids)).
- **Scopes come from `authContext.scopes`** only, never from the user's claims.
- `list_jobs` and `list_workflows` aren't in `listTools()`, but `listJobs()` and `listWorkflows()` call them anyway. A job's run belongs to the user who started it: check its status with the same `authContext`, or it's `Run "…" not found`.

---

## Usage

### Calling a tool as a signed-in user

A tool that decides from [`this.auth`](https://frontmcp.dev/learn/authorizing-calls) needs a caller. Name one in `authContext`, with the claims your tool reads:

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

@Tool({
  name: "reassign_ticket",
  description: "Move a ticket to another agent. Team leads only.",
  inputSchema: { id: z.string(), to: z.string() },
})
export class ReassignTicket extends ToolContext {
  async execute({ id, to }: { id: string; to: string }) {
    if (!this.auth.hasRole("lead")) this.fail(new PublicMcpError("Only team leads can reassign tickets.", "LEADS_ONLY"));
    return { id, assignee: to, by: this.auth.user.sub };
  }
}
```

```ts reassign.test.ts
import { test, expect } from "@frontmcp/testing";
import { create } from "@frontmcp/sdk";
import { ReassignTicket } from "./reassign-ticket.tool";

const info = { name: "help-desk", version: "1.0.0" };

test("a lead can reassign", async () => {
  const server = await create({ info, tools: [ReassignTicket] });
  try {
    const nour = { user: { sub: "nour", roles: ["lead"] } };
    const result = await server.callTool("reassign_ticket", { id: "T-1", to: "sam" }, { authContext: nour });
    expect(result.structuredContent).toEqual({ id: "T-1", assignee: "sam", by: "nour" });
  } finally {
    await server.dispose();
  }
});

test("an agent without the role can't", async () => {
  const server = await create({ info, tools: [ReassignTicket] });
  try {
    const sam = { user: { sub: "sam", roles: ["agent"] } };
    await expect(server.callTool("reassign_ticket", { id: "T-1", to: "bo" }, { authContext: sam })).rejects.toMatchObject({ code: "LEADS_ONLY" });
  } finally {
    await server.dispose();
  }
});
```

### What a tool sees in-process

This tool reports what it knows about its call. The tests show the `direct` user FrontMCP fills in without `authContext`, what `authContext` and `metadata` pass through, and what's missing because there's no client:

```ts describe-call.tool.ts active
import { Tool, ToolContext } from "@frontmcp/sdk";

@Tool({ name: "describe_call", description: "Describe the caller and the request. For debugging.", inputSchema: {} })
export class DescribeCall extends ToolContext {
  async execute() {
    return {
      user: this.auth.user.sub,
      anonymous: this.auth.isAnonymous,
      roles: this.auth.roles,
      scopes: this.auth.scopes,
      canWrite: this.auth.hasScope("tickets:write"),
      token: this.context.authInfo.token ?? null,
      extra: this.context.authInfo.extra ?? null,
      metadata: this.context.metadata,
      traceId: this.context.traceContext.traceId,
      client: this.clientInfo ?? null,
      platform: this.platform,
      progressSent: await this.progress(1, 2),
    };
  }
}
```

```ts describe-call.test.ts
import { test, expect } from "@frontmcp/testing";
import { create } from "@frontmcp/sdk";
import { DescribeCall } from "./describe-call.tool";

const info = { name: "help-desk", version: "1.0.0" };

test("🚩 without authContext, a signed-in user called `direct`", async () => {
  const server = await create({ info, tools: [DescribeCall] });
  try {
    expect((await server.callTool("describe_call")).structuredContent).toMatchObject({ user: "direct", anonymous: false, scopes: [] });
  } finally {
    await server.dispose();
  }
});

test("authContext and metadata reach the tool", async () => {
  const server = await create({ info, tools: [DescribeCall] });
  try {
    const result = await server.callTool("describe_call", {}, {
      authContext: { user: { sub: "nour", roles: ["lead"] }, scopes: ["tickets:write"], token: "tok_123", extra: { tenant: "acme" } },
      metadata: { userAgent: "nightly/1.0", clientIp: "10.0.0.7", customHeaders: { "x-frontmcp-desk": "berlin", "x-api-key": "not kept" } },
    });
    expect(result.structuredContent).toEqual({
      user: "nour",
      anonymous: false,
      roles: ["lead"],
      scopes: ["tickets:write"],
      canWrite: true,
      token: "tok_123",
      extra: { tenant: "acme" },
      metadata: { userAgent: "nightly/1.0", clientIp: "10.0.0.7", customHeaders: { "x-frontmcp-desk": "berlin" } },
      traceId: expect.stringMatching(/^[0-9a-f]{32}$/),
      client: null,
      platform: "unknown",
      progressSent: false,
    });
  } finally {
    await server.dispose();
  }
});

test("🚩 a scope claim in user isn't read; x-frontmcp-trace-id continues a trace", async () => {
  const server = await create({ info, tools: [DescribeCall] });
  try {
    const result = await server.callTool("describe_call", {}, {
      authContext: { user: { sub: "nour", scope: "tickets:write" } },
      metadata: { customHeaders: { "x-frontmcp-trace-id": "4bf92f3577b34da6a3ce929d0e0e4736" } },
    });
    expect(result.structuredContent).toMatchObject({ scopes: [], canWrite: false, traceId: "4bf92f3577b34da6a3ce929d0e0e4736" });
  } finally {
    await server.dispose();
  }
});
```

The Playground's own call goes through its client, so there the caller is an anonymous `anon:` user and the client is the Playground.

### Handling failures

A failed call rejects with the error the tool failed with, so its `code` tells you why. Give each call its own `try`, so one failure doesn't stop the rest:

```ts close-all.ts active
import { create } from "@frontmcp/sdk";
import { CloseTicket } from "./close-ticket.tool";

export async function closeAll(ids: string[]) {
  const server = await create({ info: { name: "help-desk", version: "1.0.0" }, tools: [CloseTicket] });
  const closed: string[] = [];
  const failed: { id: string; code: string }[] = [];
  try {
    for (const id of ids) {
      try {
        await server.callTool("close_ticket", { id }, { authContext: { user: { sub: "nightly-cleanup" } } });
        closed.push(id);
      } catch (err) {
        failed.push({ id, code: (err as { code?: string }).code ?? "UNKNOWN" });
      }
    }
  } finally {
    await server.dispose();
  }
  return { closed, failed };
}
```

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

const open = new Set(["T-1", "T-2"]);

@Tool({ name: "close_ticket", description: "Close a support ticket", inputSchema: { id: z.string().regex(/^T-\d+$/) } })
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    if (!open.has(id)) this.fail(new PublicMcpError(`There's no open ticket ${id}.`, "TICKET_NOT_FOUND"));
    open.delete(id);
    return { id, status: "closed" };
  }
}
```

```ts close-all.test.ts
import { test, expect } from "@frontmcp/testing";
import { create } from "@frontmcp/sdk";
import { closeAll } from "./close-all";
import { CloseTicket } from "./close-ticket.tool";

test("tickets after a failed one still get closed", async () => {
  expect(await closeAll(["T-1", "T-9", "bad id", "T-2"])).toEqual({
    closed: ["T-1", "T-2"],
    failed: [
      { id: "T-9", code: "TICKET_NOT_FOUND" },
      { id: "bad id", code: "INVALID_INPUT" },
    ],
  });
});

test("each failure is an error with a code", async () => {
  const server = await create({ info: { name: "help-desk", version: "1.0.0" }, tools: [CloseTicket] });
  try {
    await expect(server.callTool("close_ticket", { id: 9 })).rejects.toMatchObject({ name: "InvalidInputError", message: "Invalid tool input" });
    await expect(server.callTool("reopen_ticket", {})).rejects.toMatchObject({ code: "TOOL_NOT_FOUND", message: 'Tool "reopen_ticket" not found' });
  } finally {
    await server.dispose();
  }
});
```

### Reading resources and prompts

Resources and prompts work the same way. Results are what a client would get:

```ts desk.ts active
import { Prompt, PromptContext, Resource, ResourceContext } from "@frontmcp/sdk";

@Resource({ uri: "tickets://open", name: "open-tickets", description: "Open tickets", mimeType: "application/json" })
export class OpenTickets extends ResourceContext {
  async execute() {
    return { tickets: [{ id: "T-1", title: "Cannot log in" }] };
  }
}

@Prompt({ name: "triage", description: "Triage a ticket", arguments: [{ name: "id", required: true }] })
export class Triage extends PromptContext {
  async execute({ id }: { id: string }) {
    return { messages: [{ role: "user" as const, content: { type: "text" as const, text: `Triage ticket ${id}. Say how urgent it is.` } }] };
  }
}
```

```ts desk.test.ts
import { test, expect } from "@frontmcp/testing";
import { create } from "@frontmcp/sdk";
import { OpenTickets, Triage } from "./desk";

test("reads a resource and gets a prompt", async () => {
  const server = await create({ info: { name: "help-desk", version: "1.0.0" }, resources: [OpenTickets], prompts: [Triage] });
  try {
    expect(await server.readResource("tickets://open")).toMatchObject({
      contents: [{ uri: "tickets://open", mimeType: "application/json", text: '{"tickets":[{"id":"T-1","title":"Cannot log in"}]}' }],
    });
    expect((await server.getPrompt("triage", { id: "T-1" })).messages).toEqual([
      { role: "user", content: { type: "text", text: "Triage ticket T-1. Say how urgent it is." } },
    ]);
    await expect(server.readResource("tickets://closed")).rejects.toMatchObject({ code: "RESOURCE_NOT_FOUND" });
  } finally {
    await server.dispose();
  }
});
```

### Keeping the server's options

`create()` takes the server options `@FrontMcp` takes. Here the server lets results contain `NaN`, with `output: { allowNonFinite: true }`, limits each tool to one call a minute with `throttle`, and defines the `admin` profile that `purge_closed`'s `authorities: "admin"` names. (A client would see the `NaN` as `null`, as the text block shows. In-process, `structuredContent` is the object itself, never serialized, so it's still `NaN`.) `createDirect()` takes the same options, with the entries in an app:

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

@Tool({ name: "average_reply_hours", description: "Average hours to first reply this week", inputSchema: {} })
export class AverageReplyHours extends ToolContext {
  async execute() {
    const replies: number[] = [];
    return { hours: replies.reduce((a, b) => a + b, 0) / replies.length }; // 0 / 0 is NaN
  }
}

// Stops a server without `authorities` from starting, so it's made in the tests, not served by the Playground.
export const purgeClosed = () => {
  @Tool({ name: "purge_closed", description: "Delete closed tickets, for admins", inputSchema: {}, authorities: "admin" })
  class PurgeClosed extends ToolContext {
    async execute() {
      return { purged: 31 };
    }
  }
  return PurgeClosed;
};

export const info = { name: "help-desk", version: "1.0.0" };
export const output = { allowNonFinite: true };
export const throttle = { enabled: true, defaultRateLimit: { maxRequests: 1, windowMs: 60_000 } };
export const authorities = { profiles: { admin: { roles: { any: ["admin"] } } } };
```

```ts options.test.ts
import { test, expect } from "@frontmcp/testing";
import { App, FrontMcpInstance, create } from "@frontmcp/sdk";
import { AverageReplyHours, authorities, info, output, purgeClosed, throttle } from "./options";

test("create() keeps output and throttle", async () => {
  const server = await create({ info, tools: [AverageReplyHours], output, throttle });
  try {
    const result = await server.callTool("average_reply_hours");
    expect(result.content).toEqual([{ type: "text", text: '{"hours":null}' }]);
    expect(result.structuredContent).toEqual({ hours: NaN }); // not serialized in-process
    await expect(server.callTool("average_reply_hours")).rejects.toMatchObject({ code: "RATE_LIMIT_EXCEEDED" });
  } finally {
    await server.dispose();
  }
});

test("create() keeps authorities", async () => {
  const server = await create({ info, tools: [purgeClosed()], authorities });
  try {
    const admin = { authContext: { user: { sub: "nour", roles: ["admin"] } } };
    expect((await server.callTool("purge_closed", {}, admin)).structuredContent).toEqual({ purged: 31 });
    const agent = { authContext: { user: { sub: "sam", roles: ["agent"] } } };
    await expect(server.callTool("purge_closed", {}, agent)).rejects.toMatchObject({ code: "AUTHORITY_DENIED" });
  } finally {
    await server.dispose();
  }
});

test("createDirect() takes the same options", async () => {
  @App({ id: "reports", name: "Reports", tools: [AverageReplyHours, purgeClosed()] })
  class Reports {}

  const server = await FrontMcpInstance.createDirect({ info, apps: [Reports], output, throttle, authorities });
  try {
    const admin = { authContext: { user: { sub: "nour", roles: ["admin"] } } };
    expect((await server.callTool("purge_closed", {}, admin)).structuredContent).toEqual({ purged: 31 });
    expect((await server.callTool("average_reply_hours")).structuredContent).toEqual({ hours: NaN });
    await expect(server.callTool("average_reply_hours")).rejects.toMatchObject({ code: "RATE_LIMIT_EXCEEDED" });
  } finally {
    await server.dispose();
  }
});
```

### Listing more than one page of tools

`tools/list` returns 40 tools at a time once a server has more than 40, with a `nextCursor` for the rest (see [`pagination`](https://frontmcp.dev/reference/sdk/frontmcp#paging-long-tool-lists)). `listTools()` follows `nextCursor` to the end and returns every tool. To page through the list as a client does, pass `{ paginate: true }` for the first page and `{ cursor }` for each one after. A `DirectClient` from `server.connect()` reads every page too, with its `listTools()`, `listResources()`, `listResourceTemplates()` and `listPrompts()`:

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

function numberedTool(n: number) {
  @Tool({ name: `tool_${n}`, description: `Tool number ${n}`, inputSchema: {} })
  class NumberedTool extends ToolContext {
    async execute() {
      return { n };
    }
  }
  return NumberedTool;
}

export const numbered = (count: number) => Array.from({ length: count }, (_, i) => numberedTool(i + 1));

@Tool({ name: "ping", description: "Check that the server is up", inputSchema: {} })
export class Ping extends ToolContext {
  async execute() {
    return { ok: true };
  }
}
```

```ts paging.test.ts
import { test, expect } from "@frontmcp/testing";
import { create } from "@frontmcp/sdk";
import { numbered } from "./tools";

const info = { name: "help-desk", version: "1.0.0" };

test("listTools() reads every page", async () => {
  const server = await create({ info, tools: numbered(45) });
  try {
    const all = await server.listTools();
    expect(all.tools).toHaveLength(45);
    expect(all.nextCursor).toBeUndefined();
  } finally {
    await server.dispose();
  }
});

test("`paginate: true` returns one page, and `cursor` the next", async () => {
  const server = await create({ info, tools: numbered(45) });
  try {
    const first = await server.listTools({ paginate: true });
    expect(first.tools).toHaveLength(40);
    expect(first.nextCursor).toBeDefined();
    const second = await server.listTools({ cursor: first.nextCursor });
    expect(second.tools).toHaveLength(5);
    expect(second.nextCursor).toBeUndefined();
  } finally {
    await server.dispose();
  }
});

test("a client from connect() reads every page", async () => {
  const server = await create({ info, tools: numbered(45) });
  const client = await server.connect();
  try {
    expect(await client.listTools()).toHaveLength(45);
  } finally {
    await client.close();
    await server.dispose();
  }
});

test("`pagination` changes the page size, or turns paging off", async () => {
  const small = await create({ info, tools: numbered(45), pagination: { tools: { mode: true, pageSize: 10 } } });
  const off = await create({ info, tools: numbered(45), pagination: { tools: { mode: false } } });
  try {
    expect((await small.listTools({ paginate: true })).tools).toHaveLength(10);
    const all = await off.listTools({ paginate: true });
    expect(all.tools).toHaveLength(45);
    expect(all.nextCursor).toBeUndefined();
  } finally {
    await small.dispose();
    await off.dispose();
  }
});
```

Changed in 1.9: before, `listTools()` returned only the first page, and its `nextCursor` couldn't be passed back. New in 1.8.7: `listResources()`, `listResourceTemplates()` and `listPrompts()` of a `DirectClient` follow every page too. Before, they returned one; `listTools()` already followed them. The server's `pagination` option covers tools only, and 45 resources or 45 prompts come back in one list, so with your own entries this changes nothing you can see.

### Running jobs

With `jobDefinitions`, the server has [jobs](https://frontmcp.dev/reference/sdk/job), and the job methods run them. A run belongs to the user who started it, so ask for its status as the same user:

```ts count-open.job.ts active
import { Job, JobContext, z } from "@frontmcp/sdk";

@Job({
  name: "count-open-tickets",
  description: "Count a customer's open tickets",
  inputSchema: { customer: z.string() },
  outputSchema: { customer: z.string(), open: z.number() },
})
export class CountOpenTickets extends JobContext {
  async execute({ customer }: { customer: string }) {
    return { customer, open: customer === "acme" ? 3 : 0 };
  }
}
```

```ts jobs.test.ts
import { test, expect } from "@frontmcp/testing";
import { create } from "@frontmcp/sdk";
import { CountOpenTickets } from "./count-open.job";

const asNour = { authContext: { user: { sub: "nour" } } };

test("runs a job and reads its status", async () => {
  const server = await create({ info: { name: "help-desk", version: "1.0.0" }, jobDefinitions: [CountOpenTickets] });
  try {
    const run = (await server.executeJob("count-open-tickets", { customer: "acme" }, asNour)).structuredContent as { runId: string };
    expect(run).toMatchObject({ state: "completed", result: { customer: "acme", open: 3 } });

    const status = await server.getJobStatus(run.runId, asNour);
    expect(status.structuredContent).toMatchObject({ jobName: "count-open-tickets", state: "completed" });
    await expect(server.getJobStatus(run.runId, { authContext: { user: { sub: "sam" } } })).rejects.toMatchObject({ code: "INVALID_INPUT" });
  } finally {
    await server.dispose();
  }
});
```

### Passing a Worker's bindings

On Cloudflare, a server you build with `create()` in a Durable Object, a queue consumer or a route of your own gets no request from [`createFetchHandler()`](https://frontmcp.dev/reference/sdk/create-fetch-handler#reading-a-workers-bindings), so nothing hands its tools the Worker's `env`. Pass it as `workerEnv`: on `create()` or `createDirect()` for every call, on one call for that call, or on `server.connect()` for one client. Tools, resources, prompts, jobs and agents read it as [`this.workerEnv`](https://frontmcp.dev/reference/sdk/contexts#thisworkerenv). Here a stand-in KV namespace plays the Worker's:

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

type Env = { DESK?: string; TICKETS?: { get(key: string): Promise<string | null> } };

@Tool({ name: "get_ticket", description: "Get one support ticket from the desk's KV namespace", inputSchema: { id: z.string() } })
export class GetTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    const env = this.workerEnv as Env | undefined;
    return { id, title: env?.TICKETS ? await env.TICKETS.get(id) : null, desk: env?.DESK ?? null };
  }
}
```

```ts bindings.test.ts
import { test, expect } from "@frontmcp/testing";
import { create } from "@frontmcp/sdk";
import { GetTicket } from "./tickets.tool";

const info = { name: "help-desk", version: "1.0.0" };
const berlin = { DESK: "berlin", TICKETS: { get: async (id: string) => `Cannot log in (${id})` } };

test("every call reads the server's `workerEnv`", async () => {
  const server = await create({ info, tools: [GetTicket], workerEnv: berlin });
  try {
    const result = await server.callTool("get_ticket", { id: "T-1" });
    expect(result.structuredContent).toEqual({ id: "T-1", title: "Cannot log in (T-1)", desk: "berlin" });
    expect(process.env.DESK).toBeUndefined();
  } finally {
    await server.dispose();
  }
});

test("a call's own `workerEnv` replaces the server's, for that call only", async () => {
  const server = await create({ info, tools: [GetTicket], workerEnv: berlin });
  try {
    const lisbon = await server.callTool("get_ticket", { id: "T-1" }, { workerEnv: { DESK: "lisbon" } });
    expect(lisbon.structuredContent).toEqual({ id: "T-1", title: null, desk: "lisbon" });
    expect((await server.callTool("get_ticket", { id: "T-1" })).structuredContent).toMatchObject({ desk: "berlin" });
  } finally {
    await server.dispose();
  }
});

test("a client from `server.connect()` gets the server's, or its own", async () => {
  const server = await create({ info, tools: [GetTicket], workerEnv: berlin });
  const usual = await server.connect();
  const oslo = await server.connect({ workerEnv: { DESK: "oslo" } });
  try {
    expect(((await usual.callTool("get_ticket", { id: "T-1" })) as any).structuredContent).toMatchObject({ desk: "berlin" });
    expect(((await oslo.callTool("get_ticket", { id: "T-1" })) as any).structuredContent).toMatchObject({ desk: "oslo" });
  } finally {
    await usual.close();
    await oslo.close();
    await server.dispose();
  }
});

test("without `workerEnv`, `this.workerEnv` is undefined", async () => {
  const server = await create({ info, tools: [GetTicket] });
  try {
    expect((await server.callTool("get_ticket", { id: "T-1" })).structuredContent).toEqual({ id: "T-1", title: null, desk: null });
  } finally {
    await server.dispose();
  }
});
```

A call's or a client's bindings replace the server's: they aren't merged, so the Lisbon call has no `TICKETS`. They live in that request's context only, and FrontMCP never copies them into `process.env`. A job that `executeJob()` runs reads them too, in the background as well. [`connect()`](https://frontmcp.dev/reference/sdk/connect#parameters) takes `workerEnv` the same way. New in 1.9.4: before, `this.workerEnv` was `undefined` in every in-process call.

### Reusing one server

Building a server takes a moment. When many parts of a process need the same one, like each test in a file, give them a `cacheKey`: every `create()` with that key gets the same server until it's disposed.

```ts server.ts active
import { create } from "@frontmcp/sdk";
import { Ping } from "./ping.tool";

export function helpDesk() {
  return create({ info: { name: "help-desk", version: "1.0.0" }, tools: [Ping], cacheKey: "help-desk" });
}
```

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

@Tool({ name: "ping", description: "Check that the server is up", inputSchema: {} })
export class Ping extends ToolContext {
  async execute() {
    return { ok: true };
  }
}
```

```ts cache.test.ts
import { test, expect } from "@frontmcp/testing";
import { create } from "@frontmcp/sdk";
import { helpDesk } from "./server";

test("the same key gives the same server, until it's disposed", async () => {
  const first = await helpDesk();
  expect(await helpDesk()).toBe(first);
  expect(await create({ info: { name: "other", version: "1.0.0" }, tools: [], cacheKey: "help-desk", workerEnv: { DESK: "oslo" } })).toBe(first);

  await first.dispose();
  await expect(first.callTool("ping")).rejects.toThrow("DirectMcpServer has been disposed");

  const next = await helpDesk();
  try {
    expect(next).not.toBe(first);
    expect((await next.callTool("ping")).structuredContent).toEqual({ ok: true });
  } finally {
    await next.dispose();
  }
});
```

The key is the only thing compared: a second `create()` with the same key and different tools, or a different `workerEnv`, still gets the first server, with the first one's `workerEnv`. Pass bindings that change from call to call with each call's `workerEnv`.

---

## Troubleshooting

### `DirectMcpServer has been disposed`

Something called the server after `dispose()`. Often a shared server with a `cacheKey` was disposed by one part of the code while another still used it; dispose a shared server once, when everything is done with it.

### `Invalid input: expected array, received undefined`

The error's path is `apps`. You passed a `create()`-style configuration, with `tools` at the top level, to `createDirect()` or `connect()`, which need `apps`. Either call `create()`, or put the tools in an `@App` and list it in `apps`.

### A tool that refuses anonymous callers lets my script through

Without `authContext`, the caller is a signed-in user called `direct`. Pass the user the script acts for: `{ authContext: { user: { sub: "nightly-cleanup" } } }`.

### `this.auth.hasScope()` is always `false`

The call passed no `authContext.scopes`. In-process, a caller's scopes come only from there: a `scope` claim in `authContext.user` isn't read. Pass `{ authContext: { user: { sub: "nour" }, scopes: ["tickets:write"] } }`, as in [What a tool sees in-process](#what-a-tool-sees-in-process).

### `Authorities configuration required`

The full message starts `Authorities configuration required: Tool "purge_closed" declare 'authorities' metadata but authorities enforcement is not fully configured`. A tool has `authorities`, and the server has no `authorities` option. Pass the profiles it names, as in [Keeping the server's options](#keeping-the-servers-options). Before 1.9.3, `create()` dropped `authorities`, so a server built with it always failed this way.

### `Run "…" not found`

`getJobStatus()` was called as a different user from the one that started the run, or without `authContext` after starting it with one. Pass the same `authContext` to both.

### A tool that asks the user returns "This tool requires user input to continue"

In-process there's no client to show a form, so `this.elicit()` can't ask. With `elicitation: { enabled: true }`, the call returns FrontMCP's fallback result instead, meant for clients without forms, and never gets an answer:

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

@Tool({ name: "close_ticket", description: "Close a ticket once the user confirms", inputSchema: { id: z.string() } })
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    const answer = await this.elicit(`Close ticket ${id}?`, z.object({ confirm: z.boolean() }));
    return { id, status: answer.status === "accept" && answer.content?.confirm ? "closed" : "open" };
  }
}
```

```ts elicit.test.ts
import { test, expect } from "@frontmcp/testing";
import { create } from "@frontmcp/sdk";
import { CloseTicket } from "./close-ticket.tool";

test("🚩 the call ends with the fallback text, not an answer", async () => {
  const server = await create({ info: { name: "help-desk", version: "1.0.0" }, tools: [CloseTicket], elicitation: { enabled: true } });
  try {
    const result = await server.callTool("close_ticket", { id: "T-1" });
    expect(result.content).toEqual([{ type: "text", text: expect.stringContaining("This tool requires user input to continue") }]);
    expect(result.structuredContent).toBeUndefined();
  } finally {
    await server.dispose();
  }
});
```

The fallback can still be answered in-process: call `sendElicitationResult` with the `elicitId` from the result's `_meta.elicitationPending`, as the same user, and FrontMCP runs the tool again as that user, with `this.auth` (since 1.8.4). [`this.elicit()`](https://frontmcp.dev/reference/sdk/elicit#the-model-is-asked-to-call-sendelicitationresult) shows it with `createDirect()`.

Test tools that ask the user through the [`@frontmcp/testing` client](https://frontmcp.dev/reference/testing), which answers with `mcp.onElicitation()`, or split the question from the work: a tool that takes the confirmation as an argument can be called in-process.
