Securing a Server

Intermediate

Every server so far in this course has let anyone call any tool, as often as they liked, for as long as the tool took. That's fine on your laptop. Once a server is reachable over a network, you need answers to three questions: who is calling, what may they do, and how much of it. This chapter answers each one with FrontMCP.

In this chapter

Authenticating clients

A client proves who it is with a credential in the Authorization header of every request, and the server's auth mode decides which credentials count. With no auth at all, a server is public: every request gets in, as an anonymous caller.

Open
import { Tool, ToolContext } from "@frontmcp/sdk";

@Tool({
  name: "whoami",
  description: "Say who the server thinks is calling, and with which scopes.",
  inputSchema: {},
  annotations: { readOnlyHint: true },
})
export class WhoAmI extends ToolContext {
  async execute() {
    return { caller: this.auth.user.sub, scopes: this.auth.scopes };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Call it again and the id changes: under MCP 2026-07-28, each anonymous request is a new caller. One line of configuration requires a shared key instead, and other modes accept tokens from your identity provider or sign users in themselves:

auth: { mode: "static", tokens: [process.env.DESK_API_KEY!] },

Ready to learn this topic?

Learn where credentials travel, how the public, static, transparent, local and remote modes differ, what a client gets back when it isn't let in, and how to choose.

Read Authenticating Clients

Deciding who can call what

Knowing who is calling isn't the same as deciding what they may do. A tool can read the caller from this.auth and refuse, with a message the model can pass on to the user. Here only callers with the tickets:write scope may close tickets, and an anonymous caller doesn't have it:

Open
import { PublicMcpError, Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({
  name: "close_ticket",
  description: "Close a support ticket. Only signed-in support agents can close tickets.",
  inputSchema: { id: z.string().describe("Ticket id, like T-1") },
  annotations: { destructiveHint: true },
})
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    if (!this.auth.hasScope("tickets:write")) {
      this.fail(new PublicMcpError("Only signed-in support agents can close tickets. Tell the user to sign in as an agent.", "FORBIDDEN"));
    }
    return { id, closed: true };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

A tool can also declare who may use it with authorities. FrontMCP then refuses other callers before execute() runs, and leaves the tool out of their tools/list.

Ready to learn this topic?

Learn what this.auth holds in each auth mode, how to refuse a call well, how to declare roles and permissions with authorities, and why visibility: "hidden" doesn't protect a tool.

Read Deciding Who Can Call What

Limiting and timing out calls

A model can call a tool in a loop. rateLimit, concurrency and timeout on @Tool cap how often it runs, how many calls run at once, and how long one may take. This export runs at most twice an hour: it ran once when the example started, so press Call twice more.

Open
import { App, FrontMcp, Tool, ToolContext } from "@frontmcp/sdk";

@Tool({
  name: "export_tickets",
  description: "Export every support ticket as CSV. At most 2 exports an hour.",
  inputSchema: {},
  annotations: { readOnlyHint: true },
  rateLimit: { maxRequests: 2, windowMs: 60 * 60_000 },
})
class ExportTickets extends ToolContext {
  async execute() {
    return { csv: "id,title,status\nT-1,Cannot log in,open\nT-2,Invoice total is wrong,closed" };
  }
}

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

@FrontMcp({
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDeskApp],
})
export default class Server {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The third call is refused with RATE_LIMIT_EXCEEDED and says when to retry.

Ready to learn this topic?

Learn what callers get back when a limit trips, whose calls a limit counts, how to stop a tool's runs overlapping, what happens when a call runs out of time, and how to choose limits for expensive and destructive tools.

Read Limiting and Timing Out Calls

What's next?

Start the chapter with Authenticating Clients. After it, Doing Work in the Background covers work that's too slow, too flaky or too long for a single tool call.