# @App

> Group related tools, resources, prompts and providers into an app that a FrontMCP server hosts.

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

`@App` groups related capabilities, the tools, resources and prompts for one area like tickets or billing, together with the providers they use. A [`@FrontMcp`](https://frontmcp.dev/reference/sdk/frontmcp) server hosts one or more apps on the same endpoint. Each app keeps its providers to itself, and can have its own auth and defaults.

```ts
@App(options)
class MyApp {}
```

---

## Reference

### `@App(options)`

Apply `@App` to an empty class, and list the class in `@FrontMcp({ apps })`.

```ts help-desk.app.ts
import { App } from "@frontmcp/sdk";
import { CloseTicket, SearchTickets } from "./tools";
import { TicketResource } from "./ticket.resource";
import { TriageTicket } from "./triage.prompt";
import { TicketStore } from "./ticket-store";

@App({
  id: "help-desk",
  name: "Help Desk",
  description: "Search, triage and close support tickets",
  tools: [SearchTickets, CloseTicket],
  resources: [TicketResource],
  prompts: [TriageTicket],
  providers: [TicketStore],
})
export class HelpDeskApp {}
```

[See more examples below.](#usage)

#### Options

Required:

| Option | Type | Description |
| --- | --- | --- |
| `name` | `string` | A readable name for logs and tooling, like `"Help Desk"`. |

Optional:

| Option | Type | Description |
| --- | --- | --- |
| `id` | `string` | A stable identifier. It prefixes names that clash with another app's, and is the URL path of a [standalone](#standalone-apps) app. Defaults to `name` with spaces replaced by `-`, so set it: short, lowercase, like `help-desk`. |
| `description` | `string` | What the app is for. For your own documentation and tooling; FrontMCP doesn't send it to clients. |
| `tools` | `ToolType[]` | [`@Tool`](https://frontmcp.dev/reference/sdk/tool) classes or `tool()` functions. |
| `resources` | `ResourceType[]` | [`@Resource`](https://frontmcp.dev/reference/sdk/resource) and [`@ResourceTemplate`](https://frontmcp.dev/reference/sdk/resource-template) classes. |
| `prompts` | `PromptType[]` | [`@Prompt`](https://frontmcp.dev/reference/sdk/prompt) classes or `prompt()` functions. |
| `providers` | `ProviderType[]` | [Providers](https://frontmcp.dev/reference/sdk/provider) for this app's tools, resources and prompts. Other apps can't see them. |
| `plugins` | `PluginType[]` | Plugins that hook into this app's requests, such as caching or auditing, and can add tools and providers of their own. See [`@Plugin`](https://frontmcp.dev/reference/sdk/plugin). |
| `adapters` | `AdapterType[]` | Adapters that generate tools from another source, such as an [OpenAPI spec](https://frontmcp.dev/reference/adapters/openapi). |
| `authProviders` | `AuthProviderType[]` | Credentials this app's tools can ask for with `authProviders` on `@Tool`, such as a GitHub token. |
| `auth` | `AuthOptionsInput` | This app's auth, overriding the server's `auth`, for an app with its own endpoint (`standalone` or `splitByApp`). On the shared endpoint only the server's `auth` checks callers, so where the app's would go unchecked the server refuses to start: a server with no `auth`, or in `public`, `static` or `transparent` mode (a `static` app that accepts all of a `static` server's tokens is allowed). In `local` and `remote` mode it starts, and app grants are checked per call only with `incrementalAuth`. The same modes as [`@FrontMcp`](https://frontmcp.dev/reference/sdk/frontmcp#auth). See [Auth for one app](https://frontmcp.dev/reference/auth/modes#auth-for-one-app). (Changed in 1.9.2: before, the shared endpoint ignored it, and the app's tools were as open as the server's.) |
| `output` | `OutputPolicy` | Defaults for this app's tool results: `schemaMode`, `schemaDescriptionFormat`, `allowNonFinite`. Overrides the server's `output`; a tool's own `output` overrides both. See [overriding server defaults](#overriding-server-defaults-for-one-app). |
| `ui` | `{ servingMode? }` | The [serving mode](https://frontmcp.dev/reference/ui#serving-modes) of this app's tools that don't set one. Overrides the server's `ui.servingMode`; a tool's own `ui.servingMode` overrides both. A value that isn't a serving mode throws a `ZodError` when the class is decorated. See [Defaults for the server and an app](https://frontmcp.dev/reference/ui#defaults-for-the-server-and-an-app). New in 1.9.4. |
| `standalone` | `boolean \| "includeInParent"` | Serve the app on its own path. Defaults to `false`. See [standalone apps](#standalone-apps). |
| `agents` | `AgentType[]` | Agents: tools backed by their own model and tools. Each is exposed as a tool. |
| `skills` | `SkillType[]` | Skills: step-by-step guides for using several tools together, exposed as resources. |
| `jobs` | `JobType[]` | Jobs: named units of work with typed input and output, for the server's jobs system. |
| `workflows` | `WorkflowType[]` | Workflows that run jobs in steps. |
| `channels` | `ChannelType[]` | Channels that push events to Claude Code sessions. See [`@Channel`](https://frontmcp.dev/reference/sdk/channel). |

#### Standalone apps

By default every app is served on the server's one MCP endpoint. `standalone` gives an app its own endpoint, under its `id`:

| `standalone` | On the main endpoint | On `/<id>` |
| --- | --- | --- |
| `false` (default) | Yes | No |
| `true` | No | Yes |
| `"includeInParent"` | Yes | Yes |

```ts main.ts
@App({ id: "billing", name: "Billing", tools: [GetInvoice], standalone: true })
class BillingApp {}

@FrontMcp({
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDeskApp, BillingApp],
  http: { port: 3000, entryPath: "/mcp" },
})
export default class Server {}

// http://localhost:3000/mcp          → Help Desk tools only
// http://localhost:3000/mcp/billing  → Billing tools only
```

The path is relative to `http.entryPath`: with the default entry path, `/`, the billing app is at `/billing`. The Playground has one endpoint and no paths, so standalone apps can't be shown in it. To give **every** app its own path, use `splitByApp: true` on [`@FrontMcp`](https://frontmcp.dev/reference/sdk/frontmcp).

#### Apps from outside your code

`App.remote()` connects to another MCP server and serves its tools, resources and prompts as an app. `App.esm()` loads an app from an npm package and runs it in your process. [Remote servers](https://frontmcp.dev/reference/server/remote) and [Loading apps from npm](https://frontmcp.dev/reference/server/esm) document every option, with examples that run against stand-in servers in the Playground.

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

@FrontMcp({
  info: { name: "help-desk", version: "1.0.0" },
  apps: [
    HelpDeskApp,
    App.remote("https://status.example.com/mcp", {
      namespace: "status",
      transportOptions: { timeout: 10_000 },
      remoteAuth: { mode: "static", credentials: { type: "bearer", value: process.env.STATUS_TOKEN! } },
    }),
    App.esm("@acme/crm-mcp@^2.0.0", { namespace: "crm" }),
  ],
})
export default class Server {}
```

| Option | Applies to | Description |
| --- | --- | --- |
| `namespace` | both | Prefix for the app's tool, resource and prompt names. For `App.esm()` it defaults to the package name. |
| `name`, `description` | both | Override the name derived from the URL or package, and describe the app. The app's id is the `name` you set, else the `namespace`, else the derived name; two apps with one id stop the server from starting, with `DuplicateAppIdError`. (Changed in 1.9.3: before, the `namespace` didn't count, and the server started.) |
| `standalone` | both | As for `@App`. |
| `filter` | both | Which of the app's tools, resources and prompts to serve, like `{ exclude: { tools: ["delete_*"] } }`. (Changed in 1.9.2: before, it was ignored.) |
| `transportOptions` | `remote` | `timeout` (default 30000 ms, for tool calls only), `fallbackToSSE` (`true`), `headers` for every request, and `retryAttempts` and `retryDelayMs`: a tool call that fails on the network, times out, or gets HTTP `408`, `425`, `429` or a `5xx` other than `501` and `505` is tried again, 3 times in all by default, or `retryAttempts` + 1. See [retries](https://frontmcp.dev/reference/server/remote#retrying-calls-that-fail-on-the-way). (Changed in 1.9.2: before, `retryAttempts` and `retryDelayMs` were ignored. Changed in 1.9.3: before, only network failures were retried.) |
| `remoteAuth` | `remote` | Credentials for the remote server: `{ mode: "static", credentials: { type: "bearer", value } }` sends them on every request. [Remote servers](https://frontmcp.dev/reference/server/remote#authenticating-to-the-remote-server) has the other modes: `forward`, `mapped` (since 1.9.3) and `oauth`. (Changed in 1.9.2: before, it sent nothing.) |
| `refreshInterval`, `cacheTTL` | `remote` | How often to re-read the remote server's capabilities (default never), and how long to cache them (default 60000 ms). |
| `autoUpdate`, `cacheTTL`, `loader`, `importMap` | `esm` | Poll for new versions that match the range; how long to cache a loaded package; a [loader](https://frontmcp.dev/reference/server/esm#the-loader) that replaces the server's for this app; and an import map, which rewrites the package's imports of the packages it names. (Changed in 1.9.2: before, `importMap` was ignored.) |

Without its own `loader`, `App.esm()` fetches packages with the server's [`loader`](https://frontmcp.dev/reference/server/esm#the-loader) settings.

#### `app(options)`

The function form takes the same options and returns an app class, for code that builds apps from data rather than declaring them. See [building an app without a decorator](#building-an-app-without-a-decorator).

#### Caveats

- The class body is ignored. Keep it empty.
- Every entry in `tools`, `resources`, `prompts` and `providers` must be decorated (or made with the function form). The server fails to start otherwise.
- An app's providers are **private to it**. To share one between apps, register it on `@FrontMcp({ providers })`.
- When two apps expose a tool with the same name, FrontMCP renames **both** to `<id>:<name>`. See [when two apps use the same name](#when-two-apps-use-the-same-name).

---

## Usage

### Grouping capabilities into an app

Put everything one area needs in one app: its tools, its resources, its prompts, and the providers they share. Open the **Capabilities** tab to see what the app exposes.

```ts help-desk.app.ts active
import { App } from "@frontmcp/sdk";
import { SearchTickets } from "./search-tickets.tool";
import { TicketResource } from "./ticket.resource";
import { TriageTicket } from "./triage.prompt";
import { TicketStore } from "./ticket-store";

@App({
  id: "help-desk",
  name: "Help Desk",
  tools: [SearchTickets],
  resources: [TicketResource],
  prompts: [TriageTicket],
  providers: [TicketStore],
})
export class HelpDeskApp {}
```

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

@Tool({
  name: "search_tickets",
  description: "Search support tickets by words in their title",
  inputSchema: { query: z.string() },
})
export class SearchTickets extends ToolContext {
  async execute({ query }: { query: string }) {
    return { tickets: this.get(TicketStore).search(query) };
  }
}
```

```ts ticket.resource.ts
import { ResourceContext, ResourceTemplate } from "@frontmcp/sdk";
import { TicketStore } from "./ticket-store";

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

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

@Prompt({
  name: "triage_ticket",
  description: "Suggest a priority and a next step for a ticket",
  arguments: [{ name: "id", description: "Ticket id, like T-1", required: true }],
})
export class TriageTicket extends PromptContext {
  async execute({ id }: Record<string, string>): Promise<GetPromptResult> {
    return { messages: [{ role: "user", content: { type: "text", text: `Read ticket://${id}, then suggest a priority and a next step.` } }] };
  }
}
```

```ts ticket-store.ts
import { Provider } from "@frontmcp/sdk";

@Provider({ name: "TicketStore" })
export class TicketStore {
  private tickets = [
    { id: "T-1", title: "Cannot log in", status: "open" },
    { id: "T-2", title: "Invoice total is wrong", status: "closed" },
    { id: "T-3", title: "Login link expired", status: "open" },
  ];
  search(query: string) {
    return this.tickets.filter((t) => t.title.toLowerCase().includes(query.toLowerCase()));
  }
  find(id: string) {
    return this.tickets.find((t) => t.id === id);
  }
}
```

### Hosting several apps on one server

List each app in `@FrontMcp({ apps })`. Clients see one server, with every app's tools in one `tools/list`.

```ts main.ts active
import { FrontMcp } from "@frontmcp/sdk";
import { HelpDeskApp } from "./help-desk.app";
import { BillingApp } from "./billing.app";

@FrontMcp({
  info: { name: "support", version: "1.0.0" },
  apps: [HelpDeskApp, BillingApp],
})
export default class Server {}
```

```ts help-desk.app.ts
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "close_ticket", description: "Close a support ticket", inputSchema: { id: z.string() } })
class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    return { id, status: "closed" };
  }
}

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

```ts billing.app.ts
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "get_invoice", description: "Get an invoice by id", inputSchema: { id: z.string() } })
class GetInvoice extends ToolContext {
  async execute({ id }: { id: string }) {
    return { id, total: 120, currency: "EUR", paid: false };
  }
}

@App({ id: "billing", name: "Billing", tools: [GetInvoice] })
export class BillingApp {}
```

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

test("one tools/list has both apps' tools", async ({ mcp }) => {
  const names = (await mcp.tools.list()).map((t) => t.name);
  expect(names).toEqual(expect.arrayContaining(["close_ticket", "get_invoice"]));
  expect(names).toHaveLength(2);
});
```

### When two apps use the same name

Tool names must be unique on a server. When two apps each have a `search` tool, FrontMCP keeps both and renames them to `<id>:<name>`. The model sees the prefixed names. A call to the plain name still works, and reaches the first app in `apps` without saying so.

```ts main.ts active
import { App, FrontMcp, Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "search", description: "Search support tickets", inputSchema: { query: z.string() } })
class SearchTickets extends ToolContext {
  async execute({ query }: { query: string }) {
    return { tickets: [`T-1 (matched "${query}")`] };
  }
}

@Tool({ name: "search", description: "Search invoices", inputSchema: { query: z.string() } })
class SearchInvoices extends ToolContext {
  async execute({ query }: { query: string }) {
    return { invoices: [`INV-7 (matched "${query}")`] };
  }
}

@App({ id: "desk", name: "Help Desk", tools: [SearchTickets] })
class DeskApp {}

@App({ id: "billing", name: "Billing", tools: [SearchInvoices] })
class BillingApp {}

@FrontMcp({ info: { name: "support", version: "1.0.0" }, apps: [DeskApp, BillingApp] })
export default class Server {}
```

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

test("both tools are renamed with their app's id", async ({ mcp }) => {
  const names = (await mcp.tools.list()).map((t) => t.name);
  expect(names).toEqual(["billing:search", "desk:search"]);
});

test("the plain name reaches the first app", async ({ mcp }) => {
  expect((await mcp.tools.call("search", { query: "log" })).json()).toHaveProperty("tickets");
});
```

The renaming only happens on a clash, so adding an app can rename tools that clients already use. Give tools names that are unique across the server, like `search_tickets` and `search_invoices`, and the question never comes up.

### Keeping providers private to an app

A provider registered on an app is visible to that app only. One registered on `@FrontMcp` is shared by every app, as a single instance. Here both apps count into the same `Metrics`, but only the help desk can see `TicketStore`.

```ts main.ts active
import { App, FrontMcp, Provider, Tool, ToolContext, z } from "@frontmcp/sdk";

@Provider({ name: "Metrics" })
class Metrics {
  calls = 0;
}

@Provider({ name: "TicketStore" })
class TicketStore {
  count = 3;
}

@Tool({ name: "count_tickets", description: "Count open tickets", inputSchema: {} })
class CountTickets extends ToolContext {
  async execute() {
    const calls = ++this.get(Metrics).calls;
    return { open: this.get(TicketStore).count, calls };
  }
}

@Tool({ name: "get_invoice", description: "Get an invoice by id", inputSchema: { id: z.string() } })
class GetInvoice extends ToolContext {
  async execute({ id }: { id: string }) {
    const calls = ++this.get(Metrics).calls;
    return { id, calls, canSeeTickets: this.tryGet(TicketStore) !== undefined };
  }
}

@App({ id: "desk", name: "Help Desk", tools: [CountTickets], providers: [TicketStore] })
class DeskApp {}

@App({ id: "billing", name: "Billing", tools: [GetInvoice] })
class BillingApp {}

@FrontMcp({ info: { name: "support", version: "1.0.0" }, apps: [DeskApp, BillingApp], providers: [Metrics] })
export default class Server {}
```

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

test("apps share server providers, not each other's", async ({ mcp }) => {
  await mcp.tools.call("count_tickets", {});
  const invoice = await mcp.tools.call("get_invoice", { id: "INV-7" });
  expect(invoice.json()).toEqual({ id: "INV-7", calls: 2, canSeeTickets: false });
});
```

### Overriding server defaults for one app

Some settings cascade from the server to its apps to each tool, and the most specific one wins. `output` is one: here the server hides output schemas, and the help desk app puts them in its tools' descriptions instead.

```ts main.ts active
import { App, FrontMcp, Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({
  name: "ticket_stats",
  description: "Count open and closed tickets",
  inputSchema: {},
  outputSchema: z.object({ open: z.number(), closed: z.number() }),
})
class TicketStats extends ToolContext {
  async execute() {
    return { open: 12, closed: 40 };
  }
}

@Tool({
  name: "invoice_stats",
  description: "Count paid and unpaid invoices",
  inputSchema: {},
  outputSchema: z.object({ paid: z.number(), unpaid: z.number() }),
})
class InvoiceStats extends ToolContext {
  async execute() {
    return { paid: 30, unpaid: 4 };
  }
}

@App({ id: "desk", name: "Help Desk", tools: [TicketStats], output: { schemaMode: "description" } })
class DeskApp {}

@App({ id: "billing", name: "Billing", tools: [InvoiceStats] })
class BillingApp {}

@FrontMcp({ info: { name: "support", version: "1.0.0" }, apps: [DeskApp, BillingApp], output: { schemaMode: "none" } })
export default class Server {}
```

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

test("the app's setting beats the server's", async ({ mcp }) => {
  const tool = (await mcp.tools.list()).find((t) => t.name === "ticket_stats");
  expect(tool?.outputSchema).toBeUndefined();
  expect(tool?.description).toContain("**Returns:**");
});

test("other apps keep the server's setting", async ({ mcp }) => {
  const tool = (await mcp.tools.list()).find((t) => t.name === "invoice_stats");
  expect(tool?.outputSchema).toBeUndefined();
  expect(tool?.description).toBe("Count paid and unpaid invoices");
});
```

`schemaMode` can be `"definition"` (the default: send it as `outputSchema`), `"description"`, `"both"` or `"none"`. `ui.servingMode` cascades the same way (since 1.9.4): see [Defaults for the server and an app](https://frontmcp.dev/reference/ui#defaults-for-the-server-and-an-app).

### Building an app without a decorator

`app()` builds an app from a plain object. It's useful when the list of tools comes from data, such as a config file or a loop.

```ts help-desk.ts
import { app, tool } from "@frontmcp/sdk";

const counts = { open: 12, closed: 40, pending: 3 };

const tools = Object.entries(counts).map(([status, count]) =>
  tool({ name: `count_${status}`, description: `How many tickets are ${status}`, inputSchema: {} })(async () => ({ count })),
);

export const helpDesk = app({ id: "help-desk", name: "Help Desk", tools });
```

---

## Troubleshooting

### `tools items must be annotated with @Tool() | @FrontMcpTool(), be a package specifier string, or come from Tool.esm() | Tool.remote().`

The server fails to start with `@App invalid metadata for "tools"`, and the index of the bad entry. That entry is a class without `@Tool`. (Changed in 1.9.1: the message used to end at `or be a package specifier string.`, before `Tool.esm()` and `Tool.remote()` entries were accepted.) Check that the decorator is there and that you imported the class, not something else with the same name. The same kind of message appears for `resources`, `prompts` and `providers`.

### A tool was renamed to `desk:search`

Another app on the same server has a tool with the same name, so FrontMCP prefixed both with their app's `id`. Rename one of the tools. See [when two apps use the same name](#when-two-apps-use-the-same-name).

### `Provider "TicketStore" is not available: not found in local or parent registries`

The tool's app can't see the provider. Often it's registered on a **different** app:

```ts main.ts active
import { App, FrontMcp, Provider, Tool, ToolContext, z } from "@frontmcp/sdk";

@Provider({ name: "TicketStore" })
class TicketStore {
  forInvoice(id: string) {
    return ["T-2"];
  }
}

@Tool({ name: "get_invoice", description: "Get an invoice and its tickets", inputSchema: { id: z.string() } })
class GetInvoice extends ToolContext {
  async execute({ id }: { id: string }) {
    return { id, tickets: this.get(TicketStore).forInvoice(id) };
  }
}

// 🚩 TicketStore belongs to the help desk app, so billing can't see it
@App({ id: "desk", name: "Help Desk", providers: [TicketStore] })
class DeskApp {}

@App({ id: "billing", name: "Billing", tools: [GetInvoice] })
class BillingApp {}

@FrontMcp({ info: { name: "support", version: "1.0.0" }, apps: [DeskApp, BillingApp] })
export default class Server {}
```

Move the provider to `@FrontMcp({ providers })` to share it, or register it in the app that uses it.

### A standalone app's tools are missing

With `standalone: true`, an app is only served at `<entryPath>/<id>`, not on the main endpoint. Point the client at the app's own URL, or use `standalone: "includeInParent"` to serve it in both places.
