最近 MCP 大火,其實 tRPC 也可以提供泛 HTTP 接入的能力。內網其實已經對 mcp-go 進行了封裝並支持,但是相關代碼還沒有同步到開源版上。
不過實際上,在 tRPC 框架也是可以接入各種泛 HTTP 能力的。本文就以 mcp-go 和 tRPC 結合作為引子,也介紹一下在 Cursor 等 AI 生產力工具中如何開發和使用 MCP 能力吧。
系列文章
- 騰訊 tRPC-Go 教學——(1)搭建服務
- 騰訊 tRPC-Go 教學——(2)trpc HTTP 能力
- 騰訊 tRPC-Go 教學——(3)微服務間調用
- 騰訊 tRPC-Go 教學——(4)tRPC 組件生態和使用
- 騰訊 tRPC-Go 教學——(5)filter、context 和日誌組件
- 騰訊 tRPC-Go 教學——(6)服務發現
- 騰訊 tRPC-Go 教學——(7)服務配置和指標上報
- 騰訊 tRPC-Go 教學——(8)通過泛 HTTP 能力實現和觀測 MCP 服務
MCP 應用場景簡介
LLM的MCP(Model Context Protocol,模型上下文協議)是由 Anthropic 公司主導開發的一種開放協議,旨在為大型語言模型(LLM)與外部數據源、工具和服務提供標準化交互接口,解決傳統開發中因接口碎片化導致的功能擴展難題。其核心設計類似“AI領域的USB-C標準”,通過統一協議打破數據孤島,使LLM能夠安全、高效地調用外部資源。
上面官話看起來其實雲裏霧裏的,我們簡單地説:MCP 就是提供了一個大家都遵循的協議格式, 這樣你可以在你的大模型應用中調用 MCP 服務, 從而為大模型提供更多更強的能力。落地到這兩年特別火的 AI 開發工具 Copilot, Cursor 等,我們在與其對話的時候,其實我們也可以理解為它們的工作流程也是一個定製化的 MCP 流程。現在,我們可以自己給這些大模型工具提供我們自定義的 MCP 能力了。
其實賦予大模型獲取現實世界信息的能力,甚至是修改現實世界信息的能力,相信絕大多數使用 LLM 的開發者們都會想到。MCP 就是這樣的一個能力,它並沒有什麼高深的技術含量,只是由於各種機緣巧合,成為了行業內使用最為廣泛的協議罷了。
mcp-go 框架簡介
目前最流行的 Go MCP 框架就是 mark3labs/mcp-go,從去年 11 月第一個 commit 到現在不到半年就擁有了 3k+ 的 Star,足見其受歡迎程度。
在 mcp-go 的 README 頁中,給出的第一個例子非常簡單。從功能上,它包含了以下幾個部分:
聲明 MCP 服務能力和參數
// Add tool
tool := mcp.NewTool("hello_world",
mcp.WithDescription("Say hello to someone"),
mcp.WithString("name",
mcp.Required(),
mcp.Description("Name of the person to greet"),
),
)
這段代碼聲明瞭一個名為 hello_world 的 MCP 工具。其實名字不重要,更重要的是它剩餘的參數:
- 功能描述:
mcp.WithDescription("Say hello to someone")包含的是對這個工具的完整描述, 這是一份交給大模型閲讀理解工具功能的文檔,因此開發者 務必 在此處將工具的功能完整的描述清楚,最好將工具的出參作詳細説明。只有有了足夠的資料,大模型才能夠在它的問答鏈路中,正確地識別是否應該調用該 MCP 工具 - 入參描述:
mcp.WithString("name", ...這裏是對入參及其格式的描述。本例中,入參name是一個 string 類型參數。MCP 採用古老(但不一定標準)的 jsonrpc 協議進行交互,因此只要是符合 json 定義的數據類型參數,都是合法可用的。
實現 MCP 邏輯
在 mcp-go 框架下,聲明瞭 MCP 工具之後,只需要再實現一個函數用來對接 mcp-go 就行了,當 MCP 請求到來之後,自然會調用你的函數。在 mcp-go 的最簡示例中,就只簡單地返回了一個 hello message:
func helloHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
name, ok := request.Params.Arguments["name"].(string)
if !ok {
return nil, errors.New("name must be a string")
}
return mcp.NewToolResultText(fmt.Sprintf("Hello, %s!", name)), nil
}
這個例子已經是非常清晰地説明了獲取入參、返回出參的過程。當實現了這個 handler 之後,只需再加一行就可以接入到 mcp-go 框架內了:
s.AddTool(tool, helloHandler)
使用 HTTP 接入 MCP
mcp-go 接入 HTTP 的方式
在 mcp-go 的最簡例子中,是使用 server.ServeStdio(s) 啓動 MCP 服務的。正如函數名描述的那樣,這是使用 stdio 來承接數據的輸入和輸出,這適合於個人、純本地環境。不過如果希望將自己的 MCP 服務開放使用的話,就必須得通過 HTTP 來提供 MCP 服務了。在 mcp-go 框架中,如果我們要將 mcp 通過 http 進行服務,其實只需要 server.Start() 函數就行。
我們可以看 server.Start() 方法,可以看到其實它也只是簡單實現了一下 http.Handler 接口,然後使用原生的 net/http 包啓動 HTTP 服務。
這就簡單了,其實 tRPC 也是支持這種模式的。
接入方法
在原生的 net/http 中,http.Handler 接口要求實現一個方法 ServeHTTP(http.ResponseWriter, *http.Request)。而在 tRPC-Go 中,則是按照 path 註冊 handler 的模式,每一個 handler 的類型與 http.Handler 其實差不多,只是多了一個 error 返回而已,大不了就返回 nil 嘛。
具體的實現方式,讀者可以看我的實現代碼的 serveHTTP 函數:
// serveHTTP 啓動HTTP服務
func serveHTTP(svc *trpcserver.Server, mcpSvr *server.MCPServer) {
// 創建SSE服務器
// SSE端點會自動變為 /mcp/sse
// 消息端點會自動變為 /mcp/message
sseServer := server.NewSSEServer(mcpSvr, server.WithBasePath("/mcp"))
wrappedHTTP := &wrappedHTTP{Handler: sseServer}
thttp.HandleFunc("/mcp/sse", wrappedHTTP.ServeHTTP)
thttp.HandleFunc("/mcp/message", wrappedHTTP.ServeHTTP)
thttp.RegisterNoProtocolService(svc.Service("trpc.amc.demo.mcp"))
if err := svc.Serve(); err != nil {
log.Errorf("TRPC server error: '%v'", err)
}
}
MCP path
這段函數首先需要注意的部分, 在函數中的註釋已經説明了。簡單地説,通過 HTTP 暴露 MCP 服務的話,是通過 sse 和 message 兩個接口來實現的,其中前者負責給 MCP client 下發臨時憑證,後者則負責主要的數據交互。sse 和 message 兩個接口我們都可以人工指定,默認就是 base path + /sse 和 base path + /message 的模式。
HTTP Wrapping
第二個需要注意的點,就是我定義了一個 wrappedHTTP 實例,用來包裝一層 mcp-go 的 HTTP 函數。這主要是為了適配標準 net/http 的 handler 和 tRPG 的 handler 格式(加一個 error 返回)。此外,讀者也可以具體看這個類型的 實現,除了加 error 這一點之外,還攔截了一下 http 收包和回包的過程,便於我們觀察 MCP 的交互過程。
tRPC Service
第三點則是 thttp.RegisterNoProtocolService(svc.Service("trpc.amc.demo.mcp"))。在 net/http 中,啓動 HTTP 服務的時候,監聽在哪一個網卡、什麼端口,是需要在代碼中傳入的。而 tRPC 框架則將這些參數轉移到了 trpc_go.yaml 配置文件中。讀者可以看看 示例配置。
因此,這一句主要就是將 HTTP 服務與 yaml 配置文件中的具體項目綁定起來。
啓動 MCP 服務
讀者可以把我的倉庫 clone 下來,然後到 app/mcp 目錄下執行 go run . 命令,就可以在 localhost 的 8080 端口下啓動一個 MCP HTTP 服務——如果要修改啓動參數,可以在代碼中的 trpc_go.yaml 文件中修改。
Cursor 配置
因為我目前用的 IDE 是 Cursor,因此我就以 Cursor 為例子説明吧。對於 Mac 用户,打開 Cursor 之後,在菜單欄中的 Cursor 項下拉,找到 Cursor Settings:
在 Settings tab,找到 MCP:
選擇 "+ Add new global MCP server",填入以下內容:
{
"mcpServers": {
"demo_mcp": {
"url": "http://localhost:8080/mcp/sse"
}
}
}
這個 /mcp/sse 的路徑,就對應了前文我提到的 sse 接口。
如果你的服務還沒啓動,你關掉配置頁之後可能會發現出現這樣的錯誤:
如果你按照我前文所説的 go run . 啓動了的話,或者是啓動之後,點一下右上角的刷新按鈕,那麼就會看到樸實無華的綠燈
測試
綠燈亮起後,我們在 Cursor 中驗證一下 MCP 工具是否生效:
這裏我把思考過程和 MCP 調用過程都展開來了。可以看到,Cursor 在思考中推斷出可以用 MCP 工具來獲取當前的真實時間,然後在根據它自己的知識,推測出印度時間與北京時間的差異,最後經過 MCP 返回的數據,計算出印度時間。我們都知道,大模型的時間是滯後的,這裏給出了正確的時間,也就説明了 MCP 的有效性。
觀察 MCP 交互
前文我提到了,我使用 wrappedHTTP 攔截了輸入和輸出請求。讀者可以通過服務的標準輸出查看 Cursor 和服務的交互過程。
建立連接
首先,Cursor 啓動後,首先通過 /mcp/sse 接口與 server 建立連接並獲取實際交互的接口以及 token。就本例子來説,從日誌中我們可以觀測到,Cursor 向 /mcp/sse 發起了一個 GET 請求,然後 mcp-go 返回了以下數據:
event: endpoint
data: /mcp/message?sessionId=d4f3737e-d4ae-48b1-a1c9-7661baff8814
MCP 功能探測
如果我們點擊 Cursor 的刷新按鈕,Cursor 根據上一輪的響應,發起 /mcp/message?sessionId=d4f3737e-d4ae-48b1-a1c9-7661baff8814 請求,這次是一個 POST 請求,請求正文格式化之後為:
{
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": true,
"prompts": false,
"resources": true,
"logging": false,
"roots": {
"listChanged": false
}
},
"clientInfo": {
"name": "cursor-vscode",
"version": "1.0.0"
}
},
"jsonrpc": "2.0",
"id": 0
}
mcp-go 按照我們的配置,返回:
{
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "ip-mcp",
"version": "1.0.0"
}
}
}
這是對整個 server 的初始化,不重要。接着 Cursor 再次發起一個 POST 請求:
{"method":"notifications/initialized","jsonrpc":"2.0"}
MCP 響應:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"description": "\n根據行政區劃代碼獲取行政區劃名稱, 返回 JSON 格式, 包含以下字段:\n\n- province: 規範化的省級行政區名稱\n- city: 規範化的市級行政區名稱\n- county: 規範化的區縣級行政區名稱\n- province_code: 省級行政區代碼, 如廣東省為 44\n- city_code: 市級行政區代碼, 如廣州市為 01\n- county_code: 區縣級行政區代碼, 如越秀區為 04\n",
"inputSchema": {
"type": "object",
"properties": {
"city": {"description": "市級行政區名稱","type": "string"},
"county": {"description": "縣級行政區名稱","type": "string"},
"province": {"description": "省級行政區名稱","type": "string"}
},
"required": ["province"]
},
"name": "admin_division_query"
},
{
"description": "\n返回當前的時間,以 JSON 格式輸出,包含以下字段:\n\n- utc: 格式為 \"YYYY-MM-DD HH:MM:SS\" 的當前 UTC 時間\n- beijing: 格式為 \"YYYY-MM-DD HH:MM:SS\" 的當前北京時間\n- timestamp_sec: 當前時間戳,單位為秒\n",
"inputSchema": {
"type": "object",
"properties": {}
},
"name": "datetime_query"
}
]
}
}
這就是我們在代碼中定義的兩個工具了
MCP 邏輯交互
這次我們不用前面的 datetime 工具了,我們來問一個帶參數的。同樣,我展開了思考和 MCP 交互過程:
這裏大模型調用了兩次,兩次請求大同小異,我們就看第一個請求。Cursor 發起了一個請求 /mcp/message?sessionId=bac39787-91e9-4b4a-8503-090af903a662,可以看到 session ID 變化了,這是在某次 /mcp/sse 刷新的,這不重要。
這次請求依舊是 POST,請求正文為:
{
"method": "tools/call",
"params": {
"name": "admin_division_query",
"arguments": {
"province": "雲南省",
"city": "西雙版納"
}
},
"jsonrpc": "2.0",
"id": 4
}
參數、function call 名稱,都很清晰。MCP 的響應為
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [
{
"type": "text",
"text": "{\"province\":\"雲南省\",\"province_code\":\"53\",\"city\":\"西雙版納傣族自治州\",\"city_code\":\"28\"}"
}
]
}
}
我是使用 text 格式返回的,然後我把字段序列化之後存在這個 text 之後。當然我們也可以看到,Cursor 的大模型拿到這個 text 之後,成功把其中的信息解析出來了。這也可見 LLM 的泛用性之強。
總結和應用
好了,這篇文章我們從MCP的基本概念聊到了如何在tRPC-Go中實現MCP服務。從最簡單的mcp-go框架示例出發,我們看到了如何在tRPC-Go框架下通過HTTP接口來提供MCP服務能力。通過幾個實際例子,我們觀察到了MCP服務與Cursor這樣的大模型工具之間建立連接、探測能力和交互的全過程。
如果你想開發自己的MCP服務,希望這篇文章能給你一些啓發,也順便展示了一下 tRPC 實現普通泛 HTTP 服務的能力。
其實我自己還實現了通過 IP 地址獲取本機地理位置的功能,然後再實現了一個獲取天氣信息的 API(基於高德 API),實驗後我們可以發現,LLM 也能夠根據所有的 MCP 工具的能力,將不同階段的參數串起來,最終實現我想要的要求,看 Cursor 的思考和調用過程其實還是蠻有趣的:
參考資料
- 使用Go開發MCP Server, 太簡單了! - ThinkInAI 社區
- 如何使用Golang創建MCP Server - 潘子夜個人博客
- 幾十行代碼輕鬆打造屬於自己的MCP服務器
- 認識 MCP Go 工具
本文章採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。
原作者: amc,原文發佈於騰訊雲開發者社區,也是本人的博客。歡迎轉載,但請註明出處。
原文標題:《騰訊 tRPC-Go 教學——(8)通過泛 HTTP 能力實現和觀測 MCP 服務》
發佈日期:2025-04-18
原文鏈接:https://cloud.tencent.com/developer/article/2514815。