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
From source (until PyPI publish):
pip install git+https://github.com/false200/toolschema.git
Extras:
pip install toolschema[fastmcp] # FastMCP MCP servers
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 + dev tools
Requires Python 3.10+. Core has zero framework dependencies (typing_extensions on 3.10 only).
Docs: tutorials · provider quirks · Pre-PEP alignment · Claude Desktop smoke test
Usage
Define a tool
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}
definition = schema(get_weather)
definition.to_openai()
definition.to_mcp()
definition.to_anthropic()
Works without @tool — any typed function with a docstring:
def add(a: int, b: int = 1) -> int:
"""Add two integers."""
return a + b
definition = schema(add)
FastMCP (MCP server)
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() # stdio MCP server
register_tool uses your pre-built schema — no double generation inside FastMCP.
LangChain
from toolschema import schema
from toolschema.integrations.langchain import from_toolschema
from myapp.tools import search
tool = from_toolschema(schema(search), search)
result = tool.invoke({"query": "laptop", "limit": 5})
OpenAI Agents SDK
from toolschema import schema
from toolschema.integrations.openai_agents import to_agents_function_tool
from myapp.tools import add
agents_tool = to_agents_function_tool(schema(add), 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
tool = from_toolschema(schema(add), add)
agent = Agent("openai:gpt-4o", tools=[tool])
Scaffold an MCP project
toolschema init my-mcp-server
cd my-mcp-server
uv sync
uv run python -m my_mcp_server --check # smoke test
uv run python -m my_mcp_server # start server
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
@tool
Optional decorator. Attaches tool metadata; does not change call semantics.
@tool(name="custom_name", description="Override docstring")
def my_fn(x: str) -> str: ...
schema(fn) -> ToolDefinition
Introspect a typed callable and return the canonical intermediate representation.
fn
Required
Type: Callable
Any function or @tool-decorated callable with type hints. Docstring becomes the tool description.
from toolschema import schema
definition = schema(my_function)
definition.name # function name (or @tool override)
definition.description # docstring (or @tool override)
definition.parameters # JSON Schema 2020-12 object
definition.output # return-type schema, or None
Field(...)
Attach JSON Schema constraints and descriptions via Annotated:
from typing import Annotated
from toolschema import Field
city: Annotated[str, Field(description="City name", min_length=1)]
Plain string shorthand (Pre-PEP style):
city: Annotated[str, "City name"]
ToolDefinition
Frozen dataclass — single source of truth for all adapters.
to_json_schema() -> dict
Canonical record: name, description, parameters, optional output.
to_openai(*, strict=False) -> dict
OpenAI function-calling shape: {"type": "function", "function": {...}}.
When strict=True, sets additionalProperties: false and marks every property required.
to_anthropic() -> dict
Anthropic Messages API tool shape. Constraints like minLength move into description text.
to_mcp(*, inline_refs=True) -> dict
MCP tools/list shape with inputSchema and optional outputSchema.
When inline_refs=True (default), flattens $ref / $defs for Claude Desktop and VS Code Copilot.
to_gemini() -> dict
Google Gemini FunctionDeclaration shape. Parameter types uppercased (STRING, INTEGER, …).
validate(args) -> ValidationResult
Thin argument checking against parameters. Returns ValidationSuccess or ValidationFailure.
from toolschema import ValidationSuccess
result = definition.validate({"city": "London"})
if isinstance(result, ValidationSuccess):
print(result.value)
standard (property)
Standard Schema + Standard JSON Schema protocol host (tool.standard["~standard"]).
Integrations
| Function | Package extra | Purpose |
|---|---|---|
register_tool(mcp, definition, fn) |
fastmcp |
Register on FastMCP without @mcp.tool schema regen |
from_toolschema(definition, fn) |
langchain |
StructuredTool with infer_schema=False |
to_agents_function_tool(definition, fn) |
openai-agents |
OpenAI Agents FunctionTool |
from_toolschema(definition, fn) |
pydantic-ai |
Pydantic AI Tool.from_schema |
Import from submodules:
from toolschema.integrations.fastmcp import register_tool
from toolschema.integrations.langchain import from_toolschema
from toolschema.integrations.openai_agents import to_agents_function_tool
from toolschema.integrations.pydantic_ai import from_toolschema
Type coverage
| 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 with additionalProperties |
Literal["a"], Enum |
enum |
Annotated[T, Field(...)] |
constraints + description |
TypedDict, @dataclass |
object with properties |
Union[A, B], tuple[...] |
anyOf, prefixItems |
| Default values | "default" key; omitted from required |
| Return type | output / outputSchema |
Deferred: generics, ParamSpec, docstring parameter parsing (Google/NumPy).
Architecture
Python function + type hints
│
▼
schema(fn) ──► ToolDefinition (IR)
│ │
│ ┌───────────┼───────────┬──────────┐
▼ ▼ ▼ ▼ ▼
validate() to_openai() to_mcp() to_anthropic() to_gemini()
│ │
▼ ▼
integrations/ register_tool()
langchain fastmcp
openai_agents pydantic_ai
Rule: adapters read ToolDefinition only. Schema is never generated twice.
Why not framework decorators alone?
Your function FastMCP LangChain OpenAI
------------ ------- --------- ------
@mcp.tool() → MCP JSON only rewrite needed rewrite needed
@tool (LC) → rewrite needed LC schema only rewrite needed
raw OpenAI SDK → rewrite needed rewrite needed OpenAI JSON only
- Pre-PEP
inspect.tool_schema: proposed stdlib fix, not shipped yet - FastMCP
@mcp.tool(): MCP transport + Pydantic inference, not portable - Pydantic
model_json_schema(): domain models, not function-tool IR - LangChain
StructuredTool.from_function: infers schema per call site
Comparison
| Solution | OpenAI | Anthropic | MCP | LangChain | FastMCP | Zero lock-in |
|---|---|---|---|---|---|---|
Framework @tool |
partial | partial | partial | partial | partial | no |
| Pydantic JSON Schema | manual | manual | manual | manual | manual | yes |
| toolschema | yes | yes | yes | yes | yes | yes |
Provider quirks
| Provider | Behavior |
|---|---|
| OpenAI | strict=True → all properties required, additionalProperties: false |
| Anthropic | minLength, pattern, etc. folded into description |
| MCP | inline_refs=True default; camelCase inputSchema / outputSchema |
| Gemini | uppercased types; parameters only (no output schema yet) |
Details: docs/provider-quirks.md
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 |
Sample tools module |
examples/verify_package.py |
End-to-end package verification script |
examples/deep_agents_demo.py |
Cross-framework deep integration demo |
Testing
uv sync --extra dev --extra fastmcp --extra langchain --extra openai-agents --extra pydantic-ai
uv run pytest -v
uv run python examples/deep_agents_demo.py
92 tests: unit, golden snapshots, parity vs native FastMCP/LangChain, MCP stdio smoke, deep cross-agent harness.
Non-goals
- Replacing Pydantic for domain modeling / validation
- Agent orchestration, memory, or MCP transport
- Live LLM API translation
Related
- Pre-PEP:
inspect.tool_schema— API shape we align with - Standard Schema — interop protocol
- FastMCP tools — MCP
$refclient limitations - MCP specification —
inputSchema/outputSchema
License
MIT. See LICENSE.
Contributing
PRs welcome. See CONTRIBUTING.md. Run uv run pytest && uv run ruff check src tests before submitting.
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.0.tar.gz.
File metadata
- Download URL: toolschema-1.0.0.tar.gz
- Upload date:
- Size: 246.3 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 |
fdb95e7718e7948692a75419e8da23dc38864b2c9a58934690c0bd14d5823bf9
|
|
| MD5 |
6d85e7243ea34cd416a5ee1c1c89f58e
|
|
| BLAKE2b-256 |
be9e0f1edb66d8ea0d200b5a9c26b73f960029c9df7aeadf5b5e1073194ba4e0
|
File details
Details for the file toolschema-1.0.0-py3-none-any.whl.
File metadata
- Download URL: toolschema-1.0.0-py3-none-any.whl
- Upload date:
- Size: 30.4 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 |
b5c1f20cbcd8f66e6e45cef3f69739b32c1e6505d26c0b6653d2c305ae24ddfa
|
|
| MD5 |
6fb625b75b07b2836026c7457fb57c68
|
|
| BLAKE2b-256 |
a35c8ffe8cadd765e19ded6174d604c8af754031db3514c59cf6a9c9412f4d84
|