Skip to content

false200/toolschema

toolschema

PyPI version Python CI License: MIT

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

License

MIT. See LICENSE.

Community

About

Canonical Python layer for AI agent tools - function to JSON Schema, once, everywhere. OpenAI, Anthropic, MCP, LangChain, FastMCP.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Contributors

Languages