@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 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.
@App(options)
class MyApp {}
Reference
@App(options)
Apply @App to an empty class, and list the class in @FrontMcp({ apps }).
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 {}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 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 classes or tool() functions. |
resources | ResourceType[] | @Resource and @ResourceTemplate classes. |
prompts | PromptType[] | @Prompt classes or prompt() functions. |
providers | ProviderType[] | Providers 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. |
adapters | AdapterType[] | Adapters that generate tools from another source, such as an OpenAPI spec. |
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. See 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. |
ui | { servingMode? } | The serving mode 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. New in 1.9.4. |
standalone | boolean | "includeInParent" | Serve the app on its own path. Defaults to false. See 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. |
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 |
@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 onlyThe 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.
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 and Loading apps from npm document every option, with examples that run against stand-in servers in the Playground.
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. (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 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 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 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.
Caveats
- The class body is ignored. Keep it empty.
- Every entry in
tools,resources,promptsandprovidersmust 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.
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.
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 {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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.
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 {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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.
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 {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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.
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 {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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.
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 {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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.
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.
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 });Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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.
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:
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 {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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.