mcpconsole

package
v1.28.5 Latest Latest
Warning

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

Go to latest
Published: Aug 15, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package mcpconsole wires the MCP-Connections Protocol surface to its runtime-side dependencies — the MCP driver registry and the tool-side OAuth provider.

Why a separate package

The `internal/protocol` package owns the MCPSurface dispatcher and the MCPAccessor / MCPOAuthAccessor interfaces, but it MUST NOT import the `mcp` driver or the `tools/auth` package (CLAUDE.md §13 — the Protocol package stays driver-free; a Protocol type that re-exported a driver type would be the reject-on-sight smell). The adapters that bridge the two live here, in a wiring package both `cmd/harbor` and the integration test import. The MCPSurface depends ONLY on the interfaces; this package is the single concrete that satisfies them.

Concurrent reuse

RegistryAccessor and OAuthAccessor are thin, immutable adapters — the wrapped Registry / Provider are themselves safe for concurrent reuse compiled artifacts, and the adapters add no mutable state.

Index

Constants

This section is empty.

Variables

View Source
var ErrAppToolExposureDenied = fmt.Errorf(
	"%w: mcpconsole: tool unavailable — paused or disabled by agent configuration",
	protocol.ErrAccessorScopeDenied)

ErrAppToolExposureDenied — an App app→host tool call targeted a tool whose MCP server is currently paused, or a tool that is currently disabled, in the agent's active config. It is an authorization rejection (mapped to CodeScopeMismatch at the wire edge), the functional basis for the operator-legible "paused by a system administrator" overlay.

It wraps protocol.ErrAccessorScopeDenied so the wire edge classifies it by errors.Is. The edge previously matched this message's TEXT, which a southbound server's error could contribute to; see the Protocol sentinel's godoc for why a refusal must not be mintable from foreign wording.

View Source
var ErrAppsMisconfigured = errors.New("mcpconsole: AppsAccessor missing a mandatory dependency")

ErrAppsMisconfigured — NewAppsAccessor was called with a missing mandatory dependency. Fails closed.

View Source
var ErrNoOAuthConfigured = errors.New("mcpconsole: no OAuth provider configured (set tools.oauth_providers in your config to enable OAuth binding flows)")

ErrNoOAuthConfigured — the Runtime has no OAuth providers configured, so OAuth flow methods cannot be served. Surfaces as a Protocol CodeRuntimeError with an actionable message naming the missing config key.

View Source
var ErrToolContextMisconfigured = errors.New("mcpconsole: ToolContextStore missing a mandatory dependency")

ErrToolContextMisconfigured — NewToolContextStore was called with a missing mandatory dependency. Fails closed.

Functions

This section is empty.

Types

type AppsAccessor added in v1.4.0

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

AppsAccessor is the runtime-side adapter that satisfies BOTH the protocol.MCPResourceReader and protocol.AppToolInvoker seams the AppsSurface reads through — the MCP Apps host's two methods.

It bridges the driver-free `protocol` package to the runtime concretes it must not import directly (CLAUDE.md §13): the MCP driver registry (for `ui://` resource reads) and the tool catalog (for the app-tool- call proxy's re-entry into the approval / OAuth / identity invocation path). The heavy-content safety net (RFC §6.5) lives here too: content at or above the configured threshold is routed through the ArtifactStore by reference, with a loud `mcp.resource_offloaded` bypass event — never inlined past the threshold, never silently truncated.

A `ui://` MCP App document is the one carve-out: it is a Console-render payload, not LLM context, so it rides inline up to the dedicated, much larger App-document cap instead of the ordinary inline-payload bound — see appDocumentInlineCap. Above that cap it still offloads (the same loud bypass), so a pathologically large App is never inlined unbounded.

Concurrent reuse: AppsAccessor is an immutable adapter — the wrapped registry / catalog / store / bus are themselves concurrent-safe compiled artifacts, and the adapter adds no mutable state. Per-call identity rides ctx.

func NewAppsAccessor added in v1.4.0

func NewAppsAccessor(deps AppsDeps) (*AppsAccessor, error)

NewAppsAccessor builds the MCP Apps host adapter. Registry, Catalog, Store, and Bus are mandatory; a nil fails loud.

func (*AppsAccessor) CallTool added in v1.4.0

func (a *AppsAccessor) CallTool(ctx context.Context, serverID, tool string, args json.RawMessage) (protocol.MCPAppToolResultRow, error)

CallTool implements protocol.AppToolInvoker. It resolves the named tool and invokes the SAME wrapped descriptor the planner resolves — so the approval gate (the unified pause primitive) + tool-side OAuth fire exactly as on a planner call. The result is heavy-content-disciplined (the context-window safety net); a `ui://` MCP App the invoked tool declared (on its tool-definition `_meta.ui`, reconciled by the driver onto the result's app reference) is projected onto the App field.

Two deliberately different resolution views

The tool is resolved against the ordinary planner/model catalog FIRST (the pre-existing path — ordinary tools, including ones that declare a `ui://` app for a rendered App to re-invoke). When the name does NOT resolve there, the only remaining authority is the serverID-named server's App dispatch catalog — the app-only callbacks the provider declared with `_meta.ui.visibility: ["app"]` and the attach path kept OUT of the ordinary catalog. That catalog is keyed by the host-derived server identity, not by any string prefix or remembered global name:

  • an app-only callback is NOT resolvable without its own serverID (missing server identity → typed not-found before invocation);
  • a serverID that does not hold the name answers not-found, so one server's rendered App can never invoke another server's callback;
  • an ordinary tool called with a serverID that disagrees with its actual Source is refused as a scope mismatch BEFORE invocation (the App claims an identity it does not hold). An ordinary tool called with NO serverID keeps the pre-catalog behavior (legacy clients).

Whatever view the tool resolved through, the invocation path is the SAME: the identity / agent-reach / current-state exposure gate below and the wrapped descriptor's approval / OAuth / identity path.

func (*AppsAccessor) CallToolAdmitted added in v1.28.0

func (a *AppsAccessor) CallToolAdmitted(ctx context.Context, serverID, resourceURI, tool string, args json.RawMessage) (protocol.MCPAppToolResultRow, error)

CallToolAdmitted implements protocol.AppToolAdmissionInvoker — the distinct, narrow admission-verified invocation seam (HA-56). The AppsSurface calls it ONLY after verifying a fresh render admission for the exact (identity, agent, server, resource) tuple and minting the unforgeable call-local proof.

The call-local proof is the authority — never method selection

This exported method must not reach ResolveAppTool merely because an internal caller invoked it with a fully verified identity / effective-agent context. The accessor therefore verifies the call-local proof (protocol.CheckRenderAdmissionProof) against the EXACT tuple this call names — identity from ctx, the effective agent, serverID, the resourceURI, AND the exact CURRENT provider/catalog generation read from the registry — BEFORE any resolution, before the paused/disabled exposure gate, and before any invocation. A direct call with no proof, or a proof that does not bind the exact tuple, is refused here with zero callbacks. The proof can only be minted by the Protocol surface after it opened the sealed admission AND re-verified the current render tuple, so a call that did not ride the surface's verified path can never resolve an app-only callback.

The generation closes the TOCTOU window

The proof binds the generation the surface verified. The accessor re-reads the CURRENT generation before resolving, so a refresh/replacement that landed after the surface's verification changes the generation and the proof no longer matches — the call is refused with zero callbacks. Even the window between this generation read and the descriptor lookup is closed: resolution goes through the registry's atomic ResolveAppToolAtGeneration, which compares the exact generation and resolves the app-only descriptor under ONE read lock, so a race that changes the generation mid-call fails typed and never returns (never executes) a newer-generation row.

Unlike CallTool / CallToolWithBinding, this path resolves the named tool EXCLUSIVELY through its own server's App dispatch catalog — never the ordinary planner/model catalog, never a provider-local ValidateAppBinding. The sealed admission token is never handed to a provider-local validator. The current paused/disabled exposure gate re-runs, and the SAME wrapped descriptor the ordinary path invokes fires, so approval / OAuth / policy / redaction / retry / audit still work exactly as they do on a planner call.

A host-derived server identity is MANDATORY: an empty server never falls through to ordinary/global resolution.

func (*AppsAccessor) CallToolWithBinding added in v1.27.0

func (a *AppsAccessor) CallToolWithBinding(ctx context.Context, serverID, binding, resourceURI, tool string, args json.RawMessage) (protocol.MCPAppToolResultRow, error)

CallToolWithBinding dispatches an app callback using its opaque render capability while preserving the ordinary invoker compatibility seam. It is the LEGACY live-binding path (HA-56): the binding is passed to the provider-local ValidateAppBinding for app-only resolution. A render-admission-backed call NEVER rides here — it rides the distinct admission-aware seam (CallToolAdmitted).

func (*AppsAccessor) CurrentGeneration added in v1.28.0

func (a *AppsAccessor) CurrentGeneration(serverID string) (string, bool)

CurrentGeneration returns the deterministic current provider/catalog generation fingerprint for the named server, delegating to the MCP registry. The value is content-derived from the canonical CURRENT descriptor set (resources + app-only callbacks + ordinary catalog), so it changes on detach, replacement, and every successful discovery change, and is stable across replicas with the same canonical current descriptor set. Unknown/empty fails closed (false) — a render admission never binds an empty generation.

This is the "current provider/catalog generation" a fresh render-admission gate binds into the sealed tuple. It is NOT itself a proof of erasure / session+agent exposure / paused+disabled state — the gate composes those checks (G4).

func (*AppsAccessor) ReadResource added in v1.4.0

func (a *AppsAccessor) ReadResource(ctx context.Context, serverID, resourceURI string) (protocol.MCPResourceContent, error)

ReadResource implements protocol.MCPResourceReader. It fetches the resource bytes from the MCP registry under the ctx identity triple, then applies the inline-or-offload decision against an effective cap: a `ui://` MCP App document — a Console-render payload that never enters the LLM context — rides inline up to the dedicated App-document cap (appDocumentInlineCap); any other resource keeps the Console inline-payload bound. Content at or above the effective cap is offloaded to the ArtifactStore by reference (with a loud `mcp.resource_offloaded` event); below it, the content rides inline.

func (*AppsAccessor) ToolContext added in v1.4.1

func (a *AppsAccessor) ToolContext(ctx context.Context, serverID, toolCallID string) (protocol.AppToolContextRow, error)

ToolContext implements protocol.AppToolContextReader. It resolves the tool context (input + lowered result) a captured MCP App call produced, scoped to the ctx identity triple, projecting each half inline or by reference exactly as ReadResource does. An unknown or cross-identity (serverID, toolCallID) is not found. It delegates to the same ToolContextStore the MCP providers capture through.

type AppsDeps added in v1.4.0

type AppsDeps struct {
	// Registry resolves `ui://` resource reads against the live MCP
	// providers. Mandatory.
	Registry *mcp.Registry
	// Catalog resolves the app-tool-call proxy target — the SAME catalog
	// a planner resolves against, so the approval gate + tool-side OAuth
	// fire on the proxy path. Mandatory.
	Catalog tools.ToolCatalog
	// Store is the ArtifactStore the heavy-content branch offloads to.
	// Mandatory.
	Store artifacts.ArtifactStore
	// Bus carries the `mcp.resource_offloaded` loud bypass event.
	// Mandatory.
	Bus events.EventBus
	// ToolContext is the tool-context store backing `mcp.apps.tool_context`
	// — the same store the MCP providers capture through, so a discovered
	// app's context is readable here. Mandatory.
	ToolContext *ToolContextStore
	// AgentConfig is the agent-config desired-state registry the app→host
	// call gate reads CURRENT exposure from (the planner-snapshot /
	// app-call-current asymmetry): an App callback to a tool whose server is
	// currently paused — or whose tool is currently disabled — is rejected,
	// while an in-flight planner snapshot is undisturbed. Optional: a nil
	// registry (or empty AgentID) leaves the gate inert (backward
	// compatible). The transport is NEVER consulted — pause is desired-state,
	// not transport state; the live transport stays warm.
	AgentConfig agentcfg.Registry
	// AgentID is the legacy/default slot the gate reads only when a direct
	// embedder did not seat EffectiveAgentConfig on ctx. Production Protocol
	// dispatch always seats its reach-admitted effective selection first.
	// Required when AgentConfig is set; ignored when it is nil.
	AgentID string
	// SessionOverlay is the session-safe narrow-only disable set the app→host
	// gate UNIONS into the admin exposure, so a tool a SESSION user disabled
	// (via agent_config.session.set_source_disables) is also rejected from an
	// App callback — parity with the run-start planner-view projection, which
	// unions the same overlay. Optional: nil leaves only the admin exposure
	// gating. Keyed by the ctx (tenant, user, session) triple.
	SessionOverlay sessionoverlay.Store
	// Threshold is the inline-payload bound in bytes. ≤ 0 falls back to
	// the Console inline-payload bound. The wiring passes that pinned
	// bound rather than the operator's LLM-context heavy-output
	// threshold: these payloads are browser-rendered, never prompt bytes.
	Threshold int
	// Clock returns the current wall-clock time. Optional — defaults to
	// time.Now.
	Clock func() time.Time
}

AppsDeps bundles the runtime collaborators the AppsAccessor bridges. Registry, Catalog, Store, and Bus are mandatory — a nil fails closed at construction (CLAUDE.md §5). Threshold ≤ 0 falls back to the Console inline-payload bound; Clock defaults to time.Now.

type NoOAuthAccessor

type NoOAuthAccessor struct{}

NoOAuthAccessor is the protocol.MCPOAuthAccessor implementation for Runtimes that have an MCP registry but NO operator-configured OAuth providers — the V1 `harbor dev` default when an operator attaches an MCP server without OAuth (the common case today).

The accessor projects the canonical "no binding configured" shape: ListBindings returns an empty slice for every server (no bindings to project), and InitiateBinding / RevokeBinding fail loudly with ErrNoOAuthConfigured so the Console renders an actionable message instead of a silent no-op.

This keeps the F6 contract clean: a Runtime can serve the read-only `mcp.servers.list` / `.get` / `.resources` / `.prompts` / `.health` methods (which the Console MCP Connections page leans on) without needing OAuth wiring, while the OAuth-specific verbs fail loudly per CLAUDE.md §13 (no silent degradation).

func NewNoOAuthAccessor

func NewNoOAuthAccessor() *NoOAuthAccessor

NewNoOAuthAccessor returns a NoOAuthAccessor.

func (*NoOAuthAccessor) InitiateBinding

func (a *NoOAuthAccessor) InitiateBinding(_ context.Context, server, _ string) (string, string, error)

InitiateBinding fails loud — without an OAuth provider there is no flow to initiate. Operators see the message in the Console toast.

func (*NoOAuthAccessor) ListBindings

func (a *NoOAuthAccessor) ListBindings(_ context.Context, _ string) ([]protocol.MCPBindingRow, error)

ListBindings reports zero bindings for every server. The Console MCP Connections page renders this as "OAuth not connected" rather than as an error — the empty list is a first-class state.

func (*NoOAuthAccessor) RevokeBinding

func (a *NoOAuthAccessor) RevokeBinding(_ context.Context, server, _ string) (bool, error)

RevokeBinding fails loud — without an OAuth provider there is no binding to revoke.

type OAuthAccessor

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

OAuthAccessor adapts a *auth.Provider to the protocol.MCPOAuthAccessor interface — the runtime-side seam the MCPSurface calls for the OAuth binding methods (`bindings.list` read + the `refresh_binding` / `revoke_binding` admin verbs).

V1 binding-enumeration scope

The `auth.Provider` keys tokens by `(BindingScope, subject, source)` and exposes no fleet-wide binding enumeration API. At V1 the adapter therefore projects the caller-visible binding state: it reports the configured binding scope for the source and the caller's own token freshness. A fleet-wide per-server binding catalog is a post-V1 `auth.Provider` extension (page-mcp-connections.md §8 — non-admin operators see only their own ScopeUser binding regardless).

func NewOAuthAccessor

func NewOAuthAccessor(provider *auth.Provider) (*OAuthAccessor, error)

NewOAuthAccessor wraps a *auth.Provider as a protocol.MCPOAuthAccessor. A nil provider is rejected — fail closed.

func (*OAuthAccessor) InitiateBinding

func (a *OAuthAccessor) InitiateBinding(ctx context.Context, server, _ string) (string, string, error)

InitiateBinding implements protocol.MCPOAuthAccessor. It invokes auth.Provider.InitiateFlow and returns the AuthorizeURL + flow State the Console opens in a popup. The principalID argument is reserved for a post-V1 delegated-flow API; V1 drives the flow for the caller's own identity (the auth.Provider reads the subject from ctx).

func (*OAuthAccessor) ListBindings

func (a *OAuthAccessor) ListBindings(ctx context.Context, server string) ([]protocol.MCPBindingRow, error)

ListBindings implements protocol.MCPOAuthAccessor. It projects the configured binding for the server (the OAuthConfig's BindingScope + requested scopes) and the caller's own token freshness. NEVER returns token plaintext (invariant).

func (*OAuthAccessor) RevokeBinding

func (a *OAuthAccessor) RevokeBinding(ctx context.Context, server, _ string) (bool, error)

RevokeBinding implements protocol.MCPOAuthAccessor. It invokes auth.Provider.Revoke for the server's binding.

type RegistryAccessor

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

RegistryAccessor adapts a *mcp.Registry to the protocol.MCPAccessor interface. It is the runtime-side read/control seam the MCPSurface calls for the nine `mcp.servers.*` read methods plus the raw-HTML trust toggle.

func NewRegistryAccessor

func NewRegistryAccessor(reg *mcp.Registry, opts ...RegistryAccessorOption) (*RegistryAccessor, error)

NewRegistryAccessor wraps a *mcp.Registry as a protocol.MCPAccessor. A nil registry is rejected — fail closed (CLAUDE.md §5).

func (*RegistryAccessor) GetServer

func (a *RegistryAccessor) GetServer(ctx context.Context, name string) (protocol.MCPServerRow, error)

GetServer implements protocol.MCPAccessor.

func (*RegistryAccessor) Health

Health implements protocol.MCPAccessor.

func (*RegistryAccessor) ListPrompts

func (a *RegistryAccessor) ListPrompts(ctx context.Context, name string) ([]protocol.MCPPromptRow, error)

ListPrompts implements protocol.MCPAccessor.

func (*RegistryAccessor) ListResources

func (a *RegistryAccessor) ListResources(ctx context.Context, name string) ([]protocol.MCPResourceRow, error)

ListResources implements protocol.MCPAccessor.

func (*RegistryAccessor) ListServers

ListServers implements protocol.MCPAccessor.

func (*RegistryAccessor) Probe

Probe implements protocol.MCPAccessor. Beyond the transport round-trip, a probe TRIGGERS OAuth-requirement discovery when the connection has captured a `WWW-Authenticate` challenge and a discoverer is wired: it walks the RFC 9728RFC 8414 chain and records the verbatim requirement on the registry state, which the operator then reads via mcp.servers.get. The probe's own return shape is UNCHANGED — discovery rides the view, never the probe row. The probe error (e.g. a 401) is preserved: a server that requires auth still reports the failure, and discovery still runs off the captured challenge so the requirement is inspectable.

func (*RegistryAccessor) RefreshDiscovery

func (a *RegistryAccessor) RefreshDiscovery(ctx context.Context, name string) (protocol.MCPDiscoveryRow, error)

RefreshDiscovery implements protocol.MCPAccessor.

func (*RegistryAccessor) SetRawHTMLTrust

func (a *RegistryAccessor) SetRawHTMLTrust(ctx context.Context, name string, trusted bool) (bool, error)

SetRawHTMLTrust implements protocol.MCPAccessor.

type RegistryAccessorOption added in v1.13.0

type RegistryAccessorOption func(*RegistryAccessor)

RegistryAccessorOption configures a RegistryAccessor.

func WithOAuthDiscoverer added in v1.13.0

func WithOAuthDiscoverer(d *auth.Discoverer) RegistryAccessorOption

WithOAuthDiscoverer wires the on-demand OAuth-requirement discovery walker into the probe path: a probe that captures a `WWW-Authenticate` challenge triggers the chain walk and records the discovered requirement on the connection's registry state (read back via mcp.servers.get). A nil discoverer is a no-op — the probe path stays discovery-free.

type ToolContextDeps added in v1.4.1

type ToolContextDeps struct {
	// State is the StateStore the captured context persists through.
	// Mandatory.
	State state.StateStore
	// Store is the ArtifactStore a heavy (input or result) payload offloads
	// to by reference. Mandatory.
	Store artifacts.ArtifactStore
	// Bus carries the `mcp.resource_offloaded` loud bypass event on a heavy
	// offload. Mandatory.
	Bus events.EventBus
	// Threshold is the inline-payload bound in bytes. ≤ 0 falls back to
	// the Console inline-payload bound. The wiring passes that pinned
	// bound rather than the operator's LLM-context heavy-output
	// threshold: a captured tool context is read back by the Console,
	// never replayed into a prompt.
	Threshold int
	// Clock returns the current wall-clock time. Optional — defaults to
	// time.Now.
	Clock func() time.Time
}

ToolContextDeps bundles the collaborators the ToolContextStore rides on. State, Store, and Bus are mandatory — a nil fails closed at construction. Threshold ≤ 0 falls back to the Console inline-payload bound; Clock defaults to time.Now.

type ToolContextStore added in v1.4.1

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

ToolContextStore is the runtime-side store behind the MCP Apps "Data Delivery" lifecycle: the WRITE half (Capture) persists the input + lowered result behind a declared `ui://` app at the tool-invocation site, and the READ half (Load) projects it back for `mcp.apps.tool_context`. It rides the EXISTING StateStore — all three drivers (in-mem / SQLite / Postgres) and identity isolation come free — keyed by the caller's identity triple + a per-call Kind; the heavy content (a large result) offloads to the ArtifactStore by reference through the shared loud-bypass path, so a bulky context never bloats a StateRecord and never reaches the LLM.

The record is scoped to the (tenant, user, session) triple with an EMPTY RunID: the captured context is session-scoped (a rendered app reads it within the session that produced it), so the read — which knows the triple but not necessarily the producing run — resolves it. The per-invocation tool-call id disambiguates calls within the session via the Kind. A cross-identity Load returns not-found by construction (StateStore.Load filters by the triple).

Concurrent reuse: ToolContextStore is an immutable adapter — the wrapped StateStore / ArtifactStore / Bus are concurrent-safe compiled artifacts and the adapter adds no mutable state. Per-call identity rides ctx (CLAUDE.md §5).

func NewToolContextStore added in v1.4.1

func NewToolContextStore(deps ToolContextDeps) (*ToolContextStore, error)

NewToolContextStore builds the MCP Apps tool-context store. State, Store, and Bus are mandatory; a nil fails loud.

func (*ToolContextStore) Capture added in v1.4.1

Capture implements mcp.ToolContextCapturer. It persists the tool context (input + lowered result) keyed by the ctx identity triple + the per-call Kind. Each payload below the inline-payload bound rides inline in the envelope; at or above it, the payload offloads to the ArtifactStore by reference (the shared loud-bypass path) and the envelope carries the reference. A missing identity fails closed (CLAUDE.md §6).

func (*ToolContextStore) Load added in v1.4.1

func (s *ToolContextStore) Load(ctx context.Context, serverID, toolCallID string) (protocol.AppToolContextRow, error)

Load resolves a captured tool context for `mcp.apps.tool_context`, scoped to the ctx identity triple. An unknown or cross-identity (serverID, toolCallID) returns a wrapped not-found error whose marker the Protocol surface maps to CodeNotFound — existence is never revealed across identities. A missing identity fails closed.

Directories

Path Synopsis
Package admission provides the narrow, stateless, sealed render-admission authority for MCP App `ui://` render documents.
Package admission provides the narrow, stateless, sealed render-admission authority for MCP App `ui://` render documents.

Jump to

Keyboard shortcuts

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