Goframe 框架下HTTP反向代理并支持MCP所需的SSE協議的實現

一、需求背景

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 都可以

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/920943.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/920943.shtml
英文地址,請注明出處:http://en.pswp.cn/news/920943.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Git將多筆patch合并成一筆

一、方法1、在你的代碼中把這多筆patch都打上2、git reset到origin那一筆(默認模式,不帶soft或者hard)3、再add和commit,push二、種模式對比模式命令示例影響范圍適用場景--softgit reset --soft HEAD~1僅移動 HEAD,保留修改在暫存區修改提交…

【SpringBoot】Dubbo、Zookeeper

文章目錄前提知識概要分布式系統單體架構垂直應用架構分布式架構流式架構RPCDubbo概念Dubbo環境搭建Zookeeper測試 ZookeeperWindow環境下使用Dubbo-admin版本匹配不對服務注冊實戰內容總結導入相關依賴選擇 Zookeeper 版本配置并啟用 Zookeeper創建服務接口和實現(DubboServic…

【不說廢話】pytorch張量相對于numpy數組的優勢

核心關系 我們首先需要了解:PyTorch 張量在設計上深受 NumPy 數組的影響,它們共享許多相似的 API 和概念。實際上,PyTorch 張量可以看作是支持 GPU 加速和自動求導功能的 NumPy 數組。PyTorch 張量的主要優勢 1. GPU 加速支持(最重…

拼團小程序源碼分享拼團余額提現小程序定制教程開發源碼二開

功能詳細說明(一)首頁功能進入首頁,可看到以下核心功能:1、優惠券,錢包,簽到,拼團,分銷等各種功能入口2、推薦的商品和活動3、下方功能欄的各種功能(二)客服功…

pikachu之XSS

XSS(跨站腳本)概述Cross-Site Scripting 簡稱為“CSS”,為避免與前端疊成樣式表的縮寫"CSS"沖突,故又稱XSS。一般XSS可以分為如下幾種常見類型:1.反射性XSS;2.存儲型XSS;3.DOM型XSS;XSS漏洞一直被評估為web漏…

【Element Plus `el-select` 下拉菜單響應式定位問題深度解析】

Element Plus el-select 下拉菜單響應式定位問題深度解析 本文檔旨在深入剖析一個在響應式布局中常見的 UI 問題:如何確保一個靠近屏幕邊緣的 el-select 組件的下拉菜單,在任何屏幕尺寸下都能以預期的、優雅的方式顯示。 1. 需求背景 在一個大屏數據展示…

Qt 項目文件(.pro)中添加 UI 文件相關命令

在 Qt 的 .pro 項目文件中,處理 UI 文件(.ui 文件)通常需要以下配置: 基本 UI 文件配置 自動包含 UI 文件: qmake FORMS yourfile.ui \anotherfile.ui Qt 構建系統會自動使用 uic(用戶界面編譯器&#xff…

展會回顧 | 聚焦醫療前沿 , 禮達先導在廣州醫博會展示類器官自動化培養技術

8月22-24日,廣州醫博會在廣交會展館B區圓滿落幕。此次盛會匯聚了來自全球醫療健康領域的頂尖專家學者、企業代表與合作伙伴。展會內容涵蓋基礎研發、臨床應用、前沿技術、產業轉化、醫療服務及金融支持,全景呈現醫療健康產業的創新生態,成為連…

華為eNSP防火墻綜合網絡結構訓練.docx

1.IP及VLAN規劃情況 設備 接口 IP vlan 備注 AR1 g0/0/0 1.1.1.2/28 PPPOE g0/0/1 3.3.3.1/30 g0/0/2 114.114.114.254/24 AR2 g0/0/0 2.2.2.2/28 DHCP g0/0/1 3.3.3.2/30 g0/0/2 100.100.100.254/24 FW1 g1/0/0 10.0.0.1/30 tr…

從 Oracle 到 TiDB,通過ETL工具,高效實現數據拉通

在當前企業數字化轉型的浪潮中,打破數據孤島、實現異構數據庫間的數據高效流轉已成為提升業務敏捷性與決策效率的關鍵。許多企業在要將 Oracle 數據庫中的海量數據準確地同步至TiDB 分布式數據庫時遇到了挑戰。這一過程不僅要求數據的絕對一致性,還對同步…

Effective c++ 35條款詳解

您問到了最關鍵的一點!這正是策略模式的精妙之處——它通過組合(composition)而非繼承(inheritance)來實現多態效果。讓我詳細解釋這是如何工作的,以及它與傳統繼承多態的區別。🔄 策略模式如何…

51c自動駕駛~合集19

自己的原文哦~ https://blog.51cto.com/whaosoft/11793894 #DRAMA 首個基于Mamba的端到端運動規劃器 運動規劃是一項具有挑戰性的任務,在高度動態和復雜的環境中生成安全可行的軌跡,形成自動駕駛汽車的核心能力。在本文中,我…

大數據新視界 -- Hive 數據倉庫:架構深度剖析與核心組件詳解(上)(1 / 30)

💖💖💖親愛的朋友們,熱烈歡迎你們來到 青云交的博客!能與你們在此邂逅,我滿心歡喜,深感無比榮幸。在這個瞬息萬變的時代,我們每個人都在苦苦追尋一處能讓心靈安然棲息的港灣。而 我的…

軟考 系統架構設計師系列知識點之雜項集萃(137)

接前一篇文章:軟考 系統架構設計師系列知識點之雜項集萃(136) 第253題 在面向對象設計中,用于描述目標軟件與外部環境之間交互的類被稱為( ),它可以( )。 第1空 A. 實體類 B. 邊界類 C. 模型類 D. 控制類 正確答案:B。 第2空 A. 表示目標軟件系統中具有持久…

(附源碼)基于Spring Boot公務員考試信息管理系統設計與實現

摘 要 隨著公務員考試日益受到社會的廣泛關注,一個高效、便捷的公務員考試信息管理系統顯得尤為重要。本文設計并實現了一個基于前端Vue框架,后端采用Java與Spring Boot技術,數據庫選用MySQL,并部署在Tomcat服務器上的信息管理系統…

學習JavaScript的第一個簡單程序:Hello World

在JavaScript中,最簡單的程序是打印"Hello World"。可以通過以下方式實現: console.log("Hello World");將上述代碼保存為hello.js文件,通過Node.js運行或在瀏覽器控制臺中執行。 瀏覽器環境實現 在HTML文件中嵌入Jav…

【Big Data】Alluxio 首個基于云的數據分析和開源AI數據編排技術

目錄 1. 什么是 Alluxio?? 2. Alluxio 的誕生背景:為什么需要數據編排層?? 痛點 1:計算與存儲強耦合,適配成本高? 痛點 2:跨集群 / 跨云數據移動效率低? 痛點 3:數據訪問延遲高&#x…

uniApp App 嵌入 H5 全流程:通信與跳轉細節拆解

在 uniApp App 開發中,通過 WebView 嵌入 H5 頁面是常見需求(如活動頁、第三方頁面),核心需解決「H5 與 App 通信」「H5 操作后返回/跳轉 App」兩大問題。本文基于 DCloud 官方方案(原文鏈接),對…

技能提升必備:鴻蒙HarmonyOS應用開發者認證

技能提升必備:鴻蒙HarmonyOS應用開發者認證,HarmonyOS 認證是華為為開發者打造的能力衡量體系。隨著 HarmonyOS 系統影響力不斷擴大,市場對相關開發人才需求激增。該認證分為基礎與高級等不同級別,覆蓋應用開發、設備開發等方向。…

Chromium 架構中的 ContentClient / ContentBrowserClient 設計原理全解析

一、前言在閱讀 Chromium 源碼時,很多人會對這樣一段調用產生疑惑:bool BrowserMainLoop::AudioServiceOutOfProcess() const { return base::FeatureList::IsEnabled(features::kAudioServiceOutOfProcess) && !GetContentClient()->browser…