Canonical Python layer for AI agent tools — function to JSON Schema, once, everywhere.
Project description
toolschema
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.
Install
pip install toolschema
Extras:
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 integrations
Requires Python 3.10+. Core depends on stdlib only (typing_extensions on 3.10).
Documentation: https://toolschema.readthedocs.io
Usage
Core
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)
FastMCP
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()
LangChain
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})
OpenAI Agents
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)
Pydantic AI
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])
CLI
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-server
API
schema(fn) -> ToolDefinition
Build the canonical tool IR from a function signature, type hints, defaults, and docstring.
fn
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 None
@tool
Optional decorator. Overrides name or description; does not change call semantics.
@tool(name="web_search", description="Search the web.")
def search(query: str) -> list[dict]: ...
Field(...)
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"].
ToolDefinition.to_openai(*, strict=False)
OpenAI function-calling payload: {"type": "function", "function": {...}}.
strict
Type: boolean
Default: false
When true, sets additionalProperties: false and lists every property in required.
ToolDefinition.to_anthropic()
Anthropic Messages API shape: {"name", "description", "input_schema"}.
Numeric and string constraints (minLength, pattern, etc.) are folded into property description text.
ToolDefinition.to_mcp(*, inline_refs=True)
MCP tools/list shape: name, description, inputSchema, optional outputSchema.
inline_refs
Type: boolean
Default: true
When true, flatten $ref / $defs before return. Required for Claude Desktop and VS Code Copilot.
ToolDefinition.to_gemini()
Google Gemini FunctionDeclaration shape. JSON Schema type values are uppercased (STRING, INTEGER, …). Parameters only; no output schema in v1.0.
ToolDefinition.validate(args) -> ValidationResult
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_tool(mcp, tool, fn) (FastMCP)
Register a pre-built ToolDefinition on a FastMCP server without regenerating schema inside FastMCP.
mcp
Required
Type: fastmcp.FastMCP
tool
Required
Type: ToolDefinition
fn
Required
Type: Callable
from_toolschema(tool, fn) (LangChain)
Build a StructuredTool with infer_schema=False and args_schema=tool.parameters.
to_agents_function_tool(tool, fn, *, strict=False) (OpenAI Agents)
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().
from_toolschema(tool, fn) (Pydantic AI)
Build a Pydantic AI Tool via Tool.from_schema() and the pre-built JSON Schema.
Also: to_pydantic_ai_tool(), prepare_toolset().
toolschema init NAME [--path DIR]
Scaffold an MCP server project from the packaged template. Runs slugify_package_name() on the directory name for the Python package.
Canonical schema
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.
Why not framework @tool alone?
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
Comparison
| 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 |
Security
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.
Examples
| 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 |
Related
- Pre-PEP:
inspect.tool_schema: target API shape - Standard Schema:
tool.standard["~standard"]protocol - FastMCP tools: MCP
$refclient limits - MCP specification:
inputSchema/outputSchema
License
MIT. See LICENSE.
Community
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file toolschema-1.0.1.tar.gz.
File metadata
- Download URL: toolschema-1.0.1.tar.gz
- Upload date:
- Size: 277.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
df7c099e4fbc166aa36363c6d08f54fc1248575655b360203c5ae73c707b5dfd
|
|
| MD5 |
16cc64f192241b390f61cc09b7630ecb
|
|
| BLAKE2b-256 |
bfa9c3bac3a9498fbd3d16f400af8f2eb3db9c32372c19c053c13544212dee2d
|
File details
Details for the file toolschema-1.0.1-py3-none-any.whl.
File metadata
- Download URL: toolschema-1.0.1-py3-none-any.whl
- Upload date:
- Size: 30.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
50a9a7fba09a0da94183c9d3d9596062db82143657800da20b0f083d26a30b58
|
|
| MD5 |
0c5e38f1c2c56b44999aa3b58b8da135
|
|
| BLAKE2b-256 |
173841b997f93fe50c9dfaf46efc3d71baa4a1ff78836d4b53d32a11294de249
|