# Quick Start: build an MCP server in TypeScript

> Build an MCP server in TypeScript with FrontMCP. The 80% you'll use every day, with every example running in your browser.

Source: https://frontmcp.dev/learn

Welcome to FrontMCP. This page covers the concepts you'll use most days when building an MCP server. Every example runs the real FrontMCP SDK in your browser: change the code and the server restarts with your edits.

**You will learn**
- How to define a tool, and what an AI model sees when it looks at one
- How schemas check input before your code runs
- How to return structured results
- How to expose data with resources and reusable prompts
- How apps and a server fit together
- How to share a service between tools with a provider

## Creating a tool

An MCP server gives AI clients, like Claude or Cursor, a set of **capabilities**. The most common kind is a **tool**: a function the model can decide to call when it needs to act or look something up.

In FrontMCP, a tool is a class decorated with `@Tool` that extends `ToolContext` and implements `execute()`:

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

@Tool({
  name: "add",
  description: "Adds two numbers together",
  inputSchema: { a: z.number(), b: z.number() },
})
export class AddTool extends ToolContext {
  async execute(input: { a: number; b: number }) {
    return input.a + input.b;
  }
}
```

The panel on the right is an MCP client talking to your server:

- **Capabilities** shows what `tools/list` returns. That card is everything the model knows about your tool, so the name and description are the model's only documentation.
- **Call** sends `tools/call`. The page already sent `{ "a": 2, "b": 3 }`, and the result came back as `{ "value": 5 }`.
- **Wire** shows each JSON-RPC request and response exactly as a client would send and receive them.

Try changing `description` and watch the Capabilities card change.

## Checking input with schemas

`inputSchema` is written with [Zod](https://zod.dev), re-exported from `@frontmcp/sdk` as `z`. FrontMCP turns it into the JSON Schema the model reads, and checks every call against it **before** `execute()` runs.

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

@Tool({
  name: "greet",
  description: "Greets someone by name",
  inputSchema: {
    name: z.string().min(1).describe("Who to greet"),
    style: z.enum(["casual", "formal"]).default("casual"),
  },
})
export class GreetTool extends ToolContext {
  async execute({ name, style }: { name: string; style: "casual" | "formal" }) {
    return style === "formal" ? `Good day, ${name}.` : `Hi ${name}!`;
  }
}
```

Clear the **name** field and call it again. FrontMCP rejects the call with `Invalid tool input` and a list of what's wrong, and your `execute()` never runs. The model gets that message back and can correct itself.

`.describe()` text shows up next to the field in the Capabilities tab. Models use it the same way you'd use a parameter comment.

## Returning structured results

Whatever `execute()` returns is sent back to the client. Plain values, like the number and string above, arrive wrapped as `{ "value": ... }`. When the result has a shape, declare it with `outputSchema`, so clients know what fields to expect before they call:

```ts search-tickets.tool.ts
import { 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: "closed" },
  { id: "T-3", title: "Login link expired", status: "open" },
] as const;

@Tool({
  name: "search_tickets",
  description: "Search support tickets by words in the title",
  inputSchema: { query: z.string().min(1) },
  outputSchema: z.object({
    tickets: z.array(z.object({ id: z.string(), title: z.string(), status: z.enum(["open", "closed"]) })),
  }),
  annotations: { readOnlyHint: true },
})
export class SearchTicketsTool extends ToolContext {
  async execute({ query }: { query: string }) {
    const q = query.toLowerCase();
    return { tickets: tickets.filter((t) => t.title.toLowerCase().includes(q)) };
  }
}
```

The result arrives as `structuredContent`, and FrontMCP also includes it as text for clients that only read text. The `readOnlyHint` annotation tells clients this tool doesn't change anything. Clients can use hints like this to decide how carefully to confirm a call with the user.

## Exposing data with resources and prompts

Tools are for actions the **model** decides to take. MCP has two other kinds of capability:

- A **resource** is data the client application can read and attach to a conversation, addressed by a URI. `@ResourceTemplate` covers a whole family of URIs, like `ticket://{id}`.
- A **prompt** is a reusable message template the **user** picks, often from a slash menu.

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

@Resource({ name: "sla", uri: "policy://sla", mimeType: "text/plain", description: "How fast we reply" })
export class SlaPolicy extends ResourceContext {
  async execute(uri: string) {
    return { contents: [{ uri, mimeType: "text/plain", text: "Reply to every ticket within 4 business hours." }] };
  }
}

@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, params: { id: string }) {
    const ticket = { id: params.id, title: "Cannot log in", status: "open" };
    return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(ticket) }] };
  }
}

@Prompt({
  name: "triage",
  description: "Triage a ticket",
  arguments: [{ name: "id", description: "Ticket id", required: true }],
})
export class TriagePrompt extends PromptContext {
  async execute(args: Record<string, string>) {
    return {
      messages: [{ role: "user" as const, content: { type: "text" as const, text: `Read ticket://${args.id}, then suggest a priority and a next step.` } }],
    };
  }
}
```

Use the **Request** menu in the Call tab to read `ticket://{id}` with any id, or to get the `triage` prompt.

## Grouping capabilities into an app and a server

So far the Playground has wrapped your exports in a server for you. In a real project you write that wiring yourself, in two layers:

- An **app** (`@App`) groups related tools, resources, prompts and providers. Apps can have their own auth and settings.
- The **server** (`@FrontMcp`) is the entry point. It lists your apps and holds server-wide settings such as `info` and `http`.

```ts main.ts active
import { FrontMcp } from "@frontmcp/sdk";
import { CalcApp } from "./calc.app";

@FrontMcp({
  info: { name: "Hello MCP", version: "0.1.0" },
  apps: [CalcApp],
})
export default class Server {}
```

```ts calc.app.ts
import { App } from "@frontmcp/sdk";
import { AddTool } from "./add.tool";

@App({ id: "calc", name: "Calculator", tools: [AddTool] })
export class CalcApp {}
```

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

@Tool({
  name: "add",
  description: "Adds two numbers together",
  inputSchema: { a: z.number(), b: z.number() },
})
export class AddTool extends ToolContext {
  async execute(input: { a: number; b: number }) {
    return input.a + input.b;
  }
}
```

`npx frontmcp create` generates this same structure, with the tool in a `tools/` folder. Open the Wire tab: the `_meta.serverInfo` in each response now reads `Hello MCP`, from `info` in `main.ts`.

## Sharing a service with a provider

Tools often need the same thing: a database client, an API wrapper, a cache. Put it in a **provider** (`@Provider`), register it on the app, and get it inside any tool with `this.get()`:

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

@Tool({ name: "count_open_tickets", description: "How many tickets are still open", inputSchema: {} })
export class CountOpenTickets extends ToolContext {
  async execute() {
    return this.get(TicketStore).open().length;
  }
}

@Tool({
  name: "close_ticket",
  description: "Close a ticket by id",
  inputSchema: { id: z.string() },
  annotations: { destructiveHint: true, idempotentHint: true },
})
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    return this.get(TicketStore).close(id);
  }
}
```

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

type Ticket = { id: string; title: string; open: boolean };

@Provider({ name: "TicketStore", scope: ProviderScope.GLOBAL })
export class TicketStore {
  private tickets: Ticket[] = [
    { id: "T-1", title: "Cannot log in", open: true },
    { id: "T-2", title: "Invoice total is wrong", open: false },
    { id: "T-3", title: "Login link expired", open: true },
  ];
  open() {
    return this.tickets.filter((t) => t.open);
  }
  close(id: string) {
    const ticket = this.tickets.find((t) => t.id === id);
    if (ticket) ticket.open = false;
    return { id, open: false };
  }
}
```

```ts help-desk.app.ts
import { App } from "@frontmcp/sdk";
import { CloseTicket, CountOpenTickets } from "./tools";
import { TicketStore } from "./ticket-store";

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

Call `close_ticket` with `T-1`, then call `count_open_tickets` again: the count drops from 2 to 1. Both tools share one `TicketStore`, because `ProviderScope.GLOBAL` creates one instance for the whole server. `ProviderScope.CONTEXT` would create one per request instead.

## Next steps

You now know the pieces you'll use most: tools, schemas, structured results, resources, prompts, apps, servers and providers.

- To run a server on your own machine and connect a real client, read [Installation](https://frontmcp.dev/learn/installation).
- To learn tools properly, start the [Describing Capabilities](https://frontmcp.dev/learn/describing-capabilities) chapter with [Your First Tool](https://frontmcp.dev/learn/your-first-tool).
- To look up every `@Tool` option, see the [`@Tool` reference](https://frontmcp.dev/reference/sdk/tool).
- Coming from the official MCP SDK or another framework? See [how FrontMCP compares](https://frontmcp.dev/compare).
