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。
This commit is contained in:
commit
d5dac5a6fc
140 files changed
+26497
No files matched your search
+190
@@ -0,0 +1,190 @@
|
||||
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 }
|
||||
Reference in new issue
Block a user