# What Is the MCP Elicitation Process and How Do Goose Extensions Expose Tools?

> Understand the MCP elicitation process and how Goose extensions expose tools. Learn about the round-trip workflow and tool ID validation for agents.

- Repository: [Block Open Source/goose](https://github.com/block/goose)
- Tags: internals
- Published: 2026-04-05

---

**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:

1. **Agent detects missing data** – When a tool requires parameters that cannot be inferred automatically, the agent initiates an elicitation request.

2. **`McpClient::create_elicitation` builds the request** – Located in [`crates/goose/src/agents/mcp_client.rs`](https://github.com/block/goose/blob/main/crates/goose/src/agents/mcp_client.rs) (lines 302-341), this method constructs a JSON schema describing the required data and hands it to the global `ActionRequiredManager`. The manager generates a unique request ID, wraps the message as an `action_required_elicitation` type, and transmits it to the client UI or CLI via an unbounded channel.

3. **`ActionRequiredManager::request_and_wait` blocks for response** – In [`crates/goose/src/action_required_manager.rs`](https://github.com/block/goose/blob/main/crates/goose/src/action_required_manager.rs) (lines 39-78), the manager stores a `PendingRequest` containing a oneshot channel keyed by the unique ID. It then blocks execution using `tokio::time::timeout` with a configurable duration, preventing indefinite hangs.

4. **User submits response through UI** – In the CLI implementation ([`crates/goose-cli/src/session/elicitation.rs`](https://github.com/block/goose/blob/main/crates/goose-cli/src/session/elicitation.rs)), the `collect_elicitation_input` helper renders the prompt and parses the JSON payload. The CLI then calls `ActionRequiredManager::submit_response` with the request ID and user-provided data, as seen in [`crates/goose/src/agents/agent.rs`](https://github.com/block/goose/blob/main/crates/goose/src/agents/agent.rs) (lines 984-1004).

5. **Agent resumes execution** – The pending oneshot channel receives the response, `create_elicitation` returns a successful `CreateElicitationResult`, 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:

```rust
// 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`](https://github.com/block/goose/blob/main/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`](https://github.com/block/goose/blob/main/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. The `available_tools` field lists the tool names the command implements.
- **Builtin** – Runs built-in MCP server extensions (e.g., memory, computer-controller) shipped with Goose. The `available_tools` vector enumerates the tool IDs provided by the service.
- **Platform** – Executes inside the agent process with direct Rust API access. `available_tools` lists the IDs of Rust functions exposed to the model.
- **StreamableHttp** – Calls remote HTTP endpoints following the MCP Streamable HTTP specification. `available_tools` enumerates 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 identifier
- `description` – User-facing UI text
- `available_tools` – **Explicit list of tool IDs** validated before forwarding
- `bundled` – Boolean indicating if the extension runs inside the bundled MCP server
- `timeout` – Optional per-extension execution limit

### Tool Registration Flow

When a session initializes, `ExtensionManager` (in [`crates/goose/src/agents/extension_manager.rs`](https://github.com/block/goose/blob/main/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:

```toml

# 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:

```rust
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`](https://github.com/block/goose/blob/main/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_elicitation` pauses execution to request user data via `ActionRequiredManager`, which blocks on a oneshot channel until `submit_response` receives the JSON payload.
- **Timeout protection** prevents indefinite hangs through `tokio::time::timeout` enforced in `ActionRequiredManager::request_and_wait`.
- **Extensions expose tools** through the `ExtensionConfig` enum's `available_tools` field, which explicitly lists valid tool IDs that the agent validates before forwarding to any model.
- **Registration flow** involves `ExtensionManager` reading 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`](https://github.com/block/goose/blob/main/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`](https://github.com/block/goose/blob/main/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`.