Building an MCP Server in Go: A Hands-On Look at Model Context Protocol's Tool Discovery
The Anatomy of an MCP Handshake: Beyond the Basics
At its core, the Model Context Protocol (MCP) is a formalized conversation between an AI model (the host) and an external tool server. This conversation begins with a discovery phase, governed by strict JSON-RPC 2.0 specifications. Understanding these initial messages is critical for building robust, interoperable tools. The host doesn't just "find" your tool; it asks for it through a precise sequence.
The protocol starts with an initialize request. The client (the AI host) sends its capabilities and protocol version. Your server must respond with its own capabilities and, most importantly, confirm the protocol version. This ensures both parties are speaking the same language. Only after this handshake is complete does the host request the tool list with an tools/list call. Your server's response to this is the heart of tool discovery: a structured JSON array describing every tool it offers, its parameters, and its return schema.
Dissecting the Discovery: The `tools/list` Response
When the host sends a tools/list request, your server's response is a direct advertisement of its capabilities. Each tool in the returned array is an object with a unique name, a human-readable description (which the AI uses to decide when to call the tool), and a parameters object defining the input schema. This schema isn't just a suggestion; it's a JSON Schema document that allows the host to validate inputs before execution.
Consider a tool designed to fetch a GitHub user's public repositories. Its entry in the discovery list might specify a required parameter username of type string. The AI model, upon understanding a user's request like "Show me projects by tor-nexus," uses the tool's description to select it, then validates the username against the schema before your server ever sees the call. This pre-validation is a cornerstone of MCP's reliability and security, preventing malformed requests from entering your processing pipeline.
Our Minimal MCP Server: 50 Lines of Go
We'll build a simple server that exposes a single "greet" tool. It will listen for standard input/output (stdio) connections, the most common transport for MCP, allowing it to be seamlessly discovered by tools like TormentNexus. Here is the complete, functional server.
package main
import (
"encoding/json"
"fmt"
"os"
)
type MCPRequest struct {
JSONRPC string `json:"jsonrpc"`
ID interface{} `json:"id"`
Method string `json:"method"`
Params interface{} `json:"params,omitempty"`
}
type MCPResponse struct {
JSONRPC string `json:"jsonrpc"`
ID interface{} `json:"id"`
Result interface{} `json:"result,omitempty"`
Error interface{} `json:"error,omitempty"`
}
func main() {
var req MCPRequest
decoder := json.NewDecoder(os.Stdin)
encoder := json.NewEncoder(os.Stdout)
for decoder.More() {
decoder.Decode(&req)
var resp MCPResponse
resp.JSONRPC = "2.0"
resp.ID = req.ID
switch req.Method {
case "initialize":
resp.Result = map[string]interface{}{
"protocolVersion": "2024-11-05",
"capabilities": map[string]interface{}{"tools": map[string]interface{}{}},
"serverInfo": map[string]interface{}{"name": "GoGreetServer", "version": "0.1.0"},
}
case "tools/list":
resp.Result = map[string]interface{}{
"tools": []interface{}{
map[string]interface{}{
"name": "greet",
"description": "Greets a user by their name.",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string",
"description": "The name of the user to greet",
},
},
"required": []string{"name"},
},
},
},
}
case "tools/call":
params := req.Params.(map[string]interface{})
toolName := params["name"].(string)
arguments := params["arguments"].(map[string]interface{})
if toolName == "greet" {
name := arguments["name"].(string)
greeting := fmt.Sprintf("Hello, %s! Welcome to the MCP universe.", name)
resp.Result = map[string]interface{}{
"content": []interface{}{
map[string]interface{}{
"type": "text",
"text": greeting,
},
},
}
}
default:
resp.Error = map[string]interface{}{"code": -32601, "message": "Method not found"}
}
encoder.Encode(resp)
}
}
That's it. In roughly 50 lines, we have a server that can handle the three critical phases: initialization, tool discovery, and tool execution. We used Go's powerful standard library for JSON encoding and stdio handling, requiring no external dependencies. The logic is straightforward: read a request, switch on the method, and encode a properly formatted JSON-RPC response.
Auto-Discovery in Action with TormentNexus
The true test of a server is its discovery. Let's run our Go server and point TormentNexus at it. First, we compile and run our server, which now waits for input on standard input. In a separate terminal, we use the TormentNexus CLI, which is designed to interact with MCP servers via stdio pipes.
# Build and run the Go MCP server in the background
go run server.go &
# Use a named pipe (FIFO) to communicate, a common pattern for MCP
mkfifo /tmp/mcp_pipe
echo '{}' > /tmp/mcp_pipe & # Kickstart the communication
cat /tmp/mcp_pipe | go run server.go | tee /tmp/mcp_out
With this pipe established, we launch TormentNexus and tell it to connect. The system performs the handshake flawlessly. It sends the initialize request, receives our capabilities, then immediately sends tools/list. TormentNexus parses our schema, recognizes the greet tool, and indexes it. From this point on, any natural language prompt that involves greeting a user—like "Say hi to Alex"—can be routed by the AI model directly to our server. TormentNexus handles the translation from "greet Alex" to the precise JSON-RPC call {"method": "tools/call", "params": {"name": "greet", "arguments": {"name": "Alex"}}}.
Why This Strict Protocol Matters for Scalable Tool Ecosystems
You might wonder why we don't just use a simple REST API. The MCP's structured discovery phase, powered by JSON-RPC, solves several key problems in the AI tool ecosystem simultaneously. First, it enables **type safety and validation at the protocol level**. The AI host knows the exact shape of your tool's inputs before invoking it. Second, it creates a **uniform discovery interface**. TormentNexus, or any MCP client, doesn't need custom adapters for your tool; it just follows the standard, dramatically reducing integration friction. Finally, this structure allows for advanced features like capability negotiation and future extensibility without breaking existing tools. When you build for MCP, you're not just building a tool; you're building a node in a growing, interoperable network of AI-accessible services.
Ready to build your own tool and see it discovered in real-time? Explore more deep dives, advanced patterns, and the full power of the TormentNexus MCP ecosystem at https://tormentnexus.site.