> ## 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.

# Browser Compatibility

> What works in the browser and what requires Node.js

FrontMCP supports browser environments for client-side use cases like JWT verification, key persistence, cryptographic operations, and agent LLM adapters.

## What Works in Browser

| Feature | Details |
| - | - |
| **Crypto operations** | SHA-256, AES-GCM, HKDF, PKCE via WebCrypto API |
| **JWT verification** | `rsaVerify` via WebCrypto, `decodeJwtPayloadSafe` |
| **Key persistence** | IndexedDB and localStorage backends |
| **Storage adapters** | `MemoryStorageAdapter`, `IndexedDBStorageAdapter`, `LocalStorageAdapter` |
| **Agent adapters** | `OpenAIAdapter`, `AnthropicAdapter` (HTTP-based, no Node dependencies) |
| **MCP client** | Direct client connections |
| **ESM Dynamic Loading** | In-memory cache mode, Blob URL module evaluation via `App.esm()` |

## What's Node-only

| Feature | Reason |
| - | - |
| **JWT signing** | `createSignedJwt`, `rsaSign`, `generateRsaKeyPair` require Node crypto |
| **File system** | `FileSystemStorageAdapter`, `fs` utilities |
| **Server infrastructure** | Express adapter, SSE/Streamable HTTP transport |
| **Redis/TCP storage** | `ioredis` requires TCP sockets |
| **CLI and Nx Plugin** | Node-only tooling |

## Request Context in the Browser

Every request runs in its own `FrontMcpContext` (auth info, session, request-scoped providers), and tools see the tool they run as and the surface they were called on. On Node and Workers, `AsyncLocalStorage` keeps these apart for overlapping requests. A browser has no `AsyncLocalStorage`, so the browser build of `@frontmcp/utils` picks one of two modes (`getAsyncContextMode()`):

| Mode | When | Behavior |
| - | - | - |
| `'native'` | The runtime provides TC39 `AsyncContext` (`AsyncContext.Variable`) | Requests overlap freely, as on Node. |
| `'serialized'` | Otherwise (today's browsers) | Requests take turns: `DirectMcpServer` calls, `connect()` clients and `createFetchHandler` requests run one at a time, so a request never reads another request's context. |

In `'serialized'` mode:

* A request keeps its turn until it returns **and** everything it started has unwound. Background jobs, workflows and tasks take a turn of their own after the request that started them.
* A request that waits on its client (an elicitation answer, a `roots/list` reply, the elicitation fallback) steps aside while it waits, so the client can call the server before answering.
* Concurrent calls **inside one request** (a tool that starts two tool calls with `Promise.all`) cannot be told apart once they overlap. They are refused with `AsyncContextOverlapError` instead of reading each other's context. Run such calls one after another.
* A workflow runs its ready steps **one at a time**, whatever its `maxConcurrency`: steps running at once would overlap in the workflow's turn. Each step runs once, and the workflow takes about the sum of its steps.
* A tool must not call its own server through a `DirectClient` or `DirectMcpServer`; it would wait for its own turn. Call other tools through `this.scope` flows instead. A request that waits more than 10 seconds for its turn logs a warning that says so.
* Timers and promises a tool starts without awaiting run in whichever request holds the turn, so they must not read request context.

<Note>
  Up to 1.8.3, the browser build shared one context stack between all requests, so a request that awaited could continue with another request's session, auth info and running tool.
</Note>

***

## Key Persistence in Browser

Key persistence auto-detects the best available backend:

**IndexedDB** (preferred) → **localStorage** (fallback) → **memory** (last resort)

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
import { createKeyPersistence } from '@frontmcp/utils';

// Auto-detect best backend
const store = createKeyPersistence();

// Explicit backend
const indexedDbStore = createKeyPersistence({ type: 'indexeddb' });
const localStorageStore = createKeyPersistence({ type: 'localstorage' });
```

***

## Storage Adapters in Browser

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
import { IndexedDBStorageAdapter, LocalStorageAdapter, MemoryStorageAdapter } from '@frontmcp/utils';

// IndexedDB — best for structured data, large values
const idb = new IndexedDBStorageAdapter({ dbName: 'my-app', storeName: 'state' });

// localStorage — simpler, synchronous-friendly
const ls = new LocalStorageAdapter({ prefix: 'my-app:' });

// Memory — ephemeral, works everywhere
const mem = new MemoryStorageAdapter();
```

***

## Agent LLM Adapters in Browser

Both built-in adapters work in browser environments since they use HTTP-based APIs:

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
import { OpenAIAdapter, AnthropicAdapter } from '@frontmcp/sdk';

// OpenAI — requires `npm install openai`
const openai = new OpenAIAdapter({
  model: 'gpt-4o',
  apiKey: 'sk-...',
});

// Anthropic — requires `npm install @anthropic-ai/sdk`
const anthropic = new AnthropicAdapter({
  model: 'claude-sonnet-4-20250514',
  apiKey: 'sk-ant-...',
});
```

<Warning>
  When using LLM adapters in the browser, API keys are exposed to the client. Use a backend proxy or token-scoped keys for production deployments.
</Warning>

***

## ESM Dynamic Loading in Browser

The [`App.esm()`](/frontmcp/servers/esm-packages) API works in browser environments with these differences:

* **Cache**: Memory-only (no disk persistence). Bundles are stored in an in-memory `Map`.
* **Module evaluation**: Bundles are evaluated via `Blob` + `URL.createObjectURL` instead of writing to the file system.
* **Same API**: No code changes needed — the environment is auto-detected.

<Warning>
  Browser-loaded ESM packages must avoid Node.js-only modules (`fs`, `crypto`, `path`) at the top level. Use dynamic imports for platform-specific code.
</Warning>

***

## Crypto Operations

All `@frontmcp/utils` crypto functions use the WebCrypto API in browser and `node:crypto` in Node.js:

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
import {
  sha256Hex,
  generateCodeVerifier,
  generateCodeChallenge,
  encryptAesGcm,
  decryptAesGcm,
  randomBytes,
  randomUUID,
  base64urlEncode,
  base64urlDecode,
} from '@frontmcp/utils';

// All of these work identically in both environments
const hash = await sha256Hex('data');
const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);
```


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