diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57e56dbb..a2932cec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,6 +90,23 @@ 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 + + - uses: Swatinem/rust-cache@v2 + + - name: Generate documentation + run: | + cargo doc --no-deps -p rmcp -p rmcp-macros + env: + RUSTDOCFLAGS: -Dwarnings + release: name: Release crates runs-on: ubuntu-latest 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/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..08a1908b 100644 --- a/crates/rmcp/src/lib.rs +++ b/crates/rmcp/src/lib.rs @@ -1,4 +1,3 @@ -#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/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 {