mcp-server-go
Golang implementation of the streaming MCP HTTP transport with sessions, auth and horizontal scaling
Install / Use
claude mcp add ggoodman -- npx -y github:ggoodman/mcp-server-goIf the server publishes to npm under a different name, use that package instead — check the repo README.
MCP Server
Model Context Protocol server
Quality Score
Category
Development & EngineeringSupported Platforms
Skill content
View source on GitHubmcp-server-go
<strong>Build Model Context Protocol servers that scale from a 20‑line stdio prototype to a horizontally scaled, OIDC‑protected streaming HTTP deployment — without rewriting business logic.</strong>
<p> <a href="https://pkg.go.dev/github.com/ggoodman/mcp-server-go"><img alt="Go Reference" src="https://pkg.go.dev/badge/github.com/ggoodman/mcp-server-go.svg" /></a> <a href="https://goreportcard.com/report/github.com/ggoodman/mcp-server-go"><img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/ggoodman/mcp-server-go" /></a> <a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-green.svg" /></a> <a href="https://github.com/ggoodman/mcp-server-go/actions/workflows/ci-test.yml"><img alt="CI - Test" src="https://github.com/ggoodman/mcp-server-go/actions/workflows/ci-test.yml/badge.svg?branch=main" /></a> </p> </div>Install / Import
Go module: github.com/ggoodman/mcp-server-go
Add to your project:
go get github.com/ggoodman/mcp-server-go@latest
Browse documentation: https://pkg.go.dev/github.com/ggoodman/mcp-server-go
Minimum Go version: as declared in go.mod (currently 1.24). The module follows standard Go module semantic import versioning (no v2 path yet).
TL;DR Quickstart (stdio CLI)
Below is a tiny CLI MCP server exposing a single vibe-checking tool. The tool demonstrates using sampling and elicitation. It also shows how a response can be constructed through a http.ResponseWriter-like API.
package main
import (
"context"
"fmt"
"log"
"github.com/ggoodman/mcp-server-go/mcpservice"
"github.com/ggoodman/mcp-server-go/sessions"
"github.com/ggoodman/mcp-server-go/sessions/sampling"
"github.com/ggoodman/mcp-server-go/stdio"
)
// Minimal args: none needed to start the interaction.
type VibeArgs struct{}
type VibePrompt struct {
Phrase string `json:"phrase" jsonschema:"minLength=3,description=How are you feeling?,title=Vibe"`
}
func vibeCheck(ctx context.Context, s sessions.Session, w mcpservice.ToolResponseWriter, r *mcpservice.ToolRequest[VibeArgs]) error {
el, ok := s.GetElicitationCapability()
if !ok {
return fmt.Errorf("elicitation capability not available in this session")
}
var prompt VibePrompt
// Below, the reference to prompt both documents the expected response shape
// and populates it when the user accepts the elicitation.
action, err := el.Elicit(ctx, "What's the vibe?", &prompt)
if err != nil {
return err
}
if action != sessions.ElicitActionAccept {
w.AppendText("the user is not feeling it")
w.SetError(true)
return nil
}
samp, ok := s.GetSamplingCapability()
if !ok {
return fmt.Errorf("sampling capability not available in this session")
}
// Sample host LLM for a single whimsical word (new ergonomic API).
res, err := samp.CreateMessage(ctx,
"Respond with short phrase, capturing the emotional vibe of the submitted message with a touch of whimsy.",
sampling.UserText(prompt.Phrase),
sampling.WithMaxTokens(50),
)
if err != nil {
return err
}
w.AppendBlocks(res.Message.Content.AsContentBlock())
if txt, ok := res.Message.Content.(sampling.Text); ok {
w.AppendText(txt.Text)
}
return nil
}
func main() {
tools := mcpservice.NewToolsContainer(
mcpservice.NewTool("vibe_check", vibeCheck, mcpservice.WithToolDescription("Herein lies the answer when the question is vibe.")),
)
server := mcpservice.NewServer(
mcpservice.WithServerInfo(mcpservice.StaticServerInfo("vibe-check-demo", "0.0.1")),
mcpservice.WithToolsCapability(tools),
mcpservice.WithInstructions(mcpservice.StaticInstructions("Your finger is on the pulse of the inter-webs. You can feel it. You can help others feel it too.")),
)
h := stdio.NewHandler(server)
if err := h.Serve(context.Background()); err != nil {
log.Fatal(err)
}
}
Upgrade path: swap the transport + host; your server value is unchanged and you layer in authorization.
From stdio prototype → horizontally scaled streaming HTTP (with auth)
Below is an end‑to‑end sketch showing how you take the earlier stdio server and run it behind the streaming HTTP transport with:
- A distributed session host (Redis) for fan‑out + durability.
- OIDC discovery (single line) OR a manual static JWT config (offline / air‑gapped environments).
- Automatic well‑known metadata advertisement (protected resource + authorization server mirrors) sourced solely from one
auth.SecurityConfig.
// (Sketch – not a full program)
ctx := context.Background()
// 1. Construct (or reuse) your MCP server capabilities (same as stdio)
server := buildServer() // from earlier snippet
// 2. Pick a SessionHost implementation (memory for single node; redis for scale)
host, _ := redishost.New(os.Getenv("REDIS_ADDR"))
publicEndpoint := "https://mcp.example.com/mcp" // the full public URL path clients will call
issuer := "https://issuer.example" // your OIDC issuer
// 3a. Discovery-based auth (recommended when you control / trust the AS metadata)
authn, _ := auth.NewFromDiscovery(ctx, issuer, publicEndpoint)
// 3b. OR manual static JWT validation (no discovery). You MUST supply advertisement fields explicitly.
// jwksURL := "https://issuer.example/jwks.json"
// sec := auth.SecurityConfig{
// Issuer: issuer,
// Audiences: []string{publicEndpoint},
// JWKSURL: jwksURL,
// Advertise: true, // serve well-known endpoints
// OIDC: &auth.OIDCExtra{ // ONLY fields you populate here will be advertised.
// ResponseTypesSupported: []string{"code"}, // required by our strict policy (discovery would have enforced)
// },
// }
// sec.Normalize()
// authn, _ := sec.NewManualJWTAuthenticator(ctx)
// 4. Create transport. If an authenticator implements auth.SecurityDescriptor the handler
// derives the SecurityConfig from it; you can also pass a SecurityConfig explicitly via option.
httpHandler, _ := streaminghttp.New(
ctx,
publicEndpoint,
host,
server,
authn,
streaminghttp.WithServerName("reverse-prod"),
)
http.Handle("/mcp", httpHandler)
Key points:
- One source of truth:
auth.SecurityConfig(exposed by the authenticator or provided directly) feeds all advertisement (no duplicated issuer/audience/JWKS in transport options). - Discovery path: strict validation — fails fast if required metadata (
jwks_uri,authorization_endpoint,token_endpoint,response_types_supported) is missing so clients get a complete picture. - Manual path: you control exactly what is advertised; nothing is synthesized. If you want clients to know supported response or grant types you must set the corresponding slices in
OIDCExtra. - Horizontal scale requires only swapping the session host; capability logic is untouched.
Capability Model (Server Side)
At initialization the client sends its ClientCapabilities; the server responds with ServerCapabilities. Each negotiated capability unlocks a method set (see mcp/messages.go). Server implementations choose between:
- Containers (static sets) – simple, mutation helpers, built‑in pagination and change notifications.
- Provider funcs (dynamic) – per session logic (return (cap, ok, err)).
Each capability is configured by a single With*Capability option that accepts a provider:
- Pass a container (e.g.
NewToolsContainer) directly – containers self‑implement the provider. - Or pass an
XCapabilityProviderFuncfor per‑session logic.
Authorization, validation & advertisement
The streaming HTTP transport now derives all advertised security metadata from a single auth.SecurityConfig exposed by the authenticator (it implements auth.SecurityDescriptor) or provided explicitly via streaminghttp.WithSecurityConfig.
Typical pattern:
authn, _ := auth.NewFromDiscovery(ctx, issuerURL, publicURL)
handler, _ := streaminghttp.New(ctx, publicURL, host, server, authn)
If the resolved SecurityConfig.Advertise is true, the handler automatically:
- Serves Protected Resource Metadata (
/.well-known/oauth-protected-resource<endpoint-path>) - Mirrors Authorization Server Metadata (
/.well-known/oauth-authorization-server) - Emits
WWW-Authenticateheaders pointing at the resource metadata on auth failures
To override or supply metadata without discovery (e.g. offline environments) pass:
streaminghttp.WithSecurityConfig(auth.SecurityConfig{Issuer: issuerURL, Audiences: []string{"my-aud"}, JWKSURL: jwksURL, Advertise: true})
No more duplicated issuer/audience across transport options—one source of truth.
Capability providers (static & dynamic)
Every capability is configured via exactly one option: WithResourcesCapability, WithToolsCapability, WithPromptsCapability, WithLoggingCapability, etc. Each option takes a provider – something implementing the corresponding XCapabilityProvider interface.
Three ergonomic patterns:
- Static constant: use the
StaticXhelpers, e.g.WithProtocolVersion(StaticProtocolVersion("2025-06-18"))orWithServerInfo(StaticServerInfo("name", "version", WithServerInfoTitle("Nice Title"))). - Self-providing container: pass a container directly.
NewToolsContainer(...)andNewResourcesContainer(...)implement both the capability and its provider; just doWithToolsCapability(tools). - Per-session dynamic logic: provide an
XCapabilityProviderFuncclosure. It receives context + session and can return a tailored capability (orok=falseto omit it for that session).
Return (value, ok=true, nil) to advertise a capability even if its list is empty. Return ok=false to omit that capability altogether.
ListChanged notifications are emitted when underlying containers signal a change (e.g. Replace / ReplaceResources). This works uniformly for static containers and dynamic implementations.
Dynamic capabilities (and static containers)
Prefer dynamic? Provide an XCapabilityProviderFunc closure when constructing the server.
Humorous Elicitation Mini-Example
Collect user input mid-tool without designing a new schema manually. Below a tool elicits a favorite snack then responds with two content blocks.
tool := mcpservice.NewTool[struct{}]("snack_oracle", func(ctx context.Context, s sessions.Session, w mcpservice.ToolResponseWriter, r *mcpservice.ToolRequest[struct{}]) error {
if el, ok := s.GetElicitationCapability(); ok {
type Snack struct { Name string `json:"name" jsonschema:"minLength=2,description=Favorite snack"` }
var sn Snack
dec := elicitation.BindStruct(&sn)
action, err := el.Elicit(ctx, "What snack fuels your coding?", dec)
if err != nil || action != sessions.ElicitActionAccept { return nil } // minimal handling
_ = w.AppendText("Crunching the data...")
_ = w.AppendBlocks(mcp.ContentBlock{Type: mcp.ContentTypeText, Text: "Consensus: " + sn.Name + " increases bug-free LOC by 0%. Delicious anyway."})
return nil
}
_ = w.AppendText("Client can't elicit. Falling back to generic advice: hydrate.")
return nil
}, mcpservice.WithToolDescription("Politely asks for your favorite snack."))
Add it next to your other tools in a container; structured schema is auto-reflected.
Session-Level Capabilities (Client → Server negotiated)
After initialization you work with a sessions.Session value that exposes optional per-session capabilities negotiated during the handshake. These sit alongside the server capability interfaces and let you bridge user workflows that require client cooperation (sampling, roots, elicitation) while keeping domain logic cohesive.
Session (excerpt):
type Session interface {
SessionID() string
UserID() string
ProtocolVersion() string
GetSamplingCapability() (cap SamplingCapability, ok bool)
GetRootsCapability() (cap RootsCapability, ok bool)
GetElicitationCapability() (cap ElicitationCapability, ok bool)
}
Pattern: fetch capability → if present call → honor context cancellation.
SamplingCap
Truncated for display — read the full file on GitHub.
Related Skills
Agent-Reach
84.2kGive your AI agent eyes to see the entire internet. Read & search Twitter, Reddit, YouTube, GitHub, Bilibili, XiaoHongShu — one CLI, zero API fees.
headroom
73.4kCompress tool outputs, logs, files, and RAG chunks before they reach the LLM. 20% fewer tokens for coding agents, 60-95% fewer tokens for JSON, same answers. Library, proxy, MCP server.
ruflo
73.0k🌊 The original agent harness. Deploy intelligent multi-player swarms, coordinate autonomous workflows, and build conversational AI systems. Features adaptive memory, self-learning intelligence, federation, vector RAG integration, and native Claude Code / Codex / Hermes and many more Integrated
career-ops
72.3kOpen-source AI job search: scan job portals, evaluate listings into a structured A-H report with a global 1-5 score, tailor your CV, track applications — runs locally in your AI coding CLI (Claude Code, Codex, OpenCode, Antigravity…)
