22.2 C
New York
Sunday, June 28, 2026

Construct an MCP Server in Go: A Manufacturing-Prepared Tutorial for the Mannequin Context Protocol


The Mannequin Context Protocol (MCP) is a standardized interface for connecting AI fashions to exterior instruments and knowledge sources. Constructing an MCP server in Go provides distinct benefits for manufacturing deployments, combining Go’s concurrency mannequin and single-binary compilation with a protocol designed to finish the fragmented, vendor-specific integration sample that has held again AI tooling. This tutorial walks by way of the whole strategy of setting up a production-ready MCP server, from preliminary scaffolding to swish shutdown, utilizing the mcp-go SDK.

The way to Construct an MCP Server in Go

  1. Initialize a Go module and pin the mcp-go SDK to a selected model with go get.
  2. Create the server entry level utilizing server.NewMCPServer with functionality choices for instruments, sources, and prompts.
  3. Outline instrument schemas with mcp.NewTool, specifying enter parameters, varieties, and descriptions.
  4. Implement instrument handlers that validate inputs, name exterior APIs with timeouts, and return outcomes through CallToolResult.
  5. Register sources and immediate templates to reveal read-only knowledge and reusable interplay patterns.
  6. Configure structured logging to stderr and add enter validation with allowlist patterns for safety.
  7. Add swish shutdown utilizing OS sign seize and context cancellation as an alternative of os.Exit.
  8. Begin the server on stdio transport for native shoppers or HTTP/SSE transport for distant deployment.

Desk of Contents

What Is the Mannequin Context Protocol (MCP) and Why Does It Matter?

The Drawback MCP Solves

Massive language fashions are remoted. With out exterior integrations, they can’t entry real-time knowledge, work together with APIs, or carry out actions on the planet. Earlier than MCP, connecting an AI mannequin to a instrument or knowledge supply required constructing vendor-specific integrations for every mixture of mannequin and repair. This created an M×N drawback: M fashions instances N instruments, every requiring a customized connector.

MCP eliminates this by defining a single, open protocol that any AI mannequin can use to speak with any suitable server. The protocol is open-source. Shoppers throughout the ecosystem have adopted it, together with Claude Desktop, VS Code with GitHub Copilot, Cursor, and different MCP-compatible shoppers.

MCP Structure at a Look

MCP follows a client-server structure with three distinct roles. Hosts are the AI purposes (like Claude Desktop) that provoke connections. Shoppers are protocol-level connectors maintained inside the host, every establishing a one-to-one session with a server. Servers expose capabilities to the AI mannequin by way of three core primitives.

Instruments are executable capabilities the mannequin can invoke, roughly analogous to POST endpoints. Learn-only knowledge entry comes by way of Sources, that are recognized by URIs and behave like GET endpoints. Prompts are reusable template messages with arguments that construction how the mannequin interacts with the server’s capabilities.

Communication occurs over certainly one of a number of transport mechanisms: stdio (commonplace enter/output) for native processes, HTTP with Server-Despatched Occasions (SSE), or the newer Streamable HTTP transport for stateless distant deployments.

MCP eliminates this by defining a single, open protocol that any AI mannequin can use to speak with any suitable server.

Why Construct Your MCP Server in Go?

Go’s Strengths for MCP Servers

Go’s goroutine-based concurrency mannequin maps naturally to MCP’s requirement for dealing with a number of simultaneous shopper connections and gear invocations. A single MCP server could subject concurrent requests from a number of AI classes. Goroutines begin with a stack of roughly 2-8 KB, in comparison with the 1-8 MB typical of OS thread stacks, so a server can maintain hundreds of concurrent handlers with out important reminiscence strain.

The one-binary compilation mannequin simplifies deployment significantly. An MCP server inbuilt Go compiles to a standalone executable with no runtime dependencies, making it straightforward to distribute, containerize, or reference from a shopper’s configuration file. Go’s commonplace library offers HTTP primitives with configurable timeouts, connection pooling, and HTTP/2 assist, which the mcp-go SDK builds upon for MCP’s transport layer. Go’s sort system enforces handler interface compliance at compile time; instrument enter schema validation happens at runtime inside the SDK.

Stipulations and Undertaking Setup

What You will Want

This tutorial requires Go 1.21 or later put in and out there on the system PATH (confirm with go model). You must know Go modules, structs, and interface patterns. Any editor with Go assist will work, although VS Code with the Go extension or GoLand offers jump-to-definition for SDK varieties, which helps when exploring the API floor.

You want an MCP-compatible shopper for testing. Claude Desktop requires solely a JSON config entry pointing to the binary plus a restart, making it the quickest choice for native stdio servers. VS Code with GitHub Copilot’s agent mode additionally helps MCP server connections.

Initializing the Undertaking

mkdir mcp-go-server
cd mcp-go-server
go mod init github.com/yourname/mcp-go-server
go get github.com/mark3labs/mcp-go@v0.26.0

Be aware: Pin the SDK model explicitly. Change v0.26.0 with the present secure launch listed at github.com/mark3labs/mcp-go/releases. Working go get with no model tag fetches the most recent, which can introduce breaking modifications after this tutorial was revealed.

The mcp-go SDK by Mark3Labs is a well-adopted Go implementation of the MCP specification. It offers server building primitives, transport handlers, and helper capabilities for outlining instrument schemas.

Constructing Your First MCP Server with stdio Transport

Creating the Server Entry Level

The minimal MCP server requires initialization with a reputation and model, then startup on a transport. The stdio transport communicates over commonplace enter and output, which is the anticipated mechanism when an MCP shopper launches the server as a subprocess.

package deal predominant

import (
	"fmt"
	"os"

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

func predominant() {
	s := server.NewMCPServer(
		"my-mcp-server",
		"1.0.0",
		server.WithToolCapabilities(true),              
		server.WithResourceCapabilities(true, false),   
		server.WithPromptCapabilities(true),
	)

	if err := server.ServeStdio(s); err != nil {
		fmt.Fprintf(os.Stderr, "Server error: %v
", err)
		os.Exit(1)
	}
}

The server.NewMCPServer perform takes the server title and model as its first two arguments. The choice capabilities declare which MCP capabilities this server helps. WithToolCapabilities(true) advertises that the server can notify shoppers when its instrument record modifications. WithResourceCapabilities(true, false) allows useful resource itemizing however not useful resource subscriptions. The server.ServeStdio name blocks, studying JSON-RPC messages from stdin and writing responses to stdout.

Registering Your First Software

Instruments are the first mechanism by way of which an AI mannequin takes motion through an MCP server. Every instrument requires a schema defining its inputs and a handler perform that executes the logic.

package deal predominant

import (
	"context"
	"fmt"
	"os"

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

func predominant() {
	s := server.NewMCPServer(
		"my-mcp-server",
		"1.0.0",
		server.WithToolCapabilities(true),
	)

	helloTool := mcp.NewTool("hi there",
		mcp.WithDescription("Greets a consumer by title"),
		mcp.WithString("title",
			mcp.Required(),
			mcp.Description("The title of the individual to greet"),
		),
	)

	s.AddTool(helloTool, helloHandler)

	if err := server.ServeStdio(s); err != nil {
		fmt.Fprintf(os.Stderr, "Server error: %v
", err)
		os.Exit(1)
	}
}

func helloHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	title, okay := request.Params.Arguments["name"].(string)
	if !okay || title == "" {
		return mcp.NewToolResultError("title parameter is required"), nil
	}

	greeting := fmt.Sprintf("Hey, %s! Welcome to the MCP server.", title)
	return mcp.NewToolResultText(greeting), nil
}

The mcp.NewTool perform creates a instrument definition with a JSON Schema for its inputs. Helper capabilities like mcp.WithString, mcp.Required(), and mcp.Description() construct the schema declaratively. The handler receives a CallToolRequest and returns a *mcp.CallToolResult. Be aware that the "context" import is required for the handler’s context.Context parameter.

Testing with an MCP Shopper

To check with Claude Desktop, add a server entry to the configuration file. On macOS, discover it at ~/Library/Software Assist/Claude/claude_desktop_config.json. On Home windows, it lives at %APPDATApercentClaudeclaude_desktop_config.json. On Linux, it lives at ~/.config/Claude/claude_desktop_config.json.

{
  "mcpServers": {
    "my-mcp-server": {
      "command": "/absolute/path/to/mcp-go-server"
    }
  }
}

Compile the binary first with go construct -o mcp-go-server . and use absolutely the path to the ensuing executable. After restarting Claude Desktop, the instrument ought to seem within the out there instruments record. Asking Claude to “say hi there to Alice” ought to set off the instrument invocation. If the instrument does not seem, examine Claude Desktop’s MCP log output for configuration or path errors.

Including Sources and Prompts

Exposing Sources

MCP sources characterize read-only knowledge that the AI mannequin can retrieve. Every useful resource is recognized by a URI and returns content material with a specified MIME sort. Sources are appropriate for exposing configuration knowledge, documentation, or database data.

s.AddResource(mcp.NewResource(
	"config://app/settings",
	"Software Settings",
	mcp.WithResourceDescription("Present utility configuration"),
	mcp.WithMIMEType("utility/json"),
), func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
	settings := `{"debug": false, "max_connections": 100, "area": "us-east-1"}`
	return []mcp.ResourceContents{
		mcp.NewTextResourceContents(request.Params.URI, "utility/json", settings),
	}, nil
})

The useful resource handler returns a slice of ResourceContents, permitting a single URI to return a number of items of content material. For dynamic sources the place the URI accommodates variable segments, mcp.NewResourceTemplate can be utilized with URI templates following RFC 6570 syntax.

Defining Immediate Templates

Prompts present reusable message templates that information how the AI mannequin interacts with the server. They settle for arguments and return structured message sequences.

s.AddPrompt(mcp.NewPrompt("summarize_issue",
	mcp.WithPromptDescription("Summarize a GitHub situation for a standing report"),
	mcp.WithArgument("issue_title",
		mcp.ArgumentDescription("The title of the difficulty"),
		mcp.RequiredArgument(),
	),
	mcp.WithArgument("issue_body",
		mcp.ArgumentDescription("The physique content material of the difficulty"),
	),
), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
	title, okay := request.Params.Arguments["issue_title"].(string)
	if !okay || title == "" {
		return nil, fmt.Errorf("issue_title is required and have to be a non-empty string")
	}
	physique, _ := request.Params.Arguments["issue_body"].(string)

	return &mcp.GetPromptResult{
		Description: "Summarize a GitHub situation",
		Messages: []mcp.PromptMessage{
			{
				Position: mcp.RoleUser,
				Content material: mcp.TextContent{
					Sort: "textual content",
					Textual content: fmt.Sprintf("Summarize this GitHub situation for a standing report.

Title: %s

Physique: %s", title, physique),
				},
			},
		},
	}, nil
})

The immediate handler returns a GetPromptResult containing a sequence of messages with outlined roles. This permits servers to offer structured interplay patterns that shoppers can current to customers or inject into mannequin conversations.

Designing the Software Schema

A sensible MCP instrument demonstrates the mixing sample for exterior APIs. A GitHub situation lookup instrument wants three parameters: the repository proprietor, repository title, and situation quantity.

Implementing the Software Handler

import "regexp"


var validIdentifier = regexp.MustCompile(`^[a-zA-Z0-9_.-]{1,100}$`)

const maxBodyBytes = 1 << 20 

var githubHTTPClient = &http.Shopper{
	Timeout: 10 * time.Second,
	Transport: &http.Transport{
		ResponseHeaderTimeout: 5 * time.Second,
		TLSHandshakeTimeout:   5 * time.Second,
		MaxIdleConnsPerHost:   10,
	},
}

func githubIssueTool() (mcp.Software, server.ToolHandlerFunc) {
	instrument := mcp.NewTool("github_issue_lookup",
		mcp.WithDescription("Search for a GitHub situation by proprietor, repo, and situation quantity"),
		mcp.WithString("proprietor", mcp.Required(), mcp.Description("Repository proprietor")),
		mcp.WithString("repo", mcp.Required(), mcp.Description("Repository title")),
		mcp.WithNumber("issue_number", mcp.Required(), mcp.Description("Problem quantity")),
	)

	handler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		proprietor, _ := request.Params.Arguments["owner"].(string)
		repo, _ := request.Params.Arguments["repo"].(string)
		issueNum, _ := request.Params.Arguments["issue_number"].(float64)

		if proprietor == "" || repo == "" || issueNum < 1 {
			return mcp.NewToolResultError("proprietor, repo, and issue_number are required (issue_number have to be >= 1)"), nil
		}

		
		if !validIdentifier.MatchString(proprietor) || !validIdentifier.MatchString(repo) {
			return mcp.NewToolResultError("proprietor and repo should comprise solely alphanumeric characters, hyphens, underscores, or dots (max 100 chars)"), nil
		}

		slog.Data("instrument invoked", "instrument", "github_issue_lookup", "proprietor", proprietor, "repo", repo, "issue_number", int(issueNum))

		apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/points/%d", proprietor, repo, int(issueNum))

		req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("did not construct request: %v", err)), nil
		}
		req.Header.Set("Consumer-Agent", "mcp-go-server/1.0.0")
		req.Header.Set("Settle for", "utility/vnd.github+json")

		
		if token := os.Getenv("GITHUB_TOKEN"); token != "" {
			req.Header.Set("Authorization", "Bearer "+token)
		}

		resp, err := githubHTTPClient.Do(req)
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("API request failed: %v", err)), nil
		}
		defer resp.Physique.Shut()

		
		
		if resp.StatusCode == http.StatusForbidden {
			remaining := resp.Header.Get("X-RateLimit-Remaining")
			if remaining == "0" {
				return &mcp.CallToolResult{
					Content material: []mcp.Content material{mcp.TextContent{
						Sort: "textual content",
						Textual content: "GitHub API major fee restrict exceeded. Set GITHUB_TOKEN to boost limits.",
					}},
					IsError: true,
				}, nil
			}
			return mcp.NewToolResultError("GitHub API returned 403 Forbidden (examine token permissions or repo visibility)"), nil
		}
		if resp.StatusCode == http.StatusTooManyRequests {
			return &mcp.CallToolResult{
				Content material: []mcp.Content material{mcp.TextContent{
					Sort: "textual content",
					Textual content: "GitHub API secondary fee restrict hit. Wait earlier than retrying.",
				}},
				IsError: true,
			}, nil
		}

		if resp.StatusCode != http.StatusOK {
			return mcp.NewToolResultError(fmt.Sprintf("GitHub API returned standing %d", resp.StatusCode)), nil
		}

		var situation struct {
			Title  string `json:"title"`
			State  string `json:"state"`
			Physique   string `json:"physique"`
			Labels []struct {
				Title string `json:"title"`
			} `json:"labels"`
		}

		limitedBody := io.LimitReader(resp.Physique, maxBodyBytes)
		if err := json.NewDecoder(limitedBody).Decode(&situation); err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("did not parse response: %v", err)), nil
		}

		labelNames := make([]string, len(situation.Labels))
		for i, l := vary situation.Labels {
			labelNames[i] = l.Title
		}

		end result := fmt.Sprintf("Title: %s
State: %s
Labels: %s

%s",
			situation.Title, situation.State, strings.Be part of(labelNames, ", "), situation.Physique)
		return mcp.NewToolResultText(end result), nil
	}

	return instrument, handler
}

Be aware that issue_number arrives as float64 as a result of JSON numbers are unmarshaled to float64 in Go’s interface{} sort system. This can be a widespread supply of bugs when working with JSON-RPC in Go.

Vital: GitHub’s API requires a Consumer-Agent header on all requests. Calls with out one could obtain an HTTP 403 response. The handler above units a Consumer-Agent and in addition helps an elective GITHUB_TOKEN setting variable to boost fee limits from 60 requests/hour (unauthenticated) to five,000 requests/hour.

Dealing with Errors Gracefully

The instrument handler above already contains rate-limit detection, however the sample is value highlighting explicitly:



if resp.StatusCode == http.StatusForbidden {
	remaining := resp.Header.Get("X-RateLimit-Remaining")
	if remaining == "0" {
		return &mcp.CallToolResult{
			Content material: []mcp.Content material{mcp.TextContent{
				Sort: "textual content",
				Textual content: "GitHub API major fee restrict exceeded. Set GITHUB_TOKEN to boost limits.",
			}},
			IsError: true,
		}, nil
	}
	return mcp.NewToolResultError("GitHub API returned 403 Forbidden (examine token permissions or repo visibility)"), nil
}
if resp.StatusCode == http.StatusTooManyRequests {
	return &mcp.CallToolResult{
		Content material: []mcp.Content material{mcp.TextContent{
			Sort: "textual content",
			Textual content: "GitHub API secondary fee restrict hit. Wait earlier than retrying.",
		}},
		IsError: true,
	}, nil
}

MCP error reporting makes use of the IsError: true subject on CallToolResult fairly than returning a Go error. Returning a Go error from the handler alerts a protocol-level failure, whereas IsError: true alerts an application-level error that the mannequin can purpose about and probably retry or clarify to the consumer.

Returning a Go error from the handler alerts a protocol-level failure, whereas IsError: true alerts an application-level error that the mannequin can purpose about and probably retry or clarify to the consumer.

Manufacturing Hardening

Structured Logging

When utilizing stdio transport, stdout is solely reserved for MCP protocol messages. Any log output written to stdout will corrupt the JSON-RPC stream and break communication. Direct all logging to stderr or a file.

logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
	Stage: slog.LevelInfo,
}))
slog.SetDefault(logger)


slog.Data("instrument invoked", "instrument", "github_issue_lookup", "proprietor", proprietor, "repo", repo)

Go’s slog package deal, out there since Go 1.21, offers structured logging with JSON output that integrates nicely with log aggregation programs. Writing to stderr retains the stdio transport clear whereas offering full observability.

Enter Validation and Safety

All instrument inputs have to be validated earlier than use. String parameters destined for URL building ought to be validated towards a strict allowlist sample (e.g., ^[a-zA-Z0-9_.-]{1,100}$) fairly than a denylist of particular characters, as denylist approaches miss URL-encoded variants and different bypass methods. When instrument outputs embody content material retrieved from exterior sources, that content material may comprise immediate injection makes an attempt. Servers mustn’t try to sanitize this content material, as that’s the mannequin’s duty, however ought to be conscious that instrument outputs circulation instantly into the mannequin’s context window.

Add fee limiting (e.g., a per-client token bucket) for any HTTP-transported server serving a number of shoppers. The mcp-go SDK doesn’t present built-in fee limiting (confirm towards the SDK model you’re utilizing), so middleware or exterior options are wanted.

Swish Shutdown

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

sigChan := make(chan os.Sign, 1)
sign.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

go func() {
	choose {
	case sig := <-sigChan:
		slog.Data("shutdown sign obtained", "sign", sig.String())
		cancel()
	case <-ctx.Completed():
		
	}
}()

This sample captures interrupt and termination alerts and cancels the context, which ought to be threaded by way of to energetic instrument handlers to allow cooperative cancellation of in-flight requests. The choose on each sigChan and ctx.Completed() ensures the goroutine exits cleanly even when the server shuts down for a purpose apart from an OS sign. After cancel() known as, the ServeStdio name (or HTTP server) ought to detect the cancelled context and return, permitting deferred cleanup capabilities in predominant to execute usually.

Warning: Keep away from calling os.Exit() contained in the sign goroutine. os.Exit terminates the method instantly, bypassing all deferred capabilities, which means database connections, file handles, and log buffers is not going to be cleaned up. As an alternative, cancel the context and let the server shut down by way of its regular return path.

Switching to HTTP/SSE Transport for Distant Deployment

When to Use HTTP vs. stdio

The stdio transport is acceptable when the MCP shopper spawns the server as an area subprocess. That is the usual mode for Claude Desktop and most IDE integrations. For distant deployments, multi-client entry, or cloud-hosted servers, HTTP-based transports are required.

Be aware: HTTP and stdio transports are mutually unique startup paths. Use one or the opposite in a given binary invocation, not each.


sseServer := server.NewSSEServer(s, server.WithBaseURL("http://localhost:8080"))
if err := sseServer.Begin(":8080"); err != nil {
	slog.Error("SSE server failed", "error", err)
}


httpServer := server.NewStreamableHTTPServer(s)
if err := httpServer.Begin(":8080"); err != nil {
	slog.Error("HTTP server failed", "error", err)
}

For distant or manufacturing deployments, use your TLS-terminated HTTPS URL as the bottom URL (e.g., server.WithBaseURL("https://your-server.instance.com")). TLS termination could be dealt with by a reverse proxy reminiscent of Nginx or Caddy, as mentioned in Subsequent Steps beneath.

The SSE transport maintains a persistent connection for server-to-client notifications, whereas Streamable HTTP helps stateless request-response patterns appropriate for serverless and load-balanced deployments. The server logic stays an identical throughout transports; solely the startup name modifications.

Manufacturing-Prepared Implementation Guidelines

Earlier than deploying an MCP server, confirm every of these things:

  • Go module initialized with a pinned mcp-go model in go.mod
  • Server title and model set in ServerInfo through NewMCPServer
  • All instruments have full enter schemas with descriptions for each parameter
  • Software handlers validate all inputs earlier than use, utilizing allowlist patterns for URL-destined strings
  • HTTP requests to exterior APIs embody a Consumer-Agent header and a timeout
  • Response our bodies from exterior APIs are size-limited (e.g., through io.LimitReader)
  • Report errors through IsError: true on CallToolResult, by no means panics
  • Direct logging to stderr (stdio transport) or a structured logger (HTTP transport)
  • Swish shutdown on OS alerts applied through context cancellation (not os.Exit)
  • Sources use applicable MIME varieties
  • If deploying through HTTP, add fee limiting on the server or reverse-proxy stage
  • Transport chosen based mostly on deployment goal: stdio for native, HTTP/SSE for distant
  • Examined with a minimum of one MCP shopper (Claude Desktop or VS Code)
  • Construct and take a look at the binary on course OS/structure through GOOS and GOARCH

Full Server Code

The next consolidated implementation combines all components lined on this tutorial right into a single, copy-paste-ready predominant.go file.

package deal predominant

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"internet/http"
	"os"
	"os/sign"
	"regexp"
	"strings"
	"syscall"
	"time"

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


var validIdentifier = regexp.MustCompile(`^[a-zA-Z0-9_.-]{1,100}$`)

const maxBodyBytes = 1 << 20 

var githubHTTPClient = &http.Shopper{
	Timeout: 10 * time.Second,
	Transport: &http.Transport{
		ResponseHeaderTimeout: 5 * time.Second,
		TLSHandshakeTimeout:   5 * time.Second,
		MaxIdleConnsPerHost:   10,
	},
}

func predominant() {
	logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Stage: slog.LevelInfo}))
	slog.SetDefault(logger)

	s := server.NewMCPServer(
		"go-mcp-production-server",
		"1.0.0",
		server.WithToolCapabilities(true),            
		server.WithResourceCapabilities(true, false), 
		server.WithPromptCapabilities(true),
	)

	
	instrument, handler := githubIssueTool()
	s.AddTool(instrument, handler)

	
	s.AddResource(mcp.NewResource(
		"config://app/settings",
		"Software Settings",
		mcp.WithResourceDescription("Present utility configuration"),
		mcp.WithMIMEType("utility/json"),
	), func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
		return []mcp.ResourceContents{
			mcp.NewTextResourceContents(request.Params.URI, "utility/json",
				`{"debug": false, "max_connections": 100}`),
		}, nil
	})

	
	s.AddPrompt(mcp.NewPrompt("summarize_issue",
		mcp.WithPromptDescription("Summarize a GitHub situation"),
		mcp.WithArgument("issue_title",
			mcp.ArgumentDescription("The title of the difficulty"),
			mcp.RequiredArgument(),
		),
		mcp.WithArgument("issue_body",
			mcp.ArgumentDescription("The physique content material of the difficulty"),
		),
	), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
		title, okay := request.Params.Arguments["issue_title"].(string)
		if !okay || title == "" {
			return nil, fmt.Errorf("issue_title is required and have to be a non-empty string")
		}
		physique, _ := request.Params.Arguments["issue_body"].(string)

		return &mcp.GetPromptResult{
			Messages: []mcp.PromptMessage{{
				Position: mcp.RoleUser,
				Content material: mcp.TextContent{
					Sort: "textual content",
					Textual content: fmt.Sprintf("Summarize: %s

%s", title, physique),
				},
			}},
		}, nil
	})

	
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	sigChan := make(chan os.Sign, 1)
	sign.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		choose {
		case sig := <-sigChan:
			slog.Data("shutdown sign obtained", "sign", sig.String())
			cancel()
		case <-ctx.Completed():
			
		}
	}()

	slog.Data("beginning MCP server", "transport", "stdio")
	if err := server.ServeStdio(s, server.WithContext(ctx)); err != nil {
		slog.Error("server error", "error", err)
		
		return
	}
}

func githubIssueTool() (mcp.Software, server.ToolHandlerFunc) {
	instrument := mcp.NewTool("github_issue_lookup",
		mcp.WithDescription("Search for a GitHub situation"),
		mcp.WithString("proprietor", mcp.Required(), mcp.Description("Repository proprietor")),
		mcp.WithString("repo", mcp.Required(), mcp.Description("Repository title")),
		mcp.WithNumber("issue_number", mcp.Required(), mcp.Description("Problem quantity")),
	)
	return instrument, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		proprietor, _ := req.Params.Arguments["owner"].(string)
		repo, _ := req.Params.Arguments["repo"].(string)
		num, _ := req.Params.Arguments["issue_number"].(float64)
		if proprietor == "" || repo == "" || num < 1 {
			return mcp.NewToolResultError("lacking required parameters (issue_number have to be >= 1)"), nil
		}

		
		if !validIdentifier.MatchString(proprietor) || !validIdentifier.MatchString(repo) {
			return mcp.NewToolResultError("proprietor and repo should comprise solely alphanumeric characters, hyphens, underscores, or dots (max 100 chars)"), nil
		}

		slog.Data("instrument invoked", "instrument", "github_issue_lookup", "proprietor", proprietor, "repo", repo, "issue_number", int(num))

		apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/points/%d", proprietor, repo, int(num))

		httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("did not construct request: %v", err)), nil
		}
		httpReq.Header.Set("Consumer-Agent", "mcp-go-server/1.0.0")
		httpReq.Header.Set("Settle for", "utility/vnd.github+json")

		
		if token := os.Getenv("GITHUB_TOKEN"); token != "" {
			httpReq.Header.Set("Authorization", "Bearer "+token)
		}

		resp, err := githubHTTPClient.Do(httpReq)
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("request failed: %v", err)), nil
		}
		defer resp.Physique.Shut()

		
		
		if resp.StatusCode == http.StatusForbidden {
			remaining := resp.Header.Get("X-RateLimit-Remaining")
			if remaining == "0" {
				return &mcp.CallToolResult{
					Content material: []mcp.Content material{mcp.TextContent{
						Sort: "textual content",
						Textual content: "GitHub API major fee restrict exceeded. Set GITHUB_TOKEN to boost limits.",
					}},
					IsError: true,
				}, nil
			}
			return mcp.NewToolResultError("GitHub API returned 403 Forbidden (examine token permissions or repo visibility)"), nil
		}
		if resp.StatusCode == http.StatusTooManyRequests {
			return &mcp.CallToolResult{
				Content material: []mcp.Content material{mcp.TextContent{
					Sort: "textual content",
					Textual content: "GitHub API secondary fee restrict hit. Wait earlier than retrying.",
				}},
				IsError: true,
			}, nil
		}

		if resp.StatusCode != http.StatusOK {
			return &mcp.CallToolResult{
				Content material: []mcp.Content material{mcp.TextContent{Sort: "textual content", Textual content: fmt.Sprintf("API error: %d", resp.StatusCode)}},
				IsError: true,
			}, nil
		}

		var situation struct {
			Title  string `json:"title"`
			State  string `json:"state"`
			Physique   string `json:"physique"`
			Labels []struct {
				Title string `json:"title"`
			} `json:"labels"`
		}

		limitedBody := io.LimitReader(resp.Physique, maxBodyBytes)
		if err := json.NewDecoder(limitedBody).Decode(&situation); err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("did not parse response: %v", err)), nil
		}

		labels := make([]string, len(situation.Labels))
		for i, l := vary situation.Labels {
			labels[i] = l.Title
		}

		return mcp.NewToolResultText(fmt.Sprintf("Title: %s
State: %s
Labels: %s

%s",
			situation.Title, situation.State, strings.Be part of(labels, ", "), situation.Physique)), nil
	}
}

Be aware on server.WithContext(ctx): In case your model of the mcp-go SDK doesn’t assist passing a context to ServeStdio, the sign goroutine can name os.Exit(0) as a fallback, however remember that this bypasses deferred cleanup. Examine the SDK documentation to your pinned model.

Keep away from calling os.Exit() contained in the sign goroutine. os.Exit terminates the method instantly, bypassing all deferred capabilities, which means database connections, file handles, and log buffers is not going to be cleaned up.

Wrapping Up and Subsequent Steps

This tutorial confirmed the right way to assemble an entire MCP server in Go, from undertaking scaffolding by way of instrument registration, useful resource publicity, immediate templates, and manufacturing hardening with structured logging and swish shutdown. The server helps each stdio and HTTP/SSE transports with minimal code modifications between them.

Pure subsequent steps embody including authentication for HTTP-transported servers (the MCP specification helps OAuth 2.0 flows), deploying behind a reverse proxy like Nginx or Caddy for TLS termination, and implementing Streamable HTTP transport for stateless cloud deployments. The complete MCP specification is out there at spec.modelcontextprotocol.io. The mcp-go SDK repository at github.com/mark3labs/mcp-go accommodates further examples and transport choices. For locating present MCP servers and patterns, the registries at mcp.so and Smithery present searchable catalogs of group implementations.



Supply hyperlink

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles