SkillAgentSearch skills...

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 Go

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
View on GitHub
GitHub Stars3
CategoryDevelopment
Updated2mo ago
Forks2

Languages

Go

Security Score

90/100

Audited on Jan 21, 2026

No findings