How to Add an MCP Tool
Benchmarked against: Anthropic — How to implement tool use Protocol: Model Context Protocol (MCP) specification Applies to: All SuperPortia MCP servers
This guide walks through adding a new tool to an existing MCP server. Whether you're adding a new UB operation, a fleet management command, or a utility function, the process follows the same pattern.
Prerequisites
Before adding a tool, verify:
- The tool belongs in an existing server — Check MCP Servers to see if an appropriate server already exists
- No duplicate exists — Search existing tools in the MCP Tools Overview to avoid redundancy
- The tool has a clear purpose — One tool, one responsibility. Don't combine unrelated operations
MCP tool anatomy
Every MCP tool has three parts:
┌─────────────────────────────────────┐
│ Tool Definition │
│ ├─ name: "search_brain" │
│ ├─ description: "Search UB..." │
│ └─ inputSchema: { JSON Schema } │
│ │
│ Tool Handler │
│ └─ async function(params) → result │
│ │
│ Tool Registration │
│ └─ server.tool("name", handler) │
└─────────────────────────────────────┘
1. Tool definition (JSON Schema)
The tool definition tells the AI agent what the tool does and what parameters it accepts:
{
"name": "get_entry",
"description": "Get a single entry by its ID.\n\nArgs:\n entry_id: The entry ID (e.g., 'ub-0001-a1b2c3d4')\n\nReturns:\n JSON object with full entry details, or error message",
"inputSchema": {
"type": "object",
"properties": {
"entry_id": {
"type": "string",
"description": "The entry ID"
}
},
"required": ["entry_id"]
}
}
Description best practices:
| Do | Don't |
|---|---|
| Explain what the tool does in the first line | Write vague descriptions ("does stuff") |
| Document all parameters with types and examples | Leave parameters undocumented |
| Describe the return format | Omit return value description |
| Note any side effects | Hide that the tool modifies state |
Use Args: and Returns: sections | Use inconsistent formatting |
2. Tool handler (implementation)
The handler is the function that executes when the agent calls the tool:
# Python MCP server example (Local UBI style)
async def handle_get_entry(entry_id: str) -> str:
"""Get a single entry by its ID."""
try:
entry = await db.get_entry(entry_id)
if not entry:
return json.dumps({"error": f"Entry {entry_id} not found"})
return json.dumps(entry, default=str)
except Exception as e:
return json.dumps({"error": str(e)})
// TypeScript MCP server example (Cloud UB / Cloudflare Workers style)
async function handleGetEntry(entryId: string, env: Env): Promise<string> {
const entry = await env.D1.prepare(
"SELECT * FROM entries WHERE entry_id = ?"
).bind(entryId).first();
if (!entry) {
return JSON.stringify({ error: `Entry ${entryId} not found` });
}
return JSON.stringify(entry);
}
Handler best practices:
| Practice | Why |
|---|---|
| Always return JSON strings | Consistent parsing by AI agent |
| Include error handling with descriptive messages | Agent needs to understand failures |
| Validate inputs early | Prevent downstream errors |
| Keep handlers focused | One tool = one operation |
| Log important operations | Debugging and audit trail |
3. Tool registration
Register the tool with the MCP server:
# Python (stdio MCP server)
@server.tool()
async def get_entry(entry_id: str) -> str:
"""Get a single entry by its ID."""
return await handle_get_entry(entry_id)
// TypeScript (Cloudflare Worker MCP server)
server.tool("get_entry", {
description: "Get a single entry by its ID.",
inputSchema: { /* JSON Schema */ },
handler: async (params) => handleGetEntry(params.entry_id, env)
});
Step-by-step: Adding a tool to Local UBI
The Local UBI MCP server is a Python stdio server. To add a new tool:
Step 1: Define the function
Add your handler function in the appropriate module within the UBI codebase:
async def handle_new_tool(param1: str, param2: int = 10) -> str:
"""
What this tool does.
Args:
param1: Description of param1
param2: Optional, defaults to 10
Returns:
JSON with result details
"""
# Implementation here
result = {"status": "ok", "data": "..."}
return json.dumps(result, default=str)
Step 2: Register with the MCP server
Add the tool registration in the server's tool list:
@server.tool()
async def new_tool(param1: str, param2: int = 10) -> str:
"""What this tool does.
Args:
param1: Description
param2: Optional param (default 10)
Returns:
JSON with result
"""
return await handle_new_tool(param1, param2)
Step 3: Test locally
# Start the MCP server in debug mode
python -m ubi.mcp_server --debug
# Or test via Claude Code by calling the tool
Step 4: Update documentation
- Add the tool to MCP Tools Overview in the appropriate category
- If it's a significant new capability, add a dedicated tool page
- Update the MCP server page if the tool count or capabilities changed
Step-by-step: Adding a tool to Cloud UB
The Cloud UB MCP server is a Cloudflare Worker (TypeScript). To add a new tool:
Step 1: Add the handler in worker.js
// In the appropriate section of worker.js
async function handleNewTool(params: any, env: Env): Promise<Response> {
const { param1, param2 = 10 } = params;
// Implementation
const result = await env.D1.prepare("...").bind(param1).all();
return Response.json({
success: true,
data: result.results
});
}
Step 2: Add the route
// In the router section
case '/brain/new-tool':
return handleNewTool(body, env);
Step 3: Register the MCP tool definition
Add the tool to the MCP tool list exposed by the Cloud UB server so agents can discover and call it.
Step 4: Deploy and test
# Deploy to Cloudflare
npx wrangler deploy
# Test via curl
curl -X POST https://cloud-ub.superportia.workers.dev/brain/new-tool \
-H "Content-Type: application/json" \
-d '{"param1": "test"}'
Naming conventions
| Convention | Example | Why |
|---|---|---|
| Use snake_case | search_brain, get_entry | MCP standard, consistent across Python/TS |
| Start with verb | create_, get_, list_, update_, delete_ | Clear action intent |
| Include the domain | search_brain not just search | Avoids collision across servers |
| No abbreviations | get_work_order_detail not get_wo_dtl | Agents read descriptions, but clear names help |
Common patterns
Read-only tools
# Simple query, no side effects
async def get_stats() -> str:
stats = await db.get_stats()
return json.dumps(stats)
State-changing tools
# Modifies data — include confirmation in response
async def update_status(order_id: str, new_status: str) -> str:
result = await db.update_wo_status(order_id, new_status)
return json.dumps({
"success": True,
"order_id": order_id,
"old_status": result["old"],
"new_status": result["new"],
"timestamp": get_taipei_time()
})
Tools with validation
# Validate inputs before processing
VALID_STATUSES = ["pending", "accepted", "in_progress", "blocked", "review"]
async def update_status(order_id: str, new_status: str) -> str:
if new_status not in VALID_STATUSES:
return json.dumps({
"error": f"Invalid status '{new_status}'",
"valid_statuses": VALID_STATUSES
})
# ... proceed
Token cost awareness
Every tool definition consumes tokens in the agent's context. With ~35 tools across servers, tool descriptions alone use ~4,000+ tokens.
Minimize token cost:
| Strategy | How |
|---|---|
| Concise descriptions | Say what the tool does in 1–2 sentences |
| Essential parameters only | Don't expose internal implementation details |
| Role-based tool assignment | Give each agent only the tools it needs |
| Progressive loading | Load specialized tools on demand, not at startup |
Related pages
| Page | Relationship |
|---|---|
| MCP Tools Overview | Complete tool catalog |
| MCP Servers | Server configuration and deployment |
| Agent Intelligence Protocol | How agents decide which tools to use |
| Cost Awareness | Token cost implications of tool definitions |