go-mcp

repository·main·Indexed 20 days ago

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

A high-performance, type-safe Go implementation of the Model Context Protocol (MCP) for communication between AI applications and external systems. It features a three-layer architecture consisting of a Transport Layer (supporting stdio and SSE), a Protocol Layer for MCP definitions, and a User Layer for client and server implementations. The SDK supports full MCP capabilities including prompts, resources, tools, sampling, and logging.

Tokens
9.3K
Snippets
19
Records
24
Agent score
69%

What's inside go-mcp

  1. Project Structure and Module Organization

    main

    The SDK is organized into the following packages to help you locate specific functionality:

    • transports/: Contains transport implementations and the core interface.
      • stdio_client.go, stdio_server.go (Standard Input/Output)
      • sse_client.go, sse_server.go (Server-Sent Events)
      • transport.go (Interface definition)
    • protocol/: The core of the MCP logic. Contains definitions for all protocol messages (e.g., tools.go, resources.go, prompts.go, jsonrpc.go, etc.).
    • server/: High-level server implementation. Includes logic for sending/receiving messages and handling client requests (server.go, call.go, handle.go, etc.).
    • client/: High-level client implementation. Includes logic for calling the server and handling server responses (client.go, call.go, handle.go, etc.).
    • pkg/: Shared utilities like errors.go and log.go.
  2. Understand the MCP Go SDK architecture

    main

    The MCP Go SDK is designed with a three-layer abstraction to decouple communication from protocol logic:

    1. Transport Layer (transports/): Handles the raw communication medium. It is decoupled from the protocol via the transport interface. Supported implementations include stdio (Standard I/O) and sse (Server-Sent Events).
    2. Protocol Layer (protocol/): Contains all definitions related to the Model Context Protocol (MCP). This includes data structures, request construction, and response parsing for all MCP features (e.g., prompts, resources, tools, sampling).
    3. User Layer (server/ and client/): The high-level interface used by developers. Both the server and client layers implement send and receive capabilities, allowing them to handle requests, notifications, and responses. While the underlying mechanism uses asynchronous routing, the user layer provides a synchronous experience (request $\rightarrow$ process $\rightarrow$ return).
  3. How Go-MCP architecture works

    main

    Go-MCP uses a three-layer architecture to separate concerns and allow for easy extensibility:

    1. Transport Layer: Handles the underlying communication protocols. Supported methods include:
      • HTTP SSE/POST: Uses HTTP for server-to-client pushes (SSE) and client-to-server requests (POST). Ideal for web environments.
      • Streamable HTTP: Supports both stateless and stateful HTTP requests. Stateful mode uses SSE to stream multiple messages, enabling server-to-client notifications.
      • Stdio: Uses standard input/output streams, suitable for local process-to-process communication.
    2. Protocol Layer: Responsible for the encoding/decoding of the MCP protocol and defining the data structures.
    3. User Layer: Provides the high-level, developer-friendly API for building clients and servers.

    The Transport Layer uses a unified interface, making it easy to add new transport methods (like WebSocket or gRPC) without affecting the upper layers.

  4. Create an MCP Client with SSE Transport

    main

    To implement an MCP client, use transport.NewSSEClientTransport to establish a connection to an SSE endpoint, then initialize the client using client.NewClient. You can then interact with the server, for example, by calling ListTools to retrieve available tools.

    package main
    
    import (
      "context"
      "log"
    
      "github.com/ThinkInAIXYZ/go-mcp/client"
      "github.com/ThinkInAIXYZ/go-mcp/transport"
    )
    
    func main() {
      // Create SSE transport client
      transportClient, err := transport.NewSSEClientTransport("http://127.0.0.1:8080/sse")
      if err != nil {
        log.Fatalf("Failed to create transport client: %v", err)
      }
    
      // Initialize MCP client
      mcpClient, err := client.NewClient(transportClient)
      if err != nil {
        log.Fatalf("Failed to create MCP client: %v", err)
      }
      defer mcpClient.Close()
    
      // Get available tools list
      tools, err := mcpClient.ListTools(context.Background())
      if err != nil {
        log.Fatalf("Failed to get tools list: %v", err)
      }
      log.Printf("Available tools: %+v", tools)
    }
  5. Create an MCP Client using SSE

    main

    To implement an MCP client, use transport.NewSSEClientTransport to establish a connection via Server-Sent Events (SSE) and client.NewClient to initialize the MCP client. You can then call methods like ListTools to interact with the server.

    package main
    
    import (
    	"context"
    	"log"
    
    	"github.com/ThinkInAIXYZ/go-mcp/client"
    	"github.com/ThinkInAIXYZ/go-mcp/transport"
    )
    
    func main() {
    	// Create SSE transport client
    	transportClient, err := transport.NewSSEClientTransport("http://127.0.0.1:8080/sse")
    	if err != nil {
    		log.Fatalf("Failed to create transport client: %v", err)
    	}
    
    	// Initialize MCP client
    	mcpClient, err := client.NewClient(transportClient)
    	if err != nil {
    		log.Fatalf("Failed to create MCP client: %v", err)
    	}
    	defer mcpClient.Close()
    
    	// Get available tools list
    	tools, err := mcpClient.ListTools(context.Background())
    	if err != nil {
    		log.Fatalf("Failed to get tools list: %v", err)
    	}
    	log.Printf("Available tools: %+v", tools)
    }
  6. Create an MCP Server and Register Tools

    main

    To build an MCP server:

    1. Create a transport server using transport.NewSSEServerTransport.
    2. Initialize the MCP server with server.NewServer.
    3. Define a tool using protocol.NewTool, specifying its name, description, and an input schema struct (using field tags for metadata).
    4. Register the tool with mcpServer.RegisterTool and a handler function.
    5. Start the server with mcpServer.Run().

    In the handler, use protocol.VerifyAndUnmarshal to parse the RawArguments into your input struct, and return a *protocol.CallToolResult containing the response content.

    package main
    
    import (
      "context"
      "fmt"
      "log"
      "time"
    
      "github.com/ThinkInAIXYZ/go-mcp/protocol"
      "github.com/ThinkInAIXYZ/go-mcp/server"
      "github.com/ThinkInAIXYZ/go-mcp/transport"
    )
    
    type TimeRequest struct {
      Timezone string `json:"timezone" description:"Timezone" required:"true"` 
    }
    
    func main() {
      transportServer, err := transport.NewSSEServerTransport("127.0.0.1:8080")
      if err != nil {
        log.Fatalf("Failed to create transport server: %v", err)
      }
    
      mcpServer, err := server.NewServer(transportServer)
      if err != nil {
        log.Fatalf("Failed to create MCP server: %v", err)
      }
    
      tool, err := protocol.NewTool("current_time", "Get current time for a specific timezone", TimeRequest{})
      if err != nil {
        log.Fatalf("Failed to create tool: %v", err)
        return
      }
      mcpServer.RegisterTool(tool, handleTimeRequest)
    
      if err = mcpServer.Run(); err != nil {
        log.Fatalf("Failed to start server: %v", err)
      }
    }
    
    func handleTimeRequest(ctx context.Context, req *protocol.CallToolRequest) (*protocol.CallToolResult, error) {
      var timeReq TimeRequest
      if err := protocol.VerifyAndUnmarshal(req.RawArguments, &timeReq); err != nil {
        return nil, err
      }
    
      loc, err := time.LoadLocation(timeReq.Timezone)
      if err != nil {
        return nil, fmt.Errorf("invalid timezone: %v", err)
      }
    
      return &protocol.CallToolResult{
        Content: []protocol.Content{
          &protocol.TextContent{
            Type: "text",
            Text: time.Now().In(loc).String(),
          },
        },
      }, nil
    }
  7. Implement an MCP Client

    main

    To create an MCP client, you need to initialize a transport layer (e.g., SSE) and then wrap it with a new MCP client. This allows you to interact with MCP servers, such as listing available tools.

    Key steps:

    1. Create a transport client using transport.NewSSEClientTransport(url).
    2. Initialize the MCP client using client.NewClient(transportClient).
    3. Use methods like ListTools(ctx) to interact with the server.
    package main
    
    import (
    	"context"
    	"log"
    
    	"github.com/ThinkInAIXYZ/go-mcp/client"
    	"github.com/ThinkInAIXYZ/go-mcp/transport"
    )
    
    func main() {
    	// Create SSE transport client
    	transportClient, err := transport.NewSSEClientTransport("http://127.0.0.1:8080/sse")
    	if err != nil {
    		log.Fatalf("Failed to create transport client: %v", err)
    	}
    
    	// Initialize MCP client
    	mcpClient, err := client.NewClient(transportClient)
    	if err != nil {
    		log.Fatalf("Failed to create MCP client: %v", err)
    	}
    	defer mcpClient.Close()
    
    	// Get available tools
    	tools, err := mcpClient.ListTools(context.Background())
    	if err != nil {
    		log.Fatalf("Failed to list tools: %v", err)
    	}
    	log.Printf("Available tools: %+v", tools)
    }
  8. Integrate Go-MCP with Gin Framework

    main

    You can integrate Go-MCP into a Gin web server by using transport.NewSSEServerTransportAndHandler(messageEndpointURL). This provides an http.Handler that you can mount to specific Gin routes.

    Integration pattern:

    1. Call transport.NewSSEServerTransportAndHandler("/message") to get the transport and the handler.
    2. Create the mcpServer using the transport.
    3. Run the mcpServer.Run() in a goroutine.
    4. In Gin, mount the SSE handler to a GET route and the message handler to a POST route using the provided mcpHandler.HandleSSE() and mcpHandler.HandleMessage() methods.
    package main
    
    import (
    	"context"
    	"log"
    
    	"github.com/ThinkInAIXYZ/go-mcp/protocol"
    	"github.com/ThinkInAIXYZ/go-mcp/server"
    	"github.com/ThinkInAIXYZ/go-mcp/transport"
    	"github.com/gin-gonic/gin"
    )
    
    func main() {
    	messageEndpointURL := "/message"
    
    	sseTransport, mcpHandler, err := transport.NewSSEServerTransportAndHandler(messageEndpointURL)
    	if err != nil {
    		log.Panicf("new sse transport and hander with error: %v", err)
    	}
    
    	// Create mcp server
    	mcpServer, _ := server.NewServer(sseTransport)
    
    	// Run mcp Server
    	go func() {
    		mcpServer.Run()
    	}()
    
    	defer mcpServer.Shutdown(context.Background())
    
    	r := gin.Default()
    	r.GET("/sse", func(ctx *gin.Context) {
    		mcpHandler.HandleSSE().ServeHTTP(ctx.Writer, ctx.Request)
    	})
    	r.POST(messageEndpointURL, func(ctx *gin.Context) {
    		mcpHandler.HandleMessage().ServeHTTP(ctx.Writer, ctx.Request)
    	})
    
    	if err = r.Run(":8080"); err != nil {
    		return
    	}
    }
  9. Integrate Go-MCP with Gin Web Framework

    main

    To integrate MCP into a Gin application, use transport.NewSSEServerTransportAndHandler to get both the transport and the http.Handler components. You then map the SSE and Message endpoints to Gin routes:

    • GET /sse: Use mcpHandler.HandleSSE().ServeHTTP(ctx.Writer, ctx.Request)
    • POST /message: Use mcpHandler.HandleMessage().ServeHTTP(ctx.Writer, ctx.Request)
    package main
    
    import (
      "context"
      "log"
    
      "github.com/ThinkInAIXYZ/go-mcp/server"
      "github.com/ThinkInAIXYZ/go-mcp/transport"
      "github.com/gin-gonic/gin"
    )
    
    func main() {
      messageEndpointURL := "/message"
    
      sseTransport, mcpHandler, err := transport.NewSSEServerTransportAndHandler(messageEndpointURL)
      if err != nil {
        log.Panicf("Failed to create SSE transport and handler: %v", err)
      }
    
      mcpServer, _ := server.NewServer(sseTransport)
    
      go func() {
        mcpServer.Run()
      }()
    
      defer mcpServer.Shutdown(context.Background())
    
      r := gin.Default()
      r.GET("/sse", func(ctx *gin.Context) {
        mcpHandler.HandleSSE().ServeHTTP(ctx.Writer, ctx.Request)
      })
      r.POST(messageEndpointURL, func(ctx *gin.Context) {
        mcpHandler.HandleMessage().ServeHTTP(ctx.Writer, ctx.Request)
      })
    
      if err = r.Run(":8080"); err != nil {
        return
      }
    }
  10. Create an MCP Server with Tools

    main

    To build an MCP server:

    1. Create an SSE transport server using transport.NewSSEServerTransport.
    2. Initialize the server with server.NewServer.
    3. Define a tool using protocol.NewTool, passing a struct to define the input schema via field tags.
    4. Register the tool with mcpServer.RegisterTool and a handler function.
    5. Start the server with mcpServer.Run().

    In the handler, use protocol.VerifyAndUnmarshal to parse the req.RawArguments into your input struct.

    package main
    
    import (
    	"context"
    	"fmt"
    	"log"
    	"time"
    
    	"github.com/ThinkInAIXYZ/go-mcp/protocol"
    	"github.com/ThinkInAIXYZ/go-mcp/server"
    	"github.com/ThinkInAIXYZ/go-mcp/transport"
    )
    
    type TimeRequest struct {
    	Timezone string `json:"timezone" description:"Timezone" required:"true"` // Use field tags for inputschema
    }
    
    func main() {
    	// Create SSE transport server
    	transportServer, err := transport.NewSSEServerTransport("127.0.0.1:8080")
    	if err != nil {
    		log.Fatalf("Failed to create transport server: %v", err)
    	}
    
    	// Initialize MCP server
    	mcpServer, err := server.NewServer(transportServer)
    	if err != nil {
    		log.Fatalf("Failed to create MCP server: %v", err)
    	}
    
    	// Register time query tool
    
    tool, err := protocol.NewTool("current_time", "Get current time for a specific timezone", TimeRequest{})
    	if err != nil {
    		log.Fatalf("Failed to create tool: %v", err)
    		return
    	}
    	mcpServer.RegisterTool(tool, handleTimeRequest)
    
    	// Start server
    	if err = mcpServer.Run(); err != nil {
    		log.Fatalf("Server run failed: %v", err)
    	}
    }
    
    func handleTimeRequest(ctx context.Context, req *protocol.CallToolRequest) (*protocol.CallToolResult, error) {
    	var timeReq TimeRequest
    	if err := protocol.VerifyAndUnmarshal(req.RawArguments, &timeReq); err != nil {
    		return nil, err
    	}
    
    	timezone, err := time.LoadLocation(timeReq.Timezone)
    	if err != nil {
    		return nil, fmt.Errorf("invalid timezone: %v", err)
    	}
    
    	return &protocol.CallToolResult{
    		Content: []protocol.Content{
    			&protocol.TextContent{
    				Type: "text",
    				Text: time.Now().In(timezone).String(),
    			},
    		},
    	}, nil
    }
  11. Implement an MCP Server

    main

    To build an MCP server, you define tools and register them with the server instance. The server uses a transport layer to communicate with clients.

    Key steps:

    1. Create a transport server using transport.NewSSEServerTransport(address).
    2. Initialize the MCP server using server.NewServer(transportServer).
    3. Define a tool using protocol.NewTool(name, description, inputSchema) where the input schema is a struct with JSON tags.
    4. Register the tool with mcpServer.RegisterTool(tool, handler).
    5. Start the server with mcpServer.Run().

    To handle tool requests, use protocol.VerifyAndUnmarshal(req.RawArguments, &targetStruct) to parse the incoming arguments into your defined schema struct.

    package main
    
    import (
    	"context"
    	"fmt"
    	"log"
    	"time"
    
    	"github.com/ThinkInAIXYZ/go-mcp/protocol"
    	"github.com/ThinkInAIXYZ/go-mcp/server"
    	"github.com/ThinkInAIXYZ/go-mcp/transport"
    )
    
    type TimeRequest struct {
    	Timezone string `json:"timezone" description:"timezone" required:"true"` // Use field tag to describe input schema
    }
    
    func main() {
    	// Create SSE transport server
    	transportServer, err := transport.NewSSEServerTransport("127.0.0.1:8080")
    	if err != nil {
    		log.Fatalf("Failed to create transport server: %v", err)
    	}
    
    	// Initialize MCP server
    	mcpServer, err := server.NewServer(transportServer)
    	if err != nil {
    		log.Fatalf("Failed to create MCP server: %v", err)
    	}
    
    	// Register time query tool
    	tool, err := protocol.NewTool("current_time", "Get current time for specified timezone", TimeRequest{})
    	if err != nil {
    		log.Fatalf("Failed to create tool: %v", err)
    	}
    	mcpServer.RegisterTool(tool, handleTimeRequest)
    
    	// Start server
    	if err = mcpServer.Run(); err != nil {
    		log.Fatalf("Server failed to start: %v", err)
    	}
    }
    
    func handleTimeRequest(ctx context.Context, req *protocol.CallToolRequest) (*protocol.CallToolResult, error) {
    	var timeReq TimeRequest
    	if err := protocol.VerifyAndUnmarshal(req.RawArguments, &timeReq); err != nil {
    		return nil, err
    	}
    
    	loc, err := time.LoadLocation(timeReq.Timezone)
    	if err != nil {
    		return nil, fmt.Errorf("invalid timezone: %v", err)
    	}
    
    	return &protocol.CallToolResult{
    		Content: []protocol.Content{
    			&protocol.TextContent{
    				Type: "text",
    				Text: time.Now().In(loc).String(),
    			},
    		},
    	}, nil
    }