# this.context

> The request a call belongs to, with its id, trace, auth info and HTTP metadata, what each entry point fills in, and a store that lasts for the request.

Source: https://frontmcp.dev/reference/sdk/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.

```ts
const { requestId, traceContext, authInfo, metadata, sessionId } = this.context
```

---

## Reference

### `this.context`

Read `this.context` inside `execute()`.

```ts reopen-ticket.tool.ts
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}`));
    }
  }
}
```

[See more examples below.](#usage)

#### 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()`](https://frontmcp.dev/reference/sdk/call-tool) share it. |
| `traceContext` | `{ traceId, parentId, traceFlags, raw }` | The request's [W3C trace context](https://www.w3.org/TR/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()`](https://frontmcp.dev/reference/sdk/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](#authinfo). [`this.auth`](https://frontmcp.dev/reference/sdk/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](https://frontmcp.dev/reference/sdk/create#call-options) 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`](https://frontmcp.dev/reference/sdk/contexts#thisclientinfo-and-thisplatform), 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](https://frontmcp.dev/reference/sdk/fetch#server-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()`](https://frontmcp.dev/reference/sdk/connect)'s; `sessionMetadata` for the Node server's only. `transport.supportsElicit` and `transport.elicit()` are the low-level form of [`this.elicit()`](https://frontmcp.dev/reference/sdk/elicit): use that. |
| `platformEnv` | `unknown` | On a Cloudflare Worker, the `env` the handler was called with, which [`this.workerEnv`](https://frontmcp.dev/reference/sdk/contexts#thisworkerenv) reads; in-process, the `workerEnv` given to [`create()`, the call](https://frontmcp.dev/reference/sdk/create#passing-a-workers-bindings) or [`connect()`](https://frontmcp.dev/reference/sdk/connect#parameters) (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](https://frontmcp.dev/reference/server/flows) 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()`](#getcallsurface-and-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`](https://frontmcp.dev/reference/sdk/contexts#thisscope). (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](#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`](https://frontmcp.dev/reference/sdk/contexts#thislogger-and-thiscontextlogger) 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()`](https://frontmcp.dev/reference/sdk/fetch) calls. |

#### What each entry point fills in

| | Node HTTP server | [`createFetchHandler()`](https://frontmcp.dev/reference/sdk/create-fetch-handler), the Playground | [`connect()`](https://frontmcp.dev/reference/sdk/connect) | [`create()`, `createDirect()`](https://frontmcp.dev/reference/sdk/create) |
| --- | --- | --- | --- | --- |
| `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](https://frontmcp.dev/reference/sdk/create-fetch-handler#the-clients-address) | `{ 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](#seeing-what-each-entry-point-fills-in) shows the last three columns. The Node server column is what [Reading the Request](https://frontmcp.dev/learn/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()`](https://frontmcp.dev/reference/sdk/fetch#forwarding-the-callers-token-to-your-own-service) 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`](https://frontmcp.dev/reference/sdk/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](https://frontmcp.dev/reference/sdk/agent) calls, from its model or its `execute()`. `"job"` in a [job](https://frontmcp.dev/reference/sdk/job), a workflow's steps included, and in the tools it calls. `undefined` in a tool another tool runs with [`this.callTool()`](https://frontmcp.dev/reference/sdk/call-tool), 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](https://frontmcp.dev/reference/sdk/channel#sources) 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()`](https://frontmcp.dev/reference/sdk/register-tool). |
| `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`](https://frontmcp.dev/reference/server/environment) 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](#telling-a-clients-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](https://frontmcp.dev/reference/sdk/contexts#where-there-is-no-request-context). See [Troubleshooting](#requestcontext-not-available).

#### Caveats

- Everything in `metadata`, `clientInfo` and `platformType` came from the client, which can write anything there. Use them for formatting, logging and tracing, never to decide access. [Reading the Request](https://frontmcp.dev/learn/reading-the-request#arguments-client-info-and-headers-are-claims) explains why.
- The store holds values for one request. To keep something between requests, use a [provider](https://frontmcp.dev/reference/sdk/provider).
- `this.context` throws where there's no request, rather than returning `undefined`. Use `this.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](https://frontmcp.dev/learn/reading-the-request#tagging-logs-and-errors-with-the-request-id) walks through this example:

```ts reopen-ticket.tool.ts active
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"));
    }
  }
}
```

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

test("the message ends with the request id, new for each request", async ({ mcp }) => {
  const first = (await mcp.tools.call("reopen_ticket", { id: "T-2" })).text()!;
  const second = (await mcp.tools.call("reopen_ticket", { id: "T-2" })).text()!;
  expect(first).toMatch(/reference: [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
  expect(first).not.toBe(second);
  expect(first).not.toContain("SQLITE_BUSY");
});
```

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()`](https://frontmcp.dev/reference/sdk/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](https://frontmcp.dev/reference/server/observability#following-a-request-across-services).

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

```ts trace.test.ts
import { test, expect } from "@frontmcp/testing";
import { FrontMcpInstance } from "@frontmcp/sdk";
import { HelpDesk } from "./trace.tool";

test("without a traceparent, each request starts a trace", async ({ mcp }) => {
  const first = (await mcp.tools.call("trace_info", {})).json();
  const second = (await mcp.tools.call("trace_info", {})).json();
  expect(first.traceId).toMatch(/^[0-9a-f]{32}$/);
  expect(first.raw).toBe(`00-${first.traceId}-${first.parentId}-01`);
  expect(second.traceId).not.toBe(first.traceId);
});

test("with a traceparent header, the request joins that trace", async () => {
  const handler = await FrontMcpInstance.createFetchHandler({ info: { name: "help-desk", version: "1.0.0" }, apps: [HelpDesk] });
  const traceparent = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
  const response = await handler(
    new Request("https://desk.example.com/", {
      method: "POST",
      headers: { "content-type": "application/json", "mcp-protocol-version": "2026-07-28", "mcp-method": "tools/call", "mcp-name": "trace_info", traceparent },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "trace_info", arguments: {}, _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } } }),
    }),
  );
  const { result } = await response.json();
  expect(result.structuredContent).toEqual({ traceId: "0af7651916cd43dd8448eb211c80319c", parentId: "b7ad6b7169203331", traceFlags: 1, raw: traceparent });
});

test("a traceparent in _meta isn't used", async ({ mcp }) => {
  const traceparent = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
  const response = await mcp.raw.request({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "trace_info", arguments: {}, _meta: { traceparent } } });
  expect(response.result.structuredContent.traceId).not.toBe("0af7651916cd43dd8448eb211c80319c");
});
```

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:

```ts desk.plugin.ts active
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");
  }
}
```

```ts tools.ts
import { Tool, ToolContext } from "@frontmcp/sdk";
import { DESK } from "./desk.plugin";

@Tool({ name: "whoami", description: "Which desk is handling this request", inputSchema: {} })
export class WhoAmI extends ToolContext {
  async execute() {
    const inner = await this.callTool("desk_queue", {});
    return { desk: this.context.get<string>(DESK), queue: inner.structuredContent };
  }
}

@Tool({ name: "desk_queue", description: "The desk's queue", inputSchema: {}, visibility: "internal" })
export class DeskQueue extends ToolContext {
  async execute() {
    return { desk: this.context.get<string>(DESK), open: 7 };
  }
}
```

```ts help-desk.app.ts
import { App } from "@frontmcp/sdk";
import { DeskPlugin } from "./desk.plugin";
import { DeskQueue, WhoAmI } from "./tools";

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

```ts desk.test.ts
import { test, expect } from "@frontmcp/testing";
import { FrontMcpInstance } from "@frontmcp/sdk";
import { HelpDesk } from "./help-desk.app";

test("the Playground sends no desk header", async ({ mcp }) => {
  expect((await mcp.tools.call("whoami", {})).json()).toEqual({ desk: "general", queue: { desk: "general", open: 7 } });
});

test("the header's desk reaches both tools", async () => {
  const handler = await FrontMcpInstance.createFetchHandler({ info: { name: "help-desk", version: "1.0.0" }, apps: [HelpDesk] });
  const call = (headers: Record<string, string>) =>
    handler(
      new Request("https://desk.example.com/", {
        method: "POST",
        headers: { "content-type": "application/json", "mcp-protocol-version": "2026-07-28", "mcp-method": "tools/call", "mcp-name": "whoami", ...headers },
        body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "whoami", arguments: {}, _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } } }),
      }),
    ).then(async (r) => (await r.json()).result.structuredContent);
  expect(await call({ "x-frontmcp-desk": "berlin" })).toEqual({ desk: "berlin", queue: { desk: "berlin", open: 7 } });
  expect(await call({})).toEqual({ desk: "general", queue: { desk: "general", open: 7 } }); // a new request, a new store
});
```

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](https://frontmcp.dev/reference/sdk/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:

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

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

test("marks measure the parts of the call", async ({ mcp }) => {
  const { timings, marks, startedAt, log } = (await mcp.tools.call("export_tickets", {})).json();
  expect(marks).toEqual(["init", "query", "format"]);
  expect(startedAt).toBe(true);
  expect(Object.keys(log)).toEqual(["requestId", "traceId", "parentId", "sessionIdHash", "scopeId", "flowName", "elapsed"]);
  expect(log.flowName).toBe("tools:call-tool");
  expect(log.sessionIdHash).toMatch(/^[0-9a-f]{12}$/);
  expect(timings.queryMs).toBeGreaterThanOrEqual(35);
  expect(timings.totalMs).toBeGreaterThanOrEqual(timings.queryMs + timings.formatMs);
});
```

`this.context.mark()` is a timing mark. `this.mark()`, on the context classes, is something else: it only sets [`this.activeStage`](https://frontmcp.dev/reference/sdk/contexts#thisrunid-thisactivestage-and-thismark).

### Telling a client's call from an in-process one

[`getCallSurface()` and `getRunningTool()`](#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:

```ts audit.ts active
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" };
}
```

```ts tickets.tools.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";
import { auditEntry } from "./audit";

@Tool({ name: "notify_customer", description: "Email the customer about a ticket", inputSchema: { id: z.string() } })
export class NotifyCustomer extends ToolContext {
  async execute({ id }: { id: string }) {
    return { audit: [auditEntry(`emailed the customer of ${id}`)] };
  }
}

@Tool({ name: "close_ticket", description: "Close a ticket and tell the customer", inputSchema: { id: z.string() } })
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    const notified = await this.callTool("notify_customer", { id });
    const { audit } = notified.structuredContent as { audit: object[] };
    return { audit: [auditEntry(`closed ${id}`), ...audit] };
  }
}
```

```ts help-desk.app.ts
import { App } from "@frontmcp/sdk";
import { CloseTicket, NotifyCustomer } from "./tickets.tools";

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

```ts audit.test.ts
import { test, expect } from "@frontmcp/testing";
import { Agent, AgentContext, App, FrontMcpInstance, Job, JobContext, Prompt, PromptContext, getCallSurface, z } from "@frontmcp/sdk";
import { HelpDeskApp } from "./help-desk.app";
import { NotifyCustomer } from "./tickets.tools";

test("each entry names its tool, and only the client's call is from mcp", async ({ mcp }) => {
  const result = await mcp.tools.call("close_ticket", { id: "T-1" });
  expect(result.json()).toEqual({
    audit: [
      { action: "closed T-1", tool: "help-desk:close_ticket", from: "mcp" },
      { action: "emailed the customer of T-1", tool: "help-desk:notify_customer", from: "in-process" },
    ],
  });
});

test("a client calling notify_customer itself is from mcp", async ({ mcp }) => {
  const result = await mcp.tools.call("notify_customer", { id: "T-2" });
  expect(result.json()).toEqual({ audit: [{ action: "emailed the customer of T-2", tool: "help-desk:notify_customer", from: "mcp" }] });
});

test("so is a call through createDirect()", async () => {
  const server = await FrontMcpInstance.createDirect({ info: { name: "help-desk", version: "1.0.0" }, apps: [HelpDeskApp] });
  const result = await server.callTool("notify_customer", { id: "T-3" });
  await server.dispose();
  expect(result.structuredContent).toEqual({ audit: [{ action: "emailed the customer of T-3", tool: "help-desk:notify_customer", from: "mcp" }] });
});

test("an agent's calls are from agent, a job's from job, and a prompt is from mcp", async () => {
  @Agent({
    name: "closer",
    inputSchema: {},
    tools: [NotifyCustomer],
    // A scripted model: it calls notify_customer once, then answers with the tool's result.
    llm: {
      adapter: {
        async completion(prompt) {
          const last = prompt.messages[prompt.messages.length - 1];
          if (last.role === "user") return { content: null, finishReason: "tool_calls", toolCalls: [{ id: "1", name: "notify_customer", arguments: { id: "T-4" } }] };
          return { content: last.content, finishReason: "stop" };
        },
      },
    },
  })
  class Closer extends AgentContext {}

  @Job({ name: "nightly-notify", inputSchema: {}, outputSchema: { audit: z.array(z.any()) } })
  class NightlyNotify extends JobContext {
    async execute() {
      return (await this.callTool("notify_customer", { id: "T-5" })).structuredContent as { audit: object[] };
    }
  }

  @Prompt({ name: "surface", description: "Say where the prompt was asked from", arguments: [] })
  class Surface extends PromptContext {
    async execute() {
      return { messages: [{ role: "user" as const, content: { type: "text" as const, text: String(getCallSurface()) } }] };
    }
  }

  @App({ id: "help-desk", name: "Help Desk", tools: [NotifyCustomer], agents: [Closer], jobs: [NightlyNotify], prompts: [Surface] })
  class Desk {}
  const server = await FrontMcpInstance.createDirect({ info: { name: "help-desk", version: "1.0.0" }, apps: [Desk] });
  const byAgent = await server.callTool("invoke_closer", {});
  const byJob = await server.callTool("execute_job", { name: "nightly-notify", input: {} });
  const prompt = await server.getPrompt("surface");
  await server.dispose();
  expect(byAgent.structuredContent).toEqual({ audit: [{ action: "emailed the customer of T-4", tool: "agent:closer:notify_customer", from: "agent" }] });
  expect(byJob.structuredContent).toMatchObject({ result: { audit: [{ action: "emailed the customer of T-5", tool: "help-desk:notify_customer", from: "job" }] } });
  expect(prompt.messages).toEqual([{ role: "user", content: { type: "text", text: "mcp" } }]);
});
```

### Seeing what each entry point fills in

One tool, called through `createFetchHandler()` with headers, in three auth modes, then through `connect()` and `create()`:

```ts help-desk.app.ts active
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 {}
```

```ts entry-points.test.ts
import { test, expect } from "@frontmcp/testing";
import { FrontMcpInstance, connect, create } from "@frontmcp/sdk";
import { DescribeRequest, HelpDesk } from "./help-desk.app";
import { SAM } from "./tokens";

const info = { name: "help-desk", version: "1.0.0" };

async function describeOverHttp(auth: object, headers: Record<string, string> = {}) {
  const handler = await FrontMcpInstance.createFetchHandler({ info, apps: [HelpDesk], auth });
  const response = await handler(
    new Request("https://desk.example.com/", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        accept: "application/json, text/event-stream",
        "user-agent": "desk-console/2.1",
        "mcp-protocol-version": "2026-07-28",
        "mcp-method": "tools/call",
        "mcp-name": "describe_request",
        "x-frontmcp-desk": "berlin",
        ...headers,
      },
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: {
          name: "describe_request",
          arguments: {},
          _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": { name: "claude-ai", version: "1.0" } },
        },
      }),
    }),
  );
  return (await response.json()).result.structuredContent;
}

test("createFetchHandler(): headers, _meta and a per-request session id", async () => {
  const described = await describeOverHttp({ mode: "public" });
  expect(described).toMatchObject({
    sessionId: expect.stringMatching(/^anon:/),
    verifiedSessionId: null,
    scopeId: "root",
    metadata: {
      contentType: "application/json",
      accept: "application/json, text/event-stream",
      customHeaders: { "x-frontmcp-desk": "berlin" },
    },
    clientInfo: { name: "claude-ai", version: "1.0" },
    platformType: "claude",
    authInfo: { keys: ["clientId", "expiresAt", "extra", "scopes", "token", "user"], token: "", clientId: expect.stringMatching(/^anon:/), scopes: ["anonymous"] },
    config: { forwardCallerTokenTo: [], forwardCustomHeadersTo: [], autoInjectTracingHeaders: true, requestTimeout: 30000 },
    flow: "tools:call-tool",
    sameScope: true,
    transport: false,
    sessionMetadata: false,
    sameAsToken: true,
  });
});

test("an mcp-session-id header the client sent is used as it is, unverified", async () => {
  const described = await describeOverHttp({ mode: "public" }, { "mcp-session-id": "chosen-by-the-client" });
  expect(described).toMatchObject({ sessionId: "chosen-by-the-client", verifiedSessionId: null });
});

test("static mode: the key isn't passed on", async () => {
  const described = await describeOverHttp({ mode: "static", tokens: ["desk-key-1"] }, { authorization: "Bearer desk-key-1" });
  expect(described.authInfo).toMatchObject({ token: "", clientId: expect.stringMatching(/^static:/), scopes: ["static"] });
  expect(described.sessionId).toMatch(/^anon:/); // a placeholder, not the caller
});

test("transparent mode: the caller's token, and expiresAt in milliseconds", async () => {
  const jwks = { keys: [{ kty: "EC", crv: "P-256", kid: "desk-1", alg: "ES256", use: "sig", x: "1CjT9Do3B6PgOMVsIY0Amz_l1QbuUfoNLcq3E1OFip0", y: "4Je6s1T1RHT7obF4lowUe-63m0pat0TZfmnRxLfDCE8" }] };
  const auth = { mode: "transparent", provider: "https://auth.example.com", expectedAudience: "https://desk.example.com", providerConfig: { jwks } };
  const described = await describeOverHttp(auth, { authorization: `Bearer ${SAM}` });
  expect(described.authInfo).toMatchObject({ token: SAM, clientId: "sam", scopes: ["tickets:read"], expiresAt: 4102444800000 });
});

test("connect(): one session for the client's life, no headers", async () => {
  const client = await connect({ info, apps: [HelpDesk] }, { clientInfo: { name: "cursor", version: "1.4.0" }, session: { user: { sub: "nour" }, scopes: ["tickets:read"] } });
  const first = ((await client.callTool("describe_request", {})) as { structuredContent: any }).structuredContent;
  const second = ((await client.callTool("describe_request", {})) as { structuredContent: any }).structuredContent;
  await client.close();
  expect(first).toMatchObject({
    sessionId: expect.stringMatching(/^direct:/),
    verifiedSessionId: first.sessionId,
    metadata: { customHeaders: {} },
    clientInfo: null,
    platformType: null,
    authInfo: { keys: ["scopes", "sessionId", "transport", "user"], scopes: ["tickets:read"] },
    transport: true,
  });
  expect(second.sessionId).toBe(first.sessionId);
});

test("create(): the calling code's authContext and metadata", async () => {
  const server = await create({ info, tools: [DescribeRequest] });
  const result = await server.callTool("describe_request", {}, {
    authContext: { user: { sub: "nour" }, scopes: ["tickets:read"] },
    metadata: { customHeaders: { "x-frontmcp-desk": "berlin" } },
  });
  await server.dispose();
  expect(result.structuredContent).toMatchObject({
    sessionId: expect.stringMatching(/^direct:[0-9a-f-]{36}:[0-9a-f]{64}$/),
    metadata: { customHeaders: { "x-frontmcp-desk": "berlin" } },
    clientInfo: null,
    authInfo: { keys: ["clientId", "extra", "scopes", "sessionId", "user"], clientId: "nour", scopes: ["tickets:read"] },
    flow: "tools:call-tool",
  });
});
```

```ts tokens.ts
// An access token for "sam" (scope "tickets:read"), signed ahead of time with the test key above.
export const SAM = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlc2stMSJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJzYW0iLCJhdWQiOiJodHRwczovL2Rlc2suZXhhbXBsZS5jb20iLCJpYXQiOjE3OTAwMDAwMDAsImV4cCI6NDEwMjQ0NDgwMCwic2NvcGUiOiJ0aWNrZXRzOnJlYWQiLCJuYW1lIjoiU2FtIE9ydGl6In0.VHIKGx65AgtNp4Y4PtdLCJoQgXg5pOeBYp03oLgpr1l2elL9Gui6EcQuuBUeb-sYYFJo2-labA_nJwhSV9BLpw";
```

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`](https://frontmcp.dev/reference/sdk/contexts#thisclientinfo-and-thisplatform), 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:

```ts handover.prompt.ts active
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 } }] };
  }
}
```

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

test("the prompt gets the request's id and trace", async ({ mcp }) => {
  const text = (await mcp.prompts.get("handover_note", { id: "T-1" })).messages[0].content.text as string;
  expect(text).toMatch(/ref [0-9a-f-]{36}, trace [0-9a-f]{32}"\.$/);
});
```

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:

```ts outages.ts active
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 };
  }
}
```

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

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

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

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

test("a channel has no request context", async ({ mcp }) => {
  expect((await mcp.tools.call("report_outage", { service: "login" })).json()).toEqual({
    reported: "login",
    sameRef: true,
    error: "RequestContext not available. Ensure execution runs within a request scope created by RequestContextStorage.run().",
  });
});
```

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`](https://frontmcp.dev/reference/sdk/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](https://frontmcp.dev/reference/sdk/create-fetch-handler#reading-the-clients-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](https://frontmcp.dev/reference/sdk/provider), keyed by `this.auth.user.sub` when they belong to a user.
