---
title: Building Tools
description: Build MCP tools in TypeScript with full type safety
icon: Wrench
---
import Callout from "../../../../components/ui/Callout.astro";

## Anatomy of a Tool

Every MCP tool has these components:

1. **ID** — unique snake_case identifier (e.g., `hello_world`)
2. **Description** — tells the AI when and how to use the tool
3. **Input/Output schemas** — Zod schemas for type-safe validation
4. **Execute handler** — the function that runs when the tool is called
5. **`_meta.ui.resourceUri`** (optional) — links the tool to a rich UI
6. **Annotations** (optional) — hints about tool behavior (`readOnlyHint`, `destructiveHint`, etc.)

## Basic Example

Here's the `hello_world` tool from the template (`api/tools/hello.ts`):

```typescript
import { createTool } from "@decocms/runtime/tools";
import { z } from "zod";
import type { Env } from "../types/env.ts";

export const HELLO_RESOURCE_URI = "ui://mcp-app/hello";

export const helloInputSchema = z.object({
  name: z.string().describe("The name to greet"),
});

export type HelloInput = z.infer<typeof helloInputSchema>;

export const helloOutputSchema = z.object({
  greeting: z.string(),
  timestamp: z.string(),
});

export type HelloOutput = z.infer<typeof helloOutputSchema>;

export const helloTool = (_env: Env) =>
  createTool({
    id: "hello_world",
    description:
      "Say hello! Takes a name and returns a friendly greeting.",
    inputSchema: helloInputSchema,
    outputSchema: helloOutputSchema,
    _meta: { ui: { resourceUri: HELLO_RESOURCE_URI } },
    annotations: {
      readOnlyHint: true,
      destructiveHint: false,
      idempotentHint: true,
      openWorldHint: false,
    },
    execute: async ({ context }) => {
      const { name } = context;
      return {
        greeting: `Hello, ${name}! Welcome to MCP Apps on deco.`,
        timestamp: new Date().toISOString(),
      };
    },
  });
```

Key points:
- **`createTool`** from `@decocms/runtime/tools` provides type-safe tool creation
- The tool creator function receives `Env` — your app's typed environment
- **`_meta.ui.resourceUri`** links this tool to a rich UI (see [Building MCP Apps](/en/full-code-guides/building-views))
- **`annotations`** tell MCP clients about the tool's behavior

## Linking Tools to UI

When a tool includes `_meta.ui.resourceUri`, MCP clients can render a rich interactive UI alongside the tool result:

```typescript
_meta: { ui: { resourceUri: "ui://mcp-app/hello" } },
```

The URI points to an MCP App resource that serves an HTML bundle. The UI receives the tool's input and result through the MCP Apps SDK and renders an interactive display.

See [Building MCP Apps](/en/full-code-guides/building-views) for building the UI and [Resources](/en/full-code-guides/resources) for serving it.

## Common Patterns

### External API Calls

```typescript
export const weatherTool = (env: Env) =>
  createTool({
    id: "get_weather",
    description: "Get current weather for a city",
    inputSchema: z.object({
      city: z.string().describe("City name"),
    }),
    outputSchema: z.object({
      temperature: z.number(),
      condition: z.string(),
    }),
    annotations: { readOnlyHint: true, openWorldHint: true },
    execute: async ({ context }) => {
      const response = await fetch(
        `https://api.weather.example/v1/current?city=${encodeURIComponent(context.city)}`
      );
      const data = await response.json();
      return {
        temperature: data.temp,
        condition: data.condition,
      };
    },
  });
```

### Using Integrations

Call other MCP servers installed in the same workspace. The `env` is available from the tool factory closure:

```typescript
export const myTool = (env: Env) =>
  createTool({
    id: "my_tool",
    description: "Call an installed integration",
    inputSchema: z.object({ value: z.string() }),
    outputSchema: z.object({ result: z.unknown() }),
    execute: async ({ context }) => {
      const integration = env["my-integration-id"];
      const result = await integration.callTool("some_tool", {
        param: context.value,
      });
      return { result };
    },
  });
```

## Registering Tools

Add your tool creator function to the `tools` array in `api/tools/index.ts`:

```typescript
import { helloTool } from "./hello.ts";
import { weatherTool } from "./weather.ts";

export const tools = [helloTool, weatherTool];
```

Then add the tool creators to the `tools` option in `withRuntime()` in `api/main.ts` (the template already does this via the re-exported array).

## Testing Tools

1. Run `bun run dev` to start the development server
2. Go to [admin.decocms.com](https://admin.decocms.com)
3. **Add Integration** → **Custom Integration**
4. Paste `http://localhost:3001/api/mcp`
5. Test your tool through the admin interface

## Best Practices

- **Single Responsibility** — each tool does one thing well
- **Descriptive Names** — use `snake_case` (e.g., `hello_world`, `get_weather`)
- **Strong Typing** — define Zod schemas for both input and output
- **Annotations** — set `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint` to help MCP clients make better decisions
- **Error Handling** — throw descriptive errors; the runtime wraps them in MCP error responses

## Organizing Tools

Group tools by domain in `api/tools/`:

```
api/tools/
├── index.ts          # Exports all tools
├── hello.ts          # Greeting tool
├── weather.ts        # Weather tools
└── analytics.ts      # Analytics tools
```

Export everything from `index.ts`:

```typescript
import { helloTool } from "./hello.ts";
import { weatherTool } from "./weather.ts";
import { analyticsTool } from "./analytics.ts";

export const tools = [helloTool, weatherTool, analyticsTool];
```
