Full-Code Guides

Building Tools

Build MCP tools in TypeScript with full type safety

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 ):

 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)
  • 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:

 _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 for building the UI and Resources for serving it.

Common Patterns

External API Calls

 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:

 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 :

 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
  3. Add IntegrationCustom 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 :

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

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

Found an error or want to improve this page?

Edit this page