一文弄懂用Go实现MCP服务:从STDIO到Streamable HTTP的完整实现

什么是MCP?

Model Context Protocol (MCP) 是Anthropic推出的开放标准,旨在为大型语言模型提供统一的外部工具和数据源访问接口。在过去,如果想要让AI处理特定的数据,通常只能依赖于预训练数据或者手动上传数据,这既麻烦又低效。MCP解决了这个问题,它使得AI不再局限于静态知识库,而是能够像人类一样调用搜索引擎、访问本地文件、连接API服务等。

MCP总体架构

MCP的核心是"客户端-服务器"架构,其中MCP客户端可以连接到多个服务器。客户端是指希望通过MCP访问数据的应用程序,如CLI工具、IDE插件或AI应用。

arduino 复制代码
┌─────────────┐    JSON-RPC    ┌─────────────┐
│ MCP Client  │◄──────────────►│ MCP Server  │
│ (AI应用)    │                │ (工具提供者) │
└─────────────┘                └─────────────┘

MCP协议通过JSON-RPC 2.0进行通信,核心概念包括:

  • Tools: 可调用的函数,让AI执行特定操作
  • Resources: 可访问的数据源,如文件、API等
  • Prompts: 模板化的提示词,支持参数化

使用mcp-sdk-go构建MCP服务

要开始使用Go语言构建MCP项目,首先需要安装mcp-sdk-go库:

bash 复制代码
go get github.com/voocel/mcp-sdk-go

架构设计

该SDK采用分层架构设计,清晰地分离了不同的职责:

javascript 复制代码
Application Layer    │ 业务逻辑:Tools, Resources, Prompts
Protocol Layer       │ MCP协议:JSON-RPC, 生命周期管理  
Transport Layer      │ 传输实现:STDIO, SSE, Streamable HTTP

核心接口设计如下:

go 复制代码
// 传输层抽象
type Transport interface {
    Send(ctx context.Context, message []byte) error
    Receive(ctx context.Context) ([]byte, error)
    Close() error
}

// 服务器接口
type Server interface {
    AddTool(name string, tool Tool, handler ToolHandler)
    AddResource(uri string, resource Resource, handler ResourceHandler)
    AddPrompt(name string, prompt Prompt, handler PromptHandler)
    Serve(ctx context.Context, transport Transport) error
}

构建MCP服务端

以下是一个完整的MCP服务器实现示例:

go 复制代码
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "path/filepath"
    "strings"
    
    "github.com/voocel/mcp-sdk-go/server"
    "github.com/voocel/mcp-sdk-go/protocol"
    "github.com/voocel/mcp-sdk-go/transport/stdio"
)

func main() {
    // 创建服务器实例
    srv := server.New(server.Options{
        Name:    "file-manager",
        Version: "1.0.0",
    })

    // 添加文件搜索工具
    srv.AddTool("search_files", protocol.Tool{
        Description: "搜索指定模式的文件",
        InputSchema: protocol.ToolInputSchema{
            Type: "object",
            Properties: map[string]interface{}{
                "pattern": map[string]interface{}{
                    "type":        "string",
                    "description": "搜索模式,支持通配符",
                },
                "directory": map[string]interface{}{
                    "type":        "string", 
                    "description": "搜索目录",
                    "default":     ".",
                },
            },
            Required: []string{"pattern"},
        },
    }, handleFileSearch)

    // 添加资源
    srv.AddResource("file://readme", protocol.Resource{
        Name:        "README文件",
        Description: "项目说明文档",
        MimeType:    "text/markdown",
    }, handleReadmeResource)

    // 启动服务器
    transport := stdio.NewServerTransport()
    if err := srv.Serve(context.Background(), transport); err != nil {
        log.Fatal(err)
    }
}

// 文件搜索工具处理函数
func handleFileSearch(ctx context.Context, req protocol.CallToolRequest) (*protocol.CallToolResult, error) {
    pattern := req.Params.Arguments["pattern"].(string)
    directory := "."
    if dir, ok := req.Params.Arguments["directory"]; ok {
        directory = dir.(string)
    }
    
    var results []string
    err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return nil
        }
        if matched, _ := filepath.Match(pattern, info.Name()); matched {
            results = append(results, path)
        }
        return nil
    })
    
    if err != nil {
        return nil, err
    }
    
    return &protocol.CallToolResult{
        Content: []protocol.Content{
            protocol.TextContent{
                Type: "text",
                Text: strings.Join(results, "\n"),
            },
        },
    }, nil
}

// README资源处理函数
func handleReadmeResource(ctx context.Context, req protocol.ReadResourceRequest) ([]protocol.ResourceContents, error) {
    content, err := os.ReadFile("README.md")
    if err != nil {
        return nil, err
    }

    return []protocol.ResourceContents{
        protocol.TextResourceContents{
            URI:      "file://readme",
            MimeType: "text/markdown",
            Text:     string(content),
        },
    }, nil
}

构建MCP客户端

客户端的实现同样简洁:

go 复制代码
package main

import (
    "context"
    "fmt"
    "log"
    
    "github.com/voocel/mcp-sdk-go/client"
    "github.com/voocel/mcp-sdk-go/transport/stdio"
)

func main() {
    // 创建客户端
    mcpClient := client.New("file-client", "1.0.0")
    
    // 使用STDIO传输连接到服务器
    transport := stdio.New("./server", []string{})
    if err := mcpClient.Connect(context.Background(), transport); err != nil {
        log.Fatal(err)
    }
    defer mcpClient.Close()

    // 列出可用工具
    tools, err := mcpClient.ListTools(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("发现 %d 个工具:\n", len(tools.Tools))
    for _, tool := range tools.Tools {
        fmt.Printf("- %s: %s\n", tool.Name, tool.Description)
    }

    // 调用文件搜索工具
    result, err := mcpClient.CallTool(context.Background(), protocol.CallToolRequest{
        Params: protocol.CallToolParams{
            Name: "search_files",
            Arguments: map[string]interface{}{
                "pattern": "*.go",
            },
        },
    })
    
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("搜索结果:\n%s\n", result.Content[0].(protocol.TextContent).Text)
}

三种传输方式详解

mcp-sdk-go支持三种不同的传输方式,适用于不同的使用场景。

1. STDIO传输

STDIO传输是最简单的传输方式,适用于本地进程通信:

go 复制代码
type StdioTransport struct {
    cmd    *exec.Cmd
    stdin  io.WriteCloser
    stdout io.ReadCloser
}

func (t *StdioTransport) Send(ctx context.Context, message []byte) error {
    _, err := t.stdin.Write(append(message, '\n'))
    return err
}

func (t *StdioTransport) Receive(ctx context.Context) ([]byte, error) {
    scanner := bufio.NewScanner(t.stdout)
    if scanner.Scan() {
        return scanner.Bytes(), nil
    }
    return nil, scanner.Err()
}

2. SSE传输

基于Server-Sent Events的HTTP传输,适合Web应用场景:

go 复制代码
type SSETransport struct {
    baseURL    string
    httpClient *http.Client
    eventChan  chan []byte
}

func (t *SSETransport) Send(ctx context.Context, message []byte) error {
    resp, err := t.httpClient.Post(
        t.baseURL+"/message", 
        "application/json", 
        bytes.NewReader(message),
    )
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    return nil
}

3. Streamable HTTP传输

这是最新的传输协议,支持MCP 2025-03-26规范。与传统SSE需要两个端点不同,Streamable HTTP使用单一端点:

go 复制代码
type StreamableTransport struct {
    endpoint  string
    sessionID string
    client    *http.Client
}

func (t *StreamableTransport) Send(ctx context.Context, message []byte) error {
    req, _ := http.NewRequestWithContext(ctx, "POST", t.endpoint, bytes.NewReader(message))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("MCP-Protocol-Version", "2025-03-26")
    
    if t.sessionID != "" {
        req.Header.Set("Mcp-Session-Id", t.sessionID)
    }
    
    resp, err := t.client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    // 提取会话ID
    if sessionID := resp.Header.Get("Mcp-Session-Id"); sessionID != "" {
        t.sessionID = sessionID
    }
    
    return nil
}

Streamable HTTP的技术特点

单端点设计

Streamable HTTP服务器通过HTTP方法区分不同操作:

go 复制代码
func (s *StreamableServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case "POST":
        s.handleJSONRPC(w, r)      // 处理JSON-RPC请求
    case "GET":
        s.handleEventStream(w, r)   // 建立SSE流
    case "DELETE":
        s.handleSessionTermination(w, r) // 终止会话
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

会话管理

go 复制代码
type Session struct {
    ID         string
    Client     chan []byte
    LastActive time.Time
    mu         sync.RWMutex
}

func (s *StreamableServer) getOrCreateSession(r *http.Request) *Session {
    sessionID := r.Header.Get("Mcp-Session-Id")
    if sessionID == "" {
        sessionID = generateSessionID()
    }
    
    s.mu.Lock()
    defer s.mu.Unlock()
    
    session, exists := s.sessions[sessionID]
    if !exists {
        session = &Session{
            ID:         sessionID,
            Client:     make(chan []byte, 100),
            LastActive: time.Now(),
        }
        s.sessions[sessionID] = session
    }
    
    session.LastActive = time.Now()
    return session
}

协议版本协商

go 复制代码
func (s *StreamableServer) negotiateProtocol(r *http.Request) string {
    clientVersion := r.Header.Get("MCP-Protocol-Version")
    
    supportedVersions := []string{"2025-03-26", "2024-11-05"}
    for _, version := range supportedVersions {
        if version == clientVersion {
            return version
        }
    }
    
    return supportedVersions[len(supportedVersions)-1]
}

处理多态JSON序列化

MCP协议中的Content类型是多态的,需要自定义序列化处理:

go 复制代码
type Content interface {
    GetType() string
}

type TextContent struct {
    Type string `json:"type"`
    Text string `json:"text"`
}

type ImageContent struct {
    Type string `json:"type"`
    Data string `json:"data"`
    MimeType string `json:"mimeType"`
}

func UnmarshalContent(data []byte, content *Content) error {
    var base struct {
        Type string `json:"type"`
    }
    
    if err := json.Unmarshal(data, &base); err != nil {
        return err
    }
    
    switch base.Type {
    case "text":
        var textContent TextContent
        if err := json.Unmarshal(data, &textContent); err != nil {
            return err
        }
        *content = textContent
    case "image":
        var imageContent ImageContent
        if err := json.Unmarshal(data, &imageContent); err != nil {
            return err
        }
        *content = imageContent
    default:
        return fmt.Errorf("unknown content type: %s", base.Type)
    }
    
    return nil
}

性能优化考虑

并发处理

使用goroutine池避免无限制创建goroutine:

go 复制代码
func (s *Server) handleRequest(ctx context.Context, req []byte) {
    s.workerPool <- struct{}{}
    go func() {
        defer func() { <-s.workerPool }()
        s.processRequest(ctx, req)
    }()
}

内存管理

使用对象池减少GC压力:

go 复制代码
var messagePool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 0, 1024)
    },
}

func (t *Transport) Send(ctx context.Context, message []byte) error {
    buf := messagePool.Get().([]byte)
    defer messagePool.Put(buf[:0])
    
    buf = append(buf, message...)
    return t.sendRaw(ctx, buf)
}

测试实践

单元测试

go 复制代码
func TestStreamableTransport(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Mcp-Session-Id", "test-session")
        w.WriteHeader(http.StatusOK)
    }))
    defer server.Close()
    
    transport := streamable.New(server.URL)
    err := transport.Send(context.Background(), []byte(`{"test": "message"}`))
    
    assert.NoError(t, err)
    assert.Equal(t, "test-session", transport.SessionID())
}

集成测试

go 复制代码
func TestMCPIntegration(t *testing.T) {
    srv := server.New(server.Options{Name: "test"})
    srv.AddTool("echo", echoTool, echoHandler)
    
    transport := memory.NewTransport()
    go srv.Serve(context.Background(), transport)
    
    client := client.New("test-client", "1.0.0")
    err := client.Connect(context.Background(), transport)
    require.NoError(t, err)
    
    result, err := client.CallTool(context.Background(), /* ... */)
    require.NoError(t, err)
    assert.NotNil(t, result)
}

传输方式对比

特性 STDIO SSE Streamable HTTP
使用场景 本地进程 Web应用 现代Web应用
端点数量 N/A 2个 1个
状态管理 无状态 有状态 可选
部署复杂度 简单 中等 简单
协议版本 全部 2024-11-05 2025-03-26

快速开始

  1. 安装依赖:
bash 复制代码
go get github.com/voocel/mcp-sdk-go
  1. 创建基本服务器:
bash 复制代码
mkdir mcp-project && cd mcp-project
go mod init mcp-project
  1. 运行示例:
bash 复制代码
# 克隆项目
git clone https://github.com/voocel/mcp-sdk-go.git
cd mcp-sdk-go/examples/calculator
go run server/main.go

总结

通过分层架构设计,mcp-sdk-go提供了一个功能完整的MCP实现,支持三种传输方式。Streamable HTTP作为最新的传输协议,在保持简洁性的同时提供了更好的扩展性和部署灵活性。

关键技术点包括:

  • 清晰的接口抽象和分层设计
  • 多态JSON序列化处理
  • 会话管理和协议版本协商
  • 并发安全和性能优化

该SDK为Go开发者提供了构建MCP服务的完整解决方案,无论是本地工具集成还是Web服务部署,都能找到合适的传输方式。

相关推荐
love530love38 分钟前
是否需要预先安装 CUDA Toolkit?——按使用场景分级推荐及进阶说明
linux·运维·前端·人工智能·windows·后端·nlp
泯泷2 小时前
「译」为 Rust 及所有语言优化 WebAssembly
前端·后端·rust
梦想很大很大2 小时前
把业务逻辑写进数据库中:老办法的新思路(以 PostgreSQL 为例)
前端·后端·架构
Android洋芋3 小时前
GitHub开源协作实践:HelloGitHub项目详解与企业级应用实战
后端
雨果talk3 小时前
Spring Boot集成Mina的Socket资源管理:从稳定通信到高性能优化
spring boot·后端·性能优化
雨果talk3 小时前
【一文看懂多模块Bean初始化难题】Spring Boot多模块项目中的Bean初始化难题:包名不一致的优雅解决方案
java·spring boot·后端·spring·springboot
ZHOU_WUYI4 小时前
flask JWT 认证
后端·flask·jwt
小奏技术5 小时前
Jason Evans:jemalloc的开源20年回忆录
后端·开源
程序员爱钓鱼5 小时前
Go语言同步原语与数据竞争:数据竞争的检测工具
后端·google·go
汪子熙5 小时前
编写一个 Word Macro,调用 DeepSeek API
后端