Skip to content

Tools & MCP

Understanding function calling and the Model Context Protocol.

Tools (also called “function calling”) allow LLMs to execute code, query databases, call APIs, and interact with external systems.

Extend capabilities: LLMs can do more than generate text
Real-time data: Access current information
Take actions: Update databases, send emails, etc.
Accuracy: Use code for math, not LLM reasoning

  1. Define tools: Describe available functions
  2. LLM decides: When to use which tool
  3. Execute: Run the tool and get results
  4. LLM responds: Incorporate tool results in response
User: "What's the weather in Paris?"
LLM: "I should use the weather tool"
Tool Call: get_weather(city="Paris")
Tool Result: {"temp": 18, "condition": "cloudy"}
LLM: "It's 18°C and cloudy in Paris"
import "github.com/AltairaLabs/PromptKit/runtime/tools"
// Define tool
weatherTool := &tools.ToolDescriptor{
Name: "get_weather",
Description: "Get current weather for a city",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name"
}
},
"required": ["city"]
}`),
}
type WeatherExecutor struct{}
func (e *WeatherExecutor) Name() string {
return "weather"
}
func (e *WeatherExecutor) Execute(
ctx context.Context, descriptor *tools.ToolDescriptor, args json.RawMessage,
) (json.RawMessage, error) {
var params struct {
City string `json:"city"`
}
json.Unmarshal(args, &params)
weather := getWeather(params.City)
return json.Marshal(map[string]any{
"temp": weather.Temp,
"condition": weather.Condition,
})
}
// Create registry and register the tool
registry := tools.NewRegistry()
registry.RegisterExecutor(&WeatherExecutor{})
err := registry.Register(weatherTool)
if err != nil {
log.Fatal(err)
}

The tool registry makes tools available for the LLM to call during conversations.

MCP is a standard for connecting LLMs to external data sources and tools.

MCP provides:

  • Standard interface: Connect any tool to any LLM
  • Tool discovery: LLMs learn available tools
  • Secure execution: Sandboxed tool execution
  • Composability: Combine multiple tool servers
LLM Application
MCP Client
MCP Server(s)
External Systems (filesystem, databases, APIs)
Terminal window
# Filesystem server
npx @modelcontextprotocol/server-filesystem ~/documents
# Memory server
npx @modelcontextprotocol/server-memory
import "github.com/AltairaLabs/PromptKit/runtime/mcp"
// Create MCP registry with server configuration
mcpRegistry := mcp.NewRegistry()
defer mcpRegistry.Close()
err := mcpRegistry.RegisterServer(mcp.ServerConfig{
Name: "filesystem",
Command: "npx",
Args: []string{"-y", "@modelcontextprotocol/server-filesystem", "/path/to/files"},
})
if err != nil {
log.Fatal(err)
}
// Discover tools from all registered servers
ctx := context.Background()
serverTools, err := mcpRegistry.ListAllTools(ctx)
if err != nil {
log.Fatal(err)
}
fileTools := []tools.ToolDescriptor{
{
Name: "read_file",
Description: "Read contents of a file",
},
{
Name: "write_file",
Description: "Write contents to a file",
},
{
Name: "list_directory",
Description: "List files in a directory",
},
}
dbTool := &tools.ToolDescriptor{
Name: "query_database",
Description: "Execute SQL query",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"query": {"type": "string"}
}
}`),
}
apiTool := &tools.ToolDescriptor{
Name: "fetch_url",
Description: "Fetch data from URL",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"url": {"type": "string"}
}
}`),
}
calcTool := &tools.ToolDescriptor{
Name: "calculate",
Description: "Perform mathematical calculation",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"expression": {"type": "string"}
}
}`),
}

Bad:

Description: "Gets stuff"

Good:

Description: "Get current weather for a specified city. Returns temperature in Celsius and current conditions."
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name (e.g., 'Paris', 'New York')"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature units",
"default": "celsius"
}
},
"required": ["city"]
}`)
func (e *WeatherExecutor) Execute(
ctx context.Context, descriptor *tools.ToolDescriptor, args json.RawMessage,
) (json.RawMessage, error) {
var params struct {
City string `json:"city"`
}
if err := json.Unmarshal(args, &params); err != nil {
return nil, fmt.Errorf("invalid arguments: %w", err)
}
if params.City == "" {
return nil, errors.New("city is required")
}
weather, err := api.GetWeather(params.City)
if err != nil {
return nil, fmt.Errorf("failed to get weather: %w", err)
}
return json.Marshal(weather)
}

Validate inputs:

if !isValidCity(params.City) {
return "", errors.New("invalid city name")
}

Limit access:

// Only allow reading specific directories
allowedPaths := []string{"/data", "/docs"}

Timeout operations:

ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

Don’t expose sensitive data:

// Bad: Returns API keys
return fmt.Sprintf("API_KEY=%s", apiKey)
// Good: Returns only needed data
return fmt.Sprintf("weather=%s", weather)
func TestWeatherTool(t *testing.T) {
executor := &WeatherExecutor{}
call := &types.ToolCall{
Name: "get_weather",
Arguments: json.RawMessage(`{"city": "Paris"}`),
}
result, err := executor.Execute(context.Background(), call)
assert.NoError(t, err)
assert.Contains(t, result, "temp")
}
# arena.yaml
tests:
- name: Weather Tool Test
prompt: "What's the weather in Paris?"
assertions:
- type: tool_call
tool_name: get_weather
- type: contains
value: "Paris"
type ToolMetrics struct {
CallCount map[string]int
ErrorCount map[string]int
AvgLatency map[string]time.Duration
}
func RecordToolCall(toolName string, duration time.Duration, err error) {
metrics.CallCount[toolName]++
if err != nil {
metrics.ErrorCount[toolName]++
}
metrics.AvgLatency[toolName] = updateAverage(duration)
}
logger.Info("tool executed",
zap.String("tool", call.Name),
zap.Duration("duration", duration),
zap.Bool("success", err == nil),
)

LLM uses multiple tools:

User: "Analyze the sales data"
Tool 1: read_file("sales.csv")
Tool 2: calculate("sum(column)")
Tool 3: create_chart(data)
Response: "Here's your sales analysis [chart]"
if userTier == "premium" {
registry.Register(advancedAnalyticsTool)
}
type CachedExecutor struct {
inner tools.Executor
cache map[string]json.RawMessage
}
func (e *CachedExecutor) Name() string { return e.inner.Name() }
func (e *CachedExecutor) Execute(
ctx context.Context, descriptor *tools.ToolDescriptor, args json.RawMessage,
) (json.RawMessage, error) {
key := getCacheKey(descriptor.Name, args)
if cached, ok := e.cache[key]; ok {
return cached, nil
}
result, err := e.inner.Execute(ctx, descriptor, args)
if err == nil {
e.cache[key] = result
}
return result, err
}

Tools & MCP provide:

Extended capabilities - LLMs can do more
Real-time data - Access current information
Actions - Interact with external systems
Standardization - MCP provides common interface
Composability - Combine multiple tools