# Difference Between stdio and streamable-http Extensions in Goose: MCP Transport Guide

> Understand the difference between stdio and streamable-http extensions in Goose. Learn how stdio uses local child processes and streamable-http connects to remote servers via HTTP for MCP transport.

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

---

**`stdio` extensions spawn local child processes and communicate over standard input/output pipes, while `streamable_http` extensions connect to remote MCP servers via HTTP using the Streamable HTTP protocol.**

The Goose agent framework (block/goose) supports two distinct transport mechanisms for Model Context Protocol (MCP) extensions. Understanding the architectural differences between **stdio** and **streamable-http** extensions helps developers choose the right integration strategy for local tools versus remote services.

## Core Architectural Differences

| Aspect | stdio | streamable_http |
|--------|-------|-----------------|
| **Transport Layer** | Uses `rmcp::transport::stdio`—a bidirectional pipe that feeds the MCP protocol through the child’s `stdin` and `stdout` | Uses `rmcp::transport::streamable_http_client`—an HTTP client (`reqwest`) that follows the Streamable HTTP MCP specification |
| **Process Management** | Goose spawns a local binary or runs the command inside a Docker container; the binary implements the MCP server side | No local process is started; Goose acts as an HTTP client talking to an already-running MCP server reachable at a URI |
| **Configuration Fields** | `cmd`, `args`, `envs`, `env_keys`, `timeout` | `uri`, `headers`, `envs`, `env_keys`, `timeout` |
| **CLI Interface** | `--with-extension "<env>=... command args"` | `--with-streamable-http-extension "<url> [timeout=...]"` |
| **Typical Use Case** | Quick, self-contained tools (e.g., `uvx mcp_gdrive@latest`) or extensions needing direct OS access | Cloud-hosted MCP services or internal HTTP-based tools (e.g., Asana integration) |

Both variants are defined in the `ExtensionConfig` enum located in [`crates/goose/src/agents/extension.rs`](https://github.com/block/goose/blob/main/crates/goose/src/agents/extension.rs) at lines 167–236, and both are handled by the same extension manager logic in [`extension_manager.rs`](https://github.com/block/goose/blob/main/extension_manager.rs).

## Configuration Structure in extension.rs

The `ExtensionConfig` enum distinguishes the two transport types through distinct struct variants. In [`crates/goose/src/agents/extension.rs`](https://github.com/block/goose/blob/main/crates/goose/src/agents/extension.rs), the definitions specify which fields are required for each transport:

```rust
pub enum ExtensionConfig {
    #[serde(rename = "stdio")]
    Stdio {
        name: String,
        description: String,
        cmd: String,
        args: Vec<String>,
        envs: Envs,
        env_keys: Vec<String>,
        timeout: Option<u64>,
        bundled: Option<bool>,
        available_tools: Vec<String>,
    },

    #[serde(rename = "streamable_http")]
    StreamableHttp {
        name: String,
        description: String,
        uri: String,
        envs: Envs,
        env_keys: Vec<String>,
        headers: HashMap<String, String>,
        timeout: Option<u64>,
        bundled: Option<bool>,
        available_tools: Vec<String>,
    },
}

```

The `Stdio` variant requires a `cmd` field specifying the executable path, while `StreamableHttp` requires a `uri` field pointing to the HTTP endpoint. Validation logic in [`crates/goose/src/agents/validate_extensions.rs`](https://github.com/block/goose/blob/main/crates/goose/src/agents/validate_extensions.rs) enforces these requirements, ensuring `stdio` extensions have a valid command and `streamable_http` extensions have a valid URI.

## Transport Implementation Details

### Stdio Extension Lifecycle

When Goose initializes a stdio extension, it spawns a child process and establishes bidirectional pipes. In [`crates/goose/src/agents/extension_manager.rs`](https://github.com/block/goose/blob/main/crates/goose/src/agents/extension_manager.rs) at lines 700–730, the `ExtensionConfig::Stdio` match arm creates the transport:

```rust
ExtensionConfig::Stdio {
    name,
    cmd,
    args,
    envs,
    env_keys,
    timeout,
    ..
} => {
    // Spawn child with environment variables and arguments
    let child = Command::new(&cmd)
        .args(&args)
        .envs(envs.get_env())
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()?;
    
    let transport = stdio::StdIOTransport::new(child);
    // Connect the MCP client using the stdio transport...
}

```

This approach runs the MCP server as a separate process, with Docker isolation available via the `--container` flag for enhanced security.

### Streamable-HTTP Client Setup

For HTTP-based extensions, Goose builds a `reqwest` client and configures the `StreamableHttpClientTransport`. The implementation in [`extension_manager.rs`](https://github.com/block/goose/blob/main/extension_manager.rs) at lines 306–380 handles the `ExtensionConfig::StreamableHttp` variant:

```rust
ExtensionConfig::StreamableHttp {
    name,
    uri,
    headers,
    timeout,
    ..
} => {
    let client = reqwest::Client::builder()
        .default_headers(build_headers(headers))
        .build()?;
    
    let transport = StreamableHttpClientTransport::with_client(
        client,
        StreamableHttpClientTransportConfig { uri: uri.into(), ..Default::default() },
    );
    // Connect the MCP client with optional OAuth fallback...
}

```

If the remote server returns an authentication challenge, Goose initiates an OAuth flow defined in [`extension_manager.rs`](https://github.com/block/goose/blob/main/extension_manager.rs) at lines 558–590.

## CLI Usage and Practical Examples

### Running Local Tools with Stdio

Use the `--with-extension` flag to launch local executables. The CLI parser in [`crates/goose-cli/src/session/mod.rs`](https://github.com/block/goose/blob/main/crates/goose-cli/src/session/mod.rs) (lines 300–320) handles environment variable assignments and command parsing:

```bash
goose run \
  --with-extension "MY_API_KEY=abc123 uvx mcp_gdrive@latest" \
  --recipe my_recipe.yaml

```

This string parses into an `ExtensionConfig::Stdio` with:
- `cmd`: "uvx"
- `args`: ["mcp_gdrive@latest"]
- `envs`: {MY_API_KEY: "abc123"}

### Connecting to Remote HTTP Endpoints

Use the `--with-streamable-http-extension` flag for remote services. The parser in [`crates/goose-cli/src/cli.rs`](https://github.com/block/goose/blob/main/crates/goose-cli/src/cli.rs) (lines 17–40) extracts the URL and optional timeout:

```bash
goose run \
  --with-streamable-http-extension "https://mcp.asana.com/mcp timeout=120" \
  --recipe my_recipe.yaml

```

This creates an `ExtensionConfig::StreamableHttp` with the specified URI and a 120-second timeout.

### Programmatic Extension Configuration

Factory methods in [`crates/goose/src/agents/extension.rs`](https://github.com/block/goose/blob/main/crates/goose/src/agents/extension.rs) (lines 295–322) allow programmatic construction:

```rust
use goose::agents::ExtensionConfig;

// Stdio extension for local Google Drive integration
let stdio_ext = ExtensionConfig::stdio(
    "gdrive",
    "uvx",
    "Google Drive MCP",
    300_u64,
).with_args(vec!["mcp_gdrive@latest"]);

// Streamable HTTP extension for Asana
let http_ext = ExtensionConfig::streamable_http(
    "asana",
    "https://mcp.asana.com/mcp",
    "Asana MCP",
    300_u64,
);

```

## Key Implementation Files

Understanding the full lifecycle requires examining these specific source files:

- **[`crates/goose/src/agents/extension.rs`](https://github.com/block/goose/blob/main/crates/goose/src/agents/extension.rs)**: Defines the `ExtensionConfig` enum, including the `Stdio` and `StreamableHttp` variants and their factory constructors.
- **[`crates/goose/src/agents/extension_manager.rs`](https://github.com/block/goose/blob/main/crates/goose/src/agents/extension_manager.rs)**: Contains the runtime logic that spawns child processes for stdio or creates HTTP client transports, plus the OAuth fallback handler for streamable-http authentication.
- **[`crates/goose-cli/src/cli.rs`](https://github.com/block/goose/blob/main/crates/goose-cli/src/cli.rs)**: Implements `StreamableHttpOptions` and the parser for `--with-streamable-http-extension` arguments.
- **[`crates/goose-cli/src/session/mod.rs`](https://github.com/block/goose/blob/main/crates/goose-cli/src/session/mod.rs)**: Provides `parse_stdio_extension` for handling `--with-extension` CLI strings.
- **[`crates/goose/src/agents/validate_extensions.rs`](https://github.com/block/goose/blob/main/crates/goose/src/agents/validate_extensions.rs)**: Validates that stdio extensions have a `cmd` field and streamable-http extensions have a `uri` field.
- **[`crates/goose/tests/providers.rs`](https://github.com/block/goose/blob/main/crates/goose/tests/providers.rs)**: Test suite demonstrating example configurations for both transport types.

## Summary

- **stdio extensions** launch **local executables** and communicate over **standard input/output pipes**, making them ideal for self-contained tools requiring OS access.
- **streamable-http extensions** connect to **remote HTTP endpoints** using the **Streamable HTTP protocol**, suitable for cloud-hosted MCP services without spawning local processes.
- Both types are defined in [`extension.rs`](https://github.com/block/goose/blob/main/extension.rs), managed by [`extension_manager.rs`](https://github.com/block/goose/blob/main/extension_manager.rs), and interchangeable in Goose recipes.
- **stdio** requires a `cmd` field and spawns a child process with optional Docker isolation.
- **streamable-http** requires a `uri` field and uses a `reqwest` client with optional OAuth authentication.

## Frequently Asked Questions

### When should I choose stdio over streamable-http extensions?

Choose **stdio** when you need to run local tools that require filesystem access, environment variables, or specific system binaries—such as `uvx` packages or Docker containers. Choose **streamable-http** when integrating with cloud-hosted services, serverless functions, or internal HTTP APIs that already expose an MCP endpoint, eliminating the need to manage local process lifecycles.

### Can streamable-http extensions run in Docker containers?

No. **streamable-http** extensions do not spawn local processes, so Docker isolation does not apply. They function purely as HTTP clients connecting to external servers. If you need containerized execution, use a **stdio** extension with the `--container` flag, which runs the command inside a Docker container as implemented in [`extension_manager.rs`](https://github.com/block/goose/blob/main/extension_manager.rs).

### How does authentication work for HTTP-based extensions?

Goose handles authentication for **streamable-http** extensions through an OAuth fallback mechanism. If the remote MCP server responds with an authentication challenge, the code in [`extension_manager.rs`](https://github.com/block/goose/blob/main/extension_manager.rs) (lines 558–590) initiates an OAuth flow. You can also pass static headers (such as API keys) via the `headers` field in the configuration.

### Is it possible to use both extension types simultaneously?

Yes. Goose supports mixing **stdio** and **streamable-http** extensions within the same session. You can pass multiple `--with-extension` and `--with-streamable-http-extension` flags to the CLI, or construct both variants programmatically and add them to the same agent configuration. The extension manager initializes each transport independently while exposing all tools to the Goose agent uniformly.