// mcphandler.go:MCP Streamable HTTP(2026 无状态)POST /mcp。 // JSON-RPC 2.0 单请求单响应,无 SSE。tools/call 转发 /v1 同一内核。 package gateway import ( "encoding/json" "io" "net/http" "strings" ) // MCP 协议常量。 const ( mcpProtocolVersionDefault = "2026-07-28" // 2026 无状态规范目标版 mcpServerName = "onesvm-browser-server" mcpServerVersion = "0.1.0" ) // jsonRPCError codes(协议标准)。 const ( errParse = -32700 errInvalidRequest = -32600 errMethodNotFound = -32601 errInvalidParams = -32602 errInternal = -32603 ) // jsonRPCReq JSON-RPC 2.0 请求。 type jsonRPCReq struct { JSONRPC string `json:"jsonrpc"` ID json.RawMessage `json:"id"` Method string `json:"method"` Params json.RawMessage `json:"params"` } // jsonRPCResp JSON-RPC 2.0 响应。 type jsonRPCResp struct { JSONRPC string `json:"jsonrpc"` ID json.RawMessage `json:"id"` Result any `json:"result,omitempty"` Error *jsonRPCError `json:"error,omitempty"` } // jsonRPCError 错误对象。 type jsonRPCError struct { Code int `json:"code"` Message string `json:"message"` Data any `json:"data,omitempty"` } // handleMCP POST /mcp 单 handler。 func (s *Server) handleMCP(w http.ResponseWriter, r *http.Request, a *consumerAuth) { // 协议头校验:Content-Type 必须 application/json(容忍 charset 后缀) ct := r.Header.Get("Content-Type") if ct != "" && !strings.HasPrefix(strings.ToLower(strings.TrimSpace(strings.Split(ct, ";")[0])), "application/json") { s.mcpWriteErr(w, r, nil, &jsonRPCError{Code: errInvalidRequest, Message: "Content-Type 须为 application/json"}) return } // MCP-Protocol-Version:缺省容忍(默认按 2026 版处理) _ = r.Header.Get("MCP-Protocol-Version") body, err := io.ReadAll(io.LimitReader(r.Body, 4*1024*1024)) if err != nil { s.mcpWriteErr(w, r, nil, &jsonRPCError{Code: errParse, Message: "请求体读取失败"}) return } var req jsonRPCReq if err := json.Unmarshal(body, &req); err != nil { s.mcpWriteErr(w, r, nil, &jsonRPCError{Code: errParse, Message: "JSON 解析失败: " + err.Error()}) return } if req.JSONRPC != "2.0" || req.Method == "" { s.mcpWriteErr(w, r, req.ID, &jsonRPCError{Code: errInvalidRequest, Message: "非法 JSON-RPC 2.0 请求"}) return } switch req.Method { case "initialize": s.mcpWrite(w, r, req.ID, s.mcpInitialize()) case "tools/list": s.mcpWrite(w, r, req.ID, s.mcpToolsList()) case "tools/call": s.mcpToolsCall(w, r, &req, a) case "ping": s.mcpWrite(w, r, req.ID, map[string]any{}) case "notifications/initialized": // 通知:无响应体(2026 无状态;返回 202 Accepted) w.WriteHeader(http.StatusAccepted) default: s.mcpWriteErr(w, r, req.ID, &jsonRPCError{Code: errMethodNotFound, Message: "未知 method: " + req.Method}) } } // mcpWrite JSON-RPC 响应输出(错误对象与 result 二选一)。 func (s *Server) mcpWrite(w http.ResponseWriter, _ *http.Request, id json.RawMessage, result any, errObj ...*jsonRPCError) { resp := jsonRPCResp{JSONRPC: "2.0", ID: id} if len(errObj) > 0 && errObj[0] != nil { resp.Error = errObj[0] } else { resp.Result = result } if resp.ID == nil { resp.ID = json.RawMessage("null") } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(resp) } // mcpWriteErr JSON-RPC error 对象输出(-32600/-32601/-32602/-32700 协议错误走 error 槽)。 func (s *Server) mcpWriteErr(w http.ResponseWriter, _ *http.Request, id json.RawMessage, errObj *jsonRPCError) { resp := jsonRPCResp{JSONRPC: "2.0", ID: id, Error: errObj} if resp.ID == nil { resp.ID = json.RawMessage("null") } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(resp) } // mcpInitialize initialize 响应:serverInfo + capabilities.tools + instructions(合规规则)。 func (s *Server) mcpInitialize() map[string]any { return map[string]any{ "protocolVersion": mcpProtocolVersionDefault, "capabilities": map[string]any{ "tools": map[string]any{"listChanged": false}, }, "serverInfo": map[string]any{ "name": mcpServerName, "version": mcpServerVersion, }, "instructions": mcpInstructions, } } // mcpInstructions 合规使用规则(mcp-usage §5 五条 + 纪律)。 const mcpInstructions = "onesvm-browser-server 合规使用规则:\n" + "1. 不得将本服务用于任何违反中国法律法规的数据获取。\n" + "2. 不得用于绕过登录墙 / 支付墙 / 验证码(本服务默认也不具备此能力)。\n" + "3. 不得进行整站搬迁式 crawl(每 host 有频率上限;60 会话封顶本身也是合规阀)。\n" + "4. 不得把公开网页中的个人信息汇总成档案再分发;响应中的疑似 PII 默认已被脱敏,不要试图还原。\n" + "5. 不得把包含境内个人信息的 query 发给 region=overseas(query 会经境外代理出口)。\n" + "其它纪律:普通 key 遵守 robots.txt;denied / blocked 的语义是「没数据」," + "不要重试对抗、不要换出口绕过——反复对抗触发审计告警并可能导致 key 被吊销。" // mcpToolDef 工具 schema 描述。 type mcpToolDef struct { Name string `json:"name"` Description string `json:"description"` InputSchema map[string]any `json:"inputSchema"` } // mcpToolsList tools/list:search/read 工具 schema(参数表与 mcp-usage §2 一致)。 func (s *Server) mcpToolsList() map[string]any { tools := []mcpToolDef{ { Name: "search", Description: "关键词搜索发现。region 必填(domestic=国内直连,overseas=经代理,当前仅 Bing)。返回 ≤800 字符片段,非全文。", InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ "query": map[string]any{"type": "string", "description": "关键词(必填)"}, "region": map[string]any{"type": "string", "enum": []string{"domestic", "overseas"}, "description": "出口区域(必填,无默认)"}, "max_results": map[string]any{"type": "integer", "minimum": 1, "maximum": 20, "default": 5}, "time_range": map[string]any{"type": "string", "enum": []string{"day", "week", "month", "year"}}, "lang": map[string]any{"type": "string", "description": "如 zh-CN / en-US"}, }, "required": []string{"query", "region"}, }, }, { Name: "read", Description: "单 URL 精读(fit markdown)。默认 formats=[markdown];links/images 可加;html/screenshot 与 extract 为特权 scope。仅 http/https 80/443。", InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ "url": map[string]any{"type": "string", "description": "目标页 URL(必填)"}, "formats": map[string]any{"type": "array", "items": map[string]any{"type": "string", "enum": []string{"markdown", "links", "images", "html", "screenshot"}}, "default": []string{"markdown"}}, "max_chars": map[string]any{"type": "integer", "default": 20000}, "extract": map[string]any{"type": "object", "description": "特权:{schema, prompt?},JSON Schema 结构化抽取"}, "region": map[string]any{"type": "string", "enum": []string{"domestic", "overseas"}}, }, "required": []string{"url"}, }, }, } return map[string]any{"tools": tools} } // mcpToolsCall tools/call:转发 /v1 同一内核。 // 工具结果 content:[{type:"text",text:JSON字符串}];isError 与 HTTP 错误码对齐。 func (s *Server) mcpToolsCall(w http.ResponseWriter, r *http.Request, req *jsonRPCReq, a *consumerAuth) { var params struct { Name string `json:"name"` Arguments json.RawMessage `json:"arguments"` } if err := json.Unmarshal(req.Params, ¶ms); err != nil || params.Name == "" { s.mcpWriteErr(w, r, req.ID, &jsonRPCError{Code: errInvalidParams, Message: "params.name 必填"}) return } var res result switch params.Name { case "search": var in contractSearchInput if err := unmarshalInto(params.Arguments, &in); err != nil { s.mcpWriteErr(w, r, req.ID, &jsonRPCError{Code: errInvalidParams, Message: "arguments 非法: " + err.Error()}) return } rc := runCtx{Intent: IntentSearch, Search: in.search(), Auth: a, W: w, R: r} res = s.pipeline(rc) case "read": var in contractReadInput if err := unmarshalInto(params.Arguments, &in); err != nil { s.mcpWriteErr(w, r, req.ID, &jsonRPCError{Code: errInvalidParams, Message: "arguments 非法: " + err.Error()}) return } rc := runCtx{Intent: IntentRead, Read: in.read(), Auth: a, W: w, R: r} res = s.pipeline(rc) default: s.mcpWriteErr(w, r, req.ID, &jsonRPCError{Code: errInvalidParams, Message: "未知工具: " + params.Name + "(可选 search|read)"}) return } s.mcpRespondToolResult(w, r, req.ID, res) } // mcpRespondToolResult 把管线结果转 MCP tool result。 // 前置错误(HTTPStatus!=200):isError=true + error.code 对齐 mcp-usage §3; // 信封结果:text=信封 JSON;信封内 error 非空 → isError=true。 func (s *Server) mcpRespondToolResult(w http.ResponseWriter, r *http.Request, id json.RawMessage, res result) { var body []byte isError := false var structuredErr any if res.HTTPStatus != http.StatusOK && res.Body == nil { body = res.Body2 isError = true var env struct { Error *struct { Code string `json:"code"` Message string `json:"message"` } `json:"error"` } if json.Unmarshal(body, &env) == nil && env.Error != nil { structuredErr = map[string]string{"code": env.Error.Code, "message": env.Error.Message} } } else { body = res.Body var env struct { OK bool `json:"ok"` Error *struct { Code string `json:"code"` Message string `json:"message"` } `json:"error"` } if json.Unmarshal(body, &env) == nil { if env.Error != nil { isError = true structuredErr = map[string]string{"code": env.Error.Code, "message": env.Error.Message} } else if !env.OK { isError = true structuredErr = map[string]string{"code": "internal", "message": "信封 ok=false 无 error 详情"} } } } toolRes := map[string]any{ "content": []map[string]any{{"type": "text", "text": string(body)}}, "isError": isError, } if structuredErr != nil { toolRes["error"] = structuredErr } s.mcpWrite(w, r, id, toolRes) } // unmarshalInto JSON 解码辅助。 func unmarshalInto(raw json.RawMessage, v any) error { if len(raw) == 0 { raw = json.RawMessage("{}") } return json.Unmarshal(raw, v) }