Tools are the primary way MCP clients interact with your server. They represent functions that can be invoked with parameters and return results. This guide covers everything you need to know about creating powerful and reliable tools.
Response Helpers: Throughout this guide, you’ll see code examples using response helpers like text(), object(), and mix(). These utilities simplify creating tool responses with proper typing and metadata. See Response Helpers for a complete reference.
Tools use Zod schemas for input validation. The server automatically validates inputs before calling your handler, so you can trust that the parameters match your schema.
import { z } from 'zod';server.tool({ name: 'process_message', description: 'Process a message with various options', schema: z.object({ // Required string message: z.string().describe('The message to process'), // Optional number with default count: z.number().default(10).describe('Number of items'), // Optional boolean with default verbose: z.boolean().default(false).describe('Enable verbose output'), // Optional object config: z.object({ setting1: z.string(), setting2: z.number() }).optional().describe('Configuration object'), // Array of strings items: z.array(z.string()).describe('List of items to process') })}, async ({ message, count, verbose, config, items }) => { // message, count, verbose, config, items are fully typed and validated // count will be 10 if not provided // verbose will be false if not provided return text('Results...')})
This is the recommended way to expose widgets to a model. Since exposeAsTool defaults to false, widgets are registered as MCP resources only — defining a custom tool that calls widget() gives you full control over the tool’s name, description, schema, and business logic.
You must include the widget: { name, ... } config in your tool definition when returning widgets. This sets up all the registration-time metadata needed for proper widget rendering. The widget file must exist in your resources/ folder, but does not need exposeAsTool: true — leaving it unset (or false) is the correct setup for this pattern.
import { widget, text } from 'mcp-use/server';import { z } from 'zod';server.tool({ name: 'get-weather', description: 'Get current weather for a city', schema: z.object({ city: z.string().describe('City name') }), // Widget config sets all registration-time metadata widget: { name: 'weather-display', // Must match a widget in resources/ invoking: 'Fetching weather...', invoked: 'Weather loaded' }}, async ({ city }) => { // Fetch weather data const weatherData = await fetchWeather(city); // Return widget with runtime data only return widget({ props: weatherData, output: text(`Weather in ${city}: ${weatherData.temp}°C`), message: `Current weather in ${city}` });});
How it works:
widget: { name, invoking, invoked, ... } on tool definition - Configures all widget metadata at registration time
Recommended Approach: The recommended way to expose widgets to a model is to use the widget helper in a custom tool as shown above. Alternatively, you can set exposeAsTool: true in your widget’s metadata to auto-register it as a tool. The manual approach below is shown for reference.
The error() helper provides a standardized way to return error responses from tools. It sets the isError flag to true, allowing clients to distinguish between successful results and error conditions. This ensures consistent error handling across your MCP server.The error() helper creates a properly formatted error response with:
isError: true flag to indicate failure
Text content with your error message
Proper MIME type metadata
import { object, error } from 'mcp-use/server';server.tool({ name: 'external_api', description: 'Call external API', schema: z.object({ endpoint: z.string().url().describe('The API endpoint URL') })}, async ({ endpoint }) => { try { const data = await callExternalAPI(endpoint); return object(data); } catch (err) { // Use error() helper to signal failure to the client return error( `Unable to fetch data from ${endpoint}.\n` + `Error: ${err.message}\n` + `Please check the endpoint and try again.` ); }})
ctx.client.user() returns normalized metadata from params._meta that some clients send with every tools/call request. It returns undefined for clients that do not include this metadata (Inspector, Claude Desktop, CLI, etc.).
This data is client-reported and unverified. Do not use it for access control. For verified identity, configure OAuth authentication and use ctx.auth.
Stable opaque user identifier (same across conversations)
conversationId
string
Current chat thread ID (changes per chat)
locale
string
BCP-47 locale, e.g. "it-IT" — server-side, set at session start (see note below)
location
object
{ city, region, country, timezone, latitude, longitude }
userAgent
string
Browser / host user-agent string
timezoneOffsetMinutes
number
UTC offset in minutes
locale vs useWidget().locale: ctx.client.user()?.locale is detected server-side from the user’s ChatGPT account language at session start. Inside a widget, useWidget().locale is the preferred alternative — it reads the same preference client-side (from window.openai.locale for the Apps SDK, or HostContext.locale for SEP-1865 hosts) and is therefore fresher and browser-aware. The values are usually the same but can differ when the account language differs from the browser language.
ChatGPT establishes a single MCP session for all users of a deployed app. The MCP session ID alone is not enough to identify individual callers — use ctx.client.user() for that:
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
server.tool({ name: 'identify-caller', schema: z.object({}) }, async (_p, ctx) => { 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 });});
Tools can send log messages to clients during execution using ctx.log(). This is useful for reporting progress, debugging tool behavior, and providing real-time feedback during long-running operations.
The ctx.log() function accepts a log level ('debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'), a message string, and an optional logger name. See Server Logging for complete documentation on log levels and best practices.