Tools

Tools

Expose Python functions as executable capabilities for your MCP clients.

Tools are the core primitives that let an LLM reach outside its training data: query a database, hit an API, crunch numbers, or open a file. In Hyperia, a tool is just a Python function decorated with @mcp.tool() and surfaced through the Model Context Protocol.


What are Tools?

When an LLM decides to call a tool:

  1. It sends a request containing parameters that match the tool’s input schema.

  2. Hyperia validates those parameters against your function signature (types, constraints, defaults).

  3. Your function executes with the validated inputs.

  4. The result is returned to the LLM, which can use it to craft its next response.

That single loop enables powerful behaviours—calculations, searches, side‑effects—well beyond what the model “knows” natively.


The @tool Decorator

Creating a tool is as simple as decorating a function:

from hyperia import Hyperia

mcp = Hyperia(name="CalculatorServer")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Adds two numbers."""
    return a + b

When registered, Hyperia automatically:

  • Uses the function name (add) as the tool name.

  • Uses the docstring as the description.

  • Generates a JSON input schema from type hints.

  • Handles validation and error reporting.

Note Variadic signatures (*args, **kwargs) are not supported—Hyperia must know every parameter to build a complete schema.


Parameter Basics

Type Annotations

Type hints are required for robust schemas and validation.

@mcp.tool()
def analyze_text(
    text: str,
    max_tokens: int = 100,
    language: str | None = None,
) -> dict:
    """Analyse the provided text."""
    ...

Rich Metadata with Annotated + Field

Use Annotated to attach Pydantic Field metadata—descriptions, ranges, regex, etc.—without polluting the default value column:

from typing import Annotated, Literal
from pydantic import Field

@mcp.tool()
def process_image(
    image_url: Annotated[str, Field(description="URL of the image to process")],
    resize: Annotated[bool, Field(description="Whether to resize the image")] = False,
    width: Annotated[int, Field(description="Target width", ge=1, le=2000)] = 800,
    fmt: Annotated[Literal["jpeg", "png", "webp"], Field(description="Output format")] = "jpeg",
) -> dict:
    """Process an image with optional resizing."""

You can embed Field as the default value, but Annotated keeps type hints clear:

@mcp.tool()
def search_db(
    query: str = Field(description="Search query"),
    limit: int = Field(10, description="Max results", ge=1, le=100),
) -> list:
    ...

Supported Types

Hyperia supports nearly every Pydantic‑compatible type. A non‑exhaustive list:

Category
Examples
Notes

Basic scalars

int, float, str, bool

Binary

bytes

Raw bytes (Base64 handled manually)

Date/Time

datetime, date, timedelta

ISO‑8601 strings auto‑parsed

Collections

list[int], dict[str, float], set[str], tuple[int, int]

Nested combos allowed

Optional / Union

`int

None, str

Constrained

Literal["A", "B"], Enum

Value whitelists

Paths

Path

Auto‑converted from str

UUIDs

UUID

Auto‑converted

Pydantic models

User

Full validation & nested schemas

See Parameter Types below for deep dives and examples.


Optional Arguments

Standard Python rules apply: parameters without defaults are required; those with defaults (or | None) are optional.

@mcp.tool()
def search_products(
    query: str,
    max_results: int = 10,
    sort_by: str = "relevance",
    category: str | None = None,
) -> list[dict]:
    ...

Advanced Decorator Metadata

Override auto‑inferred values or add tags:

@mcp.tool(
    name="find_products",
    description="Search the product catalogue with optional filtering.",
    tags={"catalogue", "search"},
)
def _search_impl(query: str, category: str | None = None) -> list[dict]:
    ...
  • name – Explicit tool identifier exposed via MCP.

  • description – Overrides docstring for LLM visibility.

  • tags – Arbitrary strings clients may use for grouping or filtering.


Async vs Sync Tools

Hyperia is fully async‑aware:

# CPU‑bound / quick
@mcp.tool()
def distance(a: float, b: float) -> float:
    ...

# I/O‑bound
@mcp.tool()
async def fetch_weather(city: str) -> dict:
    async with aiohttp.ClientSession() as s:
        async with s.get(f"https://api.weather/{city}") as resp:
            resp.raise_for_status()
            return await resp.json()

Use async def whenever your tool performs blocking I/O (HTTP, DB, file access) to keep the event loop responsive.


Return Values

Hyperia serialises return values automatically:

Return Type
MCP Content

str

TextContent

dict, list, BaseModel

JSON‑serialised TextContent

bytes

Base64‑encoded BlobResourceContents

hyperia.Image helper

ImageContent

None

No content

Other types are coerced to str if possible.

from hyperia import Hyperia, Image
import io
from PIL import Image as PILImage

mcp = Hyperia("ImageDemo")

@mcp.tool()
def solid_png(width: int, height: int, colour: str) -> Image:
    img = PILImage.new("RGB", (width, height), colour)
    buf = io.BytesIO()
    img.save(buf, format="PNG")
    return Image(data=buf.getvalue(), format="png")

Error Handling

If a tool fails, raise any Python exception or hyperia.ToolError.

  • Standard exceptions → logged; generic error sent to client.

  • ToolError → message forwarded, letting the LLM reason about the cause.

from hyperia import Hyperia, ToolError

mcp = Hyperia("DivideDemo")

@mcp.tool()
def divide(a: float, b: float) -> float:
    if b == 0:
        raise ToolError("Division by zero is not allowed.")
    return a / b

Tool Annotations

Add advisory metadata (does not consume LLM tokens) via annotations=:

@mcp.tool(
    annotations={
        "title": "Calculate Sum",
        "readOnlyHint": True,
        "openWorldHint": False,
    }
)
def calc_sum(a: int, b: int) -> int:
    return a + b
Annotation
Type
Default
Purpose

title

string

UI‑friendly label

readOnlyHint

bool

False

Indicates no state change

destructiveHint

bool

True

If changes are irreversible

idempotentHint

bool

False

Repeat calls ⇒ same effect

openWorldHint

bool

True

Touches external systems


Accessing MCP Context

Inject a Context‑typed param to tap logging, resources, sampling, progress, etc.

from hyperia import Hyperia, Context

mcp = Hyperia("ContextDemo")

@mcp.tool()
async def process_data(uri: str, ctx: Context) -> dict:
    await ctx.info(f"Processing {uri}")
    res = await ctx.read_resource(uri)
    data = res[0].content if res else ""
    await ctx.report_progress(50, 100)
    summary = await ctx.sample(f"Summarise: {data[:200]}")
    await ctx.report_progress(100, 100)
    return {"length": len(data), "summary": summary.text}

See the Context chapter for the full API.


Server Behaviour Controls

Duplicate Tool Names

Configure via on_duplicate_tools= at server creation:

mcp = Hyperia(name="StrictServer", on_duplicate_tools="error")

Options: "warn" (default), "error", "replace", "ignore".

Removing Tools Dynamically

mcp.remove_tool("calculate_sum")

Legacy JSON Parsing

Set HYPERIA_TOOL_ATTEMPT_PARSE_JSON_ARGS=1 to re‑enable old behaviour that auto‑parsed stringified JSON args. Default is off for strict validation.


Deep‑Dive: Parameter Types

Hyperia leans on Pydantic for validation and coercion. Highlights below—see linked sections for more.

Built‑in Scalars

@mcp.tool()
def process_values(name: str, count: int, amount: float, enabled: bool):
    ...

Strings like "42" are coerced to int where appropriate.

Date & Time

from datetime import datetime, date, timedelta

@mcp.tool()
def schedule(event_date: date, event_time: datetime, duration: timedelta = timedelta(hours=1)):
    ...

Collections

@mcp.tool()
def analyse(values: list[float], props: dict[str, str], ids: set[int]):
    ...

Union / Optional

@mcp.tool()
def flexible(query: str | int, filters: dict | None = None):
    ...

Constrained Literals & Enums

from typing import Literal

@mcp.tool()
def sort(data: list[float], order: Literal["asc", "desc"] = "asc"):
    ...
from enum import Enum
class Colour(Enum):
    RED = "red"
    GREEN = "green"

@mcp.tool()
def filter_colour(img: bytes, colour: Colour = Colour.RED):
    ...

Binary Data

Raw bytes or base64 strings (decoded manually):

@mcp.tool()
def process_binary(data: bytes):
    ...

Paths & UUIDs

from pathlib import Path
from uuid import UUID

@mcp.tool()
def operate(path: Path, item_id: UUID):
    ...

Pydantic Models

from pydantic import BaseModel, Field
class User(BaseModel):
    username: str
    email: str = Field(description="Email")

@mcp.tool()
def create_user(user: User):
    ...

Field‑Level Validation Examples

from typing import Annotated
from pydantic import Field

@mcp.tool()
def analyse_metrics(
    count: Annotated[int, Field(ge=0, le=100)],
    ratio: Annotated[float, Field(gt=0, lt=1)],
    user_id: Annotated[str, Field(pattern=r"^[A-Z]{2}\d{4}$")],
    comment: Annotated[str, Field(min_length=3, max_length=500)] = "",
):
    ...

Hyperia returns clear validation errors if input fails constraints.

Last updated