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
+130
-337
@@ -2,210 +2,155 @@ package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
sdkjsonrpc "github.com/modelcontextprotocol/go-sdk/jsonrpc"
|
||||
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
||||
)
|
||||
|
||||
// initializeParams is the MCP initialize request body.
|
||||
type initializeParams struct {
|
||||
ProtocolVersion string `json:"protocolVersion"`
|
||||
Capabilities map[string]any `json:"capabilities"`
|
||||
ClientInfo clientInfo `json:"clientInfo"`
|
||||
}
|
||||
|
||||
// initializeResult is the MCP initialize response body.
|
||||
type initializeResult struct {
|
||||
ProtocolVersion string `json:"protocolVersion"`
|
||||
Capabilities map[string]any `json:"capabilities"`
|
||||
ServerInfo serverInfo `json:"serverInfo"`
|
||||
}
|
||||
|
||||
// clientInfo identifies the client to the server.
|
||||
type clientInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// serverInfo identifies the server to the client.
|
||||
type serverInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// Connect selects the transport (unless one was injected), starts it, completes
|
||||
// the MCP initialize handshake, and marks the client connected. It is
|
||||
// idempotent; a second call while connected returns nil.
|
||||
// Connect 选择传输(未注入时)、建立官方 SDK 会话并完成 initialize 握手。
|
||||
// 幂等:已连接时直接返回 nil;已关闭时返回 ErrClosed。
|
||||
func (c *Client) Connect(ctx context.Context) error {
|
||||
c.mu.Lock()
|
||||
switch c.state {
|
||||
case stateClosed:
|
||||
c.mu.Unlock()
|
||||
return fmt.Errorf("mcp connect %q: %w", c.cfg.Name, ErrClosed)
|
||||
case stateConnected:
|
||||
c.mu.Unlock()
|
||||
same, err := c.begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if same {
|
||||
return nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
tr := c.tr
|
||||
if tr == nil {
|
||||
var err error
|
||||
if tr, err = newTransport(c.cfg, c.log); err != nil {
|
||||
if tr, err = newTransport(c.cfg); err != nil {
|
||||
return fmt.Errorf("mcp connect %q: %w", c.cfg.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tr.Start(ctx); err != nil {
|
||||
tr.Close()
|
||||
return fmt.Errorf("mcp connect %q: %w", c.cfg.Name, err)
|
||||
}
|
||||
// Wire server→client notification routing (list_changed) before the server
|
||||
// can start sending them after notifications/initialized.
|
||||
attachNotificationSink(tr, c.handleServerNotification)
|
||||
|
||||
info, err := c.initialize(ctx, tr)
|
||||
session, err := c.newSDKClient().Connect(ctx, tr, c.sessionOptions())
|
||||
if err != nil {
|
||||
tr.Close()
|
||||
return fmt.Errorf("mcp connect %q: %w", c.cfg.Name, err)
|
||||
return fmt.Errorf("mcp connect %q: %w", c.cfg.Name, wrapConnectErr(err))
|
||||
}
|
||||
// Let the transport echo the negotiated protocol version (Streamable HTTP).
|
||||
attachProtocolProvider(tr, c.protocolVersion)
|
||||
c.sendInitialized(ctx, tr)
|
||||
|
||||
info := serverInfoFrom(session, c.hostFeatures())
|
||||
c.mu.Lock()
|
||||
c.session = session
|
||||
c.tr = tr
|
||||
c.info = info
|
||||
c.state = stateConnected
|
||||
c.info = info
|
||||
c.mu.Unlock()
|
||||
|
||||
c.startKeepalive()
|
||||
c.log.Debug("mcp connected", "name", c.cfg.Name, "server", info.Name, "protocol", c.protocolVersion())
|
||||
c.log.Debug("mcp connected", "name", c.cfg.Name, "server", info.Name,
|
||||
"version", info.Version, "protocol", info.ProtocolVersion,
|
||||
"tools_declared", info.Features.Tools)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize performs the MCP initialize handshake on an already-connected
|
||||
// client. It returns the server identity.
|
||||
func (c *Client) Initialize(ctx context.Context) (ServerInfo, error) {
|
||||
tr := c.transport()
|
||||
if tr == nil {
|
||||
return ServerInfo{}, fmt.Errorf("mcp initialize %q: %w", c.cfg.Name, ErrNotConnected)
|
||||
// Close 关闭 SDK 会话并标记客户端已关闭。幂等。
|
||||
func (c *Client) Close() error {
|
||||
c.mu.Lock()
|
||||
if c.state == stateClosed {
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
return c.initialize(ctx, tr)
|
||||
session := c.session
|
||||
c.session = nil
|
||||
c.state = stateClosed
|
||||
c.mu.Unlock()
|
||||
|
||||
if session != nil {
|
||||
if err := session.Close(); err != nil {
|
||||
return fmt.Errorf("mcp close %q: %w", c.cfg.Name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// initialize performs the MCP initialize handshake with protocol version
|
||||
// negotiation (DevDocs/design/09 §1.1). It does not hard-code a single version:
|
||||
// it first requests latestProtocolVersion, and if the server rejects that version
|
||||
// (a JSON-RPC error with code codeUnsupportedProtocolVersion carrying the
|
||||
// supported set) or replies with a version this client cannot speak, it downgrades
|
||||
// once to the highest mutually supported version and retries. It records the
|
||||
// agreed version so the HTTP transport can echo it back as Mcp-Protocol-Version.
|
||||
func (c *Client) initialize(ctx context.Context, tr Transport) (ServerInfo, error) {
|
||||
sent := c.preferredProtocolVersion
|
||||
if sent == "" {
|
||||
sent = latestProtocolVersion
|
||||
// ListTools 返回服务器暴露的工具(SDK 自动翻页)。
|
||||
// 服务器在 initialize 里没有声明 tools 能力族时拒绝调用、不发出 RPC:
|
||||
// design/09 §1.1「声明了才算可用,未声明不得调用」。
|
||||
func (c *Client) ListTools(ctx context.Context) ([]ToolInfo, error) {
|
||||
session := c.connected()
|
||||
if session == nil {
|
||||
return nil, fmt.Errorf("mcp list tools %q: %w", c.cfg.Name, ErrNotConnected)
|
||||
}
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < maxInitializeAttempts; attempt++ {
|
||||
params := initializeParams{
|
||||
ProtocolVersion: sent,
|
||||
Capabilities: c.capabilities(sent),
|
||||
ClientInfo: clientInfo{Name: "cyrene", Version: "0.1.0"},
|
||||
}
|
||||
var res initializeResult
|
||||
err := tr.Call(ctx, "initialize", params, &res)
|
||||
if !c.Features().Tools {
|
||||
return nil, fmt.Errorf("mcp list tools %q: %w", c.cfg.Name, ErrCapabilityNotDeclared)
|
||||
}
|
||||
out := make([]ToolInfo, 0, 8)
|
||||
for tool, err := range session.Tools(ctx, nil) {
|
||||
if err != nil {
|
||||
// Server rejected the requested version and may advertise the versions
|
||||
// it supports; downgrade to the highest mutually supported one.
|
||||
if supported, ok := unsupportedVersions(err); ok {
|
||||
if v := negotiateMutuallySupportedVersion(supported); v != "" && v != sent {
|
||||
c.log.Debug("mcp downgrading protocol version",
|
||||
"name", c.cfg.Name, "requested", sent, "version", v)
|
||||
sent = v
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return ServerInfo{}, fmt.Errorf("mcp initialize %q: %w: %w", c.cfg.Name, ErrUnsupportedProtocolVersion, err)
|
||||
}
|
||||
return ServerInfo{}, err
|
||||
return nil, fmt.Errorf("mcp list tools %q: %w", c.cfg.Name, err)
|
||||
}
|
||||
negotiated := res.ProtocolVersion
|
||||
if negotiated == "" {
|
||||
negotiated = sent
|
||||
}
|
||||
if !isSupportedVersion(negotiated) {
|
||||
// The server replied with a version this client does not speak;
|
||||
// downgrade once to the highest version we both accept.
|
||||
if v := highestCommonVersion(negotiated); v != "" && v != sent {
|
||||
c.log.Debug("mcp renegotiating protocol version",
|
||||
"name", c.cfg.Name, "server", negotiated, "version", v)
|
||||
sent = v
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return ServerInfo{}, fmt.Errorf("mcp initialize %q: %w: server advertised %q", c.cfg.Name, ErrUnsupportedProtocolVersion, negotiated)
|
||||
}
|
||||
c.setProtocolVersion(negotiated)
|
||||
return ServerInfo{Name: res.ServerInfo.Name, Version: res.ServerInfo.Version}, nil
|
||||
out = append(out, toolInfoFrom(tool))
|
||||
}
|
||||
return ServerInfo{}, fmt.Errorf("mcp initialize %q: %w: %w", c.cfg.Name, ErrUnsupportedProtocolVersion, lastErr)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// isMethodNotFound reports whether err wraps a JSON-RPC "method not found" error
|
||||
// (code -32601), which a peer returns when it does not implement a method such as
|
||||
// the keepalive ping.
|
||||
func isMethodNotFound(err error) bool {
|
||||
var re *rpcError
|
||||
return errors.As(err, &re) && re.Code == codeMethodNotFound
|
||||
}
|
||||
|
||||
// unsupportedVersions reports the protocol versions the server supports, extracted
|
||||
// from the data field of a codeUnsupportedProtocolVersion JSON-RPC error. It
|
||||
// reports ok=false when err does not carry such an error or the data is unusable.
|
||||
func unsupportedVersions(err error) ([]string, bool) {
|
||||
var re *rpcError
|
||||
if !errors.As(err, &re) || re.Code != codeUnsupportedProtocolVersion {
|
||||
return nil, false
|
||||
}
|
||||
if re.Data == nil {
|
||||
return nil, false
|
||||
}
|
||||
data, err := json.Marshal(re.Data)
|
||||
// CallTool 调用服务器上的工具。args 必须是 JSON 对象(map[string]any)。
|
||||
// 调用前用 schema 缓存里的 input schema 校验参数并补默认值。
|
||||
func (c *Client) CallTool(ctx context.Context, name string, args any) (ToolResult, error) {
|
||||
arguments, err := normalizeArgs(args)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
return ToolResult{}, fmt.Errorf("mcp call tool %q: %w", name, err)
|
||||
}
|
||||
var payload struct {
|
||||
Supported []string `json:"supported"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &payload); err != nil || len(payload.Supported) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
return payload.Supported, true
|
||||
return c.callTool(ctx, name, arguments, nil)
|
||||
}
|
||||
|
||||
// sendInitialized best-effort sends the notifications/initialized advisory ack.
|
||||
func (c *Client) sendInitialized(ctx context.Context, tr Transport) {
|
||||
n, ok := tr.(notifier)
|
||||
if !ok {
|
||||
return
|
||||
// callTool 是 CallTool 的内部实现,fallback 是缓存未命中时可用的 input schema
|
||||
// (来自 tools/list 的发现结果)。
|
||||
func (c *Client) callTool(ctx context.Context, name string, args map[string]any, fallback map[string]any) (ToolResult, error) {
|
||||
session := c.connected()
|
||||
if session == nil {
|
||||
return ToolResult{}, fmt.Errorf("mcp call tool %q: %w", name, ErrNotConnected)
|
||||
}
|
||||
if err := n.notify(ctx, "notifications/initialized", map[string]any{}); err != nil {
|
||||
c.log.Debug("mcp initialized notification failed", "name", c.cfg.Name, "err", err)
|
||||
prepared, err := c.schemas.Prepare(c.cfg.Name, name, fallback, args)
|
||||
if err != nil {
|
||||
return ToolResult{}, fmt.Errorf("mcp call tool %q: %w", name, err)
|
||||
}
|
||||
res, err := session.CallTool(ctx, &mcpsdk.CallToolParams{Name: name, Arguments: prepared})
|
||||
if err != nil {
|
||||
return ToolResult{}, fmt.Errorf("mcp call tool %q: %w", name, err)
|
||||
}
|
||||
return ToolResult{
|
||||
Content: toContentBlocks(res.Content),
|
||||
Structured: res.StructuredContent,
|
||||
IsError: res.IsError,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DiscoverTools 列出服务器工具,把每把工具包装成 sdk.Tool 并把 input schema
|
||||
// 放进缓存,使 MCP server 看起来就是一个普通工具提供者。
|
||||
func (c *Client) DiscoverTools(ctx context.Context) ([]sdk.Tool, error) {
|
||||
tools, err := c.ListTools(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]sdk.Tool, 0, len(tools))
|
||||
for _, t := range tools {
|
||||
c.cacheSchema(t)
|
||||
out = append(out, &mcpTool{client: c, info: t})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// cacheSchema 把一次发现到的 input schema 解析并放进缓存;解析失败只降级为
|
||||
// 「调用时不校验」,不影响工具接入。
|
||||
func (c *Client) cacheSchema(t ToolInfo) {
|
||||
if err := c.schemas.Put(c.cfg.Name, t.Name, t.InputSchema); err != nil {
|
||||
c.log.Warn("mcp tool input schema not resolvable; validation skipped",
|
||||
"name", c.cfg.Name, "tool", t.Name, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleServerNotification is the transport-level sink for server→client
|
||||
// notifications. It offloads the actual work to a fresh goroutine because it runs
|
||||
// on the transport read path and must not block or re-enter the same transport
|
||||
// synchronously; refreshPending coalesces bursts so at most one refresh runs at a
|
||||
// time.
|
||||
func (c *Client) handleServerNotification(method string) {
|
||||
if method != notificationToolsListChanged || c.onToolListChanged == nil {
|
||||
// handleToolListChanged 是 SDK 的 notifications/tools/list_changed 回调:
|
||||
// 先让 schema 缓存失效(design/09 §1.1),再合并触发宿主的增量刷新。
|
||||
// 它可能运行在 SDK 的读路径上,因此真正的刷新放到新 goroutine 里。
|
||||
func (c *Client) handleToolListChanged(context.Context, *mcpsdk.ToolListChangedRequest) {
|
||||
c.schemas.Invalidate(c.cfg.Name)
|
||||
if c.onToolListChanged == nil {
|
||||
return
|
||||
}
|
||||
if !c.refreshPending.CompareAndSwap(false, true) {
|
||||
@@ -217,163 +162,41 @@ func (c *Client) handleServerNotification(method string) {
|
||||
}()
|
||||
}
|
||||
|
||||
// ping sends a JSON-RPC ping request and reports whether the server responded.
|
||||
// Pings are used by the keepalive loop to detect a dead connection.
|
||||
func (c *Client) ping(ctx context.Context) error {
|
||||
tr := c.transport()
|
||||
if tr == nil {
|
||||
return fmt.Errorf("mcp ping: %w", ErrNotConnected)
|
||||
// newSDKClient 按配置构建官方 SDK 客户端:只宣告宿主真正具备的客户端能力。
|
||||
func (c *Client) newSDKClient() *mcpsdk.Client {
|
||||
client := mcpsdk.NewClient(&mcpsdk.Implementation{Name: clientName, Version: clientVersion}, &mcpsdk.ClientOptions{
|
||||
Logger: c.log,
|
||||
Capabilities: c.clientCapabilities(),
|
||||
CreateMessageHandler: c.wrapSampling(),
|
||||
ElicitationHandler: c.wrapElicitation(),
|
||||
ToolListChangedHandler: c.handleToolListChanged,
|
||||
KeepAlive: c.keepAlive,
|
||||
KeepAliveFailureThreshold: c.keepAliveFailures,
|
||||
})
|
||||
if len(c.roots) > 0 {
|
||||
client.AddRoots(c.roots...)
|
||||
}
|
||||
if err := tr.Call(ctx, "ping", map[string]any{}, nil); err != nil {
|
||||
return fmt.Errorf("mcp ping: %w", err)
|
||||
}
|
||||
return nil
|
||||
return client
|
||||
}
|
||||
|
||||
// startKeepalive launches the ping keepalive loop when configured. It closes the
|
||||
// client once keepAliveFailureThreshold consecutive pings fail, so the host can
|
||||
// rebuild the session. The loop exits when the client is closed.
|
||||
func (c *Client) startKeepalive() {
|
||||
interval := c.keepAlive
|
||||
if interval <= 0 {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c.mu.Lock()
|
||||
c.keepaliveCancel = cancel
|
||||
c.mu.Unlock()
|
||||
threshold := c.keepAliveFailures
|
||||
if threshold < 1 {
|
||||
threshold = keepAliveFailureThreshold
|
||||
}
|
||||
go func() {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
failures := 0
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
pingCtx, pingCancel := context.WithTimeout(context.Background(), interval/2)
|
||||
err := c.ping(pingCtx)
|
||||
pingCancel()
|
||||
if err != nil {
|
||||
// A peer that does not implement ping is still healthy; stop
|
||||
// keepalive rather than treating every ping as a failure.
|
||||
if isMethodNotFound(err) {
|
||||
c.log.Debug("mcp keepalive stopped; server does not support ping", "name", c.cfg.Name)
|
||||
return
|
||||
}
|
||||
failures++
|
||||
c.log.Warn("mcp keepalive ping failed", "name", c.cfg.Name, "err", err, "failures", failures)
|
||||
if failures < threshold {
|
||||
continue
|
||||
}
|
||||
c.log.Error("mcp keepalive ping failed; closing session", "name", c.cfg.Name, "failures", failures)
|
||||
_ = c.Close()
|
||||
return
|
||||
}
|
||||
failures = 0
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ListTools returns the tools exposed by the connected MCP server.
|
||||
func (c *Client) ListTools(ctx context.Context) ([]ToolInfo, error) {
|
||||
tr := c.transport()
|
||||
if tr == nil {
|
||||
return nil, fmt.Errorf("mcp list tools %q: %w", c.cfg.Name, ErrNotConnected)
|
||||
}
|
||||
var res listToolsResult
|
||||
if err := tr.Call(ctx, "tools/list", map[string]any{}, &res); err != nil {
|
||||
return nil, fmt.Errorf("mcp list tools %q: %w", c.cfg.Name, err)
|
||||
}
|
||||
out := make([]ToolInfo, 0, len(res.Tools))
|
||||
for _, t := range res.Tools {
|
||||
out = append(out, ToolInfo{Name: t.Name, Description: t.Description, InputSchema: t.InputSchema})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CallTool invokes a tool on the connected MCP server.
|
||||
func (c *Client) CallTool(ctx context.Context, name string, args any) (ToolResult, error) {
|
||||
tr := c.transport()
|
||||
if tr == nil {
|
||||
return ToolResult{}, fmt.Errorf("mcp call tool %q: %w", name, ErrNotConnected)
|
||||
}
|
||||
arguments, err := normalizeArgs(args)
|
||||
if err != nil {
|
||||
return ToolResult{}, fmt.Errorf("mcp call tool %q: %w", name, err)
|
||||
}
|
||||
params := callToolParams{Name: name, Arguments: arguments}
|
||||
var res callToolResult
|
||||
if err := tr.Call(ctx, "tools/call", params, &res); err != nil {
|
||||
return ToolResult{}, fmt.Errorf("mcp call tool %q: %w", name, err)
|
||||
}
|
||||
return ToolResult{Content: toContentBlocks(res.Content), IsError: res.IsError}, nil
|
||||
}
|
||||
|
||||
// DiscoverTools lists the server tools and maps each to an sdk.Tool so the MCP
|
||||
// server appears as a normal tool provider.
|
||||
func (c *Client) DiscoverTools(ctx context.Context) ([]sdk.Tool, error) {
|
||||
tools, err := c.ListTools(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]sdk.Tool, 0, len(tools))
|
||||
for _, t := range tools {
|
||||
out = append(out, &mcpTool{client: c, info: t})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Close closes the transport and marks the client closed. It is idempotent. It
|
||||
// also cancels the keepalive loop so no ping goroutine outlives the client.
|
||||
func (c *Client) Close() error {
|
||||
c.mu.Lock()
|
||||
if c.state == stateClosed {
|
||||
c.mu.Unlock()
|
||||
// sessionOptions 返回 SDK 会话选项;宿主固定了协议版本时才返回非 nil。
|
||||
func (c *Client) sessionOptions() *mcpsdk.ClientSessionOptions {
|
||||
if c.preferredProtocolVersion == "" {
|
||||
return nil
|
||||
}
|
||||
c.state = stateClosed
|
||||
tr := c.tr
|
||||
if c.keepaliveCancel != nil {
|
||||
c.keepaliveCancel()
|
||||
c.keepaliveCancel = nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if tr != nil {
|
||||
return tr.Close()
|
||||
}
|
||||
return nil
|
||||
return &mcpsdk.ClientSessionOptions{ProtocolVersion: c.preferredProtocolVersion}
|
||||
}
|
||||
|
||||
// newTransport selects the transport matching cfg. An explicit TransportType
|
||||
// ("stdio", "sse" or "http") wins; otherwise the transport is inferred from the
|
||||
// fields, preserving the legacy behaviour of stdio-when-Command and SSE-when-URL.
|
||||
func newTransport(cfg MCPConfig, log *slog.Logger) (Transport, error) {
|
||||
switch cfg.TransportType {
|
||||
case "http", "streamable":
|
||||
return newHTTPTransport(cfg, log), nil
|
||||
case "sse":
|
||||
return newSSETransport(cfg, log), nil
|
||||
case "stdio":
|
||||
return newStdioTransport(cfg, log), nil
|
||||
}
|
||||
switch {
|
||||
case cfg.Command != "":
|
||||
return newStdioTransport(cfg, log), nil
|
||||
case cfg.URL != "":
|
||||
return newSSETransport(cfg, log), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("mcp: %w", ErrNoTransport)
|
||||
// wrapConnectErr 把 SDK 报的「协议版本不支持」映射到本包哨兵错误,便于调用方判定。
|
||||
func wrapConnectErr(err error) error {
|
||||
var wire *sdkjsonrpc.Error
|
||||
if errors.As(err, &wire) && wire.Code == mcpsdk.CodeUnsupportedProtocolVersion {
|
||||
return fmt.Errorf("%w: %w", ErrUnsupportedProtocolVersion, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// normalizeArgs converts an agent-supplied argument value into the MCP arguments
|
||||
// object, requiring a JSON object.
|
||||
// normalizeArgs 把 agent 传来的参数值规整成 MCP arguments 对象(必须是 JSON 对象)。
|
||||
func normalizeArgs(args any) (map[string]any, error) {
|
||||
if args == nil {
|
||||
return map[string]any{}, nil
|
||||
@@ -383,33 +206,3 @@ func normalizeArgs(args any) (map[string]any, error) {
|
||||
}
|
||||
return nil, fmt.Errorf("arguments must be a JSON object, got %T", args)
|
||||
}
|
||||
|
||||
// listToolsResult is the MCP tools/list response body.
|
||||
type listToolsResult struct {
|
||||
Tools []toolSpec `json:"tools"`
|
||||
}
|
||||
|
||||
// toolSpec is a single tool in a tools/list response.
|
||||
type toolSpec struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
InputSchema map[string]any `json:"inputSchema"`
|
||||
}
|
||||
|
||||
// callToolParams is the MCP tools/call request body.
|
||||
type callToolParams struct {
|
||||
Name string `json:"name"`
|
||||
Arguments any `json:"arguments,omitempty"`
|
||||
}
|
||||
|
||||
// callToolResult is the MCP tools/call response body.
|
||||
type callToolResult struct {
|
||||
Content []contentBlock `json:"content"`
|
||||
IsError bool `json:"isError"`
|
||||
}
|
||||
|
||||
// contentBlock is a wire content block in a tools/call response.
|
||||
type contentBlock struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
Reference in new issue
Block a user