Skip to main content

Canonical Python layer for AI agent tools — function to JSON Schema, once, everywhere.

Project description

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

toolschema-1.0.1.tar.gz (277.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

toolschema-1.0.1-py3-none-any.whl (30.0 kB view details)

Uploaded Python 3

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

Hashes for toolschema-1.0.1.tar.gz
Algorithm Hash digest
SHA256 df7c099e4fbc166aa36363c6d08f54fc1248575655b360203c5ae73c707b5dfd
MD5 16cc64f192241b390f61cc09b7630ecb
BLAKE2b-256 bfa9c3bac3a9498fbd3d16f400af8f2eb3db9c32372c19c053c13544212dee2d

See more details on using hashes here.

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

Hashes for toolschema-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 50a9a7fba09a0da94183c9d3d9596062db82143657800da20b0f083d26a30b58
MD5 0c5e38f1c2c56b44999aa3b58b8da135
BLAKE2b-256 173841b997f93fe50c9dfaf46efc3d71baa4a1ff78836d4b53d32a11294de249

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page