From b8d02d1c77bd9fc6858dffef15b458f7fb9b0363 Mon Sep 17 00:00:00 2001 From: jokemanfire Date: Sat, 29 Mar 2025 16:34:34 +0800 Subject: [PATCH 1/2] ci: add documentation generation job 1. add doc ci in workflow 2. remove the readme in rmcp crate Signed-off-by: jokemanfire --- .github/workflows/ci.yml | 26 ++++++ crates/rmcp/README.md | 183 --------------------------------------- crates/rmcp/src/lib.rs | 2 +- 3 files changed, 27 insertions(+), 184 deletions(-) delete mode 100644 crates/rmcp/README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57e56dbb..d28fb7e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,6 +90,32 @@ jobs: - name: Run tests run: cargo test --all-features + doc: + name: Generate Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Generate documentation + run: | + cargo doc --no-deps -p rmcp -p rmcp-macros + # there must be no warnings , doc hasn't been fix yet, so skip it + # env: + # RUSTDOCFLAGS: -Dwarnings + release: name: Release crates runs-on: ubuntu-latest diff --git a/crates/rmcp/README.md b/crates/rmcp/README.md deleted file mode 100644 index 80b15e20..00000000 --- a/crates/rmcp/README.md +++ /dev/null @@ -1,183 +0,0 @@ -# RMCP -[![Crates.io Version](https://img.shields.io/crates/v/rmcp)](https://crates.io/crates/rmcp) -![Release status](https://github.commodelcontextprotocol/rust-sdk/actions/workflows/release.yml/badge.svg) -[![docs.rs](https://img.shields.io/docsrs/rmcp)](https://docs.rs/rmcp/latest/rmcp) - -A better and clean rust Model Context Protocol SDK implementation with tokio async runtime. - -## Comparing to official SDK - -The [Official SDK](https://github.com/modelcontextprotocol/rust-sdk/pulls) has too much limit and it was originally built for [goose](https://github.com/block/goose) rather than general using purpose. - -All the features listed on specification would be implemented in this crate. And the first and most important thing is, this crate has the correct and intact data [types](crate::model). See it yourself. - -## Usage - -### Import -```toml -rmcp = { version = "0.1", features = ["server"] } -``` - -### Quick start -Start a client in one line: -```rust,ignore -# use rmcp::{ServiceExt, transport::child_process::TokioChildProcess}; -# use tokio::process::Command; - -let client = ().serve( - TokioChildProcess::new(Command::new("npx").arg("-y").arg("@modelcontextprotocol/server-everything"))? -).await?; -``` - - -Start a client in one line: -```rust,ignore -# use rmcp::{ServiceExt, transport::TokioChildProcess}; -# use tokio::process::Command; - -let client = ().serve( - TokioChildProcess::new(Command::new("npx").arg("-y").arg("@modelcontextprotocol/server-everything"))? -).await?; -``` - - -#### 1. Build a transport -The transport type must implemented [`IntoTransport`](crate::transport::IntoTransport) trait, which allow split into a sink and a stream. - -For client, the sink item is [`ClientJsonRpcMessage`](crate::model::ClientJsonRpcMessage) and stream item is [`ServerJsonRpcMessage`](crate::model::ServerJsonRpcMessage) - -For server, the sink item is [`ServerJsonRpcMessage`](crate::model::ServerJsonRpcMessage) and stream item is [`ClientJsonRpcMessage`](crate::model::ClientJsonRpcMessage) - -##### These types is automatically implemented [`IntoTransport`](crate::transport::IntoTransport) trait -1. For type that already implement both [`Sink`](futures::Sink) and [`Stream`](futures::Stream) trait, they are automatically implemented [`IntoTransport`](crate::transport::IntoTransport) trait -2. For tuple of sink `Tx` and stream `Rx`, type `(Tx, Rx)` are automatically implemented [`IntoTransport`](crate::transport::IntoTransport) trait -3. For type that implement both [`tokio::io::AsyncRead`] and [`tokio::io::AsyncWrite`] trait, they are automatically implemented [`IntoTransport`](crate::transport::IntoTransport) trait -4. For tuple of [`tokio::io::AsyncRead`] `R `and [`tokio::io::AsyncWrite`] `W`, type `(R, W)` are automatically implemented [`IntoTransport`](crate::transport::IntoTransport) trait - - -```rust, ignore -use tokio::io::{stdin, stdout}; -let transport = (stdin(), stdout()); -``` - -#### 2. Build a service -You can easily build a service by using [`ServerHandler`](crate::handler::server) or [`ClientHandler`](crate::handler::client). - -```rust, ignore -let service = common::counter::Counter::new(); -``` - -Or if you want to use `tower`, you can [`TowerHandler`] as a adapter. - -You can reference the [server examples](https://github.commodelcontextprotocol/rust-sdk/tree/release/examples/servers). - -#### 3. Serve them together -```rust, ignore -// this call will finish the initialization process -let server = service.serve(transport).await?; -``` - -#### 4. Interact with the server -Once the server is initialized, you can send requests or notifications: - -```rust, ignore -// request -let roots = server.list_roots().await?; - -// or send notification -server.notify_cancelled(...).await?; -``` - -#### 5. Waiting for service shutdown -```rust, ignore -let quit_reason = server.waiting().await?; -// or cancel it -let quit_reason = server.cancel().await?; -``` - -### Use marcos to declaring tool -Use `toolbox` and `tool` macros to create tool quickly. - -Check this [file](https://github.commodelcontextprotocol/rust-sdk/tree/release/examples/servers/src/common/calculator.rs). -```rust, ignore -use rmcp::{ServerHandler, model::ServerInfo, schemars, tool}; - -use super::counter::Counter; - -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct SumRequest { - #[schemars(description = "the left hand side number")] - pub a: i32, - #[schemars(description = "the right hand side number")] - pub b: i32, -} -#[derive(Debug, Clone)] -pub struct Calculator; - -// create a static toolbox to store the tool attributes -#[tool(tool_box)] -impl Calculator { - // async function - #[tool(description = "Calculate the sum of two numbers")] - async fn sum(&self, #[tool(aggr)] SumRequest { a, b }: SumRequest) -> String { - (a + b).to_string() - } - - // sync function - #[tool(description = "Calculate the sum of two numbers")] - fn sub( - &self, - #[tool(param)] - // this macro will transfer the schemars and serde's attributes - #[schemars(description = "the left hand side number")] - a: i32, - #[tool(param)] - #[schemars(description = "the right hand side number")] - b: i32, - ) -> String { - (a - b).to_string() - } -} - -// impl call_tool and list_tool by querying static toolbox -#[tool(tool_box)] -impl ServerHandler for Calculator { - fn get_info(&self) -> ServerInfo { - ServerInfo { - instructions: Some("A simple calculator".into()), - ..Default::default() - } - } -} - -``` -The only thing you should do is to make the function's return type implement `IntoCallToolResult`. - -And you can just implement `IntoContents`, and the return value will be marked as success automatically. - -If you return a type of `Result` where `T` and `E` both implemented `IntoContents`, it's also OK. - -### Manage Multi Services -For many cases you need to manage several service in a collection, you can call `into_dyn` to convert services into the same type. -```rust, ignore -let service = service.into_dyn(); -``` - - -### Examples -See [examples](https://github.commodelcontextprotocol/rust-sdk/tree/release/examples/README.md) - -### Features -- `client`: use client side sdk -- `server`: use server side sdk -- `macros`: macros default -#### Transports -- `transport-io`: Server stdio transport -- `transport-sse-server`: Server SSE transport -- `transport-child-process`: Client stdio transport -- `transport-sse`: Client sse transport - -## Related Resources -- [MCP Specification](https://spec.modelcontextprotocol.io/specification/2024-11-05/) - -- [Schema](https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.ts) diff --git a/crates/rmcp/src/lib.rs b/crates/rmcp/src/lib.rs index b01ef2cd..db5f807f 100644 --- a/crates/rmcp/src/lib.rs +++ b/crates/rmcp/src/lib.rs @@ -1,4 +1,4 @@ -#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] +#![doc = include_str!("../../../README.md")] mod error; pub use error::Error; From addfd2169985d6526ee649a0433d9d0120df85ae Mon Sep 17 00:00:00 2001 From: jokemanfire Date: Sat, 29 Mar 2025 17:11:11 +0800 Subject: [PATCH 2/2] docs: fix doc test in README.md 1) fix doc test in readme 2) fix some fmt Signed-off-by: jokemanfire --- .github/workflows/ci.yml | 15 +++------------ README.md | 10 +++++++--- crates/rmcp/src/lib.rs | 1 - examples/servers/src/common/counter.rs | 15 +++++++-------- 4 files changed, 17 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d28fb7e5..a2932cec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,22 +99,13 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo- + - uses: Swatinem/rust-cache@v2 - name: Generate documentation run: | cargo doc --no-deps -p rmcp -p rmcp-macros - # there must be no warnings , doc hasn't been fix yet, so skip it - # env: - # RUSTDOCFLAGS: -Dwarnings + env: + RUSTDOCFLAGS: -Dwarnings release: name: Release crates diff --git a/README.md b/README.md index 7612347f..e682c725 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,13 @@ Start a client in one line: use rmcp::{ServiceExt, transport::TokioChildProcess}; use tokio::process::Command; -let client = ().serve( - TokioChildProcess::new(Command::new("npx").arg("-y").arg("@modelcontextprotocol/server-everything"))? -).await?; +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = ().serve( + TokioChildProcess::new(Command::new("npx").arg("-y").arg("@modelcontextprotocol/server-everything"))? + ).await?; + Ok(()) +} ``` #### 1. Build a transport diff --git a/crates/rmcp/src/lib.rs b/crates/rmcp/src/lib.rs index db5f807f..08a1908b 100644 --- a/crates/rmcp/src/lib.rs +++ b/crates/rmcp/src/lib.rs @@ -1,4 +1,3 @@ -#![doc = include_str!("../../../README.md")] mod error; pub use error::Error; diff --git a/examples/servers/src/common/counter.rs b/examples/servers/src/common/counter.rs index dbdfb421..aeeccd91 100644 --- a/examples/servers/src/common/counter.rs +++ b/examples/servers/src/common/counter.rs @@ -164,14 +164,13 @@ impl ServerHandler for Counter { match name.as_str() { "example_prompt" => { let message = arguments - .and_then( - |json| - json.get("message") - ?.as_str() - .map(|s| s.to_string())) - .ok_or_else(|| McpError::invalid_params("No message provided to example_prompt", None))?; - - let prompt = format!("This is an example prompt with your message here: '{message}'"); + .and_then(|json| json.get("message")?.as_str().map(|s| s.to_string())) + .ok_or_else(|| { + McpError::invalid_params("No message provided to example_prompt", None) + })?; + + let prompt = + format!("This is an example prompt with your message here: '{message}'"); Ok(GetPromptResult { description: None, messages: vec![PromptMessage {