# How to Build an MCP Server in Golang Using the Official MCP SDK

### **What is MCP?**

The Model Context Protocol is an open standard (by Anthropic) that lets LLM clients (Claude, Cursor, etc.) call external tools, read resources, and get prompted context in a uniform way — without any bespoke plugin per-model. You can read more about this on Model Context Protocol [Docs](https://modelcontextprotocol.io/docs/getting-started/intro)

In this tutorial, we'll build a basic currency conversion MCP server using Golang and the official MCP SDK. We'll explore Streamable HTTP transport for remote access, add authentication to secure our server, and finally deploy the application to Render. By the end of this guide, you'll have a solid understanding of the complete lifecycle of building and deploying an MCP server in Go.

### **Prerequisites**

*   Go 1.22+
    
*   Claude Desktop installed (for local testing)
    
*   Basic familiarity with Go modules and `net/http`
    

### MCP Transports

MCP supports [stdio](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#stdio) and [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) transport mechanisms that define how an MCP client and server communicate with each other. In this tutorial, we'll work with **Streamable HTTP.**

**Streamable HTTP**

Streamable HTTP allows MCP clients and servers to communicate over standard HTTP connections. The server runs independently and can be hosted remotely, making it accessible from different environments. Unlike stdio, Streamable HTTP allows the MCP server to live anywhere (local machine, Docker container, cloud VM, Kubernetes, etc.), making it much easier to share across multiple users.

Streamable HTTP is a better choice when you want to:

*   Deploy your MCP server to the cloud
    
*   Allow multiple clients to access the same server
    
*   Add features such as authentication and centralized monitoring
    
*   Manage your MCP server independently from the client application
    

Since we'll eventually deploy our currency conversion server to Render, Streamable HTTP will play an important role in moving from a local setup to a production-ready deployment.

### Setting up Golang project

You can refer following [blog](https://go.dev/doc/tutorial/getting-started) for setting up a basic golang project. We will use the [go-sdk](https://github.com/modelcontextprotocol/go-sdk) for creating mcp server which is maintend by the [mcp](https://modelcontextprotocol.io/community/contributing#repository-structure) team.

```shell
go mod init example/mcp-server
go get -u github.com/gorilla/mux
go get https://github.com/modelcontextprotocol/go-sdk
```

### Basic MCP Server

To start, we'll set up a very basic mcp server with just 1 tool. Before building the currency conversion tool, let's start with a minimal Greeting tool so we can understand the basic structure of an MCP server.

```go
// main.go
package main

import (
...
)

func main() {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))

	err := godotenv.Load()
	if err != nil {
		logger.Error("error loading .env file", "err", err)
	}

	mcpServer := server.NewMcpServer(logger)
	mcpServer.AddReceivingMiddleware(security.LoggingMiddleware())

	mcpHandler := mcp.NewStreamableHTTPHandler(func(request *http.Request) *mcp.Server {
		return mcpServer
	}, &mcp.StreamableHTTPOptions{
		Logger:                     logger,
		JSONResponse:               true,
		Stateless:                  true,
		DisableLocalhostProtection: true,
	})

	router := server.NewHttpServer(mcpServer, mcpHandler)

	mcp.AddTool(mcpServer, &mcp.Tool{
		Title:       "Greeting",
		Name:        "greeting",
		Description: "Greet a person by name",
	}, SayHi)

	port := os.Getenv("PORT")
	if port == "" {
		port = "3000"
	}

	server := &http.Server{
		Addr:              ":" + port,
		Handler:           router,
		ReadTimeout:       5 * time.Minute,
		ReadHeaderTimeout: 10 * time.Second,
		WriteTimeout:      5 * time.Minute,
		IdleTimeout:       5 * time.Minute,
		MaxHeaderBytes:    1 << 20,
	}

	go func() {
		logger.Info("starting server", "addr", port)

		if err := server.ListenAndServe(); err != nil &&
			err != http.ErrServerClosed {

			logger.Error("server failed", "err", err)
			os.Exit(1)
		}
	}()

	ctx, stop := signal.NotifyContext(
		context.Background(),
		syscall.SIGINT,
		syscall.SIGTERM,
	)
	defer stop()

	<-ctx.Done()

	logger.Info("shutdown initiated")

	shutdownCtx, cancel := context.WithTimeout(
		context.Background(),
		30*time.Second,
	)
	defer cancel()

	if err := server.Shutdown(shutdownCtx); err != nil {
		logger.Error("shutdown failed", "err", err)
	}

	logger.Info("shutdown complete")
}

type GreetingInput struct {
	Name string `json:"name" jsonschema:"the name of the person to greet"`
}

type GreetingOutput struct {
	Greeting string `json:"greeting" jsonschema:"the greeting to tell to the user"`
}

func SayHi(ctx context.Context, req *mcp.CallToolRequest, input GreetingInput) (
	*mcp.CallToolResult, any, error) {
	return &mcp.CallToolResult{
		Content: []mcp.Content{
			&mcp.TextContent{Text: "Hi " + input.Name},
		},
	}, GreetingOutput{Greeting: "Hi " + input.Name}, nil
}
```

```go
// server.go
package server

import (
...
)

func NewMcpServer(logger *slog.Logger) *mcp.Server {
 implementation := &mcp.Implementation{
  Title:   "MCP Tutorial For Greeting",
  Name:    "greeting-mcp",
  Version: "1.0.0",
 }

 serverOptions := &mcp.ServerOptions{
  Logger:    logger,
  KeepAlive: 5 * time.Minute,
 }

 return mcp.NewServer(implementation, serverOptions)
}

// NewHttpServer
func NewHttpServer(server *mcp.Server, mcpHandler *mcp.StreamableHTTPHandler) *mux.Router {
	router := mux.NewRouter()

	router.HandleFunc("/health", func(w http.ResponseWriter, req *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ok"))
	}).Methods(http.MethodGet)

	// Optional root route to avoid confusion when testing in browser.
	router.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("MCP server is running. Use /mcp endpoint."))
	}).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)

	router.HandleFunc("/mcp", func(w http.ResponseWriter, req *http.Request) {
		fmt.Printf(
			"method=%s path=%s auth=%q\n",
			req.Method,
			req.URL.Path,
			req.Header.Get("Authorization"),
		)
		mcpHandler.ServeHTTP(w, req)
	}).Methods(http.MethodPost, http.MethodGet, http.MethodOptions)

	return router
}
```

Here you can see for the http server we expose `/mcp` endpoint. This endpoint is **not** a REST endpoint that your application calls directly. It speaks the MCP protocol over HTTP and is intended to be consumed by MCP clients.

### Understanding MCP Tools

I like to think of MCP tools as API endpoint which an AI agent or client understands and sends request to based on its analysis of the prompt user has typed. So it becomes very important how we name our MCP tool. You can read more on it [here](https://modelcontextprotocol.io/specification/2025-11-25/server/tools)

Now lets build a real life example tool which could do currency conversion. For currency conversion I am using this free API which is provided by [fawazahmed0](https://github.com/fawazahmed0) in the following github [repo](https://github.com/fawazahmed0/exchange-api).

```go
package tool

import (
	...
)

// ConvertCurrencyInput is the input for the ConvertCurrencyTool
type ConvertCurrencyInput struct {
	FromCurrency string  `json:"from_currency" jsonschema:"currency codes are ISO 4217 format, e.g. USD, EUR, GBP, etc. the currency to convert from"`
	ToCurrency   string  `json:"to_currency" jsonschema:"currency codes are ISO 4217 format, e.g. USD, EUR, GBP, etc. the currency to convert to"`
	Amount       float64 `json:"amount" jsonschema:"the amount to convert"`
}

// ConvertCurrencyOutput is the output for the ConvertCurrencyTool
type ConvertCurrencyOutput struct {
	ConvertedAmount float64 `json:"converted_amount" jsonschema:"the converted amount"`
}

type CurrencyResponse struct {
	Date string                        `json:"date" jsonschema:"the date of the currency rates"`
	Data map[string]map[string]float64 `json:"-" jsonschema:"the currency rates"`
}

const BASE_CURRENCY_API_URL = `https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies`

// ConvertCurrencyTool converts a currency to another currency
func ConvertCurrencyTool(ctx context.Context, req *mcp.CallToolRequest, input ConvertCurrencyInput) (*mcp.CallToolResult, any, error) {
	input.FromCurrency = strings.ToLower(input.FromCurrency)
	input.ToCurrency = strings.ToLower(input.ToCurrency)
	fmt.Println("inside convert currency tool input", input)

	apiURL := fmt.Sprintf("%s/%s.json", BASE_CURRENCY_API_URL, input.FromCurrency)
	resp, err := http.Get(apiURL)
	if err != nil {
		return nil, nil, err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, nil, err
	}

	response, err := UnmarshalJSON(body)
	if err != nil {
		return nil, nil, err
	}

	if _, ok := response.Data[input.FromCurrency]; !ok {
		return nil, nil, fmt.Errorf("currency %s not found", input.FromCurrency)
	}

	if _, ok := response.Data[input.FromCurrency][input.ToCurrency]; !ok {
		return nil, nil, fmt.Errorf("currency %s not found", input.ToCurrency)
	}

	convertedAmount := response.Data[input.FromCurrency][input.ToCurrency] * input.Amount
	return &mcp.CallToolResult{
		Content: []mcp.Content{
			&mcp.TextContent{
				Text: fmt.Sprintf("Converted amount: %f", convertedAmount),
			},
		},
	}, nil, nil
}

func UnmarshalJSON(data []byte) (*CurrencyResponse, error) {
	var r CurrencyResponse
	var raw map[string]json.RawMessage

	if err := json.Unmarshal(data, &raw); err != nil {
		return nil, err
	}

	// Extract date
	if v, ok := raw["date"]; ok {
		if err := json.Unmarshal(v, &r.Date); err != nil {
			return nil, err
		}
		delete(raw, "date")
	}

	// Remaining keys are currency codes
	r.Data = make(map[string]map[string]float64)

	for currency, v := range raw {
		var rates map[string]float64
		if err := json.Unmarshal(v, &rates); err != nil {
			return nil, err
		}
		r.Data[currency] = rates
	}

	return &r, nil
}
```

Now to add the mcp server we do the following

```go
// main.go

....

func main() {
...

mcp.AddTool(mcpServer, &mcp.Tool{
		Title:       "Currency Conversion",
		Name:        "currency-conversion",
		Description: "Convert an amount from one currency to another. Requires from_currency, to_currency (ISO 4217 codes), and amount. Uses latest exchange rates from an external API.",
	}, ConvertCurrencyTool)
...
}
```

Now lets understand what is happening here and how it works. First we register this tool using `mcp.AddTool`. Here Title is just human readable text and does not provide any help or context to the AI agent. Name and Description are 2 important things when registering a tool as this 2 piece of information helps the AI agent to make the decision on which MCP server should be used.

Now lets go to the `ConvertCurrencyTool`. This function takes 3 arguments which is the method signature and the 3rd parameter which is `input` is what we will focus on. It is like the request body that we expect to receive from the AI agent. In my case I am expecting `fromCurrency`, `toCurrency` and `amount`. For all the fields I have `jsonschema` specified which, like the tool description, helps the AI understand **what** each field represents. The SDK converts these tags into the MCP Tool schema that the model receives during tool discovery

The `ConvertCurrencyTool` makes an API call to fetch the latest exchange rates, performs the currency conversion, and returns the result to the MCP client.

There are a few more things which can be done during mcp server creation. Another useful server configuration is `Instructions`. These provide additional context to the AI model and can improve tool selection and overall responses.

```go
func NewMcpServer(logger *slog.Logger) *mcp.Server {
	implementation := &mcp.Implementation{
		Title:   "MCP Tutorial For Currency Conversion",
		Name:    "currency-conversion-mcp",
		Version: "1.0.0",
	}

	serverOptions := &mcp.ServerOptions{
		Logger:       logger,
		KeepAlive:    5 * time.Minute,
		Instructions: `You are a helpful assistant that can convert currencies. You can convert currencies using the currency-conversion tool.`,
        Capabilities: &mcp.ServerCapabilities{
			Tools: &mcp.ToolCapabilities{},
		},
	}

	return mcp.NewServer(implementation, serverOptions)
}
```

There is also one more important setting that we should we about when configuring the streamable http transport

```go
	mcpHandler := mcp.NewStreamableHTTPHandler(func(request *http.Request) *mcp.Server {
		return mcpServer
	}, &mcp.StreamableHTTPOptions{
		Logger:                     logger,
		JSONResponse:               true,
		Stateless:                  true,
		DisableLocalhostProtection: true,
	})
```

Here the options part is important to understand. `JSONResponse` is basic tells if the response type is json or not. `Stateless` - this is one of the key option to understand. `Stateless` mode tells the server not to rely on MCP sessions. Every request should contain all information required to process it, making deployment behind load balancers and horizontal scaling much easier.. This is very well explained in Anthropic courses named [Model Context Protocol: Advanced Topics](https://anthropic.skilljar.com/model-context-protocol-advanced-topics)l-advanced-topics. I ideally like to keep it to `true` as a REST API is always stateless and we use Authorization headers to validate the user so we can follow the same setup here too. The last option which is `DisableLocalhostProtection` again an important flag to be set to `true` when deploying to production. You can read more on this [here](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices#local-mcp-server-compromise).

To find more examples on how to create [Prompt](https://modelcontextprotocol.io/specification/2025-11-25/server/prompts), [Resource](https://modelcontextprotocol.io/specification/2025-11-25/server/resources) you can always refer following [docs](https://github.com/modelcontextprotocol/go-sdk/tree/main/examples)

### How registration works

When we call:

```go
mcp.AddTool(mcpServer, &mcp.Tool{
    Title:       "Currency Conversion",
    Name:        "currency-conversion",
    Description: "Convert an amount from one currency to another...",
}, ConvertCurrencyTool)
```

we are **not** creating a new HTTP endpoint like we would in a typical REST application. Instead, we're registering the tool with the MCP server.

From this point on, the lifecycle looks like this:

1.  **The tool is registered**
    
    *   The MCP server stores the tool's metadata, including its name, description, and input schema.
        
2.  **The MCP client connects**
    
    *   When a client such as Claude Desktop connects to the server, it asks the server which tools are available.
        
3.  **The server advertises its tools**
    
    *   The server responds with the list of registered tools, including their descriptions and JSON schemas.
        
4.  **The LLM decides whether to use a tool**
    
    *   When the user asks a question like:
        
        > *How much is 28 USD in INR?*
        
        the model analyzes the request and determines that the `currency-conversion` tool is appropriate.
        
5.  **The client invokes the tool**
    
    *   The client sends a tool invocation request to the MCP server containing the required input:
        
        ```json
        {
          "from_currency": "USD",
          "to_currency": "INR",
          "amount": 28
        }
        ```
        
6.  **Your Go function executes**
    
    *   The Go SDK automatically deserializes the request into a `ConvertCurrencyInput` struct and calls:
        
        ```go
        ConvertCurrencyTool(ctx, req, input)
        ```
        
7.  **The result is returned**
    
    *   Your function performs the currency conversion, returns the result, and the SDK sends it back to the client. Claude then uses that result to generate its final response to the user.
        

### Claude Desktop Connection

Now how can we connect this with lets say claude desktop

1.  Open Settings
    
    1.  Open the Claude Desktop application on your machine.
        
    2.  At the bottom left of the window, click on your **profile icon**.
        
    3.  From the menu that appears, select **Settings**.
        
        ![](https://cdn.hashnode.com/uploads/covers/60deae7dd7a2b817245127ef/9085d777-31f2-4880-b22e-e0d3809a6f95.png align="center")
        
2.  Open the Developer config
    
    1.  In the Settings window, scroll down in the left sidebar and click on **Developer**.
        
    2.  Click the **Edit Config** button.
        
        ![](https://cdn.hashnode.com/uploads/covers/60deae7dd7a2b817245127ef/5602db05-6b54-4b44-b2f5-aef8c847efba.png align="center")
        
3.  Open the config file in a text editor
    
    1.  Right-click on `claude_desktop_config.json`.
        
    2.  Open it with any text editor — for example Notepad (Windows), TextEdit (macOS), or VS Code.
        
        ![](https://cdn.hashnode.com/uploads/covers/60deae7dd7a2b817245127ef/8219d33e-081a-4661-953f-96f2568a7970.png align="center")
        
4.  Add the Currency convertor MCP server entry
    
    1.  In the text editor, find the `mcpServers` section. If the file is empty or new, paste the entire block below. If `mcpServers` already exists with other entries, add only the `"`currency-convertor`"` block inside it.  
        Replace `YOUR_ACCESS_TOKEN` with the token with your access token.  
        
        ```json
        {
          "mcpServers": {
            "currency-convertor": {
              "command": "npx",
              "args": [
                "-y",
                "mcp-remote",
                "http://localhost:8080/mcp",
                "--header",
                "Authorization:Bearer <YOUR_ACCESS_TOKEN>"
              ]
            }
          }
        }
        ```
        

### Testing the MCP Server

Now that Claude Desktop is connected to the server, you can try asking questions that require currency conversion.

For example:

*   "How much is 28 USD in INR?"
    
*   "Convert 150 EUR to GBP."
    
*   "If I have 1000 JPY, how many USD is that worth?"
    

Claude will recognize that your request requires currency conversion, invoke the `currency-conversion` MCP tool, retrieve the latest exchange rate, and respond with the converted amount.

This is a good way to verify that your MCP server is working correctly.

### Conclusion

In this tutorial, we built a complete MCP server in Go using the official Go SDK. We started with a simple greeting tool, expanded it into a practical currency conversion service, exposed it using Streamable HTTP, discussed important server configuration options, and connected it to Claude Desktop.

In the next step, you can deploy the same server to Render and make it accessible remotely without changing the MCP client configuration.

The complete source code for this tutorial is available on GitHub:

**GitHub Repository:** [https://github.com/shaileshhb/mcp-tutorial](https://github.com/shaileshhb/mcp-tutorial)
