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
| Class | Extended by | FrontMCP creates one | Base class |
|---|---|---|---|
ToolContext | @Tool classes | For every tools/call | ExecutionContextBase |
ResourceContext | @Resource and @ResourceTemplate classes | For every resources/read | ExecutionContextBase |
PromptContext | @Prompt classes | For every prompts/get | ExecutionContextBase (since 1.9.2) |
AgentContext | @Agent classes | For every call to the agent's invoke_<name> tool | ExecutionContextBase |
JobContext | @Job classes | For every run a client starts with execute_job, or a workflow step | ExecutionContextBase |
ChannelContext | @Channel classes | For every event, or once for a service channel | ExecutionContextBase |
SkillContext | @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 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.
| Member | Tool | Resource | Prompt | Agent | Job | Channel |
|---|---|---|---|---|---|---|
this.get(), this.tryGet() | Yes | Yes | Yes | Yes | Yes | Yes |
this.fail() | Yes | Yes | Yes | Yes | Yes | Yes |
this.auth | Yes | Yes | Yes | Yes | The caller who started the run | An empty caller |
this.context, this.tryGetContext() | Yes | Yes | Yes | Yes | The request that started the run | Throws |
this.fetch() | Yes, aborted with this.signal | Yes | Yes | Yes | Yes | Without headers |
this.callTool() | Yes | Yes | Yes | Yes, on the "agent" surface | Yes, on the "job" surface | As an anonymous caller |
this.scope | Yes | Yes | Yes | Yes | Yes | Yes |
this.logger | Yes | Yes | Yes | Yes | Yes | Yes |
this.contextLogger | Yes | Yes | Yes | Yes | Yes | Yes |
this.metadata | Yes | Yes | Yes | Yes | Yes | Yes |
this.config | Yes | Yes | Yes | Yes | Yes | Yes |
this.workerEnv | Yes | Yes | Yes | Yes | The request's, if it runs in one | The request's, if it runs in one |
this.authInfo, this.getAuthInfo() | Yes | Yes | Yes | Yes | Yes | Yes |
this.runId, this.activeStage, this.mark() | Yes | Yes | Yes | Yes | Yes | Yes |
this.error | Yes | Yes | Yes | Yes | Yes | Yes |
this.runtimeContext, isEnv() and the other checks | Yes | Yes | Yes | Yes | Yes | Yes |
Members of some classes
| Member | Tool | Resource | Prompt | Agent | Job | Channel |
|---|---|---|---|---|---|---|
this.respond() | Yes | Yes (since 1.9.3) | Yes (since 1.9.3) | Yes | Yes: ends the run | No |
this.notify() | Yes | No | No | Yes | No | No |
this.progress() | Yes | No | No | Yes | Sends nothing | No |
this.elicit() | Yes | No | No | Yes | No | No |
this.input | Yes | No | No | Yes | Yes | No |
this.inputHistory | Yes | No | No | Yes | No | No |
this.output | Yes | Yes | Yes | Yes | Yes | No |
this.outputHistory | Yes | Yes | Yes | Yes | No | No |
this.signal | Yes | No | No | No | No | No |
this.clientInfo, this.platform | Yes | No | No | Yes | No | No |
this.sample(), this.listRoots() | Yes | No | No | No | No | No |
this.notifyResourceUpdated(), this.notifyResourceListChanged() | Yes | No | No | No | No | No |
this.uri, this.params | No | Yes | No | No | No | No |
this.notifyUpdated() | No | Yes | No | No | No | No |
this.getArgumentCompleter() | No | Yes | No | No | No | No |
this.args | No | No | Yes | No | No | No |
this.log(), this.attempt, this.getLogs() | No | No | No | No | Yes | No |
| The model loop's methods | No | No | No | Yes | No | No |
onEvent(), onReply(), onConnect(), onDisconnect(), pushIncoming() | 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.
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
thisstart fresh each time. Share state through providers. protectedmembers work only inside the class. That coversthis.fail(),this.elicit(),this.logger,this.contextLogger, an agent'snotify()andprogress(), and more. A helper function outside the class should return values, or throw aPublicMcpError, whichthis.fail()treats the same. A tool'snotify(),progress(),notifyResourceUpdated()andnotifyResourceListChanged(), and a job'slog()andprogress(), are public since 1.9.1, so atool()orjob()handler can call them on itsctx.- 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
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):
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:
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:
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:
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:
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:
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:
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:
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:
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.