Skip to content

18. Tool Protocols and Tool Servers

Why This Chapter?

As agents grow, tools become complex services. Instead of embedding tool code directly, you can run tools as separate processes or services. This requires protocols for communication, versioning, and security.

This chapter covers tool server patterns: stdio protocols, HTTP API, gRPC, schema versioning, and authentication/authorization.

Real-World Case Study

Situation: You have 50+ tools. Some are written in Go, some in Python, some are external services. Embedding all in one agent binary is impractical.

Problem:

  • Different languages require different integration approaches
  • Tool updates require agent redeployment
  • No isolation between tools
  • Hard to scale individual tools

Solution: Tool servers: Each tool runs as a separate process/service. Agent communicates through a standard protocol (stdio, HTTP, or gRPC). Tools can be updated independently, scaled separately, and isolated for security.

Theory in Simple Terms

Tool Server Architecture

Agent Runtime:

  • Manages conversation flow
  • Calls tools through protocol
  • Processes tool responses

Tool Server:

  • Implements tool logic
  • Provides protocol interface
  • Can be a separate process/service

Protocol:

  • Communication contract
  • Request/response format
  • Error handling

Protocol Types

1. stdio Protocol:

  • Tool runs as subprocess
  • Communication through stdin/stdout
  • Simple, good for local tools

2. HTTP Protocol:

  • Tool runs as HTTP service
  • REST API interface
  • Good for distributed systems

3. gRPC Protocol:

  • Tool runs as a gRPC service
  • Strict contract via Protobuf (IDL)
  • Type safety and backward compatibility of schemas
  • Rich Go ecosystem: client/server code generation, interceptors, reflection
  • Built-in mechanisms: TLS/mTLS, authentication via metadata, deadlines, retries, load balancing
  • Observability: integration with tracing/metrics/logging
  • A practical choice for production tool servers

How It Works (Step by Step)

Step 1: Tool Protocol Interface

type ToolServer interface {
    ListTools() ([]ToolDefinition, error)
    ExecuteTool(name string, args map[string]any) (any, error)
}

type ToolDefinition struct {
    Name        string
    Description string
    Schema      json.RawMessage
    Version     string
}

Step 2: stdio Protocol

// Tool server reads from stdin, writes to stdout
type StdioToolServer struct {
    tools map[string]Tool
}

func (s *StdioToolServer) Run() {
    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        var req ToolRequest
        json.Unmarshal(scanner.Bytes(), &req)

        result, err := s.ExecuteTool(req.Name, req.Args)

        resp := ToolResponse{
            Result: result,
            Error:  errString(err),
        }

        json.NewEncoder(os.Stdout).Encode(resp)
    }
}

type ToolRequest struct {
    Name string
    Args map[string]any
}

type ToolResponse struct {
    Result any
    Error  string
}

Step 3: Agent Calls Tool Server

func executeToolViaStdio(toolName string, args map[string]any) (any, error) {
    cmd := exec.Command("tool-server")
    stdin, _ := cmd.StdinPipe()
    stdout, _ := cmd.StdoutPipe()

    cmd.Start()

    req := ToolRequest{Name: toolName, Args: args}
    json.NewEncoder(stdin).Encode(req)
    stdin.Close()

    var resp ToolResponse
    json.NewDecoder(stdout).Decode(&resp)

    cmd.Wait()

    if resp.Error != "" {
        return nil, fmt.Errorf(resp.Error)
    }
    return resp.Result, nil
}

Step 4: HTTP Protocol

// Tool server as HTTP service
func (s *HTTPToolServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if r.Method != "POST" {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    var req ToolRequest
    json.NewDecoder(r.Body).Decode(&req)

    result, err := s.ExecuteTool(req.Name, req.Args)

    resp := ToolResponse{
        Result: result,
        Error:  errString(err),
    }

    json.NewEncoder(w).Encode(resp)
}

Step 5: gRPC Protocol

gRPC provides a strict contract via Protocol Buffers. Service definition:

syntax = "proto3";

package tools.v1;

service ToolServer {
  rpc ListTools(ListToolsRequest) returns (ListToolsResponse);
  rpc ExecuteTool(ExecuteToolRequest) returns (ExecuteToolResponse);
}

message ListToolsRequest {
  string version = 1; // Protocol version
}

message ListToolsResponse {
  repeated ToolDefinition tools = 1;
}

message ExecuteToolRequest {
  string tool_name = 1;
  string version = 2;
  bytes arguments = 3; // JSON-serialized arguments
}

message ExecuteToolResponse {
  bytes result = 1;
  string error = 2;
}

message ToolDefinition {
  string name = 1;
  string description = 2;
  string schema = 3; // JSON Schema
  string version = 4;
  repeated string compatible_versions = 5;
}

Advantages of gRPC for tool servers:

  • Strict contract: Protobuf guarantees type safety and schema evolution without breaking changes
  • Go ecosystem: Automatic client/server generation, interceptors for authn/authz, health checks
  • Security: Built-in TLS/mTLS support, authentication via metadata (tokens, API keys)
  • Reliability: Deadlines, retries, load balancing at client level or via service mesh
  • Observability: Integration with OpenTelemetry, gRPC metrics, structured logging

Step 6: Schema Versioning

type ToolDefinition struct {
    Name        string
    Version     string
    Schema      json.RawMessage
    CompatibleVersions []string
}

func (s *ToolServer) GetToolDefinition(name string, version string) (*ToolDefinition, error) {
    tool := s.tools[name]
    if tool.Version == version {
        return &tool, nil
    }

    // Check compatibility
    for _, v := range tool.CompatibleVersions {
        if v == version {
            return &tool, nil
        }
    }

    return nil, fmt.Errorf("incompatible version")
}

Common Errors

Error 1: No Versioning

Symptom: Tool updates break the agent.

Cause: No versioning, agent expects old interface.

Solution: Version tool schemas, check compatibility.

Error 2: No Authentication

Symptom: Unauthorized access to tools.

Cause: No authn/authz for tool servers.

Solution: Implement authentication (API keys, tokens).

Mini-Exercises

Exercise 1: Implement stdio Tool Server

Create a tool server that communicates via stdio:

func main() {
    server := NewStdioToolServer()
    server.Run()
}

Expected result:

  • Reads requests from stdin
  • Executes tools
  • Writes responses to stdout

Completion Criteria / Checklist

Completed:

  • Understand tool server architecture
  • Can implement stdio protocol
  • Can implement HTTP protocol
  • Understand advantages of gRPC for production tool servers
  • Understand schema versioning

Not completed:

  • No versioning, updates break compatibility
  • No authentication, security risk

Connection with Other Chapters

What's Next?

After understanding tool protocols, proceed to:


Navigation: ← Security and Governance | Table of Contents | Observability and Tracing →