找回密码
 立即注册
首页 业界区 业界 MCP官方Go SDK尝鲜

MCP官方Go SDK尝鲜

翁谌缜 18 小时前
前言

此前在 MCP 官网就注意到官方提供了 Go SDK,近期由于在 Python 环境下开发 MCP Server 有点"审美疲劳",因此决定使用 Go 语言尝尝鲜。
从个人实际体验来看,Go 语言在并发处理方面确实具有显著优势:无需纠结于同步阻塞、异步事件循环、多进程多线程通信等复杂的并发问题,goroutine 一把梭哈。同时,Go 语言的部署也非常便捷,编译后生成的静态二进制文件具有良好的可移植性,可以在不同环境中直接运行。
然而,这种便利性也伴随着一定的代价。相较于 Python,使用 Go 语言实现 MCP 功能相对复杂一些,开发效率略低。这就是软件工程中的经典权衡了:运行成本与开发成本往往难以兼得,需要根据具体场景进行取舍。
MCP 协议简介

可能都耳熟能详了,但以防还有不熟悉的朋友,先简单介绍下MCP
Model Context Protocol (MCP) 是一种标准化的协议,旨在为 AI 模型提供统一的工具调用接口。通过 MCP,开发者可以将各种工具、服务和数据源暴露给 AI 模型,使其能够执行超出基础语言模型能力范围的操作。MCP 支持多种传输协议,包括 HTTP 和 Stdio,为不同场景下的集成提供了灵活性。
一个简单的 MCP Server 示例

MCP 官方 Go SDK 在定义工具(Tool)时,要求明确指定输入参数和输出结果的数据结构。对于功能较为简单的工具,也可以直接使用 any 类型。以下是一个完整的 MCP Server 示例,提供了三个实用工具:

  • getCurrentDatetime:获取当前时间,返回 RFC3339 格式(2006-01-02T15:04:05Z07:00)的时间戳字符串。由于不需要输入参数,因此参数类型定义为 any,输出同样使用 any 类型。
  • getComputerStatus:获取当前系统的关键信息,包括 CPU 使用率、内存使用情况、系统版本等。该工具接受一个 CPUSampleTime 参数,对应的输入结构体为 GetComputerStatusIn,输出结构体为 GetComputerStatusOut(Go SDK 的示例中通常采用 xxxIn 和 xxxOut 的命名约定来区分工具的输入输出结构体)。
  • getDiskInfo:获取所有硬盘分区的使用信息和文件系统详情。该工具无需输入参数,仅定义了输出结构体 GetDiskInfoOut。
在完成所有工具逻辑的实现后,最后一步是启动服务。以下示例采用 Streamable HTTP 模式启动,同时也保留了 Stdio Transport 模式的注释代码供参考。
  1. package main
  2. import (
  3.         "context"
  4.         "fmt"
  5.         "log"
  6.         "net"
  7.         "net/http"
  8.         "time"
  9.         "github.com/modelcontextprotocol/go-sdk/mcp"
  10.         "github.com/shirou/gopsutil/v4/cpu"
  11.         "github.com/shirou/gopsutil/v4/disk"
  12.         "github.com/shirou/gopsutil/v4/host"
  13.         "github.com/shirou/gopsutil/v4/mem"
  14. )
  15. func getCurrentDatetime(ctx context.Context, req *mcp.CallToolRequest, arg any) (*mcp.CallToolResult, any, error) {
  16.         now := time.Now().Format(time.RFC3339)
  17.         return nil, now, nil
  18. }
  19. type GetComputerStatusIn struct {
  20.         CPUSampleTime time.Duration `json:"cpu_sample_time" jsonschema:"the sample time of cpu usage. Default is 1s"`
  21. }
  22. type GetComputerStatusOut struct {
  23.         Hostinfo    string `json:"host info" jsonschema:"the hostinfo of the computer"`
  24.         TimeZone    string `json:"time_zone" jsonschema:"the time zone of the computer"`
  25.         IPAddress   string `json:"ip_address" jsonschema:"the ip address of the computer"`
  26.         CPUUsage    string `json:"cpu_usage" jsonschema:"the cpu usage of the computer"`
  27.         MemoryUsage string `json:"memory_usage" jsonschema:"the memory usage of the computer"`
  28. }
  29. func getComputerStatus(ctx context.Context, req *mcp.CallToolRequest, args GetComputerStatusIn) (*mcp.CallToolResult, GetComputerStatusOut, error) {
  30.         if args.CPUSampleTime == 0 {
  31.                 args.CPUSampleTime = time.Second
  32.         }
  33.         hInfo, err := host.Info()
  34.         if err != nil {
  35.                 return nil, GetComputerStatusOut{}, err
  36.         }
  37.         var resp GetComputerStatusOut
  38.         resp.Hostinfo = fmt.Sprintf("%+v", *hInfo)
  39.         name, offset := time.Now().Zone()
  40.         resp.TimeZone = fmt.Sprintf("Timezone: %s (UTC%+d)\n", name, offset/3600)
  41.         // CPU Usage
  42.         percent, err := cpu.Percent(time.Second, false)
  43.         if err != nil {
  44.                 return nil, GetComputerStatusOut{}, err
  45.         }
  46.         resp.CPUUsage = fmt.Sprintf("CPU Usage: %.2f%%\n", percent[0])
  47.         // Memory Usage
  48.         v, err := mem.VirtualMemory()
  49.         if err != nil {
  50.                 return nil, GetComputerStatusOut{}, err
  51.         }
  52.         resp.MemoryUsage = fmt.Sprintf("Mem Usage: %.2f%% (Used: %vMB / Total: %vMB)\n",
  53.                 v.UsedPercent, v.Used/1024/1024, v.Total/1024/1024)
  54.         // Ip Address
  55.         conn, err := net.Dial("udp", "8.8.8.8:80")
  56.         if err != nil {
  57.                 return nil, GetComputerStatusOut{}, err
  58.         }
  59.         defer conn.Close()
  60.         localAddr := conn.LocalAddr().(*net.UDPAddr)
  61.         resp.IPAddress = localAddr.IP.String()
  62.         return nil, resp, nil
  63. }
  64. type DiskInfo struct {
  65.         Device     string   `json:"device" jsonschema:"the device name"`
  66.         Mountpoint string   `json:"mountpoint" jsonschema:"the mountpoint"`
  67.         Fstype     string   `json:"fstype" jsonschema:"the filesystem type"`
  68.         Opts       []string `json:"opts" jsonschema:"the mount options"`
  69.         DiskTotal  uint64   `json:"disk_total" jsonschema:"the total disk space in GiB"`
  70.         DiskUsage  float64  `json:"disk_usage" jsonschema:"the disk usage percentage"`
  71. }
  72. type GetDiskInfoOut struct {
  73.         PartInfos []DiskInfo `json:"part_infos" jsonschema:"the disk partitions"`
  74. }
  75. func getDiskInfo(ctx context.Context, req *mcp.CallToolRequest, args any) (*mcp.CallToolResult, GetDiskInfoOut, error) {
  76.         partInfos, err := disk.Partitions(false)
  77.         if err != nil {
  78.                 return nil, GetDiskInfoOut{}, err
  79.         }
  80.         var resp []DiskInfo
  81.         for _, part := range partInfos {
  82.                 diskUsage, err := disk.Usage(part.Mountpoint)
  83.                 if err != nil {
  84.                         continue
  85.                 }
  86.                 resp = append(resp, DiskInfo{
  87.                         Device:     part.Device,
  88.                         Mountpoint: part.Mountpoint,
  89.                         Fstype:     part.Fstype,
  90.                         Opts:       part.Opts,
  91.                         DiskTotal:  diskUsage.Total / 1024 / 1024 / 1024,
  92.                         DiskUsage:  diskUsage.UsedPercent,
  93.                 })
  94.         }
  95.         return nil, GetDiskInfoOut{PartInfos: resp}, nil
  96. }
  97. func main() {
  98.         // ctx := context.Background()
  99.         server := mcp.NewServer(&mcp.Implementation{Name: "MCP_Demo", Version: "0.0.1"}, &mcp.ServerOptions{
  100.                 Instructions: "日期时间相关的 Server",
  101.         })
  102.         mcp.AddTool(server, &mcp.Tool{
  103.                 Name:        "get_current_datetime",
  104.                 Description: "Get current datetime in RFC3339 format",
  105.         }, getCurrentDatetime)
  106.         mcp.AddTool(server, &mcp.Tool{
  107.                 Name:        "get_computer_status",
  108.                 Description: "Get computer status",
  109.         }, getComputerStatus)
  110.         mcp.AddTool(server, &mcp.Tool{
  111.                 Name:        "get_disk_info",
  112.                 Description: "Get disk information",
  113.         }, getDiskInfo)
  114.         // if err := server.Run(ctx, &mcp.StdioTransport{}); err != nil {
  115.         //         log.Fatalln(err)
  116.         // }
  117.         //
  118.         handler := mcp.NewStreamableHTTPHandler(func(req *http.Request) *mcp.Server {
  119.                 path := req.URL.Path
  120.                 switch path {
  121.                 case "/api/mcp":
  122.                         return server
  123.                 default:
  124.                         return nil
  125.                 }
  126.         }, nil)
  127.         url := "127.0.0.1:18001"
  128.         if err := http.ListenAndServe(url, handler); err != nil {
  129.                 log.Fatalln(err)
  130.         }
  131. }
复制代码
MCP Server 代码编译通过后,可以在支持 MCP 协议的开发工具(如 VS Code)中进行测试验证。以下是一个典型的 .vscode/mcp.json 配置示例:
  1. {
  2.     "servers": {
  3.         "demo-http": {
  4.             // "command": "/home/rainux/Documents/workspace/go-dev/mcp-dev/mcp-server-dev/mcp-server-dev"
  5.             "type": "http",
  6.             "url": "http://127.0.0.1:18001/api/mcp"
  7.         }
  8.     }
  9. }
复制代码
启动 MCP Server 后,可以通过向 LLM 提出相关问题来验证工具是否能够被正确调度和执行。
一个完整的 MCP Client 实现

为了构建端到端的 MCP 应用,我们还需要实现一个 MCP Client,使其能够与 LLM 协同工作,自动选择并调用合适的工具。以下是一个功能完整的 MCP Client 实现,其中包含了与 OpenAI 兼容 API 的集成示例(callOpenAI 函数)。
  1. package main
  2. import (
  3.         "context"
  4.         "encoding/json"
  5.         "flag"
  6.         "fmt"
  7.         "log"
  8.         "net/http"
  9.         "os/exec"
  10.         "time"
  11.         "github.com/modelcontextprotocol/go-sdk/mcp"
  12.         "github.com/openai/openai-go/v3"
  13.         "github.com/openai/openai-go/v3/option"
  14.         "github.com/openai/openai-go/v3/packages/param"
  15. )
  16. var (
  17.         FLAG_ModelName     string
  18.         FLAG_BaseURL       string
  19.         FLAG_APIKEY        string
  20.         FLAG_MCP_TRANSPORT string
  21.         FLAG_MCP_URI       string
  22.         FLAG_QUESTION      string
  23.         FLAG_STREAM        bool
  24. )
  25. func main() {
  26.         // Parse command-line flags
  27.         flag.StringVar(&FLAG_BaseURL, "base-url", "https://dashscope.aliyuncs.com/compatible-mode/v1", "llm base url")
  28.         flag.StringVar(&FLAG_ModelName, "model", "qwen-plus", "LLM Model Name")
  29.         flag.StringVar(&FLAG_MCP_TRANSPORT, "mcp-transport", "http", "MCP transport protocol (stdio or http)")
  30.         flag.StringVar(&FLAG_MCP_URI, "mcp-uri", "", "MCP server address")
  31.         flag.StringVar(&FLAG_APIKEY, "api-key", "", "llm api key")
  32.         flag.StringVar(&FLAG_QUESTION, "q", "Hi", "question")
  33.         flag.BoolVar(&FLAG_STREAM, "s", false, "stream response")
  34.         flag.Parse()
  35.         // Get configuration from environment variables with flag overrides
  36.         if FLAG_APIKEY == "" {
  37.                 log.Fatalln("api key is empty")
  38.         }
  39.         if FLAG_QUESTION == "" {
  40.                 log.Fatalln("question is empty")
  41.         }
  42.         // Configure OpenAI client
  43.         // config :=
  44.         ctx := context.Background()
  45.         // question := "Write me a haiku about computers"
  46.         if FLAG_MCP_URI != "" {
  47.                 callOpenAIWithTools(ctx, FLAG_QUESTION)
  48.         } else {
  49.                 callOpenAI(ctx, FLAG_QUESTION, FLAG_STREAM)
  50.         }
  51. }
  52. // callOpenAI 调用 OpenAI API 接口处理用户问题
  53. // 该函数支持流式(stream)和非流式(non-stream)两种响应方式
  54. //
  55. // 参数:
  56. //   - ctx: 控制操作生命周期的上下文
  57. //   - question: 用户提出的问题字符串
  58. //   - stream: 布尔值,指定是否使用流式响应
  59. func callOpenAI(ctx context.Context, question string, stream bool) {
  60.         client := openai.NewClient(option.WithAPIKey(FLAG_APIKEY), option.WithBaseURL(FLAG_BaseURL))
  61.         systemPrompt := "请用亲切热情的风格回答用户的问题"
  62.         if stream {
  63.                 // 创建流式响应请求
  64.                 streamResp := client.Chat.Completions.NewStreaming(ctx, openai.ChatCompletionNewParams{
  65.                         Messages: []openai.ChatCompletionMessageParamUnion{
  66.                                 openai.SystemMessage(systemPrompt),
  67.                                 openai.UserMessage(question),
  68.                         },
  69.                         Model: FLAG_ModelName,
  70.                 })
  71.                 // defer streamResp.Close()
  72.                 defer func() {
  73.                         err := streamResp.Close()
  74.                         if err != nil {
  75.                                 log.Fatalln(err)
  76.                         }
  77.                 }()
  78.                 // 遍历流式响应并逐块输出内容
  79.                 for streamResp.Next() {
  80.                         data := streamResp.Current()
  81.                         fmt.Print(data.Choices[0].Delta.Content)
  82.                         if err := streamResp.Err(); err != nil {
  83.                                 log.Fatalln(err)
  84.                         }
  85.                 }
  86.         } else {
  87.                 // 创建非流式响应请求
  88.                 chatCompletion, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
  89.                         Messages: []openai.ChatCompletionMessageParamUnion{
  90.                                 openai.SystemMessage(systemPrompt),
  91.                                 openai.UserMessage(question),
  92.                         },
  93.                         Model: FLAG_ModelName,
  94.                 })
  95.                 if err != nil {
  96.                         log.Fatalln(err)
  97.                 }
  98.                 // 输出非流式响应内容
  99.                 fmt.Println(chatCompletion.Choices[0].Message.Content)
  100.         }
  101. }
  102. // callOpenAIWithTools 使用 OpenAI API 和 MCP 工具调用来处理用户问题
  103. // 该函数创建一个 OpenAI 客户端和 MCP 客户端,将 MCP 工具转换为 OpenAI 可使用的格式,
  104. // 并执行完整的工具调用流程,包括初始调用和可能的后续调用
  105. //
  106. // 参数:
  107. //   - ctx: 控制操作生命周期的上下文
  108. //   - question: 用户提出的问题字符串
  109. func callOpenAIWithTools(ctx context.Context, question string) {
  110.         // 创建 OpenAI 客户端,使用 API 密钥和基础 URL 配置
  111.         llmClient := openai.NewClient(option.WithAPIKey(FLAG_APIKEY), option.WithBaseURL(FLAG_BaseURL))
  112.         // 创建 MCP 客户端,指定名称和版本
  113.         mcpClient := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "0.0.1"}, nil)
  114.         var transport mcp.Transport
  115.         // 根据命令行标志选择传输协议(stdio 或 http)
  116.         switch FLAG_MCP_TRANSPORT {
  117.         case "stdio":
  118.                 transport = &mcp.CommandTransport{Command: exec.Command(FLAG_MCP_URI)}
  119.         case "http":
  120.                 transport = &mcp.StreamableClientTransport{HTTPClient: &http.Client{Timeout: time.Second * 10}, Endpoint: FLAG_MCP_URI}
  121.         default:
  122.                 log.Fatalf("unknown transport, %s", FLAG_MCP_TRANSPORT)
  123.         }
  124.         // 建立与 MCP 服务器的连接
  125.         session, err := mcpClient.Connect(ctx, transport, nil)
  126.         if err != nil {
  127.                 log.Fatalf("MCP client connects to mcp server failed, err: %v", err)
  128.         }
  129.         defer func() {
  130.                 err := session.Close()
  131.                 if err != nil {
  132.                         log.Fatalln(err)
  133.                 }
  134.         }()
  135.         // 获取可用的 MCP 工具列表
  136.         mcpTools, err := session.ListTools(ctx, &mcp.ListToolsParams{})
  137.         if err != nil {
  138.                 log.Fatalf("List mcp tools failed, err: %v", err)
  139.         }
  140.         var legacyTools []openai.ChatCompletionToolUnionParam
  141.         // 遍历所有 MCP 工具并将其转换为 OpenAI 兼容的工具格式
  142.         for _, tool := range mcpTools.Tools {
  143.                 // 将 MCP 工具输入模式转换为 OpenAI 函数参数
  144.                 if inputSchema, ok := tool.InputSchema.(map[string]any); ok {
  145.                         legacyTools = append(legacyTools, openai.ChatCompletionFunctionTool(
  146.                                 openai.FunctionDefinitionParam{
  147.                                         Name:        tool.Name,
  148.                                         Description: openai.String(tool.Description),
  149.                                         Parameters:  openai.FunctionParameters(inputSchema),
  150.                                 },
  151.                         ))
  152.                 } else {
  153.                         // 如果 InputSchema 不是 map[string]any,使用空参数
  154.                         legacyTools = append(legacyTools, openai.ChatCompletionFunctionTool(
  155.                                 openai.FunctionDefinitionParam{
  156.                                         Name:        tool.Name,
  157.                                         Description: openai.String(tool.Description),
  158.                                         Parameters:  openai.FunctionParameters{},
  159.                                 },
  160.                         ))
  161.                 }
  162.         }
  163.         // 设置初始聊天消息,包括系统提示和用户问题
  164.         messages := []openai.ChatCompletionMessageParamUnion{
  165.                 openai.SystemMessage("请用亲切热情的风格回答用户的问题。你可以使用可用的工具来获取信息。"),
  166.                 openai.UserMessage(question),
  167.         }
  168.         // 调用 LLM 获取初步响应
  169.         chatCompletion, err := llmClient.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
  170.                 Messages: messages,
  171.                 Model:    FLAG_ModelName,
  172.                 Tools:    legacyTools,
  173.                 ToolChoice: openai.ChatCompletionToolChoiceOptionUnionParam{
  174.                         OfAuto: param.Opt[string]{
  175.                                 Value: "auto",
  176.                         },
  177.                 },
  178.         })
  179.         if err != nil {
  180.                 log.Fatalf("LLM call failed, err: %v", err)
  181.         }
  182.         choice := chatCompletion.Choices[0]
  183.         fmt.Printf("LLM response: %s\n", choice.Message.Content)
  184.         // 检查是否需要调用工具
  185.         if choice.FinishReason == "tool_calls" && len(choice.Message.ToolCalls) > 0 {
  186.                 // 遍历所有需要调用的工具
  187.                 for _, toolCall := range choice.Message.ToolCalls {
  188.                         if toolCall.Type != "function" {
  189.                                 continue
  190.                         }
  191.                         fmt.Printf("Executing tool: %s with args: %s\n", toolCall.Function.Name, toolCall.Function.Arguments)
  192.                         // 解析 JSON 参数
  193.                         var argsObj map[string]any
  194.                         args := toolCall.Function.Arguments
  195.                         if args != "" {
  196.                                 if err := json.Unmarshal([]byte(args), &argsObj); err != nil {
  197.                                         log.Printf("Failed to parse tool arguments: %v", err)
  198.                                         argsObj = make(map[string]any)
  199.                                 }
  200.                         } else {
  201.                                 argsObj = make(map[string]any)
  202.                         }
  203.                         fmt.Printf("Executing tool: %s with parsed args: %v\n", toolCall.Function.Name, argsObj)
  204.                         // 执行 MCP 工具调用
  205.                         result, err := session.CallTool(ctx, &mcp.CallToolParams{
  206.                                 Name:      toolCall.Function.Name,
  207.                                 Arguments: argsObj,
  208.                         })
  209.                         if err != nil {
  210.                                 log.Printf("Tool call failed: %v", err)
  211.                                 continue
  212.                         }
  213.                         // 将 MCP 内容转换为字符串
  214.                         var toolResult string
  215.                         if len(result.Content) > 0 {
  216.                                 if textContent, ok := result.Content[0].(*mcp.TextContent); ok {
  217.                                         toolResult = textContent.Text
  218.                                 } else {
  219.                                         // 如果不是 TextContent,转换为 JSON
  220.                                         if jsonBytes, err := json.Marshal(result.Content[0]); err == nil {
  221.                                                 toolResult = string(jsonBytes)
  222.                                         } else {
  223.                                                 toolResult = "Tool executed successfully"
  224.                                         }
  225.                                 }
  226.                         }
  227.                         fmt.Printf("Tool result: %s\n", toolResult)
  228.                         // 添加工具调用消息和工具响应消息
  229.                         messages = append(messages, openai.ChatCompletionMessageParamUnion{
  230.                                 OfAssistant: &openai.ChatCompletionAssistantMessageParam{
  231.                                         Role: "assistant",
  232.                                         ToolCalls: []openai.ChatCompletionMessageToolCallUnionParam{
  233.                                                 {
  234.                                                         OfFunction: &openai.ChatCompletionMessageFunctionToolCallParam{
  235.                                                                 ID: toolCall.ID,
  236.                                                                 Function: openai.ChatCompletionMessageFunctionToolCallFunctionParam{
  237.                                                                         Name:      toolCall.Function.Name,
  238.                                                                         Arguments: toolCall.Function.Arguments,
  239.                                                                 },
  240.                                                         },
  241.                                                 },
  242.                                         },
  243.                                 },
  244.                         })
  245.                         messages = append(messages, openai.ToolMessage(
  246.                                 toolResult,
  247.                                 toolCall.ID,
  248.                         ))
  249.                         // 进行后续调用以获得最终响应
  250.                         chatCompletion, err = llmClient.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
  251.                                 Messages: messages,
  252.                                 Model:    FLAG_ModelName,
  253.                         })
  254.                         if err != nil {
  255.                                 log.Fatalf("LLM follow-up failed, err: %v", err)
  256.                         }
  257.                         fmt.Printf("Final response: %s\n", chatCompletion.Choices[0].Message.Content)
  258.                 }
  259.         }
  260. }
复制代码
运行测试验证

编译完成后,我们可以进行多轮测试来验证功能的正确性。
普通问答测试
  1. ./mcp-client-dev -api-key "sk-xxx" -q "how are you"
复制代码
还可以加上 -s 参数启用流式输出:
  1. ./mcp-client-dev -api-key "sk-xxx" -q "how are you" -s
复制代码
预期输出:
[code]Hi there!
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册