I Built a Custom MCP Server in Golang To Make Claude Smarter—Here's How

Wait 5 sec.

I feel like we're at a point where the Model Context Protocol (MCP) feels almost synonymous with GenAI engineering. Anthropic introduced MCP in November 2024, revolutionizing GenAI engineering. It brought us to the point where, as engineers, we can implement various tools based on our use case, and direct the LLM of our choice to use those tools from the comfort of our favorite IDEs or desktop clients like Claude. Ever since its advent, there have been numerous MCP servers developed by enthusiastic engineers. This was possible due to the rapid development of MCP SDKs in various languages, including Python, TypeScript, and most recently, Golang. (yay 🚀). I saw this official proposal for a Golang SDK, and I knew I had to take it for a spin!What is MCP, you ask?Let's start with a quick intro to MCP, which is short for Model Context Protocol. Until recently, AI engineering required a careful evaluation of the capabilities of various LLMs to select the right one. With MCP, you can select the LLM of your choice, and extend its capabilities by implementing custom tools and connecting to external data sources yourself! The main constructs of the protocol are:MCP clientMCP serverCapabilities (Tools, Resources, Prompts)The LLMAn MCP server has certain capabilities determined by:Tools - The functions it can execute, such as list all emails from a sender, lookup a PR on GithubResources - External data source, such as a list of documents that you want the LLM to refer toPrompts - A set of templates that help users get desired answers fastThere are many MCP servers already available that you can start using for your applications. You can refer to this compilation of awesome MCP servers: https://github.com/punkpeye/awesome-mcp-serversBYOM (Bring your own MCP server)In this post, I want to go over how we can develop our own MCP server using Golang. A few days back, for whatever reaso,n I wanted to go over all repositories in the Kubernetes GitHub org.Now I could have used the official GitHub MCP server, but while it offers a great toolset for repositories and organizations, it didn’t have a direct tool for listing all repositories in an organization. That’s why this seemed like good opportunity to learn how to develop an MCP server for the specific tool I needed while using Golang.Developing an MCP server in GolangThis is the official Golang MCP SDK: https://github.com/modelcontextprotocol/go-sdk. The README and examples/ folders contain great examples on developing an MCP server and client. Following those examples, I set out to create an MCP server as follows: server := mcp.NewServer(&mcp.Implementation{ Name: "demo-github-mcp", Title: "A demo github mcp server", Version: "0.0.1", }, nil)The next step was to provide the server with the capability of listing all repositories in a GitHub org. This can be done by implementing a tool of type ToolHandler as provided by the SDKtype ToolHandlerFor[In, Out any] func(context.Context, *ServerSession, *CallToolParamsFor[In]) (*CallToolResultFor[Out], error)\Following that, I created a ListRepositories tool, which accepts the following arguments related to the Github org as input:// User can pass in either the name of the org (example: kubernetes), or its URL (example: https://github.com/kubernetes)type GithubOrgArgs struct { Name string URL string}func ListRepositories(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolParamsFor[GithubOrgArgs]) (*mcp.CallToolResultFor[struct{}], error) {\Now let's go over the body of ListRepositories step-by-step:\Input verification to ensure that we only accept valid arguments if params == nil { return nil, fmt.Errorf("empty params") } args := params.Arguments if args.Name == "" && args.URL == "" { return nil, fmt.Errorf("empty args") }\Forming the GitHub API URL from the input based on GitHub API docs var apiURL string var organization string if args.URL != "" { // If URL is provided, extract org name and build API URL url := strings.TrimPrefix(args.URL, "https://") url = strings.TrimPrefix(url, "http://") url = strings.TrimPrefix(url, "github.com/") url = strings.TrimSuffix(url, "/") orgName := strings.Split(url, "/")[0] apiURL = fmt.Sprintf("https://api.github.com/orgs/%s/repos", orgName) organization = orgName } else { // Use the provided organization name apiURL = fmt.Sprintf("https://api.github.com/orgs/%s/repos", args.Name) organization = args.Name } apiURL = apiURL + "?per_page=100"\Send the request and receive a response client := &http.Client{Timeout: 10 * time.Second} req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) if err != nil { return nil, err } req.Header.Add("Accept", "application/vnd.github.v3+json") resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("GitHub API error (status %d): %s", resp.StatusCode, string(body)) }\Parse the response from the GitHub API type repository struct { Name string `json:"name"` FullName string `json:"full_name"` HTMLURL string `json:"html_url"` Private bool `json:"private"` } // Parse the JSON response var repositories []repository if err := json.NewDecoder(resp.Body).Decode(&repositories); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) }\Return the response as a textual context var result strings.Builder result.WriteString(fmt.Sprintf("Repositories for organization %s:", organization)) for _, repo := range repositories { result.WriteString(fmt.Sprintf("Name: %s, URL: %s", repo.Name, repo.HTMLURL)) } return &mcp.CallToolResultFor[struct{}]{ Content: []mcp.Content{ &mcp.TextContent{Text: result.String()}, }, }, nil\After defining the tool, the next step is to register it with the MCP server: mcp.AddTool(server, &mcp.Tool{ Name: "list-repositories", Description: "A tool to list all repositories in a Github org", InputSchema: &jsonschema.Schema{ Type: "object", Properties: map[string]*jsonschema.Schema{ "name": { Type: "string", Description: "GitHub organization name (e.g., kubernetes)", }, "url": { Type: "string", Description: "GitHub organization URL (e.g., https://github.com/kubernetes)", }, }, }, }, ListRepositories)\Next, it's time to start the server! For this demo MCP server, I'm using the stdio transport, which allows the server to communicate via STDIN and STDOUT. This is the standard approach for local MCP integrations with clients like Claude Desktop or VSCode. t := mcp.NewLoggingTransport(mcp.NewStdioTransport(), os.Stderr) log.Println("🚀 MCP server starting up...") if err := server.Run(context.Background(), t); err != nil { log.Printf("Server failed: %v", err) } log.Println("🚀 MCP server shutting down...")\This is what the code looks like with everything put together:func main() { server := mcp.NewServer(&mcp.Implementation{ Name: "demo-github-mcp", Title: "A demo github mcp server", Version: "0.0.1", }, nil) mcp.AddTool(server, &mcp.Tool{ Name: "list-repositories", Description: "A tool to list all repositories in a Github org", InputSchema: &jsonschema.Schema{ Type: "object", Properties: map[string]*jsonschema.Schema{ "name": { Type: "string", Description: "GitHub organization name (e.g., kubernetes)", }, "url": { Type: "string", Description: "GitHub organization URL (e.g., https://github.com/kubernetes)", }, }, }, }, ListRepositories) t := mcp.NewLoggingTransport(mcp.NewStdioTransport(), os.Stderr) log.Println("🚀 MCP server starting up...") if err := server.Run(context.Background(), t); err != nil { log.Printf("Server failed: %v", err) } log.Println("🚀 MCP server shutting down...")}type GithubOrgArgs struct { Name string URL string}func ListRepositories(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolParamsFor[GithubOrgArgs]) (*mcp.CallToolResultFor[struct{}], error) { if params == nil { return nil, fmt.Errorf("empty params") } args := params.Arguments if args.Name == "" && args.URL == "" { return nil, fmt.Errorf("empty args") } var apiURL string var organization string if args.URL != "" { // If URL is provided, extract org name and build API URL url := strings.TrimPrefix(args.URL, "https://") url = strings.TrimPrefix(url, "http://") url = strings.TrimPrefix(url, "github.com/") url = strings.TrimSuffix(url, "/") orgName := strings.Split(url, "/")[0] apiURL = fmt.Sprintf("https://api.github.com/orgs/%s/repos", orgName) organization = orgName } else { // Use the provided organization name apiURL = fmt.Sprintf("https://api.github.com/orgs/%s/repos", args.Name) organization = args.Name } apiURL = apiURL + "?per_page=100" client := &http.Client{Timeout: 10 * time.Second} req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) if err != nil { return nil, err } req.Header.Add("Accept", "application/vnd.github.v3+json") resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("GitHub API error (status %d): %s", resp.StatusCode, string(body)) } type repository struct { Name string `json:"name"` FullName string `json:"full_name"` HTMLURL string `json:"html_url"` Private bool `json:"private"` } // Parse the JSON response var repositories []repository if err := json.NewDecoder(resp.Body).Decode(&repositories); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } var result strings.Builder result.WriteString(fmt.Sprintf("Repositories for organization %s:", organization)) for _, repo := range repositories { result.WriteString(fmt.Sprintf("Name: %s, URL: %s", repo.Name, repo.HTMLURL)) } return &mcp.CallToolResultFor[struct{}]{ Content: []mcp.Content{ &mcp.TextContent{Text: result.String()}, }, }, nil}Now the final step is to compile this and generate the executable with:go build\Using the MCP server with a client like ClaudeLet's look at how to add this server to Claude's desktop.Open the Claude desktop app, and go to Claude -> Settings -> Developer.Here you will see a section called Local MCP Servers. Click on the button below called Edit Config. This will open up the Claude config file.Edit the file to add the MCP server as follows:{ "mcpServers": { "demo-github-mcp": { "command": "/path/to/executable/generated/from/go build", "args": [] } }}Restart the Claude app (Currently, this is the only way to refresh MCP servers within Claude)And now it's time to test this out! This is the prompt I gave:List all repositories in the Kubernetes github org\Claude recognized the locally running demo-github-mcp server and asked if it could use that!As soon as I approved it, Claude listed the repositories in the org and even displayed the tool used (list-repositories) at the start of the response:And there we go! We saw how we can develop a simple MCP server using Golang, and use it through MCP clients such as Claude!The “AHA” momentAs I was developing this server, I kept asking myself: “If I’m writing the code for listing repositories, what is the LLM going to do?”, and then suddenly it clicked - the LLM enables communication to this server via natural language!When Claude Sonnet 4 read my prompt - “List all repositories in the Kubernetes GitHub org” - and on its own recognized that the locally running MCP server would work best for the prompt, my mind was blown away :grinning:. At that point, I didn’t have to provide it with any information on the tool name or the parameters it accepted. I asked my question through a simple sentence, and it figured out which tool to use, how to call the tool, and got the job done!Production ConsiderationsWhile our MCP server works great for demo purposes, there are several important features missing for production use:\Pagination handling: GitHub's API returns paginated results, the default being 30 entries per page. In the above code I've increased that to 100 with the per_page query parameter. For production code, you'd process the response header Link to get information about the next page and total number of pages. (Refer to this)Rate limiting: The above code does not take into account GitHub API rate limitsAuthentication: Authenticated requests have a higher rate limit than unauthenticated requests. Authentication is also required for requests to private GitHub repositories/enterprise GitHub.What's next?This demo GitHub MCP server was a good start. But the main reason I'm writing this is to demonstrate how easy it can be to customize your favorite GenAI tool to your liking! It's also surprisingly simple to get started with an MCP server in Golang for your own projects! I can't wait to see the powerful services created from the combination of MCP's flexibility and Golang's high-performing nature!