The @platools.tool() decorator
The @platools.tool() decorator registers a function as an MCP-compliant tool. All arguments are keyword-only, and the decorator always requires parentheses.
Signature (from platools/core/decorator.py):
def tool( *, name: str | None = None, description: str | None = None, auth: AuthLevel = "none", roles: list[str] | tuple[str, ...] | None = None, rate_limit: str | None = None, timeout: int | None = None, annotations: dict[str, Any] | None = None,) -> Callable[[F], F]: ...All arguments are keyword-only. The decorator is called with parentheses every time, even when you don’t pass any arguments:
@platools.tool() # correctdef foo(x: int) -> int: ...
@platools.tool # WRONG - this tries to decorate the factory itselfdef foo(x: int) -> int: ...Parameters
name: str | None
The tool’s external name. Defaults to func.__name__. Must be unique within a Platools instance - duplicates raise ValueError at registration time.
@platools.tool(name="refund.create")def process_refund(order_id: str, reason: str) -> RefundResult: ...description: str | None
The description surfaced to the agent. Defaults to the function’s docstring (short + long descriptions joined, via docstring_parser). Explicit descriptions override the docstring.
auth: AuthLevel
One of "none", "user", "admin". Enforced by the platform before your function is invoked - the SDK only records the declaration. See PRD §5.1.
| Level | Meaning |
|---|---|
"none" | Any caller. Reserved for idempotent, read-only, low-risk tools. |
"user" | Requires an authenticated end user. The user’s JWT is passed through. |
"admin" | Requires a platform-admin role. Combine with roles=["..."] for finer gating. |
Invalid values raise ValueError("auth must be one of ['admin', 'none', 'user'], got ...") immediately at import time - broken auth is never silently shipped.
roles: list[str] | tuple[str, ...] | None
Allowlist of role names the caller must have. Stored on the ToolDef and enforced server-side. Doctor warns when an auth="admin" tool has an empty roles list.
@platools.tool(auth="admin", roles=["billing", "finance"])def freeze_account(account_id: str, reason: str) -> None: ...rate_limit: str | None
Human-readable rate limit hint, e.g. "10/min", "100/hour". Parsed and enforced by the platform; the SDK treats it as an opaque string and passes it through on registration.
timeout: int | None
Max execution time in seconds. Must be positive or None. The platform enforces this across the WebSocket - a tool that runs past timeout gets an error back to the caller.
annotations: dict[str, Any] | None
Free-form metadata attached to the tool schema. Use this for doctor-readable hints like:
@platools.tool( auth="admin", roles=["support"], annotations={"destructive": True, "idempotent": False},)def delete_customer(customer_id: str) -> None: ...Doctor’s check_destructive_annotations rule looks for destructive: true tools without a confirming role or a “confirmation required” field - it will warn if the annotation is missing on a tool whose verb looks destructive (delete_*, freeze_*, cancel_*, etc.).
Typing requirements
Every parameter (except self / cls) must have a type hint. build_input_schema raises SchemaError at decoration time if any parameter is untyped:
@platools.tool()def broken(order_id, reason: str) -> None: # SchemaError on load ...*args and **kwargs are explicitly unsupported - tool signatures must be fully named so the JSON Schema can declare properties and required.
Supported annotation shapes
Everything Pydantic handles, you get for free:
Literal["a", "b"],int | None,Union[...],Optional[...]list[T],dict[K, V],tuple[T, ...]- Nested
BaseModelclasses Annotated[T, Field(description=..., ge=..., le=...)]for inline constraintspydantic.StrictInt,StrictStr, etc.
Docstring Args: descriptions are merged into the generated schema’s properties[*].description automatically - you don’t have to duplicate them in Field(description=...).
Sync or async
Both are supported. The transport layer runs sync tools via asyncio.to_thread() so they don’t block the event loop:
@platools.tool()def sync_tool(x: int) -> int: return x * 2
@platools.tool()async def async_tool(x: int) -> int: await asyncio.sleep(0.01) return x * 2The ToolDef.is_async flag is set automatically via inspect.iscoroutinefunction(func).