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).
141 lines
4.0 KiB
Go
141 lines
4.0 KiB
Go
// Package mcp 实现 Model Context Protocol 客户端(stdio 与 Streamable HTTP
|
||
// 两种传输),并把 server 工具桥接为 runtime/tools.Registry 的普通工具。
|
||
//
|
||
// 边界声明:server 提供的描述与 schema 视为不可信输入(长度受限、不解释);
|
||
// 工具调用的策略(allowlist/确认)仍由 tools.Registry 策略层统一执行。
|
||
package mcp
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"time"
|
||
)
|
||
|
||
// ProtocolVersion 是本客户端声明的 MCP 协议版本。
|
||
const ProtocolVersion = "2024-11-05"
|
||
|
||
// ServerConfig 声明一个 MCP server 接入点:Command(stdio)或 URL(HTTP)二选一。
|
||
type ServerConfig struct {
|
||
Name string `json:"name"`
|
||
Command string `json:"command,omitempty"`
|
||
Args []string `json:"args,omitempty"`
|
||
Env []string `json:"env,omitempty"`
|
||
URL string `json:"url,omitempty"`
|
||
Headers map[string]string `json:"headers,omitempty"`
|
||
// Timeout 单次请求上限(默认 30s)。
|
||
Timeout time.Duration `json:"timeout,omitempty"`
|
||
}
|
||
|
||
// Validate 校验配置完整性(fail-closed:命名不规范或双传输/无传输都拒绝)。
|
||
func (c ServerConfig) Validate() error {
|
||
if !validName(c.Name) {
|
||
return fmt.Errorf("mcp server name %q must be [a-zA-Z0-9_-]+", c.Name)
|
||
}
|
||
hasCmd := c.Command != ""
|
||
hasURL := c.URL != ""
|
||
if hasCmd == hasURL {
|
||
return fmt.Errorf("mcp server %q: exactly one of command or url is required", c.Name)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validName(s string) bool {
|
||
if s == "" {
|
||
return false
|
||
}
|
||
for _, r := range s {
|
||
switch {
|
||
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_':
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// ---- JSON-RPC 2.0 envelope ----
|
||
|
||
type rpcRequest struct {
|
||
JSONRPC string `json:"jsonrpc"`
|
||
ID int64 `json:"id,omitempty"`
|
||
Method string `json:"method"`
|
||
Params any `json:"params,omitempty"`
|
||
}
|
||
|
||
type rpcNotification struct {
|
||
JSONRPC string `json:"jsonrpc"`
|
||
Method string `json:"method"`
|
||
}
|
||
|
||
type rpcResponse struct {
|
||
JSONRPC string `json:"jsonrpc"`
|
||
ID *int64 `json:"id"`
|
||
Result json.RawMessage `json:"result,omitempty"`
|
||
Error *rpcError `json:"error,omitempty"`
|
||
}
|
||
|
||
type rpcError struct {
|
||
Code int `json:"code"`
|
||
Message string `json:"message"`
|
||
Data json.RawMessage `json:"data,omitempty"`
|
||
}
|
||
|
||
func (e *rpcError) Error() string {
|
||
return fmt.Sprintf("mcp rpc error %d: %s", e.Code, e.Message)
|
||
}
|
||
|
||
// ---- MCP 语义类型 ----
|
||
|
||
type initializeParams struct {
|
||
ProtocolVersion string `json:"protocolVersion"`
|
||
Capabilities json.RawMessage `json:"capabilities,omitempty"`
|
||
ClientInfo clientInfo `json:"clientInfo"`
|
||
}
|
||
|
||
type clientInfo struct {
|
||
Name string `json:"name"`
|
||
Version string `json:"version"`
|
||
}
|
||
|
||
type initializeResult struct {
|
||
ProtocolVersion string `json:"protocolVersion"`
|
||
Capabilities json.RawMessage `json:"capabilities"`
|
||
ServerInfo clientInfo `json:"serverInfo"`
|
||
Instructions string `json:"instructions,omitempty"`
|
||
}
|
||
|
||
type toolInfo struct {
|
||
Name string `json:"name"`
|
||
Description string `json:"description,omitempty"`
|
||
InputSchema json.RawMessage `json:"inputSchema,omitempty"`
|
||
}
|
||
|
||
type toolsListResult struct {
|
||
Tools []toolInfo `json:"tools"`
|
||
NextCursor string `json:"nextCursor,omitempty"`
|
||
}
|
||
|
||
type callToolParams struct {
|
||
Name string `json:"name"`
|
||
Arguments json.RawMessage `json:"arguments,omitempty"`
|
||
}
|
||
|
||
type callToolResult struct {
|
||
Content []contentBlock `json:"content"`
|
||
IsError bool `json:"isError,omitempty"`
|
||
}
|
||
|
||
type contentBlock struct {
|
||
Type string `json:"type"`
|
||
Text string `json:"text,omitempty"`
|
||
}
|
||
|
||
// Client 是一个已连接 MCP server 的最小面(initialize / tools/list / tools/call)。
|
||
type Client interface {
|
||
Initialize(ctx context.Context) (initializeResult, error)
|
||
ListTools(ctx context.Context) ([]toolInfo, error)
|
||
CallTool(ctx context.Context, name string, args json.RawMessage) (callToolResult, error)
|
||
Close() error
|
||
}
|