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)
184 lines
5.9 KiB
Go
184 lines
5.9 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 协议版本(2026-07-28 RC:
|
||
// resultType 判别、input_required 协议级结果、tasks/* 长任务面)。
|
||
const ProtocolVersion = "2026-07-28"
|
||
|
||
// LegacyProtocolVersion 是旧版协议(无 resultType/tasks),协商回落时用。
|
||
const LegacyProtocolVersion = "2024-11-05"
|
||
|
||
// SupportsResultType 报告协商版本是否支持 2026-07-28 的 resultType 语义
|
||
// (ISO 日期版本按字典序可比)。
|
||
func SupportsResultType(negotiated string) bool {
|
||
return negotiated >= "2026-07-28"
|
||
}
|
||
|
||
// 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"`
|
||
// ResultType 是 2026-07-28 的协议级结果判别(空 = 普通结果)。
|
||
// "input_required" 表示服务端需要用户输入:客户端经 ask 端口取得
|
||
// 应答后(有 TaskID 时先 tasks/update)用原参数重试 tools/call。
|
||
ResultType string `json:"resultType,omitempty"`
|
||
TaskID string `json:"taskId,omitempty"`
|
||
}
|
||
|
||
// taskResult 是 tasks/get 的返回(2026-07-28 长任务面;字段集按 RC 常用
|
||
// 形态解析,未识别字段忽略)。
|
||
type taskResult struct {
|
||
TaskID string `json:"taskId"`
|
||
Status string `json:"status"`
|
||
Detail string `json:"detail,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)
|
||
// NegotiatedVersion 是 initialize 后服务端确认的协议版本(未握手为空)。
|
||
NegotiatedVersion() string
|
||
ListTools(ctx context.Context) ([]toolInfo, error)
|
||
CallTool(ctx context.Context, name string, args json.RawMessage) (callToolResult, error)
|
||
// TasksGet/Update/Cancel 是 2026-07-28 的长任务面。
|
||
TasksGet(ctx context.Context, taskID string) (taskResult, error)
|
||
TasksUpdate(ctx context.Context, taskID, input string) error
|
||
TasksCancel(ctx context.Context, taskID string) error
|
||
Close() error
|
||
}
|