# this.fetch

> Call other HTTP services from a tool or resource, with the request's tracing headers and a default timeout.

Source: https://frontmcp.dev/reference/sdk/fetch

`this.fetch()` is the web platform's `fetch()`, with two additions for [calling other services](https://frontmcp.dev/learn/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()`.

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

---

## Reference

### `this.fetch(input, init?)`

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

```ts 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.](#usage)

#### Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| `input` | `string \| URL \| Request` | What to fetch. Use an absolute URL. A `Request` works too, with its own headers, unless `init` has [`headers` of its own](#the-service-says-401-though-the-request-has-an-authorization-header). |
| `init` | `RequestInit` | Optional. The same options as `fetch()`: `method`, `headers`, `body`, `signal` and the rest. |

Three options behave differently from plain `fetch()`:

| Option | In `this.fetch()` |
| --- | --- |
| `headers` | Sent as you give them. FrontMCP [adds its own](#what-it-adds) only where you haven't set a header of the same name. |
| `signal` | Replaces FrontMCP's 30-second timeout. Without one, FrontMCP adds its own signal that aborts after 30 seconds, [`requestTimeout`](#server-options) if you set one. |
| `credentials` | A 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](#the-service-gets-no-credentials-from-credentialsprovider). |

#### 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

| Header | Value | Sent |
| --- | --- | --- |
| `traceparent` | The request's [W3C trace context](https://www.w3.org/TR/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](https://frontmcp.dev/reference/server/observability#spans) 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-id` | `this.context.requestId`, new for every request. | Unless `autoInjectTracingHeaders` is `false`. |
| `Authorization` | `Bearer`, then the token the caller authenticated with, `this.context.authInfo.token`. | Only to origins in [`forwardCallerTokenTo`](#server-options), 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:

```ts 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 {}
```

| Option | Default | What 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. |
| `requestTimeout` | `30000` | Milliseconds before a request without a `signal` is aborted. |
| `autoInjectTracingHeaders` | `true` | Whether 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:

| Error | When |
| --- | --- |
| `TypeError`: "fetch failed" in Node, "Failed to fetch" in browsers | The 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 `AbortError` | A 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`](#server-options), 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()`](https://frontmcp.dev/reference/sdk/create#keeping-the-servers-options) 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](#forwarding-the-callers-token-to-your-own-service). (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.

```ts get-invoice.tool.ts active
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();
  }
}
```

```ts billing-api.ts
// Stands in for the billing service at https://billing.example, so this example runs
// without a network. It replaces the global fetch() for that host only, and this.fetch()
// ends in the global fetch(). In a project, you don't need it.
const invoices: Record<string, { id: string; customer: string; total: number; status: string }> = {
  "INV-7": { id: "INV-7", customer: "Acme", total: 120, status: "paid" },
};
const passThrough = globalThis.fetch;

globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
  const url = input instanceof Request ? input.url : String(input);
  if (!url.startsWith("https://billing.example/")) return passThrough(input, init);
  const request = new Request(input, init);
  const { pathname } = new URL(request.url);

  if (request.method === "GET" && pathname.startsWith("/invoices/")) {
    const invoice = invoices[pathname.slice("/invoices/".length)];
    return invoice ? Response.json(invoice) : Response.json({ error: "no such invoice" }, { status: 404 });
  }
  if (request.method === "POST" && pathname === "/refunds") {
    if (request.headers.get("authorization") !== "Bearer sk_test_desk") {
      return Response.json({ error: "bad API key" }, { status: 401 });
    }
    const { invoice, amount } = await request.json();
    return Response.json({ id: "RF-1", invoice, amount }, { status: 201 });
  }
  return Response.json({ error: "not found" }, { status: 404 });
};
```

```ts get-invoice.test.ts
import { test, expect } from "@frontmcp/testing";

test("returns the invoice", async ({ mcp }) => {
  const result = await mcp.tools.call("get_invoice", { id: "INV-7" });
  expect(result.json()).toEqual({ id: "INV-7", customer: "Acme", total: 120, status: "paid" });
});

test("a 404 becomes an error the model can read", async ({ mcp }) => {
  const result = await mcp.tools.call("get_invoice", { id: "INV-404" });
  expect(result).toBeError();
  expect(result.text()).toBe("There's no invoice INV-404. Invoice ids look like INV-7.");
});
```

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.

```ts compare-fetch.tool.ts active
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,
    };
  }
}
```

```ts echo-service.ts
// Stands in for a service at https://echo.example that answers with the headers it
// received, and whether the request came with an AbortSignal.
const passThrough = globalThis.fetch;

globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
  const url = input instanceof Request ? input.url : String(input);
  if (!url.startsWith("https://echo.example/")) return passThrough(input, init);
  const headers: Record<string, string> = {};
  new Request(input, init).headers.forEach((value, name) => (headers[name] = value));
  return Response.json({ headers, signal: init?.signal instanceof AbortSignal });
};
```

```ts compare-fetch.test.ts
import { test, expect } from "@frontmcp/testing";

test("this.fetch() sends the request's trace and id, with a signal", async ({ mcp }) => {
  const { requestId, traceparent, withContext } = (await mcp.tools.call("compare_fetch", {})).json();
  expect(withContext).toEqual({ headers: { traceparent, "x-request-id": requestId }, signal: true });
  expect(traceparent).toMatch(/^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/);
});

test("the global fetch() sends neither", async ({ mcp }) => {
  const { plain } = (await mcp.tools.call("compare_fetch", {})).json();
  expect(plain).toEqual({ headers: {}, signal: false });
});
```

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

```ts refund-invoice.tool.ts active
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();
  }
}
```

```ts billing-config.ts
import { Provider } from "@frontmcp/sdk";

@Provider({ name: "BillingConfig" })
export class BillingConfig {
  baseUrl = "https://billing.example";
  apiKey = "sk_test_desk"; // in a project, read it from the environment
}
```

```ts billing-api.ts
// Stands in for the billing service at https://billing.example, so this example runs
// without a network. It replaces the global fetch() for that host only, and this.fetch()
// ends in the global fetch(). In a project, you don't need it.
const invoices: Record<string, { id: string; customer: string; total: number; status: string }> = {
  "INV-7": { id: "INV-7", customer: "Acme", total: 120, status: "paid" },
};
const passThrough = globalThis.fetch;

globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
  const url = input instanceof Request ? input.url : String(input);
  if (!url.startsWith("https://billing.example/")) return passThrough(input, init);
  const request = new Request(input, init);
  const { pathname } = new URL(request.url);

  if (request.method === "GET" && pathname.startsWith("/invoices/")) {
    const invoice = invoices[pathname.slice("/invoices/".length)];
    return invoice ? Response.json(invoice) : Response.json({ error: "no such invoice" }, { status: 404 });
  }
  if (request.method === "POST" && pathname === "/refunds") {
    if (request.headers.get("authorization") !== "Bearer sk_test_desk") {
      return Response.json({ error: "bad API key" }, { status: 401 });
    }
    const { invoice, amount } = await request.json();
    return Response.json({ id: "RF-1", invoice, amount }, { status: 201 });
  }
  return Response.json({ error: "not found" }, { status: 404 });
};
```

```ts refund-invoice.test.ts
import { test, expect } from "@frontmcp/testing";

test("the refund is created with the server's API key", async ({ mcp }) => {
  const result = await mcp.tools.call("refund_invoice", { invoice: "INV-7", amount: 20 });
  expect(result.json()).toEqual({ id: "RF-1", invoice: "INV-7", amount: 20 });
});
```

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.

```ts main.ts active
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 {}
```

```ts check-upstreams.tool.ts
import { Tool, ToolContext } from "@frontmcp/sdk";

@Tool({ name: "check_upstreams", description: "Show which services receive the caller's token", inputSchema: {} })
export class CheckUpstreams extends ToolContext {
  async execute() {
    const ownApi = await (await this.fetch("https://tickets-api.example/me")).json();
    const thirdParty = await (await this.fetch("https://echo.example/")).json();
    return { ownApi: ownApi.headers.authorization ?? null, thirdParty: thirdParty.headers.authorization ?? null };
  }
}
```

```ts echo-service.ts
// Stands in for two services, https://tickets-api.example and https://echo.example,
// that answer with the headers they received.
const passThrough = globalThis.fetch;

globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
  const url = input instanceof Request ? input.url : String(input);
  if (!/^https:\/\/(tickets-api|echo)\.example\//.test(url)) return passThrough(input, init);
  const headers: Record<string, string> = {};
  new Request(input, init).headers.forEach((value, name) => (headers[name] = value));
  return Response.json({ headers });
};
```

```ts forward.test.ts
import { test, expect } from "@frontmcp/testing";
import { create, FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./main";
import { CheckUpstreams } from "./check-upstreams.tool";

test("only the listed origin gets the caller's token", async () => {
  // The Playground has no sign-in, so this runs the server in-process as a signed-in user.
  const server = await FrontMcpInstance.createDirect(config);
  const result = await server.callTool("check_upstreams", {}, { authContext: { user: { sub: "nour" }, token: "mcp-token-for-nour" } });
  await server.dispose();
  expect(result.structuredContent).toEqual({ ownApi: "Bearer mcp-token-for-nour", thirdParty: null });
});

test("an anonymous caller sends no token, even to the listed origin", async ({ mcp }) => {
  const result = await mcp.tools.call("check_upstreams", {});
  expect(result.json()).toEqual({ ownApi: null, thirdParty: null });
});

test("create() takes the fetch option too", async () => {
  const server = await create({ info: config.info, tools: [CheckUpstreams], fetch: config.fetch });
  const result = await server.callTool("check_upstreams", {}, { authContext: { user: { sub: "nour" }, token: "mcp-token-for-nour" } });
  await server.dispose();
  expect(result.structuredContent).toEqual({ ownApi: "Bearer mcp-token-for-nour", thirdParty: null });
});
```

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](#sending-json-with-your-own-credentials).

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

```ts weekly-report.tool.ts active
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();
  }
}
```

```ts reports-service.ts
// Stands in for a slow reporting service at https://reports.example: it takes 3 seconds
// to answer, and stops when the request's signal aborts, as a real fetch() does.
const passThrough = globalThis.fetch;

globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
  const url = input instanceof Request ? input.url : String(input);
  if (!url.startsWith("https://reports.example/")) return passThrough(input, init);
  const signal = init?.signal ?? (input instanceof Request ? input.signal : undefined);
  await new Promise((resolve, reject) => {
    const timer = setTimeout(resolve, 3000);
    signal?.addEventListener("abort", () => {
      clearTimeout(timer);
      reject(signal.reason);
    });
  });
  return Response.json({ week: 38, opened: 41, closed: 37 });
};
```

```ts weekly-report.test.ts
import { test, expect } from "@frontmcp/testing";

test("gives up after half a second", async ({ mcp }) => {
  const started = Date.now();
  const result = await mcp.tools.call("weekly_report", {});
  expect(result).toBeError();
  expect(result.text()).toBe("The reports service didn't answer within half a second. Try again later.");
  expect(Date.now() - started).toBeLessThan(2000);
});
```

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

> **Pitfall: Don't call this.fail() inside the try**
`this.fail()` ends the call by throwing, so a `catch` around it catches it too. Fail in the `catch` block, or after the `try`, as above. See [`this.fail`](https://frontmcp.dev/reference/sdk/fail#a-tool-call-succeeds-after-thisfail).

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

```ts 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") },
})
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();
  }
}
```

```ts get-invoice.test.ts
import { test, expect } from "@frontmcp/testing";

test("an unreachable service becomes a readable error", async ({ mcp }) => {
  const result = await mcp.tools.call("get_invoice", { id: "INV-7" });
  expect(result).toBeError();
  expect(result.raw._meta?.code).toBe("PUBLIC_ERROR");
  expect(result.text()).toBe("The billing service can't be reached right now. Try again in a few minutes.");
});
```

### 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`](https://frontmcp.dev/reference/sdk/fail#in-resources-and-prompts)).

<Examples title="Other entries">

#### Example: Resource
```ts invoice.resource.ts active
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() }] };
  }
}
```

```ts billing-api.ts
// Stands in for the billing service at https://billing.example, so this example runs
// without a network. It replaces the global fetch() for that host only, and this.fetch()
// ends in the global fetch(). In a project, you don't need it.
const invoices: Record<string, { id: string; customer: string; total: number; status: string }> = {
  "INV-7": { id: "INV-7", customer: "Acme", total: 120, status: "paid" },
};
const passThrough = globalThis.fetch;

globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
  const url = input instanceof Request ? input.url : String(input);
  if (!url.startsWith("https://billing.example/")) return passThrough(input, init);
  const request = new Request(input, init);
  const { pathname } = new URL(request.url);

  if (request.method === "GET" && pathname.startsWith("/invoices/")) {
    const invoice = invoices[pathname.slice("/invoices/".length)];
    return invoice ? Response.json(invoice) : Response.json({ error: "no such invoice" }, { status: 404 });
  }
  if (request.method === "POST" && pathname === "/refunds") {
    if (request.headers.get("authorization") !== "Bearer sk_test_desk") {
      return Response.json({ error: "bad API key" }, { status: 401 });
    }
    const { invoice, amount } = await request.json();
    return Response.json({ id: "RF-1", invoice, amount }, { status: 201 });
  }
  return Response.json({ error: "not found" }, { status: 404 });
};
```

#### Example: Prompt
```ts explain-invoice.prompt.ts active
import { Prompt, PromptContext, PublicMcpError, type GetPromptResult } from "@frontmcp/sdk";

@Prompt({
  name: "explain_invoice",
  description: "Explain a customer's invoice in plain words",
  arguments: [{ name: "id", description: "Invoice id, like INV-7", required: true }],
})
export class ExplainInvoice extends PromptContext {
  async execute({ id }: Record<string, string>): Promise<GetPromptResult> {
    const response = await this.fetch(`https://billing.example/invoices/${id}`);
    if (!response.ok) throw new PublicMcpError(`There's no invoice ${id}.`);
    const invoice = await response.json();
    const text = `Explain this invoice to the customer in two sentences:\n${JSON.stringify(invoice)}`;
    return { messages: [{ role: "user", content: { type: "text", text } }] };
  }
}
```

```ts billing-api.ts
// Stands in for the billing service at https://billing.example, so this example runs
// without a network. It replaces the global fetch() for that host only, and this.fetch()
// ends in the global fetch(). In a project, you don't need it.
const invoices: Record<string, { id: string; customer: string; total: number; status: string }> = {
  "INV-7": { id: "INV-7", customer: "Acme", total: 120, status: "paid" },
};
const passThrough = globalThis.fetch;

globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
  const url = input instanceof Request ? input.url : String(input);
  if (!url.startsWith("https://billing.example/")) return passThrough(input, init);
  const request = new Request(input, init);
  const { pathname } = new URL(request.url);

  if (request.method === "GET" && pathname.startsWith("/invoices/")) {
    const invoice = invoices[pathname.slice("/invoices/".length)];
    return invoice ? Response.json(invoice) : Response.json({ error: "no such invoice" }, { status: 404 });
  }
  if (request.method === "POST" && pathname === "/refunds") {
    if (request.headers.get("authorization") !== "Bearer sk_test_desk") {
      return Response.json({ error: "bad API key" }, { status: 401 });
    }
    const { invoice, amount } = await request.json();
    return Response.json({ id: "RF-1", invoice, amount }, { status: 201 });
  }
  return Response.json({ error: "not found" }, { status: 404 });
};
```

---

## 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](#when-the-service-cant-be-reached) and fail with a message of your own.

```ts get-invoice.tool.ts
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();
  }
}
```

### `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](#setting-a-timeout).

### `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](#sending-json-with-your-own-credentials). (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:

```ts refund-invoice.tool.ts active
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();
  }
}
```

```ts billing-api.ts
// Stands in for the billing service at https://billing.example, so this example runs
// without a network. It replaces the global fetch() for that host only, and this.fetch()
// ends in the global fetch(). In a project, you don't need it.
const invoices: Record<string, { id: string; customer: string; total: number; status: string }> = {
  "INV-7": { id: "INV-7", customer: "Acme", total: 120, status: "paid" },
};
const passThrough = globalThis.fetch;

globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
  const url = input instanceof Request ? input.url : String(input);
  if (!url.startsWith("https://billing.example/")) return passThrough(input, init);
  const request = new Request(input, init);
  const { pathname } = new URL(request.url);

  if (request.method === "GET" && pathname.startsWith("/invoices/")) {
    const invoice = invoices[pathname.slice("/invoices/".length)];
    return invoice ? Response.json(invoice) : Response.json({ error: "no such invoice" }, { status: 404 });
  }
  if (request.method === "POST" && pathname === "/refunds") {
    if (request.headers.get("authorization") !== "Bearer sk_test_desk") {
      return Response.json({ error: "bad API key" }, { status: 401 });
    }
    const { invoice, amount } = await request.json();
    return Response.json({ id: "RF-1", invoice, amount }, { status: 201 });
  }
  return Response.json({ error: "not found" }, { status: 404 });
};
```

Put every header in one place:

```ts
// 🚩 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):

```text
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:

```ts ping-billing.tool.ts active
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();
  }
}
```

```ts echo-service.ts
// Stands in for a service at https://echo.example that answers with the headers it
// received, and how the request asked for redirects to be handled.
const passThrough = globalThis.fetch;

globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
  const url = input instanceof Request ? input.url : String(input);
  if (!url.startsWith("https://echo.example/")) return passThrough(input, init);
  const headers: Record<string, string> = {};
  new Request(input, init).headers.forEach((value, name) => (headers[name] = value));
  return Response.json({ headers, redirect: init?.redirect ?? "follow" });
};
```

```ts ping-billing.test.ts
import { test, expect } from "@frontmcp/testing";

test("no credential is added", async ({ mcp }) => {
  const { headers } = (await mcp.tools.call("ping_billing", {})).json();
  expect(Object.keys(headers).sort()).toEqual(["traceparent", "x-request-id"]);
});

test("redirects aren't followed", async ({ mcp }) => {
  expect((await mcp.tools.call("ping_billing", {})).json().redirect).toBe("manual");
});
```

Set the header yourself from a provider, as in [Sending JSON with your own credentials](#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`](#server-options). Send the service's own credentials, as in [Sending JSON with your own credentials](#sending-json-with-your-own-credentials), or, for a service of your own that accepts the same tokens, [list it](#forwarding-the-callers-token-to-your-own-service).
