loading…
Search for a command to run...
loading…
About The fast, idiomatic way to build MCP servers in Go. Gin-like DX with struct-tag auto schema, middleware, adapters for Gin/OpenAPI/gRPC.
About The fast, idiomatic way to build MCP servers in Go. Gin-like DX with struct-tag auto schema, middleware, adapters for Gin/OpenAPI/gRPC.
Go Version License Release gomcp MCP server
The fast, idiomatic way to build MCP servers in Go.
GoMCP is a framework for building Model Context Protocol (MCP) servers — not just an SDK. Think of it as "Gin for MCP".
MCP is the open protocol that lets AI applications (Claude Desktop, Cursor, Kiro, VS Code Copilot) call external tools, read data sources, and use prompt templates. GoMCP makes building those servers trivial.
| mcp-go (mark3labs) | Official Go SDK | GoMCP | |
|---|---|---|---|
| Level | SDK | SDK | Framework |
| Schema generation | Manual | jsonschema tag |
mcp tag + auto validation |
| Middleware | Basic hooks | None | Full chain (Logger, Auth, RateLimit, OTel…) |
| Tool groups | No | No | Yes (user.get, admin.delete) |
| Import Gin routes | No | No | ✅ One line |
| Import OpenAPI/Swagger | No | No | ✅ One line |
| Import gRPC services | No | No | ✅ |
| Built-in auth | No | No | Bearer / API Key / Basic + RBAC (Bearer = your token/JWT validator) |
| Inspector UI | No | No | ✅ |
| Test utilities | Basic | No | mcptest package |
| Requirement | Version |
|---|---|
| Go | ≥ 1.25 |
| MCP Protocol | 2024-11-05 (backward compatible with 2025-11-25) |
Note on the Go 1.25 requirement. GoMCP's
go.moddeclaresgo 1.25.0so the project always builds with the toolchain that ships current security and runtime fixes. If you are running Go 1.21+ locally with the defaultGOTOOLCHAIN=auto, Go will automatically download and use the matching toolchain for you — no manual upgrade is needed. If you have pinnedGOTOOLCHAIN=local, install Go 1.25+ or unset the pin.
| Technology | Description |
|---|---|
| Go standard library | Framework routing, JSON-RPC, transports — no forced DB/ORM deps |
| Gin | Adapter only — import existing Gin routes |
| gRPC | Adapter only — import gRPC services |
| OpenTelemetry | Optional — distributed tracing |
| YAML v3 | Provider only — hot-reload tool definitions |
mcp tags, JSON Schema generated automaticallyfunc(*Context, Input) (Output, error) — no manual parameter parsingname@versionClose(), session idle eviction, async concurrency — see Server lifecycle, sessions & async tasks.┌──────────────────────────────────────────────────────────────┐
│ User Code │
│ s.Tool() / s.ToolFunc() / s.Resource() / s.Prompt() │
├──────────────────────────────────────────────────────────────┤
│ Framework Core │
│ Router → Middleware Chain → Validation → Handler → Result │
├────────────┬─────────────┬───────────────┬───────────────────┤
│ Schema │ Validator │ Adapters │ Observability │
│ Generator │ Engine │ Gin/OpenAPI/ │ OTel / Logger │
│ (mcp tags) │ (auto) │ gRPC │ / Inspector │
├────────────┴─────────────┴───────────────┴───────────────────┤
│ Protocol Layer │
│ JSON-RPC 2.0 / MCP / Capability Negotiation │
├──────────────────────────────────────────────────────────────┤
│ Transport Layer │
│ stdio / Streamable HTTP + SSE │
└──────────────────────────────────────────────────────────────┘
gomcp/
├── server.go # Server core, tool/resource/prompt registration
├── context.go # Request context with typed accessors
├── group.go # Tool groups with prefix naming
├── middleware.go # Middleware chain helpers (SkipAuthForMCPMethods, handshake skips)
├── middleware_builtin.go # Logger, Recovery, RequestID, Timeout, RateLimit
├── middleware_auth.go # Bearer/API key/Basic auth, RBAC, SSE auth helpers
├── middleware_otel.go # OpenTelemetry tracing
├── schema/ # struct tag → JSON Schema generator + validator
├── transport/ # stdio + Streamable HTTP + optional CORS helper
├── adapter/ # Gin, OpenAPI, gRPC adapters
├── mcptest/ # Testing utilities
├── task.go # Async task support
├── completion.go # Auto-completions
├── inspector.go # Web debug UI
├── provider.go # Hot-reload from YAML
└── examples/ # Working examples
├── basic/ # Minimal stdio server
├── filesystem/ # Real-world file ops
├── gin-adapter/ # Import Gin routes
├── openapi-adapter/ # Import Swagger/OpenAPI
└── grpc-adapter/ # Import gRPC services
Step-by-step guides for common tasks (5 minutes each):
go get github.com/zhangpanda/gomcp
package main
import (
"fmt"
"github.com/zhangpanda/gomcp"
)
type SearchInput struct {
Query string `json:"query" mcp:"required,desc=Search keyword"`
Limit int `json:"limit" mcp:"default=10,min=1,max=100"`
}
type SearchResult struct {
Items []string `json:"items"`
Total int `json:"total"`
}
func main() {
s := gomcp.New("my-server", "1.0.0")
s.ToolFunc("search", "Search documents by keyword", func(ctx *gomcp.Context, in SearchInput) (SearchResult, error) {
items := []string{fmt.Sprintf("Result for %q", in.Query)}
return SearchResult{Items: items, Total: len(items)}, nil
})
s.Stdio()
}
The SearchInput struct automatically generates this JSON Schema:
{
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search keyword" },
"limit": { "type": "integer", "default": 10, "minimum": 1, "maximum": 100 }
},
"required": ["query"]
}
Invalid parameters are rejected before your handler runs:
validation failed: query: required; limit: must be <= 100
| Tag | Type | Description | Example |
|---|---|---|---|
required |
flag | Field must be provided | mcp:"required" |
desc |
string | Human-readable description | mcp:"desc=Search keyword" |
default |
any | Default value | mcp:"default=10" |
min |
number | Minimum value (inclusive) | mcp:"min=0" |
max |
number | Maximum value (inclusive) | mcp:"max=100" |
enum |
string | Pipe-separated allowed values | mcp:"enum=asc|desc" |
pattern |
string | Regex validation | mcp:"pattern=^[a-z]+$" |
Combine: mcp:"required,desc=User email,pattern=^[^@]+@[^@]+$"
Supported types: string, int, float64, bool, []T, nested structs.
Simple handler:
s.Tool("hello", "Say hello", func(ctx *gomcp.Context) (*gomcp.CallToolResult, error) {
return ctx.Text("Hello, " + ctx.String("name")), nil
})
Typed handler (recommended):
type Input struct {
Name string `json:"name" mcp:"required,desc=User name"`
Email string `json:"email" mcp:"required,pattern=^[^@]+@[^@]+$"`
}
s.ToolFunc("create_user", "Create user", func(ctx *gomcp.Context, in Input) (User, error) {
return db.CreateUser(in.Name, in.Email)
})
// Static
s.Resource("config://app", "App config", func(ctx *gomcp.Context) (any, error) {
return map[string]any{"version": "1.0"}, nil
})
// Dynamic URI template
s.ResourceTemplate("db://{table}/{id}", "DB record", func(ctx *gomcp.Context) (any, error) {
return db.Find(ctx.String("table"), ctx.String("id")), nil
})
s.Prompt("code_review", "Code review",
[]gomcp.PromptArgument{gomcp.PromptArg("language", "Language", true)},
func(ctx *gomcp.Context) ([]gomcp.PromptMessage, error) {
return []gomcp.PromptMessage{
gomcp.UserMsg(fmt.Sprintf("Review this %s code for bugs.", ctx.String("language"))),
}, nil
},
)
s.Use(gomcp.Logger()) // Log MCP method + duration
s.Use(gomcp.Recovery()) // Recover from panics
s.Use(gomcp.RequestID()) // Unique request ID
s.Use(gomcp.Timeout(10 * time.Second)) // Deadline enforcement
s.Use(gomcp.RateLimit(100)) // 100 calls/minute
s.Use(gomcp.OpenTelemetry()) // Distributed tracing
// Pick one: BearerAuth (token required on initialize too) or BearerAuthSkipHandshake (initialize/ping anonymous).
s.Use(gomcp.BearerAuthSkipHandshake(tokenValidator))
s.Use(gomcp.APIKeyAuthSkipHandshake("X-API-Key", keyValidator))
Auth error shape. When a BearerAuth / APIKeyAuth / BasicAuth / RequireRole / RequirePermission middleware rejects a call, the framework returns a regular JSON-RPC
resultwithisError = trueand the reason incontent[0].text— not a JSON-RPCerrorobject, and not an HTTP 401/403. Clients must inspect the tool-call result'sisErrorto detect auth failures.
Custom middleware:
func AuditLog() gomcp.Middleware {
return func(ctx *gomcp.Context, next func() error) error {
start := time.Now()
err := next()
log.Printf("tool=%s duration=%s err=%v", ctx.String("_tool_name"), time.Since(start), err)
return err
}
}
user := s.Group("user", authMiddleware)
user.Tool("get", "Get user", getUser) // → user.get
user.Tool("update", "Update user", updateUser) // → user.update
admin := user.Group("admin", gomcp.RequireRole("admin"))
admin.Tool("delete", "Delete user", deleteUser) // → user.admin.delete
Gin — one line to import your existing API:
adapter.ImportGin(s, ginRouter, adapter.ImportOptions{
IncludePaths: []string{"/api/v1/"},
})
// GET /api/v1/users/:id → Tool get_api_v1_users_by_id (id = required param)
OpenAPI — generate from Swagger docs:
adapter.ImportOpenAPI(s, "./swagger.yaml", adapter.OpenAPIOptions{
TagFilter: []string{"pets"},
ServerURL: "https://api.example.com",
})
gRPC:
adapter.ImportGRPC(s, grpcConn, adapter.GRPCOptions{
Services: []string{"user.UserService"},
})
s.ToolFunc("search", "v1", searchV1, gomcp.Version("1.0"))
s.ToolFunc("search", "v2 with embeddings", searchV2, gomcp.Version("2.0"))
// "search" → latest, "[email protected]" → exact version
s.AsyncTool("report", "Generate report", func(ctx *gomcp.Context) (*gomcp.CallToolResult, error) {
// long-running work
return ctx.Text("done"), nil
})
// Client gets taskId immediately, polls tasks/get, can tasks/cancel
s.LoadDir("./tools/", gomcp.DirOptions{Watch: true})
YAML tool file shape:
name: search
description: Full-text document search
version: "1.0" # OPTIONAL — non-empty renames the tool to "[email protected]"
method: GET
handler: https://example.com/search
params:
- {name: query, type: string, required: true, description: Query text}
Heads up —
versionrenames the tool. A non-emptyversionfield makes the Provider register the tool asname@version(e.g.[email protected]), following the same convention as gomcp.Version(). That is the name clients must pass totools/call, and the name that shows up intools/list. Drop theversionfield entirely if you want an unversioned tool called plainsearch.
Server.Close() — When your process or test tears down a server that uses LoadDir(..., Watch: true) or long-lived HTTP, call Close() once. It stops the YAML watch loop, the session eviction background goroutine, and the async task manager eviction loop. The call is idempotent.Mcp-Session-Id header (see Streamable HTTP). Sessions idle for 30 minutes (no access via that ID) are removed; the next request with the same ID gets a new empty session. Do not rely on Session storage across long idle periods unless the client keeps traffic or you refresh state yourself.SetMaxConcurrentTasks(n) — Set this before the first AsyncTool / AsyncToolFunc. After the internal task manager is created, later calls are a no-op (avoids races with in-flight work).Close() does not wait for async tool handlers that are still running; add your own timeout / wait if you need hard guarantees before exit.s.Dev(":9090") // http://localhost:9090 — browse and test all tools
func TestSearch(t *testing.T) {
c := mcptest.NewClient(t, setupServer())
c.Initialize()
result := c.CallTool("search", map[string]any{"query": "golang"})
result.AssertNoError(t)
result.AssertContains(t, "golang")
mcptest.MatchSnapshot(t, "search_result", result)
}
s.Stdio() // Claude Desktop, Cursor, Kiro
s.HTTP(":8080") // Remote deployment with SSE
s.Handler() // Embed in existing HTTP server
// Browser clients: wrap the handler with transport.WrapCORS(h, []string{"https://your.app"}) when needed
{
"mcpServers": {
"my-server": {
"command": "/path/to/your/binary"
}
}
}
Works with Claude Desktop, Cursor, Kiro, Windsurf, VS Code Copilot, and any MCP-compatible client.
💡 Recommended reading: How To Ask Questions The Smart Way
Use middleware runs for every JSON-RPC method (except the notification notifications/initialized, which has no response): initialize, tools/list, tools/call, resources/read, prompts/get, tasks/*, completion/complete, etc. Use BearerAuth / APIKeyAuth / BasicAuth when exposing POST /mcp. For APIKeyAuth, api_key (and other merged params) come from tools/call arguments, prompts/get arguments, or resources/read params JSON when no header is sent—prefer headers for production.BearerAuthSkipHandshake / APIKeyAuthSkipHandshake / BasicAuthSkipHandshake (or SkipAuthForMCPMethods) let initialize and ping run without credentials while keeping other methods protected—typical for MCP HTTP clients that negotiate before sending tokens.Authorization, and injected headers from Streamable HTTP).GET /mcp) does not execute MCP middleware. Use WithSSEAuth with SSEBearerAuth, SSEAPIKeyAuth, SSEBasicAuth, or your own gate. Without WithSSEAuth, any client that can open GET receives broadcast notifications.fetch: wrap your /mcp handler with transport.WrapCORS(h, allowedOrigins) from github.com/zhangpanda/gomcp/transport; never use * with credentials—only list trusted origins.When deploying Streamable HTTP in production, combine TLS, authentication middleware on POST, and WithSSEAuth when notifications must not be public.
To report security vulnerabilities, see SECURITY.md.
Copyright © 2026 GoMCP Contributors
Licensed under the Apache License 2.0.
Certain features of this framework (struct-tag schema generation, HTTP-to-MCP automatic adapter, OpenAPI-to-MCP automatic adapter) are the subject of pending patent applications. The Apache 2.0 license grants you a perpetual, worldwide, royalty-free patent license to use these features as part of this software.
If you find GoMCP useful, please consider giving it a star! It helps others discover the project.
Выполни в терминале:
claude mcp add gomcp -- npx CSA PROJECT - FZCO © 2026 IFZA Business Park, DDP, Premises Number 31174 - 001
Безопасность
Низкий рискАвтоматическая эвристика по публичным данным — не гарантия безопасности.