mcp go golang tutorial

MCP in Go: Connecting Agents to Memory Stores

Learn how to build MCP clients in Go and connect your AI agents to persistent memory stores like CodeMem.

CodeMem Team

Why Go for MCP Clients?

Go has become the language of choice for building robust infrastructure tools. Its excellent concurrency primitives, simple deployment (single binary), and strong standard library make it perfect for building MCP (Model Context Protocol) clients that connect AI agents to external services.

Whether you're building a custom AI agent, a development tool, or integrating memory capabilities into your existing Go application, understanding how to implement MCP clients opens up powerful possibilities.

Understanding MCP Architecture

MCP uses JSON-RPC 2.0 over HTTP for server-client communication. An MCP client needs to:

  • Discover available tools from the server
  • Send tool invocation requests with proper parameters
  • Handle responses and errors gracefully

Let's build a Go client that connects to CodeMem's MCP server and manages memories programmatically.

Setting Up the Project

First, create a new Go module and define the core types:

package codemem

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

// MCPRequest represents a JSON-RPC 2.0 request
type MCPRequest struct {
    JSONRPC string      `json:"jsonrpc"`
    ID      int         `json:"id"`
    Method  string      `json:"method"`
    Params  interface{} `json:"params,omitempty"`
}

// MCPResponse represents a JSON-RPC 2.0 response
type MCPResponse struct {
    JSONRPC string          `json:"jsonrpc"`
    ID      int             `json:"id"`
    Result  json.RawMessage `json:"result,omitempty"`
    Error   *MCPError       `json:"error,omitempty"`
}

// MCPError represents an error in the response
type MCPError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

Building the Client

Now let's create a reusable client struct with methods for interacting with CodeMem:

// Client handles communication with CodeMem MCP server
type Client struct {
    baseURL    string
    apiKey     string
    httpClient *http.Client
    requestID  int
}

// NewClient creates a new CodeMem MCP client
func NewClient(apiKey string) *Client {
    return &Client{
        baseURL:    "https://app.codemem.dev/mcp",
        apiKey:     apiKey,
        httpClient: &http.Client{Timeout: 30 * time.Second},
        requestID:  0,
    }
}

// call sends an MCP request and returns the response
func (c *Client) call(method string, params interface{}) (*MCPResponse, error) {
    c.requestID++
    
    req := MCPRequest{
        JSONRPC: "2.0",
        ID:      c.requestID,
        Method:  method,
        Params:  params,
    }
    
    body, err := json.Marshal(req)
    if err != nil {
        return nil, fmt.Errorf("marshal request: %w", err)
    }
    
    httpReq, err := http.NewRequest("POST", c.baseURL, bytes.NewReader(body))
    if err != nil {
        return nil, fmt.Errorf("create request: %w", err)
    }
    
    httpReq.Header.Set("Content-Type", "application/json")
    httpReq.Header.Set("Authorization", "Bearer "+c.apiKey)
    
    resp, err := c.httpClient.Do(httpReq)
    if err != nil {
        return nil, fmt.Errorf("send request: %w", err)
    }
    defer resp.Body.Close()
    
    var mcpResp MCPResponse
    if err := json.NewDecoder(resp.Body).Decode(&mcpResp); err != nil {
        return nil, fmt.Errorf("decode response: %w", err)
    }
    
    if mcpResp.Error != nil {
        return nil, fmt.Errorf("mcp error %d: %s", 
            mcpResp.Error.Code, mcpResp.Error.Message)
    }
    
    return &mcpResp, nil
}

Implementing Memory Operations

With the base client ready, let's add methods for the core memory operations:

// Memory represents a stored memory item
type Memory struct {
    ID        string    `json:"id"`
    Content   string    `json:"content"`
    Project   string    `json:"project,omitempty"`
    CreatedAt time.Time `json:"created_at"`
}

// AddMemory stores a new memory
func (c *Client) AddMemory(content, project string) (*Memory, error) {
    params := map[string]interface{}{
        "name": "add_memory",
        "arguments": map[string]string{
            "content": content,
            "project": project,
        },
    }
    
    resp, err := c.call("tools/call", params)
    if err != nil {
        return nil, err
    }
    
    var memory Memory
    if err := json.Unmarshal(resp.Result, &memory); err != nil {
        return nil, fmt.Errorf("parse memory: %w", err)
    }
    
    return &memory, nil
}

// SearchMemories finds memories matching a query
func (c *Client) SearchMemories(query string, limit int) ([]Memory, error) {
    params := map[string]interface{}{
        "name": "search_memories",
        "arguments": map[string]interface{}{
            "query": query,
            "limit": limit,
        },
    }
    
    resp, err := c.call("tools/call", params)
    if err != nil {
        return nil, err
    }
    
    var memories []Memory
    if err := json.Unmarshal(resp.Result, &memories); err != nil {
        return nil, fmt.Errorf("parse memories: %w", err)
    }
    
    return memories, nil
}

Putting It All Together

Here's how to use the client in your application:

func main() {
    client := codemem.NewClient(os.Getenv("CODEMEM_API_KEY"))
    
    // Store a coding preference
    memory, err := client.AddMemory(
        "Project uses Go 1.22 with generics for type-safe collections",
        "my-go-project",
    )
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Stored memory: %s\n", memory.ID)
    
    // Search for relevant context
    memories, err := client.SearchMemories("generics", 5)
    if err != nil {
        log.Fatal(err)
    }
    
    for _, m := range memories {
        fmt.Printf("Found: %s\n", m.Content)
    }
}

Best Practices for Go MCP Clients

  • Use contexts: Add context.Context to all methods for timeout and cancellation control
  • Implement retries: Network issues happen—use exponential backoff for transient failures
  • Pool connections: Reuse the HTTP client to benefit from connection pooling
  • Handle rate limits: Respect Retry-After headers when you hit API limits
  • Log strategically: Use structured logging for debugging without exposing sensitive data

What's Next?

You now have the foundation for building Go applications that leverage AI memory. Consider extending this client with:

  • Concurrent memory operations with goroutines
  • Local caching for frequently accessed memories
  • Integration with your existing agent framework

Ready to Build?

Get your free CodeMem API key and start building memory-powered Go applications today. Your AI agents deserve persistent context.

Get Your API Key →