Documentation
¶
Overview ¶
Package mcp wires user-configured Model Context Protocol servers into the agent loop.
At startup the host calls Build, which reads .agents/mcp.json, spawns each declared server (stdio child or Streamable HTTP client), wraps the resulting MCP toolsets via ADK's google.golang.org/adk/tool/mcptoolset, and returns:
- the toolsets, so they can be passed to agent.New(WithToolsets…)
- per-server records the host can render (e.g. a /mcp slash command).
Failures are non-fatal: a server whose process won't start surfaces in the per-server record with its error; the agent continues with whichever servers did connect.
Index ¶
- Constants
- func DeclineHandler(serverName string, send func(string)) func(context.Context, *mcp.ElicitRequest) (*mcp.ElicitResult, error)
- func InterpolateEnv(s string) string
- func InterpolateMap(m map[string]string) map[string]string
- func SetImplementationName(name string)
- type AuthSpec
- type DigestOptions
- type ElicitorFn
- type GoogleOAuthAuth
- type LLMFallbackResult
- type Server
- type ServerSpec
- type Servers
- type ToolInfo
Constants ¶
const ( StatusOK = "ok" StatusError = "error" )
Status values surfaced via the per-server records.
const DefaultAgenticWrapThreshold = 8000
DefaultAgenticWrapThreshold is the default byte threshold below which MCP responses bypass the digest wrapper. 8000 bytes ≈ 2000 tokens — the wrap/router overhead exceeds the bloat cost below this.
const MCPFileName = "mcp.json"
MCPFileName is the project-local MCP config file inside .agents/.
Variables ¶
This section is empty.
Functions ¶
func DeclineHandler ¶
func DeclineHandler(serverName string, send func(string)) func(context.Context, *mcp.ElicitRequest) (*mcp.ElicitResult, error)
DeclineHandler returns an MCP elicitation handler that declines every request and emits a one-line notice through send. Used as the fallback when no interactive elicitor is wired.
func InterpolateEnv ¶
InterpolateEnv is retained as a delegating alias for pkg/agentenv. The regex + resolver logic migrated to pkg/agentenv when the wider ${env:VAR} substitution mechanism landed (#322); this file keeps the symbol so lifecycle.go call sites don't need touching. New callers should use agentenv.NewResolver to get manifest-aware interpolation (fail-loud required checks, sensitive-value tracking, drift diagnostics) rather than this bare-os.Getenv path.
func InterpolateMap ¶
InterpolateMap runs each value through InterpolateEnv. Used for ServerSpec.Env and ServerSpec.Headers. Same delegation note as InterpolateEnv applies.
func SetImplementationName ¶
func SetImplementationName(name string)
SetImplementationName overrides the name reported during the MCP client handshake. Useful for hosts that want to identify themselves to the server. Call before Build.
Types ¶
type AuthSpec ¶
type AuthSpec struct {
GoogleOAuth *GoogleOAuthAuth `json:"google_oauth,omitempty"`
}
AuthSpec selects an authentication strategy for an HTTP MCP server. Exactly one inner field may be set. Future strategies (audience- scoped ID tokens for Cloud Run / IAP, mTLS, etc.) slot in as sibling pointers.
type DigestOptions ¶
type DigestOptions struct {
// Store is the CCR backing for retrieve_raw. When nil, digest
// still runs but retrieve_raw returns "no raw payload" — matches
// digest-design.md OQ1 default when --session-db is off.
Store digest.Store
// Threshold is the byte size below which responses bypass the
// router entirely. Zero → DefaultAgenticWrapThreshold.
Threshold int
// NeverServers names MCP servers (by mcp.json key) that opt out
// of digesting. Operator escape hatch for debug-sensitive or
// known-tiny servers.
NeverServers map[string]bool
// LLMFallback, when non-nil, invokes a small-tier subagent to
// digest MCP responses the structural JSON pruner can't reduce
// (prose, malformed JSON, or JSON that's structurally minimal
// and mostly-values). Callers pass a closure that owns a
// reference to an Agent + a resolved small-model; the wrapper
// invokes it with the raw payload and gets back a compressed
// digest plus subagent usage numbers.
//
// Returned SubagentModel + token counts populate
// digest.Savings.Subagent* on the resulting Result so /stats,
// per-tool footer, and OTel span attributes have real cost
// figures for the agentic path.
//
// A nil LLMFallback preserves the #128-shipped structural-only
// behavior (bounded passthrough for anything structural can't
// reduce). No opt-in from operators required to keep that.
LLMFallback func(ctx context.Context, raw []byte) (LLMFallbackResult, error)
// OnResult, when non-nil, fires after every successful Process
// call with the (fully-decorated) Result. Callers use this to
// aggregate per-call Savings into session-level counters — the
// usage.Tracker sink in cmd/core-agent wires this to a cumulative
// digest-savings block rendered by /context.
//
// Firing is best-effort: skipped on Process errors / marshal
// failures where Result is undefined. Runs synchronously on the
// wrapper's Run goroutine, so callers should keep the callback
// fast (increment counters, don't do I/O).
OnResult func(*digest.Result)
}
DigestOptions configures how Build wraps MCP tool responses through pkg/digest. A nil *DigestOptions passed to Build disables wrapping entirely (existing behavior). A non-nil options struct wraps every tool from every server that isn't in NeverServers.
LLMFallback opt-in (#223): a non-nil LLMFallback enables the LLM subagent digester for prose-shaped MCP responses that the structural pruner can't reduce below Threshold. Left nil, those responses take the bounded passthrough branch (#128's shipped default).
type ElicitorFn ¶
type ElicitorFn func(ctx context.Context, serverName string, req *mcp.ElicitRequest) (*mcp.ElicitResult, error)
ElicitorFn is the host-supplied bridge that turns a server's elicitation request into a user response. Interactive hosts plug in a function that opens a prompt and blocks; the headless path leaves it nil and falls back to DeclineHandler.
The implementation must respect ctx — if it returns ctx.Err the SDK translates that into a protocol-level cancel.
type GoogleOAuthAuth ¶
type GoogleOAuthAuth struct {
// Scopes is the OAuth 2.0 scopes requested on the access token.
// No default — each server documents its own scope requirements,
// and an implicit broad default (e.g. cloud-platform) would grant
// more privilege than necessary. Explicit is safer.
//
// For the GKE MCP server, typical values:
// https://www.googleapis.com/auth/container.read-only
// https://www.googleapis.com/auth/container
Scopes []string `json:"scopes"`
}
GoogleOAuthAuth authenticates outbound MCP requests with a Google OAuth 2.0 access token sourced from Application Default Credentials. Suitable for Google-hosted API endpoints that accept scoped access tokens (e.g. the GKE MCP server at container.googleapis.com/mcp).
For audience-scoped ID-token auth (Cloud Run / IAP / custom OIDC), add a sibling GoogleIDToken field when a consumer needs it.
func (*GoogleOAuthAuth) Validate ¶
func (g *GoogleOAuthAuth) Validate(name string) error
Validate reports whether the GoogleOAuthAuth is usable.
type LLMFallbackResult ¶
type LLMFallbackResult struct {
Text string
SubagentModel string
SubagentInputTokens int
SubagentOutputTokens int
}
LLMFallbackResult is what an operator-supplied LLMFallback returns after running the raw MCP payload through a small-tier subagent. Text is the digest the model sees; SubagentModel + token counts feed digest.Savings.Subagent* for /stats + OTel display without pkg/mcp needing to import pkg/agent (which would create an import cycle down the line — pkg/agent lives above pkg/mcp in the layer hierarchy).
The caller (cmd/core-agent, or any host code) constructs the closure supplying LLMFallback and captures the *Agent needed to invoke RunSubtask in that closure's environment.
type Server ¶
type Server struct {
Name string
Status string
Tools []string // tool names exposed; populated lazily by Toolset
ToolInfos []ToolInfo // name + description pairs, parallel to Tools
Err error // non-nil when Status == StatusError
// contains filtered or unexported fields
}
Server is one configured MCP server's runtime state.
func Build ¶
func Build(ctx context.Context, agentsDir string, send func(string), gate *permissions.Gate, elicitor ElicitorFn, digestOpts *DigestOptions) ([]*Server, []tool.Toolset, error)
Build reads .agents/mcp.json and starts every declared server in parallel. The send callback is plumbed into each server's elicitation handler (when no interactive elicitor is provided) so the host can surface elicitation requests in the right place.
gate (optional) gates each MCP tool call through the permission system so MCP tools are subject to the same ask/allow/yolo rules as built-in tools. Pass nil to skip gating.
elicitor (optional) is the interactive bridge for elicitation requests. Headless callers leave it nil and fall back to the decline-with-notice stub.
Servers that fail to start come back with Status==StatusError so they're visible without breaking the rest of the agent.
type ServerSpec ¶
type ServerSpec struct {
Transport string `json:"transport"` // "stdio" | "http"
Command string `json:"command,omitempty"` // stdio
Args []string `json:"args,omitempty"` // stdio
Env map[string]string `json:"env,omitempty"` // stdio
URL string `json:"url,omitempty"` // http
Headers map[string]string `json:"headers,omitempty"` // http
Auth *AuthSpec `json:"auth,omitempty"` // http
AgenticNever bool `json:"agentic_never,omitempty"` // skip digest wrap for this server
}
ServerSpec describes one MCP server. Either Command (stdio) or URL (Streamable HTTP) must be set; we intentionally don't support both.
AgenticNever opts this specific server out of the digest wrap layer — the operator escape hatch for debug-sensitive or known-tiny servers where wrapping would hurt more than it helps.
func (ServerSpec) Validate ¶
func (s ServerSpec) Validate(name string) error
Validate checks that the spec describes a single, complete transport.
type Servers ¶
type Servers struct {
Version int `json:"version"`
Servers map[string]ServerSpec `json:"servers"`
AgenticWrap *bool `json:"agentic_wrap,omitempty"` // default true; ptr for explicit off
AgenticWrapThreshold int `json:"agentic_wrap_threshold,omitempty"` // default 8000
AgenticWrapLLM *bool `json:"agentic_wrap_llm,omitempty"` // default false; ptr for explicit on
AgenticWrapModel string `json:"agentic_wrap_model,omitempty"` // MCP-specific small-model override; empty falls through to --agentic-small-model resolution
}
Servers is the on-disk schema for .agents/mcp.json.
AgenticWrap + AgenticWrapThreshold are the operator knobs for the structural-digester wrap layer (#130). CLI --no-mcp-digest kills the whole surface regardless; per-server AgenticNever opts one server out without touching the global flag.
AgenticWrapLLM + AgenticWrapModel gate the #223 LLM subagent second-chance path — a small-tier subagent that digests responses the structural pruner can't reduce under threshold. Default off: the mechanism is safe but the cost profile depends on the operator's MCP surface, so opt-in until dogfooded.
func Load ¶
Load reads <agentsDir>/mcp.json. A missing file is treated as "no servers configured" — not an error, since most projects never declare MCP servers.
func (*Servers) AgenticWrapEnabled ¶
AgenticWrapEnabled reports whether the operator has opted the wrap layer in. Default true; the flag ships as a kill switch (matches #217 OTel posture — no --enable-* flags), so absence == on.
func (*Servers) AgenticWrapLLMEnabled ¶
AgenticWrapLLMEnabled reports whether the operator has opted the LLM subagent second-chance path in via mcp.json. Absence == off (opposite of AgenticWrap): the structural pruner is a safe default but the LLM fallback trades wall-clock + subagent cost for its compression win, so opt-in until an operator confirms the trade-off works for their MCP surface.
func (*Servers) AgenticWrapThresholdBytes ¶
AgenticWrapThresholdBytes returns the operator-configured threshold or the built-in default when unset / zero.
type ToolInfo ¶
ToolInfo is a name + description pair for one exposed MCP tool. Surfaced so the TUI's /mcp command can render the same rich "name + description" format /tools uses, without each consumer re-enumerating the toolset (which requires constructing a stub ReadonlyContext). Sorted by Name to match Tools' ordering.