this.fetch

this.fetch() is the web platform's fetch(), with two additions for calling other services from inside a request: it sends the request's trace and id along, so the call can be followed across services, and it gives up after 30 seconds unless you choose another timeout. It doesn't send the caller's token or headers anywhere unless you list the service. Everything else, from the options to the Response you get back, is plain fetch().

const response = await this.fetch(input, init?)

Reference

this.fetch(input, init?)

Call this.fetch() inside the execute() of a tool or a resource.

get-invoice.tool.ts
import { PublicMcpError, Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({
  name: "get_invoice",
  description: "Get a customer's invoice from the billing service",
  inputSchema: { id: z.string().describe("Invoice id, like INV-7") },
  annotations: { readOnlyHint: true, openWorldHint: true },
})
export class GetInvoice extends ToolContext {
  async execute({ id }: { id: string }) {
    const response = await this.fetch(`https://billing.example/invoices/${id}`);
    if (!response.ok) this.fail(new PublicMcpError(`There's no invoice ${id}.`));
    return await response.json();
  }
}

See more examples below.

Parameters

ParameterTypeDescription
inputstring | URL | RequestWhat to fetch. Use an absolute URL. A Request works too, with its own headers, unless init has headers of its own.
initRequestInitOptional. The same options as fetch(): method, headers, body, signal and the rest.

Three options behave differently from plain fetch():

OptionIn this.fetch()
headersSent as you give them. FrontMCP adds its own only where you haven't set a header of the same name.
signalReplaces FrontMCP's 30-second timeout. Without one, FrontMCP adds its own signal that aborts after 30 seconds, requestTimeout if you set one.
credentialsA string ("include", "omit", "same-origin") is passed on. The types also accept { provider: "…" }, which adds the headers the server's auth-provider registry gives for that provider. FrontMCP 1.9.3 doesn't register such a registry itself, so out of the box the option is dropped, nothing is added, and a warning is logged.

Returns

A promise of a standard Response. Like fetch(), it resolves for every HTTP status, 404 and 500 included, so check response.ok before you use the body.

What it adds

HeaderValueSent
traceparentThe request's W3C trace context, this.context.traceContext.raw.Unless autoInjectTracingHeaders is false. A traceparent header the client sent is passed on as it arrived; otherwise each request starts a new trace. With tracing on, the same trace with this request's own GET span as the parent, so the service you call records its work under it. (Changed in 1.9.3: before, tracing passed on the request's own.)
x-request-idthis.context.requestId, new for every request.Unless autoInjectTracingHeaders is false.
AuthorizationBearer, then the token the caller authenticated with, this.context.authInfo.token.Only to origins in forwardCallerTokenTo, which is empty by default.
x-frontmcp-*Every header of the client's request whose name starts with x-frontmcp-.Only to origins in forwardCustomHeadersTo, which is empty by default.

A header you set yourself is never replaced. While a request carries the caller's token or headers, or asks for credentials: { provider }, this.fetch() doesn't follow redirects (redirect: "manual"), so a redirect can't take them to an origin you didn't list; the 3xx response comes back to you instead. An anonymous caller has no token, so a listed origin gets no Authorization header from it.

Server options

@FrontMcp({ fetch }) sets how this.fetch() behaves for the whole server:

main.ts
@FrontMcp({
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDeskApp],
  fetch: { forwardCallerTokenTo: ["https://tickets-api.example.com"], requestTimeout: 10_000 },
})
export default class Server {}
OptionDefaultWhat it does
forwardCallerTokenTo[]Origins that get Authorization: Bearer and the caller's token. Write each as a URL; only its origin counts (scheme, host and port), so https://tickets-api.example.com/v2 covers every path on that host, and https://tickets-api.example.com:8443 is another origin.
forwardCustomHeadersTo[]Origins that get the request's x-frontmcp-* headers.
requestTimeout30000Milliseconds before a request without a signal is aborted.
autoInjectTracingHeaderstrueWhether to send traceparent and x-request-id.

An entry that isn't a URL, like "tickets-api.example.com", stops the server from starting with Invalid URL.

Errors

this.fetch() rejects when fetch() would:

ErrorWhen
TypeError: "fetch failed" in Node, "Failed to fetch" in browsersThe service couldn't be reached: the name didn't resolve, the connection was refused, TLS failed, or, in a browser, CORS blocked the response.
TypeError: "Failed to parse URL from …"In Node, the URL is relative or malformed.
DOMException named TimeoutError: "The operation was aborted due to timeout"FrontMCP's 30 seconds ran out. Your own AbortSignal.timeout() gives the same error name, with the runtime's own message.
DOMException named AbortErrorA signal you passed was aborted.

If you don't catch the error, the request fails. A tool call gets an isError result with _meta.code TOOL_EXECUTION_ERROR, reading Tool "get_invoice" execution failed: fetch failed in development and "Internal FrontMCP error" in production. A resource read fails with error -32603: Resource "invoices://INV-7" read failed: fetch failed, and "Internal FrontMCP error" in production.

Caveats

  • this.fetch() is available in ToolContext, ResourceContext and PromptContext. Prompts have it since 1.9.2; before, a prompt called this.get(FRONTMCP_CONTEXT).fetch(), which still does the same thing.
  • It's still fetch(): no retries, no base URL, no JSON parsing, no caching.
  • Change the 30 seconds for the whole server with requestTimeout, or for one request by passing a signal. A signal that never aborts, like a plain AbortController's, means no timeout at all.
  • A Request keeps its headers and gets the default timeout. Pass init.headers with it, though, and those replace every header the Request had, as with fetch().
  • The caller's token stays on your server unless you list a service in forwardCallerTokenTo. FrontMCP 1.8.0 sent it to every host; on 1.8.0, set Authorization yourself on every call. An anonymous caller has no token, and a listed origin gets no Authorization header for it (before 1.9.2 it got Authorization: Bearer with nothing after it).
  • create() keeps the fetch option, as createDirect() does, so a call with an authContext token sends it to the origins in forwardCallerTokenTo: see forwarding the caller's token. (Changed in 1.9.3: before, create() dropped fetch.)
  • The request is made by the server's runtime. In Node, CORS doesn't apply. In the Playground, which runs in your browser, a real service must allow CORS.

Usage

Calling an HTTP API

Check response.ok and fail with a message the model can act on, then return the body.

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

@Tool({
  name: "get_invoice",
  description: "Get a customer's invoice from the billing service",
  inputSchema: { id: z.string().describe("Invoice id, like INV-7") },
  annotations: { readOnlyHint: true, openWorldHint: true },
})
export class GetInvoice extends ToolContext {
  async execute({ id }: { id: string }) {
    const response = await this.fetch(`https://billing.example/invoices/${id}`);
    if (response.status === 404) this.fail(new PublicMcpError(`There's no invoice ${id}. Invoice ids look like INV-7.`));
    if (!response.ok) this.fail(new PublicMcpError(`The billing service answered HTTP ${response.status}. Try again later.`));
    return await response.json();
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The billing-api.ts tab stands in for the billing service, so the example runs without a network. It replaces the global fetch() for https://billing.example only, and this.fetch() ends in the global fetch(), so the tool's request lands there.

Seeing what this.fetch() adds

This tool calls the same service twice, once with the global fetch() and once with this.fetch(), and the service answers with the headers it got. Only this.fetch() sends the request's trace and id, and a signal for the timeout. There's no Authorization: the server doesn't list any service in its fetch options.

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

@Tool({ name: "compare_fetch", description: "Show what this.fetch() adds to a request", inputSchema: {} })
export class CompareFetch extends ToolContext {
  async execute() {
    const plain = await (await fetch("https://echo.example/")).json();
    const withContext = await (await this.fetch("https://echo.example/")).json();
    return {
      requestId: this.context.requestId,
      traceparent: this.context.traceContext.raw,
      plain,
      withContext,
    };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Sending JSON with your own credentials

Pass method, headers and body as you would to fetch(). A service's API key is your server's secret, not the caller's, so keep it in a provider and set Authorization yourself. FrontMCP keeps your header as it is.

Open
import { PublicMcpError, Tool, ToolContext, z } from "@frontmcp/sdk";
import { BillingConfig } from "./billing-config";

@Tool({
  name: "refund_invoice",
  description: "Refund part or all of a customer's invoice",
  inputSchema: { invoice: z.string().describe("Invoice id, like INV-7"), amount: z.number().positive() },
  annotations: { destructiveHint: true, openWorldHint: true },
})
export class RefundInvoice extends ToolContext {
  async execute({ invoice, amount }: { invoice: string; amount: number }) {
    const billing = this.get(BillingConfig);
    const response = await this.fetch(`${billing.baseUrl}/refunds`, {
      method: "POST",
      headers: { "content-type": "application/json", authorization: `Bearer ${billing.apiKey}` },
      body: JSON.stringify({ invoice, amount }),
    });
    if (!response.ok) this.fail(new PublicMcpError(`The billing service refused the refund (HTTP ${response.status}).`));
    return await response.json();
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Change apiKey in billing-config.ts and call the tool again: the service answers 401, and the tool fails with a message.

Forwarding the caller's token to your own service

The caller's token was issued for your MCP server. Send it on only to a service of your own that accepts the same tokens, and list that service's origin in forwardCallerTokenTo. Every other host still gets nothing.

Open
import { App, FrontMcp } from "@frontmcp/sdk";
import { CheckUpstreams } from "./check-upstreams.tool";

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

export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDeskApp],
  // Your own tickets API trusts the same identity provider as this server.
  fetch: { forwardCallerTokenTo: ["https://tickets-api.example"] },
};

@FrontMcp(config)
export default class Server {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Only list a service that is meant to accept these tokens, such as one whose audience your identity provider includes. A third-party API never is: give it its own credentials, as above.

Setting a timeout

Without a signal, this.fetch() waits up to 30 seconds, which is a long time for a model to wait. Pass AbortSignal.timeout() to give up sooner, and turn the TimeoutError into a message the model can act on. The reports service here takes 3 seconds to answer.

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

@Tool({ name: "weekly_report", description: "How many tickets were opened and closed this week", inputSchema: {} })
export class WeeklyReport extends ToolContext {
  async execute() {
    let response: Response;
    try {
      response = await this.fetch("https://reports.example/weekly", { signal: AbortSignal.timeout(500) });
    } catch (e) {
      if (e instanceof DOMException && e.name === "TimeoutError") {
        this.fail(new PublicMcpError("The reports service didn't answer within half a second. Try again later."));
      }
      throw e;
    }
    return await response.json();
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Change the timeout to 5000 and the report comes back after 3 seconds.

When the service can't be reached

A service that's down or unreachable makes this.fetch() reject with a TypeError. Uncaught, the model gets "Internal FrontMCP error" in production, which tells it nothing, so catch it and say what happened. billing.invalid never resolves: the .invalid domain is reserved for names that don't exist.

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

@Tool({
  name: "get_invoice",
  description: "Get a customer's invoice from the billing service",
  inputSchema: { id: z.string().describe("Invoice id, like INV-7") },
})
export class GetInvoice extends ToolContext {
  async execute({ id }: { id: string }) {
    let response: Response;
    try {
      response = await this.fetch(`https://billing.invalid/invoices/${id}`);
    } catch (e) {
      if (e instanceof TypeError) {
        this.fail(new PublicMcpError("The billing service can't be reached right now. Try again in a few minutes."));
      }
      throw e;
    }
    if (!response.ok) this.fail(new PublicMcpError(`There's no invoice ${id}.`));
    return await response.json();
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Fetching from a resource or a prompt

Resources and prompts have this.fetch() too, with the same headers and timeout. A PublicMcpError reaches the client with its message from both, whether you throw it or pass it to this.fail() (see this.fail).

Other entries

Example 1 of 2

Resource

Open
import { PublicMcpError, ResourceContext, ResourceTemplate } from "@frontmcp/sdk";

@ResourceTemplate({ name: "invoice", uriTemplate: "invoices://{id}", mimeType: "application/json" })
export class Invoice extends ResourceContext<{ id: string }> {
  async execute(uri: string, { id }: { id: string }) {
    const response = await this.fetch(`https://billing.example/invoices/${id}`);
    if (!response.ok) throw new PublicMcpError(`There's no invoice ${id}.`);
    return { contents: [{ uri, mimeType: "application/json", text: await response.text() }] };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.


Troubleshooting

Tool "get_invoice" execution failed: fetch failed

this.fetch() rejected and nothing caught it. "fetch failed" is Node's message for a service it couldn't reach; browsers, like the Playground, say "Failed to fetch". In production the model gets "Internal FrontMCP error" instead. Check the URL and that the service is up, then catch the error and fail with a message of your own.

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

@Tool({ name: "get_invoice", description: "Get a customer's invoice", inputSchema: { id: z.string() } })
export class GetInvoice extends ToolContext {
  async execute({ id }: { id: string }) {
    const response = await this.fetch(`https://billing.invalid/invoices/${id}`); // 🚩 nothing catches a failure
    return await response.json();
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The operation was aborted due to timeout

The service didn't answer within FrontMCP's default of 30 seconds, so the call was aborted: a tool call fails with Tool "…" execution failed: The operation was aborted due to timeout. Pass a signal with the time you want, and turn the timeout into a message.

Failed to parse URL from /invoices/INV-7

The URL is relative. In Node there's no page to resolve it against, so fetch() rejects. Build absolute URLs from a base URL in your configuration, as refund_invoice does with a provider. (In a browser, a relative URL resolves against the page, so the same code fetches from the wrong server instead of failing.)

The service says 401, though the Request has an Authorization header

this.fetch() was given a Request object and init.headers next to it. As with fetch(), headers in init replace every header the Request had, the API key included:

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

@Tool({
  name: "refund_invoice",
  description: "Refund part or all of a customer's invoice",
  inputSchema: { invoice: z.string(), amount: z.number().positive() },
})
export class RefundInvoice extends ToolContext {
  async execute({ invoice, amount }: { invoice: string; amount: number }) {
    const request = new Request("https://billing.example/refunds", {
      method: "POST",
      headers: { "content-type": "application/json", authorization: "Bearer sk_test_desk" },
      body: JSON.stringify({ invoice, amount }),
    });
    const response = await this.fetch(request, { headers: { "idempotency-key": `refund-${invoice}-${amount}` } }); // 🚩 replaces the headers above
    if (!response.ok) this.fail(new PublicMcpError(`The billing service refused the refund (HTTP ${response.status}).`));
    return await response.json();
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Put every header in one place:

// 🚩 The Request's headers are replaced
await this.fetch(new Request(url, { method: "POST", headers, body }), { headers: { "idempotency-key": key } });

// ✅ Your headers are sent, with FrontMCP's added
await this.fetch(url, { method: "POST", headers: { ...headers, "idempotency-key": key }, body });

The service gets no credentials from credentials.provider

The types accept credentials: { provider: "billing" }, and this.fetch() asks the server's auth-provider registry for that provider's headers. FrontMCP 1.9.2 doesn't register one itself, and @frontmcp/sdk doesn't export the token to register one under, so the option is removed, nothing is added in its place, and the server logs a warning (look in the Logs tab):

fetch(): credentials.provider was given, but no auth providers are configured; sent without them

The request still doesn't follow redirects, as with any credential:

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

@Tool({ name: "ping_billing", description: "Check what the billing service receives", inputSchema: {} })
export class PingBilling extends ToolContext {
  async execute() {
    const response = await this.fetch("https://echo.example/", { credentials: { provider: "billing" } });
    return await response.json();
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Set the header yourself from a provider, as in Sending JSON with your own credentials.

The service gets no Authorization header, though the caller signed in

this.fetch() doesn't send the caller's token to a service unless its origin is in forwardCallerTokenTo. Send the service's own credentials, as in Sending JSON with your own credentials, or, for a service of your own that accepts the same tokens, list it.