Back

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.

Skyll
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 server

References (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>
  );
}
```