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!
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:
An MCP server has certain capabilities determined by:
There 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-servers
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.
This 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 SDK
type 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:
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
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...")
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
Let’s look at how to add this server to Claude’s desktop.
{
"mcpServers": {
"demo-github-mcp": {
"command": "/path/to/executable/generated/from/go build",
"args": []
}
}
}
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!
As 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 😀. 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!
While our MCP server works great for demo purposes, there are several important features missing for production use:
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)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!
This article was originally published by Rajashree M on HackerNoon.
Hoffmann's work to advance ESG, Agenda 2030, and a great reset of the global economic…
Digitalizing and pricing nature with derivatives is becoming the new gold for powerful groups looking…
The public-private financial revolution requires every person to have a digital identity, so they can…
At the outset of 2025, digital workflow platform ServiceNow revealed plans to hire up to…
Online age checks are not just about children; they're about getting everybody onboard with digital…
Big-name expos and star-studded keynotes may grab headlines, but in 2025, it’s the smaller, more…