Skip to main content
Goose extensions communicate using MCP (Model Context Protocol), a standardized protocol for AI agent-extension communication.

What is MCP?

MCP (Model Context Protocol) is an open protocol that enables AI applications to integrate with external data sources and tools. It provides:
  • Standardized communication - Consistent interface across extensions
  • Tool discovery - Extensions expose available capabilities
  • Bidirectional streaming - Efficient data transfer
  • Type safety - JSON Schema validation

Protocol Overview

MCP uses JSON-RPC 2.0 over stdio (standard input/output) for communication.

Message Format

Requests:
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}
Responses:
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "read_file",
        "description": "Read file contents",
        "inputSchema": { ... }
      }
    ]
  }
}
Errors:
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32601,
    "message": "Method not found"
  }
}

Core MCP Methods

Tools

List Tools
Request:
{
  "method": "tools/list",
  "params": {}
}

Response:
{
  "result": {
    "tools": [
      {
        "name": "tool_name",
        "description": "What the tool does",
        "inputSchema": {
          "type": "object",
          "properties": { ... },
          "required": [ ... ]
        }
      }
    ]
  }
}
Call Tool
Request:
{
  "method": "tools/call",
  "params": {
    "name": "tool_name",
    "arguments": { ... }
  }
}

Response:
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Tool result"
      }
    ],
    "isError": false
  }
}

Resources

List Resources
Request:
{
  "method": "resources/list",
  "params": {}
}

Response:
{
  "result": {
    "resources": [
      {
        "uri": "ext://path/to/resource",
        "name": "Resource Name",
        "description": "What it contains",
        "mimeType": "application/json"
      }
    ]
  }
}
Read Resource
Request:
{
  "method": "resources/read",
  "params": {
    "uri": "ext://path/to/resource"
  }
}

Response:
{
  "result": {
    "contents": [
      {
        "uri": "ext://path/to/resource",
        "mimeType": "application/json",
        "text": "{ ... }"
      }
    ]
  }
}

Prompts

List Prompts
Request:
{
  "method": "prompts/list",
  "params": {}
}

Response:
{
  "result": {
    "prompts": [
      {
        "name": "prompt_name",
        "description": "What the prompt does",
        "arguments": [
          {
            "name": "arg_name",
            "description": "Argument purpose",
            "required": true
          }
        ]
      }
    ]
  }
}
Get Prompt
Request:
{
  "method": "prompts/get",
  "params": {
    "name": "prompt_name",
    "arguments": { ... }
  }
}

Response:
{
  "result": {
    "messages": [
      {
        "role": "user",
        "content": {
          "type": "text",
          "text": "Generated prompt"
        }
      }
    ]
  }
}

MCP in Goose

Goose uses the rmcp crate for MCP implementation.

Server Implementation

Implement the ServerHandler trait:
use rmcp::{ServerHandler, model::*};
use async_trait::async_trait;

struct MyMCPServer;

#[async_trait]
impl ServerHandler for MyMCPServer {
    async fn list_tools(
        &self,
        _params: ListToolsRequestParams,
    ) -> rmcp::Result<ListToolsResult> {
        Ok(ListToolsResult {
            tools: vec![
                Tool {
                    name: "example_tool".into(),
                    description: Some("Example tool".to_string()),
                    input_schema: serde_json::json!({
                        "type": "object",
                        "properties": {
                            "input": {"type": "string"}
                        },
                        "required": ["input"]
                    }),
                }
            ],
        })
    }

    async fn call_tool(
        &self,
        params: CallToolRequestParams,
    ) -> rmcp::Result<CallToolResult> {
        match params.name.as_str() {
            "example_tool" => {
                // Extract arguments
                let input = params.arguments
                    .get("input")
                    .and_then(|v| v.as_str())
                    .ok_or_else(|| rmcp::Error::invalid_params(
                        "Missing input parameter"
                    ))?;

                // Perform operation
                let result = process(input);

                // Return result
                Ok(CallToolResult {
                    content: vec![ToolResponseContent::text(result)],
                    is_error: Some(false),
                })
            }
            _ => Err(rmcp::Error::method_not_found(
                format!("Unknown tool: {}", params.name)
            )),
        }
    }
}

Serving the Extension

Stdio transport:
use rmcp::ServiceExt;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let server = MyMCPServer;
    let stdin = tokio::io::stdin();
    let stdout = tokio::io::stdout();
    
    server.serve((stdin, stdout)).await?.waiting().await;
    Ok(())
}
In-process (for builtin extensions):
fn spawn_and_serve<S>(
    server: S,
    transport: (tokio::io::DuplexStream, tokio::io::DuplexStream),
) where
    S: ServerHandler + Send + 'static,
{
    tokio::spawn(async move {
        match server.serve(transport).await {
            Ok(running) => {
                let _ = running.waiting().await;
            }
            Err(e) => tracing::error!("Server error: {}", e),
        }
    });
}

Extension Manager

Goose’s ExtensionManager handles MCP communication:
use goose::agents::extension_manager::ExtensionManager;

// Create manager with extensions
let manager = ExtensionManager::new(
    vec![extension_config],
    cancellation_token,
).await?;

// List all tools from all extensions
let tools = manager.list_tools().await?;

// Call a tool
let result = manager.call_tool(
    "tool_name",
    tool_params,
).await?;

Testing MCP Extensions

MCP Inspector

Use the official MCP Inspector to test extensions:
npx @modelcontextprotocol/inspector cargo run -p goose-mcp --example mcp
For external extensions:
npx @modelcontextprotocol/inspector python my_extension.py

Integration Tests

Record and replay MCP interactions (crates/goose/tests/mcp_integration_test.rs):
#[tokio::test]
async fn test_mcp_extension() {
    let config = ExtensionConfig::builtin("memory");
    let manager = ExtensionManager::new(
        vec![config],
        CancellationToken::new(),
    ).await.unwrap();

    let tools = manager.list_tools().await.unwrap();
    assert!(!tools.is_empty());
}
Record interactions:
GOOSE_RECORD_MCP=1 cargo test --package goose --test mcp_integration_test

Error Handling

Standard Error Codes

  • -32700 - Parse error
  • -32600 - Invalid request
  • -32601 - Method not found
  • -32602 - Invalid params
  • -32603 - Internal error

Custom Errors

use rmcp::Error;

// Invalid parameters
Err(Error::invalid_params("Missing required field"))

// Method not found
Err(Error::method_not_found("Unknown tool"))

// Internal error
Err(Error::internal_error("Database connection failed"))

Transport Layer

MCP supports multiple transports:

Stdio (Standard)

Communication over stdin/stdout:
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
server.serve((stdin, stdout)).await?

Duplex Stream (Builtin Extensions)

In-process communication:
let (client_read, server_write) = tokio::io::duplex(8192);
let (server_read, client_write) = tokio::io::duplex(8192);

server.serve((server_read, server_write)).await?

Best Practices

1. Input Validation

Validate all inputs using JSON Schema:
input_schema: serde_json::json!({
    "type": "object",
    "properties": {
        "path": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9/_-]+$"
        }
    },
    "required": ["path"]
})

2. Clear Descriptions

Provide detailed descriptions:
Tool {
    name: "read_file".into(),
    description: Some(
        "Read contents of a file. Returns the file contents as text. \
        Fails if file doesn't exist or is not readable.".to_string()
    ),
    // ...
}

3. Error Reporting

Return actionable error messages:
if !path.exists() {
    return Ok(CallToolResult {
        content: vec![ToolResponseContent::text(
            format!("File not found: {}", path.display())
        )],
        is_error: Some(true),
    });
}

4. Timeouts

Implement timeouts for long operations:
tokio::time::timeout(
    Duration::from_secs(30),
    perform_operation(),
).await??

MCP Resources

Next Steps

Build docs developers (and LLMs) love