Armory and Replicators: Creating Tools


MCP Deep-Dive Series

🔫 What Are Tools?

In the Model Context Protocol, Tools are executable functions that the AI model can call. Think of them as the USS Enterprise's tactical systems — the phaser array, the shields, the replicator. Each tool is a capability the ship (your server) exposes to Starfleet Command (the LLM client).

Unlike Resources (read-only data feeds — the ship's sensors), Tools perform actions. They fire phasers. They open hailing frequencies. They replicate Earl Grey, hot. The model decides when to invoke them based on the user's request, and your server executes the operation and returns a result.

Key properties of a Tool:

🎯 The @mcp.tool() Decorator — Simple Function Registration

Registering a tool is as simple as decorating a function. The MCP SDK introspects your type annotations and docstring to build the JSON Schema that describes your tool to the client.

from mcp.server.fastmcp import FastMCP

enterprise = FastMCP("USS Enterprise NCC-1701-D")

@enterprise.tool()
def fire_phasers(target: str, power_level: int = 50) -> str:
    """Fire phaser array at specified target. Power level 1-100."""
    if power_level > 80:
        return f'Maximum phaser discharge at {target}. Direct hit!'
    return f'Phaser burst at {target}, power level {power_level}%. Standing by.'

That's it. The fire_phasers function is now available as a tool. The model sees the name, the docstring (as description), and the parameters with their types. When it decides to fire phasers, it sends a tools/call request and your function executes.

🔧 Registering Existing Functions as Tools

Sometimes you already have utility functions written — legacy code from a previous stardate. You don't need to rewrite them. Just register them directly:

def existing_function(system: str, diagnostic_level: int = 1) -> str:
    """Run a diagnostic on a ship system."""
    return f"Level {diagnostic_level} diagnostic on {system}: All systems nominal."

# Register as a tool without modifying the original function
enterprise.tool()(existing_function)

This pattern is useful when you want to expose third-party library functions or keep your tool registration separate from your business logic.

📡 Tools with External API Calls — Hailing Frequencies

Tools can reach out to external services — databases, APIs, other microservices. Think of it as opening hailing frequencies to a Federation starbase:

import requests

@enterprise.tool()
def hail_starbase(starbase_id: int) -> str:
    """Open hailing frequencies to a Federation starbase."""
    response = requests.get(f'https://api.example.com/starbase/{starbase_id}')
    return response.json().get('message', 'No response on subspace frequency.')

The model doesn't know (or care) that your tool is making an HTTP request. It just sees: "I can hail a starbase by providing an ID, and I'll get a message back." The complexity is hidden behind the tool interface.

🚨 Error Handling — Red Alert Conditions

When things go wrong (and in space, they always do), your tools should handle errors gracefully. MCP tools can raise exceptions or return error information — the SDK will translate these into proper MCP error responses:

@enterprise.tool()
def engage_warp(factor: float) -> str:
    """Engage warp drive at the specified warp factor."""
    if factor < 1 or factor > 9.99:
        raise ValueError(f"Warp factor {factor} is outside safe parameters (1-9.99)!")
    if factor > 9.9:
        return f"WARNING: Warp {factor} engaged. Hull stress at critical levels!"
    return f"Warp {factor} engaged. Estimated arrival: 2 hours, 14 minutes."

If a ValueError is raised, the client receives an error response with isError: true and the exception message. The model can then inform the user that something went wrong — Red Alert!

⚖️ Tools vs Resources — Decision Matrix

When should you use a Tool vs a Resource? Here's your tactical decision matrix:

CriterionResource (Sensors)Tool (Weapons/Replicators)
PurposeRead dataPerform actions / compute
Side effectsNone (idempotent reads)May have side effects
Who initiates?Client (application-controlled)Model (LLM-controlled)
ExamplesDatabase records, file contents, configSend email, run query, call API
Star Trek analogyLong-range sensors, ship's computer lookupFire phasers, raise shields, replicate

Rule of thumb: If the operation is safe to repeat and just returns data → Resource. If it does something or requires computation → Tool.

🖥️ The Client Calling Tools Programmatically

On the other side of the subspace channel, a client can discover and call your tools programmatically. Here's how to connect to a server and invoke fire_phasers:

import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    params = StdioServerParameters(command='python', args=['warp_core.py'])
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # Discover available tools
            tools = await session.list_tools()
            print(f"Available weapons systems: {[t.name for t in tools.tools]}")

            # Fire!
            result = await session.call_tool(
                'fire_phasers',
                {'target': 'Borg cube', 'power_level': 95}
            )
            print(result.content[0].text)

asyncio.run(main())
# Output:
# Available weapons systems: ['fire_phasers', 'engage_warp', 'hail_starbase']
# Maximum phaser discharge at Borg cube. Direct hit!

The client spawns your server as a subprocess (stdio transport), initializes the session, lists the tools (like running a systems check), and then calls one. The result comes back as structured content — text, images, or embedded resources.

Make it so. 🖖