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.

list-tickets.tool.ts
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 };
  }
}

See more examples below.

Parameters

ParameterTypeDescription
resultCallToolResultThe 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

InWhat respond(value) doesUse 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, like this.fail(). A try/catch around it catches it, as a FlowControl with type: "respond", and execute() carries on. See the call carries on.
  • outputSchema is only checked for returned values. A result you pass to this.respond() reaches the client even when its structuredContent doesn't match.
  • Prefer return. It builds content and structuredContent from one value, and checks it. Reach for this.respond() only when the result needs a shape return can'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:

Open
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.

Open
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

Open
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:

Open
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:

Open
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:

Open
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:

Open
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:

Open
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.