After successfully building a weather MCP server using Python with stdio transport, I decided to explore the Go ecosystem for MCP development. This journey led me to discover the power of go-zero framework and Server-Sent Events (SSE) for building robust MCP servers. Today, I’ll share my experience building an intelligent calculator server that showcases a different approach to MCP implementation.
Why Go for MCP Development?
While Python provides excellent MCP libraries and is perfect for rapid prototyping, Go offers several advantages for production MCP servers:
- Performance: Go’s compiled nature and excellent concurrency support
- Deployment: Single binary deployment without dependency management
- Scalability: Built-in goroutines for handling multiple concurrent connections
- Type Safety: Strong typing system reduces runtime errors
The choice of go-zero framework was particularly compelling because it handles all the underlying complexities, allowing me to focus on business logic and creating intelligent experiences.
The Project: Building an Intelligent Calculator
Unlike my previous weather server that integrated with external APIs, this calculator server demonstrates pure computational capabilities. The server exposes a single powerful tool:
- calculator: Performs basic mathematical operations (add, subtract, multiply, divide) with intelligent error handling and formatted responses
My Go-Zero Journey
Project Setup and Configuration
I started by setting up the project structure. Go-zero emphasizes configuration-driven development, so I began with a clean configuration file:
# config.yaml
name: calculator
port: 8080
This simple configuration tells go-zero everything it needs to know about our service - the name and the port to run on.
Core Server Implementation
The beauty of go-zero lies in its simplicity. Here’s how I implemented the main server logic:
package main
import (
"context"
"fmt"
"log"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/mcp"
)
func main() {
// Load configuration
var c mcp.McpConf
conf.MustLoad("config.yaml", &c)
// Create MCP server
server := mcp.NewMcpServer(c)
defer server.Stop()
// Register calculator tool
calculatorTool := mcp.Tool{
Name: "calculator",
Description: "Perform basic mathematical operations",
InputSchema: createCalculatorSchema(),
Handler: handleCalculatorOperation,
}
// Register tool with server
if err := server.RegisterTool(calculatorTool); err != nil {
log.Fatalf("Failed to register calculator tool: %v", err)
}
fmt.Printf("Starting MCP server on port: %d\n", c.Port)
server.Start()
}
Building the Calculator Tool Schema
One of the most important aspects of MCP tool development is creating a comprehensive input schema. This schema helps AI models understand exactly what parameters are expected:
func createCalculatorSchema() mcp.InputSchema {
return mcp.InputSchema{
Properties: map[string]any{
"operation": map[string]any{
"type": "string",
"description": "The operation to perform (add, subtract, multiply, divide)",
"enum": []string{"add", "subtract", "multiply", "divide"},
},
"a": map[string]any{
"type": "number",
"description": "The first operand",
},
"b": map[string]any{
"type": "number",
"description": "The second operand",
},
},
Required: []string{"operation", "a", "b"},
}
}
Implementing the Calculator Handler
The handler function is where the actual business logic lives. I focused on creating a robust implementation with proper error handling:
func handleCalculatorOperation(ctx context.Context, params map[string]any) (any, error) {
var req struct {
Operation string `json:"operation"`
A float64 `json:"a"`
B float64 `json:"b"`
}
if err := mcp.ParseArguments(params, &req); err != nil {
return nil, fmt.Errorf("failed to parse arguments: %v", err)
}
// Execute operation
var result float64
switch req.Operation {
case "add":
result = req.A + req.B
case "subtract":
result = req.A - req.B
case "multiply":
result = req.A * req.B
case "divide":
if req.B == 0 {
return nil, fmt.Errorf("division by zero is not allowed")
}
result = req.A / req.B
default:
return nil, fmt.Errorf("unknown operation: %s", req.Operation)
}
// Return formatted result
return map[string]any{
"expression": fmt.Sprintf("%g %s %g", req.A, getOperationSymbol(req.Operation), req.B),
"result": result,
}, nil
}
func getOperationSymbol(op string) string {
switch op {
case "add":
return "+"
case "subtract":
return "-"
case "multiply":
return "×"
case "divide":
return "÷"
default:
return op
}
}
Running and Testing the Server
Local Development
Starting the server is straightforward:
go run main.go
If everything is configured correctly, you’ll see:
Starting MCP server on port: 8080
The server now runs on port 8080, ready to accept MCP connections via Server-Sent Events.
Claude Desktop Integration
The integration with Claude Desktop required a different configuration approach compared to my Python weather server. Since this server uses HTTP/SSE instead of stdio, I needed to use the mcp-remote
package:
{
"mcpServers": {
"calculator": {
"command": "npx",
"args": ["mcp-remote", "http://localhost:8080/sse"]
}
}
}
Key difference: Instead of directly running the Go binary, we use mcp-remote
to bridge between Claude Desktop and our HTTP-based MCP server.
Real-World Testing Results
After restarting Claude Desktop, I tested the calculator with various queries:
Successful Operations
- “Calculate 15 + 27” ✅
- “What’s 144 divided by 12?” ✅
- “Multiply 8.5 by 3.2” ✅
- “Subtract 45 from 100” ✅
Error Handling
- “Divide 10 by 0” → Proper error message about division by zero ✅
- Invalid operations → Clear error messages ✅
Response Quality
The structured response format proved particularly effective:
{
"expression": "15 + 27",
"result": 42
}
Claude interprets this beautifully, presenting results in a conversational format like: “15 + 27 equals 42.”
Lessons Learned and Best Practices
1. SSE vs Stdio Transport
The transition from stdio (Python) to SSE (Go) revealed important differences:
SSE Advantages:
- HTTP-based communication is more familiar to web developers
- Easier to debug with standard HTTP tools
- Better suited for distributed deployments
- Natural fit for web-based MCP clients
Stdio Advantages:
- Direct process communication
- Lower overhead
- Simpler configuration in Claude Desktop
2. Go-Zero Framework Benefits
Using go-zero provided several advantages:
- Configuration Management: Centralized configuration handling
- Graceful Shutdown: Built-in server lifecycle management
- Tool Registration: Clean API for registering MCP tools
- Error Handling: Consistent error handling patterns
3. Type Safety Matters
Go’s strong typing system caught several issues during development:
- Parameter validation happens at compile time
- JSON marshaling/unmarshaling is type-safe
- Interface definitions prevent runtime errors
4. Debugging Strategies
For Go-based MCP servers:
- Use
fmt.Printf
for debugging during development - Check network connectivity with
curl http://localhost:8080/sse
- Validate JSON responses with standard HTTP tools
- Monitor Claude Desktop logs for connection issues
Performance Insights
The Go implementation showed significant performance improvements:
- Startup Time: Nearly instantaneous compared to Python
- Memory Usage: Significantly lower baseline memory consumption
- Response Time: Faster mathematical operations and JSON serialization
- Concurrent Connections: Better handling of multiple simultaneous requests
Comparison: Python vs Go for MCP Development
Having built MCP servers in both languages, here’s my assessment:
Python Strengths
- Rapid prototyping
- Rich ecosystem of libraries
- Excellent for data processing and API integration
- Built-in MCP libraries
Go Strengths
- Superior performance and memory efficiency
- Single binary deployment
- Excellent concurrency support
- Strong typing system
- Better suited for production deployments
When to Choose Each
- Python: Rapid prototyping, complex data processing, extensive third-party integrations
- Go: Production deployments, performance-critical applications, microservices architectures