Function → JSON Schema, once, everywhere.
Python's answer to the gap TypeScript solved with Zod + Standard Schema. Write a typed function. Export one schema. Use it in OpenAI, Anthropic, Gemini, MCP, LangChain, FastMCP, and Pydantic AI — without rewriting.
Every agent framework generates tool JSON differently. FastMCP is MCP-only. LangChain infers its own schema. OpenAI strict mode wants every field required. Claude Desktop breaks on $ref. toolschema is Layer 1 only: introspect a function once, adapt at the edge.
pip install toolschemaExtras:
pip install toolschema[fastmcp] # FastMCP
pip install toolschema[langchain] # LangChain StructuredTool
pip install toolschema[openai-agents] # OpenAI Agents SDK
pip install toolschema[pydantic-ai] # Pydantic AI Tool.from_schema
pip install toolschema[all] # all integrationsRequires Python 3.10+. Core depends on stdlib only (typing_extensions on 3.10).
Documentation: https://toolschema.readthedocs.io
from typing import Annotated
from toolschema import tool, schema, Field
@tool
def get_weather(
city: Annotated[str, Field(description="City name")],
units: str = "celsius",
) -> dict:
"""Get current weather for a city."""
return {"city": city, "temp": 22, "units": units}
t = schema(get_weather)
t.to_openai()
t.to_mcp()
t.to_anthropic()Works without @tool on any typed function with a docstring:
def add(a: int, b: int = 1) -> int:
"""Add two integers."""
return a + b
t = schema(add)from fastmcp import FastMCP
from toolschema import schema
from toolschema.integrations.fastmcp import register_tool
from myapp.tools import greet, add
mcp = FastMCP("my-server")
register_tool(mcp, schema(greet), greet)
register_tool(mcp, schema(add), add)
mcp.run()from toolschema import schema
from toolschema.integrations.langchain import from_toolschema
from myapp.tools import search
definition = schema(search)
tool = from_toolschema(definition, search)
result = tool.invoke({"query": "laptop", "limit": 5})from toolschema import schema
from toolschema.integrations.openai_agents import to_agents_function_tool
from myapp.tools import add
definition = schema(add)
agents_tool = to_agents_function_tool(definition, add)from pydantic_ai import Agent
from toolschema import schema
from toolschema.integrations.pydantic_ai import from_toolschema
from myapp.tools import add
definition = schema(add)
tool = from_toolschema(definition, add)
agent = Agent("openai:gpt-4o", tools=[tool])toolschema inspect myapp.tools:search --format mcp
toolschema inspect myapp.tools:search --format openai,mcp,anthropic
toolschema diff myapp.tools:search --targets openai,mcp
toolschema export myapp.tools
toolschema init my-mcp-serverBuild the canonical tool IR from a function signature, type hints, defaults, and docstring.
Required
Type: Callable
Any typed callable, or @tool-decorated function.
from toolschema import schema
t = schema(my_function)
t.name
t.description
t.parameters # JSON Schema 2020-12 object
t.output # return type schema, or NoneOptional decorator. Overrides name or description; does not change call semantics.
@tool(name="web_search", description="Search the web.")
def search(query: str) -> list[dict]: ...Metadata for Annotated parameters. Maps to JSON Schema constraints.
from typing import Annotated
from toolschema import Field
city: Annotated[str, Field(description="City name", min_length=1)]Plain string in Annotated sets description only: Annotated[str, "City name"].
OpenAI function-calling payload: {"type": "function", "function": {...}}.
Type: boolean
Default: false
When true, sets additionalProperties: false and lists every property in required.
Anthropic Messages API shape: {"name", "description", "input_schema"}.
Numeric and string constraints (minLength, pattern, etc.) are folded into property description text.
MCP tools/list shape: name, description, inputSchema, optional outputSchema.
Type: boolean
Default: true
When true, flatten $ref / $defs before return. Required for Claude Desktop and VS Code Copilot.
Google Gemini FunctionDeclaration shape. JSON Schema type values are uppercased (STRING, INTEGER, …). Parameters only; no output schema in v1.0.
Thin argument check against parameters. Returns ValidationSuccess with defaults applied, or ValidationFailure with ValidationIssue list.
from toolschema import ValidationSuccess
result = t.validate({"city": "London"})
if isinstance(result, ValidationSuccess):
get_weather(**result.value)Register a pre-built ToolDefinition on a FastMCP server without regenerating schema inside FastMCP.
Required
Type: fastmcp.FastMCP
Required
Type: ToolDefinition
Required
Type: Callable
Build a StructuredTool with infer_schema=False and args_schema=tool.parameters.
Build an OpenAI Agents SDK FunctionTool with on_invoke_tool wired to fn.
Also: function_tool_kwargs(), to_openai_agent_tool(), invoke_agents_tool(), invoke_agents_tool_sync().
Build a Pydantic AI Tool via Tool.from_schema() and the pre-built JSON Schema.
Also: to_pydantic_ai_tool(), prepare_toolset().
Scaffold an MCP server project from the packaged template. Runs slugify_package_name() on the directory name for the Python package.
Internal dialect: JSON Schema 2020-12. Parameters object always has additionalProperties: false.
{
"name": "add",
"description": "Add two integers.",
"parameters": {
"type": "object",
"properties": {
"a": { "type": "integer" },
"b": { "type": "integer", "default": 1 }
},
"required": ["a"],
"additionalProperties": false
}
}| Python | JSON Schema |
|---|---|
str, int, float, bool |
string, integer, number, boolean |
T | None |
anyOf: [schema(T), {"type": "null"}] |
list[T], dict[str, T] |
array, object + additionalProperties |
Literal, Enum |
enum |
Annotated[T, Field(...)] |
constraints on property |
TypedDict, @dataclass, Union, tuple |
object, anyOf, prefixItems |
| default value | "default" key; omitted from required |
| return annotation | output / MCP outputSchema |
All adapters read from ToolDefinition only. Schema is not generated twice inside an adapter.
Function FastMCP LangChain OpenAI
-------- ------- --------- ------
@mcp.tool() → MCP JSON rewrite rewrite
LC @tool → rewrite LC schema rewrite
hand-written → drift drift drift
- Pre-PEP
inspect.tool_schema: proposed stdlib fix, not shipped - FastMCP
@mcp.tool(): MCP transport + inference, not portable - LangChain
StructuredTool.from_function: infers schema per call site - Pydantic
model_json_schema(): domain models, not function-tool IR
| Solution | OpenAI | Anthropic | MCP | LangChain | FastMCP | Lock-in free |
|---|---|---|---|---|---|---|
Framework @tool |
partial | partial | partial | partial | partial | no |
| Hand-written JSON Schema | manual | manual | manual | manual | manual | yes |
| toolschema | yes | yes | yes | yes | yes | yes |
toolschema export and toolschema inspect import user-specified modules. Only point them at code you trust.
validate() checks arguments against generated schema; it does not sandbox tool execution. Your agent framework still runs the callable.
| Path | Description |
|---|---|
examples/01_basic.py |
@tool, schema(), adapter output |
examples/02_mcp_server.py |
FastMCP stdio server + --check smoke test |
examples/03_langchain.py |
LangChain from_toolschema + invoke |
examples/04_multi_provider.py |
One function, all provider formats |
examples/demo_tools.py |
Reusable tools module |
examples/verify_package.py |
PyPI install verification |
examples/deep_agents_demo.py |
Cross-framework integration smoke test |
- Pre-PEP:
inspect.tool_schema: target API shape - Standard Schema:
tool.standard["~standard"]protocol - FastMCP tools: MCP
$refclient limits - MCP specification:
inputSchema/outputSchema
MIT. See LICENSE.