this.respond
this.respond() ends a tool call on the spot with a result you pass. It's lower level than return: the value you pass is the tools/call result, sent as it is, so it has to be a complete result, with content. FrontMCP doesn't build the text block, fill in structuredContent or check outputSchema for you. Use it when you need a result of an exact shape. In a hook, the flow's ctx.respond() answers a call without running the tool at all, which is how a cache works. In resources, prompts, jobs and agents, respond(value) ends execute() as return value would.
this.respond(result)
Reference
this.respond(result)
Call this.respond() inside a tool's execute(). Nothing after it runs.
import { Tool, ToolContext, z } from "@frontmcp/sdk";
@Tool({ name: "list_tickets", description: "List a customer's open tickets", inputSchema: { customer: z.string() } })
export class ListTickets extends ToolContext {
async execute({ customer }: { customer: string }) {
const tickets = await openTicketsOf(customer);
if (tickets.length === 0) {
this.respond({ content: [{ type: "text", text: `${customer} has no open tickets.` }] });
}
return { customer, tickets };
}
}Parameters
| Parameter | Type | Description |
|---|---|---|
result | CallToolResult | The whole result the client gets: content (required by MCP), and optionally structuredContent, isError and _meta. TypeScript accepts any value here, so nothing stops you from passing a plain object by mistake: see the result has no content. |
Returns
never. It throws to end execute(), like this.fail().
What the client receives
result, exactly as you passed it. Under MCP 2026-07-28 FrontMCP adds resultType: "complete" and its serverInfo in _meta, as it does to every result; nothing else changes. Compare that with returning the same object:
In execute() | The client gets |
|---|---|
return { id: "T-1" } | content: [{ type: "text", text: '{"id":"T-1"}' }] and structuredContent: { id: "T-1" }, checked against outputSchema |
this.respond({ id: "T-1" }) | { id: "T-1" } as the whole result: no content, no structuredContent, not checked |
this.respond({ content: [...], structuredContent: {...} }) | That result, as it is. structuredContent isn't checked against outputSchema either. |
Did("execute") hooks still run after this.respond(), and see result as the tool's output.
Where it works
| In | What respond(value) does | Use instead |
|---|---|---|
A tool's execute() | Ends the call. value is the whole result. | return, for everything except hand-built results |
A hook, as the flow's ctx.respond(value) | Ends the call. The tool doesn't run, and Did("execute") hooks are skipped. value is the whole result; one without content gets content: []. | |
A hook, as ctx.state.toolContext.respond(value) | Ends the call like ctx.respond(value), but sends value exactly as it is, without even an empty content. | ctx.respond() |
A resource's execute() | Ends the read, and value is sent as a returned value would be: { contents } as it is, anything else as a text block, an object as JSON. (Changed in 1.9.3: before, the read failed with Resource output not found.) | Either |
A prompt's execute() | Ends the request, and value is sent as a returned value would be. (Changed in 1.9.3: before, the request failed with Prompt output not found.) | Either |
A job's execute() | Ends the run, and value becomes its result, checked against outputSchema as a returned value is. (Changed in 1.9.2: before, value replaced the whole execute_job response.) | Either |
An agent's execute() | Ends the call, and value is sent as a returned value would be, with content and structuredContent. (Changed in 1.8.5: before, value was the whole result, with no content.) | Either |
Caveats
this.respond()works by throwing, likethis.fail(). Atry/catcharound it catches it, as aFlowControlwithtype: "respond", andexecute()carries on. See the call carries on.outputSchemais only checked for returned values. A result you pass tothis.respond()reaches the client even when itsstructuredContentdoesn't match.- Prefer
return. It buildscontentandstructuredContentfrom one value, and checks it. Reach forthis.respond()only when the result needs a shapereturncan't give it, like several content blocks.
Usage
Sending a result you built yourself
A returned object becomes one JSON text block and structuredContent. To send several text blocks, one per ticket here, build the result yourself:
import { Tool, ToolContext, z } from "@frontmcp/sdk";
const tickets = [
{ id: "T-1", customer: "acme", title: "Cannot log in" },
{ id: "T-3", customer: "acme", title: "Login link expired" },
{ id: "T-2", customer: "globex", title: "Invoice total is wrong" },
];
@Tool({ name: "list_tickets", description: "List a customer's open tickets", inputSchema: { customer: z.string() } })
export class ListTickets extends ToolContext {
async execute({ customer }: { customer: string }) {
const open = tickets.filter((t) => t.customer === customer);
this.respond({
content: open.map((t) => ({ type: "text" as const, text: `${t.id}: ${t.title}` })),
structuredContent: { customer, tickets: open },
});
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
execute() doesn't need a return after this.respond(): nothing after it runs.
Answering a call from a hook
A Will("execute") hook can answer a call itself, with the flow's ctx.respond(), and the tool never runs. Here a plugin answers repeated calls to read-only tools from memory. Hooking into Calls builds the same cache step by step, in "Answering without running the tool". For a real cache, with expiry, Redis and per-caller keys, use the Cache plugin.
import { DynamicPlugin, FlowCtxOf, Plugin, ToolHook } from "@frontmcp/sdk";
type CallCtx = FlowCtxOf<"tools:call-tool">;
const readOnly = (ctx: CallCtx) => ctx.state.tool?.metadata.annotations?.readOnlyHint === true;
@Plugin({ name: "cache", description: "Answers repeated read-only calls from memory" })
export class CachePlugin extends DynamicPlugin<object> {
private results = new Map<string, Record<string, unknown>>();
private key(ctx: CallCtx) {
return `${ctx.state.tool?.metadata.name}:${JSON.stringify(ctx.state.toolContext?.input)}`;
}
@ToolHook.Will("execute", { filter: readOnly })
async answerFromCache(ctx: CallCtx) {
const cached = this.results.get(this.key(ctx));
if (cached) ctx.respond({ content: [{ type: "text", text: JSON.stringify(cached) }], structuredContent: cached });
}
@ToolHook.Did("execute", { filter: readOnly })
async store(ctx: CallCtx) {
this.results.set(this.key(ctx), ctx.state.toolContext?.output as Record<string, unknown>);
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
ctx.respond() ends the whole call: the tool, the Did("execute") hooks and the result formatting are skipped, so pass a finished result, with content. Neither ctx.respond() nor ctx.state.toolContext.respond() builds one from a plain value: see a hook's answer is empty. The hook decorators page lists the stages you can answer from.
Ending early everywhere else
Outside tools, respond(value) ends execute() as return value would: FrontMCP builds and checks the result from value as it does from a returned value. Each tab shows it next to return.
respond() outside tools
Example 1 of 4
Resource
import { ResourceContext, ResourceTemplate } from "@frontmcp/sdk";
@ResourceTemplate({ name: "ticket", uriTemplate: "tickets://{id}", mimeType: "application/json" })
export class Ticket extends ResourceContext<{ id: string }> {
async execute(uri: string, { id }: { id: string }) {
if (id === "T-0") {
this.respond({ contents: [{ uri, mimeType: "application/json", text: "{}" }] }); // ✅ since 1.9.3
}
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ id, title: "Cannot log in" }) }] }; // ✅
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Troubleshooting
The result has no content
this.respond() was given the tool's output, like this.respond({ id, status }), and sent it as the whole result. MCP clients read content, so the model sees nothing, and structuredContent is missing too:
import { Tool, ToolContext, z } from "@frontmcp/sdk";
@Tool({ name: "close_ticket", description: "Close a support ticket", inputSchema: { id: z.string() } })
export class CloseTicket extends ToolContext {
async execute({ id }: { id: string }) {
this.respond({ id, status: "closed" }); // 🚩 the whole result, not the output
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Replace this.respond(value) with return value, and FrontMCP builds content and structuredContent from it. If you do need this.respond(), pass a complete result: { content: [...], structuredContent: value }.
A hook's answer reaches the client empty
The hook passed the tool's output to ctx.respond(), as a cache that stored toolContext.output would. The flow's ctx.respond() only adds an empty content to it, and ctx.state.toolContext.respond() sends it as it is:
import { DynamicPlugin, FlowCtxOf, Plugin, ToolHook } from "@frontmcp/sdk";
type CallCtx = FlowCtxOf<"tools:call-tool">;
@Plugin({ name: "pinned", description: "Answers get_ticket for pinned tickets from memory" })
export class PinnedPlugin extends DynamicPlugin<object> {
private pinned = new Map<string, any>([["T-1", { id: "T-1", title: "Cannot log in", status: "open" }]]);
@ToolHook.Will("execute")
async answer(ctx: CallCtx) {
const input = ctx.state.toolContext?.input as { id: string; mode?: string };
const ticket = this.pinned.get(input.id);
if (!ticket) return;
if (input.mode === "toolContext") ctx.state.toolContext?.respond(ticket); // 🚩 sent as it is
else ctx.respond(ticket); // 🚩 gets an empty content
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
TypeScript refuses a plain object in ctx.respond(), which wants a result with content, but not a value typed any, like this Map<string, any> or anything from JSON.parse(). Build the result from the value: ctx.respond({ content: [{ type: "text", text: JSON.stringify(value) }], structuredContent: value }), as the cache in Answering a call from a hook does.
A text result arrives as {"0":"C","1":"l",…}
this.respond("Closed T-1.") sends the string as the whole result. A result must be an object, so the string is spread into one, a key per character:
import { Tool, ToolContext, z } from "@frontmcp/sdk";
@Tool({ name: "close_ticket", description: "Close a support ticket", inputSchema: { id: z.string() } })
export class CloseTicket extends ToolContext {
async execute({ id }: { id: string }) {
this.respond(`Closed ${id}.`); // 🚩 a string isn't a result
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Respond with a complete result, { content: [{ type: "text", text: "Closed T-1." }] }, or return an object and let FrontMCP build the result.
Prompt execution failed: Prompt output not found
A prompt's execute() finished without returning anything: a code path has no return. Return the messages on every path, or end early with this.respond(), as in the Prompt tab above. (Before 1.9.3, calling this.respond() in a prompt failed this way too.)
Resource "…" read failed: Resource output not found
The same for a resource: execute() finished without a return. Return { contents: [...] }, or end early with this.respond(). (Before 1.9.3, calling this.respond() in a resource failed this way too.)
The call carries on after this.respond()
this.respond() throws to end execute(), and a try/catch around it catches that. The call then continues after the catch, and whatever execute() returns is the result:
import { Tool, ToolContext, z } from "@frontmcp/sdk";
@Tool({ name: "reopen_ticket", description: "Reopen a closed ticket", inputSchema: { id: z.string() } })
export class ReopenTicket extends ToolContext {
async execute({ id }: { id: string }) {
try {
if (id === "T-1") this.respond({ content: [{ type: "text", text: `${id} is already open.` }] });
return { id, reopened: true };
} catch {
// 🚩 this also catches respond()
return { id, reopened: false };
}
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Call this.respond() outside the try, or rethrow what you didn't expect in the catch. this.fail() has the same catch.
The client got a result that doesn't match outputSchema
outputSchema is checked for returned values only. A result passed to this.respond(), or to ctx.respond() in a hook, is sent as it is, and a Did("execute") hook sees it as the tool's output:
import { Tool, ToolContext, z } from "@frontmcp/sdk";
@Tool({
name: "close_ticket",
description: "Close a support ticket",
inputSchema: { id: z.string(), how: z.enum(["respond", "return"]) },
outputSchema: { id: z.string(), status: z.enum(["open", "closed"]) },
})
export class CloseTicket extends ToolContext {
async execute({ id, how }: { id: string; how: "respond" | "return" }) {
const result = { id, status: "done" as "closed" }; // 🚩 not a status the schema allows
if (how === "respond") {
this.respond({ content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result });
}
return result;
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Return the value to have it checked. A hook that reads toolContext.output gets the whole result after this.respond(), and the output value after a return: check for content if it has to handle both.