Skip to content

docs(macros): document the Send limitation on async #[tool]/#[prompt] bodies - #1396

Open
gocamille wants to merge 2 commits into
mainfrom
docs/1092-tool-send-limitation
Open

gocamille wants to merge 2 commits into
mainfrom
docs/1092-tool-send-limitation

Conversation

@gocamille

Copy link
Copy Markdown
Contributor

Fixes #1092. The docs and tests are @IgorKasianenko's work (IgorKasianenko/rust-sdk@a408ab9), carried over with them as co-author.

Motivation and Context

#[tool] and #[prompt] turn an async fn into a Send boxed future. So a closure that borrows its item and returns an async move block, fed into .buffer_unordered(..) or .buffered(..), fails with "implementation of FnOnce is not general enough", and the error points at the macro. Igor showed in the issue that the same code fails under any Send bound with no macro involved, so this documents the limitation rather than changing the codegen.

  • #[tool] docs now describe the limitation, include the error text so it's searchable, and list the ways round it: .boxed(), owning the items, .then(..) if the futures don't need to run at the same time, or the local feature.
  • #[prompt] docs point to the same section, since it shares the codegen.

Two changes from Igor's commit (the clippy one is folded into the first commit):

  • The .then(..) bullet used to say to move the closure body into an async fn helper. .then(..) compiles with the inline closure too, so the helper isn't the fix. It does run the futures one at a time, so the bullet now says when that's a fair trade.
  • stream::iter(items.into_iter()) is now stream::iter(items) (clippy useless_conversion).

How Has This Been Tested?

  • test_async_tool_with_buffered_stream (test_tool_macros.rs): one tool fans out over borrowed items with .boxed(), another over owned items. Removing the .boxed() brings back the exact error, so the test pins the workaround.
  • test_async_prompt_with_buffered_stream (test_prompt_macros.rs): the same .boxed() shape under #[prompt].
  • I checked the .then(..) claim inside a real #[tool] on rustc 1.96 and 1.94: it compiles with both the helper and the inline closure, and a counting test showed at most 1 future in flight with .then(..) vs 4 with .buffer_unordered(4).

Validation:

  • cargo test -p rmcp --features 'server client macros schemars' --test test_tool_macros --test test_prompt_macros: passed (16 and 8).
  • cargo test -p rmcp --all-features: passed.
  • cargo clippy --all-targets --all-features -- -D warnings, clippy on the two test files without local (--features 'server client macros schemars'), RUSTDOCFLAGS=-Dwarnings cargo doc -p rmcp-macros --no-deps and cargo +nightly fmt --all -- --check: clean.

Breaking Changes

None.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Both test files start with #![cfg(not(feature = "local"))], so only the "Run Tests (no local feature)" job runs them. --all-features turns on local.

gocamille and others added 2 commits October 26, 2026 22:02
… bodies

An `async fn` under `#[tool]` or `#[prompt]` is rewritten into
`-> Pin<Box<dyn Future<Output = T> + Send + '_>>`, because the router keeps
every handler as a `Send` boxed future, so the whole call tree has to pass the
`Send` check. A closure that takes a reference and returns an `async move`
block, buffered by a `futures` combinator, fails that check with
"implementation of `FnOnce` is not general enough", and the span points at the
attribute macro.

The trigger is the `Send` bound, not the codegen: the same call tree under a
bare `F: Future + Send` bound, with no macro, no `Box::pin` and no trait
object, produces the identical error. That is rustc issues 104382 and 126551 in
rust-lang/rust, both open.

Document the limitation and the ways round it on `#[tool]`, cross-reference it
from `#[prompt]`, which shares the codegen and the same failure, and add
regression tests so the documented workarounds keep compiling.

Refs #1092

Co-authored-by: Igor Kasianenko <igor.kasianenko@outlook.com>
… a time

`.then(..)` compiles with the original inline closure too, so the `async fn`
helper isn't what fixes the error. It does run the futures one after another,
unlike `buffer_unordered(n)`, so the bullet now says when it's a fair trade.

Refs #1092
@gocamille
gocamille requested a review from a team as a code owner October 26, 2026 21:17
@github-actions github-actions Bot added T-test Testing related changes T-macros Macro changes labels Oct 26, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-26T21:18:09.740881Z f42c86d PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions
github-actions Bot requested a review from jamadeo October 26, 2026 21:17
@github-actions

Copy link
Copy Markdown
Contributor

@jamadeo is on triage rotation this week and is the reviewer for this one.

Labels come from the PR labeler. Please review the change.

Automated triage. Labels are a first pass and the assigned maintainer has the final say.

@gocamille gocamille assigned gocamille and unassigned gocamille Oct 26, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-macros Macro changes T-test Testing related changes

Projects

None yet

1 participant