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 entire means of establishing a production-ready MCP server, from preliminary scaffolding to sleek shutdown, utilizing the mcp-go SDK.

The best way to Construct an MCP Server in Go

  1. Initialize a Go module and pin the mcp-go SDK to a particular model with go get.
  2. Create the server entry level utilizing server.NewMCPServer with functionality choices for instruments, assets, 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 by way of CallToolResult.
  5. Register assets 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 sleek shutdown utilizing OS sign seize and context cancellation as an alternative of os.Exit.
  8. Begin the server on stdio transport for native purchasers or HTTP/SSE transport for distant deployment.

Desk of Contents

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

The Downside 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 earth. 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 downside: M fashions occasions 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 appropriate server. The protocol is open-source. Purchasers throughout the ecosystem have adopted it, together with Claude Desktop, VS Code with GitHub Copilot, Cursor, and different MCP-compatible purchasers.

MCP Structure at a Look

MCP follows a client-server structure with three distinct roles. Hosts are the AI functions (like Claude Desktop) that provoke connections. Purchasers are protocol-level connectors maintained throughout 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 features the mannequin can invoke, roughly analogous to POST endpoints. Learn-only knowledge entry comes by way of Assets, 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 one among a number of transport mechanisms: stdio (customary 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 appropriate 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 power invocations. A single MCP server might 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 vital reminiscence strain.

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

Stipulations and Venture Setup

What You will Want

This tutorial requires Go 1.21 or later put in and accessible on the system PATH (confirm with go model). You must know Go modules, structs, and interface patterns. Any editor with Go help will work, although VS Code with the Go extension or GoLand gives 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 Venture

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. Exchange v0.26.0 with the present secure launch listed at github.com/mark3labs/mcp-go/releases. Operating go get with out a model tag fetches the most recent, which can introduce breaking adjustments after this tutorial was revealed.

The mcp-go SDK by Mark3Labs is a well-adopted Go implementation of the MCP specification. It gives server development primitives, transport handlers, and helper features 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 customary enter and output, which is the anticipated mechanism when an MCP shopper launches the server as a subprocess.

package deal most important

import (
	"fmt"
	"os"

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

func most important() {
	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 operate takes the server identify and model as its first two arguments. The choice features declare which MCP capabilities this server helps. WithToolCapabilities(true) advertises that the server can notify purchasers when its instrument record adjustments. 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 by way of an MCP server. Every instrument requires a schema defining its inputs and a handler operate that executes the logic.

package deal most important

import (
	"context"
	"fmt"
	"os"

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

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

	helloTool := mcp.NewTool("hi there",
		mcp.WithDescription("Greets a person by identify"),
		mcp.WithString("identify",
			mcp.Required(),
			mcp.Description("The identify 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) {
	identify, okay := request.Params.Arguments["name"].(string)
	if !okay || identify == "" {
		return mcp.NewToolResultError("identify parameter is required"), nil
	}

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

The mcp.NewTool operate creates a instrument definition with a JSON Schema for its inputs. Helper features 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/Utility 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 accessible instruments record. Asking Claude to “say hi there to Alice” ought to set off the instrument invocation. If the instrument would not seem, verify Claude Desktop’s MCP log output for configuration or path errors.

Including Assets and Prompts

Exposing Assets

MCP assets signify 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 kind. Assets are appropriate for exposing configuration knowledge, documentation, or database data.

s.AddResource(mcp.NewResource(
	"config://app/settings",
	"Utility Settings",
	mcp.WithResourceDescription("Present software configuration"),
	mcp.WithMIMEType("software/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, "software/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 assets the place the URI incorporates 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 problem 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 should be a non-empty string")
	}
	physique, _ := request.Params.Arguments["issue_body"].(string)

	return &mcp.GetPromptResult{
		Description: "Summarize a GitHub problem",
		Messages: []mcp.PromptMessage{
			{
				Function: mcp.RoleUser,
				Content material: mcp.TextContent{
					Sort: "textual content",
					Textual content: fmt.Sprintf("Summarize this GitHub problem 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 enables servers to supply structured interplay patterns that purchasers 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 problem lookup instrument wants three parameters: the repository proprietor, repository identify, and problem 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("Lookup a GitHub problem by proprietor, repo, and problem quantity"),
		mcp.WithString("proprietor", mcp.Required(), mcp.Description("Repository proprietor")),
		mcp.WithString("repo", mcp.Required(), mcp.Description("Repository identify")),
		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 should be >= 1)"), nil
		}

		
		if !validIdentifier.MatchString(proprietor) || !validIdentifier.MatchString(repo) {
			return mcp.NewToolResultError("proprietor and repo should include 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("didn't construct request: %v", err)), nil
		}
		req.Header.Set("Consumer-Agent", "mcp-go-server/1.0.0")
		req.Header.Set("Settle for", "software/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 (verify 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 problem struct {
			Title  string `json:"title"`
			State  string `json:"state"`
			Physique   string `json:"physique"`
			Labels []struct {
				Identify string `json:"identify"`
			} `json:"labels"`
		}

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

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

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

%s",
			problem.Title, problem.State, strings.Be part of(labelNames, ", "), problem.Physique)
		return mcp.NewToolResultText(consequence), 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{} kind system. This can be a frequent supply of bugs when working with JSON-RPC in Go.

Essential: GitHub’s API requires a Consumer-Agent header on all requests. Calls with out one might 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 (verify 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 somewhat than returning a Go error. Returning a Go error from the handler indicators a protocol-level failure, whereas IsError: true indicators an application-level error that the mannequin can motive about and probably retry or clarify to the person.

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

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, accessible since Go 1.21, gives structured logging with JSON output that integrates effectively with log aggregation methods. Writing to stderr retains the stdio transport clear whereas offering full observability.

Enter Validation and Safety

All instrument inputs should be validated earlier than use. String parameters destined for URL development needs to be validated in opposition to a strict allowlist sample (e.g., ^[a-zA-Z0-9_.-]{1,100}$) somewhat than a denylist of particular characters, as denylist approaches miss URL-encoded variants and different bypass strategies. When instrument outputs embrace content material retrieved from exterior sources, that content material may include immediate injection makes an attempt. Servers mustn’t try to sanitize this content material, as that’s the mannequin’s duty, however needs to be conscious that instrument outputs move straight 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 purchasers. The mcp-go SDK doesn’t present built-in fee limiting (confirm in opposition to the SDK model you might be 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 acquired", "sign", sig.String())
		cancel()
	case <-ctx.Completed():
		
	}
}()

This sample captures interrupt and termination indicators and cancels the context, which needs 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 motive aside 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 features in most important to execute usually.

Warning: Keep away from calling os.Exit() contained in the sign goroutine. os.Exit terminates the method instantly, bypassing all deferred features, which means database connections, file handles, and log buffers won’t be cleaned up. As a substitute, 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 a neighborhood 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 may be dealt with by a reverse proxy corresponding to Nginx or Caddy, as mentioned in Subsequent Steps under.

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 adjustments.

Manufacturing-Prepared Implementation Guidelines

Earlier than deploying an MCP server, confirm every of this stuff:

  • Go module initialized with a pinned mcp-go model in go.mod
  • Server identify and model set in ServerInfo by way of 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 embrace a Consumer-Agent header and a timeout
  • Response our bodies from exterior APIs are size-limited (e.g., by way of io.LimitReader)
  • Report errors by way of IsError: true on CallToolResult, by no means panics
  • Direct logging to stderr (stdio transport) or a structured logger (HTTP transport)
  • Swish shutdown on OS indicators applied by way of context cancellation (not os.Exit)
  • Assets use acceptable MIME varieties
  • If deploying by way of HTTP, add fee limiting on the server or reverse-proxy degree
  • Transport chosen primarily based on deployment goal: stdio for native, HTTP/SSE for distant
  • Examined with no less than one MCP shopper (Claude Desktop or VS Code)
  • Construct and take a look at the binary on course OS/structure by way of GOOS and GOARCH

Full Server Code

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

package deal most important

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 most important() {
	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",
		"Utility Settings",
		mcp.WithResourceDescription("Present software configuration"),
		mcp.WithMIMEType("software/json"),
	), func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
		return []mcp.ResourceContents{
			mcp.NewTextResourceContents(request.Params.URI, "software/json",
				`{"debug": false, "max_connections": 100}`),
		}, nil
	})

	
	s.AddPrompt(mcp.NewPrompt("summarize_issue",
		mcp.WithPromptDescription("Summarize a GitHub problem"),
		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 should be a non-empty string")
		}
		physique, _ := request.Params.Arguments["issue_body"].(string)

		return &mcp.GetPromptResult{
			Messages: []mcp.PromptMessage{{
				Function: 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 acquired", "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("Lookup a GitHub problem"),
		mcp.WithString("proprietor", mcp.Required(), mcp.Description("Repository proprietor")),
		mcp.WithString("repo", mcp.Required(), mcp.Description("Repository identify")),
		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 should be >= 1)"), nil
		}

		
		if !validIdentifier.MatchString(proprietor) || !validIdentifier.MatchString(repo) {
			return mcp.NewToolResultError("proprietor and repo should include 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("didn't construct request: %v", err)), nil
		}
		httpReq.Header.Set("Consumer-Agent", "mcp-go-server/1.0.0")
		httpReq.Header.Set("Settle for", "software/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 (verify 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 problem struct {
			Title  string `json:"title"`
			State  string `json:"state"`
			Physique   string `json:"physique"`
			Labels []struct {
				Identify string `json:"identify"`
			} `json:"labels"`
		}

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

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

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

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

Be aware on server.WithContext(ctx): In case your model of the mcp-go SDK doesn’t help passing a context to ServeStdio, the sign goroutine can name os.Exit(0) as a fallback, however bear in mind 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 features, which means database connections, file handles, and log buffers won’t be cleaned up.

Wrapping Up and Subsequent Steps

This tutorial confirmed assemble a whole MCP server in Go, from challenge scaffolding by way of instrument registration, useful resource publicity, immediate templates, and manufacturing hardening with structured logging and sleek shutdown. The server helps each stdio and HTTP/SSE transports with minimal code adjustments between them.

Pure subsequent steps embrace 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 total MCP specification is accessible at spec.modelcontextprotocol.io. The mcp-go SDK repository at github.com/mark3labs/mcp-go incorporates further examples and transport choices. For locating current MCP servers and patterns, the registries at mcp.so and Smithery present searchable catalogs of group implementations.


Related Articles

Latest Articles