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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions