Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive guide for building production-ready MCP servers with tools, resources, prompts, and React widgets using mcp-use.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/server/tools.md
1# Tools23Tools are backend actions the AI can call. They take structured input and return output.45**Use tools for:** Actions, operations, API calls, mutations, data fetching67---89## Basic Tool1011```typescript12import { MCPServer, text } from "mcp-use/server";13import { z } from "zod";1415const server = new MCPServer({16name: "my-server",17version: "1.0.0",18baseUrl: process.env.MCP_URL || "http://localhost:3000"19});2021server.tool(22{23name: "send-email",24description: "Send an email to a user",25schema: z.object({26to: z.string().email().describe("Recipient email address"),27subject: z.string().describe("Email subject line"),28body: z.string().describe("Email body content"),29priority: z.enum(["low", "normal", "high"]).optional().describe("Email priority")30})31},32async ({ to, subject, body, priority = "normal" }) => {33// Your logic here34await sendEmail(to, subject, body, priority);35return text(`Email sent to ${to}`);36}37);38```3940**Key points:**41- First argument: tool configuration (name, description, schema)42- Second argument: async handler function43- Handler receives validated input matching schema44- Must return a response helper (`text()`, `object()`, `widget()`, etc.)4546---4748## Tool Definition4950### Name51- Use kebab-case: `send-email`, `fetch-user`, `create-todo`52- Be specific: ❌ `manage-users` ✅ `create-user`, `delete-user`, `list-users`53- One tool = one capability5455### Description56Clear, actionable description of what the tool does:57```typescript58✅ "Send an email to a user with subject and body"59❌ "Email tool"60```6162The AI uses this to decide when to call your tool.6364### Schema (Zod)6566**Always use `.describe()` on every field:**67```typescript68// ✅ Good69z.object({70city: z.string().describe("City name (e.g., 'New York', 'Tokyo')"),71units: z.enum(["celsius", "fahrenheit"]).optional().describe("Temperature units"),72limit: z.number().min(1).max(50).optional().describe("Max results to return")73})7475// ❌ Bad - no descriptions76z.object({77city: z.string(),78units: z.string(),79limit: z.number()80})81```8283**Schema best practices:**84- Use `.optional()` for non-required fields85- Add validation: `.min()`, `.max()`, `.email()`, `.url()`86- Use `z.enum()` for fixed sets of values (not `z.string()`)87- Use `z.array()` for lists88- Use `z.record(z.string(), z.string())` for key-value maps (Zod v4 requires both key and value schemas)8990---9192## Tool Annotations9394Declare the nature of your tool so clients can warn users:9596```typescript97server.tool(98{99name: "delete-user",100description: "Permanently delete a user account",101schema: z.object({ userId: z.string().describe("User ID") }),102annotations: {103destructiveHint: true, // Deletes or overwrites data104readOnlyHint: false, // Has side effects105openWorldHint: false // Stays within user's account (not external APIs)106}107},108async ({ userId }) => {109await deleteUser(userId);110return text(`User ${userId} deleted`);111}112);113```114115**Annotations:**116- `destructiveHint: true` - Deletes/overwrites data, client may require confirmation117- `readOnlyHint: true` - No side effects, safe to call repeatedly118- `openWorldHint: true` - Calls external APIs or services outside user's control119120---121122## Tool Context123124The second parameter to tool handlers provides advanced capabilities:125126```typescript127server.tool(128{129name: "process-large-file",130schema: z.object({ fileUrl: z.string().describe("URL to file") })131},132async ({ fileUrl }, ctx) => {133// Progress reporting134await ctx.reportProgress?.(0, 100, "Starting download...");135const file = await downloadFile(fileUrl);136137await ctx.reportProgress?.(50, 100, "Processing...");138const result = await processFile(file);139140// Structured logging141await ctx.log("info", `Processed ${file.size} bytes`);142143// Structured logging with additional context (optional third parameter)144await ctx.log("info", "Processing complete", `fileSize: ${file.size} bytes, duration: 2.5s`);145146// Check client capabilities147if (ctx.client.can("sampling")) {148// Ask the LLM to help analyze results149const summary = await ctx.sample(`Summarize this data: ${result}`);150return text(summary);151}152153await ctx.reportProgress?.(100, 100, "Complete");154return object(result);155}156);157```158159**Context methods:**160- `ctx.reportProgress(current: number, total: number, message: string)` - Show progress to user161- `ctx.log(level: "debug" | "info" | "warn" | "error", message: string, data?: string)` - Structured logging with optional additional context as a string162- `ctx.sample(prompt: string)` - Ask the LLM for help (requires client support)163- `ctx.client.can(capability: string)` - Check if client supports a feature164165### Client Identity & Caller Context166167`ctx.client` also exposes per-invocation caller context from `params._meta`:168169```typescript170server.tool({ name: "personalise", schema: z.object({}) }, async (_p, ctx) => {171// Session-level (stable for the connection lifetime)172const { name, version } = ctx.client.info(); // "openai-mcp", "1.0.0"173const isAppsClient = ctx.client.supportsApps(); // true for ChatGPT174175// Per-invocation — may differ on every tool call176const caller = ctx.client.user();177if (caller) {178const city = caller.location?.city ?? "there";179const greeting = caller.locale?.startsWith("it") ? "Ciao" : "Hello";180return text(`${greeting} from ${city}! (via ${name})`);181}182return text(`Hello! (via ${name})`);183});184```185186**`ctx.client.user()` fields:**187- `subject` — stable opaque user ID (same across conversations, e.g. `openai/subject`)188- `conversationId` — current chat thread ID (changes per chat, e.g. `openai/session`)189- `locale` — BCP-47 locale, e.g. `"it-IT"` (server-side; inside widgets prefer `useWidget().locale` which is client-side and fresher)190- `location` — `{ city, region, country, timezone, latitude, longitude }`191- `userAgent` — browser/host user-agent string192- `timezoneOffsetMinutes` — UTC offset in minutes193194**Key rules:**195- Returns `undefined` on clients that don't send this metadata (Inspector, CLI, non-ChatGPT clients)196- **Unverified / advisory** — self-reported by the client, not suitable for access control197- For verified identity, use `ctx.auth` (requires OAuth)198199**ChatGPT multi-tenant model:**200ChatGPT uses a single MCP session for ALL users of a deployed app. Use `ctx.client.user()` to distinguish callers:201202```2031 MCP session ctx.session.sessionId — shared across ALL users204N subjects ctx.client.user()?.subject — one per ChatGPT user account205M threads ctx.client.user()?.conversationId — one per chat conversation206```207208```typescript209// Identify who is calling this specific invocation210const caller = ctx.client.user();211return object({212mcpSession: ctx.session.sessionId, // shared transport session213user: caller?.subject ?? null, // ChatGPT user ID214conversation: caller?.conversationId ?? null, // this chat thread215});216```217218---219220## Error Handling221222**Always use `error()` helper, don't throw:**223224```typescript225import { text, error } from "mcp-use/server";226227server.tool(228{ name: "fetch-user", schema: z.object({ id: z.string() }) },229async ({ id }) => {230try {231const user = await fetchUser(id);232233if (!user) {234return error(`User not found: ${id}`);235}236237return object(user);238} catch (err) {239// Log for debugging240console.error("Failed to fetch user:", err);241242// Return error to client243return error(244`Failed to fetch user: ${err instanceof Error ? err.message : "Unknown error"}`245);246}247}248);249```250251**Error handling rules:**252- ✅ Return `error()` for graceful failure253- ❌ Don't throw exceptions (client sees raw error)254- ✅ Include helpful context in error messages255- ✅ Log errors server-side for debugging256257---258259## Tool with Widget260261When your tool returns visual UI:262263```typescript264import { widget, text } from "mcp-use/server";265266server.tool(267{268name: "search-products",269description: "Search products by keyword",270schema: z.object({271query: z.string().describe("Search query")272}),273widget: {274name: "product-list", // Must match resources/product-list.tsx275invoking: "Searching products...",276invoked: "Products loaded"277}278},279async ({ query }) => {280const products = await searchProducts(query);281282return widget({283props: {284products,285query,286totalCount: products.length287},288output: text(`Found ${products.length} products matching "${query}"`)289});290}291);292```293294**Widget tool requirements:**295- Add `widget: { name }` to tool config296- Return `widget({ props, output })` from handler297- Create matching widget file: `resources/{name}.tsx`298- `exposeAsTool` defaults to `false` — omitting it is correct for this pattern299300See [../widgets/basics.md](../widgets/basics.md) for widget implementation.301302---303304## Structured Output Schema305306Validate tool output at runtime:307308```typescript309server.tool(310{311name: "calculate-stats",312schema: z.object({313data: z.array(z.number()).describe("Array of numbers")314}),315outputSchema: z.object({316mean: z.number(),317median: z.number(),318stdDev: z.number(),319count: z.number()320})321},322async ({ data }) => {323const stats = calculateStats(data);324325// Output is validated against outputSchema326return object({327mean: stats.mean,328median: stats.median,329stdDev: stats.stdDev,330count: data.length331});332}333);334```335336**When to use `outputSchema`:**337- You want runtime validation of tool output338- Multiple code paths return different shapes339- Debugging output consistency issues340341---342343## Environment Variables344345Securely handle API keys and configuration:346347```typescript348// index.ts349const WEATHER_API_KEY = process.env.WEATHER_API_KEY;350351server.tool(352{353name: "get-weather",354schema: z.object({ city: z.string() })355},356async ({ city }) => {357if (!WEATHER_API_KEY) {358return error(359"WEATHER_API_KEY not configured. Please set it in environment variables."360);361}362363const data = await fetch(364`https://api.weather.com/v1?key=${WEATHER_API_KEY}&city=${city}`365);366367// ... rest of logic368}369);370```371372**Best practices:**373- ❌ Never hardcode secrets in code374- ✅ Use `process.env.VAR_NAME`375- ✅ Check if required vars are set376- ✅ Document required vars in `.env.example`377378**Example `.env.example`:**379```bash380# Weather API key (get from weatherapi.com)381WEATHER_API_KEY=382383# Database connection string384DATABASE_URL=385```386387---388389## Performance Patterns390391### Caching392393Cache expensive operations:394395```typescript396const cache = new Map<string, { data: any; expires: number }>();397398server.tool(399{ name: "fetch-weather", schema: z.object({ city: z.string() }) },400async ({ city }) => {401const cacheKey = `weather:${city}`;402const cached = cache.get(cacheKey);403404// Return cached data if not expired405if (cached && cached.expires > Date.now()) {406return object(cached.data);407}408409// Fetch fresh data410const data = await fetchWeather(city);411412// Cache for 5 minutes413cache.set(cacheKey, {414data,415expires: Date.now() + 5 * 60 * 1000416});417418return object(data);419}420);421```422423### Rate Limiting424425Prevent abuse using `hono-rate-limiter`:426427```typescript428import { rateLimiter } from "hono-rate-limiter";429430server.use(rateLimiter({431windowMs: 15 * 60 * 1000, // 15 minutes432limit: 100, // 100 requests per window per key433keyGenerator: (c) =>434c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ??435c.req.header("cf-connecting-ip") ??436c.req.header("x-real-ip") ??437"unknown",438}));439```440441> Adjust the key generator depending on your hosting environment.442443**Note:** mcp-use is built on Hono and supports both Hono-compatible middleware and Express middleware. Express middleware (e.g., `express-rate-limit`, `morgan`) is automatically detected and adapted. For custom middleware or advanced routing, see [../foundations/architecture.md](../foundations/architecture.md).444445---446447## Security Checklist448449Before deploying tools:450451- [ ] All schema fields have `.describe()`452- [ ] Input validation with Zod453- [ ] User input sanitized (no SQL injection, XSS)454- [ ] API keys in environment variables455- [ ] Errors return `error()` helper (not thrown)456- [ ] Try/catch around async operations457- [ ] Rate limiting on expensive operations458- [ ] Destructive operations have `destructiveHint: true`459460---461462## Next Steps463464- **Format responses** → [response-helpers.md](response-helpers.md)465- **Add visual UI** → [../widgets/basics.md](../widgets/basics.md)466- **See examples** → [../patterns/common-patterns.md](../patterns/common-patterns.md)467