feat(mcp): 协议层换成官方 Model Context Protocol SDK,删自研实现(裁决 10.4.6 + AGENTS §9.5)
- 新增依赖:github.com/modelcontextprotocol/go-sdk v1.8.0(direct)+ github.com/google/jsonschema-go v0.4.3 (SDK 自带依赖,直接用于 schema 校验/默认值,避免再造一份);go.sum 纯新增 - 删除自研协议层:rpc.go / stdio.go / http.go / sse.go 及其测试 - 用 SDK 的成熟逻辑:命令传输(stdio 子进程)/ SSE / Streamable HTTP、协议版本协商与 -32022 回退、keepalive(含失败阈值)、tools/list_changed 订阅、能力协商(InitializeResult.Capabilities) - 保留自研的四件事(其余零自研):配置→传输、发现→registry 注册、能力族推断、schema 缓存(Prepare 命中不重复解析) - 能力闸诚实化:服务器声明 capabilities 但不含 tools 时 ListTools/DiscoverTools 返回 ErrCapabilityNotDeclared(不静默空集);完全不声明 capabilities 的老式服务器仍按 legacy 处理 - 复核:build 绿 + capability 全包测试绿(mcp 42 用例,含真子进程 stdio / httptest Streamable HTTP / 内存传输三类假 server,不依赖外网)+ `-race` 干净;变异 2 条(featuresFrom 恒 true / 删 applySchema 调用)已还原 - 无 REST/WS/env/数据目录变更;包内 API 有增删(见回报),仓外调用方签名未变(未改 cmd/cyrene 与 plugin/manager)
This commit is contained in:
1 parent
7b05b4bfa9
commit
a1083a322f
26 files changed
+1903
-3011
No files matched your search
+191
-276
@@ -1,264 +1,172 @@
|
||||
// Package mcp provides a minimal Model Context Protocol (MCP) client and the
|
||||
// auto-registration glue that surfaces MCP server tools as capabilities.
|
||||
// Package mcp 用官方 Model Context Protocol Go SDK 接入外部 MCP server,
|
||||
// 并把服务器暴露的工具自动注册进 capability registry。
|
||||
//
|
||||
// An MCP server is an external tool provider. This package speaks JSON-RPC 2.0
|
||||
// to it over stdio (a spawned subprocess), SSE or Streamable HTTP, discovers the
|
||||
// tools it exposes via tools/list, maps each one to a pkg/plugins/sdk Tool, and
|
||||
// registers them into the capability registry so any character can invoke them
|
||||
// (DevDocs/design/09 插件生态 §1).
|
||||
// 协议层(协议版本协商、stdio / SSE / Streamable HTTP 传输、keepalive ping、
|
||||
// notifications/tools/list_changed 订阅)全部交给官方 SDK
|
||||
// (github.com/modelcontextprotocol/go-sdk/mcp)——本包不自造协议,只做四件事:
|
||||
//
|
||||
// The client aligns with the MCP Go SDK's mature behaviour: it negotiates the
|
||||
// protocol version during initialize (requesting the newest version first and
|
||||
// downgrading to a mutually supported one), subscribes to
|
||||
// notifications/tools/list_changed via its capabilities so the registry can
|
||||
// re-sync its tool set, and runs an optional ping keepalive that closes a dead
|
||||
// session so the host can rebuild it.
|
||||
// 1. 配置 → 传输选择(MCPConfig → SDK Transport,含 APIKey 鉴权);
|
||||
// 2. 工具自动发现 → 注册进 capability registry(Registry);
|
||||
// 3. 保留服务器声明的 capabilities 并推断可用能力族(ServerFeatures,
|
||||
// 裁决 10.4.6 / design/09 §1.1「声明了才算可用」);
|
||||
// 4. tools/list input schema 缓存(SchemaCache)+ 调用前校验与默认值填充。
|
||||
//
|
||||
// It is deliberately dependency-light: only the standard library and the
|
||||
// project packages (plugins/sdk, capability/registry, internal/logger) are
|
||||
// used. The wire protocol is implemented directly, so no third-party MCP SDK is
|
||||
// required.
|
||||
//
|
||||
// A caller usually wires it up as:
|
||||
// 典型接法:
|
||||
//
|
||||
// reg := registry.New()
|
||||
// mcpReg := mcp.New(reg, mcp.MCPConfig{Name: "calc", Command: "mcp-calc"})
|
||||
// mcpReg := mcp.NewRegistry(reg, mcp.MCPConfig{Name: "calc", Command: "mcp-calc"})
|
||||
// if err := mcpReg.Register(ctx); err != nil {
|
||||
// log.Fatal(err)
|
||||
// log.Warn("mcp server skipped", "error", err)
|
||||
// }
|
||||
// defer mcpReg.Unregister(ctx)
|
||||
//
|
||||
// which connects to the server, discovers its tools, and registers them. Call
|
||||
// sites then invoke the tools by their MCP name through the registry.
|
||||
// 注册后的工具就是普通 sdk.Tool:Dispatcher 每轮实时读 registry,
|
||||
// 因此热接入的 MCP 工具下一轮模型请求即作为 Request.Tools 注入,模型可直接调用。
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
)
|
||||
|
||||
// MCP protocol versions this client can speak, newest first. The client advertises
|
||||
// its preferred version (latestProtocolVersion) during initialize and accepts a
|
||||
// compatible version from the server, downgrading to a mutually supported version
|
||||
// when the server rejects or cannot match the request (DevDocs/design/09 §1.1).
|
||||
// clientName / clientVersion 是 initialize 时上报给 MCP server 的 clientInfo。
|
||||
const (
|
||||
// protocolVersion20251125 is the current preferred protocol version (the
|
||||
// MCP 2025-11-25 revision that introduced the Streamable HTTP migration).
|
||||
protocolVersion20251125 = "2025-11-25"
|
||||
// protocolVersion20241105 is the legacy protocol version, retained for
|
||||
// servers that have not yet migrated.
|
||||
protocolVersion20241105 = "2024-11-05"
|
||||
// latestProtocolVersion is what the client requests first during initialize.
|
||||
latestProtocolVersion = protocolVersion20251125
|
||||
// codeUnsupportedProtocolVersion is the JSON-RPC error code a server returns
|
||||
// during initialize when it does not support the requested protocol version
|
||||
// and wishes to advertise the versions it does support.
|
||||
codeUnsupportedProtocolVersion = -32022
|
||||
// codeMethodNotFound is the JSON-RPC error code a server returns when it does
|
||||
// not implement a requested method (e.g. a keepalive ping).
|
||||
codeMethodNotFound = -32601
|
||||
// maxInitializeAttempts bounds the initialize version-negotiation retries
|
||||
// (the first attempt plus one downgrade).
|
||||
maxInitializeAttempts = 2
|
||||
// notificationToolsListChanged is the server→client notification signalling
|
||||
// that the tool list has changed.
|
||||
notificationToolsListChanged = "notifications/tools/list_changed"
|
||||
// keepAliveFailureThreshold is the number of consecutive ping failures the
|
||||
// keepalive loop tolerates before closing the connection for a rebuild.
|
||||
keepAliveFailureThreshold = 3
|
||||
clientName = "cyrene"
|
||||
clientVersion = "0.1.0"
|
||||
)
|
||||
|
||||
// supportedProtocolVersions lists every protocol version this client understands,
|
||||
// newest first. Version negotiation picks the first entry that the server also
|
||||
// supports.
|
||||
var supportedProtocolVersions = []string{protocolVersion20251125, protocolVersion20241105}
|
||||
// keepAliveFailureThreshold 是 keepalive 关闭会话前容忍的连续 ping 失败次数默认值。
|
||||
const keepAliveFailureThreshold = 3
|
||||
|
||||
// isSupportedVersion reports whether v is a protocol version this client speaks.
|
||||
func isSupportedVersion(v string) bool {
|
||||
return slices.Contains(supportedProtocolVersions, v)
|
||||
}
|
||||
|
||||
// negotiateMutuallySupportedVersion returns the highest protocol version that is
|
||||
// supported by both this client and the server (which supports supported). It
|
||||
// returns "" when there is no overlap.
|
||||
func negotiateMutuallySupportedVersion(supported []string) string {
|
||||
for _, v := range supportedProtocolVersions {
|
||||
if slices.Contains(supported, v) {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// highestCommonVersion returns the highest protocol version this client supports
|
||||
// that is not newer than v (used to downgrade when the server picks a version the
|
||||
// client cannot speak). It returns "" when no acceptable version exists.
|
||||
func highestCommonVersion(v string) string {
|
||||
for _, s := range supportedProtocolVersions {
|
||||
if s <= v {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Sentinel errors returned by this package. Test with errors.Is.
|
||||
// 本包对外哨兵错误,一律用 errors.Is 判定。
|
||||
var (
|
||||
// ErrNotConnected is returned when an operation requires an established
|
||||
// connection but the client is not yet connected or is closed.
|
||||
// ErrNotConnected 表示操作要求已建立的连接,但客户端尚未连接或已关闭。
|
||||
ErrNotConnected = errors.New("mcp: not connected")
|
||||
// ErrClosed is returned when operating on a client or transport that has
|
||||
// been closed.
|
||||
// ErrClosed 表示在已关闭的客户端上执行操作。
|
||||
ErrClosed = errors.New("mcp: closed")
|
||||
// ErrNoTransport is returned when the configuration selects no transport
|
||||
// (neither Command nor URL is set).
|
||||
// ErrNoTransport 表示配置既没有 Command 也没有 URL,选不出传输。
|
||||
ErrNoTransport = errors.New("mcp: no transport configured")
|
||||
// ErrRPC is returned when the MCP server replies with a JSON-RPC error
|
||||
// object. The concrete server error is wrapped and accessible via
|
||||
// errors.Is/errors.As on an *rpcError.
|
||||
ErrRPC = errors.New("mcp: rpc error")
|
||||
// ErrUnsupportedProtocolVersion is returned when the client and server
|
||||
// cannot agree on a mutually supported protocol version after the
|
||||
// negotiation retries are exhausted.
|
||||
// ErrUnsupportedProtocolVersion 表示客户端与服务器无法就协议版本达成一致。
|
||||
ErrUnsupportedProtocolVersion = errors.New("mcp: unsupported protocol version")
|
||||
// ErrCapabilityNotDeclared 表示调用了服务器在 initialize 里未声明的能力族
|
||||
// (design/09 §1.1「声明了才算可用,未声明不得调用」)。
|
||||
ErrCapabilityNotDeclared = errors.New("mcp: capability not declared by server")
|
||||
)
|
||||
|
||||
// MCPConfig describes a single MCP server connection. The transport is selected
|
||||
// by TransportType when set, otherwise inferred from the fields: stdio when
|
||||
// Command is set, SSE when URL is set. A blank TransportType keeps that legacy
|
||||
// inference so existing configuration keeps working unchanged.
|
||||
// MCPConfig 描述一台 MCP server 的连接方式。
|
||||
// TransportType 非空时按其选择传输;为空则沿用历史推断:
|
||||
// 有 Command 走 stdio,有 URL 走 SSE(TransportType 可显式改走 Streamable HTTP)。
|
||||
type MCPConfig struct {
|
||||
// Name is the human-readable label of the server, used in logs and as the
|
||||
// prefix for registering its tools uniquely.
|
||||
// Name 是服务器标签,用于日志与工具去重前缀。
|
||||
Name string `env:"MCP_NAME"`
|
||||
// Command is the executable path to spawn for a stdio MCP server.
|
||||
// Command 是 stdio 模式下要拉起的可执行文件。
|
||||
Command string `env:"MCP_COMMAND"`
|
||||
// Args are the arguments passed to Command.
|
||||
// Args 是传给 Command 的参数。
|
||||
Args []string `env:"MCP_ARGS"`
|
||||
// URL is the endpoint of an MCP server (SSE stream endpoint, or the
|
||||
// Streamable HTTP endpoint when TransportType is "http").
|
||||
// URL 是 SSE 端点(TransportType="sse")或 Streamable HTTP 端点("http"/"streamable")。
|
||||
URL string `env:"MCP_URL"`
|
||||
// TransportType selects the transport explicitly: "stdio", "sse" or
|
||||
// "http" (Streamable HTTP). Empty means infer from Command/URL.
|
||||
// TransportType 显式选择传输:"stdio"、"sse"、"http"(等价 "streamable")。
|
||||
TransportType string `env:"MCP_TRANSPORT"`
|
||||
// APIKey is the bearer credential sent to an http/sse server as an
|
||||
// Authorization header. Empty means the server needs no auth.
|
||||
// APIKey 是 http/sse 服务器用的 bearer 凭据,作为 Authorization 头发送。
|
||||
APIKey string `env:"MCP_API_KEY"`
|
||||
// Env is extra environment for a stdio subprocess, merged over the parent
|
||||
// process environment (extra keys win). It is ignored by the HTTP transports.
|
||||
// Env 是 stdio 子进程的额外环境变量,叠加在父进程环境之上(同名以这里为准)。
|
||||
Env map[string]string `env:"MCP_ENV"`
|
||||
// Enabled reports whether this server should be activated at startup.
|
||||
// Enabled 表示该服务器是否在启动时接入。
|
||||
Enabled bool `env:"MCP_ENABLED"`
|
||||
}
|
||||
|
||||
// Config holds the set of configured MCP servers.
|
||||
// Config 是一组 MCP server 配置。
|
||||
type Config struct {
|
||||
// Servers is the ordered list of MCP server configurations.
|
||||
// Servers 是按顺序排列的服务器配置。
|
||||
Servers []MCPConfig
|
||||
}
|
||||
|
||||
// ServerInfo describes the MCP server reported by the initialize handshake.
|
||||
// ServerFeatures 是从服务器 initialize 声明里推断出的能力族可用性。
|
||||
// 规则见 featuresFrom:声明了才算可用,未声明不得调用(design/09 §1.1)。
|
||||
type ServerFeatures struct {
|
||||
// Tools 表示服务器声明提供工具(tools/list、tools/call 可用)。
|
||||
Tools bool
|
||||
// Prompts 表示服务器声明提供提示模板。
|
||||
Prompts bool
|
||||
// Resources 表示服务器声明提供资源。
|
||||
Resources bool
|
||||
// Logging 表示服务器声明会发日志消息。
|
||||
Logging bool
|
||||
// Completions 表示服务器声明支持参数自动补全。
|
||||
Completions bool
|
||||
// Sampling 表示允许服务器回调宿主代跑 LLM:宿主配了回调,
|
||||
// 且服务器声明过能力族(即确实是会主动交互的对端)才为真。
|
||||
Sampling bool
|
||||
// Elicitation 表示允许服务器回调宿主向用户追加提问,判定同 Sampling。
|
||||
Elicitation bool
|
||||
// Roots 表示允许向服务器暴露工作区根目录,判定同 Sampling。
|
||||
Roots bool
|
||||
}
|
||||
|
||||
// ServerInfo 是 initialize 握手的结果,服务器声明的 capabilities 原样保留。
|
||||
type ServerInfo struct {
|
||||
// Name is the server-provided name.
|
||||
// Name 是服务器自报名称。
|
||||
Name string
|
||||
// Version is the server-provided version string.
|
||||
// Version 是服务器自报版本。
|
||||
Version string
|
||||
// Instructions 是服务器给出的使用说明(可注入系统提示)。
|
||||
Instructions string
|
||||
// ProtocolVersion 是协商后实际使用的 MCP 协议版本。
|
||||
ProtocolVersion string
|
||||
// Capabilities 是服务器声明的 capabilities 原文(未做删改),对客户端可读。
|
||||
Capabilities map[string]any
|
||||
// Features 是按 Capabilities 推断出的可用能力族。
|
||||
Features ServerFeatures
|
||||
}
|
||||
|
||||
// ToolInfo is a tool exposed by an MCP server.
|
||||
type ToolInfo struct {
|
||||
// Name is the tool identifier used to invoke it.
|
||||
Name string
|
||||
// Description explains what the tool does.
|
||||
Description string
|
||||
// InputSchema is the tool's JSON Schema, typically
|
||||
// {"type":"object","properties":{...}}.
|
||||
InputSchema map[string]any
|
||||
}
|
||||
// SamplingHandler 由宿主提供:服务器请求客户端代跑一次 LLM(sampling/createMessage)时调用。
|
||||
type SamplingHandler func(context.Context, *mcpsdk.CreateMessageRequest) (*mcpsdk.CreateMessageResult, error)
|
||||
|
||||
// ContentBlock is a single item in a tool call result (MCP content block).
|
||||
type ContentBlock struct {
|
||||
// Type is the block type, most commonly "text".
|
||||
Type string `json:"type"`
|
||||
// Text is the textual payload.
|
||||
Text string `json:"text"`
|
||||
}
|
||||
// ElicitationHandler 由宿主提供:服务器向用户索要补充输入(elicitation/create)时调用。
|
||||
type ElicitationHandler func(context.Context, *mcpsdk.ElicitRequest) (*mcpsdk.ElicitResult, error)
|
||||
|
||||
// ToolResult is the structured result of a tools/call.
|
||||
type ToolResult struct {
|
||||
// Content holds the response content blocks.
|
||||
Content []ContentBlock
|
||||
// IsError reports whether the server flagged the call as an error.
|
||||
IsError bool
|
||||
}
|
||||
|
||||
// Client is a minimal MCP client bound to a single server described by cfg. It
|
||||
// is safe for concurrent use; all calls are serialized internally.
|
||||
// Client 是一台 MCP server 的客户端。它内部持有一个官方 SDK 会话,
|
||||
// 并发安全:Connect/Close 互斥,其余调用由 SDK 串行化。
|
||||
type Client struct {
|
||||
cfg MCPConfig
|
||||
log *slog.Logger
|
||||
tr Transport
|
||||
mu sync.Mutex
|
||||
state state
|
||||
info ServerInfo
|
||||
cfg MCPConfig
|
||||
log *slog.Logger
|
||||
|
||||
// negotiatedVersion is the protocol version agreed during initialize; it is
|
||||
// sent back to the server in the Mcp-Protocol-Version header by the HTTP
|
||||
// transport.
|
||||
negotiatedVersion string
|
||||
mu sync.Mutex
|
||||
state state
|
||||
session *mcpsdk.ClientSession
|
||||
info ServerInfo
|
||||
|
||||
// preferredProtocolVersion, when non-empty, overrides latestProtocolVersion
|
||||
// as the version requested first during initialize. Tests use it to drive the
|
||||
// negotiation path deterministically.
|
||||
// tr 是注入或按 cfg 选出的传输;schemas 是 tools/list input schema 缓存。
|
||||
tr mcpsdk.Transport
|
||||
schemas *SchemaCache
|
||||
|
||||
// preferredProtocolVersion 非空时固定 initialize 请求的协议版本(测试/排障用)。
|
||||
preferredProtocolVersion string
|
||||
|
||||
// keepAlive is the ping interval for the connection keepalive; zero disables
|
||||
// it. keepAliveFailures is the tolerated consecutive-failure count.
|
||||
// keepAlive 是 keepalive ping 间隔,零值关闭;keepAliveFailures 是容忍失败数。
|
||||
keepAlive time.Duration
|
||||
keepAliveFailures int
|
||||
keepaliveCancel context.CancelFunc
|
||||
|
||||
// onToolListChanged is invoked (in a fresh goroutine) when the server sends
|
||||
// notifications/tools/list_changed, so the host can refresh its tool set.
|
||||
sampling SamplingHandler
|
||||
elicitation ElicitationHandler
|
||||
roots []*mcpsdk.Root
|
||||
|
||||
// onToolListChanged 在服务器发 notifications/tools/list_changed 时被调用
|
||||
// (新 goroutine 里,已被 refreshPending 合并),用于增量刷新 registry。
|
||||
onToolListChanged func()
|
||||
refreshPending atomic.Bool
|
||||
}
|
||||
|
||||
// protocolVersion returns the protocol version negotiated by initialize, or ""
|
||||
// before a successful handshake.
|
||||
func (c *Client) protocolVersion() string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.negotiatedVersion
|
||||
}
|
||||
|
||||
// setProtocolVersion records the version negotiated by initialize.
|
||||
func (c *Client) setProtocolVersion(v string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.negotiatedVersion = v
|
||||
}
|
||||
|
||||
// capabilities returns the client capability declaration for the given protocol
|
||||
// version. It advertises tools.listChanged so the server notifies the client when
|
||||
// the tool list changes.
|
||||
func (c *Client) capabilities(version string) map[string]any {
|
||||
caps := map[string]any{}
|
||||
if c.onToolListChanged != nil {
|
||||
caps["tools"] = map[string]any{"listChanged": true}
|
||||
}
|
||||
return caps
|
||||
}
|
||||
|
||||
// state tracks the client lifecycle.
|
||||
// state 是客户端生命周期。
|
||||
type state int
|
||||
|
||||
const (
|
||||
@@ -267,63 +175,10 @@ const (
|
||||
stateClosed
|
||||
)
|
||||
|
||||
// Transport is the JSON-RPC message exchange with an MCP server over a concrete
|
||||
// transport (stdio subprocess, SSE or Streamable HTTP). Implementations must be
|
||||
// safe for concurrent use. This is exported so tests and hosts can inject a fake
|
||||
// transport.
|
||||
type Transport interface {
|
||||
// Start makes the transport ready to exchange messages.
|
||||
Start(ctx context.Context) error
|
||||
// Call sends a JSON-RPC request and decodes the matching response into
|
||||
// result. Params is marshaled into the request body; result is unmarshaled
|
||||
// from the response's result field.
|
||||
Call(ctx context.Context, method string, params any, result any) error
|
||||
// Close releases every underlying resource (subprocess, streams, sockets).
|
||||
Close() error
|
||||
}
|
||||
|
||||
// notifier is implemented by transports that can send a JSON-RPC notification
|
||||
// (a request with no id and no response).
|
||||
type notifier interface {
|
||||
notify(ctx context.Context, method string, params any) error
|
||||
}
|
||||
|
||||
// serverNotifier is implemented by transports that can deliver server-initiated
|
||||
// notifications (messages with no id, e.g. notifications/tools/list_changed) back
|
||||
// to the client. The handler is invoked from the transport's read path, so it must
|
||||
// not block or re-enter the same transport synchronously.
|
||||
type serverNotifier interface {
|
||||
setOnNotification(fn func(method string))
|
||||
}
|
||||
|
||||
// attachNotificationSink wires a server→client notification handler into tr, if
|
||||
// tr supports one.
|
||||
func attachNotificationSink(tr Transport, fn func(method string)) {
|
||||
if on, ok := tr.(serverNotifier); ok {
|
||||
on.setOnNotification(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// protocolProvider is implemented by transports that include the negotiated MCP
|
||||
// protocol version in requests (the Mcp-Protocol-Version header of the HTTP
|
||||
// transport).
|
||||
type protocolProvider interface {
|
||||
setProtocolProvider(fn func() string)
|
||||
}
|
||||
|
||||
// attachProtocolProvider wires the negotiated-version getter into tr, if tr
|
||||
// supports one.
|
||||
func attachProtocolProvider(tr Transport, fn func() string) {
|
||||
if p, ok := tr.(protocolProvider); ok {
|
||||
p.setProtocolProvider(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// Option customizes a Client built by New.
|
||||
// Option 定制 New 构建的 Client。
|
||||
type Option func(*Client)
|
||||
|
||||
// WithLogger overrides the client's logger. The given logger is tagged with the
|
||||
// source capability/mcp.
|
||||
// WithLogger 覆盖客户端日志器;给定的日志器会被标上 source=capability/mcp。
|
||||
func WithLogger(l *slog.Logger) Option {
|
||||
return func(c *Client) {
|
||||
if l != nil {
|
||||
@@ -332,19 +187,15 @@ func WithLogger(l *slog.Logger) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithTransport overrides the transport used by the client. This is primarily
|
||||
// for tests that inject a fake transport instead of spawning a subprocess or
|
||||
// dialing SSE.
|
||||
func WithTransport(t Transport) Option {
|
||||
// WithTransport 注入传输(官方 SDK 的 Transport),主要用于测试。
|
||||
func WithTransport(t mcpsdk.Transport) Option {
|
||||
return func(c *Client) {
|
||||
c.tr = t
|
||||
}
|
||||
}
|
||||
|
||||
// WithKeepAlive enables the connection keepalive, sending an MCP ping every
|
||||
// interval and closing the session after keepAliveFailures consecutive missed
|
||||
// pings. A zero interval leaves keepalive disabled. When keepAliveFailures is
|
||||
// omitted, the package default (keepAliveFailureThreshold) is used.
|
||||
// WithKeepAlive 打开 keepalive:每 interval 发一次 ping,连续失败 keepAliveFailures
|
||||
// 次即关闭会话(由 SDK 执行)。间隔为零则关闭 keepalive;失败次数缺省用包默认值。
|
||||
func WithKeepAlive(interval time.Duration, keepAliveFailures ...int) Option {
|
||||
failures := keepAliveFailureThreshold
|
||||
if len(keepAliveFailures) > 0 && keepAliveFailures[0] > 0 {
|
||||
@@ -356,33 +207,60 @@ func WithKeepAlive(interval time.Duration, keepAliveFailures ...int) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithToolListChangedHandler registers a handler that is invoked (in a fresh
|
||||
// goroutine) whenever the server sends notifications/tools/list_changed, letting
|
||||
// the host refresh its registered tool set.
|
||||
// WithToolListChangedHandler 注册工具清单变更回调,由 registry 用于增量刷新。
|
||||
func WithToolListChangedHandler(fn func()) Option {
|
||||
return func(c *Client) {
|
||||
c.onToolListChanged = fn
|
||||
}
|
||||
}
|
||||
|
||||
// WithProtocolVersion overrides the protocol version requested first during
|
||||
// initialize. It is primarily for tests that drive the negotiation path
|
||||
// deterministically; callers should normally leave it unset to use
|
||||
// latestProtocolVersion.
|
||||
// WithProtocolVersion 固定 initialize 请求的协议版本;不设置则用 SDK 最新版本,
|
||||
// 服务器拒绝时由 SDK 自动回退到双方都支持的版本。
|
||||
func WithProtocolVersion(v string) Option {
|
||||
return func(c *Client) {
|
||||
c.preferredProtocolVersion = v
|
||||
}
|
||||
}
|
||||
|
||||
// New builds a Client for cfg. By default it uses a discard logger and selects a
|
||||
// stdio or SSE transport from cfg; pass WithLogger and/or WithTransport to
|
||||
// override.
|
||||
// WithSchemaCache 复用外部 schema 缓存;不设置则每个客户端自建一份。
|
||||
func WithSchemaCache(cache *SchemaCache) Option {
|
||||
return func(c *Client) {
|
||||
if cache != nil {
|
||||
c.schemas = cache
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithSamplingHandler 注册 sampling 回调。设了才向服务器宣告 sampling 能力;
|
||||
// 服务器未声明能力族时回调会被拒绝(ErrCapabilityNotDeclared)。
|
||||
func WithSamplingHandler(h SamplingHandler) Option {
|
||||
return func(c *Client) {
|
||||
c.sampling = h
|
||||
}
|
||||
}
|
||||
|
||||
// WithElicitationHandler 注册 elicitation 回调。设了才向服务器宣告 elicitation 能力。
|
||||
func WithElicitationHandler(h ElicitationHandler) Option {
|
||||
return func(c *Client) {
|
||||
c.elicitation = h
|
||||
}
|
||||
}
|
||||
|
||||
// WithRoots 注册工作区根目录。设了才向服务器宣告 roots 能力。
|
||||
func WithRoots(roots ...*mcpsdk.Root) Option {
|
||||
return func(c *Client) {
|
||||
c.roots = append([]*mcpsdk.Root(nil), roots...)
|
||||
}
|
||||
}
|
||||
|
||||
// New 构建 cfg 对应的 Client,默认用丢弃日志器并自动选传输;
|
||||
// 用 WithLogger / WithTransport 覆盖。
|
||||
func New(cfg MCPConfig, opts ...Option) *Client {
|
||||
c := &Client{
|
||||
cfg: cfg,
|
||||
log: defaultLogger(),
|
||||
state: stateNew,
|
||||
cfg: cfg,
|
||||
log: defaultLogger(),
|
||||
state: stateNew,
|
||||
schemas: NewSchemaCache(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
@@ -390,19 +268,56 @@ func New(cfg MCPConfig, opts ...Option) *Client {
|
||||
return c
|
||||
}
|
||||
|
||||
// defaultLogger returns a discard logger tagged with the mcp source. Hosts
|
||||
// should prefer WithLogger so records are observable.
|
||||
// defaultLogger 返回带 capability/mcp 来源标签的丢弃日志器;宿主应传 WithLogger。
|
||||
func defaultLogger() *slog.Logger {
|
||||
base := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelWarn}))
|
||||
return logger.WithSource(base, "capability/mcp")
|
||||
}
|
||||
|
||||
// transport returns the established transport, or nil when not connected.
|
||||
func (c *Client) transport() Transport {
|
||||
// Name 返回服务器标签。
|
||||
func (c *Client) Name() string { return c.cfg.Name }
|
||||
|
||||
// ServerInfo 返回 initialize 握手结果;未连接时为零值。
|
||||
func (c *Client) ServerInfo() ServerInfo {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.state != stateConnected || c.tr == nil {
|
||||
return c.info
|
||||
}
|
||||
|
||||
// Features 返回按服务器声明推断出的能力族可用性;未连接时全为 false。
|
||||
func (c *Client) Features() ServerFeatures {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.info.Features
|
||||
}
|
||||
|
||||
// SchemaCache 返回该客户端使用的 input schema 缓存。
|
||||
func (c *Client) SchemaCache() *SchemaCache { return c.schemas }
|
||||
|
||||
// Session 返回底层官方 SDK 会话(未连接时为 nil),供需要 SDK 原生能力的调用方使用。
|
||||
func (c *Client) Session() *mcpsdk.ClientSession {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.state != stateConnected {
|
||||
return nil
|
||||
}
|
||||
return c.tr
|
||||
return c.session
|
||||
}
|
||||
|
||||
// connected 返回已建立的 SDK 会话,未连接时返回 nil。
|
||||
func (c *Client) connected() *mcpsdk.ClientSession {
|
||||
return c.Session()
|
||||
}
|
||||
|
||||
// begin 做 Connect 前的状态检查:已连接返回 same=true(幂等),已关闭返回错误。
|
||||
func (c *Client) begin() (same bool, err error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
switch c.state {
|
||||
case stateClosed:
|
||||
return false, fmt.Errorf("mcp connect %q: %w", c.cfg.Name, ErrClosed)
|
||||
case stateConnected:
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
Reference in new issue
Block a user