Building Tools
Build MCP tools in TypeScript with full type safety
Examples follow the MCP App starter at commit 3fc9bd15. Start with Project structure for the pinned setup.
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().optional().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;
const displayName = name || "World";
return {
greeting: `Hello, ${displayName}! 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/app.ts (the template already does this via the re-exported array).
Testing Tools
- Run
bun run dev. The local MCP endpoint ishttp://localhost:3001/api/mcp; the template rejects/mcp. - To test from hosted Studio, expose port 3001 through an HTTPS tunnel. The template's
bun run startscript startsdeco linkand the dev server when thedecoCLI is installed; alternatively, use an HTTPS tunnel you already run. - Open Studio and go to Settings → Connections → Add connection → Custom Connection. Select HTTP and paste the tunnel's URL with
/api/mcp. - Configure authentication if your server requires it, then attach the connection and its selected tools in the agent's Settings.
- Call
hello_worldwith a name, check the returned greeting, and confirm its linked UI loads.
A local MCP client running on your machine can use the localhost endpoint directly. The hosted Studio server cannot reach your machine's localhost. See Connections for the shared connection model.
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];