This is the Rust companion to Build an MCP Server in Go with the Official SDK
and Build an MCP Server in TypeScript with the Official SDK. It builds the same
kind of server — two tools and a resource — with the official Rust SDK
(rmcp), served over the stdio transport so a local client launches it as a
subprocess. The result is a single compiled binary with no runtime beyond
itself.
The Rust SDK is macro-driven and async. You annotate methods with #[tool]
inside a #[tool_router] impl block; the macro reads each method’s argument
struct, derives the JSON Schema from it with schemars, and routes incoming
calls to the method with the arguments already decoded and typed. A second macro,
#[tool_handler], wires that router into the ServerHandler trait. The whole
thing runs on tokio.
What you will build
- A
GreeterServerwith two tools (add,greet) and a resource (info://server). - Pure tool logic in its own module, unit-tested without the SDK.
- A stdio entry point compiled to
target/release/macmcp. - A smoke binary that launches the built server and speaks MCP to it, plus a
cargo testsuite that drives the server in-process over an in-memory transport. - Registration with Claude Code.
Prerequisites
- A recent stable Rust toolchain with
cargo(install via rustup). This tutorial was validated on Rust 1.97. make(pre-installed on macOS and most Linux distributions).- Familiarity with MCP concepts (tools, resources, transports).
- macOS commands are shown, but the project is cross-platform.
Step 1: Scaffold the project
The server is a normal Cargo binary crate. We will split it into a library plus a thin binary so the tests can import the real server type and drive it in-process.
Create the files
cargo new mcp-server-rust
cd mcp-server-rust
cargo new creates Cargo.toml, src/main.rs, and a .gitignore. When run
inside an existing Git repository it skips Git initialization; when run standalone
it also initializes a repo. Replace the generated .gitignore with entries for
this stack:
Add the code: .gitignore
# Rust
/target
# OS / editor noise
.DS_Store
*.log
Detailed breakdown
/targetexcludes the Cargo build directory: compiled binaries, intermediate artifacts, and the dependency cache. It is rebuilt on demand, so it never belongs in version control..DS_Storeand*.logcover common macOS and runtime noise.
Step 2: Declare dependencies
Replace the generated Cargo.toml with the manifest below. It pulls in rmcp
with the feature flags this project uses, plus the async runtime and the
serialization crates the macros expand into.
Add the code: Cargo.toml
[package]
name = "macmcp"
version = "0.1.0"
edition = "2021"
[dependencies]
rmcp = { version = "2.2.0", features = [
"server",
"client",
"macros",
"transport-io",
"transport-child-process",
] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std", "sync"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
Detailed breakdown
rmcpis the official SDK (repositorygithub.com/modelcontextprotocol/rust-sdk). The features select exactly what is compiled:serverandmacrosprovideServerHandlerplus the#[tool],#[tool_router], and#[tool_handler]macros.macrosalso brings inschemars, which turns an argument struct into a JSON Schema.transport-ioprovidesstdio(), the transport the entry point serves on.clientandtransport-child-processare only needed by the smoke binary and the tests (rmcp::ServiceExt::serveon the client side,TokioChildProcessto spawn the server). A server-only crate can drop both.
tokiois the async runtime.rt-multi-threadandmacrosenable#[tokio::main];io-stdbacks the stdio transport;syncis a common companion for shared state (unused here but conventional).serde(withderive) andserde_jsonback the argument structs and the resource JSON. The package name ismacmcp, which becomes the binary name and the crate path inuse macmcp::....
Step 3: The tool logic
Keep the actual work in its own module, free of any SDK types, so it unit-tests directly.
Create the file
touch src/logic.rs
Add the code: src/logic.rs
//! Pure logic behind the MCP tools, with no dependency on the MCP SDK. Keeping
//! the work here means it unit-tests without a server, a transport, or a client.
/// Return the sum of two integers.
pub fn add(a: i64, b: i64) -> i64 {
a + b
}
/// Build a greeting for `name`, optionally shouting it in upper case.
pub fn greet(name: &str, shout: bool) -> String {
let msg = format!("Hello, {name}!");
if shout {
msg.to_uppercase()
} else {
msg
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_sums() {
assert_eq!(add(2, 3), 5);
assert_eq!(add(-1, 1), 0);
}
#[test]
fn greet_plain() {
assert_eq!(greet("Ada", false), "Hello, Ada!");
}
#[test]
fn greet_shout() {
assert_eq!(greet("Ada", true), "HELLO, ADA!");
}
}
Detailed breakdown
- Plain functions, no MCP imports. The server module adapts them to the protocol; the tests call them directly.
- The
#[cfg(test)]module holds unit tests compiled only undercargo test.use super::*imports the parent module so the tests reachaddandgreetwithout a full path. This is the standard Rust pattern for co-locating tests with the code they exercise.
Step 4: The library crate root
src/lib.rs turns the crate into a library that both the binary and the tests
can import. It only needs to declare the modules.
Create the file
touch src/lib.rs
Add the code: src/lib.rs
//! Library crate for the MCP server. Splitting the crate into a library plus a
//! thin binary lets the integration tests in `tests/` import the real server
//! type and drive it in-process, while `src/main.rs` only wires it to stdio.
pub mod logic;
pub mod server;
Detailed breakdown
- A Cargo package can hold both a library (
src/lib.rs) and a binary (src/main.rs) at once. The library carries the reusable code; the binary is a thinmain. pub mod logic;andpub mod server;expose the two modules under the crate namemacmcp, sosrc/main.rs,src/bin/smoke.rs, and the integration tests all reach them asmacmcp::logicandmacmcp::server. Integration tests undertests/compile as separate crates and can only see a package’s public library API, which is why the server type has to live here rather than inmain.rs.
Step 5: Construct the server
This is the core of the SDK usage. The GreeterServer type carries a
ToolRouter; the #[tool_router] macro builds that router from the
#[tool]-annotated methods, and #[tool_handler] connects it to the
ServerHandler trait. The resource is served by hand-implementing
list_resources and read_resource.
Create the file
touch src/server.rs
Add the code: src/server.rs
//! The MCP server: it registers two tools (`add`, `greet`) and one resource
//! (`info://server`) and returns the handler type. Connecting it to a transport
//! happens in the caller — `src/main.rs` runs it on stdio, and the tests attach
//! an in-memory transport to the same type.
use rmcp::{
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
model::*,
schemars,
service::RequestContext,
tool, tool_handler, tool_router,
ErrorData as McpError, RoleServer, ServerHandler,
};
use serde_json::json;
use crate::logic;
/// Advertised server identity, also returned by the `info://server` resource.
pub const NAME: &str = "mcp-rust";
pub const VERSION: &str = "0.1.0";
pub const INFO_URI: &str = "info://server";
/// `AddRequest` and `GreetRequest` describe each tool's input. The derives drive
/// both JSON decoding (`serde::Deserialize`) and the JSON Schema the SDK
/// advertises to clients (`schemars::JsonSchema`); doc comments become field
/// descriptions in that schema.
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct AddRequest {
/// the first addend
pub a: i64,
/// the second addend
pub b: i64,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct GreetRequest {
/// who to greet
pub name: String,
/// upper-case the greeting
#[serde(default)]
pub shout: bool,
}
/// The server handler. The `#[tool_router]` macro fills the `tool_router` field
/// with a router built from the `#[tool]`-annotated methods below.
#[derive(Clone)]
pub struct GreeterServer {
// Read by the `#[tool_handler]`-generated dispatch code; the dead-code lint
// misses that use because the struct also derives Clone, so silence it.
#[allow(dead_code)]
tool_router: ToolRouter<Self>,
}
#[tool_router]
impl GreeterServer {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
#[tool(description = "Add two integers and return the sum.")]
fn add(&self, Parameters(AddRequest { a, b }): Parameters<AddRequest>) -> String {
logic::add(a, b).to_string()
}
#[tool(description = "Return a greeting for a name.")]
fn greet(
&self,
Parameters(GreetRequest { name, shout }): Parameters<GreetRequest>,
) -> String {
logic::greet(&name, shout)
}
}
impl Default for GreeterServer {
fn default() -> Self {
Self::new()
}
}
#[tool_handler]
impl ServerHandler for GreeterServer {
fn get_info(&self) -> ServerInfo {
// Implementation is #[non_exhaustive], so start from the build-env
// defaults and override the identity fields.
let mut identity = Implementation::from_build_env();
identity.name = NAME.into();
identity.version = VERSION.into();
ServerInfo::new(
ServerCapabilities::builder()
.enable_tools()
.enable_resources()
.build(),
)
.with_server_info(identity)
.with_instructions(
"A minimal MCP server with two tools (add, greet) and a \
server-info resource."
.to_string(),
)
}
async fn list_resources(
&self,
_request: Option<PaginatedRequestParams>,
_: RequestContext<RoleServer>,
) -> Result<ListResourcesResult, McpError> {
Ok(ListResourcesResult {
resources: vec![Resource::new(INFO_URI, "server-info".to_string())],
next_cursor: None,
meta: None,
})
}
async fn read_resource(
&self,
request: ReadResourceRequestParams,
_: RequestContext<RoleServer>,
) -> Result<ReadResourceResult, McpError> {
match request.uri.as_str() {
INFO_URI => {
let body = json!({ "name": NAME, "version": VERSION }).to_string();
Ok(ReadResourceResult::new(vec![ResourceContents::text(
body,
request.uri.clone(),
)]))
}
_ => Err(McpError::resource_not_found(
"resource_not_found",
Some(json!({ "uri": request.uri })),
)),
}
}
}
Detailed breakdown
- The argument structs derive
serde::Deserializeandschemars::JsonSchema.serdedecodes the incoming JSON arguments;schemarsgenerates the input schema the client sees intools/list. A///doc comment on a field becomes that field’sdescriptionin the schema.#[serde(default)]onshoutmakes it optional: omit it and it defaults tofalse. GreeterServerholds aToolRouter<Self>field namedtool_router. The#[tool_router]macro generates an associatedSelf::tool_router()that builds the router from every#[tool]method in the block, andnew()stores it in that field. The field is read by the generated dispatch code, but the dead-code lint misses that use once the struct derivesClone, so#[allow(dead_code)]silences a false positive.#[tool(description = "...")]registers a method as a tool. The argument is destructured out of theParameters<T>wrapper:Parameters(AddRequest { a, b })binds the decoded fields directly. ReturningStringis the shortcut for a single text result; the macro wraps it in aCallToolResultwith one text block. (For richer results, such as multiple blocks, images, or an error, returnResult<CallToolResult, McpError>and build the result explicitly.)#[tool_handler]onimpl ServerHandlergenerates thecall_toolandlist_toolsmethods from thetool_routerfield, so you only writeget_infoand the resource methods.get_infoadvertises capabilities.ServerCapabilities::builder() .enable_tools().enable_resources()tells clients this server has both.Implementationis#[non_exhaustive], so you cannot build it with a struct literal; start fromImplementation::from_build_env()and overridenameandversion.list_resourcesandread_resourceare hand-written, because tools have macros but resources do not.list_resourcesreturns oneResourceforinfo://server;read_resourcematches on the requested URI, returns the server identity as JSON for the known URI, and returnsMcpError::resource_not_foundfor anything else so an unknown URI is a clean protocol error rather than a panic.New/Defaultreturn the server unconnected, which is what lets Step 8’s tests attach an in-memory transport instead of stdio.
Step 6: The stdio entry point
Create the file
The src/main.rs already exists from cargo new. Replace its contents.
Add the code: src/main.rs
//! Runs the MCP server over the stdio transport — the transport a local client
//! (Claude Desktop, Claude Code) launches as a subprocess. The client speaks
//! JSON-RPC over stdin/stdout, so all logging goes to stderr; the SDK owns
//! stdout.
use anyhow::Result;
use macmcp::server::GreeterServer;
use rmcp::{transport::stdio, ServiceExt};
#[tokio::main]
async fn main() -> Result<()> {
// Send tracing to stderr. stdout is the protocol channel; a stray write to
// it corrupts the JSON-RPC stream and disconnects the client.
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_ansi(false)
.init();
let service = GreeterServer::new().serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
Detailed breakdown
GreeterServer::new().serve(stdio())starts the server on the stdio transport.servecomes from theServiceExtextension trait and returns a running service handle;stdio()pairstokio’s stdin and stdout as the transport.service.waiting().awaitblocks until the client disconnects (or the process is signalled), keeping the server alive to answer requests.- Logging goes to stderr. The
tracing_subscriberwriter is set tostd::io::stderr, because stdout is the protocol channel and a stray write to it corrupts the JSON-RPC stream and drops the client. Neverprintln!from a stdio server — usetracing/eprintln!(stderr) for diagnostics.
Step 7: Build and smoke-test
Compile the release binary:
cargo build --release
Then launch it the way a client does and call its tools. The smoke binary lives
under src/bin/, so Cargo builds it as a second binary named smoke.
Create the file
mkdir -p src/bin
touch src/bin/smoke.rs
Add the code: src/bin/smoke.rs
//! Launches the built server over stdio and speaks MCP to it, the way a real
//! client does. A green run proves the exact command you register with a client
//! works. Build first (`make build`), then run from the project root so the
//! relative binary path resolves.
use anyhow::Result;
use rmcp::{
model::{CallToolRequestParams, CallToolResult, ReadResourceRequestParams},
object,
transport::TokioChildProcess,
ServiceExt,
};
use tokio::process::Command;
const SERVER_BIN: &str = "target/release/macmcp";
#[tokio::main]
async fn main() -> Result<()> {
// The unit type `()` is the default client handler. `TokioChildProcess`
// spawns the built binary and connects over its stdin/stdout.
let client = ()
.serve(TokioChildProcess::new(Command::new(SERVER_BIN))?)
.await?;
for tool in client.list_all_tools().await? {
println!("tool: {}", tool.name);
}
let sum = client
.call_tool(CallToolRequestParams::new("add").with_arguments(object!({ "a": 2, "b": 3 })))
.await?;
println!("add -> {}", first_text(&sum));
let hi = client
.call_tool(
CallToolRequestParams::new("greet")
.with_arguments(object!({ "name": "Ada", "shout": true })),
)
.await?;
println!("greet -> {}", first_text(&hi));
let res = client
.read_resource(ReadResourceRequestParams::new("info://server"))
.await?;
if let Some(rmcp::model::ResourceContents::TextResourceContents { text, .. }) =
res.contents.first()
{
println!("resource -> {text}");
}
client.cancel().await?;
Ok(())
}
/// Pull the first text block out of a tool result.
fn first_text(result: &CallToolResult) -> String {
result
.content
.iter()
.find_map(|block| block.as_text().map(|t| t.text.clone()))
.unwrap_or_default()
}
Run it from the project root:
cargo run --quiet --bin smoke
tool: add
tool: greet
add -> 5
greet -> HELLO, ADA!
resource -> {"name":"mcp-rust","version":"0.1.0"}
Detailed breakdown
().serve(TokioChildProcess::new(Command::new(SERVER_BIN))?)is the client half of stdio. The unit type()is the SDK’s default client handler;TokioChildProcessspawns the built binary and connects over its stdin/stdout, exactly as a real client would.client.call_tool(CallToolRequestParams::new("add").with_arguments(...))sends the tool name and a JSON object of arguments;object!({ ... })is the SDK’s JSON-object macro.read_resourcefetches the resource by URI.first_textwalks the result’s content blocks and returns the first text one.block.as_text()returnsNonefor non-text blocks (images, embedded resources), so the helper degrades to an empty string rather than panicking.SERVER_BINis a relative path, so run the smoke binary from the project root aftercargo build --release. The output confirms the whole path end to end, using the exact launch command you will register with a client.
Step 8: Tests
Unit tests already live beside the logic (Step 3). Add an integration test that
drives the real server in-process over an in-memory transport — no subprocess, no
stdio. Files under tests/ compile as separate crates that see only the public
library API.
Create the file
mkdir -p tests
touch tests/server.rs
Add the code: tests/server.rs
//! Drive the real server in-process over an in-memory transport pair — no
//! subprocess, no stdio. `tokio::io::duplex` returns two linked byte streams;
//! the server serves one end and the client connects to the other, all in one
//! process, which is faster and more deterministic than spawning a binary.
use anyhow::Result;
use macmcp::server::GreeterServer;
use rmcp::{
model::{CallToolRequestParams, ReadResourceRequestParams, ResourceContents},
object,
service::{RoleClient, RunningService},
ServiceExt,
};
/// Wire a client to the real server in-process and return the running client.
async fn connect() -> Result<RunningService<RoleClient, ()>> {
let (server_transport, client_transport) = tokio::io::duplex(4096);
tokio::spawn(async move {
let service = GreeterServer::new().serve(server_transport).await?;
service.waiting().await?;
anyhow::Ok(())
});
let client = ().serve(client_transport).await?;
Ok(client)
}
fn first_text(result: &rmcp::model::CallToolResult) -> String {
result
.content
.iter()
.find_map(|block| block.as_text().map(|t| t.text.clone()))
.unwrap_or_default()
}
#[tokio::test]
async fn lists_both_tools() -> Result<()> {
let client = connect().await?;
let names: Vec<String> = client
.list_all_tools()
.await?
.into_iter()
.map(|t| t.name.into_owned())
.collect();
assert!(names.contains(&"add".to_string()), "missing add: {names:?}");
assert!(names.contains(&"greet".to_string()), "missing greet: {names:?}");
client.cancel().await?;
Ok(())
}
#[tokio::test]
async fn calls_add() -> Result<()> {
let client = connect().await?;
let result = client
.call_tool(CallToolRequestParams::new("add").with_arguments(object!({ "a": 40, "b": 2 })))
.await?;
assert_eq!(first_text(&result), "42");
client.cancel().await?;
Ok(())
}
#[tokio::test]
async fn calls_greet_with_shout() -> Result<()> {
let client = connect().await?;
let result = client
.call_tool(
CallToolRequestParams::new("greet")
.with_arguments(object!({ "name": "Ada", "shout": true })),
)
.await?;
assert_eq!(first_text(&result), "HELLO, ADA!");
client.cancel().await?;
Ok(())
}
#[tokio::test]
async fn reads_info_resource() -> Result<()> {
let client = connect().await?;
let result = client
.read_resource(ReadResourceRequestParams::new("info://server"))
.await?;
let ResourceContents::TextResourceContents { text, .. } = result
.contents
.first()
.expect("resource has contents")
else {
panic!("expected text resource contents");
};
assert_eq!(text, r#"{"name":"mcp-rust","version":"0.1.0"}"#);
client.cancel().await?;
Ok(())
}
Run everything:
cargo test
running 3 tests
test logic::tests::add_sums ... ok
test logic::tests::greet_shout ... ok
test logic::tests::greet_plain ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
...
running 4 tests
test reads_info_resource ... ok
test lists_both_tools ... ok
test calls_greet_with_shout ... ok
test calls_add ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
Detailed breakdown
tokio::io::duplex(4096)returns a linked pair of in-memory byte streams — the in-process equivalent of a stdio pipe. The server serves one end on a spawned task; the client connects to the other. Everything runs in one process, faster and more deterministic than spawning the binary.().serve(client_transport)builds the client, andconnect()returns the running handle typedRunningService<RoleClient, ()>. Each test lists tools, calls a tool, or reads the resource, thenclient.cancel()shuts the session down.calls_addasserts40 + 2 = 42andreads_info_resourceasserts the exact JSON{"name":"mcp-rust","version":"0.1.0"}.serde_jsonorders object keys alphabetically, sonameprecedesversiondeterministically.- Tool names arrive as
Cow<str>, hencet.name.into_owned()to compare against ownedStrings. The unit tests and the four integration tests run under onecargo testinvocation.
Step 9: The Makefile
Wrap the commands so plain make prints help.
Create the file
touch Makefile
Add the code: Makefile
.DEFAULT_GOAL := help
BINARY := target/release/macmcp
DIR := $(shell pwd)
.PHONY: help build test smoke run register-code unregister-code clean
help: ## Show this help screen
@grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*## "}; {printf " %-16s %s\n", $$1, $$2}'
build: ## Build the release binary
cargo build --release
test: ## Run all tests
cargo test
smoke: build ## Build, then launch the server over stdio and call its tools
cargo run --quiet --bin smoke
run: build ## Run the server on stdio (Ctrl-C to stop)
./$(BINARY)
register-code: build ## Register the built server with Claude Code (local scope)
claude mcp add mcp-rust -- $(DIR)/$(BINARY)
unregister-code: ## Remove the server from Claude Code
-claude mcp remove mcp-rust
clean: ## Remove build artifacts
cargo clean
Verify the default target prints help:
make
help Show this help screen
build Build the release binary
test Run all tests
smoke Build, then launch the server over stdio and call its tools
run Run the server on stdio (Ctrl-C to stop)
register-code Register the built server with Claude Code (local scope)
unregister-code Remove the server from Claude Code
clean Remove build artifacts
Detailed breakdown
.DEFAULT_GOAL := helpmakes plainmakeprint the help screen, which is built bygrep/awkscraping the##comments off each target.register-codedepends onbuild, so the binary a client points at is current, and it uses an absolute path ($(DIR)) because a client may launch the server from any directory.- Makefiles require tab indentation, not spaces. If a recipe line is
space-indented,
makereportsmissing separator.
Step 10: Register with Claude Code
The server is a compiled stdio program, so register it by its binary path (see
Register a FastMCP Server with Claude Desktop and Claude Code for scopes and the
Claude Desktop config). From the project directory, after cargo build --release:
claude mcp add mcp-rust -- "$(pwd)/target/release/macmcp"
claude mcp get mcp-rust
mcp-rust:
Scope: Local config (private to you in this project)
Status: ✔ Connected
Type: stdio
Command: /Users/you/mcp-server-rust/target/release/macmcp
✔ Connected means Claude Code launched the binary and completed the MCP
handshake. Start a session and ask it to use mcp-rust to add 2 and 3. Remove it
with claude mcp remove mcp-rust.
Troubleshooting
cargo buildfails resolvingrmcpfeatures. The feature set matters:stdio()needstransport-io, and the smoke binary needsclientplustransport-child-process. A server-only build can trim to["server", "macros", "transport-io"].error: cannot create non-exhaustive struct.Implementationis#[non_exhaustive]; build it fromImplementation::from_build_env()and override fields, rather than with a struct literal.- The client connects but sees no tools, or disconnects at once. Something
wrote to stdout. Point
tracing_subscriberatstd::io::stderrand neverprintln!from the server — stdout is the protocol channel. #[tool]or#[tool_router]does not compile. Every#[tool]method must live inside the#[tool_router]impl block, and the struct must hold atool_router: ToolRouter<Self>field thatnew()fills withSelf::tool_router().- Arguments arrive missing or mistyped. The field names in the request struct
must match the JSON keys the client sends (
a,b,name,shouthere).#[serde(default)]makes a field optional. claude mcp getshowsFailed to connect. Run./target/release/macmcpdirectly — it should start and wait on stdin. If it exits immediately, rebuild withcargo build --release; the binary may be missing or stale.
Recap
- The official Rust SDK builds an MCP server from a
#[tool_router]impl of#[tool]methods, wired intoServerHandlerby#[tool_handler]. Argument structs deriveserde::Deserialize+schemars::JsonSchema, so inputs arrive decoded and the schema is generated for you. - Resources have no macro: implement
list_resourcesandread_resourceon the handler by hand, matching on the requested URI. GreeterServer::new().serve(stdio())serves it as a subprocess; stdout is the protocol, so logging goes to stderr.tokio::io::duplexlinks a client to the server in-process for fast tests, alongside aTokioChildProcesssmoke binary over real stdio.- The compiled binary registers with any MCP client by its path.
Next improvements
- Return structured output by returning a type that derives
serde::Serialize+schemars::JsonSchema, so clients get a typedstructuredContentpayload alongside the text. - Add the Streamable HTTP transport for remote clients (enable the
transport-streamable-http-serverfeature), keeping stdio for local use. - Add prompts with the
#[prompt_router]/#[prompt]macros, exposing reusable templates next to the tools. - Cross-compile and distribute the binary for other targets (
rustup target add ...,cargo build --release --target ...) so users install a single file with no toolchain.