mcp-code is a small server that connects my GitHub to remote AI clients such as claude.ai and ChatGPT, which speak the remote Streamable HTTP MCP protocol. I wanted those clients to actually read my repositories, navigate the code, and answer questions about it, and the existing GitHub connectors weren’t great: too much surface area, permissions I didn’t want to grant. So I wrote my own. Now I manage my own infrastructure and extend it whenever I want. The whole thing is a single main.go, and it’s deliberately one shape: one process, one MCP endpoint, one GitHub token, stateless HTTP. It’s built to deploy on serverless, for anyone who wants the same capability in a lightweight Go server.
Stateless Streamable HTTP, for serverless
Most MCP servers are written as local stdio processes or long-lived SSE services. This one targets Vercel and a remote client, so it uses the official MCP Go SDK’s Streamable HTTP handler in stateless mode with JSON responses:
mcp.NewStreamableHTTPHandler(..., &mcp.StreamableHTTPOptions{ Stateless: true, JSONResponse: true, DisableLocalhostProtection: hosted,})Stateless means there’s no session memory, no per-client state, no background workers, no caches. Each request stands on its own, which is exactly what a serverless function needs: an instance can spin up, serve one MCP call, and disappear. Every tool call builds a fresh GitHub client from the environment with a 20-second timeout, so nothing lingers or hangs an invocation.
The tools match how an agent works a repo
There are seventeen tools, chosen to cover the loop an agent actually runs against GitHub. The ones I reach for most:
github_search_codeandgithub_get_file: search broadly across a repo, then open the one file that matters. This discover-then-read pair is the difference between an agent that reasons its way to the right place and one that reads blindly.github_list_repositories,github_get_repository, andgithub_search_repositories: find and inspect repos.github_list_issues,github_get_issue,github_list_pull_requests, andgithub_get_pull_request: read the discussion around the code.github_create_branchandgithub_upsert_file: the minimal write path. Make a branch, commit a file, no clone required.
Each tool is a typed Go struct in and out, with jsonschema tags the SDK turns into the client-facing contract. For an LLM those tags are affordances, not documentation. jsonschema:"GitHub code search query, for example 'repo:owner/name path:cmd main'" teaches the model how to call the tool. And the outputs are reshaped for the caller: GitHub’s objects are huge and nested, so a code-search hit collapses to owner/repo/path/name/sha/url/matches, and a pull request flattens to author, refs, additions, and mergeability. The response shape is what an agent plans against, so I trimmed it to the fields that matter.
The API key lives in the URL, on purpose
Two secrets do two jobs. MCP_API_KEY authenticates the client to my server; GITHUB_TOKEN authenticates my server to GitHub and never leaves it. A client that reaches the endpoint can call my tools, but it never touches my GitHub token.
The interesting decision is that the server accepts the API key as an ?api_key= query param (as well as a bearer header). That’s intentional, not a compatibility hack. Remote MCP clients tend to offer either no auth at all or full OAuth, and OAuth is far too much machinery for something this small. Putting the key in the URL keeps the server dead simple: it’s a public Vercel deployment, but the only person who holds the key is me, so I’m the only one who can call it. On Vercel I also add a proxy-aware same-origin check (derived from X-Forwarded-Proto / X-Forwarded-Host, since r.Host breaks behind a proxy). Beyond that, the real permission boundary is the token’s own scopes. Deploy with a fine-grained token, and give it a read-only token if you don’t want the write tools active.
Small helpers that make it pleasant to call
A few helpers carry the ergonomics. Ref resolution takes whatever an agent provides (main, v1.2.3, refs/heads/main, a short SHA) and resolves it to a concrete commit by trying commit lookup, then heads/, tags/, the raw ref, and finally GitHub’s SHA-1 resolver, so branch creation just works. github_upsert_file tries to create a file first and falls back to update if it already exists, presenting a single “upsert” verb over GitHub’s two APIs. And github_get_file caps returned content at 20,000 characters so a large file can’t blow up the client’s context window.
It’s a simple tool that does one job well, and because it’s mine, I add to it whenever a client needs something new.
It sits alongside the rest of my MCP work. Where Zodex hands ChatGPT a whole Linux machine to code on, this just lets a remote client read and understand my repos. It’s part of the set-and-forget workflow I wrote about in ChatGPT is my background coding agent. See also my other MCP servers, terminal-mcp and mcp-manager.
The stack
Go, the official MCP Go SDK over stateless Streamable HTTP, go-github for the REST calls, oauth2 for the token client, and Vercel’s Go runtime for hosting. Configuration is entirely environment variables (PORT, MCP_API_KEY, GITHUB_TOKEN, plus Vercel’s own), so the same binary runs identically on my laptop and in production. No config file, no database, no framework.
