mcp

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 10, 2026 License: MIT Imports: 13 Imported by: 0

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

View Source
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.

View Source
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

View Source
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

func NewManager(servers []config.MCPServer) *Manager

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

func (m *Manager) Add(ctx context.Context, cfg config.MCPServer) (StartResult, error)

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

func (m *Manager) Client(name string) Client

Client looks up a client by configured name. Returns nil if no such server is configured.

func (*Manager) Clients

func (m *Manager) Clients() []Client

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

func (m *Manager) Names() []string

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

func (m *Manager) Remove(ctx context.Context, name string) error

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

func (m *Manager) Restart(ctx context.Context, name string) (StartResult, error)

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.

func (*Manager) Stop

func (m *Manager) Stop(ctx context.Context)

Stop shuts every client down concurrently. Each Stop call is bounded by the SDK's TerminateGracePeriod (closing stdin, then SIGTERM, then SIGKILL); the caller's ctx is forwarded so a cancelled shutdown surfaces quickly.

type StartResult

type StartResult struct {
	Name      string
	Err       error
	ToolCount int
	Warnings  []string
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL