Building Tools
Build MCP tools in TypeScript with full type safety
Anatomy of a Tool
Every MCP tool has these components:
- ID — unique snake_case identifier (e.g.,
hello_world) - Description — tells the AI when and how to use the tool
- Input/Output schemas — Zod schemas for type-safe validation
- Execute handler — the function that runs when the tool is called
_meta.ui.resourceUri(optional) — links the tool to a rich UI- 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:
createToolfrom@decocms/runtime/toolsprovides type-safe tool creation- The tool creator function receives
Env— your app’s typed environment _meta.ui.resourceUrilinks this tool to a rich UI (see Building MCP Apps)annotationstell 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
- Run
bun run devto start the development server - Go to admin.decocms.com
- Add Integration → Custom Integration
- Paste
http://localhost:3001/api/mcp - 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, andopenWorldHintto 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]; Found an error or want to improve this page?
Edit this page