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

> Learn to implement function tools with automatic JSON schema generation in the Microsoft Agent Framework. Convert Python functions to LLM callable tools efficiently using the @tool decorator.

- Repository: [Microsoft/agent-framework](https://github.com/microsoft/agent-framework)
- Tags: how-to-guide
- Published: 2026-04-05

---

**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`](https://github.com/microsoft/agent-framework/blob/main/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.

```python
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`](https://github.com/microsoft/agent-framework/blob/main/python/packages/core/agent_framework/_tools.py), the decorator overloads handle both direct function wrapping and configuration via keyword arguments.

```python
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.

```python
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.

```python
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.

```python
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.

```python
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`](https://github.com/microsoft/agent-framework/blob/main/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.