浅谈下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,欢迎参考。

相关推荐
且去填词5 小时前
Go 语言的“反叛”——为什么少即是多?
开发语言·后端·面试·go
饭勺oO14 小时前
AI 编程配置太头疼?ACP 帮你一键搞定,再也不用反复折腾!
ai·prompt·agent·acp·mcp·skills·agent skill
AlienZHOU14 小时前
MCP 是最大骗局?Skills 才是救星?
agent·mcp·vibecoding
Linux内核拾遗16 小时前
人人都在聊 MCP,它到底解决了什么?
aigc·ai编程·mcp
用户268516121075616 小时前
GMP 调度器深度学习笔记
后端·go
Coding君17 小时前
每日一Go-20、Go语言实战-利用Gin开发用户注册登录功能
go
用户268516121075617 小时前
GMP 三大核心结构体字段详解
后端·go
谷哥的小弟17 小时前
SQLite MCP服务器安装以及客户端连接配置
服务器·数据库·人工智能·sqlite·大模型·源码·mcp
tyw1519 小时前
解决 Trae MySQL MCP 连接失败(Fail to start)
mcp·trae·fail to start·mysql mcp·mcp兼容
谷哥的小弟19 小时前
File System MCP服务器安装以及客户端连接配置
服务器·人工智能·大模型·file system·mcp·ai项目