用 Go 語言輕鬆構建 MCP 客戶端與服務器

前言

模型上下文協議(Model Context Protocol,簡稱 MCP)是一種開放標準,旨在標準化大型語言模型(LLM)與外部數據源和工具之間的交互方式。隨着 MCP 越來越受歡迎,Go MCP 庫應運而生。本文將介紹如何在 Go 語言裏面輕鬆構建 MCP 客戶端和服務器。

如果你不熟悉 MCP 協議,可以看我之前寫的這篇文章:一文掌握 MCP 上下文協議:從理論到實踐。

準備好了嗎?準備一杯你最喜歡的咖啡或茶,隨着本文一探究竟吧。

mcp-go

要構建 MCP 客戶端和服務器,我們需要使用 mcp-go 庫。

mcp-go 是 Go 語言實現的 Model Context ProtocolMCP)庫,通過這個庫可以實現 LLM 應用與外部數據源和工具之間的無縫集成。

主要特點

安裝 MCP 庫

在 Go 項目根目錄下,執行以下命令:

go get github.com/mark3labs/mcp-go

構建 MCP 服務器

接下來,我們使用 mcp-go 提供的 server 模塊,構建一個通過 stidio 方式連接的 MCP 服務器。

創建 server 對象

s := server.NewMCPServer(
    "Server Demo",
    "1.0.0",
)

創建 server 對象時,我們可以指定 服務器名版本號 等參數。

添加工具(tools)

以下是一個示例,用於創建並註冊一個簡單的計算器工具:

calculatorTool := mcp.NewTool("calculate",
    mcp.WithDescription("執行基本的算術運算"),
    mcp.WithString("operation",
        mcp.Required(),
        mcp.Description("要執行的算術運算類型"),
        mcp.Enum("add""subtract""multiply""divide"), // 保持英文
    ),
    mcp.WithNumber("x",
        mcp.Required(),
        mcp.Description("第一個數字"),
    ),
    mcp.WithNumber("y",
        mcp.Required(),
        mcp.Description("第二個數字"),
    ),
)

s.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    op := request.Params.Arguments["operation"].(string)
    x := request.Params.Arguments["x"].(float64)
    y := request.Params.Arguments["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 {
            returnnil, errors.New("不允許除以零")
        }
        result = x / y
    }

    return mcp.FormatNumberResult(result), nil
})

添加工具的步驟如下:

添加資源(Resources)

下面的示例展示瞭如何創建並註冊一個靜態資源,用於讀取並提供 README.md 文件的內容。

resource := mcp.NewResource(
    "docs://readme",
    "項目說明文檔",
    mcp.WithResourceDescription("項目的 README 文件"),
    mcp.WithMIMEType("text/markdown"),
)

s.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
    content, err := os.ReadFile("README.md")
    if err != nil {
        returnnil, err
    }

    return []mcp.ResourceContents{
        mcp.TextResourceContents{
            URI:      "docs://readme",
            MIMEType: "text/markdown",
            Text:     string(content),
        },
    }, nil
})

添加資源的步驟如下:

添加提示詞(Prompts)

以下示例展示瞭如何創建並添加一個帶參數的簡單提示詞,用於生成個性化的問候語。

s.AddPrompt(mcp.NewPrompt("greeting",
    mcp.WithPromptDescription("一個友好的問候提示"),
    mcp.WithArgument("name",
        mcp.ArgumentDescription("要問候的人的名字"),
    ),
), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
    name := request.Params.Arguments["name"]
    if name == "" {
        name = "朋友"
    }

    return mcp.NewGetPromptResult(
        "友好的問候",
        []mcp.PromptMessage{
            mcp.NewPromptMessage(
                mcp.RoleAssistant,
                mcp.NewTextContent(fmt.Sprintf("你好,%s!今天有什麼可以幫您的嗎?", name)),
            ),
        },
    ), nil
})

添加提示詞的步驟如下:

啓動基於 stdio 傳輸類型的服務器

// 啓動基於 stdio 的服務器
if err := server.ServeStdio(s); err != nil {
    fmt.Printf("Server error: %v\n", err)
}

使用 server.ServeStdio 方法可以啓動一個基於標準輸入 / 輸出(stdio)的 MCP 服務器。

這種方式適用於本地集成與命令行工具。

啓動基於 sse(Server-Sent Events)傳輸類型的服務器

如果需要通過 HTTP 的方式提供服務,支持服務端推送數據,可以使用 SSE(Server-Sent Events)傳輸模式。

s := server.NewMCPServer(
    "My Server", // Server 名稱
    "1.0.0",     // 版本號
)

// 創建基於 SSE 的服務器實例
sseServer := server.NewSSEServer(s)

// 啓動服務器,監聽指定端口(如 :8080)
err := sseServer.Start(":8080")
if err != nil {
    panic(err)
}

與 stdio 不同,sse 模式基於 HTTP 協議,更適合 Web 應用中的長連接場景,支持服務端推送數據。

完整的 stdio 代碼示例

package main

import (
"context"
"errors"
"fmt"
"os"

"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)

func main() {
 s := server.NewMCPServer(
"Server Demo",
"1.0.0",
 )

// 添加工具
 {
  calculatorTool := mcp.NewTool("calculate",
   mcp.WithDescription("執行基本的算術運算"),
   mcp.WithString("operation",
    mcp.Required(),
    mcp.Description("要執行的算術運算類型"),
    mcp.Enum("add""subtract""multiply""divide"), // 保持英文
   ),
   mcp.WithNumber("x",
    mcp.Required(),
    mcp.Description("第一個數字"),
   ),
   mcp.WithNumber("y",
    mcp.Required(),
    mcp.Description("第二個數字"),
   ),
  )

  s.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
   op := request.Params.Arguments["operation"].(string)
   x := request.Params.Arguments["x"].(float64)
   y := request.Params.Arguments["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 {
     returnnil, errors.New("不允許除以零")
    }
    result = x / y
   }

   return mcp.FormatNumberResult(result), nil
  })
 }

// 添加資源
 {
// 靜態資源示例 - 暴露一個 README 文件
  resource := mcp.NewResource(
   "docs://readme",
   "項目說明文檔",
   mcp.WithResourceDescription("項目的 README 文件"),
   mcp.WithMIMEType("text/markdown"),
  )

// 添加資源及其處理函數
  s.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
   content, err := os.ReadFile("README.md")
   if err != nil {
    returnnil, err
   }

   return []mcp.ResourceContents{
    mcp.TextResourceContents{
     URI:      "docs://readme",
     MIMEType: "text/markdown",
     Text:     string(content),
    },
   }, nil
  })
 }

// 添加提示詞
 {
// 簡單問候提示
  s.AddPrompt(mcp.NewPrompt("greeting",
   mcp.WithPromptDescription("一個友好的問候提示"),
   mcp.WithArgument("name",
    mcp.ArgumentDescription("要問候的人的名字"),
   ),
  ), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
   name := request.Params.Arguments["name"]
   if name == "" {
    name = "朋友"
   }

   return mcp.NewGetPromptResult(
    "友好的問候",
    []mcp.PromptMessage{
     mcp.NewPromptMessage(
      mcp.RoleAssistant,
      mcp.NewTextContent(fmt.Sprintf("你好,%s!今天有什麼可以幫您的嗎?", name)),
     ),
    },
   ), nil
  })
 }

// 啓動基於 stdio 的服務器
if err := server.ServeStdio(s); err != nil {
  fmt.Printf("Server error: %v\n", err)
 }

}

構建 MCP 客戶端

接下來,我們使用 mcp-go 提供的 client 模塊,構建一個通過 stdio 方式連接到前面打包好的 MCP 服務器的客戶端。

該客戶端將展示以下功能:

創建 MCP 客戶端

mcpClient, err := client.NewStdioMCPClient(
    "./client/server", // 服務器可執行文件路徑
    []string{},        // 啓動參數(如果有)
)
if err != nil {
    panic(err)
}
defer mcpClient.Close()

通過 client.NewStdioMCPClient 方法可以創建一個基於 stdio 傳輸的客戶端,並連接到指定的 MCP 服務器可執行文件。

初始化客戶端連接

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

initRequest := mcp.InitializeRequest{}
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
initRequest.Params.ClientInfo = mcp.Implementation{
    Name:    "Client Demo",
    Version: "1.0.0",
}

initResult, err := mcpClient.Initialize(ctx, initRequest)
if err != nil {
    panic(err)
}
fmt.Printf("初始化成功,服務器信息: %s %s\n", initResult.ServerInfo.Name, initResult.ServerInfo.Version)

初始化操作通過 Initialize 方法完成,需指定協議版本及客戶端信息。


獲取提示詞(Prompts)列表

promptsRequest := mcp.ListPromptsRequest{}
prompts, err := mcpClient.ListPrompts(ctx, promptsRequest)
if err != nil {
    panic(err)
}
for _, prompt := range prompts.Prompts {
    fmt.Printf("- %s: %s\n", prompt.Name, prompt.Description)
    fmt.Println("參數:", prompt.Arguments)
}

客戶端可以使用 ListPrompts 獲取服務器上定義的所有提示詞,包括名稱、描述和參數結構。

獲取資源(Resources)列表

resourcesRequest := mcp.ListResourcesRequest{}
resources, err := mcpClient.ListResources(ctx, resourcesRequest)
if err != nil {
    panic(err)
}
for _, resource := range resources.Resources {
    fmt.Printf("- uri: %s, name: %s, description: %s, MIME類型: %s\n",
        resource.URI, resource.Name, resource.Description, resource.MIMEType)
}

通過 ListResources 方法,客戶端可以查看服務器上可用的靜態或動態資源信息。

獲取工具(Tools)列表

toolsRequest := mcp.ListToolsRequest{}
tools, err := mcpClient.ListTools(ctx, toolsRequest)
if err != nil {
    panic(err)
}
for _, tool := range tools.Tools {
    fmt.Printf("- %s: %s\n", tool.Name, tool.Description)
    fmt.Println("參數:", tool.InputSchema.Properties)
}

通過 ListTools,客戶端可以獲取所有註冊的工具信息,方便用戶交互式選擇或自動生成表單調用。

調用工具(Tool)

toolRequest := mcp.CallToolRequest{
    Request: mcp.Request{
        Method: "tools/call",
    },
}
toolRequest.Params.Name = "calculate"
toolRequest.Params.Arguments = map[string]any{
    "operation""add",
    "x":         1,
    "y":         1,
}

result, err := mcpClient.CallTool(ctx, toolRequest)
if err != nil {
    panic(err)
}
fmt.Println("調用工具結果:", result.Content[0].(mcp.TextContent).Text)

通過構造 CallToolRequest,客戶端可以向 MCP 服務器發起工具調用請求,並獲取返回的結構化結果。

在此示例中,我們調用了服務器端註冊的 calculate 工具,實現 1 + 1 運算。

完整代碼示例

package main

import (
"context"
"fmt"
"time"

"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
)

func main() {

// 創建一個基於 stdio 的MCP客戶端
 mcpClient, err := client.NewStdioMCPClient(
"./client/server",
  []string{},
 )
if err != nil {
panic(err)
 }
defer mcpClient.Close()

 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

 fmt.Println("初始化 mcp 客戶端...")
 initRequest := mcp.InitializeRequest{}
 initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
 initRequest.Params.ClientInfo = mcp.Implementation{
  Name:    "Client Demo",
  Version: "1.0.0",
 }

// 初始化MCP客戶端並連接到服務器
 initResult, err := mcpClient.Initialize(ctx, initRequest)
if err != nil {
panic(err)
 }
 fmt.Printf(
"\n初始化成功,服務器信息: %s %s\n\n",
  initResult.ServerInfo.Name,
  initResult.ServerInfo.Version,
 )

// 從服務器獲取提示詞列表
 fmt.Println("提示詞列表:")
 promptsRequest := mcp.ListPromptsRequest{}
 prompts, err := mcpClient.ListPrompts(ctx, promptsRequest)
if err != nil {
panic(err)
 }
for _, prompt := range prompts.Prompts {
  fmt.Printf("- %s: %s\n", prompt.Name, prompt.Description)
  fmt.Println("參數:", prompt.Arguments)
 }

// 從服務器獲取資源列表
 fmt.Println()
 fmt.Println("資源列表:")
 resourcesRequest := mcp.ListResourcesRequest{}
 resources, err := mcpClient.ListResources(ctx, resourcesRequest)
if err != nil {
panic(err)
 }
for _, resource := range resources.Resources {
  fmt.Printf("- uri: %s, name: %s, description: %s, MIME類型: %s\n", resource.URI, resource.Name, resource.Description, resource.MIMEType)
 }

// 從服務器獲取工具列表
 fmt.Println()
 fmt.Println("可用工具列表:")
 toolsRequest := mcp.ListToolsRequest{}
 tools, err := mcpClient.ListTools(ctx, toolsRequest)
if err != nil {
panic(err)
 }

for _, tool := range tools.Tools {
  fmt.Printf("- %s: %s\n", tool.Name, tool.Description)
  fmt.Println("參數:", tool.InputSchema.Properties)
 }
 fmt.Println()

// 調用工具
 fmt.Println("調用工具: calculate")
 toolRequest := mcp.CallToolRequest{
  Request: mcp.Request{
   Method: "tools/call",
  },
 }
 toolRequest.Params.Name = "calculate"
 toolRequest.Params.Arguments = map[string]any{
"operation""add",
"x":         1,
"y":         1,
 }
// Call the tool
 result, err := mcpClient.CallTool(ctx, toolRequest)
if err != nil {
panic(err)
 }
 fmt.Println("調用工具結果:", result.Content[0].(mcp.TextContent).Text)
}

小結

本文介紹瞭如何使用 mcp-go 構建一個完整的 MCP 應用,包括服務端和客戶端兩部分。

你好,我是陳明勇,一名熱愛技術、樂於分享的開發者,同時也是開源愛好者。

成功的路上並不擁擠,有沒有興趣結個伴?

關注公衆號,即可獲取我的微信二維碼,加我好友,一起學習一起進步!

如果想和大家一起交流技術的讀者,可以關注公衆號,回覆【加羣】,獲取進羣鏈接。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/8SRAgeLUPJ7US52ICWmmJA