Why: a tool-budget stop left tool calls without results, host callbacks raced, a canceled MCP read stole the next response, and fetch re-resolved after the SSRF check. Evidence: GOWORK=off go vet ./... && go test -count=1 ./... && go test -race ./runtime/... ./adapters/native/... passed, including TestLoopBudgetToolCallCap, TestParallelHostCallbacksAreSerial, TestStdioSlowCancelThenEcho, TestPresetsAndClientForChatCompletions, TestFetchResolveChangesToLoopbackRejected, and TestFetchHostCheckRedirectStillRejectsPrivate. Impact: over-budget calls get an error result and no tool event; one Run serializes host callbacks; MCP dispatches by id; anthropic and google presets use OpenAI-compatible chat endpoints; fetch dials the address it just checked. deps/agent-intent is 3130190; error-code-system and logging-system are a3ee67b and 49b5d8b. Refs: N1, N2, N3, N4, N13, N14 Co-authored-by: Cursor <cursoragent@cursor.com>
104 lines
2.9 KiB
Go
104 lines
2.9 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":
|
||
return respond(initializeResult{
|
||
ProtocolVersion: ProtocolVersion,
|
||
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"}}})
|
||
default:
|
||
return fail(-32602, "unknown tool "+p.Name)
|
||
}
|
||
default:
|
||
return fail(-32601, "method not found: "+req.Method)
|
||
}
|
||
}
|
||
|
||
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":{}}`),
|
||
},
|
||
}
|
||
}
|
||
|
||
// FakeServerEnv 是拉起 fake server 的环境变量名。
|
||
const FakeServerEnv = "AGENT_NATIVE_TEST_MCP_STDIO"
|