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:
It sends a request containing parameters that match the tool’s input schema.
Hyperia validates those parameters against your function signature (types, constraints, defaults).
Your function executes with the validated inputs.
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
@tool
DecoratorCreating 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
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:
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:
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
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