genaimcpllm

Building MCP Servers in Go: From Basic Tools to Interactive Experiences

MCP (Model Context Protocol) lets you build tools that AI models can use. Here are three approaches in Go, from simple to interactive.

Part 1: Basic MCP Server

Start with a simple addition server using the mcp-go package:

package main

import (
    "context"
    "fmt"
    "log"
    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

func main() {
    s := server.NewStdioServer()
    
    // Add our tool
    s.AddTool("add_numbers", "Add two numbers together", map[string]interface{}{
        "type": "object",
        "properties": map[string]interface{}{
            "a": map[string]interface{}{"type": "number"},
            "b": map[string]interface{}{"type": "number"},
        },
        "required": []string{"a", "b"},
    }, handleAdd)
    
    log.Fatal(s.Serve())
}

func handleAdd(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    args := req.Params.Arguments
    
    a := args["a"].(float64)
    b := args["b"].(float64)
    result := a + b
    
    return &mcp.CallToolResult{
        Content: []mcp.Content{{
            Type: "text",
            Text: fmt.Sprintf("%.1f", result),
        }},
    }, nil
}

Debugging with mcp-inspector

Install the inspector:

npm install -g @modelcontextprotocol/inspector

Run your server and connect:

# Terminal 1
go run main.go

# Terminal 2
mcp-inspector go run main.go

The inspector gives you a web interface to test tool calls and debug issues.

Part 2: Streaming HTTP

Standard MCP uses stdio, but you can also do HTTP with streaming responses:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "time"
)

type Request struct {
    Arguments map[string]interface{} `json:"arguments"`
}

func handleAdd(w http.ResponseWriter, r *http.Request) {
    var req Request
    json.NewDecoder(r.Body).Decode(&req)
    
    w.Header().Set("Content-Type", "application/json")
    
    a := req.Arguments["a"].(float64)
    b := req.Arguments["b"].(float64)
    
    encoder := json.NewEncoder(w)
    
    // Send progress updates (streaming demo)
    for i := 25; i <= 100; i += 25 {
        encoder.Encode(map[string]interface{}{
            "type": "progress", 
            "value": i,
        })
        w.(http.Flusher).Flush()
        time.Sleep(200 * time.Millisecond)
    }
    
    // Send result
    result := a + b
    encoder.Encode(map[string]interface{}{
        "result": fmt.Sprintf("%.1f", result),
    })
}

func main() {
    http.HandleFunc("/add", handleAdd)
    fmt.Println("Server running on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Testing with curl

# Call the add tool
curl -X POST http://localhost:8080/add \
  -H "Content-Type: application/json" \
  -d '{"arguments":{"a":5,"b":3}}' \
  --no-buffer

# Shows streaming progress updates followed by the result

The --no-buffer flag shows streaming responses as they arrive.

Part 3: Elicitation for Interactive Tools

Here's where it gets interesting - tools that ask users for additional input:

package main

import (
    "context"
    "fmt"
    "log"
    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

func main() {
    s := server.NewStdioServer()
    
    // Interactive tool - only needs one number
    s.AddTool("add_interactive", "Add numbers with user prompt", map[string]interface{}{
        "type": "object",
        "properties": map[string]interface{}{
            "number": map[string]interface{}{"type": "number"},
        },
        "required": []string{"number"},
    }, handleInteractive)
    
    log.Fatal(s.Serve())
}

func handleInteractive(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    args := req.Params.Arguments
    num := args["number"].(float64)
    
    return &mcp.CallToolResult{
        Content: []mcp.Content{{
            Type: "text",
            Text: fmt.Sprintf("Got first number: %.1f", num),
        }},
        Meta: map[string]interface{}{
            "elicit": map[string]interface{}{
                "prompt": "Enter the second number:",
                "callback": "complete_add",
            },
        },
    }, nil
}

The elicitation pattern lets tools gather additional info from users when needed. The tool starts with one number and prompts for the second.

Why This Matters

MCP isn't just about building static tools - it's about creating dynamic experiences that adapt to what AI models and users need.

These three patterns cover most use cases:

  • Basic stdio: Simple, reliable tools
  • HTTP streaming: Complex integrations with progress feedback
  • Elicitation: Interactive experiences that gather user input

The protocol is designed to be extended, so there's room for creativity here.