Tools & Function Calling in Microsoft Agent Framework – Part 4

Part 4 of the Microsoft Agent Framework Series

In Part 2 and Part 3, we built our first agents with basic tools. Now it’s time to go deeper into the heart of agentic AI: function calling.

Tools transform agents from mere conversationalists into action-takers. They’re the bridge between AI reasoning and real-world outcomes.

Understanding the Tool Invocation Flow

  1. User sends a message to the agent
  2. Agent analyzes the request and decides if tools are needed
  3. Agent selects tool(s) and generates arguments
  4. Framework executes the tool function
  5. Result is returned to the agent
  6. Agent synthesizes the final response

Tool Definition Patterns – Python

The @ai_function decorator automatically generates JSON schemas from your type annotations:

from typing import Annotated, Literal, Optional, List
from pydantic import Field, BaseModel
from agent_framework import ai_function
import aiohttp

# Pattern 1: Simple function with typed parameters
@ai_function
def get_weather(
    city: Annotated[str, Field(description="City name")],
    units: Annotated[Literal["celsius", "fahrenheit"], Field(description="Temperature unit")] = "celsius"
) -> str:
    """Get current weather for a city."""
    weather_data = {
        "new york": {"temp": 22, "condition": "Sunny"},
        "london": {"temp": 15, "condition": "Cloudy"},
        "tokyo": {"temp": 28, "condition": "Humid"}
    }
    data = weather_data.get(city.lower(), {"temp": 20, "condition": "Unknown"})
    temp = data["temp"] if units == "celsius" else (data["temp"] * 9/5) + 32
    return f"Weather in {city}: {temp}°{'C' if units == 'celsius' else 'F'}, {data['condition']}"

# Pattern 2: Async function for I/O operations
@ai_function
async def fetch_api_data(
    endpoint: Annotated[str, Field(description="API endpoint URL")]
) -> str:
    """Fetch data from an external API."""
    try:
        async with aiohttp.ClientSession() as session:
            async with session.get(endpoint, timeout=10) as response:
                if response.status == 200:
                    return await response.text()
                return f"API returned status {response.status}"
    except aiohttp.ClientError as e:
        return f"API error: {str(e)}"

# Pattern 3: Complex return types with Pydantic
class SearchResult(BaseModel):
    title: str
    url: str
    snippet: str

@ai_function
def search_documents(
    query: Annotated[str, Field(description="Search query")],
    limit: Annotated[int, Field(description="Max results", ge=1, le=20)] = 10
) -> List[dict]:
    """Search internal document repository."""
    results = [
        {"title": f"Document about {query}", "url": f"/docs/{i}", "snippet": f"Content related to {query}..."}
        for i in range(min(limit, 3))
    ]
    return results

# Pattern 4: Optional parameters with defaults
@ai_function
def send_notification(
    message: Annotated[str, Field(description="Message content")],
    recipient: Annotated[str, Field(description="Email or user ID")],
    priority: Annotated[Optional[str], Field(description="Priority: low, normal, high")] = "normal",
    schedule: Annotated[Optional[str], Field(description="ISO datetime to send")] = None
) -> str:
    """Send a notification to a user."""
    scheduled_info = f" scheduled for {schedule}" if schedule else ""
    return f"Notification ({priority}) queued for {recipient}{scheduled_info}"

Tool Definition Patterns – .NET (C#)

In .NET, use [Description] attributes to document your tools:

using System;
using System.ComponentModel;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using System.Collections.Generic;

public class AdvancedTools
{
    private static readonly HttpClient _httpClient = new();
    
    // Pattern 1: Simple tool with descriptions
    [Description("Get current weather for a location")]
    public static string GetWeather(
        [Description("City name")] string city,
        [Description("Temperature unit: celsius or fahrenheit")] string units = "celsius")
    {
        var weatherData = new Dictionary<string, (int Temp, string Condition)>
        {
            ["new york"] = (22, "Sunny"),
            ["london"] = (15, "Cloudy"),
            ["tokyo"] = (28, "Humid")
        };
        
        var data = weatherData.GetValueOrDefault(city.ToLower(), (20, "Unknown"));
        var temp = units == "fahrenheit" ? (data.Temp * 9 / 5) + 32 : data.Temp;
        var unit = units == "fahrenheit" ? "F" : "C";
        
        return $"Weather in {city}: {temp}°{unit}, {data.Condition}";
    }
    
    // Pattern 2: Async tool for external calls
    [Description("Fetch data from an external REST API")]
    public static async Task<string> FetchApiDataAsync(
        [Description("Full API endpoint URL")] string endpoint)
    {
        try
        {
            var response = await _httpClient.GetAsync(endpoint);
            if (response.IsSuccessStatusCode)
                return await response.Content.ReadAsStringAsync();
            return $"API returned status {(int)response.StatusCode}";
        }
        catch (HttpRequestException ex)
        {
            return $"API error: {ex.Message}";
        }
    }
    
    // Pattern 3: Complex operations with validation
    [Description("Create a support ticket in the system")]
    public static string CreateTicket(
        [Description("Customer email address")] string email,
        [Description("Issue description")] string description,
        [Description("Priority: Low, Medium, High, Critical")] string priority = "Medium")
    {
        if (!email.Contains("@"))
            return "Error: Invalid email format.";
        
        if (string.IsNullOrWhiteSpace(description))
            return "Error: Description cannot be empty.";
        
        var ticketId = $"TKT-{DateTime.Now:yyyyMMddHHmmss}";
        return $"Created ticket {ticketId} for {email} with priority {priority}";
    }
    
    // Pattern 4: Search with pagination
    [Description("Search documents in the repository")]
    public static string SearchDocuments(
        [Description("Search query")] string query,
        [Description("Maximum results (1-20)")] int limit = 10)
    {
        limit = Math.Clamp(limit, 1, 20);
        var results = new List<string>();
        
        for (int i = 1; i <= Math.Min(limit, 3); i++)
        {
            results.Add($"{i}. Document about {query} - /docs/{i}");
        }
        
        return $"Found {results.Count} results:\n" + string.Join("\n", results);
    }
}

MCP Integration – Python

The Model Context Protocol (MCP) enables dynamic tool discovery from external servers:

import asyncio
import os
from agent_framework.mcp import MCPClient, MCPServerConfig
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential

async def main():
    # Configure MCP server connection
    mcp_config = MCPServerConfig(
        url="http://localhost:8080",
        auth_token=os.getenv("MCP_TOKEN"),
        timeout=30
    )
    
    # Create MCP client
    mcp_client = MCPClient(config=mcp_config)
    
    # Discover available tools
    tools = await mcp_client.list_tools()
    print(f"Available MCP tools: {[t.name for t in tools]}")
    
    # Create agent with MCP tools
    agent = AzureOpenAIResponsesClient(
        credential=AzureCliCredential()
    ).create_agent(
        name="MCPAgent",
        instructions="You can use external tools via MCP to help users.",
        mcp_clients=[mcp_client]  # Auto-discovers and registers tools
    )
    
    # Agent can now use MCP tools
    result = await agent.run("Search for recent issues in the GitHub repo")
    print(f"Result: {result.text}")

if __name__ == "__main__":
    asyncio.run(main())

MCP Integration – .NET (C#)

using Microsoft.Agents.AI;
using Microsoft.Agents.AI.MCP;
using Azure.Identity;

// Configure MCP connection
var mcpConfig = new MCPServerConfig
{
    Url = "http://localhost:8080",
    AuthToken = Environment.GetEnvironmentVariable("MCP_TOKEN"),
    TimeoutSeconds = 30
};

var mcpClient = new MCPClient(mcpConfig);

// Discover tools
var tools = await mcpClient.ListToolsAsync();
Console.WriteLine($"Available tools: {string.Join(", ", tools.Select(t => t.Name))}");

// Create agent with MCP
var agent = new AzureOpenAIClient(
        new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!),
        new DefaultAzureCredential())
    .GetOpenAIResponseClient("gpt-4o")
    .CreateAIAgent(
        name: "MCPAgent",
        instructions: "You can use external tools via MCP.",
        mcpClients: new[] { mcpClient });

// Use MCP tools
var result = await agent.RunAsync("Search for recent GitHub issues");
Console.WriteLine(result);

External API Integration – Python

import os
import aiohttp
from typing import Annotated
from pydantic import Field
from agent_framework import ai_function

class CRMTools:
    """Tools for CRM integration."""
    
    def __init__(self, api_key: str, base_url: str):
        self.api_key = api_key
        self.base_url = base_url
    
    @ai_function
    async def get_customer(
        self,
        customer_id: Annotated[str, Field(description="Customer ID")]
    ) -> str:
        """Retrieve customer information from CRM."""
        async with aiohttp.ClientSession() as session:
            headers = {"Authorization": f"Bearer {self.api_key}"}
            try:
                async with session.get(
                    f"{self.base_url}/customers/{customer_id}",
                    headers=headers,
                    timeout=10
                ) as response:
                    if response.status == 200:
                        data = await response.json()
                        return f"""Customer Found:
                        - ID: {data.get('id')}
                        - Name: {data.get('name')}
                        - Email: {data.get('email')}
                        - Status: {data.get('status')}"""
                    elif response.status == 404:
                        return f"Customer {customer_id} not found."
                    else:
                        return f"API error: status {response.status}"
            except aiohttp.ClientError as e:
                return f"Connection error: {str(e)}"
    
    @ai_function
    async def create_order(
        self,
        customer_id: Annotated[str, Field(description="Customer ID")],
        product_ids: Annotated[str, Field(description="Comma-separated product IDs")],
        quantity: Annotated[int, Field(description="Quantity")] = 1
    ) -> str:
        """Create a new order in the system."""
        async with aiohttp.ClientSession() as session:
            payload = {
                "customer_id": customer_id,
                "products": product_ids.split(","),
                "quantity": quantity
            }
            try:
                async with session.post(
                    f"{self.base_url}/orders",
                    json=payload,
                    headers={"Authorization": f"Bearer {self.api_key}"},
                    timeout=10
                ) as response:
                    if response.status == 201:
                        data = await response.json()
                        return f"Order created! Order ID: {data.get('order_id')}"
                    else:
                        error = await response.text()
                        return f"Failed to create order: {error}"
            except aiohttp.ClientError as e:
                return f"Connection error: {str(e)}"

# Usage
crm_tools = CRMTools(
    api_key=os.getenv("CRM_API_KEY"),
    base_url="https://api.example.com"
)

agent = client.create_agent(
    name="OrderAgent",
    instructions="Help customers with orders using the CRM tools.",
    tools=[crm_tools.get_customer, crm_tools.create_order]
)

External API Integration – .NET (C#)

using System;
using System.ComponentModel;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;

public class CRMTools
{
    private readonly HttpClient _client;
    private readonly string _baseUrl;
    
    public CRMTools(string apiKey, string baseUrl)
    {
        _baseUrl = baseUrl;
        _client = new HttpClient();
        _client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
        _client.Timeout = TimeSpan.FromSeconds(10);
    }
    
    [Description("Retrieve customer information from CRM")]
    public async Task<string> GetCustomerAsync(
        [Description("Customer ID")] string customerId)
    {
        try
        {
            var response = await _client.GetAsync($"{_baseUrl}/customers/{customerId}");
            
            if (response.IsSuccessStatusCode)
            {
                var customer = await response.Content.ReadFromJsonAsync<CustomerDto>();
                return $@"Customer Found:
                    - ID: {customer?.Id}
                    - Name: {customer?.Name}
                    - Email: {customer?.Email}
                    - Status: {customer?.Status}";
            }
            
            if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
                return $"Customer {customerId} not found.";
            
            return $"API error: {(int)response.StatusCode}";
        }
        catch (HttpRequestException ex)
        {
            return $"Connection error: {ex.Message}";
        }
    }
    
    [Description("Create a new order in the system")]
    public async Task<string> CreateOrderAsync(
        [Description("Customer ID")] string customerId,
        [Description("Comma-separated product IDs")] string productIds,
        [Description("Quantity")] int quantity = 1)
    {
        try
        {
            var order = new
            {
                CustomerId = customerId,
                Products = productIds.Split(','),
                Quantity = quantity
            };
            
            var response = await _client.PostAsJsonAsync($"{_baseUrl}/orders", order);
            
            if (response.StatusCode == System.Net.HttpStatusCode.Created)
            {
                var result = await response.Content.ReadFromJsonAsync<OrderResponseDto>();
                return $"Order created! Order ID: {result?.OrderId}";
            }
            
            var error = await response.Content.ReadAsStringAsync();
            return $"Failed to create order: {error}";
        }
        catch (HttpRequestException ex)
        {
            return $"Connection error: {ex.Message}";
        }
    }
}

public record CustomerDto(string Id, string Name, string Email, string Status);
public record OrderResponseDto(string OrderId);

// Usage
var crmTools = new CRMTools(
    Environment.GetEnvironmentVariable("CRM_API_KEY")!,
    "https://api.example.com"
);

var agent = client.CreateAIAgent(
    name: "OrderAgent",
    instructions: "Help customers with orders.",
    tools: new object[] { crmTools.GetCustomerAsync, crmTools.CreateOrderAsync });

Error Handling – Python

Tools should never raise exceptions. Return informative error messages instead:

import aiohttp
from typing import Annotated
from pydantic import Field
from agent_framework import ai_function

@ai_function
async def safe_api_call(
    endpoint: Annotated[str, Field(description="API endpoint URL")]
) -> str:
    """Make a safe API call with comprehensive error handling."""
    
    # Validate input
    if not endpoint.startswith(("http://", "https://")):
        return "Error: Invalid endpoint. URL must start with http:// or https://"
    
    try:
        timeout = aiohttp.ClientTimeout(total=10)
        async with aiohttp.ClientSession(timeout=timeout) as session:
            async with session.get(endpoint) as response:
                if response.status == 200:
                    content = await response.text()
                    if len(content) > 1000:
                        return content[:1000] + "... (truncated)"
                    return content
                elif response.status == 404:
                    return "Error: Resource not found. Please check the URL."
                elif response.status == 401:
                    return "Error: Authentication required."
                elif response.status == 403:
                    return "Error: Access forbidden."
                elif response.status == 429:
                    return "Error: Rate limit exceeded. Try again later."
                elif response.status >= 500:
                    return f"Error: Server error ({response.status})."
                else:
                    return f"Error: Unexpected status code {response.status}"
                    
    except aiohttp.ClientTimeout:
        return "Error: Request timed out."
    except aiohttp.ClientConnectionError:
        return "Error: Could not connect to server."
    except aiohttp.ClientError as e:
        return f"Error: Network error - {str(e)}"
    except Exception as e:
        return f"Error: Unexpected error - {str(e)}"

Error Handling – .NET (C#)

using System;
using System.ComponentModel;
using System.Net.Http;
using System.Threading.Tasks;

public class SafeApiTools
{
    private static readonly HttpClient _client = new()
    {
        Timeout = TimeSpan.FromSeconds(10)
    };
    
    [Description("Make a safe API call with comprehensive error handling")]
    public static async Task<string> SafeApiCallAsync(
        [Description("API endpoint URL")] string endpoint)
    {
        // Validate input
        if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var uri))
            return "Error: Invalid URL format.";
        
        if (uri.Scheme != "http" && uri.Scheme != "https")
            return "Error: URL must use http or https protocol.";
        
        try
        {
            var response = await _client.GetAsync(endpoint);
            
            return response.StatusCode switch
            {
                System.Net.HttpStatusCode.OK => await GetTruncatedContent(response),
                System.Net.HttpStatusCode.NotFound => "Error: Resource not found.",
                System.Net.HttpStatusCode.Unauthorized => "Error: Authentication required.",
                System.Net.HttpStatusCode.Forbidden => "Error: Access forbidden.",
                System.Net.HttpStatusCode.TooManyRequests => "Error: Rate limit exceeded.",
                >= System.Net.HttpStatusCode.InternalServerError => 
                    $"Error: Server error ({(int)response.StatusCode}).",
                _ => $"Error: Unexpected status {(int)response.StatusCode}"
            };
        }
        catch (TaskCanceledException)
        {
            return "Error: Request timed out.";
        }
        catch (HttpRequestException ex)
        {
            return $"Error: Connection failed - {ex.Message}";
        }
        catch (Exception ex)
        {
            return $"Error: Unexpected error - {ex.Message}";
        }
    }
    
    private static async Task<string> GetTruncatedContent(HttpResponseMessage response)
    {
        var content = await response.Content.ReadAsStringAsync();
        if (content.Length > 1000)
            return content[..1000] + "... (truncated)";
        return content;
    }
}

Tool Chaining – Python

Design tools that work well together for complex workflows:

from typing import Annotated
from pydantic import Field
from agent_framework import ai_function

@ai_function
def search_products(
    query: Annotated[str, Field(description="Product search query")]
) -> str:
    """Search for products. Returns a list of product IDs."""
    products = {
        "laptop": ["PROD-001", "PROD-002", "PROD-003"],
        "phone": ["PROD-101", "PROD-102"],
        "tablet": ["PROD-201", "PROD-202"]
    }
    
    for key, ids in products.items():
        if key in query.lower():
            return f"Found products: {', '.join(ids)}"
    
    return "No products found for your search."

@ai_function
def get_product_details(
    product_id: Annotated[str, Field(description="Product ID from search")]
) -> str:
    """Get detailed information for a specific product."""
    products = {
        "PROD-001": {"name": "ProBook Laptop 15", "price": 999.99, "stock": 50},
        "PROD-002": {"name": "UltraBook Air", "price": 1299.99, "stock": 25},
        "PROD-003": {"name": "WorkStation Pro", "price": 1899.99, "stock": 10},
        "PROD-101": {"name": "SmartPhone X", "price": 799.99, "stock": 100},
        "PROD-102": {"name": "SmartPhone Pro", "price": 999.99, "stock": 75},
    }
    
    product = products.get(product_id.upper())
    if product:
        return f"""Product Details:
        - ID: {product_id}
        - Name: {product['name']}
        - Price: ${product['price']:.2f}
        - Stock: {product['stock']} units"""
    return f"Product {product_id} not found."

@ai_function
def add_to_cart(
    product_id: Annotated[str, Field(description="Product ID to add")],
    quantity: Annotated[int, Field(description="Quantity to add")] = 1
) -> str:
    """Add a product to the shopping cart."""
    if quantity < 1:
        return "Error: Quantity must be at least 1."
    if quantity > 10:
        return "Error: Maximum 10 items per product."
    
    return f"Added {quantity}x {product_id} to cart successfully."

# Agent can chain: search -> get details -> add to cart
agent = client.create_agent(
    name="ShoppingAssistant",
    instructions="Help users find and purchase products. Use tools in sequence.",
    tools=[search_products, get_product_details, add_to_cart]
)

Tool Chaining – .NET (C#)

using System;
using System.ComponentModel;
using System.Collections.Generic;

public class ShoppingTools
{
    private static readonly Dictionary<string, List<string>> ProductSearch = new()
    {
        ["laptop"] = new() { "PROD-001", "PROD-002", "PROD-003" },
        ["phone"] = new() { "PROD-101", "PROD-102" },
        ["tablet"] = new() { "PROD-201", "PROD-202" }
    };
    
    private static readonly Dictionary<string, (string Name, decimal Price, int Stock)> Products = new()
    {
        ["PROD-001"] = ("ProBook Laptop 15", 999.99m, 50),
        ["PROD-002"] = ("UltraBook Air", 1299.99m, 25),
        ["PROD-003"] = ("WorkStation Pro", 1899.99m, 10),
        ["PROD-101"] = ("SmartPhone X", 799.99m, 100),
        ["PROD-102"] = ("SmartPhone Pro", 999.99m, 75)
    };
    
    [Description("Search for products by query. Returns product IDs.")]
    public static string SearchProducts(
        [Description("Product search query")] string query)
    {
        foreach (var (key, ids) in ProductSearch)
        {
            if (query.ToLower().Contains(key))
                return $"Found products: {string.Join(", ", ids)}";
        }
        return "No products found.";
    }
    
    [Description("Get detailed information for a product.")]
    public static string GetProductDetails(
        [Description("Product ID")] string productId)
    {
        if (Products.TryGetValue(productId.ToUpper(), out var product))
        {
            return $@"Product Details:
                - ID: {productId}
                - Name: {product.Name}
                - Price: ${product.Price:F2}
                - Stock: {product.Stock} units";
        }
        return $"Product {productId} not found.";
    }
    
    [Description("Add a product to the shopping cart.")]
    public static string AddToCart(
        [Description("Product ID")] string productId,
        [Description("Quantity")] int quantity = 1)
    {
        if (quantity < 1) return "Error: Quantity must be at least 1.";
        if (quantity > 10) return "Error: Maximum 10 items per product.";
        return $"Added {quantity}x {productId} to cart.";
    }
}

// Usage
var agent = client.CreateAIAgent(
    name: "ShoppingAssistant",
    instructions: "Help users find and purchase products.",
    tools: new object[] {
        ShoppingTools.SearchProducts,
        ShoppingTools.GetProductDetails,
        ShoppingTools.AddToCart
    });

Key Takeaways

  • Tools are the bridge between AI reasoning and real-world actions
  • Use clear descriptions and type annotations — they become the LLM’s documentation
  • Never raise exceptions in tools — return informative error messages
  • MCP provides dynamic tool discovery and vendor-neutral integration
  • Design tools to chain together for complex workflows
  • Use async patterns for I/O-bound operations

📦 Source Code

All code examples from this article series are available on GitHub:

👉 https://github.com/nithinmohantk/microsoft-agent-framework-series-examples

Clone the repository to follow along:

git clone https://github.com/nithinmohantk/microsoft-agent-framework-series-examples.git
cd microsoft-agent-framework-series-examples

Series Navigation

References


Discover more from C4: Container, Code, Cloud & Context

Subscribe to get the latest posts sent to your email.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.