> ## Documentation Index
> Fetch the complete documentation index at: https://sunpeak.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Tool File

> Define tool metadata, schemas, and handlers.

<Badge color="yellow">sunpeak API</Badge>

## Overview

Tool files define your [MCP tools](/mcp-apps/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:

```ts theme={null}
// 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)

<ResponseField name="resource" type="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).
</ResponseField>

<ResponseField name="title" type="string">
  Human-readable title for the tool, shown in host UIs.
</ResponseField>

<ResponseField name="description" type="string">
  Description of what the tool does.
</ResponseField>

<ResponseField name="annotations" type="object">
  [MCP tool annotations](/mcp-apps/mcp/tools#annotations).

  ```typescript theme={null}
  {
    readOnlyHint?: boolean;
    destructiveHint?: boolean;
    idempotentHint?: boolean;
    openWorldHint?: boolean;
  }
  ```
</ResponseField>

<ResponseField name="_meta" type="object">
  Tool metadata, including UI visibility.

  ```typescript theme={null}
  {
    ui?: {
      visibility?: ('model' | 'app')[];
    }
  }
  ```

  * `"model"` — The AI model can call this tool
  * `"app"` — The app can call this tool (via `useCallServerTool`)
</ResponseField>

### `schema` (Zod record)

A record of Zod types defining the tool's input parameters. Automatically converted to JSON Schema for the MCP server.

```ts theme={null}
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.

```ts theme={null}
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:

<ResponseField name="authInfo" type="AuthInfo">
  Authentication info from the optional `src/server.ts` auth function.
</ResponseField>

<ResponseField name="sessionId" type="string">
  Unique session identifier.
</ResponseField>

<ResponseField name="signal" type="AbortSignal">
  Abort signal for cancellation.
</ResponseField>

## 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`](/app-framework/hooks/use-call-server-tool) after the user confirms:

```ts theme={null}
// 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:

```ts theme={null}
// 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 still appear in the inspector's tool picker. Selecting one shows `Tool does not render a UI` in the preview, while the sidebar still lets you edit Tool Input, click **Run**, and inspect Tool Result. They are 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

<Card horizontal title="Server Entry" icon="server" href="/app-framework/tools/server-entry">
  Optional auth and server configuration.
</Card>

<Card horizontal title="Simulation" icon="code" href="/testing/simulations">
  Define test fixtures for your tools.
</Card>
