Documentation
¶
Overview ¶
Package mcp implements yottacode's client for the Model Context Protocol (MCP). v1 supports the stdio subprocess transport only — HTTP/SSE is a follow-up wedge. The package is structured around a transport-agnostic Client interface so HTTPClient can land later without churning callers.
Lifecycle: Manager is built from config and started once at session start; each healthy client's tools are then enumerated and registered in the agent tool registry under the `mcp/<server>/<tool>` namespace. Failed clients are non-fatal — yottacode keeps running with whichever servers came up.
Errors: protocol failures surface as Go errors from CallTool. Tool-level errors (the server's own `isError=true` envelope) surface as a non-nil CallResult with CallResult.IsError set; callers (the agent tool bridge) translate that to the model.
Index ¶
- Constants
- Variables
- type CallResult
- type Client
- type Manager
- func (m *Manager) Add(ctx context.Context, cfg config.MCPServer) (StartResult, error)
- func (m *Manager) Client(name string) Client
- func (m *Manager) Clients() []Client
- func (m *Manager) Names() []string
- func (m *Manager) Remove(ctx context.Context, name string) error
- func (m *Manager) Restart(ctx context.Context, name string) (StartResult, error)
- func (m *Manager) Start(ctx context.Context) []StartResult
- func (m *Manager) Status(name string) StartResult
- func (m *Manager) Statuses() []StartResult
- func (m *Manager) Stop(ctx context.Context)
- type StartResult
- type StdioClient
- func (c *StdioClient) CallTool(ctx context.Context, toolName, argsJSON string) (CallResult, error)
- func (c *StdioClient) ListTools(ctx context.Context) ([]ToolDescriptor, error)
- func (c *StdioClient) Name() string
- func (c *StdioClient) Start(ctx context.Context) error
- func (c *StdioClient) StderrTail() []string
- func (c *StdioClient) Stop(ctx context.Context) error
- func (c *StdioClient) Warnings() []string
- type ToolDescriptor
Constants ¶
const InitializeTimeout = 30 * time.Second
InitializeTimeout is the wall-clock budget for the MCP initialize handshake. Slow servers don't block other servers (Manager.Start runs them concurrently); a per-server timeout keeps the picture bounded.
const TerminateGracePeriod = 3 * time.Second
TerminateGracePeriod is how long the SDK's CommandTransport waits after closing stdin before escalating to SIGTERM/SIGKILL. Match the roadmap's 3s grace; the SDK then handles the signal escalation.
Variables ¶
var ErrNotStarted = fmt.Errorf("mcp: client not started")
ErrNotStarted is returned by Client methods other than Start that are invoked before Start succeeds. Surfaced verbatim for tests; production callers normally hit this only on a transport that failed Start silently in a previous call.
Functions ¶
This section is empty.
Types ¶
type CallResult ¶
type CallResult struct {
// Text is the concatenated text content from the server, joined
// with newlines. Empty when the server returned only non-text
// content (image/audio/resource).
Text string
// IsError mirrors the server's CallToolResult.isError flag. The
// bridge translates IsError=true into a tool_result block with
// is_error=true so the model self-corrects.
IsError bool
}
CallResult is the agent-bridge-friendly view of CallToolResult. Implementations concatenate the server's text content blocks into Text — image/audio/resource content blocks are ignored in v1 (tools-only scope; multi-modal MCP content lands when the agent loop itself learns to forward multi-modal tool results to the model).
type Client ¶
type Client interface {
// Name returns the user-configured identifier for this server
// (matches MCPServer.Name in config). Stable for the client's
// lifetime; used both for tool-namespace prefixes
// (`mcp/<Name>/<tool>`) and for /mcp display.
Name() string
// Start launches the underlying transport (spawns the subprocess
// for stdio; opens the HTTPS connection for http) and performs
// the MCP `initialize` handshake. Returns the first error from
// either step. After Start returns nil, [ListTools] and
// [CallTool] are safe to invoke.
Start(ctx context.Context) error
// ListTools returns the catalog the server advertised at
// initialize time. Safe to call repeatedly — implementations
// cache; the call is cheap.
ListTools(ctx context.Context) ([]ToolDescriptor, error)
// CallTool invokes one of the server's tools with the given
// JSON-encoded arguments. argsJSON is the raw payload the
// agent loop received from the model — implementations parse it
// internally.
//
// Returns (CallResult, nil) for both successful calls and
// server-reported tool errors (where CallResult.IsError is true).
// Returns (zero, err) only for transport-level failures
// (subprocess died, ctx cancelled, JSON-RPC framing error).
CallTool(ctx context.Context, toolName string, argsJSON string) (CallResult, error)
// Stop releases the underlying transport. For stdio: closes
// stdin, waits up to a few seconds, then sends SIGTERM/SIGKILL
// (handled by the SDK's CommandTransport.TerminateDuration).
// Idempotent.
Stop(ctx context.Context) error
}
Client is the transport-agnostic surface every MCP transport implementation exposes to the rest of yottacode. The bridge in internal/agent/mcp_tool.go takes a Client (not a *StdioClient) so the HTTPClient that lands in v1.1 plugs in without further work.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager owns the lifecycle of every configured MCP client across a yottacode session. Built once from config, started concurrently at session init, restarted per-server via /mcp restart, stopped at shutdown. The agent loop never touches Manager — it sees the registered tools and goes through the same dispatch path as native tools.
The Manager keeps the original config entries indexed by name so Restart can rebuild a fresh client without the caller re-loading config.toml.
func NewManager ¶
NewManager constructs a Manager from the parsed config block. Each non-disabled MCPServer becomes one StdioClient. Disabled entries are dropped — they don't appear in /mcp at all in v1; the file is the only place that knows about them.
func (*Manager) Add ¶
Add registers a new MCP server at runtime and starts it. Used by /mcp add to hot-start the server without requiring a TUI restart. Returns the StartResult so the caller can register tools. Returns an error if a server with the same name already exists.
func (*Manager) Client ¶
Client looks up a client by configured name. Returns nil if no such server is configured.
func (*Manager) Clients ¶
Clients returns the (read-only) set of clients managed here in registration order. Healthy or not — callers that only want successful ones should filter on Status(name).Err == nil.
func (*Manager) Names ¶
Names returns the registered server names in registration order. Useful for callers (e.g. /mcp restart completion) that need to enumerate without retrieving every Client.
func (*Manager) Remove ¶
Remove stops the named client and removes it from the manager's internal state. After Remove, the name no longer appears in Names(), Statuses(), or Client(). Callers must Deregister the server's tools from the agent registry separately. Returns an error if the name is unknown.
func (*Manager) Restart ¶
Restart stops the named client, rebuilds a fresh StdioClient from the stored config, and runs the same Start + ListTools dance as the initial session-start path. After Restart returns, callers must Deregister the prior generation's tools from the agent Registry and Register the new generation — the manager doesn't touch the agent registry directly (the tui package owns that wiring; see cmd_mcp.go).
Returns the new StartResult. The unknown-server case surfaces as an error rather than a zero StartResult so callers can distinguish "restart failed" from "no such server."
Concurrency: a per-server generation counter makes overlapping restarts of the same server safe. Each call claims the next generation under the lock; only the latest generation publishes its rebuilt client. A restart that gets superseded mid-flight stops the client it spawned (so the subprocess doesn't leak) and returns the winner's StartResult instead of clobbering it.
func (*Manager) Start ¶
func (m *Manager) Start(ctx context.Context) []StartResult
Start spawns every configured client concurrently. Each client gets its own goroutine so slow / failing servers don't block the rest. Returns one StartResult per client in registration order. Always safe to ignore the error result on individual clients — the manager retains the failure state for /mcp inspection via Statuses.
func (*Manager) Status ¶
func (m *Manager) Status(name string) StartResult
Status returns the most recent Start outcome for a client by name. Zero-value StartResult.Name is empty when the name is unknown.
func (*Manager) Statuses ¶
func (m *Manager) Statuses() []StartResult
Statuses returns every recorded StartResult, ordered by registration order. Used by /mcp to render the full picture.
type StartResult ¶
StartResult is the per-server outcome of Start / Restart. Err is nil for healthy starts; ToolCount is 0 when Err is non-nil. Warnings carries non-fatal config-time observations (e.g. an unresolved $VAR in the env block) so the caller can surface them — they don't prevent the server from starting.
type StdioClient ¶
type StdioClient struct {
// contains filtered or unexported fields
}
StdioClient launches an MCP server as a stdio subprocess, performs the initialize handshake, and proxies tools/list + tools/call. Wraps the official Go SDK's CommandTransport — we don't re-implement JSON-RPC framing.
func NewStdioClient ¶
func NewStdioClient(name, command string, args []string, env map[string]string) *StdioClient
NewStdioClient constructs a StdioClient. The subprocess is NOT spawned here — Start does that. env values may contain $VAR references that get resolved against the process environment at Start time.
func (*StdioClient) CallTool ¶
func (c *StdioClient) CallTool(ctx context.Context, toolName, argsJSON string) (CallResult, error)
CallTool invokes the named tool with the raw JSON arguments payload. argsJSON is the literal payload the model emitted; we unmarshal into map[string]any and pass through to the SDK.
func (*StdioClient) ListTools ¶
func (c *StdioClient) ListTools(ctx context.Context) ([]ToolDescriptor, error)
ListTools returns the cached catalog from Start. Returns an error if Start hasn't been called or already failed.
func (*StdioClient) Name ¶
func (c *StdioClient) Name() string
Name returns the configured server name.
func (*StdioClient) Start ¶
func (c *StdioClient) Start(ctx context.Context) error
Start resolves the command via exec.LookPath, spawns the subprocess, and performs the MCP initialize handshake. Bound by InitializeTimeout regardless of the caller-supplied ctx — slow servers don't hang the whole session.
func (*StdioClient) StderrTail ¶
func (c *StdioClient) StderrTail() []string
StderrTail returns the recent stderr lines captured from the subprocess. Exposed for the /mcp logs subcommand.
func (*StdioClient) Stop ¶
func (c *StdioClient) Stop(ctx context.Context) error
Stop closes the SDK session, which closes the subprocess stdin and triggers the graceful-shutdown ladder (close → wait → SIGTERM → kill) implemented in the SDK's pipeRWC. The subprocess-lifetime context is then cancelled as a backstop in case session.Close left a zombie. Idempotent — repeat calls return nil.
func (*StdioClient) Warnings ¶
func (c *StdioClient) Warnings() []string
Warnings returns the (immutable post-construction) list of warnings recorded for this client — typically unresolved $VAR references in the configured env block. Surfaced by the manager via StartResult.Warnings so /mcp + run.go startup notices can show them.
type ToolDescriptor ¶
type ToolDescriptor struct {
// Name is the server-side tool name (without the `mcp/<server>/`
// prefix; the bridge prepends that).
Name string
// Description is the human-readable hint shown to the model.
Description string
// InputSchema is the JSON Schema for the tool's arguments,
// passed through to the model verbatim. Always a JSON-object
// value (map[string]any) for SDK-supplied tools.
InputSchema map[string]any
// ReadOnlyHint mirrors the MCP `annotations.readOnlyHint` flag.
// True iff the server explicitly declared the tool read-only;
// any other case (missing annotations, hint absent) defaults
// to false so the approval modal fires by default.
ReadOnlyHint bool
}
ToolDescriptor is one tool the server advertised. The fields mirror the relevant subset of the MCP `Tool` schema — we deliberately don't import the SDK's Tool struct here so the rest of yottacode stays transport-agnostic.