this.context
this.context is the FrontMcpContext of the request a call belongs to: the id FrontMCP gave it, its W3C trace, the raw result of authentication, parts of the HTTP request and the client's description of itself, plus a small store that lives as long as the request. Use it to tag logs and errors with the request, to join a distributed trace, and to hand values from a hook to a tool. Tools, resources, prompts, agents and jobs have it. A channel's onEvent() runs without it.
const { requestId, traceContext, authInfo, metadata, sessionId } = this.context
Reference
this.context
Read this.context inside execute().
import { PublicMcpError, Tool, ToolContext, z } from "@frontmcp/sdk";
@Tool({ name: "reopen_ticket", description: "Reopen a closed support ticket", inputSchema: { id: z.string() } })
export class ReopenTicket extends ToolContext {
async execute({ id }: { id: string }) {
try {
return await reopen(id);
} catch (err) {
this.contextLogger.error(`reopen ${id} failed: ${String(err)}`);
this.fail(new PublicMcpError(`Couldn't reopen ${id}. Give support this reference: ${this.context.requestId}`));
}
}
}Properties
| Property | Type | Description |
|---|---|---|
requestId | string | A UUID that FrontMCP gives each request. It isn't the JSON-RPC id, and the client never sees it unless you put it in a result. Tools called with this.callTool() share it. |
traceContext | { traceId, parentId, traceFlags, raw } | The request's W3C trace context. Read from the request's traceparent header when there is one, otherwise new. raw is the traceparent value to send on, which this.fetch() does for you. |
authInfo | Partial<AuthInfo> | The raw result of authentication: token (the caller's token), clientId, scopes, expiresAt, user (the token's claims) and extra. Its shape differs per entry point. this.auth is the typed view of it. |
metadata | { userAgent?, contentType?, accept?, clientIp?, customHeaders } | Parts of the HTTP request. customHeaders holds every x-frontmcp-* header. In-process, what createDirect()'s metadata call option passes, or only customHeaders: {}. |
clientInfo | { name, version } | undefined | The client's description of itself from this request's _meta (MCP 2026-07-28). Session clients send it once, at initialize, and it isn't here then: read this.clientInfo, which covers both. |
platformType | string | undefined | The AI platform FrontMCP guesses from clientInfo.name, like "claude" or "cursor", or "generic-mcp". |
sessionId | string | For session clients and in-process calls, the session's id (direct: and a UUID in-process). MCP 2026-07-28 has no sessions: each request gets anon: and a new UUID, whoever the caller is, or the value of an mcp-session-id header the client sent, unchecked. It's never the caller's id; use this.auth.user.sub. |
verifiedSessionId | string | undefined | sessionId when the server verified it, as for in-process clients. A request is in a session only if it presented one, in its mcp-session-id header (or a legacy SSE client's sessionId), and the server accepted it: undefined for every 2026-07-28 request, and for a request that presented no session. sessionIdPresentedBy(request), exported by @frontmcp/sdk, reads the id a request presents. |
scopeId | string | The id of the scope serving the request: "root" for a @FrontMcp server. |
timestamp | number | When the request started, in milliseconds since 1970. |
config | { forwardCallerTokenTo, forwardCustomHeadersTo, autoInjectTracingHeaders, requestTimeout } | The server's fetch options, with defaults filled in. |
transport, sessionMetadata | undefined here | Set for session clients: transport for the Node HTTP server's, which the Playground can't run, and for connect()'s; sessionMetadata for the Node server's only. transport.supportsElicit and transport.elicit() are the low-level form of this.elicit(): use that. |
platformEnv | unknown | On a Cloudflare Worker, the env the handler was called with, which this.workerEnv reads; in-process, the workerEnv given to create(), the call or connect() (since 1.9.4). undefined where nothing passes one, as in the Playground's own calls. (New in 1.8.7.) |
flow | { name } | undefined | The innermost flow running for the request where the code runs: flow.name is "tools:call-tool" in a tool, "prompts:get-prompt" in a prompt. For the tool that's running, use getRunningTool(). (New in 1.9.2: before, nothing set it.) |
scope | { id, logger } | undefined | The scope of that flow: the same object as this.scope. (New in 1.9.2.) |
Methods
| Method | Description |
|---|---|
set(key, value), get(key), has(key), delete(key) | A key-value store for this request, with string or symbol keys. Hooks, the tool, and tools it calls with this.callTool() all see the same store; the next request starts empty. See Passing a value from a hook to the tool. |
mark(name) | Records the current time under name. "init" is recorded when the request starts. |
elapsed(from?, to?) | Milliseconds between two marks. from defaults to "init", to to now. |
getMarks() | Every mark, as a ReadonlyMap of name to time. |
getLogger(parent) | A child of parent whose lines start with the first eight characters of requestId and of the trace id. this.contextLogger is one. |
toLogContext() | { requestId, traceId, parentId, sessionIdHash, scopeId, flowName, elapsed }, for structured logs. sessionIdHash is 12 hex characters, so the session id itself doesn't reach your logs. flowName is flow.name. |
fetch(input, init?) | What this.fetch() calls. |
What each entry point fills in
| Node HTTP server | createFetchHandler(), the Playground | connect() | create(), createDirect() | |
|---|---|---|---|---|
requestId | New per request | New per request | New per request | New per request |
traceContext | From traceparent, or new | From traceparent, or new | New | New, or from metadata's x-frontmcp-trace-id |
metadata | User agent, content type, accept, client IP, x-frontmcp-* headers | The same; the client IP from the runtime or a trusted proxy | { customHeaders: {} } | The call's metadata, or { customHeaders: {} } |
clientInfo, platformType | From each 2026-07-28 request's _meta | From each request's _meta | undefined (this.clientInfo has connect()'s clientInfo) | undefined |
sessionId | The session's id, or anon:… per 2026-07-28 request | anon:…, new per request | direct:…, the same for the client's life | direct:…, plus a hash of the user when there's an authContext.user |
authInfo | From your auth mode | From your auth mode | { token, user, scopes, sessionId, transport }, from authToken and session | { sessionId, user, scopes, clientId, extra }, from authContext |
Seeing what each entry point fills in shows the last three columns. The Node server column is what Reading the Request found running frontmcp dev.
authInfo
this.context.authInfo is what the auth layer produced, before FrontMCP turns it into this.auth:
| Caller | authInfo |
|---|---|
public mode | token: "", clientId and user.sub the caller's anon: id, scopes the mode's anonymousScopes, ["anonymous"] by default, user: { iss: "public", sub, name: "Anonymous", scope }, and the same user in extra. (Changed in 1.9.2: before, scopes was ["public"].) |
static mode | token: "" (the key isn't passed on), clientId and user.sub static:…, scopes the mode's scopes |
transparent mode, with a token | token the caller's token, clientId the token's sub, scopes from its scope claim, expiresAt in milliseconds, user every claim |
connect() | token from authToken, user from session.user, scopes from session.scopes (left out without it), sessionId, and transport, the client's connection. No clientId. (Changed in 1.9.3: before, there was no way to give scopes.) |
create(), createDirect() | user from authContext.user ({ iss: "direct", sub: "direct" } without one), scopes from authContext.scopes ([] without them), clientId the user's sub, extra from authContext.extra, and token if you pass one |
The token is what this.fetch() forwards to the origins you list. Don't put it in results or logs.
Related members
| Member | Description |
|---|---|
this.tryGetContext() | Returns the context, or undefined where this.context would throw, as in a channel's onEvent(). |
this.contextLogger | A logger whose lines carry the request id and trace id. protected: use it inside the class. |
FRONTMCP_CONTEXT | The token for the context. In a tool, this.get(FRONTMCP_CONTEXT) is this.context. A CONTEXT-scoped provider can inject it: see @Provider. |
getCallSurface() and getRunningTool()
Two functions exported by @frontmcp/sdk describe the call the code runs in. Unlike this.context, they work anywhere in that call, including helper functions that have no this:
| Function | Returns |
|---|---|
getCallSurface() | "mcp" in a tool, resource or prompt a client asked for, a createDirect() call included, and in an agent's own execute(). "agent" in a tool an agent calls, from its model or its execute(). "job" in a job, a workflow's steps included, and in the tools it calls. undefined in a tool another tool runs with this.callTool(), and outside any call. Its type, CallSurface, also allows "cli", for a CLI build's own client, "http-trigger", in a channel handling a webhook and the tools it calls (since 1.8.5; before, webhook sources were never served), and, since 1.9, "webmcp", for an agent in the browser calling the page's tools: see registerTool(). |
getRunningTool() | { name, fullName } of the tool whose code is running, like { name: "close_ticket", fullName: "help-desk:close_ticket" }. A tool called with this.callTool() sees itself, and the caller sees itself again once it returns. undefined outside a tool, as in a resource or a prompt. |
surface in availableWhen is checked against the same value, so a tool whose surface leaves out "agent" or "job" can't be called by agents or jobs. See Telling a client's call from an in-process one. Changed in 1.8.4: before, only MCP requests had a surface, and getCallSurface() was undefined in a prompt and in anything an agent or a job called.
Where it's available
| In | this.context |
|---|---|
| Tools, including the tools of an agent | The request's context. |
An agent's own execute() | The request's context, as in a tool. (Changed in 1.8.5: before, it threw RequestContextNotAvailableError.) |
| Resources | The request's context. |
| Tool hooks | ctx.state.toolContext.context, the same object the tool gets. |
| Prompts | The request's context. (Changed in 1.9.2: before, PromptContext had no context getter.) |
| Jobs | The context of the execute_job or execute_workflow request that started the run: its requestId, trace, caller and metadata. A run in the background gets a copy of it, with a requestId of its own. (Changed in 1.9.2: before, it threw RequestContextNotAvailableError.) |
| Channels | Throws RequestContextNotAvailableError: a channel's onEvent() runs outside any request. |
Where it throws, this.tryGetContext() returns undefined, and this.fetch() adds no headers. See Troubleshooting.
Caveats
- Everything in
metadata,clientInfoandplatformTypecame from the client, which can write anything there. Use them for formatting, logging and tracing, never to decide access. Reading the Request explains why. - The store holds values for one request. To keep something between requests, use a provider.
this.contextthrows where there's no request, rather than returningundefined. Usethis.tryGetContext()in code that runs in both.
Usage
Tagging logs and errors with the request id
requestId identifies the request in your logs, and this.contextLogger puts it in every line it writes. Give it to the user in an error message, and support can find the failure. Reading the Request walks through this example:
import { PublicMcpError, Tool, ToolContext, z } from "@frontmcp/sdk";
@Tool({ name: "reopen_ticket", description: "Reopen a closed support ticket", inputSchema: { id: z.string() } })
export class ReopenTicket extends ToolContext {
async execute({ id }: { id: string }) {
try {
if (id === "T-2") throw new Error("SQLITE_BUSY: database is locked");
return { id, status: "open" };
} catch (err) {
this.contextLogger.error(`reopen ${id} failed: ${String(err)}`);
this.fail(new PublicMcpError(`Couldn't reopen ${id}. Give support this reference: ${this.context.requestId}`, "STORE_UNAVAILABLE"));
}
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
The Logs tab shows the error line, with the first eight characters of the request id and of the trace id in brackets.
Joining a trace
A request with a traceparent header continues that trace: traceContext.traceId is the caller's, and parentId the caller's span. this.fetch() sends it on, so a trace follows the call through your services. Without the header, each request starts a trace of its own.
Tracing with OpenTelemetry, and how a trace continues into the services your tools call, are in Observability and telemetry.
import { App, Tool, ToolContext } from "@frontmcp/sdk";
@Tool({ name: "trace_info", description: "Show this request's trace. For debugging.", inputSchema: {} })
export class TraceInfo extends ToolContext {
async execute() {
const { traceId, parentId, traceFlags, raw } = this.context.traceContext;
return { traceId, parentId, traceFlags, raw };
}
}
@App({ id: "help-desk", name: "Help Desk", tools: [TraceInfo] })
export class HelpDesk {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
FrontMCP reads the header, not the traceparent a client puts in _meta: that one is only echoed back.
Passing a value from a hook to the tool
The store lets a hook work something out once, for the tool to use. Here a plugin decides which support desk a request belongs to, from a header, and tools read it. A tool called with this.callTool() sees the same store, and the next request starts with an empty one:
import { DynamicPlugin, FlowCtxOf, Plugin, ToolHook } from "@frontmcp/sdk";
export const DESK = Symbol("desk");
@Plugin({ name: "desk", description: "Works out which support desk a request belongs to" })
export class DeskPlugin extends DynamicPlugin<object> {
@ToolHook.Will("execute")
async pickDesk(ctx: FlowCtxOf<"tools:call-tool">) {
const context = ctx.state.toolContext?.context;
if (!context || context.has(DESK)) return; // once per request
context.set(DESK, context.metadata.customHeaders["x-frontmcp-desk"] ?? "general");
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Use a Symbol or a prefixed string as the key, so plugins don't overwrite each other's values. The hook runs again for the inner call, and has() keeps it from redoing the work. To give tools a typed service instead of a loose value, a CONTEXT-scoped provider is the other way to share per-request state.
Timing parts of a call
mark() records a point in time, and elapsed() measures between marks. "init" is when the request started, so elapsed() with no arguments is how long the request has been running:
import { Tool, ToolContext } from "@frontmcp/sdk";
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
@Tool({ name: "export_tickets", description: "Export every ticket as CSV", inputSchema: {} })
export class ExportTickets extends ToolContext {
async execute() {
const ctx = this.context;
ctx.mark("query");
await sleep(40); // stands in for the database query
ctx.mark("format");
await sleep(10); // stands in for building the CSV
const timings = { queryMs: ctx.elapsed("query", "format"), formatMs: ctx.elapsed("format"), totalMs: ctx.elapsed() };
this.contextLogger.info(`export timings ${JSON.stringify(timings)}`);
return { rows: 43, timings, marks: [...ctx.getMarks().keys()], startedAt: ctx.getMarks().get("init") === ctx.timestamp, log: ctx.toLogContext() };
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
this.context.mark() is a timing mark. this.mark(), on the context classes, is something else: it only sets this.activeStage.
Telling a client's call from an in-process one
getCallSurface() and getRunningTool() work in a plain function, so a helper that several tools share can record which tool it ran in, and who asked for it: a client, an agent, a job, or another tool. Here close_ticket notifies the customer through another tool, and both write to the audit log:
import { getCallSurface, getRunningTool } from "@frontmcp/sdk";
/** One line for the audit log, from whichever tool calls it. */
export function auditEntry(action: string) {
return { action, tool: getRunningTool()?.fullName, from: getCallSurface() ?? "in-process" };
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Seeing what each entry point fills in
One tool, called through createFetchHandler() with headers, in three auth modes, then through connect() and create():
import { App, FRONTMCP_CONTEXT, Tool, ToolContext } from "@frontmcp/sdk";
@Tool({ name: "describe_request", description: "Describe the request this call arrived in. For debugging.", inputSchema: {} })
export class DescribeRequest extends ToolContext {
async execute() {
const c = this.context;
const { token, clientId, scopes, expiresAt } = c.authInfo;
return {
sessionId: c.sessionId,
verifiedSessionId: c.verifiedSessionId ?? null,
scopeId: c.scopeId,
metadata: c.metadata,
clientInfo: c.clientInfo ?? null,
platformType: c.platformType ?? null,
authInfo: { keys: Object.keys(c.authInfo).sort(), token: token ?? null, clientId: clientId ?? null, scopes: scopes ?? null, expiresAt: expiresAt ?? null },
config: c.config,
flow: c.flow?.name ?? null,
sameScope: c.scope === this.scope,
transport: c.transport !== undefined,
sessionMetadata: c.sessionMetadata !== undefined,
sameAsToken: this.get(FRONTMCP_CONTEXT) === c,
};
}
}
@App({ id: "help-desk", name: "Help Desk", tools: [DescribeRequest] })
export class HelpDesk {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
The request sets a User-Agent header, but a browser doesn't let a script set one, so in this Playground metadata.userAgent is missing. From a real client, it's the client's User-Agent.
connect() gives the tool its clientInfo too, but as this.clientInfo, not in this.context. The Node server can't run in the Playground; its column in the table above comes from running frontmcp dev.
Reading the request in a prompt
A prompt has this.context, as a tool does. Here a prompt ends its text with the request id and the trace, for a note that support can trace back:
import { Prompt, PromptContext, type GetPromptResult } from "@frontmcp/sdk";
@Prompt({
name: "handover_note",
description: "Write a handover note for a ticket",
arguments: [{ name: "id", description: "Ticket id, like T-1", required: true }],
})
export class HandoverNote extends PromptContext {
async execute({ id }: Record<string, string>): Promise<GetPromptResult> {
const { requestId, traceContext } = this.context;
const text = `Write a handover note for ticket ${id}. End it with "ref ${requestId}, trace ${traceContext.traceId}".`;
return { messages: [{ role: "user", content: { type: "text", text } }] };
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Changed in 1.9.2: before, PromptContext had no context getter, and this.get(FRONTMCP_CONTEXT) was the way to the request. It still returns the same object.
Troubleshooting
RequestContext not available
The full message is RequestContext not available. Ensure execution runs within a request scope created by RequestContextStorage.run(), from a RequestContextNotAvailableError. The code read this.context where FrontMCP 1.9.4 has no request context: in a channel's onEvent(), which runs outside any request, even for an event a tool emitted:
import { Channel, ChannelContext, Tool, ToolContext, z, type ChannelEventBus, type ChannelNotification } from "@frontmcp/sdk";
export const seen: { error?: string; ref?: string } = {};
@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, ref } = payload as { service: string; ref: string };
try {
this.context; // 🚩 throws in a channel
} catch (err) {
seen.error = (err as Error).message;
}
seen.ref = ref; // ✅ what it needs came in the payload
return { content: `${service} is down (ref ${ref})` };
}
}
@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 };
delete seen.error;
delete seen.ref;
channelEventBus?.emit("outage", { service, ref: this.context.requestId });
for (let i = 0; i < 100 && !seen.ref; i++) await new Promise((r) => setTimeout(r, 10)); // let onEvent() finish
return { reported: service, sameRef: seen.ref === this.context.requestId, error: seen.error ?? null };
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Pass what a channel needs, like a reference to log, in the event's payload. And use this.tryGetContext() in code that runs both in requests and elsewhere. this.auth works in both: in a channel it's an empty caller. A job has the context of the request that started it since 1.9.2; before, it threw here too.
this.context.clientInfo is undefined
The client sent its info at initialize, as clients before MCP 2026-07-28 and connect() do, not in this request's _meta. this.clientInfo on a tool covers both, so read that.
this.context.metadata.clientIp is undefined
Behind createFetchHandler(), the IP comes from the runtime when it says who connected (Deno, Bun, Cloudflare Workers), and from X-Forwarded-For or X-Real-IP only when the environment variable FRONTMCP_TRUST_PROXY says the proxy can be trusted. With neither, and in the Playground, it's undefined. In-process calls never have one. Reading the client's address shows each case.
this.context.sessionId is different on every call
Under MCP 2026-07-28 there are no sessions, so each request gets its own anon: placeholder, even from a signed-in caller. Don't use it to recognize a caller or to key per-user data: use this.auth.user.sub.
A value I set() is gone on the next call
The store belongs to one request. Keep values across requests in a provider, keyed by this.auth.user.sub when they belong to a user.