This document explains how the MCP (Model Context Protocol) implementation works in go-mcp-host.
The MCP implementation is organized into three main layers:
┌─────────────────────────────────────────────┐
│ Manager (pkg/mcp/manager) │
│ - Manages multiple MCP sessions │
│ - Per-conversation session lifecycle │
│ - Tool/Resource aggregation │
│ - Database persistence │
└──────────────────┬──────────────────────────┘
│
┌─────────┴─────────┐
│ │
┌────────▼────────┐ ┌───────▼────────┐
│ Client Layer │ │ Client Layer │
│ (pkg/mcp/client)│ │ (per server) │
│ - Tool calls │ │ - Resource read│
│ - Notifications │ │ - Prompts │
└────────┬────────┘ └───────┬────────┘
│ │
┌────────▼────────┐ ┌───────▼────────┐
│ Stdio Transport │ │ HTTP Transport │
│ (local servers) │ │(remote servers)│
└─────────────────┘ └────────────────┘
pkg/mcp/protocol/)Defines all MCP protocol types according to the specification.
Key Files:
types.go - All JSON-RPC and MCP message typesKey Types:
JSONRPCRequest, JSONRPCResponse, JSONRPCNotification - JSON-RPC 2.0 messagesInitializeRequest/Result - Connection initializationTool, Resource, Prompt - Core MCP primitivesClientCapabilities, ServerCapabilities - Feature negotiationpkg/mcp/transport/)Handles communication with MCP servers via different mechanisms.
Files:
transport.go - Transport interface definitionstdio.go - Stdio transport implementationhttp.go - HTTP/SSE transport implementationStdio Transport:
// Creates a new process and communicates via stdin/stdout
transport, err := transport.NewStdioTransport(
"npx",
[]string{"-y", "@modelcontextprotocol/server-filesystem", "/tmp"},
[]string{"DEBUG=true"},
)
Features:
exec.CommandHTTP Transport:
// Connects to remote HTTP MCP server
transport, err := transport.NewHTTPTransport(
"https://api.example.com/mcp",
map[string]string{"Authorization": "Bearer token"},
false, // TLS skip verify
)
Features:
pkg/mcp/client/)Implements the MCP client that uses transports to communicate with servers.
Files:
client.go - Core MCP client implementationfactory.go - Client creation from configurationClient Usage:
// Create client with transport
config := client.ClientConfig{
ClientName: "go-mcp-host",
ClientVersion: "1.0.0",
Capabilities: protocol.ClientCapabilities{
Roots: &protocol.RootsCapability{
ListChanged: true,
},
},
}
client := client.NewClient(transport, config)
// Initialize connection
err := client.Initialize(ctx, config)
// List available tools
tools, err := client.ListTools(ctx)
// Call a tool
result, err := client.CallTool(ctx, "read_file", map[string]interface{}{
"path": "/tmp/test.txt",
})
// Set up notification handlers
client.SetOnToolsListChanged(func() {
// Refresh tools when list changes
})
Client Methods:
Initialize() - Perform MCP handshakeListTools() - Discover available toolsCallTool() - Execute a toolListResources() - Discover available resourcesReadResource() - Read resource contentsListPrompts() - Discover available promptsGetPrompt() - Get prompt templatePing() - Health checkClose() - Close connectionpkg/mcp/manager/)Manages multiple MCP clients per conversation with lifecycle and caching.
File:
manager.go - Session manager implementationManager Usage:
// Create manager
manager := manager.NewManager(db, 1*time.Hour) // 1 hour session timeout
// Get or create session for conversation
session, err := manager.GetOrCreateSession(ctx, conversationID, serverConfig)
// Get all tools from all servers in conversation
tools, err := manager.GetAllTools(ctx, conversationID)
// Call a tool on a specific server
result, err := manager.CallTool(ctx, conversationID, serverName, toolName, args)
// Get all resources
resources, err := manager.GetAllResources(ctx, conversationID)
// Read a resource
content, err := manager.ReadResource(ctx, conversationID, serverName, resourceURI)
// Cleanup when conversation ends
err := manager.CloseAllSessionsForConversation(conversationID)
Manager Features:
Session Lifecycle:
MCP servers are configured in config.yaml:
mcp_servers:
# Local filesystem server (stdio)
- name: filesystem
type: stdio
command: npx
args:
- "-y"
- "@modelcontextprotocol/server-filesystem"
- "/Users/yourusername/Documents"
enabled: true
description: "Local filesystem access"
# Remote HTTP server
- name: sentry
type: http
url: "https://mcp.sentry.io"
headers:
Authorization: "Bearer ${SENTRY_TOKEN}"
enabled: false
description: "Sentry integration"
The manager persists sessions and caches in PostgreSQL:
Tables:
mcp_sessions - Active MCP server connections
mcp_tools - Cached tool definitions
tools/list_changed notification receivedmcp_resources - Cached resource metadata
resources/list_changed notification receivedThe agent will use the MCP manager to:
tools, err := mcpManager.GetAllTools(ctx, conversationID)
for _, toolWithServer := range tools {
llmTools = append(llmTools, formatToolForLLM(toolWithServer))
}
result, err := mcpManager.CallTool(
ctx,
conversationID,
serverName,
toolName,
arguments,
)
resources, err := mcpManager.GetAllResources(ctx, conversationID)
for _, res := range resources {
if isRelevant(res.Resource, userQuery) {
content, _ := mcpManager.ReadResource(ctx, conversationID, res.ServerName, res.Resource.URI)
// Add to LLM context
}
}
package main
import (
"context"
"fmt"
"time"
"github.com/d4l-data4life/go-mcp-host/pkg/config"
"github.com/d4l-data4life/go-mcp-host/pkg/mcp/manager"
"github.com/google/uuid"
"gorm.io/gorm"
)
func main() {
// Assume db is initialized
var db *gorm.DB
// Create manager
mcpManager := manager.NewManager(db, 1*time.Hour)
// Configure filesystem server
serverConfig := config.MCPServerConfig{
Name: "filesystem",
Type: "stdio",
Command: "npx",
Args: []string{"-y", "@modelcontextprotocol/server-filesystem", "/tmp"},
Enabled: true,
}
// Create session
ctx := context.Background()
conversationID := uuid.New()
session, err := mcpManager.GetOrCreateSession(ctx, conversationID, serverConfig)
if err != nil {
panic(err)
}
// List tools
tools, err := mcpManager.GetAllTools(ctx, conversationID)
if err != nil {
panic(err)
}
fmt.Printf("Found %d tools:\n", len(tools))
for _, t := range tools {
fmt.Printf(" - %s: %s\n", t.Tool.Name, t.Tool.Description)
}
// Call read_file tool
result, err := mcpManager.CallTool(ctx, conversationID, "filesystem", "read_file", map[string]interface{}{
"path": "/tmp/test.txt",
})
if err != nil {
panic(err)
}
// Print result
for _, content := range result.Content {
fmt.Printf("File contents: %s\n", content.Text)
}
// Cleanup
mcpManager.CloseAllSessionsForConversation(conversationID)
}
The implementation uses Go’s error wrapping with github.com/pkg/errors:
if err != nil {
return nil, errors.Wrap(err, "failed to initialize MCP client")
}
Common errors:
"transport not connected" - Transport was closed or failed"client not initialized" - Forgot to call Initialize()"no active session for server X" - Server not configured or session died"JSON-RPC error CODE: MESSAGE" - Server returned an errorTo test the MCP implementation:
# Start Ollama (for later integration)
ollama serve
# Start PostgreSQL
make docker-database
# Test with a simple MCP server
npx @modelcontextprotocol/inspector npx -y @modelcontextprotocol/server-filesystem /tmp
# Build and run the service
make run
With the MCP implementation complete, the next phase is to:
See TODO.md for detailed task breakdown.