Files
Agent-Native/runtime/mcp/protocol.go
weichen fcd2539b9e
ci / vet-and-test (pull_request) Successful in 52s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Align the MCP client with 2026-07-28: resultType, input_required retry, tasks surface (N2 WP3)
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)
2026-10-07 13:38:51 +00:00

184 lines
5.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}