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

What Works in Browser

What’s Node-only

Bundling for the Browser

Use the packages from npm in a browser bundler that honors the browser export condition — Vite 7 and 8, build and dev server — without Node polyfills, process defines or an express alias. A bundler that resolves the browser condition gets @frontmcp/sdk’s browser build for an import and for a CommonJS require() alike (bundlers through the module condition; a CommonJS runtime with the browser condition, such as jest-environment-jsdom, gets a CommonJS copy of it), in which the Express host and the legacy SSE transport are stubs that throw if constructed, and the browser variants of @frontmcp/protocol (no stdio transports) and @frontmcp/utils (runtime context, paths, crypto, env). Node, and bundlers targeting Node, keep resolving the Node builds.

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()): 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. Runtime tools are the exception: their execute runs outside the turn.
  • Timers and promises a tool starts without awaiting run in whichever request holds the turn, so they must not read request context.
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.

Runtime Tools

server.registerTool() adds a tool to the server create() returned, while it runs. It is the way page code — a React component, a form, a store — gives agents something to do: the tool becomes a regular tool of the server’s app, listed by tools/list, run by the tools:call-tool flow (so the app’s plugin hooks, authorities, quota and availableWhen apply), and announced to connected clients with notifications/tools/list_changed. useDynamicTool is built on it.
execute runs outside the request’s turn, so it may call the server back (server.callTool(...)) or wait on the network without holding up other requests. A definition tools/list could not list — an inputSchema that is not an object schema, a title or description that is not a string, malformed annotations or availableWhen — rejects with EntryValidationError, so one bad tool never breaks the listing of the others. Registering on a server that is disposed (or is disposed before the registration completes) rejects with InternalMcpError.

Key Persistence in Browser

Key persistence auto-detects the best available backend: IndexedDB (preferred) → localStorage (fallback) → memory (last resort)

Storage Adapters in Browser


Agent LLM Adapters in Browser

Both built-in adapters work in browser environments since they use HTTP-based APIs:
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.

ESM Dynamic Loading in Browser

The App.esm() 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.
Browser-loaded ESM packages must avoid Node.js-only modules (fs, crypto, path) at the top level. Use dynamic imports for platform-specific code.

Crypto Operations

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