Grouping Capabilities into Apps

Beginner

As a server grows, its tools, resources and prompts fall into areas: tickets, billing, a knowledge base. An app (@App) is one of those areas, with the capabilities that belong to it and the providers they use. A server (@FrontMcp) puts several apps behind one endpoint. Clients never see the apps, only one list of everything, so splitting a server into apps is mostly about organising your code. It does change two things, and this lesson covers both: what happens when names collide, and which providers each app can reach.

You will learn

  • What an @App groups, and how a @FrontMcp server lists apps
  • What clients see when a server has several apps
  • What FrontMCP does when two apps use the same name
  • Why a provider registered on one app isn't available to another, and how to share it
  • When it's worth splitting a server into apps

One app

Here is the help desk from the previous lessons, laid out as a small project: a server in main.ts, and one app with its tools, resource, prompt and provider in its own folder.

Open
import { FrontMcp } from "@frontmcp/sdk";
import { TicketsApp } from "./apps/tickets/tickets.app";

@FrontMcp({
  info: { name: "Help Desk", version: "1.0.0" },
  apps: [TicketsApp],
})
export default class HelpDeskServer {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Two decorators hold it together:

  • @App lists what belongs to one area: its tools, resources, prompts, and the providers they get with this.get(). id is a short, stable identifier that FrontMCP uses to tell apps apart, and name and description are for people reading your code and logs. A capability that isn't listed in an app doesn't exist as far as clients are concerned.
  • @FrontMcp is the server. It lists apps and holds server-wide settings like info, which clients see as the server's name and version.

Open the Capabilities tab. It shows two tools, a template and a prompt, and nothing about an app called Tickets. MCP has no idea of apps: tools/list, resources/templates/list and prompts/list each return one flat list. (The @App and @FrontMcp references list every option.)

A server with several apps

Billing is a different area, with its own data and its own tools, so it gets its own app. The server lists both:

Open
import { FrontMcp } from "@frontmcp/sdk";
import { TicketsApp } from "./apps/tickets/tickets.app";
import { BillingApp } from "./apps/billing/billing.app";

@FrontMcp({
  info: { name: "Help Desk", version: "1.0.0" },
  apps: [TicketsApp, BillingApp],
})
export default class HelpDeskServer {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The Tests tab shows what a client sees: four tools in one list, each under the name it was declared with. Nothing tells the client which app a tool came from. For a client, this server is no different from one app with four tools.

When two apps use the same name

Each app is written on its own, often by a different person, so it's easy for two of them to pick the same name. Say both teams called their search tool search:

Open
import { FrontMcp } from "@frontmcp/sdk";
import { TicketsApp } from "./apps/tickets.app";
import { BillingApp } from "./apps/billing.app";

@FrontMcp({
  info: { name: "Help Desk", version: "1.0.0" },
  apps: [TicketsApp, BillingApp],
})
export default class HelpDeskServer {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

FrontMCP doesn't refuse to start. When a tool name appears in more than one app, it renames every tool with that name to <app id>:<name>, and logs a warning about the conflict (look in the Logs tab). Names that don't collide are left alone. Open the Tests tab to see the result.

That keeps each tool reachable, but it isn't a good place to stay:

  • The names depend on what else is installed. search was called search until the billing app arrived. Anything that referred to it by name, like a prompt that says "call search", now means something else.
  • The old name still answers. A client that calls plain search reaches the tool in the first app listed in apps, here the ticket search, with no error to say it was ambiguous.
  • The model has only the prefixes and the descriptions to tell them apart. Neither tool declared a title, so tools/list has none for them, and an app id like billing is a word you picked for your code, not for the model.

The fix is to give tools names that are unique across the whole server, and that say what they search: search_tickets and search_invoices. Treat the prefixes as a warning, not as a way to namespace tools.

Sharing a provider between apps

When an invoice is refunded, the ticket that asked for the refund should close. So the billing app's refund_invoice needs the TicketStore, which belongs to the tickets app:

Open
import { PublicMcpError, Tool, ToolContext, z } from "@frontmcp/sdk";
import { TicketStore } from "../tickets/ticket-store";

const invoices = [
  { id: "INV-7", customer: "Acme Corp", total: 1200, status: "paid", ticket: "T-4" },
  { id: "INV-8", customer: "Globex", total: 300, status: "paid", ticket: null },
];

@Tool({
  name: "refund_invoice",
  description: "Refund an invoice in full, and close the ticket that asked for it",
  inputSchema: { id: z.string() },
})
export class RefundInvoice extends ToolContext {
  async execute({ id }: { id: string }) {
    const invoice = invoices.find((i) => i.id === id);
    if (!invoice) this.fail(new PublicMcpError(`There's no invoice ${id}.`));
    invoice.status = "refunded";
    if (invoice.ticket) this.get(TicketStore).close(invoice.ticket);
    return invoice;
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The call fails with Provider "TicketStore" is not available: not found in local or parent registries. Importing the class isn't enough. A provider registered on an app is private to that app: FrontMCP looks for it in the calling tool's app, then in the server, and TicketStore is in neither place for a billing tool.

Move it up to the server. Providers listed in @FrontMcp({ providers }) are available to every app, and with ProviderScope.GLOBAL there's one instance for all of them:

Open
import { FrontMcp } from "@frontmcp/sdk";
import { TicketsApp } from "./apps/tickets/tickets.app";
import { BillingApp } from "./apps/billing/billing.app";
import { TicketStore } from "./shared/ticket-store";

@FrontMcp({
  info: { name: "Help Desk", version: "1.0.0" },
  apps: [TicketsApp, BillingApp],
  providers: [TicketStore],
})
export default class HelpDeskServer {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The store moved to shared/, TicketsApp no longer lists it, and the server does. Both apps' tools get it with the same this.get(TicketStore), and the Tests tab shows a refund in one app closing a ticket that the other app then reads as closed.

When to split into apps

Apps are mostly for you: clients get the same flat list either way. Splitting changes two things at runtime, both covered above: providers registered on an app are private to it, and names that collide across apps get prefixed. That suggests when to split and when not to:

  • Split along the areas of your domain. Tickets, billing and a knowledge base each have their own data, their own services and their own words for things. An app per area keeps each one's providers out of the others' reach.
  • Split where ownership splits. If a different team owns billing, its app is the unit they change, review and test, and it's a class another @FrontMcp server could list too.
  • Put what several apps need on the server. A ticket store that billing also uses, a clock, an HTTP client with your credentials: register it in @FrontMcp({ providers }) once.
  • Don't split just to organise files. Folders do that. Two apps that share everything are one app with extra wiring.
  • Keep names unique across the server. The model sees one list, so search_tickets and search_invoices are better than two search tools in two apps.

Recap

  • @App groups the tools, resources, prompts and providers of one area. @FrontMcp is the server that lists the apps and holds settings like info.
  • Clients don't see apps. tools/list, resources/list and prompts/list each return one flat list across all apps.
  • When two apps use the same tool name, FrontMCP renames each to <app id>:<name> and logs a warning. The bare name still reaches the first app. Pick names that are unique across the server instead.
  • Resource URIs are never renamed. When two apps serve the same URI, only the first app's resource is listed and read. Give each app its own scheme.
  • A provider registered on an app is private to that app. Register providers that several apps need on the server, once.

Try some challenges

Each challenge runs hidden checks against your code. Edit the code, then press Check.

Challenge 1 of 4

Register the billing app

The billing app is written, but nobody can call get_invoice. Add the app to the server, and name the server Help Desk.

Open
import { FrontMcp } from "@frontmcp/sdk";
import { TicketsApp } from "./apps/tickets.app";

@FrontMcp({
  info: { name: "Untitled", version: "1.0.0" },
  apps: [TicketsApp],
})
export default class HelpDeskServer {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.