Environment awareness

The same server can run on a laptop, in a Docker container, on a serverless platform or inside another program. FrontMCP works out where it's running the first time something asks, and uses the answer in two places: availableWhen, which takes an entry out of the server where it can't work, and this.runtimeContext, which lets code inside a call branch on it.

@Tool({ name: "export_tickets", availableWhen: { runtime: ["node", "bun"], env: ["production"] } })
class ExportTickets extends ToolContext {
  async execute() {
    this.runtimeContext; // { os, platform, runtime, deployment, provider, target, env }
    this.isEnv("production");
  }
}

Reference

availableWhen

An option of @Tool, @Resource, @ResourceTemplate, @Prompt and @Skill. It takes an object of arrays; each array lists the values a field may have.

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

@Tool({
  name: "export_tickets",
  description: "Write every open ticket to a CSV file on the server's disk",
  inputSchema: {},
  availableWhen: { runtime: ["node", "bun"], deployment: ["standalone"] },
})
export class ExportTickets extends ToolContext {
  async execute() {
    return { path: "/var/exports/tickets.csv" };
  }
}

See more examples below.

Fields

FieldMatchesValues FrontMCP detects
osThe operating system, as Node's process.platform names it."darwin", "linux", "win32", "freebsd", …, and "browser" in FrontMCP's browser build
platformThe same as os. Deprecated: use os.
runtimeThe JavaScript runtime."node", "bun", "deno", "edge", and "browser" in FrontMCP's browser build
deploymentHow the server is deployed."standalone", "serverless", "distributed"
providerThe hosting provider."bare", "docker", "vercel", "lambda", "cloudflare", "netlify", "azure", "gcp", "fly", "render", "railway"
targetThe build target: what frontmcp build --target made, or FRONTMCP_BUILD_TARGET."unknown", "node", "distributed", "cli", "vercel", "lambda", "cloudflare", "browser", "sdk", "mcpb"
envNODE_ENV."development" when it isn't set, or whatever it's set to: "production", "test", …
surfaceWhere the call comes from, not the machine: "mcp", "cli", "agent", "job", "http-trigger" or "webmcp". See Surfaces."mcp" for MCP clients and createDirect(); "cli" for the commands of a server built with frontmcp build --target cli; "agent" for an agent's calls; "job" for a job's this.callTool(); "http-trigger" for a channel's this.callTool() while it handles a webhook; "webmcp" for an agent in the user's browser, through @frontmcp/plugin-webmcp.

How each value is detected is below. The types also list "browser" for deployment, but FrontMCP never reports it: its browser build says "standalone", as in the Playground.

Matching

  • Every field you set must match. { runtime: ["node"], env: ["production"] } needs both.
  • Within a field, any one value matches. runtime: ["node", "bun"] matches either.
  • A field you leave out matches everything. No availableWhen means available everywhere.
  • An empty array matches nothing: the entry is never available.
  • Values aren't checked. A misspelled one, like runtime: ["nodejs"], never matches, so the entry is silently never available. A misspelled field fails at startup with Unrecognized key.

What happens where it doesn't match

EntryListed?Usable?
@ToolNo: left out of tools/list.No: a call fails with ENTRY_UNAVAILABLE, from a client or from this.callTool().
@Resource, @ResourceTemplateNo: left out of resources/list and resources/templates/list.No: resources/read fails with -32003.
@PromptNo: left out of prompts/list.No: prompts/get fails with -32003.
@SkillNo: left out of skills/list, skills/search and the skill:// resources.No: skills/load can't find it. See @Skill.
@AgentNo: its invoke_<name> tool is left out of tools/list.No: a call fails with ENTRY_UNAVAILABLE, as for a tool.
@ChannelNo: the channel isn't registered, so a two-way channel's channel-reply tool isn't listed either.No. FrontMCP logs Channel "…" is not available in this runtime; skipped at verbose.
@Job, @WorkflowNot an option.

Before FrontMCP 1.8.3, a resource or prompt that didn't match was only left out of its list and could still be read by its URI or name, and availableWhen on an @Agent was ignored. Before 1.8.4, a skill that didn't match was still listed in skills/list and skills/search.

Two tools may share a name when their availableWhen never match at the same time. Clients see the one that matches: see one tool, a version per platform.

Surfaces

surface is about who is calling, not where the server runs. An entry whose surface leaves out the caller's surface is treated as if it didn't exist: it's left out of every list, and a call, read or get answers as for an unknown name: Tool "…" not found, Resource not found: …, Prompt not found: …. invoke_<agent> tools work the same way, and so do skills in skills/list and skills/load. Keeping a tool from MCP clients shows it.

CallerSurface
An MCP client, over HTTP or stdio, and createDirect()"mcp"
The commands of a server built with frontmcp build --target cli"cli"
An agent: the tools its model is offered and calls, and this.callTool() in the agent's own code"agent"
A job's this.callTool()"job"
this.callTool() in a tool, resource or promptNone: surface isn't checked, except surface: [], which nothing matches.
A channel's this.callTool() while it handles a request to its webhook source"http-trigger"
An agent in the user's browser, calling a tool of a server that runs in the page, through @frontmcp/plugin-webmcp"webmcp": ["webmcp"] offers a tool only to those agents, ["mcp"] keeps it from them. See server.registerTool().

So surface: ["agent"] hides a tool from every MCP client while an agent can still call it, and surface: ["mcp"] keeps a tool from agents and jobs: an agent's model isn't offered it, and a job's this.callTool() fails with Tool "…" not found, as does a webhook channel's. Keeping a tool from agents and jobs shows it. Webhook routes are served by the Node server only: through createFetchHandler(), and so in the Playground, the webhook's path answers 404, which is why "http-trigger" was checked in Node. "webmcp", new in 1.9.0, is the surface of calls from an agent in the user's browser through @frontmcp/plugin-webmcp. Changed in 1.8.5: webhook sources weren't served, so no call carried "http-trigger". Changed in 1.8.4: agents and jobs had no surface, so surface didn't keep a tool from them. Before FrontMCP 1.8.3, surface wasn't checked at all, and only surface: [] had an effect.

How FrontMCP detects the environment

The first time anything asks (usually when the server builds its tool list), FrontMCP detects each field and keeps the result for the life of the process, except env, which it reads from NODE_ENV every time. Changing any other environment variable afterwards changes nothing; changing NODE_ENV changes env from then on: see Changing NODE_ENV while the server runs.

FieldHow it's detected, first match wins
os, platformprocess.platform.
runtime"bun" or "deno" when their global exists; "edge" when the global scope looks like an edge runtime, or when both EDGE_RUNTIME and VERCEL_ENV are set; otherwise "node". FrontMCP's browser build, which a bundler picks with the browser condition, always says "browser".
deploymentFRONTMCP_DEPLOYMENT_MODE when it's "serverless" or "distributed"; "serverless" when any of VERCEL, NETLIFY, CF_PAGES, AWS_LAMBDA_FUNCTION_NAME, AZURE_FUNCTIONS_ENVIRONMENT, K_SERVICE, RAILWAY_ENVIRONMENT, RENDER or FLY_APP_NAME is set; otherwise "standalone".
providerFRONTMCP_PROVIDER, when it's one of the known names (anything else is ignored); then VERCEL → "vercel", AWS_LAMBDA_FUNCTION_NAME → "lambda", CF_PAGES → "cloudflare", NETLIFY → "netlify", AZURE_FUNCTIONS_ENVIRONMENT → "azure", K_SERVICE → "gcp", FLY_APP_NAME → "fly", RENDER → "render", RAILWAY_ENVIRONMENT → "railway"; a /.dockerenv file → "docker"; otherwise "bare".
targetThe target a bundle from frontmcp build --target was built for, which it sets when it starts (since 1.9.0); else FRONTMCP_BUILD_TARGET, when it's one of the known names; otherwise "unknown", as for a server started with frontmcp dev or tsx.
envNODE_ENV, or "development" when it isn't set, each time it's read. (Before 1.8.7 it was read once, like the rest.)

frontmcp build does set FRONTMCP_DEPLOYMENT_MODE in the bundles it makes: "serverless" for the Vercel, Lambda and Cloudflare targets, "distributed" for the distributed target. FRONTMCP_SERVERLESS=1, which makes @FrontMcp serve a handler instead of listening, doesn't change deployment.

When any entry has availableWhen, each registry logs a summary at startup, at info level, and each entry it filtered at verbose:

[ToolRegistry] availability: 5 total, 4 with availableWhen constraint, 3 available, 1 filtered [ctx: platform=darwin, runtime=node, deployment=standalone, env=development]

In the Playground

A Playground on this site runs FrontMCP's browser build in your browser (since FrontMCP 1.9, the SDK ships one), and that build doesn't detect anything: it always says { os: "browser", platform: "browser", runtime: "browser", deployment: "standalone", provider: "bare", target: "browser" }. Its env follows NODE_ENV, as in Node: the page's process.env.NODE_ENV when there is one, else the value a bundler built in, else "development". Error details follow the same value. The Playground sets NODE_ENV to "development". (Before 1.9.2 the browser build's env was always "production", while its errors were formatted for development.) When the same examples run in Node, as they do in this site's tests and under frontmcp test, os is your operating system, runtime is "node" and target is "unknown". So the examples on this page restrict by fields that are the same in both, like deployment or env, or show both answers. Before 1.9, the Playground's build said runtime: "node" and target: "unknown".

this.runtimeContext and the checks

Every context class has them: tools, resources, prompts, agents, jobs and channels.

MemberReturnsDescription
this.runtimeContextRuntimeContextThe detected environment: { os, platform, runtime, deployment, provider, target, env }. The same object for every call.
this.isEnv(env)booleanruntimeContext.env === env.
this.isRuntime(runtime)booleanruntimeContext.runtime === runtime.
this.isDeployment(deployment)booleanruntimeContext.deployment === deployment.
this.isPlatform(os)booleanruntimeContext.platform === os.

There's no isProvider() or isTarget(): compare this.runtimeContext.provider and .target.

Runtime modes

How a server is started is a separate thing from where it runs, and runtimeContext doesn't tell the ways apart:

ModeStarted byWhat runtimeContext says
HTTP server@FrontMcp (the default serve: true) or FrontMcpInstance.bootstrap()deployment: "standalone", or "distributed" with FRONTMCP_DEPLOYMENT_MODE=distributed.
stdioFRONTMCP_STDIO=1, or FrontMcpInstance.runStdio()The same as for HTTP.
Request handlerFRONTMCP_SERVERLESS=1, FrontMcpInstance.createHandler() or createFetchHandler()"serverless" only when a platform variable or FRONTMCP_DEPLOYMENT_MODE says so.
In your processcreate(), createDirect() or connect()The process's environment, like any other mode.

If a tool needs to know the mode, set a variable of your own where you start the server and read it with ConfigPlugin.

Caveats

  • Everything but env is detected once per process, so in one process every server sees the same os, runtime, deployment, provider and target. env follows NODE_ENV: change it while a server runs, and this.runtimeContext.env and the entries availableWhen: { env } allows change with it.
  • availableWhen is about the machine and, with surface, the kind of caller, never about who the caller is. To decide which users may call something, use authorities.
  • The ENTRY_UNAVAILABLE message, and the -32003 error of a resource or prompt, tell any client the server's OS, runtime, deployment, provider, target and NODE_ENV, in production too. If that's more than callers should know, leave the entry out of the server where it can't run.

Usage

Offering a tool only where it works

A tool that needs something only some environments have, like a disk that keeps its files or a Node API, says so with availableWhen. Where it doesn't match, the model never sees it, so it never tries it. Here export_tickets writes to the disk of a long-running server; upload_tickets is for serverless deployments, whose functions don't keep files from one call to the next:

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

@Tool({
  name: "export_tickets",
  description: "Write every open ticket to a CSV file on the server's disk",
  inputSchema: {},
  availableWhen: { deployment: ["standalone"] },
})
export class ExportTickets extends ToolContext {
  async execute() {
    return { path: "/var/exports/tickets.csv", rows: 42 };
  }
}

@Tool({
  name: "upload_tickets",
  description: "Upload every open ticket as a CSV file to object storage",
  inputSchema: {},
  availableWhen: { deployment: ["serverless"] },
})
export class UploadTickets extends ToolContext {
  async execute() {
    return { url: "https://files.desk.example/tickets.csv", rows: 42 };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The same works for resources and prompts, which then don't appear in their lists, and can't be read by a client that knows their URI or name.

One tool, a version per platform

When the same capability needs different code on different platforms, write one class per platform with the same name and a different availableWhen. The model sees a single export_tickets tool, and calls reach whichever version matches. Here the server runtimes write to disk, and the runtimes without a file system, the browser and edge runtimes, upload instead:

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

@Tool({
  name: "export_tickets",
  description: "Export every open ticket as a CSV file and return where it is",
  inputSchema: {},
  availableWhen: { runtime: ["node", "bun", "deno"] },
})
export class ExportTicketsToDisk extends ToolContext {
  async execute() {
    return { location: "/var/exports/tickets.csv" };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

In your browser the Call tab shows the upload's location; run the same server in Node and the disk version answers.

Keep the name, description and inputSchema the same in every version, so the model's view of the tool doesn't depend on where the server runs. Make sure the availableWhen of the versions don't overlap: when two match, clients see and call only the first one registered, and FrontMCP reports no conflict.

Tools for development only

env restricts a tool to some values of NODE_ENV. A tool that resets demo data should never reach production:

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

@Tool({
  name: "reset_demo_data",
  description: "Delete every ticket and load the demo tickets again",
  inputSchema: {},
  availableWhen: { env: ["development", "test"] },
})
export class ResetDemoData extends ToolContext {
  async execute() {
    return { tickets: 3 };
  }
}

@Tool({ name: "where", description: "Where the server is running", inputSchema: {} })
export class Where extends ToolContext {
  async execute() {
    return { env: this.runtimeContext.env };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The Playground runs as development, so the Call tab shows "env": "development" and the tools list has reset_demo_data. Start the server with NODE_ENV=production and it's gone; Changing NODE_ENV while the server runs tests both.

Keeping a tool from MCP clients

surface restricts who an entry is offered to. A tool for operators, meant for the commands of the server's CLI build, shouldn't reach the model: with surface: ["cli"], MCP clients don't see it, and a call by its name fails as if it didn't exist. A tool's own code isn't a surface, so a tool that calls it with this.callTool() still runs it:

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

@Tool({
  name: "purge_closed_tickets",
  description: "Delete every closed ticket for good",
  inputSchema: {},
  availableWhen: { surface: ["cli"] },
})
export class PurgeClosedTickets extends ToolContext {
  async execute() {
    return { purged: 12 };
  }
}

@Tool({ name: "close_ticket", description: "Close a support ticket", inputSchema: { id: z.string() } })
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    return { id, status: "closed" };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

surface decides what clients are offered, not what your code may do. Keep tools that call a restricted one restricted too, and to decide which users may call something, use authorities.

Keeping a tool from agents and jobs

surface works the other way too. A tool that only a person should run, through their MCP client, gets surface: ["mcp"]: an agent's model isn't offered it even when it's in the agent's tools, and a job can't call it. Here the triage agent may read tickets but not delete them, and neither may the spam job. get_ticket reports the surface it was called on, with getCallSurface():

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

@Tool({ name: "get_ticket", description: "Get a support ticket by its id", inputSchema: { id: z.string() } })
export class GetTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    return { id, title: "Charged twice", calledFrom: getCallSurface() };
  }
}

@Tool({
  name: "delete_ticket",
  description: "Delete a support ticket for good",
  inputSchema: { id: z.string() },
  availableWhen: { surface: ["mcp"] }, // people, through their MCP client
})
export class DeleteTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    return { id, deleted: true };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

An agent's model only learns about the tools it's offered, so it never sees delete_ticket; the Call tab shows what it got when it asked for it anyway. this.callTool() in the agent's own code is on "agent" too, so it can't reach delete_ticket either.

Offering an agent or a channel only where it works

availableWhen works on an @Agent as on a tool: its invoke_<name> tool is left out of tools/list where it doesn't match, and a call fails with ENTRY_UNAVAILABLE. On a @Channel, the channel isn't registered at all, so a two-way channel's channel-reply tool is gone too:

Open
import { Agent, AgentContext, App, Channel, ChannelContext, FrontMcp, z, type ChannelNotification } from "@frontmcp/sdk";

@Agent({
  name: "triage",
  description: "Sort a support ticket into a queue",
  inputSchema: { ticket: z.string() },
  availableWhen: { runtime: ["deno"] },
  llm: { adapter: { async completion() { return { content: "billing", finishReason: "stop" }; } } },
})
class Triage extends AgentContext {}

@Channel({
  name: "deploys",
  description: "Deploys of the help desk, as they happen",
  source: { type: "app-event", event: "deploy" },
  twoWay: true,
  availableWhen: { runtime: ["deno"] },
})
class DeploysChannel extends ChannelContext {
  async onEvent(payload: unknown): Promise<ChannelNotification> {
    return { content: `deployed ${(payload as { version: string }).version}` };
  }
  async onReply() {}
}

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

@FrontMcp({ info: { name: "help-desk", version: "1.0.0" }, apps: [HelpDeskApp], channels: { enabled: true } })
export default class Server {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Remove availableWhen from the channel, and channel-reply is listed. Before 1.9.2, availableWhen on a channel was ignored; before 1.8.3, on an agent too.

Branching inside a call

When most of a tool works everywhere and only one step differs, check this.runtimeContext inside execute() instead of writing two tools. Here the tool keeps its cache in memory on a single long-running server, and in a shared store when the server runs as short-lived functions that don't share memory:

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

@Tool({ name: "cache_status", description: "Where search results are cached", inputSchema: {} })
export class CacheStatus extends ToolContext {
  async execute() {
    const { runtime, deployment, provider } = this.runtimeContext;
    const cache = this.isDeployment("serverless") ? "shared store" : "memory";
    return { cache, runtime, deployment, provider };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Resources and prompts have the same members. Context classes has an example that adds debugging details outside production.

Setting the environment for a deployment

Detection covers the common platforms. Where it can't tell, set the variables yourself, before the server starts, for example in a Dockerfile:

Dockerfile
ENV NODE_ENV=production
ENV FRONTMCP_PROVIDER=fly
ENV FRONTMCP_DEPLOYMENT_MODE=distributed
ENV FRONTMCP_BUILD_TARGET=node

These can't be shown in a Playground: it runs in one process, whose environment is fixed. Configuration files lists every environment variable FrontMCP reads.


Troubleshooting

Tool "…" is not available in the current environment

A client called a tool whose availableWhen doesn't match where the server runs. The result is a tool error with the code ENTRY_UNAVAILABLE, and the message says what the tool requires, what the server has, and which fields didn't match:

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

@Tool({ name: "run_on_deno", description: "Only works on Deno", inputSchema: {}, availableWhen: { runtime: ["deno"] } })
export class RunOnDeno extends ToolContext {
  async execute() {
    return { ok: true };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The client had the tool's name from an older list, or guessed it; the model won't see it in tools/list. If the tool should run here, fix its availableWhen. A tool called with this.callTool() fails the same way, by throwing an EntryUnavailableError.

A tool is missing, and nothing says why

Its availableWhen doesn't match, most often because of a misspelled value ("nodejs", "prod"), an empty array, or a surface without "mcp"; none is an error. Look for the startup line [ToolRegistry] availability: … filtered, and set the log level to Verbose to see filtered: "<name>" with the constraint and the detected value. Compare with what this.runtimeContext returns in a tool that has no availableWhen.

Tool "…" not found for a tool that has surface

Since FrontMCP 1.8.3, surface is checked. A tool whose surface leaves out "mcp" isn't offered to MCP clients or to createDirect(), and a call to it fails with TOOL_NOT_FOUND, as for a tool that doesn't exist; a resource or prompt answers Resource not found: … or Prompt not found: …. Before 1.8.3 such a tool was listed and callable. Add "mcp" to its surface if clients should reach it, or remove surface.

Since 1.8.4, agents and jobs are surfaces too. An agent whose tool's surface leaves out "agent" isn't offered that tool, and when its model calls it anyway, the model gets Tool "…" not found in agent "…"; a job's this.callTool() of a tool without "job" throws Tool "…" not found. Add the caller's surface, or remove surface. See Surfaces, Keeping a tool from MCP clients and Keeping a tool from agents and jobs.

Resource "…" is not available in the current environment

A client read a resource, or got a prompt, whose availableWhen doesn't match where the server runs. Both are left out of their lists, and since FrontMCP 1.8.3 reading or getting them fails too, with JSON-RPC error -32003 and the same kind of message as for a tool. Before 1.8.3 they could still be read by a client that knew the URI or the name:

Open
import { Prompt, PromptContext, Resource, ResourceContext } from "@frontmcp/sdk";

@Resource({ name: "deno-config", uri: "desk://deno-config", mimeType: "application/json", availableWhen: { runtime: ["deno"] } })
export class DenoConfig extends ResourceContext {
  async execute(uri: string) {
    return { contents: [{ uri, text: JSON.stringify({ permissions: ["--allow-net"] }) }] };
  }
}

@Prompt({ name: "deno-review", description: "Review a Deno module", arguments: [], availableWhen: { runtime: ["deno"] } })
export class DenoReview extends PromptContext {
  async execute() {
    return { messages: [{ role: "user" as const, content: { type: "text" as const, text: "Review this Deno module." } }] };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The client had the URI or the name from elsewhere; the model won't see either in a list. If the resource or prompt should work here, fix its availableWhen.

Unrecognized key in availableWhen

The server fails to start with a Zod error: Unrecognized key: "environment", path availableWhen. The fields are os, platform, runtime, deployment, provider, target, env and surface:

// 🚩 Not a field
availableWhen: { environment: ["production"] }

// ✅
availableWhen: { env: ["production"] }

target is "unknown"

The server didn't start from a bundle that frontmcp build --target … made: frontmcp dev and tsx start your source. (The Playground says "browser".) A bundle sets its target when it starts, since FrontMCP 1.9.0; node dist/node/<name>.bundle.js reports target: "node". Elsewhere, set FRONTMCP_BUILD_TARGET in the environment, or restrict by deployment or provider, which are detected. Before 1.9.0, frontmcp build didn't pass the target to the bundle, so it was always "unknown".

Changing NODE_ENV while the server runs

env is read from NODE_ENV each time, so a tool that's availableWhen: { env: ["production"] } appears, and one that's ["development"] disappears, as soon as NODE_ENV becomes production, and this.runtimeContext.env says so too. That is useful in a test and surprising in a server: set NODE_ENV before the server starts, in the shell, the Dockerfile or the platform's settings, and leave it. A .env file only works if something loads it before FrontMCP starts, such as frontmcp dev: see Configuration files. Every other field is detected once, and changing its variable afterwards does nothing. Before 1.8.7 env was fixed too, so NODE_ENV set after the first read changed nothing. The Playground reads its own NODE_ENV the same way, so the tests pass in your browser and in Node.

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

@Tool({ name: "reset_demo_data", description: "Delete every ticket and load the demo tickets again", inputSchema: {}, availableWhen: { env: ["development"] } })
export class ResetDemoData extends ToolContext {
  async execute() {
    return { tickets: 3 };
  }
}

@Tool({ name: "export_audit_log", description: "Write the audit log for the auditors", inputSchema: {}, availableWhen: { env: ["production"] } })
export class ExportAuditLog extends ToolContext {
  async execute() {
    return { rows: 120 };
  }
}

@Tool({ name: "where", description: "Where the server is running", inputSchema: {} })
export class Where extends ToolContext {
  async execute() {
    return { env: this.runtimeContext.env };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.