浅谈下MCP协议中SSE传输方式的实现

在之前介绍的几篇文章中,关于MCP server的实现都是基于 STDIO 的方式进行通信,这种方式是靠本地进程间的标准的输入输出协议实现通信的,但是通常我们现有的微服务都是web端的应用,STDIO 的方式在这种场景下并不适用,因此,MCP协议提供了另一种通信方式,即SSE (Server-Sent Events) 传输方式。 MCP的 SSE 传输是一种基于 HTTP 的通信机制,主要用于实现服务器到客户端的流式传输。下面我们提供一个简单的示例来说明其工作原理。


实现一个简单的 SSE 传输的 MCP Server

下面是一个用go语言的 mcp-go 框架实现的一个简单的示例,它实现了一个 echo 工具,该工具接受一个字符串参数,并将该字符串原封不动地返回给客户端。

go 复制代码
func NewMCPServer() *MCPServer {
	mcpServer := server.NewMCPServer(
		"example-server",
		"1.0.0",
		server.WithResourceCapabilities(true, true),
		server.WithPromptCapabilities(true),
		server.WithToolCapabilities(true),
	)
	// Add echo tool
	mcpServer.AddTool(mcp.NewTool("echo",
		mcp.WithDescription("Echo back the input"),
		mcp.WithString("message",
			mcp.Required(),
			mcp.Description("Message to echo back"),
		),
	), echoHandler)

	return &MCPServer{
		server: mcpServer,
	}
}

func main() {
	s := NewMCPServer()
	sseServer := s.ServeSSE("localhost:8080")
	log.Printf("SSE server listening on :8080")
	if err := sseServer.Start(":8080"); err != nil {
		log.Fatalf("Server error: %v", err)
	}
}

func (s *MCPServer) ServeSSE(addr string) *server.SSEServer {
	return server.NewSSEServer(s.server,
		server.WithBaseURL(fmt.Sprintf("http://%s", addr)),
	)
}

但是由于框架封装了对外暴露的 endpoint ,所以我们需要点击NewSSEServer方法去看内部的实现,可以看到框架默认帮我们封装了两个 endpoint,如下所示:

go 复制代码
	s := &SSEServer{
		server:            server,
		sseEndpoint:       "/sse",
		messageEndpoint:   "/message",
    useFullURLForMessageEndpoint: true,
		keepAlive:         false,
		keepAliveInterval: 10 * time.Second,
	}

因此可以知道服务端与客户端的通信是基于这两个接口实现的。接下来我们再实现个 MCP client,来看看它具体是如何与服务端进行通信的。


实现一个简单的 SSE 传输的 MCP Client

下面代码是一段与上面实现的服务端交互的 mcp client,它通过 SSE 协议与上面实现的服务端进行通信。

go 复制代码
func main() {
	ctx := context.Background()
	client, err := client.NewSSEMCPClient("http://localhost:8080/sse")
	if err != nil {
		log.Fatalf("Failed to create SSE MCP client: %v", err)
	}
	err = client.Start(ctx)
	if err != nil {
		log.Fatalf("Failed to start SSE MCP client: %v", err)
	}
	// Initialize
	initRequest := mcp.InitializeRequest{}
	initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
	initRequest.Params.ClientInfo = mcp.Implementation{
		Name:    "test-client",
		Version: "1.0.0",
	}

	_, err = client.Initialize(ctx, initRequest)
	if err != nil {
		log.Fatalf("Failed to Initialize SSE MCP client: %v", err)
	}
	request := mcp.CallToolRequest{
		Request: mcp.Request{
			Method: "tools/call",
		},
	}

	arguments := map[string]interface{}{
		"message": "Hello SSE!",
	}

	request.Params.Name = "echo"
	request.Params.Arguments = arguments

	// Test echo tool
	result, err := client.CallTool(context.Background(), request)
	if err != nil {
		return
	}
	textContent := result.Content[0].(mcp.TextContent)
	fmt.Println(textContent.Text)
	time.Sleep(100 * time.Second)
}

服务启动后,就会打印出 Echo: Hello SSE!,接下来我们仔细看看它是如何实现的。

首先点击进入 client.Start(ctx) 方法,可以看到客户端会向服务端发送一个GET请求,请求建立SSE连接。 这段代码主要有两个作用:

  • 通过 get 请求建立 sse 连接
  • 异步读取 sse 端口收到的数据
go 复制代码
func (c *SSEMCPClient) Start(ctx context.Context) error {
	req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL.String(), nil)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Accept", "text/event-stream")
	req.Header.Set("Cache-Control", "no-cache")
	req.Header.Set("Connection", "keep-alive")
	for k, v := range c.headers {
		req.Header.Set(k, v)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to connect to SSE stream: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		resp.Body.Close()
		return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	go c.readSSE(resp.Body)
	// Wait for the endpoint to be received
	select {
	case <-c.endpointChan:
		// Endpoint received, proceed
	case <-ctx.Done():
		return fmt.Errorf("context cancelled while waiting for endpoint")
	case <-time.After(30 * time.Second): // Add a timeout
		return fmt.Errorf("timeout waiting for endpoint")
	}

	return nil
}

进入 c.readSSE(resp.Body) 方法后,可以看到服务端给客户端返回了一个message端口的url信息。

继续执行代码到了 client.Initialize(ctx, initRequest) ,其主要作用是与服务端提供的message 端口的url做连接验证,没有问题后将c.initialized = true,那么后续调用tool就不会因为没有initialized而报错了。

再往下进入 client.CallTool(context.Background(), request) 方法,可以看到此时的请求是发送到localhost:8080/message 接口的,并且是POST请求。

完成该方法的调用后 readSSE 方法就会读取到 localhost:8080/sse 返回的数据,如下图所示:

ok,通过上面的调式,你应该很清楚 SSE 模式下 MCP Client是如何与 MCP Server 通信的了。


小结

1. SSE 传输的基本架构

  • SSE 端点:客户端通过向该端点发送 GET 请求建立连接,服务器通过这个连接向客户端推送消息。
  • HTTP POST 端点:客户端通过向该端点发送 POST 请求向服务器发送消息。

2. 连接建立过程

  • 客户端请求 :客户端向服务器的 /sse 端点发送一个 GET 请求,请求建立 SSE 连接。
  • 服务器响应:服务器接收到请求后,返回一个包含消息端点地址的事件消息。这个消息端点地址通常是一个 HTTP POST 接口的地址。
  • 客户端记录消息端点:客户端从服务器返回的事件消息中获取消息端点地址,并保存下来用于后续的消息发送。

3. 消息交互过程

  • 客户端发送消息:客户端通过 HTTP POST 请求将消息发送到服务器的消息端点。
  • 服务器处理消息:服务器接收到客户端发送的消息后,通过之前建立的 SSE 连接向客户端发送响应消息。
  • 客户端接收消息:客户端从 SSE 连接中读取服务器发送的事件消息,并解析其中的数据。

上面的相关代码,我都提交到了github仓库demo_for_AI,欢迎参考。

相关推荐
白总Server1 天前
Golang领域Beego框架的中间件开发实战
服务器·网络·websocket·网络协议·udp·go·ssl
yi念zhi间1 天前
如何把ASP.NET Core WebApi打造成Mcp Server
后端·ai·mcp
ん贤2 天前
GoWeb开发
开发语言·后端·tcp/ip·http·https·go·goweb
纪元A梦2 天前
华为OD机试真题——荒岛求生(2025A卷:200分)Java/python/JavaScript/C/C++/GO最佳实现
java·c语言·javascript·c++·python·华为od·go
杨浦老苏2 天前
MCPHub:一站式MCP服务器聚合平台
人工智能·docker·ai·群晖·mcp
伊织code2 天前
AWS MCP Servers
服务器·python·ai·云计算·aws·mcp
Generalzy2 天前
Model Context Protocol (MCP)笔记
笔记·ai·mcp
极小狐3 天前
如何创建并使用极狐GitLab 项目访问令牌?
数据库·ci/cd·gitlab·devops·mcp
chxii4 天前
3.2goweb框架GORM
go
gs801405 天前
MCP智能体多Agent协作系统设计(Multi-Agent Cooperation)
人工智能·mcp