feat(capability/mcp): MCP 客户端(stdlib JSON-RPC over stdio/SSE)+ Registry 自动注册/Unregister(registry 增 Remove)
This commit is contained in:
1 parent
08223c06a5
commit
488cf9c79c
14 files changed
+2212
No files matched your search
@@ -0,0 +1,209 @@
|
||||
// Package mcp provides a minimal Model Context Protocol (MCP) client and the
|
||||
// auto-registration glue that surfaces MCP server tools as capabilities.
|
||||
//
|
||||
// An MCP server is an external tool provider. This package speaks JSON-RPC 2.0
|
||||
// to it over stdio (a spawned subprocess) or SSE, 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).
|
||||
//
|
||||
// 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"})
|
||||
// if err := mcpReg.Register(ctx); err != nil {
|
||||
// log.Fatal(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.
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
)
|
||||
|
||||
// protocolVersion is the MCP protocol version this client advertises during the
|
||||
// initialize handshake.
|
||||
const protocolVersion = "2024-11-05"
|
||||
|
||||
// Sentinel errors returned by this package. Test with errors.Is.
|
||||
var (
|
||||
// ErrNotConnected is returned when an operation requires an established
|
||||
// connection but the client is not yet connected or is closed.
|
||||
ErrNotConnected = errors.New("mcp: not connected")
|
||||
// ErrClosed is returned when operating on a client or transport that has
|
||||
// been closed.
|
||||
ErrClosed = errors.New("mcp: closed")
|
||||
// ErrNoTransport is returned when the configuration selects no transport
|
||||
// (neither Command nor URL is set).
|
||||
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")
|
||||
)
|
||||
|
||||
// MCPConfig describes a single MCP server connection. Exactly one transport is
|
||||
// selected: stdio when Command is set, SSE when URL is set.
|
||||
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 string `env:"MCP_NAME"`
|
||||
// Command is the executable path to spawn for a stdio MCP server.
|
||||
Command string `env:"MCP_COMMAND"`
|
||||
// Args are the arguments passed to Command.
|
||||
Args []string `env:"MCP_ARGS"`
|
||||
// URL is the SSE endpoint of an MCP server.
|
||||
URL string `env:"MCP_URL"`
|
||||
// Enabled reports whether this server should be activated at startup.
|
||||
Enabled bool `env:"MCP_ENABLED"`
|
||||
}
|
||||
|
||||
// Config holds the set of configured MCP servers.
|
||||
type Config struct {
|
||||
// Servers is the ordered list of MCP server configurations.
|
||||
Servers []MCPConfig
|
||||
}
|
||||
|
||||
// ServerInfo describes the MCP server reported by the initialize handshake.
|
||||
type ServerInfo struct {
|
||||
// Name is the server-provided name.
|
||||
Name string
|
||||
// Version is the server-provided version string.
|
||||
Version string
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
type Client struct {
|
||||
cfg MCPConfig
|
||||
log *slog.Logger
|
||||
tr Transport
|
||||
mu sync.Mutex
|
||||
state state
|
||||
info ServerInfo
|
||||
}
|
||||
|
||||
// state tracks the client lifecycle.
|
||||
type state int
|
||||
|
||||
const (
|
||||
stateNew state = iota
|
||||
stateConnected
|
||||
stateClosed
|
||||
)
|
||||
|
||||
// Transport is the JSON-RPC message exchange with an MCP server over a concrete
|
||||
// transport (stdio subprocess or SSE). 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
|
||||
}
|
||||
|
||||
// Option customizes a Client built by New.
|
||||
type Option func(*Client)
|
||||
|
||||
// WithLogger overrides the client's logger. The given logger is tagged with the
|
||||
// source capability/mcp.
|
||||
func WithLogger(l *slog.Logger) Option {
|
||||
return func(c *Client) {
|
||||
if l != nil {
|
||||
c.log = logger.WithSource(l, "capability/mcp")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return func(c *Client) {
|
||||
c.tr = t
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
func New(cfg MCPConfig, opts ...Option) *Client {
|
||||
c := &Client{
|
||||
cfg: cfg,
|
||||
log: defaultLogger(),
|
||||
state: stateNew,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// defaultLogger returns a discard logger tagged with the mcp source. Hosts
|
||||
// should prefer WithLogger so records are observable.
|
||||
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 {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.state != stateConnected || c.tr == nil {
|
||||
return nil
|
||||
}
|
||||
return c.tr
|
||||
}
|
||||
Reference in new issue
Block a user