Delegating to Agents
Every tool in this course so far does one thing, and the client's model decides what to call next. Some tasks are a string of those decisions. Triaging a ticket means reading it, looking up the customer, choosing a priority and setting it. The client's model can do that one call at a time, if it knows how and you trust it with every tool. Or your server can do it: an agent is a tool that runs a model of its own, with instructions and tools you choose, so the client's model hands over the whole task in one call. This chapter covers agents in FrontMCP 1.8, and skills, the other way to teach a model a procedure, and points out where 1.8 behaves differently from its docs.
In this chapter
Your first agent
An agent is a class, like a tool. @Agent gives it a description for the client's model, instructions for its own model, an input schema, and llm, the model it runs. It has no execute(): FrontMCP sends the input to the model and returns the model's answer. A client sees one tool, invoke_triage:
import { Agent, AgentContext, z } from "@frontmcp/sdk";
import { model } from "./model.example";
@Agent({
name: "triage",
description: "Suggest a priority for a new support ticket, with a one-line reason. Pass the ticket's title and body.",
systemInstructions:
"You triage tickets for a help desk. Give the ticket a priority, High, Normal or Low, " +
"and say why in one sentence. High is for anything that stops the customer from working.",
inputSchema: {
title: z.string().describe("The ticket's title"),
body: z.string().describe("What the customer wrote"),
},
llm: { adapter: model },
})
export class Triage extends AgentContext {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
The Playground can't reach a real model, so every example in this chapter has a model.example.ts that plays one: an object with a completion() method, which is all FrontMCP asks of a model. It follows a script, so the examples are the same every time you run them.
Ready to learn this topic?
Learn how to write and register an agent, how a client sees it, what its model is sent and how FrontMCP builds that from the input, and what comes back, checked by outputSchema.
Giving an agent tools
With tools, an agent's model can look things up and change things, as a support agent would. FrontMCP runs a loop: it asks the model what to do, runs the tools the model asks for, gives it the results, and asks again, until the model answers. The tools are the agent's alone. The client doesn't see them:
import { Agent, AgentContext, PublicMcpError, Tool, ToolContext, z } from "@frontmcp/sdk";
import { model } from "./model.example";
export const tickets = new Map([
["T-1", { id: "T-1", title: "Cannot log in", priority: "none" }],
["T-3", { id: "T-3", title: "Export is slow", priority: "none" }],
]);
@Tool({ name: "get_ticket", description: "Get a support ticket by its id.", inputSchema: { id: z.string().describe("Ticket id, like T-1") } })
class GetTicket extends ToolContext {
async execute({ id }: { id: string }) {
const ticket = tickets.get(id);
if (!ticket) throw new PublicMcpError(`There's no ticket ${id}.`);
return ticket;
}
}
@Tool({
name: "set_priority",
description: "Set a ticket's priority.",
inputSchema: { id: z.string().describe("Ticket id, like T-1"), priority: z.enum(["high", "normal", "low"]) },
})
class SetPriority extends ToolContext {
async execute({ id, priority }: { id: string; priority: "high" | "normal" | "low" }) {
const ticket = tickets.get(id);
if (!ticket) throw new PublicMcpError(`There's no ticket ${id}.`);
ticket.priority = priority;
return { id, priority };
}
}
@Agent({
name: "triage",
description: "Triage a support ticket: read it and set its priority. Pass the ticket id.",
systemInstructions:
"You triage support tickets. Read the ticket with get_ticket, then set its priority with set_priority: " +
"high if the customer can't work, normal otherwise. Finish with one sentence that says what you set.",
inputSchema: { ticketId: z.string().describe("Ticket id, like T-1") },
tools: [GetTicket, SetPriority],
llm: { adapter: model },
})
export class Triage extends AgentContext {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
One call to invoke_triage, three requests to the agent's model. This loop is what every agent runs:
- The client's model calls
invoke_triage, and waits. - FrontMCP sends the agent's model its instructions, the input and the agent's tools.
- The model asks for a tool. FrontMCP runs it and adds the call and its result to the conversation.
- FrontMCP sends the whole conversation back, and the model decides again, as often as it needs, up to a limit you set.
- When the model answers instead of asking for a tool, the answer is
invoke_triage's result.
The client's model sees steps 1 and 5. Everything between, including every tool call and every failed one, happens on your server.
Ready to learn this topic?
Learn how the loop runs step by step, which tools the client and the agent can each see, what the model hears when a tool fails, how to limit the loop, how to add your own steps before and after it, and what a client hears while it runs.
Read Giving an Agent ToolsConnecting a real model
Outside the Playground, llm names a provider, a model and where the API key is:
@Agent({
name: "triage",
// …
llm: {
provider: "anthropic", // or "openai", with the `openai` package
model: "claude-opus-5",
apiKey: { env: "ANTHROPIC_API_KEY" },
},
})
export class Triage extends AgentContext {}The Playground can't run this: it has no network and no API key. A real model also changes what an agent costs. Every turn of the loop is a request to the provider, paid for by your server's key, carrying the whole conversation so far, and the client waits for all of them.
Ready to learn this topic?
Learn which packages to install, where the key comes from and what happens when it's missing, exactly what FrontMCP sends Anthropic and OpenAI, how failed requests are retried, and how to see and keep down what each call costs.
Read Connecting a Real ModelAgents that call agents
Billing questions need billing's tools and billing's rules. FrontMCP's docs describe nesting agents and "swarm" settings for this, but in 1.8 neither connects one agent to another. What works is one agent calling another the way it would call any tool, from its own execute():
import { Agent, AgentContext, z } from "@frontmcp/sdk";
import { triageModel } from "./model.example";
type Answer = { response?: string; handoff?: "billing"; question?: string };
@Agent({
name: "triage",
description: "Answer a customer's message, or hand billing questions to the billing agent.",
systemInstructions:
"Answer the customer's message. If it's about an invoice or a charge, answer only with " +
'{"handoff": "billing", "question": "<the customer\'s words>"}.',
inputSchema: { message: z.string().describe("What the customer wrote") },
llm: { adapter: triageModel },
})
export class Triage extends AgentContext {
async execute(input: { message: string }) {
const answer = (await super.execute(input)) as Answer; // triage's own loop
if (answer.handoff === "billing") {
const billing = await this.callTool("invoke_billing", { question: answer.question });
return billing.structuredContent as Answer;
}
return answer;
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Ready to learn this topic?
Learn what two agents on one server see of each other, what nested agents and swarm settings do in 1.8, how to hand a task off from execute(), what to do when the other agent fails, and how to stop handoffs that never end.
Teaching the model skills
An agent does a task with your model. A skill hands the procedure to the client's model instead: the steps, the tools they use and the rules, which the client's model reads and then follows with your tools. A skill adds no tools; FrontMCP serves it as a document, SKILL.md:
import { Skill, SkillContext } from "@frontmcp/sdk";
@Skill({
name: "refund-duplicate-charge",
description: "Refund a customer who was charged more than once for the same invoice.",
instructions: `1. Read the invoice with get_invoice. Refund only if it has more than one charge.
2. Refund every charge after the first with refund_charge. Never refund the first charge.`,
tools: ["get_invoice", "refund_charge"],
})
export class RefundDuplicateCharge extends SkillContext {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Ready to learn this topic?
Learn how to write a skill with instructions, tools, parameters and examples, how a client finds and reads skills, how to search and load them with FrontMCP's own requests, where instructions can come from, and when to write a skill, an agent or a prompt.
Read Teaching the Model SkillsTool, agent or skill?
| If the work… | Write |
|---|---|
| Is one action the model calls and uses the answer of right away | A tool |
| Is a procedure the client's model can follow with your tools, once it knows the steps | A skill |
| Needs a model of its own, uses tools the client shouldn't have, or should run the same way for every client | An agent |
The difference that's easiest to miss is who pays. A skill runs on the client's model. An agent runs on yours, for every client that calls it, so write one when the task is worth that, and keep it behind authentication and rate limits.
What's next?
Start the chapter with Your First Agent. To see an agent with a full set of help desk tools in one server, read the Triage Agent example, and look up every option in the @Agent and @Skill references. After this chapter, Tools with a UI shows how a tool can answer with a small page the host displays, not just data.