function_to_schema()is Milo's single source of truth for MCP input schemas.
There is no parallel model layer: the Python function signature, annotations,
defaults,Annotated[...]metadata, and docstring determine the JSON Schema.
Input Schema
from typing import Annotated
from milo import CLI, Context, Description, MinLen
cli = CLI(name="deploy")
@cli.command("deploy", description="Deploy a service")
def deploy(
environment: Annotated[str, MinLen(1), Description("Target environment")],
service: Annotated[str, MinLen(1)],
version: str = "latest",
ctx: Context = None,
) -> dict[str, str]:
"""Deploy a service.
Args:
service: Service name.
version: Version or image tag.
"""
ctx.log(f"Deploying {service}", level=1)
return {"environment": environment, "service": service, "version": version}
The MCP inputSchema contains environment, service, and version.
ctxis injected by Milo and is intentionally omitted.
Required Fields
| Python parameter | Schema behavior |
|---|---|
name: str |
Required |
name: str = "World" |
Optional with"default": "World" |
name: str | None |
Required unless it has a default; unwrapped to the base type |
name: str | None = None |
Optional with"default": null |
ctx: Context = None |
Omitted from CLI and MCP schemas |
Only JSON-serializable defaults are emitted as schema defaults.
Supported Types
| Python annotation | JSON Schema |
|---|---|
str |
{"type": "string"} |
int |
{"type": "integer"} |
float |
{"type": "number"} |
bool |
{"type": "boolean"} |
list[T] |
{"type": "array", "items": ...} |
tuple[T, ...] |
Array with item schema |
set[T] / frozenset[T] |
Array with"uniqueItems": true |
dict |
{"type": "object"} |
dict[str, T] |
Object withadditionalProperties |
Enum |
String or integer enum, based on member values |
Literal[...] |
{"type": ...,"enum": [...]}when all values share a JSON type |
A | B |
{"anyOf": [...]} |
dataclass |
Object with field properties |
TypedDict |
Object with key properties and required keys |
Unknown annotations fall back to{"type": "string"}and emit a warning unless
strict=Trueis passed.
Annotated Constraints
Usetyping.Annotatedto add JSON Schema constraints without adding a model
class.
| Marker | Schema key |
|---|---|
MinLen(n) |
minLength, or minItemsfor arrays |
MaxLen(n) |
maxLength, or maxItemsfor arrays |
Gt(n) |
exclusiveMinimum |
Lt(n) |
exclusiveMaximum |
Ge(n) |
minimum |
Le(n) |
maximum |
Pattern(regex) |
pattern |
Description(text) |
description |
Docstring parameter descriptions are used when noDescription(...)marker is
present.milo verifywarns when public parameters are undocumented.
Output Schema
return_to_schema() derives an MCP outputSchemafrom the return annotation.
Commands without a return annotation, or with-> None, do not get an
outputSchema.
def status(service: str) -> dict[str, str]:
return {"service": service, "state": "ok"}
For MCP calls, structured returns such as dicts, lists, numbers, and booleans
also appear asstructuredContent in tools/callresponses.
Strict Mode
from milo.schema import function_to_schema
schema = function_to_schema(command, strict=True)
Strict mode raises TypeErrorfor unsupported annotations instead of falling
back to"string". Use it in tests when schema drift would be worse than a hard
failure.
Context Exclusion
A parameter namedctx, or annotated directly as Context, is not part of the
public input schema. Milo injects it at dispatch time for CLI, programmatic, and
MCP calls.
Prefer this form:
def build(output: str = "_site", ctx: Context = None) -> dict[str, str]:
...
Do not expose context-like knobs to agents by adding separate schema fields.