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 请求

Go 复制代码
{
  jsonrpc: "2.0";
  id: string | number;
  method: string;
  params?: {
    [key: string]: unknown;
  };
}

4.2.2 响应

Go 复制代码
{
  jsonrpc: "2.0";
  id: string | number;
  result?: {
    [key: string]: unknown;
  }
  error?: {
    code: number;
    message: string;
    data?: unknown;
  }
}

4.2.3 通知

Go 复制代码
{
  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 服务

Go 复制代码
package main

import (
	"context"
	"fmt"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

func main() {
	// Create a new MCP server
	s := server.NewMCPServer(
		"Demo 🚀",
		"1.0.0",
		server.WithToolCapabilities(false),
	)

	// 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"),
		),
	)

	// Add tool handler
	s.AddTool(tool, helloHandler)

	// Start the stdio server
	sseSrv := 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,进行测试

bash 复制代码
// 需要先安装 nodejs, npx 工具,再运行下面的命令
npx @modelcontextprotocol/inspector

5.2 反向代理服务

Go 复制代码
package main

import (
	"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 协议的兼容问题

Go 复制代码
// 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

Go 复制代码
// 单元位置: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 = true
		w.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()
	}
}

解决方法:

Go 复制代码
// 使用 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

Go 复制代码
s.BindHandler("/message", func(r *ghttp.Request) {
	proxy.ServeHTTP(r.Response.ResponseWriter, r.Request)
})

Goframe v1 版本源码如下:

Go 复制代码
// gogf/gf@v1.16.9/net/ghttp/ghttp_response_writer.go

package 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 = true
		w.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 都可以

相关推荐
Stara05117 小时前
基于MCP架构的OpenWeather API服务端设计与实现
人工智能·python·mcp·openweather_api·cherry_studio·llm_tools·uvx
Joey_Chen8 小时前
【Golang开发】Gin框架学习笔记——服务器的运行机制
后端·go
程序员老刘8 小时前
5分钟上手Dart MCP Server
flutter·ai编程·mcp
2302_7995257410 小时前
【Hot100】15.三数之和
算法·leetcode·go·hot100
Pocker_Spades_A12 小时前
Trae + MCP : 一键生成专业封面
人工智能·mcp·蓝耘
dylan_QAQ15 小时前
Java转Go全过程02-面向对象编程部分
java·后端·go
MonkeyKing_sunyuhua16 小时前
mcp和A2A到底什么关系,有什么联系和区别
mcp·a2a
带刺的坐椅16 小时前
让 Java AI 再伟大些!Solon AI & MCP v3.5.1 发布
java·ai·solon·mcp·a2a
火山引擎开发者社区1 天前
来自火山引擎的 MCP 安全授权新范式
安全·火山引擎·mcp