
Building Model Context Protocol Servers in Go: Enhancing AI Tools with Your Data
This blog is also available as a video lesson that you can watch for free here
If you’ve been on Twitter lately, you’ve probably seen a lot of buzz about the Model Context Protocol (MCP). This open standard is gaining traction among developers looking to enhance their AI-powered tools with private data sources. But what exactly is MCP, and more importantly, how can you implement it in Go?
What is the Model Context Protocol?
The Model Context Protocol is an open standard that enables developers to build secure two-way connections between their data sources and AI-powered tools. In simpler terms, it allows you to feed your private data into AI models during reasoning.
Imagine you’re using an AI coding assistant like Cursor, and you want it to consider data from your PostgreSQL database or a proprietary API when generating code. MCP creates that bridge, making your AI tools more powerful and contextually aware of your specific environment.
While there are plenty of examples showing how to implement MCP in Python, Node.js, and Java, there’s surprisingly little documentation on doing it in Go. That’s what we’re going to fix today.
Why Build an MCP Server in Go?
Go’s simplicity, performance, and concurrency model make it an excellent choice for building MCP servers. If your backend services are already written in Go, it makes sense to keep your MCP implementation in the same ecosystem.
Additionally, Go’s strong typing and compilation checks help prevent runtime errors, which is crucial when building services that interact with AI systems.
Getting Started with MCP in Go
After exploring various libraries, I found metoro-io/mcp-golang to be the most straightforward and well-designed option. It provides a clean API that makes implementing MCP servers relatively painless.
Let’s walk through building a simple MCP server that provides several tools:
- A greeting tool that personalizes messages
- A Bitcoin price checker that fetches real-time cryptocurrency data
- A prompt test example
- A resource test example
The complete code for this tutorial is available on GitHub
Setting Up Your MCP Server
First, let’s create a new Go project and install the MCP Golang library:
go mod init mcp-server
go get github.com/metoro-io/mcp-golang
Now, let’s start building our main.go
file. We’ll begin with the necessary imports:
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
mcp_golang "github.com/metoro-io/mcp-golang"
"github.com/metoro-io/mcp-golang/transport/stdio"
)
Defining Our Data Structures
Before implementing our tools, we need to define the data structures that will be used for request and response handling:
// Content represents the main structure for submitting title and optional description as part of a request body.
type Content struct {
Title string `json:"title" jsonschema:"required,description=The title to submit"`
Description *string `json:"description" jsonschema:"description=The description to submit"`
}
// MyFunctionsArguments represents the arguments required for specific function execution, including a submitter and content.
type MyFunctionsArguments struct {
Submitter string `json:"submitter" jsonschema:"required,description=The name of the thing calling this tool (openai, google, claude, etc)"`
Content Content `json:"content" jsonschema:"required,description=The content of the message"`
}
// BitcoinPriceArguments defines the structure for arguments used to request Bitcoin price in a specific currency.
type BitcoinPriceArguments struct {
Currency string `json:"currency" jsonschema:"required,description=The currency to get the Bitcoin price in (USD, EUR, GBP, etc)"`
}
// CoinGeckoResponse represents the response structure for Bitcoin price data from the CoinGecko API across multiple currencies.
type CoinGeckoResponse struct {
Bitcoin struct {
USD float64 `json:"usd"`
EUR float64 `json:"eur"`
GBP float64 `json:"gbp"`
JPY float64 `json:"jpy"`
AUD float64 `json:"aud"`
CAD float64 `json:"cad"`
CHF float64 `json:"chf"`
CNY float64 `json:"cny"`
KRW float64 `json:"krw"`
RUB float64 `json:"rub"`
} `json:"bitcoin"`
}
Notice how we’re using JSON schema annotations to provide metadata about each field. This helps the MCP client understand the expected input format and provide better error messages.
Implementing the Main Function
Now, let’s implement the main
function that will initialize our MCP server and register our tools:
// main initializes and starts the MCP server, registers tools, prompts, and resources, and handles incoming requests.
func main() {
log.Println("Starting MCP Server...")
server := mcp_golang.NewServer(stdio.NewStdioServerTransport())
// Register tools, prompts, and resources here...
// Start the server
log.Println("MCP Server is now running and waiting for requests...")
err := server.Serve()
if err != nil {
log.Fatalf("Server error: %v", err)
}
select {} // Keeps the server running
}
We’re using the stdio
transport, which allows Cursor to communicate with our MCP server through standard input and output. This is the simplest way to get started, but you could also use HTTP or other transport mechanisms.
Implementing Our First Tool: Hello
Let’s implement our first tool, which simply greets a person with a personalized message:
// Register "hello" tool
err := server.RegisterTool("hello", "Say hello to a person with a personalized greeting message", func(arguments MyFunctionsArguments) (*mcp_golang.ToolResponse, error) {
log.Println("Received request for hello tool")
return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %s! Welcome to the MCP Example.", arguments.Submitter))), nil
})
if err != nil {
log.Fatalf("Error registering hello tool: %v", err)
}
This tool is straightforward. It takes a MyFunctionsArguments
struct, which includes a submitter name, and returns a personalized greeting. The structure is similar to handling HTTP requests - you receive a request, process it, and return a response.
Building a More Useful Tool: Bitcoin Price Checker
Now, let’s implement something more practical - a tool that fetches the latest Bitcoin price from the CoinGecko API:
// Register "bitcoin_price" tool
err = server.RegisterTool("bitcoin_price", "Get the latest Bitcoin price in various currencies", func(arguments BitcoinPriceArguments) (*mcp_golang.ToolResponse, error) {
log.Printf("Received request for bitcoin_price tool with currency: %s", arguments.Currency)
// Default to USD if no currency is specified
currency := arguments.Currency
if currency == "" {
currency = "USD"
}
// Call CoinGecko API to get the latest Bitcoin price
price, err := getBitcoinPrice(currency)
if err != nil {
return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Error fetching Bitcoin price: %v", err))), nil
}
return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("The current Bitcoin price is %.2f %s (as of %s)",
price,
currency,
time.Now().Format(time.RFC1123)))), nil
})
if err != nil {
log.Fatalf("Error registering bitcoin_price tool: %v", err)
}
This tool makes an HTTP request to the CoinGecko API, parses the JSON response, and returns the current Bitcoin price in the requested currency. We’ve also added a timestamp to indicate when the price was fetched.
Implementing the Bitcoin Price Helper Function
To keep our code clean, we’ve separated the Bitcoin price fetching logic into a helper function:
// getBitcoinPrice retrieves the current Bitcoin price in the specified currency using the CoinGecko API.
// The function returns the price as a float64 and an error if the currency is unsupported or the API call fails.
func getBitcoinPrice(currency string) (float64, error) {
// Create HTTP client with timeout
client := &http.Client{
Timeout: 10 * time.Second,
}
// Make request to CoinGecko API
resp, err := client.Get("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd,eur,gbp,jpy,aud,cad,chf,cny,krw,rub")
if err != nil {
return 0, fmt.Errorf("error making request to CoinGecko API: %w", err)
}
defer resp.Body.Close()
// Read response body
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, fmt.Errorf("error reading response body: %w", err)
}
// Parse JSON response
var coinGeckoResp CoinGeckoResponse
err = json.Unmarshal(body, &coinGeckoResp)
if err != nil {
return 0, fmt.Errorf("error parsing JSON response: %w", err)
}
// Get price for requested currency
var price float64
switch currency {
case "USD", "usd":
price = coinGeckoResp.Bitcoin.USD
case "EUR", "eur":
price = coinGeckoResp.Bitcoin.EUR
case "GBP", "gbp":
price = coinGeckoResp.Bitcoin.GBP
case "JPY", "jpy":
price = coinGeckoResp.Bitcoin.JPY
case "AUD", "aud":
price = coinGeckoResp.Bitcoin.AUD
case "CAD", "cad":
price = coinGeckoResp.Bitcoin.CAD
case "CHF", "chf":
price = coinGeckoResp.Bitcoin.CHF
case "CNY", "cny":
price = coinGeckoResp.Bitcoin.CNY
case "KRW", "krw":
price = coinGeckoResp.Bitcoin.KRW
case "RUB", "rub":
price = coinGeckoResp.Bitcoin.RUB
default:
return 0, fmt.Errorf("unsupported currency: %s", currency)
}
return price, nil
}
This function handles the HTTP request to the CoinGecko API, parses the JSON response, and returns the price for the requested currency. We’ve added support for multiple currencies and proper error handling.
Adding Prompt and Resource Examples
MCP isn’t just about tools - it also supports prompts and resources. Let’s add examples of those as well:
// Register "prompt_test" prompt
err = server.RegisterPrompt("prompt_test", "This is a test prompt", func(arguments Content) (*mcp_golang.PromptResponse, error) {
log.Println("Received request for prompt_test")
return mcp_golang.NewPromptResponse("description", mcp_golang.NewPromptMessage(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %s!", arguments.Title)), mcp_golang.RoleUser)), nil
})
if err != nil {
log.Fatalf("Error registering prompt_test: %v", err)
}
// Register test resource
err = server.RegisterResource("test://resource", "resource_test", "This is a test resource", "application/json",
func() (*mcp_golang.ResourceResponse, error) {
log.Println("Received request for resource: test://resource")
return mcp_golang.NewResourceResponse(mcp_golang.NewTextEmbeddedResource(
"test://resource", "This is a test resource", "application/json",
)), nil
})
if err != nil {
log.Fatalf("Error registering resource: %v", err)
} else {
log.Println("Successfully registered resource: test://resource") // Debug log
}
Prompts allow you to provide pre-defined conversation starters or templates to the AI, while resources let you expose data that the AI can reference during its reasoning.
Connecting Your MCP Server to Cursor
Now that we have our MCP server up and running, let’s connect it to Cursor, a popular AI-powered code editor. This will allow Cursor to use our custom tools during its reasoning process.
-
First, build your Go binary:
go build -o mcp-server
-
Get the full path to your binary:
pwd
-
In Cursor, click on the settings cog in the top right corner and navigate to the MCP section.
-
Click “Add MCP Server” and enter:
- Name:
Go MCP Server
- Path:
[Your working directory]/mcp-server
- Name:
-
Click “Save” and then “Refresh”. If everything is set up correctly, you should see a green dot next to your server name, indicating that it’s connected and working.
Testing Your MCP Tools in Cursor
Now that your MCP server is connected to Cursor, you can test your tools:
-
Open the Cursor chat window.
-
Ask it to use your hello tool:
Can you say hello to Matt using my custom tool?
-
Cursor should recognize that you have a custom tool for this and use it in its response:
I'll use your custom hello tool to greet Matt. Hello, Matt! Welcome to the MCP Example.
-
Now, try the Bitcoin price tool:
What's the current Bitcoin price in USD?
-
Cursor should fetch the real-time price using your tool:
Let me check the current Bitcoin price for you using your custom tool. The current Bitcoin price is 67,245.32 USD (as of Mon, 10 Mar 2025 15:30:45 GMT). This price was fetched in real-time from the CoinGecko API through your custom MCP server.
Practical Applications of MCP Servers
The examples we’ve built are simple, but they demonstrate the power of MCP. Here are some practical ways you might use MCP servers in your development workflow:
-
Company-Specific Coding Standards: Create tools that check code against your organization’s style guides and best practices.
-
Database Schema Access: Allow AI tools to query your database schema to generate more accurate SQL or ORM code.
-
API Documentation: Provide access to your internal API documentation to help generate better client code.
-
Compliance Checking: Implement tools that verify code against regulatory requirements specific to your industry.
-
Custom Knowledge Base: Connect your internal knowledge base to provide context about your codebase’s architecture and design decisions.
Conclusion
The Model Context Protocol opens up exciting possibilities for enhancing AI-powered development tools with your private data. By building MCP servers in Go, you can leverage the language’s performance and simplicity while creating secure bridges between your data and AI tools.
As AI continues to become more integrated into our development workflows, technologies like MCP will be crucial for making these tools more contextually aware and useful for specific environments and requirements.
If you want to explore the complete code for this tutorial, check out the repository on GitHub .
Have you built an MCP server in Go? What tools have you implemented? Drop me an email, I’d love to hear from you.
If you enjoyed this, check out more lessons from ByteSizego here.
Resources: