# Rules of FrontMCP

> The short list of rules that keep FrontMCP servers correct, safe and easy for models to use.

Source: https://frontmcp.dev/reference/rules

These rules come from how FrontMCP and the MCP protocol work. Following them keeps your tools predictable for models and safe in production.

## Describe tools for the model

A model decides whether and how to call a tool from its name, description and schema alone. Give every tool a specific `verb_noun` name and a description that says what it does, what it returns and when to use it.

```ts
// 🚩 The model can't tell what this looks up
@Tool({ name: "lookup", inputSchema: { q: z.string() } })

// ✅ Says what, returns what, and in what format
@Tool({
  name: "get_ticket",
  description: "Get one support ticket by its id. Returns the title and status.",
  inputSchema: { id: z.string().describe("Ticket id, like T-1") },
})
```

Read more in [Your First Tool](https://frontmcp.dev/learn/your-first-tool).

## Put constraints in the schema

The model can read your schema but not your code, and FrontMCP enforces the schema before `execute()` runs. A limit written as an `if` statement is invisible until the model hits it.

```ts
// 🚩 The model only finds out after calling
async execute({ query }) {
  if (query.length < 3) throw new Error("query too short");
}

// ✅ The model sees minLength: 3 in tools/list
inputSchema: { query: z.string().min(3) }
```

## Let the schema type the handler

The type of `execute()`'s parameter must match `inputSchema`. FrontMCP checks this at compile time. Write the schema first, then type the parameter to match.

## Fail with errors the model can read

Use `this.fail(new PublicMcpError("..."))` for failures the model should know about. A plain `Error` is hidden in production and becomes "Internal FrontMCP error" with an error ID.

```ts
// 🚩 In production the model sees "Internal FrontMCP error"
throw new Error("Ticket not found");

// ✅ The model sees your message and can recover
this.fail(new PublicMcpError("There's no ticket T-9. Search by title with search_tickets."));
```

## Declare side effects

Set `readOnlyHint`, `destructiveHint`, `idempotentHint` and `openWorldHint` so clients can decide how carefully to confirm a call. Annotations are hints, not enforcement: a read-only hint on a tool that writes is a lie clients may act on.

## Keep a tool idempotent until it asks for input (MCP 2026-07-28)

Under MCP 2026-07-28 there's no session to pause. When a tool calls `this.elicit()` to ask the user something, the client re-sends the whole request with the answer, and **`execute()` runs again from the top**. Anything before `this.elicit()` runs once per round trip.

```ts
// 🚩 The email is sent twice: once before asking, once on the replay
async execute({ id }) {
  await this.get(Mailer).send(`Closing ${id}`);
  const answer = await this.elicit("Close this ticket?", z.object({ confirmed: z.boolean() }));
  // ...
}

// ✅ Ask first, then act
async execute({ id }) {
  const answer = await this.elicit("Close this ticket?", z.object({ confirmed: z.boolean() }));
  if (answer.status === "accept" && answer.content.confirmed) {
    await this.get(Mailer).send(`Closing ${id}`);
  }
}
```

## Load `reflect-metadata` first

FrontMCP's decorators store their configuration with `reflect-metadata`. Import it once, as the first line of your entry file, and set `experimentalDecorators` and `emitDecoratorMetadata` in `tsconfig.json`. `npx frontmcp init` does the tsconfig part for you.
