mcp

package
v1.3.0-beta.1 Latest Latest
Warning

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

Go to latest
Published: May 29, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package mcp implements evva's Model Context Protocol client. It loads MCP server configurations from settings.json, connects to each configured server via the official modelcontextprotocol/go-sdk, and surfaces discovered tools and resources so the agent can register them dynamically into the deferred-tool channel.

The package is Experimental — public types may change in a minor version (see docs/sdk-stability.md). Stabilization candidate for v1.7 or later once downstream consumers have exercised the surface.

Architectural seam:

  • The host calls mcp.Load(workdir, evvaHome) once at boot to read the mcpServers block, then mcp.Open(ctx, cfg, opts) to build a *Manager. The Manager opens connections concurrently and is safe to call before any agent exists.
  • The host passes the *Manager into agent.New via WithMcpManager; internal/agent installs it on the per-agent ToolState and registers a dynamic factory per discovered tool on pubtoolset.DefaultRegistry.
  • When the model invokes mcp__server__tool, agent.ResolveTool builds the tool through the dynamic factory, which captures the Manager's session for that server.

Subagents inherit the parent's *Manager — no re-connection, no session duplication. The Manager is the single source of truth for every MCP interaction in the agent tree.

Transports supported: stdio (subprocess) and Streamable HTTP (2025-03-26 spec). SSE-only, WebSocket, SDK, claudeai-proxy, SSE-IDE, and WS-IDE transports are deliberately out of scope — see docs/roadmap/v1/v1-3-mcp.md §6.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BuildToolName

func BuildToolName(server, tool string) string

BuildToolName returns "mcp__<server>__<tool>" with both names normalized. Inverse of ParseToolName.

func ConvertResult

func ConvertResult(r *mcpsdk.CallToolResult, server, tool, evvaHome string) (tools.Result, error)

ConvertResult turns an SDK CallToolResult into the agent's tools.Result. Text blocks concatenate into Content; image blocks become ContentBlocks; embedded binary resource blobs are persisted to disk under <evvaHome>/mcp-blobs/<random> and replaced with a "[binary saved at <path>]" line.

evvaHome is the resolved cfg.AppHome — passed in so this stays a pure conversion helper (no env reads, no global state). When evvaHome is empty, blob persistence is disabled and the conversion emits a "[binary received but no AppHome configured]" note instead. That is the only path that touches the filesystem.

func ExpandEnv

func ExpandEnv(s string) (expanded string, missing []string)

ExpandEnv expands ${VAR} and ${VAR:-default} references in s using the process environment. Returns the expanded string and a slice of any referenced variables that were unset and had no default. Direct port of ref/src/services/mcp/envExpansion.ts:expandEnvVarsInString.

func Load

func Load(workdir, evvaHome string) (*Config, []Warning)

Load reads .evva/settings.json (project) and <evvaHome>/settings.json (user), merges the mcpServers blocks (project wins on name collision), expands env vars, and returns the normalized config + non-fatal warnings. Missing files are not errors. Malformed entries become Warnings; the rest of the file still loads.

func NewListResourcesTool

func NewListResourcesTool(m *Manager) tools.Tool

NewListResourcesTool builds the deferred list_mcp_resources tool against the supplied manager. A nil manager yields a tool that reports "no MCP manager configured" — safe for hosts with no MCP servers.

func NewReadResourceTool

func NewReadResourceTool(m *Manager) tools.Tool

NewReadResourceTool builds the deferred read_mcp_resource tool against the supplied manager.

func NormalizeName

func NormalizeName(name string) string

NormalizeName maps an arbitrary server or tool name into the API-safe pattern ^[a-zA-Z0-9_-]{1,64}$. Replaces every invalid character with an underscore. Direct port of ref/src/services/mcp/normalization.ts: normalizeNameForMCP, minus the claude.ai-specific underscore-collapse branch (we don't ship claude.ai-proxy servers).

func Open

func Open(ctx context.Context, cfg *Config, opts OpenOptions) (*Manager, []Warning)

Open is the one-call constructor: build a Manager, connect every non-disabled server in parallel, fetch each connected server's tool catalog, and return the result. Connection failures do not abort — they surface as Warnings and a per-server failed/needs-auth status.

func ToolNamePrefix

func ToolNamePrefix(server string) string

ToolNamePrefix returns "mcp__<server>__" for the normalized server.

Types

type Client

type Client struct {
	Name   string
	Config ServerConfig
	// contains filtered or unexported fields
}

Client wraps one SDK ClientSession with the lifecycle policy this phase needs: lazy re-connect on session-expired, lock-protected swap of the underlying session, and a small set of convenience methods the dynamic tool factories call.

func (*Client) CallTool

func (c *Client) CallTool(ctx context.Context, name string, args map[string]any) (*mcpsdk.CallToolResult, error)

CallTool invokes a tool, retrying once on session-expired.

func (*Client) ListTools

func (c *Client) ListTools(ctx context.Context) ([]*mcpsdk.Tool, error)

ListTools fetches the server's tool catalog. Returns nil for disconnected / failed / auth-needed states (no error — these are expected states the manager already tracks).

func (*Client) Status

func (c *Client) Status() ServerStatus

Status returns the live connection status under the read lock.

type Config

type Config struct {
	Servers []ServerConfig
}

Config is the merged + normalized server list ready for Open.

type ConfigScope

type ConfigScope string

ConfigScope identifies where a server config was loaded from. Mirrors hooks/skills sourcing — workdir overrides user.

const (
	ScopeUser    ConfigScope = "user"    // <APP_HOME>/settings.json
	ScopeProject ConfigScope = "project" // <workdir>/.evva/settings.json
)

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager holds every Client and is the seam internal/agent threads through ToolState. Safe for concurrent use.

func NewManager

func NewManager(opts OpenOptions) *Manager

NewManager returns an empty Manager configured from opts.

func (*Manager) Client

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

Client returns the named client or nil.

func (*Manager) DiscoveredToolNames

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

DiscoveredToolNames returns every mcp__<server>__<tool> name across all connected servers, plus one mcp__<server>__authenticate name per needs-auth server, sorted. Called by internal/agent at profile-build time to extend the deferred allowlist.

func (*Manager) RegisterFactories

func (m *Manager) RegisterFactories(reg *pubtoolset.Registry)

RegisterFactories registers a pubtoolset.ToolFactory for every tool discovered across every connected server, keyed by the qualified mcp__<server>__<tool> name, plus one authenticate factory per needs-auth server. Idempotent: duplicate registrations are absorbed (same instance, same factory).

func (*Manager) SetOnToolsChanged

func (m *Manager) SetOnToolsChanged(fn func())

SetOnToolsChanged installs a callback the Manager fires after the authenticate flow discovers a server's real tools, so the host can refresh its deferred allowlist. nil-safe.

func (*Manager) Shutdown

func (m *Manager) Shutdown()

Shutdown closes every active session. Idempotent; bound to the agent's RootContext cancel.

func (*Manager) Status

func (m *Manager) Status() []ServerState

Status returns a snapshot of every server's runtime state, sorted by name for stable output.

type OAuthHandler

type OAuthHandler struct {
	// contains filtered or unexported fields
}

OAuthHandler wraps the SDK's auth.AuthorizationCodeHandler with one piece of evva-specific glue: the URL the SDK derives from the server's OAuth metadata is routed through promptFn so a human (or any other prompt sink the host chose) can confirm completion. A local HTTP listener captures the authorization-code redirect out of band, as the SDK's AuthorizationCodeFetcher contract requires.

func NewOAuthHandler

func NewOAuthHandler(server string, logger *slog.Logger, promptFn OAuthPromptFn) *OAuthHandler

NewOAuthHandler builds a handler for one server. promptFn may be nil — fetchCode then returns a clear "no prompt callback installed" error.

func (*OAuthHandler) SDKHandler

func (h *OAuthHandler) SDKHandler() (auth.OAuthHandler, error)

SDKHandler returns the SDK-shaped handler set on StreamableClientTransport.OAuthHandler. It stands up a local callback listener on 127.0.0.1:<random-port> so the authorization-code redirect can be captured; the redirect URI is registered via Dynamic Client Registration. The listener is closed when fetchCode returns (or after a backstop timeout if the flow never reaches the fetcher).

type OAuthPrompt

type OAuthPrompt struct {
	Server  string // MCP server name from settings.json
	AuthURL string // URL the user must open in their browser
}

OAuthPrompt is the data an MCP-driven OAuth flow needs to surface to the end user. The host receives this struct via OAuthPromptFn, shows it to the user however it sees fit (TUI dialog, ask_user_question, custom UI, headless allow-list), and returns whether the user completed the in-browser flow or cancelled.

pkg/mcp deliberately does NOT import internal/question. Callers adapt the OAuthPromptFn signature into whatever question/broker shape their host uses; the bundled cmd/evva does this in internal/agent/mcp_wiring.go.

type OAuthPromptFn

type OAuthPromptFn func(ctx context.Context, prompt OAuthPrompt) (OAuthPromptResult, error)

OAuthPromptFn is the seam the host installs to surface auth URLs to the user. Returning OAuthCancelled aborts the auth. Returning a non-nil error is treated as a transport failure — the connect fails and the server's status moves to failed/needs-auth.

type OAuthPromptResult

type OAuthPromptResult int

OAuthPromptResult is the user's decision after seeing an OAuthPrompt.

const (
	// OAuthCompleted means the user reports the browser flow finished; the
	// local callback listener has captured the code+state.
	OAuthCompleted OAuthPromptResult = iota
	// OAuthCancelled aborts the connect — the server stays needs-auth until
	// the user retries via the per-server authenticate tool.
	OAuthCancelled
)

type OpenOptions

type OpenOptions struct {
	// Logger receives mcp.* slog entries. nil yields a discard logger.
	Logger *slog.Logger

	// EvvaHome is the resolved per-user home dir used for binary-blob
	// persistence under <EvvaHome>/mcp-blobs. Empty disables blob writes.
	EvvaHome string

	// OAuthPrompt is called when an HTTP MCP server requires OAuth. nil
	// disables the interactive flow — needs-auth servers surface a clear
	// error if their authenticate tool is invoked.
	OAuthPrompt OAuthPromptFn
}

OpenOptions carries the host-supplied dependencies the Manager needs at construction time. Every field is optional with a defined nil/zero behavior so Open's signature stays stable as the option set grows.

type ServerConfig

type ServerConfig struct {
	Name     string // map key from settings.json
	Type     TransportType
	Disabled bool // "disabled": true skips connect

	// Stdio fields
	Command string // required when Type == TransportStdio
	Args    []string
	Env     map[string]string // ${VAR} / ${VAR:-default} expansion happens at Load

	// HTTP fields
	URL     string // required when Type == TransportStreamableHTTP
	Headers map[string]string

	// Common
	Timeout time.Duration // connect timeout; default 30s; max 600s
	Scope   ConfigScope   // Project | User — for telemetry/logging only
}

ServerConfig is the parsed shape of one entry under mcpServers. Mirrors ref/src/services/mcp/types.ts McpStdioServerConfigSchema + McpHTTPServerConfigSchema, simplified to the two transports v1.6 ships.

type ServerState

type ServerState struct {
	Name          string
	Config        ServerConfig
	Status        ServerStatus
	Error         string    // populated for StatusFailed / StatusNeedsAuth
	ToolCount     int       // number of tools discovered (0 unless Connected)
	ResourceCount int       // 0 unless server advertises resources/list capability
	ConnectedAt   time.Time // zero unless Connected
}

ServerState is what Manager.Status() returns per server.

type ServerStatus

type ServerStatus string

ServerStatus is the live runtime state of one server's connection.

const (
	StatusConnected ServerStatus = "connected"
	StatusPending   ServerStatus = "pending"    // Connect in flight
	StatusFailed    ServerStatus = "failed"     // Connect returned err; tools=0
	StatusNeedsAuth ServerStatus = "needs-auth" // HTTP 401; auth tool offered
	StatusDisabled  ServerStatus = "disabled"
)

type ToolNameInfo

type ToolNameInfo struct {
	Server string
	Tool   string
}

ToolNameInfo is the parsed shape: server + tool.

func ParseToolName

func ParseToolName(name string) *ToolNameInfo

ParseToolName extracts server + tool from a mcp__<server>__<tool> string. Returns nil if name lacks the prefix or has no tool segment. Known limitation: if a server name contains "__", parsing reports the first segment as the server. Server names with double-underscore are rare in practice (ref has the same limitation).

type TransportType

type TransportType string

TransportType is the wire-level transport kind.

const (
	TransportStdio          TransportType = "stdio"
	TransportStreamableHTTP TransportType = "http"
)

type Warning

type Warning struct {
	Path string
	Err  error
}

Warning is a non-fatal load issue. Mirrors hooks.Warning shape so callers surface MCP warnings the same way they surface hook ones.

func (Warning) Error

func (w Warning) Error() string

Jump to

Keyboard shortcuts

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