What Is the MCP Elicitation Process and How Do Goose Extensions Expose Tools?
The MCP elicitation process is a round-trip workflow where a Goose agent pauses tool execution to request missing user data via a JSON schema form, while extensions expose tools through the ExtensionConfig enum which explicitly lists available tool IDs that the agent validates before forwarding to the model.
The block/goose repository implements a Model Context Protocol (MCP) agent framework that handles dynamic user interactions through structured elicitation and modular tool discovery. Understanding how Goose manages incomplete tool parameters through its elicitation flow—and how it registers external capabilities via its extension architecture—reveals the robustness of its MCP implementation. This article examines the source code to explain the technical mechanisms behind both the elicitation process and tool exposure.
Understanding the MCP Elicitation Process
MCP elicitation allows an MCP-enabled Goose agent to request additional information from the user when a tool’s schema cannot be satisfied automatically. According to the block/goose source code, this process involves coordinated interaction between the MCP client, an action manager, and the user interface.
The Five-Step Elicitation Flow
The elicitation process follows a precise sequence implemented across several core files:
-
Agent detects missing data – When a tool requires parameters that cannot be inferred automatically, the agent initiates an elicitation request.
-
McpClient::create_elicitationbuilds the request – Located incrates/goose/src/agents/mcp_client.rs(lines 302-341), this method constructs a JSON schema describing the required data and hands it to the globalActionRequiredManager. The manager generates a unique request ID, wraps the message as anaction_required_elicitationtype, and transmits it to the client UI or CLI via an unbounded channel. -
ActionRequiredManager::request_and_waitblocks for response – Incrates/goose/src/action_required_manager.rs(lines 39-78), the manager stores aPendingRequestcontaining a oneshot channel keyed by the unique ID. It then blocks execution usingtokio::time::timeoutwith a configurable duration, preventing indefinite hangs. -
User submits response through UI – In the CLI implementation (
crates/goose-cli/src/session/elicitation.rs), thecollect_elicitation_inputhelper renders the prompt and parses the JSON payload. The CLI then callsActionRequiredManager::submit_responsewith the request ID and user-provided data, as seen incrates/goose/src/agents/agent.rs(lines 984-1004). -
Agent resumes execution – The pending oneshot channel receives the response,
create_elicitationreturns a successfulCreateElicitationResult, and the agent continues the tool call with the completed parameters.
Technical Implementation Details
The elicitation mechanism relies on specific message types and timeout handling:
- Message Type: The request is sent as a
MessageContent::action_required_elicitation, which the UI renders as a dynamic form based on the supplied JSON schema. - Timeout Protection: The system enforces time limits via
tokio::time::timeout, ensuring the session does not hang if the user fails to respond. - Model Agnostic: Any provider enabling the elicitation capability can participate in this workflow, making it extensible across different LLM backends.
Triggering an Elicitation from Tool Code
The following Rust example demonstrates how an MCP-enabled tool initiates the elicitation flow:
// Inside an MCP‑enabled tool implementation
let request = CreateElicitationRequestParams::FormElicitationParams {
message: "Please upload a CSV file".into(),
requested_schema: json_schema, // serde_json::Value describing the form
..Default::default()
};
let result = mcp_client.create_elicitation(request, ctx).await?;
match result.action {
ElicitationAction::Accept => {
let user_data = result.content.unwrap(); // JSON supplied by the user
// Continue tool logic with `user_data`
}
_ => return Err(anyhow!("User declined elicitation")),
}
This implementation in mcp_client.rs handles the complex coordination of building schemas and managing the blocking wait for user input.
How Goose Extensions Expose Tools
Goose extensions define tool providers through a structured configuration system that explicitly advertises available capabilities to the agent. This ensures the model only invokes tools that are properly registered and validated.
The ExtensionConfig Enum
Extensions are defined by the ExtensionConfig enum in crates/goose/src/agents/extension.rs (lines 150-204). This enum supports multiple transport mechanisms, each specifying how tools are advertised:
- Stdio – Executes a local command (
cmd+args) in a subprocess. Theavailable_toolsfield lists the tool names the command implements. - Builtin – Runs built-in MCP server extensions (e.g., memory, computer-controller) shipped with Goose. The
available_toolsvector enumerates the tool IDs provided by the service. - Platform – Executes inside the agent process with direct Rust API access.
available_toolslists the IDs of Rust functions exposed to the model. - StreamableHttp – Calls remote HTTP endpoints following the MCP Streamable HTTP specification.
available_toolsenumerates the tool names the remote service offers. - Sse – Legacy Server-Sent-Events transport maintained for configuration compatibility (deprecated, no tool list).
Common fields across all variants include:
name– Session identifierdescription– User-facing UI textavailable_tools– Explicit list of tool IDs validated before forwardingbundled– Boolean indicating if the extension runs inside the bundled MCP servertimeout– Optional per-extension execution limit
Tool Registration Flow
When a session initializes, ExtensionManager (in crates/goose/src/agents/extension_manager.rs) reads the ExtensionConfig objects from the recipe, validates environment variables, starts required subprocesses for Stdio variants, and registers the declared tools. The manager injects these tool descriptors into the MCP client’s Initialize request, ensuring the model receives an accurate capability catalog.
Defining Extensions in Configuration
The following TOML configuration demonstrates enabling a built-in memory extension:
# In a Goose recipe (YAML) – enable a builtin memory extension
extensions:
- type: builtin
name: memory
description: "In‑session vector memory"
available_tools: ["memory.search", "memory.add"]
bundled: true
The Builtin variant in ExtensionConfig (lines 188-203) stores these available_tools, which the extension manager registers as callable MCP tools during session startup.
Programmatic Extension Definition
For external tools requiring subprocess execution, use the Stdio variant:
ExtensionConfig::Stdio {
name: "pdf_extractor".into(),
description: "Extract text from PDF".into(),
cmd: "python".into(),
args: vec!["extract.py".into()],
available_tools: vec!["pdf.extract".into()],
envs: Envs::default(),
timeout: Some(30),
..Default::default()
}
This configuration in crates/goose/src/agents/extension.rs (lines 167-187) directs Goose to spawn python extract.py, while explicitly advertising only the "pdf.extract" tool to the model.
Summary
- MCP elicitation is a structured round-trip where
McpClient::create_elicitationpauses execution to request user data viaActionRequiredManager, which blocks on a oneshot channel untilsubmit_responsereceives the JSON payload. - Timeout protection prevents indefinite hangs through
tokio::time::timeoutenforced inActionRequiredManager::request_and_wait. - Extensions expose tools through the
ExtensionConfigenum'savailable_toolsfield, which explicitly lists valid tool IDs that the agent validates before forwarding to any model. - Registration flow involves
ExtensionManagerreading configurations, starting subprocesses if needed, and injecting tool descriptors into the MCP client's initialization sequence.
Frequently Asked Questions
What triggers an MCP elicitation request in Goose?
An elicitation request triggers when an MCP-enabled tool detects that required parameters cannot be satisfied automatically from the context or conversation history. The agent then calls McpClient::create_elicitation in crates/goose/src/agents/mcp_client.rs to build a JSON schema form for the user to complete before execution resumes.
How does the ActionRequiredManager prevent indefinite blocking?
The manager enforces timeouts using tokio::time::timeout within the request_and_wait method (lines 39-78 of crates/goose/src/action_required_manager.rs). If the user fails to respond within the configured duration, the pending request expires and the elicitation returns an error, allowing the agent to fail gracefully rather than hanging indefinitely.
What distinguishes Builtin from Stdio extensions?
Builtin extensions run as internal MCP servers bundled within the Goose binary, accessing Rust APIs directly and listing their tools in available_tools. Stdio extensions spawn external subprocesses (like Python scripts) and communicate via standard input/output, requiring Goose to manage the process lifecycle while still validating calls against the explicitly declared available_tools list.
How does Goose validate tool calls against extensions?
Before forwarding any tool invocation to an extension, Goose validates the requested tool ID against the available_tools vector defined in the extension's ExtensionConfig. This verification occurs in the agent layer, ensuring the model can only invoke tools explicitly advertised during the session initialization managed by ExtensionManager.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →