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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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…