Skip to main content

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:

  1. The tool belongs in an existing server — Check MCP Servers to see if an appropriate server already exists
  2. No duplicate exists — Search existing tools in the MCP Tools Overview to avoid redundancy
  3. 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:

DoDon't
Explain what the tool does in the first lineWrite vague descriptions ("does stuff")
Document all parameters with types and examplesLeave parameters undocumented
Describe the return formatOmit return value description
Note any side effectsHide that the tool modifies state
Use Args: and Returns: sectionsUse 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:

PracticeWhy
Always return JSON stringsConsistent parsing by AI agent
Include error handling with descriptive messagesAgent needs to understand failures
Validate inputs earlyPrevent downstream errors
Keep handlers focusedOne tool = one operation
Log important operationsDebugging 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

ConventionExampleWhy
Use snake_casesearch_brain, get_entryMCP standard, consistent across Python/TS
Start with verbcreate_, get_, list_, update_, delete_Clear action intent
Include the domainsearch_brain not just searchAvoids collision across servers
No abbreviationsget_work_order_detail not get_wo_dtlAgents 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:

StrategyHow
Concise descriptionsSay what the tool does in 1–2 sentences
Essential parameters onlyDon't expose internal implementation details
Role-based tool assignmentGive each agent only the tools it needs
Progressive loadingLoad specialized tools on demand, not at startup

PageRelationship
MCP Tools OverviewComplete tool catalog
MCP ServersServer configuration and deployment
Agent Intelligence ProtocolHow agents decide which tools to use
Cost AwarenessToken cost implications of tool definitions