Files
weichen fcd2539b9e
ci / vet-and-test (pull_request) Successful in 52s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Align the MCP client with 2026-07-28: resultType, input_required retry, tasks surface (N2 WP3)
Why: the completion doc requires the client to recognize resultType,
treat input_required as a protocol-level result whose answer retries the
original tools/call, and expose tasks/get|update|cancel; today only a
content-block special case existed and the advertised version was never
checked against the server's.
Evidence: GOWORK=off go vet + go test -count=1 ./... green;
TestInputRequiredResultTypeRetriesOriginalCall (ask → tasks/update →
retry, call order and final content), TestNegotiatedVersionFallbackKeepsLegacyPath
(byte-level legacy behavior on 2024-11-05), TestTasksGetAndCancelRoundTrip,
TestRunEndCancelsSeenTasks, TestDefaultSchemaDeclares202012; the
acceptance-③ regression TestStdioSlowCancelThenEcho passes unchanged.
Impact: advertise 2026-07-28 and branch on the negotiated version
(older servers keep byte-identical behavior); synthesized default tool
schemas now declare JSON Schema 2020-12; runs cancel the MCP tasks they
saw via a per-run TaskTracker (other runs on the shared manager are
unaffected).
Refs: Agent-System-Documents#42 (N2 plan WP3)
2026-10-07 13:38:51 +00:00

179 lines
5.3 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
// 本文件是跨测试二进制复用的最小 MCP fake server(stdio JSON-RPC)。
// 测试以「自身可执行文件 + AGENT_NATIVE_TEST_MCP_STDIO=1」re-exec 方式拉起,
// 供真实传输链路测试使用;非测试代码不得调用。
import (
"bufio"
"encoding/json"
"os"
"time"
)
// RunFakeServer 在 stdin/stdout 上运行最小 MCP server(echo / boom / 未知方法),
// 直到 stdin 关闭。测试二进制在 TestMain 的 re-exec 分支调用。
func RunFakeServer() {
scanner := bufio.NewScanner(os.Stdin)
scanner.Buffer(make([]byte, 1<<20), 1<<20)
w := bufio.NewWriter(os.Stdout)
defer w.Flush()
for scanner.Scan() {
var req rpcRequest
if err := json.Unmarshal(scanner.Bytes(), &req); err != nil {
continue
}
if req.ID == 0 {
continue // 通知帧
}
resp := fakeServerHandle(req)
body, _ := json.Marshal(resp)
w.Write(body)
w.WriteByte('\n')
w.Flush()
}
}
func fakeServerHandle(req rpcRequest) rpcResponse {
respond := func(result any) rpcResponse {
raw, _ := json.Marshal(result)
return rpcResponse{JSONRPC: "2.0", ID: &req.ID, Result: raw}
}
fail := func(code int, msg string) rpcResponse {
return rpcResponse{JSONRPC: "2.0", ID: &req.ID, Error: &rpcError{Code: code, Message: msg}}
}
switch req.Method {
case "initialize":
// 协商:取(客户端请求, 自身上限)的较小者(ISO 日期字典序可比);
// AGENT_NATIVE_TEST_MCP_VERSION 可压低上限以测回落路径。
own := ProtocolVersion
if v := os.Getenv("AGENT_NATIVE_TEST_MCP_VERSION"); v != "" {
own = v
}
negotiated := own
var p initializeParams
if req.Params != nil {
raw, _ := json.Marshal(req.Params)
_ = json.Unmarshal(raw, &p)
if p.ProtocolVersion != "" && p.ProtocolVersion < negotiated {
negotiated = p.ProtocolVersion
}
}
return respond(initializeResult{
ProtocolVersion: negotiated,
ServerInfo: clientInfo{Name: "fake-mcp", Version: "0"},
})
case "tools/list":
return respond(toolsListResult{Tools: fakeServerTools()})
case "tools/call":
var p callToolParams
if req.Params != nil {
raw, _ := json.Marshal(req.Params)
_ = json.Unmarshal(raw, &p)
}
switch p.Name {
case "echo":
var args struct {
Text string `json:"text"`
}
_ = json.Unmarshal(p.Arguments, &args)
return respond(callToolResult{Content: []contentBlock{{Type: "text", Text: "echo: " + args.Text}}})
case "boom":
return respond(callToolResult{
Content: []contentBlock{{Type: "text", Text: "boom failed"}},
IsError: true,
})
case "slow":
time.Sleep(2 * time.Second)
return respond(callToolResult{Content: []contentBlock{{Type: "text", Text: "slow done"}}})
case "need_input":
// 2026-07-28 协议级 input_required:tasks/update 投递后重试得终结果。
if input, ok := fakeTaskInputs()["task-1"]; ok {
fakeTaskInputs()["task-1"] = ""
return respond(callToolResult{Content: []contentBlock{{Type: "text", Text: "答案已收到: " + input}}})
}
return respond(callToolResult{
Content: []contentBlock{{Type: "text", Text: "要答案"}},
ResultType: "input_required",
TaskID: "task-1",
})
case "legacy_ask":
return respond(callToolResult{Content: []contentBlock{{Type: "input_required", Text: "选一个颜色"}}})
default:
return fail(-32602, "unknown tool "+p.Name)
}
case "tasks/update":
var p struct {
TaskID string `json:"taskId"`
Input string `json:"input"`
}
if req.Params != nil {
raw, _ := json.Marshal(req.Params)
_ = json.Unmarshal(raw, &p)
}
fakeTaskInputs()[p.TaskID] = p.Input
return respond(map[string]any{"ok": true})
case "tasks/get":
var p struct {
TaskID string `json:"taskId"`
}
if req.Params != nil {
raw, _ := json.Marshal(req.Params)
_ = json.Unmarshal(raw, &p)
}
return respond(taskResult{TaskID: p.TaskID, Status: "running"})
case "tasks/cancel":
var p struct {
TaskID string `json:"taskId"`
}
if req.Params != nil {
raw, _ := json.Marshal(req.Params)
_ = json.Unmarshal(raw, &p)
}
delete(fakeTaskInputs(), p.TaskID)
return respond(map[string]any{"ok": true})
default:
return fail(-32601, "method not found: "+req.Method)
}
}
// fakeTaskInputs 是 need_input 工具的任务输入存储(进程内)。
func fakeTaskInputs() map[string]string {
return taskInputStore
}
var taskInputStore = map[string]string{}
func fakeServerTools() []toolInfo {
return []toolInfo{
{
Name: "echo",
Description: "Echo the text back",
InputSchema: json.RawMessage(`{"type":"object","required":["text"],"properties":{"text":{"type":"string"}}}`),
},
{
Name: "boom",
Description: "Always fails",
InputSchema: json.RawMessage(`{"type":"object","properties":{}}`),
},
{
Name: "slow",
Description: "Responds after a delay",
InputSchema: json.RawMessage(`{"type":"object","properties":{}}`),
},
{
Name: "need_input",
Description: "Requires user input (2026-07-28 resultType)",
InputSchema: json.RawMessage(`{"type":"object","properties":{}}`),
},
{
Name: "legacy_ask",
Description: "Requires user input (legacy content block)",
// 故意无 InputSchema:触发 manager 的合成默认 schema 路径。
},
}
}
// FakeServerEnv 是拉起 fake server 的环境变量名。
const FakeServerEnv = "AGENT_NATIVE_TEST_MCP_STDIO"