The Quick Answer
Create a high-performance MCP server in Go using the popular mark3labs/mcp-go SDK:
$go get github.com/mark3labs/mcp-go
Create server.go
:
package main
import (
"context"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func main() {
s := server.NewMCPServer("My Server", "1.0.0")
tool := mcp.NewTool("hello",
mcp.WithDescription("Greet someone"),
mcp.WithString("name", mcp.Required()),
)
s.AddTool(tool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
name, _ := req.RequireString("name")
return mcp.NewToolResultText("Hello, " + name + "!"), nil
})
server.ServeStdio(s)
}
Go's strong typing and native concurrency make it ideal for building production MCP servers that need high performance and reliability.
Prerequisites
- Go 1.21 or later installed
- Basic understanding of Go modules and packages
- Familiarity with JSON-RPC protocol concepts
Installation
Install the most popular Go MCP SDK:
$go get github.com/mark3labs/mcp-go
For alternative implementations:
# Metoro's implementation with Gin integration$go get github.com/metoro-io/mcp-golang# Lightweight implementation$go get github.com/riza-io/mcp-go
Configuration
MCP servers in Go support multiple transport modes. Configure your server based on how clients will connect. The mark3labs SDK provides built-in support for stdio, SSE, and HTTP transports.
Configure in Claude Desktop's claude_desktop_config.json
:
{
"mcpServers": {
"my-go-server": {
"command": "go",
"args": ["run", "/path/to/server.go"]
}
}
}
For production deployments, build a binary first:
$CGO_ENABLED=0 go build -o mcp-server
Then update the configuration:
{
"mcpServers": {
"my-go-server": {
"command": "/path/to/mcp-server"
}
}
}
The CGO_ENABLED=0
flag ensures your binary is statically linked, making it portable across different environments without dependency issues.
Usage
Go's type system provides excellent support for MCP's schema requirements. Define structured arguments using Go structs with JSON tags for automatic validation.
Basic Tool Implementation
package main
import (
"context"
"fmt"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
type CalculatorArgs struct {
Operation string `json:"op" jsonschema:"enum=add,enum=subtract"`
X float64 `json:"x" jsonschema:"minimum=0"`
Y float64 `json:"y" jsonschema:"minimum=0"`
}
func main() {
s := server.NewMCPServer("Calculator", "1.0.0")
// Define tool with structured arguments
tool := mcp.NewTool("calculate",
mcp.WithDescription("Perform basic calculations"),
mcp.WithInputSchema(CalculatorArgs{}),
)
s.AddTool(tool, handleCalculate)
server.ServeStdio(s)
}
func handleCalculate(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
var args CalculatorArgs
if err := req.UnmarshalParams(&args); err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
var result float64
switch args.Operation {
case "add":
result = args.X + args.Y
case "subtract":
result = args.X - args.Y
}
return mcp.NewToolResultText(fmt.Sprintf("Result: %.2f", result)), nil
}
The SDK automatically generates JSON schemas from your Go structs, ensuring type safety and validation without manual schema definition.
Adding Resources
Resources provide read-only data access. Go's io.Reader interface makes it natural to expose various data sources:
s.AddResource(mcp.NewResource("config://settings",
mcp.WithResourceDescription("Application settings"),
mcp.WithResourceMimeType("application/json"),
), func(ctx context.Context, req mcp.ReadResourceRequest) (string, error) {
config := map[string]interface{}{
"version": "1.0.0",
"features": []string{"calculator", "converter"},
}
data, err := json.Marshal(config)
if err != nil {
return "", err
}
return string(data), nil
})
Resources use URI schemes to organize different types of data. Common patterns include config://
, data://
, and file://
prefixes.
Implementing Prompts
Prompts allow servers to provide templated interactions:
prompt := mcp.NewPrompt("debug_issue",
mcp.WithPromptDescription("Help debug an issue"),
mcp.WithString("error_message",
mcp.Required(),
mcp.Description("The error message to debug"),
),
)
s.AddPrompt(prompt, func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
errorMsg, _ := req.RequireString("error_message")
return mcp.NewPromptResult(
mcp.NewPromptMessage(
mcp.PromptMessageRoleUser,
mcp.NewTextContent(fmt.Sprintf(
"I'm seeing this error: %s\nCan you help me understand what's wrong?",
errorMsg,
)),
),
), nil
})
Advanced Usage
Go's concurrency primitives excel at handling complex MCP scenarios. Session management, streaming responses, and multi-tenant support benefit from goroutines and channels.
Session Management
type UserSession struct {
id string
data map[string]interface{}
mu sync.RWMutex
}
func (s *UserSession) SessionID() string {
return s.id
}
func (s *UserSession) Get(key string) (interface{}, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
val, ok := s.data[key]
return val, ok
}
func (s *UserSession) Set(key string, value interface{}) {
s.mu.Lock()
defer s.mu.Unlock()
s.data[key] = value
}
// Use in server setup
s := server.NewMCPServer("Stateful Server", "1.0.0",
server.WithSessionProvider(func(id string) server.ClientSession {
return &UserSession{
id: id,
data: make(map[string]interface{}),
}
}),
)
Session state enables maintaining context across multiple tool calls, essential for complex workflows that span multiple interactions.
HTTP Transport with Streaming
For web-based clients, HTTP transport with Server-Sent Events provides real-time updates:
package main
import (
"github.com/mark3labs/mcp-go/server"
"net/http"
)
func main() {
s := server.NewMCPServer("HTTP Server", "1.0.0")
// Add tools, resources, prompts...
// Serve with SSE support
http.HandleFunc("/sse", server.HandleSSE(s))
http.ListenAndServe(":8080", nil)
}
The SSE transport automatically handles connection management, heartbeats, and reconnection logic.
Common Issues
Error: "nil pointer dereference" when creating server
This occurs when the SDK's internal initialization order is disrupted. The mark3labs SDK requires specific initialization sequences for client connections. Always create the server instance before adding any tools or resources:
// Correct order
s := server.NewMCPServer("My Server", "1.0.0")
s.AddTool(tool, handler) // Add after creation
// Incorrect - will cause nil pointer
var s *server.MCPServer
s.AddTool(tool, handler) // s is nil
s = server.NewMCPServer("My Server", "1.0.0")
To prevent this, use the constructor pattern and configure everything through options when possible.
Error: "duplicate tool name"
Go maps silently overwrite duplicate keys, making it easy to accidentally replace tools. The MCP protocol requires unique tool names across a server. Track registered tools explicitly:
toolRegistry := make(map[string]bool)
func addToolSafely(s *server.MCPServer, tool *mcp.Tool, handler mcp.ToolHandler) error {
if toolRegistry[tool.Name] {
return fmt.Errorf("tool %s already registered", tool.Name)
}
s.AddTool(tool, handler)
toolRegistry[tool.Name] = true
return nil
}
This pattern helps catch configuration errors early during server startup rather than discovering conflicts at runtime.
Error: "transport already in use"
Each transport mode (stdio, SSE, HTTP) binds to specific system resources. Attempting to serve multiple transports simultaneously from the same server instance causes conflicts:
// This will fail - can't use both
server.ServeStdio(s)
server.ServeSSE(s, ":8080")
// Solution: Choose one transport per process
if os.Getenv("MCP_TRANSPORT") == "http" {
server.ServeSSE(s, ":8080")
} else {
server.ServeStdio(s)
}
For multi-transport support, run separate processes or use a transport multiplexer pattern.
Examples
File System MCP Server
This example demonstrates exposing file system operations through MCP, showcasing Go's excellent file handling capabilities:
package main
import (
"context"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
type FileArgs struct {
Path string `json:"path" jsonschema:"description=File path relative to base directory"`
}
func main() {
baseDir := "/safe/directory" // Restrict access
s := server.NewMCPServer("File Server", "1.0.0")
// List files tool
s.AddTool(
mcp.NewTool("list_files",
mcp.WithDescription("List files in directory"),
mcp.WithInputSchema(FileArgs{}),
),
func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
var args FileArgs
req.UnmarshalParams(&args)
fullPath := filepath.Join(baseDir, args.Path)
files, err := ioutil.ReadDir(fullPath)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
fileList := make([]map[string]interface{}, 0)
for _, f := range files {
fileList = append(fileList, map[string]interface{}{
"name": f.Name(),
"size": f.Size(),
"isDir": f.IsDir(),
})
}
data, _ := json.MarshalIndent(fileList, "", " ")
return mcp.NewToolResultText(string(data)), nil
},
)
server.ServeStdio(s)
}
This server restricts file access to a specific directory, preventing security issues while providing useful file system functionality. The pattern of using a base directory ensures the MCP client cannot access sensitive system files.
External API Integration
Integrating external APIs demonstrates Go's HTTP client capabilities and error handling:
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
type WeatherResponse struct {
Main struct {
Temp float64 `json:"temp"`
Humidity int `json:"humidity"`
} `json:"main"`
Weather []struct {
Description string `json:"description"`
} `json:"weather"`
}
func main() {
s := server.NewMCPServer("Weather Service", "1.0.0")
client := &http.Client{Timeout: 10 * time.Second}
s.AddTool(
mcp.NewTool("get_weather",
mcp.WithDescription("Get current weather"),
mcp.WithString("city", mcp.Required()),
),
func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
city, _ := req.RequireString("city")
apiKey := os.Getenv("OPENWEATHER_API_KEY")
url := fmt.Sprintf(
"https://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s&units=metric",
city, apiKey,
)
resp, err := client.Get(url)
if err != nil {
return mcp.NewToolResultError("Failed to fetch weather"), nil
}
defer resp.Body.Close()
var weather WeatherResponse
json.NewDecoder(resp.Body).Decode(&weather)
result := fmt.Sprintf(
"Weather in %s: %.1f°C, %s (Humidity: %d%%)",
city,
weather.Main.Temp,
weather.Weather[0].Description,
weather.Main.Humidity,
)
return mcp.NewToolResultText(result), nil
},
)
server.ServeStdio(s)
}
The HTTP client timeout prevents hanging requests, while structured response types ensure reliable parsing. Production deployments should add retry logic, circuit breakers, and comprehensive error handling for resilience.
With installation complete and basic examples covered, you're ready to build production-grade MCP servers in Go. The language's performance characteristics and simple deployment model make it an excellent choice for MCP servers that need to scale.
Related Guides
Building an MCP server in TypeScript
Build type-safe MCP servers using TypeScript with full SDK support and production best practices.
Building an MCP server in Python using FastMCP
Build production-ready MCP servers in Python using the FastMCP framework with automatic tool handling.
Installing MCP servers globally vs locally: which approach to choose
Choose between global and local MCP server installations based on your project needs and workflow.