evals/architecture.json
[
{
"query": "Build me a weather app",
"expected_behavior": "Creates a widget tool (get-weather) with visual display of temperature, conditions, humidity. Uses mock data. Does NOT require an API key by default. Widget renders a styled weather card."
},
{
"query": "I need a tool that translates text between languages",
"expected_behavior": "Creates a tool-only (translate-text) with no widget. Returns text() response. Accepts text, targetLanguage, optional sourceLanguage. No visual UI needed for text translation."
},
{
"query": "Make a recipe finder where I can search and browse recipes",
"expected_behavior": "Creates a widget tool (search-recipes) with recipe card list UI. Widget shows name, cuisine, time, ingredients. May also create get-recipe-details tool. Uses mock recipe data."
},
{
"query": "I want a todo list manager",
"expected_behavior": "Creates multiple tools: add-todo, list-todos, complete-todo, delete-todo. list-todos should have a widget with interactive checklist. Widget calls other tools via callTool."
},
{
"query": "Build a stock price checker",
"expected_behavior": "Creates a widget tool (get-stock) showing price, change, and key metrics visually. Also creates compare-stocks tool. Uses mock stock data with slight randomization."
},
{
"query": "I need a simple calculator tool",
"expected_behavior": "Creates tool-only (calculate) with no widget. Takes expression or operands. Returns text() with the result. No visual UI needed."
},
{
"query": "Make a quiz app where users can take quizzes on different topics",
"expected_behavior": "Creates a widget tool (generate-quiz) showing questions with multiple choice options. Also creates check-answer tool. Widget manages quiz state (current question, score) internally."
},
{
"query": "Build a color palette generator",
"expected_behavior": "Creates a widget tool showing color swatches visually (hex codes, color previews). Widget approach because visual color representation is far better than text descriptions of colors."
}
]
evals/implementation.json
[
{
"query": "Create a tool that fetches user profiles by ID",
"expected_behavior": "Uses server.tool() with z.object({ userId: z.string().describe('User ID') }). Returns object() with user data. Adds .describe() to all schema fields. Includes error handling for not-found case using error() helper."
},
{
"query": "Add a resource that returns the current server configuration",
"expected_behavior": "Uses server.resource() with uri 'config://settings', mimeType 'application/json'. Returns object() with config data. Does NOT use server.tool() for read-only data."
},
{
"query": "Create a prompt template for code review",
"expected_behavior": "Uses server.prompt() with z.object schema. Returns text() with the prompt message. Schema has .describe() on all fields."
},
{
"query": "I need a tool that needs an API key from OpenWeatherMap",
"expected_behavior": "Reads API key from process.env.OPENWEATHERMAP_API_KEY. Creates .env.example file. Returns error() if key is missing. Tells user to set the key in the Env tab."
},
{
"query": "How do I return an error from a tool?",
"expected_behavior": "Uses error() helper from mcp-use/server. Sets isError: true. Shows try/catch pattern with error() in catch block."
},
{
"query": "Create a parameterized resource for user profiles",
"expected_behavior": "Uses server.resourceTemplate() with uriTemplate 'user://{userId}/profile'. Callback receives (uri, { userId }) params. Returns object() with user data."
}
]
evals/README.md
# Evals
Manual evaluations for the mcp-builder skill.
## Format
Each eval file is a JSON array:
```json
[
{
"query": "User input to test",
"expected_behavior": "OUTCOME. What the response should do."
}
]
```
## Running Evals
In Claude Code:
```
Run the evals in evals/<reference>.json. For each query, spawn a Sonnet agent with the mcp-builder skill context and compare the response against expected_behavior. Report pass/fail for each.
```
evals/skill.json
[
{
"query": "I want to build a ChatGPT app for my restaurant",
"expected_behavior": "Must read design-and-architecture.md first. Identifies core actions from user request. Decides which need widgets vs tools. Does NOT jump to code immediately."
},
{
"query": "How do I create a tool in mcp-use?",
"expected_behavior": "Must reference tools-and-resources.md. Shows server.tool() syntax with schema, callback, response helpers."
},
{
"query": "What response helpers are available?",
"expected_behavior": "Must reference response-helpers.md. Lists all helpers: text, object, markdown, html, image, audio, binary, error, mix, widget, resource."
},
{
"query": "How do I create a widget for my tool?",
"expected_behavior": "Must reference widgets.md. Shows complete pattern: widget file in resources/ with widgetMetadata + default component, tool with widget config, widget() helper."
},
{
"query": "How do I define a parameterized resource with URI templates?",
"expected_behavior": "Must reference resource-templates.md. Shows server.resourceTemplate() with uriTemplate syntax."
},
{
"query": "What's the difference between a tool and a widget tool?",
"expected_behavior": "Must reference design-and-architecture.md. Tool = backend action returning data. Widget tool = tool with visual UI component. Uses widget when visual/interactive output improves UX."
},
{
"query": "How do I handle errors in tool callbacks?",
"expected_behavior": "Must reference tools-and-resources.md. Shows error() helper, try/catch pattern, graceful error messages."
},
{
"query": "What fields does useWidget return?",
"expected_behavior": "Must reference widgets.md. Lists all fields including: props, isPending, state, setState, theme, callTool, sendFollowUpMessage, openExternal, displayMode, safeArea, maxHeight, userAgent, locale."
}
]
evals/widgets.json
[
{
"query": "Create a weather widget that shows temperature and conditions",
"expected_behavior": "Creates resources/weather-display.tsx with widgetMetadata (description, props Zod schema), default export component, McpUseProvider with autoSize, isPending check. Creates matching tool with widget: { name: 'weather-display' } config. Uses widget() helper in callback."
},
{
"query": "My widget shows 'undefined' for the city name",
"expected_behavior": "Diagnoses missing isPending check. Widget renders before tool completes, so props are empty. Must check isPending first and show loading state."
},
{
"query": "I want the widget to call another tool when a button is clicked",
"expected_behavior": "Uses callTool from useWidget(). Shows onClick handler: const result = await callTool('tool-name', { args }). Includes try/catch for error handling."
},
{
"query": "How do I make a widget that remembers user selections between interactions?",
"expected_behavior": "Uses state and setState from useWidget(). Shows await setState({ selected: item }) pattern. State persists across widget reopens. NOT React useState (ephemeral)."
},
{
"query": "I'm getting a duplicate tool registration error for my widget",
"expected_behavior": "The widget has exposeAsTool: true AND a custom tool with widget: { name } config. Fix: remove exposeAsTool: true from widgetMetadata (default is false) since the custom tool handles registration."
},
{
"query": "Build a complex product search widget with multiple components",
"expected_behavior": "Uses folder-based structure: resources/product-search/widget.tsx as entry point with widgetMetadata + default export, resources/product-search/components/ProductCard.tsx for sub-components."
}
]
LICENSE.txt
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
references/authentication/auth0.md
# Auth0 Authentication
Setting up OAuth with Auth0. Two modes depending on whether you have access to Auth0's DCR feature:
- **DCR (built-in)** → use `oauthAuth0Provider`. Requires Auth0's Early Access program.
- **OAuth proxy** → use `oauthProxy` + `jwksVerifier` with a standard Auth0 Regular Web App. No Early Access required.
**Learn more:** [Auth0 MCP Authorization Guide](https://auth0.com/ai/docs/mcp/get-started/authorization-for-your-mcp-server) · [Standalone starter template](https://github.com/mcp-use/mcp-oauth-auth0-template) · [Runnable DCR example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/mcp-use/examples/server/oauth/auth0) · [Runnable proxy example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/mcp-use/examples/server/oauth/auth0-proxy)
---
## Mode 1: DCR (`oauthAuth0Provider`)
Auth0's built-in DCR feature is currently in **Early Access**. MCP clients register and authenticate directly with Auth0. To use this, join Auth0's Early Access program — see the [Auth0 MCP Authorization Guide](https://auth0.com/ai/docs/mcp/get-started/authorization-for-your-mcp-server).
### Setup
1. **Auth0 Dashboard → Settings → Advanced** — enable **Resource Parameter Compatibility Profile**.
2. Promote connections to domain-level so third-party clients (like MCP Inspector) can use them:
```bash
auth0 api get connections
auth0 api patch connections/YOUR_CONNECTION_ID --data '{"is_domain_connection": true}'
```
3. Create an API with the `rfc9068_profile_authz` token dialect (this adds `permissions` to the access token):
```bash
auth0 api post resource-servers --data '{
"identifier": "https://your-api.example.com",
"name": "MCP Tools API",
"signing_alg": "RS256",
"token_dialect": "rfc9068_profile_authz",
"enforce_policies": true,
"scopes": [{"value": "read:data", "description": "Read data"}]
}'
```
### Quick Start
```typescript
import { MCPServer, oauthAuth0Provider, object } from "mcp-use/server";
const server = new MCPServer({
name: "my-server",
version: "1.0.0",
oauth: oauthAuth0Provider(), // reads MCP_USE_OAUTH_AUTH0_DOMAIN + _AUDIENCE
});
server.tool(
{ name: "whoami", description: "Get authenticated user info" },
async (_args, ctx) =>
object({
userId: ctx.auth.user.userId,
email: ctx.auth.user.email,
permissions: ctx.auth.permissions,
})
);
server.listen();
```
With a `.env` file:
```bash
MCP_USE_OAUTH_AUTH0_DOMAIN=your-tenant.auth0.com
MCP_USE_OAUTH_AUTH0_AUDIENCE=https://your-api.example.com
```
### Configuration Options
Zero-config (reads from env vars):
```typescript
oauth: oauthAuth0Provider()
```
Explicit config:
```typescript
oauth: oauthAuth0Provider({
domain: "your-tenant.auth0.com",
audience: "https://your-api.example.com",
verifyJwt: process.env.NODE_ENV === "production", // default: true
scopesSupported: ["openid", "profile", "email", "offline_access"],
})
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `domain` | `string` | env var | Auth0 tenant domain (e.g. `your-tenant.auth0.com`) |
| `audience` | `string` | env var | Auth0 API identifier |
| `verifyJwt` | `boolean?` | `true` | Set `false` to skip JWT verification (**development only**) |
| `scopesSupported` | `string[]?` | `["openid", "profile", "email", "offline_access"]` | Override advertised scopes |
---
## Mode 2: OAuth Proxy (`oauthProxy`)
Use a standard Auth0 **Regular Web Application** — no Early Access required. Your server mediates the token exchange using pre-registered credentials.
### Setup
1. **Auth0 Dashboard → Applications → Create Application** — choose **Regular Web Application**.
2. Under **Allowed Callback URLs**, add your MCP client's redirect URI (e.g. `http://localhost:3000/inspector/oauth/callback`).
3. Copy the **Client ID** and **Client Secret**.
4. **APIs → Create API** — set an identifier (e.g. `https://my-mcp-api/`), leave signing as RS256. This is required so Auth0 issues **JWT** access tokens instead of opaque tokens.
### Quick Start
```typescript
import { MCPServer, oauthProxy, jwksVerifier, object } from "mcp-use/server";
const domain = process.env.AUTH0_DOMAIN!;
const audience = process.env.AUTH0_AUDIENCE ?? "";
const server = new MCPServer({
name: "my-server",
version: "1.0.0",
oauth: oauthProxy({
authEndpoint: `https://${domain}/authorize`,
tokenEndpoint: `https://${domain}/oauth/token`,
issuer: `https://${domain}/`,
clientId: process.env.AUTH0_CLIENT_ID!,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
scopes: ["openid", "email", "profile"],
extraAuthorizeParams: { audience }, // required for JWT access tokens
verifyToken: jwksVerifier({
jwksUrl: `https://${domain}/.well-known/jwks.json`,
issuer: `https://${domain}/`,
audience,
}),
}),
});
server.tool(
{ name: "whoami", description: "Get authenticated user info" },
async (_args, ctx) =>
object({
userId: ctx.auth.user.userId,
email: ctx.auth.user.email,
scopes: ctx.auth.scopes,
})
);
server.listen();
```
With a `.env` file:
```bash
AUTH0_DOMAIN=your-tenant.us.auth0.com
AUTH0_CLIENT_ID=<from Regular Web App>
AUTH0_CLIENT_SECRET=<from Regular Web App>
AUTH0_AUDIENCE=https://my-mcp-api/
```
> **Why `extraAuthorizeParams: { audience }`?** Without the `audience` parameter on `/authorize`, Auth0 issues opaque access tokens (no JWT, can't be verified locally). The audience turns it into a standard JWT signed by your API.
See [custom.md](custom.md) for full `oauthProxy` options.
---
## Accessing user info in tools
```typescript
server.tool(
{ name: "get-user-info", description: "Get authenticated user info" },
async (_args, ctx) =>
object({
userId: ctx.auth.user.userId,
email: ctx.auth.user.email,
name: ctx.auth.user.name,
nickname: ctx.auth.user.nickname,
picture: ctx.auth.user.picture,
permissions: ctx.auth.permissions,
scopes: ctx.auth.scopes,
})
);
```
---
## Permissions
Auth0 includes permissions directly in the access token when you use the `rfc9068_profile_authz` token dialect. Check them in tool callbacks:
```typescript
server.tool(
{
name: "delete-document",
description: "Delete a document (requires delete:documents)",
},
async ({ documentId }, ctx) => {
if (!ctx.auth.permissions?.includes("delete:documents")) {
return error("Forbidden: delete:documents permission required");
}
await db.documents.delete({ id: documentId });
return text("Document deleted");
}
);
```
---
## Which mode should I use?
| | DCR (`oauthAuth0Provider`) | Proxy (`oauthProxy`) |
|---|---|---|
| Requires Early Access | Yes | No |
| MCP clients self-register | Yes | No (single pre-registered app) |
| Your server mediates `/token` | No | Yes |
| Best for | Multi-client public MCP servers | Private/internal MCP servers |
If you aren't on the Auth0 DCR Early Access, use the proxy.
---
## Next Steps
- **Auth overview** → [overview.md](overview.md)
- **Custom providers + OAuth proxy reference** → [custom.md](custom.md)
- **WorkOS setup** → [workos.md](workos.md)
- **Build tools** → [../server/tools.md](../server/tools.md)
references/authentication/better-auth.md
# Better Auth Authentication
Setting up a self-hosted OAuth 2.1 authorization server with Better Auth.
**Learn more:** [Better Auth OAuth Provider Plugin](https://better-auth.com/docs/plugins/oauth-provider) · [Standalone starter template](https://github.com/mcp-use/mcp-oauth-better-auth-template) · [Runnable example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/mcp-use/examples/server/oauth/better-auth)
> Covers the `@better-auth/oauth-provider` plugin. The older Better Auth MCP plugin is deprecated — for legacy users, see the [mcp-use adapter](https://better-auth.com/docs/plugins/mcp#framework-adapters).
---
## How It Works
Unlike WorkOS, Auth0, or Supabase, Better Auth runs **inside** your MCP server — your server *is* the OAuth 2.1 authorization server. Better Auth handles the full OAuth flow (authorization, token issuance, JWKS); mcp-use only verifies the resulting JWTs.
This means:
- No external auth service required
- You control the login and consent UX
- Requires a database (SQLite, Postgres, etc.)
- More setup than hosted providers
---
## Install
```bash
npm install better-auth @better-auth/oauth-provider better-sqlite3
```
---
## Setup
### 1. Configure Better Auth (`auth.ts`)
```typescript
import { betterAuth } from "better-auth";
import { jwt } from "better-auth/plugins";
import { oauthProvider } from "@better-auth/oauth-provider";
import Database from "better-sqlite3";
export const auth = betterAuth({
authURL: "http://localhost:3000",
basePath: "/api/auth",
secret: process.env.BETTER_AUTH_SECRET!,
database: new Database("./sqlite.db"),
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
plugins: [
jwt(), // Required: signs and verifies access tokens
oauthProvider({
loginPage: "/sign-in",
consentPage: "/consent",
allowDynamicClientRegistration: true,
allowUnauthenticatedClientRegistration: true,
// Must include your MCP endpoint as a valid audience
validAudiences: ["http://localhost:3000/mcp"],
// Expose user profile claims in access token JWTs
customAccessTokenClaims: async ({ user }) => ({
email: user?.email,
name: user?.name,
picture: user?.image,
}),
}),
],
});
```
### 2. Generate and migrate the database
```bash
npx auth@latest generate
npx auth@latest migrate
```
### 3. Configure the MCP server (`server.ts`)
You need three things beyond the standard `MCPServer` setup:
1. Mount Better Auth routes (`/api/auth/**`)
2. Mount OAuth discovery endpoints with CORS headers (required for browser clients)
3. Mount login and consent pages
```typescript
import { MCPServer, oauthBetterAuthProvider } from "mcp-use/server";
import { auth } from "./auth.js";
import {
oauthProviderAuthServerMetadata,
oauthProviderOpenIdConfigMetadata,
} from "@better-auth/oauth-provider";
const server = new MCPServer({
name: "my-server",
version: "1.0.0",
oauth: oauthBetterAuthProvider({
authURL: "http://localhost:3000/api/auth",
}),
});
// Mount Better Auth routes
server.app.on(["GET", "POST"], "/api/auth/**", (c) => auth.handler(c.req.raw));
// OAuth discovery endpoints — CORS headers are required for browser clients (MCP Inspector)
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET",
};
const authServerMetadataHandler = oauthProviderAuthServerMetadata(auth, { headers: corsHeaders });
server.app.get("/.well-known/oauth-authorization-server", (c) => authServerMetadataHandler(c.req.raw));
server.app.get("/.well-known/oauth-authorization-server/api/auth", (c) => authServerMetadataHandler(c.req.raw));
const openIdConfigHandler = oauthProviderOpenIdConfigMetadata(auth, { headers: corsHeaders });
server.app.get("/.well-known/openid-configuration", (c) => openIdConfigHandler(c.req.raw));
server.app.get("/.well-known/openid-configuration/api/auth", (c) => openIdConfigHandler(c.req.raw));
await server.listen(3000);
```
### 4. Add login and consent pages
Better Auth requires a `/sign-in` page and a `/consent` page mounted on the Hono app.
- **Sign-in page:** POST to `/api/auth/sign-in/social` with the provider name and `callbackURL: '/api/auth/oauth2/authorize' + queryString` (preserve the OAuth query params).
- **Consent page:** POST to `/api/auth/oauth2/consent` with `{ accept: boolean, oauth_query: window.location.search.slice(1) }`.
- Both must use `credentials: 'include'` on fetch calls.
See the [runnable example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/mcp-use/examples/server/oauth/better-auth) for complete HTML/JS.
---
## Environment Variables
```bash
# Better Auth secret (used for signing cookies and tokens)
BETTER_AUTH_SECRET=your-secret-change-in-production
# Social provider credentials (GitHub shown — swap for any supported provider)
# Set callback URL in provider dashboard to: http://localhost:3000/api/auth/callback/<provider>
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
```
Better Auth supports many social providers (GitHub, Google, Discord, etc.). See [Better Auth social providers docs](https://www.better-auth.com/docs/concepts/oauth) for the full list.
---
## Configuration Options
```typescript
oauthBetterAuthProvider({
authURL: "https://yourapp.com/api/auth", // Required: full URL including basePath
verifyJwt: process.env.NODE_ENV === "production", // default: true
scopesSupported: ["openid", "profile", "email", "offline_access"], // override advertised scopes
getUserInfo: (payload) => ({
userId: payload.sub as string,
email: payload.email as string,
name: payload.name as string,
roles: (payload.roles as string[]) || [],
permissions: (payload.permissions as string[]) || [],
}),
})
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `authURL` | `string` | env var | Better Auth base URL including `/api/auth` path |
| `verifyJwt` | `boolean?` | `true` | Set `false` to skip JWT verification (**development only**) |
| `scopesSupported` | `string[]?` | `["openid", "profile", "email", "offline_access"]` | Override advertised scopes |
| `getUserInfo` | `function?` | built-in | Custom extraction of user info from JWT payload |
---
## Accessing user info in tools
```typescript
server.tool(
{ name: "get-user-info", description: "Get information about the authenticated user" },
async (_args, ctx) =>
object({
userId: ctx.auth.user.userId,
email: ctx.auth.user.email,
name: ctx.auth.user.name,
scopes: ctx.auth.scopes,
permissions: ctx.auth.permissions,
})
);
```
---
## Common Mistakes
- **Missing `validAudiences`** — Must include your MCP endpoint (e.g. `http://localhost:3000/mcp`) or JWT verification will fail with an audience mismatch.
- **Missing CORS headers on discovery endpoints** — Browser clients like MCP Inspector require `Access-Control-Allow-Origin: *` on `/.well-known/*` routes.
- **Skipping `jwt()` plugin** — Required for token signing; omitting it breaks token issuance.
- **Wrong `authURL`** — `oauthBetterAuthProvider({ authURL })` must include the full basePath (e.g. `/api/auth`), not just the host.
- **Missing `credentials: 'include'`** — Login and consent page fetch calls must include cookies or the session will be lost.
---
## Next Steps
- **Auth overview** → [overview.md](overview.md)
- **WorkOS setup** → [workos.md](workos.md)
- **Supabase setup** → [supabase.md](supabase.md)
- **Keycloak setup** → [keycloak.md](keycloak.md)
- **Build tools** → [../server/tools.md](../server/tools.md)
references/authentication/clerk.md
# Clerk Authentication
Setting up OAuth with Clerk. DCR mode only — MCP clients register themselves directly with Clerk via Dynamic Client Registration; the MCP server only verifies the resulting JWTs against Clerk's JWKS.
**Learn more:** [Clerk OAuth Docs](https://clerk.com/docs/guides/configure/auth-strategies/oauth/how-clerk-implements-oauth) · [Runnable example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/mcp-use/examples/server/oauth/clerk)
---
## Quick Start
```typescript
import { MCPServer, oauthClerkProvider, object } from "mcp-use/server";
const server = new MCPServer({
name: "my-server",
version: "1.0.0",
oauth: oauthClerkProvider(),
});
server.tool(
{ name: "whoami", description: "Get authenticated user info" },
async (_args, ctx) =>
object({
userId: ctx.auth.user.userId,
email: ctx.auth.user.email,
name: ctx.auth.user.name,
})
);
server.listen();
```
With a `.env` file:
```bash
# Development: https://[verb-noun-##].clerk.accounts.dev
# Production: https://clerk.[YOUR_APP_DOMAIN].com
MCP_USE_OAUTH_CLERK_FRONTEND_API_URL=https://verb-noun-42.clerk.accounts.dev
```
That's it. JWT verification, OAuth discovery, and `.well-known` passthrough are handled automatically.
---
## Setup
1. Sign up at [clerk.com](https://clerk.com) and create an application.
2. **Clerk Dashboard → Configure → OAuth Applications** — enable **Dynamic Client Registration**. This is required for MCP clients to self-register.
3. **Clerk Dashboard → Configure → API Keys** — copy the **Frontend API URL** (shown in the `.env.local` quickstart).
- Development example: `https://verb-noun-42.clerk.accounts.dev`
- Production example: `https://clerk.yourdomain.com`
---
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `MCP_USE_OAUTH_CLERK_FRONTEND_API_URL` | Yes | Clerk Frontend API URL (e.g. `https://verb-noun-42.clerk.accounts.dev`) |
---
## Configuration Options
Zero-config (reads from env vars):
```typescript
oauth: oauthClerkProvider()
```
Explicit config (overrides env vars):
```typescript
oauth: oauthClerkProvider({
frontendApiUrl: "https://verb-noun-42.clerk.accounts.dev",
verifyJwt: process.env.NODE_ENV === "production", // default: true
})
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `frontendApiUrl` | `string` | env var | Clerk Frontend API URL |
| `verifyJwt` | `boolean?` | `true` | Set `false` to skip JWT verification (**development only**) |
---
## User Context
Clerk populates these fields on `ctx.auth.user`:
| Field | Type | Source |
|-------|------|--------|
| `userId` | `string` | `sub` claim |
| `email` | `string?` | `email` claim |
| `name` | `string?` | `name` claim |
| `roles` | `string[]?` | `roles` claim |
| `permissions` | `string[]?` | `permissions` claim |
### Organization Context
Clerk includes the active organization on the JWT payload. The fields are non-standard, so cast off `ctx.auth.user`:
```typescript
server.tool(
{ name: "get-organization-info", description: "Get the active organization" },
async (_args, ctx) => {
const { org_id, org_role, org_slug } = ctx.auth.user as {
org_id?: string;
org_role?: string;
org_slug?: string;
};
if (!org_id) {
return error("No active organization. Select one in Clerk first.");
}
return object({ org_id, org_role, org_slug });
}
);
```
---
## Common Mistakes
- **DCR not enabled** — If MCP Inspector stalls at the registration step, confirm **Dynamic Client Registration** is enabled in Clerk Dashboard → Configure → OAuth Applications. Without DCR, there is no `client_id` for MCP clients to obtain.
- **Wrong Frontend API URL** — Must be the full URL (with `https://`), not just the subdomain.
- **Skipping JWT verification in production** — `verifyJwt: false` is development only.
---
## Next Steps
- **Auth overview** → [overview.md](overview.md)
- **Auth0 setup** → [auth0.md](auth0.md)
- **WorkOS setup** → [workos.md](workos.md)
- **Build tools** → [../server/tools.md](../server/tools.md)
references/authentication/custom.md
# Custom & OAuth Proxy Authentication
Two factories cover everything that isn't a built-in provider:
- **`oauthCustomProvider`** — for identity providers that support **Dynamic Client Registration (DCR)** and advertise a `registration_endpoint` in their OAuth metadata. MCP clients register themselves directly with the upstream; your server only verifies tokens.
- **`oauthProxy`** — for providers **without DCR** (Google, GitHub, Okta, Azure AD, standard Auth0 apps, etc.). You register an app in the provider's dashboard, pass the fixed `clientId` / `clientSecret` here, and your server mediates the token exchange.
> Picking between them: if your provider lists a `registration_endpoint` at `https://<provider>/.well-known/oauth-authorization-server`, use `oauthCustomProvider`. Otherwise use `oauthProxy`.
---
## `oauthCustomProvider` (DCR)
Use this when your identity provider supports DCR and advertises a `registration_endpoint`. Clients discover the endpoints and register themselves against the upstream.
```typescript
import { MCPServer, oauthCustomProvider, object } from "mcp-use/server";
import { jwtVerify, createRemoteJWKSet } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://auth.example.com/.well-known/jwks.json")
);
const server = new MCPServer({
name: "my-server",
version: "1.0.0",
oauth: oauthCustomProvider({
issuer: "https://auth.example.com",
authEndpoint: "https://auth.example.com/oauth/authorize",
tokenEndpoint: "https://auth.example.com/oauth/token",
async verifyToken(token) {
const result = await jwtVerify(token, JWKS, {
issuer: "https://auth.example.com",
audience: "your-audience",
});
return { payload: result.payload as Record<string, unknown> };
},
getUserInfo(payload) {
return {
userId: payload.sub as string,
email: payload.email as string | undefined,
name: payload.name as string | undefined,
roles: (payload.roles as string[]) || [],
};
},
}),
});
server.tool(
{ name: "whoami", description: "Get authenticated user info" },
async (_args, ctx) =>
object({
userId: ctx.auth.user.userId,
email: ctx.auth.user.email,
})
);
server.listen();
```
### Configuration Options
```typescript
oauthCustomProvider({
// Required: OAuth endpoints
issuer: "https://auth.example.com",
authEndpoint: "https://auth.example.com/oauth/authorize",
tokenEndpoint: "https://auth.example.com/oauth/token",
// Required: must return { payload: Record<string, unknown> } or throw
async verifyToken(token) {
const { payload } = await jwtVerify(token, JWKS, { issuer: "..." });
return { payload: payload as Record<string, unknown> };
},
// Optional
jwksUrl: "https://auth.example.com/.well-known/jwks.json", // advertised in discovery metadata
userInfoEndpoint: "https://auth.example.com/userinfo",
scopesSupported: ["openid", "profile", "email"],
grantTypesSupported: ["authorization_code", "refresh_token"],
audience: "your-api-identifier",
getUserInfo: (payload) => ({
userId: payload.sub as string,
email: payload.email as string | undefined,
name: payload.name as string | undefined,
}),
})
```
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `issuer` | `string` | Yes | OAuth issuer URL |
| `authEndpoint` | `string` | Yes | Authorization endpoint |
| `tokenEndpoint` | `string` | Yes | Token endpoint |
| `verifyToken` | `(token: string) => Promise<{ payload: Record<string, unknown> }>` | Yes | Token verification function |
| `jwksUrl` | `string?` | No | JWKS endpoint advertised in discovery metadata |
| `userInfoEndpoint` | `string?` | No | User info endpoint URL |
| `scopesSupported` | `string[]?` | No | Default: `["openid", "profile", "email"]` |
| `grantTypesSupported` | `string[]?` | No | Default: `["authorization_code", "refresh_token"]` |
| `audience` | `string?` | No | Audience for JWT verification |
| `getUserInfo` | `(payload) => UserInfo` | No | Custom user info extraction |
### Default Claim Extraction
Without `getUserInfo`, the provider extracts standard OIDC claims automatically:
| Field | Extracted From |
|-------|---------------|
| `userId` | `sub`, `user_id`, or `id` |
| `email` | `email` |
| `name` | `name` |
| `username` | `username` or `preferred_username` |
| `nickname` | `nickname` |
| `picture` | `picture` or `avatar_url` |
| `roles` | `roles` (if array) |
| `permissions` | `permissions` (if array) |
| `scopes` | Parsed from `scope` string |
Override if your provider uses non-standard claim names:
```typescript
getUserInfo: (payload) => ({
userId: payload.user_id as string,
email: payload.mail as string,
name: payload.display_name as string,
roles: (payload.groups as string[]) || [],
})
```
---
## `oauthProxy` (non-DCR providers)
Use this for providers that don't support DCR — Google, GitHub, Okta, Azure AD, standard Auth0 Regular Web Apps, and anything else where you register an app in a dashboard and receive a fixed `clientId` / `clientSecret`.
The proxy flow:
```
Client → /register → MCP server returns the pre-registered client_id
Client → /authorize → MCP server redirects to upstream /authorize
Upstream → redirect → authorization code returned to the client
Client → /token → MCP server injects clientId + clientSecret, forwards to upstream
Upstream → token → returned to the client
Client → /mcp/... → MCP server verifies bearer via verifyToken()
```
### `jwksVerifier` helper
Use `jwksVerifier` to build a standard JWT+JWKS `verifyToken`. It handles signature verification, issuer checking, and optional audience validation. Pair it with `oauthProxy` for any JWT-based provider.
```typescript
import { oauthProxy, jwksVerifier } from "mcp-use/server";
oauthProxy({
// ...
verifyToken: jwksVerifier({
jwksUrl: "https://<provider>/.well-known/jwks.json",
issuer: "https://<provider>/",
audience: "your-audience", // optional — enforces `aud` claim
}),
})
```
> `verifyToken` — whether from `jwksVerifier` or handwritten — **must resolve to `{ payload: Record<string, unknown> }`** or throw. The proxy surfaces `payload` to `getUserInfo` and to `ctx.auth.payload`.
For non-JWT providers (GitHub opaque tokens), write your own `verifyToken` that calls the provider's API — see [GitHub](#github-opaque-tokens) below.
### `oauthProxy` Options
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `authEndpoint` | `string` | Yes | Upstream authorization endpoint |
| `tokenEndpoint` | `string` | Yes | Upstream token endpoint |
| `issuer` | `string` | Yes | Token issuer (used in metadata and enforced by `jwksVerifier`) |
| `clientId` | `string` | Yes | Pre-registered OAuth client ID |
| `clientSecret` | `string?` | No | Client secret (omit for public clients) |
| `verifyToken` | `VerifyToken` | Yes | Token verification — use `jwksVerifier()` or a custom function |
| `scopes` | `string[]?` | No | Scopes to request. Default: `["openid", "email", "profile"]` |
| `grantTypes` | `string[]?` | No | Default: `["authorization_code", "refresh_token"]` |
| `extraAuthorizeParams` | `Record<string, string>?` | No | Extra query params on `/authorize` (e.g. `access_type`, `audience`, `prompt`) |
| `getUserInfo` | `(payload) => UserInfo` | No | Custom user info extraction from the verified payload |
---
## Provider Examples (`oauthProxy`)
### Google
```typescript
oauth: oauthProxy({
authEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
tokenEndpoint: "https://oauth2.googleapis.com/token",
issuer: "https://accounts.google.com",
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
scopes: ["openid", "email", "profile"],
extraAuthorizeParams: { access_type: "offline" },
verifyToken: jwksVerifier({
jwksUrl: "https://www.googleapis.com/oauth2/v3/certs",
issuer: "https://accounts.google.com",
audience: process.env.GOOGLE_CLIENT_ID!,
}),
})
```
### Okta
```typescript
const oktaDomain = process.env.OKTA_DOMAIN!; // e.g. "https://dev-123.okta.com"
oauth: oauthProxy({
authEndpoint: `${oktaDomain}/oauth2/default/v1/authorize`,
tokenEndpoint: `${oktaDomain}/oauth2/default/v1/token`,
issuer: `${oktaDomain}/oauth2/default`,
clientId: process.env.OKTA_CLIENT_ID!,
clientSecret: process.env.OKTA_CLIENT_SECRET,
scopes: ["openid", "email", "profile"],
verifyToken: jwksVerifier({
jwksUrl: `${oktaDomain}/oauth2/default/v1/keys`,
issuer: `${oktaDomain}/oauth2/default`,
}),
})
```
### Azure AD (Microsoft Entra ID)
```typescript
const tenantId = process.env.AZURE_TENANT_ID!;
const base = `https://login.microsoftonline.com/${tenantId}/v2.0`;
oauth: oauthProxy({
authEndpoint: `${base}/oauth2/v2.0/authorize`,
tokenEndpoint: `${base}/oauth2/v2.0/token`,
issuer: base,
clientId: process.env.AZURE_CLIENT_ID!,
clientSecret: process.env.AZURE_CLIENT_SECRET,
scopes: ["openid", "profile", "email"],
verifyToken: jwksVerifier({
jwksUrl: "https://login.microsoftonline.com/common/discovery/v2.0/keys",
issuer: base,
audience: process.env.AZURE_CLIENT_ID!,
}),
})
```
### Auth0 (Regular Web App, no Early Access)
For Auth0 with a standard Regular Web App (no DCR Early Access), use the proxy. See [auth0.md](auth0.md) for the full guide.
```typescript
const domain = process.env.AUTH0_DOMAIN!;
const audience = process.env.AUTH0_AUDIENCE ?? "";
oauth: oauthProxy({
authEndpoint: `https://${domain}/authorize`,
tokenEndpoint: `https://${domain}/oauth/token`,
issuer: `https://${domain}/`,
clientId: process.env.AUTH0_CLIENT_ID!,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
scopes: ["openid", "email", "profile"],
extraAuthorizeParams: { audience },
verifyToken: jwksVerifier({
jwksUrl: `https://${domain}/.well-known/jwks.json`,
issuer: `https://${domain}/`,
audience,
}),
})
```
### GitHub (opaque tokens)
GitHub uses non-JWT opaque tokens. Use a custom `verifyToken` that calls the GitHub API instead of `jwksVerifier`:
```typescript
oauth: oauthProxy({
authEndpoint: "https://github.com/login/oauth/authorize",
tokenEndpoint: "https://github.com/login/oauth/access_token",
issuer: "https://github.com",
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
scopes: ["read:user", "user:email"],
// GitHub uses opaque tokens — validate by calling the API
async verifyToken(token) {
const res = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${token}`,
"User-Agent": "my-mcp-server",
},
});
if (!res.ok) throw new Error("Invalid GitHub token");
const user = await res.json();
return { payload: { sub: String(user.id), ...user } };
},
getUserInfo(payload) {
return {
userId: payload.sub as string,
username: payload.login as string | undefined,
name: payload.name as string | undefined,
email: payload.email as string | undefined,
picture: payload.avatar_url as string | undefined,
};
},
})
```
---
## Accessing user info in tools
```typescript
server.tool(
{ name: "get-user-info", description: "Get authenticated user info" },
async (_args, ctx) =>
object({
userId: ctx.auth.user.userId,
email: ctx.auth.user.email,
name: ctx.auth.user.name,
scopes: ctx.auth.scopes,
})
);
```
---
## Common Mistakes
- **Using `oauthCustomProvider` for Google / GitHub / Okta / Azure AD** — those don't support DCR. Use `oauthProxy` instead.
- **`verifyToken` returning the wrong shape** — must resolve to `{ payload: Record<string, unknown> }` or throw. Returning `jwtVerify`'s raw result doesn't satisfy this in TypeScript — cast explicitly: `return { payload: result.payload as Record<string, unknown> }`.
- **Forgetting `audience` for JWT verification** — most providers issue per-client tokens; without `audience` in `jwksVerifier`, tokens from other clients could be accepted.
- **Missing `extraAuthorizeParams`** — Google needs `access_type: "offline"` for refresh tokens; Auth0 needs `audience` to issue JWT access tokens instead of opaque ones.
---
## Resources
- [`jose` library](https://github.com/panva/jose) — JWT verification primitives (already a dependency of `mcp-use`)
- [OAuth 2.1 Specification](https://oauth.net/2.1/)
- [OIDC Specification](https://openid.net/specs/openid-connect-core-1_0.html)
- [Runnable Auth0 proxy example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/mcp-use/examples/server/oauth/auth0-proxy)
---
## Next Steps
- **Auth overview** → [overview.md](overview.md)
- **Auth0 setup** → [auth0.md](auth0.md)
- **WorkOS setup** → [workos.md](workos.md)
- **Supabase setup** → [supabase.md](supabase.md)
- **Keycloak setup** → [keycloak.md](keycloak.md)
- **Build tools** → [../server/tools.md](../server/tools.md)
references/authentication/keycloak.md
# Keycloak Authentication
Setting up OAuth with Keycloak. Keycloak exposes full OAuth 2.1 + OIDC endpoints on every realm, including native [Dynamic Client Registration](https://www.keycloak.org/securing-apps/client-registration) (RFC 7591). MCP clients discover Keycloak via `.well-known` metadata, register themselves, complete a PKCE flow, and send the access token as a bearer on MCP requests — **the MCP server only verifies the JWT against Keycloak's JWKS**.
**Learn more:** [Keycloak Documentation](https://www.keycloak.org/documentation) · [Keycloak DCR](https://www.keycloak.org/securing-apps/client-registration) · [Standalone starter template](https://github.com/mcp-use/mcp-oauth-keycloak-template) · [Runnable example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/mcp-use/examples/server/oauth/keycloak)
---
## Quick Start
```typescript
import { MCPServer, oauthKeycloakProvider, object } from "mcp-use/server";
const server = new MCPServer({
name: "my-server",
version: "1.0.0",
oauth: oauthKeycloakProvider(), // reads MCP_USE_OAUTH_KEYCLOAK_SERVER_URL + _REALM
});
server.tool(
{ name: "whoami", description: "Get authenticated user info" },
async (_args, ctx) =>
object({
userId: ctx.auth.user.userId,
username: ctx.auth.user.username,
email: ctx.auth.user.email,
roles: ctx.auth.user.roles,
permissions: ctx.auth.permissions,
scopes: ctx.auth.scopes,
})
);
server.listen();
```
With a `.env` file:
```bash
MCP_USE_OAUTH_KEYCLOAK_SERVER_URL=http://localhost:8080
MCP_USE_OAUTH_KEYCLOAK_REALM=demo
```
---
## Setup
This guide assumes you already have a Keycloak instance running with admin access. If not, see [Keycloak's getting started guide](https://www.keycloak.org/guides#getting-started).
### 1. Enable Dynamic Client Registration
Keycloak exposes `/{realm}/clients-registrations/openid-connect` on every realm. Anonymous (no-token) registration is gated by **Client Registration Policies**:
1. **Realm settings → Client registration → Anonymous Access Policies**
2. Open the **Trusted Hosts** policy
3. Add the hostnames MCP clients will register from (`localhost`, `127.0.0.1`) to **Trusted Hosts**
4. Make sure **Client URIs Must Match** is enabled so `redirect_uris` in the registration request are validated
> **Browser MCP clients** (like the Inspector) also need the **Allowed Registration Web Origins** policy (Keycloak 26.6+) listing every origin the client runs from. Without it, DCR requests fail with CORS `403 Invalid origin`.
For non-localhost redirect URIs, mint an **Initial Access Token** (Realm settings → Client registration → Initial access token) and have clients pass it on the DCR POST.
### 2. (Optional) Audience enforcement
Keycloak doesn't set `aud` to the resource server by default. To require it:
1. Add an **Audience** protocol mapper to a client scope in Keycloak.
2. Set `MCP_USE_OAUTH_KEYCLOAK_AUDIENCE` to the matching value.
Without the mapper, tokens will be rejected if `audience` is configured.
---
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `MCP_USE_OAUTH_KEYCLOAK_SERVER_URL` | Yes | Base URL of your Keycloak server (no trailing slash, no `/realms` path) |
| `MCP_USE_OAUTH_KEYCLOAK_REALM` | Yes | Realm name (e.g. `demo`) |
| `MCP_USE_OAUTH_KEYCLOAK_AUDIENCE` | No | If set, enforced as the access token's `aud` claim. Requires an Audience protocol mapper. |
---
## Configuration Options
Zero-config (reads from env vars):
```typescript
oauth: oauthKeycloakProvider()
```
Explicit config:
```typescript
oauth: oauthKeycloakProvider({
serverUrl: "https://keycloak.example.com",
realm: "demo",
audience: "https://my-mcp-server.example.com/mcp", // optional
verifyJwt: process.env.NODE_ENV === "production", // default: true
scopesSupported: ["openid", "profile", "email"], // override advertised scopes
})
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `serverUrl` | `string` | env var | Base URL of your Keycloak server |
| `realm` | `string` | env var | Realm name |
| `audience` | `string?` | env var | Required `aud` claim — needs an Audience mapper in Keycloak |
| `verifyJwt` | `boolean?` | `true` | Set `false` to skip JWT verification (**development only**) |
| `scopesSupported` | `string[]?` | `["openid", "profile", "email", "offline_access", "roles"]` | Override advertised scopes |
---
## The Flow
```
MCP Client ──(1) GET /.well-known/oauth-protected-resource ─▶ MCP Server
MCP Client ──(2) GET /.well-known/oauth-authorization-server ─▶ MCP Server ─▶ Keycloak
MCP Client ──(3) POST /clients-registrations/openid-connect ─▶ Keycloak (DCR)
MCP Client ──(4) GET /protocol/openid-connect/auth ─▶ Keycloak (PKCE)
MCP Client ──(5) POST /protocol/openid-connect/token ─▶ Keycloak
MCP Client ──(6) MCP request + Bearer <token> ─▶ MCP Server (verifies JWT via JWKS)
```
Step 2 is a passthrough from the MCP server back to Keycloak's metadata — it's what tells the client where to register and where to send the user for login. Everything else goes directly to Keycloak.
---
## User Context
Keycloak puts realm roles in `realm_access.roles` and resource roles in `resource_access.{client}.roles`. The provider normalizes them onto `ctx.auth`:
| Field | Type | Source |
|-------|------|--------|
| `userId` | `string` | `sub` claim |
| `email` | `string?` | `email` claim |
| `name` | `string?` | `name` claim |
| `username` | `string?` | `preferred_username` claim |
| `roles` | `string[]?` | `realm_access.roles` — **realm roles only** |
| `permissions` | `string[]?` | `resource_access.{client}.roles` — as `"client:role"` strings |
| `scopes` | `string[]?` | Parsed from `scope` claim |
### Role-Based Access Control
```typescript
server.tool(
{ name: "admin-action", description: "Admin-only action" },
async (_args, ctx) => {
if (!ctx.auth.user.roles?.includes("admin")) {
return error("Forbidden: admin role required");
}
// ... admin logic
return text("Done");
}
);
```
For per-client roles, check `ctx.auth.permissions` (formatted as `client:role`).
---
## Making Keycloak API Calls
Call Keycloak's userinfo endpoint with the raw access token:
```typescript
server.tool(
{
name: "get-keycloak-userinfo",
description: "Fetch the full userinfo document from Keycloak",
},
async (_args, ctx) => {
const serverUrl = process.env.MCP_USE_OAUTH_KEYCLOAK_SERVER_URL!;
const realm = process.env.MCP_USE_OAUTH_KEYCLOAK_REALM!;
const res = await fetch(
`${serverUrl}/realms/${realm}/protocol/openid-connect/userinfo`,
{ headers: { Authorization: `Bearer ${ctx.auth.accessToken}` } }
);
return object(await res.json());
}
);
```
---
## Example `.env`
```bash
# Required: base URL of your Keycloak server — no trailing slash, no /realms path
MCP_USE_OAUTH_KEYCLOAK_SERVER_URL=http://localhost:8080
# Required: realm name
MCP_USE_OAUTH_KEYCLOAK_REALM=demo
# Optional: if set, the provider enforces that the token's `aud` claim equals
# this value. Requires an Audience protocol mapper on the client scope.
# MCP_USE_OAUTH_KEYCLOAK_AUDIENCE=http://localhost:3000
```
---
## Production Notes
- **Audience enforcement.** Keycloak doesn't set `aud` to the resource server by default. To require it, add an *Audience* protocol mapper to the client scope and set `MCP_USE_OAUTH_KEYCLOAK_AUDIENCE`.
- **Anonymous DCR.** Fine for local dev, risky in production. Disable anonymous access and issue Initial Access Tokens that clients pass on the DCR request.
- **Transport.** Always serve Keycloak and the MCP server over HTTPS outside of local dev.
- **Scope of realm roles.** `ctx.auth.user.roles` only contains realm roles. Use `ctx.auth.permissions` (formatted as `client:role`) for per-client roles.
---
## Next Steps
- **Auth overview** → [overview.md](overview.md)
- **Custom providers + OAuth proxy reference** → [custom.md](custom.md)
- **WorkOS setup** → [workos.md](workos.md)
- **Build tools** → [../server/tools.md](../server/tools.md)
references/authentication/overview.md
# Authentication
Adding OAuth 2.0/2.1 authentication to your MCP server.
**Use for:** Protecting tools behind user authentication, accessing user identity in tool handlers, integrating with identity providers (Auth0, Better Auth, Clerk, WorkOS, Supabase, Keycloak, Google, GitHub, Okta, Azure AD, and more).
> **Two integration modes.** Pick by whether your identity provider supports Dynamic Client Registration (DCR):
> - **Remote auth** (`oauthAuth0Provider`, `oauthBetterAuthProvider`, `oauthClerkProvider`, `oauthKeycloakProvider`, `oauthSupabaseProvider`, `oauthWorkOSProvider`, `oauthCustomProvider`) — clients register and authenticate directly with the upstream provider; your server only verifies the resulting bearer token. Requires DCR on the upstream.
> - **OAuth proxy** (`oauthProxy` + `jwksVerifier`) — your server holds pre-registered client credentials and mediates the token exchange. Use this for Google, GitHub, Okta, Azure AD, or any provider where you register the app in a dashboard and receive a fixed `clientId` / `clientSecret`.
---
## How It Works
Pass an OAuth provider to the `oauth` option on `MCPServer`:
```typescript
import { MCPServer } from "mcp-use/server";
const server = new MCPServer({
name: "my-server",
version: "1.0.0",
oauth: yourProvider(), // see provider-specific guides
});
```
This single property:
- Protects all `/mcp/*` routes with bearer token authentication
- Verifies tokens on every request (JWT + JWKS by default)
- Sets up OAuth discovery endpoints (`/.well-known/oauth-authorization-server`, `/.well-known/openid-configuration`, `/.well-known/oauth-protected-resource`)
- In proxy mode, also sets up `/register`, `/authorize`, and `/token` endpoints that mediate the upstream flow
- Populates `ctx.auth` in all tool/resource/prompt handlers
---
## Accessing User Context
Every tool handler receives `ctx.auth` when OAuth is enabled:
```typescript
server.tool(
{
name: "get-profile",
description: "Get the authenticated user's profile",
},
async (_args, ctx) =>
object({
userId: ctx.auth.user.userId,
email: ctx.auth.user.email,
name: ctx.auth.user.name,
})
);
```
### `ctx.auth` Shape
```typescript
ctx.auth.user // UserInfo object (see below)
ctx.auth.accessToken // Raw bearer token string
ctx.auth.scopes // string[] — parsed from JWT `scope` claim
ctx.auth.permissions // string[] — parsed from JWT `permissions` claim
ctx.auth.payload // Raw JWT payload (all claims, Record<string, unknown>)
```
### `ctx.auth.user` (UserInfo)
All providers populate these base fields:
| Field | Type | Description |
|-------|------|-------------|
| `userId` | `string` | Unique user identifier (`sub` claim) |
| `email` | `string?` | User's email |
| `name` | `string?` | Display name |
| `username` | `string?` | Username |
| `nickname` | `string?` | Nickname |
| `picture` | `string?` | Avatar URL |
| `roles` | `string[]?` | User roles |
| `permissions` | `string[]?` | User permissions |
Providers may add extra fields (e.g., WorkOS adds `organization_id`, Keycloak adds `username`, Supabase adds `aal`). Access them via `ctx.auth.user.organization_id` or `ctx.auth.payload` for raw claims.
### `ctx.auth.payload` (Raw Claims)
**Type:** `Record<string, unknown>` — values require explicit casts. Applies to **all providers** since `verifyToken` always returns `{ payload: Record<string, unknown> }`.
**Prefer typed accessors** (`ctx.auth.user.*`, `ctx.auth.scopes`, `ctx.auth.permissions`) over raw payload access. If your provider has non-standard claims, map them into typed `ctx.auth.user` fields via `getUserInfo` rather than casting in every tool handler:
```typescript
// ✅ Preferred: map claims once in getUserInfo
oauth: oauthCustomProvider({
// ...endpoints and verifyToken...
getUserInfo: (payload) => ({
userId: payload.sub as string,
email: payload.mail as string,
name: payload.display_name as string,
roles: (payload.groups as string[]) || [],
}),
})
// Then access typed fields in tools:
async (_args, ctx) => object({ email: ctx.auth.user.email })
```
```typescript
// ❌ Avoid: casting raw payload in every tool handler
async (_args, ctx) => {
const exp = ctx.auth.payload.exp as number; // unknown → number cast needed
return object({ expiresAt: new Date(exp * 1000).toISOString() });
}
```
If you must read raw claims (debugging or one-off provider-specific fields), cast explicitly:
```typescript
const exp = ctx.auth.payload.exp as number | undefined;
const customField = ctx.auth.payload.my_field as string;
```
---
## Zero-Config Setup
All built-in remote-auth providers support zero-config via environment variables. Call the factory with no arguments and it reads from `MCP_USE_OAUTH_*` env vars:
```typescript
oauth: oauthAuth0Provider() // reads MCP_USE_OAUTH_AUTH0_*
oauth: oauthWorkOSProvider() // reads MCP_USE_OAUTH_WORKOS_*
oauth: oauthSupabaseProvider() // reads MCP_USE_OAUTH_SUPABASE_*
oauth: oauthKeycloakProvider() // reads MCP_USE_OAUTH_KEYCLOAK_*
```
Or pass config explicitly to override env vars. See each provider's guide for available options.
`oauthProxy` and `oauthCustomProvider` have no zero-config mode — all endpoints must be passed explicitly.
---
## Available Providers
### Remote auth (DCR)
| Provider | Factory | Required Config | Guide |
|----------|---------|-----------------|-------|
| **Auth0** | `oauthAuth0Provider()` | `domain`, `audience` (env: `MCP_USE_OAUTH_AUTH0_DOMAIN`, `MCP_USE_OAUTH_AUTH0_AUDIENCE`) | [auth0.md](auth0.md) |
| **Better Auth** | `oauthBetterAuthProvider({ authURL })` | `BETTER_AUTH_SECRET` | [better-auth.md](better-auth.md) |
| **Clerk** | `oauthClerkProvider()` | `frontendApiUrl` (env: `MCP_USE_OAUTH_CLERK_FRONTEND_API_URL`) | [clerk.md](clerk.md) |
| **WorkOS** | `oauthWorkOSProvider()` | `subdomain` (env: `MCP_USE_OAUTH_WORKOS_SUBDOMAIN`) | [workos.md](workos.md) |
| **Supabase** | `oauthSupabaseProvider()` | `projectId` (env: `MCP_USE_OAUTH_SUPABASE_PROJECT_ID`) | [supabase.md](supabase.md) |
| **Keycloak** | `oauthKeycloakProvider()` | `serverUrl`, `realm` (env: `MCP_USE_OAUTH_KEYCLOAK_SERVER_URL`, `MCP_USE_OAUTH_KEYCLOAK_REALM`) | [keycloak.md](keycloak.md) |
| **Custom (DCR)** | `oauthCustomProvider({ ... })` | `issuer`, endpoints, `verifyToken` | [custom.md](custom.md) |
### OAuth proxy (non-DCR)
| Use for | Factory | Guide |
|---------|---------|-------|
| Google, GitHub, Okta, Azure AD, Auth0 (non-EA), any pre-registered app | `oauthProxy({ ... })` + `jwksVerifier({ ... })` | [custom.md](custom.md#oauth-proxy-non-dcr-providers) |
---
## Making Authenticated API Calls
Use `ctx.auth.accessToken` to call your provider's API on behalf of the user:
```typescript
server.tool(
{ name: "fetch-data", description: "Fetch user data from API" },
async (_args, ctx) => {
const res = await fetch("https://api.example.com/me", {
headers: {
Authorization: `Bearer ${ctx.auth.accessToken}`,
},
});
if (!res.ok) {
return error(`API call failed: ${res.status}`);
}
return object(await res.json());
}
);
```
Provider-specific examples (Supabase, Keycloak, Auth0, etc.) live in each provider's guide.
---
## Common Mistakes
- **Wrong `ctx.auth` shape** — User info is nested: `ctx.auth.user.email`, not `ctx.auth.email`
- **Using `oauthCustomProvider` for non-DCR providers** — For Google, GitHub, Okta, Azure AD, etc., use `oauthProxy` + `jwksVerifier` instead. `oauthCustomProvider` only works with providers that advertise a `registration_endpoint`.
- **Custom `verifyToken` returning the wrong shape** — It must resolve to `{ payload: Record<string, unknown> }` or throw. The proxy surfaces `payload` to `getUserInfo` and to `ctx.auth`.
- **Hardcoding provider credentials** — Use env vars; never commit secrets
- **Skipping JWT verification in production** — `verifyJwt: false` is development only
- **Throwing errors instead of returning `error()`** — Use the `error()` response helper for auth-related failures
---
## Next Steps
- **Auth0 setup** → [auth0.md](auth0.md)
- **Better Auth setup** → [better-auth.md](better-auth.md)
- **Clerk setup** → [clerk.md](clerk.md)
- **WorkOS setup** → [workos.md](workos.md)
- **Supabase setup** → [supabase.md](supabase.md)
- **Keycloak setup** → [keycloak.md](keycloak.md)
- **Custom provider / OAuth proxy** → [custom.md](custom.md)
- **Build tools** → [../server/tools.md](../server/tools.md)
- **See examples** → [../patterns/common-patterns.md](../patterns/common-patterns.md)
references/authentication/supabase.md
# Supabase Authentication
Setting up OAuth with Supabase's OAuth 2.1 server. Supabase hosts `/authorize`, `/token`, `/register`, and `.well-known` discovery on your Supabase project — the MCP server only verifies the resulting JWTs.
**You host the consent UI.** Supabase redirects the browser to a URL you configure, and your route uses the Supabase JS SDK to load authorization details, render sign-in + consent, and submit the decision back to Supabase. mcp-use is not involved in that step — follow Supabase's [OAuth Server — Getting Started guide](https://supabase.com/docs/guides/auth/oauth-server/getting-started).
**Learn more:** [Supabase OAuth Server — MCP Authentication](https://supabase.com/docs/guides/auth/oauth-server/mcp-authentication) · [Standalone starter template](https://github.com/mcp-use/mcp-oauth-supabase-template) · [Runnable example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/mcp-use/examples/server/oauth/supabase)
> This targets Supabase's **OAuth 2.1 server** — a different feature from Supabase Auth's social logins. Enable it explicitly in the dashboard under **Authentication → OAuth Server**.
---
## Quick Start
```typescript
import { MCPServer, oauthSupabaseProvider, object } from "mcp-use/server";
const server = new MCPServer({
name: "my-server",
version: "1.0.0",
oauth: oauthSupabaseProvider(),
});
server.tool(
{ name: "whoami", description: "Get authenticated user info" },
async (_args, ctx) =>
object({
userId: ctx.auth.user.userId,
email: ctx.auth.user.email,
})
);
server.listen();
```
With a `.env` file:
```bash
MCP_USE_OAUTH_SUPABASE_PROJECT_ID=your-project-id
MCP_USE_OAUTH_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
```
JWT verification and `.well-known` passthrough are automatic. You still need to mount your own consent route (see [Hosting the consent UI](#hosting-the-consent-ui) below).
---
## Setup
1. Create (or select) a project in the [Supabase Dashboard](https://app.supabase.com/). Copy the **Project ID** from **Project Settings → General**.
2. **Authentication → OAuth Server** — enable the OAuth 2.1 server and set the **consent screen URL** to the route your MCP server will host (e.g. `http://localhost:3000/auth/consent`). Supabase appends `?authorization_id=<uuid>` when redirecting users there.
3. **Authentication → Sign In / Providers** — enable at least one sign-in method so users can authenticate before consenting:
- **Anonymous sign-ins** — one-click guest sessions, ideal for demos
- **Email + password**, **magic links**, or **OAuth providers** (Google, GitHub, etc.) — for real apps
4. Copy the **publishable key** (`sb_publishable_...`) from **Project Settings → API Keys**. You'll use it in the consent UI and in any tool that calls Supabase REST APIs.
---
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `MCP_USE_OAUTH_SUPABASE_PROJECT_ID` | Yes (hosted) | Your Supabase project ID. Used to derive `https://<id>.supabase.co`. |
| `MCP_USE_OAUTH_SUPABASE_URL` | Yes (self-hosted/local) | Explicit base URL — use for `supabase start` or self-hosted (e.g. `http://localhost:54321`). Overrides `PROJECT_ID` when both are set. |
| `MCP_USE_OAUTH_SUPABASE_PUBLISHABLE_KEY` | Recommended | Publishable key (`sb_publishable_...`) — used by the consent UI and any tools calling Supabase REST/APIs |
| `MCP_USE_OAUTH_SUPABASE_JWT_SECRET` | Only for legacy HS256 projects | JWT secret for HS256 verification |
> New Supabase projects issue `sb_publishable_...` keys. Legacy projects using `anon` JWT keys still work, but prefer publishable keys going forward.
### Finding Your Credentials
- **Project ID**: Project Settings → **General** → Reference ID
- **Publishable key**: Project Settings → **API Keys**
- **JWT Secret** (legacy HS256 only): Project Settings → **JWT Settings** (Legacy)
---
## Configuration Options
Zero-config (reads from env vars):
```typescript
oauth: oauthSupabaseProvider()
```
Explicit config (overrides env vars):
```typescript
oauth: oauthSupabaseProvider({
projectId: "my-project-id",
// supabaseUrl: "http://localhost:54321", // self-hosted / local override
jwtSecret: process.env.MCP_USE_OAUTH_SUPABASE_JWT_SECRET, // legacy HS256 only
verifyJwt: process.env.NODE_ENV === "production",
scopesSupported: ["openid", "profile", "email"],
})
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `projectId` | `string?` | env var | Supabase project ID — derives `https://<id>.supabase.co` |
| `supabaseUrl` | `string?` | env var | Explicit base URL for self-hosted or local Supabase. Takes precedence over `projectId` when set. |
| `jwtSecret` | `string?` | env var | JWT secret for HS256 tokens (legacy projects) |
| `verifyJwt` | `boolean?` | `true` | Set `false` to skip JWT verification (**development only**) |
| `scopesSupported` | `string[]?` | `["openid", "profile", "email"]` | Override advertised scopes |
> One of `projectId` / `supabaseUrl` (or their env vars) is required. Use `supabaseUrl` for `supabase start` or any non-`supabase.co` deployment.
---
## JWT Signing: ES256 vs HS256
The provider auto-detects the algorithm from the token header.
- **ES256 (new projects)** — asymmetric signing via elliptic curve keys. The provider fetches the JWKS endpoint automatically. No `jwtSecret` needed.
- **HS256 (legacy projects)** — symmetric signing via a shared secret. Provide `MCP_USE_OAUTH_SUPABASE_JWT_SECRET` or `jwtSecret` in config.
---
## Hosting the consent UI
Supabase redirects the browser to the consent screen URL you configured. Your route must:
1. Sign the user in if they aren't already (anonymous, magic link, OAuth, etc. — whatever you enabled)
2. Use the Supabase JS SDK to load the authorization details for `authorization_id`
3. Render approve/deny UI
4. Submit the decision back to Supabase and redirect to the resulting URL
Follow Supabase's [OAuth Server — Getting Started guide](https://supabase.com/docs/guides/auth/oauth-server/getting-started) for the canonical implementation. See the [runnable example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/mcp-use/examples/server/oauth/supabase) for a wired-up version (look at `auth-routes.ts`).
---
## User Context
Supabase populates these fields on `ctx.auth.user`:
| Field | Type | Source |
|-------|------|--------|
| `userId` | `string` | `sub` or `user_id` claim |
| `email` | `string?` | `email` claim |
| `name` | `string?` | `user_metadata.name` or `user_metadata.full_name` |
| `username` | `string?` | `user_metadata.username` |
| `picture` | `string?` | `user_metadata.avatar_url` |
| `roles` | `string[]?` | `role` claim (e.g. `["authenticated"]`) |
| `permissions` | `string[]?` | Derived from AAL (e.g. `["aal:aal1"]`) |
| `aal` | `string?` | Authentication Assurance Level |
| `amr` | `array?` | Authentication Methods References |
| `session_id` | `string?` | Supabase session ID |
---
## Making Supabase API Calls
Create a Supabase client scoped to the request using the user's access token so Row Level Security (RLS) policies see the caller as the authenticated user:
```typescript
import { createClient } from "@supabase/supabase-js";
server.tool(
{ name: "list-notes", description: "Fetch the user's notes" },
async (_args, ctx) => {
const supabase = createClient(
`https://${process.env.MCP_USE_OAUTH_SUPABASE_PROJECT_ID}.supabase.co`,
process.env.MCP_USE_OAUTH_SUPABASE_PUBLISHABLE_KEY!,
{
auth: {
persistSession: false,
autoRefreshToken: false,
detectSessionInUrl: false,
},
global: {
headers: { Authorization: `Bearer ${ctx.auth.accessToken}` },
},
}
);
const { data, error: queryError } = await supabase.from("notes").select();
if (queryError) return error(`Failed to fetch notes: ${queryError.message}`);
return object({ notes: data ?? [] });
}
);
```
**Key point:** The `Authorization` header uses the user's access token (for RLS); the publishable key is passed to `createClient` for SDK/API access.
---
## Example `.env`
```bash
# Required: Supabase project ID (Dashboard → Project Settings → General)
MCP_USE_OAUTH_SUPABASE_PROJECT_ID=your-project-id
# Self-hosted / local override — point at `supabase start` or your own host.
# When set, this wins over PROJECT_ID. Omit on supabase.co.
# MCP_USE_OAUTH_SUPABASE_URL=http://localhost:54321
# Recommended: Publishable key (Dashboard → Project Settings → API Keys)
# Used by the consent UI and by tools calling Supabase REST/APIs
MCP_USE_OAUTH_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
# Legacy HS256 projects only (Dashboard → Project Settings → JWT Settings)
# New projects use ES256 + JWKS — leave this unset
# MCP_USE_OAUTH_SUPABASE_JWT_SECRET=your-jwt-secret
```
### Local Supabase (`supabase start`)
```typescript
oauth: oauthSupabaseProvider({
supabaseUrl: "http://localhost:54321",
})
```
The provider derives the issuer and JWKS endpoint from this URL — the local stack exposes `/auth/v1/.well-known/openid-configuration` out of the box, so no extra wiring is needed.
---
## Next Steps
- **Auth overview** → [overview.md](overview.md)
- **WorkOS setup** → [workos.md](workos.md)
- **Auth0 setup** → [auth0.md](auth0.md)
- **Build tools** → [../server/tools.md](../server/tools.md)
references/authentication/workos.md
# WorkOS Authentication
Setting up OAuth with WorkOS AuthKit. DCR mode only — MCP clients register themselves directly with WorkOS; the MCP server just verifies the resulting tokens.
**Learn more:** [WorkOS AuthKit MCP docs](https://workos.com/docs/authkit/mcp) · [Standalone starter template](https://github.com/mcp-use/mcp-oauth-workos-template) · [Runnable example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/mcp-use/examples/server/oauth/workos)
---
## Quick Start
```typescript
import { MCPServer, oauthWorkOSProvider, object } from "mcp-use/server";
const server = new MCPServer({
name: "my-server",
version: "1.0.0",
oauth: oauthWorkOSProvider(),
});
server.tool(
{ name: "whoami", description: "Get authenticated user info" },
async (_args, ctx) =>
object({
userId: ctx.auth.user.userId,
email: ctx.auth.user.email,
name: ctx.auth.user.name,
})
);
server.listen();
```
With a `.env` file:
```bash
MCP_USE_OAUTH_WORKOS_SUBDOMAIN=your-company.authkit.app
```
That's it. JWT verification, OAuth discovery, and `.well-known` passthrough are handled automatically.
---
## Setup
1. Sign up at the [WorkOS Dashboard](https://dashboard.workos.com/) and create a project.
2. **Connect → Configuration** — enable **Dynamic Client Registration**.
3. **Configuration → Redirects** — add your MCP client redirect URI (e.g. `http://localhost:3000/callback`).
4. Copy the full AuthKit domain from **Domains → AuthKit Domain** (e.g. `your-company.authkit.app` — including `.authkit.app`, not just `your-company`).
---
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `MCP_USE_OAUTH_WORKOS_SUBDOMAIN` | Yes | Full AuthKit domain (e.g. `my-company.authkit.app`) |
> If you also need to call the WorkOS Management API from a tool handler, store the API key in any env var you like (e.g. `WORKOS_API_KEY`) and read it inside the tool. It's **not** part of the OAuth provider config anymore.
---
## Configuration Options
Zero-config (reads from env vars):
```typescript
oauth: oauthWorkOSProvider()
```
Explicit config (overrides env vars):
```typescript
oauth: oauthWorkOSProvider({
subdomain: "my-company.authkit.app",
verifyJwt: process.env.NODE_ENV === "production", // default: true
scopesSupported: ["email", "offline_access", "openid", "profile"], // override advertised scopes
})
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `subdomain` | `string` | env var | Full AuthKit domain (e.g., `my-company.authkit.app`) |
| `verifyJwt` | `boolean?` | `true` | Set `false` to skip JWT verification (**development only**) |
| `scopesSupported` | `string[]?` | `["email", "offline_access", "openid", "profile"]` | Override advertised scopes |
---
## User Context
WorkOS populates these fields on `ctx.auth.user`:
| Field | Type | Source |
|-------|------|--------|
| `userId` | `string` | `sub` claim |
| `email` | `string?` | `email` claim |
| `name` | `string?` | `name` claim |
| `username` | `string?` | `preferred_username` claim |
| `picture` | `string?` | `picture` claim |
| `roles` | `string[]?` | `roles` claim |
| `permissions` | `string[]?` | `permissions` claim |
| `email_verified` | `boolean?` | `email_verified` claim |
| `organization_id` | `string?` | `org_id` claim — multi-tenant org ID |
| `sid` | `string?` | Session ID |
### Role-Based Access
```typescript
server.tool(
{ name: "admin-action", description: "Admin-only operation" },
async (_args, ctx) => {
if (!ctx.auth.user.roles?.includes("admin")) {
return error("Forbidden: admin role required");
}
// ... admin logic
return text("Done");
}
);
```
### Multi-Tenant Filtering
Scope queries to the authenticated user's organization:
```typescript
server.tool(
{ name: "get-documents", description: "Get documents for the authenticated org" },
async (_args, ctx) => {
// Custom claims come back as `unknown` — narrow before use
const orgId = ctx.auth.user.organization_id as string | undefined;
if (!orgId) return error("Organization context required");
const documents = await db.documents.findMany({
where: { organizationId: orgId },
});
return object(documents);
}
);
```
---
## Making WorkOS API Calls
Use a WorkOS API key (not the user's access token) for management API calls:
```typescript
const WORKOS_API_KEY = process.env.WORKOS_API_KEY;
server.tool(
{ name: "get-workos-user", description: "Fetch user profile from WorkOS" },
async (_args, ctx) => {
if (!WORKOS_API_KEY) return error("WorkOS API key not configured");
const res = await fetch(
`https://api.workos.com/user_management/users/${ctx.auth.user.userId}`,
{
headers: {
Authorization: `Bearer ${WORKOS_API_KEY}`,
"Content-Type": "application/json",
},
}
);
if (!res.ok) return error(`WorkOS API failed: ${res.status}`);
return object(await res.json());
}
);
```
---
## Example `.env`
```bash
# Required: Full AuthKit domain (WorkOS Dashboard → Domains → AuthKit Domain)
MCP_USE_OAUTH_WORKOS_SUBDOMAIN=my-company.authkit.app
# Optional: WorkOS API key if you call the Management API from tools
WORKOS_API_KEY=sk_test_...
```
---
## Troubleshooting
### Redirect URI Mismatch
If you get a redirect URI error during OAuth, add your client's callback URL to WorkOS:
1. WorkOS Dashboard → **Developer** → **Redirects**
2. Click **Edit redirect URIs** and add the one the client expects (e.g. `http://localhost:3000/oauth/callback`).
Changes may take a few minutes to propagate.
### DCR Not Registering
If the MCP Inspector stalls at the registration step, double-check that **Dynamic Client Registration** is enabled in WorkOS Dashboard → **Connect → Configuration**. Without DCR enabled, there's no pre-configured `client_id` for the Inspector to use.
---
## Next Steps
- **Auth overview** → [overview.md](overview.md)
- **Supabase setup** → [supabase.md](supabase.md)
- **Auth0 setup** → [auth0.md](auth0.md)
- **Build tools** → [../server/tools.md](../server/tools.md)
references/foundations/architecture.md
# Architecture
Understanding how mcp-use servers are structured under the hood.
---
## Server Structure
mcp-use is **built on top of the Hono web framework**. When you create an `MCPServer`, you get:
```typescript
const server = new MCPServer({
name: "my-server",
version: "1.0.0"
});
```
The server instance has three key components:
### 1. `server.app` - Hono Instance
The underlying Hono web application that handles HTTP routing and middleware.
```typescript
// Add custom HTTP routes
server.get('/health', (c) => c.json({ status: 'ok' }));
// Add Hono middleware
server.use(async (c, next) => {
console.log(`Request: ${c.req.method} ${c.req.url}`);
await next();
});
```
**Use for:**
- Custom HTTP endpoints
- Hono-specific middleware
- Direct access to Hono features
### 2. `server.nativeServer` - MCP SDK
The official MCP protocol server from `@modelcontextprotocol/sdk`.
```typescript
// Access native MCP SDK methods (advanced)
server.nativeServer.server.setRequestHandler(...);
```
**Use for:**
- Advanced MCP protocol features
- Direct SDK access (rare)
### 3. MCP Server Methods
High-level methods for defining MCP primitives:
```typescript
server.tool({ ... }, async (input) => { ... });
server.resource({ ... }, async () => { ... });
server.prompt({ ... }, async (input) => { ... });
server.proxy({ child: { url: "..." } });
```
---
## Middleware System
mcp-use supports two kinds of middleware registered via `server.use()`:
1. **HTTP middleware** — intercepts HTTP requests (Hono layer)
2. **MCP middleware** — intercepts MCP operations like tool calls, resource reads, etc.
The `mcp:` prefix in the pattern string distinguishes them:
```typescript
// MCP middleware — fires when MCP operations occur
server.use("mcp:tools/call", async (ctx, next) => { ... });
// HTTP middleware — fires on every HTTP request (no mcp: prefix)
server.use(async (c, next) => { ... });
```
### MCP Middleware
MCP middleware intercepts operations at the protocol level, after HTTP routing. It is ideal for cross-cutting concerns like logging, authentication, rate limiting, and tool filtering.
```typescript
// Catch-all: every MCP operation
server.use("mcp:*", async (ctx, next) => {
const start = Date.now();
const result = await next();
console.log(`${ctx.method} — ${Date.now() - start}ms`);
return result;
});
// Specific operation
server.use("mcp:tools/call", async (ctx, next) => {
if (!ctx.auth?.scopes.includes("tools:call")) {
throw new Error("Insufficient scope");
}
return next();
});
// Namespace wildcard
server.use("mcp:tools/*", async (ctx, next) => {
console.log(`Tool operation: ${ctx.method}`);
return next();
});
```
**Pattern matching:**
| Pattern | Matches |
|---------|---------|
| `mcp:*` | Every MCP operation |
| `mcp:tools/*` | All tool operations |
| `mcp:tools/call` | Tool calls only |
| `mcp:resources/*` | All resource operations |
| `mcp:prompts/*` | All prompt operations |
**`MiddlewareContext` fields:**
| Field | Description |
|-------|-------------|
| `ctx.method` | MCP method name (e.g. `"tools/call"`) |
| `ctx.params` | Request params — mutable |
| `ctx.session` | Session ID when available |
| `ctx.auth` | OAuth info (scopes, permissions, user) when OAuth is configured |
| `ctx.state` | `Map` for passing data across middleware in the same request |
### HTTP Middleware
mcp-use uses **Hono's middleware system** for HTTP middleware. Both Hono and Express middleware are supported.
```typescript
// ✅ Hono style
server.use(async (c, next) => {
// c = Hono Context object
await next();
});
// ✅ Express style (auto-adapted)
import morgan from "morgan";
server.use(morgan("combined"));
```
#### Hono-Compatible Middleware
```typescript
import { rateLimiter } from "hono-rate-limiter";
server.use(rateLimiter({
windowMs: 15 * 60 * 1000,
limit: 100,
keyGenerator: (c) =>
c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown",
}));
```
#### Express Middleware (Auto-Adapted)
```typescript
import rateLimit from "express-rate-limit";
server.use("/api", rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
}));
```
---
## server.use() routing
`server.use()` handles three kinds of middleware depending on the first argument:
```typescript
// MCP middleware (mcp: prefix) — intercepts MCP operations
server.use("mcp:tools/call", async (ctx, next) => { ... });
server.use("mcp:*", async (ctx, next) => { ... });
// HTTP middleware with path (leading /) — Hono path-scoped middleware
server.use("/api/*", someHttpMiddleware);
// HTTP middleware without path — Hono catch-all
server.use(async (c, next) => { ... });
```
`server.app.use()` always routes to Hono and never to MCP middleware:
```typescript
// Always Hono, never MCP
server.app.use(async (c, next) => { ... });
```
**In practice:** Use `server.use("mcp:...", handler)` for MCP middleware and `server.use(handler)` or `server.app.use(handler)` for HTTP middleware.
---
## Request Lifecycle
Understanding the flow of a request:
```
1. HTTP Request arrives
↓
2. Hono middleware chain (server.use without mcp: prefix)
↓
3. OAuth bearer auth (if `oauth` is configured — verifies JWT, populates ctx.auth)
↓
4. MCP protocol routing
↓
5. MCP middleware chain (server.use('mcp:...') handlers)
↓
6. Tool/Resource/Prompt handler
↓
7. Response helpers (text, object, etc.)
↓
8. MCP protocol response
↓
9. HTTP Response
```
> When `oauth` is configured, unauthenticated requests to `/mcp/*` receive a `401` with a `WWW-Authenticate` header that tells MCP clients where to start the OAuth flow. See [../authentication/overview.md](../authentication/overview.md) for setup.
### Example Flow
```typescript
server.app.use(async (c, next) => {
console.log("1. Middleware start");
await next();
console.log("5. Middleware end");
});
server.tool(
{ name: "greet", schema: z.object({ name: z.string() }) },
async ({ name }) => {
console.log("3. Tool handler");
return text(`Hello, ${name}`); // 4. Response helper
}
);
```
---
## Custom HTTP Endpoints
You can mix MCP tools with custom HTTP routes:
```typescript
// MCP tool (called via MCP protocol)
server.tool({ name: "calculate", ... }, async (input) => { ... });
// Custom HTTP endpoint (called via HTTP)
server.app.get('/api/status', (c) => {
return c.json({
uptime: process.uptime(),
tools: server.registeredTools.length
});
});
// Both coexist on the same server
```
**Use cases:**
- Health check endpoints
- Webhooks
- Admin APIs
- Public data endpoints
---
## Common Patterns
### Logging Middleware
```typescript
server.use(async (c, next) => {
const start = Date.now();
await next();
const duration = Date.now() - start;
console.log(`${c.req.method} ${c.req.path} - ${duration}ms`);
});
```
### Error Handling
```typescript
server.use(async (c, next) => {
try {
await next();
} catch (err) {
console.error("Server error:", err);
return c.json({ error: "Internal server error" }, 500);
}
});
```
---
## Best Practices
### 1. Use Hono-Compatible Middleware Packages
```typescript
✅ import { rateLimiter } from "hono-rate-limiter";
✅ server.use(rateLimiter({ ... }));
❌ Writing custom rate limiting logic
```
### 2. Understand the Signature
```typescript
✅ server.use(async (c, next) => { ... });
❌ server.use((req, res, next) => { ... }); // Express style
```
### 3. Access Hono When Needed
```typescript
✅ server.app.get('/custom', (c) => c.json({ ... }));
❌ Trying to add routes via server.get() // Doesn't exist
```
### 4. Keep MCP Separate from HTTP
```typescript
✅ MCP tools for AI interactions
✅ HTTP routes for webhooks/admin
✅ Both on same server
❌ Mixing concerns in tool handlers
```
---
## Key Takeaways
- **Built on Hono** - mcp-use wraps the Hono web framework
- **Two middleware layers** - HTTP (Hono) layer and MCP operation layer
- **`mcp:` prefix** - Use `server.use('mcp:tools/call', handler)` for MCP middleware; no prefix = HTTP middleware
- **Hono style** - HTTP middleware uses `(c, next) => ...` signature
- **Two access points** - `server.use()` and `server.app.use()` both work for HTTP
---
## Next Steps
- **Build tools** → [../server/tools.md](../server/tools.md)
- **Add resources** → [../server/resources.md](../server/resources.md)
- **Understand primitives** → [concepts.md](concepts.md)
references/foundations/concepts.md
# MCP Concepts
Core primitives you'll use to build MCP servers with mcp-use.
## The Four Primitives
### 1. Tool
A **backend action** the AI can call. Takes input, returns output.
**Use for:** Actions, operations, mutations, API calls
```typescript
server.tool({ name, description, schema }, async (input) => {
// Your logic here
return text("result");
});
```
→ **Detailed guide:** [../server/tools.md](../server/tools.md)
### 2. Resource
**Read-only data** that clients can fetch. No input parameters (use resource templates for that).
**Use for:** Configuration, static data, listings
```typescript
server.resource({ uri, name, mimeType }, async () => {
return object({ data });
});
```
→ **Detailed guide:** [../server/resources.md](../server/resources.md)
### 3. Prompt
A **reusable message template** with parameters.
**Use for:** Common prompts, instruction templates
```typescript
server.prompt({ name, description, schema }, async (input) => {
return text(`Your prompt template with ${input.param}`);
});
```
→ **Detailed guide:** [../server/prompts.md](../server/prompts.md)
### 4. Widget (Tool + UI)
A **tool that returns visual UI**. Same as a tool but renders a React component.
**Use for:** Browsing data, interactive selection, visual feedback
```typescript
server.tool(
{ name, schema, widget: { name: "widget-name" } },
async (input) => widget({ props: { data }, output: text("...") })
);
```
→ **Detailed guide:** [../widgets/basics.md](../widgets/basics.md)
---
## Decision Matrix
| Need | Use | Example |
|------|-----|---------|
| Backend action | Tool | send-email, create-user, fetch-data |
| Read-only data | Resource | config, user-profile, api-docs |
| Prompt template | Prompt | code-review, summarize, translate |
| Visual UI | Widget Tool | search-results, calendar, dashboard |
---
## Tool vs Widget?
**Use a tool (no widget) when:**
- Output is simple text or data
- No visual representation helps
- Quick conversational response
**Use a widget when:**
- Browsing/comparing multiple items
- Visual data improves understanding (charts, images)
- Interactive selection is easier visually
**When in doubt:** Use a widget. It makes the experience better.
---
## Key Patterns
### 1. One tool = one capability
❌ `manage-users` (too broad)
✅ `create-user`, `delete-user`, `list-users`
### 2. Don't lazy-load
Tool calls are expensive. Return all needed data upfront.
❌ `list-products` + `get-product-details` (two calls)
✅ `list-products` returns full data including details
### 3. Widget handles its own state
UI state (selections, filters) lives in the widget via `useState` or `setState`.
❌ `select-item` tool, `set-filter` tool
✅ Widget manages internally
### 4. `exposeAsTool` defaults to `false`
Widgets are not auto-registered as tools by default. When defining a custom tool with `widget: { name }`, omitting `exposeAsTool` (or leaving it `false`) is correct — the custom tool handles registration:
```typescript
export const widgetMetadata: WidgetMetadata = {
description: "...",
props: z.object({...}),
// exposeAsTool defaults to false — correct for custom-tool pattern
};
```
---
## Next Steps
- Build your first tool: [quickstart.md](quickstart.md)
- Deep dive on tools: [../server/tools.md](../server/tools.md)
- Learn about widgets: [../widgets/basics.md](../widgets/basics.md)
references/foundations/deployment.md
# Deployment
Guide for deploying MCP servers to production.
## ⚠️ FIRST: Ensure the User is Logged In
**Before any deployment command, always verify authentication:**
```bash
mcp-use whoami
```
If this fails or the user has never logged in, run `mcp-use login` first — it opens a browser for OAuth.
---
## Quick Deploy (Manufact Cloud)
The fastest path to production — one command:
```bash
mcp-use deploy
```
Or via the npm script (pre-configured in all templates):
```bash
npm run deploy
```
Your server is live at `https://{slug}.run.mcp-use.com/mcp`.
---
## Prerequisites
Before running `mcp-use deploy`:
1. **Logged in** — run `mcp-use whoami` to verify, or `mcp-use login` if needed
2. **Git repository** — your project must be a git repo
3. **GitHub remote** — the `origin` remote must point to GitHub (SSH or HTTPS)
4. **Changes pushed** — deployment pulls from GitHub, not your local files. Commit and push first.
5. **GitHub App installed** — the mcp-use GitHub App must have access to the repo. The CLI will prompt you to install it if missing.
---
## Deploy Options
```bash
mcp-use deploy [options]
```
| Flag | Description | Default |
|------|-------------|---------|
| `--name <name>` | Custom deployment name | `package.json` name or directory name |
| `--port <port>` | Server port | `3000` |
| `--runtime <runtime>` | `"node"` or `"python"` | Auto-detected from project files |
| `--env <KEY=VALUE>` | Set environment variable (repeatable) | — |
| `--env-file <path>` | Load env vars from a file | — |
| `--open` | Open deployment in browser after success | `false` |
| `--new` | Force a fresh deployment (ignore existing link) | `false` |
### Setting Environment Variables
```bash
# Inline
mcp-use deploy --env API_KEY=sk-xxx --env DATABASE_URL=postgres://...
# From file
mcp-use deploy --env-file .env.production
```
**NEVER commit secrets to git.** Use `--env` or `--env-file` for API keys, database URLs, and other sensitive values.
---
## Common Mistakes
- ❌ Running `mcp-use deploy` without verifying auth first
- ✅ Always run `mcp-use whoami` before deploying — run `mcp-use login` if needed
- ❌ Running `mcp-use deploy` with uncommitted/unpushed changes
- ✅ The cloud builds from GitHub — always `git push` first
- ❌ Hardcoding secrets in code or committing `.env`
- ✅ Use `--env` / `--env-file` flags, or `mcp-use deployments env set`
- ❌ Forgetting to install the mcp-use GitHub App on the repo
- ✅ The CLI will prompt you, but you can also install it at `github.com/apps/mcp-use`
- ❌ Running `mcp-use start` without `mcp-use build` first
- ✅ Always build before starting in production
references/foundations/quickstart.md
# Quick Start
Build your first MCP server tool in 5 minutes.
## Setup
### Scaffolding a New Project
```bash
npx create-mcp-use-app my-server
cd my-server
npm run dev
```
This installs dependencies, starts the server on port 3000, and opens the inspector at `http://localhost:3000/inspector`.
### Choosing a Template
Pick the template that matches what the user is building:
| Template | Command | Use When |
|----------|---------|----------|
| **starter** (default) | `npx create-mcp-use-app my-server` | Full-featured server with tools, resources, prompts, and widget examples |
| **mcp-apps** | `npx create-mcp-use-app my-server --template mcp-apps` | Widget-focused for ChatGPT, Claude, and other MCP Apps-compatible clients |
| **blank** | `npx create-mcp-use-app my-server --template blank` | Clean slate — bare server with commented-out examples |
| **GitHub repo** | `npx create-mcp-use-app my-server --template owner/repo` | Custom or community templates from any GitHub repository |
**When unsure, use `mcp-apps`.** It's the recommended default with widget support for ChatGPT, Claude, and other MCP Apps-compatible clients.
### Common Flags
```bash
# Choose package manager
npx create-mcp-use-app my-server --npm
npx create-mcp-use-app my-server --pnpm
# Skip interactive prompts
npx create-mcp-use-app my-server --install --skills
# List all available templates
npx create-mcp-use-app --list-templates
```
### What Each Template Produces
**starter:**
```
my-server/
├── index.ts # Server with example tool, resource, and prompt
├── resources/ # Widget directory (display-weather.tsx example)
├── public/ # Static assets (favicon, icon)
├── package.json # Pre-configured scripts: dev, build, start, deploy
└── tsconfig.json
```
**mcp-apps:**
```
my-server/
├── index.ts # Server with widget-returning tools
├── resources/ # Widget directory (product-search-result/ example)
│ └── product-search-result/
│ ├── widget.tsx # React widget with carousel UI
│ └── components/ # Reusable widget components
├── public/
├── package.json
└── tsconfig.json
```
**blank:**
```
my-server/
├── index.ts # Bare MCPServer with commented-out examples
├── public/
├── package.json
└── tsconfig.json
```
### Development Workflow
After scaffolding:
1. `npm run dev` — starts server with hot reload + inspector
2. Edit `index.ts` to add tools, resources, prompts
3. Add widgets as `.tsx` files in `resources/`
4. Test everything at `http://localhost:3000/inspector`
5. `npm run build` — production build
6. `npm run deploy` — deploy to production
---
## Your First Tool
Open `index.ts` and you'll see a basic server. Let's add a simple tool:
```typescript
import { MCPServer, text } from "mcp-use/server";
import { z } from "zod";
const server = new MCPServer({
name: "my-server",
title: "My Server",
version: "1.0.0",
baseUrl: process.env.MCP_URL || "http://localhost:3000"
});
// Add this tool
server.tool(
{
name: "greet",
description: "Greet a user by name",
schema: z.object({
name: z.string().describe("User's name")
})
},
async ({ name }) => {
return text(`Hello, ${name}! Welcome to MCP.`);
}
);
server.listen();
```
**Save the file** - the server auto-reloads!
**Test it:**
1. Open inspector (`http://localhost:3000/inspector`)
2. Click "List Tools"
3. Find "greet" tool
4. Click "Call Tool"
5. Enter `{"name": "Alice"}`
6. See response: "Hello, Alice! Welcome to MCP."
---
## Add Mock Data
Let's build a weather tool with mock data:
```typescript
// Mock weather data
const mockWeather: Record<string, { temp: number; conditions: string }> = {
"New York": { temp: 22, conditions: "Partly Cloudy" },
"London": { temp: 15, conditions: "Rainy" },
"Tokyo": { temp: 28, conditions: "Sunny" },
"Paris": { temp: 18, conditions: "Overcast" }
};
server.tool(
{
name: "get-weather",
description: "Get current weather for a city",
schema: z.object({
city: z.string().describe("City name")
})
},
async ({ city }) => {
const weather = mockWeather[city];
if (!weather) {
return text(`No weather data for ${city}`);
}
return text(
`Weather in ${city}: ${weather.temp}°C, ${weather.conditions}`
);
}
);
```
**Test it:**
- Call tool with `{"city": "Tokyo"}`
- Response: "Weather in Tokyo: 28°C, Sunny"
---
## Add Structure
Return structured data with `object()`:
```typescript
import { MCPServer, text, object } from "mcp-use/server";
server.tool(
{
name: "get-weather-detailed",
description: "Get detailed weather information",
schema: z.object({
city: z.string().describe("City name")
})
},
async ({ city }) => {
const weather = mockWeather[city];
if (!weather) {
return object({ error: `No data for ${city}` });
}
return object({
city,
temperature: weather.temp,
conditions: weather.conditions,
unit: "celsius",
timestamp: new Date().toISOString()
});
}
);
```
---
## Add a Resource
Resources provide read-only data:
```typescript
server.resource(
{
name: "available_cities",
uri: "weather://available-cities",
title: "Available Cities",
description: "List of cities with weather data"
},
async () => object({
cities: Object.keys(mockWeather)
})
);
```
**Test it:**
1. Inspector → "List Resources"
2. Find "Available Cities"
3. Click "Read Resource"
4. See: `{"cities": ["New York", "London", "Tokyo", "Paris"]}`
---
## Next Steps
**Now that you have the basics:**
1. **Learn response helpers** → [../server/response-helpers.md](../server/response-helpers.md)
2. **Build your first widget** → [../widgets/basics.md](../widgets/basics.md)
3. **See complete examples** → [../patterns/common-patterns.md](../patterns/common-patterns.md)
**Want to add visual UI?** Continue to widgets:
- [Widget Basics](../widgets/basics.md) - Create your first interactive widget
references/patterns/common-patterns.md
# Common Patterns
Complete end-to-end examples showing server + widget implementations for common use cases.
**Examples:** Weather app, Todo list, Recipe browser, File manager
---
## Weather App
### Server (index.ts)
```typescript
import { MCPServer, text, widget, object, error } from "mcp-use/server";
import { z } from "zod";
const server = new MCPServer({
name: "weather-server",
title: "Weather Server",
version: "1.0.0",
baseUrl: process.env.MCP_URL || "http://localhost:3000"
});
// Mock weather data
const weatherData: Record<string, { temp: number; conditions: string; icon: string }> = {
"New York": { temp: 22, conditions: "Partly Cloudy", icon: "⛅" },
"London": { temp: 15, conditions: "Rainy", icon: "🌧️" },
"Tokyo": { temp: 28, conditions: "Sunny", icon: "☀️" },
"Paris": { temp: 18, conditions: "Overcast", icon: "☁️" },
"Sydney": { temp: 25, conditions: "Clear", icon: "🌤️" }
};
// Tool: Get weather with widget
server.tool(
{
name: "get-weather",
description: "Get current weather for a city",
schema: z.object({
city: z.string().describe("City name (e.g., 'New York', 'Tokyo')")
}),
widget: {
name: "weather-display",
invoking: "Fetching weather...",
invoked: "Weather loaded"
}
},
async ({ city }) => {
const data = weatherData[city];
if (!data) {
return error(`No weather data for ${city}. Available cities: ${Object.keys(weatherData).join(", ")}`);
}
return widget({
props: {
city,
temp: data.temp,
conditions: data.conditions,
icon: data.icon,
timestamp: new Date().toISOString()
},
output: text(`Weather in ${city}: ${data.temp}°C, ${data.conditions}`)
});
}
);
// Resource: Available cities
server.resource(
{
name: "available_cities",
uri: "weather://cities",
title: "Available Cities",
description: "List of cities with weather data"
},
async () => object({
cities: Object.keys(weatherData)
})
);
server.listen();
```
### Widget (resources/weather-display.tsx)
```tsx
import { McpUseProvider, useWidget, useWidgetTheme, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
export const widgetMetadata: WidgetMetadata = {
description: "Display weather information for a city",
props: z.object({
city: z.string(),
temp: z.number(),
conditions: z.string(),
icon: z.string(),
timestamp: z.string()
}),
exposeAsTool: false
};
export default function WeatherDisplay() {
const { props, isPending } = useWidget();
const theme = useWidgetTheme();
if (isPending) {
return (
<McpUseProvider autoSize>
<div style={{ padding: 40, textAlign: "center" }}>
<div style={{ fontSize: 48, marginBottom: 16 }}>🌍</div>
<p>Loading weather...</p>
</div>
</McpUseProvider>
);
}
const bgColor = theme === "dark" ? "#1e1e1e" : "#ffffff";
const textColor = theme === "dark" ? "#e0e0e0" : "#1a1a1a";
const secondaryColor = theme === "dark" ? "#b0b0b0" : "#666";
return (
<McpUseProvider autoSize>
<div style={{
padding: 24,
backgroundColor: bgColor,
color: textColor,
borderRadius: 8
}}>
<h2 style={{ margin: "0 0 8px 0", fontSize: 24 }}>{props.city}</h2>
<p style={{ margin: "0 0 20px 0", color: secondaryColor, fontSize: 12 }}>
{new Date(props.timestamp).toLocaleString()}
</p>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<div style={{ fontSize: 64 }}>{props.icon}</div>
<div>
<div style={{ fontSize: 48, fontWeight: "bold" }}>{props.temp}°C</div>
<div style={{ fontSize: 18, color: secondaryColor }}>{props.conditions}</div>
</div>
</div>
</div>
</McpUseProvider>
);
}
```
---
## Todo List
### Server (index.ts)
```typescript
import { MCPServer, text, widget, object, error } from "mcp-use/server";
import { z } from "zod";
const server = new MCPServer({
name: "todo-server",
title: "Todo Server",
version: "1.0.0"
});
// Mock database
let todos: Array<{ id: string; title: string; completed: boolean }> = [
{ id: "1", title: "Learn MCP", completed: true },
{ id: "2", title: "Build first widget", completed: false },
{ id: "3", title: "Deploy server", completed: false }
];
// Tool: List todos with widget
server.tool(
{
name: "list-todos",
description: "List all todos",
schema: z.object({}),
widget: {
name: "todo-list",
invoking: "Loading todos...",
invoked: "Todos loaded"
}
},
async () => {
return widget({
props: {
todos,
totalCount: todos.length,
completedCount: todos.filter(t => t.completed).length
},
output: text(`Found ${todos.length} todos (${todos.filter(t => t.completed).length} completed)`)
});
}
);
// Tool: Create todo
server.tool(
{
name: "create-todo",
description: "Create a new todo",
schema: z.object({
title: z.string().describe("Todo title")
})
},
async ({ title }) => {
const newTodo = {
id: Date.now().toString(),
title,
completed: false
};
todos.push(newTodo);
return text(`Created todo: ${title}`);
}
);
// Tool: Toggle todo
server.tool(
{
name: "toggle-todo",
description: "Toggle todo completion status",
schema: z.object({
id: z.string().describe("Todo ID"),
completed: z.boolean().describe("New completion status")
})
},
async ({ id, completed }) => {
const todo = todos.find(t => t.id === id);
if (!todo) {
return error(`Todo not found: ${id}`);
}
todo.completed = completed;
return text(`Todo ${completed ? "completed" : "uncompleted"}`);
}
);
// Tool: Delete todo
server.tool(
{
name: "delete-todo",
description: "Delete a todo",
schema: z.object({
id: z.string().describe("Todo ID")
}),
annotations: {
destructiveHint: true
}
},
async ({ id }) => {
const index = todos.findIndex(t => t.id === id);
if (index === -1) {
return error(`Todo not found: ${id}`);
}
const deleted = todos.splice(index, 1)[0];
return text(`Deleted todo: ${deleted.title}`);
}
);
server.listen();
```
### Widget (resources/todo-list.tsx)
```tsx
import { useState } from "react";
import { McpUseProvider, useWidget, useWidgetTheme, useCallTool, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
export const widgetMetadata: WidgetMetadata = {
description: "Interactive todo list",
props: z.object({
todos: z.array(z.object({
id: z.string(),
title: z.string(),
completed: z.boolean()
})),
totalCount: z.number(),
completedCount: z.number()
}),
exposeAsTool: false
};
export default function TodoList() {
const { props, isPending } = useWidget();
const theme = useWidgetTheme();
const { callTool: createTodo, isPending: isCreating } = useCallTool("create-todo");
const { callTool: toggleTodo } = useCallTool("toggle-todo");
const { callTool: deleteTodo } = useCallTool("delete-todo");
const [newTodo, setNewTodo] = useState("");
const [deletingId, setDeletingId] = useState<string | null>(null);
if (isPending) {
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>Loading todos...</div>
</McpUseProvider>
);
}
// Theme-aware colors (see ui-guidelines.md for useColors() hook pattern)
const colors = {
bg: theme === "dark" ? "#1e1e1e" : "#ffffff",
text: theme === "dark" ? "#e0e0e0" : "#1a1a1a",
border: theme === "dark" ? "#404040" : "#e0e0e0",
hover: theme === "dark" ? "#2a2a2a" : "#f5f5f5"
};
const handleCreate = (e: React.FormEvent) => {
e.preventDefault();
if (!newTodo.trim()) return;
createTodo({ title: newTodo }, {
onSuccess: () => setNewTodo(""),
onError: () => alert("Failed to create todo"),
});
};
const handleToggle = (id: string, completed: boolean) => {
toggleTodo({ id, completed: !completed });
};
const handleDelete = (id: string) => {
setDeletingId(id);
deleteTodo({ id }, {
onError: () => alert("Failed to delete"),
onSettled: () => setDeletingId(null),
});
};
return (
<McpUseProvider autoSize>
<div style={{ padding: 20, backgroundColor: colors.bg, color: colors.text }}>
<h2 style={{ margin: "0 0 8px 0" }}>
Todos ({props.completedCount}/{props.totalCount})
</h2>
{/* Create form */}
<form onSubmit={handleCreate} style={{ marginBottom: 16, display: "flex", gap: 8 }}>
<input
type="text"
value={newTodo}
onChange={e => setNewTodo(e.target.value)}
placeholder="New todo..."
disabled={isCreating}
style={{
flex: 1,
padding: 8,
border: `1px solid ${colors.border}`,
borderRadius: 4,
backgroundColor: colors.bg,
color: colors.text
}}
/>
<button
type="submit"
disabled={isCreating}
style={{
padding: "8px 16px",
border: "none",
borderRadius: 4,
backgroundColor: "#0066cc",
color: "white",
cursor: isCreating ? "not-allowed" : "pointer"
}}
>
{isCreating ? "Adding..." : "Add"}
</button>
</form>
{/* Todo list */}
<div>
{props.todos.map(todo => (
<div
key={todo.id}
style={{
display: "flex",
alignItems: "center",
gap: 8,
padding: 12,
borderBottom: `1px solid ${colors.border}`,
backgroundColor: colors.bg
}}
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id, todo.completed)}
style={{ cursor: "pointer" }}
/>
<span style={{
flex: 1,
textDecoration: todo.completed ? "line-through" : "none",
opacity: todo.completed ? 0.6 : 1
}}>
{todo.title}
</span>
<button
onClick={() => handleDelete(todo.id)}
disabled={deletingId === todo.id}
style={{
padding: "4px 12px",
border: "none",
borderRadius: 4,
backgroundColor: "transparent",
color: "#dc3545",
cursor: deletingId === todo.id ? "not-allowed" : "pointer"
}}
>
{deletingId === todo.id ? "..." : "Delete"}
</button>
</div>
))}
</div>
{props.todos.length === 0 && (
<p style={{ textAlign: "center", color: colors.border, padding: 40 }}>
No todos yet. Create one above!
</p>
)}
</div>
</McpUseProvider>
);
}
```
---
## Recipe Browser
### Server (index.ts)
```typescript
import { MCPServer, widget, text } from "mcp-use/server";
import { z } from "zod";
const server = new MCPServer({
name: "recipe-server",
title: "Recipe Server",
version: "1.0.0"
});
// Mock recipe data
const recipes = [
{
id: "1",
name: "Spaghetti Carbonara",
category: "Italian",
time: 20,
difficulty: "Easy",
ingredients: ["Spaghetti", "Eggs", "Bacon", "Parmesan", "Black pepper"],
instructions: "Cook pasta. Fry bacon. Mix eggs and cheese. Combine all with pasta."
},
{
id: "2",
name: "Chicken Tikka Masala",
category: "Indian",
time: 45,
difficulty: "Medium",
ingredients: ["Chicken", "Yogurt", "Tomatoes", "Cream", "Spices"],
instructions: "Marinate chicken. Cook in spiced tomato sauce. Add cream."
},
{
id: "3",
name: "Caesar Salad",
category: "Salad",
time: 15,
difficulty: "Easy",
ingredients: ["Romaine lettuce", "Croutons", "Parmesan", "Caesar dressing"],
instructions: "Toss lettuce with dressing. Top with croutons and cheese."
}
];
// Tool: Browse recipes
server.tool(
{
name: "browse-recipes",
description: "Browse recipe collection",
schema: z.object({
category: z.string().optional().describe("Filter by category (Italian, Indian, Salad)")
}),
widget: {
name: "recipe-browser",
invoking: "Loading recipes...",
invoked: "Recipes loaded"
}
},
async ({ category }) => {
const filtered = category
? recipes.filter(r => r.category === category)
: recipes;
return widget({
props: {
recipes: filtered,
categories: ["All", ...new Set(recipes.map(r => r.category))],
selectedCategory: category || "All"
},
output: text(`Found ${filtered.length} recipes`)
});
}
);
server.listen();
```
### Widget (resources/recipe-browser.tsx)
```tsx
import { useState, useEffect } from "react";
import { McpUseProvider, useWidget, useWidgetTheme, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
export const widgetMetadata: WidgetMetadata = {
description: "Browse and view recipes",
props: z.object({
recipes: z.array(z.object({
id: z.string(),
name: z.string(),
category: z.string(),
time: z.number(),
difficulty: z.string(),
ingredients: z.array(z.string()),
instructions: z.string()
})),
categories: z.array(z.string()),
selectedCategory: z.string()
}),
exposeAsTool: false
};
export default function RecipeBrowser() {
const { props, isPending } = useWidget();
const theme = useWidgetTheme();
const [selectedRecipe, setSelectedRecipe] = useState<string | null>(null);
const [filter, setFilter] = useState<string>("all");
// Sync initial filter from props once loaded
useEffect(() => {
if (!isPending && props.selectedCategory) {
setFilter(props.selectedCategory);
}
}, [isPending, props.selectedCategory]);
if (isPending) {
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>Loading recipes...</div>
</McpUseProvider>
);
}
// Theme-aware colors (see ui-guidelines.md for useColors() hook pattern)
const colors = {
bg: theme === "dark" ? "#1e1e1e" : "#ffffff",
text: theme === "dark" ? "#e0e0e0" : "#1a1a1a",
secondary: theme === "dark" ? "#b0b0b0" : "#666",
border: theme === "dark" ? "#404040" : "#e0e0e0",
hover: theme === "dark" ? "#2a2a2a" : "#f5f5f5"
};
const filteredRecipes = filter === "All"
? props.recipes
: props.recipes.filter(r => r.category === filter);
const selected = filteredRecipes.find(r => r.id === selectedRecipe);
return (
<McpUseProvider autoSize>
<div style={{ backgroundColor: colors.bg, color: colors.text }}>
{/* Header */}
<div style={{ padding: 16, borderBottom: `1px solid ${colors.border}` }}>
<h2 style={{ margin: "0 0 12px 0" }}>Recipe Browser</h2>
{/* Category filters */}
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
{props.categories.map(cat => (
<button
key={cat}
onClick={() => setFilter(cat)}
style={{
padding: "6px 12px",
border: `1px solid ${colors.border}`,
borderRadius: 4,
backgroundColor: filter === cat ? "#0066cc" : "transparent",
color: filter === cat ? "white" : colors.text,
cursor: "pointer",
fontSize: 14
}}
>
{cat}
</button>
))}
</div>
</div>
{/* Recipe list */}
<div style={{ display: "flex" }}>
<div style={{ flex: 1, borderRight: `1px solid ${colors.border}` }}>
{filteredRecipes.map(recipe => (
<div
key={recipe.id}
onClick={() => setSelectedRecipe(recipe.id)}
style={{
padding: 16,
borderBottom: `1px solid ${colors.border}`,
cursor: "pointer",
backgroundColor: selectedRecipe === recipe.id ? colors.hover : "transparent"
}}
>
<h3 style={{ margin: "0 0 4px 0", fontSize: 16 }}>{recipe.name}</h3>
<p style={{
margin: 0,
fontSize: 12,
color: colors.secondary
}}>
{recipe.category} • {recipe.time} min • {recipe.difficulty}
</p>
</div>
))}
</div>
{/* Recipe detail */}
<div style={{ flex: 2, padding: 16 }}>
{selected ? (
<div>
<h2 style={{ margin: "0 0 8px 0" }}>{selected.name}</h2>
<p style={{ margin: "0 0 16px 0", color: colors.secondary }}>
{selected.category} • {selected.time} minutes • {selected.difficulty}
</p>
<h3 style={{ fontSize: 16, marginBottom: 8 }}>Ingredients</h3>
<ul style={{ marginBottom: 16, paddingLeft: 20 }}>
{selected.ingredients.map((ing, i) => (
<li key={i}>{ing}</li>
))}
</ul>
<h3 style={{ fontSize: 16, marginBottom: 8 }}>Instructions</h3>
<p style={{ lineHeight: 1.6 }}>{selected.instructions}</p>
</div>
) : (
<p style={{ color: colors.secondary, textAlign: "center", paddingTop: 40 }}>
Select a recipe to view details
</p>
)}
</div>
</div>
</div>
</McpUseProvider>
);
}
```
---
## Key Patterns Demonstrated
### 1. **Mock Data First**
All examples use mock data, making it easy to prototype and test before connecting real APIs.
### 2. **Widget + Tool Combination**
Each example shows how to pair a tool with a widget for visual output.
### 3. **Interactive Actions**
Todo list shows create/update/delete operations from within widgets using `useCallTool()`.
### 4. **Theme Support**
All widgets use `useWidgetTheme()` to adapt to light/dark mode.
### 5. **State Management**
Recipe browser demonstrates local widget state (selected recipe, filters) vs server state (recipe data).
### 6. **Error Handling**
Weather app shows proper error responses when data not found.
### 7. **Loading States**
All widgets check `isPending` and show loading UI.
### 8. **Master-Detail Layout**
Recipe browser shows a master-detail pattern with list + detail view.
---
## Expanding These Examples
### Add Real APIs
Replace mock data with API calls:
```typescript
// Weather with real API
const WEATHER_API_KEY = process.env.WEATHER_API_KEY;
server.tool(
{ name: "get-weather", schema: z.object({ city: z.string() }), widget: { name: "weather-display" } },
async ({ city }) => {
if (!WEATHER_API_KEY) {
return error("WEATHER_API_KEY not configured. Set it in environment variables.");
}
const response = await fetch(
`https://api.weatherapi.com/v1/current.json?key=${WEATHER_API_KEY}&q=${city}`
);
if (!response.ok) {
return error(`Weather API error: ${response.statusText}`);
}
const data = await response.json();
return widget({
props: {
city: data.location.name,
temp: data.current.temp_c,
conditions: data.current.condition.text,
icon: data.current.condition.icon,
timestamp: data.current.last_updated
},
output: text(`Weather in ${city}: ${data.current.temp_c}°C, ${data.current.condition.text}`)
});
}
);
```
### Add Database
Replace in-memory data with database:
```typescript
import { Database } from "better-sqlite3";
const db = new Database("todos.db");
db.exec(`
CREATE TABLE IF NOT EXISTS todos (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
)
`);
server.tool(
{ name: "list-todos", schema: z.object({}), widget: { name: "todo-list" } },
async () => {
const todos = db.prepare("SELECT * FROM todos ORDER BY created_at DESC").all();
return widget({
props: {
todos: todos.map(t => ({ ...t, completed: Boolean(t.completed) })),
totalCount: todos.length,
completedCount: todos.filter(t => t.completed).length
},
output: text(`Found ${todos.length} todos`)
});
}
);
```
---
## Next Steps
- **Review server concepts** → [../server/tools.md](../server/tools.md)
- **Learn widget basics** → [../widgets/basics.md](../widgets/basics.md)
- **Check best practices** → [../../SKILL.md](../../SKILL.md)
references/server/prompts.md
# Prompts
Prompts are reusable message templates with parameters that help structure AI interactions.
**Use prompts for:** Code review templates, summarization patterns, translation templates, instruction templates
---
## Basic Prompt
```typescript
import { MCPServer, text } from "mcp-use/server";
import { z } from "zod";
const server = new MCPServer({
name: "my-server",
version: "1.0.0"
});
server.prompt(
{
name: "code-review",
description: "Generate a code review prompt for given language",
schema: z.object({
language: z.string().describe("Programming language (e.g., 'TypeScript', 'Python')"),
focusArea: z.string().optional().describe("Specific area to focus on (e.g., 'security', 'performance')")
})
},
async ({ language, focusArea }) => {
const focus = focusArea ? ` with emphasis on ${focusArea}` : "";
return text(
`Please review this ${language} code for best practices and potential issues${focus}.\n\n` +
`Consider:\n` +
`- Code quality and readability\n` +
`- Potential bugs or edge cases\n` +
`- Performance implications\n` +
`- Security vulnerabilities\n` +
`- Adherence to ${language} idioms`
);
}
);
```
**Key points:**
- First argument: prompt configuration (name, description, schema)
- Second argument: async handler that returns prompt text
- Handler receives validated input matching schema
- Returns `text()` or `markdown()` helper with prompt content
---
## Prompt Definition
### Name
Use kebab-case, descriptive names:
```typescript
✅ "code-review"
✅ "summarize-document"
✅ "translate-text"
✅ "explain-concept"
❌ "prompt1"
❌ "review" // Too vague
```
### Description
Explain what the prompt template does:
```typescript
✅ "Generate a code review prompt for any programming language with optional focus areas"
✅ "Create a translation prompt from source to target language"
✅ "Build a summarization prompt with configurable length and style"
❌ "Code review" // Not descriptive enough
```
### Schema
Define parameters with Zod, always use `.describe()`:
```typescript
// ✅ Good
z.object({
language: z.string().describe("Programming language to review"),
focusArea: z.enum(["security", "performance", "style", "bugs"])
.optional()
.describe("Specific aspect to focus on"),
severity: z.enum(["all", "critical", "major"])
.default("all")
.describe("Minimum issue severity to report")
})
// ❌ Bad - no descriptions
z.object({
language: z.string(),
focusArea: z.string().optional()
})
```
---
## Common Prompt Patterns
### Code Review
```typescript
server.prompt(
{
name: "code-review",
description: "Generate code review instructions",
schema: z.object({
language: z.string().describe("Programming language"),
style: z.enum(["strict", "moderate", "lenient"]).optional().describe("Review strictness")
})
},
async ({ language, style = "moderate" }) => {
const strictness = {
strict: "Be thorough and point out all issues, including minor style problems.",
moderate: "Focus on significant issues and best practices.",
lenient: "Only highlight critical bugs and security issues."
};
return text(
`Review this ${language} code.\n\n` +
`${strictness[style]}\n\n` +
`Check for:\n` +
`- Correctness and potential bugs\n` +
`- Security vulnerabilities\n` +
`- Performance issues\n` +
`- Code clarity and maintainability`
);
}
);
```
### Summarization
```typescript
server.prompt(
{
name: "summarize",
description: "Create a summarization prompt",
schema: z.object({
length: z.enum(["brief", "medium", "detailed"]).describe("Summary length"),
format: z.enum(["paragraph", "bullets", "outline"]).describe("Output format")
})
},
async ({ length, format }) => {
const lengthGuide = {
brief: "2-3 sentences",
medium: "1 paragraph (5-7 sentences)",
detailed: "2-3 paragraphs with key points"
};
const formatGuide = {
paragraph: "as a cohesive paragraph",
bullets: "as bullet points",
outline: "as a hierarchical outline"
};
return text(
`Summarize the following content in ${lengthGuide[length]} ` +
`${formatGuide[format]}.\n\n` +
`Focus on the main points and key takeaways.`
);
}
);
```
### Translation
```typescript
server.prompt(
{
name: "translate",
description: "Generate translation instructions",
schema: z.object({
sourceLang: z.string().describe("Source language"),
targetLang: z.string().describe("Target language"),
tone: z.enum(["formal", "casual", "technical"]).optional().describe("Translation tone")
})
},
async ({ sourceLang, targetLang, tone = "formal" }) => {
return text(
`Translate the following text from ${sourceLang} to ${targetLang}.\n\n` +
`Maintain a ${tone} tone.\n` +
`Preserve the original meaning and nuance.\n` +
`Keep any technical terms or proper nouns intact unless they have established translations.`
);
}
);
```
### Explanation
```typescript
server.prompt(
{
name: "explain-concept",
description: "Generate explanation instructions for technical concepts",
schema: z.object({
concept: z.string().describe("Concept to explain"),
audience: z.enum(["beginner", "intermediate", "expert"]).describe("Target audience"),
includeExamples: z.boolean().default(true).describe("Include code examples")
})
},
async ({ concept, audience, includeExamples }) => {
const audienceLevel = {
beginner: "someone new to programming",
intermediate: "a developer with 1-2 years experience",
expert: "an experienced software engineer"
};
let prompt = `Explain ${concept} to ${audienceLevel[audience]}.\n\n`;
if (includeExamples) {
prompt += `Include practical code examples.\n`;
}
prompt += `Use clear language and avoid unnecessary jargon.`;
return text(prompt);
}
);
```
---
## Markdown Prompts
For longer, structured prompts use `markdown()`:
```typescript
import { markdown } from "mcp-use/server";
server.prompt(
{
name: "api-design-review",
description: "Generate comprehensive API design review prompt",
schema: z.object({
apiType: z.enum(["REST", "GraphQL", "gRPC"]).describe("API type")
})
},
async ({ apiType }) => {
return markdown(`
# ${apiType} API Design Review
Please review this ${apiType} API design for the following aspects:
## 1. Design Quality
- RESTful principles (if REST)
- Resource naming and structure
- Consistency across endpoints
- Use of HTTP methods appropriately
## 2. Security
- Authentication/authorization strategy
- Input validation
- Rate limiting considerations
- Sensitive data handling
## 3. Performance
- Pagination strategy
- Caching headers
- Payload size optimization
- N+1 query prevention
## 4. Developer Experience
- Clear, predictable patterns
- Good error messages
- Comprehensive examples
- Documentation clarity
## 5. Versioning & Evolution
- Versioning strategy
- Backward compatibility
- Deprecation plan
Please provide specific, actionable feedback with examples.
`);
}
);
```
---
## Dynamic Prompts
Build prompts that adapt based on context:
```typescript
server.prompt(
{
name: "debug-help",
description: "Generate debugging assistance prompt",
schema: z.object({
errorType: z.string().describe("Type of error (e.g., 'TypeError', 'NetworkError')"),
language: z.string().describe("Programming language"),
hasStackTrace: z.boolean().describe("Whether a stack trace is available")
})
},
async ({ errorType, language, hasStackTrace }) => {
let prompt = `Help me debug this ${errorType} in ${language}.\n\n`;
if (hasStackTrace) {
prompt += `I have a stack trace. Please:\n`;
prompt += `1. Identify the root cause from the stack trace\n`;
prompt += `2. Explain why this error occurred\n`;
prompt += `3. Suggest fixes with code examples\n`;
} else {
prompt += `I don't have a full stack trace. Please:\n`;
prompt += `1. Ask questions to narrow down the issue\n`;
prompt += `2. Suggest common causes of ${errorType}\n`;
prompt += `3. Recommend debugging steps\n`;
}
return text(prompt);
}
);
```
---
## Multi-Step Prompts
Chain multiple prompts for complex workflows:
```typescript
server.prompt(
{
name: "refactor-guide",
description: "Generate step-by-step refactoring instructions",
schema: z.object({
codeSmell: z.string().describe("Type of code smell to address"),
safetyLevel: z.enum(["aggressive", "moderate", "conservative"]).describe("Refactoring approach")
})
},
async ({ codeSmell, safetyLevel }) => {
const steps = {
aggressive: [
"Identify all instances of the code smell",
"Propose bold refactoring that may require significant changes",
"Show before/after code",
"List breaking changes"
],
moderate: [
"Identify the code smell",
"Propose incremental improvements",
"Show refactoring steps",
"Ensure backward compatibility"
],
conservative: [
"Identify the code smell",
"Propose minimal, safe changes",
"Preserve existing behavior exactly",
"Add tests before refactoring"
]
};
return text(
`Refactor this code to address: ${codeSmell}\n\n` +
`Approach: ${safetyLevel}\n\n` +
`Follow these steps:\n` +
steps[safetyLevel].map((step, i) => `${i + 1}. ${step}`).join('\n')
);
}
);
```
---
## Prompt with Context
Include environmental or system context:
```typescript
server.prompt(
{
name: "optimize-for-runtime",
description: "Generate optimization suggestions for specific runtime",
schema: z.object({
runtime: z.enum(["node", "browser", "edge", "serverless"]).describe("Target runtime"),
metric: z.enum(["latency", "throughput", "memory", "cost"]).describe("Optimization goal")
})
},
async ({ runtime, metric }) => {
const runtimeContext = {
node: "Node.js server environment with access to filesystem and native modules",
browser: "Browser environment with limited resources and network constraints",
edge: "Edge runtime with fast cold starts but limited execution time",
serverless: "Serverless function with cold start concerns and pay-per-invocation pricing"
};
return text(
`Optimize this code for ${runtime} runtime to improve ${metric}.\n\n` +
`Context: ${runtimeContext[runtime]}\n\n` +
`Consider:\n` +
`- ${metric === "latency" ? "Reduce response time" : ""}\n` +
`- ${metric === "throughput" ? "Increase requests/second" : ""}\n` +
`- ${metric === "memory" ? "Reduce memory footprint" : ""}\n` +
`- ${metric === "cost" ? "Minimize execution time and resources" : ""}\n\n` +
`Provide specific code changes with explanations.`
);
}
);
```
---
## Completion (Autocomplete)
Add autocomplete suggestions to prompt arguments using `completable()`:
```typescript
import { MCPServer, text, completable } from "mcp-use/server";
import { z } from "zod";
// Static list of suggestions
server.prompt(
{
name: "code-review",
schema: z.object({
language: completable(z.string().describe("Programming language"), ["python", "typescript", "go", "rust"]),
code: z.string().describe("Code to review")
})
},
async ({ language, code }) => text(`Review this ${language} code...`)
);
```
### Dynamic Completion
Use a callback for suggestions that depend on context or external data:
```typescript
server.prompt(
{
name: "analyze-project",
schema: z.object({
userId: completable(z.string().describe("User ID"), async (value) => {
const users = await fetchUsers();
return users.map(u => u.id).filter(id => id.startsWith(value));
}),
projectId: completable(z.string().describe("Project ID"), async (value, ctx) => {
// Use other argument values for contextual suggestions
const userId = ctx?.arguments?.userId;
const projects = await fetchProjects(userId);
return projects.map(p => p.id).filter(id => id.startsWith(value));
})
})
},
async ({ userId, projectId }) => text(`Analyzing project ${projectId}...`)
);
```
**Key points:**
- `completable(schema, values)` — static list, prefix-matched automatically
- `completable(schema, callback)` — dynamic, receives `(value, ctx)` where `ctx.arguments` has other field values
- Works with `z.string()`, `z.number()`, and `z.enum()` schemas
- Clients request suggestions via MCP `completion/complete`
---
## Best Practices
### 1. Clear Instructions
```typescript
✅ "Translate the following text from English to Spanish. Maintain formal tone."
❌ "Translate this."
```
### 2. Structured Output
When you want structured responses, specify format:
```typescript
return text(
`Review this code and respond in the following format:\n\n` +
`## Issues Found\n` +
`- [Issue description]\n\n` +
`## Suggested Fixes\n` +
`- [Fix description with code]\n\n` +
`## Severity\n` +
`[Critical/Major/Minor]`
);
```
### 3. Provide Context
Include relevant context in the prompt:
```typescript
return text(
`You are reviewing ${language} code for a ${projectType} project.\n` +
`Team follows ${styleGuide} style guide.\n\n` +
`Review the code below...`
);
```
### 4. Be Specific
```typescript
✅ "Summarize in 3-5 bullet points, each under 20 words"
❌ "Summarize briefly"
```
---
## Prompts vs Tools
**Use a prompt when:**
- ✅ Providing instructions to the AI
- ✅ Creating reusable message templates
- ✅ Structuring AI interactions
- ✅ No backend logic needed
**Use a tool when:**
- ✅ Executing backend actions
- ✅ Calling APIs or databases
- ✅ Returning computed data
- ✅ Has side effects
**Example:**
```typescript
// ✅ Prompt - Just instructions
server.prompt(
{ name: "review-code", ... },
async ({ language }) => text(`Review this ${language} code...`)
);
// ✅ Tool - Executes action
server.tool(
{ name: "run-linter", schema: z.object({ file: z.string() }) },
async ({ file }) => {
const results = await runLinter(file);
return object(results);
}
);
```
---
## Complete Example
```typescript
import { MCPServer, text, markdown } from "mcp-use/server";
import { z } from "zod";
const server = new MCPServer({
name: "prompt-server",
version: "1.0.0"
});
// Simple text prompt
server.prompt(
{
name: "explain-error",
description: "Generate error explanation prompt",
schema: z.object({
errorMessage: z.string().describe("The error message"),
context: z.string().optional().describe("Additional context")
})
},
async ({ errorMessage, context }) => {
let prompt = `Explain this error message in simple terms:\n"${errorMessage}"\n\n`;
if (context) {
prompt += `Context: ${context}\n\n`;
}
prompt += `Provide:\n1. What it means\n2. Common causes\n3. How to fix it`;
return text(prompt);
}
);
// Structured markdown prompt
server.prompt(
{
name: "pr-review",
description: "Generate pull request review checklist",
schema: z.object({
type: z.enum(["feature", "bugfix", "refactor"]).describe("PR type")
})
},
async ({ type }) => {
return markdown(`
# Pull Request Review Checklist (${type})
## Code Quality
- [ ] Code follows project style guide
- [ ] No unnecessary code duplication
- [ ] Functions are small and focused
- [ ] Variable names are clear and descriptive
## Testing
- [ ] ${type === "feature" ? "New tests added for new functionality" : ""}
- [ ] ${type === "bugfix" ? "Regression test added" : ""}
- [ ] All tests pass
- [ ] Edge cases covered
## Documentation
- [ ] Code comments where necessary
- [ ] README updated if needed
- [ ] API docs updated
## ${type === "feature" ? "Feature Specific" : type === "bugfix" ? "Bug Fix Specific" : "Refactor Specific"}
${type === "feature" ? "- [ ] Feature flag implemented if needed\n- [ ] Backward compatible" : ""}
${type === "bugfix" ? "- [ ] Root cause identified\n- [ ] Fix verified in production-like environment" : ""}
${type === "refactor" ? "- [ ] No behavior changes\n- [ ] Performance impact assessed" : ""}
`);
}
);
server.listen();
```
---
## Next Steps
- **Format responses** → [response-helpers.md](response-helpers.md)
- **Create tools** → [tools.md](tools.md)
- **See examples** → [../patterns/common-patterns.md](../patterns/common-patterns.md)
references/server/proxy.md
# Server Proxying & Composition
The `mcp-use` TypeScript SDK allows you to easily proxy and compose multiple MCP servers into a single unified "Aggregator" server.
This is extremely useful when you have multiple microservices or specialized MCP servers (like a database server, a weather server, and an internal API server) and you want to expose all of their tools, resources, and prompts through a single unified endpoint.
## High-Level API (Config Object)
Pass a configuration object directly to `server.proxy()`. The keys act as the **namespaces** for the child servers to prevent naming collisions.
```typescript
import { MCPServer } from "mcp-use/server";
import path from "node:path";
const server = new MCPServer({ name: "UnifiedServer", version: "1.0.0" });
// The SDK handles all connections, sessions, and synchronization automatically
await server.proxy({
// Proxy a local TypeScript server (using the 'tsx' runner)
database: {
command: "tsx",
args: [path.resolve(__dirname, "./db-server.ts")]
},
// Proxy a local Python FastMCP server
weather: {
command: "uv",
args: ["run", "weather_server.py"],
env: { ...process.env, FASTMCP_LOG_LEVEL: "ERROR" }
},
// Proxy a remote server over HTTP
manufact: {
url: "https://manufact.com/docs/mcp"
}
});
// Start the unified server
await server.listen(3000);
```
In the example above, the `database` tools will be prefixed with `database_` (e.g. `database_query`), the `weather` tools will be prefixed with `weather_` (e.g. `weather_get_forecast`), and so on. Resource URIs will be prefixed like `database://app://config`.
## Low-Level API (Explicit Session)
For advanced use cases (dynamic auth headers, manual session lifecycles, or custom connectors), you can inject an explicit `MCPSession` directly into the `proxy` method using the `mcp-use/client` SDK.
```typescript
import { MCPServer } from "mcp-use/server";
import { MCPClient } from "mcp-use/client";
const server = new MCPServer({ name: "UnifiedServer", version: "1.0.0" });
// Create a custom client orchestration
const customClient = new MCPClient({
mcpServers: {
secure_db: {
url: "https://secure-db.example.com/mcp"
}
}
});
// Manage the session manually
const dbSession = await customClient.createSession("secure_db");
// Proxy the active session, manually specifying the namespace
await server.proxy(dbSession, { namespace: "secure_db" });
await server.listen(3000);
```
## Advanced Features Supported
The `mcp-use` proxying system goes far beyond simple tool forwarding:
1. **Schema Translation**: Automatically translates raw JSON Schemas from child servers into runtime Zod schemas.
2. **LLM Sampling & Elicitation**: Automatically intercepts out-of-band JSONRPC requests (sampling/elicitation) from child servers, resolves the HTTP context of the original user who triggered the tool, and routes the request securely back to that user's client.
3. **Progress Tracking**: If a child tool emits `notifications/progress/report`, the Aggregator catches and pipes those directly through the unified `ToolContext` back to the parent client.
4. **State Syncing**: The Aggregator listens to `list_changed` events emitted by the child server and instantly forwards them to all connected clients.
references/server/resources.md
# Resources
Resources expose read-only data that clients can fetch. They don't take input parameters (use resource templates for that).
**Use resources for:** Configuration, static data, documentation, listings, app state
---
## Basic Resource
```typescript
import { MCPServer, object, text, markdown } from "mcp-use/server";
const server = new MCPServer({
name: "my-server",
version: "1.0.0"
});
server.resource(
{
name: "app_settings",
uri: "config://settings",
title: "Application Settings",
description: "Current server configuration"
},
async () => object({
theme: "dark",
version: "1.0.0",
language: "en",
features: {
notifications: true,
analytics: false
}
})
);
```
**Key points:**
- First argument: resource configuration (uri, name, description, mimeType)
- Second argument: async handler function (no input parameters)
- Handler returns response helper (`object()`, `text()`, `markdown()`, etc.)
- URI scheme is arbitrary (use meaningful prefixes: `config://`, `docs://`, `data://`)
---
## Resource Definition
### URI
Use a scheme-based format for organization:
```typescript
✅ "config://settings" // Configuration data
✅ "docs://user-guide" // Documentation
✅ "data://available-cities" // Static data
✅ "state://current-user" // Current state
❌ "settings" // Missing scheme
❌ "http://example.com/data" // Don't use http:// (reserved)
```
### Name
Machine-readable identifier (kebab-case):
```typescript
✅ "app_settings"
✅ "user_guide"
✅ "available_cities"
```
### Title
Human-readable name shown to users:
```typescript
✅ "Application Settings"
✅ "User Guide"
✅ "Available Cities"
```
### Description
Optional but recommended. Explains what the resource contains:
```typescript
description: "Current server configuration including theme, language, and feature flags"
```
### MIME Type
Indicates content format:
| Content Type | MIME Type |
|--------------|-----------|
| JSON object | `application/json` |
| Plain text | `text/plain` |
| Markdown | `text/markdown` |
| HTML | `text/html` |
| Image | `image/png`, `image/jpeg` |
| Binary | `application/octet-stream` |
---
## Static Resources
Resources that return fixed data:
```typescript
server.resource(
{
name: "supported_languages",
uri: "data://supported-languages",
title: "Supported Languages",
description: "List of supported language codes"
},
async () => object({
languages: ["en", "es", "fr", "de", "ja"],
default: "en"
})
);
server.resource(
{
name: "api_guide",
uri: "docs://api-guide",
title: "API Documentation",
description: "Complete API reference and examples"
},
async () => markdown(`
# API Guide
## Authentication
Use Bearer token in Authorization header...
## Endpoints
- POST /api/users - Create user
- GET /api/users/:id - Get user
`)
);
```
---
## Dynamic Resources
Resources that fetch or compute data at request time:
```typescript
server.resource(
{
name: "current_stats",
uri: "stats://current",
title: "Current Statistics",
description: "Real-time server statistics"
},
async () => {
const stats = await calculateStats();
return object({
users: stats.totalUsers,
requests: stats.requestCount,
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
}
);
server.resource(
{
name: "active_sessions",
uri: "state://active-sessions",
title: "Active Sessions",
description: "Currently active user sessions"
},
async () => {
const sessions = await getActiveSessions();
return object({
count: sessions.length,
sessions: sessions.map(s => ({
id: s.id,
user: s.userId,
started: s.startTime
}))
});
}
);
```
**When to use dynamic resources:**
- Data changes over time
- Data is expensive to compute (compute on demand)
- Data reflects current server state
---
## Resource Templates
When you need parameters, use resource templates with URI placeholders:
```typescript
server.resourceTemplate(
{
name: "user_profile",
uriTemplate: "user://{userId}/profile",
title: "User Profile",
description: "Get user profile by ID"
},
async (uri: URL, params: Record<string, string>) => {
// Extract parameters from params object
const { userId } = params;
const user = await fetchUser(userId);
if (!user) {
return error(`User not found: ${userId}`);
}
return object({
id: user.id,
name: user.name,
email: user.email,
createdAt: user.createdAt
});
}
);
```
**URI template syntax:**
- `{param}` - Single path segment
- `{param*}` - Multiple path segments (greedy)
**Examples:**
```typescript
// Single parameter
"user://{userId}/profile" // Matches: user://123/profile
"docs://{section}" // Matches: docs://getting-started
// Multiple parameters
"files://{folder}/{filename}" // Matches: files://documents/report.pdf
"api://{version}/users/{id}" // Matches: api://v1/users/42
// Greedy parameter (matches multiple segments)
"docs://{path*}" // Matches: docs://guides/api/authentication
```
**Handler signature:**
```typescript
async (uri: URL, params: Record<string, string>) => {
// uri: URL object of the matched URI
// params: Record<string, string> with extracted template parameters
// Extract the parameters you need
const { userId } = params;
}
```
**⚠️ TypeScript Best Practice:**
- **Recommended:** Use explicit types and extract params inside the function body (as shown above)
- **Not recommended:** Parameter destructuring like `async (uri, { userId }) => {}` works at runtime but TypeScript has trouble matching it against the `Record<string, string>` type, potentially causing compilation errors
---
## Completion (Autocomplete) for Templates
Add autocomplete suggestions for resource template variables using the `callbacks.complete` option:
```typescript
// Static list of suggestions per variable
server.resourceTemplate(
{
uriTemplate: "docs://{docId}",
name: "Documentation",
description: "Get documentation by ID",
callbacks: {
complete: {
docId: ["getting-started", "api-reference", "faq", "changelog"]
}
}
},
async (uri: URL, params: Record<string, string>) => {
const { docId } = params;
return markdown(await fetchDoc(docId));
}
);
// Dynamic suggestions via callback
server.resourceTemplate(
{
uriTemplate: "user://{userId}/profile",
name: "User Profile",
callbacks: {
complete: {
userId: async (value: string) => {
const users = await searchUsers(value);
return users.map(u => u.id);
}
}
}
},
async (uri: URL, params: Record<string, string>) => {
const { userId } = params;
return object(await fetchUser(userId));
}
);
```
**Key points:**
- `complete` maps each template variable to either a `string[]` (prefix-matched automatically) or a callback `(value: string) => Promise<string[]>`
- Clients request suggestions via MCP `completion/complete`
---
## Error Handling
Resources can fail - handle errors gracefully with `error()` helper:
```typescript
server.resource({ uri: "data://external-api", ... }, async () => {
try {
const data = await fetch("https://api.example.com/data");
if (!data.ok) return error(`API returned status ${data.status}`);
return object(await data.json());
} catch (err) {
return error(`Failed to fetch: ${err.message}`);
}
});
```
See [tools.md](tools.md#error-handling) for comprehensive error handling patterns.
---
## Listing Resources
Clients can list all available resources. Organize resources by URI scheme for discoverability:
```typescript
// Good organization
server.resource({ uri: "config://settings", ... });
server.resource({ uri: "config://features", ... });
server.resource({ uri: "config://limits", ... });
server.resource({ uri: "docs://guide", ... });
server.resource({ uri: "docs://api", ... });
server.resource({ uri: "docs://faq", ... });
server.resource({ uri: "data://cities", ... });
server.resource({ uri: "data://countries", ... });
```
When a client lists resources, they see:
```json
{
"resources": [
{ "uri": "config://settings", "name": "Application Settings", ... },
{ "uri": "config://features", "name": "Feature Flags", ... },
{ "uri": "docs://guide", "name": "User Guide", ... },
...
]
}
```
---
## Resource vs Tool
**Use a resource when:**
- ✅ Read-only data
- ✅ No input parameters (or use resource templates)
- ✅ Data that clients might browse or list
- ✅ Configuration, docs, static data
**Use a tool when:**
- ✅ Action with side effects
- ✅ Complex input validation needed
- ✅ Needs Zod schema for structured input
- ✅ May return visual UI (widgets)
**Example:**
```typescript
// ❌ Bad - Use resource instead
server.tool(
{ name: "get-settings", schema: z.object({}) },
async () => object({ theme: "dark" })
);
// ✅ Good
server.resource(
{ uri: "config://settings", ... },
async () => object({ theme: "dark" })
);
// ✅ Good - Tool appropriate here (has input)
server.tool(
{ name: "update-settings", schema: z.object({ theme: z.string() }) },
async ({ theme }) => {
await saveSettings({ theme });
return text("Settings updated");
}
);
```
---
## Caching Resources
Since resources are read-only, caching is often beneficial:
```typescript
const cache = new Map<string, { data: any; expires: number }>();
server.resource(
{
uri: "data://expensive-computation",
name: "Expensive Data",
mimeType: "application/json"
},
async () => {
const cacheKey = "expensive-computation";
const cached = cache.get(cacheKey);
// Return cached data if not expired
if (cached && cached.expires > Date.now()) {
return object(cached.data);
}
// Compute fresh data
const data = await expensiveComputation();
// Cache for 10 minutes
cache.set(cacheKey, {
data,
expires: Date.now() + 10 * 60 * 1000
});
return object(data);
}
);
```
---
## Complete Example
```typescript
import { MCPServer, object, markdown, error } from "mcp-use/server";
const server = new MCPServer({
name: "docs-server",
version: "1.0.0"
});
// Static configuration
server.resource(
{
uri: "config://settings",
name: "Server Settings",
mimeType: "application/json"
},
async () => object({
version: "1.0.0",
environment: process.env.NODE_ENV || "development"
})
);
// Dynamic list
server.resource(
{
uri: "data://available-docs",
name: "Available Documentation",
mimeType: "application/json"
},
async () => {
const docs = await listDocuments();
return object({ docs });
}
);
// Parameterized access
server.resourceTemplate(
{
uriTemplate: "docs://{docId}",
name: "Documentation",
description: "Get documentation by ID",
mimeType: "text/markdown"
},
async (uri: URL, params: Record<string, string>) => {
const { docId } = params;
const doc = await fetchDocument(docId);
if (!doc) {
return error(`Document not found: ${docId}`);
}
return markdown(doc.content);
}
);
server.listen();
```
---
## Next Steps
- **Format responses** → [response-helpers.md](response-helpers.md)
- **Create tools** → [tools.md](tools.md)
- **See examples** → [../patterns/common-patterns.md](../patterns/common-patterns.md)
references/server/response-helpers.md
# Response Helpers
Response helpers format output from tools, resources, and prompts. Always use helpers instead of returning raw values.
**Available helpers:** `text()`, `object()`, `markdown()`, `image()`, `error()`, `widget()`, `mix()`, `resource()`
---
## Why Use Response Helpers?
```typescript
// ❌ Bad - Raw return value
server.tool(
{ name: "get-data", schema: z.object({}) },
async () => {
return { status: "ok", data: [1, 2, 3] }; // Wrong!
}
);
// ✅ Good - Using helper
server.tool(
{ name: "get-data", schema: z.object({}) },
async () => {
return object({ status: "ok", data: [1, 2, 3] });
}
);
```
**Why:**
- Helpers set correct MIME types
- Ensure proper serialization
- Enable client-side rendering
- Support multi-content responses
---
## text()
Simple string responses. Most common helper.
```typescript
import { text } from "mcp-use/server";
server.tool(
{ name: "greet", schema: z.object({ name: z.string() }) },
async ({ name }) => {
return text(`Hello, ${name}!`);
}
);
// Multi-line text
server.tool(
{ name: "format-address", schema: z.object({
address: z.object({
street: z.string(),
city: z.string(),
state: z.string(),
zip: z.string(),
country: z.string()
})
}) },
async ({ address }) => {
return text(
`${address.street}\n` +
`${address.city}, ${address.state} ${address.zip}\n` +
`${address.country}`
);
}
);
```
**Use for:**
- Simple messages
- Confirmation text
- Status updates
- Plain text output
---
## object()
Structured JSON data. Use when AI or client needs structured information.
```typescript
import { object } from "mcp-use/server";
server.tool(
{ name: "get-user", schema: z.object({ id: z.string() }) },
async ({ id }) => {
const user = await fetchUser(id);
return object({
id: user.id,
name: user.name,
email: user.email,
createdAt: user.createdAt,
settings: {
theme: user.theme,
notifications: user.notifications
}
});
}
);
// With arrays
server.tool(
{ name: "list-todos", schema: z.object({}) },
async () => {
const todos = await getTodos();
return object({
total: todos.length,
items: todos.map(t => ({
id: t.id,
title: t.title,
completed: t.completed,
dueDate: t.dueDate
}))
});
}
);
```
**Use for:**
- Structured data
- Lists and arrays
- Nested objects
- Data that AI needs to parse
---
## markdown()
Formatted text with markdown syntax. Great for documentation, reports, explanations.
```typescript
import { markdown } from "mcp-use/server";
server.tool(
{ name: "generate-report", schema: z.object({ data: z.array(z.any()) }) },
async ({ data }) => {
return markdown(`
# Daily Report
## Summary
Total items processed: ${data.length}
## Breakdown
| Category | Count |
|----------|-------|
| Success | ${data.filter(d => d.status === "ok").length} |
| Failed | ${data.filter(d => d.status === "error").length} |
## Details
${data.map(d => `- **${d.id}**: ${d.message}`).join('\n')}
---
*Generated at ${new Date().toISOString()}*
`);
}
);
// Code examples in markdown
server.tool(
{ name: "explain-function", schema: z.object({ name: z.string() }) },
async ({ name }) => {
return markdown(`
# ${name}() Function
## Usage
\`\`\`typescript
const result = await ${name}(params);
\`\`\`
## Description
This function performs...
## Parameters
- \`params\` - Configuration object
## Returns
Returns a Promise that resolves to...
`);
}
);
```
**Use for:**
- Documentation
- Formatted reports
- Explanations with structure
- Code examples
- Rich text output
---
## image()
Embed images in responses. Supports URLs or base64 data.
```typescript
import { image } from "mcp-use/server";
// Image from URL
server.tool(
{ name: "get-chart", schema: z.object({ data: z.array(z.number()) }) },
async ({ data }) => {
const chartUrl = await generateChart(data);
return image(chartUrl);
}
);
// Base64 image
server.tool(
{ name: "generate-qr", schema: z.object({ text: z.string() }) },
async ({ text }) => {
const qrCodeBase64 = await generateQRCode(text);
return image(`data:image/png;base64,${qrCodeBase64}`);
}
);
// Image with MIME type
server.tool(
{ name: "get-diagram", schema: z.object({ id: z.string() }) },
async ({ id }) => {
const diagramUrl = await getDiagram(id);
return image(diagramUrl, "image/svg+xml");
}
);
```
**Use for:**
- Charts and graphs
- QR codes
- Diagrams
- Generated images
- Visual output
---
## error()
Error responses. Always use this instead of throwing exceptions.
```typescript
import { error } from "mcp-use/server";
server.tool(
{ name: "fetch-data", schema: z.object({ id: z.string() }) },
async ({ id }) => {
try {
const data = await fetchData(id);
if (!data) {
return error(`No data found for ID: ${id}`);
}
return object(data);
} catch (err) {
return error(
`Failed to fetch data: ${err instanceof Error ? err.message : "Unknown error"}`
);
}
}
);
// Multiple error cases
server.tool(
{ name: "update-user", schema: z.object({ id: z.string(), name: z.string() }) },
async ({ id, name }) => {
const user = await getUser(id);
if (!user) {
return error(`User not found: ${id}`);
}
if (!name.trim()) {
return error("Name cannot be empty");
}
await updateUser(id, { name });
return text("User updated successfully");
}
);
```
**Use for:**
- Validation errors
- Not found errors
- Permission errors
- Operation failures
---
## widget()
Return visual UI alongside data. Tool must have `widget: { name }` config.
```typescript
import { widget, text } from "mcp-use/server";
server.tool(
{
name: "search-products",
description: "Search products by query",
schema: z.object({
query: z.string().describe("Search query")
}),
widget: {
name: "product-list", // Must match resources/product-list.tsx
invoking: "Searching...",
invoked: "Products loaded"
}
},
async ({ query }) => {
const products = await searchProducts(query);
return widget({
props: {
products,
query,
totalCount: products.length
},
output: text(`Found ${products.length} products matching "${query}"`)
});
}
);
```
**Widget response structure:**
- `props` - Data sent to widget component
- `output` - Text/object the AI model sees
**Use for:**
- Browsing lists
- Comparing items
- Interactive selection
- Visual data representation
See [../widgets/basics.md](../widgets/basics.md) for widget implementation.
---
## mix()
Combine multiple content types in a single response.
```typescript
import { mix, text, image, markdown } from "mcp-use/server";
server.tool(
{ name: "generate-report", schema: z.object({ id: z.string() }) },
async ({ id }) => {
const report = await getReport(id);
const chartUrl = await generateChart(report.data);
return mix(
markdown(`# Report: ${report.title}\n\n${report.summary}`),
image(chartUrl),
text(`Generated at ${new Date().toISOString()}`)
);
}
);
// Text + structured data
server.tool(
{ name: "analyze-code", schema: z.object({ code: z.string() }) },
async ({ code }) => {
const analysis = await analyzeCode(code);
return mix(
text(`Analysis complete. Found ${analysis.issues.length} issues.`),
object({
complexity: analysis.complexity,
issues: analysis.issues,
suggestions: analysis.suggestions
})
);
}
);
```
**Use for:**
- Rich responses with multiple formats
- Text + image combinations
- Summary + detailed data
- Multiple related pieces of content
---
## Embedding Resources
Reference resources in tool responses:
```typescript
import { text, resource } from "mcp-use/server";
server.tool(
{ name: "get-help", schema: z.object({ topic: z.string() }) },
async ({ topic }) => {
return mix(
text(`Help documentation for: ${topic}`),
resource(`docs://${topic}`, "text/markdown")
);
}
);
```
---
## Response Patterns
### Success with Data
```typescript
return object({
success: true,
data: { ... },
message: "Operation completed successfully"
});
```
### Success with Message
```typescript
return text("User created successfully");
```
### Not Found
```typescript
if (!item) {
return error(`Item not found: ${id}`);
}
```
### Validation Error
```typescript
if (!email.includes("@")) {
return error("Invalid email address");
}
```
### Server Error
```typescript
try {
// ... operation
} catch (err) {
console.error("Operation failed:", err);
return error("Operation failed. Please try again.");
}
```
---
## Complete Example
```typescript
import {
MCPServer,
text,
object,
markdown,
image,
error,
widget,
mix
} from "mcp-use/server";
import { z } from "zod";
const server = new MCPServer({
name: "response-examples",
version: "1.0.0"
});
// Simple text
server.tool(
{ name: "greet", schema: z.object({ name: z.string() }) },
async ({ name }) => text(`Hello, ${name}!`)
);
// Structured data
server.tool(
{ name: "get-stats", schema: z.object({}) },
async () => object({
users: 1523,
active: 342,
growth: "+12%"
})
);
// Markdown report
server.tool(
{ name: "daily-summary", schema: z.object({}) },
async () => markdown(`
# Daily Summary
## Metrics
- **Users**: 1,523
- **Revenue**: $4,231
- **Orders**: 87
## Top Products
1. Widget Pro
2. Gadget Plus
3. Tool Kit
`)
);
// Error handling
server.tool(
{ name: "fetch-item", schema: z.object({ id: z.string() }) },
async ({ id }) => {
try {
const item = await db.get(id);
if (!item) {
return error(`Item not found: ${id}`);
}
return object(item);
} catch (err) {
return error("Database error");
}
}
);
// Mixed content
server.tool(
{ name: "analyze", schema: z.object({ data: z.array(z.number()) }) },
async ({ data }) => {
const stats = calculateStats(data);
const chartUrl = await generateChart(data);
return mix(
markdown(`## Analysis Results\n\nProcessed ${data.length} data points`),
object(stats),
image(chartUrl)
);
}
);
// Widget response
server.tool(
{
name: "browse-items",
schema: z.object({ category: z.string() }),
widget: { name: "item-browser" }
},
async ({ category }) => {
const items = await getItems(category);
return widget({
props: { items, category },
output: text(`Found ${items.length} items in ${category}`)
});
}
);
server.listen();
```
---
## Best Practices
### 1. Always Use Helpers
```typescript
❌ return { data: "value" };
✅ return object({ data: "value" });
```
### 2. Match Helper to Content
```typescript
✅ text("Simple message")
✅ object({ structured: "data" })
✅ markdown("# Formatted text")
✅ error("Error message")
```
### 3. Handle Errors Gracefully
```typescript
✅ return error("User not found: userId_123");
❌ throw new Error("Raw exception");
```
### 4. Provide Context in Errors
```typescript
✅ error(`User not found: ${userId}`);
✅ error(`Invalid email format: ${email}`);
❌ error("Error");
```
### 5. Use Markdown for Rich Content
```typescript
✅ markdown(`# Title\n\n- Point 1\n- Point 2`);
❌ text("Title\nPoint 1\nPoint 2"); // No formatting
```
### 6. Widget Output is for AI
```typescript
return widget({
props: { /* visual data */ },
output: text("Concise summary for AI") // AI only sees this
});
```
---
## Response Helper Reference
| Helper | Returns | Use For | Example |
|--------|---------|---------|---------|
| `text(string)` | Plain text | Simple messages | `text("Hello")` |
| `object(any)` | JSON | Structured data | `object({ id: 1 })` |
| `markdown(string)` | Formatted text | Documentation, reports | `markdown("# Title")` |
| `image(url, mime?)` | Image | Charts, diagrams | `image(url)` |
| `error(msg)` | Error | Failures | `error("Not found")` |
| `widget(config)` | UI + data | Visual interfaces | `widget({ props, output })` |
| `mix(...results)` | Multiple | Rich responses | `mix(text(), image())` |
| `resource(uri, mime)` | Resource ref | Embed resources | `resource("docs://guide", "text/markdown")` |
---
## Next Steps
- **Create tools** → [tools.md](tools.md)
- **Add resources** → [resources.md](resources.md)
- **Build widgets** → [../widgets/basics.md](../widgets/basics.md)
- **See examples** → [../patterns/common-patterns.md](../patterns/common-patterns.md)
references/server/tools.md
# Tools
Tools are backend actions the AI can call. They take structured input and return output.
**Use tools for:** Actions, operations, API calls, mutations, data fetching
---
## Basic Tool
```typescript
import { MCPServer, text } from "mcp-use/server";
import { z } from "zod";
const server = new MCPServer({
name: "my-server",
version: "1.0.0",
baseUrl: process.env.MCP_URL || "http://localhost:3000"
});
server.tool(
{
name: "send-email",
description: "Send an email to a user",
schema: z.object({
to: z.string().email().describe("Recipient email address"),
subject: z.string().describe("Email subject line"),
body: z.string().describe("Email body content"),
priority: z.enum(["low", "normal", "high"]).optional().describe("Email priority")
})
},
async ({ to, subject, body, priority = "normal" }) => {
// Your logic here
await sendEmail(to, subject, body, priority);
return text(`Email sent to ${to}`);
}
);
```
**Key points:**
- First argument: tool configuration (name, description, schema)
- Second argument: async handler function
- Handler receives validated input matching schema
- Must return a response helper (`text()`, `object()`, `widget()`, etc.)
---
## Tool Definition
### Name
- Use kebab-case: `send-email`, `fetch-user`, `create-todo`
- Be specific: ❌ `manage-users` ✅ `create-user`, `delete-user`, `list-users`
- One tool = one capability
### Description
Clear, actionable description of what the tool does:
```typescript
✅ "Send an email to a user with subject and body"
❌ "Email tool"
```
The AI uses this to decide when to call your tool.
### Schema (Zod)
**Always use `.describe()` on every field:**
```typescript
// ✅ Good
z.object({
city: z.string().describe("City name (e.g., 'New York', 'Tokyo')"),
units: z.enum(["celsius", "fahrenheit"]).optional().describe("Temperature units"),
limit: z.number().min(1).max(50).optional().describe("Max results to return")
})
// ❌ Bad - no descriptions
z.object({
city: z.string(),
units: z.string(),
limit: z.number()
})
```
**Schema best practices:**
- Use `.optional()` for non-required fields
- Add validation: `.min()`, `.max()`, `.email()`, `.url()`
- Use `z.enum()` for fixed sets of values (not `z.string()`)
- Use `z.array()` for lists
- Use `z.record(z.string(), z.string())` for key-value maps (Zod v4 requires both key and value schemas)
---
## Tool Annotations
Declare the nature of your tool so clients can warn users:
```typescript
server.tool(
{
name: "delete-user",
description: "Permanently delete a user account",
schema: z.object({ userId: z.string().describe("User ID") }),
annotations: {
destructiveHint: true, // Deletes or overwrites data
readOnlyHint: false, // Has side effects
openWorldHint: false // Stays within user's account (not external APIs)
}
},
async ({ userId }) => {
await deleteUser(userId);
return text(`User ${userId} deleted`);
}
);
```
**Annotations:**
- `destructiveHint: true` - Deletes/overwrites data, client may require confirmation
- `readOnlyHint: true` - No side effects, safe to call repeatedly
- `openWorldHint: true` - Calls external APIs or services outside user's control
---
## Tool Context
The second parameter to tool handlers provides advanced capabilities:
```typescript
server.tool(
{
name: "process-large-file",
schema: z.object({ fileUrl: z.string().describe("URL to file") })
},
async ({ fileUrl }, ctx) => {
// Progress reporting
await ctx.reportProgress?.(0, 100, "Starting download...");
const file = await downloadFile(fileUrl);
await ctx.reportProgress?.(50, 100, "Processing...");
const result = await processFile(file);
// Structured logging
await ctx.log("info", `Processed ${file.size} bytes`);
// Structured logging with additional context (optional third parameter)
await ctx.log("info", "Processing complete", `fileSize: ${file.size} bytes, duration: 2.5s`);
// Check client capabilities
if (ctx.client.can("sampling")) {
// Ask the LLM to help analyze results
const summary = await ctx.sample(`Summarize this data: ${result}`);
return text(summary);
}
await ctx.reportProgress?.(100, 100, "Complete");
return object(result);
}
);
```
**Context methods:**
- `ctx.reportProgress(current: number, total: number, message: string)` - Show progress to user
- `ctx.log(level: "debug" | "info" | "warn" | "error", message: string, data?: string)` - Structured logging with optional additional context as a string
- `ctx.sample(prompt: string)` - Ask the LLM for help (requires client support)
- `ctx.client.can(capability: string)` - Check if client supports a feature
### Client Identity & Caller Context
`ctx.client` also exposes per-invocation caller context from `params._meta`:
```typescript
server.tool({ name: "personalise", schema: z.object({}) }, async (_p, ctx) => {
// Session-level (stable for the connection lifetime)
const { name, version } = ctx.client.info(); // "openai-mcp", "1.0.0"
const isAppsClient = ctx.client.supportsApps(); // true for ChatGPT
// Per-invocation — may differ on every tool call
const caller = ctx.client.user();
if (caller) {
const city = caller.location?.city ?? "there";
const greeting = caller.locale?.startsWith("it") ? "Ciao" : "Hello";
return text(`${greeting} from ${city}! (via ${name})`);
}
return text(`Hello! (via ${name})`);
});
```
**`ctx.client.user()` fields:**
- `subject` — stable opaque user ID (same across conversations, e.g. `openai/subject`)
- `conversationId` — current chat thread ID (changes per chat, e.g. `openai/session`)
- `locale` — BCP-47 locale, e.g. `"it-IT"` (server-side; inside widgets prefer `useWidget().locale` which is client-side and fresher)
- `location` — `{ city, region, country, timezone, latitude, longitude }`
- `userAgent` — browser/host user-agent string
- `timezoneOffsetMinutes` — UTC offset in minutes
**Key rules:**
- Returns `undefined` on clients that don't send this metadata (Inspector, CLI, non-ChatGPT clients)
- **Unverified / advisory** — self-reported by the client, not suitable for access control
- For verified identity, use `ctx.auth` (requires OAuth)
**ChatGPT multi-tenant model:**
ChatGPT uses a single MCP session for ALL users of a deployed app. Use `ctx.client.user()` to distinguish callers:
```
1 MCP session ctx.session.sessionId — shared across ALL users
N subjects ctx.client.user()?.subject — one per ChatGPT user account
M threads ctx.client.user()?.conversationId — one per chat conversation
```
```typescript
// Identify who is calling this specific invocation
const caller = ctx.client.user();
return object({
mcpSession: ctx.session.sessionId, // shared transport session
user: caller?.subject ?? null, // ChatGPT user ID
conversation: caller?.conversationId ?? null, // this chat thread
});
```
---
## Error Handling
**Always use `error()` helper, don't throw:**
```typescript
import { text, error } from "mcp-use/server";
server.tool(
{ name: "fetch-user", schema: z.object({ id: z.string() }) },
async ({ id }) => {
try {
const user = await fetchUser(id);
if (!user) {
return error(`User not found: ${id}`);
}
return object(user);
} catch (err) {
// Log for debugging
console.error("Failed to fetch user:", err);
// Return error to client
return error(
`Failed to fetch user: ${err instanceof Error ? err.message : "Unknown error"}`
);
}
}
);
```
**Error handling rules:**
- ✅ Return `error()` for graceful failure
- ❌ Don't throw exceptions (client sees raw error)
- ✅ Include helpful context in error messages
- ✅ Log errors server-side for debugging
---
## Tool with Widget
When your tool returns visual UI:
```typescript
import { widget, text } from "mcp-use/server";
server.tool(
{
name: "search-products",
description: "Search products by keyword",
schema: z.object({
query: z.string().describe("Search query")
}),
widget: {
name: "product-list", // Must match resources/product-list.tsx
invoking: "Searching products...",
invoked: "Products loaded"
}
},
async ({ query }) => {
const products = await searchProducts(query);
return widget({
props: {
products,
query,
totalCount: products.length
},
output: text(`Found ${products.length} products matching "${query}"`)
});
}
);
```
**Widget tool requirements:**
- Add `widget: { name }` to tool config
- Return `widget({ props, output })` from handler
- Create matching widget file: `resources/{name}.tsx`
- `exposeAsTool` defaults to `false` — omitting it is correct for this pattern
See [../widgets/basics.md](../widgets/basics.md) for widget implementation.
---
## Structured Output Schema
Validate tool output at runtime:
```typescript
server.tool(
{
name: "calculate-stats",
schema: z.object({
data: z.array(z.number()).describe("Array of numbers")
}),
outputSchema: z.object({
mean: z.number(),
median: z.number(),
stdDev: z.number(),
count: z.number()
})
},
async ({ data }) => {
const stats = calculateStats(data);
// Output is validated against outputSchema
return object({
mean: stats.mean,
median: stats.median,
stdDev: stats.stdDev,
count: data.length
});
}
);
```
**When to use `outputSchema`:**
- You want runtime validation of tool output
- Multiple code paths return different shapes
- Debugging output consistency issues
---
## Environment Variables
Securely handle API keys and configuration:
```typescript
// index.ts
const WEATHER_API_KEY = process.env.WEATHER_API_KEY;
server.tool(
{
name: "get-weather",
schema: z.object({ city: z.string() })
},
async ({ city }) => {
if (!WEATHER_API_KEY) {
return error(
"WEATHER_API_KEY not configured. Please set it in environment variables."
);
}
const data = await fetch(
`https://api.weather.com/v1?key=${WEATHER_API_KEY}&city=${city}`
);
// ... rest of logic
}
);
```
**Best practices:**
- ❌ Never hardcode secrets in code
- ✅ Use `process.env.VAR_NAME`
- ✅ Check if required vars are set
- ✅ Document required vars in `.env.example`
**Example `.env.example`:**
```bash
# Weather API key (get from weatherapi.com)
WEATHER_API_KEY=
# Database connection string
DATABASE_URL=
```
---
## Performance Patterns
### Caching
Cache expensive operations:
```typescript
const cache = new Map<string, { data: any; expires: number }>();
server.tool(
{ name: "fetch-weather", schema: z.object({ city: z.string() }) },
async ({ city }) => {
const cacheKey = `weather:${city}`;
const cached = cache.get(cacheKey);
// Return cached data if not expired
if (cached && cached.expires > Date.now()) {
return object(cached.data);
}
// Fetch fresh data
const data = await fetchWeather(city);
// Cache for 5 minutes
cache.set(cacheKey, {
data,
expires: Date.now() + 5 * 60 * 1000
});
return object(data);
}
);
```
### Rate Limiting
Prevent abuse using `hono-rate-limiter`:
```typescript
import { rateLimiter } from "hono-rate-limiter";
server.use(rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 100, // 100 requests per window per key
keyGenerator: (c) =>
c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ??
c.req.header("cf-connecting-ip") ??
c.req.header("x-real-ip") ??
"unknown",
}));
```
> Adjust the key generator depending on your hosting environment.
**Note:** mcp-use is built on Hono and supports both Hono-compatible middleware and Express middleware. Express middleware (e.g., `express-rate-limit`, `morgan`) is automatically detected and adapted. For custom middleware or advanced routing, see [../foundations/architecture.md](../foundations/architecture.md).
---
## Security Checklist
Before deploying tools:
- [ ] All schema fields have `.describe()`
- [ ] Input validation with Zod
- [ ] User input sanitized (no SQL injection, XSS)
- [ ] API keys in environment variables
- [ ] Errors return `error()` helper (not thrown)
- [ ] Try/catch around async operations
- [ ] Rate limiting on expensive operations
- [ ] Destructive operations have `destructiveHint: true`
---
## Next Steps
- **Format responses** → [response-helpers.md](response-helpers.md)
- **Add visual UI** → [../widgets/basics.md](../widgets/basics.md)
- **See examples** → [../patterns/common-patterns.md](../patterns/common-patterns.md)
references/widgets/advanced.md
# Advanced Widget Patterns
Advanced techniques for building complex, performant widgets.
**Topics:** Error boundaries, memoization, async data fetching, code splitting, complex state management
---
## Error Boundaries
Catch React errors and display fallback UI:
```tsx
import { Component, ReactNode } from "react";
import { McpUseProvider, useWidget } from "mcp-use/react";
interface ErrorBoundaryProps {
children: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: any) {
console.error("Widget error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: 20, color: "#c62828" }}>
<h3>Something went wrong</h3>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
// Usage
export default function SafeWidget() {
const { props, isPending } = useWidget();
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<ErrorBoundary>
<WidgetContent props={props} />
</ErrorBoundary>
</McpUseProvider>
);
}
```
---
## useMemo for Performance
Memoize expensive computations:
```tsx
import { useMemo } from "react";
import { McpUseProvider, useWidget } from "mcp-use/react";
export default function OptimizedWidget() {
const { props, isPending } = useWidget();
// Expensive computation - only runs when props.items changes
// Guard against isPending where props.items is undefined
const sortedAndFiltered = useMemo(() => {
if (!props.items) return { items: [], total: 0, avgScore: 0 };
let result = props.items;
// Filter
result = result.filter(item => item.active);
// Sort
result.sort((a, b) => b.score - a.score);
// Compute stats
return {
items: result,
total: result.length,
avgScore: result.length > 0
? result.reduce((sum, item) => sum + item.score, 0) / result.length
: 0
};
}, [props.items]);
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
<p>Total: {sortedAndFiltered.total}</p>
<p>Average: {sortedAndFiltered.avgScore.toFixed(2)}</p>
{sortedAndFiltered.items.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
</McpUseProvider>
);
}
```
---
## useCallback for Stable Functions
Prevent unnecessary re-renders:
```tsx
import { useCallback, useState } from "react";
import { McpUseProvider, useWidget, useCallTool } from "mcp-use/react";
export default function CallbackWidget() {
const { props, isPending } = useWidget();
const { callToolAsync } = useCallTool("process-item");
const [loadingId, setLoadingId] = useState<string | null>(null);
// Stable function reference
const handleAction = useCallback(async (id: string) => {
setLoadingId(id);
try {
await callToolAsync({ id });
} finally {
setLoadingId(null);
}
}, [callToolAsync]);
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div>
{props.items.map(item => (
<ItemRow
key={item.id}
item={item}
onAction={handleAction}
loading={loadingId === item.id}
/>
))}
</div>
</McpUseProvider>
);
}
// Child component won't re-render unnecessarily
const ItemRow = React.memo(({ item, onAction, loading }: any) => (
<div>
<span>{item.name}</span>
<button onClick={() => onAction(item.id)} disabled={loading}>
{loading ? "Processing..." : "Process"}
</button>
</div>
));
```
---
## Async Data Fetching (Client-Side)
Fetch additional data from widget:
```tsx
import { useState, useEffect } from "react";
import { McpUseProvider, useWidget } from "mcp-use/react";
export default function AsyncWidget() {
const { props, isPending } = useWidget();
const [details, setDetails] = useState<Record<string, unknown> | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!isPending && props.itemId) {
setLoading(true);
fetch(`/api/items/${props.itemId}/details`)
.then(res => res.json())
.then(data => setDetails(data))
.catch(err => console.error("Failed to load details:", err))
.finally(() => setLoading(false));
}
}, [isPending, props.itemId]);
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
<h2>{props.title}</h2>
{loading && <p>Loading details...</p>}
{details && (
<div>
<h3>Details</h3>
<pre>{JSON.stringify(details, null, 2)}</pre>
</div>
)}
</div>
</McpUseProvider>
);
}
```
**Prefer tool calls over direct API calls:**
```tsx
// ✅ Better - Use useCallTool
const { callToolAsync } = useCallTool("get-item-details");
useEffect(() => {
if (!isPending && props.itemId) {
setLoading(true);
callToolAsync({ id: props.itemId })
.then(result => setDetails(result))
.finally(() => setLoading(false));
}
}, [isPending, props.itemId, callToolAsync]);
```
---
## Complex State Management
Use useReducer for complex state:
```tsx
import { useReducer } from "react";
import { McpUseProvider, useWidget } from "mcp-use/react";
type State = {
selectedIds: Set<string>;
filters: { category: string; search: string };
sortBy: string;
sortOrder: "asc" | "desc";
};
type Action =
| { type: "TOGGLE_SELECT"; id: string }
| { type: "SET_FILTER"; key: string; value: string }
| { type: "SET_SORT"; by: string }
| { type: "TOGGLE_SORT_ORDER" }
| { type: "RESET" };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "TOGGLE_SELECT":
const newSelection = new Set(state.selectedIds);
if (newSelection.has(action.id)) {
newSelection.delete(action.id);
} else {
newSelection.add(action.id);
}
return { ...state, selectedIds: newSelection };
case "SET_FILTER":
return {
...state,
filters: { ...state.filters, [action.key]: action.value }
};
case "SET_SORT":
return { ...state, sortBy: action.by };
case "TOGGLE_SORT_ORDER":
return {
...state,
sortOrder: state.sortOrder === "asc" ? "desc" : "asc"
};
case "RESET":
return {
selectedIds: new Set(),
filters: { category: "all", search: "" },
sortBy: "name",
sortOrder: "asc"
};
default:
return state;
}
}
export default function ComplexWidget() {
const { props, isPending } = useWidget();
const [state, dispatch] = useReducer(reducer, {
selectedIds: new Set(),
filters: { category: "all", search: "" },
sortBy: "name",
sortOrder: "asc"
});
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
<input
type="text"
value={state.filters.search}
onChange={e => dispatch({ type: "SET_FILTER", key: "search", value: e.target.value })}
placeholder="Search..."
/>
<button onClick={() => dispatch({ type: "RESET" })}>
Reset Filters
</button>
{/* ... render items with state */}
</div>
</McpUseProvider>
);
}
```
---
## Virtualization for Large Lists
Render only visible items:
```tsx
import { useState, useRef, useEffect } from "react";
import { McpUseProvider, useWidget } from "mcp-use/react";
export default function VirtualizedList() {
const { props, isPending } = useWidget();
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const itemHeight = 50;
const containerHeight = 400;
const overscan = 3;
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
const visibleStart = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const visibleEnd = Math.min(
props.items.length,
Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
);
const visibleItems = props.items.slice(visibleStart, visibleEnd);
return (
<McpUseProvider autoSize>
<div
ref={containerRef}
onScroll={e => setScrollTop(e.currentTarget.scrollTop)}
style={{
height: containerHeight,
overflow: "auto",
position: "relative"
}}
>
<div style={{ height: props.items.length * itemHeight, position: "relative" }}>
{visibleItems.map((item, index) => (
<div
key={item.id}
style={{
position: "absolute",
top: (visibleStart + index) * itemHeight,
height: itemHeight,
width: "100%",
padding: 12,
borderBottom: "1px solid #eee"
}}
>
{item.name}
</div>
))}
</div>
</div>
</McpUseProvider>
);
}
```
---
## Debounced Search
> **Prerequisites:** For interactive widgets (buttons, forms, tool calls), read [interactivity.md](interactivity.md) first for foundational patterns.
Delay search to avoid excessive calls:
```tsx
import { useState, useEffect } from "react";
import { McpUseProvider, useWidget, useCallTool } from "mcp-use/react";
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
export default function DebouncedSearchWidget() {
const { props, isPending } = useWidget();
const { callToolAsync } = useCallTool("search");
const [search, setSearch] = useState("");
const [results, setResults] = useState<{ id: string; name: string }[]>([]);
const [searching, setSearching] = useState(false);
const debouncedSearch = useDebounce(search, 300);
useEffect(() => {
if (!debouncedSearch.trim()) {
setResults([]);
return;
}
setSearching(true);
callToolAsync({ query: debouncedSearch })
.then(result => setResults(result.structuredContent?.items || []))
.catch(err => console.error("Search failed:", err))
.finally(() => setSearching(false));
}, [debouncedSearch, callToolAsync]);
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
<input
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search..."
style={{ width: "100%", padding: 8 }}
/>
{searching && <p>Searching...</p>}
<div>
{results.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
</div>
</McpUseProvider>
);
}
```
---
## Infinite Scroll
Load more items as user scrolls:
```tsx
import { useState, useRef, useEffect } from "react";
import { McpUseProvider, useWidget, useCallTool } from "mcp-use/react";
interface Item {
id: string;
name: string;
}
export default function InfiniteScrollWidget() {
const { props, isPending } = useWidget<{ items: Item[] }>();
const { callToolAsync } = useCallTool("load-more");
const [items, setItems] = useState<Item[]>([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const observerTarget = useRef<HTMLDivElement>(null);
// Sync initial items from props once loaded
useEffect(() => {
if (!isPending && props.items) {
setItems(props.items);
}
}, [isPending, props.items]);
useEffect(() => {
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting && hasMore && !loading) {
loadMore();
}
},
{ threshold: 1.0 }
);
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => observer.disconnect();
}, [hasMore, loading]);
const loadMore = async () => {
setLoading(true);
try {
const result = await callToolAsync({
offset: items.length,
limit: 20
});
const newItems = result.structuredContent?.items || [];
if (newItems.length === 0) {
setHasMore(false);
} else {
setItems(prev => [...prev, ...newItems]);
}
} catch (error) {
console.error("Failed to load more:", error);
} finally {
setLoading(false);
}
};
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
{items.map(item => (
<div key={item.id} style={{ padding: 12, borderBottom: "1px solid #eee" }}>
{item.name}
</div>
))}
<div ref={observerTarget} style={{ height: 20 }}>
{loading && <p>Loading more...</p>}
{!hasMore && <p>No more items</p>}
</div>
</div>
</McpUseProvider>
);
}
```
---
## Local Storage Persistence
Persist widget state across sessions:
```tsx
import { useState, useEffect } from "react";
import { McpUseProvider, useWidget } from "mcp-use/react";
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error("Error reading from localStorage:", error);
return initialValue;
}
});
const setValue = (value: T) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error("Error writing to localStorage:", error);
}
};
return [storedValue, setValue];
}
export default function PersistentWidget() {
const { props, isPending } = useWidget();
const [favorites, setFavorites] = useLocalStorage<string[]>("favorites", []);
const toggleFavorite = (id: string) => {
setFavorites(prev =>
prev.includes(id) ? prev.filter(fav => fav !== id) : [...prev, id]
);
};
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div>
{props.items.map(item => (
<div key={item.id}>
<button onClick={() => toggleFavorite(item.id)}>
{favorites.includes(item.id) ? "⭐" : "☆"}
</button>
{item.name}
</div>
))}
</div>
</McpUseProvider>
);
}
```
---
## Drag and Drop
Reorder items with drag and drop:
```tsx
import { useState, useEffect } from "react";
import { McpUseProvider, useWidget } from "mcp-use/react";
interface Item {
id: string;
name: string;
}
export default function DraggableList() {
const { props, isPending } = useWidget<{ items: Item[] }>();
const [items, setItems] = useState<Item[]>([]);
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
// Sync items from props once loaded
useEffect(() => {
if (!isPending && props.items) {
setItems(props.items);
}
}, [isPending, props.items]);
const handleDragStart = (index: number) => {
setDraggedIndex(index);
};
const handleDragOver = (e: React.DragEvent, index: number) => {
e.preventDefault();
if (draggedIndex === null || draggedIndex === index) return;
const newItems = [...items];
const draggedItem = newItems[draggedIndex];
newItems.splice(draggedIndex, 1);
newItems.splice(index, 0, draggedItem);
setItems(newItems);
setDraggedIndex(index);
};
const handleDragEnd = () => {
setDraggedIndex(null);
// Optionally save new order with useCallTool
};
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div>
{items.map((item, index) => (
<div
key={item.id}
draggable
onDragStart={() => handleDragStart(index)}
onDragOver={e => handleDragOver(e, index)}
onDragEnd={handleDragEnd}
style={{
padding: 12,
margin: "4px 0",
backgroundColor: draggedIndex === index ? "#e3f2fd" : "white",
border: "1px solid #ddd",
cursor: "move"
}}
>
⋮⋮ {item.name}
</div>
))}
</div>
</McpUseProvider>
);
}
```
---
## Keyboard Shortcuts
```tsx
import { useEffect } from "react";
import { McpUseProvider, useWidget, useCallTool } from "mcp-use/react";
export default function KeyboardWidget() {
const { props, isPending } = useWidget();
const { callTool: save } = useCallTool("save");
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Ctrl+S to save
if (e.ctrlKey && e.key === "s") {
e.preventDefault();
save({});
}
// Escape to cancel
if (e.key === "Escape") {
// Handle escape
}
// Arrow keys for navigation
if (e.key === "ArrowDown") {
// Navigate down
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [save]);
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div>
<p>Keyboard shortcuts:</p>
<ul>
<li><kbd>Ctrl+S</kbd> - Save</li>
<li><kbd>Esc</kbd> - Cancel</li>
<li><kbd>↑/↓</kbd> - Navigate</li>
</ul>
</div>
</McpUseProvider>
);
}
```
---
## Best Practices
1. **Use Error Boundaries** - Catch errors gracefully
2. **Memoize Expensive Computations** - Use `useMemo` for performance
3. **Debounce User Input** - Avoid excessive API calls
4. **Virtualize Large Lists** - Render only visible items
5. **Persist State When Useful** - Use localStorage for preferences
6. **Handle Loading States** - Show spinners, disable buttons
7. **Implement Keyboard Shortcuts** - Improve power user experience
8. **Profile Performance** - Use React DevTools Profiler
---
## Performance Checklist
- [ ] Large lists virtualized or paginated
- [ ] Expensive computations memoized with `useMemo`
- [ ] Event handlers memoized with `useCallback`
- [ ] Search inputs debounced
- [ ] Images lazy-loaded
- [ ] Error boundaries in place
- [ ] Console warnings addressed
---
## Next Steps
- **See examples** → [../patterns/common-patterns.md](../patterns/common-patterns.md)
- **Review best practices** → [../../SKILL.md](../../SKILL.md)
references/widgets/basics.md
# Widget Basics
Widgets are React components that provide visual UI for MCP tools. They let users browse, compare, and interact with data visually.
**Use widgets for:** Product lists, calendars, dashboards, search results, file browsers, any visual data representation
---
## When to Use Widgets
**Use a widget when:**
- ✅ Browsing or comparing multiple items
- ✅ Visual representation improves understanding (charts, images, layouts)
- ✅ Interactive selection is easier visually than through text
- ✅ User needs to see data structure at a glance
**Use plain tool (no widget) when:**
- ❌ Output is simple text or a single value
- ❌ No visual representation adds value
- ❌ Quick conversational response is sufficient
**When in doubt:** Use a widget. It makes the experience better.
---
## Minimal Widget
### 1. Create Tool with Widget Config
```typescript
// index.ts
import { MCPServer, widget, text } from "mcp-use/server";
import { z } from "zod";
const server = new MCPServer({
name: "my-server",
version: "1.0.0"
});
server.tool(
{
name: "show-weather",
description: "Display weather for a city",
schema: z.object({
city: z.string().describe("City name")
}),
widget: {
name: "weather-display", // Must match filename: resources/weather-display.tsx
invoking: "Fetching weather...", // Optional: shown while loading
invoked: "Weather loaded" // Optional: shown when complete
}
},
async ({ city }) => {
const data = await getWeather(city);
return widget({
props: {
city: data.city,
temp: data.temperature,
conditions: data.conditions,
icon: data.icon
},
output: text(`Weather in ${city}: ${data.temperature}°C, ${data.conditions}`)
});
}
);
```
### 2. Create Widget Component
```tsx
// resources/weather-display.tsx
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
const propsSchema = z.object({
city: z.string(),
temp: z.number(),
conditions: z.string(),
icon: z.string()
});
export const widgetMetadata: WidgetMetadata = {
description: "Display weather information for a city",
props: propsSchema,
exposeAsTool: false // ← Critical: prevents duplicate tool registration
};
type Props = z.infer<typeof propsSchema>;
export default function WeatherDisplay() {
const { props, isPending } = useWidget<Props>();
if (isPending) {
return (
<McpUseProvider autoSize>
<div>Loading weather...</div>
</McpUseProvider>
);
}
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
<h2>{props.city}</h2>
<img src={props.icon} alt={props.conditions} width={64} />
<div style={{ fontSize: 48 }}>{props.temp}°C</div>
<p>{props.conditions}</p>
</div>
</McpUseProvider>
);
}
```
**Key requirements:**
1. Export `widgetMetadata` with props schema
2. Infer type from schema and pass to `useWidget<Props>()`
3. `exposeAsTool` defaults to `false` — correct when pairing with a custom tool
4. Wrap root in `<McpUseProvider autoSize>`
5. **Always check `isPending` before accessing `props`**
**Production builds (`mcp-use build`):** Never use bare `useWidget()` without a props generic — fields default to `unknown` and TypeScript will fail (e.g. TS2322). If you use `callTool` from `useWidget()`, treat `structuredContent` and nested values as `unknown` until you parse with Zod, narrow with `typeof`/`Array.isArray`, or assign to typed variables; do not pass `unknown` directly as JSX children or string props.
---
## Widget Metadata
The `widgetMetadata` export defines your widget's contract:
```typescript
export const widgetMetadata: WidgetMetadata = {
description: "Brief description of what this widget displays",
props: z.object({
// Define all props the widget expects
id: z.string(),
title: z.string(),
count: z.number(),
items: z.array(z.object({
name: z.string(),
value: z.number()
}))
}),
exposeAsTool: false // Default; omit or set explicitly when pairing with a custom tool
};
```
**Fields:**
- `description` - What the widget displays/does
- `props` - Zod schema defining expected props shape
- `exposeAsTool` - Set to `true` to auto-register as a tool (default: `false`)
- `metadata.invoking` - Status text shown in inspector while tool runs (auto-default: `"Loading {name}..."`)
- `metadata.invoked` - Status text shown in inspector after tool completes (auto-default: `"{name} ready"`)
```typescript
export const widgetMetadata: WidgetMetadata = {
description: "Display weather information for a city",
props: propsSchema,
metadata: {
invoking: "Fetching weather...", // Shimmer text while tool runs
invoked: "Weather loaded", // Static text when complete
csp: { connectDomains: ["https://api.weather.com"] },
},
};
```
These status texts appear as animated shimmer text (pending) and static text (complete) in the MCP Inspector and ChatGPT. The values also flow to `openai/toolInvocation/invoking`/`invoked` in tool metadata automatically.
---
## useWidget() Hook
The `useWidget()` hook provides access to props and widget state:
```typescript
const {
props, // Widget props from tool response
isPending, // True while props are loading
setState, // Update widget state
state, // Current widget state
} = useWidget();
```
**To call tools from a widget**, use the dedicated `useCallTool()` hook — see [interactivity.md](interactivity.md).
### props
Data passed from tool's `widget({ props })` response:
```typescript
const { props } = useWidget();
// Access props after isPending check
if (!isPending) {
console.log(props.city); // "Tokyo"
console.log(props.temp); // 28
}
```
**Always check `isPending` before accessing `props`:**
```typescript
❌ const { props } = useWidget();
return <div>{props.city}</div>; // Error! props undefined while loading
✅ const { props, isPending } = useWidget();
if (isPending) return <div>Loading...</div>;
return <div>{props.city}</div>; // Safe
```
### isPending
Boolean indicating if props are still loading.
**CRITICAL:** Widgets render **before** the tool completes execution. On first render:
- `isPending` is `true`
- `props` is an empty object `{}`
- Accessing `props` fields will cause errors
**Widget Lifecycle:**
1. Widget mounts immediately when tool is called → `isPending = true`, `props = {}`
2. Tool executes and returns `widget({ props })`
3. Widget re-renders → `isPending = false`, `props` contains data
```typescript
const { isPending } = useWidget();
if (isPending) {
return (
<McpUseProvider autoSize>
<div>Loading...</div>
</McpUseProvider>
);
}
// Now safe to access props - guaranteed to have data
```
**Multiple patterns for handling isPending:**
```typescript
// ✅ Pattern 1: Early return (recommended)
if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
return <McpUseProvider autoSize><div>{props.data}</div></McpUseProvider>;
// ✅ Pattern 2: Conditional rendering
return (
<McpUseProvider autoSize>
{isPending ? <div>Loading...</div> : <div>{props.data}</div>}
</McpUseProvider>
);
// ✅ Pattern 3: Optional chaining (when props might be undefined)
return <McpUseProvider autoSize><div>{props?.data ?? "Loading..."}</div></McpUseProvider>;
```
---
## McpUseProvider
**Required wrapper** for all widgets. Provides context and handles iframe sizing.
```typescript
import { McpUseProvider } from "mcp-use/react";
export default function MyWidget() {
const { props, isPending } = useWidget();
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div>
{/* Your widget content */}
</div>
</McpUseProvider>
);
}
```
**Props:**
- `autoSize={true}` - Automatically resize iframe to content (recommended)
- `autoSize={false}` - Fixed height, widget handles scrolling
**Must wrap:**
- ✅ Every return path (including loading states)
- ✅ Root element of component
---
## Props Handling Patterns
### Simple Props
```typescript
export const widgetMetadata: WidgetMetadata = {
props: z.object({
message: z.string(),
count: z.number()
}),
exposeAsTool: false
};
export default function SimpleWidget() {
const { props, isPending } = useWidget();
if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
return (
<McpUseProvider autoSize>
<div>
<p>{props.message}</p>
<p>Count: {props.count}</p>
</div>
</McpUseProvider>
);
}
```
### Array Props
```typescript
export const widgetMetadata: WidgetMetadata = {
props: z.object({
items: z.array(z.object({
id: z.string(),
name: z.string()
}))
}),
exposeAsTool: false
};
export default function ListWidget() {
const { props, isPending } = useWidget();
if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
return (
<McpUseProvider autoSize>
<ul>
{props.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</McpUseProvider>
);
}
```
### Nested Props
```typescript
export const widgetMetadata: WidgetMetadata = {
props: z.object({
user: z.object({
name: z.string(),
profile: z.object({
bio: z.string(),
avatar: z.string()
})
})
}),
exposeAsTool: false
};
export default function ProfileWidget() {
const { props, isPending } = useWidget();
if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
const { user } = props;
return (
<McpUseProvider autoSize>
<div>
<img src={user.profile.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.profile.bio}</p>
</div>
</McpUseProvider>
);
}
```
### Optional Props
```typescript
export const widgetMetadata: WidgetMetadata = {
props: z.object({
title: z.string(),
subtitle: z.string().optional(), // May be undefined
items: z.array(z.string())
}),
exposeAsTool: false
};
export default function FlexibleWidget() {
const { props, isPending } = useWidget();
if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
return (
<McpUseProvider autoSize>
<div>
<h1>{props.title}</h1>
{props.subtitle && <h2>{props.subtitle}</h2>}
<ul>
{props.items.map((item, i) => <li key={i}>{item}</li>)}
</ul>
</div>
</McpUseProvider>
);
}
```
---
## File Location
Widgets live in `resources/` directory:
```
my-server/
├── index.ts # Server code
├── resources/
│ ├── weather-display.tsx # Widget component
│ ├── product-list.tsx
│ └── calendar-view.tsx
└── package.json
```
**Naming convention:**
- Use kebab-case for widget names
- Tool config: `widget: { name: "weather-display" }`
- File: `resources/weather-display.tsx`
---
## TypeScript Types
For type safety, infer props type from schema:
⚠️ **CRITICAL:** Always define your Zod schema in a separate constant before `widgetMetadata`. Never infer types from `widgetMetadata.props` - TypeScript will lose type information and the result will be `unknown`.
```typescript
import { z } from "zod";
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
const propsSchema = z.object({
city: z.string(),
temp: z.number(),
conditions: z.string()
});
export const widgetMetadata: WidgetMetadata = {
description: "Display weather",
props: propsSchema,
exposeAsTool: false
};
type Props = z.infer<typeof propsSchema>;
export default function WeatherWidget() {
const { props, isPending } = useWidget<Props>();
if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
// Now props is fully typed!
return (
<McpUseProvider autoSize>
<div>
<h2>{props.city}</h2> {/* ✓ TypeScript knows this is string */}
<p>{props.temp}°C</p> {/* ✓ TypeScript knows this is number */}
</div>
</McpUseProvider>
);
}
```
---
## Common Mistakes
### ❌ Missing isPending Check
```typescript
// ❌ Bad - props undefined during loading
export default function BadWidget() {
const { props } = useWidget();
return (
<McpUseProvider autoSize>
<div>{props.title}</div> {/* Error! */}
</McpUseProvider>
);
}
// ✅ Good
export default function GoodWidget() {
const { props, isPending } = useWidget();
if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
return (
<McpUseProvider autoSize>
<div>{props.title}</div>
</McpUseProvider>
);
}
```
### ❌ Missing McpUseProvider
```typescript
// ❌ Bad - Missing provider
export default function BadWidget() {
const { props, isPending } = useWidget();
if (isPending) return <div>Loading...</div>;
return <div>{props.title}</div>; {/* Won't render correctly */}
}
// ✅ Good
export default function GoodWidget() {
const { props, isPending } = useWidget();
if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
return (
<McpUseProvider autoSize>
<div>{props.title}</div>
</McpUseProvider>
);
}
```
### `exposeAsTool` — default is `false`
```typescript
// ✅ Default — widget is a resource only, exposed via a custom tool
export const widgetMetadata: WidgetMetadata = {
description: "...",
props: z.object({ ... })
// exposeAsTool defaults to false
};
// ✅ Explicit opt-in to auto-registration
export const widgetMetadata: WidgetMetadata = {
description: "...",
props: z.object({ ... }),
exposeAsTool: true // Auto-registers widget as a tool
};
```
### ❌ Missing Type Parameter on useWidget
```typescript
// ❌ Bad - props is UnknownObject, no autocomplete or type safety
const propsSchema = z.object({
title: z.string(),
count: z.number()
});
export default function BadWidget() {
const { props } = useWidget(); // props is UnknownObject
return <div>{props.title}</div>; // No IDE support, runtime errors possible
}
// ✅ Good - props is fully typed with IDE support
const propsSchema = z.object({
title: z.string(),
count: z.number()
});
type Props = z.infer<typeof propsSchema>;
export default function GoodWidget() {
const { props } = useWidget<Props>(); // props is properly typed
return <div>{props.title}</div>; // Full autocomplete and type checking
}
```
### ❌ Inferring Type from widgetMetadata.props
```typescript
// ❌ Bad - Type inference fails, Props is unknown
export const widgetMetadata: WidgetMetadata = {
description: "...",
props: z.object({
title: z.string(),
count: z.number()
}) // Inline schema definition
};
type Props = z.infer<typeof widgetMetadata.props>; // Props is unknown!
export default function BadWidget() {
const { props } = useWidget<Props>();
return <div>{props.title}</div>; // No autocomplete, no type safety
}
// ✅ Good - Extract schema first for proper type inference
const propsSchema = z.object({
title: z.string(),
count: z.number()
});
export const widgetMetadata: WidgetMetadata = {
description: "...",
props: propsSchema // Reference the schema variable
};
type Props = z.infer<typeof propsSchema>; // Props is properly typed!
export default function GoodWidget() {
const { props } = useWidget<Props>();
return <div>{props.title}</div>; // Full autocomplete and type checking
}
```
**Why this happens:** The `WidgetMetadata` type is generic, so TypeScript can't preserve the specific Zod schema type when defined inline. Always extract your schema to a separate constant before using it in `widgetMetadata`.
---
## Testing Widgets
### Option 1: Inspector (interactive)
1. Start dev server: `npm run dev`
2. Open inspector: `http://localhost:3000/inspector`
3. Click "List Tools" → Find your tool
4. Click "Call Tool" → Enter test input
5. Widget renders in inspector
**Quick iteration:**
- Change widget code → Auto-reload
- Adjust props schema → Update tool call input
- Test edge cases (empty lists, missing optional props)
### Option 2: Headless screenshot (agent-friendly)
For visual feedback loops where you want to verify a widget change without leaving the terminal — call the tool, save a PNG, eyeball it, edit, repeat:
```bash
# Saved-server form (assumes you ran `mcp-use client connect dev <url>` once)
npx mcp-use client dev screenshot --tool get-weather city=Tokyo \
--width 800 --height 600 --theme light \
--output ./weather.png
# Ad-hoc form — no saved server, pass auth headers inline if needed
npx mcp-use client screenshot --mcp http://localhost:3000/mcp \
--tool get-weather city=Tokyo
```
- Args are `key=value` pairs, `key:='<json>'` for nested values, or one full JSON object
- The saved-server form reuses the auth from `mcp-use client connect` (OAuth or `--auth <token>`); the ad-hoc form accepts `-H "Header: value"` (repeatable) for authenticated servers
- Add `--device-scale-factor 2` for Retina output
- For sandboxed environments without a local Chrome, point `--cdp-url <ws>` at a hosted Chromium (e.g. Notte) and `--inspector <publicly-reachable-url>` at a deployed inspector
Equivalently, `mcp-use client <name> tools call <tool> ... --screenshot` calls the tool *and* saves a widget PNG in one step — useful for one-shot verification.
---
## Complete Example
```typescript
// index.ts
import { MCPServer, widget, text } from "mcp-use/server";
import { z } from "zod";
const server = new MCPServer({
name: "product-server",
version: "1.0.0"
});
server.tool(
{
name: "search-products",
description: "Search products by keyword",
schema: z.object({
query: z.string().describe("Search query")
}),
widget: {
name: "product-list",
invoking: "Searching products...",
invoked: "Products loaded"
}
},
async ({ query }) => {
const products = await searchProducts(query);
return widget({
props: {
products,
query,
totalCount: products.length
},
output: text(`Found ${products.length} products matching "${query}"`)
});
}
);
server.listen();
```
```tsx
// resources/product-list.tsx
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
export const widgetMetadata: WidgetMetadata = {
description: "Display product search results",
props: z.object({
products: z.array(z.object({
id: z.string(),
name: z.string(),
price: z.number(),
image: z.string()
})),
query: z.string(),
totalCount: z.number()
}),
exposeAsTool: false
};
export default function ProductList() {
const { props, isPending } = useWidget();
if (isPending) {
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>Loading products...</div>
</McpUseProvider>
);
}
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
<h2>Search: "{props.query}"</h2>
<p>Found {props.totalCount} products</p>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 16 }}>
{props.products.map(product => (
<div key={product.id} style={{ border: "1px solid #ddd", padding: 12, borderRadius: 8 }}>
<img src={product.image} alt={product.name} style={{ width: "100%", height: 150, objectFit: "cover" }} />
<h3 style={{ fontSize: 16, margin: "8px 0" }}>{product.name}</h3>
<p style={{ fontSize: 18, fontWeight: "bold" }}>${product.price}</p>
</div>
))}
</div>
</div>
</McpUseProvider>
);
}
```
---
## Next Steps
- **Manage widget state** → [state.md](state.md)
- **Add interactivity** → [interactivity.md](interactivity.md)
- **Style with themes** → [ui-guidelines.md](ui-guidelines.md)
- **Advanced patterns** → [advanced.md](advanced.md)
references/widgets/files.md
# File Handling in Widgets
Upload and download files from within widgets using the `useFiles` hook.
> **ChatGPT Apps SDK only.** The MCP Apps spec (SEP-1865) has [deferred file handling](https://github.com/modelcontextprotocol/ext-apps/issues/201). MCP Apps clients (Claude, Goose, VS Code) do not support file operations. Always check `isSupported` before calling `upload` or `getDownloadUrl`.
---
## `useFiles` Hook
```tsx
import { useFiles } from "mcp-use/react";
const { upload, getDownloadUrl, isSupported } = useFiles();
```
| Property | Type | Description |
|----------|------|-------------|
| `isSupported` | `boolean` | `true` only in ChatGPT Apps SDK. Always check before calling upload/getDownloadUrl. |
| `upload` | `(file: File, options?) => Promise<FileMetadata>` | Upload a file. Model-visible by default. |
| `getDownloadUrl` | `(file: FileMetadata) => Promise<{ downloadUrl: string }>` | Get a temporary download URL (~5 min). |
`FileMetadata` type: `{ fileId: string }`
---
## Basic Pattern
Always guard with `isSupported`. Render a fallback for MCP Apps clients:
```tsx
import { useFiles, useWidget, McpUseProvider } from "mcp-use/react";
import { useState } from "react";
export default function FileWidget() {
const { isPending } = useWidget();
const { upload, getDownloadUrl, isSupported } = useFiles();
const [fileId, setFileId] = useState<string | null>(null);
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
// Renders in MCP Apps clients (Claude, Goose, etc.)
if (!isSupported) {
return (
<McpUseProvider autoSize>
<p>File operations require ChatGPT Apps SDK.</p>
</McpUseProvider>
);
}
async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
const { fileId } = await upload(file);
setFileId(fileId);
}
async function handleGetLink() {
if (!fileId) return;
const { downloadUrl } = await getDownloadUrl({ fileId });
setDownloadUrl(downloadUrl);
}
return (
<McpUseProvider autoSize>
<input type="file" onChange={handleUpload} />
{fileId && <button onClick={handleGetLink}>Get download link</button>}
{downloadUrl && <a href={downloadUrl} target="_blank">Download</a>}
</McpUseProvider>
);
}
```
---
## Model Visibility
By default, uploaded files are tracked in widget state under `imageIds` so ChatGPT includes them in the model's conversation context.
```tsx
// Model-visible (default) — ChatGPT includes the file in context
const { fileId } = await upload(file);
// Private — file is uploaded but model won't see it
const { fileId } = await upload(file, { modelVisible: false });
```
Use `modelVisible: false` when the file is for widget-internal use only (e.g. a reference image the widget processes, a config file the user uploads privately).
When `modelVisible: true` (default), the new `fileId` is appended to `imageIds` in widget state, preserving any existing IDs and other state fields.
---
## Storing fileId for Later
Download URLs are **temporary** (~5 minutes). Store the `fileId`, not the URL:
```tsx
const { state, setState } = useWidget<{ uploadedFileId: string | null }>();
async function handleUpload(file: File) {
const { fileId } = await upload(file);
// Persist fileId in widget state so model can reference the upload
await setState({ uploadedFileId: fileId });
}
async function handleDownload() {
if (!state?.uploadedFileId) return;
// Call getDownloadUrl fresh each time — don't cache the URL
const { downloadUrl } = await getDownloadUrl({ fileId: state.uploadedFileId });
window.open(downloadUrl, "_blank");
}
```
---
## Error Handling
Both `upload` and `getDownloadUrl` throw if called when `isSupported` is `false`. They also throw on host-level errors (network failure, policy violation):
```tsx
try {
const { fileId } = await upload(file);
setFileId(fileId);
} catch (err) {
console.error("Upload failed:", err);
setError(err instanceof Error ? err.message : "Upload failed");
}
```
---
## Key Rules
- **Always check `isSupported` first** — render a fallback UI for non-ChatGPT hosts
- **Never cache download URLs** — they expire; call `getDownloadUrl` each time you need one
- **Store `fileId` in widget state** if the user may need to re-download later
- **Use `{ modelVisible: false }`** for files the model should not see
- `upload` + `getDownloadUrl` are only available in ChatGPT Apps SDK — MCP Apps support is deferred
---
## Reference
- Full API reference: https://docs.mcp-use.com/typescript/server/widget-components/usefiles
- Example server: `examples/server/ui/files/`
references/widgets/interactivity.md
# Widget Interactivity
Widgets interact with the outside world using hooks from `mcp-use/react`. `useCallTool()` provides tool calling with built-in state management. `sendFollowUpMessage` from `useWidget()` triggers LLM conversation turns.
**Use `useCallTool()` for:** Creating items, updating data, triggering actions, submitting forms
**Use `sendFollowUpMessage` for:** Asking the AI to analyze, compare, summarize, or respond based on widget context
---
## useCallTool() Basics
`useCallTool()` provides a TanStack Query-like state machine for calling MCP tools:
```tsx
import { useCallTool } from "mcp-use/react";
const { callTool, callToolAsync, isPending, isSuccess, isError, data, error } =
useCallTool("tool-name");
// Fire-and-forget with optional callbacks
callTool({ param: "value" }, {
onSuccess: (result) => console.log(result.structuredContent),
onError: (err) => console.error(err),
onSettled: () => hideSpinner(),
});
// Or async/await
const result = await callToolAsync({ param: "value" });
```
**State flags:**
| Property | Description |
|---|---|
| `isPending` | Tool is executing |
| `isSuccess` | Succeeded — `data` is available |
| `isError` | Failed — `error` is available |
| `isIdle` | No call made yet |
| `callTool` | Fire-and-forget; optional `onSuccess`/`onError`/`onSettled` callbacks |
| `callToolAsync` | Returns `Promise<CallToolResult>` |
**Type inference:** When using `mcp-use dev`, types for tool names, inputs, and outputs are auto-generated to `.mcp-use/tool-registry.d.ts`. The hook is fully typed with autocomplete.
---
## Simple Button Action
```tsx
import { McpUseProvider, useWidget, useCallTool, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
export const widgetMetadata: WidgetMetadata = {
description: "Todo list with actions",
props: z.object({
todos: z.array(z.object({
id: z.string(),
title: z.string(),
completed: z.boolean()
}))
}),
exposeAsTool: false
};
export default function TodoList() {
const { props, isPending: isLoading } = useWidget();
const { callTool, isPending } = useCallTool("toggle-todo");
if (isLoading) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div>
{props.todos.map(todo => (
<div key={todo.id} style={{ display: "flex", gap: 8, padding: 8 }}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => callTool({ id: todo.id, completed: !todo.completed })}
disabled={isPending}
/>
<span style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
{todo.title}
</span>
</div>
))}
</div>
</McpUseProvider>
);
}
```
**Corresponding tool:**
```typescript
server.tool(
{
name: "toggle-todo",
description: "Toggle todo completion status",
schema: z.object({
id: z.string(),
completed: z.boolean()
})
},
async ({ id, completed }) => {
await updateTodo(id, { completed });
return text(`Todo ${completed ? "completed" : "uncompleted"}`);
}
);
```
---
## Form Submission
`isPending` from `useCallTool` replaces manual `submitting` state:
```tsx
import { useState } from "react";
import { McpUseProvider, useWidget, useCallTool } from "mcp-use/react";
export default function CreateItemWidget() {
const { props, isPending: isLoading } = useWidget();
const { callTool, isPending } = useCallTool("create-todo");
const [title, setTitle] = useState("");
if (isLoading) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim()) return;
callTool({ title }, {
onSuccess: () => setTitle(""),
onError: () => alert("Failed to create todo"),
});
};
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="New todo..."
disabled={isPending}
style={{ padding: 8, width: 300, marginRight: 8 }}
/>
<button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Add Todo"}
</button>
</form>
<div style={{ marginTop: 16 }}>
{props.todos.map(todo => (
<div key={todo.id}>{todo.title}</div>
))}
</div>
</div>
</McpUseProvider>
);
}
```
**Corresponding tool:**
```typescript
server.tool(
{
name: "create-todo",
schema: z.object({
title: z.string().describe("Todo title")
})
},
async ({ title }) => {
const todo = await createTodo(title);
return text(`Created todo: ${todo.title}`);
}
);
```
---
## Delete Action
```tsx
const { callTool: deleteTodo, isPending: isDeleting } = useCallTool("delete-todo");
const handleDelete = (id: string) => {
if (!confirm("Are you sure you want to delete this item?")) return;
deleteTodo({ id }, {
onError: () => alert("Failed to delete item"),
});
};
return (
<McpUseProvider autoSize>
<div>
{props.todos.map(todo => (
<div key={todo.id} style={{ display: "flex", justifyContent: "space-between", padding: 8 }}>
<span>{todo.title}</span>
<button onClick={() => handleDelete(todo.id)} disabled={isDeleting}>Delete</button>
</div>
))}
</div>
</McpUseProvider>
);
```
---
## Optimistic Updates
Update UI immediately, then call tool:
```tsx
import { useState, useEffect } from "react";
import { McpUseProvider, useWidget, useCallTool } from "mcp-use/react";
interface Todo {
id: string;
title: string;
completed: boolean;
}
export default function OptimisticWidget() {
const { props, isPending: isLoading } = useWidget<{ todos: Todo[] }>();
const { callToolAsync } = useCallTool("toggle-todo");
const [todos, setTodos] = useState<Todo[]>([]);
useEffect(() => {
if (!isLoading && props.todos) {
setTodos(props.todos);
}
}, [isLoading, props.todos]);
const handleToggle = async (id: string) => {
// Optimistic update
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
try {
await callToolAsync({ id });
} catch {
// Revert on failure
setTodos(props.todos);
alert("Failed to update todo");
}
};
if (isLoading) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div>
{todos.map(todo => (
<div key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id)}
/>
{todo.title}
</div>
))}
</div>
</McpUseProvider>
);
}
```
---
## Action Buttons
Multiple actions per item — declare a hook for each tool:
```tsx
const { callTool: editItem } = useCallTool("edit-item");
const { callTool: duplicateItem } = useCallTool("duplicate-item");
const { callTool: archiveItem } = useCallTool("archive-item");
const { callTool: deleteItem } = useCallTool("delete-item");
return (
<McpUseProvider autoSize>
<div>
{props.items.map(item => (
<div key={item.id} style={{ padding: 12, border: "1px solid #ddd", marginBottom: 8 }}>
<h3>{item.title}</h3>
<p>{item.description}</p>
<div style={{ display: "flex", gap: 8 }}>
<button onClick={() => editItem({ id: item.id })}>Edit</button>
<button onClick={() => duplicateItem({ id: item.id })}>Duplicate</button>
<button onClick={() => archiveItem({ id: item.id })}>Archive</button>
<button onClick={() => deleteItem({ id: item.id })} style={{ color: "red" }}>
Delete
</button>
</div>
</div>
))}
</div>
</McpUseProvider>
);
```
---
## Inline Editing
```tsx
import { useState } from "react";
import { McpUseProvider, useWidget, useCallTool } from "mcp-use/react";
export default function EditableList() {
const { props, isPending: isLoading } = useWidget();
const { callToolAsync, isPending: isSaving } = useCallTool("update-item");
const [editingId, setEditingId] = useState<string | null>(null);
const [editValue, setEditValue] = useState("");
const startEdit = (id: string, currentValue: string) => {
setEditingId(id);
setEditValue(currentValue);
};
const saveEdit = async (id: string) => {
try {
await callToolAsync({ id, title: editValue });
setEditingId(null);
} catch {
alert("Failed to save");
}
};
const cancelEdit = () => {
setEditingId(null);
setEditValue("");
};
if (isLoading) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div>
{props.items.map(item => (
<div key={item.id} style={{ padding: 8, display: "flex", gap: 8 }}>
{editingId === item.id ? (
<>
<input
type="text"
value={editValue}
onChange={e => setEditValue(e.target.value)}
autoFocus
/>
<button onClick={() => saveEdit(item.id)} disabled={isSaving}>Save</button>
<button onClick={cancelEdit}>Cancel</button>
</>
) : (
<>
<span>{item.title}</span>
<button onClick={() => startEdit(item.id, item.title)}>Edit</button>
</>
)}
</div>
))}
</div>
</McpUseProvider>
);
}
```
---
## Batch Actions
Select multiple items and act on them:
```tsx
import { useState } from "react";
import { McpUseProvider, useWidget, useCallTool } from "mcp-use/react";
export default function BatchActions() {
const { props, isPending: isLoading } = useWidget();
const { callTool: archiveItems, isPending: isArchiving } = useCallTool("archive-items");
const { callTool: deleteItems, isPending: isDeleting } = useCallTool("delete-items");
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const processing = isArchiving || isDeleting;
const toggleSelection = (id: string) => {
const newSelection = new Set(selectedIds);
if (newSelection.has(id)) {
newSelection.delete(id);
} else {
newSelection.add(id);
}
setSelectedIds(newSelection);
};
const handleBatchArchive = () => {
archiveItems({ ids: Array.from(selectedIds) }, {
onSuccess: () => setSelectedIds(new Set()),
onError: () => alert("Failed to archive items"),
});
};
const handleBatchDelete = () => {
deleteItems({ ids: Array.from(selectedIds) }, {
onSuccess: () => setSelectedIds(new Set()),
onError: () => alert("Failed to delete items"),
});
};
if (isLoading) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div>
{selectedIds.size > 0 && (
<div style={{ padding: 12, backgroundColor: "#f5f5f5", marginBottom: 16 }}>
<span>{selectedIds.size} selected</span>
<button onClick={handleBatchArchive} disabled={processing} style={{ marginLeft: 8 }}>
Archive
</button>
<button onClick={handleBatchDelete} disabled={processing} style={{ marginLeft: 8 }}>
Delete
</button>
</div>
)}
{props.items.map(item => (
<div key={item.id} style={{ padding: 8, display: "flex", gap: 8 }}>
<input
type="checkbox"
checked={selectedIds.has(item.id)}
onChange={() => toggleSelection(item.id)}
/>
<span>{item.title}</span>
</div>
))}
</div>
</McpUseProvider>
);
}
```
**Corresponding tool:**
```typescript
server.tool(
{
name: "delete-items",
schema: z.object({
ids: z.array(z.string()).describe("Item IDs to delete")
})
},
async ({ ids }) => {
await Promise.all(ids.map(id => deleteItem(id)));
return text(`Deleted ${ids.length} items`);
}
);
```
---
## Handling Tool Errors
Use `isError` and `error` from the hook instead of manual error state:
```tsx
const { callTool, isError, error, isPending } = useCallTool("some-tool");
return (
<McpUseProvider autoSize>
<div>
{isError && (
<div style={{ padding: 12, backgroundColor: "#ffebee", color: "#c62828", marginBottom: 16 }}>
{error instanceof Error ? error.message : "Action failed"}
</div>
)}
<button onClick={() => callTool({ /* params */ })} disabled={isPending}>
Perform Action
</button>
</div>
</McpUseProvider>
);
```
---
## Per-Item Loading States
For per-item loading when sharing one hook instance, track the active ID separately:
```tsx
const { callToolAsync } = useCallTool("process-item");
const [loadingId, setLoadingId] = useState<string | null>(null);
const handleAction = async (id: string) => {
setLoadingId(id);
try {
await callToolAsync({ id });
} catch {
alert("Failed");
} finally {
setLoadingId(null);
}
};
return (
<McpUseProvider autoSize>
<div>
{props.items.map(item => (
<div key={item.id}>
<span>{item.title}</span>
<button
onClick={() => handleAction(item.id)}
disabled={loadingId === item.id}
>
{loadingId === item.id ? "Processing..." : "Process"}
</button>
</div>
))}
</div>
</McpUseProvider>
);
```
---
## Confirmation Dialogs
```tsx
const { callTool: deleteItem } = useCallTool("delete-item");
const handleDelete = (id: string, title: string) => {
if (!confirm(`Are you sure you want to delete "${title}"?`)) return;
deleteItem({ id }, {
onError: () => alert("Failed to delete"),
});
};
```
Or with a custom dialog:
```tsx
import { useState } from "react";
import { useCallTool } from "mcp-use/react";
const { callToolAsync } = useCallTool("delete-item");
const [confirmDialog, setConfirmDialog] = useState<{ id: string; title: string } | null>(null);
const handleDeleteClick = (id: string, title: string) => {
setConfirmDialog({ id, title });
};
const handleConfirmDelete = async () => {
if (!confirmDialog) return;
try {
await callToolAsync({ id: confirmDialog.id });
setConfirmDialog(null);
} catch {
alert("Failed to delete");
}
};
return (
<McpUseProvider autoSize>
<div>
{props.items.map(item => (
<div key={item.id}>
<span>{item.title}</span>
<button onClick={() => handleDeleteClick(item.id, item.title)}>Delete</button>
</div>
))}
{confirmDialog && (
<div style={{
position: "fixed", top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: "rgba(0,0,0,0.5)", display: "flex",
alignItems: "center", justifyContent: "center"
}}>
<div style={{ backgroundColor: "white", padding: 24, borderRadius: 8 }}>
<h3>Confirm Delete</h3>
<p>Delete "{confirmDialog.title}"?</p>
<button onClick={handleConfirmDelete}>Delete</button>
<button onClick={() => setConfirmDialog(null)}>Cancel</button>
</div>
</div>
)}
</div>
</McpUseProvider>
);
```
---
## Triggering LLM Responses: `sendFollowUpMessage`
`sendFollowUpMessage` from `useWidget()` sends a message to the conversation and triggers a new LLM turn — as if the user typed it. Use this to let widget interactions drive the conversation.
```tsx
import { McpUseProvider, useWidget } from "mcp-use/react";
export default function AnalysisWidget() {
const { props, isPending, sendFollowUpMessage } = useWidget();
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
<h2>Results for "{props.query}"</h2>
{props.items.map(item => (
<div key={item.id} style={{ padding: 8, borderBottom: "1px solid #ddd" }}>
<strong>{item.name}</strong> — ${item.price}
</div>
))}
<button
onClick={() => sendFollowUpMessage(
`Compare the top 3 results for "${props.query}" and recommend the best one.`
)}
style={{ marginTop: 16, padding: "8px 16px" }}
>
Ask AI to Compare
</button>
</div>
</McpUseProvider>
);
}
```
### Combining with `useCallTool`
A widget can use both — `useCallTool` for data mutations and `sendFollowUpMessage` for triggering LLM reasoning:
```tsx
import { useState } from "react";
import { McpUseProvider, useWidget, useCallTool } from "mcp-use/react";
export default function TodoWidget() {
const { props, isPending, state, setState, sendFollowUpMessage } = useWidget();
const { callTool: toggleTodo } = useCallTool("toggle-todo");
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
const tasks = state?.tasks || props.tasks || [];
const remaining = tasks.filter(t => !t.completed).length;
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
{tasks.map(t => (
<div key={t.id} style={{ display: "flex", gap: 8, padding: 8 }}>
<input
type="checkbox"
checked={t.completed}
onChange={() => toggleTodo({ id: t.id, completed: !t.completed })}
/>
{t.title}
</div>
))}
<button
onClick={() => sendFollowUpMessage(
`I have ${remaining} tasks left. Help me prioritize them.`
)}
style={{ marginTop: 16, padding: "8px 16px" }}
>
Ask AI to Prioritize
</button>
</div>
</McpUseProvider>
);
}
```
---
## Complete Example
```tsx
import { useState } from "react";
import { McpUseProvider, useWidget, useCallTool, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
const propsSchema = z.object({
todos: z.array(z.object({
id: z.string(),
title: z.string(),
completed: z.boolean()
}))
});
type Props = z.infer<typeof propsSchema>;
export const widgetMetadata: WidgetMetadata = {
description: "Interactive todo list",
props: propsSchema,
exposeAsTool: false
};
export default function InteractiveTodoList() {
const { props, isPending: isLoading } = useWidget<Props>();
const { callTool: createTodo, isPending: isCreating } = useCallTool("create-todo");
const { callTool: toggleTodo } = useCallTool("toggle-todo");
const { callTool: deleteTodo } = useCallTool("delete-todo");
const [newTodo, setNewTodo] = useState("");
const [deletingId, setDeletingId] = useState<string | null>(null);
if (isLoading) {
return <McpUseProvider autoSize><div>Loading todos...</div></McpUseProvider>;
}
const handleCreate = (e: React.FormEvent) => {
e.preventDefault();
if (!newTodo.trim()) return;
createTodo({ title: newTodo }, {
onSuccess: () => setNewTodo(""),
onError: () => alert("Failed to create todo"),
});
};
const handleToggle = (id: string, completed: boolean) => {
toggleTodo({ id, completed: !completed });
};
const handleDelete = (id: string) => {
setDeletingId(id);
deleteTodo({ id }, {
onError: () => alert("Failed to delete"),
onSettled: () => setDeletingId(null),
});
};
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
<h2>Todos ({props.todos.length})</h2>
<form onSubmit={handleCreate} style={{ marginBottom: 16 }}>
<input
type="text"
value={newTodo}
onChange={e => setNewTodo(e.target.value)}
placeholder="New todo..."
disabled={isCreating}
style={{ padding: 8, width: 300, marginRight: 8 }}
/>
<button type="submit" disabled={isCreating}>
{isCreating ? "Adding..." : "Add"}
</button>
</form>
<div>
{props.todos.map(todo => (
<div
key={todo.id}
style={{
display: "flex", alignItems: "center", gap: 8,
padding: 8, borderBottom: "1px solid #eee"
}}
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id, todo.completed)}
/>
<span style={{
flex: 1,
textDecoration: todo.completed ? "line-through" : "none",
color: todo.completed ? "#999" : "inherit"
}}>
{todo.title}
</span>
<button
onClick={() => handleDelete(todo.id)}
disabled={deletingId === todo.id}
style={{ color: "red" }}
>
{deletingId === todo.id ? "Deleting..." : "Delete"}
</button>
</div>
))}
</div>
{props.todos.length === 0 && (
<p style={{ color: "#999", textAlign: "center" }}>No todos yet</p>
)}
</div>
</McpUseProvider>
);
}
```
---
## Best Practices
1. **Use `useCallTool` for built-in state management** - No need for manual `isPending`/`error` state
2. **Declare hooks at the top level** - One hook per tool name; React rules apply
3. **Use `callTool` for fire-and-forget** - Handle success/error via callbacks
4. **Use `callToolAsync` for sequential operations** - When you need to await results or chain calls
5. **Use `isError`/`error` from the hook** - Instead of manual error state for single-tool widgets
6. **Optimistic updates** - Update local state before the call, revert on error
7. **Confirm destructive actions** - Use confirm() for deletes
8. **Use `sendFollowUpMessage` for LLM reasoning** - When you want the AI to analyze, compare, or respond based on widget context rather than mutating data
---
## Next Steps
- **Style widgets** → [ui-guidelines.md](ui-guidelines.md)
- **Advanced patterns** → [advanced.md](advanced.md)
- **See examples** → [../patterns/common-patterns.md](../patterns/common-patterns.md)
references/widgets/model-context.md
# Model Context Annotations
Keep the AI model aware of what the user is currently seeing in your widget — without requiring explicit tool calls or storing data in developer-managed state.
Both APIs feed into a shared global registry that serializes to an indented tree string, pushed to the host via `ui/update-model-context` (MCP Apps) or `setWidgetState` (ChatGPT Apps SDK). Updates are batched via `queueMicrotask`.
---
## When to Use
| Situation | Use |
|-----------|-----|
| Annotate what the user is *seeing* right now | `<ModelContext>` or `modelContext.set()` |
| Annotate in JSX, tied to component lifecycle | `<ModelContext content="...">` |
| Annotate from an event handler or outside React | `modelContext.set(key, value)` |
| Persist structured state the model reads on future turns | `setState` from `useWidget` |
Do **not** use `<ModelContext>` as a replacement for `setState`. They serve different purposes:
- `setState` = developer-managed state (cart, selections, filters). Explicit, you control the shape.
- `ModelContext` = declarative description of what the user *sees*. Set it and forget it.
---
## `<ModelContext>` Component
Declarative, lifecycle-tied, nesting-aware. Removes itself from the tree on unmount — no cleanup needed.
```tsx
import { ModelContext, useWidget, McpUseProvider } from "mcp-use/react";
export default function DashboardWidget() {
const { props, isPending } = useWidget<{ activeTab: string }>();
const [hovered, setHovered] = useState<string | null>(null);
if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
return (
<McpUseProvider autoSize>
{/* Root annotation — present for the full widget lifetime */}
<ModelContext content="User is viewing the analytics dashboard">
{/* Re-registers automatically when props.activeTab changes */}
<ModelContext content={`Active tab: ${props.activeTab}`} />
{/* Conditional — only present while something is hovered */}
{hovered && (
<ModelContext content={`Hovering over chart: ${hovered}`} />
)}
{/* Nesting — children become child nodes in the model's tree */}
<ModelContext content="Metrics panel">
<ModelContext content="Revenue chart visible" />
<ModelContext content="User count visible" />
</ModelContext>
<div>{/* widget UI */}</div>
</ModelContext>
</McpUseProvider>
);
}
```
The model receives an indented tree:
```
- User is viewing the analytics dashboard
- Active tab: overview
- Hovering over chart: Revenue Q4
- Metrics panel
- Revenue chart visible
- User count visible
```
### Nesting Rules
- `<ModelContext>` at the same JSX level → siblings (flat list at that depth)
- `<ModelContext>` inside another's `children` → child node in the tree
- Self-closing `<ModelContext content="..." />` → leaf node, no children needed
---
## `modelContext` Module-Level API
Imperative, works anywhere — event handlers, plain functions, `useEffect`, outside React entirely. Entries are always root-level (no parent).
```tsx
import { modelContext } from "mcp-use/react";
// From an event handler
function onProductSelect(product: Product) {
modelContext.set("selection", `User selected: ${product.name} ($${product.price})`);
}
function onDrawerClose() {
modelContext.remove("selection");
}
// From useEffect (lifecycle-aware)
useEffect(() => {
modelContext.set("page", `Viewing page ${currentPage} of ${totalPages}`);
return () => modelContext.remove("page");
}, [currentPage, totalPages]);
```
| Method | Description |
|--------|-------------|
| `modelContext.set(key, content)` | Register or update a named entry. Same key = overwrite. |
| `modelContext.remove(key)` | Remove an entry by key. |
| `modelContext.clear()` | Remove all entries (component-based and imperative). |
**Important:** Unlike `<ModelContext>`, `modelContext.set()` entries are NOT automatically cleaned up on component unmount. Always call `.remove(key)` in cleanup logic, or use `<ModelContext>` when you need automatic lifecycle management.
---
## How the Tree is Sent to the Model
The serialized string is sent under a reserved `__model_context` key:
- **MCP Apps**: `ui/update-model-context` with `structuredContent.__model_context`
- **ChatGPT Apps SDK**: `setWidgetState` with `__model_context` merged into the state object
`__model_context` is **filtered from the developer-facing `state`** returned by `useWidget` — it never appears in your code.
Calling `setState` from `useWidget` preserves the current `__model_context` value, so user state updates never wipe annotations.
---
## Common Patterns
### Tab-switching widget
```tsx
const [tab, setTab] = useState("overview");
<ModelContext content={`User is on the ${tab} tab`}>
<TabContent tab={tab} />
</ModelContext>
```
### Selected item (imperative)
```tsx
function onSelect(item: Item) {
modelContext.set("selected", `Selected: ${item.name}`);
}
function onDeselect() {
modelContext.remove("selected");
}
```
### Multi-level dashboard
```tsx
<ModelContext content="Dashboard — overview mode">
<ModelContext content={`Showing ${period} data`} />
<ModelContext content={`${visibleCharts.length} charts visible`} />
</ModelContext>
```
---
## Reference
- Full API reference: https://docs.mcp-use.com/typescript/server/widget-components/modelcontext
- Example server: `examples/server/ui/model-context/`
references/widgets/state.md
# Widget State
Widgets manage their own UI state (selections, filters, tabs, pagination). Never create tools to manage widget state.
**Key principle:** UI state lives in the widget. Server state lives in tools.
---
## Widget State vs Tool State
### Widget State (UI State)
**Managed by widget with `useState` or `setState`:**
- Current selected item
- Active tab
- Filter settings
- Sort order
- Pagination page
- Expanded/collapsed sections
- Form input values (before submission)
### Tool State (Server State)
**Managed by server, returned in tool response:**
- List of items
- User data
- API results
- Computation results
- Database queries
---
## Using React useState
Standard React state management works in widgets:
```tsx
import { useState } from "react";
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
export const widgetMetadata: WidgetMetadata = {
description: "Product list with filtering",
props: z.object({
products: z.array(z.object({
id: z.string(),
name: z.string(),
category: z.string(),
price: z.number()
}))
}),
exposeAsTool: false
};
export default function ProductList() {
const { props, isPending } = useWidget();
const [selectedCategory, setSelectedCategory] = useState<string>("all");
const [sortBy, setSortBy] = useState<"name" | "price">("name");
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
// Filter and sort based on state
const filtered = selectedCategory === "all"
? props.products
: props.products.filter(p => p.category === selectedCategory);
const sorted = [...filtered].sort((a, b) => {
if (sortBy === "name") return a.name.localeCompare(b.name);
return a.price - b.price;
});
const categories = ["all", ...new Set(props.products.map(p => p.category))];
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
{/* Category filter */}
<div style={{ marginBottom: 16 }}>
{categories.map(cat => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
style={{
padding: "8px 16px",
margin: "0 4px",
backgroundColor: selectedCategory === cat ? "#007bff" : "#f0f0f0",
color: selectedCategory === cat ? "white" : "black",
border: "none",
borderRadius: 4,
cursor: "pointer"
}}
>
{cat}
</button>
))}
</div>
{/* Sort controls */}
<div style={{ marginBottom: 16 }}>
<label>
Sort by:
<select value={sortBy} onChange={(e) => setSortBy(e.target.value as any)} style={{ marginLeft: 8 }}>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
</label>
</div>
{/* Product list */}
<div>
{sorted.map(product => (
<div key={product.id} style={{ padding: 12, border: "1px solid #ddd", marginBottom: 8 }}>
<h3>{product.name}</h3>
<p>Category: {product.category} | ${product.price}</p>
</div>
))}
</div>
</div>
</McpUseProvider>
);
}
```
**Pattern:**
- Tool provides data (products)
- Widget manages UI state (selectedCategory, sortBy)
- Widget renders filtered/sorted view
- No additional tool calls needed
---
## Using setState from useWidget
The `setState` method from `useWidget()` is an alternative to React's `useState` with automatic state persistence across widget interactions. See [basics.md](basics.md#usewidget-hook) for full `useWidget()` API reference.
**When to use `setState` vs `useState`:**
- Use `useState` for simple, ephemeral UI state (resets on widget unmount)
- Use `setState` from `useWidget` for state that persists across interactions
---
## Selection State
Track which item(s) are selected:
```tsx
import { useState } from "react";
export default function ItemSelector() {
const { props, isPending } = useWidget();
const [selectedId, setSelectedId] = useState<string | null>(null);
if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
return (
<McpUseProvider autoSize>
<div>
{props.items.map(item => (
<div
key={item.id}
onClick={() => setSelectedId(item.id)}
style={{
padding: 12,
border: `2px solid ${selectedId === item.id ? "#007bff" : "#ddd"}`,
marginBottom: 8,
cursor: "pointer"
}}
>
{item.name}
</div>
))}
</div>
</McpUseProvider>
);
}
```
### Multi-Select
```tsx
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const toggleSelection = (id: string) => {
const newSelection = new Set(selectedIds);
if (newSelection.has(id)) {
newSelection.delete(id);
} else {
newSelection.add(id);
}
setSelectedIds(newSelection);
};
return (
<McpUseProvider autoSize>
<div>
{props.items.map(item => (
<div
key={item.id}
onClick={() => toggleSelection(item.id)}
style={{
padding: 12,
backgroundColor: selectedIds.has(item.id) ? "#e3f2fd" : "white",
border: "1px solid #ddd"
}}
>
<input
type="checkbox"
checked={selectedIds.has(item.id)}
readOnly
/>
{item.name}
</div>
))}
</div>
</McpUseProvider>
);
```
---
## Tab State
Manage tabs without additional tool calls:
```tsx
const [activeTab, setActiveTab] = useState<"overview" | "details" | "history">("overview");
return (
<McpUseProvider autoSize>
<div>
{/* Tab buttons */}
<div style={{ borderBottom: "1px solid #ddd", marginBottom: 16 }}>
{["overview", "details", "history"].map(tab => (
<button
key={tab}
onClick={() => setActiveTab(tab as any)}
style={{
padding: "8px 16px",
border: "none",
borderBottom: activeTab === tab ? "2px solid #007bff" : "none",
background: "none",
cursor: "pointer"
}}
>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
</button>
))}
</div>
{/* Tab content */}
{activeTab === "overview" && <div>{/* Overview content */}</div>}
{activeTab === "details" && <div>{/* Details content */}</div>}
{activeTab === "history" && <div>{/* History content */}</div>}
</div>
</McpUseProvider>
);
```
---
## Pagination State
Paginate large lists client-side:
```tsx
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;
const totalPages = Math.ceil(props.items.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const currentItems = props.items.slice(startIndex, startIndex + itemsPerPage);
return (
<McpUseProvider autoSize>
<div>
{/* Items */}
<div>
{currentItems.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
{/* Pagination controls */}
<div style={{ marginTop: 16, display: "flex", gap: 8 }}>
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
>
Previous
</button>
<span>
Page {currentPage} of {totalPages}
</span>
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
>
Next
</button>
</div>
</div>
</McpUseProvider>
);
```
---
## Filter State
Complex filtering:
```tsx
interface Filters {
search: string;
category: string;
priceMin: number;
priceMax: number;
}
const [filters, setFilters] = useState<Filters>({
search: "",
category: "all",
priceMin: 0,
priceMax: 1000
});
const filteredItems = props.items.filter(item => {
if (filters.search && !item.name.toLowerCase().includes(filters.search.toLowerCase())) {
return false;
}
if (filters.category !== "all" && item.category !== filters.category) {
return false;
}
if (item.price < filters.priceMin || item.price > filters.priceMax) {
return false;
}
return true;
});
return (
<McpUseProvider autoSize>
<div>
{/* Filter controls */}
<div style={{ marginBottom: 16 }}>
<input
type="text"
placeholder="Search..."
value={filters.search}
onChange={e => setFilters({ ...filters, search: e.target.value })}
style={{ padding: 8, marginRight: 8 }}
/>
<select
value={filters.category}
onChange={e => setFilters({ ...filters, category: e.target.value })}
style={{ padding: 8, marginRight: 8 }}
>
<option value="all">All Categories</option>
{/* ... category options */}
</select>
<input
type="number"
value={filters.priceMin}
onChange={e => setFilters({ ...filters, priceMin: Number(e.target.value) })}
placeholder="Min price"
style={{ width: 80, padding: 8, marginRight: 8 }}
/>
<input
type="number"
value={filters.priceMax}
onChange={e => setFilters({ ...filters, priceMax: Number(e.target.value) })}
placeholder="Max price"
style={{ width: 80, padding: 8 }}
/>
</div>
{/* Filtered items */}
<div>
{filteredItems.map(item => (
<div key={item.id}>{item.name} - ${item.price}</div>
))}
</div>
</div>
</McpUseProvider>
);
```
---
## Expand/Collapse State
Accordion or expandable sections:
```tsx
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const toggleExpand = (id: string) => {
const newExpanded = new Set(expandedIds);
if (newExpanded.has(id)) {
newExpanded.delete(id);
} else {
newExpanded.add(id);
}
setExpandedIds(newExpanded);
};
return (
<McpUseProvider autoSize>
<div>
{props.items.map(item => (
<div key={item.id} style={{ marginBottom: 8 }}>
<div
onClick={() => toggleExpand(item.id)}
style={{
padding: 12,
backgroundColor: "#f5f5f5",
cursor: "pointer",
display: "flex",
justifyContent: "space-between"
}}
>
<span>{item.title}</span>
<span>{expandedIds.has(item.id) ? "▼" : "▶"}</span>
</div>
{expandedIds.has(item.id) && (
<div style={{ padding: 12, border: "1px solid #ddd" }}>
{item.details}
</div>
)}
</div>
))}
</div>
</McpUseProvider>
);
```
---
## Form State
Track form inputs before submission:
```tsx
const [formData, setFormData] = useState({
name: "",
email: "",
message: ""
});
const handleChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
return (
<McpUseProvider autoSize>
<form onSubmit={(e) => {
e.preventDefault();
// Handle submission (see interactivity.md)
}}>
<input
type="text"
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
placeholder="Name"
/>
<input
type="email"
value={formData.email}
onChange={(e) => handleChange("email", e.target.value)}
placeholder="Email"
/>
<textarea
value={formData.message}
onChange={(e) => handleChange("message", e.target.value)}
placeholder="Message"
/>
<button type="submit">Send</button>
</form>
</McpUseProvider>
);
```
---
## State Initialization
Initialize state based on props:
```tsx
const [selectedCategory, setSelectedCategory] = useState<string>("");
// Initialize when props load
useEffect(() => {
if (props.categories && props.categories.length > 0 && !selectedCategory) {
setSelectedCategory(props.categories[0]);
}
}, [props.categories, selectedCategory]);
```
**Note:** Lazy initialization like `useState(() => props.categories?.[0] || "all")` won't work here — on the first render `isPending` is `true` and `props` is `{}`, so the initializer always resolves to `"all"`. The `useEffect` pattern above is the correct approach for props that arrive asynchronously.
---
## Common Patterns
### Search + Filter + Sort
```tsx
const [search, setSearch] = useState("");
const [category, setCategory] = useState("all");
const [sortBy, setSortBy] = useState("name");
let filtered = props.items;
// Apply search
if (search) {
filtered = filtered.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase())
);
}
// Apply category filter
if (category !== "all") {
filtered = filtered.filter(item => item.category === category);
}
// Apply sort
filtered.sort((a, b) => {
if (sortBy === "name") return a.name.localeCompare(b.name);
if (sortBy === "price") return a.price - b.price;
return 0;
});
```
### Master-Detail View
```tsx
const [selectedId, setSelectedId] = useState<string | null>(null);
const selectedItem = selectedId
? props.items.find(item => item.id === selectedId)
: null;
return (
<div style={{ display: "flex", gap: 16 }}>
{/* Master list */}
<div style={{ flex: 1 }}>
{props.items.map(item => (
<div
key={item.id}
onClick={() => setSelectedId(item.id)}
style={{
padding: 12,
backgroundColor: selectedId === item.id ? "#e3f2fd" : "white"
}}
>
{item.name}
</div>
))}
</div>
{/* Detail panel */}
<div style={{ flex: 2 }}>
{selectedItem ? (
<div>
<h2>{selectedItem.name}</h2>
<p>{selectedItem.description}</p>
</div>
) : (
<p>Select an item to view details</p>
)}
</div>
</div>
);
```
---
## Anti-Patterns
### ❌ Don't Create Tools for UI State
```typescript
// ❌ Bad - Tool for UI state
server.tool(
{ name: "set-filter", schema: z.object({ category: z.string() }) },
async ({ category }) => {
// This is wrong! Filters should be widget state
}
);
// ✅ Good - Widget manages its own filters
const [filter, setFilter] = useState("all");
```
### ❌ Don't Call Tools for Filtering/Sorting
```typescript
// ❌ Bad - Using a tool call for client-side filtering
const { callTool: filterItems } = useCallTool("filter-items");
<button onClick={() => filterItems({ category: "electronics" })}>
Filter
</button>
// ✅ Good - Filter in widget
<button onClick={() => setCategory("electronics")}>
Filter
</button>
```
### ❌ Don't Store UI State in Props
```typescript
// ❌ Bad - Trying to mutate props
props.selectedId = "123"; // Error! Props are read-only
// ✅ Good - Use state
const [selectedId, setSelectedId] = useState<string | null>(null);
```
---
## Best Practices
1. **Keep state local** - Don't lift state unless necessary
2. **Initialize from props** - Use props as initial data, state for UI
3. **Use descriptive names** - `selectedCategory` not `filter`
4. **Reset state appropriately** - When props change, update dependent state
5. **Avoid unnecessary re-renders** - Use `useMemo` for expensive computations
---
## Next Steps
- **Add interactivity** → [interactivity.md](interactivity.md)
- **Style widgets** → [ui-guidelines.md](ui-guidelines.md)
- **Advanced patterns** → [advanced.md](advanced.md)
references/widgets/ui-guidelines.md
# Widget UI Guidelines
Build widgets that adapt to themes, look professional, and provide great user experience.
**Key topics:** Theme support, light/dark mode, responsive layouts, accessibility, CSS best practices
---
## Theme Support with useWidgetTheme()
Widgets should adapt to the user's theme (light/dark mode):
```tsx
import { McpUseProvider, useWidget, useWidgetTheme, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
export const widgetMetadata: WidgetMetadata = {
description: "Theme-aware widget",
props: z.object({
message: z.string()
}),
exposeAsTool: false
};
export default function ThemedWidget() {
const { props, isPending } = useWidget();
const theme = useWidgetTheme();
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div style={{
padding: 20,
backgroundColor: theme === "dark" ? "#1e1e1e" : "#ffffff",
color: theme === "dark" ? "#ffffff" : "#000000"
}}>
<p>{props.message}</p>
</div>
</McpUseProvider>
);
}
```
**useWidgetTheme() returns:** `"light"` or `"dark"`
---
## Theme-Aware Colors
Define color palettes for both themes:
```tsx
const theme = useWidgetTheme();
const colors = {
background: theme === "dark" ? "#1e1e1e" : "#ffffff",
text: theme === "dark" ? "#e0e0e0" : "#1a1a1a",
border: theme === "dark" ? "#404040" : "#e0e0e0",
primary: theme === "dark" ? "#4a9eff" : "#0066cc",
secondary: theme === "dark" ? "#6c757d" : "#6c757d",
hover: theme === "dark" ? "#2a2a2a" : "#f5f5f5",
error: theme === "dark" ? "#ff6b6b" : "#dc3545",
success: theme === "dark" ? "#51cf66" : "#28a745"
};
return (
<McpUseProvider autoSize>
<div style={{
backgroundColor: colors.background,
color: colors.text,
border: `1px solid ${colors.border}`
}}>
{/* Your content */}
</div>
</McpUseProvider>
);
```
Or extract to a hook:
```tsx
function useColors() {
const theme = useWidgetTheme();
return {
background: theme === "dark" ? "#1e1e1e" : "#ffffff",
text: theme === "dark" ? "#e0e0e0" : "#1a1a1a",
border: theme === "dark" ? "#404040" : "#e0e0e0",
primary: theme === "dark" ? "#4a9eff" : "#0066cc",
hover: theme === "dark" ? "#2a2a2a" : "#f5f5f5",
error: theme === "dark" ? "#ff6b6b" : "#dc3545"
};
}
export default function ThemedWidget() {
const colors = useColors();
// ... rest of component
}
```
---
## Responsive Layouts
### Grid Layout
```tsx
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
gap: 16,
padding: 20
}}>
{props.items.map(item => (
<div key={item.id} style={{
padding: 12,
border: `1px solid ${colors.border}`,
borderRadius: 8
}}>
{item.name}
</div>
))}
</div>
```
### Flexbox Layout
```tsx
<div style={{
display: "flex",
gap: 16,
padding: 20,
flexWrap: "wrap"
}}>
{props.items.map(item => (
<div key={item.id} style={{
flex: "1 1 200px",
padding: 12,
border: `1px solid ${colors.border}`
}}>
{item.name}
</div>
))}
</div>
```
### Two-Column Layout
```tsx
<div style={{
display: "flex",
gap: 16,
padding: 20
}}>
{/* Sidebar */}
<div style={{ flex: "0 0 250px" }}>
{/* Navigation or filters */}
</div>
{/* Main content */}
<div style={{ flex: 1 }}>
{/* Primary content */}
</div>
</div>
```
---
## Button Styles
Theme-aware buttons:
```tsx
const theme = useWidgetTheme();
const buttonStyle: React.CSSProperties = {
padding: "8px 16px",
border: "none",
borderRadius: 4,
cursor: "pointer",
fontSize: 14,
fontWeight: 500,
backgroundColor: theme === "dark" ? "#4a9eff" : "#0066cc",
color: "#ffffff"
};
const secondaryButtonStyle: React.CSSProperties = {
...buttonStyle,
backgroundColor: "transparent",
border: `1px solid ${theme === "dark" ? "#404040" : "#e0e0e0"}`,
color: theme === "dark" ? "#e0e0e0" : "#1a1a1a"
};
return (
<McpUseProvider autoSize>
<div>
<button style={buttonStyle}>Primary Action</button>
<button style={secondaryButtonStyle}>Secondary</button>
</div>
</McpUseProvider>
);
```
### Button States
```tsx
const [hovered, setHovered] = useState(false);
<button
style={{
padding: "8px 16px",
backgroundColor: hovered ? (theme === "dark" ? "#5aa8ff" : "#0052a3") : (theme === "dark" ? "#4a9eff" : "#0066cc"),
color: "#ffffff",
border: "none",
borderRadius: 4,
cursor: "pointer",
transition: "background-color 0.2s"
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
Hover Me
</button>
```
---
## Card Components
```tsx
const theme = useWidgetTheme();
const cardStyle: React.CSSProperties = {
padding: 16,
border: `1px solid ${theme === "dark" ? "#404040" : "#e0e0e0"}`,
borderRadius: 8,
backgroundColor: theme === "dark" ? "#1e1e1e" : "#ffffff",
color: theme === "dark" ? "#e0e0e0" : "#1a1a1a"
};
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
{props.items.map(item => (
<div key={item.id} style={{
...cardStyle,
marginBottom: 12
}}>
<h3 style={{ margin: "0 0 8px 0" }}>{item.title}</h3>
<p style={{ margin: 0, color: theme === "dark" ? "#b0b0b0" : "#666" }}>
{item.description}
</p>
</div>
))}
</div>
</McpUseProvider>
);
```
---
## Typography
```tsx
const theme = useWidgetTheme();
<div style={{ padding: 20 }}>
{/* Heading */}
<h1 style={{
fontSize: 24,
fontWeight: 600,
margin: "0 0 16px 0",
color: theme === "dark" ? "#ffffff" : "#1a1a1a"
}}>
Title
</h1>
{/* Subheading */}
<h2 style={{
fontSize: 18,
fontWeight: 500,
margin: "0 0 12px 0",
color: theme === "dark" ? "#e0e0e0" : "#333"
}}>
Subtitle
</h2>
{/* Body text */}
<p style={{
fontSize: 14,
lineHeight: 1.5,
margin: "0 0 12px 0",
color: theme === "dark" ? "#b0b0b0" : "#666"
}}>
Body content here
</p>
{/* Small text */}
<span style={{
fontSize: 12,
color: theme === "dark" ? "#808080" : "#999"
}}>
Small text or metadata
</span>
</div>
```
---
## Form Inputs
```tsx
const theme = useWidgetTheme();
const inputStyle: React.CSSProperties = {
padding: 8,
fontSize: 14,
border: `1px solid ${theme === "dark" ? "#404040" : "#d0d0d0"}`,
borderRadius: 4,
backgroundColor: theme === "dark" ? "#2a2a2a" : "#ffffff",
color: theme === "dark" ? "#e0e0e0" : "#1a1a1a",
outline: "none"
};
<form style={{ padding: 20 }}>
<label style={{
display: "block",
marginBottom: 4,
fontSize: 14,
fontWeight: 500,
color: theme === "dark" ? "#e0e0e0" : "#333"
}}>
Name
</label>
<input
type="text"
style={inputStyle}
placeholder="Enter name..."
/>
<label style={{ display: "block", marginTop: 12, marginBottom: 4 }}>
Description
</label>
<textarea
style={{
...inputStyle,
width: "100%",
minHeight: 80,
resize: "vertical"
}}
placeholder="Enter description..."
/>
</form>
```
---
## Lists
```tsx
const theme = useWidgetTheme();
<ul style={{
listStyle: "none",
padding: 0,
margin: 0
}}>
{props.items.map(item => (
<li
key={item.id}
style={{
padding: 12,
borderBottom: `1px solid ${theme === "dark" ? "#2a2a2a" : "#f0f0f0"}`,
cursor: "pointer",
transition: "background-color 0.15s"
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = theme === "dark" ? "#2a2a2a" : "#f5f5f5";
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = "transparent";
}}
>
{item.name}
</li>
))}
</ul>
```
---
## Badges and Tags
```tsx
const theme = useWidgetTheme();
const badgeStyle: React.CSSProperties = {
display: "inline-block",
padding: "4px 8px",
fontSize: 12,
fontWeight: 500,
borderRadius: 12,
backgroundColor: theme === "dark" ? "#2a4a6a" : "#e3f2fd",
color: theme === "dark" ? "#4a9eff" : "#0066cc"
};
<div>
<span style={badgeStyle}>New</span>
<span style={{ ...badgeStyle, marginLeft: 8 }}>Featured</span>
</div>
```
---
## Loading States
```tsx
const theme = useWidgetTheme();
if (isPending) {
return (
<McpUseProvider autoSize>
<div style={{
padding: 40,
textAlign: "center",
color: theme === "dark" ? "#808080" : "#999"
}}>
<div style={{
width: 40,
height: 40,
border: `4px solid ${theme === "dark" ? "#404040" : "#e0e0e0"}`,
borderTop: `4px solid ${theme === "dark" ? "#4a9eff" : "#0066cc"}`,
borderRadius: "50%",
margin: "0 auto 16px",
animation: "spin 1s linear infinite"
}} />
<p>Loading...</p>
</div>
</McpUseProvider>
);
}
```
Add spin animation:
```tsx
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
```
---
## Empty States
```tsx
const theme = useWidgetTheme();
{props.items.length === 0 && (
<div style={{
padding: 40,
textAlign: "center",
color: theme === "dark" ? "#808080" : "#999"
}}>
<div style={{
fontSize: 48,
marginBottom: 16,
opacity: 0.5
}}>
📭
</div>
<h3 style={{
fontSize: 18,
fontWeight: 500,
margin: "0 0 8px 0",
color: theme === "dark" ? "#b0b0b0" : "#666"
}}>
No items yet
</h3>
<p style={{
fontSize: 14,
margin: 0,
color: theme === "dark" ? "#808080" : "#999"
}}>
Get started by creating your first item
</p>
</div>
)}
```
---
## Error States
```tsx
const theme = useWidgetTheme();
{error && (
<div style={{
padding: 12,
marginBottom: 16,
backgroundColor: theme === "dark" ? "#3d1f1f" : "#ffebee",
color: theme === "dark" ? "#ff6b6b" : "#c62828",
border: `1px solid ${theme === "dark" ? "#6b2a2a" : "#ffcdd2"}`,
borderRadius: 4
}}>
<strong>Error:</strong> {error}
</div>
)}
```
---
## Icons
Use Unicode emojis or SVG icons:
```tsx
// Emojis
<span style={{ fontSize: 24, marginRight: 8 }}>⚙️</span>
<span style={{ fontSize: 20 }}>✓</span>
<span>❌</span>
// SVG icon
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z"/>
</svg>
```
---
## Spacing Guidelines
```tsx
// Consistent spacing units
const spacing = {
xs: 4,
sm: 8,
md: 12,
lg: 16,
xl: 20,
xxl: 24
};
<div style={{
padding: spacing.lg,
gap: spacing.md
}}>
{/* Content */}
</div>
```
---
## Accessibility
### Labels for Inputs
```tsx
<label htmlFor="email-input">Email</label>
<input id="email-input" type="email" />
```
### Alt Text for Images
```tsx
<img src={item.image} alt={item.name} />
```
### Button Labels
```tsx
<button aria-label="Delete item">🗑️</button>
```
### Keyboard Navigation
```tsx
<div
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleClick();
}
}}
>
Clickable item
</div>
```
---
## Auto-Size Best Practices
`<McpUseProvider autoSize>` automatically resizes iframe to content.
**Tips:**
- Use `autoSize` for dynamic content
- Avoid fixed heights unless necessary
- Widget resizes when content changes
- Test with varying content sizes
```tsx
// ✅ Good - autoSize handles height
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
{/* Dynamic content */}
</div>
</McpUseProvider>
// ❌ Bad - Fixed height defeats autoSize
<McpUseProvider autoSize>
<div style={{ height: 400, overflow: "auto" }}>
{/* Content */}
</div>
</McpUseProvider>
```
---
## Complete Themed Widget
```tsx
import { useState } from "react";
import { McpUseProvider, useWidget, useWidgetTheme, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
function useColors() {
const theme = useWidgetTheme();
return {
background: theme === "dark" ? "#1e1e1e" : "#ffffff",
text: theme === "dark" ? "#e0e0e0" : "#1a1a1a",
textSecondary: theme === "dark" ? "#b0b0b0" : "#666",
border: theme === "dark" ? "#404040" : "#e0e0e0",
hover: theme === "dark" ? "#2a2a2a" : "#f5f5f5",
primary: theme === "dark" ? "#4a9eff" : "#0066cc"
};
}
export const widgetMetadata: WidgetMetadata = {
description: "Fully themed product list",
props: z.object({
products: z.array(z.object({
id: z.string(),
name: z.string(),
price: z.number(),
category: z.string()
}))
}),
exposeAsTool: false
};
export default function ThemedProductList() {
const { props, isPending } = useWidget();
const colors = useColors();
const [selectedCategory, setSelectedCategory] = useState("all");
if (isPending) {
return (
<McpUseProvider autoSize>
<div style={{
padding: 40,
textAlign: "center",
color: colors.textSecondary
}}>
Loading...
</div>
</McpUseProvider>
);
}
const categories = ["all", ...new Set(props.products.map(p => p.category))];
const filtered = selectedCategory === "all"
? props.products
: props.products.filter(p => p.category === selectedCategory);
return (
<McpUseProvider autoSize>
<div style={{
padding: 20,
backgroundColor: colors.background,
color: colors.text
}}>
<h2 style={{ margin: "0 0 16px 0" }}>Products</h2>
{/* Category filters */}
<div style={{ marginBottom: 16, display: "flex", gap: 8 }}>
{categories.map(cat => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
style={{
padding: "8px 16px",
borderRadius: 4,
cursor: "pointer",
backgroundColor: selectedCategory === cat ? colors.primary : "transparent",
color: selectedCategory === cat ? "#fff" : colors.text,
border: `1px solid ${selectedCategory === cat ? colors.primary : colors.border}`
}}
>
{cat}
</button>
))}
</div>
{/* Product grid */}
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
gap: 12
}}>
{filtered.map(product => (
<div
key={product.id}
style={{
padding: 12,
border: `1px solid ${colors.border}`,
borderRadius: 8,
backgroundColor: colors.background
}}
>
<h3 style={{ margin: "0 0 4px 0", fontSize: 16 }}>
{product.name}
</h3>
<p style={{ margin: "0 0 8px 0", fontSize: 12, color: colors.textSecondary }}>
{product.category}
</p>
<p style={{ margin: 0, fontSize: 18, fontWeight: "bold", color: colors.primary }}>
${product.price}
</p>
</div>
))}
</div>
{filtered.length === 0 && (
<div style={{
padding: 40,
textAlign: "center",
color: colors.textSecondary
}}>
No products in this category
</div>
)}
</div>
</McpUseProvider>
);
}
```
---
## Next Steps
- **Advanced patterns** → [advanced.md](advanced.md)
- **See examples** → [../patterns/common-patterns.md](../patterns/common-patterns.md)