How to Implement Function Tools with Automatic JSON Schema Generation in the Agent Framework

The Microsoft Agent Framework converts ordinary Python functions into LLM-callable tools by automatically generating JSON schemas from type hints using the @tool decorator and FunctionTool class.

The microsoft/agent-framework repository provides a robust system for building function tools with automatic JSON schema generation, eliminating the need to manually define parameter schemas for large language model (LLM) interactions. By leveraging Python type inspection and Pydantic model creation, the framework transforms annotated functions into validated, executable tools that any compatible chat provider can invoke. This approach ensures type safety at runtime while maintaining a clean, decorator-based developer experience.

How Automatic JSON Schema Generation Works

The framework implements a six-stage pipeline in python/packages/core/agent_framework/_tools.py that bridges Python functions and LLM-compatible schemas.

Input Model Discovery via Type Inspection

When you decorate a function with @tool, the framework calls _resolve_input_model to determine how to validate incoming arguments. If you provide a Pydantic model via the schema parameter, the framework uses it verbatim. Otherwise, inspect.signature extracts parameter names, type hints, and default values from the function definition. The _parse_annotation helper then converts Annotated[T, "description"] markers into Pydantic field definitions.

from agent_framework import tool

@tool()
def calculate_area(length: float, width: float = 1.0) -> float:
    """Calculate rectangle area."""
    return length * width

In this example, the framework dynamically constructs a Pydantic model equivalent to create_model("calculate_area_input", length=(float, ...), width=(float, 1.0)).

JSON Schema Construction

The _input_schema property lazily generates the JSON schema by calling self.input_model.model_json_schema() on the discovered Pydantic model. This result is cached on first use to avoid repeated computation. The resulting dictionary conforms to JSON Schema standards and includes property types, descriptions, and required field markers derived from your function signature.

Runtime Validation and Invocation

When an LLM requests a tool execution, the invoke method validates arguments against the cached schema using _validate_arguments_against_schema. This ensures type safety before your function executes. The framework also checks invocation limits, approval modes, and handles FunctionInvocationContext injection if your signature includes a context parameter.

Creating Function Tools: Implementation Patterns

Basic Synchronous Tool with the @tool Decorator

The simplest pattern uses the @tool decorator with type-hinted parameters. In python/packages/core/agent_framework/_tools.py, the decorator overloads handle both direct function wrapping and configuration via keyword arguments.

from agent_framework import tool

@tool(approval_mode="never_require")
def add_numbers(
    a: int,
    b: int,
    description: str = "sum"
) -> int:
    """Add two numbers and return the sum."""
    return a + b

The framework automatically builds the input model, generates the JSON schema, and exposes add_numbers.to_json_schema_spec() for LLM consumption.

Asynchronous Tools with Context Injection

For async operations requiring runtime metadata, add a FunctionInvocationContext parameter. The _discover_injected_parameters method detects context parameters by name or annotation, allowing access to request metadata, authentication tokens, or tracing IDs.

import asyncio
from agent_framework import tool, FunctionInvocationContext

@tool()
async def fetch_weather(
    location: str,
    ctx: FunctionInvocationContext,
) -> str:
    """Simulate an async API call with context access."""
    # Access ctx.kwargs for runtime data

    await asyncio.sleep(0.1)
    return f"Sunny in {location}"

Explicit Schema with Pydantic Models

When you need fine-grained control over field descriptions or validation rules, pass a Pydantic model to the schema parameter. This bypasses automatic signature inspection while maintaining the same runtime validation pipeline.

from pydantic import BaseModel, Field
from agent_framework import tool

class WeatherInput(BaseModel):
    city: str = Field(..., description="City name")
    unit: str = Field("celsius", description="Temperature unit")

@tool(schema=WeatherInput)
def get_weather(city: str, unit: str = "celsius") -> str:
    return f"Weather in {city}: 22°{unit[0].upper()}"

Declaration-Only Tools

For UI-only actions or tools implemented externally, create a FunctionTool instance with func=None. The declaration_only property identifies these non-executable specifications, allowing the LLM to request them while the framework rejects actual execution attempts.

from agent_framework import FunctionTool

weather_declaration = FunctionTool(
    name="weather_query",
    description="Ask the user for weather information",
    input_model={"type": "object", "properties": {"city": {"type": "string"}}},
    approval_mode="never_require",
)

Integrating with LLM Providers

The to_json_schema_spec() method returns an OpenAI-compatible function specification dictionary with type: "function", name, description, and parameters keys. Use the internal _tools_to_dict helper to convert multiple tools for API consumption.

from openai import OpenAI
from agent_framework import tool, _tools_to_dict

client = OpenAI()
messages = [{"role": "user", "content": "Add 3 and 5"}]

@tool()
def add_numbers(a: int, b: int) -> int:
    return a + b

# Convert to LLM-compatible format

specs = _tools_to_dict([add_numbers])

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    tools=specs,
    tool_choice="auto",
)

Summary

  • Automatic schema generation eliminates manual JSON Schema writing by inspecting Python type hints and building Pydantic models on-the-fly in _resolve_input_model.
  • The @tool decorator handles both sync and async functions, with support for approval modes, invocation limits, and context injection via FunctionInvocationContext.
  • Runtime validation ensures LLM-provided arguments match the generated schema before your function executes via _validate_arguments_against_schema.
  • Declaration-only tools allow registering UI actions or external tools using FunctionTool with func=None.
  • Provider compatibility is achieved through to_json_schema_spec(), producing OpenAI-style specifications without provider-specific lock-in.

Frequently Asked Questions

How does the framework handle complex type annotations like List or Optional?

The _parse_annotation method in python/packages/core/agent_framework/_tools.py recursively processes nested types, converting List[str], Optional[int], and other generic aliases into corresponding Pydantic field definitions. Union types become anyOf schemas in the final JSON output, ensuring complex signatures serialize correctly for LLM consumption.

Can I customize how return values are formatted for the LLM?

Yes. The result_parser parameter in FunctionTool accepts a callable that transforms your function's return value into list[Content] objects. If not provided, the default parse_result method handles common types like strings, numbers, and Pydantic models automatically.

What happens if I don't provide type hints for function parameters?

Without type hints, the framework treats parameters as Any type in the generated Pydantic model. While this still produces a valid JSON schema, you lose runtime type validation and LLMs receive less guidance about expected input formats. Best practice requires explicit Annotated types or Pydantic models for production tools.

How do invocation limits and approval modes work?

The max_invocations and max_invocation_exceptions parameters enforce per-instance execution caps, while approval_mode="always_require" forces user confirmation before the invoke method executes the wrapped function. These checks occur in the __call__ and invoke methods of FunctionTool, providing safety guardrails for sensitive operations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →