> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentfront.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Environment Awareness

> Conditionally expose tools, resources, prompts, skills, and agents based on platform, runtime, and environment

FrontMCP's environment-awareness system lets you **declaratively restrict** when entries (tools, resources, prompts, skills, agents) are discoverable and executable, based on the runtime environment. Entries that don't match the current environment are automatically filtered from discovery and blocked from execution.

<Info>
  This enables platform-specific tools (e.g., Apple Notes on macOS only), runtime-specific resources (e.g., Node.js file system), and environment-gated features (e.g., debug tools in development only).
</Info>

***

## The `availableWhen` Option

Add `availableWhen` to any entry's metadata to constrain its availability:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@Tool({
  name: 'apple_notes_search',
  description: 'Search Apple Notes',
  inputSchema: { query: z.string() },
  availableWhen: { os: ['darwin'] },
})
class AppleNotesSearchTool extends ToolContext {
  async execute({ query }: { query: string }) {
    // This tool only appears on macOS
  }
}
```

### Matching Semantics

* **AND across fields** — all specified fields must match
* **OR within arrays** — at least one value in an array must match
* **Omitted fields** — unconstrained (matches everything)
* **Empty array** — matches nothing (entry is never available)
* **No `availableWhen`** — always available (default)

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
// Matches: Node.js OR Bun, AND production only
availableWhen: {
  runtime: ['node', 'bun'],
  env: ['production'],
}
```

***

## Available Fields

### `os` (replaces `platform`)

Operating system, matching [`process.platform`](https://nodejs.org/api/process.html#processplatform) values. Renamed from `platform` in issue #417 — the old name still works as a deprecated alias.

| Value | OS |
| - | - |
| `'darwin'` | macOS |
| `'linux'` | Linux |
| `'win32'` | Windows |
| `'freebsd'` | FreeBSD |

### `runtime`

JavaScript runtime:

| Value | Runtime |
| - | - |
| `'node'` | Node.js |
| `'bun'` | Bun |
| `'deno'` | Deno |
| `'edge'` | Edge runtime (Vercel, CF) |
| `'browser'` | Browser |

### `deployment`

Coarse deployment mode:

| Value | Description |
| - | - |
| `'standalone'` | Standard server |
| `'serverless'` | Serverless (Lambda, Vercel) |
| `'distributed'` | Multi-pod HA mode |
| `'browser'` | Browser bundle |

### `provider` (issue #417)

Discriminated deploy provider. Lets `@Tool({ availableWhen: { provider: ['vercel'] } })` express provider-specific rules where `deployment: ['serverless']` is too coarse.

| Value | Detected via env var |
| - | - |
| `'bare'` | No discriminating env var (default) |
| `'docker'` | `/.dockerenv` exists |
| `'vercel'` | `VERCEL` |
| `'lambda'` | `AWS_LAMBDA_FUNCTION_NAME` |
| `'cloudflare'` | `CF_PAGES` |
| `'netlify'` | `NETLIFY` |
| `'azure'` | `AZURE_FUNCTIONS_ENVIRONMENT` |
| `'gcp'` | `K_SERVICE` |
| `'fly'` | `FLY_APP_NAME` |
| `'render'` | `RENDER` |
| `'railway'` | `RAILWAY_ENVIRONMENT` |

Override the detection with `FRONTMCP_PROVIDER=<name>` (useful for tests, Docker images without a discriminating env var, etc.).

### `target` (issue #417)

Build target produced by `frontmcp build --target <x>`. Always `'unknown'` in dev (`frontmcp dev`).

| Value | Source |
| - | - |
| `'unknown'` | Dev mode (no build) or unrecognized value |
| `'node'` | `frontmcp build --target node` |
| `'distributed'` | `frontmcp build --target distributed` |
| `'cli'` | `frontmcp build --target cli` |
| `'vercel'` | `frontmcp build --target vercel` |
| `'lambda'` | `frontmcp build --target lambda` |
| `'cloudflare'` | `frontmcp build --target cloudflare` |
| `'browser'` | `frontmcp build --target browser` |
| `'sdk'` | `frontmcp build --target sdk` |
| `'mcpb'` | `frontmcp build --target mcpb` |

Resolution order: `globalThis.FRONTMCP_BUILD_TARGET` (inlined by the adapter) → `process.env.FRONTMCP_BUILD_TARGET` → `'unknown'`.

### `surface` (issue #417)

Per-call axis — set by the transport adapter / dispatcher on the request `ctx` to discriminate "who is calling this tool." Unlike the other axes (which are constant for the process lifetime), `surface` varies per request.

| Value | Source |
| - | - |
| `'mcp'` | Every MCP request: listings, `tools/call`, `resources/read`, `prompts/get`, `completion/complete` (also `connect()`, `createDirect()` and an MCP App's `ui/callServerTool`) |
| `'cli'` | The in-process client of a `frontmcp build --target cli` binary (issue #409). Its daemon and `--stdio` modes serve MCP, so they are `'mcp'` |
| `'agent'` | The tool calls an agent's model makes (with `execution.useToolFlow: false` too), and `this.callTool()` in an agent's own code |
| `'job'` | `this.callTool()` in a job, including a job a workflow step runs |
| `'http-trigger'` | `this.callTool()` in a `@Channel` handling a `webhook` source's request |

An entry whose `surface` excludes the caller's is left out of that caller's listings and answers like an entry that doesn't exist (`Tool "x" not found`, `Resource not found`, `Prompt not found`, no completions, a refused `resources/subscribe`), so the caller can't tell it exists. An agent's model isn't offered its tools whose `surface` leaves out `'agent'`, and a call it makes to one anyway fails as a call to a tool the agent doesn't have. Skills follow the same rule on every path that serves them, for every axis (a skill `runtime`, `env` or another process-wide axis excludes is absent too): `skills/list`, `skills/search`, `skills/load`, the `skill://` resources (including each skill's `skill://<path>/SKILL.md` entry in `resources/list`), the server instructions and the skills HTTP endpoints (`/skills`, `/llm.txt`, `/llm_full.txt`), which count as `'mcp'`. CodeCall acts for the client that called it, so `codecall:*` search, describe, invoke and execute apply that client's surface too.

Code running for a call reads its surface with `getCallSurface()`: in a tool, a resource read, a prompt and a completion, it is the surface of the call being served (`'job'` in a job, `'http-trigger'` in a channel handling a webhook). A tool, resource or prompt calling `this.callTool()` is in-process dispatch: that call carries no surface and isn't restricted by this axis. A task a detached CLI worker runs keeps the surface of the call that created it.

Example — block external MCP calls but allow agent dispatch:

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@Tool({
  name: 'internal_helper',
  availableWhen: { surface: ['agent'] },
  ...
})
```

### `env`

`NODE_ENV` value:

| Value | Description |
| - | - |
| `'production'` | Production |
| `'development'` | Development |
| `'test'` | Testing |

### Structured errors

When a tool, resource, resource template or prompt exists but a process-wide axis (`os`, `runtime`, `deployment`, `provider`, `target`, `env`) excludes it, `tools/call`, `resources/read` and `prompts/get` throw `EntryUnavailableError` with a `missingAxes` array in `data` (issue #417). Clients can surface "this tool isn't reachable because provider=vercel / …" without parsing prose. A `surface` mismatch answers like an unknown entry instead (see `surface` above).

```json theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
{
  "jsonrpc": "2.0",
  "id": 5,
  "error": {
    "code": -32003,
    "message": "Tool \"deploy_to_lambda\" is not available in the current environment ...",
    "data": {
      "entryType": "Tool",
      "entry": "deploy_to_lambda",
      "missingAxes": ["provider"],
      "constraint": { "provider": ["lambda"] },
      "context": { "provider": "vercel", "os": "linux", "runtime": "node", "surface": "mcp", ... }
    }
  }
}
```

***

## Supported Entry Types

`availableWhen` works on all five entry types:

<Tabs>
  <Tab title="Tool">
    ```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    @Tool({
      name: 'apple_notes_search',
      inputSchema: { query: z.string() },
      availableWhen: { os: ['darwin'] },
    })
    class AppleNotesSearchTool extends ToolContext { ... }
    ```
  </Tab>

  <Tab title="Resource">
    ```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    @Resource({
      name: 'system-info',
      uri: 'system://info',
      availableWhen: { os: ['darwin', 'linux'] },
    })
    class SystemInfoResource extends ResourceContext { ... }
    ```
  </Tab>

  <Tab title="Prompt">
    ```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    @Prompt({
      name: 'xcode-review',
      arguments: [{ name: 'project', required: true }],
      availableWhen: { os: ['darwin'] },
    })
    class XcodeReviewPrompt extends PromptContext { ... }
    ```
  </Tab>

  <Tab title="Skill">
    ```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    @Skill({
      name: 'ios-deploy',
      description: 'Deploy to iOS devices',
      instructions: { file: './skills/ios-deploy.md' },
      availableWhen: { os: ['darwin'] },
    })
    class IosDeploySkill extends SkillContext { ... }
    ```
  </Tab>

  <Tab title="Agent">
    ```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    @Agent({
      name: 'local-build-agent',
      llm: { provider: 'anthropic', model: 'claude-sonnet-4-20250514', apiKey: { env: 'ANTHROPIC_API_KEY' } },
      availableWhen: { runtime: ['node'], deployment: ['standalone'] },
    })
    class LocalBuildAgent extends AgentContext { ... }
    ```
  </Tab>
</Tabs>

***

## Runtime Context API

Inside `execute()` methods, use runtime context helpers for imperative checks:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@Tool({
  name: 'system_command',
  inputSchema: { cmd: z.string() },
})
class SystemCommandTool extends ToolContext {
  async execute({ cmd }: { cmd: string }) {
    if (this.isPlatform('win32')) {
      // Windows-specific path
      return await this.runPowershell(cmd);
    }
    // Unix path
    return await this.runBash(cmd);
  }
}
```

### Available Methods

`RuntimeContext` exposes the operating system via the `os` property; `platform` remains as a deprecated alias for backward compatibility (issue #417) and resolves to the same value. New code should read `this.runtimeContext.os` directly.

| Method | Returns | Description |
| - | - | - |
| `this.runtimeContext.os` | `string` | Modern OS axis access — same values as `process.platform` (`'darwin'`, `'linux'`, `'win32'`, …) |
| `this.isPlatform('darwin')` | `boolean` | **Deprecated alias** — checks the legacy `.platform` field. Prefer `this.runtimeContext.os === 'darwin'`. |
| `this.isRuntime('node')` | `boolean` | Check JavaScript runtime |
| `this.isDeployment('serverless')` | `boolean` | Check deployment mode |
| `this.isEnv('production')` | `boolean` | Check NODE\_ENV |
| `this.runtimeContext` | `RuntimeContext` | Full context object (`os`, `platform`, `runtime`, `deployment`, `env`) |

These methods are available on `ToolContext`, `ResourceContext`, `PromptContext`, and `AgentContext`.

***

## Multi-Platform Pattern

When building tools that serve the same purpose across platforms, use separate files with `availableWhen` in each:

```
tools/
  notes/
    notes.darwin.tool.ts     # Apple Notes (macOS)
    notes.win32.tool.ts      # OneNote (Windows)
    notes.linux.tool.ts      # GNOME Notes (Linux)
    index.ts                 # Re-exports all variants
```

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
// tools/notes/notes.darwin.tool.ts
@Tool({
  name: 'notes_search',
  description: 'Search notes on this system',
  inputSchema: { query: z.string() },
  availableWhen: { os: ['darwin'] },
})
export class AppleNotesSearchTool extends ToolContext {
  async execute({ query }) { /* Apple Notes API */ }
}
```

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
// tools/notes/index.ts
export { AppleNotesSearchTool } from './notes.darwin.tool';
export { OneNoteSearchTool } from './notes.win32.tool';
export { GnomeNotesSearchTool } from './notes.linux.tool';
```

Register all variants — the SDK automatically exposes only the one matching the current platform:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
import { AppleNotesSearchTool, OneNoteSearchTool, GnomeNotesSearchTool } from './tools/notes';

@App({
  id: 'notes',
  tools: [AppleNotesSearchTool, OneNoteSearchTool, GnomeNotesSearchTool],
})
class NotesApp {}
```

<Tip>
  The multi-platform file pattern (`name.platform.tool.ts`) is recommended when you have platform-specific implementations of the same logical capability. For simple cases where a single tool needs a platform check, use `this.isPlatform()` inside `execute()` instead.
</Tip>

***

## Error Handling

When a client tries to call a tool that exists but is unavailable in the current environment, the SDK returns an `EntryUnavailableError` (HTTP 403) with both the constraint and the current context. The `data` payload includes a `missingAxes` array so clients can show a precise reason for the failure without parsing the message string:

```json theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
{
  "code": -32003,
  "message": "Tool \"apple_notes_search\" is not available in the current environment (missing axes: os) (requires: {\"os\":[\"darwin\"]}) (current: {\"os\":\"linux\",\"runtime\":\"node\",\"deployment\":\"standalone\",\"env\":\"production\"})",
  "data": {
    "entryType": "tool",
    "entry": "apple_notes_search",
    "missingAxes": ["os"],
    "constraint": { "os": ["darwin"] },
    "context": {
      "os": "linux",
      "runtime": "node",
      "deployment": "standalone",
      "env": "production"
    }
  }
}
```

This is distinct from a `ToolNotFoundError` (404), helping clients understand why a tool is inaccessible. See the [Structured errors](#structured-errors) reference (if present) for the full schema.

***

## How It Works: Registry-Level Filtering

<Warning>
  `availableWhen` is **not** the same as authorization or rule-based filtering. It is evaluated at the **registry level** during server boot, not in HTTP request flows.
</Warning>

| Concern | Layer | When | Scope |
| - | - | - | - |
| **`availableWhen`** | Registry (boot) | Server startup | Process-wide, immutable |
| **Authorization** | HTTP flow (request) | Per request | Per session/user |
| **Rule-based filtering** | HTTP flow (request) | Per request | Dynamic, policy-driven |
| **`hideFromDiscovery`** | Registry (listing) | Per list call | Soft hide (entry still callable) |

Key differences:

* `availableWhen` is a **hard constraint** — filtered entries cannot be listed OR called
* It runs at **registry initialization**, not in HTTP flows — no per-request overhead
* The runtime context (OS, runtime, deployment, NODE\_ENV) is detected once and cached
* Results are logged at boot time for operational visibility

### Boot-Time Logging

When entries have `availableWhen` constraints, the SDK logs a summary at startup:

```
[ToolRegistry] availability: 10 total, 5 with availableWhen constraint, 3 available, 2 filtered [ctx: platform=darwin, runtime=node, deployment=standalone, env=production]
[ToolRegistry] filtered: "windows_tool" — constraint={"platform":["win32"]}, current=platform=darwin
[ToolRegistry] filtered: "browser_tool" — constraint={"runtime":["browser"]}, current=runtime=node
```

Empty constraint arrays trigger a warning (likely a configuration bug):

```
[ToolRegistry] "broken_tool" has empty availableWhen arrays for [platform] — this entry will never be available.
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.