registerTool
Register an MCP tool on the server. Provide a view to bind the tool to a React view; omit view for a plain tool with no UI.
Signature
Section titled “Signature”server.registerTool( config: ToolConfig, handler: ToolHandler,): McpServerConfig
Section titled “Config”type ToolConfig = { name: string; // Tool name, exposed to the model title?: string; // Human-readable title description?: string; // Shown to the model inputSchema?: ZodRawShape; // Input validation (Zod) outputSchema?: ZodRawShape;// Output type hints (Zod) annotations?: ToolAnnotations; view?: ViewConfig; // ← present = view-tool, absent = plain tool securitySchemes?: SecurityScheme[]; // Declare per-tool auth requirements _meta?: ToolMeta;};
type ToolMeta = { // Enable view-initiated tool calls (see useCallTool) "openai/widgetAccessible"?: boolean; // Custom status messages "openai/toolInvocation/invoking"?: string; // While running "openai/toolInvocation/invoked"?: string; // After completion "openai/fileParams"?: string[]; // List of top-level input fields that references files. // Controls whether a tool is available to the model, the UI (app), or both. ui?: { visibility?: Array<"model" | "app"> }; // Mirror of ToolConfig.securitySchemes securitySchemes?: SecurityScheme[]; [key: string]: unknown;};
type SecurityScheme = | { type: "noauth" } | { type: "oauth2"; scopes?: string[] };Binds the tool to a React view.
type ViewConfig = { component: ViewName; // File name in src/views/ (typed — see below) description?: string; // Shown during view discovery hosts?: ("apps-sdk" | "mcp-app")[]; // Defaults to both prefersBorder?: boolean; // Removes the default iframe border when false domain?: string; // Custom view domain csp?: { resourceDomains?: string[]; // Static assets (images, fonts, scripts) connectDomains?: string[]; // Fetch / XHR targets frameDomains?: string[]; // Iframe embed origins redirectDomains?: string[]; // Allowed external redirects (skips safe-link modal) baseUriDomains?: string[]; // `<base href>` origins (mcp-apps only) }; _meta?: Record<string, unknown>;};See CSP & Domains below.
securitySchemes
Section titled “securitySchemes”Declare which auth schemes this tool supports so clients can label it as public vs. requires-sign-in before invocation, and request the right OAuth scopes during sign-in.
type SecurityScheme = | { type: "noauth" } | { type: "oauth2"; scopes?: string[] };- Across the array: match ANY. Each entry is an alternative. The request is authorized if any one scheme is satisfied.
- Within an
oauth2entry’sscopes: match ALL. The validated token must carry every listed scope. - Listing
noauth+oauth2means “works anonymously, but auth gives more features.”
Examples
Section titled “Examples”Protected tool:
const SEARCH_SCOPES = ["search.read"];
server.registerTool( { name: "search-private-docs", description: "Search documents in the user's workspace.", inputSchema: { q: z.string() }, securitySchemes: [{ type: "oauth2", scopes: SEARCH_SCOPES }], }, async ({ q }, extra) => { if (!extra.authInfo) { return { content: "Sign in required.", isError: true }; } const missing = SEARCH_SCOPES.filter( (s) => !extra.authInfo!.scopes.includes(s), ); if (missing.length > 0) { return { content: `Missing scope(s): ${missing.join(", ")}.`, isError: true, }; } return { content: await search(q) }; },);Public tool:
server.registerTool( { name: "search-public-docs", description: "Search public documents — no sign-in required.", inputSchema: { q: z.string() }, securitySchemes: [{ type: "noauth" }], }, async ({ q }) => ({ content: await search(q) }),);Anonymous OR authenticated (single tool, different behavior):
server.registerTool( { name: "search", description: "Search docs. Authenticated users see their private workspace too.", inputSchema: { q: z.string() }, securitySchemes: [ { type: "noauth" }, { type: "oauth2", scopes: ["search.read"] }, ], }, async ({ q }, extra) => { const includePrivate = !!extra.authInfo; return { content: await search(q, { includePrivate }) }; },);For the transport-level middleware that pairs with mixed-auth tools, see optionalBearerAuth.
Handler
Section titled “Handler”type ToolHandler = ( input: Input, extra: RequestHandlerExtra,) => Promise<{ content?: string | ContentBlock | ContentBlock[]; // Text(s) for the model structuredContent?: StructuredData; // Data for the view _meta?: Record<string, unknown>; // Response metadata}>;content can be a plain string, a single ContentBlock, or an array of ContentBlocks. See the different content helpers that frac provides.
The extra parameter exposes:
extra.authInfo— populated by auth middleware (e.g.authInfo.clientId,authInfo.extra.email)extra.requestInfo— HTTP request details (headers, URL)extra.signal— abort signal for cancellationextra._meta— client-supplied hints injected by the host (see Client hints on_meta)
Client hints on _meta
Section titled “Client hints on _meta”ChatGPT may inject context about the current conversation in tool calls. frac types these fields on extra._meta so you can use them in the handler:
type ClientHintsMeta = { // Requested locale (BCP-47, e.g. "en-US") "openai/locale"?: string; // Browser user-agent string of the ChatGPT client "openai/userAgent"?: string; // Coarse user location "openai/userLocation"?: { city?: string; region?: string; country?: string; timezone?: string; longitude?: number; latitude?: number; }; // Anonymized user id (for rate limiting / identification) "openai/subject"?: string; // Anonymized conversation id, stable within a ChatGPT session "openai/session"?: string; // Anonymized organization id, when the user account is part of an organization "openai/organization"?: string; // Stable id for the currently mounted widget instance "openai/widgetSessionId"?: string;};server.registerTool( { name: "weather", description: "Show weather for a location.", inputSchema: { city: z.string().optional() }, }, async ({ city }, extra) => { // Fall back to the user's current city when they didn't specify one. const target = city ?? extra._meta?.["openai/userLocation"]?.city ?? "Paris"; // Imperial for en-US, metric everywhere else. const locale = extra._meta?.["openai/locale"] ?? "en-US"; const units = locale === "en-US" ? "imperial" : "metric";
const weather = await fetchWeather(target, units); return { content: `${target}: ${weather.temperature}° ${weather.conditions}`, }; },);Response Fields
Section titled “Response Fields”Three fields, three audiences:
| Field | Purpose | Consumed by |
|---|---|---|
content | Text description | The host (shown in conversation) |
structuredContent | Typed data | Host and view (useToolInfo, useCallTool) |
_meta | Response metadata | View only (hidden from the model) |
See Data Flow: Response Fields.
Examples
Section titled “Examples”Tool with a view
Section titled “Tool with a view”File src/views/greeting.tsx exports the React component. view.component matches its file name.
import { McpServer } from "@usefractal/frac/server";import { z } from "zod";
const server = new McpServer({ name: "my-app", version: "0.0.1" }, {}) .registerTool( { name: "greet", description: "Greet someone by name.", inputSchema: { name: z.string() }, view: { component: "greeting", description: "A greeting card" }, }, async ({ name }) => { return { content: `Greeting ${name}`, structuredContent: { message: `Hello, ${name}!` }, }; }, );Tool without view
Section titled “Tool without view”server.registerTool( { name: "calculate", description: "Evaluate an arithmetic expression.", inputSchema: { expression: z.string() }, }, async ({ expression }) => { const value = evaluate(expression); return { content: `Result: ${value}` }; },);With external API and CSP
Section titled “With external API and CSP”server.registerTool( { name: "weather", description: "Show weather for a location.", inputSchema: { city: z.string(), }, outputSchema: { temperature: z.number(), conditions: z.string(), humidity: z.number(), }, view: { component: "weather", }, }, async ({ city }) => { const weather = await fetchWeather(city); return { content: `Current weather in ${city}: ${weather.temperature}°, ${weather.conditions}`, structuredContent: weather, }; },);With auth context
Section titled “With auth context”Pull authentication info from extra (requires auth middleware via .use()):
server.registerTool( { name: "user-profile", description: "Show user profile.", inputSchema: { userId: z.string() }, view: { component: "user-profile" }, }, async ({ userId }, extra) => { const email = extra.authInfo?.extra?.email; const user = await getUser(userId); if (!user) { return { content: "Error: User not found", isError: true, }; }
return { content: `Showing profile for ${user.name}`, structuredContent: { ...user, viewedBy: email }, }; },);Content helpers
Section titled “Content helpers”@usefractal/frac/server exports helpers for building ContentBlocks:
import { audio, embeddedResource, image, resourceLink, text } from "@usefractal/frac/server";
return { content: [ text("Here's your chart:"), image(pngBuffer, "image/png"), ], structuredContent: { /* … */ },};| Helper | Block type |
|---|---|
text(value, annotations?) | TextContent |
image(data, mimeType, annotations?) | ImageContent (base64-encoded) |
audio(data, mimeType, annotations?) | AudioContent (base64-encoded) |
embeddedResource(resource, annotations?) | EmbeddedResource |
resourceLink(link, annotations?) | ResourceLink |
You can still return plain ContentBlock objects — helpers are opt-in.
Input Schema
Section titled “Input Schema”Define expected inputs with Zod:
inputSchema: { query: z.string().describe("Search query"), limit: z.number().optional().default(10), sortBy: z.enum(["price", "rating", "distance"]), filters: z.object({ minPrice: z.number().optional(), maxPrice: z.number().optional(), }).optional(), amenities: z.array(z.string()).optional(),}Output Schema
Section titled “Output Schema”Optional type hints:
outputSchema: { results: z.array(z.object({ id: z.string(), name: z.string(), price: z.number(), })), totalCount: z.number(),}CSP & Domains
Section titled “CSP & Domains”Views run in a sandboxed iframe with strict Content Security Policy. If your view needs to:
- Fetch data from external APIs → add to
connectDomains - Load images, fonts, or scripts from CDNs → add to
resourceDomains - Embed external iframes → add to
frameDomains - Redirect to external sites → add to
redirectDomains
Configuration
Section titled “Configuration”server.registerTool( { name: "flight-search", description: "Search for flights", inputSchema: { /* … */ }, view: { component: "flight-search", prefersBorder: false, csp: { connectDomains: ["https://api.flights.com"], resourceDomains: ["https://cdn.flights.com", "https://fonts.googleapis.com"], frameDomains: ["https://maps.google.com"], redirectDomains: ["https://booking.flights.com"], }, }, }, async (input) => { /* … */ },);CSP Fields
Section titled “CSP Fields”| Field | Purpose | Example |
|---|---|---|
connectDomains | Fetch/XHR targets | ["https://api.example.com"] |
resourceDomains | Static assets (images, fonts, scripts, styles) | ["https://cdn.example.com"] |
frameDomains | Iframe embed origins | ["https://youtube.com"] |
redirectDomains | External redirect destinations | ["https://checkout.example.com"] |
baseUriDomains | <base href> origins (mcp-apps only) | ["https://app.example.com"] |
Related
Section titled “Related”- McpServer - Server class
- useToolInfo - Access view data
- Data Flow - The communication loop
- Type Safety - End-to-end inference