MCP Client Manager Go
Package mcpmgr provides a high-level manager for connecting to, monitoring, and coordinating multiple Model Context Protocol (MCP) servers from Go applications. The companion mcpgateway package builds on top of mcpmgr to expose every managed server through a single Streamable HTTP endpoint.
Install / Use
/learn @VikashLoomba/MCP Client Manager GoQuality Score
Category
Development & EngineeringSupported Platforms
README
MCP Client Manager (Go)
mcpmgr is a lightweight orchestration layer around the
modelcontextprotocol/go-sdk
client. It keeps multiple MCP transports (stdio or HTTP) alive, fans out
events, and exposes ergonomic helpers for listing or invoking tools, prompts,
and resources from Go applications. The companion mcpgateway package builds
on top of mcpmgr to expose every managed server through a single Streamable
HTTP endpoint.
Install
go get github.com/vikashloomba/mcp-client-manager-go/pkg/mcpmgr
go get github.com/vikashloomba/mcp-client-manager-go/pkg/mcp-gateway
Initialize with pre-registered servers
package main
import (
"context"
"time"
"github.com/vikashloomba/mcp-client-manager-go/pkg/mcpmgr"
)
func main() {
manager := mcpmgr.NewManager(map[string]mcpmgr.ServerConfig{
"stdio-example": &mcpmgr.StdioServerConfig{
BaseServerConfig: mcpmgr.BaseServerConfig{Timeout: 30 * time.Second},
Command: "npx",
Args: []string{"@modelcontextprotocol/server-everything"},
},
"streamable-example": &mcpmgr.HTTPServerConfig{
BaseServerConfig: mcpmgr.BaseServerConfig{Timeout: 30 * time.Second},
Endpoint: "https://gitmcp.io/modelcontextprotocol/go-sdk",
},
}, &mcpmgr.ManagerOptions{DefaultClientName: "my-app", AutoConnect: true})
ctx := context.Background()
// AutoConnect will dial the transports in the background; ensure you close
// them before exiting.
defer manager.DisconnectAllServers(ctx)
}
Add a server after initialization
ctx := context.Background()
config := &mcpmgr.HTTPServerConfig{
BaseServerConfig: mcpmgr.BaseServerConfig{Timeout: 45 * time.Second},
Endpoint: "https://gitmcp.io/modelcontextprotocol/go-sdk",
}
if _, err := manager.ConnectToServer(ctx, "docs-server", config); err != nil {
panic(err)
}
List and call tools
tools, err := manager.ListTools(ctx, "streamable-example", nil)
if err != nil {
panic(err)
}
for _, tool := range tools.Tools {
println("Tool:", tool.Name)
}
result, err := manager.ExecuteTool(ctx, "streamable-example", "fetch_url_content", map[string]any{
"url": "https://example.com",
})
if err != nil {
panic(err)
}
println("Result:", result.Content)
Read prompts and resources
prompts, err := manager.ListPrompts(ctx, "stdio-example", nil)
if err != nil {
panic(err)
}
for _, prompt := range prompts.Prompts {
println("Prompt:", prompt.Name)
}
resources, err := manager.ListResources(ctx, "stdio-example", nil)
if err != nil {
panic(err)
}
for _, resource := range resources.Resources {
println("Resource:", resource.URI)
}
details, err := manager.ReadResource(ctx, "stdio-example", &mcp.ReadResourceParams{URI: resources.Resources[0].URI})
if err != nil {
panic(err)
}
println("First resource size:", len(details.Resource.Data))
Run a Streamable MCP gateway
The mcpgateway package re-exports every tool, prompt, and resource managed by
mcpmgr through a single Streamable HTTP endpoint so downstream clients only
have to connect once.
package main
import (
"context"
"log"
"time"
mcpgateway "github.com/vikashloomba/mcp-client-manager-go/pkg/mcp-gateway"
"github.com/vikashloomba/mcp-client-manager-go/pkg/mcpmgr"
)
func main() {
manager := mcpmgr.NewManager(map[string]mcpmgr.ServerConfig{
"stdio-example": &mcpmgr.StdioServerConfig{
BaseServerConfig: mcpmgr.BaseServerConfig{Timeout: 30 * time.Second},
Command: "npx",
Args: []string{"@modelcontextprotocol/server-everything"},
},
}, &mcpmgr.ManagerOptions{DefaultClientName: "gateway-example", AutoConnect: true})
gateway, err := mcpgateway.NewGateway(manager, &mcpgateway.Options{Addr: ":8787", Path: "/mcp"})
if err != nil {
log.Fatalf("gateway init failed: %v", err)
}
ctx := context.Background()
defer manager.DisconnectAllServers(ctx)
log.Println("Serving MCP gateway on http://localhost:8787/mcp")
if err := gateway.ListenAndServe(ctx); err != nil {
log.Fatalf("gateway stopped: %v", err)
}
}
Check cmd/gateway-example for a runnable sample and the package docs under
pkg/mcp-gateway for customization options like namespace strategies,
notification hooks, progress fan-out, and elicitation bridging.
Inspect and serialize server configs
GetServerSummaries() returns a slice of summaries where Config is a
ServerConfig interface implemented by *StdioServerConfig or
*HTTPServerConfig. To avoid type switches at every call site, use the helper
guards and narrowers:
summaries := manager.GetServerSummaries()
for _, s := range summaries {
switch mcpmgr.TransportOf(s.Config) {
case mcpmgr.TransportStdio:
if cfg, ok := mcpmgr.AsStdio(s.Config); ok {
// Use cfg.Command, cfg.Args, cfg.Env, etc.
}
case mcpmgr.TransportHTTP:
if cfg, ok := mcpmgr.AsHTTP(s.Config); ok {
// Use cfg.Endpoint, cfg.MaxRetries, cfg.PreferSSE, etc.
}
}
}
Note: BaseServerConfig contains function fields (e.g., OnError, RPCLogger)
which encoding/json cannot marshal. When building an API that returns
summaries as JSON, construct a JSON‑safe DTO instead of marshaling the config
directly. For example:
type serverSummaryDTO struct {
ID string `json:"id"`
Status mcpmgr.ConnectionStatus `json:"status"`
Config map[string]any `json:"config"`
}
func buildSummaryDTOs(m *mcpmgr.Manager) ([]serverSummaryDTO, error) {
sums := m.GetServerSummaries()
out := make([]serverSummaryDTO, 0, len(sums))
for _, s := range sums {
dto := serverSummaryDTO{ID: s.ID, Status: s.Status, Config: map[string]any{}}
switch mcpmgr.TransportOf(s.Config) {
case mcpmgr.TransportStdio:
if c, ok := mcpmgr.AsStdio(s.Config); ok {
dto.Config = map[string]any{
"type": "stdio",
"command": c.Command,
"args": c.Args,
"env": c.Env,
"timeoutSeconds": int(c.BaseServerConfig.Timeout / time.Second),
"version": c.BaseServerConfig.Version,
}
}
case mcpmgr.TransportHTTP:
if c, ok := mcpmgr.AsHTTP(s.Config); ok {
dto.Config = map[string]any{
"type": "http",
"endpoint": c.Endpoint,
"maxRetries": c.MaxRetries,
"sessionId": c.SessionID,
"preferSse": c.PreferSSE,
"timeoutSeconds": int(c.BaseServerConfig.Timeout / time.Second),
"version": c.BaseServerConfig.Version,
}
}
}
out = append(out, dto)
}
return out, nil
}
Add custom HTTP routes
If you want to host extra endpoints alongside the MCP gateway (for health
checks, metrics, etc.), call ServeMux() to get the underlying mux and
register routes before starting the server:
gateway, _ := mcpgateway.NewGateway(manager, &mcpgateway.Options{Addr: ":8787", Path: "/mcp"})
// Add custom routes on the same server.
mux := gateway.ServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })
// Start the gateway's server.
_ = gateway.ListenAndServe(context.Background())
Alternatively, you can run your own http.Server and reuse the gateway's
handler and state:
srv := &http.Server{Addr: ":8787"}
// If Handler is nil, ListenAndServeServer will install gateway.Handler().
_ = gateway.ListenAndServeServer(context.Background(), srv)
When bearer-token protection is enabled via Options.TokenVerifier, only the
Streamable MCP endpoint is protected by that middleware. Additional routes you
register are not automatically wrapped; apply your own auth as needed.
UI Roots mirroring
Some MCP servers restrict access to file resources based on the client's UI "roots". The gateway can mirror the UI's root set to every downstream server. Use the new helpers when your UI learns its effective roots:
// Replace all roots at once (diffed and propagated to all downstream servers):
gateway.SetUIRoots([]*mcp.Root{{URI: "file:///workspace", Name: "Workspace"}})
// Or incrementally add/remove roots:
gateway.AddUIRoots(&mcp.Root{URI: "file:///tmp", Name: "Temp"})
gateway.RemoveUIRoots("file:///tmp")
When a new server is attached via gateway.AttachServer, the current cached
roots are pushed to that server's client so it immediately reflects the UI set.
Removing servers cleanly
The gateway now exposes DetachServer and RemoveServer helpers:
// Detach removes a server's tools/prompts/resources from the aggregated view.
_ = gateway.DetachServer(ctx, serverID)
// Remove detaches and then calls manager.RemoveServer to close and delete it.
_ = gateway.RemoveServer(ctx, serverID)
Additionally, mcpmgr.Manager emits a removal event that the gateway subscribes
to; calling manager.RemoveServer(ctx, id) automatically prunes the server's
features from the gateway so they no longer appear to clients.
Progress notifications
Both mcpmgr and the gateway preserve _meta.progressToken values and forward
notifications/progress end-to-end, even when upstream servers emit float
tokens or clients omit a token (the gateway auto-generates one per request).
Downstream consumers only need to register a handler when they connect:
client := mcp.NewClient(
&mcp.Implementation{Name: "ui", Version: "1.0.0"},
&mcp.ClientOptions{
ProgressNotificationHa
