` authenticates with a pre-approved OAuth device code for non-interactive flows (web onboarding), skipping the browser step (`#1691`)
* **Fix**: `McpUseProvider` with `autoSize` now notifies zero height when a widget renders `null`, so host iframes collapse instead of keeping the last non-zero height (`#1693`)
* **Fix** (react): `useMcp` stale `disconnect()` after a URL change or a manual disconnect racing a reconnect no longer nulls `clientRef` or resets hook state to `discovering` when superseded by a newer `connect()` (`connectEpochRef`) — fixes "MCP client is not ready (current state: ready)" and similar races in the inspector and other embeds (`#1528`)
* **Fix** (client): `BaseMCPClient.closeSession()` only deletes a session slot when it still references the closing session, so a parallel `createSession()` from a reconnect is not wiped — fixes "No active session found" on the next tool call after reconnect (`#1528`)
* **Enhancement** (react): clearer "MCP client is not ready" errors via `formatMcpNotReadyReason`, distinguishing a missing client (`client disconnected`) from a non-ready state (`#1528`)
* **Fix** (@mcp-use/inspector): MCP Apps / Apps SDK status labels no longer intercept iframe pointer events - see inspector changelog (`#1694`, `#1678`)
* **Documentation**: tip on ` ` for widgets that conditionally return `null` with `autoSize`
* **Documentation**: `deploy --no-github`, `login --device-code`, and dual deploy paths in the CLI reference and Manufact Cloud deployment guide
* **Update**: Updated @mcp-use/inspector to v8.0.1
* **Update**: Updated @mcp-use/cli to v3.4.0
` authenticates with a pre-approved OAuth device code for non-interactive flows (web onboarding), skipping the browser step (`#1691`)
* **Fix**: `McpUseProvider` with `autoSize` now notifies zero height when a widget renders `null`, so host iframes collapse instead of keeping the last non-zero height (`#1693`)
* **Fix** (react): `useMcp` stale `disconnect()` after a URL change or a manual disconnect racing a reconnect no longer nulls `clientRef` or resets hook state to `discovering` when superseded by a newer `connect()` (`connectEpochRef`) — fixes "MCP client is not ready (current state: ready)" and similar races in the inspector and other embeds (`#1528`)
* **Fix** (client): `BaseMCPClient.closeSession()` only deletes a session slot when it still references the closing session, so a parallel `createSession()` from a reconnect is not wiped — fixes "No active session found" on the next tool call after reconnect (`#1528`)
* **Enhancement** (react): clearer "MCP client is not ready" errors via `formatMcpNotReadyReason`, distinguishing a missing client (`client disconnected`) from a non-ready state (`#1528`)
* **Fix** (@mcp-use/inspector): MCP Apps / Apps SDK status labels no longer intercept iframe pointer events - see inspector changelog (`#1694`, `#1678`)
* **Documentation**: tip on ` ` for widgets that conditionally return `null` with `autoSize`
* **Documentation**: `deploy --no-github`, `login --device-code`, and dual deploy paths in the CLI reference and Manufact Cloud deployment guide
* **Update**: Updated @mcp-use/inspector to v8.0.1
* **Update**: Updated @mcp-use/cli to v3.4.0
` | Redeem a short-lived, pre-approved RFC 8628 device code. | None |
| `--org ` | Select the active organization by ID or slug. | Account default |
| `--no-open` | Do not open the verification URL during interactive login. | Opens on a TTY |
| `--json` | Emit one machine-readable JSON result. | Human-readable text |
**Examples:**
```bash theme={null}
mcp-use login
mcp-use login --device-code ABC123
mcp-use login --api-key mcp_... --org my-org
```
**Notes and behavior:**
* Interactive login starts a device-code flow and opens a browser.
* `--device-code` skips the browser and redeems the code through the same token endpoint. The CLI then creates, validates, and stores a persistent API key.
* `--api-key` and `--device-code` cannot be combined. An explicit `--device-code` takes precedence over `MCP_USE_API_KEY`.
* The CLI never writes the device code, bearer token, or API key to human-readable or JSON output.
* If `--org` is omitted, the CLI stores the account-default organization when one is available.
* `MCP_USE_API_KEY` can provide the API key for non-interactive login.
### `whoami`
**Purpose:** Print the current authentication status and user information.
**Syntax:**
```bash theme={null}
mcp-use whoami
```
**Options:**
| Option | Description | Default |
| - | - | - |
| `--json` | Emit a machine-readable identity object. | Human-readable text |
**Examples:**
```bash theme={null}
mcp-use whoami
```
**Notes and behavior:** The command reads `~/.mcp-use/config.json`. When that file selects no organization, it reports the account default, matching `org current` and the `servers`/`deployments` commands. This is the usual case when authenticating with `MCP_USE_API_KEY` instead of `mcp-use login`.
### `logout`
**Purpose:** Remove saved Manufact Cloud credentials.
**Syntax:**
```bash theme={null}
mcp-use logout [options]
```
**Options:**
| Option | Description | Default |
| - | - | - |
| `--yes` | Skip confirmation. Required for non-interactive and JSON runs. | Prompts on a TTY |
| `--json` | Emit one machine-readable JSON result. | Human-readable text |
**Examples:**
```bash theme={null}
mcp-use logout --yes
```
**Notes and behavior:** The command removes credentials from `~/.mcp-use/config.json`.
## Organization commands
Organization commands control the active Manufact Cloud organization for cloud commands.
| Command | Purpose | Syntax |
| - | - | - |
| `org list` | List organizations available to the authenticated user. | `mcp-use org list` |
| `org current` | Show the active organization. | `mcp-use org current` |
| `org use ` | Select and persist a different active organization. | `mcp-use org use ` |
**Notes and behavior:**
* `deploy`, `deployments`, and `servers` use the active organization unless `--org` overrides it.
* For CI or agent runs, pass `--org ` to `login`, `deploy`, `servers`, or `deployments` commands instead of switching global state. The value can be an organization slug, ID, or name.
## Deployment management commands
**Purpose:** Inspect and manage existing Manufact Cloud deployments.
**Syntax:**
```bash theme={null}
mcp-use deployments [options]
```
**Subcommands:**
| Subcommand | Purpose | Options |
| - | - | - |
| `list` | List deployments. | `--org `, `--server `, `--limit `, `--skip `, `--json` |
| `get ` | Show deployment details. | `--json` |
| `logs ` | View runtime logs. | `--build`, `--follow`, `--json` |
| `restart ` | Trigger a new deployment on the same server. | `--follow`, `--branch `, `--json` |
| `stop ` | Stop a running deployment. | `--yes`, `--json` |
| `delete ` | Delete a deployment. | `--yes`, `--json` |
**Examples:**
```bash theme={null}
mcp-use deployments list
mcp-use deployments get dep_abc123
mcp-use deployments logs dep_abc123 --build --follow
mcp-use deployments restart dep_abc123 --branch main --follow
mcp-use deployments stop dep_abc123 --yes
mcp-use deployments delete dep_abc123 --yes
```
**Notes and behavior:**
* `logs` shows runtime logs by default. Use `--build` for build logs.
* `restart` redeploys the same server and uses the deployment branch unless `--branch` is set.
* `stop` and `delete` prompt for confirmation on a TTY. Pass `--yes` in non-interactive or JSON runs.
## Server management commands
**Purpose:** Manage cloud servers, which are long-lived Git-backed deploy targets that own deployments.
**Syntax:**
```bash theme={null}
mcp-use servers [options]
```
**Subcommands:**
| Subcommand | Purpose | Options |
| - | - | - |
| `list` | List servers for an organization. | `--org `, `--limit `, `--skip ` |
| `get ` | Show server details and recent deployments. | `--org ` |
| `update ` | Update server configuration. | `--branch `, `--name `, `--description `, `--build-command `, `--start-command `, `--root-dir `, `--watch-paths `, `--deploy-branches `, `--wait-for-ci`, `--no-wait-for-ci`, `--org ` |
| `delete ` | Delete a server and its deployments. | `--yes`, `--org ` |
| `env ` | Manage server environment variables. | See [Server environment commands](#server-environment-commands). |
**Examples:**
```bash theme={null}
mcp-use servers list
mcp-use servers get srv_abc123
mcp-use servers update srv_abc123 --branch main
mcp-use servers update srv_abc123 --build-command "npm run build"
mcp-use servers update srv_abc123 --watch-paths "apps/api/**" --watch-paths "packages/shared/**" --deploy-branches "release/*"
mcp-use servers delete srv_abc123 --yes
```
**Notes and behavior:**
* `servers update --branch` changes the production branch for future production deploys.
* `--watch-paths` and `--deploy-branches` are repeatable. Pass the flag once per pattern; a second bare value is read as a positional, not as another pattern.
* Pass an empty string to `--build-command`, `--start-command`, `--root-dir`, `--watch-paths`, or `--deploy-branches` to clear that setting.
* `--wait-for-ci` and `--no-wait-for-ci` control whether auto-deploys wait for other check runs before deploying.
* `servers delete` prompts for confirmation unless `--yes` is set.
## Server environment commands
**Purpose:** List, set, and unset environment variables on a cloud server.
**Syntax:**
```bash theme={null}
mcp-use servers env [options]
```
**Subcommands:**
| Subcommand | Purpose | Required arguments | Other options |
| - | - | - | - |
| `list ` | List environment variable metadata. | Server ID or slug | `--org `, `--branch ` |
| `set ` | Create or update an environment variable. | Server ID or slug, value | `--org `, `--branch `, `--secret` |
| `unset ` | Delete an environment variable by key. | Server ID or slug, key | `--org `, `--branch `, `--yes` |
**Examples:**
```bash theme={null}
mcp-use servers env list srv_abc123
mcp-use servers env list srv_abc123 --branch feature/example
mcp-use servers env set srv_abc123 API_KEY=secret --secret
mcp-use servers env set srv_abc123 FEATURE_FLAG=true --branch feature/example
mcp-use servers env unset srv_abc123 API_KEY --yes
```
**Notes and behavior:**
* `env list` returns metadata only. Values are never returned.
* `env set` creates a production variable when `--branch` is omitted, or a preview variable when it is supplied.
* `--secret` stores a value as write-only. It must be passed on **every** write to the variable, including updates and rotations: omitting it stores the value as non-sensitive, even if the variable was previously write-only.
* `--branch ` scopes preview environment variables to a branch. Omit `--branch` for production resolution.
* `env unset` prompts for confirmation unless `--yes` is set. If the key doesn't exist in the selected production or branch scope, it fails with `env_variable_not_found`.
## `client`
**Purpose:** Save HTTP MCP server connections, call tools, read resources, get prompts, and manage OAuth credentials from the terminal.
**Syntax:**
```bash theme={null}
mcp-use client
mcp-use client
```
**Top-level subcommands:**
| Command | Purpose | Key options |
| - | - | - |
| `connect ` | Verify and save an HTTP(S) MCP server. | `-H, --header`, `--no-oauth`, `--auth-timeout `, `--protocol `, `--no-open`, `--json` |
| `list` | List saved servers. | `--json` |
| `remove ` | Remove a saved server and credentials. | `--json` |
**Per-server scopes:**
| Scope | Actions |
| - | - |
| `tools` | `list`, `describe `, `call [args...] [--timeout ]` |
| `resources` | `list`, `read ` |
| `prompts` | `list`, `get [args...]` |
| `auth` | `login`, `status`, `logout [--yes]` |
Every action in this table accepts `--json` except where the top-level table does not advertise it.
**Examples:**
```bash theme={null}
mcp-use client connect local http://localhost:3000/mcp
mcp-use client list
mcp-use client local tools list
mcp-use client local tools call read_file path=/tmp/test.txt
mcp-use client local resources read "file:///tmp/data.json"
mcp-use client local prompts get greeting name=Alice
mcp-use client local auth login
mcp-use client local auth status
```
**Notes and behavior:**
* Saved server metadata is stored in `~/.mcp-use/client/servers.json`. Credentials are stored separately under `~/.mcp-use/client/credentials/`.
* In an interactive TTY, OAuth displays `This server requires OAuth. Press Enter to open your browser.` and opens the browser only after Enter.
* When protected-resource metadata identifies mixed auth, `connect` succeeds anonymously, reports that public tools are available, and suggests `mcp-use client auth login`. The JSON connection result includes `authorization` metadata.
* `auth login` starts OAuth for an already-saved server; otherwise the first wire-level OAuth challenge can start the flow later and the CLI retries that operation once.
* `--no-open` and non-TTY human output never prompt or open a browser; they print a state-free local launcher URL while the loopback callback continues waiting. The local route redirects to the provider without exposing OAuth state in terminal output.
* `--json` never opens a browser or prints an OAuth URL or state. If fresh consent is required, it exits `1` with `oauth_interaction_required` and an interactive retry command in `error.details.nextSteps`.
* `connect --no-oauth` prevents automatic OAuth when the server returns `401`.
* `--protocol auto` prefers the modern wire and falls back to legacy. `legacy`
selects only the legacy wire. `modern` selects the stateless, sessionless
modern wire with no fallback.
* Repeated `-H, --header <"Key: Value">` options store static headers as credentials.
* Tool and prompt arguments accept `key=value`, `key:=`, or a single JSON object argument.
* `--json` may appear anywhere after `client`. Success emits exactly one JSON value to stdout. Errors emit exactly one JSON error envelope to stderr and no stdout value.
* Calls, resource reads, and prompt gets emit raw MCP result envelopes. Lists emit arrays, and `tools describe` emits one tool object.
* Tool `isError` results exit `1` and retain the original protocol result in `error.details` under `--json`; they and auth-like error text are not interpreted as OAuth challenges.
Use the guide for connection workflows, OAuth behavior, argument parsing
examples, and scripting patterns.
## `screenshot`
**Purpose:** Call a view-backed MCP tool and capture its rendered MCP App as a PNG.
**Syntax:**
```bash theme={null}
mcp-use screenshot (--server | --mcp ) --tool [args...] [options]
```
Pass tool arguments after the options, either as one JSON object or as
`key=value` / `key:=` pairs.
**Source options:**
| Option | Description |
| - | - |
| `--server ` | Use a server saved by `mcp-use client`. |
| `--mcp ` | Connect directly to an HTTP(S) MCP endpoint. |
| `-H, --header <"Key: Value">` | Header for `--mcp`. Repeatable, and incompatible with `--server`. |
**Capture options:**
| Option | Description | Default |
| - | - | - |
| `--tool ` | View-backed tool to call. Required. | None |
| `--output ` | Output PNG path. | Timestamped view name |
| `--width ` | Host and widget width, an integer from `1` to `10000000`. | `768` |
| `--height ` | Host viewport height, an integer from `1` to `10000000`. The PNG is cropped to the widget bounds. | `720` |
| `--device-scale-factor ` | Pixel density, greater than 0 and at most 4. | `1` |
| `--theme ` | Host theme. | `light` |
| `--wait-for ` | Wait for a selector before capture. | None |
| `--delay ` | Additional delay after readiness. | `0` |
| `--timeout ` | Tool, browser, and readiness timeout. | `30000` |
| `--inspector ` | Use an existing Inspector origin. | Starts one |
| `--cdp-url ` | Use an existing Chrome DevTools endpoint. | Launches Chrome |
| `--json` | Emit one result or error and never prompt. | Off |
**Examples:**
```bash theme={null}
mcp-use screenshot --server demo --tool show-app appName=Demo
mcp-use screenshot --mcp https://example.com/mcp --tool show-app \
'{"appName":"CI"}' --theme dark --output app.png --json
```
**Exit codes:**
| Code | Meaning |
| - | - |
| `0` | Capture succeeded, or help was requested. |
| `2` | Invalid arguments. |
| `1` | MCP, tool, Inspector, browser, readiness, or write failure. |
## Environment variables
| Variable | Used by | Description | Default |
| - | - | - | - |
| `PORT` | `dev`, `start`, server runtime | Server port when `--port` is not provided. `dev` sets it to the resolved listener port before importing the entry; `start` reads it before importing the built entry. | `3000` |
| `HOST` | `dev`, `start`, server runtime | Bind host when `--host` is not provided. `start` and direct `listen()` fall back to `config.host` before `127.0.0.1`. | `127.0.0.1` |
| `NODE_ENV` | `dev`, `build`, `start` | Runtime environment. When it is not already set, `dev` uses `development`, and `build` and `start` set `production` before loading the project `.env`. | Command-specific |
| `MCP_URL` | server runtime, `dev`, `build` | **Server public origin**, such as `https://mcp.example.com`. OAuth resource URL, CSP `connectDomains`, dev HMR websocket. When OAuth derives its resource from `MCP_URL` (the provider sets no `resource`), a value with a path, query, or credentials makes the server constructor throw; views and CSP use only its origin. Lower priority than `CSP_URLS`. `dev` sets it only while importing the entry on loopback binds, `build` sets `http://localhost:3000` only while inspecting the entry, and `start` never sets it. | None (request origin) |
| `MCP_ASSETS_URL` | `build`, server runtime | **Assets URL prefix** (origin + optional path) for view JS/CSS and `public/`. Falls back to `MCP_URL` origin. When set at build, manifest paths become full CDN URLs (path segment from server `basePath`). | None (falls back to server origin) |
| `CSP_URLS` | server runtime | Comma-separated CSP domain shortcut — applied to all four MCP Apps categories when per-category vars are unset. Higher priority than `MCP_URL` auto-append. | None |
| `CSP_CONNECT_DOMAINS` | server runtime | Overrides `CSP_URLS` for `connectDomains` | None |
| `CSP_RESOURCE_DOMAINS` | server runtime | Overrides `CSP_URLS` for `resourceDomains` | None |
| `CSP_FRAME_DOMAINS` | server runtime | Overrides `CSP_URLS` for `frameDomains` | None |
| `CSP_BASE_URI_DOMAINS` | server runtime | Overrides `CSP_URLS` for `baseUriDomains` | None |
| `MCP_USE_CLOUD_WEB_URL` | `deploy` | Manufact web URL for dashboard and GitHub settings links. | `https://manufact.com` |
| `MCP_USE_CLOUD_API_URL` | `login` and all cloud commands | Manufact API URL. Takes precedence over `MCP_API_URL`. The CLI normalizes it to include `/api/v1`. | `https://cloud.manufact.com/api/v1` |
| `MCP_API_URL` | `login` and all cloud commands | Fallback Manufact API URL when `MCP_USE_CLOUD_API_URL` is unset. | `https://cloud.manufact.com/api/v1` |
| `MCP_USE_API_KEY` | `login` and all cloud commands | API key for non-interactive login. Cloud commands (`deploy`, `servers`, `deployments`, `org`, `whoami`) use it directly and prefer it over the saved credential. | None |
| `MCP_USE_WS_RELAY` | `dev --tunnel`, `start --tunnel` | WebSocket relay API origin used when creating authenticated local tunnels. | `https://api.tunnel.mcp-use.run` |
| `MCP_USE_CHROME_PATH` | `screenshot` | Chrome executable path for screenshots. | Auto-detected |
| `PUPPETEER_EXECUTABLE_PATH` | `screenshot` | Fallback Chrome executable path. | Auto-detected |
| `CHROME_PATH` | `screenshot` | Fallback Chrome executable path. | Auto-detected |
**Examples:**
```bash theme={null}
PORT=8080 mcp-use start
MCP_URL=https://my-server.example.com MCP_ASSETS_URL=https://cdn.example.com/bucket mcp-use build
MCP_USE_API_KEY=mcp_... mcp-use login --org my-org
MCP_USE_CHROME_PATH=/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
mcp-use screenshot --server local --tool show-board
```
## Related references
* [CLI Client guide](/v2/typescript/tooling/client-cli)
* [Tunneling guide](/tunneling/index)
* [Manufact Cloud deployment guide](/v2/typescript/server/deployment/mcp-use)
* [ServerConfig API reference](https://mcp-use-typescript-api-reference.vercel.app/modules/mcp-use.index.html#serverconfig)
# Authentication
Source: https://docs.mcp-use.com/v2/typescript/client/authentication
OAuth and bearer tokens for MCP client connections
HTTP connections support automatic OAuth, pre-registered public clients,
bearer tokens, custom headers, and custom authentication providers.
## Automatic OAuth with `MCPClient`
`MCPClient` creates an OAuth provider for an HTTP server unless the server
configuration already supplies an `authProvider`, `authToken`, `oauth: false`,
or an `Authorization` header.
```typescript theme={null}
import { MCPClient } from "@mcp-use/client";
const client = new MCPClient({
mcpServers: {
demo: { url: "https://api.example.com/mcp" },
},
});
try {
await client.connect("demo");
} finally {
await client.close();
}
```
In Node.js, the default provider opens a browser and waits for its loopback
callback. In the browser build, the default provider opens a popup; set
`useRedirectFlow: true` inside the server's `oauth` options to use a full-page
redirect.
Pass a custom `authProvider` when your application owns the authorization flow.
## Whole-server and mixed OAuth
Whole-server OAuth rejects the MCP connection until the user authorizes. A
mixed-auth server instead allows anonymous connection and public operations,
then requires OAuth only for protected tools, resources, or prompts.
When a successfully connected server publishes
[RFC 9728 protected-resource metadata](https://www.rfc-editor.org/rfc/rfc9728.html),
`MCPClient` exposes it as mixed auth without forcing authorization:
```typescript theme={null}
const connection = await client.connect("demo");
const authorization = await connection.discoverAuthorization();
if (authorization?.mode === "mixed" && !authorization.authenticated) {
console.log("Public operations are available without signing in.");
// Optional: authenticate now instead of waiting for a protected operation.
await connection.authenticate();
}
```
After discovery, `connection.authorization` and `connection.info.authorization`
expose the same state. It includes `resource` and `scopesSupported` when the
protected-resource metadata advertises them. Mixed-auth discovery is
best-effort and defaults to enabled for HTTP servers with an OAuth provider.
Call `discoverAuthorization()` when using `MCPClient` directly; React performs
the same discovery after publishing the ready connection. Set `detectMixedAuth:
false` on the server configuration to skip the discovery request.
If the client stays anonymous, a later wire-level OAuth challenge from a
protected operation starts the official SDK flow. Automatic flows finish OAuth
through the active transport and retry the logical operation once. Explicit
browser flows surface the prepared authentication action so the host can
authenticate and retry. MCP tool results with `isError: true` or auth-like text
are application errors, not OAuth challenges.
## React: explicit authentication
React connection APIs use an explicit authorization step by default.
`preventAutoAuth` defaults to `true`, so an unauthorized connection enters
`pending_auth` until the user calls `authenticate()`.
```tsx theme={null}
import { useMcp } from "@mcp-use/client/react";
export function ProtectedConnection() {
const mcp = useMcp({
url: "https://api.example.com/mcp",
});
if (mcp.state === "pending_auth") {
return ;
}
if (
mcp.authorization?.mode === "mixed" &&
!mcp.authorization.authenticated
) {
return (
<>
Public tools are available. Sign in to use protected tools.
>
);
}
return Connection state: {mcp.state}
;
}
```
Set `preventAutoAuth: false` on `useMcp` or a
`McpClientProvider` server configuration to start authorization automatically.
Popup mode is the default; set the top-level React option
`useRedirectFlow: true` for a full-page redirect.
On your callback route, import `onMcpAuthorization` from
`@mcp-use/client/react`. See
[React integration](/v2/typescript/client/usemcp#oauth-callback).
To show the authorized account, set `oauth.fetchUserInfo: true` and read
`mcp.userInfo`. Its status is `idle`, `loading`, `available`, `unavailable`, or
`error`; claims such as `email` are present only when released by the
authorization server. You can also fetch explicitly:
```tsx theme={null}
const result = await mcp.getUserInfo();
if (result.status === "available") console.log(result.claims.email);
```
See [OAuth UserInfo](/v2/typescript/client/usemcp#oauth-userinfo) for the
options, state details, and a React example.
## Pre-registered clients
The `MCPClient` configuration uses
`oauth.staticClientInfo.client_id`:
```typescript theme={null}
import { MCPClient } from "@mcp-use/client";
const client = new MCPClient({
mcpServers: {
slack: {
url: "https://mcp.example.com/mcp",
oauth: {
staticClientInfo: {
client_id: "my-client-id",
},
clientMetadataUrl:
"https://app.example.com/.well-known/oauth-client-metadata.json",
scope: "openid profile",
},
},
},
});
```
React uses a different, UI-oriented shape:
```tsx theme={null}
import { useMcp } from "@mcp-use/client/react";
const mcp = useMcp({
url: "https://mcp.example.com/mcp",
oauth: {
clientId: "my-client-id",
clientMetadataUrl:
"https://app.example.com/.well-known/oauth-client-metadata.json",
scope: "openid profile",
},
});
```
Browser OAuth clients are public PKCE clients. Do not put a `client_secret` in
browser configuration; `BrowserOAuthClientProvider` rejects one in
`staticClientInfo`.
## OAuth proxy in the browser
Use an OAuth proxy when the authorization server's metadata, token, or
registration endpoints do not support browser CORS:
```tsx theme={null}
import { useMcp } from "@mcp-use/client/react";
const mcp = useMcp({
url: "https://mcp.example.com/mcp",
oauthProxyUrl: "https://app.example.com/api/mcp-oauth",
});
```
For the browser `MCPClient`, `oauth.oauthProxyUrl` configures the proxy and
`oauth.proxyOAuthRequests` controls whether OAuth HTTP requests use it. The
latter defaults to `true` when the browser provider is created. React routes its
OAuth requests through the configured `oauthProxyUrl`; it does not expose
`proxyOAuthRequests` as a `useMcp` option.
## Manual browser authorization
Construct a `BrowserOAuthClientProvider` with `preventAutoAuth: true` and pass it
to the browser client when you need to present the authorization URL yourself.
Set `useRedirectFlow: true` when you navigate the current page to that URL:
```typescript theme={null}
import {
BrowserOAuthClientProvider,
isUnauthorized,
MCPClient,
} from "@mcp-use/client";
const authProvider = new BrowserOAuthClientProvider(
"https://api.example.com/mcp",
{ preventAutoAuth: true, useRedirectFlow: true }
);
const client = new MCPClient({
mcpServers: {
api: {
url: "https://api.example.com/mcp",
authProvider,
},
},
});
try {
await client.connect("api");
} catch (error) {
if (!isUnauthorized(error)) throw error;
const authorizationUrl = authProvider.getLastAttemptedAuthUrl();
if (authorizationUrl) {
window.location.assign(authorizationUrl);
}
}
```
`getLastAttemptedAuthUrl()` returns the URL for the current provider instance.
It is kept in memory only, so start a new authorization attempt after a page
reload. The auto-provisioned browser provider also accepts
`oauth.preventAutoAuth`, but an explicit provider gives your application access
to the prepared URL.
The provider records the flow type when it prepares the URL. With
`useRedirectFlow: true`, the callback page that calls `onMcpAuthorization()`
returns to the page that started authorization. Without it, the callback treats
the navigation as a popup and leaves the user on the callback page.
## Bearer tokens
```typescript theme={null}
import { MCPClient } from "@mcp-use/client";
const client = new MCPClient({
mcpServers: {
api: {
url: "https://api.example.com/mcp",
authToken: process.env.API_KEY,
},
},
});
```
An explicit `Authorization` header also disables automatic OAuth:
```typescript theme={null}
import { MCPClient } from "@mcp-use/client";
const client = new MCPClient({
mcpServers: {
api: {
url: "https://api.example.com/mcp",
headers: {
Authorization: `Bearer ${process.env.API_KEY}`,
},
},
},
});
```
## `MCPClient` server fields
| Field | Description |
| - | - |
| `authToken` | Bearer token |
| `headers` | Custom HTTP headers |
| `oauth` | Automatic OAuth options, or `false` to disable automatic OAuth |
| `authProvider` | Custom SDK-compatible provider |
| `detectMixedAuth` | Discover optional mixed OAuth after anonymous connection; defaults to `true` |
## Node OAuth helpers
```typescript theme={null}
import {
createOAuthProvider,
NodeOAuthClientProvider,
completeOAuthFlow,
isUnauthorized,
FileKVStore,
} from "@mcp-use/client";
```
Use these exports for headless scripts, custom storage, or an authorization flow
you manage directly.
# Code Mode
Source: https://docs.mcp-use.com/v2/typescript/client/code-mode
Let agents write code to call MCP tools
Code mode exposes `execute_code` and `search_tools` so agents run JavaScript with access to connected MCP tools instead of making individual tool calls. Node.js only.
## Enable
```typescript theme={null}
import { MCPClient } from "@mcp-use/client";
const client = new MCPClient(
{
mcpServers: {
fs: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "./data"],
},
},
},
{ codeMode: true },
);
const result = await client.executeCode(`
const files = await fs.list_directory({ path: "." });
return files.length;
`);
console.log(result.result, result.logs, result.error);
await client.close();
```
## Executors
| Executor | Use case |
| - | - |
| `"vm"` (default) | Local Node.js VM — fast, trusted environments |
| `"e2b"` | Cloud sandbox — untrusted code (`@e2b/code-interpreter` + API key) |
| Custom function / `BaseCodeExecutor` | Your own runtime |
```typescript theme={null}
{ codeMode: {
enabled: true,
executor: "e2b",
executorOptions: { apiKey: process.env.E2B_API_KEY!, timeoutMs: 300_000 },
}}
```
A custom function receives `(code, timeout?)` and returns an `ExecutionResult`.
It owns cleanup for whatever runtime it wraps — `client.close()` does not
release anything on its behalf. A non-arrow function is invoked with the
`MCPClient` as its `this`, so it can reach the client without closing over it:
```typescript theme={null}
{ codeMode: {
enabled: true,
executor: async function (this: MCPClient, code, timeout) {
const servers = this.getServerNames();
return {
result: await myRuntime.run(code, timeout, servers),
logs: [],
error: null,
execution_time: 0,
};
},
}}
```
## Agent integration
Use with `@mcp-use/agent` and `PROMPTS.CODE_MODE`:
```typescript theme={null}
import { MCPAgent, PROMPTS } from "@mcp-use/agent";
const agent = new MCPAgent({ llm, client, systemPrompt: PROMPTS.CODE_MODE });
await agent.run("List all files and count them");
```
## Reference
[Client API reference](https://mcp-use-typescript-api-reference.vercel.app/modules/_mcp-use_client.index.html) — config types, `searchTools`, and `ExecutionResult`.
[Anthropic: Code execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp)
# Completions
Source: https://docs.mcp-use.com/v2/typescript/client/completion
Autocomplete for prompt and resource template arguments
Request suggestions for prompt arguments or resource template URI variables:
```typescript theme={null}
import { MCPClient } from "@mcp-use/client";
const client = new MCPClient({
mcpServers: { demo: { url: "http://localhost:3000/mcp" } },
});
try {
const conn = await client.connect("demo");
const result = await conn.complete({
ref: { type: "ref/prompt", name: "code-review" },
argument: { name: "language", value: "py" },
});
console.log(result.completion.values); // e.g. ["python", "pytorch"]
} finally {
await client.close();
}
```
## React
```tsx theme={null}
const mcp = useMcp({ url: "http://localhost:3000/mcp" });
const result = await mcp.complete({
ref: { type: "ref/resource", uri: "file:///{path}" },
argument: { name: "path", value: "/tmp" },
});
```
## Reference
[`MCPConnection`](https://mcp-use-typescript-api-reference.vercel.app/modules/_mcp-use_client.index.html#mcpconnection) — `complete()`.
# Elicitation
Source: https://docs.mcp-use.com/v2/typescript/client/elicitation
Handle interactive user input requests from MCP servers
Servers can ask the client for user input mid-tool-call via `elicitation/create`. Two modes:
* **Form** — structured fields with a JSON schema
* **URL** — redirect the user to an external page (required for credentials/OAuth)
## Setup
```typescript theme={null}
import { stdin, stdout } from "node:process";
import { createInterface } from "node:readline/promises";
import {
acceptWithDefaults,
MCPClient,
type OnElicitationCallback,
} from "@mcp-use/client";
const onElicitation: OnElicitationCallback = async (params) => {
if (params.mode === "url") {
// The tool call continues as soon as this returns, so accept only after
// the user has finished the external flow.
console.log(`Visit: ${params.url}`);
const prompt = createInterface({ input: stdin, output: stdout });
await prompt.question("Press Enter after you finish in the browser. ");
prompt.close();
return { action: "accept" };
}
// Form mode — collect fields from params.requestedSchema
return acceptWithDefaults(params);
};
const client = new MCPClient(
{ mcpServers: { demo: { url: "http://localhost:3000/mcp" } } },
{ onElicitation },
);
```
The second constructor argument is available only in the Node.js entry. In a
browser build, set `onElicitation` at the top level of the configuration or on
an individual server configuration instead.
## Helpers
These helpers are exported only by the Node.js entry of `@mcp-use/client`. The
browser entry and `@mcp-use/client/react` do not export them.
| Helper | Purpose |
| - | - |
| `acceptWithDefaults(params)` | Accept using schema defaults |
| `accept(content)` | Accept with user content |
| `decline()`, `cancel()` | Reject or dismiss |
| `validate(params, content)` | Client-side validation |
| `getDefaults`, `applyDefaults` | Work with schema defaults |
## React
The helpers above are not available in a browser build, so handle the request
directly. This handler waits for the user to finish a URL request and accepts
form requests with their schema defaults:
```tsx theme={null}
import { useMcp, type ElicitResult } from "@mcp-use/client/react";
const mcp = useMcp({
url: "http://localhost:3000/mcp",
onElicitation: async (params): Promise => {
if (params.mode === "url") {
window.open(params.url, "_blank", "noopener");
// The tool call continues as soon as this resolves, so accept only
// after the user confirms they finished the external flow.
const finished = window.confirm(
`${params.message}\n\nSelect OK after you finish in the new tab.`
);
return { action: finished ? "accept" : "cancel" };
}
const content: NonNullable = {};
for (const [name, field] of Object.entries(
params.requestedSchema.properties
)) {
if (field.default !== undefined) content[name] = field.default;
}
return { action: "accept", content };
},
});
```
With the provider, use `pendingElicitationRequests` and `approveElicitation` / `rejectElicitation`.
Without an `onElicitation` handler, tool calls that require input will fail.
## Reference
Server-side: [Elicitation](/v2/typescript/server/elicitation)
# Environments
Source: https://docs.mcp-use.com/v2/typescript/client/environments
Use the MCP client in Node.js, browser, and React
The root `@mcp-use/client` import uses package conditions to select its runtime
implementation. Node gets the Node entry; browsers and other default
environments get the browser-safe entry. React connection management is
available from a separate entry point.
| Environment | Import | HTTP | Stdio | Config files | Code Mode |
| - | - | - | - | - | - |
| Node.js `>=22.22.2` | `@mcp-use/client` | Yes | Yes | Yes | Yes |
| Browser | `@mcp-use/client` | Yes | No | No | No |
| React | `@mcp-use/client/react` | Yes | No | No | No |
## Node.js
The Node entry includes HTTP and stdio transports, filesystem configuration,
Code Mode, and Node OAuth helpers.
```typescript theme={null}
import { MCPClient, loadConfigFile } from "@mcp-use/client";
const client = new MCPClient({
mcpServers: {
api: { url: "https://api.example.com/mcp" },
fs: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
},
},
});
const clientFromFile = new MCPClient(loadConfigFile("./mcp-config.json"));
try {
const connections = await client.connectAll();
await connections.api.callTool("ping", {});
} finally {
await Promise.all([client.close(), clientFromFile.close()]);
}
```
## Browser
The browser entry exposes the HTTP connection API and browser OAuth support. It
does not include process spawning, filesystem configuration, or local code
execution.
```typescript theme={null}
import { MCPClient } from "@mcp-use/client";
const client = new MCPClient({
mcpServers: {
api: { url: "https://api.example.com/mcp" },
},
});
try {
const connection = await client.connect("api");
await connection.callTool("tool", {});
} finally {
await client.close();
}
```
Do not hardcode secrets in browser bundles. Use OAuth or a backend proxy.
## React
Install the client and your application's supported React version:
```bash theme={null}
npm install @mcp-use/client react
```
`@mcp-use/client/react` provides connection-management APIs such as `useMcp`,
`McpClientProvider`, `useMcpClient`, and `useMcpServer`. It also exports
`ViewRenderer` for hosts that render an MCP App View.
Code running inside a View uses the View APIs from `mcp-use/react`, including
`useToolContext`, `useCallTool`, and `useViewState`. These hooks communicate
with the host; they do not create or manage MCP server connections.
See [React integration](/v2/typescript/client/usemcp) for connection setup and
[MCP Apps](/v2/typescript/mcp-apps) for building and hosting Views.
# MCP Client
Source: https://docs.mcp-use.com/v2/typescript/client/index
Connect a TypeScript app to MCP servers and call tools, resources, and prompts.
`@mcp-use/client` connects your app to one or more MCP servers. By default, HTTP
connections negotiate the server's supported MCP protocol automatically and
stdio connections use the sessionful protocol; set `protocolNegotiation` to
override either default. The same connection API works across sessionful and
sessionless servers.
## Install
```bash theme={null}
npm install @mcp-use/client
```
The package requires Node.js `>=22.22.2` for Node applications and is ESM-only.
## Quick start
```typescript theme={null}
import { MCPClient } from "@mcp-use/client";
const client = new MCPClient({
mcpServers: {
demo: { url: "http://localhost:3000/mcp" },
},
});
try {
const connection = await client.connect("demo");
const tools = await connection.listTools();
console.log(tools.map((tool) => tool.name));
const result = await connection.callTool("echo", { message: "hello" });
const text = result.content.find((block) => block.type === "text");
if (text?.type === "text") {
console.log(text.text);
}
} finally {
await client.close();
}
```
`connect(name)` returns a ready `MCPConnection`. Always close the client when
your application is finished so its active connections and transports are
released.
## Core types
* **`MCPClient`** — holds server configuration and opens connections.
* **`MCPConnection`** — calls tools, reads resources, and fetches prompts.
`connection.info` exposes the negotiated protocol, server metadata,
capabilities, and instructions.
* **`MCPSession`** — deprecated compatibility alias for `MCPConnection`.
Use `client.connect(name)` for one configured server or `client.connectAll()` to
connect to every configured server.
## Configure servers
```typescript theme={null}
import { MCPClient } from "@mcp-use/client";
const client = new MCPClient({
mcpServers: {
remote: {
url: "https://api.example.com/mcp",
authToken: process.env.MCP_TOKEN,
},
local: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "./"],
},
},
});
```
HTTP servers support automatic OAuth, bearer tokens, and custom headers. Stdio
connections are available only in Node.js.
Migrating existing client code? Follow the
[client migration guide](/v2/typescript/client/migration).
## Next steps
List and call server tools.
Update packages, imports, configuration, and connection code.
Configure OAuth and bearer tokens.
Manage connections with `McpClientProvider` and hooks.
Compare Node, browser, and React support.
# Migrate to v2
Source: https://docs.mcp-use.com/v2/typescript/client/migration
Migrate client, agent, and React connection code from mcp-use 1.x
This guide updates applications from the mcp-use 1.x client surface to the v2
packages. It covers client, agent, and React connection code.
Server and interactive UI APIs also have breaking changes. Do not assume that
existing server or widget code can be carried forward unchanged. Follow the
[server guide](/v2/typescript/server) and
[MCP Apps guide](/v2/typescript/mcp-apps) for those parts of your application.
## 1. Update the runtime and packages
The current client and agent packages require Node.js `>=22.22.2` and are
ESM-only.
```bash theme={null}
npm install @mcp-use/client @mcp-use/agent
```
Install `mcp-use` when the same project builds a server or MCP App View:
```bash theme={null}
npm install mcp-use
```
Ensure your project emits ESM and your bundler respects the package's `node` and
`browser` export conditions.
## 2. Move imports to their package entry points
| 1.x surface | Current import | Purpose |
| - | - | - |
| `MCPClient`, connectors, OAuth, and sessions from `mcp-use` | `@mcp-use/client` | Node or browser MCP client |
| `useMcp` and `McpClientProvider` from `mcp-use/react` | `@mcp-use/client/react` | React connection management |
| `MCPAgent` and `PROMPTS` from `mcp-use` | `@mcp-use/agent` | Native agent |
| LangChain agent compatibility and `ServerManager` | `@mcp-use/agent/langchain` | Optional LangChain integration |
| Widget hooks such as `useWidget` (removed) | `mcp-use/react` View hooks | Code running inside MCP App Views |
| `MCPServer` and server capabilities | `mcp-use` | MCP server |
```typescript theme={null}
import { MCPClient } from "@mcp-use/client";
import { MCPAgent, PROMPTS } from "@mcp-use/agent";
```
For the optional LangChain compatibility layer:
```typescript theme={null}
import {
LangChainMCPAgent,
ServerManager,
} from "@mcp-use/agent/langchain";
```
The native `MCPAgent` and the LangChain compatibility agent are separate public
entry points. Import `ServerManager` only from the LangChain entry.
### Removed client subpaths
The client package exports its conditional root, the `./react` entry, and a
`./sandbox` entry for the MCP Apps sandbox proxy document. Replace these removed
imports:
| Removed import | Use instead |
| - | - |
| `@mcp-use/client/browser` | `@mcp-use/client` in a browser build |
| `@mcp-use/client/auth` | `@mcp-use/client` |
| `@mcp-use/client/auth/node` | Node OAuth helpers from `@mcp-use/client` |
The root entry resolves to the Node build under the `node` condition and to the
browser-safe build under the `browser` or default condition.
## 3. Update the connection API
Use `connect()` and `connectAll()`:
```typescript theme={null}
import {
MCPClient,
type MCPConnection,
} from "@mcp-use/client";
const client = new MCPClient({
mcpServers: {
demo: { url: "https://api.example.com/mcp" },
},
});
try {
const connection: MCPConnection = await client.connect("demo");
await connection.callTool("echo", { message: "hello" });
} finally {
await client.close();
}
```
`createSession()` and `createAllSessions()` remain as deprecated compatibility
aliases. `MCPSession` is a deprecated runtime and type alias for
`MCPConnection`.
Every connection exposes the negotiated server information:
```typescript theme={null}
console.log(connection.info.protocolEra);
console.log(connection.info.protocolVersion);
console.log(connection.info.capabilities);
console.log(connection.info.instructions);
```
## 4. Configure protocol negotiation
The public configuration field is `protocolNegotiation`, not
`versionNegotiation`.
HTTP connections default to `protocolNegotiation: "auto"`: they probe the
sessionless protocol and fall back to the sessionful protocol. Stdio defaults to
`"legacy"` to avoid probing a process that may be launched for one invocation.
```typescript theme={null}
import { MCPClient } from "@mcp-use/client";
const client = new MCPClient({
mcpServers: {
compatible: {
url: "https://api.example.com/mcp",
protocolNegotiation: "auto",
},
},
});
```
The accepted values are `"auto"`, `"legacy"`, and `{ pin: "2026-07-28" }`.
There is no `"modern"` value; pin the modern protocol version to use the
sessionless protocol without fallback.
The client CLI exposes the same choices as `--protocol auto`, `legacy`, and
`modern`. Its `modern` mode uses `{ pin: "2026-07-28" }`:
```bash theme={null}
mcp-use client connect demo https://api.example.com/mcp --protocol auto
mcp-use client connect legacy https://legacy.example.com/mcp --protocol legacy
mcp-use client connect modern https://modern.example.com/mcp --protocol modern
```
There is no `--negotiate` flag.
## 5. Rename client configuration
Search your client configuration for the removed 1.x names and update them:
| 1.x name | Current name |
| - | - |
| `samplingCallback` | `onSampling` |
| `elicitationCallback` | `onElicitation` |
| `auth_token` | `authToken` |
| `customHeaders` | `headers` |
| `clientConfig` | `clientInfo` |
| `debug` | `logLevel` on `useMcp` |
| `BrowserTelemetry` | `Telemetry` |
| `ResourceTemplate` client type | `ResourceTemplateType` |
`MCPClient` has no `logLevel` option. Set `MCP_USE_LOG_LEVEL` (for example
`debug`; `DEBUG=2` also works) in the environment, or assign `logger.level` on
the `logger` exported by `@mcp-use/client`.
Sampling, elicitation, and notification callbacks can be set as global client
defaults or on an individual server configuration. Per-server callbacks take
precedence.
## 6. Update authentication behavior
`MCPClient` automatically creates an OAuth provider for HTTP servers that do not
already configure an `authProvider`, `authToken`, `oauth: false`, or an
`Authorization` header.
* Node OAuth opens a browser and waits for its loopback callback.
* The browser root client opens a popup by default and can use a redirect.
* `useMcp` and `McpClientProvider` default to
`preventAutoAuth: true`. Handle `pending_auth` and call `authenticate()`.
```tsx theme={null}
import { useMcpServer } from "@mcp-use/client/react";
export function ServerAuth() {
const server = useMcpServer("demo");
if (server?.state === "pending_auth") {
return (
);
}
return null;
}
```
For a React OAuth callback route, import `onMcpAuthorization` from
`@mcp-use/client/react`. A non-React browser build can use the browser
conditional root export.
Pre-registered clients also have different configuration shapes:
* `MCPClient`: `oauth.staticClientInfo.client_id`
* React: `oauth.clientId`
See [Authentication](/v2/typescript/client/authentication) for complete Node,
browser, React, proxy, and public-client examples.
## 7. Update React connection code and Views
Move connection management to `@mcp-use/client/react`:
| 1.x usage | Current usage |
| - | - |
| `useMcp` from `mcp-use/react` | `useMcp` from `@mcp-use/client/react` |
| `McpServerOptions` | `McpServerConfig` (`McpServerOptions` is deprecated) |
| Server config `name` used as a display label | `displayName` |
| Displayed `server.name` | Negotiated `server.serverInfo?.name` |
`@mcp-use/client/react` also exports `ViewRenderer` for a host application that
renders MCP App Views.
Code running inside a View stays in `mcp-use/react`, but must use focused View
hooks such as `useToolContext`, `useCallTool`, and `useViewState`. The widget
hooks `useWidget`, `useWidgetProps`, and `useWidgetState` were removed; see the
[server migration guide](/v2/typescript/server/migration) for their
replacements.
## 8. Replace removed low-level exports
The client root no longer re-exports the old Zod `*Schema` constants or
`JSONSchemaToZod`.
* Import protocol types from `@modelcontextprotocol/client`.
* Use `ResourceTemplateType` for the protocol resource-template type.
* Use Zod 4's `z.fromJSONSchema()` when you need to convert JSON Schema.
The current telemetry implementation does not bundle the PostHog browser or
Node SDKs. If you used the old telemetry override, note that `telFetch` is now a
non-throwing `(url, init) => Promise` helper.
Do not remove `@modelcontextprotocol/ext-apps` based on the old migration notes;
the current client still uses it for MCP Apps bridging.
## 9. Review server and MCP App changes
The server is now built around the root `mcp-use` entry, a stateless
`MCPServer`, and standard protocol result envelopes. Review tool, resource,
prompt, middleware, authentication, and deployment APIs against the
[server guide](/v2/typescript/server).
Interactive results use MCP Apps Views. Bind a View to a tool, return
model-facing `content` and View props in `structuredContent`, and consume the
result with focused hooks such as `useToolContext`. The widget hooks were
removed; the deprecated `widget()` response helper remains only for
compatibility. Follow the
[MCP Apps guide](/v2/typescript/mcp-apps) before updating interactive UI code.
## 10. Check Inspector callback URLs
The standalone Inspector OAuth callback resolves relative to the configured
Inspector base path. With the normal `/inspector` base, the callback is
`/inspector/oauth/callback`; deployments under another base use that base's
`/oauth/callback`.
If you run a custom OAuth proxy, continue to bind upstream targets to
SDK-discovered metadata. The Inspector proxy rejects arbitrary upstream target
URLs.
## Migration checklist
Use Node.js `>=22.22.2`, install the v2 packages, and confirm ESM output.
Search for client and agent imports from `mcp-use`, removed
`@mcp-use/client/*` subpaths, and connection hooks from `mcp-use/react`.
Keep `MCPAgent` and `PROMPTS` on `@mcp-use/agent`; move
`ServerManager` and LangChain compatibility imports to
`@mcp-use/agent/langchain`.
Search for `versionNegotiation`, `samplingCallback`,
`elicitationCallback`, `auth_token`, `customHeaders`, `clientConfig`, and
`debug`.
Replace `createSession` and `createAllSessions` calls with `connect` and
`connectAll`; replace `MCPSession` annotations with `MCPConnection`.
Handle React's `pending_auth` state, use the correct pre-registered client
shape for each API, and verify callback and proxy URLs.
Separate React connection management from code that runs inside an MCP App
View, then replace the removed widget hooks with View hooks.
Type-check Node, browser, and React entry points independently, then connect
to one sessionful and one sessionless server if your application supports
both.
## Related
* [Client overview](/v2/typescript/client/index)
* [Authentication](/v2/typescript/client/authentication)
* [React integration](/v2/typescript/client/usemcp)
* [Server guide](/v2/typescript/server)
* [MCP Apps guide](/v2/typescript/mcp-apps)
# Notifications
Source: https://docs.mcp-use.com/v2/typescript/client/notifications
Handle one-way updates from MCP servers
MCP notifications are one-way JSON-RPC messages with no response. Register a
handler when your application needs to react to server updates.
## Receive server notifications
Set a default handler on the client:
```typescript theme={null}
import { MCPClient } from "@mcp-use/client";
const client = new MCPClient({
mcpServers: {
demo: { url: "http://localhost:3000/mcp" },
},
onNotification: async (notification) => {
console.log(notification.method, notification.params);
},
});
```
An individual server configuration can provide its own `onNotification`
callback. Per-server callbacks take precedence over the global default.
You can also register a handler on a connection:
```typescript theme={null}
const connection = await client.connect("demo");
connection.on("notification", async (notification) => {
if (notification.method === "notifications/tools/list_changed") {
await connection.listTools();
}
});
```
The standard list-change method names are:
* `notifications/tools/list_changed`
* `notifications/resources/list_changed`
* `notifications/prompts/list_changed`
The client automatically refreshes its tool cache for tool list changes. Refresh
resources or prompts when your application needs their updated values.
## React
Each server managed by `McpClientProvider` exposes:
* `notifications`, the stored notification entries
* `unreadNotificationCount`
* `markNotificationRead`, `markAllNotificationsRead`, and
`clearNotifications`
Set `onNotificationReceived` on a server configuration to run custom logic when
the provider receives and stores a notification.
## Roots compatibility
`setRoots()` and `getRoots()` are compatibility APIs for sessionful servers.
Roots are not part of the current sessionless protocol.
For a compatible server, pass initial roots in its client configuration:
```typescript theme={null}
import { MCPClient } from "@mcp-use/client";
const client = new MCPClient({
mcpServers: {
legacy: {
url: "http://localhost:3000/mcp",
roots: [{ uri: "file:///home/user/project", name: "Project" }],
},
},
});
```
Calling `connection.setRoots(roots)` replaces the cached roots and sends
`notifications/roots/list_changed` when connected. `connection.getRoots()`
returns the current cached list. Use these methods only for a server that
negotiated the sessionful protocol and requests roots.
## Related
Server-side notification sending is covered in
[Notifications](/v2/typescript/server/notifications).
# Prompts
Source: https://docs.mcp-use.com/v2/typescript/client/prompts
Fetch server-defined prompt templates
Prompts are server-defined message templates. List the available prompts, then
request one by name with its arguments.
```typescript theme={null}
import { MCPClient } from "@mcp-use/client";
const client = new MCPClient({
mcpServers: {
demo: { url: "http://localhost:3000/mcp" },
},
});
try {
const connection = await client.connect("demo");
const { prompts } = await connection.listPrompts();
for (const prompt of prompts) {
console.log(prompt.name, prompt.description);
}
const result = await connection.getPrompt("plan_trip", {
destination: "Japan",
days: "7",
});
console.log(result.description);
for (const message of result.messages) {
if (message.content.type === "text") {
console.log(message.role, message.content.text);
}
}
} finally {
await client.close();
}
```
`listPrompts()` returns `{ prompts }`. `getPrompt(name, args)` returns a prompt
result with an optional `description` and a `messages` array. Each message has a
`role` and one MCP content block.
## React
```tsx theme={null}
import { useMcpServer } from "@mcp-use/client/react";
export function LoadPromptButton() {
const server = useMcpServer("demo");
async function handleLoad() {
if (server?.state !== "ready") return;
const result = await server.getPrompt("code_review", {
language: "ts",
});
console.log(result.messages);
}
return (
);
}
```
The React `getPrompt` arguments are optional and use string values.
## Reference
[`MCPConnection`](https://mcp-use-typescript-api-reference.vercel.app/modules/_mcp-use_client.index.html#mcpconnection) —
`listPrompts` and `getPrompt`.
# Resources
Source: https://docs.mcp-use.com/v2/typescript/client/resources
List and read MCP server resources
Resources are URI-addressed content exposed by MCP servers.
```typescript theme={null}
import { MCPClient } from "@mcp-use/client";
const client = new MCPClient({
mcpServers: {
demo: { url: "http://localhost:3000/mcp" },
},
});
try {
const connection = await client.connect("demo");
const { resources } = await connection.listAllResources();
for (const resource of resources) {
console.log(resource.uri, resource.name, resource.mimeType);
}
const result = await connection.readResource("file:///config.json");
for (const content of result.contents) {
if ("text" in content) {
console.log(content.text);
}
}
} finally {
await client.close();
}
```
Use `listAllResources()` when you want the complete resource list. It follows
pagination cursors for you and returns `{ resources }`.
## Paginate resources
`listResources(cursor?)` returns an envelope with `resources` and an optional
`nextCursor`. Handle the cursor yourself when you want to process one page at a
time:
```typescript theme={null}
import type { MCPConnection } from "@mcp-use/client";
async function logResourcePages(connection: MCPConnection): Promise {
let cursor: string | undefined;
do {
const page = await connection.listResources(cursor);
for (const resource of page.resources) {
console.log(resource.uri);
}
cursor = page.nextCursor;
} while (cursor);
}
```
## Resource templates
Parameterized URIs such as `db://users/{id}` are discovered with
`listResourceTemplates()`. Its result contains `resourceTemplates` and may
contain `nextCursor`. The current `MCPConnection` method retrieves the first
template page and does not accept a cursor argument.
```typescript theme={null}
const page = await connection.listResourceTemplates();
for (const template of page.resourceTemplates) {
console.log(template.uriTemplate, template.name);
}
```
Resolve a template to a concrete URI in your application, then pass that URI to
`readResource(uri)`.
## Subscribe to updates
If the server supports resource subscriptions, subscribe and unsubscribe with
the same concrete URI:
```typescript theme={null}
await connection.subscribeToResource("file:///config.json");
// Receive notifications/resources/updated through your notification handler.
await connection.unsubscribeFromResource("file:///config.json");
```
## React
`useMcpServer` exposes the resource contents envelope returned by
`readResource(uri)`:
```tsx theme={null}
import { useMcpServer } from "@mcp-use/client/react";
export function ReadResourceButton({ uri }: { uri: string }) {
const server = useMcpServer("demo");
async function handleRead() {
if (server?.state !== "ready") return;
const { contents } = await server.readResource(uri);
console.log(contents);
}
return (
);
}
```
## Reference
[`MCPConnection`](https://mcp-use-typescript-api-reference.vercel.app/modules/_mcp-use_client.index.html#mcpconnection) —
`listResources`, `listAllResources`, `listResourceTemplates`, `readResource`,
`subscribeToResource`, and `unsubscribeFromResource`.
# Sampling compatibility
Source: https://docs.mcp-use.com/v2/typescript/client/sampling
Handle sampling requests from compatible MCP server flows
Sampling is a compatibility capability, not a primary capability of the current
sessionless MCP protocol. Configure it only when a server flow requires the
client to provide model output.
The client can invoke `onSampling` in two cases:
* A legacy sessionful server sends a `sampling/createMessage` request.
* A sessionless server enters the temporary `input_required` multi-round-trip
compatibility flow.
Sampling is deprecated by the current sessionless protocol. Prefer server and
tool designs that do not depend on client-provided sampling when you control
both sides.
## Handle a request
```typescript theme={null}
import {
MCPClient,
type OnSamplingCallback,
type SamplingCreateMessageParams,
} from "@mcp-use/client";
async function completeWithModel(
params: SamplingCreateMessageParams
): Promise {
// Send params.messages and the other sampling constraints to your model.
throw new Error(`Connect a model for ${params.messages.length} messages`);
}
const onSampling: OnSamplingCallback = async (params) => {
const text = await completeWithModel(params);
return {
role: "assistant",
content: { type: "text", text },
model: "your-model",
stopReason: "endTurn",
};
};
const client = new MCPClient({
mcpServers: {
demo: {
url: "http://localhost:3000/mcp",
onSampling,
},
},
});
try {
await client.connect("demo");
// A compatible server flow can now call onSampling.
} finally {
await client.close();
}
```
`OnSamplingCallback` receives the protocol parameters for
`sampling/createMessage` and must return a
`SamplingCreateMessageResult`. Setting the callback also advertises the
sampling capability where the negotiated protocol supports it.
## Global and per-server handlers
Set `onSampling` at the top level of the `MCPClient` configuration to use it as
a global default. An `onSampling` callback on an individual server
configuration takes precedence over that default. The Node client also accepts
the global callback in its second constructor argument.
## React
`useMcp({ onSampling })` handles compatible requests directly with your
callback.
`McpClientProvider` instead installs a handler that queues each request on the
managed server:
* `server.pendingSamplingRequests` contains the requests awaiting a decision.
* `server.approveSampling(requestId, result)` resolves a request with a complete
`SamplingCreateMessageResult`.
* `server.rejectSampling(requestId, error?)` rejects it.
* `onSamplingRequest` on the server configuration can notify your UI when a
request is added to the queue.
Queued requests remain pending until they are approved, rejected, disconnected,
or timed out.
## Runnable example
The maintained
[sampling client example](https://github.com/mcp-use/mcp-use/blob/main/libraries/typescript/packages/client/examples/node/communication/sampling-client.ts)
exercises both the legacy push request and the sessionless
`input_required` compatibility flow.
# Tools
Source: https://docs.mcp-use.com/v2/typescript/client/tools
List and call MCP server tools
Use a ready `MCPConnection` to list and call tools.
```typescript theme={null}
import { MCPClient } from "@mcp-use/client";
const client = new MCPClient({
mcpServers: { demo: { url: "http://localhost:3000/mcp" } },
});
try {
const conn = await client.connect("demo");
const tools = await conn.listTools();
for (const tool of tools) {
console.log(tool.name, tool.description);
}
const result = await conn.callTool("read_file", { path: "/tmp/a.txt" });
if (result.isError) {
console.error(result.content);
} else {
for (const block of result.content) {
if (block.type === "text") console.log(block.text);
}
}
} finally {
await client.close();
}
```
## Timeouts
Pass MCP request options as the third argument:
```typescript theme={null}
const abortController = new AbortController();
await conn.callTool(
"long_task",
{ input: "data" },
{
timeout: 300_000,
resetTimeoutOnProgress: true,
maxTotalTimeout: 900_000,
signal: abortController.signal,
},
);
```
## Lazy connect
```typescript theme={null}
let conn = client.getSession("demo");
if (!conn) conn = await client.connect("demo");
await conn.callTool("echo", { message: "hello" });
```
`getSession` returns `null` when not connected. `requireSession` throws.
## Reference
[`MCPConnection`](https://mcp-use-typescript-api-reference.vercel.app/modules/_mcp-use_client.index.html#mcpconnection) — `listTools`, `callTool`, and request options.
# React Integration
Source: https://docs.mcp-use.com/v2/typescript/client/usemcp
React hooks and providers for MCP client connections
Import from `@mcp-use/client/react`:
```typescript theme={null}
import {
McpClientProvider,
useMcpClient,
useMcpServer,
useMcp,
onMcpAuthorization,
} from "@mcp-use/client/react";
```
Use **`McpClientProvider`** for multi-server apps. Use standalone **`useMcp`** only for a single server.
## Provider setup
```tsx theme={null}
import { useEffect } from "react";
import {
McpClientProvider,
useMcpClient,
useMcpServer,
} from "@mcp-use/client/react";
function App() {
return (
);
}
function Dashboard() {
const { addServer, servers, storageLoaded } = useMcpClient();
useEffect(() => {
// Wait for the provider's initial server load before adding servers.
if (!storageLoaded) return;
addServer("demo", {
url: "http://localhost:3000/mcp",
displayName: "Demo",
});
}, [addServer, storageLoaded]);
return servers.map((s) => );
}
function ServerPanel({ serverId }: { serverId: string }) {
const server = useMcpServer(serverId);
if (!server || server.state !== "ready") {
return {server?.state ?? "loading"}…;
}
return (
{server.displayName}
{server.tools.length} tools
);
}
```
Call `addServer` only after `storageLoaded` is `true`. When the provider finishes
its initial load, it replaces its server list with the servers from
`storageProvider` and the `mcpServers` prop, if either is set, so a server
added earlier is dropped. `addServer` ignores an ID that is already registered.
## Connection states
| State | Meaning |
| - | - |
| `discovering` | Connecting |
| `pending_auth` | OAuth required — call `server.authenticate()` |
| `authenticating` | OAuth in progress |
| `ready` | Connected |
| `failed` | Error — call `server.retry()` |
OAuth does not auto-start by default (`preventAutoAuth: true`). Show a sign-in button when `state === "pending_auth"`.
Mixed auth is reported separately from the connection state. A mixed-auth
server remains `ready` because its public operations are usable, while
`authorization` describes the optional OAuth state:
```tsx theme={null}
function ServerAuth({ serverId }: { serverId: string }) {
const server = useMcpServer(serverId);
if (!server) return null;
const needsOptionalAuth =
server.authorization?.mode === "mixed" &&
!server.authorization.authenticated;
return needsOptionalAuth ? (
This server is using mixed auth. Public tools are available.
) : null;
}
```
The hook detects mixed auth from RFC 9728 protected-resource metadata after an
anonymous connection. Set `detectMixedAuth: false` to disable this best-effort
discovery. With the default explicit browser flow, a wire-level OAuth challenge
from a protected operation prepares the auth action; call `authenticate()` and
retry that operation. Tool-level `isError` results do not trigger OAuth.
## OAuth callback
Create a route (default: `/oauth/callback`):
```tsx theme={null}
import { onMcpAuthorization } from "@mcp-use/client/react";
import { useEffect } from "react";
export default function OAuthCallback() {
useEffect(() => {
void onMcpAuthorization();
}, []);
return Completing sign-in…;
}
```
Override with `defaultCallbackUrl` on the provider or `callbackUrl` per server.
`McpClientProvider` skips connecting its servers only on a page whose path ends
in `/oauth/callback`. A connection attempt on the callback page can replace the
in-flight PKCE verifier before `onMcpAuthorization()` exchanges the code. If
your callback URL uses a different path, render that route outside
`McpClientProvider`.
## Proxy fallback
When direct browser connections fail (CORS / FastMCP), retry through a proxy:
```tsx theme={null}
```
MCP traffic and OAuth use separate proxy URLs so resource identity stays the upstream MCP URL.
## Persistence
```tsx theme={null}
import { LocalStorageProvider } from "@mcp-use/client/react";
;
```
Built-in providers persist only non-secret connection settings. Supply bearer
tokens, request headers, and proxy headers again at runtime after a reload.
Browser OAuth tokens remain persistent by default and are encrypted with
AES-256-GCM using a non-extractable origin key stored in IndexedDB.
## Standalone `useMcp`
For one server without the provider:
```tsx theme={null}
const mcp = useMcp({ url: "http://localhost:3000/mcp" });
if (mcp.state === "pending_auth") {
return ;
}
if (mcp.state !== "ready") return Connecting…;
return (
<>
{mcp.authorization?.mode === "mixed" &&
!mcp.authorization.authenticated && (
)}
{mcp.tools.map((t) => t.name).join("\n")}
>
);
```
## OAuth UserInfo
Set `oauth.fetchUserInfo: true` to fetch the authenticated account's UserInfo
after the MCP connection becomes ready. The fetch runs independently of the
connection; a UserInfo error does not change `mcp.state`. The authorization
server must advertise a `userinfo_endpoint`, and the requested scopes must
permit the claims you need (often `openid profile email`).
```tsx theme={null}
function Account() {
const mcp = useMcp({
url: "https://example.com/mcp",
oauth: { scope: "openid profile email", fetchUserInfo: true },
});
return (
{mcp.userInfo.status === "available" &&
(mcp.userInfo.claims.email ?? mcp.userInfo.claims.sub)}
);
}
```
`mcp.userInfo` is reactive and has these states:
| Status | Meaning |
| - | - |
| `idle` | No request is in progress. |
| `loading` | UserInfo is being fetched. |
| `available` | Read `claims` and optional `issuer`; claims such as `email` may be absent. |
| `unavailable` | Read `reason`: `not_authenticated`, `no_userinfo_endpoint`, or `session_changed`. |
| `error` | Read `error`; call `getUserInfo()` to retry. |
Automatic fetching is opt-in and retries an error once after a short delay.
You can omit `fetchUserInfo` and call `await mcp.getUserInfo()` whenever you
want to fetch the account explicitly; the method returns the same state it
publishes to `mcp.userInfo`.
## Types
* **`McpServerConfig`** — server options passed to `addServer` (replaces deprecated `McpServerOptions`).
* **`displayName`** — your label for the server in UI.
* **`serverInfo.name`** — name returned by the server at init.
* **`authorization`** — optional mixed-auth metadata with `mode`,
`authenticated`, `resource`, and `scopesSupported`.
Sampling, elicitation, and notifications are queued on each `McpServer` (`pendingSamplingRequests`, `pendingElicitationRequests`, `notifications`). See [Sampling](/v2/typescript/client/sampling) and [Elicitation](/v2/typescript/client/elicitation).
## Reference
[React client API reference](https://mcp-use-typescript-api-reference.vercel.app/modules/_mcp-use_client.react.html) — provider props, hook return values, and storage types.
# A2UI
Source: https://docs.mcp-use.com/v2/typescript/cookbook/a2ui
Let the chat model generate interactive A2UI layouts and render them in an MCP App view.
Add a `render-ui` tool that renders model-generated [A2UI](https://a2ui.org/) layouts inside an MCP App. The model writes the component layout and initial data as tool arguments, and the stock A2UI React renderer displays them in the view.
The server needs no model API key or agent runtime. The host's chat model generates the tool arguments.
## Try the live example
Ask the chat for a UI. Pick a suggestion or describe your own layout, then edit the inputs, sliders, and checkboxes in the result.
[Open the example in a new tab](https://inspector.manufact.com/inspector?embedded=true\&autoConnect=https%3A%2F%2Fmanufact-a2ui-example.run.mcp-use.com%2Fmcp\&embeddedConfig=%7B%22singleTab%22%3Atrue%2C%22defaultTab%22%3A%22chat%22%2C%22visibleTabs%22%3A%5B%22chat%22%5D%2C%22chatHideTitle%22%3Atrue%2C%22chatHideServerUrl%22%3Atrue%2C%22chatQuickQuestions%22%3A%5B%22Make%20an%20interactive%20weekend%20reading%20dashboard%20with%20two%20book%20cards%2C%20a%20reading-goal%20slider%2C%20and%20a%20checklist.%20Let%20me%20edit%20the%20reader%20name.%22%2C%22Build%20a%20packing%20checklist%20for%20a%20three-day%20beach%20trip%2C%20grouped%20into%20essentials%20and%20clothes%2C%20with%20an%20editable%20destination%20and%20a%20travel-style%20selector.%22%5D%7D).
## Prerequisites
* A working [mcp-use server](/v2/typescript/getting-started/quickstart) that can build [MCP App views](/v2/typescript/mcp-apps/quickstart).
* Node.js 22.22.2 or later.
## Add the integration
The integration has three files:
| File | Purpose |
| - | - |
| `views/generative-ui/catalog.ts` | The components the model may use, plus validation shared by the tool and the view |
| `index.ts` | Your server entry, which registers the `render-ui` tool that accepts and returns `{ spec }` |
| `views/generative-ui/view.tsx` | Converts the spec to A2UI messages and renders `` |
### Install A2UI
Install the A2UI React renderer and its core message processor:
```bash theme={null}
npm install @a2ui/react@0.11.0 @a2ui/web_core@0.10.7 react react-dom zod
```
A2UI depends on Zod 3, while mcp-use tool schemas use Zod 4. Add an `overrides` block to `package.json` so npm gives the A2UI packages their own Zod 3 copy:
```json package.json theme={null}
{
"overrides": {
"@a2ui/react": { "zod": "3.25.76" },
"@a2ui/web_core": { "zod": "3.25.76" }
}
}
```
Run `npm install` again after adding the overrides.
### Define the component catalog
The catalog is a subset of A2UI's basic catalog: Text, Row, Column, Card, Divider, TextField, CheckBox, Slider, and ChoicePicker. The refinement rejects specs the renderer cannot display, such as duplicate IDs, unreachable components, missing bindings, and initial values of the wrong type.
```typescript views/generative-ui/catalog.ts expandable theme={null}
import { z } from "zod";
const id = z
.string()
.regex(/^[a-zA-Z][\w-]*$/)
.max(64);
const binding = z.object({ path: z.string().regex(/^\/[\w-]+$/) });
const text = z.union([z.string().max(3000), binding]);
const component = z.discriminatedUnion("component", [
z.object({
id,
component: z.literal("Text"),
text,
variant: z
.enum(["h1", "h2", "h3", "h4", "h5", "body", "caption"])
.optional(),
}),
z.object({
id,
component: z.literal("Column"),
children: z.array(id).max(40),
}),
z.object({ id, component: z.literal("Row"), children: z.array(id).max(8) }),
z.object({ id, component: z.literal("Card"), child: id }),
z.object({ id, component: z.literal("Divider") }),
z.object({
id,
component: z.literal("CheckBox"),
label: text,
value: binding,
}),
z.object({
id,
component: z.literal("TextField"),
label: text,
value: binding,
variant: z.enum(["shortText", "longText", "number"]).optional(),
}),
z.object({
id,
component: z.literal("Slider"),
label: text,
value: binding,
min: z.number(),
max: z.number(),
}),
z.object({
id,
component: z.literal("ChoicePicker"),
label: text,
value: binding,
options: z
.array(
z.object({ label: z.string().max(3000), value: z.string().max(3000) })
)
.min(1)
.max(12),
variant: z.enum(["mutuallyExclusive", "multipleSelection"]),
}),
]);
/** A small subset of A2UI's basic catalog, shared by the tool and view. */
export const specSchema = z
.object({
components: z
.array(component)
.min(1)
.max(64)
.describe(
"A2UI components with unique IDs. The root component must have id 'root'. Layouts reference children by ID."
),
data: z
.record(
z.string(),
z.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
)
.describe(
"Initial values for every /key binding. CheckBox uses a boolean, Slider a number, TextField a string, ChoicePicker a string array."
),
})
.superRefine((spec, ctx) => {
const nodes = new Map(spec.components.map((node) => [node.id, node]));
const fail = (message: string) => ctx.addIssue({ code: "custom", message });
if (nodes.size !== spec.components.length)
fail("Component IDs must be unique.");
if (!nodes.has("root")) fail("Include a component with id 'root'.");
const visited = new Set();
function visit(key: string, ancestors = new Set()) {
if (ancestors.has(key)) {
fail(`Circular component reference: ${key}`);
return;
}
if (visited.has(key)) return;
const node = nodes.get(key);
if (!node) {
fail(`Missing component: ${key}`);
return;
}
const path = new Set([...ancestors, key]);
for (const child of "children" in node
? node.children
: "child" in node
? [node.child]
: [])
visit(child, path);
visited.add(key);
}
visit("root");
if (visited.size !== nodes.size)
fail("All components must be reachable from root.");
for (const node of spec.components) {
for (const value of Object.values(node)) {
if (
value &&
typeof value === "object" &&
"path" in value &&
!Object.hasOwn(spec.data, value.path.slice(1))
)
fail(`Missing initial data for ${value.path}`);
}
if ("value" in node) {
const value = spec.data[node.value.path.slice(1)];
const valid =
node.component === "CheckBox"
? typeof value === "boolean"
: node.component === "Slider"
? typeof value === "number" &&
node.min < node.max &&
value >= node.min &&
value <= node.max
: node.component === "ChoicePicker"
? Array.isArray(value) &&
value.every((item) =>
node.options.some((option) => option.value === item)
) &&
(node.variant !== "mutuallyExclusive" || value.length <= 1)
: typeof value === "string";
if (!valid)
fail(
`Invalid initial value or range for ${node.id} (${node.component}).`
);
}
}
});
/** Validated model-generated A2UI layout and initial data. */
export type UISpec = z.infer;
```
### Register the tool
The tool description is the model's only guide to the catalog, so it lists every component, the binding rules, and a small example. The callback returns the validated spec in `structuredContent`, and `view` attaches the `generative-ui` view to the result.
```typescript index.ts theme={null}
import { MCPServer } from "mcp-use";
import { z } from "zod";
import { specSchema } from "./views/generative-ui/catalog.js";
const server = new MCPServer({
name: "a2ui-generative-ui",
version: "1.0.0",
title: "A2UI Generative UI",
description: "Model-generated A2UI layouts rendered inside an MCP App.",
legacy: "stateless",
});
/** Render the model's A2UI component layout in the attached MCP App. */
export const renderUi = server.tool(
{
name: "render-ui",
title: "Render A2UI",
description: `Generate and render an interactive UI using A2UI's basic catalog.
Write one structured spec object, not JSONL or a string. You choose the layout and content.
Use Text, Row, Column, Card, Divider, TextField, CheckBox, Slider, and ChoicePicker.
Put component properties directly alongside id and component (no props wrapper).
Start with a Column named root. Refer to children by ID; a Card takes one child.
Use Text variants h2/h3/body/caption for hierarchy and Cards for related groups.
Bind editable controls with value: {path: "/key"} and put initial values in spec.data.
CheckBox values are booleans, Slider values are numbers, TextField values are strings,
and ChoicePicker values are arrays of option strings. Text can also bind to /key,
so a Text and TextField sharing a binding update together.
These controls edit local UI state. Do not invent submit buttons, saving, calculations,
network requests, or actions. This example displays the completed response.
Example: {"components":[{"id":"root","component":"Column","children":["heading","done"]},{"id":"heading","component":"Text","text":"My checklist","variant":"h2"},{"id":"done","component":"CheckBox","label":"Try A2UI","value":{"path":"/done"}}],"data":{"done":false}}`,
inputSchema: z.object({ spec: specSchema }),
outputSchema: z.object({ spec: specSchema }),
annotations: { readOnlyHint: true, openWorldHint: false },
view: { name: "generative-ui", prefersBorder: false },
},
async ({ spec }) => ({
content: [
{
type: "text",
text: `Rendered ${spec.components.length} A2UI components.`,
},
],
structuredContent: { spec },
})
);
export default server;
```
### Render the spec in the view
The view waits for the completed tool result, then passes three standard A2UI v0.9 messages to a `MessageProcessor`: `createSurface`, `updateComponents`, and `updateDataModel`. `` renders the resulting surface and keeps its bound values in local state.
```tsx views/generative-ui/view.tsx theme={null}
import { A2uiSurface, basicCatalog } from "@a2ui/react/v0_9";
import { MessageProcessor } from "@a2ui/web_core/v0_9";
import { ThemeProvider, useToolContext } from "mcp-use/react";
import { useState } from "react";
import type { UISpec } from "./catalog.js";
import "./view.css";
function GeneratedUI({ spec }: { spec: UISpec }) {
const [processor] = useState(() => {
const instance = new MessageProcessor([basicCatalog]);
instance.processMessages([
{
version: "v0.9",
createSurface: { surfaceId: "ui", catalogId: basicCatalog.id },
},
{
version: "v0.9",
updateComponents: { surfaceId: "ui", components: spec.components },
},
{
version: "v0.9",
updateDataModel: { surfaceId: "ui", path: "/", value: spec.data },
},
]);
return instance;
});
const surface = processor.model.getSurface("ui");
return surface ? (
) : (
Unable to render this interface.
);
}
function Content() {
const view = useToolContext<"render-ui">();
if (view.status === "error") return {view.error.message}
;
if (view.status !== "ready")
return Designing your interface…
;
return (
);
}
/** Render a completed model-generated A2UI spec inside an ordinary MCP App. */
export default function GenerativeUI() {
return (
);
}
```
Copy [`view.css`](https://github.com/manufacts/mcp-use-a2ui-example/blob/main/views/generative-ui/view.css) from the example for the theme, or remove the `./view.css` import to use A2UI's default styles.
## Try it
Start your server:
```bash theme={null}
npm run dev
```
Open the Inspector URL printed by the CLI, select **Chat**, and sign in with Manufact or configure a model provider. Try:
```text theme={null}
Build a packing checklist for a three-day beach trip, grouped into essentials and clothes, with an editable destination and a travel-style selector.
```
The model calls `render-ui`, and the view appears when the tool result completes. To test without a model, select **Tools → render-ui** and enter:
```json theme={null}
{
"spec": {
"components": [
{ "id": "root", "component": "Column", "children": ["heading", "done"] },
{ "id": "heading", "component": "Text", "text": "My checklist", "variant": "h2" },
{ "id": "done", "component": "CheckBox", "label": "Try A2UI", "value": { "path": "/done" } }
],
"data": { "done": false }
}
}
```
## Adapt it to your app
* **Local state only:** inputs, checkboxes, sliders, and choices edit the view's data model. Components that share a `/key` binding update together. Edits are not sent back to the server and reset when the host recreates the view.
* **Add components:** add the A2UI component shape to the `component` union in `catalog.ts`, then describe it in the tool description so the model knows when to use it.
* **Add actions:** this recipe has no submit buttons or server actions. To act on user input, call a server tool from the view with [`useCallTool`](/v2/typescript/mcp-apps/interactivity).
* **Other hosts:** the view carries its own A2UI renderer, so any MCP Apps host can display it without registering an A2UI catalog.
See the [complete A2UI example](https://github.com/manufacts/mcp-use-a2ui-example) for the runnable project and catalog tests, the [A2UI React renderer](https://github.com/a2ui-project/a2ui/tree/main/renderers/react), and [A2UI inside MCP Apps](https://a2ui.org/guides/a2ui-in-mcp-apps/). For streaming partial specs into the view while the model writes them, see [Generative UI](/v2/typescript/mcp-apps/generative-ui).
# Daytona
Source: https://docs.mcp-use.com/v2/typescript/cookbook/daytona
Add a run_code tool that executes code in a Daytona sandbox and returns the output through MCP.
Give your existing mcp-use server a `run_code` tool. Each call creates a fresh Daytona sandbox, runs the code, and deletes the sandbox afterward.
## Try the live example
Run code through our deployed Daytona server. Pick a suggestion or ask the chat to run your own snippet.
[Open the example in a new tab](https://inspector.manufact.com/inspector?embedded=true\&autoConnect=https%3A%2F%2Fmanufact-daytona-example.run.mcp-use.com%2Fmcp\&embeddedConfig=%7B%22singleTab%22%3Atrue%2C%22defaultTab%22%3A%22chat%22%2C%22visibleTabs%22%3A%5B%22chat%22%5D%2C%22chatHideTitle%22%3Atrue%2C%22chatHideServerUrl%22%3Atrue%2C%22chatQuickQuestions%22%3A%5B%22Use%20run_code%20to%20run%20Python%20that%20prints%20the%20first%2010%20Fibonacci%20numbers.%22%2C%22Use%20run_code%20to%20run%20JavaScript%20that%20calculates%20the%20sum%20of%20the%20squares%20from%201%20to%2010.%22%5D%7D).
## Prerequisites
* A working [mcp-use server](/v2/typescript/getting-started/quickstart).
* A [Daytona API key](https://app.daytona.io/).
## Add the integration
Install the Daytona SDK in your server project:
```bash theme={null}
npm install @daytona/sdk@0.210.0
```
Add your key to `.env`:
```bash .env theme={null}
DAYTONA_API_KEY=your_daytona_api_key
```
Copy this complete example into your server entry (`index.ts` in a project created with `create-mcp-use-app`), or adapt its Daytona client and `run_code` tool for your existing server:
```typescript index.ts theme={null}
import { Daytona, type Sandbox } from "@daytona/sdk";
import { MCPServer } from "mcp-use";
import { z } from "zod";
const server = new MCPServer({
name: "daytona-example",
version: "1.0.0",
description:
"Run Python, TypeScript, or JavaScript in a fresh Daytona sandbox.",
});
let daytona: Daytona | undefined;
/** Execute one code snippet and delete its isolated sandbox after the attempt. */
export const runCode = server.tool(
{
name: "run_code",
description:
"Execute Python, TypeScript, or JavaScript in a fresh Daytona sandbox. Returns output and exit code; files and state are discarded afterward.",
inputSchema: z.object({
code: z.string().min(1).max(100_000).describe("Source code to execute."),
language: z
.enum(["python", "typescript", "javascript"])
.default("python"),
timeoutSeconds: z.number().int().min(1).max(120).default(30),
}),
},
async ({ code, language, timeoutSeconds }) => {
if (!process.env.DAYTONA_API_KEY?.trim()) {
return {
isError: true,
content: [
{
type: "text" as const,
text: "Set DAYTONA_API_KEY in .env and restart the server before calling run_code.",
},
],
};
}
let sandbox: Sandbox | undefined;
let output: string | undefined;
let exitCode: number | undefined;
let error: string | undefined;
let cleanupError: string | undefined;
try {
daytona ??= new Daytona();
// Auto-stop/delete also limits leftovers if the server exits unexpectedly.
sandbox = await daytona.create(
{ language, autoStopInterval: 5, autoDeleteInterval: 0 },
{ timeout: 60 }
);
const result = await sandbox.process.codeRun(
code,
undefined,
timeoutSeconds
);
output = result.result;
exitCode = result.exitCode;
} catch {
// SDK errors can contain request details; do not expose credentials to clients.
error =
"Daytona could not create the sandbox or execute the code. Check your API key, Daytona quota and service availability, or increase timeoutSeconds (maximum 120).";
} finally {
if (daytona && sandbox) {
try {
await daytona.delete(sandbox, 30, true);
} catch {
cleanupError = `Could not confirm deletion of sandbox ${sandbox.id}. Check the Daytona dashboard and delete it if needed; automatic deletion is configured after stopping.`;
}
}
}
return {
isError: Boolean(
error || cleanupError || (exitCode !== undefined && exitCode !== 0)
),
content: [
{
type: "text" as const,
text: JSON.stringify({ output, exitCode, error, cleanupError }),
},
],
};
}
);
/** Daytona cookbook server; the mcp-use CLI owns its HTTP listener. */
export default server;
```
## Try it
Start or restart your server so it loads the key:
```bash theme={null}
npm run dev
```
In the Inspector, select **Tools → run\_code** and enter:
```json theme={null}
{
"language": "python",
"code": "print(sum([1, 2, 3]))"
}
```
The result contains `output: "6\n"` and `exitCode: 0`. With a model configured in the Inspector's **Chat** tab, try:
```text theme={null}
Use run_code to run Python that prints the first 10 Fibonacci numbers.
```
Each call uses your Daytona account, consumes its sandbox quota and credits, and discards its sandbox files afterward. Before exposing your server publicly, require authentication and enforce rate and spending limits to control who can execute code and how much they can spend. Code execution has a 30-second timeout; sandbox creation and deletion have separate timeouts.
# Cookbook
Source: https://docs.mcp-use.com/v2/typescript/cookbook/index
Extend your MCP server with practical integration examples.
Start with a working app or MCP server and add an integration. Each recipe includes the code you need and a quick way to try it.
## Recipes
Add an MCP endpoint to your existing TanStack Start app. Define a tool and mount it with the Vite plugin and a server route.
Let the chat model generate interactive layouts. Render A2UI components in an MCP App view.
Build a map-based home search MCP App. Let the model refine the open view with view tools.
# Next.js
Source: https://docs.mcp-use.com/v2/typescript/cookbook/nextjs
Add an MCP endpoint to your existing Next.js app with one simple tool.
Expose a `greet` tool at `/api/mcp` in your existing Next.js app. Next.js serves your application and the MCP endpoint together.
## Prerequisites
* A working Next.js app using the App Router and TypeScript.
## Add the integration
Install mcp-use and Zod in your Next.js project:
```bash theme={null}
npm install mcp-use zod
```
Keep `mcp-server.ts` at the root of your Next.js project, alongside `next.config.ts`. Add the MCP route inside your existing `app` directory:
```text theme={null}
my-next-app/
├── app/
│ ├── api/
│ │ └── mcp/
│ │ └── [[...path]]/
│ │ └── route.ts # Add: mount the MCP endpoint
│ ├── layout.tsx
│ └── page.tsx
├── public/
├── mcp-server.ts # Add: define your server and tools
├── next.config.ts # Update: enable the adapter
├── package.json
└── tsconfig.json
```
If your project uses `src/app`, the route goes inside that directory instead. Keep `mcp-server.ts` and `next.config.ts` at the project root.
### Define the server
Create `mcp-server.ts` at the project root. Register a tool that accepts a name and returns a greeting:
```typescript mcp-server.ts theme={null}
import { MCPServer } from "mcp-use";
import { z } from "zod";
const server = new MCPServer({
name: "my-next-app",
version: "1.0.0",
basePath: "/api/mcp",
});
server.tool(
{
name: "greet",
description: "Greet a person.",
inputSchema: z.object({ name: z.string() }),
},
async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}!` }],
}),
);
export default server;
```
### Configure the adapter
Wrap your existing Next.js configuration with `withMcpUse`. Keep your application's current options inside `nextConfig`:
```typescript next.config.ts theme={null}
import type { NextConfig } from "next";
import { withMcpUse } from "mcp-use/next";
const nextConfig: NextConfig = {};
export default withMcpUse(nextConfig, {
entry: "./mcp-server.ts",
basePath: "/api/mcp",
});
```
### Mount the endpoint
Create an optional catch-all Route Handler at `app/api/mcp/[[...path]]/route.ts`:
```typescript app/api/mcp/[[...path]]/route.ts theme={null}
import { createNextHandler } from "mcp-use/next";
import server from "../../../../mcp-server";
export const { GET, POST, DELETE, OPTIONS } = createNextHandler(server);
```
If your app uses `src/app`, put the route at `src/app/api/mcp/[[...path]]/route.ts` and use `../../../../../mcp-server` to import the server from the project root.
## Try it
Start your Next.js app with its usual command:
```bash theme={null}
npm run dev
```
In a second terminal, open the Inspector connected to your new endpoint:
```bash theme={null}
npx @mcp-use/inspector --url http://localhost:3000/api/mcp
```
Select **Tools → greet** and enter:
```json theme={null}
{
"name": "Ada"
}
```
Run the tool. It returns `Hello, Ada!`.
Use your app's port if it differs from `3000`. Restart `next dev` after editing `mcp-server.ts` so the adapter's startup build runs again.
See the [complete Next.js example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/server/examples/nextjs) for a runnable application, or the [Next.js guide](/v2/typescript/server/nextjs-drop-in) for shared UI views and standalone servers.
# Property search
Source: https://docs.mcp-use.com/v2/typescript/cookbook/property-search
Build a map-based home search MCP App that the chat model refines in place with view tools.
Build a Zillow-style MCP App: listing cards next to an interactive map with price pins. The model opens the view once with `search-homes`. After that, the view registers its own tools, so follow-ups such as "now the Mission, under \$2M" update the open map instead of rendering a second view.
The listings are fictional San Francisco homes. Map tiles come from Esri's keyless Canvas basemaps, so the example needs no listing API or API key.
## Try the live example
Ask for homes in San Francisco. Select **Fullscreen** in the view to see the listing cards, then click a card or pin to load its details.
[Open the example in a new tab](https://inspector.manufact.com/inspector?embedded=true\&autoConnect=https%3A%2F%2Fmanufact-property-search-example.run.mcp-use.com%2Fmcp\&embeddedConfig=%7B%22singleTab%22%3Atrue%2C%22defaultTab%22%3A%22chat%22%2C%22visibleTabs%22%3A%5B%22chat%22%5D%2C%22chatHideTitle%22%3Atrue%2C%22chatHideServerUrl%22%3Atrue%2C%22chatQuickQuestions%22%3A%5B%22Show%20me%20homes%20in%20San%20Francisco.%22%2C%22Show%20me%20homes%20in%20Pacific%20Heights.%22%5D%7D).
This embedded chat runs its model on Manufact's servers, which cannot reach tools registered inside the view. Follow-up refinements such as "now the Mission, under \$2M" need a host that forwards view tools to the model. To try them, [run the example](#run-the-example) with your own model provider API key.
## Run the example
The example is a complete project with about 3,500 lines of view code, so clone it instead of copying it. It requires Node.js 22.22.2 or later.
```bash theme={null}
git clone https://github.com/manufacts/mcp-use-property-search-example.git
cd mcp-use-property-search-example
npm ci
npm run dev
```
Open the Inspector URL printed by the CLI, select **Chat**, and configure a model provider with your own API key. The Inspector forwards view tools to the model only in this mode; the managed Manufact model cannot call them. Send these prompts in order:
```text theme={null}
Show me homes in San Francisco.
Now search the Mission, under $2M.
Remove the most expensive home.
Open the cheapest one on the map and save it.
Zoom in one step.
```
The first prompt calls `search-homes` and opens the view. Each later prompt calls a view tool, and the open map updates in place. Select **Fullscreen** in the view to switch display modes.
The project has four main files:
| File | Contents |
| - | - |
| `src/index.ts` | The staged listings, `search-homes`, and `get-listing-details` |
| `views/property-search/view.tsx` | The view, its view tools, and model context |
| `views/property-search/map.tsx` | The Leaflet map, Esri tiles, price pins, and camera controls |
| `views/property-search/cards.tsx` | Listing cards and the detail panel |
## How it works
The example combines five MCP Apps patterns. Each one works on its own in your server.
### Open the view once with the whole dataset
`search-homes` is the only tool that renders the view. Besides the matching listing IDs, it returns the whole staged catalog in `structuredContent`, so the view can filter any neighborhood locally without another server call. The tool description tells the model to use the view tools for follow-ups instead of calling `search-homes` again.
```typescript src/index.ts theme={null}
export const searchHomes = server.tool(
{
name: "search-homes",
title: "Search San Francisco homes",
description:
"Open the interactive HomeScout map for San Francisco homes. Call this once to put the map on screen. After that, use the view's tools, such as `search-in-view`, for every follow-up search.",
inputSchema: z.object({
location: z.string().optional(),
maxPrice: z.number().positive().optional(),
minBeds: z.number().int().min(0).optional(),
homeType: homeTypeSchema.optional(),
}),
outputSchema: z.object({
area: areaSchema.nullable(),
matchedIds: z.array(z.string()),
catalog: z.object({
listings: z.array(listingSchema),
areas: z.array(areaMetaSchema),
attribution: z.string(),
}),
}),
view: {
name: "property-search",
prefersBorder: false,
csp: {
resourceDomains: ["https://server.arcgisonline.com"],
connectDomains: ["https://server.arcgisonline.com"],
},
},
},
async ({ location = "San Francisco", maxPrice, minBeds = 0, homeType }) => {
const area = resolveArea(location);
const matched = LISTINGS.filter(
(listing) =>
(area === null || listing.area === area) &&
(maxPrice === undefined || listing.price <= maxPrice) &&
listing.beds >= minBeds &&
(homeType === undefined || listing.homeType === homeType)
);
return {
content: [
{
type: "text",
text: `HomeScout is open with ${matched.length} homes. Refine from here with the view tools.`,
},
],
structuredContent: {
area,
matchedIds: matched.map((listing) => listing.id),
catalog: { listings: LISTINGS, areas: AREAS, attribution: ATTRIBUTION },
},
};
}
);
```
This excerpt is shortened. The example also accepts `minPrice` and `minBaths` filters and returns the median price.
### Let the model drive the open view
[`useViewTool()`](/v2/typescript/mcp-apps/interactivity) registers a tool that exists only while the view is mounted. The host lists it to the model, and the handler runs inside the view with access to React state and refs. The example registers seven view tools. This one zooms the Leaflet map:
```tsx views/property-search/view.tsx theme={null}
const zoomMapDefinition = {
name: "zoom-map",
title: "Zoom the map",
description: "Zoom the live map in or out by whole steps.",
inputSchema: z.object({
direction: z.enum(["in", "out"]),
steps: z.number().int().min(1).max(4).optional().describe("Defaults to 1"),
}),
outputSchema: z.object({ zoom: z.number() }),
} as const;
// Inside the view component:
useViewTool(
zoomMapDefinition,
async ({ direction, steps = 1 }) => {
mapRef.current?.zoomBy(direction === "in" ? steps : -steps);
const zoom = mapRef.current?.zoom() ?? 0;
return {
content: [{ type: "text", text: `Zoomed ${direction} to level ${zoom.toFixed(1)}.` }],
structuredContent: { zoom },
};
}
);
```
| View tool | Purpose |
| - | - |
| `search-in-view` | Change the area, price, beds, baths, home type, or sort in place |
| `remove-listings` | Hide homes from the cards and map |
| `select-listing` | Fly to a home and open its detail card |
| `save-listings` | Save or unsave homes |
| `fit-visible-results` | Fit every visible home in frame |
| `zoom-map` | Zoom in or out |
| `pan-map` | Pan north, south, east, or west |
### Keep the model in sync with the view
Users also filter, remove, and select homes by clicking. [``](/v2/typescript/mcp-apps/model-context) reports the current view state to the model, so "remove the most expensive one" refers to what is on screen now:
```tsx views/property-search/view.tsx theme={null}
`${listing.id}: ${listing.address}, ${compactPrice(listing.price)}`)
.join("; ")}.`,
].join("\n")}
/>
```
### Load details with an app-only tool
`get-listing-details` has `visibility: "app"`, so MCP Apps hosts hide it from the model while the view can still call it. The view calls it with [`useCallTool()`](/v2/typescript/mcp-apps/interactivity) when the user selects a card or pin:
```typescript src/index.ts theme={null}
export const getListingDetails = server.tool(
{
name: "get-listing-details",
visibility: "app",
inputSchema: z.object({ id: z.string() }),
// ...
},
async ({ id }) => {
// Look up the listing and return its staged details.
}
);
```
```tsx views/property-search/view.tsx theme={null}
const detailsTool = useCallTool("get-listing-details");
// On card or pin selection:
void detailsTool.callTool({ id });
```
### Allow map tiles and fullscreen
The view loads tiles from `server.arcgisonline.com`, so the tool's `view.csp` lists that origin in `resourceDomains` and `connectDomains`. Without it, the host's [Content Security Policy](/v2/typescript/mcp-apps/content-security-policy) blocks the tiles and the map renders blank.
The view declares both display modes in `viewConfig` and requests fullscreen from a button with `useDisplayMode()`:
```tsx views/property-search/view.tsx theme={null}
export const viewConfig: ViewConfig = {
autoResize: true,
displayModes: ["inline", "fullscreen"],
};
// Inside the view component:
const { displayMode, availableDisplayModes, requestDisplayMode } = useDisplayMode();
const canFullscreen = availableDisplayModes.includes("fullscreen");
// On button click:
void requestDisplayMode({ mode: "fullscreen" });
```
## Adapt it to your app
* **Real listings:** replace the staged `LISTINGS` array with a call to your listing API inside `search-homes`. Keep the returned catalog small. For large datasets, return only the first page and fetch more from the view with an app-only tool.
* **Another tile provider:** change the tile URL in `map.tsx`, update both CSP lists, and show the attribution your provider's terms require.
* **Other domains:** the same shape fits any "search, then refine" app, such as hotels, restaurants, or job listings. Keep one rendering tool, move refinements into view tools, and report visible state with `ModelContext`.
See the [complete property search example](https://github.com/manufacts/mcp-use-property-search-example) for the runnable project, and [Interactivity](/v2/typescript/mcp-apps/interactivity) for more on view tools, app-only tools, and follow-up messages.
# Sentry
Source: https://docs.mcp-use.com/v2/typescript/cookbook/sentry
Trace MCP tool calls and report failures with Sentry.
Add Sentry tracing and error reporting to your existing Node.js mcp-use server. Each tool call gets a span with its tool name and duration. Returned tool errors and thrown exceptions also appear in Sentry Issues.
This recipe uses MCP middleware, so it does not need access to mcp-use's internal SDK instances. It monitors tool callbacks; it is not a replacement for instrumentation of the full HTTP transport or every MCP operation.
## Try the live example
Select **monitored\_report**, choose an **outcome**, and click **Execute**. Try `success` for a report, `tool_error` for a returned tool error, and `exception` for a thrown exception. No chat model or API key is needed to use the demo.
[Open the example in a new tab](https://inspector.manufact.com/inspector?embedded=true\&autoConnect=https%3A%2F%2Fmanufact-sentry-example.run.mcp-use.com%2Fmcp\&embeddedConfig=%7B%22singleTab%22%3Atrue%2C%22defaultTab%22%3A%22tools%22%2C%22visibleTabs%22%3A%5B%22tools%22%5D%7D).
This hosted demo sends traces and intentional errors to Manufact's demo Sentry project. The tool result is visible here; run your own copy with your DSN to inspect the corresponding issues and traces in your Sentry account.
## Prerequisites
* A working [mcp-use server](/v2/typescript/getting-started/quickstart) running on Node.js.
* A [Sentry project](https://sentry.io/) and its DSN, available under **Project Settings → Client Keys (DSN)**.
The demo tool runs locally and does not call an LLM or another paid API. Sentry receives the telemetry when a DSN is configured; without one, the tool still works but no events are sent.
## Add the integration
Install Sentry in your server project:
```bash theme={null}
npm install @sentry/node@10.63.0
```
Add your project's DSN to `.env`:
```bash .env theme={null}
MCP_USE_SENTRY_DSN=https://your-public-key@your-ingest-host/your-project-id
```
Keep `.env` out of version control. Initialize Sentry once in your server entry point, then add the middleware below. If your application already initializes Sentry, reuse that initialization rather than calling `Sentry.init()` again.
Copy this complete example into your server entry (`index.ts` in a project created with `create-mcp-use-app`), or keep your existing tools and add the initialization and middleware:
```typescript index.ts theme={null}
import * as Sentry from "@sentry/node";
import { MCPServer } from "mcp-use";
import { z } from "zod";
Sentry.init({
dsn: process.env.MCP_USE_SENTRY_DSN,
tracesSampleRate: 1.0,
sendDefaultPii: false,
// Explicit instrumentation keeps this recipe focused on MCP tool calls.
defaultIntegrations: false,
});
const server = new MCPServer({
name: "sentry-example",
version: "1.0.0",
description: "Trace a successful tool call, a tool error, or an exception.",
});
server.use("mcp:tools/call", (ctx, next) =>
Sentry.withIsolationScope((scope) => {
const toolName = ctx.params.name;
scope.setTag("mcp.tool.name", toolName);
return Sentry.startSpan(
{
name: `tools/call ${toolName}`,
op: "mcp.server",
attributes: {
"mcp.method.name": "tools/call",
"mcp.tool.name": toolName,
},
},
async (span) => {
try {
const result = await next();
if ("isError" in result && result.isError === true) {
span.setStatus({ code: 2, message: "internal_error" });
span.setAttribute("mcp.tool.is_error", true);
// Capture a stable message, not potentially sensitive tool output.
Sentry.captureMessage(
`MCP tool returned an error: ${toolName}`,
"error"
);
} else {
span.setStatus({ code: 1 });
}
return result;
} catch (error) {
span.setStatus({ code: 2, message: "internal_error" });
Sentry.captureException(error);
// Preserve normal MCP error handling instead of hiding the failure.
throw error;
}
}
);
})
);
/** Exercise success, returned tool errors, and thrown exceptions in Inspector. */
export const monitoredReport = server.tool(
{
name: "monitored_report",
description:
"Generate a sample report or deliberately fail to test Sentry.",
inputSchema: z.object({
outcome: z
.enum(["success", "tool_error", "exception"])
.default("success"),
}),
},
async ({ outcome }) => {
if (outcome === "exception") {
throw new Error("Demo report service unavailable");
}
if (outcome === "tool_error") {
return {
isError: true,
content: [
{
type: "text" as const,
text: "Demo report could not be generated.",
},
],
};
}
return {
content: [
{
type: "text" as const,
text: "Report generated: 3 orders, total $60.",
},
],
};
}
);
/** Sentry example server; the mcp-use CLI owns its HTTP listener. */
export default server;
```
`withIsolationScope` keeps each call's error tags separate from concurrent requests. `startSpan` ends the span when the callback finishes. Returning `isError: true` is a valid MCP result, so the middleware explicitly marks it as failed and reports a stable message. Exceptions are captured and rethrown to preserve normal MCP error handling.
## Try it
Start or restart your server so it loads `.env`:
```bash theme={null}
npm run dev
```
Open the Inspector URL printed by the CLI. Select **Tools → monitored\_report** and try each input:
| Input | MCP result | Sentry result |
| - | - | - |
| `{ "outcome": "success" }` | Sample report | Successful `tools/call monitored_report` span |
| `{ "outcome": "tool_error" }` | Tool error | Failed span and `MCP tool returned an error: monitored_report` issue |
| `{ "outcome": "exception" }` | MCP failure | Failed span and `Demo report service unavailable` exception |
In your Sentry project, look for the transaction/span named `tools/call monitored_report` in **Traces**, and the two error types in **Issues**. Error events have the `mcp.tool.name` tag. Delivery and indexing are asynchronous; leave the development server running briefly after making the calls.
## Adapt it to your server
* Replace `monitored_report` with your own tools. The middleware covers registered tool callbacks; discovery and failures rejected before callback dispatch are outside its scope.
* The demo samples every trace with `tracesSampleRate: 1.0`. Choose a suitable sample rate for production traffic.
* Tool arguments and returned content are not attached to telemetry. Captured exceptions still include their messages and stack traces; redact sensitive application errors using Sentry's `beforeSend` configuration as needed.
* `defaultIntegrations: false` intentionally disables automatic integrations for this focused example. Applications that already use Sentry should retain their existing setup and check for overlapping error capture. Automatic HTTP/database instrumentation has additional initialization requirements; this recipe only demonstrates explicit spans.
* For a long-running Node server, let Sentry send in the background. For a short-lived script, await `Sentry.flush(2000)` before exit. Serverless deployments need their platform's supported lifecycle integration so queued telemetry can finish; do not assume a returned HTTP response keeps the process alive.
See the [complete Sentry example](https://github.com/manufacts/mcp-use-sentry-example) for the runnable project and tests, [Sentry's custom instrumentation guide](https://docs.sentry.io/platforms/javascript/guides/node/tracing/instrumentation/custom-instrumentation/) and [error capture documentation](https://docs.sentry.io/platforms/javascript/guides/node/usage/). This example uses the public middleware API; Sentry's `wrapMcpServerWithSentry` targets underlying MCP SDK instances and should not be applied directly to the outer mcp-use server.
# TanStack Start
Source: https://docs.mcp-use.com/v2/typescript/cookbook/tanstack-start
Add an MCP endpoint to your existing TanStack Start app with one simple tool.
Expose a `greet` tool at `/api/mcp` in your existing TanStack Start app. TanStack Start serves your application and the MCP endpoint together.
## Prerequisites
* A working TanStack Start app using React, Vite 8, and TypeScript. Upgrade apps on older Vite majors before installing mcp-use, which requires Vite `^8.0.0`.
* Node.js 22.22.2 or later. This recipe configures Nitro for a Node.js server deployment.
## Add the integration
Install mcp-use, which includes the TanStack Start adapter, and Zod in your project:
```bash theme={null}
npm install mcp-use zod
```
If your app does not already use Nitro 3, install the version used by the example for the Node.js server build:
```bash theme={null}
npm install -D nitro@3.0.260903-beta
```
Keep the MCP server and its handler in `src/mcp`. Add a catch-all server route inside your existing `src/routes` directory:
```text theme={null}
my-tanstack-app/
├── src/
│ ├── mcp/
│ │ ├── server.ts # Add: define your server and tools
│ │ └── handler.server.ts # Add: create the MCP handler
│ ├── routes/
│ │ ├── __root.tsx
│ │ ├── index.tsx
│ │ └── api.mcp.$.ts # Add: mount the MCP endpoint
│ └── router.tsx
├── public/
├── vite.config.ts # Update: enable the adapter
├── package.json
└── tsconfig.json
```
### Define the server
Create `src/mcp/server.ts`. Register a tool that accepts a name and returns a greeting:
```typescript src/mcp/server.ts theme={null}
import { MCPServer } from "mcp-use";
import { z } from "zod";
const server = new MCPServer({
name: "my-tanstack-app",
version: "1.0.0",
basePath: "/api/mcp",
cors: {
origin: "*",
methods: ["GET", "HEAD", "POST", "DELETE", "OPTIONS"],
allowedHeaders: [
"Authorization",
"Content-Type",
"Accept",
"Mcp-Protocol-Version",
"Mcp-Method",
"Mcp-Name",
"Mcp-Session-Id",
"Last-Event-ID",
],
},
});
server.tool(
{
name: "greet",
description: "Greet a person.",
inputSchema: z.object({ name: z.string() }),
},
async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}!` }],
}),
);
export default server;
```
TanStack Start owns the HTTP listener, so do not call `server.listen()`. Configure CORS on `MCPServer` to allow browser clients such as the Inspector to connect.
### Configure the adapter
Add `mcpUseTanStackStart` before the Start and React plugins in your existing Vite configuration. Keep your application's other options and plugins:
```typescript vite.config.ts theme={null}
import { defineConfig } from "vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import react from "@vitejs/plugin-react";
import { nitro } from "nitro/vite";
import { mcpUseTanStackStart } from "mcp-use/tanstack-start/vite";
export default defineConfig({
plugins: [
mcpUseTanStackStart({
entry: "./src/mcp/server.ts",
basePath: "/api/mcp",
}),
tanstackStart(),
nitro({ preset: "node-server" }),
react(),
],
});
```
The plugin's `basePath` must match the one on `MCPServer`.
### Mount the endpoint
Create the handler in `src/mcp/handler.server.ts`. The Vite plugin loads the authored server in its dedicated MCP environment, so this route module does not import `server.ts`:
```typescript src/mcp/handler.server.ts theme={null}
import { createTanStackStartHandler } from "mcp-use/tanstack-start";
export const handler = createTanStackStartHandler();
```
Create a catch-all server route at `src/routes/api.mcp.$.ts`. Forward all HTTP methods to the handler so it can serve the MCP endpoint and nested asset paths:
```typescript src/routes/api.mcp.$.ts theme={null}
import { createFileRoute } from "@tanstack/react-router";
import { handler } from "../mcp/handler.server";
export const Route = createFileRoute("/api/mcp/$")({
server: {
handlers: { ANY: ({ request }) => handler(request) },
},
});
```
## Try it
Start your TanStack Start app with its usual command:
```bash theme={null}
npm run dev
```
In a second terminal, open the Inspector connected to your new endpoint:
```bash theme={null}
npx @mcp-use/inspector --url http://localhost:3000/api/mcp
```
Select **Tools → greet** and enter:
```json theme={null}
{
"name": "Ada"
}
```
Run the tool. It returns `Hello, Ada!`.
Use your app's port if it differs from `3000`. MCP views share the application's browser environment for React Fast Refresh and CSS HMR. Server edits replace the MCP instance and interrupt active requests; failed edits retain the last working instance. View registrations and skills refresh without restarting Vite. Configure React, CSS and aliases in the main Vite config.
See the [complete TanStack Start example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/server/examples/tanstack-start) for a runnable application with a shared UI card, an OAuth discovery route, and production build instructions.
# Upstash
Source: https://docs.mcp-use.com/v2/typescript/cookbook/upstash
Rate-limit MCP tool calls with Upstash Redis before running the tool.
Add a shared quota to your existing mcp-use server. [Upstash Redis](https://upstash.com/docs/redis/overall/getstarted) stores the counters, and `@upstash/ratelimit` checks them before a tool runs—even when requests reach different server instances.
This recipe allows three `generate_report` calls per 30-second sliding window. The report is a local calculation, so you can try the integration with just an Upstash account. Use the same middleware around an expensive API call, export, or sandbox operation.
## Try the live example
Select **generate\_report**, enter `[10, 20, 30]` in **values**, and click **Execute**. Repeat the call to see a report followed by a rate-limit error. No chat model or API key is needed to use the demo.
[Open the example in a new tab](https://inspector.manufact.com/inspector?embedded=true\&autoConnect=https%3A%2F%2Fmanufact-upstash-example.run.mcp-use.com%2Fmcp\&embeddedConfig=%7B%22singleTab%22%3Atrue%2C%22defaultTab%22%3A%22tools%22%2C%22visibleTabs%22%3A%5B%22tools%22%5D%7D).
All visitors share this demo's quota. You may hit the limit on your first call if someone else has used it. Wait about a minute and try again, or run your own copy below for an isolated demo.
## Prerequisites
* A working [mcp-use server](/v2/typescript/getting-started/quickstart).
* An [Upstash Redis database](https://console.upstash.com/redis) and its REST URL and read/write REST token. The read-only token cannot update rate-limit counters.
## Add the integration
Install the Redis client and rate limiter in your server project:
```bash theme={null}
npm install @upstash/redis@1.38.4 @upstash/ratelimit@2.0.8
```
Copy the credentials from your database's **REST** connection panel into `.env`. Use these prefixed names even though the Upstash console shows names without `MCP_USE_`:
```bash .env theme={null}
MCP_USE_UPSTASH_REDIS_REST_URL=https://your-database.upstash.io
MCP_USE_UPSTASH_REDIS_REST_TOKEN=your-rest-token
```
Keep `.env` out of version control. Both values stay on the server.
Add the highlighted middleware to your existing server, or copy this complete example into your server entry (`index.ts` in a project created with `create-mcp-use-app`). It checks only `generate_report`; other tools and tool discovery continue normally.
```typescript index.ts highlight={1-2,12-68} theme={null}
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
import { MCPServer } from "mcp-use";
import { z } from "zod";
const server = new MCPServer({
name: "upstash-example",
version: "1.0.0",
description: "Generate a report with a shared Upstash rate limit.",
});
let limiter: Ratelimit | undefined;
function toolError(message: string) {
return {
isError: true,
content: [{ type: "text" as const, text: message }],
};
}
server.use("mcp:tools/call", async (ctx, next) => {
if (ctx.params.name !== "generate_report") return next();
const url = process.env.MCP_USE_UPSTASH_REDIS_REST_URL?.trim();
const token = process.env.MCP_USE_UPSTASH_REDIS_REST_TOKEN?.trim();
if (!url || !token) {
return toolError(
"Set MCP_USE_UPSTASH_REDIS_REST_URL and MCP_USE_UPSTASH_REDIS_REST_TOKEN in .env and restart the server."
);
}
// Only provider setup/checking belongs in this catch; tool errors should
// retain their own meaning after next() runs.
try {
limiter ??= new Ratelimit({
redis: new Redis({ url, token }),
limiter: Ratelimit.slidingWindow(3, "30 s"),
prefix: "mcp-use:upstash-example",
analytics: false,
timeout: 3_000,
});
// All callers share this demo quota, including across server instances.
// For per-customer limits, key by a verified user/tenant ID + tool name.
const { success, reason, reset } = await limiter.limit("generate_report");
// Upstash allows calls on timeout by default. This example fails closed.
if (reason === "timeout") {
return toolError("The quota check timed out. Please try again shortly.");
}
if (!success) {
const retryAfterSeconds = Math.max(
1,
Math.ceil((reset - Date.now()) / 1_000)
);
return toolError(
`Rate limit reached: 3 reports per 30 seconds, shared by all callers. Retry in about ${retryAfterSeconds} seconds.`
);
}
} catch {
// Redis errors may contain request details; never return them to clients.
return toolError(
"Could not check the quota. Check your Upstash credentials and service availability, then try again."
);
}
return next();
});
/** Generate a local numeric report after the shared quota check succeeds. */
export const generateReport = server.tool(
{
name: "generate_report",
description:
"Summarize numeric values. Limited to 3 calls per 30 seconds across all callers; wait before retrying a rate-limit error.",
inputSchema: z.object({
values: z.array(z.number().min(-1e9).max(1e9)).min(1).max(1_000),
}),
},
async ({ values }) => {
// A fast local calculation keeps this demo independent of paid APIs.
// Replace this callback with your expensive report or sandbox operation.
const total = values.reduce((sum, value) => sum + value, 0);
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
count: values.length,
total,
average: total / values.length,
minimum: Math.min(...values),
maximum: Math.max(...values),
}),
},
],
};
}
);
/** Upstash example server; the mcp-use CLI owns its HTTP listener. */
export default server;
```
The middleware returns `isError: true` when the quota is exhausted, so the caller receives an MCP tool error and the report callback does not run. Missing credentials, Redis failures, and quota-check timeouts also block execution. Upstash normally allows requests after its timeout; the explicit `reason === "timeout"` check makes this example reject them.
## Try it
Start or restart your server so it loads `.env`:
```bash theme={null}
npm run dev
```
Open the Inspector URL printed by the CLI. Select **Tools → generate\_report** and enter:
```json theme={null}
{ "values": [10, 20, 30] }
```
A successful call returns:
```json theme={null}
{ "count": 3, "total": 60, "average": 20, "minimum": 10, "maximum": 30 }
```
From an unused quota, execute four calls in quick succession. The first three return reports; the fourth returns `Rate limit reached`. Its retry time is approximate because the sliding window also accounts for calls in the previous window. After a full minute without calls, try again. Restarting the server does not clear the Redis counters.
## Adapt the quota
* Change `Ratelimit.slidingWindow(3, "30 s")` to choose the allowance and window.
* Change `prefix` to separate deployments that use the same Redis database.
* Replace the constant `generate_report` key with a verified user or tenant ID plus the tool name for customer-specific quotas. Do not trust an ID supplied in tool arguments or an arbitrary header.
The limiter counts admitted attempts, even if the tool later fails. This demo does not authenticate callers; add authentication before using it to enforce customer quotas.
See the [complete Upstash example](https://github.com/manufacts/mcp-use-upstash-example) for the runnable project and tests, or [Upstash's rate-limit documentation](https://upstash.com/docs/redis/sdks/ratelimit-ts/overview) for more algorithms.
# Quickstart
Source: https://docs.mcp-use.com/v2/typescript/getting-started/quickstart
Scaffold, run, and test an MCP App with mcp-use.
For the complete documentation index, see
[llms.txt](https://mcp-use.com/docs/llms.txt).
Create an mcp-use app, run it locally, and render its View in the Inspector. The
generated project includes a server, two tools, and a React View.
This quickstart uses the `mcp-apps` template. To build a tool-only server, use
`--template mcp-server`. For an MCP client or agent, start with the [MCP
Client](/v2/typescript/client/index) or [MCP
Agent](/v2/typescript/agent/index) documentation.
## Prerequisites
* Node.js 22.22.2 or later
* npm, pnpm, or Bun
## Scaffold an mcp-use app
```bash npm theme={null}
npx create-mcp-use-app@latest my-mcp-app --template mcp-apps --install
```
```bash pnpm theme={null}
pnpm dlx create-mcp-use-app@latest my-mcp-app --template mcp-apps --install
```
```bash bun theme={null}
bunx create-mcp-use-app@latest my-mcp-app --template mcp-apps --install
```
With a project name and `--template`, the scaffolder does not prompt. `--install`
installs dependencies with the package manager that ran the command; without
it, run `npm install`, `pnpm install`, or `bun install` in the project before
starting the server. The scaffolder also installs the `mcp-apps-builder`
coding-agent skill, which requires Git. Pass `--no-skills` to skip it.
## Run the development server
```bash npm theme={null}
cd my-mcp-app
npm run dev
```
```bash pnpm theme={null}
cd my-mcp-app
pnpm dev
```
```bash bun theme={null}
cd my-mcp-app
bun run dev
```
`mcp-use dev` serves the MCP endpoint at `http://localhost:3000/mcp` and the
Inspector at `http://localhost:3000/mcp/inspector`. The Inspector opens
automatically in an interactive terminal.
## Explore the generated project
```text theme={null}
my-mcp-app/
├── public/
│ ├── icon.svg
│ └── logo-mcp-use.svg
├── views/
│ └── my-view/
│ ├── view.tsx
│ ├── view.css
│ ├── icons.tsx
│ ├── MeshGradient.tsx
│ └── Tooltip.tsx
├── index.ts
├── mcp-env.d.ts
├── package.json
└── tsconfig.json
```
* `index.ts` creates the server and registers the `show-app` and `say-hello`
tools.
* `views/my-view/view.tsx` contains the React View rendered by `show-app`.
* `mcp-env.d.ts` connects exported tool definitions to typed React hooks.
* `public/` contains assets served by the framework.
## Understand the View-bound tool
The generated `show-app` tool imports from `mcp-use`, declares `inputSchema` and
`outputSchema`, and binds the View with `view: { name: "my-view" }`.
```typescript index.ts theme={null}
import { MCPServer } from "mcp-use";
import { z } from "zod";
const server = new MCPServer({
name: "my-mcp-app",
version: "1.0.0",
});
export const showApp = server.tool(
{
name: "show-app",
description: "Display the starter MCP App",
inputSchema: z.object({
appName: z.string().optional().describe("Title shown in the MCP App"),
}),
outputSchema: z.object({
message: z.string().describe("Message displayed by the View"),
}),
view: {
name: "my-view",
description: "Starter MCP App View",
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
},
async ({ appName }) => {
const data = {
message: `The MCP App for ${appName ?? "my project"} is ready!`,
};
return {
content: [{ type: "text", text: data.message }],
structuredContent: data,
};
},
);
export default server;
```
Assign statically declared tools to exported constants. The generated
`mcp-env.d.ts` uses those exports to type View hooks. Export the server as the
default value so the CLI can run, build, and deploy it.
## Read tool results in the View
The View directory must match `view.name`. Use `useToolContext()` to read the
invocation that rendered the View:
```tsx views/my-view/view.tsx theme={null}
import { useToolContext } from "mcp-use/react";
export default function MyView() {
const view = useToolContext<"show-app">();
if (view.status === "pending") {
return Loading the app…
;
}
if (view.status === "error") {
return {view.error.message}
;
}
return {view.toolOutput.message}
;
}
```
When the tool succeeds, `view.toolOutput` is the typed value returned in
`structuredContent`. The host and model receive the text in `content`.
## Check the project
Run the generated type check after changing tools or Views:
```bash npm theme={null}
npm run typecheck
```
```bash pnpm theme={null}
pnpm typecheck
```
```bash bun theme={null}
bun run typecheck
```
The command refreshes `mcp-env.d.ts` and runs TypeScript without emitting build
files.
You can also call the server from the terminal:
```bash theme={null}
npx mcp-use client connect local http://localhost:3000/mcp
npx mcp-use client local tools list
```
## Deploy the project
Sign in to Manufact, then deploy the generated project. `deploy` does not sign
you in; without saved credentials or `MCP_USE_API_KEY`, it exits with
`Not logged in`.
```bash npm theme={null}
npx mcp-use login
npm run deploy
```
```bash pnpm theme={null}
pnpm exec mcp-use login
pnpm run deploy
```
```bash bun theme={null}
bunx mcp-use login
bun run deploy
```
The first deployment links the local project. See the
[Manufact deployment guide](/v2/typescript/server/deployment/mcp-use) for
environment variables, regions, and subsequent deployments.
## Next steps
Define validated inputs, outputs, annotations, and View-bound results.
Build production Views and integrate with MCP hosts.
Test and debug your server interactively.
Publish the server and configure production environments.
**Need help?** Join our [Discord](https://discord.gg/XkNkSkMz3V) or visit
[GitHub](https://github.com/mcp-use/mcp-use).
# Templates
Source: https://docs.mcp-use.com/v2/typescript/getting-started/templates
Ready-to-use MCP Apps you can deploy in one click or remix as your own.
Every template ships with a live demo, a public repo, and a one-click deploy button. Clone, customize, and ship.
Looking to scaffold a new project from scratch instead? Run `npx create-mcp-use-app@latest` - see the [Quickstart](/v2/typescript/getting-started/quickstart).
## Featured Templates
Generate charts from natural language. Tools: `create-chart`.
[Demo](https://yellow-shadow-21833.run.mcp-use.com/mcp) · [Deploy](https://mcp-use.com/deploy/start?repository-url=https%3A%2F%2Fgithub.com%2Fmcp-use%2Fmcp-chart-builder\&branch=main\&project-name=mcp-chart-builder\&port=3000\&runtime=node\&base-image=node%3A22)
Build and edit diagrams conversationally. Tools: `create-diagram`, `edit-diagram`.
[Demo](https://lucky-darkness-402ph.run.mcp-use.com/mcp) · [Deploy](https://mcp-use.com/deploy/start?repository-url=https%3A%2F%2Fgithub.com%2Fmcp-use%2Fmcp-diagram-builder\&branch=main\&project-name=mcp-diagram-builder\&port=3000\&runtime=node\&base-image=node%3A22)
Create and edit slide decks inline. Tools: `create-slides`, `edit-slide`.
[Demo](https://solitary-block-r6m6x.run.mcp-use.com/mcp) · [Deploy](https://mcp-use.com/deploy/start?repository-url=https%3A%2F%2Fgithub.com%2Fmcp-use%2Fmcp-slide-deck\&branch=main\&project-name=mcp-slide-deck\&port=3000\&runtime=node\&base-image=node%3A22)
Interactive maps with place details and markers. Tools: `show-map`, `get-place-details`, `add-markers`.
[Demo](https://super-night-ttde2.run.mcp-use.com/mcp) · [Deploy](https://mcp-use.com/deploy/start?repository-url=https%3A%2F%2Fgithub.com%2Fmcp-use%2Fmcp-maps-explorer\&branch=main\&project-name=mcp-maps-explorer\&port=3000\&runtime=node\&base-image=node%3A22)
Browse and preview Hugging Face Spaces. Tools: `search-spaces`, `show-space`, `trending-spaces`.
[Demo](https://gentle-frost-pvxpk.run.mcp-use.com/mcp) · [Deploy](https://mcp-use.com/deploy/start?repository-url=https%3A%2F%2Fgithub.com%2Fmcp-use%2Fmcp-huggingface-spaces\&branch=main\&project-name=mcp-huggingface-spaces\&port=3000\&runtime=node\&base-image=node%3A22)
Search recipes and plan meals. Tools: `search-recipes`, `get-recipe`. Prompts: `meal-plan`, `recipe-suggestion`.
[Demo](https://bold-tree-1fe79.run.mcp-use.com/mcp) · [Deploy](https://mcp-use.com/deploy/start?repository-url=https%3A%2F%2Fgithub.com%2Fmcp-use%2Fmcp-recipe-finder\&branch=main\&project-name=mcp-recipe-finder\&port=3000\&runtime=node\&base-image=node%3A22)
Reference gallery of every widget pattern. Tools: `show-react-widget`, `html-greeting`, `mcp-ui-poll`, `programmatic-counter`, `detect-client`.
[Demo](https://wandering-lake-mmxhs.run.mcp-use.com/mcp) · [Deploy](https://mcp-use.com/deploy/start?repository-url=https%3A%2F%2Fgithub.com%2Fmcp-use%2Fmcp-widget-gallery\&branch=main\&project-name=mcp-widget-gallery\&port=3000\&runtime=node\&base-image=node%3A22)
Aggregate and audit multiple MCP servers. Tools: `hub-status`, `hub-config-example`, `audit-log`.
[Demo](https://soft-voice-4nxfi.run.mcp-use.com/mcp) · [Deploy](https://mcp-use.com/deploy/start?repository-url=https%3A%2F%2Fgithub.com%2Fmcp-use%2Fmcp-multi-server-hub\&branch=main\&project-name=mcp-multi-server-hub\&port=3000\&runtime=node\&base-image=node%3A22)
Browse and read files through an MCP vault. Tools: `open-vault`, `get-file`, `list-files`.
[Demo](https://muddy-pond-eyays.run.mcp-use.com/mcp) · [Deploy](https://mcp-use.com/deploy/start?repository-url=https%3A%2F%2Fgithub.com%2Fmcp-use%2Fmcp-file-manager\&branch=main\&project-name=mcp-file-manager\&port=3000\&runtime=node\&base-image=node%3A22)
Long-running tools with progress and error handling. Tools: `process-data`, `fetch-report`, `delete-dataset`, `search-external`, `failing-tool`.
[Demo](https://crimson-river-pzsz1.run.mcp-use.com/mcp) · [Deploy](https://mcp-use.com/deploy/start?repository-url=https%3A%2F%2Fgithub.com%2Fmcp-use%2Fmcp-progress-demo\&branch=main\&project-name=mcp-progress-demo\&port=3000\&runtime=node\&base-image=node%3A22)
Adapt responses to caller locale and context. Tools: `show-context`, `detect-caller`.
[Demo](https://falling-grass-58yov.run.mcp-use.com/mcp) · [Deploy](https://mcp-use.com/deploy/start?repository-url=https%3A%2F%2Fgithub.com%2Fmcp-use%2Fmcp-i18n-adaptive\&branch=main\&project-name=mcp-i18n-adaptive\&port=3000\&runtime=node\&base-image=node%3A22)
Generate images, audio, PDFs, and more. Tools: `generate-image`, `generate-audio`, `generate-pdf`, and more.
[Demo](https://wandering-breeze-nuipu.run.mcp-use.com/mcp) · [Deploy](https://mcp-use.com/deploy/start?repository-url=https%3A%2F%2Fgithub.com%2Fmcp-use%2Fmcp-media-mixer\&branch=main\&project-name=mcp-media-mixer\&port=3000\&runtime=node\&base-image=node%3A22)
Subscribe to resources and toggle features live. Tools: `show-config`, `update-config`, `toggle-feature`, `list-roots`.
[Demo](https://fragrant-term-zmdks.run.mcp-use.com/mcp) · [Deploy](https://mcp-use.com/deploy/start?repository-url=https%3A%2F%2Fgithub.com%2Fmcp-use%2Fmcp-resource-watcher\&branch=main\&project-name=mcp-resource-watcher\&port=3000\&runtime=node\&base-image=node%3A22)
More templates, source code, and ready-to-run samples for Python and TypeScript.
## Deploy your own
Every template above is powered by [manufact.com](https://manufact.com) - connect your GitHub repo and your MCP Server ships in minutes with observability, metrics, logs, and branch deployments.
```bash theme={null}
npx @mcp-use/cli login
npx @mcp-use/cli deploy
```
# Welcome
Source: https://docs.mcp-use.com/v2/typescript/getting-started/welcome
Build MCP servers, apps, clients, and agents with mcp-use for TypeScript.
For the complete documentation index, see
[llms.txt](https://mcp-use.com/docs/llms.txt).
**mcp-use** is the full-stack MCP framework for TypeScript. Build MCP servers,
MCP Apps that render interactive Views in ChatGPT and Claude, MCP clients, and
tool-calling agents with end-to-end type safety.
## Get started
Scaffold an mcp-use app with the release of `create-mcp-use-app`:
```bash theme={null}
npx create-mcp-use-app@latest my-mcp-app
```
### Build a server and View
```typescript index.ts theme={null}
import { MCPServer } from "mcp-use";
import { z } from "zod";
const server = new MCPServer({
name: "greeter",
version: "1.0.0",
});
export const greet = server.tool(
{
name: "greet",
description: "Greet someone by name",
inputSchema: z.object({
name: z.string().describe("Name of the person to greet"),
}),
outputSchema: z.object({
greeting: z.string(),
}),
view: { name: "greeting" },
},
async ({ name }) => {
const data = { greeting: `Hello, ${name}!` };
return {
content: [{ type: "text", text: data.greeting }],
structuredContent: data,
};
},
);
export default server;
```
```tsx views/greeting/view.tsx theme={null}
import { useToolContext } from "mcp-use/react";
export default function Greeting() {
const view = useToolContext<"greet">();
if (view.status === "pending") return Loading…
;
if (view.status === "error") return {view.error.message}
;
return 👋 {view.toolOutput.greeting}
;
}
```
The default server export lets `mcp-use dev`, `mcp-use build`, and
`mcp-use start` manage the server lifecycle. The View directory name must match
the tool's `view.name`.
}>
Looking for the **Python** docs? Start with the [Python quickstart](/python/getting-started/quickstart).
Learn how MCP clients, servers, agents, tools, resources, and prompts fit
together.
## Choose your next step
Scaffold an MCP App, run it locally, and test its View.
Define tools, resources, prompts, authentication, and deployment.
Build interactive Views for ChatGPT, Claude, and other MCP hosts.
Connect to MCP servers programmatically.
Build agents that call tools through MCP connections.
**Need help?** Join our [Discord](https://discord.gg/XkNkSkMz3V) or visit
[GitHub](https://github.com/mcp-use/mcp-use).
# Overview
Source: https://docs.mcp-use.com/v2/typescript/mcp-apps
Understand how MCP tools render interactive UI with mcp-use.
An MCP App combines an `MCPServer`, a React component under `views/`, and a tool bound to that View. The `mcp-use` framework includes a Vite-based build system that discovers, builds, and serves your Views in development and production.
MCP Apps let your tools render interactive UI inside AI conversations. Build a View when users need to explore data, complete a form, review a preview, or act on a tool result.
## How a tool renders a View
Add `view: { name }` to a tool definition. The name must match a directory under `views/`.
```text theme={null}
product-search/
├── index.ts
├── views/
│ └── product-results/
│ └── view.tsx
├── public/
├── mcp-env.d.ts
└── package.json
```
The View-bound tool must declare an `outputSchema`. Its handler returns a normal MCP tool result with matching `structuredContent`.
```typescript theme={null}
import { MCPServer } from "mcp-use";
import { z } from "zod";
const server = new MCPServer({
name: "product-search",
version: "1.0.0",
});
export const searchProducts = server.tool(
{
name: "search-products",
inputSchema: z.object({
query: z.string(),
}),
outputSchema: z.object({
results: z.array(
z.object({
id: z.string(),
name: z.string(),
}),
),
}),
view: {
name: "product-results",
},
},
async ({ query }) => {
const results = [
{ id: "starter-kit", name: `${query} starter kit` },
{ id: "travel-kit", name: `${query} travel kit` },
];
return {
content: [
{
type: "text",
text: `Found ${results.length} products matching "${query}".`,
},
],
structuredContent: {
results,
},
};
},
);
export default server;
```
Each View can be bound to at most one tool. Export the tool reference so mcp-use can infer its input and output types inside the View.
## Read the tool result in the View
A View default-exports a React component. The component receives no React props. Read the tool lifecycle and result with `useToolContext()`.
```tsx theme={null}
import { useToolContext } from "mcp-use/react";
export default function ProductResults() {
const { status, toolInput, toolOutput, error } =
useToolContext<"search-products">();
if (status === "error") {
return {error.message}
;
}
if (status === "pending") {
return Searching for {toolInput?.query ?? "products"}…
;
}
return (
Product results
{toolOutput.results.map(({ id, name }) => (
- {name}
))}
);
}
```
## Choose what the model and View receive
MCP tool results provide separate channels for conversation output, structured data, and View-only metadata.
| Channel | Model receives it? | View receives it? | Use it for |
| - | - | - | - |
| `content` | Yes | Yes, as `content` | A concise result for the conversation and text-only clients |
| `structuredContent` | Yes | Yes, as `toolOutput` | Typed data that the model can reason over and the View can render |
| `_meta` | No | Yes, as `meta` | Data that is not visible to the model |
Do not place secrets in any result channel. A View runs on the client and can read its result data.
## Continue building
Create a project, run it locally, and render its View in the Inspector.
Call tools, open links, send follow-up messages, and change display modes.
Expose View state and visible UI information to future model turns.
Allow the external APIs, assets, and embedded content your View needs.
# ChatGPT Plugin Extensions
Source: https://docs.mcp-use.com/v2/typescript/mcp-apps/chatgpt-extensions
Add selected model context, app entrypoints, deep links, composer mentions, structured settings, and display modes with the TypeScript SDK.
Use ChatGPT Plugin Extensions to add app launchers, file viewers, incoming routes, composer mentions, native settings, and selected model context.
You can also set an initial display preference.
The TypeScript SDK converts your declarations to OpenAI extension metadata.
The host controls which extensions users can access.
Start with a [view-bound MCP App](/v2/typescript/mcp-apps/quickstart).
Each view needs one bound tool and an `outputSchema`.
Composer mention search uses a server tool and needs no View.
The examples below use the v2 TypeScript API.
| Goal | API |
| - | - |
| Open an app from navigation or a conversation | `tool.view.entrypoints` with `global` or `thread` |
| Open a file in a custom viewer | `tool.view.entrypoints` with `file` |
| Read an incoming app route | `useDeepLink()` |
| Search app-owned items in the composer | `server.mentions()` |
| Add native settings and action buttons | `server.settings()` |
| Attach selected evidence to the next model turn | `useModelContext()` |
| Request an initial presentation | `viewConfig.preferredDisplayMode` |
## Check host support
The [OpenAI platform support table](https://github.com/openai/mcp-extensions/blob/main/docs/spec.md#platform-support) describes the following expected support.
Its web column refers to the ChatGPT Work browser and excludes classic ChatGPT.
Availability depends on the host release.
| Extension | Desktop | Work web | iOS | Android |
| - | - | - | - | - |
| Global and thread entrypoints | Yes | Yes | Yes | Yes |
| File entrypoints | Yes | No | No | No |
| Deep links | Yes | Yes | Yes | No |
| Structured settings | Yes | Yes | Yes | Yes |
| Resource display modes | Yes | Yes | Yes | Yes |
ChatGPT supports `inline` and `fullscreen` display modes.
Other MCP Apps hosts can support `pip`.
Test each extension in the host that your users use.
## Attach evidence to the next model turn
Use `useModelContext()` when the user selects evidence to send with the next model turn.
For example, add a book summary when the user selects **Add to chat**.
Keep cart actions separate from attachment actions.
Import the hook from `mcp-use/react` and render it in your view.
The hook activates native context delivery. It needs no `viewConfig` option.
The SDK checks host capabilities during initialization and checks each block before delivery.
Calls made during initialization wait for the host connection.
### Add, remove, and clear selected evidence
This component adds a summary, lists selected attachments, and lets the user remove or clear them.
It catches action errors and shows the shared delivery error.
```tsx theme={null}
import { useState } from "react";
import {
useModelContext,
type ModelContextOperationResult,
} from "mcp-use/react";
export default function BookEvidence() {
const { attachments, pending, error, add, remove, clearAttachments } =
useModelContext();
const [busy, setBusy] = useState(false);
const [actionError, setActionError] = useState(null);
async function run(action: () => Promise) {
setBusy(true);
setActionError(null);
try {
await action();
} catch (failure) {
setActionError(
failure instanceof Error ? failure : new Error(String(failure)),
);
} finally {
setBusy(false);
}
}
const disabled = busy || pending;
const failure = actionError ?? error;
return (
{attachments.map(({ key, block }) => (
-
{block.title ?? block.type}
))}
{pending && Updating context…
}
{failure && {failure.message}
}
);
}
```
The hook returns these fields and methods:
| Member | Behavior |
| - | - |
| `attachments` | Read-only list of selected `{ key, block }` entries. The list can include unsynced entries after a delivery failure. |
| `pending` | `true` during initialization, image preparation, batching, or delivery. `false` alone does not prove that delivery succeeded. |
| `error` | Shared delivery or reconciliation error, or `null`. Catch each action's rejection to show validation and preparation errors. |
| `add(key, block)` | Add one attachment or replace the value at the same key. Keep the other attachments. |
| `remove(key)` | Remove one selected attachment. Use the key returned in `attachments`, including for restored entries. |
| `clearAttachments()` | Clear the shared attachment collection. Keep separately managed view state, descriptions, and restored background context. |
Each method returns `Promise<{ status: "synced" | "superseded" }>`.
`"synced"` confirms context delivery, not permanent selection or a visible composer chip.
`"superseded"` means a newer action replaced the operation before acknowledgment.
Catch rejected promises as shown above.
Use distinct, stable application keys, such as `book:summary` and `book:cover`.
Keys are shared across all hook instances in one view runtime.
Exact duplicate blocks under different keys reject.
Unmounting a hook consumer does not clear its selections.
### Choose a content block
The hook supports four block types: `text`, `image`, `resource_link`, and `resource`.
Each type accepts a friendly top-level `title`.
The title is also available in `attachments[i].block.title` after host restoration.
These blocks can replace the text block in the example above.
For the image and thumbnail, put a cover at `public/covers/book.png`.
```typescript theme={null}
import type { ModelContextBlock } from "mcp-use/react";
export const bookEvidence = {
summary: {
type: "text",
title: "Book details",
text: "An illustrated guide to garden plants, with 12 chapters.",
thumbnail: { src: "/covers/book.png", mimeType: "image/png" },
},
cover: {
type: "image",
title: "Book cover",
src: "/covers/book.png",
},
manual: {
type: "resource_link",
title: "Book manual",
name: "manual.pdf",
uri: "https://example.com/books/manual.pdf",
mimeType: "application/pdf",
},
sample: {
type: "resource",
title: "Reading sample",
resource: {
uri: "bookshop://books/garden-guide/sample",
mimeType: "text/plain",
text: "Chapter 1: Choose plants for the light in your garden.",
},
},
} satisfies Record;
```
Text blocks can include a `thumbnail` with an MCP Icon shape.
The thumbnail path resolves like the `Image` component's public path and stays a URL.
The SDK does not fetch thumbnail bytes. ChatGPT iOS currently ignores thumbnails.
Image `src` uses the same public path resolver as `Image`, including proxy and asset-host configuration.
Absolute URLs pass through the resolver.
The SDK fetches the image and converts it to native base64 content before changing the selection.
Source responses must contain PNG, JPEG, GIF, or WebP bytes with the matching `Content-Type`.
Browser CORS rules apply to remote URLs.
You can also supply base64 bytes directly:
```typescript theme={null}
import type { ModelContextImage } from "mcp-use/react";
export function bookCoverFromBytes(data: string): ModelContextImage {
return { type: "image", title: "Book cover", data, mimeType: "image/png" };
}
```
Use either `src` or `data` plus `mimeType` in an image block.
Do not combine these forms.
Returned image attachments contain `data` and `mimeType`, even when you supply `src`.
During image preparation, `pending` is `true` and the previous selection stays visible.
Other ready keys can sync independently.
A fetch failure, unsupported or missing `Content-Type`, or empty response rejects the action and preserves the previous attachment.
A later `add` or `remove` for the same key, or `clearAttachments`, cancels preparation and returns `"superseded"` for that action.
A single image is limited to 10 MiB of decoded bytes, whether supplied through `src` or base64 `data`. Source downloads stop when they exceed that limit, including when the response omits its size. An oversized image rejects without replacing the current selection. This is an SDK memory budget, not a host upload-size guarantee.
A late fetch response cannot restore the canceled attachment.
Named types include `ModelContextText`, `ModelContextImage`, `ModelContextResourceLink`, and `ModelContextResource`.
All are exported from `mcp-use/react`.
A resource link uses its native `title` field, which takes precedence over unrelated title metadata.
Text and image titles use OpenAI metadata.
Embedded-resource titles use SDK metadata; host display of these titles can vary.
Raw metadata and annotations are preserved. Conflicting shorthand and wire fields reject.
Audio blocks are not supported.
### Respect host removals
OpenAI host context can restore surviving attachments when the view initializes.
Restored keys start with `@restored:` and belong to that runtime.
Use these returned keys to remove restored entries.
Do not use the reserved prefix for new keys or infer original keys from titles, resource URIs, or list positions.
Confirmed host removals update `attachments` where the SDK can identify the removed blocks.
Do not add the same evidence again from a render effect or a cart update.
Keep removable evidence out of duplicate `useViewState` values or `ModelContext` descriptions.
The optional `audience` field accepts `"user"` and `"assistant"` and maps to MCP annotations.
Audience controls presentation. All blocks still reach the model.
Assistant-only content can be restored as background instead of a visible attachment.
Removal affects current context; it does not erase previous conversation turns or backend data.
### Combine context hooks
Render `useModelContext` in the initial view tree when you also use `useViewState` or `ModelContext`. Native delivery activates when the hook commits, before state and description effects; discarded renders do not change delivery.
The runtime sends attachments, structured view state, and descriptions through one native writer.
State and descriptions keep their model-visible JSON projection.
`clearAttachments()` preserves those separate contributions.
See [Model context](/v2/typescript/mcp-apps/model-context) to choose a channel for each value.
On OpenAI hosts, native host context restores model-visible state after the hook activates.
Private widget state keeps its separate persistence channel.
Views that do not render `useModelContext` keep their existing widget-state transport.
Activation rejects if model-visible widget `modelContent` or `imageIds` already exists, or a widget-state write was dispatched.
The SDK does not clear that persisted context for you.
Use a view without the conflicting state; reopening alone can retain the conflict.
A prior native write must finish, and OpenAI host readback must match the acknowledged context before activation.
### Handle delivery failures
A definite host rejection retains the requested selection and sets the shared `error`.
The next valid `add`, `remove`, or `clearAttachments` makes one fresh delivery attempt with the current selection.
Invalid input and background updates do not trigger another attempt.
The public hook has no retry method or automatic retry loop.
A successful model-context response does not require an OpenAI `updateId` in its metadata.
When present and valid, the SDK uses that ID to correlate a whole context revision.
The ID is not an attachment key or a compare-and-swap token.
Uncertain transport outcomes or ambiguous host ordering stop further publication and reject subsequent mutations.
A request that was already sent cannot be recalled.
Wait for outstanding remote writes to finish before you open a fresh view runtime.
Another notification or a hook remount does not establish that writes have finished.
### Verify attachment behavior
1. Open the view in the host that your users use.
2. Add a summary and a cover under different keys. Check that both remain selected.
3. Remove one attachment in the view. Check that the other remains.
4. Clear the attachments, then add one again.
5. Where the host supports composer removal, remove an attachment there and check the view's list.
6. Change the cart or filter state. Check that removed evidence does not return.
A simulated host can check protocol delivery and errors.
Use the native host to check composer chips, thumbnails, and host removal behavior.
## Add global and thread entrypoints
Declare `global` to add an app launcher in navigation.
Declare `thread` to add a content tab in a conversation.
Both launchers send `{}` to the tool.
Each conversation has its own thread app instance.
```typescript theme={null}
import { MCPServer } from "mcp-use";
import { z } from "zod";
const server = new MCPServer({ name: "parts-library", version: "1.0.0" });
server.tool(
{
name: "open_library",
title: "Parts library",
icons: [
{ src: "library.svg", mimeType: "image/svg+xml", sizes: ["20x20"] },
],
inputSchema: z.object({ category: z.string().default("all") }),
outputSchema: z.object({ category: z.string() }),
view: {
name: "library",
entrypoints: [{ type: "global" }, { type: "thread" }],
},
},
({ category }) => ({ content: [], structuredContent: { category } }),
);
```
Create `views/library/view.tsx` for this tool.
Render the initial tool result in the view.
The initial result already contains the launch data.
Global and thread tools must accept empty arguments.
Use optional fields or defaults for additional inputs.
Keep tools with required query inputs separate from launchers.
The MCP SDK validates tool calls against the input schema, including custom refinements.
Entrypoints open fullscreen in supporting hosts.
Entrypoint calls ignore the tool visibility hint.
Use a clear tool `title` to name the launcher.
### Set entrypoint tool icons
Set `icons` on the tool definition, alongside `name` and `title`, as shown above.
Put `library.svg` in `public/`; the SDK emits its absolute asset URL on `tools/list`.
Absolute HTTP(S) and image data URLs also work.
Follow the [OpenAI icon guidelines](https://github.com/openai/mcp-extensions/blob/main/docs/spec.md#icon-guidelines).
Use a monochrome SVG with a transparent background, `currentColor`, and a 20×20 viewport.
Use 1.33px strokes for stroke icons.
For example, save this book icon as `public/library.svg`:
```xml theme={null}
```
`view.entrypoints` declares launch locations in `_meta["openai/ui"].entrypoints`.
The separate top-level `icons` field supplies the tool's artwork.
Server constructor `icons` supplies server branding and remains the fallback when a tool has no icon.
Hosts prefer tool icons, then the local server icon or hosted app logo, then a generic icon.
Call `tools/list` to check the tool's `icons` and fetch each emitted `src`.
Test the SVG in light and dark host themes.
## Add a file entrypoint
Declare the file extensions that your view supports.
Each extension must start with a dot.
The host sends `file.name` and `file.resourceUri` to the tool.
```typescript theme={null}
import { MCPServer } from "mcp-use";
import { z } from "zod";
const server = new MCPServer({ name: "csv-viewer", version: "1.0.0" });
const fileInput = z.object({
file: z.object({
name: z.string().min(1),
resourceUri: z.string().trim().min(1),
}),
});
server.tool(
{
name: "open_csv",
title: "CSV viewer",
inputSchema: fileInput,
outputSchema: fileInput,
view: {
name: "csv",
entrypoints: [{ type: "file", extensions: [".csv"] }],
},
},
({ file }) => ({ content: [], structuredContent: { file } }),
);
```
Create `views/csv/view.tsx` for this tool.
The host also sends the file arguments to the view through `ui/notifications/tool-input`.
Treat `file.resourceUri` as an opaque host handle.
Use the host resource APIs to read the file.
The HTML view resource URI identifies the view, not the opened file.
Use a non-empty extension list.
Require both string fields when `file` is present.
Additional input fields must be optional or have defaults.
### Combine entrypoint types
One tool can declare all three entrypoint types.
For a combined tool, make the outer `file` field optional.
Keep `file.name` and `file.resourceUri` required when `file` is present.
Replace the CSV tool registration above with this registration:
```typescript theme={null}
const combinedInput = fileInput.partial();
server.tool(
{
name: "open_csv",
title: "CSV viewer",
inputSchema: combinedInput,
outputSchema: combinedInput,
view: {
name: "csv",
entrypoints: [
{ type: "global" },
{ type: "thread" },
{ type: "file", extensions: [".csv"] },
],
},
},
(args) => ({ content: [], structuredContent: args }),
);
```
The view must show an empty screen when the host sends no file.
### Check entrypoint metadata
Call `tools/list` to inspect the tool descriptor.
The CSV tool advertises these metadata fields:
```json theme={null}
{
"ui": { "resourceUri": "ui://views/csv.html" },
"openai/ui": {
"entrypoints": [{ "type": "file", "extensions": [".csv"] }]
}
}
```
These fields appear inside the tool's `_meta` object.
Typed `view.entrypoints` takes precedence over raw `_meta["openai/ui"].entrypoints`.
If you omit the typed field, existing raw entrypoint declarations pass through.
## Read incoming deep links
Use `useDeepLink()` to read a route that the host sends to a global entrypoint.
The hook uses the view's existing MCP Apps connection.
It reads the initial host context and updates when another deep link arrives.
```tsx theme={null}
import { useEffect, useState } from "react";
import { useDeepLink } from "mcp-use/react";
export default function LibraryView() {
const { url } = useDeepLink();
const [route, setRoute] = useState("/parts");
useEffect(() => {
if (url !== undefined) setRoute(url);
}, [url]);
const incoming = new URL(route, "https://app.example");
if (incoming.pathname === "/parts") {
const tag = incoming.searchParams.get("tag");
return (
Parts filter: {tag ?? "all"}
);
}
if (incoming.pathname === "/parts/hex-bolt") return Hex bolt
;
return Unknown route
;
}
```
The hook returns `{ url: string | undefined }`.
An undefined URL means the host has supplied no link.
Hosts without deep-link support also return an undefined URL.
The hook reads `hostContext["openai/deepLink"].url`.
It converts older `path` and `query` payloads to the same URL form.
It removes its subscription when the component unmounts.
Local route changes do not update the host URL.
Validate incoming routes before you load data.
Authorize data access on the server.
### Construct a deep link
Encode the plugin ID, tool name, and complete app-relative URL.
The app-relative URL must start with `/` and must not contain a fragment.
```typescript theme={null}
const pluginId = "parts-library"; // Replace with your registered plugin ID.
const toolName = "open_library";
const appUrl = "/parts?tag=bolt&sort=asc";
const link =
`https://chatgpt.com/plugins/${encodeURIComponent(pluginId)}` +
`/app/${encodeURIComponent(toolName)}?path=${encodeURIComponent(appUrl)}`;
```
Desktop links use `codex://plugins//app/?path=`.
iOS links use the `chatgpt` scheme.
Custom marketplace links include `@` after the plugin ID.
See the [deep-link specification](https://github.com/openai/mcp-extensions/blob/main/docs/spec.md#deep-links) for the native formats.
## Composer mention search
Register one combined search for app-owned people, files, channels, or other
items with `server.mentions`. It returns a normal typed `ToolRef` and requires
no View:
```typescript theme={null}
import { MCPServer, type ResourceLink } from "mcp-use";
const server = new MCPServer({ name: "workspace", version: "1.0.0" });
const items: ResourceLink[] = [
{
type: "resource_link",
uri: "workspace://people/u42",
name: "andrew",
title: "Andrew Khadder",
},
{
type: "resource_link",
uri: "workspace://files/f17",
name: "roadmap.md",
mimeType: "text/markdown",
},
{
type: "resource_link",
uri: "workspace://channels/c9",
name: "engineering",
title: "#engineering",
},
];
export const mentionSearch = server.mentions({
name: "workspace.mentions",
title: "Search workspace items",
search: async ({ query }, ctx) => {
// For a downstream search, pass ctx.signal to support cancellation.
const needle = query.toLowerCase();
return {
items: items.filter((item) =>
`${item.name} ${item.title ?? ""}`.toLowerCase().includes(needle),
),
};
},
});
```
Register before startup, using a non-blank tool name that does not collide
with another tool. Prefer one combined search per plugin; the mention
contract defines no selection rule for multiple providers.
The helper generates an input schema requiring a string `query` and an
output schema requiring `{ items: ResourceLink[] }`, using the official SDK
resource-link schema. Queries, including `""` and whitespace, reach your
callback unchanged. Normalize, rank, and limit results in your application.
An empty query can return initial suggestions or an empty list.
Supporting ChatGPT desktop hosts discover the tool through
`_meta["openai/extensions"]["mentions/search"] = {}` and call it with ordinary
`tools/call`. Success is `{ content: [], structuredContent: { items } }`;
no matches returns `items: []`. Invalid input or output and callback errors
use the ordinary tool error pipeline. Other hosts may ignore the marker and
can still call the tool explicitly. No client capability or iframe handshake
is required.
`title`, `description`, `icons`, `annotations`, `_meta`, and `visibility` use
the ordinary tool descriptor conventions. Search is annotated read-only by
default. Visibility defaults to `["app"]`. If you supply top-level
`visibility`, it takes precedence over raw `_meta.ui.visibility`; the helper
adds `"app"` without duplicates. For example, `visibility: "model"` produces
`["model", "app"]`. Unrelated UI and extension metadata survives without
mutating your input. The helper has no View option and does not advertise a
raw View resource URI.
Callbacks receive the same request context, OAuth/sign-in defaults, and
`ctx.signal` as ordinary tools. Use verified identity and application
permissions to return only items the caller may access. Apply authorization
again when resolving or acting on a returned URI: a link identifies an item
and does not grant access. Register resources or retrieval tools separately
when readable contents are needed; returned links do not automatically enter
`resources/list` or guarantee an immediate fetch.
Verify native composer insertion separately in a supporting desktop host.
## Add structured settings and actions
Register one settings pair before you start the server.
`server.settings()` creates read and update tools.
The host discovers these tools through the `openai/settings` capability.
This example keeps one shared settings object in memory.
The object resets when the process restarts.
```typescript theme={null}
import { MCPServer } from "mcp-use";
import { z } from "zod";
const server = new MCPServer({ name: "parts-settings", version: "1.0.0" });
let values: { units: "mm" | "in"; showGrid: boolean } = {
units: "mm",
showGrid: true,
};
server.tool(
{
name: "units_help",
title: "Measurement units help",
inputSchema: z.object({}),
annotations: { readOnlyHint: true },
},
() => ({
content: [
{ type: "text", text: "Choose millimeters or inches for dimensions." },
],
}),
);
server.settings({
fields: {
units: { schema: z.enum(["mm", "in"]), title: "Measurement units" },
showGrid: { schema: z.boolean(), title: "Show grid" },
},
layout: [
{
kind: "group",
title: "Display",
items: [
{ kind: "property", property: "units" },
{ kind: "property", property: "showGrid" },
{ kind: "tool", tool: "units_help", title: "About measurement units" },
],
},
],
read: () => ({ ...values }),
update: (set) => {
values = { ...values, ...set };
return { ...values };
},
});
```
For a production app, authorize each callback through its `RequestContext` argument.
Scope stored values to the correct account.
Persist partial updates atomically before you return all effective values.
The framework does not provide storage, account scoping, or concurrency control.
The `read` callback receives `ctx`.
The `update` callback receives `set` and `ctx`.
The context includes `ctx.request`, `ctx.signal`, and `ctx.client`.
OAuth servers also expose `ctx.auth`.
### Define fields and layout
Register at least one field.
Fields support boolean, string, string enum, number, and integer schemas.
Use a Standard Schema library with JSON Schema conversion, such as Zod.
Give each field a non-blank `title`.
You can also supply a `description`.
Primitive schema constraints still apply.
Use primitive schemas without value transforms.
Return an effective value for every field from both callbacks.
This rule also applies to optional fields and fields with schema defaults.
The wire schema omits default annotations.
Apply application defaults in the `read` callback.
Use `kind: "property"` for a field control.
Use `kind: "tool"` for a same-server action button.
Action tools must accept `{}` and exist before the server starts.
The MCP SDK validates action arguments when the host calls the tool.
The host places fields absent from `layout` in an Other settings group.
A regular tool action returns text feedback in settings.
A view-bound tool action opens an MCP App modal.
See the [structured settings contract](https://github.com/openai/mcp-extensions/blob/main/docs/spec.md#structured-settings) for host behavior.
### Check settings tools
The default tool names are `settings.read` and `settings.update`.
Set `readTool` and `updateTool` to use distinct, unused names.
The registration advertises the pair during modern discovery and legacy initialization.
| Call | Arguments | Structured result |
| - | - | - |
| `settings.read` | `{}` | `{ schema, values, layout? }` |
| `settings.update` | `{ set: { showGrid: false } }` | `{ values }` with all fields |
Updates preserve fields absent from `set` through your callback.
Empty patches, unknown keys, invalid values, and incomplete callback results produce tool errors.
## Set resource display modes
Export `viewConfig` directly from `views//view.tsx` to declare supported modes and an initial preference.
Build and development tools read the declaration before the view starts.
```tsx theme={null}
import type { ViewConfig } from "mcp-use/react";
export const viewConfig = {
autoResize: true,
displayModes: ["inline", "fullscreen"],
preferredDisplayMode: "fullscreen",
} satisfies ViewConfig;
export default function LibraryView() {
return Parts library ;
}
```
`displayModes` must include `inline` and contain no duplicates.
The default list is `["inline", "fullscreen", "pip"]`.
`autoResize` defaults to `true`.
With `autoResize: false`, use `useSendSizeChanged()` to report the view size.
`preferredDisplayMode` accepts `inline` or `fullscreen`.
The preference must appear in `displayModes`.
The preference has no default, and the host can ignore it.
Entrypoint launch behavior remains separate from this preference for ordinary tool calls.
The HTML resource content advertises the ChatGPT subset in `_meta["openai/ui"]`:
```json theme={null}
{
"availableDisplayModes": ["inline", "fullscreen"],
"preferredDisplayMode": "fullscreen"
}
```
The browser advertises the full configured list through MCP Apps capabilities.
This list can include `pip` for other hosts.
### Keep the declaration static
Use object literals, array literals, local `const` references, and spreads.
TypeScript `as` and `satisfies` wrappers also work.
Imported values and runtime expressions cause an extraction error when tooling detects the named configuration.
Keep the configuration and its shared object or array references immutable.
Tooling rejects writes to those references and calls that could mutate them during module initialization, including calls through local helpers.
Components can read the configuration through calls such as `useState` and `useMemo` after initialization.
Declare and export `viewConfig` locally in the view module. A value `export *`
without a local configuration causes an extraction error because tooling
cannot resolve the configuration through another module. Type-only wildcard
exports are allowed, as are unrelated value wildcards alongside a local
configuration.
### Request a mode at runtime
Use `useDisplayMode()` for mode changes after initialization.
Render from the reported `displayMode`.
A completed request does not guarantee that the host changed the mode.
```tsx theme={null}
import { useDisplayMode } from "mcp-use/react";
export function ExpandButton() {
const { displayMode, availableDisplayModes, requestDisplayMode } =
useDisplayMode();
if (!availableDisplayModes.includes("fullscreen")) return null;
if (displayMode === "fullscreen") return null;
return (
);
}
```
`availableDisplayModes` contains the intersection of the view modes and host modes.
If the host supplies no mode list, only `inline` is available.
Requests for other modes reject before the runtime sends them.
## Verify the integration
1. Call `tools/list` to check entrypoint metadata and settings tool names.
2. Call an entrypoint tool with its expected empty or file arguments.
3. Call `settings.read`, then update one field and check the complete result.
4. Run `npx mcp-use build` to check static `viewConfig` extraction.
5. Read the HTML view resource to check its display metadata.
6. Open the app in a supporting host to check launchers, routes, settings actions, and presentation.
Use [MCP Apps interactivity](/v2/typescript/mcp-apps/interactivity) for tool calls and host interactions inside the view.
Use [model context](/v2/typescript/mcp-apps/model-context) to share the view state with the model.
# Content Security Policy
Source: https://docs.mcp-use.com/v2/typescript/mcp-apps/content-security-policy
Configure the domains that MCP Apps widgets can load, call, and embed.
Content Security Policy (CSP) controls which domains your views can fetch from, load scripts/styles from, and embed. Views run in sandboxed iframes, so CSP must explicitly allow any external resources.
## Server and asset origins
Set **`MCP_URL`** to your server's public origin (OAuth resource URL, API calls, CSP `connectDomains`):
```env theme={null}
MCP_URL=https://myserver.com
```
Set **`MCP_ASSETS_URL`** when view JS/CSS and `public/` files are served from a different prefix (CDN, Supabase Storage):
```env theme={null}
MCP_ASSETS_URL=https://cdn.example.com/storage/v1/object/public/widgets
```
When `MCP_ASSETS_URL` is unset, assets use the same origin as `MCP_URL` (or the request origin behind a proxy).
At **`mcp-use build`**, `MCP_ASSETS_URL` rewrites manifest paths to full `https://` URLs for static upload workflows.
## CSP merge order
On each `resources/read`, the framework merges CSP in this order (high → low priority):
1. Author `view.csp` on the bound tool
2. `CSP_*_DOMAINS` env vars (per category) or the `CSP_URLS` shortcut
3. MCP auto-append: `MCP_URL` → `connectDomains`; assets origin → `resourceDomains`
`CSP_URLS` and per-category env vars rank **above** MCP auto-append. Duplicate origins keep the higher-priority entry's position.
## Add domains for one view
Declare CSP on the tool's `view:` config:
```typescript theme={null}
server.tool(
{
name: "weather",
view: {
name: "weather-display",
csp: {
connectDomains: ["https://api.weather.com"],
resourceDomains: ["https://cdn.weather.com"],
frameDomains: ["https://trusted-embed.com"],
},
},
},
async () => ({
/* ... */
}),
);
```
MCP Apps CSP fields: `connectDomains`, `resourceDomains`, `frameDomains`, `baseUriDomains`.
## Global CSP env vars
Use `CSP_URLS` when every view needs the same extra domains (shortcut for **all four** categories):
```env theme={null}
CSP_URLS=https://api.example.com,https://cdn.example.com
```
Override one category without affecting others:
```env theme={null}
CSP_CONNECT_DOMAINS=https://api.example.com
CSP_RESOURCE_DOMAINS=https://cdn.example.com
CSP_FRAME_DOMAINS=https://embed.example.com
CSP_BASE_URI_DOMAINS=https://myserver.com
```
## Static deployments
When assets live on static storage and the MCP server runs elsewhere:
```env theme={null}
MCP_URL=https://PROJECT.supabase.co/functions/v1/mcp-server
MCP_ASSETS_URL=https://PROJECT.supabase.co/storage/v1/object/public/widgets
CSP_URLS=https://PROJECT.supabase.co
```
## Verify CSP
The mcp-use Inspector provides a **CSP Mode Toggle** for testing:
* **Permissive**: Relaxed CSP for debugging
* **Widget-Declared**: Enforces the view's declared CSP (production-like)
CSP violations are logged in the console. Use Widget-Declared mode to catch
CSP issues before production deployment.
## Next Steps
* [MCP Apps](/v2/typescript/mcp-apps): View overview and routing
* [Self hosted](/v2/typescript/server/deployment/self-hosted): Run an MCP server in Docker
# Generative UI
Source: https://docs.mcp-use.com/v2/typescript/mcp-apps/generative-ui
Create an MCP App that generates UI on the fly
Generative UI allows models to create custom UI on the fly through a JSON Schema.
## What you'll build
You will create an MCP App that allows for dynamically generated content, powered by [JSON-Render](https://json-render.dev/docs).
The app:
* gives the model a catalog of allowed components and actions
* accepts a type-safe JSON-Render specification as structured input
* streams partial specifications into the mounted view
* renders the completed specification from `structuredContent`
The complete implementation is available in the [`generative-ui` example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/server/examples/views/generative-ui). For details about catalogs, specifications, and renderers, use the [official JSON-Render documentation](https://json-render.dev/docs).
## How the integration works
A JSON-Render catalog defines the components and actions the model can use. The catalog also produces the Zod schema and prompt guidance for the MCP tool.
The model writes a JSON-Render `spec` into the tool's structured arguments. While the tool call is pending, MCP Apps hosts send partial arguments to the view through `ui/notifications/tool-input-partial`. After validation, the tool returns the final specification in `structuredContent`.
The tool argument must be a structured object. Do not ask the model to stream a stringified JSON document. Structured input lets the MCP host parse and deliver usable partial values without repairing incomplete JSON strings in the view.
## Install JSON-Render
Install the JSON-Render core, React renderer, and shadcn component catalog:
```bash theme={null}
npm install @json-render/core @json-render/react @json-render/shadcn
```
This guide uses the prebuilt shadcn catalog. You can define a smaller custom catalog when your app needs tighter control over the generated interface.
## Define the component catalog
Create `views/generative-ui/catalog.ts` and export one catalog for both the server and the React view:
```ts theme={null}
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
export const catalog = defineCatalog(schema, {
components: {
...shadcnComponentDefinitions,
},
actions: {},
});
```
The catalog is the model's UI vocabulary and the runtime validation boundary. Only catalog components can appear in a valid specification.
For a production app, expose only the components the model needs. You can also add your own typed component definitions and actions. See [Catalogs in the JSON-Render documentation](https://json-render.dev/docs/catalog).
## Create the generative UI tool
Generate the tool's input schema from the catalog instead of maintaining a separate schema:
```ts theme={null}
import { MCPServer } from "mcp-use";
import { z } from "zod";
import { catalog } from "../views/generative-ui/catalog.js";
const server = new MCPServer({
name: "generative-ui",
version: "1.0.0",
});
const specSchema = catalog.zodSchema();
function catalogGuidance(): string {
const prompt = catalog.prompt();
const dynamicListsStart = prompt.indexOf("DYNAMIC LISTS (repeat field):");
const componentStart = prompt.indexOf("AVAILABLE COMPONENTS");
if (dynamicListsStart === -1 || componentStart === -1) return prompt;
return `STATE MODEL:
Put initial values in the top-level spec.state object. Use JSON Pointer paths such as /todos to refer to that state.
${prompt.slice(dynamicListsStart, componentStart)}${prompt.slice(componentStart)}`;
}
export const renderUi = server.tool(
{
name: "render-ui",
title: "Render UI",
description: `Render an interactive UI with the available JSON-Render catalog.
Generate one complete structured object in the spec argument. Do not emit JSONL patches or a stringified JSON document. Write spec.root and its root element first, then add referenced elements progressively. Every child reference in the final spec must exist.
${catalogGuidance()}`,
inputSchema: z.object({
spec: specSchema.describe(
"A JSON-Render specification that streams into the view while it is generated.",
),
}),
outputSchema: z.object({
spec: specSchema,
elementCount: z.number(),
}),
annotations: {
readOnlyHint: true,
openWorldHint: false,
},
view: {
name: "generative-ui",
description: "A live generative interface",
prefersBorder: false,
},
},
async ({ spec }) => {
const elementCount = Object.keys(spec.elements).length;
return {
content: [
{
type: "text",
text: `Rendered ${elementCount} UI elements.`,
},
],
structuredContent: { spec, elementCount },
};
},
);
export default server;
```
The tool description tells the model to write `spec.root` and the root element first. This ordering gives the view enough information to mount before the rest of the element map arrives.
`catalog.prompt()` includes output instructions for JSONL patch streaming. The
`catalogGuidance()` helper removes those instructions because this MCP tool
accepts one structured `spec` object. It preserves the catalog's component,
action, state, and event guidance.
Return the final specification in `structuredContent`. The view uses it after the tool completes and on hosts that provide a result without partial tool-input notifications. Keep `content` concise because it is the result the model reads.
## Register the React components
Create `views/generative-ui/view.tsx`. Define a registry that maps each catalog component to its React implementation:
```tsx theme={null}
import { defineRegistry } from "@json-render/react";
import { shadcnComponents } from "@json-render/shadcn";
import { catalog } from "./catalog.js";
const { registry } = defineRegistry(catalog, {
components: {
...shadcnComponents,
},
});
```
The catalog describes what the model may generate. The registry supplies the components that render those names. When you add a custom catalog component, add its React implementation to the registry too.
## Stream the generated interface
`useToolContext()` updates `toolInput` as the model generates the structured `spec` argument. Read that live input first, then use the validated `toolOutput` after the call completes:
```tsx theme={null}
import { JSONUIProvider, Renderer } from "@json-render/react";
import { ThemeProvider, useToolContext } from "mcp-use/react";
function GenerativeUiContent() {
const view = useToolContext<"render-ui">();
if (view.status === "error") {
return {view.error.message};
}
const spec = toRenderableSpec(
view.toolInput?.spec ??
(view.status === "ready" ? view.toolOutput.spec : undefined),
);
if (!spec) {
return Waiting for a renderable UI spec…;
}
return (
);
}
export default function GenerativeUi() {
return (
);
}
```
The `toRenderableSpec()` helper checks whether the partial input contains a root element and removes elements that have not received their props yet. Copy the helper from the [complete example](https://github.com/mcp-use/mcp-use/blob/main/libraries/typescript/packages/server/examples/views/generative-ui/views/generative-ui/view.tsx) rather than treating a pending value as a complete `Spec`.
`loading` tells JSON-Render that referenced children may still be on the way. The view can render the usable part of the interface without warning about missing children. When the host does not send partial input, the loading state remains visible until `structuredContent` arrives.
`ThemeProvider` applies the host's color scheme and CSS variables.
## Add state and interactions
JSON-Render specifications can include initial state, event bindings, and actions. Describe those capabilities in the tool instructions so the model generates working interactions instead of static controls.
For example, an addable task list needs:
* an array in `spec.state`
* a repeated element bound to that array
* an input bound to the new-task value
* a button event bound to an action such as `pushState`
Only promise an interaction in visible text when the specification contains the matching state and event binding. See the [JSON-Render React API](https://json-render.dev/docs/api/react) for providers, actions, visibility, validation, and state.
## Prepare the integration for production
* Keep the catalog limited to components and actions your app supports.
* Validate the completed specification with `catalog.zodSchema()`.
* Return the final specification through `structuredContent`.
* Include a concise text result for the model and hosts without views.
* Configure [Content Security Policy](/v2/typescript/mcp-apps/content-security-policy) for external images, APIs, scripts, or embeds.
## Next steps
* Run the complete [`generative-ui` example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/server/examples/views/generative-ui).
* Learn how to build a view with the [MCP Apps quickstart](/v2/typescript/mcp-apps/quickstart).
* Add tool calls and follow-up messages with [MCP App interactivity](/v2/typescript/mcp-apps/interactivity).
* Read the [JSON-Render quick start](https://json-render.dev/docs/quick-start) for custom catalogs and renderers.
# Interactivity
Source: https://docs.mcp-use.com/v2/typescript/mcp-apps/interactivity
Let a View call tools, send follow-up messages, open links, change display modes, and expose actions to the model.
Interactions in a View can target the MCP server, the model, the host, or the mounted UI. Choose the API based on where the action should happen.
| You want to... | Use |
| - | - |
| Call a server tool from the View | `useCallTool()` |
| Ask the model to continue the conversation | `useSendFollowUp()` |
| Open a URL through the host | `useOpenExternal()` |
| Change how the host displays the View | `useDisplayMode()` |
| Let the model act on the mounted View | `useViewTool()` |
## Call a server tool
Use `useCallTool()` when a button, form, or other control should call an MCP tool. The hook provides the last successful result, the latest error, and the current pending state.
Export the tool reference returned by `server.tool()` so TypeScript can infer its input and output types in the View.
```tsx theme={null}
import { useCallTool } from "mcp-use/react";
export function FruitDetailsButton({ fruit }: { fruit: string }) {
const { callTool, data, error, isPending } = useCallTool("get-fruit-details");
async function loadFruitDetails() {
try {
await callTool({ fruit });
} catch {
// The hook exposes the failure through `error`.
}
}
return (
{error && {error.message}
}
{data && {JSON.stringify(data.structuredContent, null, 2)}}
);
}
```
`callTool()` rejects when the tool returns an error or the request fails. The hook also places that failure in `error` so the View can render it.
## Ask the model to continue
Use `useSendFollowUp()` when an action requires a new model turn. The prompt appears as a user-authored message in the conversation.
```tsx theme={null}
import { useHostContext, useSendFollowUp } from "mcp-use/react";
export function CompareButton({ fruit }: { fruit: string }) {
const { hostCapabilities } = useHostContext();
const sendFollowUp = useSendFollowUp();
if (hostCapabilities?.message === undefined) {
return null;
}
return (
);
}
```
The promise resolves when the host accepts the message, not when the model responds. Use `useCallTool()` instead when the View already knows which server operation to perform.
## Open an external link
Views run in a sandbox. Use `useOpenExternal()` to ask the host to open a URL outside the View.
```tsx theme={null}
import { useHostContext, useOpenExternal } from "mcp-use/react";
export function ProducerLink({ url }: { url: string }) {
const { hostCapabilities } = useHostContext();
const openExternal = useOpenExternal();
if (hostCapabilities?.openLinks === undefined) {
return null;
}
return (
);
}
```
The host decides whether and how to open the URL. It may ask the user for confirmation.
## Change the display mode
Use `useDisplayMode()` when a View needs more or less space. Only offer modes included in `availableDisplayModes`.
```tsx theme={null}
import { useDisplayMode } from "mcp-use/react";
export function ExpandButton() {
const { displayMode, availableDisplayModes, requestDisplayMode } =
useDisplayMode();
if (
displayMode === "fullscreen" ||
!availableDisplayModes.includes("fullscreen")
) {
return null;
}
return (
);
}
```
A display-mode request is advisory. Read `displayMode` for the mode the host actually applies.
## Let the model act on the View
Use `useViewTool()` to expose an action that operates on the mounted UI. The tool exists while the component is mounted and its handler receives the latest React state.
```tsx theme={null}
import { useViewTool } from "mcp-use/react";
import { useState } from "react";
import { z } from "zod";
export function FruitResults() {
const [selectedFruit, setSelectedFruit] = useState();
useViewTool(
{
name: "highlight-fruit",
description: "Highlight a fruit in the current results",
inputSchema: z.object({
fruit: z.string(),
}),
},
async ({ fruit }) => {
setSelectedFruit(fruit);
return {
content: [{ type: "text", text: `Highlighted ${fruit}` }],
};
},
);
return (
{selectedFruit === undefined
? "No fruit selected"
: `Selected: ${selectedFruit}`}
);
}
```
`useCallTool()` lets the View call the server. `useViewTool()` lets the host or model call the mounted View.
## Next steps
* Use [Model context](/v2/typescript/mcp-apps/model-context) to share View state and visible UI information with the model.
* Configure [Content Security Policy](/v2/typescript/mcp-apps/content-security-policy) before connecting to external APIs or loading external assets.
# Model context
Source: https://docs.mcp-use.com/v2/typescript/mcp-apps/model-context
Choose what the model sees from tool results, structured view state, and the currently visible UI.
Use model context to give the next model turn the information it needs.
Tool results establish the initial context.
`useViewState` and `ModelContext` describe the view.
`useModelContext` manages selected native attachments.
Keep each piece of evidence in one context channel.
## Choose the right channel
| Need | Use |
| - | - |
| Return a concise tool result | A text `content` block |
| Send structured render data to the view | Tool-result `structuredContent`, read through `useToolContext` |
| Keep structured user choices model-visible | `useViewState` |
| Attach selected text, images, or resources | `useModelContext` |
| Describe what the user currently sees | `` |
| Keep ephemeral UI state private to the iframe | React `useState` |
| Persist durable business data | Your backend |
## Attach evidence to the next turn
Use `useModelContext()` to add selected text, images, resource links, or embedded resources to the next model turn.
The hook shares one attachment collection across the view.
You can add, replace, remove, and clear attachments with stable application keys.
Read [Attach evidence with ChatGPT Plugin Extensions](/v2/typescript/mcp-apps/chatgpt-extensions#attach-evidence-to-the-next-model-turn) for complete examples, image inputs, and error handling.
The guide also explains host removals and how to combine the hook with `useViewState` and `ModelContext`.
## Keep structured choices in view state
Use `useViewState` for selections, filters, drafts, and progress that future model turns should understand.
```tsx theme={null}
import { useViewState } from "mcp-use/react";
function Filters() {
const [state, setState] = useViewState({
selectedCategory: "all",
sort: "newest",
});
return (
);
}
```
The root state value must be a JSON-serializable object.
All components in one mounted view share it.
Separate view instances remain isolated.
By default, ChatGPT restores state through `window.openai.widgetState`.
Other MCP Apps hosts keep state for the current iframe lifetime.
After `useModelContext` activates native delivery, OpenAI host context can restore model-visible state and attachments.
Private UI persistence remains separate.
## Describe visible UI
Use `ModelContext` for natural-language descriptions that follow the React component lifecycle.
```tsx theme={null}
import { ModelContext } from "mcp-use/react";
function Dashboard() {
return (
);
}
```
Nested components serialize into an indented tree:
```text theme={null}
- Dashboard
- Revenue chart is visible
- Three alerts are unresolved
```
For descriptions from event handlers or other callbacks, store the description in React state and render it with `ModelContext`:
```tsx theme={null}
import { useState } from "react";
import { ModelContext } from "mcp-use/react";
function ProductPicker() {
const [selection, setSelection] = useState(null);
return (
<>
{selection && }
>
);
}
```
## Understand the merged snapshot
The runtime merges structured state and the serialized context tree into one complete object:
```json theme={null}
{
"selectedCategory": "audio",
"sort": "price",
"_uiContext": "- Dashboard\n - Revenue chart is visible"
}
```
`_uiContext` is a reserved model-visible field.
`useViewState` filters it from application state and rejects developer state with the same key.
Every write sends the complete snapshot.
On MCP Apps hosts, mcp-use sends the object as `structuredContent` and a JSON text block as `content` through `ui/update-model-context`.
On ChatGPT, views without `useModelContext` write the object to `modelContent` with `window.openai.setWidgetState`.
Views with `useModelContext` use the shared native writer described in [ChatGPT Plugin Extensions](/v2/typescript/mcp-apps/chatgpt-extensions#combine-context-hooks).
## Keep ephemeral values local
Use React `useState` for transient details that the model does not need, such as hover state, open tooltips, or animation progress.
```tsx theme={null}
import { useState } from "react";
function HoverPreview() {
const [hoveredId, setHoveredId] = useState(null);
return (
);
}
```
## Next steps
* Use [ChatGPT Plugin Extensions](/v2/typescript/mcp-apps/chatgpt-extensions#attach-evidence-to-the-next-model-turn) to add and remove selected evidence.
* Read the [`useViewState` API reference](https://mcp-use-typescript-api-reference.vercel.app/modules/mcp-use.react.html#useviewstate) for signatures, errors, and host behavior.
* Read the [`ModelContext` API reference](https://mcp-use-typescript-api-reference.vercel.app/modules/mcp-use.react.html#modelcontext) for lifecycle and nesting.
* Use [Interactivity](/v2/typescript/mcp-apps/interactivity) for tool calls and follow-up messages.
# Build your first MCP App
Source: https://docs.mcp-use.com/v2/typescript/mcp-apps/quickstart
Create, run, and verify an MCP App with a view-bound tool.
Create a TypeScript MCP App, run it locally, and verify that a tool returns a React view in the Inspector.
## What you'll build
This guide creates a local server with:
* an MCP endpoint at `http://localhost:3000/mcp`
* an Inspector at `http://localhost:3000/mcp/inspector`
* a `show-app` tool that returns a view
* a `say-hello` tool that the view calls
* a React view at `views/my-view/view.tsx`
## Prerequisites
* Node.js 22.22.2 or higher
* Basic TypeScript and React knowledge
## Create the project
Run the MCP Apps template and install its dependencies:
```bash theme={null}
npx create-mcp-use-app@latest my-mcp-app --template mcp-apps --install
cd my-mcp-app
```
With a project name and `--template`, the scaffolder does not prompt, and it installs dependencies only with `--install`. If you omit `--install`, run `npm install` in the project before you start the server. The scaffolder also installs the `mcp-apps-builder` coding-agent skill, which requires Git. Pass `--no-skills` to skip it.
The template also creates `README.md`, `.gitignore`, and the installed coding-agent skills. These are the files you edit most often:
```text theme={null}
my-mcp-app/
├── index.ts
├── mcp-env.d.ts
├── views/
│ └── my-view/
│ ├── view.tsx
│ ├── view.css
│ ├── icons.tsx
│ ├── MeshGradient.tsx
│ └── Tooltip.tsx
├── public/
│ ├── icon.svg
│ └── logo-mcp-use.svg
├── package.json
└── tsconfig.json
```
`public/icon.svg` is the server icon declared in `index.ts`. The view renders `public/logo-mcp-use.svg` with the `Image` component from `mcp-use/react`, which resolves root-relative paths against the `public/` folder.
## Run the server
Start the development server:
```bash theme={null}
npm run dev
```
This starts the MCP server, view build pipeline, and Inspector. Keep the process running while you test the app.
## Inspect the view tool
Open `index.ts` and find the `show-app` tool. The template also configures server branding and registers a second tool, `say-hello`, without a view. This excerpt shows the view-bound path you need to understand first.
```typescript theme={null}
// index.ts excerpt; server and z are defined above.
const showAppInputSchema = z.object({
appName: z
.string()
.optional()
.describe(
"Optional title shown in the MCP App card instead of the default",
),
});
const showAppOutputSchema = z.object({
message: z.string().describe("Message to display in the MCP App"),
});
export const showApp = server.tool(
{
name: "show-app",
title: "Show MCP App",
description: "Display the MCP App starter view",
inputSchema: showAppInputSchema,
outputSchema: showAppOutputSchema,
view: {
name: "my-view",
description: "MCP App starter card with interactive demo hooks",
prefersBorder: false,
csp: {
resourceDomains: [
"https://fonts.googleapis.com",
"https://fonts.gstatic.com",
],
},
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
},
async ({ appName }, ctx) => {
// Brief delay so the pending skeleton is visible during dev/inspector demos.
await new Promise((resolve) => setTimeout(resolve, 1500));
console.log(`The app name was: ${appName}`);
const data = { message: `The MCP App starter for ${appName} is ready!` };
return {
content: [{ type: "text", text: data.message }],
structuredContent: data,
};
},
);
```
`view.name` must match the folder under `views/`. In this template, `my-view` maps to `views/my-view/view.tsx`.
A view-bound tool must declare an `outputSchema`. The `structuredContent` object becomes view rendering data via `useToolContext()`. The `content` array is the short text result the model can read.
`view.csp` allows the Google Fonts stylesheet that `view.css` imports. See [Content Security Policy](/v2/typescript/mcp-apps/content-security-policy) for the other domain categories.
The `say-hello` tool returns `{ greeting }` in `structuredContent` and has no view. The view calls it with `useCallTool()`.
Both tools are assigned to exported constants. The generated `mcp-env.d.ts` reads these exports to type `useToolContext<"show-app">()` and `useCallTool("say-hello")` in the view.
## Inspect the React view
Open `views/my-view/view.tsx`. The view reads the tool invocation with `useToolContext()` and wraps its content in `ThemeProvider`, which applies the host theme.
The generated view also shows host context, display-mode buttons, external links, a follow-up message button, and a `set-counter` tool that the host can call while the view is mounted. This condensed excerpt keeps the template's data access and the **Update State** and **Say Hello** controls, without styling and decorative markup.
```tsx theme={null}
import {
ModelContext,
ThemeProvider,
useCallTool,
useToolContext,
} from "mcp-use/react";
import { useState } from "react";
export default function McpApp() {
const view = useToolContext<"show-app">();
const sayHello = useCallTool("say-hello");
const [count, setCount] = useState(0);
const pending = view.status === "pending";
const callResult = sayHello.data?.structuredContent?.greeting;
return (
{view.status === "error" ? (
<>
Failed to load MCP App
{view.error.message}
>
) : (
<>
{!pending && }
{view.toolInput?.appName?.trim() || "MCP App"}
{!pending && callResult !== undefined && (
Tool Response: {callResult}
)}
{!pending && sayHello.error !== undefined && (
{sayHello.error.message}
)}
>
)}
);
}
```
Views can render before the tool finishes. While `status` is `"pending"`, `toolInput` can hold partial arguments, so the heading reads `appName` from it. The starter view does not render the tool output. After you check `status === "ready"`, `view.toolOutput.message` holds the message from `structuredContent`.
`ModelContext` describes the current counter to the model. See [Model context](/v2/typescript/mcp-apps/model-context) for the available channels.
## Verify in the Inspector
Use the Inspector to confirm that the server and view work.
1. Open `http://localhost:3000/mcp/inspector`.
2. Go to the **Tools** tab.
3. Select `show-app`.
4. Run the tool with `{ "appName": "My App" }`. Without `appName`, the result message contains `undefined`.
5. Confirm that the view renders below the tool result and its heading shows "My App". The handler waits 1.5 seconds, so the view can show a loading skeleton first.
6. Select **Say Hello** in the view. Confirm that it shows `Tool Response: Hello, mcp-use!`.
If the tool runs but no view appears, check that `view.name` exactly matches the folder under `views/` and that the handler returns `structuredContent`.
## Next steps
* Use [`useToolContext()`](https://mcp-use-typescript-api-reference.vercel.app/modules/mcp-use.react.html#usetoolcontext) to read tool input, output, and lifecycle status.
* Use [`useCallTool()`](https://mcp-use-typescript-api-reference.vercel.app/modules/mcp-use.react.html#usecalltool) when a view needs to call another MCP tool. See [Interactivity](/v2/typescript/mcp-apps/interactivity) for follow-up messages, external links, display modes, and view tools.
* Configure [Content Security Policy](/v2/typescript/mcp-apps/content-security-policy) before loading external APIs, images, scripts, embeds, or static assets.
# OAuth
Source: https://docs.mcp-use.com/v2/typescript/server/authentication/index
Choose and configure OAuth authentication for MCP servers.
Configure an OAuth provider when an MCP server must identify callers or authorize access by user, organization, role, scope, or permission. `MCPServer` protects the transport, publishes discovery metadata, verifies bearer tokens, and exposes the verified identity on `ctx.auth`.
## Choose a provider
Pick the provider for your identity system. Most providers run an authorization server that supports Dynamic Client Registration (DCR), and your MCP server only verifies their tokens. For a provider without DCR, such as GitHub or Google, use the OAuth Proxy: your MCP server becomes the authorization server itself.
| Identity system | Provider |
| - | - |
| Auth0 | [Auth0](/v2/typescript/server/authentication/providers/auth0) |
| Better Auth | [Better Auth](/v2/typescript/server/authentication/providers/better-auth) |
| Clerk | [Clerk](/v2/typescript/server/authentication/providers/clerk) |
| Convex | [Convex](/v2/typescript/server/authentication/providers/convex) |
| Keycloak | [Keycloak](/v2/typescript/server/authentication/providers/keycloak) |
| Supabase | [Supabase](/v2/typescript/server/authentication/providers/supabase) |
| WorkOS | [WorkOS](/v2/typescript/server/authentication/providers/workos) |
| Scalekit | [Scalekit](/v2/typescript/server/authentication/providers/scalekit) |
| Another DCR-capable authorization server | [Custom Provider](/v2/typescript/server/authentication/providers/custom) |
| GitHub, Google, or another provider without DCR | [OAuth Proxy](/v2/typescript/server/authentication/providers/oauth-proxy) |
With a DCR provider, the MCP client registers and performs the authorization flow directly with the authorization server. Your MCP server does not handle authorization codes or exchange them for tokens. With the OAuth Proxy, your MCP server runs that flow: it shows a consent page, signs the user in upstream as one OAuth app, and issues its own tokens.
## Configure the server
Import the server from `mcp-use` and the provider from its dedicated OAuth entry point, then pass the provider's settings as options.
```typescript theme={null}
import { MCPServer } from "mcp-use";
import { oauthAuth0Provider } from "mcp-use/oauth/auth0";
const domain = process.env.AUTH0_DOMAIN;
if (!domain) throw new Error("AUTH0_DOMAIN is required");
const server = new MCPServer({
name: "secure-server",
version: "1.0.0",
oauth: oauthAuth0Provider({ domain }),
});
export default server;
```
When you omit an option, each provider in the table below reads the matching `MCP_USE_OAUTH_*` environment variable instead. With the variables set, you can call the factory with no arguments, for example `oauthAuth0Provider()`. An option you pass always takes precedence over its variable.
| Provider | Environment variables (option they replace) |
| - | - |
| Auth0 | `MCP_USE_OAUTH_AUTH0_DOMAIN` (`domain`), `MCP_USE_OAUTH_AUTH0_AUDIENCE` (`resource`) |
| Better Auth | `MCP_USE_OAUTH_BETTER_AUTH_URL` (`authURL`) |
| Clerk | `MCP_USE_OAUTH_CLERK_FRONTEND_API_URL` (`frontendApiUrl`) |
| Convex | `MCP_USE_OAUTH_CONVEX_AUTH_URL` (`authURL`) |
| Keycloak | `MCP_USE_OAUTH_KEYCLOAK_SERVER_URL` (`serverUrl`), `MCP_USE_OAUTH_KEYCLOAK_REALM` (`realm`), `MCP_USE_OAUTH_KEYCLOAK_AUDIENCE` (`resource`) |
| Scalekit | `MCP_USE_OAUTH_SCALEKIT_ENVIRONMENT_URL` (`environmentUrl`), `MCP_USE_OAUTH_SCALEKIT_RESOURCE_ID` (`resourceId`) |
| Supabase | `MCP_USE_OAUTH_SUPABASE_PROJECT_ID` (`projectId`), `MCP_USE_OAUTH_SUPABASE_URL` (`supabaseUrl`), `MCP_USE_OAUTH_SUPABASE_JWT_SECRET` (`jwtSecret`) |
| WorkOS | `MCP_USE_OAUTH_WORKOS_SUBDOMAIN` (`subdomain`) |
The OAuth Proxy reads only `MCP_USE_OAUTH_SECRET`, for its `secret` option, and always needs `upstream`. See [OAuth Proxy](/v2/typescript/server/authentication/providers/oauth-proxy).
The Auth0 and Keycloak `*_AUDIENCE` variables become the provider's `resource` when you don't pass one, so they override `MCP_URL`. The value must be the full MCP endpoint URL, with a path equal to `basePath` (default `/mcp`), or the server throws at startup.
The provider binds token verification to the resolved MCP resource. Set `MCP_URL` to the public server origin in production, or pass the provider's `resource` option when you need an explicit canonical MCP endpoint.
## Authorize a tool
An OAuth-configured server gives tool callbacks a provider-specific `ctx.auth.user`. All built-in providers normalize the stable subject to `id`; additional fields depend on the provider. The OAuth Proxy exposes the upstream user as `{ subject, profile }` unless you reshape it with `mapUser`.
```typescript theme={null}
server.tool(
{
name: "get_profile",
description: "Return the authenticated caller.",
},
async (_args, ctx) => {
const profile = {
id: ctx.auth.user.id,
email: ctx.auth.user.email ?? null,
scopes: ctx.auth.scopes,
permissions: ctx.auth.permissions,
};
return {
content: [{ type: "text", text: JSON.stringify(profile) }],
};
},
);
```
`scopes` come from the verified SDK auth information. `permissions` and `user` come from the provider's verified claim mapping. Use [User Context](/v2/typescript/server/authentication/user-context) for the complete shape and authorization patterns.
## What OAuth changes
When `oauth` is configured:
* protected-resource and authorization-server metadata are published for clients;
* MCP transport requests require a bearer token;
* invalid, expired, incorrectly resourced tokens, and tokens missing configured `requiredScopes` are rejected before tool code runs;
* callbacks receive the verified access token, identity, scopes, permissions, expiry, client ID when present, and resource.
To let clients connect and call some tools before signing in, add `mixedAuth: true` and declare `securitySchemes` with `{ type: "noauth" }` on each public or optional tool. See [Mixed Authentication](/v2/typescript/server/authentication/mixed-auth).
## Next steps
Use verified identity and authorization data inside callbacks.
Serve public, optional, and sign-in tools from one endpoint.
Integrate another DCR-capable authorization server.
Sign users in with GitHub, Google, or another provider without DCR.
Connect a TypeScript MCP client to an OAuth-protected server.
# Mixed Authentication
Source: https://docs.mcp-use.com/v2/typescript/server/authentication/mixed-auth
Serve public, optional, and sign-in tools from one OAuth-enabled mcp-use TypeScript server that works in Claude and ChatGPT.
Mixed authentication lets one MCP server URL expose tools that anyone can use next to tools that need sign-in. Clients connect and list tools without signing in. The server asks for sign-in only when a signed-out user calls a tool that needs it, and the client signs the user in and retries.
A shop is the typical case: people browse the catalog without an account and sign in when they check out. The same server works in Claude and ChatGPT.
## Enable mixed authentication
Keep the `oauth` provider on the server, add `mixedAuth: true`, and declare `securitySchemes` on each tool that does not need sign-in.
```typescript theme={null}
import { MCPServer } from "mcp-use";
import { oauthWorkOSProvider } from "mcp-use/oauth/workos";
import { z } from "zod";
const server = new MCPServer({
name: "shop",
version: "1.0.0",
oauth: oauthWorkOSProvider({
subdomain: process.env.WORKOS_SUBDOMAIN!,
requiredScopes: ["shop"],
}),
// Anyone can connect and list tools. Each tool's securitySchemes decides who can call it.
mixedAuth: true,
});
// Public: anyone can browse.
server.tool(
{
name: "browse_catalog",
description: "Browse the product catalog.",
securitySchemes: [{ type: "noauth" }],
},
async () => ({
content: [{ type: "text", text: "Coffee, Tea, Cocoa" }],
}),
);
// Sign-in with an extra scope. Refused before the callback runs unless the
// token carries "checkout" plus the provider's requiredScopes.
server.tool(
{
name: "create_checkout",
description: "Create a checkout for the selected products.",
inputSchema: z.object({ productIds: z.array(z.string()).min(1) }),
securitySchemes: [{ type: "oauth2", scopes: ["checkout"] }],
},
async ({ productIds }, ctx) => ({
content: [
{
type: "text",
text: await createCheckout(productIds, ctx.auth.user.id),
},
],
}),
);
// Omitted: sign-in with the provider's requiredScopes.
server.tool(
{ name: "order_history", description: "List the signed-in user's orders." },
async (_args, ctx) => ({
content: [{ type: "text", text: await ordersFor(ctx.auth.user.id) }],
}),
);
export default server;
```
WorkOS is only the example. Every mcp-use OAuth provider supports mixed authentication.
* `mixedAuth` requires an `oauth` provider. Setting it without one is a type error and throws a `TypeError` at construction. It is a constructor option, not a provider option.
* `mixedAuth` does not make anything public. Tools without `securitySchemes` still require sign-in, and resources and prompts always do. Only the tools' views load signed out.
* With `mixedAuth` off (the default), every request to the MCP endpoint needs a valid token.
* Every tool stays in `tools/list` for every caller. A signed-out user must see a sign-in tool, or the model never calls it and sign-in never starts.
* A server with `mixedAuth: true` and no `noauth` tools is valid. Anyone can connect and see what it offers, and every call needs sign-in. Directories and indexers can probe it before anyone signs in.
## Declare `securitySchemes` on a tool
`securitySchemes` is an array of schemes the tool accepts. It uses the same shape ChatGPT reads on `tools/list`. There are two scheme types:
* `{ type: "noauth" }`: the tool runs without a token.
* `{ type: "oauth2", scopes }`: the tool accepts a bearer token with `scopes` plus the provider's `requiredScopes`. `scopes` is required; use `[]` for the provider's `requiredScopes` alone.
The provider's `requiredScopes` is the baseline for every sign-in check.
| `securitySchemes` | Who can call the tool | `ctx.auth` |
| - | - | - |
| `[{ type: "noauth" }]` | Anyone. A token, if sent, is still verified | possibly undefined |
| `[{ type: "noauth" }, { type: "oauth2", scopes }]` | Anyone. The scopes are advertised, and the callback checks them | possibly undefined |
| `[{ type: "oauth2", scopes }]` | Signed-in callers whose token has every listed scope and the baseline | required |
| omitted | Signed-in callers whose token has the baseline | required |
* A tool requires sign-in unless its `securitySchemes` include `noauth`.
* A token that is sent is always verified, even on `noauth` tools. An invalid or expired token gets `401` so the client refreshes it instead of silently getting the signed-out result.
* `noauth` requires `mixedAuth: true`.
* `oauth2` requires an OAuth provider. It also works without `mixedAuth`; see [Without `mixedAuth`](#without-mixedauth).
* `scopes: []` requires the provider to have `requiredScopes`.
* The order of the array does not matter. Each type may appear once.
## Optional tools
An optional tool accepts both `noauth` and `oauth2`. It runs for everyone and branches on `ctx.auth`.
```typescript theme={null}
server.tool(
{
name: "recommend_products",
description: "Suggest products. Personalized when the user is signed in.",
securitySchemes: [{ type: "noauth" }, { type: "oauth2", scopes: [] }],
},
async (_args, ctx) => {
if (!ctx.auth) {
return {
content: [
{ type: "text", text: `Bestsellers: ${await bestsellers()}` },
],
};
}
const history = await orderHistory(ctx.auth.user.id);
return {
content: [
{
type: "text",
text: `Picked for you: ${await recommendFrom(history)}`,
},
],
};
},
);
```
| Request | Result |
| - | - |
| No token | The callback runs with no `ctx.auth` and returns bestsellers |
| Valid token | The callback runs with `ctx.auth` set and returns personalized picks |
| Expired or invalid token | Refused with `401` (or the ChatGPT error result) before the callback runs |
### Optional tools with scopes
The `oauth2` scopes of an optional tool advertise what unlocks more, but never refuse a caller. The server does not check them, even for a signed-in token that lacks them, so the callback checks them itself.
```typescript theme={null}
server.tool(
{
name: "recommend_products",
securitySchemes: [
{ type: "noauth" },
{ type: "oauth2", scopes: ["orders:read"] },
],
},
async (_args, ctx) => {
// Check the scope, not just ctx.auth: a signed-in token may lack orders:read.
if (ctx.auth?.scopes.includes("orders:read")) {
return personalized(ctx.auth.user.id);
}
return bestsellersResult();
},
);
```
A signed-in user without the scope gets the same result as a signed-out user. That is common: the user signed in earlier for a tool that needed fewer scopes, or declined a scope on the consent screen.
An optional tool cannot bring up a sign-in prompt. Its result text can tell
the user to sign in, but the prompt only appears when the user calls a sign-in
tool, or signs in or reconnects from the host.
## `ctx.auth` types
`server.tool()` reads the literal `securitySchemes` value. A tool that requires sign-in gets a required `ctx.auth`, and a tool that accepts `noauth` gets one that is possibly `undefined`.
```typescript theme={null}
server.tool(
{ name: "browse_catalog", securitySchemes: [{ type: "noauth" }] },
(_args, ctx) => {
const id: string | undefined = ctx.auth?.user.id;
return { content: [] };
},
);
server.tool({ name: "order_history" }, (_args, ctx) => {
const id: string = ctx.auth.user.id; // signed-out calls never reach here
return { content: [] };
});
```
When `securitySchemes` comes from a variable typed as `ToolSecurityScheme[]`, the array could include `noauth`, so `ctx.auth` is typed as possibly `undefined`. Write the array inline, or declare the variable `as const`, to get a required `ctx.auth`.
## Resources, prompts, and views
Resources, resource templates, and prompts have no `securitySchemes`. On an OAuth server they always require sign-in with the provider's `requiredScopes`, and their callbacks always get a required `ctx.auth`.
* `resources/read`, `prompts/get`, `completion/complete`, and `resources/subscribe` require sign-in. The list methods are always open on a `mixedAuth` server.
* Every tool's view loads signed out on a `mixedAuth` server, whatever the tool's `securitySchemes`. ChatGPT reads every view while an app is created, before anyone signs in, and a refused read blocks creation. A view is static UI; the data arrives in the tool result, which the tool's `securitySchemes` still gate.
* `subscriptions/listen` is open signed out only when it subscribes to no resources, so it carries only list-changed notifications.
* A refused resource read or prompt fetch always gets HTTP `401` or `403`.
Whether Claude or ChatGPT shows a sign-in prompt when a resource read or
prompt fetch is refused is untested. Both hosts document sign-in prompts only
for tool calls.
## Signed-out requests
On a `mixedAuth` server, a signed-out caller can always send:
* `initialize` (2025-era protocols) and `server/discover` (2026-07-28);
* `ping` and `logging/setLevel`, which read no data;
* `tools/list`, `resources/list`, `resources/templates/list`, and `prompts/list`;
* `resources/read` of a tool's view;
* `subscriptions/listen` without resource subscriptions;
* notifications;
* an HTML browser request for the landing page at the MCP path.
`tools/call` follows the tool's `securitySchemes`. Unknown tools require sign-in. Every other method, including `resources/read` of other resources, `prompts/get`, `tasks/*`, and the skills methods, requires sign-in.
## How hosts see a refused call
The server refuses a call before the callback runs. Claude and ChatGPT need the refusal in different forms, so mcp-use picks the format from the `User-Agent` header.
* **Claude and other spec clients** start OAuth only from HTTP `401`, or from `403` with `error="insufficient_scope"`, with a `WWW-Authenticate` header. Claude treats a `200` with `isError: true` as an ordinary tool failure.
* **ChatGPT** needs `securitySchemes` on each tool in `tools/list`, then a `200` tool result with `isError: true` and `_meta["mcp/www_authenticate"]`. It does not start OAuth from a `401` on a tool call.
A refused `tools/call` from a `User-Agent` that matches `chatgpt` or `openai` (case-insensitive) gets the tool-result format. Every other `tools/call`, including one without a `User-Agent`, and every refusal of another method gets HTTP.
No token:
```http theme={null}
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="Authentication is required for this request.", scope="shop checkout", resource_metadata="https://shop.example.com/.well-known/oauth-protected-resource/mcp"
```
Token missing a scope:
```http theme={null}
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", error_description="Additional permissions are required: checkout.", scope="shop checkout", resource_metadata="https://shop.example.com/.well-known/oauth-protected-resource/mcp"
```
ChatGPT, no token:
```json theme={null}
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [
{ "type": "text", "text": "Authentication is required for this request." }
],
"isError": true,
"_meta": {
"mcp/www_authenticate": [
"Bearer error=\"invalid_token\", error_description=\"Authentication is required for this request.\", scope=\"shop checkout\", resource_metadata=\"https://shop.example.com/.well-known/oauth-protected-resource/mcp\""
]
}
}
}
```
* `scope` is the full set the call needs, the baseline plus the tool's scopes, not only the missing ones. Claude does not reliably remember scopes from an earlier step-up.
* The messages are fixed defaults. An invalid or expired token carries the verifier's reason instead.
* Ordinary callback errors, such as an out-of-stock product, are ordinary tool results and never start OAuth.
## Tool list metadata
mcp-use advertises the resolved `securitySchemes` for every tool on a `mixedAuth` server, and for every tool that declares `securitySchemes` on other servers. The schemes appear at the top level of each tool, where ChatGPT reads them, and are mirrored in `_meta.securitySchemes`. The advertised `oauth2` scopes include the provider's `requiredScopes`, and `noauth` comes first.
| Declared `securitySchemes` | Advertised `securitySchemes` |
| - | - |
| `[{ type: "noauth" }]` | `[{ type: "noauth" }]` |
| `[{ type: "noauth" }, { type: "oauth2", scopes }]` | `[{ type: "noauth" }, { type: "oauth2", scopes: baseline + scopes }]` |
| `[{ type: "oauth2", scopes }]` | `[{ type: "oauth2", scopes: baseline + scopes }]` |
| omitted | `[{ type: "oauth2", scopes: baseline }]` |
```json theme={null}
{
"name": "create_checkout",
"inputSchema": {
"type": "object",
"properties": { "productIds": { "type": "array" } }
},
"securitySchemes": [{ "type": "oauth2", "scopes": ["shop", "checkout"] }],
"_meta": {
"securitySchemes": [{ "type": "oauth2", "scopes": ["shop", "checkout"] }]
}
}
```
ChatGPT ignores an `oauth2` scheme with an empty scope list. Give the provider `requiredScopes` so tools without `securitySchemes`, and optional tools with `scopes: []`, advertise a scope.
### Hand-written `_meta.securitySchemes`
Some servers already set `_meta.securitySchemes` on tools by hand for ChatGPT. mcp-use keeps it working and never enforces it.
| Case | Behavior |
| - | - |
| Tool without `securitySchemes`, server without `mixedAuth` | Passed through unchanged |
| Tool without `securitySchemes` on a `mixedAuth` server | Passed through unchanged, with a warning. The tool still requires sign-in |
| Tool that also declares `securitySchemes` | Replaced by the declared schemes, with a warning when the two differ |
Move the value to the top-level `securitySchemes` field to have mcp-use enforce it.
## Invalid configurations
These throw a `TypeError` at construction or registration instead of failing at request time.
| Configuration | Why it is rejected |
| - | - |
| `mixedAuth: true` without `oauth` | Nothing to sign in with. Also a type error |
| `noauth` without `mixedAuth` | Every request needs a token, so the tool could never be called signed out |
| `oauth2` without `oauth` | No provider to verify tokens |
| `oauth2` with `scopes: []` when the provider has no `requiredScopes` | ChatGPT ignores an `oauth2` scheme with empty scopes, so it would never offer sign-in |
| An empty array, or a scheme type that appears twice | Omit the field for the default; one challenge can only ask for one scope set |
| A scope that is not a single printable-ASCII RFC 6749 scope token (empty, whitespace, `"`, `\`, or non-ASCII) | Scopes are space-separated in challenges and tokens, so the value could never match a granted scope |
| An unknown scheme type or field | Includes typos such as `scope` for `scopes` |
## Without `mixedAuth`
`oauth` without `mixedAuth` keeps endpoint-wide authentication: every request needs a token with the provider's `requiredScopes`.
* `securitySchemes: [{ type: "oauth2", scopes }]` still works on tools. Those scopes are enforced on top of the baseline.
* Tools that declare `securitySchemes` advertise them. Tools without it advertise nothing, unless they set `_meta.securitySchemes` by hand.
* `noauth` throws at registration.
## Verify locally
The [mixed-oauth example](https://github.com/mcp-use/mcp-use/tree/canary/libraries/typescript/packages/server/examples/auth/mixed-oauth) runs a Better Auth authorization server and a `mixedAuth` server on `http://localhost:3000/mcp`, with a tool for every `securitySchemes` shape. Its names start with their access level: `public_ping` is public, `optional_welcome` is optional with the `profile` scope, and `protected_profile` omits `securitySchemes`. From the example's directory:
```bash theme={null}
pnpm dev
```
Call the public tool without a token. The response is `200` with the tool result:
```bash theme={null}
curl -s http://localhost:3000/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'mcp-protocol-version: 2026-07-28' \
-H 'mcp-method: tools/call' -H 'mcp-name: public_ping' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"public_ping","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
```
Call the sign-in tool without a token. The response is `401` with `WWW-Authenticate`:
```bash theme={null}
curl -si http://localhost:3000/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'mcp-protocol-version: 2026-07-28' \
-H 'mcp-method: tools/call' -H 'mcp-name: protected_profile' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"protected_profile","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
```
Send the same call as ChatGPT. The response is `200` with an `isError` result and `_meta["mcp/www_authenticate"]`:
```bash theme={null}
curl -s http://localhost:3000/mcp \
-H 'user-agent: ChatGPT/1.0' \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'mcp-protocol-version: 2026-07-28' \
-H 'mcp-method: tools/call' -H 'mcp-name: protected_profile' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"protected_profile","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
```
To test the full sign-in and retry loop in Claude or ChatGPT, run the example with `pnpm dev --tunnel`, restart it with `MCP_URL` set to the tunnel's origin, and add the tunnel's `/mcp` URL as a custom connector. The example's README lists every item and a test checklist.
## Next steps
Choose and configure an OAuth provider.
Read identity, scopes, and permissions inside callbacks.
# Auth0 Provider
Source: https://docs.mcp-use.com/v2/typescript/server/authentication/providers/auth0
Configure Auth0 Dynamic Client Registration authentication for an MCP server.
Use `oauthAuth0Provider` when Auth0 is the authorization server. MCP clients register directly with Auth0, and mcp-use verifies Auth0 access tokens against the resolved MCP resource.
## Configure Auth0
In the [Auth0 Dashboard](https://manage.auth0.com):
1. Enable the Resource Parameter Compatibility Profile.
2. Promote the login connections MCP clients may use to domain-level connections.
3. Create an API whose identifier is the canonical MCP resource, such as `https://mcp.example.com/mcp`.
4. Use the `rfc9068_profile_authz` token dialect if tools need Auth0 permissions in access tokens.
The API identifier must match the resource that mcp-use resolves for token verification. The provider has no `audience` option; it checks the token audience against that resource.
## Configure the server
Pass `domain` explicitly, as below, or set `MCP_USE_OAUTH_AUTH0_DOMAIN` and call `oauthAuth0Provider()` without it.
```bash theme={null}
AUTH0_DOMAIN=https://your-tenant.us.auth0.com
MCP_URL=https://mcp.example.com
```
```typescript theme={null}
import { MCPServer } from "mcp-use";
import { oauthAuth0Provider } from "mcp-use/oauth/auth0";
const domain = process.env.AUTH0_DOMAIN;
if (!domain) throw new Error("AUTH0_DOMAIN is required");
const server = new MCPServer({
name: "auth0-server",
version: "1.0.0",
oauth: oauthAuth0Provider({ domain }),
});
export default server;
```
When `MCP_USE_OAUTH_AUTH0_AUDIENCE` is set and you don't pass `resource`, the
provider uses its value as the MCP resource, overriding `MCP_URL`. It must be
the full MCP endpoint URL, such as `https://mcp.example.com/mcp`, with a path
equal to the server's `basePath`, or the server throws at startup. Unset
it if it still holds a different Auth0 API identifier.
## Use Auth0 identity and permissions
Auth0 normalizes the subject to `ctx.auth.user.id`. It also maps optional `email`, `name`, `nickname`, `picture`, `emailVerified`, and `updatedAt` fields plus `roles`. Token permissions are available at top-level `ctx.auth.permissions`.
```typescript theme={null}
import { z } from "zod";
server.tool(
{
name: "delete_document",
description: "Delete a document with the Auth0 permission.",
inputSchema: z.object({ documentId: z.string() }),
},
async ({ documentId }, ctx) => {
if (!ctx.auth.permissions.includes("delete:documents")) {
return {
isError: true,
content: [{ type: "text", text: "Forbidden" }],
};
}
await db.documents.delete({ id: documentId });
return {
content: [{ type: "text", text: `Deleted for ${ctx.auth.user.id}` }],
};
},
);
```
## Verify the setup
Confirm that the client registers with Auth0, the token audience is the resolved MCP resource, invalid tokens are rejected, and permission-protected tools reject callers without the required permission.
Compare with the maintained Auth0 example.
Configure Auth0 for MCP authorization.
Use normalized Auth0 identity and permissions.
# Better Auth Provider
Source: https://docs.mcp-use.com/v2/typescript/server/authentication/providers/better-auth
Use Better Auth's OAuth Provider plugin with an MCP server.
Use Better Auth when your application owns the OAuth authorization server.
Better Auth handles registration, sign-in, consent, and token issuance;
`mcp-use` advertises those endpoints and verifies the access-token JWTs.
## Configure the MCP resource server
Pass the full Better Auth issuer URL, including its base path:
```typescript theme={null}
import { MCPServer } from "mcp-use";
import { oauthBetterAuthProvider } from "mcp-use/oauth/better-auth";
const server = new MCPServer({
name: "better-auth-server",
version: "1.0.0",
oauth: oauthBetterAuthProvider({
authURL: "http://localhost:61843/api/auth",
resource: "http://localhost:43127/mcp",
}),
});
export default server;
```
The provider does not create or mount Better Auth. It derives Better Auth's
`/oauth2/authorize`, `/oauth2/token`, `/oauth2/register`, and `/jwks` endpoints
from `authURL`, then verifies JWT issuer, signature, expiration, and MCP
resource audience.
## Run Better Auth as a separate Hono app
The authorization server can be a separate application and origin. The normal
`mcp-use` server above owns `/mcp` and its protected-resource metadata; the Hono
app only mounts Better Auth, its discovery routes, and your login and consent
pages.
```typescript theme={null}
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { auth } from "./auth.js";
const app = new Hono();
app.on(["GET", "POST"], "/api/auth/*", (c) => auth.handler(c.req.raw));
serve({ fetch: app.fetch, port: 61843 });
```
Because the issuer contains `/api/auth`, also route Better Auth's RFC 8414
path-insertion metadata endpoint
`/.well-known/oauth-authorization-server/api/auth` to `auth.handler` or
`oauthProviderAuthServerMetadata(auth)`. Better Auth's OAuth Provider guide
documents the required discovery, login, and consent routes. If browser-based
MCP clients call this separate origin directly, configure Hono CORS for the MCP
origin and include that origin in Better Auth's `trustedOrigins`.
## Credential-free anonymous example
The repository's [runnable example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/server/examples/auth/better-auth)
runs a regular `mcp-use dev` process on port 43127 and a separate Hono/Better
Auth process on port 61843. It uses anonymous sign-in, so it requires no
Google/GitHub credentials, email service, or password UI.
For easy local setup, the example omits a database. Better Auth then uses
stateless cookie sessions and its memory adapter. Anonymous users, dynamic
clients, codes, and consent reset whenever the process restarts; configure a
persistent Better Auth database before using the pattern in production.
## Access the verified identity
```typescript theme={null}
server.tool({ name: "whoami" }, async (_params, ctx) => ({
content: [
{
type: "text",
text: JSON.stringify({
id: ctx.auth.user.id,
isAnonymous: ctx.auth.user.isAnonymous,
scopes: ctx.auth.scopes,
}),
},
],
}));
```
Profile and application-specific access-token fields must be added with Better
Auth's `customAccessTokenClaims`. The built-in mapper recognizes `email`,
`name`, `picture`, `email_verified`, `sid`, `is_anonymous`/`isAnonymous`,
`roles`, and `permissions` when present.
Configure Better Auth's OAuth server, discovery routes, claims, and
persistence.
# Clerk Provider
Source: https://docs.mcp-use.com/v2/typescript/server/authentication/providers/clerk
Configure Clerk OAuth authentication for an MCP server.
Use `oauthClerkProvider` when Clerk is the authorization server. MCP clients register directly with Clerk, and mcp-use verifies Clerk-issued access tokens.
## Configure Clerk
In the [Clerk Dashboard](https://dashboard.clerk.com/), enable Dynamic Client Registration for the application and copy its Frontend API URL. Enable and request `user:org:read` when tools need organization context.
Pass the Frontend API URL explicitly:
```bash theme={null}
CLERK_FRONTEND_API_URL=https://verb-noun-42.clerk.accounts.dev
# Optional when Clerk emits a dedicated access-token audience:
# CLERK_AUDIENCE=https://api.example.com
```
```typescript theme={null}
import { MCPServer } from "mcp-use";
import { oauthClerkProvider } from "mcp-use/oauth/clerk";
const frontendApiUrl = process.env.CLERK_FRONTEND_API_URL;
if (!frontendApiUrl) {
throw new Error("CLERK_FRONTEND_API_URL is required");
}
const server = new MCPServer({
name: "clerk-server",
version: "1.0.0",
oauth: oauthClerkProvider({
frontendApiUrl,
...(process.env.CLERK_AUDIENCE
? { audience: process.env.CLERK_AUDIENCE }
: {}),
scopesSupported: ["profile", "email", "offline_access", "user:org:read"],
}),
});
export default server;
```
`audience` is optional. Set it only when Clerk emits that exact `aud`; when omitted, the provider uses issuer-bound access-token verification.
## Use organization context
Clerk maps `org_id`, `org_role`, and `org_slug` to `organizationId`, `organizationRole`, and `organizationSlug`. Organization permissions are top-level `ctx.auth.permissions`.
```typescript theme={null}
server.tool(
{
name: "list_documents",
description: "List documents for the Clerk organization.",
},
async (_args, ctx) => {
const organizationId = ctx.auth.user.organizationId;
if (
!organizationId ||
!ctx.auth.permissions.includes("org:documents:read")
) {
return {
isError: true,
content: [{ type: "text", text: "Organization access required" }],
};
}
const documents = await db.documents.findMany({
where: { organizationId },
});
return {
content: [{ type: "text", text: JSON.stringify(documents) }],
};
},
);
```
The mapped user also includes `id`, optional `email`, `name`, `username`, `picture`, and `emailVerified`, plus `roles`.
Compare with the maintained Clerk example.
Review Clerk OAuth and DCR behavior.
Use Clerk identity, organization, and permissions.
# Convex Provider
Source: https://docs.mcp-use.com/v2/typescript/server/authentication/providers/convex
Configure Convex OAuth authentication for a TypeScript MCP server.
Use the Convex provider when you host your own OAuth authorization server using the [Convex OAuth Provider component](https://www.convex.dev/components/codefox-inc/oauth-provider). MCP clients register directly with your Convex deployment through Dynamic Client Registration, and your MCP server verifies Convex-issued tokens.
## Start from the template
Use the [Convex OAuth template](https://github.com/manufacts/mcp-oauth-convex-template) for a standalone MCP server with an authenticated `whoami` tool.
```bash theme={null}
git clone https://github.com/manufacts/mcp-oauth-convex-template.git
cd mcp-oauth-convex-template
npm ci
cp .env.example .env
```
Configure your Convex OAuth Provider as described below, set `MCP_USE_OAUTH_CONVEX_AUTH_URL` in `.env`, then run `npm run dev`. Connect your MCP client to `http://localhost:3000/mcp`.
The template hosts the MCP resource server. Your Convex deployment must provide the OAuth Provider component, login, consent, and token issuance; Convex Auth alone is not sufficient.
You can also [deploy the template to Manufact](https://mcp-use.com/deploy/start?repository-url=https%3A%2F%2Fgithub.com%2Fmanufacts%2Fmcp-oauth-convex-template). Set the issuer URL in the deployment environment before starting the server. For public or tunnel deployments, set `MCP_URL` to the public MCP server origin, without `/mcp`.
## Configure Convex
In your Convex deployment, set up the OAuth Provider component:
1. Install the [Convex OAuth Provider component](https://www.convex.dev/components/codefox-inc/oauth-provider) in your Convex project.
2. Enable **Dynamic Client Registration** on the OAuth component so MCP clients can register automatically.
3. Configure the allowed scopes for your deployment. At minimum, include `openid profile email`.
4. Deploy your Convex project and note the base URL of your Convex site. It will look like:
```text theme={null}
https://your-deployment-name.convex.site/oauth
```
## Set environment variables
```bash theme={null}
MCP_USE_OAUTH_CONVEX_AUTH_URL=https://your-deployment-name.convex.site/oauth
```
This must be the base URL of your Convex OAuth Provider endpoint, without a trailing slash.
## Configure the MCP server
```typescript theme={null}
import { MCPServer } from "mcp-use";
import { oauthConvexProvider } from "mcp-use/oauth/convex";
const server = new MCPServer({
name: "convex-server",
version: "1.0.0",
oauth: oauthConvexProvider({
authURL: process.env.MCP_USE_OAUTH_CONVEX_AUTH_URL!,
}),
});
await server.listen(3000);
```
You can also pass the URL directly instead of reading from an environment variable:
```typescript theme={null}
oauth: oauthConvexProvider({
authURL: "https://your-deployment-name.convex.site/oauth",
});
```
## Use Convex identity in tools
When a request is authenticated, tools receive the verified Convex identity on `ctx.auth`.
```typescript theme={null}
import { error, object } from "mcp-use";
server.tool(
{
name: "whoami",
description: "Return the verified Convex identity for this request.",
},
async (_args, ctx) => {
if (!ctx.auth) {
return error("Unauthorized");
}
return object({
id: ctx.auth.user.id,
clientId: ctx.auth.user.clientId,
scopes: ctx.auth.scopes,
permissions: ctx.auth.permissions,
expiresAt: ctx.auth.expiresAt,
});
},
);
```
Use [User Context](/v2/typescript/server/authentication/user-context) for broader access-control patterns inside tools.
## Verify the setup
Run the server and connect with an OAuth-capable MCP client.
```bash theme={null}
npm run dev
```
Confirm these cases:
* The client discovers Convex OAuth metadata from your MCP server.
* The client registers with your Convex deployment.
* Authenticated tool calls include `ctx.auth.user.id`.
* Tool calls without a valid bearer token are rejected.
When using the Inspector, set the **Scope** to `openid profile email` if the client does not send a scope by default.
## Next steps
Clone or deploy a standalone MCP server with Convex authentication.
Set up the OAuth Provider component in your Convex deployment.
Use Convex identity data inside tools.
# Custom Provider
Source: https://docs.mcp-use.com/v2/typescript/server/authentication/providers/custom
Integrate a DCR-capable OAuth authorization server with an MCP server.
Use `oauthCustomProvider` when your authorization server supports Dynamic Client Registration (DCR) but mcp-use does not provide a built-in adapter. The MCP client registers upstream; your server publishes metadata, verifies access tokens, and maps verified claims into application identity.
## Required contract
Import `MCPServer` from `mcp-use` and the provider contract from `mcp-use/oauth`.
```typescript theme={null}
import { MCPServer } from "mcp-use";
import {
createJwtVerifier,
oauthCustomProvider,
type OAuthAuthInfo,
type OAuthMetadata,
} from "mcp-use/oauth";
const issuer = "https://auth.example.com";
const oauthMetadata = {
issuer,
authorization_endpoint: `${issuer}/oauth/authorize`,
token_endpoint: `${issuer}/oauth/token`,
registration_endpoint: `${issuer}/oauth/register`,
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
code_challenge_methods_supported: ["S256"],
} satisfies OAuthMetadata;
type AppUser = {
id: string;
email?: string;
organizationId?: string;
};
const server = new MCPServer({
name: "custom-auth-server",
version: "1.0.0",
oauth: oauthCustomProvider({
oauthMetadata,
createTokenVerifier(resource) {
return createJwtVerifier({
issuer,
jwksUrl: new URL(`${issuer}/.well-known/jwks.json`),
resource,
});
},
mapAuthInfo(authInfo: OAuthAuthInfo) {
const payload = authInfo.extra?.payload as Record;
if (typeof payload.sub !== "string") {
throw new Error("Verified token is missing sub");
}
return {
user: {
id: payload.sub,
email: typeof payload.email === "string" ? payload.email : undefined,
organizationId:
typeof payload.organization_id === "string"
? payload.organization_id
: undefined,
},
payload,
permissions: Array.isArray(payload.permissions)
? payload.permissions.filter(
(value): value is string => typeof value === "string",
)
: [],
};
},
}),
});
export default server;
```
`createTokenVerifier(resource)` receives the resolved canonical MCP resource. The verifier must validate token signature or introspection, issuer, expiry, and that resource, then return SDK auth information including `scopes`, `expiresAt`, and the validated `resource`. `resource` must be a `URL` matching the MCP resource, `clientId` must be a string (`""` when the token has no client), and `expiresAt` must be in the future, in Unix seconds. Auth information that breaks this contract is refused like an invalid token.
If your authorization server issues standard JWTs, `createJwtVerifier` does all of that for you and is the same helper the built-in providers use. Given a `jwksUrl`, it checks the signature, issuer, required `exp` and `sub` claims, and resource binding. It then returns the auth information in the shape the SDK expects.
For standards-conformant JWT access tokens, the verifier checks the [`aud` claim](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3) for the MCP resource URL. The `aud` claim can be a string or an array containing the resource URL. [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html#section-2) defines `resource` as an authorization and token request parameter, not as a JWT access-token claim. For compatibility with providers that include a non-standard `resource` claim, the verifier accepts a string-valued `resource` claim equal to the MCP resource URL and uses it instead of `aud`. Pass `audience` when the provider's JWT audience differs from the MCP resource.
Write the verifier by hand when the provider does not issue JWTs, for example when you have to call a token introspection endpoint. Reject an invalid, expired, or revoked token by throwing `new OAuthError(OAuthErrorCode.InvalidToken, message)`, both exported from `mcp-use/oauth`. The client then gets the same sign-in challenge as for a missing token; see [How hosts see a refused call](/v2/typescript/server/authentication/mixed-auth#how-hosts-see-a-refused-call) for the format each host receives. An error that is not an `OAuthError`, such as a failed request to an unreachable introspection endpoint, is answered with `500`.
`oauthMetadata` is the authorization server's RFC 8414 metadata. It must describe the actual upstream endpoints and include the `registration_endpoint` used by MCP clients.
`mapAuthInfo(authInfo)` receives only verified SDK auth information. It must return:
* `user`: your provider-specific normalized user, with `id` as the stable subject;
* `payload`: the verified claims or introspection response;
* `permissions`: application permissions derived from verified data.
The server exposes those values as `ctx.auth.user`, `ctx.auth.payload`, and `ctx.auth.permissions`. `ctx.auth.scopes` remains the verifier's verified `authInfo.scopes`.
## Extend the server from the provider
Some authorization models need more than token verification: a consent step exposed as a tool, or a policy check before every tool call. Pass an optional `setup(host)` hook and the server invokes it once while mounting, before the first request is served.
```typescript theme={null}
import { oauthCustomProvider, type OAuthProviderHost } from "mcp-use/oauth";
const oauth = oauthCustomProvider({
createTokenVerifier,
oauthMetadata,
mapAuthInfo,
setup(host: OAuthProviderHost) {
for (const tool of host.listTools()) {
if (tool.annotations?.destructiveHint && !tool._meta?.["shop/tags"]) {
throw new Error(`Destructive tool "${tool.name}" needs shop/tags`);
}
}
host.use("mcp:tools/call", async (ctx, next) => {
// Let the consent tool through, or consent could never be recorded.
if (ctx.params.name === "grant_consent") return next();
if (!(await isAllowed(ctx.auth, ctx.params.name))) {
return {
isError: true,
content: [{ type: "text", text: "Complete consent first." }],
};
}
return next();
});
host.tool(
{ name: "grant_consent", description: "Record consent." },
async (_params, ctx) => recordConsent(ctx.auth.user),
);
host.instructions((current) =>
`${current ?? ""}\n\nCall grant_consent before other tools.`.trim(),
);
},
});
```
The host exposes:
| Member | Purpose |
| - | - |
| `resourceUrl`, `basePath` | The resolved canonical resource URL and the MCP endpoint path. |
| `mixedAuth` | Whether the server accepts signed-out clients. Throw from `setup` if your model needs anonymous discovery and this is `false`. |
| `use(pattern, handler)` | Registers `mcp:` middleware, like `server.use()`. |
| `tool`, `resource`, `resourceTemplate`, `prompt` | Register provider-owned items, like the `server` methods of the same name. |
| `listTools()` | Returns every tool registered so far, so the provider can validate the application's tools. |
| `instructions(transform)` | Rewrites the instructions text advertised to clients. Each call receives the result of the previous one. |
Provider-owned items go through the same OAuth gate as application items, and `ctx.auth` carries the provider's user type. On a [mixed authentication](/v2/typescript/server/authentication/mixed-auth) server, a provider tool can declare `securitySchemes` exactly like an application tool: with `noauth`, signed-out clients can call it and `ctx.auth` is optional. Resources, resource templates, and prompts always require sign-in. `mcp:` middleware also runs on signed-out calls to `noauth` tools, so check `ctx.auth` before relying on it. Registering a name that the application already uses throws, so reserved names never silently replace application items.
`setup` must be synchronous, and the host only works while it runs; calling it later throws. If `setup` throws or returns a promise, the server does not start, and every later request fails with the same error.
## Verify the integration
Test that:
* the client discovers metadata containing the correct registration endpoint;
* client registration, PKCE authorization, and token exchange happen upstream;
* tokens with the wrong issuer, expiry, signature, or MCP resource are rejected;
* mapped identity and permissions contain only verified data.
## Next steps
Use the mapped user, scopes, and permissions in callbacks.
Compare built-in providers.
# Keycloak Provider
Source: https://docs.mcp-use.com/v2/typescript/server/authentication/providers/keycloak
Configure Keycloak Dynamic Client Registration authentication for an MCP server.
Use `oauthKeycloakProvider` when a Keycloak realm is the authorization server. MCP clients register directly with Keycloak, and mcp-use verifies realm-issued access tokens against the resolved MCP resource.
## Prepare Keycloak
Configure Dynamic Client Registration for the hosts used by your MCP clients. Keep redirect URI policies strict, and add browser client origins when browser-based clients require them.
For production, only require Initial Access Tokens after confirming that every intended MCP client can send one during registration. The mcp-use provider verifies tokens; it does not perform registration on the client's behalf.
## Configure the server
`serverUrl` and `realm` are required. Pass them explicitly, as below, or set `MCP_USE_OAUTH_KEYCLOAK_SERVER_URL` and `MCP_USE_OAUTH_KEYCLOAK_REALM` and call `oauthKeycloakProvider()` without them.
```bash theme={null}
KEYCLOAK_SERVER_URL=https://keycloak.example.com
KEYCLOAK_REALM=mcp
MCP_URL=https://mcp.example.com
```
```typescript theme={null}
import { MCPServer } from "mcp-use";
import { oauthKeycloakProvider } from "mcp-use/oauth/keycloak";
const serverUrl = process.env.KEYCLOAK_SERVER_URL;
const realm = process.env.KEYCLOAK_REALM;
if (!serverUrl || !realm) {
throw new Error("KEYCLOAK_SERVER_URL and KEYCLOAK_REALM are required");
}
const server = new MCPServer({
name: "keycloak-server",
version: "1.0.0",
oauth: oauthKeycloakProvider({ serverUrl, realm }),
});
export default server;
```
There is no Keycloak provider `audience` option. Verification is bound to the resolved MCP resource, so configure Keycloak to issue access tokens for that resource.
When `MCP_USE_OAUTH_KEYCLOAK_AUDIENCE` is set and you don't pass `resource`,
the provider uses its value as the MCP resource, overriding `MCP_URL`. It must
be the full MCP endpoint URL, such as `https://mcp.example.com/mcp`, with a
path equal to the server's `basePath`, or the server throws at startup.
Unset it if it still holds a client ID or another audience value.
## Use roles and permissions
Realm roles are `ctx.auth.user.roles`. Resource roles are flattened into top-level permissions as `resource:role`.
```typescript theme={null}
server.tool(
{
name: "admin_action",
description: "Run an operation for a Keycloak resource administrator.",
},
async (_args, ctx) => {
if (!ctx.auth.permissions.includes("mcp-api:admin")) {
return {
isError: true,
content: [{ type: "text", text: "Forbidden" }],
};
}
return {
content: [{ type: "text", text: `Allowed ${ctx.auth.user.id}` }],
};
},
);
```
The mapped user also includes optional profile fields and the raw `realmAccess` and `resourceAccess` objects. Prefer normalized `roles` and `permissions`; inspect raw access objects only when the flattened form is insufficient.
## Production checks
* Serve both Keycloak and the MCP server over HTTPS.
* Configure token audience/resource claims for the canonical MCP endpoint.
* Restrict DCR according to the capabilities of your MCP clients.
* Decide whether each tool uses realm roles, flattened resource permissions, or both.
Compare with the maintained Keycloak example.
Configure Dynamic Client Registration.
Use Keycloak user fields, roles, and permissions.
# OAuth Proxy
Source: https://docs.mcp-use.com/v2/typescript/server/authentication/providers/oauth-proxy
Sign MCP clients in with GitHub, Google, or another OAuth provider that has no Dynamic Client Registration, using oauthProxy in a TypeScript MCP server.
Use `oauthProxy` when your identity provider gives you one fixed `clientId` and `clientSecret` and cannot register MCP clients itself. GitHub and Google work this way. With `oauthProxy`, your MCP server becomes its own OAuth authorization server for MCP clients, and signs users in to the upstream provider as one ordinary OAuth app.
The upstream client secret and the user's upstream tokens stay on your server. MCP clients only receive tokens that your server issues. Tools call the upstream API as the user with `upstreamToken(ctx.auth)`.
If your provider supports Dynamic Client Registration (DCR), use a [built-in provider](/v2/typescript/server/authentication/index) or a [Custom Provider](/v2/typescript/server/authentication/providers/custom) instead. Those keep your server a resource server only.
## How the proxy works
The proxy sits between two OAuth relationships. Toward MCP clients, it is the authorization server: it identifies clients, shows a consent page, and issues its own access and refresh tokens. Toward the upstream provider, it is a confidential OAuth client with one registered callback URL.
| Who holds it | Credential |
| - | - |
| MCP client | The proxy's access token (`mcp_at_…`) and refresh token (`mcp_rt_…`) |
| Your server's storage | Clients, sign-in state, grants, and the upstream tokens, all encrypted with your `secret` |
| Your server's configuration | The upstream `clientId` and `clientSecret` |
| Upstream provider | The single callback URL `https:///mcp/oauth/callback` |
### The sign-in flow
```mermaid theme={null}
sequenceDiagram
autonumber
participant C as MCP client
participant B as Browser
participant P as MCP server
participant U as Upstream
C->>P: MCP request
P-->>C: 401
C->>P: Read metadata
C->>B: Open /authorize
B->>P: GET /authorize
P-->>B: Consent page
B->>P: Allow
P-->>B: Redirect
B->>U: Sign in
U-->>B: Redirect
B->>P: GET /callback
P->>U: Exchange code
P-->>B: Redirect
B->>C: Code
C->>P: POST /token
P-->>C: Tokens
C->>P: MCP request
```
1. The client calls the MCP endpoint without a token.
2. The server answers 401 with the URL of its protected resource metadata.
3. The metadata names the server itself as the authorization server and lists its `/mcp/oauth/*` endpoints. The client identifies itself with a client ID metadata document URL, or registers at `/mcp/oauth/register`.
4. The client opens `/mcp/oauth/authorize` in the browser with its PKCE challenge and `state`.
5. The proxy checks the client, the redirect URI, the PKCE challenge, and the RFC 8707 `resource`.
6. The proxy shows its consent page.
7. The user approves.
8. The proxy redirects the browser to the upstream provider with the proxy's own `client_id` and its own PKCE challenge.
9. The user signs in to the upstream provider and approves the OAuth app.
10. The upstream provider redirects the browser to `/mcp/oauth/callback` with a code.
11. The proxy checks that the callback belongs to the browser that approved consent, using a signed cookie.
12. The proxy exchanges the code on the server with the client secret and its PKCE verifier, then reads the user, for example from Google's userinfo endpoint.
13. The proxy redirects the browser to the client's redirect URI with a one-time code and the client's original `state`.
14. The client receives the code.
15. The client exchanges the code at `/mcp/oauth/token` with its PKCE verifier.
16. The proxy returns its own access token (`mcp_at_…`) and refresh token (`mcp_rt_…`).
17. The client calls the MCP endpoint with the access token. The proxy looks up the grant and refreshes the upstream token if it expires soon.
The proxy runs its own PKCE round with the upstream provider, so an intercepted upstream authorization code is useless to anyone but the proxy.
## Set up a Google sign-in
In the [Google Cloud console](https://console.cloud.google.com/auth/overview), open **Google Auth Platform** for your project and set up the app's branding and audience. While the publishing status is **Testing**, add each Google account that signs in under **Audience** > **Test users**.
Under **Clients**, create a client of type **Web application**. Add the proxy's callback as an **Authorized redirect URI**: `http://localhost:3000/mcp/oauth/callback` for local development. Google never sees the MCP client's own redirect URI, so this one URL serves every client. Copy the client ID and the client secret.
```bash .env theme={null}
# The OAuth client from the previous step.
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# At least 32 characters. Generate one with: openssl rand -base64 32
MCP_USE_OAUTH_SECRET=
```
```typescript src/index.ts theme={null}
import { MCPServer } from "mcp-use";
import { google, memoryStorage, oauthProxy } from "mcp-use/oauth";
const server = new MCPServer({
name: "google-tools",
version: "1.0.0",
oauth: oauthProxy({
upstream: google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
// The secret defaults to MCP_USE_OAUTH_SECRET.
// memoryStorage() works on localhost only. See "Choose storage" below.
storage: memoryStorage(),
}),
});
server.tool(
{ name: "whoami", description: "Return the signed-in Google account." },
async (_input, ctx) => {
// The profile holds the fields of Google's userinfo response.
const { name, email } = ctx.auth.user.profile;
return { content: [{ type: "text", text: `${String(name)} <${String(email)}>` }] };
},
);
export default server;
```
Run `mcp-use dev` and connect a client to `http://localhost:3000/mcp`:
```bash theme={null}
claude mcp add --transport http google-tools http://localhost:3000/mcp
```
The client opens the consent page, then Google. After sign-in, call `whoami`.
The complete [Google example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/server/examples/auth/oauth-proxy-firebase) keeps sign-ins in Firestore with `firestoreStorage` and stores each user's notes under their Google subject.
## Register the callback URL
Register exactly one callback URL with the upstream provider:
```text theme={null}
/oauth/callback
```
With the default `basePath` of `/mcp`, that is `https://mcp.example.com/mcp/oauth/callback` in production and `http://localhost:3000/mcp/oauth/callback` locally.
The proxy serves all of its endpoints under `/oauth/`:
| Endpoint | Method | Purpose |
| - | - | - |
| `/mcp/oauth/authorize` | `GET` | Validates the request and shows the consent page |
| `/mcp/oauth/consent` | `POST` | Records the consent decision and redirects upstream |
| `/mcp/oauth/callback` | `GET` | Receives the upstream code and redirects back to the client |
| `/mcp/oauth/token` | `POST` | Authorization-code and refresh-token grants |
| `/mcp/oauth/revoke` | `POST` | RFC 7009 token revocation; ends the whole sign-in |
| `/mcp/oauth/register` | `POST` | RFC 7591 dynamic client registration, only while `dcr` is on |
The server publishes protected resource metadata that names itself as the authorization server, and RFC 8414 authorization server metadata that lists these endpoints. You do not configure either by hand.
## Use the upstream token in tools
`upstreamToken(ctx.auth)` returns the upstream access token for the signed-in user. It takes only the `auth` object, so a helper that receives `ctx.auth` can call it too. It throws when the request is not signed in through `oauthProxy`, so a tool never calls the upstream API without the user's token.
This tool lists the user's next Google Calendar events. Enable the Google Calendar API in the Google Cloud project, and request its scope from Google:
```typescript theme={null}
upstream: google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
scopes: ["email", "profile", "https://www.googleapis.com/auth/calendar.readonly"],
}),
```
Then call the Calendar API with the user's Google token:
```typescript theme={null}
import { upstreamToken } from "mcp-use/oauth";
server.tool(
{ name: "upcoming-events", description: "List the user's next 10 calendar events." },
async (_input, ctx) => {
const params = new URLSearchParams({
timeMin: new Date().toISOString(),
maxResults: "10",
singleEvents: "true",
orderBy: "startTime",
});
const response = await fetch(
`https://www.googleapis.com/calendar/v3/calendars/primary/events?${params}`,
{ headers: { authorization: `Bearer ${upstreamToken(ctx.auth)}` } },
);
if (!response.ok) throw new Error(`Google Calendar returned ${response.status}`);
const { items } = (await response.json()) as {
items: { summary?: string; start: { dateTime?: string; date?: string } }[];
};
const lines = items.map(
(event) => `${event.start.dateTime ?? event.start.date} ${event.summary ?? "(no title)"}`,
);
return { content: [{ type: "text", text: lines.join("\n") || "No upcoming events." }] };
},
);
```
Keep the upstream token on the server. Do not return it in tool results, views, or `_meta`.
`ctx.auth` on a proxy server contains:
| Field | Value |
| - | - |
| `ctx.auth.user` | The `UpstreamUser` `{ subject, profile }`, or the result of `mapUser` |
| `ctx.auth.upstream` | `{ accessToken, scopes, expiresAt? }` for the upstream provider |
| `ctx.auth.accessToken` | The proxy's own `mcp_at_…` token, not the upstream token |
| `ctx.auth.clientId` | The MCP client's ID: a metadata document URL, or `mcp_client_…` for a registered client |
| `ctx.auth.scopes` | The proxy scopes granted to this sign-in |
| `ctx.auth.payload` | `{ sub, client_id, scope, provider, profile }` |
| `ctx.auth.permissions` | Always `[]`. Derive permissions in `mapUser` or in your tools |
`subject` is the provider's stable user ID: the OpenID Connect `sub` for Google, or the numeric user ID for GitHub. It is never an email address or a login name, which can change. Use `subject` as the key for any data you store per user.
### Map the user
Pass `mapUser` to shape `ctx.auth.user`, for example to match the `id` field that the other providers use:
```typescript theme={null}
oauthProxy({
upstream: google({ clientId, clientSecret }),
mapUser: (user) => ({
id: user.subject,
email: typeof user.profile.email === "string" ? user.profile.email : undefined,
emailVerified: user.profile.email_verified === true,
}),
});
```
`mapUser` runs on every request. It controls the user shape, not who can sign in: any user of the upstream provider can complete the sign-in. To restrict users, throw from your tools, or check `ctx.auth.user` in [middleware](/v2/typescript/server/middleware).
## Choose an upstream provider
| Upstream | Function | Subject | Profile fields |
| - | - | - | - |
| Google | `google()` | OpenID Connect `sub` | `sub`, `email`, `email_verified`, `name`, `picture`, `hd` |
| GitHub and GitHub Enterprise Server | `github()` | Numeric GitHub user ID | `id`, `login`, `name`, `email`, `avatar_url` |
| Any OAuth 2.0 provider | `upstream()` | What your `fetchUser` returns | What your `fetchUser` returns |
### Google
`google()` always requests `openid` and, by default, `email` and `profile`. Add Google API scopes to `scopes` as your tools need them.
`google()` requests offline access, so Google returns a refresh token and the proxy refreshes Google tokens on the server. If Google returns no refresh token and none is stored for the user, the proxy sends the user through Google consent once more.
While the app's publishing status is **Testing**, Google expires refresh tokens after 7 days unless the app requests only the `openid`, `email`, and `profile` scopes. Users then sign in again. Publish the app to remove the limit.
### GitHub
In [GitHub Developer settings](https://github.com/settings/developers), choose **OAuth Apps** > **New OAuth App**, and set the callback URL to `/mcp/oauth/callback`. Turn on **Expire user access tokens** so that GitHub issues refresh tokens; the proxy then refreshes the GitHub token on the server.
```typescript theme={null}
import { github, oauthProxy } from "mcp-use/oauth";
oauthProxy({
upstream: github({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
// Default: ["read:user"]. Add "repo" for private repositories.
scopes: ["read:user"],
}),
storage,
});
```
For GitHub Enterprise Server, set `baseUrl` to the web host, and `apiUrl` when the REST API lives elsewhere. The [GitHub example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/server/examples/auth/oauth-proxy-github) calls the GitHub API as the user with Octokit and keeps sign-ins in Redis.
### Any OAuth 2.0 provider
Use `upstream()` for a provider that has authorization and token endpoints. `fetchUser` reads the signed-in user with the upstream tokens and must return a stable `subject`.
```typescript theme={null}
import { oauthProxy, upstream } from "mcp-use/oauth";
oauthProxy({
upstream: upstream({
id: "acme",
name: "Acme",
authorizationEndpoint: "https://id.acme.dev/oauth/authorize",
tokenEndpoint: "https://id.acme.dev/oauth/token",
clientId: process.env.ACME_CLIENT_ID!,
clientSecret: process.env.ACME_CLIENT_SECRET!,
scopes: ["profile"],
fetchUser: async (tokens, fetch) => {
const response = await fetch("https://api.acme.dev/me", {
headers: { authorization: `Bearer ${tokens.accessToken}` },
});
if (!response.ok) throw new Error(`Acme /me returned ${response.status}`);
const me = (await response.json()) as { id: string; email?: string };
return { subject: String(me.id), profile: me };
},
}),
});
```
`id` appears in storage keys and `ctx.auth.payload.provider`; `name` appears on the consent page. Endpoints must use HTTPS (HTTP is accepted on localhost). PKCE is on by default; set `pkce: false` only for a provider that rejects it. Set `issuer` when the token endpoint returns OpenID Connect ID tokens, and `authorizeParams` for extra query parameters such as `prompt`.
## Choose storage
The proxy keeps clients, sign-in state, one-time codes, grants, and upstream tokens between requests, so it needs a store that every instance of the server shares. The proxy encrypts every value with your `secret` before the store sees it, and stores only SHA-256 hashes of tokens in keys.
```typescript theme={null}
import { google, oauthProxy } from "mcp-use/oauth";
import { upstashStorage } from "mcp-use/oauth/storage";
oauthProxy({
upstream: google({ clientId, clientSecret }),
storage: upstashStorage({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
}),
});
```
`mcp-use/oauth/storage` has adapters for Upstash, Redis, Postgres, Firestore, and Cloudflare D1 or Durable Objects. See [OAuth Proxy storage](/v2/typescript/server/authentication/providers/oauth-proxy-storage) to pick one or write your own.
### What each environment requires
| Where the server runs | `storage` | `secret` |
| - | - | - |
| `listen()` on a localhost resource, bound to `127.0.0.1`, `localhost`, or `::1` | Optional (memory default) | Optional (random key per process) |
| `mcp-use dev` on localhost | Required: pass `memoryStorage()` or a shared store | Required |
| Any other deployment, tunnel, or public URL | Required: a shared store | Required |
`oauthProxy` throws when the server mounts without a required `storage` or `secret`. `memoryStorage()` is refused outside localhost. Without a secret, the proxy logs a warning and uses a random key, so sign-ins end when the process stops.
## Set and rotate the secret
The proxy derives its encryption and cookie keys from `secret`, which defaults to the `MCP_USE_OAUTH_SECRET` environment variable. Use a random string of at least 32 characters:
```bash theme={null}
openssl rand -base64 32
```
To rotate, pass an array. The first secret encrypts and signs; the others still decrypt and verify:
```typescript theme={null}
oauthProxy({
upstream,
storage,
secret: [process.env.MCP_USE_OAUTH_SECRET!, process.env.MCP_USE_OAUTH_SECRET_PREVIOUS!],
});
```
Keep the previous secret until every record written with it has expired: 30 days for refresh tokens, 60 days for stored upstream tokens. Records that no secret can decrypt read as missing, so their users sign in again. The proxy logs a warning, once for each kind of record, with the key ID that wrote it.
## Restrict which clients connect
By default, any MCP client can connect. A client identifies itself in one of two ways:
* **Client ID metadata document (CIMD).** The client uses an HTTPS URL as its `client_id`, and the proxy fetches its metadata from that URL. The MCP specification prefers this. Claude and ChatGPT use it.
* **Dynamic client registration (DCR).** The client registers at `/mcp/oauth/register` and gets a `mcp_client_…` ID. Registered clients are public clients: the proxy never issues a client secret.
Set `clients` to allow only the apps you list. Dynamic client registration turns off while `clients` is set, because a registered client names itself.
```typescript theme={null}
oauthProxy({
upstream,
storage,
clients: [
// One metadata document: exactly this client.
"https://claude.ai/oauth/mcp-client-metadata.json",
// An HTTPS origin: every metadata document on chatgpt.com.
"https://chatgpt.com",
// A client defined by hand, for a client that can use neither method.
{
clientId: "internal-cli",
redirectUris: ["http://127.0.0.1/callback"],
clientName: "Internal CLI",
},
],
});
```
| `clients` entry | Allows |
| - | - |
| Metadata document URL | That one client |
| HTTPS origin | Every metadata document on that origin. ChatGPT uses a different document URL per connector, so list `https://chatgpt.com` |
| `OAuthProxyClient` object | A client with a fixed `clientId` and exact `redirectUris`. On loopback (`http://127.0.0.1`), any port matches |
Removing a client from `clients`, or turning off `dcr`, ends that client's sign-ins for good. The next request with one of its tokens deletes the grant, and adding the client back does not restore it. A briefly wrong allowlist signs those users out.
`clients` controls which apps connect, not which users sign in. `dcr: true` together with `clients` throws.
The proxy fetches metadata documents only from hosts that resolve to public addresses. On Node.js and Bun it connects only to the addresses it checked. On Cloudflare Workers and Deno, `fetch` resolves the host again, so list the trusted metadata documents in `clients` to close the DNS-rebinding window there. Passing a custom `fetch` turns the host check off; your `fetch` must then enforce its own network policy.
## Configure the consent page
The proxy shows a consent page before it sends the user upstream. The page names your server by its domain, the app that asks, where the browser returns, the upstream provider, and the scopes. Consent defends against a malicious client that reuses an earlier upstream approval (a confused-deputy attack), so it is on by default.
| `consent` value | Behavior |
| - | - |
| Omitted or `{ mode: "always" }` | Asks on every sign-in |
| `{ mode: "remember" }` | Asks once per browser for each client and HTTPS redirect URI, for 1 year. Loopback and custom-scheme redirects still ask every time, because any local program can claim them |
| `{ page: Component }` | Renders your React component instead of the built-in page |
| `false` | No consent page. Allowed only on localhost; refused on a `listen()` that binds beyond loopback |
### Render a custom consent page
`consent.page` takes a React component that the proxy renders on the server with `react-dom/server`, so install `react` and `react-dom`. The component receives `ConsentPageProps` and must render a form that posts to `form.action` with two `decision` buttons. `form.action` already carries the CSRF token, so post to it unchanged and add no hidden fields:
```tsx src/consent.tsx theme={null}
import type { ConsentPageProps } from "mcp-use/oauth";
export function Consent({ client, serverHost, upstream, scopes, form }: ConsentPageProps) {
return (
Allow {client.name ?? "this app"} to connect to {serverHost}?
You will sign in with {upstream}. Scopes: {scopes.join(", ") || "none"}.
);
}
```
```typescript src/index.ts theme={null}
import { Consent } from "./consent.js";
oauthProxy({ upstream, storage, consent: { page: Consent } });
```
The page is never hydrated, and its Content Security Policy allows inline styles only. Put CSS in a `