Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,29 @@ let service = common::counter::Counter::new();
<summary>Start the server</summary>

```rust, ignore
// this call will finish the initialization process
// Await protocol startup, then receive a handle to the running service.
let server = service.serve(transport).await?;
```

For a server, `serve()` waits for peer input that establishes the protocol
lifecycle, such as `initialize` from a legacy client. A stdio MCP server is
normally launched by an MCP client as a child process. Running the executable
alone in a terminal can leave this await pending because no client has sent
protocol messages yet.

After `serve()` returns, the service handles messages in the background. You can
run other asynchronous work, then use `waiting()` to wait for shutdown. To cancel
during startup, use `serve_with_ct()` with a `CancellationToken`. Tokio's stdin
uses a blocking read that cannot be cancelled, so runtime shutdown may still wait
for input or EOF; see [Tokio's stdin documentation](https://docs.rs/tokio/latest/tokio/io/fn.stdin.html).

For example, build the counter server from the repository root, then let the
MCP Inspector CLI launch it and list its tools:

```sh
cargo build -p mcp-server-examples --example servers_counter_stdio
npx @modelcontextprotocol/inspector --cli ./target/debug/examples/servers_counter_stdio --method tools/list
```
</details>

<details>
Expand Down
20 changes: 20 additions & 0 deletions crates/rmcp/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,17 @@ pub trait ServiceExt<R: ServiceRole>: Service<R> + Sized {
fn into_dyn(self) -> Box<dyn DynService<R>> {
Box::new(self)
}
/// Initialize the protocol over `transport` and return a running service.
///
/// On a server, this waits for peer input that establishes a protocol
/// lifecycle, such as a legacy `initialize` request. A stdio server normally
/// runs as a child process of an MCP client; starting it in a terminal with
/// no protocol input leaves this future pending.
///
/// Once this returns, the service handles messages in the background and
/// other asynchronous work can run. Use [`RunningService::waiting`] to wait
/// for shutdown or [`RunningService::cancel`] to stop the running service.
/// Use [`Self::serve_with_ct`] to supply a cancellation token during startup.
fn serve<T, E, A>(
self,
transport: T,
Expand All @@ -332,6 +343,15 @@ pub trait ServiceExt<R: ServiceRole>: Service<R> + Sized {
{
Self::serve_with_ct(self, transport, Default::default())
}
/// Initialize and run the service with the supplied cancellation token.
///
/// Like [`Self::serve`], this waits for protocol initialization before
/// returning a [`RunningService`]. Cancelling `ct` interrupts initialization
/// or signals shutdown of the running service.
///
/// Transport shutdown can have additional constraints: Tokio's standard
/// input uses a blocking read that cancellation cannot interrupt. See
/// <https://docs.rs/tokio/latest/tokio/io/fn.stdin.html>.
fn serve_with_ct<T, E, A>(
self,
transport: T,
Expand Down
4 changes: 4 additions & 0 deletions crates/rmcp/src/transport/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
/// Every read from and write to these handles is executed on Tokio's blocking
/// thread pool. When both standard streams are pipes, prefer [`stdio_pipes`],
/// which avoids the per-operation thread hand-off on Linux.
///
/// A pending read on [`tokio::io::Stdin`] cannot be cancelled. Cancelling an MCP
/// service can therefore still leave Tokio runtime shutdown waiting for stdin
/// input or EOF. See the [Tokio stdin documentation](https://docs.rs/tokio/latest/tokio/io/fn.stdin.html).
pub fn stdio() -> (tokio::io::Stdin, tokio::io::Stdout) {
(tokio::io::stdin(), tokio::io::stdout())
}
Expand Down
12 changes: 9 additions & 3 deletions examples/servers/src/counter_stdio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ use common::counter::Counter;
use rmcp::{ServiceExt, transport::stdio};
use tracing_subscriber::{self, EnvFilter};
mod common;
/// npx @modelcontextprotocol/inspector cargo run -p mcp-server-examples --example std_io
/// From the repository root:
/// ```sh
/// cargo build -p mcp-server-examples --example servers_counter_stdio
/// npx @modelcontextprotocol/inspector --cli ./target/debug/examples/servers_counter_stdio --method tools/list
/// ```
#[tokio::main]
async fn main() -> Result<()> {
// Initialize the tracing subscriber with file and stdout logging
// Write logs to stderr; stdout carries MCP protocol messages.
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::DEBUG.into()))
.with_writer(std::io::stderr)
Expand All @@ -15,11 +19,13 @@ async fn main() -> Result<()> {

tracing::info!("Starting MCP server");

// Create an instance of our counter router
// The MCP client launches this process and writes protocol messages to stdin.
// This await waits for startup input (e.g. initialize for a legacy client).
let service = Counter::new().serve(stdio()).await.inspect_err(|e| {
tracing::error!("serving error: {:?}", e);
})?;

// Wait for the running service to stop after startup has completed.
service.waiting().await?;
Ok(())
}
Loading