mcp

package
v0.25.1 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Index

Constants

View Source
const ToolPrefix = "mcp__"

ToolPrefix is the namespace prefix for MCP tool names ("mcp__<server>__<tool>"). Exported so other packages can detect MCP tools without hardcoding the prefix.

Variables

View Source
var ErrRestartUnsupported = errors.New("restarting a single MCP server is not supported on this platform")

ErrRestartUnsupported is returned by RestartServer on platforms that cannot reap a subprocess's whole tree (Windows). Restarting there would risk orphaning the old process tree, so it is refused; a full session restart recreates servers instead.

View Source
var ErrServerDisabled = errors.New("MCP server is disabled")

ErrServerDisabled is returned when an operation (e.g. restart) is attempted on a server that is currently disabled by policy; enable it first.

View Source
var ErrUnknownServer = errors.New("unknown MCP server")

ErrUnknownServer is returned by RestartServer for a name the Manager does not manage.

Functions

func ServerToolPrefix

func ServerToolPrefix(server string) string

ServerToolPrefix returns the sanitized name prefix shared by every registered tool of a given server, so callers can match a server's tools in a registry. It mirrors the sanitization wrapTool applies, so it stays correct even for server names containing characters that get rewritten.

Types

type Controller

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

Controller coordinates one session's MCP manager, its tool registry, and the disable policy, so neither the web nor the TUI frontend has to orchestrate process + registry + prompt by hand. It is the single place that:

  • resolves the desired enabled/disabled set from the policy;
  • reconciles the running manager to match (enable/disable/restart);
  • keeps the tool registry in sync by EXACT tool names (never by a sanitized textual prefix, which is unreliable for long server names);
  • refreshes the base system prompt after the tool set changes, so the model is never told about a tool that is no longer registered.

The manager stays the authority on subprocess/SDK state; the Controller adds the policy and registry/prompt coordination on top.

func NewController

func NewController(cfg ControllerConfig) *Controller

NewController builds a Controller over an already-started manager and the registry its tools were registered into. It captures the current server→tool name index from the manager so later reconciles can re-sync by exact name.

func (*Controller) Close

func (c *Controller) Close()

Close shuts down the manager. The registry is left as-is; the session is being torn down.

func (*Controller) Policy

func (c *Controller) Policy() core.MCPDisablePolicy

Policy returns a deep copy of the controller's current in-memory disable policy (all scopes). Callers use it to rebuild a replacement manager/controller on reload from the LIVE policy, not a stale bootstrap snapshot.

func (*Controller) Reconcile

func (c *Controller) Reconcile(ctx context.Context) []ControllerStatus

Reconcile brings the manager in line with the current policy: every server whose desired-enabled differs from its applied state is enabled or disabled, and the registry + prompt are re-synced. It is safe to call repeatedly; a server already in the desired state is left untouched (SetServerEnabled is idempotent).

Reconcile is where a caller must have already established quiescence: it mutates the live tool set, so it must not run under an in-flight model turn.

func (*Controller) Restart

func (c *Controller) Restart(ctx context.Context, name string) (ServerStatus, error)

Restart restarts one server and re-syncs its tools by exact name. It refuses a server the policy wants disabled — whether already applied (Manager returns ErrServerDisabled) or still pending a deferred reconcile — so restart never bypasses a desired disable and spawns a process that policy says should be off.

func (*Controller) SessionDisabled

func (c *Controller) SessionDisabled() []string

SessionDisabled returns the server names currently vetoed in SESSION scope.

func (*Controller) SetPolicy

SetPolicy replaces the disable policy and reconciles the manager to match.

func (*Controller) SetRefreshPrompt

func (c *Controller) SetRefreshPrompt(fn func())

SetRefreshPrompt sets the prompt-refresh hook after construction. The frontend creates the hook once its runtime exists (the runtime owns the base system prompt), which is later than when bootstrap builds the Controller. Guarded by op so it can't race an in-flight reconcile.

func (*Controller) SetScopeDisabled

func (c *Controller) SetScopeDisabled(scope core.MCPDisableScope, name string, disabled bool)

SetScopeDisabled adds or removes a server name from one scope of this Controller's in-memory policy. It mutates only the requested scope's set (vetoes in other scopes are independent). It does NOT reconcile or persist; the caller decides when to Reconcile (at quiescence) and whether to persist (project/global scopes). Session scope is process-lifetime only.

func (*Controller) SetSessionDisabled

func (c *Controller) SetSessionDisabled(names []string)

SetSessionDisabled replaces the SESSION-scope veto set wholesale (global and project scopes are untouched). It does not reconcile; the caller reconciles at quiescence. Used by single-controller frontends (the TUI) to swap the process-memory session scope when the active conversation changes.

func (*Controller) Status

func (c *Controller) Status() []ControllerStatus

Status returns the manager's server snapshots decorated with policy: whether each server is enabled by policy (desired), which scopes veto it, the applied enabled state, and any pending action (desired differs from applied).

func (*Controller) UnmatchedDisabled

func (c *Controller) UnmatchedDisabled() []UnmatchedDisabled

UnmatchedDisabled reports disabled-server preferences that don't correspond to any server configured in this session, split by the scopes that veto each. A global name absent here may still exist in another project, so these are "unmatched", not globally "orphaned".

type ControllerConfig

type ControllerConfig struct {
	Manager  *Manager
	Registry *core.Registry
	Policy   core.MCPDisablePolicy
	// RefreshPrompt rebuilds the base system prompt from the registry. Optional.
	RefreshPrompt func()
}

ControllerConfig configures a Controller.

type ControllerStatus

type ControllerStatus struct {
	ServerStatus
	Enabled        bool                   `json:"enabled"`
	DesiredEnabled bool                   `json:"desired_enabled"`
	DisabledScopes []core.MCPDisableScope `json:"disabled_scopes,omitempty"`
	PendingAction  string                 `json:"pending_action,omitempty"`
}

ControllerStatus is a manager ServerStatus plus the policy view: what the configuration wants (DesiredEnabled) and why (DisabledScopes), the applied runtime enablement (Enabled), and PendingAction when the two disagree (the server is mid-transition or awaiting quiescence).

type Manager

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

Manager owns MCP client sessions and their lifecycle.

It is safe for concurrent use: tool calls run the moment the agent schedules them, while restarts and exit-detection mutate server state from other goroutines. Callers observe changes through the OnChange callback (set once before Start) plus Status snapshots.

func NewManager

func NewManager(logger *slog.Logger, cwd string) *Manager

NewManager creates a Manager whose servers run in cwd. Pass nil for the default logger; an empty cwd inherits the process working directory.

func (*Manager) Close

func (m *Manager) Close()

Close gracefully shuts down all server sessions and kills their process trees.

func (*Manager) OnChange

func (m *Manager) OnChange(fn func(ServerStatus))

OnChange registers a callback fired whenever a server's state changes (connect, exit, restart). It must be set before Start and is not called concurrently for the same server. A nil callback disables notifications.

func (*Manager) RestartServer

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

RestartServer tears down one server's process tree and starts it again, re-discovering its tools. Other servers are untouched. The returned status is the post-restart snapshot; a failed restart leaves the server in StateFailed (still restartable). Tool names may differ across generations, so the caller should re-sync its tool registry from Tools() afterwards.

func (*Manager) SetServerEnabled

func (m *Manager) SetServerEnabled(ctx context.Context, name string, enabled bool) (ServerStatus, error)

SetServerEnabled applies an enable/disable transition to one server, honoring the same per-server lifecycle serialization as RestartServer.

Disabling tears the process tree down and leaves the server in StateDisabled (no process, no tools, no error). Enabling dials it exactly like a start; success yields StateReady, failure StateFailed (the preference stays enabled, so the failure is visible and retryable rather than silently reverted).

It is idempotent: enabling an already-running server or disabling an already-disabled one is a no-op that returns the current status. Tool sets change across this transition, so the caller must re-sync its tool registry from Tools() afterwards.

func (*Manager) Start

func (m *Manager) Start(ctx context.Context, servers map[string]core.MCPServer, initiallyDisabled map[string]bool)

Start spawns all configured MCP servers in parallel, performs the handshake, and discovers tools. Servers that fail to start are recorded as failed and skipped (non-fatal) — they still appear in Status so the UI can show the error and offer a restart.

initiallyDisabled names servers that are configured but must not be spawned: they get a StateDisabled placeholder (visible in Status, no process, no tools) so the UI can offer to enable them. A nil map starts everything.

func (*Manager) Status

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

Status returns an immutable snapshot of every server's health, in config discovery order, for the UI.

func (*Manager) Tools

func (m *Manager) Tools() []core.Tool

Tools returns all discovered MCP tools wrapped as core.Tool. The wrapped closures route calls through the Manager by server name, so they keep working across a restart (which swaps the underlying session) and return a clear error when the server is down.

func (*Manager) ToolsForServer

func (m *Manager) ToolsForServer(name string) ([]core.Tool, bool)

ToolsForServer returns the wrapped tools of one server, and whether that server is managed. Callers use it to re-sync a registry after a per-server transition (restart/enable/disable) by exact tool name, rather than matching a sanitized textual prefix — which is unreliable for long server names, where the truncating hash of the prefix differs from the hash of a full tool name.

type ServerState

type ServerState string

ServerState is the lifecycle state of a single MCP server, surfaced to the UI.

const (
	// StateReady means the server is connected and its tools are callable.
	StateReady ServerState = "ready"
	// StateFailed means the server never connected (or a restart failed).
	StateFailed ServerState = "failed"
	// StateExited means the server was connected but its process has since died.
	StateExited ServerState = "exited"
	// StateRestarting means a restart is in progress.
	StateRestarting ServerState = "restarting"
	// StateStarting means an enable is in progress (initial connect after being
	// disabled). Distinct from restarting so the UI doesn't imply a prior run.
	StateStarting ServerState = "starting"
	// StateDisabling means a disable is in progress: the process tree is being
	// torn down. It exists so we never report a server as fully disabled while
	// its resources are still being released.
	StateDisabling ServerState = "disabling"
	// StateDisabled means the server is configured but intentionally not
	// running (no process, no tools). It is an expected state, not a failure.
	StateDisabled ServerState = "disabled"
)

type ServerStatus

type ServerStatus struct {
	Name      string      `json:"name"`
	State     ServerState `json:"state"`
	ToolCount int         `json:"tool_count"`
	ToolNames []string    `json:"tool_names,omitempty"`
	Error     string      `json:"error,omitempty"`
	// StartedAt is when the server last connected; zero if it never has.
	StartedAt time.Time `json:"started_at,omitempty"`
	// ChangedAt is when the state last changed.
	ChangedAt time.Time `json:"changed_at,omitempty"`
}

ServerStatus is an immutable snapshot of one server's health, for the UI.

type UnmatchedDisabled

type UnmatchedDisabled struct {
	Name   string                 `json:"name"`
	Scopes []core.MCPDisableScope `json:"scopes,omitempty"`
}

UnmatchedDisabled is a disabled-server preference with no matching configured server in the session, plus the scopes that veto it.

Jump to

Keyboard shortcuts

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