Why: models need a capped way to call several tools from one script, and a slow MCP server must not block the first prompt. Evidence: GOWORK=off go test ./runtime/codeorch/ ./runtime/tools/ ./runtime/mcp/ ./runtime/policy/ ./adapters/native/ Impact: run_tool_script stays unregistered until the caller turns it on. Deferred tools stay out of the model list. An unready MCP server connects in the background. Refs: feature/code-orchestration Co-authored-by: Cursor <cursoragent@cursor.com>
155 lines
4.5 KiB
Go
155 lines
4.5 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"`
|
||
// Enabled 为 false 时跳过这台服务器。nil 表示启用。
|
||
Enabled *bool `json:"enabled,omitempty"`
|
||
// Exposure 为 direct 或 deferred。空则工具多时延迟暴露。
|
||
Exposure string `json:"exposure,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)
|
||
}
|
||
switch c.Exposure {
|
||
case "", "direct", "deferred":
|
||
default:
|
||
return fmt.Errorf("mcp server %q: exposure must be direct or deferred", c.Name)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// IsEnabled 报告这台服务器是否要连接。
|
||
func (c ServerConfig) IsEnabled() bool {
|
||
return c.Enabled == nil || *c.Enabled
|
||
}
|
||
|
||
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
|
||
}
|