Build an IP Geolocation MCP Server with Go’s mcp-go Library

This tutorial walks through setting up a Go MCP server using the mark3labs/mcp-go library, adding static resources, defining calculator and IP‑lookup tools, creating prompts, compiling the binary, and testing the server with DeepChat, illustrating a complete end‑to‑end workflow.

BirdNest Tech Talk
BirdNest Tech Talk
BirdNest Tech Talk
Build an IP Geolocation MCP Server with Go’s mcp-go Library

Assuming familiarity with the Model Context Protocol (MCP), the article compares two popular Go MCP libraries— mark3labs/mcp-go and metoro-io/mcp-golang —and selects the former based on its higher star count.

Getting Started with mcp-go

First, create a basic MCP server and start it using standard input/output:

// Create a basic server
s := server.NewMCPServer(
    "My Server", // Server name
    "1.0.0",     // Version
)

// Start the server using stdio
if err := server.ServeStdio(s); err != nil {
    log.Fatalf("Server error: %v", err)
}

Adding Resources

Resources expose data to LLMs; they can be static (fixed URI) or dynamic (URI template). A static example that serves a README file:

// Static resource example - exposing a README file
resource := mcp.NewResource(
    "docs://readme",
    "Project README",
    mcp.WithResourceDescription("The project's README file"),
    mcp.WithMIMEType("text/markdown"),
)

// Add resource with its handler
s.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
    content, err := os.ReadFile("README.md")
    if err != nil {
        return nil, err
    }
    return []mcp.ResourceContents{
        mcp.TextResourceContents{
            URI:      "docs://readme",
            MIMEType: "text/markdown",
            Text:     string(content),
        },
    }, nil
})

Adding Tools

Tools let LLMs perform actions and may have side effects, similar to POST endpoints. The following calculator tool demonstrates argument definition, validation, and error handling:

calculatorTool := mcp.NewTool(
    "calculate",
    mcp.WithDescription("Perform basic arithmetic calculations"),
    mcp.WithString("operation", mcp.Required(), mcp.Description("The arithmetic operation to perform"), mcp.Enum("add", "subtract", "multiply", "divide")),
    mcp.WithNumber("x", mcp.Required(), mcp.Description("First number")),
    mcp.WithNumber("y", mcp.Required(), mcp.Description("Second number")),
)

s.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    op := request.Params.Arguments["operation"].(string)
    x := request.Params.Arguments["x"].(float64)
    y := request.Params.Arguments["y"].(float64)

    var result float64
    switch op {
    case "add":
        result = x + y
    case "subtract":
        result = x - y
    case "multiply":
        result = x * y
    case "divide":
        if y == 0 {
            return nil, errors.New("Division by zero is not allowed")
        }
        result = x / y
    }
    return mcp.FormatNumberResult(result), nil
})

Each tool should have a clear description, validate inputs, handle errors gracefully, return structured responses, and use appropriate result types.

Adding Prompts

A simple greeting prompt that takes a name argument and returns a friendly message:

// Simple greeting prompt
s.AddPrompt(mcp.NewPrompt(
    "greeting",
    mcp.WithPromptDescription("A friendly greeting prompt"),
    mcp.WithArgument("name", mcp.ArgumentDescription("Name of the person to greet")),
), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
    name := request.Params.Arguments["name"]
    if name == "" {
        name = "friend"
    }
    return mcp.NewGetPromptResult(
        "A friendly greeting",
        []mcp.PromptMessage{
            mcp.NewPromptMessage(
                mcp.RoleAssistant,
                mcp.NewTextContent(fmt.Sprintf("Hello, %s! How can I help you today?", name)),
            ),
        },
    ), nil
})

Practical Example: IP Geolocation MCP Tool

The main program creates an MCP server, defines an ip_query tool that requires an ip string, registers the ipQueryHandler, and runs the server via stdio:

package main

import (
    "context"
    "errors"
    "fmt"
    "io"
    "net"
    "net/http"

    "github.com/kataras/golog"
    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

func main() {
    // Create MCP server
    s := server.NewMCPServer("ip-mcp", "1.0.0")

    // Add tool
    tool := mcp.NewTool(
        "ip_query",
        mcp.WithDescription("query geo location of an IP address"),
        mcp.WithString("ip", mcp.Required(), mcp.Description("IP address to query")),
    )
    s.AddTool(tool, ipQueryHandler)

    // Start the stdio server
    if err := server.ServeStdio(s); err != nil {
        fmt.Printf("Server error: %v
", err)
    }
}

The handler validates the ip argument, parses it, calls the external API https://ip.rpcx.io/api/ip?ip=, reads the JSON response, and returns it as plain text. Errors are logged with golog and propagated:

func ipQueryHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    ip, ok := request.Params.Arguments["ip"].(string)
    if !ok {
        return nil, errors.New("ip must be a string")
    }
    parsedIP := net.ParseIP(ip)
    if parsedIP == nil {
        golog.Errorf("invalid IP address: %s", ip)
        return nil, errors.New("invalid IP address")
    }
    resp, err := http.Get("https://ip.rpcx.io/api/ip?ip=" + ip)
    if err != nil {
        golog.Errorf("Error fetching IP information: %v", err)
        return nil, fmt.Errorf("Error fetching IP information: %v", err)
    }
    defer resp.Body.Close()
    data, err := io.ReadAll(resp.Body)
    if err != nil {
        golog.Errorf("Error reading response body: %v", err)
        return nil, fmt.Errorf("Error reading response body: %v", err)
    }
    return mcp.NewToolResultText(string(data)), nil
}

Compile the program into an executable binary and place it where convenient for later configuration.

Testing the MCP Server with DeepChat

DeepChat (which already supports MCP) is used to configure the server: add a new MCP Server entry, point to the compiled binary path, and start it. The chat window then shows the running MCP tool, allowing the user to query an IP address and receive the JSON result, which DeepChat forwards to DeepSeek for further processing.

Conclusion: By wrapping an existing HTTP API into an MCP tool with mcp-go, developers can rapidly expose functionality to LLMs, a technique also employed by services such as Baidu Maps and AMap.

MCPGotutorialservertoolIP lookupdeepchat
BirdNest Tech Talk
Written by

BirdNest Tech Talk

Author of the rpcx microservice framework, original book author, and chair of Baidu's Go CMC committee.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.