mcp-go

repository·main·Indexed 27 days ago

https://github.com/mark3labs/mcp-go

A Go implementation of the Model Context Protocol (MCP) that enables developers to build servers exposing data (Resources), functionality (Tools), and interaction patterns (Prompts) to LLM applications. It supports standard input/output communication, task-augmented tools for long-running operations, and OpenTelemetry tracing for both servers and clients.

Tokens
102.6K
Snippets
221
Records
294
Agent score
93%

What's inside mcp-go

  1. Compare MCP-Go transport options

    main

    MCP-Go provides four transport methods depending on your deployment needs. Choose based on your use case, performance requirements, and whether you need sampling support:

    TransportUse CaseProsConsSampling Support
    STDIOCLI tools, desktop appsSimple, secure, no networkSingle client, local only✅ Full support
    SSEWeb apps, real-timeMulti-client, real-time, web-friendlyHTTP overhead, one-way streaming❌ Not supported
    StreamableHTTPWeb services, APIsStandard protocol, caching, load balancingNo real-time, more complex❌ Not supported
    In-ProcessEmbedded, testingNo serialization, fastestSame process only✅ Full support
  2. Choose the right MCP client transport

    main

    MCP-Go provides several client implementations optimized for different scenarios. Choose based on your requirements for connection type, real-time capabilities, and client scaling:

    TransportBest ForConnectionReal-timeMulti-client
    STDIOCLI tools, desktop appsProcess pipesNoNo
    StreamableHTTPWeb services, APIsHTTP requestsNoYes
    SSEWeb apps, real-timeHTTP + EventSourceYesYes
    In-ProcessTesting, embeddedDirect callsYesNo
  3. Quickstart: Create and use an MCP client

    main

    This example demonstrates the full lifecycle of an MCP client: creating a connection via STDIO, initializing the connection, discovering capabilities (tools and resources), and executing operations like calling a tool and reading a resource.

    package main
    
    import (
        "context"
        "fmt"
        "log"
    
        "github.com/mark3labs/mcp-go/client"
        "github.com/mark3labs/mcp-go/mcp"
    )
    
    func main() {
        // Create STDIO client
        c, err := client.NewStdioMCPClient(
            "go", []string{} , "run", "/path/to/server/main.go",
        )
        if err != nil {
            log.Fatal(err)
        }
        defer c.Close()
    
        ctx := context.Background()
    
        // Initialize the connection
        if err := c.Initialize(ctx, initRequest); err != nil {
            log.Fatal(err)
        }
    
        // Discover available capabilities
        if err := demonstrateClientOperations(ctx, c); err != nil {
            log.Fatal(err)
        }
    }
    
    func demonstrateClientOperations(ctx context.Context, c client.Client) error {
        // List available tools
        tools, err := c.ListTools(ctx, mcp.ListToolsRequest{})
        if err != nil {
            return fmt.Errorf("failed to list tools: %w", err)
        }
    
        fmt.Printf("Available tools: %d\n", len(tools.Tools))
        for _, tool := range tools.Tools {
            fmt.Printf("- %s: %s\n", tool.Name, tool.Description)
        }
    
        // List available resources
        resources, err := c.ListResources(ctx, mcp.ListResourcesRequest{})
        if err != nil {
            return fmt.Errorf("failed to list resources: %w", err)
        }
    
        fmt.Printf("\nAvailable resources: %d\n", len(resources.Resources))
        for _, resource := range resources.Resources {
            fmt.Printf("- %s: %s\n", resource.URI, resource.Name)
        }
    
        // Call a tool if available
        if len(tools.Tools) > 0 {
            tool := tools.Tools[0]
            fmt.Printf("\nCalling tool: %s\n", tool.Name)
    n
            result, err := c.CallTool(ctx, mcp.CallToolRequest{
                Params: mcp.CallToolRequestParams{
                    Name: tool.Name,
                    Arguments: map[string]interface{}{
                        "input": "example input",
                        "format": "text",
                    },
                },
            })
            if err != nil {
                return fmt.Errorf("tool call failed: %w", err)
            }
    
            fmt.Printf("Tool result: %+v\n", result)
        }
    
        // Read a resource if available
        if len(resources.Resources) > 0 {
            resource := resources.Resources[0]
            fmt.Printf("\nReading resource: %s\n", resource.URI)
    
            content, err := c.ReadResource(ctx, mcp.ReadResourceRequest{
                Params: mcp.ReadResourceRequestParams{
                    URI: resource.URI,
                },
            })
            if err != nil {
                return fmt.Errorf("resource read failed: %w", err)
            }
    
            fmt.Printf("Resource content: %+v\n", content)
        }
    
        return nil
    }
  4. Implement synchronous Tools

    main

    Traditional synchronous tools execute immediately and return results. Use s.AddTool to register them. Tools should validate inputs, handle errors gracefully, and return structured responses using types like mcp.FormatNumberResult, mcp.NewToolResultText, or mcp.NewToolResultError.

    calculatorTool := mcp.NewTool("calculate",
        mcp.WithDescription("Perform basic arithmetic calculations"),
        mcp.WithString("operation",
            mcp.Required(),
            mcp.Description("The arithmetic operation to perform"),
            mcp.Enum("add", "subtract", "multiply", "divide"),
        ),
        mcp.WithNumber("x", mcp.Required(), mcp.Description("First number")),
        mcp.WithNumber("y", mcp.Required(), mcp.Description("Second number")),
    )
    
    s.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
        args := request.GetArguments()
        op := args["operation"].(string)
        x := args["x"].(float64)
        y := args["y"].(float64)
    
        var result float64
        switch op {
        case "add":
            result = x + y
        case "subtract":
            result = x - y
        case "multiply":
            result = x * y
        case "divide":
            if y == 0 {
                return mcp.NewToolResultError("cannot divide by zero"), nil
            }
            result = x / y
        }
        
        return mcp.FormatNumberResult(result), nil
    })
  5. Integrate MCP into an Embedded Application

    main

    To embed MCP functionality within a larger application, encapsulate the MCPServer and InProcessClient within your application's struct. This allows your application to expose its own internal state or configuration via MCP tools while simultaneously using the MCP client to call other tools (even those hosted on the same server) for internal processing.

    Key steps:

    1. Define an application struct containing *server.MCPServer and *client.InProcessClient.
    2. Initialize the server and client in your constructor.
    3. Register tools that interact with your application's internal state (e.g., get_app_status or update_config).
    4. Use the client within application methods to invoke tools via app.mcpClient.CallTool.
    // Embedded MCP server in a larger application
    type Application struct {
        mcpServer *server.MCPServer
        mcpClient *client.InProcessClient
        config    *Config
    }
    
    func NewApplication(config *Config) *Application {
        app := &Application{
            config: config,
        }
    
        // Create embedded MCP server
        app.mcpServer = server.NewMCPServer("Embedded Server", "1.0.0",
            server.WithToolCapabilities(true),
        )
    
        // Add application-specific tools
        app.addApplicationTools()
    
        // Create in-process client for internal use
        var err error
        app.mcpClient, err = client.NewInProcessClient(app.mcpServer)
        if err != nil {
            panic(err)
        }
    
        return app
    }
    
    type Config struct {
        AppName string
        Debug   bool
    }
    
    func (app *Application) addApplicationTools() {
        // Application status tool
        app.mcpServer.AddTool(
            mcp.NewTool("get_app_status",
                mcp.WithDescription("Get current application status"),
            ),
            func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
                return mcp.NewToolResultText(fmt.Sprintf(`{"app_name":"%s","debug":%t,"status":"running"}`, 
                    app.config.AppName, app.config.Debug)), nil
            },
        )
    
        // Configuration tool
        app.mcpServer.AddTool(
            mcp.NewTool("update_config",
                mcp.WithDescription("Update application configuration"),
                mcp.WithString("key", mcp.Required()),
                mcp.WithString("value", mcp.Required()),
            ),
            func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
                key := req.GetString("key", "")
                value := req.GetString("value", "")
    
                // Update configuration based on key
                switch key {
                case "debug":
                    app.config.Debug = value == "true"
                case "app_name":
                    app.config.AppName = value
                default:
                    return mcp.NewToolResultError(fmt.Sprintf("unknown config key: %s", key)), nil
                }
    
                return mcp.NewToolResultText(fmt.Sprintf("Updated %s to %s", key, value)), nil
            },
        )
    }
    
    func (app *Application) ProcessWithMCP(ctx context.Context, operation string) (interface{}, error) {
        // Use MCP tools internally for processing
        result, err := app.mcpClient.CallTool(ctx, mcp.CallToolRequest{
            Params: mcp.CallToolParams{
                Name: "calculate",
                Arguments: map[string]interface{}{
                    "operation": operation,
                    "x":         10.0,
                    "y":         5.0,
                },
            },
        })
        if err != nil {
            return nil, err
        }
    
        // Extract text from the first content item
        if len(result.Content) > 0 {
            if textContent, ok := mcp.AsTextContent(result.Content[0]); ok {
                return textContent.Text, nil
            }
        }
        
        return "no result", nil
    }
    
    // Usage example
    func main() {
        config := &Config{
            AppName: "My App",
            Debug:   true,
        }
    
        app := NewApplication(config)
        ctx := context.Background()
    
        // Initialize the embedded MCP client
        _, err := app.mcpClient.Initialize(ctx, mcp.InitializeRequest{
            Params: mcp.InitializeRequestParams{
                ProtocolVersion: "2024-11-05",
                Capabilities: mcp.ClientCapabilities{
                    Tools: &mcp.ToolsCapability{},
                },
                ClientInfo: mcp.Implementation{
                    Name:    "embedded-client",
                    Version: "1.0.0",
                },
            },
        })
        if err != nil {
            log.Fatal(err)
        }
    
        // Use MCP functionality within the application
        result, err := app.ProcessWithMCP(ctx, "add")
        if err != nil {
            log.Fatal(err)
        }
    
        fmt.Printf("Application result: %v\n", result)
    }
  6. Implement Typed Tools with Automatic Validation

    main

    Typed tools provide compile-time type safety and automatic parameter validation using Go structs. By using mcp.NewTypedToolHandler, you can pass a handler function that accepts a specific input struct instead of manually parsing mcp.CallToolRequest arguments.

    To use typed tools:

    1. Define input and output structs with appropriate json tags.
    2. Use validate struct tags (compatible with go-playground/validator) for automatic validation.
    3. Register the tool using s.AddTool(tool, mcp.NewTypedToolHandler(handlerFunc)).
    4. Ensure the server is initialized with server.WithToolCapabilities(true).
    type CalculateInput struct {
        Operation string  `json:"operation" validate:"required,oneof=add subtract multiply divide"`
        X         float64 `json:"x" validate:"required"`
        Y         float64 `json:"y" validate:"required"`
    }
    
    type CalculateOutput struct {
        Result    float64 `json:"result"`
        Operation string  `json:"operation"`
    }
    
    // Handler signature for typed tools
    func handleCalculateTyped(ctx context.Context, req mcp.CallToolRequest, input CalculateInput) (*mcp.CallToolResult, error) {
        // ... logic ...
        return mcp.NewToolResultText(string(jsonData)), nil
    }
    
    // Registration
    s.AddTool(tool, mcp.NewTypedToolHandler(handleCalculateTyped))
  7. Integrate STDIO MCP Server with Claude Desktop

    main

    To use your MCP server with Claude Desktop, add it to your local configuration file. The configuration defines the command to run the server and any necessary environment variables.

    Configuration Paths:

    • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
    • Windows: %APPDATA%\Claude\claude_desktop_config.json
    {
      "mcpServers": {
        "file-tools": {
          "command": "go",
          "args": ["run", "/path/to/your/server/main.go"],
          "env": {
            "LOG_LEVEL": "info"
          }
        }
      }
    }
  8. Embed StreamableHTTPServer in non-net/http frameworks

    main

    If you are using a web framework that does not implement the standard net/http interface (such as fasthttp or fiber), use the Handle method instead of ServeHTTP. This allows you to embed MCP directly into your framework's request lifecycle without needing a buffering adaptor, enabling native chunked SSE (Server-Sent Events) streaming.

    Choosing an entry point

    SituationUse
    Standalone server, mounted in http.ServeMux, chi, gorilla/mux, etc.ServeHTTP (or srv.Start(":8080"))
    Mounted in a fiber/echo/gin app via adaptor.HTTPHandler and you don't need streamingServeHTTP
    Mounted in fasthttp/fiber and you want native chunked SSE without an adaptor in the pathHandle
    Custom transport (test harness, in-memory dispatcher, alternative wire format)Handle

    :::tip If you only need request/response (no SSE notifications and no GET listening connection), using adaptor.HTTPHandler with a standard ServeHTTP implementation is the simplest supported path. :::

    mcpServer.Handle(w, r)
  9. Select a Client Transport

    main

    Choose a transport based on your application's requirements for networking, performance, and concurrency. Use the following logic to decide:

    • inprocess: Use when NetworkRequired is false and Performance is high.
    • stdio: Use when NetworkRequired is false and MultiClient is false (or as a default fallback).
    • sse: Use when RealTime is true and MultiClient is true.
    • streamablehttp: Use when NetworkRequired is true and MultiClient is true.
    type TransportRequirements struct {
        RealTime        bool
        MultiClient     bool
        NetworkRequired bool
        Performance     string // "high", "medium", "low"
        Complexity      string // "low", "medium", "high"
    }
    
    func SelectTransport(req TransportRequirements) string {
        switch {
        case !req.NetworkRequired && req.Performance == "high":
            return "inprocess"
    
        case !req.NetworkRequired && !req.MultiClient:
            return "stdio"
    
        case req.RealTime && req.MultiClient:
            return "sse"
    
        case req.NetworkRequired && req.MultiClient:
            return "streamablehttp"
    
        default:
            return "stdio" // Default fallback
        }
    }
  10. Implement role-based system prompts

    main

    You can dynamically construct a system message based on user arguments to define the AI's persona or expertise. This allows a single prompt handler to serve different professional roles (e.g., software_engineer, data_scientist) by injecting specific instructions into the system role of the mcp.GetPromptResult.

    // Example of setting a system role based on expertise argument
    var systemMessage string
    switch expertise {
    case "software_engineer":
        systemMessage = "You are an experienced software engineer..."
    case "data_scientist":
        systemMessage = "You are a data scientist..."
    default:
        systemMessage = fmt.Sprintf("You are an expert in %s.", expertise)
    }
    
    messages := []mcp.PromptMessage{
        {
            Role: "system",
            Content: mcp.NewTextContent(systemMessage),
        },
    }
  11. Implement graceful shutdown for clients

    main

    To ensure a client shuts down cleanly, especially when initializing in the background, use a pattern that manages a context.CancelFunc and a done channel. This allows you to wait for initialization to complete or time out before calling client.Close().

    // Example of a managed client pattern for graceful shutdown
    type ManagedClient struct {
        client client.Client
        ctx    context.Context
        cancel context.CancelFunc
        done   chan struct{}
    }
    
    func (mc *ManagedClient) Close() error {
        mc.cancel() // Signal cancellation to background tasks
        
        // Wait for initialization to complete or timeout
        select {
        case <-mc.done:
        case <-time.After(5 * time.Second):
            log.Println("Timeout waiting for client shutdown")
        }
    
        return mc.client.Close()
    }