# Context classes

> What `this` is inside a tool, resource, prompt, agent, job, channel or skill, and every member each context class has.

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

Inside `execute()`, `this` is a context object that FrontMCP creates for the call: a `ToolContext` for a tool, a `ResourceContext` for a resource, and so on. It carries the call's input, the caller, the request, and the helpers you call, like `this.get()`, `this.fetch()` and `this.fail()`. Every context class extends one base class, `ExecutionContextBase`, and so has the same core members. This page lists every member of every context class, as FrontMCP 1.9.4 has them, and describes the ones that don't have a page of their own.

```ts
@Tool(options)     class MyTool     extends ToolContext     { async execute(input) { this.… } }
@Resource(options) class MyResource extends ResourceContext { async execute(uri, params) { this.… } }
@Prompt(options)   class MyPrompt   extends PromptContext   { async execute(args) { this.… } }
@Agent(options)    class MyAgent    extends AgentContext    { async execute(input) { this.… } }
@Job(options)      class MyJob      extends JobContext      { async execute(input) { this.… } }
@Channel(options)  class MyChannel  extends ChannelContext  { async onEvent(payload) { this.… } }
```

---

## Reference

### The context classes

| Class | Extended by | FrontMCP creates one | Base class |
| --- | --- | --- | --- |
| [`ToolContext`](#toolcontext) | [`@Tool`](https://frontmcp.dev/reference/sdk/tool) classes | For every `tools/call` | `ExecutionContextBase` |
| [`ResourceContext`](#resourcecontext) | [`@Resource`](https://frontmcp.dev/reference/sdk/resource) and [`@ResourceTemplate`](https://frontmcp.dev/reference/sdk/resource-template) classes | For every `resources/read` | `ExecutionContextBase` |
| [`PromptContext`](#promptcontext) | [`@Prompt`](https://frontmcp.dev/reference/sdk/prompt) classes | For every `prompts/get` | `ExecutionContextBase` (since 1.9.2) |
| [`AgentContext`](#agentcontext) | [`@Agent`](https://frontmcp.dev/reference/sdk/agent) classes | For every call to the agent's `invoke_<name>` tool | `ExecutionContextBase` |
| [`JobContext`](#jobcontext) | [`@Job`](https://frontmcp.dev/reference/sdk/job) classes | For every run a client starts with `execute_job`, or a workflow step | `ExecutionContextBase` |
| [`ChannelContext`](#channelcontext) | [`@Channel`](https://frontmcp.dev/reference/sdk/channel) classes | For every event, or once for a service channel | `ExecutionContextBase` |
| [`SkillContext`](#skillcontext) | [`@Skill`](https://frontmcp.dev/reference/sdk/skill) classes, optionally | Once, when the skill is first loaded, for a class that overrides `loadInstructions()` or `build()` | `ExecutionContextBase` |

Each one is created for one call and dropped after it (only a service channel and a skill keep one instance), so a value you keep in a field of `this` is gone by the next call: [keep it in a provider](#a-value-kept-on-this-is-gone-on-the-next-call). A workflow runs in a `WorkflowContext` too, but you never extend that one: [`@Workflow`](https://frontmcp.dev/reference/sdk/workflow) has no `execute()` of its own.

### Members every class shares

These come from `ExecutionContextBase`. Where a cell says more than "Yes", the member works differently in that class.

| Member | Tool | Resource | Prompt | Agent | Job | Channel |
| --- | --- | --- | --- | --- | --- | --- |
| [`this.get()`, `this.tryGet()`](https://frontmcp.dev/reference/sdk/get) | Yes | Yes | Yes | Yes | Yes | Yes |
| [`this.fail()`](https://frontmcp.dev/reference/sdk/fail) | Yes | Yes | Yes | Yes | Yes | Yes |
| [`this.auth`](https://frontmcp.dev/reference/sdk/auth) | Yes | Yes | Yes | Yes | The caller who started the run | An empty caller |
| [`this.context`, `this.tryGetContext()`](https://frontmcp.dev/reference/sdk/context) | Yes | Yes | Yes | Yes | [The request that started the run](#where-there-is-no-request-context) | [Throws](#where-there-is-no-request-context) |
| [`this.fetch()`](https://frontmcp.dev/reference/sdk/fetch) | Yes, aborted with `this.signal` | Yes | Yes | Yes | Yes | [Without headers](#where-there-is-no-request-context) |
| [`this.callTool()`](https://frontmcp.dev/reference/sdk/call-tool) | Yes | Yes | Yes | Yes, on the `"agent"` surface | Yes, on the `"job"` surface | As an anonymous caller |
| [`this.scope`](#thisscope) | Yes | Yes | Yes | Yes | Yes | Yes |
| [`this.logger`](#thislogger-and-thiscontextlogger) | Yes | Yes | Yes | Yes | Yes | Yes |
| [`this.contextLogger`](#thislogger-and-thiscontextlogger) | Yes | Yes | Yes | Yes | Yes | Yes |
| [`this.metadata`](#thismetadata) | Yes | Yes | Yes | Yes | Yes | Yes |
| [`this.config`](#thisconfig) | Yes | Yes | Yes | Yes | Yes | Yes |
| [`this.workerEnv`](#thisworkerenv) | Yes | Yes | Yes | Yes | The request's, if it runs in one | The request's, if it runs in one |
| [`this.authInfo`, `this.getAuthInfo()`](#thisauthinfo-and-thisgetauthinfo) | Yes | Yes | Yes | Yes | Yes | Yes |
| [`this.runId`, `this.activeStage`, `this.mark()`](#thisrunid-thisactivestage-and-thismark) | Yes | Yes | Yes | Yes | Yes | Yes |
| [`this.error`](#thiserror) | Yes | Yes | Yes | Yes | Yes | Yes |
| [`this.runtimeContext`, `isEnv()` and the other checks](#thisruntimecontext-and-the-isenv-checks) | Yes | Yes | Yes | Yes | Yes | Yes |

### Members of some classes

| Member | Tool | Resource | Prompt | Agent | Job | Channel |
| --- | --- | --- | --- | --- | --- | --- |
| [`this.respond()`](https://frontmcp.dev/reference/sdk/respond) | Yes | Yes (since 1.9.3) | Yes (since 1.9.3) | Yes | Yes: ends the run | No |
| [`this.notify()`](https://frontmcp.dev/reference/sdk/notify) | Yes | No | No | Yes | No | No |
| [`this.progress()`](https://frontmcp.dev/reference/sdk/progress) | Yes | No | No | Yes | Sends nothing | No |
| [`this.elicit()`](https://frontmcp.dev/reference/sdk/elicit) | Yes | No | No | Yes | No | No |
| [`this.input`](#thisinput-and-thisinputhistory) | Yes | No | No | Yes | Yes | No |
| [`this.inputHistory`](#thisinput-and-thisinputhistory) | Yes | No | No | Yes | No | No |
| [`this.output`](#thisoutput-and-thisoutputhistory) | Yes | Yes | Yes | Yes | Yes | No |
| [`this.outputHistory`](#thisoutput-and-thisoutputhistory) | Yes | Yes | Yes | Yes | No | No |
| [`this.signal`](#thissignal) | Yes | No | No | No | No | No |
| [`this.clientInfo`, `this.platform`](#thisclientinfo-and-thisplatform) | Yes | No | No | Yes | No | No |
| [`this.sample()`, `this.listRoots()`](#thissample-and-thislistroots) | Yes | No | No | No | No | No |
| [`this.notifyResourceUpdated()`, `this.notifyResourceListChanged()`](#telling-clients-a-resource-changed) | Yes | No | No | No | No | No |
| [`this.uri`, `this.params`](#thisuri-and-thisparams) | No | Yes | No | No | No | No |
| [`this.notifyUpdated()`](#telling-clients-a-resource-changed) | No | Yes | No | No | No | No |
| [`this.getArgumentCompleter()`](https://frontmcp.dev/reference/sdk/resource-template) | No | Yes | No | No | No | No |
| [`this.args`](#thisargs) | No | No | Yes | No | No | No |
| [`this.log()`, `this.attempt`, `this.getLogs()`](https://frontmcp.dev/reference/sdk/job#jobcontext) | No | No | No | No | Yes | No |
| [The model loop's methods](https://frontmcp.dev/reference/sdk/agent#agentcontext) | No | No | No | Yes | No | No |
| [`onEvent()`, `onReply()`, `onConnect()`, `onDisconnect()`, `pushIncoming()`](https://frontmcp.dev/reference/sdk/channel#channelcontext) | No | No | No | No | No | Yes |

Each class also has its entry's name and id, as `protected` fields: `toolName` and `toolId`, `resourceName` and `resourceId`, `promptName` and `promptId`, `agentName` and `agentId`, `jobName` and `jobId`, and `channelName`.

Plugins can add members of their own to every context class, like `this.remember`. None are there unless a plugin that adds them is installed: see [`@Plugin`](https://frontmcp.dev/reference/sdk/plugin).

### Members without a page of their own

#### `this.scope`

The scope serving the call: the server, with its registries of tools, resources, prompts, agents, skills and providers. `this.scope.id` is `"root"` for a `@FrontMcp` server. Every context class has it. [Scope and registries](https://frontmcp.dev/reference/sdk/scope) documents what you can do with it.

#### `this.logger` and `this.contextLogger`

`this.logger` writes to the server's own log, never to the client. It has `verbose()`, `debug()`, `info()`, `warn()`, `error()` and `child()`. Lines go to the server's output; in the Playground, to the **Logs** tab.

Levels, transports, prefixes and where the lines end up are in [Logging](https://frontmcp.dev/reference/server/logging).

`this.contextLogger` is the same logger, with the first eight characters of the request id and of the trace id at the start of each line, so the lines of one request can be found together. Both are `protected`: call them inside the class. For messages meant for the client, use [`this.notify()`](https://frontmcp.dev/reference/sdk/notify). See [Logging from a call](#logging-from-a-call).

#### `this.metadata`

The options of the decorator, as FrontMCP parsed them: the `@Tool`'s `name`, `inputSchema` and so on, the `@Resource`'s `uri`, the `@Job`'s `retry`. Read-only.

#### `this.input` and `this.inputHistory`

`this.input` is the validated input, the same object `execute()` receives, with defaults filled in and unknown fields dropped. A hook can replace it before `execute()` runs, by setting `ctx.state.toolContext.input`. `this.inputHistory` records every value it had, as `{ at, stage, value }`: the first is what the client sent, validated. See [Reading what a hook changed](#reading-what-a-hook-changed).

#### `this.output` and `this.outputHistory`

`this.output` is `execute()`'s result once it has returned, and `undefined` before. It's what `Did("execute")` hooks read, as `ctx.state.toolContext.output`, and a hook can replace it. `this.outputHistory` records each value, like `inputHistory`.

#### `this.signal`

An `AbortSignal` that aborts when the tool's [`timeout`](https://frontmcp.dev/reference/sdk/guard) passes, or, for a tool called by [`this.callTool()`](https://frontmcp.dev/reference/sdk/call-tool#giving-the-called-tool-less-time), when the caller's `opts.signal` aborts. `this.fetch()` in a tool is aborted with it. Pass it to anything else that takes one; work that ignores it keeps running after the call has ended. Only `ToolContext` has it. [Limiting and Timing Out Calls](https://frontmcp.dev/learn/limiting-calls) shows it in use.

#### `this.clientInfo` and `this.platform`

`this.clientInfo` is the client's `{ name, version }`, from each MCP 2026-07-28 request's `_meta`, or from `initialize` for session clients and [`connect()`](https://frontmcp.dev/reference/sdk/connect). `this.platform` is the AI platform FrontMCP guesses from the name, like `"claude"` for `claude-ai` or `"cursor"`, `"generic-mcp"` for a name it doesn't know, and `"unknown"` when there's no client info. A client that declares the MCP Apps extension is `"ext-apps"` whatever it's called, unless one of your `platformDetection` mappings matches its name first (since 1.9.1; [how FrontMCP recognizes the client](https://frontmcp.dev/reference/ui/hosts#how-frontmcp-recognizes-the-client)). The client writes both: use them for formatting, never to decide access. [Reading the Request](https://frontmcp.dev/learn/reading-the-request) shows them per entry point. `AgentContext` has them too, and since 1.8.5 they're set in an agent's own `execute()` as in a tool.

#### `this.runId`, `this.activeStage` and `this.mark()`

`this.runId` is a UUID for this call's context object. It isn't the request id (that's [`this.context.requestId`](https://frontmcp.dev/reference/sdk/context)), and in a job it isn't the run's `runId`. `this.activeStage` names the stage the call is in, `"execute"` inside `execute()`; `this.mark(name)` sets it to `name`, for your own debugging. Both are `protected`. For timings, use [`this.context.mark()`](https://frontmcp.dev/reference/sdk/context#timing-parts-of-a-call), which is a different method.

#### `this.error`

The error the call failed with through [`this.fail()`](https://frontmcp.dev/reference/sdk/fail), once it has. `undefined` before. You'll only see it in code that runs after `this.fail()`, like a `catch` around it. `protected`, except in `PromptContext`.

#### `this.config`

Typed access to configuration and environment variables: `get(key, default?)`, `getRequired(key)`, `getNumber()`, `getBoolean()`, `has()`. It needs the built-in `ConfigPlugin` in the server's `plugins`. Without it, reading `this.config` throws [`Provider "ConfigService" is not available`](#provider-configservice-is-not-available).

`ConfigPlugin`, which adds `this.config`, and how its values combine with the environment and `.env` files are in [Configuration files](https://frontmcp.dev/reference/server/config-files#configplugin).

#### `this.authInfo` and `this.getAuthInfo()`

The raw result of authentication, the same data as [`this.context.authInfo`](https://frontmcp.dev/reference/sdk/context#authinfo). `this.authInfo` is deprecated: use [`this.auth`](https://frontmcp.dev/reference/sdk/auth) for the caller, and `this.context.authInfo` for the rest. `this.getAuthInfo()` returns `this.context.authInfo` where there's a request context. A prompt reads its caller from `this.auth` too: see [Reading the caller in a prompt](https://frontmcp.dev/reference/sdk/auth#reading-the-caller-in-a-prompt). (Changed in 1.9.2: before, `PromptContext` had no `this.auth`, and its `this.authInfo` field was the way to the caller.)

#### `this.workerEnv`

The hosting platform's bindings for the request: on a Cloudflare Worker, the `env` object that holds KV namespaces, D1 databases, R2 buckets, `[vars]` and secrets, as the Worker passed it to [`createFetchHandler()`'s handler](https://frontmcp.dev/reference/sdk/create-fetch-handler#reading-a-workers-bindings). A server you build in your process with [`create()`, `createDirect()`](https://frontmcp.dev/reference/sdk/create#passing-a-workers-bindings) or [`connect()`](https://frontmcp.dev/reference/sdk/connect#parameters) passes the `workerEnv` you give it, or the one a call or a client gives. It's `undefined` when nothing passes one, as with stdio or the Node server. A job, and a channel's `onEvent()`, read the `env` of the request they run in: a job a client started with `execute_job`, or an event a tool emitted. (New in 1.8.7. Changed in 1.9: prompts have it, and jobs and channels read it; before, they got `undefined`. Changed in 1.9.4: before, it was `undefined` in every in-process call.)

#### `this.runtimeContext` and the `isEnv()` checks

`this.runtimeContext` describes where the server runs: `{ os, platform, runtime, deployment, provider, target, env }`, like `runtime: "node"`, `deployment: "standalone"` and `env: "development"` for a server run with Node. `this.isEnv(env)`, `this.isRuntime(runtime)`, `this.isDeployment(deployment)` and `this.isPlatform(os)` compare one field. In Node, `env` is `NODE_ENV`, or `"development"` when it isn't set, read each time you ask. In FrontMCP's browser build, the other fields are fixed: `runtime`, `platform` and `os` are `"browser"`. Its `env` is the page's `process.env.NODE_ENV` if the page defines one, else the value the bundler wrote in, else `"development"`; the Playground on this site sets `"development"`. (Changed in 1.9.2: before, the browser build's `env` was always `"production"`.) (Changed in 1.8.7: before, Node read `NODE_ENV` once, so a change after the first call went unseen.) See [Checking where the server runs](#checking-where-the-server-runs).

How FrontMCP detects each value, and `availableWhen`, which uses the same values, are in [Environment awareness](https://frontmcp.dev/reference/server/environment).

#### `this.sample()` and `this.listRoots()`

`this.sample(options)` asks the client's model for a completion (`sampling/createMessage`), and `this.listRoots()` asks which filesystem roots the client exposes (`roots/list`). Both are deprecated in MCP 2026-07-28, and both need a client that declares the capability: otherwise the call fails with error `-32021`, [`This request requires the sampling client capability`](#this-request-requires-the-sampling-client-capability). To use a model from your server, call a provider's API from a tool, or write an [agent](https://frontmcp.dev/reference/sdk/agent). `protected`, and only in `ToolContext`.

#### Telling clients a resource changed

`this.notifyResourceUpdated(uri)` in a tool, and `this.notifyUpdated(uri?)` in a resource, send `notifications/resources/updated` to the sessions that subscribed to `uri` with `resources/subscribe`. `this.notifyResourceListChanged()` sends `notifications/resources/list_changed` to every session. Only clients before MCP 2026-07-28 have sessions to subscribe with; a 2026-07-28 client that listens with `subscriptions/listen` gets a resource's own `this.notifyUpdated()`, but not a tool's `this.notifyResourceUpdated()` ([Protocol versions](https://frontmcp.dev/reference/server/protocol-versions)). The methods never throw.

#### `this.uri` and `this.params`

In a resource, the URI being read and, for a [`@ResourceTemplate`](https://frontmcp.dev/reference/sdk/resource-template), the values of its variables: the same two values `execute(uri, params)` receives.

#### `this.args`

In a prompt, the arguments the client sent, as strings: the same object `execute(args)` receives.

### The classes

#### `ExecutionContextBase`

The base class of every context class but `PromptContext`, and the source of the [shared members](#members-every-class-shares). You don't extend it yourself. `import { ExecutionContextBase } from "@frontmcp/sdk"` lets a helper accept any context, but the helper can't call its `protected` members: see [the TypeScript error](#property-fail-is-protected).

#### `ToolContext`

The fullest context: everything in the tables above except the resource, prompt, job and channel members. `execute(input)` receives the validated input and returns the result. See [`@Tool`](https://frontmcp.dev/reference/sdk/tool).

#### `ResourceContext`

`execute(uri, params)` returns `{ contents }`. It has the shared members, `this.uri`, `this.params`, `this.output` and `this.notifyUpdated()`, and `getArgumentCompleter()` for template variables. No `this.signal`, `this.clientInfo`, `this.notify()` or `this.progress()`. See [`@Resource`](https://frontmcp.dev/reference/sdk/resource).

#### `PromptContext`

`execute(args)` returns the prompt's messages. It has every shared member, `this.args` and `this.output`. No `this.input`, `this.signal`, `this.clientInfo`, `this.notify()`, `this.progress()` or `this.elicit()`. [`this.respond(value)`](https://frontmcp.dev/reference/sdk/respond#ending-early-everywhere-else) ends the request as `return value` would (since 1.9.3: before, it failed the request). See [`@Prompt`](https://frontmcp.dev/reference/sdk/prompt). (Changed in 1.9.2: before, `PromptContext` was a class of its own, without `this.auth`, `this.context`, `this.fetch()`, `this.callTool()`, `this.contextLogger` or `this.config`.)

#### `AgentContext`

Has most of what `ToolContext` has, plus the model loop's methods. In an agent's own `execute()`, `this.context`, `this.clientInfo`, `this.platform` and `this.fetch()`'s tracing headers work as in a tool (since 1.8.5), and so do `this.notify()` and `this.progress()` (since 1.9.2: before, they sent nothing). `this.elicit()` reaches the client (since 1.8.5: see [Asking the user from an agent](https://frontmcp.dev/reference/sdk/agent#asking-the-user-from-an-agent)), and `this.callTool()` calls on the `"agent"` surface. The tools the agent's model calls are ordinary tool calls, with all of it. Since 1.9.4, the model also reads the agent's resources and prompts through built-in tools, and with `execution.enableStreaming` the loop calls `streamCompletion()` and sends the model's text as progress; `streamsReplies()` decides whether a run streams. See [`@Agent`](https://frontmcp.dev/reference/sdk/agent#agentcontext).

#### `JobContext`

`execute(input)` returns the job's result. It adds `this.log()`, `this.attempt` and `this.getLogs()`, and has the shared members. `this.context` is the context of the `execute_job` or `execute_workflow` request that started the run, or a copy of it for a run in the background, so `this.fetch()` carries the request's tracing headers. `this.get()` sees its app's providers, and `this.callTool()` calls on the `"job"` surface. See [`@Job`](https://frontmcp.dev/reference/sdk/job#jobcontext). (Changed in 1.9.2: before, a job had no `this.context`, and `this.get()` saw only the server's providers.)

#### `ChannelContext`

`onEvent(payload)` turns an event into what the channel sends. It has the shared members, but runs outside any request: no `this.context`, and an empty caller in `this.auth`. `this.get()` sees its app's providers (since 1.9.2; before, only the server's). See [`@Channel`](https://frontmcp.dev/reference/sdk/channel#channelcontext).

#### `SkillContext`

A `@Skill` class may extend it. When the class overrides `loadInstructions()` or `build()`, FrontMCP creates one instance, the first time the skill is loaded, and the skill's content comes from those methods; their result is kept for later loads. Without an override, the content comes from the decorator's options. See [`@Skill`](https://frontmcp.dev/reference/sdk/skill). (Changed in 1.9.2: before, FrontMCP never created one, and overrides had no effect.)

#### Caveats

- **A new object for every call.** Fields of `this` start fresh each time. Share state through [providers](https://frontmcp.dev/reference/sdk/provider).
- **`protected` members work only inside the class.** That covers `this.fail()`, `this.elicit()`, `this.logger`, `this.contextLogger`, an agent's `notify()` and `progress()`, and more. A helper function outside the class should return values, or throw a `PublicMcpError`, which [`this.fail()` treats the same](https://frontmcp.dev/reference/sdk/fail#failing-vs-throwing). A tool's `notify()`, `progress()`, `notifyResourceUpdated()` and `notifyResourceListChanged()`, and a job's `log()` and `progress()`, are public since 1.9.1, so a `tool()` or `job()` handler can call them on its `ctx`.
- Where a member exists but does nothing useful in 1.9.4, like `this.progress()` in a job, it's marked in the tables above. The pages it links to say more.

---

## Usage

### Where there is no request context

A channel's `onEvent()` runs without a request context. `this.tryGetContext()` is `undefined` there, `this.context` throws, and `this.fetch()` is plain `fetch()`, without the `traceparent` and `x-request-id` headers it adds in a tool. `this.auth` still works. A job runs in the request that started it, so it has all three (since 1.9.2; before, it was like a channel). Each tab calls the same echo service, which answers with the headers it received:

<Examples title="Jobs and channels">

#### Example: Job
```ts sync.job.ts active
import { Job, JobContext, z } from "@frontmcp/sdk";

@Job({
  name: "sync-tickets",
  description: "Sync tickets with the CRM",
  inputSchema: {},
  outputSchema: { hasContext: z.boolean(), headers: z.record(z.string(), z.string()), caller: z.string(), contextRunId: z.string() },
})
export class SyncTickets extends JobContext {
  async execute() {
    const response = await this.fetch("https://echo.example/headers");
    return { hasContext: this.tryGetContext() !== undefined, headers: await response.json(), caller: this.auth.user.sub, contextRunId: this.runId };
  }
}
```

```ts help-desk.app.ts
import { App } from "@frontmcp/sdk";
import { SyncTickets } from "./sync.job";
import { SyncNow } from "./sync-now.tool";

@App({ id: "help-desk", name: "Help Desk", tools: [SyncNow], jobs: [SyncTickets] })
export class HelpDeskApp {}
```

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

// The same request from a tool, for comparison.
@Tool({ name: "sync_now", description: "Sync tickets with the CRM now", inputSchema: {} })
export class SyncNow extends ToolContext {
  async execute() {
    const response = await this.fetch("https://echo.example/headers");
    return { hasContext: this.tryGetContext() !== undefined, headers: await response.json() };
  }
}
```

```ts echo-service.ts
// Stands in for a service at https://echo.example that answers with the request's
// headers. It replaces the global fetch() for that host only.
const passThrough = globalThis.fetch;
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
  const request = new Request(input, init);
  if (!request.url.startsWith("https://echo.example/")) return passThrough(input, init);
  return Response.json(Object.fromEntries(request.headers));
};
```

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

test("a tool's fetch() carries the request's headers", async ({ mcp }) => {
  const result = (await mcp.tools.call("sync_now", {})).json();
  expect(result.hasContext).toBe(true);
  expect(result.headers).toMatchObject({ traceparent: expect.any(String), "x-request-id": expect.any(String) });
});

test("a job has the request context, and its fetch() carries the same headers", async ({ mcp }) => {
  const { runId, result } = (await mcp.tools.call("execute_job", { name: "sync-tickets", input: {} })).json();
  expect(result).toMatchObject({
    hasContext: true,
    headers: { traceparent: expect.any(String), "x-request-id": expect.any(String) },
    caller: expect.stringMatching(/^anon:/),
  });
  expect(result.contextRunId).not.toBe(runId); // this.runId isn't the run's id
});
```

#### Example: Channel
```ts outages.ts active
import { Channel, ChannelContext, Tool, ToolContext, z, type ChannelEventBus, type ChannelNotification } from "@frontmcp/sdk";

export const lastEvent: Record<string, unknown> = {};

@Channel({ name: "outages", description: "Service outages", source: { type: "app-event", event: "outage" } })
export class OutagesChannel extends ChannelContext {
  async onEvent(payload: unknown): Promise<ChannelNotification> {
    const { service } = payload as { service: string };
    const response = await this.fetch("https://echo.example/headers");
    Object.assign(lastEvent, {
      hasContext: this.tryGetContext() !== undefined,
      headers: await response.json(),
      caller: this.auth.user.sub,
      anonymous: this.auth.isAnonymous,
      calledAs: (await this.callTool("whoami", {})).structuredContent,
    });
    return { content: `${service} is down` };
  }
}

@Tool({ name: "whoami", description: "Say who the server thinks is calling", inputSchema: {} })
export class WhoAmI extends ToolContext {
  async execute() {
    return { sub: this.auth.user.sub };
  }
}

@Tool({ name: "report_outage", description: "Report that a service is down", inputSchema: { service: z.string() } })
export class ReportOutage extends ToolContext {
  async execute({ service }: { service: string }) {
    const { channelEventBus } = this.scope as unknown as { channelEventBus?: ChannelEventBus };
    for (const key of Object.keys(lastEvent)) delete lastEvent[key];
    channelEventBus?.emit("outage", { service });
    for (let i = 0; i < 100 && !("calledAs" in lastEvent); i++) await new Promise((r) => setTimeout(r, 10)); // let onEvent() finish
    return { reported: service, event: lastEvent };
  }
}
```

```ts main.ts
import { App, FrontMcp } from "@frontmcp/sdk";
import { OutagesChannel, ReportOutage, WhoAmI } from "./outages";

@App({ id: "status", name: "Status", tools: [ReportOutage, WhoAmI], channels: [OutagesChannel] })
class Status {}

@FrontMcp({ info: { name: "status", version: "1.0.0" }, apps: [Status], channels: { enabled: true } })
export default class Server {}
```

```ts echo-service.ts
// Stands in for a service at https://echo.example that answers with the request's headers.
const passThrough = globalThis.fetch;
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
  const request = new Request(input, init);
  if (!request.url.startsWith("https://echo.example/")) return passThrough(input, init);
  return Response.json(Object.fromEntries(request.headers));
};
```

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

test("onEvent() runs outside any request, as an empty caller", async ({ mcp }) => {
  const result = await mcp.tools.call("report_outage", { service: "login" });
  expect(result.json().event).toEqual({
    hasContext: false,
    headers: {},
    caller: "",
    anonymous: true,
    calledAs: { sub: expect.stringMatching(/^anon:/) },
  });
});
```

An agent's own `execute()` has the request context, as a tool does (changed in 1.8.5: before, it had none):

```ts triage.agent.ts active
import { Agent, AgentContext, z } from "@frontmcp/sdk";
import { model } from "./model.example";

@Agent({ name: "triage", description: "Suggest a priority for a new ticket", inputSchema: { query: z.string() }, llm: { adapter: model } })
export class Triage extends AgentContext {
  async execute(input: { query: string }) {
    const answer = (await super.execute(input)) as { response: string };
    const response = await this.fetch("https://echo.example/headers");
    return {
      ...answer,
      hasContext: this.tryGetContext() !== undefined,
      headers: await response.json(),
      client: this.clientInfo ?? null,
      platform: this.platform,
      caller: this.auth.user.sub,
    };
  }
}
```

```ts help-desk.app.ts
import { App } from "@frontmcp/sdk";
import { Triage } from "./triage.agent";

@App({ id: "help-desk", name: "Help Desk", agents: [Triage] })
export class HelpDeskApp {}
```

```ts model.example.ts
// Stands in for a real model, which the Playground can't reach.
import type { AgentLlmAdapter } from "@frontmcp/sdk";

export const model: AgentLlmAdapter = {
  async completion() {
    return { content: "High: the customer can't work.", finishReason: "stop" };
  },
};
```

```ts echo-service.ts
// Stands in for a service at https://echo.example that answers with the request's headers.
const passThrough = globalThis.fetch;
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
  const request = new Request(input, init);
  if (!request.url.startsWith("https://echo.example/")) return passThrough(input, init);
  return Response.json(Object.fromEntries(request.headers));
};
```

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

test("an agent's execute() has the request, as a tool does", async ({ mcp }) => {
  const result = await mcp.tools.call("invoke_triage", { query: "Nobody can log in." });
  expect(result.json()).toEqual({
    response: "High: the customer can't work.",
    hasContext: true,
    headers: { traceparent: expect.any(String), "x-request-id": expect.any(String) },
    client: { name: "frontmcp.dev playground", version: "1.0.0" },
    platform: "generic-mcp",
    caller: expect.stringMatching(/^anon:/),
  });
});
```

Pass what a channel needs, like a request id to log, in the event's payload. In an agent, the tools its model calls have the request context too.

### Looking at a tool's context

The members a tool reads most, in one call. `this.runId` is new for each call, and isn't the request id; `this.activeStage` follows `this.mark()`; `this.input` is `execute()`'s argument, with the defaults filled in; `this.error` is set once `this.fail()` has run:

```ts inspect.tool.ts active
import { PublicMcpError, Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({
  name: "inspect_call",
  description: "Describe this call's context. For debugging.",
  inputSchema: { id: z.string(), verbose: z.boolean().default(false) },
})
export class InspectCall extends ToolContext {
  async execute(input: { id: string; verbose: boolean }) {
    const stageBefore = this.activeStage;
    this.mark("described");
    let failedWith = "";
    try {
      this.fail(new PublicMcpError("just looking")); // caught below, for this example only
    } catch {
      failedWith = this.error?.message ?? "";
    }
    return {
      name: this.metadata.name,
      input: this.input,
      inputIsArgument: this.input === input,
      runIdIsRequestId: this.runId === this.context.requestId,
      stages: [stageBefore, this.activeStage],
      output: this.output ?? null,
      signalAborted: this.signal?.aborted ?? null,
      client: this.clientInfo ?? null,
      platform: this.platform,
      failedWith,
    };
  }
}
```

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

test("what a tool's context holds during execute()", async ({ mcp }) => {
  expect((await mcp.tools.call("inspect_call", { id: "T-1" })).json()).toEqual({
    name: "inspect_call",
    input: { id: "T-1", verbose: false },
    inputIsArgument: true,
    runIdIsRequestId: false,
    stages: ["execute", "described"],
    output: null,
    signalAborted: false,
    client: { name: "frontmcp.dev playground", version: "1.0.0" },
    platform: "generic-mcp",
    failedWith: "just looking",
  });
});
```

Catching `this.fail()` like this only makes sense to show `this.error`: in a real tool, the `catch` would [swallow the failure](https://frontmcp.dev/reference/sdk/fail#a-tool-call-succeeds-after-thisfail).

### Logging from a call

`this.logger` writes to the server's log. `this.contextLogger` does too, and starts each line with the request and trace ids, so all the lines of one request can be found together. Open the **Logs** tab after the call:

```ts close-ticket.tool.ts active
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.logger.info(`closing ${id}`); // the server's log, untagged
    this.contextLogger.info(`closing ${id}`); // tagged with this request
    return { id, status: "closed", requestId: this.context.requestId };
  }
}
```

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

test("the call succeeds; the log lines stay on the server", async ({ mcp }) => {
  const result = await mcp.tools.call("close_ticket", { id: "T-1" });
  expect(result.json()).toMatchObject({ id: "T-1", status: "closed" });
  expect(result.text()).not.toContain("closing");
});
```

The tagged line starts with two ids in brackets, like `[[1f0c2a9e:5d1b4c7e]]`: the first eight characters of `requestId`, which the result shows in full, and of the trace id. Neither line reaches the client.

### Reading what a hook changed

A hook can replace a call's input before `execute()` runs. `this.input` is the value `execute()` gets, and `this.inputHistory` keeps each earlier one. Here a plugin cleans up search queries:

```ts normalize.plugin.ts active
import { DynamicPlugin, FlowCtxOf, Plugin, ToolHook } from "@frontmcp/sdk";

@Plugin({ name: "normalize", description: "Trims and lowercases search queries" })
export class NormalizePlugin extends DynamicPlugin<object> {
  @ToolHook.Will("execute")
  async normalize(ctx: FlowCtxOf<"tools:call-tool">) {
    const call = ctx.state.toolContext;
    const input = call?.input as { query?: string } | undefined;
    if (call && typeof input?.query === "string") call.input = { ...input, query: input.query.trim().toLowerCase() };
  }
}
```

```ts search.tool.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "search_tickets", description: "Search tickets by words in their title", inputSchema: { query: z.string() } })
export class SearchTickets extends ToolContext {
  async execute({ query }: { query: string }) {
    return { query, sameAsInput: query === this.input.query, history: this.inputHistory.map((entry) => entry.value) };
  }
}
```

```ts help-desk.app.ts
import { App } from "@frontmcp/sdk";
import { NormalizePlugin } from "./normalize.plugin";
import { SearchTickets } from "./search.tool";

@App({ id: "help-desk", name: "Help Desk", tools: [SearchTickets], plugins: [NormalizePlugin] })
export class HelpDesk {}
```

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

test("execute() gets the cleaned query; the history keeps what was sent", async ({ mcp }) => {
  const result = await mcp.tools.call("search_tickets", { query: "  Cannot LOG IN  " });
  expect(result.json()).toEqual({
    query: "cannot log in",
    sameAsInput: true,
    history: [{ query: "  Cannot LOG IN  " }, { query: "cannot log in" }],
  });
});
```

Each history entry also has `at`, a timestamp, and `stage`, the stage the call was in when the value was set. [Hooking into Calls](https://frontmcp.dev/learn/hooking-into-calls#normalizing-input) covers normalizing input.

### Checking where the server runs

`this.runtimeContext` and its checks tell a call where the server is running. Here a tool adds debugging details only outside production:

```ts get-ticket.tool.ts active
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "get_ticket", description: "Get one support ticket by id", inputSchema: { id: z.string() } })
export class GetTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    const { runtime, env } = this.runtimeContext;
    const ticket = { id, title: "Cannot log in", where: { runtime, env } };
    if (this.isEnv("production")) return ticket;
    return { ...ticket, debug: { loadedFrom: "tickets.db" } };
  }
}
```

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

const inBrowser = typeof WorkerGlobalScope !== "undefined";

test("debugging details outside production", async ({ mcp }) => {
  const result = (await mcp.tools.call("get_ticket", { id: "T-1" })).json();
  expect(result.where).toEqual({ runtime: inBrowser ? "browser" : "node", env: "development" }); // NODE_ENV isn't set in the test run
  expect(result.debug).toEqual({ loadedFrom: "tickets.db" });
});

test("`env` follows NODE_ENV as it changes", async ({ mcp }) => {
  const getTicket = async () => (await mcp.tools.call("get_ticket", { id: "T-1" })).json();
  const before = process.env.NODE_ENV;
  try {
    process.env.NODE_ENV = "production";
    const inProduction = await getTicket();
    expect(inProduction.where.env).toBe("production");
    expect(inProduction.debug).toBeUndefined();
    process.env.NODE_ENV = "staging";
    expect((await getTicket()).where.env).toBe("staging");
  } finally {
    if (before === undefined) delete process.env.NODE_ENV;
    else process.env.NODE_ENV = before;
  }
});
```

`env` is `NODE_ENV`, or `"development"` when it isn't set: start the server with `NODE_ENV=production` and `debug` goes away. `runtime` is `"node"` in Node and `"browser"` when this example runs on this page, in your browser. (Changed in 1.9: the Playground runs FrontMCP's browser build, which says `"browser"`; before, it said `"node"`.) The browser build reads `NODE_ENV` the same way, from the page's `process.env` or else the bundler, so its `env` and the way it formats errors agree. (Changed in 1.9.2: before, the browser build's `env` was always `"production"`, while its errors were formatted for development.)

---

## Troubleshooting

### `Property 'fail' is protected`

TypeScript's full message is `Property 'fail' is protected and only accessible within class 'ExecutionContextBase<…>' and its subclasses`. The code calls `fail()` (or `elicit()`, `logger`) on a context from outside the class, like a helper that receives `this`, or a `tool()` handler's `ctx`:

```ts close.ts
async function closeAll(ctx: ToolContext, ids: string[]) {
  for (const id of ids) {
    await ctx.notify(`Closing ${id}`); // public since 1.9.1
    if (id === "T-0") ctx.fail(new PublicMcpError("There's no ticket T-0.")); // 🚩 TS2445: protected
  }
}
```

To fail from a helper, throw a `PublicMcpError`, or another [public error class](https://frontmcp.dev/reference/sdk/error-classes): it reaches the client the same way. Before 1.9.1, a tool's `notify()` and `progress()` were protected too, so `ctx.notify()` failed with `Property 'notify' is protected`; since 1.9.1 they're public.

### `Provider "ConfigService" is not available`

The code read `this.config`, and the server doesn't have the `ConfigPlugin` that provides it:

```ts support-email.tool.ts active
import { Tool, ToolContext } from "@frontmcp/sdk";

@Tool({ name: "support_email", description: "The address customers can write to", inputSchema: {} })
export class SupportEmail extends ToolContext {
  async execute() {
    return { email: this.config.get("SUPPORT_EMAIL", "help@desk.example") }; // 🚩 no ConfigPlugin
  }
}
```

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

test("🚩 this.config needs the ConfigPlugin", async ({ mcp }) => {
  const result = await mcp.tools.call("support_email", {});
  expect(result).toBeError("TOOL_EXECUTION_ERROR");
  expect(result.text()).toContain('Provider "ConfigService" is not available');
});
```

Add the built-in `ConfigPlugin` to `@FrontMcp({ plugins })`; by default it also loads `.env` files. Or read `process.env` in a [provider](https://frontmcp.dev/reference/sdk/provider) of your own.

### `This request requires the sampling client capability`

The tool called `this.sample()`, or `this.listRoots()` (`roots` then), and the client didn't declare that capability. The request fails with JSON-RPC error `-32021`, and `data.requiredCapabilities` names what's missing:

```ts summarize.tool.ts active
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "summarize_ticket", description: "Summarize a ticket with the client's model", inputSchema: { id: z.string() } })
export class SummarizeTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    const reply = await this.sample({
      messages: [{ role: "user", content: { type: "text", text: `Summarize ticket ${id} in one line.` } }],
      maxTokens: 60,
    });
    return { id, summary: reply };
  }
}
```

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

test("the Playground's client doesn't offer sampling", async ({ mcp }) => {
  const response = await mcp.raw.request({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "summarize_ticket", arguments: { id: "T-1" } } });
  expect(response.error).toMatchObject({ code: -32021, message: "This request requires the `sampling` client capability", data: { requiredCapabilities: { sampling: {} } } });
});
```

Few clients offer sampling, and MCP 2026-07-28 deprecates it. Call a model provider's API from the tool, or use an [agent](https://frontmcp.dev/reference/sdk/agent).

### A value kept on `this` is gone on the next call

FrontMCP creates a new context object for every call, so a field starts over each time:

```ts count.tool.ts active
import { Tool, ToolContext } from "@frontmcp/sdk";

let calls = 0; // module state: shared by every call (and every caller)

@Tool({ name: "count_calls", description: "Count how often this tool was called", inputSchema: {} })
export class CountCalls extends ToolContext {
  private count = 0; // 🚩 a new object each call

  async execute() {
    this.count += 1;
    calls += 1;
    return { field: this.count, module: calls };
  }
}
```

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

test("the field is 1 on every call", async ({ mcp }) => {
  await mcp.tools.call("count_calls", {});
  expect((await mcp.tools.call("count_calls", {})).json()).toEqual({ field: 1, module: 2 });
});
```

Keep state that outlasts a call in a [provider](https://frontmcp.dev/reference/sdk/provider): a `GLOBAL` one for the server, a `CONTEXT` one for one request. Module variables work too, but they're shared by every caller and every app, and hard to replace in tests.

### `RequestContext not available`

`this.context` was read in a channel's `onEvent()`. See [Where there is no request context](#where-there-is-no-request-context), and [`this.context`](https://frontmcp.dev/reference/sdk/context#requestcontext-not-available).
