Skip to main content
sunpeak API

Overview

Tool files define your MCP tools — metadata, input validation, and request handlers. Each .ts file in src/tools/ is auto-discovered by the framework.

File Convention

src/tools/{tool-name}.ts
The filename (without .ts) becomes the tool name used by the MCP server. For example, src/tools/show-albums.ts registers a tool named show-albums.

Structure

Each tool file exports three things:
// src/tools/show-albums.ts

import { z } from 'zod';
import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';

// 1. Tool metadata (with optional resource name for UI tools)
export const tool: AppToolConfig = {
  resource: 'albums',           // Links to src/resources/albums/ — omit for tools without a UI
  title: 'Show Albums',
  description: 'Show photo albums',
  annotations: { readOnlyHint: true },
  _meta: { ui: { visibility: ['model', 'app'] } },
};

// 2. Input schema (Zod)
export const schema = {
  category: z.string().describe('Filter by category'),
  search: z.string().describe('Search term'),
  limit: z.number().describe('Max albums to return'),
};

// 3. Handler
type Args = z.infer<z.ZodObject<typeof schema>>;

export default async function (args: Args, extra: ToolHandlerExtra) {
  return { structuredContent: { albums: [] } };
}

Exports

tool (AppToolConfig)

resource
string
The resource name, matching a directory in src/resources/ (e.g., 'albums' for src/resources/albums/). Links this tool to its UI. Omit for tools that return data only (no UI).
title
string
Human-readable title for the tool, shown in host UIs.
description
string
Description of what the tool does.
annotations
object
MCP tool annotations.
{
  readOnlyHint?: boolean;
  destructiveHint?: boolean;
  idempotentHint?: boolean;
  openWorldHint?: boolean;
}
_meta
object
Tool metadata, including UI visibility.
{
  ui?: {
    visibility?: ('model' | 'app')[];
  }
}
  • "model" — The AI model can call this tool
  • "app" — The app can call this tool (via useCallServerTool)

schema (Zod record)

A record of Zod types defining the tool’s input parameters. Automatically converted to JSON Schema for the MCP server.
export const schema = {
  query: z.string().describe('Search query'),
  limit: z.number().optional().describe('Max results'),
};

default (handler)

The default export is the tool handler function. It receives the validated input arguments and a ToolHandlerExtra object.
type Args = z.infer<z.ZodObject<typeof schema>>;

export default async function (args: Args, extra: ToolHandlerExtra) {
  // args is typed from your schema
  // extra has authInfo, sessionId, signal
  return { structuredContent: { data: [] } };
}
Return types:
  • { structuredContent: unknown } — Structured data passed to the resource via useToolData()
  • { content: [{ type: 'text', text: string }] } — Text content
  • A plain string is normalized to { content: [{ type: 'text', text }] }

ToolHandlerExtra

The extra parameter provides context from the MCP SDK:
authInfo
AuthInfo
Authentication info from the optional src/server.ts auth function.
sessionId
string
Unique session identifier.
signal
AbortSignal
Abort signal for cancellation.

Tools Without UI

Tools that don’t need a UI can omit the resource field. These are registered as plain MCP tools (no resource or iframe). A common pattern is pairing a UI tool (for review) with a backend-only tool (for execution). The review UI calls the backend tool via useCallServerTool after the user confirms:
// src/tools/review.ts — backend-only, no resource

import { z } from 'zod';
import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';

export const tool: AppToolConfig = {
  title: 'Confirm Review',
  description: 'Execute or cancel a reviewed action after user approval in the review UI',
  annotations: { readOnlyHint: false },
  _meta: { ui: { visibility: ['model', 'app'] } },
};

export const schema = {
  action: z.string().describe('Action identifier (e.g., "place_order", "apply_changes")'),
  confirmed: z.boolean().describe('Whether the user confirmed the action'),
  decidedAt: z.string().describe('ISO timestamp of the decision'),
  payload: z.record(z.unknown()).optional().describe('Domain-specific data for the action'),
};

type Args = z.infer<z.ZodObject<typeof schema>>;

export default async function (args: Args, _extra: ToolHandlerExtra) {
  if (!args.confirmed) {
    return {
      content: [{ type: 'text' as const, text: 'Cancelled.' }],
      structuredContent: { status: 'cancelled', message: 'Cancelled.' },
    };
  }

  // In production, dispatch to your domain logic based on args.action
  return {
    content: [{ type: 'text' as const, text: 'Completed.' }],
    structuredContent: { status: 'success', message: 'Completed.' },
  };
}
The UI tool (review-purchase) returns structuredContent with a reviewTool field that tells the review resource which backend tool to call on confirm/cancel:
// In the review-purchase handler's response:
return {
  structuredContent: {
    title: 'Confirm Your Order',
    sections: [/* ... */],
    acceptLabel: 'Place Order',
    reviewTool: {
      name: 'review',
      arguments: { action: 'place_order', payload: { orderId: 'order_123' } },
    },
  },
};
When the user clicks “Place Order”, the review resource calls useCallServerTool with the arguments plus confirmed: true. The tool returns both content (human-readable text for the host model) and structuredContent (with status and message for the UI). The review resource reads structuredContent.status to determine success/error styling and displays structuredContent.message. The same review tool handles all review variants — purchases, code diffs, social posts — differentiated by the action field. See the template’s review resource for the full implementation. Tools without a UI are hidden from the inspector’s simulation selector (there’s nothing to render). They are still registered in the dev MCP server, so they can be called by UI resources via useCallServerTool or by the host directly. In production, they work as standard MCP tools.

Multiple Tools Per Resource

Multiple tool files can reference the same resource by name:
src/resources/review/
  review.tsx              # Shared resource (directory name: 'review')

src/tools/
  review-diff.ts          # resource: 'review'
  review-post.ts          # resource: 'review'
  review-purchase.ts      # resource: 'review'
Each tool provides different data to the same UI via useToolData().

See Also

Server Entry

Optional auth and server configuration.

Simulation

Define test fixtures for your tools.