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)
179 lines
5.3 KiB
Go
179 lines
5.3 KiB
Go
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"
|