Learn how to build MCP servers in Go to connect your private data sources to AI tools like Cursor for more powerful coding experiences.

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:

  1. A greeting tool that personalizes messages
  2. A Bitcoin price checker that fetches real-time cryptocurrency data
  3. A prompt test example
  4. 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.

  1. First, build your Go binary:

    go build -o mcp-server
    
  2. Get the full path to your binary:

    pwd
    
  3. In Cursor, click on the settings cog in the top right corner and navigate to the MCP section.

  4. Click “Add MCP Server” and enter:

    • Name: Go MCP Server
    • Path: [Your working directory]/mcp-server
  5. 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:

  1. Open the Cursor chat window.

  2. Ask it to use your hello tool:

    Can you say hello to Matt using my custom tool?
    
  3. 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.
    
  4. Now, try the Bitcoin price tool:

    What's the current Bitcoin price in USD?
    
  5. 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:

  1. Company-Specific Coding Standards: Create tools that check code against your organization’s style guides and best practices.

  2. Database Schema Access: Allow AI tools to query your database schema to generate more accurate SQL or ORM code.

  3. API Documentation: Provide access to your internal API documentation to help generate better client code.

  4. Compliance Checking: Implement tools that verify code against regulatory requirements specific to your industry.

  5. 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: