SKILL.md
IMPORTANT: How to Use This Skill
This file provides a NAVIGATION GUIDE ONLY. Before implementing any MCP server features, you MUST:
- Read this overview to understand which reference files are relevant
- ALWAYS read the specific reference file(s) for the features you're implementing
- Apply the detailed patterns from those files to your implementation
Do NOT rely solely on the quick reference examples in this file - they are minimal examples only. The reference files contain critical best practices, security considerations, and advanced patterns.
MCP Server Best Practices
Comprehensive guide for building production-ready MCP servers with tools, resources, prompts, and widgets using mcp-use.
⚠️ FIRST: New Project or Existing Project?
Before doing anything else, determine whether you are inside an existing mcp-use project.
Detection: Check the workspace for a package.json that lists "mcp-use" as a dependency, OR any .ts file that imports from "mcp-use/server".
├─ mcp-use project FOUND → Do NOT scaffold. You are already in a project.
│ └─ Skip to "Quick Navigation" below to add features.
│
├─ NO mcp-use project (empty dir, unrelated project, or greenfield)
│ └─ Scaffold first with npx create-mcp-use-app, then add features.
│ See "Scaffolding a New Project" below.
│
└─ Inside an UNRELATED project (e.g. Next.js app) and user wants an MCP server
└─ Ask the user where to create it, then scaffold in that directory.
Do NOT scaffold inside an existing unrelated project root.
**NEVER manually create MCPServer boilerplate, package.json, or project structure by hand.** The CLI sets up TypeScript config, dev scripts, inspector integration, hot reload, and widget compilation that are difficult to replicate manually.
Scaffolding a New Project
npx create-mcp-use-app my-server
cd my-server
npm run dev
For full scaffolding details and CLI flags, see quickstart.md.
Quick Navigation
Choose your path based on what you're building:
🚀 Foundations
When: ALWAYS read these first when starting MCP work in a new conversation. Reference later for architecture/concept clarification.
- concepts.md - MCP primitives (Tool, Resource, Prompt, Widget) and when to use each
- architecture.md - Server structure (Hono-based), middleware system, server.use() vs server.app
- quickstart.md - Scaffolding, setup, and first tool example
- deployment.md - Deploying to Manufact Cloud, self-hosting, Docker, managing deployments
Load these before diving into tools/resources/widgets sections.
🔐 Adding Authentication?
When: Protecting your server with OAuth (Auth0, Better Auth, Clerk, WorkOS, Supabase, Keycloak, or any other provider)
-
- When: First time adding auth, understanding
ctx.auth, or choosing a provider / integration mode
- Covers: Remote auth vs OAuth proxy,
oauthconfig,ctx.authshape, provider comparison, common mistakes
-
- When: Using Auth0 — DCR (Early Access) or a standard Regular Web App via
oauthProxy
- Covers: Setup for both modes,
extraAuthorizeParams.audience, permissions viarfc9068_profile_authz
-
- When: Using Better Auth with the
@better-auth/oauth-providerplugin (self-hosted OAuth 2.1)
- Covers:
oauthBetterAuthProvider, auth URL / metadata routes, login and consent flows
-
- When: Using Clerk (DCR-based OAuth)
- Covers:
oauthClerkProvider, enabling DCR, Frontend API URL, organization context
-
- When: Using WorkOS AuthKit (DCR only)
- Covers: Setup, env vars, roles/permissions, multi-tenant org filtering, WorkOS API calls
-
- When: Using Supabase's OAuth 2.1 server
- Covers: Setup, publishable keys, ES256 vs HS256, hosting the consent UI, RLS-aware SDK calls
-
- When: Using Keycloak via native DCR
- Covers: DCR trusted hosts + web origins, audience enforcement, realm vs resource roles, userinfo
-
- When: Any other provider — DCR-capable via
oauthCustomProvider, or pre-registered (Google, GitHub, Okta, Azure AD) viaoauthProxy
- Covers:
oauthCustomProvider,oauthProxy+jwksVerifier, provider examples, opaque-token verification
🔧 Building Server Backend (No UI)?
When: Implementing MCP features (actions, data, templates). Read the specific file for the primitive you're building.
-
- When: Creating backend actions the AI can call (send-email, fetch-data, create-user)
- Covers: Tool definition, schemas, annotations, context, error handling
-
- When: Exposing read-only data clients can fetch (config, user profiles, documentation)
- Covers: Static resources, dynamic resources, parameterized resource templates, URI completion
-
- When: Creating reusable message templates for AI interactions (code-review, summarize)
- Covers: Prompt definition, parameterization, argument completion, prompt best practices
-
- When: Formatting responses from tools/resources (text, JSON, markdown, images, errors)
- Covers:
text(),object(),markdown(),image(),error(),mix()
-
- When: Composing multiple MCP servers into one unified aggregator server
- Covers:
server.proxy(), config API, explicit sessions, sampling routing
-
- When: Adding cross-cutting logic (logging, auth checks, rate limiting, tool filtering) that spans multiple tools/resources
- Covers:
server.use('mcp:...')middleware,MiddlewareContext(method, params, auth, state), pattern matching, HTTP vs MCP middleware
🎨 Building Visual Widgets (Interactive UI)?
When: Creating React-based visual interfaces for browsing, comparing, or selecting data
-
- When: Creating your first widget or adding UI to an existing tool
- Covers: Widget setup,
useWidget()hook,isPendingchecks, props handling
-
- When: Managing UI state (selections, filters, tabs) within widgets
- Covers:
useState,setState, state persistence, when to use tool vs widget state
-
- When: Adding buttons, forms, or calling tools from within widgets
- Covers:
useCallTool(), form handling, action buttons, optimistic updates
-
- When: Styling widgets to support themes, responsive layouts, or accessibility
- Covers:
useWidgetTheme(), light/dark mode,autoSize, layout patterns, CSS best practices
-
- When: Building complex widgets with async data, error boundaries, or performance optimizations
- Covers: Loading states, error handling, memoization, code splitting
-
- When: Keeping the AI model aware of what the user is currently seeing (active tab, hovered item, selected product) without requiring tool calls
- Covers:
<ModelContext>component,modelContext.set/removeimperative API, nesting, tree serialization, lifecycle rules
-
- When: Uploading or downloading files from within a widget (ChatGPT Apps SDK only)
- Covers:
useFiles()hook,isSupportedguard, model visibility (modelVisible), storingfileId, temporary download URLs
📚 Need Complete Examples?
When: You want to see full implementations of common use cases
- End-to-end examples: weather app, todo list, recipe browser
- Shows: Server code + widget code + best practices in context
🔁 Testing from the Terminal (Agent Feedback Loops)
When: You want to verify a tool or widget without the inspector UI — the canonical flow for AI agents iterating on MCP servers.
-
**mcp-use client** — drives MCP servers from the terminal. Auto-runs OAuth on 401, persists saved servers under a short name, and one-shot subcommands exit cleanly so they're safe to spawn from harnesses.
npx mcp-use client connect dev http://localhost:3000/mcp
npx mcp-use client dev tools list
npx mcp-use client dev tools call get-weather city=Tokyo --screenshot
Every per-server command takes the saved name as its first positional arg (mcp-use client <name> <scope> <action>) — there is no "active session". Args use key=value (with key:='<json>' for nested values) or a single JSON object. When a tool renders a widget, pass --screenshot to also save a PNG (./<view>-<timestamp>.png by default, or override with --screenshot-output <path>).
-
**mcp-use client screenshot** — headless render of a widget tool to a PNG. Use this when you want to visually verify a widget change without opening the inspector, especially in loops where you call a tool, screenshot, eyeball the output, and edit. Two forms:
# Saved-server form — reuses the auth from `mcp-use client connect`
npx mcp-use client dev screenshot --tool get-weather city=Tokyo \
--width 800 --height 600 --theme light \
--output ./weather.png
# Ad-hoc form — connect inline (use -H for headers on authenticated servers)
npx mcp-use client screenshot --mcp http://localhost:3000/mcp \
--tool get-weather city=Tokyo
Add --device-scale-factor 2 for Retina output, or --cdp-url <ws> plus --inspector <publicly-reachable-url> to drive a remote Chromium (e.g. Notte) from a sandbox without a local Chrome install.
Both commands are documented in full at docs/typescript/client/cli.
Decision Tree
What do you need?
├─ New project from scratch
│ └─> quickstart.md (scaffolding + setup)
│
├─ OAuth / user authentication
│ └─> authentication/overview.md → provider-specific guide
│
├─ Simple backend action (no UI)
│ └─> Use Tool: server/tools.md
│
├─ Read-only data for clients
│ └─> Use Resource: server/resources.md
│
├─ Reusable prompt template
│ └─> Use Prompt: server/prompts.md
│
├─ Cross-cutting logic (logging, auth checks, rate limiting, tool filtering)
│ └─> Use Middleware: architecture.md#mcp-middleware
│
├─ Visual/interactive UI
│ └─> Use Widget: widgets/basics.md
│
├─ Keep model aware of what user is seeing in widget
│ └─> widgets/model-context.md
├─ Upload/download files in a widget
│ └─> widgets/files.md (ChatGPT Apps SDK only)
│
├─ Verify a tool or widget from the terminal (agent feedback loop)
│ └─> See "Testing from the Terminal" above — `mcp-use client` for tool runs,
│ `mcp-use client <server> screenshot --tool <tool>` for headless widget PNGs
│
└─ Deploy to production
└─> deployment.md (cloud deploy, self-hosting, Docker)
Core Principles
- Tools for actions - Backend operations with input/output
- Resources for data - Read-only data clients can fetch
- Prompts for templates - Reusable message templates
- Widgets for UI - Visual interfaces when helpful
- Mock data first - Prototype quickly, connect APIs later
❌ Common Mistakes
Avoid these anti-patterns found in production MCP servers:
Tool Definition
- ❌ Returning raw objects instead of using response helpers
- ✅ Use
text(),object(),widget(),error()helpers
- ❌ Skipping Zod schema
.describe()on every field
- ✅ Add descriptions to all schema fields for better AI understanding
- ❌ No input validation or sanitization
- ✅ Validate inputs with Zod, sanitize user-provided data
- ❌ Throwing errors instead of returning
error()helper
- ✅ Use
error("message")for graceful error responses
Widget Development
- ❌ Accessing
propswithout checkingisPending
- ✅ Always check
if (isPending) return <Loading/>
- ❌ Widget handles server state (filters, selections)
- ✅ Widgets manage their own UI state with
useState
- ❌ Missing
McpUseProviderwrapper orautoSize
- ✅ Wrap root component:
<McpUseProvider autoSize>
- ❌ Inline styles without theme awareness
- ✅ Use
useWidgetTheme()for light/dark mode support
Security & Production
- ❌ Hardcoded API keys or secrets in code
- ✅ Use
process.env.API_KEY, document in.env.example
- ❌ No error handling in tool handlers
- ✅ Wrap in try/catch, return
error()on failure
- ❌ Expensive operations without caching
- ✅ Cache API calls, computations with TTL
- ❌ Missing CORS configuration
- ✅ Configure CORS for production deployments
🔒 Golden Rules
Opinionated architectural guidelines:
1. One Tool = One Capability
Split broad actions into focused tools:
- ❌
manage-users(too vague)
- ✅
create-user,delete-user,list-users
2. Return Complete Data Upfront
Tool calls are expensive. Avoid lazy-loading:
- ❌
list-products+get-product-details(2 calls)
- ✅
list-productsreturns full data including details
3. Widgets Own Their State
UI state lives in the widget, not in separate tools:
- ❌
select-itemtool,set-filtertool
- ✅ Widget manages with
useStateorsetState
4. exposeAsTool Defaults to false
Widgets are registered as resources only by default. Use a custom tool (recommended) or set exposeAsTool: true to expose a widget to the model:
// ✅ ALL 4 STEPS REQUIRED for proper type inference:
// Step 1: Define schema separately
const propsSchema = z.object({
title: z.string(),
items: z.array(z.string())
});
// Step 2: Reference schema variable in metadata
export const widgetMetadata: WidgetMetadata = {
description: "...",
props: propsSchema, // ← NOT inline z.object()
exposeAsTool: false
};
// Step 3: Infer Props type from schema variable
type Props = z.infer<typeof propsSchema>;
// Step 4: Use typed Props with useWidget
export default function MyWidget() {
const { props, isPending } = useWidget<Props>(); // ← Add <Props>
// ...
}
⚠️ Common mistake: Only doing steps 1-2 but skipping 3-4 (loses type safety)
5. Validate at Boundaries Only
- Trust internal code and framework guarantees
- Validate user input, external API responses
- Don't add error handling for scenarios that can't happen
6. Prefer Widgets for Browsing/Comparing
When in doubt, add a widget. Visual UI improves:
- Browsing multiple items
- Comparing data side-by-side
- Interactive selection workflows
Quick Reference
Minimal Server
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"
});
server.tool(
{
name: "greet",
description: "Greet a user",
schema: z.object({ name: z.string().describe("User's name") })
},
async ({ name }) => text("Hello " + name + "!"),
);
server.listen();
Response Helpers
Helper
Use When
Example
text()
Simple string response
text("Success!")
object()
Structured data
object({ status: "ok" })
markdown()
Formatted text
markdown("# Title\nContent")
widget()
Visual UI
widget({ props: {...}, output: text(...) })
mix()
Multiple contents
mix(text("Hi"), image(url))
error()
Error responses
error("Failed to fetch data")
resource()
Embed resource refs
resource("docs://guide", "text/markdown")
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
server.use('mcp:tools/call', fn)- MCP middleware (tools, resources, prompts, list ops)
server.use('mcp:*', fn)- Catch-all MCP middleware
server.use(fn)- HTTP middleware (Hono)