Go 使用 MCP 官方 SDK 轻松构建 MCP 客户端

随着 Model Context Protocol (MCP) 成为连接 AI 模型与数据源、工具的标准协议,使用 Go 语言构建 MCP 客户端的需求日益增长。本文将详细介绍如何使用官方 Go SDK 轻松构建一个功能完善的 MCP 客户端,涵盖从基础连接到调用工具、资源、提示的完整流程。

MCP 与官方 Go SDK 简介

什么是 MCP?

Model Context Protocol (MCP) 是一个开放协议,旨在标准化 AI 语言模型/代理与外部数据源、工具和功能之间的通信。通过 MCP,开发者可以构建:

  • MCP 客户端:连接 MCP 服务器以使用其工具、资源和提示的应用程序
  • MCP 服务器:向客户端暴露工具、资源和提示的应用程序

官方 Go SDK 概述

官方 Go SDK(github.com/modelcontextprotocol/go-sdk)是由 Model Context Protocol 团队维护的官方实现,与 Google 合作开发。该 SDK 目前处于候选发布状态,即将发布 v1.0.0 版本,API 相对稳定。

SDK 状态说明

版本状态说明
v0.6.0候选发布API 相对稳定,适合开发使用
v0.5.0不稳定包含破坏性变更,不建议用于生产
重要提示:官方 SDK 即将发布 v1.0.0 版本,目前 API 已趋于稳定,可以放心使用。

SDK 包结构

官方 SDK 包含以下几个主要包:

包路径用途
github.com/modelcontextprotocol/go-sdk/mcp构建和使用 MCP 客户端/服务器的主要 API
github.com/modelcontextprotocol/go-sdk/jsonrpc用于实现自定义传输层
github.com/modelcontextprotocol/go-sdk/authOAuth 认证相关原语
github.com/modelcontextprotocol/go-sdk/oauthexOAuth 协议扩展

开发环境准备

1. 安装要求

  • Go 版本:1.20 或更高
  • MCP 服务器:用于测试的 MCP 服务器(可使用示例服务器或任何兼容服务器)

2. 创建项目

# 创建项目目录
mkdir mcp-client-demo
cd mcp-client-demo

# 初始化 Go 模块
go mod init github.com/yourusername/mcp-client-demo

3. 安装 SDK

go get github.com/modelcontextprotocol/go-sdk@v0.6.0

基础客户端实现

最简单的客户端示例

以下代码演示了如何创建一个基础的 MCP 客户端,连接到服务器并调用工具:

package main

import (
    "context"
    "log"
    "os/exec"

    "github.com/modelcontextprotocol/go-sdk/mcp"
)

func main() {
    ctx := context.Background()

    // 创建新客户端,指定客户端名称和版本
    client := mcp.NewClient(&mcp.Implementation{
        Name:    "mcp-client-demo",
        Version: "v1.0.0",
    }, nil)

    // 通过 stdio 传输连接到服务器
    // 假设服务器可执行文件名为 "myserver"
    transport := &mcp.CommandTransport{
        Command: exec.Command("myserver"),
    }
    
    session, err := client.Connect(ctx, transport, nil)
    if err != nil {
        log.Fatalf("连接失败: %v", err)
    }
    defer session.Close()

    log.Println("成功连接到 MCP 服务器")

    // 调用服务器上的工具
    params := &mcp.CallToolParams{
        Name:      "greet",
        Arguments: map[string]any{"name": "World"},
    }
    
    res, err := session.CallTool(ctx, params)
    if err != nil {
        log.Fatalf("调用工具失败: %v", err)
    }
    
    if res.IsError {
        log.Fatal("工具执行返回错误")
    }
    
    // 处理返回内容
    for _, content := range res.Content {
        if textContent, ok := content.(*mcp.TextContent); ok {
            log.Printf("工具响应: %s", textContent.Text)
        }
    }
}

代码详解

组件说明
mcp.NewClient()创建客户端实例,传入客户端标识信息
mcp.CommandTransport基于命令的传输层,启动子进程并通过 stdio 通信
client.Connect()建立与服务器的连接,返回会话对象
session.CallTool()调用服务器上的工具
session.Close()关闭会话,释放资源

进阶功能实现

1. 检查服务器能力

在调用特定功能前,可以先检查服务器支持哪些能力:

package main

import (
    "context"
    "log"
    "os/exec"

    "github.com/modelcontextprotocol/go-sdk/mcp"
)

func checkServerCapabilities(session *mcp.Session) {
    // 获取服务器信息
    serverInfo := session.ServerInfo()
    log.Printf("服务器: %s (版本: %s)", serverInfo.Name, serverInfo.Version)
    
    // 检查能力(需要通过初始化响应获取)
    // 注意:实际能力信息在连接时已交换,可通过 session 获取
    log.Println("已与服务器完成能力协商")
}

func main() {
    ctx := context.Background()
    
    client := mcp.NewClient(&mcp.Implementation{
        Name:    "capability-checker",
        Version: "v1.0.0",
    }, nil)
    
    transport := &mcp.CommandTransport{
        Command: exec.Command("your-server"),
    }
    
    session, err := client.Connect(ctx, transport, nil)
    if err != nil {
        log.Fatal(err)
    }
    defer session.Close()
    
    checkServerCapabilities(session)
}

2. 列出可用工具

func listTools(session *mcp.Session) error {
    ctx := context.Background()
    
    // 列出所有工具
    tools, err := session.ListTools(ctx, nil)
    if err != nil {
        return err
    }
    
    log.Printf("找到 %d 个工具:", len(tools.Tools))
    for i, tool := range tools.Tools {
        log.Printf("  %d. %s: %s", i+1, tool.Name, tool.Description)
        if tool.InputSchema != nil {
            log.Printf("     输入模式: %v", tool.InputSchema)
        }
    }
    
    return nil
}

3. 列出资源

func listResources(session *mcp.Session) error {
    ctx := context.Background()
    
    // 列出资源
    resources, err := session.ListResources(ctx, nil)
    if err != nil {
        return err
    }
    
    log.Printf("找到 %d 个资源:", len(resources.Resources))
    for i, resource := range resources.Resources {
        log.Printf("  %d. %s (%s): %s", i+1, resource.Name, resource.URI, resource.Description)
    }
    
    // 列出资源模板
    templates, err := session.ListResourceTemplates(ctx, nil)
    if err != nil {
        return err
    }
    
    log.Printf("找到 %d 个资源模板:", len(templates.ResourceTemplates))
    for i, template := range templates.ResourceTemplates {
        log.Printf("  %d. %s: %s", i+1, template.Name, template.URITemplate)
    }
    
    return nil
}

4. 读取资源内容

func readResource(session *mcp.Session, uri string) error {
    ctx := context.Background()
    
    params := &mcp.ReadResourceParams{
        URI: uri,
    }
    
    result, err := session.ReadResource(ctx, params)
    if err != nil {
        return err
    }
    
    log.Printf("资源内容 (类型: %s):", result.Contents[0].GetType())
    
    for _, content := range result.Contents {
        switch c := content.(type) {
        case *mcp.TextResourceContent:
            log.Printf("  文本: %s", c.Text)
        case *mcp.BlobResourceContent:
            log.Printf("  二进制数据: %d 字节", len(c.Blob))
        }
    }
    
    return nil
}

5. 列出和使用提示

func listPrompts(session *mcp.Session) error {
    ctx := context.Background()
    
    // 列出提示
    prompts, err := session.ListPrompts(ctx, nil)
    if err != nil {
        return err
    }
    
    log.Printf("找到 %d 个提示:", len(prompts.Prompts))
    for i, prompt := range prompts.Prompts {
        log.Printf("  %d. %s: %s", i+1, prompt.Name, prompt.Description)
        if len(prompt.Arguments) > 0 {
            log.Printf("     参数:")
            for _, arg := range prompt.Arguments {
                log.Printf("       - %s: %s (必需: %v)", 
                    arg.Name, arg.Description, arg.Required)
            }
        }
    }
    
    return nil
}

func getPrompt(session *mcp.Session, name string, args map[string]string) error {
    ctx := context.Background()
    
    params := &mcp.GetPromptParams{
        Name:      name,
        Arguments: args,
    }
    
    result, err := session.GetPrompt(ctx, params)
    if err != nil {
        return err
    }
    
    log.Printf("提示内容:")
    for _, message := range result.Messages {
        log.Printf("  [%s] %v", message.Role, message.Content)
    }
    
    return nil
}

完整示例:功能完善的客户端

以下是一个综合示例,演示了如何使用 MCP SDK 的所有主要功能:

package main

import (
    "context"
    "flag"
    "fmt"
    "log"
    "os"
    "os/exec"
    "time"

    "github.com/modelcontextprotocol/go-sdk/mcp"
)

func main() {
    // 命令行参数
    serverCmd := flag.String("server", "", "MCP 服务器可执行文件路径")
    toolName := flag.String("tool", "", "要调用的工具名称")
    toolArg := flag.String("arg", "", "工具参数 (JSON 格式)")
    resourceURI := flag.String("resource", "", "要读取的资源 URI")
    promptName := flag.String("prompt", "", "要获取的提示名称")
    flag.Parse()

    if *serverCmd == "" {
        log.Fatal("请指定服务器可执行文件路径: -server <path>")
    }

    ctx := context.Background()
    
    // 创建客户端
    client := mcp.NewClient(&mcp.Implementation{
        Name:    "advanced-mcp-client",
        Version: "1.0.0",
    }, nil)
    
    // 连接到服务器
    transport := &mcp.CommandTransport{
        Command: exec.Command(*serverCmd),
    }
    
    log.Printf("正在连接到服务器: %s", *serverCmd)
    session, err := client.Connect(ctx, transport, nil)
    if err != nil {
        log.Fatalf("连接失败: %v", err)
    }
    defer session.Close()
    
    log.Println("成功连接!")
    
    // 显示服务器信息
    serverInfo := session.ServerInfo()
    log.Printf("服务器信息: %s (版本: %s)", serverInfo.Name, serverInfo.Version)
    
    // 根据命令行参数执行不同操作
    if *toolName != "" {
        // 调用工具
        args := make(map[string]any)
        if *toolArg != "" {
            // 简单解析,实际应使用 json.Unmarshal
            args["input"] = *toolArg
        }
        
        callTool(ctx, session, *toolName, args)
    } else if *resourceURI != "" {
        // 读取资源
        readResource(ctx, session, *resourceURI)
    } else if *promptName != "" {
        // 获取提示
        getPrompt(ctx, session, *promptName, nil)
    } else {
        // 默认:列出所有功能
        listAllFeatures(ctx, session)
    }
}

func callTool(ctx context.Context, session *mcp.Session, name string, args map[string]any) {
    log.Printf("调用工具: %s", name)
    log.Printf("参数: %v", args)
    
    params := &mcp.CallToolParams{
        Name:      name,
        Arguments: args,
    }
    
    ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()
    
    result, err := session.CallTool(ctx, params)
    if err != nil {
        log.Fatalf("调用工具失败: %v", err)
    }
    
    if result.IsError {
        log.Println("工具返回错误")
    }
    
    log.Println("工具响应:")
    for i, content := range result.Content {
        switch c := content.(type) {
        case *mcp.TextContent:
            log.Printf("  [%d] 文本: %s", i+1, c.Text)
        case *mcp.ImageContent:
            log.Printf("  [%d] 图片: %s (MIME: %s)", i+1, c.URL, c.MIMEType)
        case *mcp.EmbeddedResource:
            log.Printf("  [%d] 嵌入资源", i+1)
        default:
            log.Printf("  [%d] 未知内容类型", i+1)
        }
    }
}

func readResource(ctx context.Context, session *mcp.Session, uri string) {
    log.Printf("读取资源: %s", uri)
    
    params := &mcp.ReadResourceParams{
        URI: uri,
    }
    
    ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()
    
    result, err := session.ReadResource(ctx, params)
    if err != nil {
        log.Fatalf("读取资源失败: %v", err)
    }
    
    log.Printf("找到 %d 个资源内容:", len(result.Contents))
    for i, content := range result.Contents {
        switch c := content.(type) {
        case *mcp.TextResourceContent:
            log.Printf("  [%d] 文本资源: %s", i+1, c.Text)
        case *mcp.BlobResourceContent:
            log.Printf("  [%d] 二进制资源: %d 字节", i+1, len(c.Blob))
        default:
            log.Printf("  [%d] 未知资源类型", i+1)
        }
    }
}

func getPrompt(ctx context.Context, session *mcp.Session, name string, args map[string]string) {
    log.Printf("获取提示: %s", name)
    
    params := &mcp.GetPromptParams{
        Name:      name,
        Arguments: args,
    }
    
    ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()
    
    result, err := session.GetPrompt(ctx, params)
    if err != nil {
        log.Fatalf("获取提示失败: %v", err)
    }
    
    log.Printf("提示包含 %d 条消息:", len(result.Messages))
    for i, msg := range result.Messages {
        log.Printf("  [%d] %s: %v", i+1, msg.Role, msg.Content)
    }
}

func listAllFeatures(ctx context.Context, session *mcp.Session) {
    // 列出工具
    tools, err := session.ListTools(ctx, nil)
    if err != nil {
        log.Printf("列出工具失败: %v", err)
    } else {
        log.Printf("\n=== 可用工具 (%d) ===", len(tools.Tools))
        for _, tool := range tools.Tools {
            log.Printf("  - %s: %s", tool.Name, tool.Description)
        }
    }
    
    // 列出资源
    resources, err := session.ListResources(ctx, nil)
    if err != nil {
        log.Printf("列出资源失败: %v", err)
    } else {
        log.Printf("\n=== 可用资源 (%d) ===", len(resources.Resources))
        for _, resource := range resources.Resources {
            log.Printf("  - %s (%s): %s", resource.Name, resource.URI, resource.Description)
        }
    }
    
    // 列出提示
    prompts, err := session.ListPrompts(ctx, nil)
    if err != nil {
        log.Printf("列出提示失败: %v", err)
    } else {
        log.Printf("\n=== 可用提示 (%d) ===", len(prompts.Prompts))
        for _, prompt := range prompts.Prompts {
            log.Printf("  - %s: %s", prompt.Name, prompt.Description)
        }
    }
}

传输层选项

MCP SDK 支持多种传输方式:

1. Stdio 传输(默认)

// 通过命令启动服务器进程
transport := &mcp.CommandTransport{
    Command: exec.Command("path/to/server", "--arg", "value"),
}

// 或直接使用标准输入输出(假设服务器已在运行)
transport := &mcp.StdioTransport{
    In:  os.Stdin,
    Out: os.Stdout,
}

2. SSE 传输(HTTP Server-Sent Events)

// 连接到 SSE 服务器
transport := &mcp.SSETransport{
    URL: "http://localhost:8080/sse",
}

3. WebSocket 传输

// 连接到 WebSocket 服务器
transport := &mcp.WebSocketTransport{
    URL: "ws://localhost:8080/ws",
}

错误处理与最佳实践

1. 超时控制

func callToolWithTimeout(session *mcp.Session, name string, args map[string]any) error {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    params := &mcp.CallToolParams{
        Name:      name,
        Arguments: args,
    }
    
    result, err := session.CallTool(ctx, params)
    if err != nil {
        return fmt.Errorf("工具调用超时或失败: %w", err)
    }
    
    // 处理结果...
    return nil
}

2. 重试机制

func callToolWithRetry(session *mcp.Session, name string, args map[string]any, maxRetries int) (*mcp.CallToolResult, error) {
    var lastErr error
    
    for attempt := 0; attempt < maxRetries; attempt++ {
        if attempt > 0 {
            log.Printf("重试第 %d 次...", attempt)
            time.Sleep(time.Second * time.Duration(attempt))
        }
        
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        
        params := &mcp.CallToolParams{
            Name:      name,
            Arguments: args,
        }
        
        result, err := session.CallTool(ctx, params)
        if err == nil {
            return result, nil
        }
        
        lastErr = err
        log.Printf("尝试 %d 失败: %v", attempt+1, err)
    }
    
    return nil, fmt.Errorf("在 %d 次尝试后失败: %w", maxRetries, lastErr)
}

3. 资源清理

func main() {
    client := mcp.NewClient(&mcp.Implementation{Name: "client"}, nil)
    transport := &mcp.CommandTransport{Command: exec.Command("server")}
    
    session, err := client.Connect(context.Background(), transport, nil)
    if err != nil {
        log.Fatal(err)
    }
    
    // 确保会话关闭
    defer func() {
        if err := session.Close(); err != nil {
            log.Printf("关闭会话时出错: %v", err)
        }
    }()
    
    // 使用会话...
}

与其他 Go MCP SDK 对比

SDK特点适用场景
官方 SDK官方维护,即将稳定,API 简洁生产环境,追求长期稳定性
gomcp完整规范实现,支持多种传输,丰富的钩子系统需要高级特性或自定义传输
mcp-go第三方实现,社区活跃已有项目使用,社区支持
go-mcp轻量级,易上手快速原型开发

官方 SDK 的优势在于:

  • 由 MCP 团队官方维护
  • 与规范同步更新
  • API 设计简洁,易于使用
  • 即将发布 v1.0.0 稳定版本

测试与调试

使用 MCP Inspector 测试

MCP Inspector 是一个类似 Postman 的开发工具,用于测试和调试 MCP 服务器:

# 启动 MCP Inspector
npx @modelcontextprotocol/inspector

# 连接到你的服务器
# 然后在 Inspector 界面中配置服务器命令

日志记录

func setupLogging() {
    // 设置日志格式
    log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
    log.SetOutput(os.Stderr)
    
    // 可以同时输出到文件
    f, err := os.OpenFile("mcp-client.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
    if err == nil {
        log.SetOutput(io.MultiWriter(os.Stderr, f))
    }
}

总结

通过官方 Go SDK 构建 MCP 客户端具有以下优势:

  1. 简单易用:API 设计简洁,几行代码即可完成连接和调用
  2. 功能完整:支持工具、资源、提示等所有 MCP 核心功能
  3. 即将稳定:v1.0.0 即将发布,API 已趋于稳定
  4. 传输灵活:支持 stdio、SSE、WebSocket 等多种传输方式
  5. 官方维护:与 MCP 规范同步更新,长期有保障

本文从基础客户端到完整功能实现,全面介绍了使用官方 Go SDK 构建 MCP 客户端的方法。无论是简单的工具调用,还是复杂的资源管理,官方 SDK 都能提供简洁而强大的支持。

资源链接

暂无评论