一、需求背景
Go 語言開發 MCP 服務,并在?Goframe 框架下實現 Http 反向代理,代理該 MCP 服務。
二、效果演示
三、Goframe框架簡介
GoFrame 是一款模塊化、低耦合設計、高性能的Go 語言開發框架。包含了常用的基礎組件和開發工具,既可以作為完整的業務項目框架使用也可以作為獨立的組件庫使用。
官網地址:GoFrame官網 - 類似PHP-Laravel,Java-SpringBoot的Go語言開發框架
四、MCP 簡介
4.1 概念
MCP (Model Context Protocol) 是一個開放協議,用于標準化應用程序如何向 LLM 提供上下文。可以將 MCP 想象成 AI 應用程序的 USB-C 接口。
官網地址(中文版):MCP中文簡介 – MCP 中文站(Model Context Protocol 中文)
官網地址(英文版):Introduction - Model Context Protocol
從本質上講,MCP 遵循客戶端-服務器架構,其中主機應用程序可以連接到多個服務器:
用戶、大模型及MCP服務的交互流程:
4.2 消息格式
MCP 使用 JSON-RPC 2.0 作為其傳輸格式。傳輸層負責將 MCP 協議消息轉換為 JSON-RPC 格式進行傳輸,并將接收到的 JSON-RPC 消息轉換回 MCP 協議消息。
4.2.1 請求
{jsonrpc: "2.0";id: string | number;method: string;params?: {[key: string]: unknown;};
}
4.2.2 響應
{jsonrpc: "2.0";id: string | number;result?: {[key: string]: unknown;}error?: {code: number;message: string;data?: unknown;}
}
4.2.3 通知
{jsonrpc: "2.0";method: string;params?: {[key: string]: unknown;};
}
4.3 傳輸類型
- 標準輸入/輸出 (stdio)
stdio 傳輸通過標準輸入和輸出流實現通信
- 服務器發送事件 (SSE)
Server-Sent?Events(SSE,服務器發送事件)是一種基于?HTTP?協議的技術,允許服務器向客戶端單向、實時地推送數據。在?SSE?模式下,開發者可以在客戶端通過創建一個?EventSource?對象與服務器建立持久連接,服務器則通過該連接持續發送數據流,而無需客戶端反復發送請求。
- 流式傳輸HTTP(StreamableHttp)
不僅允許基本的 MCP 服務器,還允許功能更豐富的服務器支持流式傳輸以及服務器到客戶端的通知和請求。
五、實現方案
5.1 MCP 服務
package mainimport ("context""fmt""github.com/mark3labs/mcp-go/mcp""github.com/mark3labs/mcp-go/server"
)func main() {// Create a new MCP servers := server.NewMCPServer("Demo 🚀","1.0.0",server.WithToolCapabilities(false),)// Add tooltool := mcp.NewTool("hello_world",mcp.WithDescription("Say hello to someone"),mcp.WithString("name",mcp.Required(),mcp.Description("Name of the person to greet"),),)// Add tool handlers.AddTool(tool, helloHandler)// Start the stdio serversseSrv := server.NewSSEServer(s)if err := sseSrv.Start(":8081"); err != nil {fmt.Printf("Server error: %v\n", err)}
}func helloHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {name, err := request.RequireString("name")if err != nil {return mcp.NewToolResultError(err.Error()), nil}return mcp.NewToolResultText(fmt.Sprintf("Hello, %s!", name)), nil
}
運行MCP調試工具?Inspector,進行測試
// 需要先安裝 nodejs, npx 工具,再運行下面的命令
npx @modelcontextprotocol/inspector
5.2 反向代理服務
package mainimport ("log""net/http""net/http/httputil""net/url""github.com/gogf/gf/frame/g""github.com/gogf/gf/net/ghttp"
)func main() {s := g.Server()// 1. 定義后端服務器的地址backendURL, err := url.Parse("http://localhost:8081") // 后端 SSE 服務地址if err != nil {log.Fatal("Failed to parse backend URL: ", err)}// 2. 創建反向代理proxy := httputil.NewSingleHostReverseProxy(backendURL)// 3. 可選:修改請求(例如,設置特定的頭)// originalDirector := proxy.Director// proxy.Director = func(req *http.Request) {// originalDirector(req)// req.Header.Set("X-Proxy", "Go-SSE-Reverse-Proxy")// }// 4. 可選:自定義錯誤處理proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {log.Printf("Proxy error: %v", err)http.Error(w, "Backend server unavailable", http.StatusBadGateway)}// 5. 創建代理服務器并監聽s.BindHandler("/sse", func(r *ghttp.Request) {proxy.ServeHTTP(r.Response.RawWriter(), r.Request)})s.BindHandler("/message", func(r *ghttp.Request) {proxy.ServeHTTP(r.Response.ResponseWriter, r.Request)})s.BindHandler("/mcp", func(r *ghttp.Request) {proxy.ServeHTTP(r.Response.ResponseWriter, r.Request)})s.SetPort(8000)s.Run()
}
運行反向代理服務,在 MCP Inspector 中填入反向代理URL,如圖所示:
六、常見問題
6.1 基于 Goframe@1.16 版本反向代理支持 SSE 協議的兼容問題
// sse 路由的 proxy.ServeHTTP 函數的入參為 r.Response.ResponseWriter 時,不支持 SSE 協議
s.BindHandler("/sse", func(r *ghttp.Request) {proxy.ServeHTTP(r.Response.ResponseWriter, r.Request)
})
原因:SSE 協議是服務端向客戶端單向傳輸數據的長連接,需要服務端實時推送數據,而 Goframe@1.16 版本的?ResponseWriter 結構體的 Flush 方法不支持即時推送數據,所以不支持SSE
// 單元位置:gogf/gf@v1.16.9/net/ghttp/ghttp_response_writer.go// ResponseWriter is the custom writer for http response.
type ResponseWriter struct {Status int // HTTP status.writer http.ResponseWriter // The underlying ResponseWriter.buffer *bytes.Buffer // The output buffer.hijacked bool // Mark this request is hijacked or not.wroteHeader bool // Is header wrote or not, avoiding error: superfluous/multiple response.WriteHeader call.
}// 該方法沒有即時推送數據
// OutputBuffer outputs the buffer to client and clears the buffer.
func (w *ResponseWriter) Flush() {if w.hijacked {return}if w.Status != 0 && !w.wroteHeader {w.wroteHeader = truew.writer.WriteHeader(w.Status)}// Default status text output.if w.Status != http.StatusOK && w.buffer.Len() == 0 {w.buffer.WriteString(http.StatusText(w.Status))}if w.buffer.Len() > 0 {w.writer.Write(w.buffer.Bytes())w.buffer.Reset()}
}
解決方法:
// 使用 r.Response.RawWriter(),因為其返回的是 net/http 中的 http.ResponseWriter,原生支持 SSE
s.BindHandler("/sse", func(r *ghttp.Request) {proxy.ServeHTTP(r.Response.RawWriter(), r.Request)
})或者升級 goframe 版本為v2,ResponseWriter 的 Flush 函數可以即時推送數據到客戶端
goframe 有關?ResponseWriter 源碼如下:
6.2 客戶端請求工具等接口報錯 superfluous response.WriteHeader call
原因:
同一次 http 請求中多次調用了方法 WriteHeader
解決方法:
使用?r.Response.ResponseWriter 作為入參,因為?ResponseWriter 定義了??wroteHeader 屬性,用于標記是否已經寫入過 WriteHeader,避免同一次 http 請求中重復調用 WriteHeader
s.BindHandler("/message", func(r *ghttp.Request) {proxy.ServeHTTP(r.Response.ResponseWriter, r.Request)
})
Goframe v1 版本源碼如下:
// gogf/gf@v1.16.9/net/ghttp/ghttp_response_writer.gopackage ghttp// ...// ResponseWriter is the custom writer for http response.
type ResponseWriter struct {Status int // HTTP status.writer http.ResponseWriter // The underlying ResponseWriter.buffer *bytes.Buffer // The output buffer.hijacked bool // Mark this request is hijacked or not.wroteHeader bool // Is header wrote or not, avoiding error: superfluous/multiple response.WriteHeader call.
}// RawWriter returns the underlying ResponseWriter.
func (w *ResponseWriter) RawWriter() http.ResponseWriter {return w.writer
}// ...// WriteHeader implements the interface of http.ResponseWriter.WriteHeader.
func (w *ResponseWriter) WriteHeader(status int) {w.Status = status
}// ...// OutputBuffer outputs the buffer to client and clears the buffer.
func (w *ResponseWriter) Flush() {if w.hijacked {return}// 判斷是否已經寫入過響應頭if w.Status != 0 && !w.wroteHeader {w.wroteHeader = truew.writer.WriteHeader(w.Status)}// Default status text output.if w.Status != http.StatusOK && w.buffer.Len() == 0 {w.buffer.WriteString(http.StatusText(w.Status))}if w.buffer.Len() > 0 {w.writer.Write(w.buffer.Bytes())w.buffer.Reset()}
}
注意:Goframe V2 版本已完美兼容處理這兩個問題,所以使用?r.Response.RawWriter()、r.Response.ResponseWriter、r.Response.Writer 都可以