Why: external tool ecosystems speak MCP; the kernel had no way to mount them, so every external capability needed a bespoke builtin. Evidence: go vet ./... clean; go test ./... 11 packages ok. Tests run against a REAL stdio server (test binary re-exec via mcp.RunFakeServer) and httptest HTTP servers: initialize handshake, tools/list with pagination cap, tools/call success / tool-level isError / unknown-tool rpc error, JSON and SSE response modes, Mcp-Session-Id capture, manager registration as mcp__<server>__<tool>, namespace wildcard allowlist (mcp__fake__* keeps only that namespace, unknown wildcard fails closed), unreachable server failing the step, hung server bounded by per-request timeout, config validation (duplicate names, dual/missing transport, bad name chars), and an adapter end-to-end execution calling an MCP tool. Impact: new runtime/mcp package; native.Options gains MCP *mcp.Manager and Close() for process cleanup; tools.Registry.Restrict gains prefix/* namespace entries; server-provided metadata is capped (64 tools/server, 1 KiB descriptions, 64 KiB text results) and treated as untrusted input. Refs: plan B5; agent-execution standard (tool providers get authorized capability only).
95 lines
2.6 KiB
Go
95 lines
2.6 KiB
Go
package mcp
|
||
|
||
// 本文件是跨测试二进制复用的最小 MCP fake server(stdio JSON-RPC)。
|
||
// 测试以「自身可执行文件 + AGENT_NATIVE_TEST_MCP_STDIO=1」re-exec 方式拉起,
|
||
// 供真实传输链路测试使用;非测试代码不得调用。
|
||
|
||
import (
|
||
"bufio"
|
||
"encoding/json"
|
||
"os"
|
||
)
|
||
|
||
// 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,
|
||
})
|
||
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":{}}`),
|
||
},
|
||
}
|
||
}
|
||
|
||
// FakeServerEnv 是拉起 fake server 的环境变量名。
|
||
const FakeServerEnv = "AGENT_NATIVE_TEST_MCP_STDIO"
|