Files
Sakurasan d5dac5a6fc feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、
builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、
编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。

产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt)
留在 AutoGo,宿主接入方式见 MIGRATION.md。
2026-10-26 00:54:46 +08:00

191 lines
6.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mcp
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
// httpTransport 实现 Streamable HTTP 传输(2026-07-28:无会话)。
//
// 每个 JSON-RPC 请求一次 POST,响应自带结果——与 stdio 共用 Client 的年代探测与 MRTR 逻辑。
//
// 规范要点:
// - POST 单条请求(不支持批处理),Content-Type: application/json;
// - Accept 必须同时列 application/json 与 text/event-stream:服务端可能回 JSON 对象,
// 也可能回 SSE 流,客户端两种都要支持;
// - 头:MCP-Protocol-Version(必须与 params._meta 里的版本一致)、Mcp-Method(所有请求)、
// Mcp-Name(tools/call / resources/read / prompts/get);
// - 无 Mcp-Session-Id、无 Last-Event-ID 续传、无 GET 流端点(那是已废弃的 HTTP+SSE)。
// - 取消 = 关闭响应流(交给 ctx)。
//
// 年代回退:旧版 Streamable HTTP(2025-11-25 及更早)需要先 POST initialize 并在响应里拿
// Mcp-Session-Id,之后每次请求带上它。sessionID 只在这条分支使用。
type httpTransport struct {
url string
headers map[string]string
client *http.Client
sessionID string
}
func newHTTPTransport(url string, headers map[string]string) *httpTransport {
return &httpTransport{url: url, headers: headers, client: &http.Client{}}
}
func (t *httpTransport) Kind() string { return "http" }
// SetSession 记录旧版传输的会话 id(initialize 之后调用)。
func (t *httpTransport) SetSession(id string) { t.sessionID = id }
func (t *httpTransport) Call(ctx context.Context, body []byte, meta reqMeta) (rpcMsg, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, t.url, bytes.NewReader(body))
if err != nil {
return rpcMsg{}, err
}
t.applyHeaders(req, meta)
resp, err := t.client.Do(req)
if err != nil {
return rpcMsg{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return t.classifyError(resp)
}
// 旧版语义:initialize 的响应里下发 Mcp-Session-Id,之后每次请求都要带上。现代服务端
// 不发这个头,所以这里只是顺手接住,不产生副作用。
if id := resp.Header.Get("Mcp-Session-Id"); id != "" {
t.sessionID = id
}
return DecodeHTTPResponse(resp)
}
// applyHeaders 生成规范要求的请求头。
func (t *httpTransport) applyHeaders(req *http.Request, meta reqMeta) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json, text/event-stream")
req.Header.Set("MCP-Protocol-Version", meta.version)
if meta.method != "" {
req.Header.Set("Mcp-Method", meta.method)
}
if meta.name != "" {
req.Header.Set("Mcp-Name", meta.name)
}
for k, v := range t.headers {
req.Header.Set(k, v)
}
if t.sessionID != "" {
req.Header.Set("Mcp-Session-Id", t.sessionID)
}
}
// classifyError 把非 200 分成三类:
// 1. body 是"可识别的现代错误"(错误码落在规范保留的 -32020..-32099 段)→ 原样返回响应,
// 上层据此重试/换版本,**不回退**;
// 2. 400/404/405 且 body 不是现代错误 → 判为 legacy,上层回退 initialize;
// 3. 其它状态码 → 直接报错。
func (t *httpTransport) classifyError(resp *http.Response) (rpcMsg, error) {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
snippet := strings.TrimSpace(string(body))
var m rpcMsg
if json.Unmarshal(bytes.TrimSpace(body), &m) == nil && m.Error != nil && m.Error.Code <= -32020 && m.Error.Code >= -32099 {
return m, nil
}
switch resp.StatusCode {
case http.StatusBadRequest, http.StatusNotFound, http.StatusMethodNotAllowed:
return rpcMsg{}, &legacyServerError{
reason: fmt.Sprintf("服务端返回 HTTP %d 且响应体不是可识别的现代错误,按旧版处理(%s)", resp.StatusCode, snippet)}
case http.StatusUnauthorized, http.StatusForbidden:
// 常见场景:远程 MCP 要求 OAuth。本客户端还没实现 MCP 的授权流程——给一条可操作的
// 提示:先用 headers 直接塞 Bearer token(用户的 westock-mcp 正是这种情况)。
return rpcMsg{}, &authRequiredError{msg: fmt.Sprintf(
"服务端要求鉴权(HTTP %d:%s)。本客户端尚未实现 MCP 的 OAuth 授权流程;如已有 token,可暂用配置的 headers 传 Authorization。",
resp.StatusCode, snippet)}
}
return rpcMsg{}, fmt.Errorf("mcp http %d: %s", resp.StatusCode, snippet)
}
// DecodeHTTPResponse 解析 200 响应:单个 JSON 对象,或 SSE 流(流上先来的是与本次请求相关
// 的通知,最后一条带 id 的就是响应;`:` 开头的注释行忽略)。
func DecodeHTTPResponse(resp *http.Response) (rpcMsg, error) {
ct := resp.Header.Get("Content-Type")
if strings.Contains(ct, "text/event-stream") {
return decodeSSE(resp.Body)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<24))
if err != nil {
return rpcMsg{}, err
}
var m rpcMsg
if err := json.Unmarshal(bytes.TrimSpace(body), &m); err != nil {
return rpcMsg{}, fmt.Errorf("解析 MCP 响应失败: %w", err)
}
return m, nil
}
// decodeSSE 读 SSE 流,返回第一条带 id 的 JSON-RPC 消息。
func decodeSSE(r io.Reader) (rpcMsg, error) {
sc := bufio.NewScanner(r)
var data []string
flush := func() (rpcMsg, bool) {
if len(data) == 0 {
return rpcMsg{}, false
}
payload := strings.Join(data, "\n")
data = nil
var m rpcMsg
if json.Unmarshal([]byte(payload), &m) != nil {
return rpcMsg{}, false
}
return m, true
}
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, ":"):
continue // 注释 / keep-alive
case line == "":
if m, ok := flush(); ok && len(m.ID) > 0 {
return m, nil
}
case strings.HasPrefix(line, "data:"):
data = append(data, strings.TrimPrefix(line, "data:"))
}
}
if err := sc.Err(); err != nil {
return rpcMsg{}, err
}
if m, ok := flush(); ok && len(m.ID) > 0 {
return m, nil
}
return rpcMsg{}, fmt.Errorf("SSE 流结束但没拿到 JSON-RPC 响应")
}
// Notify 发送无 id 通知(仅 legacy 握手的 notifications/initialized 用;现代协议没有
// 客户端通知)。
func (t *httpTransport) Notify(body []byte, meta reqMeta) error {
req, err := http.NewRequest(http.MethodPost, t.url, bytes.NewReader(body))
if err != nil {
return err
}
t.applyHeaders(req, meta)
resp, err := t.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<16))
return fmt.Errorf("通知被拒绝: HTTP %d %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
return nil
}
func (t *httpTransport) Close() error { return nil }