Context classes

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.

@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

ClassExtended byFrontMCP creates oneBase class
ToolContext@Tool classesFor every tools/callExecutionContextBase
ResourceContext@Resource and @ResourceTemplate classesFor every resources/readExecutionContextBase
PromptContext@Prompt classesFor every prompts/getExecutionContextBase (since 1.9.2)
AgentContext@Agent classesFor every call to the agent's invoke_<name> toolExecutionContextBase
JobContext@Job classesFor every run a client starts with execute_job, or a workflow stepExecutionContextBase
ChannelContext@Channel classesFor every event, or once for a service channelExecutionContextBase
SkillContext@Skill classes, optionallyOnce, 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 workflow runs in a WorkflowContext too, but you never extend that one: @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.

MemberToolResourcePromptAgentJobChannel
this.get(), this.tryGet()YesYesYesYesYesYes
this.fail()YesYesYesYesYesYes
this.authYesYesYesYesThe caller who started the runAn empty caller
this.context, this.tryGetContext()YesYesYesYesThe request that started the runThrows
this.fetch()Yes, aborted with this.signalYesYesYesYesWithout headers
this.callTool()YesYesYesYes, on the "agent" surfaceYes, on the "job" surfaceAs an anonymous caller
this.scopeYesYesYesYesYesYes
this.loggerYesYesYesYesYesYes
this.contextLoggerYesYesYesYesYesYes
this.metadataYesYesYesYesYesYes
this.configYesYesYesYesYesYes
this.workerEnvYesYesYesYesThe request's, if it runs in oneThe request's, if it runs in one
this.authInfo, this.getAuthInfo()YesYesYesYesYesYes
this.runId, this.activeStage, this.mark()YesYesYesYesYesYes
this.errorYesYesYesYesYesYes
this.runtimeContext, isEnv() and the other checksYesYesYesYesYesYes

Members of some classes

MemberToolResourcePromptAgentJobChannel
this.respond()YesYes (since 1.9.3)Yes (since 1.9.3)YesYes: ends the runNo
this.notify()YesNoNoYesNoNo
this.progress()YesNoNoYesSends nothingNo
this.elicit()YesNoNoYesNoNo
this.inputYesNoNoYesYesNo
this.inputHistoryYesNoNoYesNoNo
this.outputYesYesYesYesYesNo
this.outputHistoryYesYesYesYesNoNo
this.signalYesNoNoNoNoNo
this.clientInfo, this.platformYesNoNoYesNoNo
this.sample(), this.listRoots()YesNoNoNoNoNo
this.notifyResourceUpdated(), this.notifyResourceListChanged()YesNoNoNoNoNo
this.uri, this.paramsNoYesNoNoNoNo
this.notifyUpdated()NoYesNoNoNoNo
this.getArgumentCompleter()NoYesNoNoNoNo
this.argsNoNoYesNoNoNo
this.log(), this.attempt, this.getLogs()NoNoNoNoYesNo
The model loop's methodsNoNoNoYesNoNo
onEvent(), onReply(), onConnect(), onDisconnect(), pushIncoming()NoNoNoNoNoYes

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.

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

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(). See 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.

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 passes, or, for a tool called by this.callTool(), 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 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(). 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). The client writes both: use them for formatting, never to decide access. 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), 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(), which is a different method.

this.error

The error the call failed with through this.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.

ConfigPlugin, which adds this.config, and how its values combine with the environment and .env files are in Configuration files.

this.authInfo and this.getAuthInfo()

The raw result of authentication, the same data as this.context.authInfo. this.authInfo is deprecated: use this.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. (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. A server you build in your process with create(), createDirect() or connect() 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.

How FrontMCP detects each value, and availableWhen, which uses the same values, are in Environment awareness.

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. To use a model from your server, call a provider's API from a tool, or write an 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). The methods never throw.

this.uri and this.params

In a resource, the URI being read and, for a @ResourceTemplate, 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. 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.

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.

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.

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) ends the request as return value would (since 1.9.3: before, it failed the request). See @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), 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.

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

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. (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.
  • 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. 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:

Jobs and channels

Example 1 of 2

Job

Open
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 };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

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

Open
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,
    };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

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:

Open
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,
    };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Catching this.fail() like this only makes sense to show this.error: in a real tool, the catch would swallow the failure.

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:

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

@Tool({ name: "close_ticket", description: "Close a support ticket", inputSchema: { id: z.string() } })
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    this.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 };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

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:

Open
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() };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Each history entry also has at, a timestamp, and stage, the stage the call was in when the value was set. Hooking into Calls 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:

Open
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" } };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

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:

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

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

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Add the built-in ConfigPlugin to @FrontMcp({ plugins }); by default it also loads .env files. Or read process.env in a 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:

Open
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 };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Few clients offer sampling, and MCP 2026-07-28 deprecates it. Call a model provider's API from the tool, or use an 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:

Open
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 };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Keep state that outlasts a call in a 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, and this.context.