上手 MCP 官方 Go SDK:一份面向實戰的入門指南
大家好,我是 Tony Bai。
隨着大型語言模型(LLM)的能力邊界不斷擴展,“function calling”或 “tool use” 已成爲釋放其潛力的關鍵。MCP(Model Context Protocol)正是爲此而生,它定義了一套標準的、與模型無關的通信規範,使得任何應用都能以 “工具” 的形式被 LLM 調用。
長期以來,mcp 官方都沒有發佈 go-sdk,Go 社區也一直在使用像 mark3labs/mcp-go 這樣的流行的第三方庫。直到 Google Go 團隊安排專人協助 mcp 組織進行了 Go SDK 的設計。
7 月初,該 Go SDK 正式以modelcontextprotocol/go-sdk倉庫的形式對外開源發佈,這是 Go 語言在這一浪潮中的一個里程碑事件。它的意義遠超一個普通的庫:
-
標準化與權威性:作爲官方 SDK,它爲 Go 開發者提供了與 MCP 規範緊密同步的、最權威的實現。這意味着更少的兼容性問題和更可靠的長期維護。
-
Go 語言哲學:該 SDK 的設計充滿了 Go 的味道——簡潔、高效、強類型和高併發。它鼓勵開發者編寫慣用的 Go 代碼,而不是將其他語言的範式生搬硬套過來。
-
生態系統的基石:官方 SDK 的出現,將極大地促進 Go AI 生態的繁榮。開發者可以基於這個穩定的基石,構建出更復雜、更健壯的上層應用、框架和平臺。
簡而言之,它不僅僅是一個工具,更是 Go 語言與 AI 模型世界之間的一座標準化橋樑。
MCP 服務架構:多種通信模式
MCP 協議設計了靈活的通信方式,以適應不同的部署場景。官方 Go SDK 對此提供了出色的支持。主要包括以下幾種類型:
-
標準輸入 / 輸出 (Stdio):這是最簡單的模式,客戶端通過啓動一個子進程(MCP Server),並通過其
stdin和stdout進行 JSON-RPC 通信。這種模式非常適合本地工具、CLI 插件或 Sidecar 模型的場景。我們將使用此模式構建基礎工具服務和文件系統服務。 -
HTTP 流式傳輸 (Streamable HTTP):這是 MCP 規範中最新、最推薦的 HTTP 模式。它通過一系列的
GET和POST請求實現了一個可恢復的、無狀態的會話管理機制,非常適合構建可擴展、高可用的網絡服務。我們將使用此模式構建多路複用 HTTP 服務。 -
服務器發送事件 (SSE):這是早期 MCP 規範中的一種 HTTP 模式,在社區版 SDK 中較爲常見。官方 SDK 也提供了
SSEHandler以支持這種模式,但新的StreamableHTTPHandler功能更強大,是未來的方向。
核心概念速覽
儘管我們在此不深入探討其完整的設計文檔,但理解以下幾個核心概念對於後續的實踐至關重要:
-
Server:代表一個 MCP 服務實例。它本身是無狀態的,是工具(Tools)、提示(Prompts)和資源(Resources)等能力的集合。 -
Client:代表一個 MCP 客戶端。 -
Session:無論是ServerSession還是ClientSession,它都代表一個已經建立的、具體的、有狀態的連接。所有的交互都通過會話(Session)進行。 -
Transport:負責建立底層通信的抽象層。它定義了客戶端和服務器如何交換 JSON-RPC 消息。
Server 定義了 “能做什麼”,而 Session 則是 “正在與誰通信” 的實例。這種解耦設計爲構建靈活、可擴展的服務提供了基礎。
實戰:構建三種典型的 MCP 服務
現在,讓我們動手構建幾個實用的 MCP 服務,來體驗官方 SDK 的強大功能。
場景一:基礎工具服務 (Greeter)
這是最經典的 “Hello, World” 場景,通過 stdio 運行,用於展示如何定義一個簡單的工具。
完整代碼:greeter/main.go
// mcp-go-sdk/greeter/main.go
package main
import (
"context"
"fmt"
"log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// HiParams 定義了工具的輸入參數,強類型保證
type HiParams struct {
Name string`json:"name"`
}
// SayHi 是工具的具體實現
func SayHi(ctx context.Context, _ *mcp.ServerSession, params *mcp.CallToolParamsFor[HiParams]) (*mcp.CallToolResultFor[any], error) {
resultText := fmt.Sprintf("Hi %s, welcome to the Go MCP world!", params.Arguments.Name)
return &mcp.CallToolResultFor[any]{
Content: []mcp.Content{
&mcp.TextContent{Text: resultText},
},
}, nil
}
func main() {
// 1. 創建 Server 實例
server := mcp.NewServer("greeter-server", "1.0.0", nil)
// 2. 添加工具
// NewServerTool 利用泛型和反射自動生成輸入 schema
server.AddTools(
mcp.NewServerTool("greet", "Say hi to someone", SayHi),
)
// 3. 通過 StdioTransport 運行服務,它會監聽標準輸入/輸出
log.Println("Greeter server running over stdio...")
if err := server.Run(context.Background(), mcp.NewStdioTransport()); err != nil {
log.Fatalf("Server run failed: %v", err)
}
}
在不依賴任何特殊客戶端的情況下,我們可以通過管道向這個基於 stdio 的服務發送一系列原生的 JSON-RPC 消息,來模擬完整的客戶端握手和工具調用流程。
步驟一:運行服務併發送請求序列
打開你的終端,執行以下命令。這行命令會使用 printf 來確保每個 JSON 對象都以換行符分隔,模擬一個完整的會話流程:
-
發送
initialize請求,啓動會話。 -
發送
initialized通知,確認會話建立。 -
發送
tools/call請求,調用greet工具。
在 greeter 目錄下執行下面命令:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"test-cli","version":"0.1"},"protocolVersion":"2025-03-26"}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"greet","arguments":{"name":"Go MCP Enthusiast"}}}' \
| go run main.go
預期輸出:
服務會處理這三個消息,並對兩個有 ID 的請求(initialize 和 tools/call)作出響應。你將看到兩個 JSON-RPC 響應對象被打印到標準輸出(順序可能會因併發處理而不同,但內容是固定的):
2025/07/08 17:05:46 Greeter server running over stdio...
{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"completions":{},"logging":{},"prompts":{"listChanged":true},"resources":{"listChanged":true},"tools":{"listChanged":true}},"protocolVersion":"2025-03-26","serverInfo":{"name":"greeter-server","version":"1.0.0"}}}
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Hi Go MCP Enthusiast, welcome to the Go MCP world!"}]}}
看到這兩個響應,證明我們的 Greeter 服務已經成功地完成了握手並正確響應了工具調用。
場景二:文件系統服務 (File System Server)
這個場景也通過 stdio 運行,展示瞭如何通過 Resource 機制,安全地向 LLM 暴露本地文件系統的讀寫能力。
// fileserver/main.go
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func main() {
server := mcp.NewServer("filesystem-server", "1.0.0", nil)
pwd, err := os.Getwd()
if err != nil {
log.Fatalf("Failed to get current directory: %v", err)
}
log.Printf("File server serving from directory: %s", pwd)
// 使用我們自己實現的 File Handler
handler := createFileHandler(pwd)
// 添加一個虛構的資源,用於列出目錄內容
server.AddResources(&mcp.ServerResource{
Resource: &mcp.Resource{
URI: "mcp://fs/list",
Name: "list_files",
Description: "List all non-directory files in the current directory.",
},
Handler: listDirectoryHandler(pwd),
})
// 添加一個資源模板,用於讀取指定的文件
server.AddResourceTemplates(&mcp.ServerResourceTemplate{
ResourceTemplate: &mcp.ResourceTemplate{
Name: "read_file",
URITemplate: "file:///{+filename}",
Description: "Read a specific file from the directory. 'filename' is the relative path to the file.",
},
Handler: handler,
})
log.Println("File system server running over stdio...")
if err := server.Run(context.Background(), mcp.NewStdioTransport()); err != nil {
log.Fatalf("Server run failed: %v", err)
}
}
// createFileHandler 是一個簡化的、用於演示的 ResourceHandler 工廠函數。
func createFileHandler(baseDir string) mcp.ResourceHandler {
returnfunc(ctx context.Context, ss *mcp.ServerSession, params *mcp.ReadResourceParams) (*mcp.ReadResourceResult, error) {
// 注意:在生產環境中,這裏必須調用 ss.ListRoots() 來獲取客戶端授權的
// 根目錄,並進行嚴格的安全檢查。
// 爲了讓這個入門示例能用簡單的管道命令驗證,我們暫時省略了這個雙向調用。
requestedPath := filepath.Join(baseDir, filepath.FromSlash(params.URI[len("file:///"):]))
data, err := os.ReadFile(requestedPath)
if err != nil {
if os.IsNotExist(err) {
returnnil, mcp.ResourceNotFoundError(params.URI)
}
returnnil, fmt.Errorf("failed to read file: %w", err)
}
return &mcp.ReadResourceResult{
Contents: []*mcp.ResourceContents{
{URI: params.URI, MIMEType: "text/plain", Text: string(data)},
},
}, nil
}
}
// listDirectoryHandler 是一個自定義的 ResourceHandler,用於實現列出目錄的功能
func listDirectoryHandler(dir string) mcp.ResourceHandler {
returnfunc(ctx context.Context, ss *mcp.ServerSession, params *mcp.ReadResourceParams) (*mcp.ReadResourceResult, error) {
// 同樣,爲簡化本地驗證,暫時省略對 ss.ListRoots() 的調用。
entries, err := os.ReadDir(dir)
if err != nil {
returnnil, fmt.Errorf("failed to read directory: %w", err)
}
var fileList string
for _, e := range entries {
if !e.IsDir() {
fileList += e.Name() + "\n"
}
}
if fileList == "" {
fileList = "(The directory is empty or contains no files)"
}
return &mcp.ReadResourceResult{
Contents: []*mcp.ResourceContents{
{URI: params.URI, MIMEType: "text/plain", Text: fileList},
},
}, nil
}
}
文件服務同樣需要完整的握手流程。我們將用與上面類似的方式來驗證其功能。
步驟一:準備測試文件
首先,在你的項目根目錄下創建一個簡單的文本文件。
echo "Hello from the File System MCP Server!" > my-test-file.txt
步驟二:驗證 “列出文件” 功能
我們發送包含 initialize、initialized 和 resources/read 的請求序列。
在 fileserver 下執行下面命令:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"test-cli","version":"0.1"},"protocolVersion":"2025-03-26"}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' \
'{"jsonrpc":"2.0","id":2,"method":"resources/read","params":{"uri":"mcp://fs/list"}}' \
| go run main.go
預期輸出:
你將看到 initialize 的響應,以及 resources/read 的響應,後者包含了目錄文件列表。
2025/07/08 18:13:47 File system server running over stdio...
{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"completions":{},"logging":{},"prompts":{"listChanged":true},"resources":{"listChanged":true},"tools":{"listChanged":true}},"protocolVersion":"2025-03-26","serverInfo":{"name":"filesystem-server","version":"1.0.0"}}}
{"jsonrpc":"2.0","id":2,"result":{"contents":[{"uri":"mcp://fs/list","mimeType":"text/plain","text":"go.mod\ngo.sum\nmain.go\nmy-test-file.txt\n"}]}}
步驟三:驗證 “讀取文件” 功能
現在,我們發送請求序列來讀取 my-test-file.txt 的內容。
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"test-cli","version":"0.1"},"protocolVersion":"2025-03-26"}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' \
'{"jsonrpc":"2.0","id":3,"method":"resources/read","params":{"uri":"file:///my-test-file.txt"}}' \
| go run main.go
預期輸出:
除了 initialize 的響應外,你將看到包含文件內容的 resources/read 響應。
2025/07/08 18:15:12 File server serving from directory: /Users/tonybai/go/src/github.com/bigwhite/experiments/mcp-go-sdk/fileserver
2025/07/08 18:15:12 File system server running over stdio...
{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"completions":{},"logging":{},"prompts":{"listChanged":true},"resources":{"listChanged":true},"tools":{"listChanged":true}},"protocolVersion":"2025-03-26","serverInfo":{"name":"filesystem-server","version":"1.0.0"}}}
{"jsonrpc":"2.0","id":3,"result":{"contents":[{"uri":"file:///my-test-file.txt","mimeType":"text/plain","text":"Hello from the File System MCP Server\n"}]}}
步驟四:清理
測試完成後,可以刪除測試文件。
rm my-test-file.txt
場景三:多路複用 HTTP 服務 (Multi-Service HTTP Server)
這個場景展示瞭如何使用 StreamableHTTPHandler 在單個 HTTP 端點上提供多個不同的 MCP 服務。
完整代碼:httpserver/main.go
package main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// HiParams 和 SayHi 函數與場景一相同
type HiParams struct{ Name string`json:"name"` }
func SayHi(ctx context.Context, _ *mcp.ServerSession, params *mcp.CallToolParamsFor[HiParams]) (*mcp.CallToolResultFor[any], error) {
resultText := fmt.Sprintf("Hi %s, this response is from the HTTP server!", params.Arguments.Name)
return &mcp.CallToolResultFor[any]{
Content: []mcp.Content{&mcp.TextContent{Text: resultText}},
}, nil
}
// AddParams 和 Add 工具的實現
type AddParams struct{ A, B int }
func Add(_ context.Context, _ *mcp.ServerSession, params *mcp.CallToolParamsFor[AddParams]) (*mcp.CallToolResultFor[any], error) {
result := params.Arguments.A + params.Arguments.B
return &mcp.CallToolResultFor[any]{
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("The sum is: %d", result)}},
}, nil
}
func main() {
// 1. 創建 Greeter 服務實例
greeterServer := mcp.NewServer("greeter-service", "1.0", nil)
greeterServer.AddTools(mcp.NewServerTool("greet", "Say hi", SayHi))
// 2. 創建 Math 服務實例
mathServer := mcp.NewServer("math-service", "1.0", nil)
mathServer.AddTools(mcp.NewServerTool("add", "Add two integers", Add))
// 3. 創建 StreamableHTTPHandler
handler := mcp.NewStreamableHTTPHandler(func(request *http.Request) *mcp.Server {
log.Printf("Routing request for URL: %s\n", request.URL.Path)
switch request.URL.Path {
case"/greeter":
return greeterServer
case"/math":
return mathServer
default:
returnnil// 返回 nil 將導致 404 Not Found
}
}, nil)
// 4. 啓動標準的 Go HTTP 服務器
addr := ":8080"
log.Printf("Multi-service MCP server listening at http://localhost%s\n", addr)
if err := http.ListenAndServe(addr, handler); err != nil {
log.Fatalf("HTTP server failed: %v", err)
}
}
與基於 stdio 的簡單服務不同,驗證 Streamable HTTP 服務使用 curl 等工具會非常繁瑣。這是因爲 MCP 是一個有狀態的協議,要求客戶端在發送工具調用之前,必須先完成一個包含 initialize 請求和 initialized 通知的多步 “握手” 流程來建立會話。
一個簡單的 curl 命令無法管理這種有狀態的交互。因此,最理想的驗證方式是使用一個真正的 MCP 客戶端。我們將在下一節構建這樣一個客戶端——agent,然後用集成了大模型的它來統一驗證我們創建的所有三個服務,包括這個 HTTP 服務。
集成大模型:讓 Go Agent 直接成爲 MCP 客戶端
在前面的章節中,我們成功構建了三種不同類型的 MCP 服務。現在,是時候將它們與 AI 大模型(以 DeepSeek 爲例)集成,構建一個能夠調度這些 mcp server 工具的智能 Agent 了。
一個常見的思路可能是創建一個通用的命令行工具(CLI)來調用這些服務,然後讓我們的 Go Agent 程序去執行這個 CLI。然而,既然我們的 Agent 本身就是用 Go 編寫的,一個更優雅、更高效、更符合 Go 語言習慣的方式是:讓 Agent 程序直接導入 modelcontextprotocol/go-sdk,將自己作爲原生的 MCP 客戶端來與服務通信。
這種方法避免了不必要的進程開銷和數據序列化,使得整個系統更加內聚和高性能。接下來,我們將編寫這樣一個 Go Agent。
Part 1: 編寫 Go Agent 程序
這個程序將承擔所有角色:它既是與 DeepSeek 模型對話的主循環,也是調用我們 MCP 服務的客戶端。
準備工作:
-
安裝 OpenAI Go SDK:
go get github.com/openai/openai-go -
獲取 DeepSeek API Key,並設置爲環境變量:
export DEEPSEEK_API_KEY="your-api-key"
完整代碼:agent/main.go
// agent/main.go
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"strings"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/openai/openai-go"
"github.com/openai/openai-go/option"
)
// serverConfig 結構體用於管理不同 MCP 服務的連接信息
type serverConfig struct {
ServerCmd string// 用於 stdio 服務
HTTPAddr string// 用於 http 服務
}
// toolRegistry 映射對 LLM 友好的工具別名到其服務配置
var toolRegistry = map[string]serverConfig{
"greet": {ServerCmd: "go run ../greeter/main.go"},
"add": {HTTPAddr: "http://localhost:8080/math"},
"list_files": {ServerCmd: "go run ../fileserver/main.go"},
"read_file": {ServerCmd: "go run ../fileserver/main.go"},
}
// invokeMCPTool 是 Agent 的核心函數,負責直接與 MCP 服務通信
func invokeMCPTool(toolAlias string, arguments map[string]interface{}) (string, error) {
config, ok := toolRegistry[toolAlias]
if !ok {
return"", fmt.Errorf("unknown tool alias: %s", toolAlias)
}
// 1. 將 LLM 友好的別名和參數,轉換爲真正的 MCP 請求
mcpToolName := toolAlias
mcpArguments := arguments
if toolAlias == "list_files" {
mcpToolName = "resources/read"
mcpArguments = map[string]interface{}{"uri": "mcp://fs/list"}
} elseif toolAlias == "read_file" {
mcpToolName = "resources/read"
if filename, ok := arguments["filename"].(string); ok {
mcpArguments = map[string]interface{}{"uri": "file:///" + filename}
} else {
return"", fmt.Errorf("tool 'read_file' requires a 'filename' argument")
}
}
// 2. 創建 MCP 客戶端實例
client := mcp.NewClient("go-agent", "1.0", nil)
// 3. 根據配置選擇並創建 Transport
var transport mcp.Transport
if config.ServerCmd != "" {
cmdParts := strings.Fields(config.ServerCmd)
transport = mcp.NewCommandTransport(exec.Command(cmdParts[0], cmdParts[1:]...))
} else {
transport = mcp.NewStreamableClientTransport(config.HTTPAddr, nil)
}
// 4. 授權客戶端訪問本地文件系統(僅對文件服務調用有效)
client.AddRoots(&mcp.Root{URI: "file://./"})
// 5. 連接到服務器,建立會話
ctx := context.Background()
session, err := client.Connect(ctx, transport)
if err != nil {
return"", fmt.Errorf("failed to connect to MCP server for tool %s: %w", toolAlias, err)
}
defer session.Close() // 每次調用都是一個獨立的會話,確保關閉
// 6. 執行調用並處理結果
var resultText string
if mcpToolName == "resources/read" {
res, err := session.ReadResource(ctx, &mcp.ReadResourceParams{
URI: mcpArguments["uri"].(string),
})
if err != nil {
return"", fmt.Errorf("ReadResource failed: %w", err)
}
var sb strings.Builder
for _, c := range res.Contents {
sb.WriteString(c.Text)
}
resultText = sb.String()
} else {
res, err := session.CallTool(ctx, &mcp.CallToolParams{
Name: mcpToolName,
Arguments: mcpArguments,
})
if err != nil {
return"", fmt.Errorf("CallTool failed: %w", err)
}
if res.IsError {
return"", fmt.Errorf("tool execution failed: %s", res.Content[0].(*mcp.TextContent).Text)
}
resultText = res.Content[0].(*mcp.TextContent).Text
}
return resultText, nil
}
func main() {
apiKey := os.Getenv("DEEPSEEK_API_KEY")
if apiKey == "" {
log.Fatal("DEEPSEEK_API_KEY environment variable not set.")
}
client := openai.NewClient(
option.WithAPIKey(apiKey),
option.WithBaseURL("https://api.deepseek.com/v1"),
)
// 爲所有工具使用合法的名稱,特別是爲 `resources/read` 創建別名
tools := []openai.ChatCompletionToolParam{
{
Function: openai.FunctionDefinitionParam{
Name: "greet",
Description: openai.String("Say hi to someone."),
Parameters: openai.FunctionParameters{
"type": "object", "properties": map[string]interface{}{"name": map[string]string{"type": "string", "description": "Name of the person to greet"}}, "required": []string{"name"},
},
},
},
{
Function: openai.FunctionDefinitionParam{
Name: "add",
Description: openai.String("Add two integers."),
Parameters: openai.FunctionParameters{
"type": "object", "properties": map[string]interface{}{"A": map[string]string{"type": "integer"}, "B": map[string]string{"type": "integer"}}, "required": []string{"A", "B"},
},
},
},
{
Function: openai.FunctionDefinitionParam{
Name: "list_files",
Description: openai.String("List all non-directory files in the current project directory."),
Parameters: openai.FunctionParameters{"type": "object", "properties": map[string]interface{}{}},
},
},
{
Function: openai.FunctionDefinitionParam{
Name: "read_file",
Description: openai.String("Read the content of a specific file."),
Parameters: openai.FunctionParameters{
"type": "object", "properties": map[string]interface{}{"filename": map[string]string{"type": "string", "description": "The name of the file to read."}}, "required": []string{"filename"},
},
},
},
}
messages := []openai.ChatCompletionMessageParamUnion{
openai.SystemMessage("You are a helpful assistant with access to local tools. You must call tools by using the tool_calls response format. Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."),
openai.UserMessage("Hi, can you greet my friend Alex, add 5 and 7, and then list the files in my project?"),
}
ctx := context.Background()
for i := 0; i < 5; i++ {
log.Println("--- Sending request to DeepSeek ---")
resp, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{Model: "deepseek-chat", Messages: messages, Tools: tools})
if err != nil {
log.Fatalf("ChatCompletion error: %v\n", err)
}
iflen(resp.Choices) == 0 {
log.Fatal("No choices returned from API")
}
msg := resp.Choices[0].Message
messages = append(messages, msg.ToParam())
if msg.ToolCalls != nil {
for _, toolCall := range msg.ToolCalls {
functionName := toolCall.Function.Name
var arguments map[string]interface{}
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &arguments); err != nil {
log.Fatalf("Failed to unmarshal function arguments: %v", err)
}
log.Printf("--- LLM wants to call tool: %s with args: %v ---\n", functionName, arguments)
// 直接調用我們的 Go 函數,該函數內建了 MCP 客戶端邏輯
toolResult, err := invokeMCPTool(functionName, arguments)
if err != nil {
log.Printf("Tool call failed: %v\n", err)
toolResult = fmt.Sprintf("Error executing tool: %v", err)
}
log.Printf("--- Tool result: ---\n%s\n---------------------\n", toolResult)
messages = append(messages, openai.ToolMessage(toolResult, toolCall.ID))
}
continue
}
log.Println("--- Final response from LLM ---")
log.Println(msg.Content)
return
}
log.Println("Reached max conversation turns.")
}
注:上述代碼使用了 OpenAI 的 function calling api,不過即便不用 function calling api,通過 prompt 依然可以實現 mcp server 接口的調用 (需要自行解析 response),大家可以自行實現一下。
Part 2: 集成驗證
現在,我們的 agent 程序已經是一個功能齊全的、內建了 MCP 客戶端的智能體。讓我們來驗證它的工作流程。
-
啓動
httpserver:agent會通過 HTTP 調用math服務,所以我們必須先在後臺運行它。go run ./httpserver/main.go & HTTP_PID=$! -
創建測試文件: 爲文件服務準備一個可供讀取的文件。
echo "This file will be read by our Go AI Agent." > agent-test.txt -
運行
agent程序: 確保你的DEEPSEEK_API_KEY已經設置。go run ./agent/main.go
預期的輸出流程:
你的終端將清晰地展示 AI Agent 的思考和行動鏈。它直接在內部與各個 MCP 服務進行高效的 Go-to-Go 通信。
$DEEPSEEK_API_KEY=<your_deepseek_api_key> go run main.go
2025/07/08 19:17:42 --- Sending request to DeepSeek ---
2025/07/08 19:17:53 --- LLM wants to call tool: greet with args: map[name:Alex] ---
2025/07/08 19:17:53 --- Tool result: ---
Hi Alex, welcome to the Go MCP world!
---------------------
2025/07/08 19:17:53 --- LLM wants to call tool: add with args: map[A:5 B:7] ---
2025/07/08 19:17:53 --- Tool result: ---
The sum is: 12
---------------------
2025/07/08 19:17:53 --- LLM wants to call tool: list_files with args: map[] ---
2025/07/08 19:17:53 --- Tool result: ---
go.mod
go.sum
main.go
---------------------
2025/07/08 19:17:53 --- Sending request to DeepSeek ---
2025/07/08 19:18:07 --- Final response from LLM ---
2025/07/08 19:18:07 Here's what you asked for:
1. **Greeting for Alex**: Hi Alex, welcome to the Go MCP world!
2. **Addition of 5 and 7**: The sum is 12.
3. **Files in your project**:
- `go.mod`
- `go.sum`
- `main.go`
Let me know if you'd like to do anything else!
4. 清理:
kill $HTTP_PID
rm agent-test.txt
小結
通過本次從零到一的實踐,我們不僅學習瞭如何使用 modelcontextprotocol/go-sdk 構建支持不同通信協議的 MCP 服務,更重要的是,我們探索並實現了將 Go Agent 程序直接作爲原生 MCP 客戶端的實踐。
這種直接通過庫調用的內聚架構,相比於通過外部 CLI 工具進行解耦的方式,充分發揮了 Go 語言的優勢:
-
高性能:避免了不必要的進程創建和數據序列化開銷,使得工具調用和響應鏈條更短、更高效。
-
強類型與健壯性:整個調用鏈路都在 Go 的類型系統內完成,錯誤處理清晰,代碼更易於維護和調試。
-
簡潔的工程實現:它展示了一種更加優雅和符合 Go 語言習慣的工程模式,讓 AI Agent 的構建過程如同編寫任何一個普通的 Go 應用一樣自然。
modelcontextprotocol/go-sdk 不僅僅是一個協議的實現,它更像一個宣言:Go 語言憑藉其出色的併發模型、強大的類型系統和簡潔的工程哲學,完全有能力成爲構建下一代高性能、高可靠性 AI Agent 和工具化應用的首選後端語言。
雖然官方 SDK 仍在快速迭代中,但其展現出的潛力和清晰的設計哲學已經足夠令人振奮。我們鼓勵所有對 Go 和 AI 結合感興趣的開發者,立即上手體驗。這個 SDK 無疑將成爲連接你的 Go 程序與廣闊智能模型世界之間最堅固、最標準的橋樑。
本文涉及源碼可以在這裏下載 - https://github.com/bigwhite/experiments/tree/master/mcp-go-sdk
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/JvPuH1q0QMm-Ak-F1TeYvg