mcp-builder
DEPRECATED: This skill has been replaced by `mcp-app-builder`. Check if `mcp-app-builder` is available in the skills folder. If not, install it: `npx skills install mcp-use/mcp-use --skill mcp-app-builder` Use `mcp-app-builder` instead of this skill. Build Model Context Protocol (MCP) servers with mcp-use framework. Use when creating MCP servers, defining tools/resources/prompts, working with mcp-use, bootstrapping MCP projects, or when user mentions MCP development, tools, resources, or prompts.
92.38,965 installsmcp-use/mcp-use
Add to your agent
curl "https://api.skyll.app/skill/mcp-builder"SKILL.md
# MCP Server Builder
Build production-ready MCP servers with tools, resources, prompts, and interactive widgets using mcp-use.
## Before You Code
Decompose user requests into tools, widgets, and resources. Decide what needs UI vs text.
Read [design-and-architecture.md](references/design-and-architecture.md): when planning what to build, deciding tool vs widget, or designing UX flows.
## Implementation
- **Tools, resources, prompts** → [tools-and-resources.md](references/tools-and-resources.md): when writing server-side `server.tool()`, `server.resource()`, `server.prompt()` code
- **Visual widgets (React TSX)** → [widgets.md](references/widgets.md): when creating interactive UI widgets in `resources/` folder
- **Response helper API** → [response-helpers.md](references/response-helpers.md): when choosing how to format tool/resource return values
- **URI template patterns** → [resource-templates.md](references/resource-templates.md): when defining parameterized resources
- **Server proxying & composition** → [proxy.md](references/proxy.md): when composing multiple MCP servers into a unified aggregator
## Quick Reference
```typescript
import { MCPServer, text, object, markdown, html, image, widget, error } from "mcp-use/server";
import { z } from "zod";
const server = new MCPServer({ name: "my-server", version: "1.0.0" });
// Tool
server.tool(
{ name: "my-tool", description: "...", schema: z.object({ param: z.string().describe("...") }) },
async ({ param }) => text("result")
);
// Resource
server.resource(
{ uri: "config://settings", name: "Settings", mimeType: "application/json" },
async () => object({ key: "value" })
);
// Prompt
server.prompt(
{ name: "my-prompt", description: "...", schema: z.object({ topic: z.string() }) },
async ({ topic }) => text(`Write about ${topic}`)
);
server.listen();
```
**Response helpers:** `text()`, `object()`, `markdown()`, `html()`, `image()`, `audio()`, `binary()`, `error()`, `mix()`, `widget()`
**Server methods:**
- `server.tool()` - Define executable tool
- `server.resource()` - Define static/dynamic resource
- `server.resourceTemplate()` - Define parameterized resource
- `server.prompt()` - Define prompt template
- `server.proxy()` - Compose/Proxy multiple MCP servers
- `server.uiResource()` - Define widget resource
- `server.listen()` - Start serverReferences (6)
📎 design-and-architecture.md
# Design and Architecture
Before writing code, think about what the user actually wants and how to decompose it into MCP primitives.
## Concepts
- **Tool**: A backend action the AI model can call. Takes input, returns data. Use `server.tool()`.
- **Widget tool**: A tool that returns visual UI. Same `server.tool()` but with a `widget` config and a React component in `resources/`.
- **Resource**: Read-only data the client can fetch. Use `server.resource()` or `server.resourceTemplate()`.
- **Prompt**: A reusable message template. Use `server.prompt()`.
## Step 1: Identify What to Build
Extract the core actions from the user's request. Stick to what they asked -- don't invent extra features.
**Examples:**
| User says | Core actions |
|---|---|
| "weather app" | Get current weather, get forecast |
| "todo list" | Add todo, list todos, complete todo, delete todo |
| "recipe finder" | Search recipes, get recipe details |
| "translator" | Translate text, detect language |
| "stock tracker" | Get stock price, compare stocks |
| "quiz app" | Generate quiz, check answer |
## Step 2: Does It Need a Widget?
For each action, decide if visual UI would meaningfully improve the experience.
**YES → widget** if:
- Browsing or comparing multiple items (search results, product cards)
- Visual data improves understanding (charts, maps, images, dashboards)
- Interactive selection is easier visually (seat picker, calendar, color picker)
**NO → tool only** if:
- Output is simple text (translation, calculation, status check)
- Input is naturally conversational (dates, amounts, descriptions)
- No visual element would meaningfully help
**When in doubt, use a widget** -- it makes the experience better.
## Step 3: Design the API
### Naming
Tools and widgets start with a verb: `get-weather`, `search-recipes`, `add-todo`, `translate-text`.
### One tool = one focused capability
Don't create one massive tool that does everything. Break it into focused actions:
❌ `manage-todos` (too broad)
✅ `add-todo`, `list-todos`, `complete-todo`, `delete-todo`
### One widget per flow
Different flows can have separate widgets. Don't split one flow into multiple widgets.
❌ `search-recipes` widget + `view-recipe` widget (same flow → merge)
✅ `search-recipes` widget (handles both list and detail views) + `meal-planner` widget (different flow)
### Don't lazy-load
Tool calls are expensive. Return all needed data upfront.
❌ `search-recipes` widget + `get-recipe-details` tool (lazy-loading details)
✅ `search-recipes` widget returns full recipe data including details
### Widget handles its own state
Selections, filters, and UI state live in the widget -- not as separate tools.
❌ `select-recipe` tool, `set-filter` tool (these are widget state)
✅ Widget manages selections and filters internally via `useState` or `setState`
### `exposeAsTool` defaults to `false`
Widgets are not auto-registered as tools by default. When you create a custom tool with `widget: { name: "my-widget" }`, omitting `exposeAsTool` in the widget file is correct — the custom tool handles making the widget callable:
```typescript
// resources/my-widget.tsx
export const widgetMetadata: WidgetMetadata = {
description: "...",
props: z.object({ ... }),
// exposeAsTool defaults to false — custom tool definition handles registration
};
```
## Common App Patterns
### Weather App
```
Widget tool: get-weather
- Input: { city }
- Widget: temperature, conditions, icon, humidity
- Output to model: text summary
Tool: get-forecast
- Input: { city, days }
- Returns: text or object with daily forecast
```
### Todo List
```
Widget tool: list-todos
- Widget: interactive checklist with complete/delete buttons
- Widget calls add-todo, complete-todo, delete-todo via callTool
Tool: add-todo { title, priority? }
Tool: complete-todo { id }
Tool: delete-todo { id }
```
### Recipe Finder
```
Widget tool: search-recipes
- Input: { query, cuisine? }
- Widget: recipe cards with images, ingredients, instructions
- Output to model: text summary of results
Resource: recipe://favorites (user's saved recipes)
```
### Translator
```
Tool: translate-text
- Input: { text, targetLanguage, sourceLanguage? }
- Returns: text (translated result)
Tool: detect-language
- Input: { text }
- Returns: object({ language, confidence })
```
### Stock Tracker
```
Widget tool: get-stock
- Input: { symbol }
- Widget: price chart, key metrics, news
- Output to model: price and change summary
Tool: compare-stocks
- Input: { symbols[] }
- Returns: object with comparison data
```
## Mock Data Strategy
When the user doesn't specify a real API, use realistic mock data:
```typescript
// Mock data - replace with real API
const mockWeather: Record<string, { temp: number; conditions: string; humidity: number }> = {
"New York": { temp: 22, conditions: "Partly Cloudy", humidity: 65 },
"London": { temp: 15, conditions: "Overcast", humidity: 80 },
"Tokyo": { temp: 28, conditions: "Sunny", humidity: 55 },
"Paris": { temp: 18, conditions: "Light Rain", humidity: 75 },
};
function getWeather(city: string) {
// Add slight randomization to feel dynamic
const base = mockWeather[city] || { temp: 20, conditions: "Clear", humidity: 60 };
return {
...base,
temp: base.temp + Math.round((Math.random() - 0.5) * 4),
humidity: base.humidity + Math.round((Math.random() - 0.5) * 10),
};
}
```
**Guidelines:**
- Use real names (cities, recipes, products) -- not "Example 1"
- Add slight randomization so it feels dynamic
- Structure like a real API would return
- Comment with `// Mock data - replace with real API`
## Iterative Development
When the user asks to modify or extend existing code:
1. **Read** the current `index.ts` to see what exists
2. **Preserve** all existing tools, resources, and widgets
3. **Add** new functionality alongside existing code
4. **Update** existing widget files rather than creating duplicates
📎 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.📎 resource-templates.md
# Resource Templates Reference
Parameterized resources using URI templates.
## Basic Resource Template
```typescript
server.resourceTemplate(
{
uriTemplate: "user://{userId}/profile",
name: "User Profile",
description: "Get user profile by ID",
mimeType: "application/json"
},
async ({ userId }) => {
const user = await fetchUser(userId);
return object(user);
}
);
```
## URI Template Patterns
### Single Parameter
```typescript
// user://123/profile
server.resourceTemplate({
uriTemplate: "user://{userId}/profile",
// ...
});
```
### Multiple Parameters
```typescript
// org://acme/team/engineering
server.resourceTemplate({
uriTemplate: "org://{orgId}/team/{teamId}",
name: "Team Details",
// ...
}, async ({ orgId, teamId }) => {
return object(await fetchTeam(orgId, teamId));
});
```
### Optional Parameters
```typescript
// file://documents or file://documents?format=json
server.resourceTemplate({
uriTemplate: "file://{path}",
name: "File Content",
// ...
}, async ({ path }, { searchParams }) => {
const format = searchParams?.get('format') || 'text';
const content = await readFile(path);
return format === 'json' ? object(content) : text(content);
});
```
## URI Scheme Conventions
| Scheme | Use Case | Example |
|--------|----------|---------|
| `config://` | Configuration data | `config://settings`, `config://env` |
| `user://` | User-related data | `user://{id}/profile` |
| `docs://` | Documentation | `docs://api`, `docs://guide` |
| `stats://` | Statistics/metrics | `stats://current`, `stats://daily` |
| `file://` | File content | `file://{path}` |
| `db://` | Database records | `db://users/{id}` |
| `api://` | API endpoints | `api://weather/{city}` |
| `ui://` | UI widgets | `ui://widget/{name}.html` |
## Complete Example
```typescript
import { MCPServer, object, text, markdown } from "mcp-use/server";
const server = new MCPServer({
name: "data-server",
version: "1.0.0"
});
// Static resource
server.resource(
{
uri: "config://database",
name: "Database Config",
mimeType: "application/json"
},
async () => object({ host: "localhost", port: 5432 })
);
// Parameterized resource
server.resourceTemplate(
{
uriTemplate: "user://{userId}",
name: "User Data",
description: "Fetch user by ID",
mimeType: "application/json"
},
async ({ userId }) => {
const user = await db.users.findById(userId);
if (!user) throw new Error(`User ${userId} not found`);
return object(user);
}
);
// Nested template
server.resourceTemplate(
{
uriTemplate: "user://{userId}/posts/{postId}",
name: "User Post",
description: "Fetch specific post by user",
mimeType: "application/json"
},
async ({ userId, postId }) => {
const post = await db.posts.findOne({ userId, id: postId });
return object(post);
}
);
// Documentation resource
server.resource(
{
uri: "docs://api",
name: "API Documentation",
mimeType: "text/markdown"
},
async () => markdown(`
# API Documentation
## Endpoints
- GET /users - List all users
- GET /users/:id - Get user by ID
`)
);
```
📎 response-helpers.md
# Response Helpers Reference
Complete reference for mcp-use response helpers.
All helpers are imported from `mcp-use/server`:
```typescript
import { text, object, markdown, html, image, audio, binary, error, mix, widget, resource } from "mcp-use/server";
```
## Table of Contents
- [Text Responses](#text-responses)
- [JSON Responses](#json-responses)
- [Markdown Responses](#markdown-responses)
- [HTML Responses](#html-responses)
- [Error Responses](#error-responses)
- [Binary Responses](#binary-responses)
- [Embedded Resources](#embedded-resources)
- [Mixed Responses](#mixed-responses)
- [Widget Responses](#widget-responses)
- [Autocompletion](#autocompletion)
## Text Responses
```typescript
import { text } from 'mcp-use/server';
// Simple text
return text("Hello, world!");
// Multi-line text
return text(`
Analysis complete.
Found 15 items.
Processing took 2.3 seconds.
`);
```
## JSON Responses
```typescript
import { object } from 'mcp-use/server';
// Object response
return object({
status: "success",
count: 42,
items: ["a", "b", "c"]
});
// Nested objects
return object({
user: { id: 1, name: "John" },
metadata: { created: new Date().toISOString() }
});
```
## Markdown Responses
```typescript
import { markdown } from 'mcp-use/server';
return markdown(`
# Report Title
## Summary
- Item 1: **complete**
- Item 2: *in progress*
## Details
\`\`\`json
{ "score": 95 }
\`\`\`
`);
```
## HTML Responses
```typescript
import { html } from 'mcp-use/server';
return html(`
<div style="padding: 20px;">
<h1>Welcome</h1>
<p>This is <strong>HTML</strong> content.</p>
</div>
`);
```
## Error Responses
```typescript
import { error } from 'mcp-use/server';
// Simple error
return error("Something went wrong");
// With context
return error(`User ${userId} not found`);
// In try/catch
try {
const data = await fetchData(id);
return object(data);
} catch (err) {
return error(`Failed to fetch: ${err instanceof Error ? err.message : "Unknown error"}`);
}
```
The `error()` helper sets `isError: true` on the response, signaling to the model that the operation failed.
## Binary Responses
### Images
```typescript
import { image } from 'mcp-use/server';
// From base64 data
return image(base64Data, "image/png");
// From buffer
return image(imageBuffer, "image/jpeg");
// From file path (async)
return await image("/path/to/image.png");
```
### Audio
```typescript
import { audio } from 'mcp-use/server';
// From base64 data
return audio(base64Data, "audio/wav");
// From buffer
return audio(audioBuffer, "audio/mp3");
// From file path (async)
return await audio("/path/to/audio.mp3");
```
### Generic Binary
```typescript
import { binary } from 'mcp-use/server';
// PDF
return binary(pdfBuffer, "application/pdf");
// ZIP
return binary(zipBuffer, "application/zip");
// Any binary data
return binary(data, "application/octet-stream");
```
## Embedded Resources
Embed a resource reference inside a tool response:
```typescript
import { resource, text } from 'mcp-use/server';
// 2-arg: uri + helper result
return resource("report://analysis-123", text("Full report content here..."));
// 3-arg: uri + mimeType + raw text
return resource("data://export", "application/json", '{"items": [1, 2, 3]}');
```
## Mixed Responses
Combine multiple content types:
```typescript
import { mix, text, object, markdown, resource } from 'mcp-use/server';
// Multiple content items
return mix(
text("Analysis complete:"),
object({ score: 95, status: "pass" }),
markdown("## Recommendations\n- Optimize query\n- Add index")
);
// With embedded resource
return mix(
text("Report generated:"),
resource("report://analysis-123", text("Full report content here...")),
object({ id: "analysis-123", timestamp: Date.now() })
);
```
## Widget Responses
Return interactive widgets from tools:
```typescript
import { widget, text } from 'mcp-use/server';
server.tool(
{
name: "show-data",
schema: z.object({ query: z.string() }),
widget: {
name: "data-display", // Widget in resources/
invoking: "Loading...",
invoked: "Data loaded"
}
},
async ({ query }) => {
const data = await fetchData(query);
return widget({
// Props passed to widget (hidden from model)
props: {
items: data.items,
query: query,
total: data.total
},
// Output shown to model
output: text(`Found ${data.total} results for "${query}"`),
// Optional message
message: `Displaying ${data.total} results`
});
}
);
```
### Widget Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `props` | object | Data passed to widget component |
| `output` | ResponseHelper | Content shown to the AI model |
| `message` | string | Optional text message |
### Widget Tool Configuration
| Field | Description |
|-------|-------------|
| `widget.name` | Name of widget file in `resources/` |
| `widget.invoking` | Text shown while tool executes |
| `widget.invoked` | Text shown after completion |
## Autocompletion
Add autocompletion to prompt arguments and resource template parameters:
```typescript
import { completable } from 'mcp-use/server';
// Static list
server.prompt(
{
name: "code-review",
schema: z.object({
language: completable(z.string(), ["python", "typescript", "go", "rust", "java"]),
}),
},
async ({ language }) => text(`Review this ${language} code.`)
);
// Dynamic callback
server.prompt(
{
name: "get-user",
schema: z.object({
username: completable(z.string(), async (value) => {
const users = await searchUsers(value);
return users.map(u => u.name);
}),
}),
},
async ({ username }) => text(`Get info for ${username}`)
);
```
## Quick Reference Table
| Helper | Return Type | Use When |
|--------|------------|----------|
| `text(str)` | Plain text | Simple text responses |
| `object(data)` | JSON | Structured data |
| `markdown(str)` | Markdown | Formatted text with headings, lists, code |
| `html(str)` | HTML | Rich HTML content |
| `image(data, mime?)` | Image | Base64 or file path images |
| `audio(data, mime?)` | Audio | Base64 or file path audio |
| `binary(data, mime)` | Binary | PDFs, ZIPs, other binary |
| `error(msg)` | Error | Operation failed |
| `resource(uri, content)` | Resource | Embed a resource reference |
| `mix(...results)` | Combined | Multiple content types in one response |
| `widget({ props, output })` | Widget | Interactive UI with data for the widget component |
📎 tools-and-resources.md
# Tools, Resources, and Prompts
Server-side implementation patterns for `server.tool()`, `server.resource()`, and `server.prompt()`.
## Tools
Tools are actions the AI model can call.
### Basic Tool
```typescript
import { MCPServer, text, object, error } 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: "translate-text",
description: "Translate text between languages",
schema: z.object({
text: z.string().describe("Text to translate"),
targetLanguage: z.string().describe("Target language (e.g., 'Spanish', 'French')"),
sourceLanguage: z.string().optional().describe("Source language (auto-detected if omitted)"),
}),
},
async ({ text: inputText, targetLanguage, sourceLanguage }) => {
// Your logic here
const translated = await translateAPI(inputText, targetLanguage, sourceLanguage);
return text(`${translated}`);
}
);
```
### Tool with Widget
When a tool returns visual UI, add `widget` config and return `widget()`:
```typescript
import { widget, text } from "mcp-use/server";
server.tool(
{
name: "get-weather",
description: "Get current weather for a city",
schema: z.object({
city: z.string().describe("City name"),
}),
widget: {
name: "weather-display", // Must match resources/weather-display.tsx
invoking: "Fetching weather...",
invoked: "Weather loaded",
},
},
async ({ city }) => {
const data = getWeather(city);
return widget({
props: { city, temp: data.temp, conditions: data.conditions }, // Sent to widget UI
output: text(`Weather in ${city}: ${data.temp}°C, ${data.conditions}`), // Model sees this
});
}
);
```
### Tool Annotations
Declare the nature of your tool:
```typescript
server.tool(
{
name: "delete-item",
description: "Delete an item permanently",
schema: z.object({ id: z.string().describe("Item ID") }),
annotations: {
destructiveHint: true, // Deletes or overwrites data
readOnlyHint: false, // Has side effects
openWorldHint: false, // Stays within user's account
},
},
async ({ id }) => {
await deleteItem(id);
return text(`Item ${id} deleted.`);
}
);
```
### Tool Context
The second parameter to tool callbacks provides advanced capabilities:
```typescript
server.tool(
{ name: "process-data", schema: z.object({ data: z.string() }) },
async ({ data }, ctx) => {
// Progress reporting
await ctx.reportProgress?.(0, 100, "Starting...");
// Structured logging
await ctx.log("info", `Processing ${data.length} chars`);
// Check client capabilities
if (ctx.client.can("sampling")) {
// Ask the LLM to help process
const result = await ctx.sample(`Summarize this: ${data}`);
}
await ctx.reportProgress?.(100, 100, "Done");
return text("Processed successfully");
}
);
```
### 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
});
```
### Structured Output
Use `outputSchema` for typed, validated output:
```typescript
server.tool(
{
name: "get-stats",
schema: z.object({ period: z.string() }),
outputSchema: z.object({
total: z.number(),
average: z.number(),
trend: z.enum(["up", "down", "flat"]),
}),
},
async ({ period }) => {
return object({ total: 150, average: 42.5, trend: "up" });
}
);
```
## Resources
Resources expose read-only data clients can fetch.
### Static Resource
```typescript
import { object, text, markdown } from "mcp-use/server";
server.resource(
{
uri: "config://settings",
name: "Application Settings",
description: "Current server configuration",
mimeType: "application/json",
},
async () => object({ theme: "dark", version: "1.0.0", language: "en" })
);
server.resource(
{
uri: "docs://guide",
name: "User Guide",
mimeType: "text/markdown",
},
async () => markdown("# User Guide\n\nWelcome to the app!")
);
```
### Dynamic Resource
```typescript
server.resource(
{
uri: "stats://current",
name: "Current Stats",
mimeType: "application/json",
},
async () => {
const stats = await getStats();
return object(stats);
}
);
```
### Parameterized Resource (Templates)
```typescript
server.resourceTemplate(
{
uriTemplate: "user://{userId}/profile",
name: "User Profile",
description: "Get user profile by ID",
mimeType: "application/json",
},
async (uri, { userId }) => {
const user = await fetchUser(userId);
return object(user);
}
);
```
For advanced URI patterns, see [resource-templates.md](resource-templates.md).
## Prompts
Prompts are reusable message templates for AI interactions.
```typescript
server.prompt(
{
name: "code-review",
description: "Generate a code review for the given language",
schema: z.object({
language: z.string().describe("Programming language"),
focusArea: z.string().optional().describe("Specific area to focus on"),
}),
},
async ({ language, focusArea }) => {
const focus = focusArea ? ` Focus on ${focusArea}.` : "";
return text(`Please review this ${language} code for best practices and potential issues.${focus}`);
}
);
```
## Zod Schema Best Practices
```typescript
// Good: descriptive, with constraints
const schema = 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
const schema = z.object({
city: z.string(),
units: z.string(),
limit: z.number(),
});
```
**Rules:**
- Always add `.describe()` to every field
- Use `.optional()` for non-required fields
- Add validation (`.min()`, `.max()`, `.enum()`) where appropriate
- Use `z.enum()` instead of `z.string()` when there's a fixed set of values
## Error Handling
```typescript
import { text, error } from "mcp-use/server";
server.tool(
{ name: "fetch-data", schema: z.object({ id: z.string() }) },
async ({ id }) => {
try {
const data = await fetchFromAPI(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"}`);
}
}
);
```
## Environment Variables
When your server needs API keys or configuration:
```typescript
// index.ts - read from environment
const API_KEY = process.env.WEATHER_API_KEY;
server.tool(
{ name: "get-weather", schema: z.object({ city: z.string() }) },
async ({ city }) => {
if (!API_KEY) {
return error("WEATHER_API_KEY not configured. Please set it in the Env tab.");
}
const data = await fetch(`https://api.weather.com/v1?key=${API_KEY}&city=${city}`);
// ...
}
);
```
Create a `.env.example` documenting required variables:
```
# Weather API key (get from weatherapi.com)
WEATHER_API_KEY=
```
## Custom HTTP Routes
MCPServer extends Hono, so you can add custom API endpoints:
```typescript
server.get("/api/health", (c) => c.json({ status: "ok" }));
server.post("/api/webhook", async (c) => {
const body = await c.req.json();
// Handle webhook
return c.json({ received: true });
});
```
## Server Startup
```typescript
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000;
server.listen(PORT);
```
📎 widgets.md
# Widgets
Create interactive visual UIs for your MCP tools using React components.
## How Widgets Work
1. You create a React component in `resources/` folder
2. The component exports `widgetMetadata` (description + props schema) and a default React component
3. mcp-use auto-registers it as both a tool and a resource
4. When the tool is called, the widget renders with the tool's output data
## Widget File Patterns
### Single File
```
resources/weather-display.tsx → widget name: "weather-display"
resources/recipe-card.tsx → widget name: "recipe-card"
```
### Folder-Based (for complex widgets)
```
resources/product-search/
widget.tsx → entry point (required name)
components/ProductCard.tsx
hooks/useFilter.ts
types.ts
```
**Naming**: File/folder name becomes the widget name. Use kebab-case.
## Creating a Widget
### Step 1: Create the Widget File
```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().describe("City name"),
temp: z.number().describe("Temperature in Celsius"),
conditions: z.string().describe("Weather conditions"),
humidity: z.number().describe("Humidity percentage"),
});
export const widgetMetadata: WidgetMetadata = {
description: "Display current weather conditions for a city",
props: propsSchema,
};
type Props = z.infer<typeof propsSchema>;
export default function WeatherDisplay() {
const { props, isPending } = useWidget<Props>();
if (isPending) {
return (
<McpUseProvider autoSize>
<div style={{ padding: 16, textAlign: "center" }}>Loading weather...</div>
</McpUseProvider>
);
}
return (
<McpUseProvider autoSize>
<div style={{ padding: 20, borderRadius: 12, background: "#f0f9ff" }}>
<h2 style={{ margin: 0, fontSize: 24 }}>{props.city}</h2>
<div style={{ fontSize: 48, fontWeight: "bold" }}>{props.temp}°C</div>
<p style={{ color: "#666" }}>{props.conditions}</p>
<p style={{ color: "#999", fontSize: 14 }}>Humidity: {props.humidity}%</p>
</div>
</McpUseProvider>
);
}
```
**TypeScript:** Always use `useWidget<z.infer<typeof propsSchema>>()` (or a `Props` alias) so `mcp-use build` typechecking passes. For `callTool` / `structuredContent`, narrow `unknown` fields before rendering.
### Step 2: Register the Tool
```typescript
// index.ts
import { MCPServer, widget, text } from "mcp-use/server";
import { z } from "zod";
const server = new MCPServer({
name: "weather-server",
version: "1.0.0",
baseUrl: process.env.MCP_URL || "http://localhost:3000",
});
server.tool(
{
name: "get-weather",
description: "Get current weather for a city",
schema: z.object({
city: z.string().describe("City name"),
}),
widget: {
name: "weather-display", // Must match resources/weather-display.tsx
invoking: "Fetching weather...",
invoked: "Weather loaded",
},
},
async ({ city }) => {
const data = getWeather(city);
return widget({
props: { city, temp: data.temp, conditions: data.conditions, humidity: data.humidity },
output: text(`Weather in ${city}: ${data.temp}°C, ${data.conditions}`),
});
}
);
server.listen();
```
## Required Widget Exports
Every widget file MUST export:
1. **`widgetMetadata`** — Object with `description` and `props` (Zod schema):
```typescript
export const widgetMetadata: WidgetMetadata = {
description: "Human-readable description of what this widget shows",
props: z.object({ /* Zod schema for widget input */ }),
};
```
2. **Default React component** — The UI:
```typescript
export default function MyWidget() { ... }
```
### WidgetMetadata Fields
| Field | Type | Required | Description |
|---|---|---|---|
| `description` | `string` | Yes | What the widget displays |
| `props` | `z.ZodObject` | Yes | Zod schema for widget input data |
| `exposeAsTool` | `boolean` | No | Auto-register as tool (default: `false`) |
| `toolOutput` | `CallToolResult \| (params => CallToolResult)` | No | What the AI model sees |
| `title` | `string` | No | Display title |
| `annotations` | `object` | No | `readOnlyHint`, `destructiveHint`, etc. |
| `metadata` | `object` | No | CSP, border, resize config, invocation status text |
| `metadata.invoking` | `string` | No | Status text while tool runs — shown as shimmer in inspector (auto-default: `"Loading {name}..."`) |
| `metadata.invoked` | `string` | No | Status text after tool completes — shown in inspector (auto-default: `"{name} ready"`) |
**Invocation status text** in `metadata` is protocol-agnostic and works for both `mcpApps` and `appsSdk` widgets. For tools using `widget: { name, invoking, invoked }` in the tool config, the `invoking`/`invoked` values in `widget:` take effect instead.
### `exposeAsTool` defaults to `false`
Widgets are registered as MCP resources only by default. When you define a custom tool with `widget: { name: "my-widget" }`, omitting `exposeAsTool` is correct — the custom tool handles making the widget callable:
```typescript
export const widgetMetadata: WidgetMetadata = {
description: "Weather display",
props: z.object({ city: z.string(), temp: z.number() }),
// exposeAsTool defaults to false — custom tool handles registration
};
```
Set `exposeAsTool: true` to auto-register a widget as a tool without a custom tool definition.
### `toolOutput`
Control what the AI model sees when the auto-registered tool is called:
```typescript
export const widgetMetadata: WidgetMetadata = {
description: "Recipe card",
props: z.object({ name: z.string(), ingredients: z.array(z.string()) }),
toolOutput: (params) => text(`Showing recipe: ${params.name} (${params.ingredients.length} ingredients)`),
};
```
## `useWidget` Hook
The primary hook for accessing widget data and capabilities.
```typescript
const {
// Core data
props, // Widget input data (from tool's widget() call or auto-registered tool)
isPending, // true while tool is still executing (props may be partial)
toolInput, // Original tool input arguments
output, // Additional tool output data
metadata, // Response metadata
// Persistent state
state, // Persisted widget state (survives re-renders)
setState, // Update persistent state: setState(newState) or setState(prev => newState)
// Host environment
theme, // 'light' | 'dark'
displayMode, // 'inline' | 'pip' | 'fullscreen'
safeArea, // { insets: { top, bottom, left, right } }
maxHeight, // Max available height in pixels
userAgent, // { device: { type }, capabilities: { hover, touch } }
locale, // User locale (e.g., 'en-US')
timeZone, // IANA timezone
// Actions
callTool, // Call another MCP tool: callTool("tool-name", { args })
sendFollowUpMessage,// Trigger LLM response: sendFollowUpMessage("analyze this") or sendFollowUpMessage([{ type: "text", text: "..." }])
openExternal, // Open external URL: openExternal("https://example.com")
requestDisplayMode, // Request mode change: requestDisplayMode("fullscreen")
mcp_url, // MCP server base URL for custom API requests
} = useWidget();
```
### Loading State (Critical)
Widgets render BEFORE tool execution completes. **Always handle `isPending`:**
```tsx
const { props, isPending } = useWidget();
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
// Now props are safe to use
return <McpUseProvider autoSize><div>{props.city}: {props.temp}°C</div></McpUseProvider>;
```
### Calling Other Tools
```tsx
const { callTool } = useWidget();
const handleRefresh = async () => {
try {
const result = await callTool("get-weather", { city: "Tokyo" });
console.log(result.content);
} catch (err) {
console.error("Tool call failed:", err);
}
};
```
### Triggering LLM Response
Accepts a plain string shorthand or a full content block array per the SEP-1865 `ui/message` spec:
```tsx
const { sendFollowUpMessage } = useWidget();
// String shorthand (most common)
<button onClick={() => sendFollowUpMessage("Compare the weather in these cities")}>
Ask AI to Compare
</button>
// Full content array (MCP Apps — supports text, image, resource blocks)
<button onClick={() => sendFollowUpMessage([
{ type: "text", text: "Compare the weather in these cities" },
])}>
Ask AI to Compare
</button>
```
### Persistent State
```tsx
const { state, setState } = useWidget();
// Set state
await setState({ favorites: [...(state?.favorites || []), city] });
// Update with function
await setState((prev) => ({ ...prev, count: (prev?.count || 0) + 1 }));
```
## Convenience Hooks
For simpler use cases:
```typescript
import { useWidgetProps, useWidgetTheme, useWidgetState } from "mcp-use/react";
// Just props
const props = useWidgetProps<MyProps>();
// Just theme
const theme = useWidgetTheme(); // 'light' | 'dark'
// Just state (like useState)
const [state, setState] = useWidgetState<MyState>({ count: 0 });
```
## McpUseProvider
Wrap your widget content in `McpUseProvider`:
```tsx
import { McpUseProvider } from "mcp-use/react";
export default function MyWidget() {
return (
<McpUseProvider autoSize>
<div>Widget content</div>
</McpUseProvider>
);
}
```
| Prop | Type | Default | Description |
|---|---|---|---|
| `autoSize` | `boolean` | `false` | Auto-resize widget height to fit content |
| `viewControls` | `boolean \| "pip" \| "fullscreen"` | `false` | Show display mode control buttons |
| `debugger` | `boolean` | `false` | Show debug inspector overlay |
## Styling
Both inline styles and Tailwind classes work:
```tsx
// Inline styles
<div style={{ padding: 20, borderRadius: 12, background: "#f0f9ff" }}>
// Tailwind
<div className="p-5 rounded-xl bg-blue-50">
```
## `widget()` Response Helper
Used in tool callbacks to send data to the widget:
```typescript
import { widget, text } from "mcp-use/server";
return widget({
props: { city: "Tokyo", temp: 25 }, // Sent to widget via useWidget().props
output: text("Weather in Tokyo: 25°C"), // What the AI model sees
message: "Current weather for Tokyo", // Optional text override
});
```
| Field | Type | Description |
|---|---|---|
| `props` | `Record<string, any>` | Data for the widget UI (hidden from model) |
| `output` | `CallToolResult` | Response helper result the model sees (`text()`, `object()`, etc.) |
| `message` | `string` | Optional text message override |
## Tool `widget` Config
```typescript
server.tool({
name: "tool-name",
schema: z.object({ ... }),
widget: {
name: "widget-name", // Must match resources/ file/folder name
invoking: "Loading...", // Text shown while tool runs
invoked: "Ready", // Text shown when complete
widgetAccessible: true, // Widget can call other tools (default: true)
},
}, async (input) => { ... });
```
## Complete End-to-End Example
**`index.ts`:**
```typescript
import { MCPServer, widget, text, object } from "mcp-use/server";
import { z } from "zod";
const server = new MCPServer({
name: "recipe-finder",
version: "1.0.0",
baseUrl: process.env.MCP_URL || "http://localhost:3000",
});
const mockRecipes = [
{ id: "1", name: "Pasta Carbonara", cuisine: "Italian", time: 30, ingredients: ["pasta", "eggs", "bacon", "parmesan"] },
{ id: "2", name: "Chicken Tikka", cuisine: "Indian", time: 45, ingredients: ["chicken", "yogurt", "spices", "rice"] },
{ id: "3", name: "Sushi Rolls", cuisine: "Japanese", time: 60, ingredients: ["rice", "nori", "fish", "avocado"] },
];
server.tool(
{
name: "search-recipes",
description: "Search for recipes by query or cuisine",
schema: z.object({
query: z.string().describe("Search query (e.g., 'pasta', 'chicken')"),
cuisine: z.string().optional().describe("Filter by cuisine"),
}),
widget: {
name: "recipe-list",
invoking: "Searching recipes...",
invoked: "Recipes found",
},
},
async ({ query, cuisine }) => {
const results = mockRecipes.filter(r =>
r.name.toLowerCase().includes(query.toLowerCase()) ||
(cuisine && r.cuisine.toLowerCase() === cuisine.toLowerCase())
);
return widget({
props: { recipes: results, query },
output: text(`Found ${results.length} recipes for "${query}"`),
});
}
);
server.listen();
```
**`resources/recipe-list.tsx`:**
```tsx
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
export const widgetMetadata: WidgetMetadata = {
description: "Display recipe search results",
props: z.object({
recipes: z.array(z.object({
id: z.string(),
name: z.string(),
cuisine: z.string(),
time: z.number(),
ingredients: z.array(z.string()),
})),
query: z.string(),
}),
exposeAsTool: false,
};
export default function RecipeList() {
const { props, isPending } = useWidget();
if (isPending) {
return <McpUseProvider autoSize><div style={{ padding: 16 }}>Searching...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div style={{ padding: 16 }}>
<h2 style={{ margin: "0 0 12px" }}>Recipes for "{props.query}"</h2>
{props.recipes.length === 0 ? (
<p style={{ color: "#999" }}>No recipes found.</p>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{props.recipes.map((recipe) => (
<div key={recipe.id} style={{
padding: 16, borderRadius: 8, border: "1px solid #e5e7eb", background: "#fff"
}}>
<h3 style={{ margin: "0 0 4px" }}>{recipe.name}</h3>
<p style={{ margin: 0, color: "#666", fontSize: 14 }}>
{recipe.cuisine} · {recipe.time} min · {recipe.ingredients.join(", ")}
</p>
</div>
))}
</div>
)}
</div>
</McpUseProvider>
);
}
```