Skip to content

Feature request: Support for !Send futures in #[tool] handlers (e.g. #[tool(local)]) #728

@Stranmor

Description

@Stranmor

Problem

The #[tool] macro currently requires all tool handler futures to be Send. This Send bound propagates virally through the entire async call chain — not just the handler itself, but every function it awaits transitively.

This creates a fundamental incompatibility with popular async libraries that return !Send futures:

  • sqlx::raw_sql() — returns a future holding PhantomData<fn(&mut PgConnection)> which is !Send. Cannot be used in any function that's transitively called from a #[tool] handler.
  • sqlx transactions — transaction futures borrow &mut PgConnection across await points, making them !Send in many patterns.
  • Other database libraries with similar patterns.

Concrete Example

// This works (sqlx::query returns Send future):
#[tool]
async fn my_tool(&self) -> Result<String> {
    sqlx::query("SELECT 1").execute(&self.pool).await?;
    Ok("done".into())
}

// This fails to compile (sqlx::raw_sql returns !Send future):
#[tool]  
async fn my_tool(&self) -> Result<String> {
    sqlx::raw_sql("CREATE TABLE IF NOT EXISTS ...").execute(&self.pool).await?;
    Ok("done".into())
}

The error propagates even through indirect calls:

error[E0277]: `(dyn Any + Send + 'static)` cannot be sent between threads safely

Current Workaround

We split SQL strings by ; and execute each statement via sqlx::query() individually. This requires writing a custom SQL statement parser to handle semicolons inside string literals, comments, and dollar-quoted strings — significant complexity for what should be a simple migration runner.

Proposed Solution

Add a #[tool(local)] attribute (or similar) that allows handlers to return !Send futures. Implementation options:

  1. #[tool(local)] — use tokio::task::LocalSet for this specific handler
  2. #[tool(spawn_blocking)] — run the handler in spawn_blocking context
  3. Relax the Send bound globally — if the MCP server doesn't actually need to send handler futures across threads (e.g., if it uses a single-threaded runtime or LocalSet internally)

Environment

  • rmcp version: 1.1.0
  • sqlx version: 0.8+
  • Rust: stable (edition 2024)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions