## Summary An `impl` annotated `#[tool_router]` that contains no **directly visible** `#[tool]` function compiles with no diagnostics and produces a router with **zero** tools. Nothing warns; the server simply advertises an empty catalogue, and the first thing to notice is a client at runtime. The easiest way to reach that state is a `macro_rules!` helper inside the impl, which is a natural thing to reach for on a server with many similarly-shaped tools. Moving the same helper one level up — so it emits the whole `#[tool_router] impl` — works correctly, which makes the failing form look like a near-miss rather than a category error. ## Version rmcp 3.1.2, rustc 1.96.1, edition 2024. ## Reproduction ```rust use rmcp::handler::server::router::tool::ToolRouter; use rmcp::tool; pub struct Probe { tool_router: ToolRouter<Probe>, } macro_rules! a_capability { (broadcast = $broadcast:literal) => { #[tool(description = $broadcast)] async fn probe(&self) -> String { "probed".to_owned() } }; } #[rmcp::tool_router] impl Probe { a_capability!(broadcast = "what a capability would own"); } #[test] fn it_registers() { // observed: 0 assert_eq!(Probe::tool_router().list_all().len(), 1); } ``` Compiles with no errors and no warnings. `list_all()` returns an empty vector. The same helper hoisted one level up registers correctly, description intact: ```rust macro_rules! a_whole_surface { ($($name:ident => $broadcast:literal),* $(,)?) => { #[rmcp::tool_router] impl WrappedImpl { $( #[tool(description = $broadcast)] async fn $name(&self) -> String { stringify!($name).to_owned() } )* } }; } a_whole_surface!(probe => "what a capability would own"); // registers 1 tool ``` ## Why it happens This is a Rust expansion-order property rather than an rmcp defect. An attribute proc-macro receives the annotated item's **unexpanded** token stream, so `#[tool_router]` sees the `a_capability!(…)` invocation and not the function it becomes. There is no fix available inside the macro for the expansion itself, and the hoisted form works precisely because an item-level `macro_rules!` expands before the attribute inside its output runs. ## Why it is still worth a diagnostic The consequence is specific to this crate rather than generic: an MCP server whose catalogue is empty does nothing at all, and it does nothing *quietly*. There is no error to grep for and no failing test unless somebody thought to assert the router is non-empty. The symptom surfaces on the other side of a connection, in a model that simply finds no tools. An `impl` annotated `#[tool_router]` that yields zero tools is almost always a mistake — collecting tools is the attribute's entire purpose. In preference order: 1. **A compile error**, whose note mentions that `macro_rules!` inside the impl is not expanded before this attribute runs and that wrapping the whole `impl` works. An escape hatch (`#[tool_router(allow_empty)]`) covers anyone who genuinely wants an empty router. 2. A warn-level diagnostic saying the same. 3. Failing both, a sentence in the `#[tool_router]` documentation. The current page describes what the attribute collects, not what happens when it collects nothing. ## What this report does not establish It does not claim the expansion order should change — that is a language property. It also does not cover `#[tool_handler]` or the `#[tool_router(server_handler)]` single-block form; only the two-attribute form was measured.