├── .devcontainer └── devcontainer.json ├── .github ├── dependabot.yml ├── instructions │ └── fetch-mcp-doc.instructions.md ├── labeler.yml └── workflows │ ├── auto-label-pr.yml │ ├── ci.yml │ └── release-plz.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── client-metadata.json ├── clippy.toml ├── crates ├── rmcp-macros │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── common.rs │ │ ├── lib.rs │ │ ├── prompt.rs │ │ ├── prompt_handler.rs │ │ ├── prompt_router.rs │ │ ├── tool.rs │ │ ├── tool_handler.rs │ │ └── tool_router.rs └── rmcp │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ ├── src │ ├── error.rs │ ├── handler.rs │ ├── handler │ │ ├── client.rs │ │ ├── client │ │ │ └── progress.rs │ │ ├── server.rs │ │ └── server │ │ │ ├── common.rs │ │ │ ├── prompt.rs │ │ │ ├── resource.rs │ │ │ ├── router.rs │ │ │ ├── router │ │ │ ├── prompt.rs │ │ │ └── tool.rs │ │ │ ├── tool.rs │ │ │ ├── tool_name_validation.rs │ │ │ ├── wrapper.rs │ │ │ └── wrapper │ │ │ ├── json.rs │ │ │ └── parameters.rs │ ├── lib.rs │ ├── model.rs │ ├── model │ │ ├── annotated.rs │ │ ├── capabilities.rs │ │ ├── content.rs │ │ ├── elicitation_schema.rs │ │ ├── extension.rs │ │ ├── meta.rs │ │ ├── prompt.rs │ │ ├── resource.rs │ │ ├── serde_impl.rs │ │ └── tool.rs │ ├── service.rs │ ├── service │ │ ├── client.rs │ │ ├── server.rs │ │ └── tower.rs │ ├── transport.rs │ └── transport │ │ ├── async_rw.rs │ │ ├── auth.rs │ │ ├── child_process.rs │ │ ├── common.rs │ │ ├── common │ │ ├── auth.rs │ │ ├── auth │ │ │ └── streamable_http_client.rs │ │ ├── client_side_sse.rs │ │ ├── http_header.rs │ │ ├── reqwest.rs │ │ ├── reqwest │ │ │ └── streamable_http_client.rs │ │ └── server_side_http.rs │ │ ├── io.rs │ │ ├── sink_stream.rs │ │ ├── streamable_http_client.rs │ │ ├── streamable_http_server.rs │ │ ├── streamable_http_server │ │ ├── session.rs │ │ ├── session │ │ │ ├── local.rs │ │ │ └── never.rs │ │ └── tower.rs │ │ ├── worker.rs │ │ └── ws.rs │ └── tests │ ├── common │ ├── calculator.rs │ ├── handlers.rs │ └── mod.rs │ ├── test_completion.rs │ ├── test_complex_schema.rs │ ├── test_deserialization.rs │ ├── test_deserialization │ └── tool_list_result.json │ ├── test_elicitation.rs │ ├── test_embedded_resource_meta.rs │ ├── test_json_schema_detection.rs │ ├── test_logging.rs │ ├── test_message_protocol.rs │ ├── test_message_schema.rs │ ├── test_message_schema │ ├── client_json_rpc_message_schema.json │ ├── client_json_rpc_message_schema_current.json │ ├── server_json_rpc_message_schema.json │ └── server_json_rpc_message_schema_current.json │ ├── test_notification.rs │ ├── test_progress_subscriber.rs │ ├── test_prompt_handler.rs │ ├── test_prompt_macro_annotations.rs │ ├── test_prompt_macros.rs │ ├── test_prompt_routers.rs │ ├── test_resource_link.rs │ ├── test_resource_link_integration.rs │ ├── test_sampling.rs │ ├── test_structured_output.rs │ ├── test_tool_builder_methods.rs │ ├── test_tool_handler.rs │ ├── test_tool_macro_annotations.rs │ ├── test_tool_macros.rs │ ├── test_tool_result_meta.rs │ ├── test_tool_routers.rs │ ├── test_with_js.rs │ ├── test_with_js │ ├── .gitignore │ ├── client.js │ ├── package.json │ ├── server.js │ ├── streamable_client.js │ └── streamable_server.js │ ├── test_with_python.rs │ └── test_with_python │ ├── .gitignore │ ├── client.py │ ├── pyproject.toml │ └── server.py ├── docs ├── CONTRIBUTE.MD ├── DEVCONTAINER.md ├── OAUTH_SUPPORT.md ├── coverage.svg └── readme │ └── README.zh-cn.md ├── examples ├── README.md ├── clients │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── auth │ │ ├── callback.html │ │ └── oauth_client.rs │ │ ├── collection.rs │ │ ├── everything_stdio.rs │ │ ├── git_stdio.rs │ │ ├── progress_client.rs │ │ ├── sampling_stdio.rs │ │ └── streamable_http.rs ├── rig-integration │ ├── Cargo.toml │ ├── config.toml │ └── src │ │ ├── chat.rs │ │ ├── config.rs │ │ ├── config │ │ └── mcp.rs │ │ ├── main.rs │ │ └── mcp_adaptor.rs ├── servers │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── calculator_stdio.rs │ │ ├── cimd_auth_streamhttp.rs │ │ ├── common │ │ │ ├── calculator.rs │ │ │ ├── counter.rs │ │ │ ├── generic_service.rs │ │ │ ├── mod.rs │ │ │ └── progress_demo.rs │ │ ├── completion_stdio.rs │ │ ├── complex_auth_streamhttp.rs │ │ ├── counter_hyper_streamable_http.rs │ │ ├── counter_stdio.rs │ │ ├── counter_streamhttp.rs │ │ ├── elicitation_stdio.rs │ │ ├── html │ │ │ └── mcp_oauth_index.html │ │ ├── memory_stdio.rs │ │ ├── progress_demo.rs │ │ ├── prompt_stdio.rs │ │ ├── sampling_stdio.rs │ │ ├── simple_auth_streamhttp.rs │ │ └── structured_output.rs │ └── templates │ │ └── mcp_oauth_authorize.html ├── simple-chat-client │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── bin │ │ └── simple_chat.rs │ │ ├── chat.rs │ │ ├── client.rs │ │ ├── config.rs │ │ ├── config.toml │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── model.rs │ │ └── tool.rs ├── transport │ ├── Cargo.toml │ └── src │ │ ├── common │ │ ├── calculator.rs │ │ └── mod.rs │ │ ├── http_upgrade.rs │ │ ├── named-pipe.rs │ │ ├── tcp.rs │ │ ├── unix_socket.rs │ │ └── websocket.rs └── wasi │ ├── Cargo.toml │ ├── README.md │ ├── config.toml │ └── src │ ├── calculator.rs │ └── lib.rs ├── justfile ├── rust-toolchain.toml └── rustfmt.toml /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/rust 3 | { 4 | "name": "Rust", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye", 7 | "features": { 8 | "ghcr.io/devcontainers/features/node:1": {}, 9 | "ghcr.io/devcontainers/features/python:1": { 10 | "version": "3.10", 11 | "toolsToInstall": "uv" 12 | } 13 | }, 14 | // Configure tool-specific properties. 15 | "customizations": { 16 | "vscode": { 17 | "settings": { 18 | "editor.formatOnSave": true, 19 | "[rust]": { 20 | "editor.defaultFormatter": "rust-lang.rust-analyzer" 21 | } 22 | } 23 | } 24 | }, 25 | // Use 'postCreateCommand' to run commands after the container is created. 26 | "postCreateCommand": "uv venv" 27 | // Use 'mounts' to make the cargo cache persistent in a Docker Volume. 28 | // "mounts": [ 29 | // { 30 | // "source": "devcontainer-cargo-cache-${devcontainerId}", 31 | // "target": "/usr/local/cargo", 32 | // "type": "volume" 33 | // } 34 | // ] 35 | // Features to add to the dev container. More info: https://containers.dev/features. 36 | // "features": {}, 37 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 38 | // "forwardPorts": [], 39 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 40 | // "remoteUser": "root" 41 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | labels: 8 | - T-dependencies 9 | open-pull-requests-limit: 3 10 | commit-message: 11 | prefix: "chore" 12 | include: "scope" 13 | 14 | # Ensure that references to actions in a repository's workflow.yml file are kept up to date. 15 | # See https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/keeping-your-actions-up-to-date-with-dependabot 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: "daily" 20 | labels: 21 | # Mark PRs as CI related change. 22 | - T-CI 23 | open-pull-requests-limit: 3 24 | commit-message: 25 | prefix: "chore" 26 | include: "scope" -------------------------------------------------------------------------------- /.github/instructions/fetch-mcp-doc.instructions.md: -------------------------------------------------------------------------------- 1 | --- 2 | applyTo: '**' 3 | --- 4 | 5 | When you need the information about the Model Context Protocol (MCP) specification, you can fetch the latest document from the official MCP website. 6 | 7 | # Overall 8 | #fetch https://modelcontextprotocol.io/specification/2025-06-18.md 9 | 10 | ## Key Changes 11 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/changelog.md 12 | 13 | ## Architecture 14 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/architecture.md 15 | 16 | # BaseProtocol 17 | ## Overview 18 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/basic.md 19 | 20 | ## Lifecycle 21 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle.md 22 | 23 | ## Transports 24 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/basic/transports.md 25 | 26 | ## Authorization 27 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization.md 28 | 29 | ## Security Best Practices 30 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices.md 31 | 32 | ## Utilities 33 | ### Cancellation 34 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation.md 35 | 36 | ### Ping 37 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/ping.md 38 | 39 | ### Progress 40 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress.md 41 | 42 | # Client Features 43 | ## Roots 44 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/client/roots.md 45 | 46 | ## Sampling 47 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/client/sampling.md 48 | 49 | ## Elicitation 50 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation.md 51 | 52 | # Server Features 53 | ## Overview 54 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/server.md 55 | 56 | ## Prompts 57 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/server/prompts.md 58 | 59 | ## Resources 60 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/server/resources.md 61 | 62 | ## Tools 63 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/server/tools.md 64 | 65 | ## Utilities 66 | ### Completion 67 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/completion.md 68 | ### Logging 69 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging.md 70 | ### Pagination 71 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/pagination.md 72 | 73 | # Schema Reference 74 | #fetch https://modelcontextprotocol.io/specification/2025-06-18/schema.md -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Configuration for GitHub Labeler Action 2 | # https://github.com/actions/labeler 3 | 4 | # Documentation changes 5 | T-documentation: 6 | - changed-files: 7 | - any-glob-to-any-file: ['**/*.md', 'docs/**/*', '**/*.rst'] 8 | 9 | # Core library changes 10 | T-core: 11 | - changed-files: 12 | - any-glob-to-any-file: ['crates/rmcp/src/**/*'] 13 | 14 | # Macro changes 15 | T-macros: 16 | - changed-files: 17 | - any-glob-to-any-file: ['crates/rmcp-macros/**/*'] 18 | 19 | # Example changes 20 | T-examples: 21 | - changed-files: 22 | - any-glob-to-any-file: ['examples/**/*'] 23 | 24 | # CI/CD changes 25 | T-CI: 26 | - changed-files: 27 | - any-glob-to-any-file: ['.github/**/*', '**/*.yml', '**/*.yaml', '**/Dockerfile*'] 28 | 29 | # Dependencies 30 | T-dependencies: 31 | - changed-files: 32 | - any-glob-to-any-file: ['**/Cargo.toml', '**/Cargo.lock', '**/package.json', '**/package-lock.json', '**/requirements.txt', '**/pyproject.toml'] 33 | 34 | # Tests 35 | T-test: 36 | - changed-files: 37 | - any-glob-to-any-file: ['**/tests/**/*', '**/*test*.rs', '**/benches/**/*'] 38 | 39 | # Transport layer changes 40 | T-transport: 41 | - changed-files: 42 | - any-glob-to-any-file: ['crates/rmcp/src/transport/**/*'] 43 | 44 | # Service layer changes 45 | T-service: 46 | - changed-files: 47 | - any-glob-to-any-file: ['crates/rmcp/src/service/**/*'] 48 | 49 | # Handler changes 50 | T-handler: 51 | - changed-files: 52 | - any-glob-to-any-file: ['crates/rmcp/src/handler/**/*'] 53 | 54 | # Model changes 55 | T-model: 56 | - changed-files: 57 | - any-glob-to-any-file: ['crates/rmcp/src/model/**/*'] 58 | 59 | # Configuration files 60 | T-config: 61 | - changed-files: 62 | - any-glob-to-any-file: ['**/*.toml', '**/*.json', '**/*.yaml', '**/*.yml', '**/*.env*'] 63 | 64 | # Security related files 65 | T-security: 66 | - changed-files: 67 | - any-glob-to-any-file: ['**/security.md', '**/SECURITY.md', '**/audit.toml'] -------------------------------------------------------------------------------- /.github/workflows/auto-label-pr.yml: -------------------------------------------------------------------------------- 1 | name: Auto Label PR 2 | on: 3 | # Runs workflow when activity on a PR in the workflow's repository occurs. 4 | pull_request_target: 5 | 6 | jobs: 7 | auto-label: 8 | permissions: 9 | contents: read 10 | pull-requests: write 11 | issues: write 12 | 13 | name: Assign labels 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 5 16 | 17 | # Required by gh 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | PR_URL: ${{ github.event.pull_request.html_url }} 21 | 22 | steps: 23 | - uses: actions/labeler@v6 24 | with: 25 | # Auto-include paths starting with dot (e.g. .github) 26 | dot: true 27 | # Remove labels when matching files are reverted or no longer changed by the PR 28 | sync-labels: true -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | 14 | # Release unpublished packages. 15 | release-plz-release: 16 | name: Release-plz release 17 | runs-on: ubuntu-latest 18 | if: ${{ github.repository_owner == 'modelcontextprotocol' }} 19 | permissions: 20 | contents: write 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v6 24 | with: 25 | fetch-depth: 0 26 | - name: Install Rust toolchain 27 | uses: dtolnay/rust-toolchain@stable 28 | - name: Run release-plz 29 | uses: release-plz/action@v0.5 30 | with: 31 | command: release 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 35 | 36 | # Create a PR with the new versions and changelog, preparing the next release. 37 | release-plz-pr: 38 | name: Release-plz PR 39 | runs-on: ubuntu-latest 40 | if: ${{ github.repository_owner == 'modelcontextprotocol' }} 41 | permissions: 42 | contents: write 43 | pull-requests: write 44 | concurrency: 45 | group: release-plz-${{ github.ref }} 46 | cancel-in-progress: false 47 | steps: 48 | - name: Checkout repository 49 | uses: actions/checkout@v6 50 | with: 51 | fetch-depth: 0 52 | - name: Install Rust toolchain 53 | uses: dtolnay/rust-toolchain@stable 54 | - name: Run release-plz 55 | uses: release-plz/action@v0.5 56 | with: 57 | command: release-pr 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | .vscode/ 16 | .idea 17 | 18 | # Python artifacts (for test directories) 19 | *.egg-info/ 20 | __pycache__/ 21 | *.pyc 22 | *.pyo 23 | 24 | # RustRover 25 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 26 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 27 | # and can be added to the global gitignore or merged into this file. For a more nuclear 28 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 29 | #.idea/ 30 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/rmcp", "crates/rmcp-macros", "examples/*"] 3 | default-members = ["crates/rmcp", "crates/rmcp-macros"] 4 | resolver = "2" 5 | 6 | [workspace.dependencies] 7 | rmcp = { version = "0.11.0", path = "./crates/rmcp" } 8 | rmcp-macros = { version = "0.11.0", path = "./crates/rmcp-macros" } 9 | 10 | [workspace.package] 11 | edition = "2024" 12 | version = "0.11.0" 13 | authors = ["4t145 "] 14 | license = "MIT" 15 | repository = "https://github.com/modelcontextprotocol/rust-sdk/" 16 | description = "Rust SDK for Model Context Protocol" 17 | keywords = ["mcp", "sdk", "tokio", "modelcontextprotocol"] 18 | homepage = "https://github.com/modelcontextprotocol/rust-sdk" 19 | categories = [ 20 | "network-programming", 21 | "asynchronous", 22 | ] 23 | readme = "README.md" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Model Context Protocol 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "client_id": "https://raw.githubusercontent.com/modelcontextprotocol/rust-sdk/refs/heads/main/client-metadata.json", 3 | "redirect_uris": ["http://localhost:4000/callback"], 4 | "grant_types": ["authorization_code"], 5 | "response_types": ["code"], 6 | "token_endpoint_auth_method": "none" 7 | } 8 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.85" 2 | too-many-arguments-threshold = 10 3 | check-private-items = false 4 | -------------------------------------------------------------------------------- /crates/rmcp-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | 3 | [package] 4 | name = "rmcp-macros" 5 | license = { workspace = true } 6 | version = { workspace = true } 7 | edition = { workspace = true } 8 | repository = { workspace = true } 9 | homepage = { workspace = true } 10 | readme = { workspace = true } 11 | description = "Rust SDK for Model Context Protocol macros library" 12 | documentation = "https://docs.rs/rmcp-macros" 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [dependencies] 18 | syn = {version = "2", features = ["full"]} 19 | quote = "1" 20 | proc-macro2 = "1" 21 | serde_json = "1.0" 22 | darling = { version = "0.23" } 23 | 24 | [features] 25 | [dev-dependencies] 26 | -------------------------------------------------------------------------------- /crates/rmcp-macros/src/common.rs: -------------------------------------------------------------------------------- 1 | //! Common utilities shared between different macro implementations 2 | 3 | use quote::quote; 4 | use syn::{Attribute, Expr, FnArg, ImplItemFn, Signature, Type}; 5 | 6 | /// Parse a None expression 7 | pub fn none_expr() -> syn::Result { 8 | syn::parse2::(quote! { None }) 9 | } 10 | 11 | /// Extract documentation from doc attributes 12 | pub fn extract_doc_line( 13 | existing_docs: Option, 14 | attr: &Attribute, 15 | ) -> syn::Result> { 16 | if !attr.path().is_ident("doc") { 17 | return Ok(None); 18 | } 19 | 20 | let syn::Meta::NameValue(name_value) = &attr.meta else { 21 | return Ok(None); 22 | }; 23 | 24 | let value = &name_value.value; 25 | let this_expr: Option = match value { 26 | // Preserve macros such as `include_str!(...)` 27 | syn::Expr::Macro(_) => Some(value.clone()), 28 | syn::Expr::Lit(syn::ExprLit { 29 | lit: syn::Lit::Str(lit_str), 30 | .. 31 | }) => { 32 | let content = lit_str.value().trim().to_string(); 33 | if content.is_empty() { 34 | return Ok(existing_docs); 35 | } 36 | Some(Expr::Lit(syn::ExprLit { 37 | attrs: Vec::new(), 38 | lit: syn::Lit::Str(syn::LitStr::new(&content, lit_str.span())), 39 | })) 40 | } 41 | _ => return Ok(None), 42 | }; 43 | 44 | match (existing_docs, this_expr) { 45 | (Some(existing), Some(this)) => { 46 | syn::parse2::(quote! { concat!(#existing, "\n", #this) }).map(Some) 47 | } 48 | (Some(existing), None) => Ok(Some(existing)), 49 | (None, Some(this)) => Ok(Some(this)), 50 | _ => Ok(None), 51 | } 52 | } 53 | 54 | /// Find Parameters type in function signature 55 | /// Returns the full Parameters type if found 56 | pub fn find_parameters_type_in_sig(sig: &Signature) -> Option> { 57 | sig.inputs.iter().find_map(|input| { 58 | if let FnArg::Typed(pat_type) = input { 59 | if let Type::Path(type_path) = &*pat_type.ty { 60 | if type_path 61 | .path 62 | .segments 63 | .last() 64 | .is_some_and(|type_name| type_name.ident == "Parameters") 65 | { 66 | return Some(pat_type.ty.clone()); 67 | } 68 | } 69 | } 70 | None 71 | }) 72 | } 73 | 74 | /// Find Parameters type in ImplItemFn 75 | pub fn find_parameters_type_impl(fn_item: &ImplItemFn) -> Option> { 76 | find_parameters_type_in_sig(&fn_item.sig) 77 | } 78 | -------------------------------------------------------------------------------- /crates/rmcp-macros/src/prompt_router.rs: -------------------------------------------------------------------------------- 1 | use darling::FromMeta; 2 | use proc_macro2::TokenStream; 3 | use quote::{format_ident, quote}; 4 | use syn::{ImplItem, ItemImpl, Visibility, parse_quote}; 5 | 6 | #[derive(FromMeta, Debug, Default)] 7 | #[darling(default)] 8 | pub struct PromptRouterAttribute { 9 | pub router: Option, 10 | pub vis: Option, 11 | } 12 | 13 | pub fn prompt_router(attr: TokenStream, input: TokenStream) -> syn::Result { 14 | let attribute = if attr.is_empty() { 15 | Default::default() 16 | } else { 17 | let attr_args = darling::ast::NestedMeta::parse_meta_list(attr)?; 18 | PromptRouterAttribute::from_list(&attr_args)? 19 | }; 20 | 21 | let mut impl_block = syn::parse2::(input)?; 22 | let self_ty = &impl_block.self_ty; 23 | 24 | let router_fn_ident = attribute 25 | .router 26 | .map(|s| format_ident!("{}", s)) 27 | .unwrap_or_else(|| format_ident!("prompt_router")); 28 | let vis = attribute.vis.unwrap_or(Visibility::Inherited); 29 | 30 | let mut prompt_route_fn_calls = Vec::new(); 31 | 32 | for item in &mut impl_block.items { 33 | if let ImplItem::Fn(fn_item) = item { 34 | let has_prompt_attr = fn_item.attrs.iter().any(|attr| { 35 | attr.path() 36 | .segments 37 | .last() 38 | .map(|seg| seg.ident == "prompt") 39 | .unwrap_or(false) 40 | }); 41 | 42 | if has_prompt_attr { 43 | let fn_ident = &fn_item.sig.ident; 44 | let attr_fn_ident = format_ident!("{}_prompt_attr", fn_ident); 45 | 46 | // Check what parameters the function takes 47 | let mut param_names = Vec::new(); 48 | let mut param_types = Vec::new(); 49 | 50 | for input in &fn_item.sig.inputs { 51 | if let syn::FnArg::Typed(pat_type) = input { 52 | // Extract parameter pattern and type 53 | param_types.push(&*pat_type.ty); 54 | param_names.push(&*pat_type.pat); 55 | } 56 | } 57 | 58 | // Use the exact same pattern as tool_router 59 | prompt_route_fn_calls.push(quote! { 60 | .with_route((Self::#attr_fn_ident(), Self::#fn_ident)) 61 | }); 62 | } 63 | } 64 | } 65 | 66 | let router_fn: ImplItem = parse_quote! { 67 | #vis fn #router_fn_ident() -> rmcp::handler::server::router::prompt::PromptRouter<#self_ty> { 68 | rmcp::handler::server::router::prompt::PromptRouter::new() 69 | #(#prompt_route_fn_calls)* 70 | } 71 | }; 72 | 73 | impl_block.items.push(router_fn); 74 | 75 | Ok(quote! { 76 | #impl_block 77 | }) 78 | } 79 | 80 | #[cfg(test)] 81 | mod test { 82 | use super::*; 83 | 84 | #[test] 85 | fn test_prompt_router_macro() -> syn::Result<()> { 86 | let input = quote! { 87 | impl MyPromptHandler { 88 | #[prompt] 89 | async fn greeting_prompt(&self) -> Result, Error> { 90 | Ok(vec![]) 91 | } 92 | 93 | #[prompt] 94 | async fn code_review_prompt(&self, Parameters(args): Parameters) -> Result, Error> { 95 | Ok(vec![]) 96 | } 97 | } 98 | }; 99 | 100 | let result = prompt_router(TokenStream::new(), input)?; 101 | let result_str = result.to_string(); 102 | 103 | // Check that the prompt_router function was generated 104 | assert!(result_str.contains("fn prompt_router")); 105 | assert!(result_str.contains("PromptRouter :: new")); 106 | assert!(result_str.contains("greeting_prompt_prompt_attr")); 107 | assert!(result_str.contains("code_review_prompt_prompt_attr")); 108 | 109 | Ok(()) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /crates/rmcp-macros/src/tool_handler.rs: -------------------------------------------------------------------------------- 1 | use darling::{FromMeta, ast::NestedMeta}; 2 | use proc_macro2::TokenStream; 3 | use quote::{ToTokens, quote}; 4 | use syn::{Expr, ImplItem, ItemImpl}; 5 | 6 | #[derive(FromMeta)] 7 | #[darling(default)] 8 | pub struct ToolHandlerAttribute { 9 | pub router: Expr, 10 | pub meta: Option, 11 | } 12 | 13 | impl Default for ToolHandlerAttribute { 14 | fn default() -> Self { 15 | Self { 16 | router: syn::parse2(quote! { 17 | self.tool_router 18 | }) 19 | .unwrap(), 20 | meta: None, 21 | } 22 | } 23 | } 24 | 25 | pub fn tool_handler(attr: TokenStream, input: TokenStream) -> syn::Result { 26 | let attr_args = NestedMeta::parse_meta_list(attr)?; 27 | let ToolHandlerAttribute { router, meta } = ToolHandlerAttribute::from_list(&attr_args)?; 28 | let mut item_impl = syn::parse2::(input.clone())?; 29 | let tool_call_fn = quote! { 30 | async fn call_tool( 31 | &self, 32 | request: rmcp::model::CallToolRequestParam, 33 | context: rmcp::service::RequestContext, 34 | ) -> Result { 35 | let tcc = rmcp::handler::server::tool::ToolCallContext::new(self, request, context); 36 | #router.call(tcc).await 37 | } 38 | }; 39 | 40 | let result_meta = if let Some(meta) = meta { 41 | quote! { Some(#meta) } 42 | } else { 43 | quote! { None } 44 | }; 45 | 46 | let tool_list_fn = quote! { 47 | async fn list_tools( 48 | &self, 49 | _request: Option, 50 | _context: rmcp::service::RequestContext, 51 | ) -> Result { 52 | Ok(rmcp::model::ListToolsResult{ 53 | tools: #router.list_all(), 54 | meta: #result_meta, 55 | next_cursor: None, 56 | }) 57 | } 58 | }; 59 | let tool_call_fn = syn::parse2::(tool_call_fn)?; 60 | let tool_list_fn = syn::parse2::(tool_list_fn)?; 61 | item_impl.items.push(tool_call_fn); 62 | item_impl.items.push(tool_list_fn); 63 | Ok(item_impl.into_token_stream()) 64 | } 65 | -------------------------------------------------------------------------------- /crates/rmcp-macros/src/tool_router.rs: -------------------------------------------------------------------------------- 1 | //! ```ignore 2 | //! #[rmcp::tool_router(router)] 3 | //! impl Handler { 4 | //! 5 | //! } 6 | //! ``` 7 | //! 8 | 9 | use darling::{FromMeta, ast::NestedMeta}; 10 | use proc_macro2::TokenStream; 11 | use quote::{ToTokens, format_ident, quote}; 12 | use syn::{Ident, ImplItem, ItemImpl, Visibility}; 13 | 14 | #[derive(FromMeta)] 15 | #[darling(default)] 16 | pub struct ToolRouterAttribute { 17 | pub router: Ident, 18 | pub vis: Option, 19 | } 20 | 21 | impl Default for ToolRouterAttribute { 22 | fn default() -> Self { 23 | Self { 24 | router: format_ident!("tool_router"), 25 | vis: None, 26 | } 27 | } 28 | } 29 | 30 | pub fn tool_router(attr: TokenStream, input: TokenStream) -> syn::Result { 31 | let attr_args = NestedMeta::parse_meta_list(attr)?; 32 | let ToolRouterAttribute { router, vis } = ToolRouterAttribute::from_list(&attr_args)?; 33 | let mut item_impl = syn::parse2::(input.clone())?; 34 | // find all function marked with `#[rmcp::tool]` 35 | let tool_attr_fns: Vec<_> = item_impl 36 | .items 37 | .iter() 38 | .filter_map(|item| { 39 | if let syn::ImplItem::Fn(fn_item) = item { 40 | fn_item 41 | .attrs 42 | .iter() 43 | .any(|attr| { 44 | attr.path() 45 | .segments 46 | .last() 47 | .is_some_and(|seg| seg.ident == "tool") 48 | }) 49 | .then_some(&fn_item.sig.ident) 50 | } else { 51 | None 52 | } 53 | }) 54 | .collect(); 55 | let mut routers = vec![]; 56 | for handler in tool_attr_fns { 57 | let tool_attr_fn_ident = format_ident!("{handler}_tool_attr"); 58 | routers.push(quote! { 59 | .with_route((Self::#tool_attr_fn_ident(), Self::#handler)) 60 | }) 61 | } 62 | let router_fn = syn::parse2::(quote! { 63 | #vis fn #router() -> rmcp::handler::server::router::tool::ToolRouter { 64 | rmcp::handler::server::router::tool::ToolRouter::::new() 65 | #(#routers)* 66 | } 67 | })?; 68 | item_impl.items.push(router_fn); 69 | Ok(item_impl.into_token_stream()) 70 | } 71 | 72 | #[cfg(test)] 73 | mod test { 74 | use super::*; 75 | #[test] 76 | fn test_router_attr() -> Result<(), Box> { 77 | let attr = quote! { 78 | router = test_router, 79 | vis = "pub(crate)" 80 | }; 81 | let attr_args = NestedMeta::parse_meta_list(attr)?; 82 | let ToolRouterAttribute { router, vis } = ToolRouterAttribute::from_list(&attr_args)?; 83 | println!("router: {}", router); 84 | if let Some(vis) = vis { 85 | println!("visibility: {}", vis.to_token_stream()); 86 | } else { 87 | println!("visibility: None"); 88 | } 89 | Ok(()) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /crates/rmcp/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, fmt::Display}; 2 | 3 | use crate::ServiceError; 4 | pub use crate::model::ErrorData; 5 | #[deprecated( 6 | note = "Use `rmcp::ErrorData` instead, `rmcp::ErrorData` could become `RmcpError` in the future." 7 | )] 8 | pub type Error = ErrorData; 9 | impl Display for ErrorData { 10 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 11 | write!(f, "{}: {}", self.code.0, self.message)?; 12 | if let Some(data) = &self.data { 13 | write!(f, "({})", data)?; 14 | } 15 | Ok(()) 16 | } 17 | } 18 | 19 | impl std::error::Error for ErrorData {} 20 | 21 | /// This is an unified error type for the errors could be returned by the service. 22 | #[derive(Debug, thiserror::Error)] 23 | pub enum RmcpError { 24 | #[error("Service error: {0}")] 25 | Service(#[from] ServiceError), 26 | #[cfg(feature = "client")] 27 | #[error("Client initialization error: {0}")] 28 | ClientInitialize(#[from] crate::service::ClientInitializeError), 29 | #[cfg(feature = "server")] 30 | #[error("Server initialization error: {0}")] 31 | ServerInitialize(#[from] crate::service::ServerInitializeError), 32 | #[error("Runtime error: {0}")] 33 | Runtime(#[from] tokio::task::JoinError), 34 | #[error("Transport creation error: {error}")] 35 | // TODO: Maybe we can introduce something like `TryIntoTransport` to auto wrap transport type, 36 | // but it could be an breaking change, so we could do it in the future. 37 | TransportCreation { 38 | into_transport_type_name: Cow<'static, str>, 39 | into_transport_type_id: std::any::TypeId, 40 | #[source] 41 | error: Box, 42 | }, 43 | // and cancellation shouldn't be an error? 44 | } 45 | 46 | impl RmcpError { 47 | pub fn transport_creation( 48 | error: impl Into>, 49 | ) -> Self { 50 | RmcpError::TransportCreation { 51 | into_transport_type_id: std::any::TypeId::of::(), 52 | into_transport_type_name: std::any::type_name::().into(), 53 | error: error.into(), 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/rmcp/src/handler.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "client")] 2 | #[cfg_attr(docsrs, doc(cfg(feature = "client")))] 3 | pub mod client; 4 | #[cfg(feature = "server")] 5 | #[cfg_attr(docsrs, doc(cfg(feature = "server")))] 6 | pub mod server; 7 | -------------------------------------------------------------------------------- /crates/rmcp/src/handler/client/progress.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use futures::{Stream, StreamExt}; 4 | use tokio::sync::RwLock; 5 | use tokio_stream::wrappers::ReceiverStream; 6 | 7 | use crate::model::{ProgressNotificationParam, ProgressToken}; 8 | type Dispatcher = 9 | Arc>>>; 10 | 11 | /// A dispatcher for progress notifications. 12 | #[derive(Debug, Clone, Default)] 13 | pub struct ProgressDispatcher { 14 | pub(crate) dispatcher: Dispatcher, 15 | } 16 | 17 | impl ProgressDispatcher { 18 | const CHANNEL_SIZE: usize = 16; 19 | pub fn new() -> Self { 20 | Self::default() 21 | } 22 | 23 | /// Handle a progress notification by sending it to the appropriate subscriber 24 | pub async fn handle_notification(&self, notification: ProgressNotificationParam) { 25 | let token = ¬ification.progress_token; 26 | if let Some(sender) = self.dispatcher.read().await.get(token).cloned() { 27 | let send_result = sender.send(notification).await; 28 | if let Err(e) = send_result { 29 | tracing::warn!("Failed to send progress notification: {e}"); 30 | } 31 | } 32 | } 33 | 34 | /// Subscribe to progress notifications for a specific token. 35 | /// 36 | /// If you drop the returned `ProgressSubscriber`, it will automatically unsubscribe from notifications for that token. 37 | pub async fn subscribe(&self, progress_token: ProgressToken) -> ProgressSubscriber { 38 | let (sender, receiver) = tokio::sync::mpsc::channel(Self::CHANNEL_SIZE); 39 | self.dispatcher 40 | .write() 41 | .await 42 | .insert(progress_token.clone(), sender); 43 | let receiver = ReceiverStream::new(receiver); 44 | ProgressSubscriber { 45 | progress_token, 46 | receiver, 47 | dispatcher: self.dispatcher.clone(), 48 | } 49 | } 50 | 51 | /// Unsubscribe from progress notifications for a specific token. 52 | pub async fn unsubscribe(&self, token: &ProgressToken) { 53 | self.dispatcher.write().await.remove(token); 54 | } 55 | 56 | /// Clear all dispatcher. 57 | pub async fn clear(&self) { 58 | let mut dispatcher = self.dispatcher.write().await; 59 | dispatcher.clear(); 60 | } 61 | } 62 | 63 | pub struct ProgressSubscriber { 64 | pub(crate) progress_token: ProgressToken, 65 | pub(crate) receiver: ReceiverStream, 66 | pub(crate) dispatcher: Dispatcher, 67 | } 68 | 69 | impl ProgressSubscriber { 70 | pub fn progress_token(&self) -> &ProgressToken { 71 | &self.progress_token 72 | } 73 | } 74 | 75 | impl Stream for ProgressSubscriber { 76 | type Item = ProgressNotificationParam; 77 | 78 | fn poll_next( 79 | mut self: std::pin::Pin<&mut Self>, 80 | cx: &mut std::task::Context<'_>, 81 | ) -> std::task::Poll> { 82 | self.receiver.poll_next_unpin(cx) 83 | } 84 | 85 | fn size_hint(&self) -> (usize, Option) { 86 | self.receiver.size_hint() 87 | } 88 | } 89 | 90 | impl Drop for ProgressSubscriber { 91 | fn drop(&mut self) { 92 | let token = self.progress_token.clone(); 93 | self.receiver.close(); 94 | let dispatcher = self.dispatcher.clone(); 95 | tokio::spawn(async move { 96 | let mut dispatcher = dispatcher.write_owned().await; 97 | dispatcher.remove(&token); 98 | }); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /crates/rmcp/src/handler/server/resource.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/rmcp/src/handler/server/wrapper.rs: -------------------------------------------------------------------------------- 1 | mod json; 2 | mod parameters; 3 | pub use json::*; 4 | pub use parameters::*; 5 | -------------------------------------------------------------------------------- /crates/rmcp/src/handler/server/wrapper/json.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use schemars::JsonSchema; 4 | use serde::Serialize; 5 | 6 | use crate::{ 7 | handler::server::tool::IntoCallToolResult, 8 | model::{CallToolResult, IntoContents}, 9 | }; 10 | 11 | /// Json wrapper for structured output 12 | /// 13 | /// When used with tools, this wrapper indicates that the value should be 14 | /// serialized as structured JSON content with an associated schema. 15 | /// The framework will place the JSON in the `structured_content` field 16 | /// of the tool result rather than the regular `content` field. 17 | pub struct Json(pub T); 18 | 19 | // Implement JsonSchema for Json to delegate to T's schema 20 | impl JsonSchema for Json { 21 | fn schema_name() -> Cow<'static, str> { 22 | T::schema_name() 23 | } 24 | 25 | fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { 26 | T::json_schema(generator) 27 | } 28 | } 29 | 30 | // Implementation for Json to create structured content 31 | impl IntoCallToolResult for Json { 32 | fn into_call_tool_result(self) -> Result { 33 | let value = serde_json::to_value(self.0).map_err(|e| { 34 | crate::ErrorData::internal_error( 35 | format!("Failed to serialize structured content: {}", e), 36 | None, 37 | ) 38 | })?; 39 | 40 | Ok(CallToolResult::structured(value)) 41 | } 42 | } 43 | 44 | // Implementation for Result, E> 45 | impl IntoCallToolResult 46 | for Result, E> 47 | { 48 | fn into_call_tool_result(self) -> Result { 49 | match self { 50 | Ok(value) => value.into_call_tool_result(), 51 | Err(error) => Ok(CallToolResult::error(error.into_contents())), 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/rmcp/src/handler/server/wrapper/parameters.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | 3 | /// Parameter extractor for tools and prompts 4 | /// 5 | /// When used in tool and prompt handlers, this wrapper extracts and deserializes 6 | /// parameters from the incoming request. The framework will automatically parse 7 | /// the JSON arguments from tool calls or prompt arguments and deserialize them 8 | /// into the specified type `P`. 9 | /// 10 | /// The `#[serde(transparent)]` attribute ensures that the wrapper doesn't add 11 | /// an extra layer in the JSON structure - it directly delegates serialization 12 | /// and deserialization to the inner type `P`. 13 | /// 14 | /// # Usage 15 | /// 16 | /// Use `Parameters` as a parameter in your tool or prompt handler functions: 17 | /// 18 | /// ```rust 19 | /// # use rmcp::handler::server::wrapper::Parameters; 20 | /// # use schemars::JsonSchema; 21 | /// # use serde::{Deserialize, Serialize}; 22 | /// #[derive(Deserialize, JsonSchema)] 23 | /// struct CalculationRequest { 24 | /// operation: String, 25 | /// a: f64, 26 | /// b: f64, 27 | /// } 28 | /// 29 | /// // In a tool handler 30 | /// async fn calculate(params: Parameters) -> Result { 31 | /// let request = params.0; // Extract the inner value 32 | /// match request.operation.as_str() { 33 | /// "add" => Ok((request.a + request.b).to_string()), 34 | /// _ => Err("Unknown operation".to_string()), 35 | /// } 36 | /// } 37 | /// ``` 38 | /// 39 | /// The framework handles the extraction automatically: 40 | /// - For tools: Parses the `arguments` field from tool call requests 41 | /// - For prompts: Parses the `arguments` field from prompt requests 42 | /// - Returns appropriate error responses if deserialization fails 43 | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] 44 | #[serde(transparent)] 45 | pub struct Parameters

(pub P); 46 | 47 | impl JsonSchema for Parameters

{ 48 | fn schema_name() -> std::borrow::Cow<'static, str> { 49 | P::schema_name() 50 | } 51 | 52 | fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { 53 | P::json_schema(generator) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /crates/rmcp/src/service/tower.rs: -------------------------------------------------------------------------------- 1 | use std::{future::poll_fn, marker::PhantomData}; 2 | 3 | use tower_service::Service as TowerService; 4 | 5 | use super::NotificationContext; 6 | use crate::service::{RequestContext, Service, ServiceRole}; 7 | 8 | pub struct TowerHandler { 9 | pub service: S, 10 | pub info: R::Info, 11 | role: PhantomData, 12 | } 13 | 14 | impl TowerHandler { 15 | pub fn new(service: S, info: R::Info) -> Self { 16 | Self { 17 | service, 18 | role: PhantomData, 19 | info, 20 | } 21 | } 22 | } 23 | 24 | impl Service for TowerHandler 25 | where 26 | S: TowerService + Sync + Send + Clone + 'static, 27 | S::Error: Into, 28 | S::Future: Send, 29 | { 30 | async fn handle_request( 31 | &self, 32 | request: R::PeerReq, 33 | _context: RequestContext, 34 | ) -> Result { 35 | let mut service = self.service.clone(); 36 | poll_fn(|cx| service.poll_ready(cx)) 37 | .await 38 | .map_err(Into::into)?; 39 | let resp = service.call(request).await.map_err(Into::into)?; 40 | Ok(resp) 41 | } 42 | 43 | fn handle_notification( 44 | &self, 45 | _notification: R::PeerNot, 46 | _context: NotificationContext, 47 | ) -> impl Future> + Send + '_ { 48 | std::future::ready(Ok(())) 49 | } 50 | 51 | fn get_info(&self) -> R::Info { 52 | self.info.clone() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/rmcp/src/transport/common.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "transport-streamable-http-server")] 2 | pub mod server_side_http; 3 | 4 | pub mod http_header; 5 | 6 | #[cfg(feature = "__reqwest")] 7 | #[cfg_attr(docsrs, doc(cfg(feature = "reqwest")))] 8 | mod reqwest; 9 | 10 | // Note: This module provides SSE stream parsing and auto-reconnect utilities. 11 | // It's used by the streamable HTTP client (which receives SSE-formatted responses), 12 | // not the removed SSE transport. The name is historical. 13 | #[cfg(feature = "client-side-sse")] 14 | #[cfg_attr(docsrs, doc(cfg(feature = "client-side-sse")))] 15 | pub mod client_side_sse; 16 | 17 | #[cfg(feature = "auth")] 18 | #[cfg_attr(docsrs, doc(cfg(feature = "auth")))] 19 | pub mod auth; 20 | -------------------------------------------------------------------------------- /crates/rmcp/src/transport/common/auth.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "transport-streamable-http-client")] 2 | #[cfg_attr(docsrs, doc(cfg(feature = "transport-streamable-http-client")))] 3 | mod streamable_http_client; 4 | -------------------------------------------------------------------------------- /crates/rmcp/src/transport/common/auth/streamable_http_client.rs: -------------------------------------------------------------------------------- 1 | use crate::transport::{ 2 | auth::AuthClient, 3 | streamable_http_client::{StreamableHttpClient, StreamableHttpError}, 4 | }; 5 | impl StreamableHttpClient for AuthClient 6 | where 7 | C: StreamableHttpClient + Send + Sync, 8 | { 9 | type Error = C::Error; 10 | 11 | async fn delete_session( 12 | &self, 13 | uri: std::sync::Arc, 14 | session_id: std::sync::Arc, 15 | mut auth_token: Option, 16 | ) -> Result<(), crate::transport::streamable_http_client::StreamableHttpError> 17 | { 18 | if auth_token.is_none() { 19 | auth_token = Some(self.get_access_token().await?); 20 | } 21 | self.http_client 22 | .delete_session(uri, session_id, auth_token) 23 | .await 24 | } 25 | 26 | async fn get_stream( 27 | &self, 28 | uri: std::sync::Arc, 29 | session_id: std::sync::Arc, 30 | last_event_id: Option, 31 | mut auth_token: Option, 32 | ) -> Result< 33 | futures::stream::BoxStream<'static, Result>, 34 | crate::transport::streamable_http_client::StreamableHttpError, 35 | > { 36 | if auth_token.is_none() { 37 | auth_token = Some(self.get_access_token().await?); 38 | } 39 | self.http_client 40 | .get_stream(uri, session_id, last_event_id, auth_token) 41 | .await 42 | } 43 | 44 | async fn post_message( 45 | &self, 46 | uri: std::sync::Arc, 47 | message: crate::model::ClientJsonRpcMessage, 48 | session_id: Option>, 49 | mut auth_token: Option, 50 | ) -> Result< 51 | crate::transport::streamable_http_client::StreamableHttpPostResponse, 52 | StreamableHttpError, 53 | > { 54 | if auth_token.is_none() { 55 | auth_token = Some(self.get_access_token().await?); 56 | } 57 | self.http_client 58 | .post_message(uri, message, session_id, auth_token) 59 | .await 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/rmcp/src/transport/common/http_header.rs: -------------------------------------------------------------------------------- 1 | pub const HEADER_SESSION_ID: &str = "Mcp-Session-Id"; 2 | pub const HEADER_LAST_EVENT_ID: &str = "Last-Event-Id"; 3 | pub const EVENT_STREAM_MIME_TYPE: &str = "text/event-stream"; 4 | pub const JSON_MIME_TYPE: &str = "application/json"; 5 | -------------------------------------------------------------------------------- /crates/rmcp/src/transport/common/reqwest.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "transport-streamable-http-client-reqwest")] 2 | #[cfg_attr(docsrs, doc(cfg(feature = "transport-streamable-http-client-reqwest")))] 3 | mod streamable_http_client; 4 | -------------------------------------------------------------------------------- /crates/rmcp/src/transport/io.rs: -------------------------------------------------------------------------------- 1 | /// # StdIO Transport 2 | /// 3 | /// Create a pair of [`tokio::io::Stdin`] and [`tokio::io::Stdout`]. 4 | pub fn stdio() -> (tokio::io::Stdin, tokio::io::Stdout) { 5 | (tokio::io::stdin(), tokio::io::stdout()) 6 | } 7 | -------------------------------------------------------------------------------- /crates/rmcp/src/transport/sink_stream.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use futures::{Sink, Stream}; 4 | use tokio::sync::Mutex; 5 | 6 | use super::{IntoTransport, Transport}; 7 | use crate::service::{RxJsonRpcMessage, ServiceRole, TxJsonRpcMessage}; 8 | 9 | pub struct SinkStreamTransport { 10 | stream: St, 11 | sink: Arc>, 12 | } 13 | 14 | impl SinkStreamTransport { 15 | pub fn new(sink: Si, stream: St) -> Self { 16 | Self { 17 | stream, 18 | sink: Arc::new(Mutex::new(sink)), 19 | } 20 | } 21 | } 22 | 23 | impl Transport for SinkStreamTransport 24 | where 25 | St: Send + Stream> + Unpin, 26 | Si: Send + Sink> + Unpin + 'static, 27 | Si::Error: std::error::Error + Send + Sync + 'static, 28 | { 29 | type Error = Si::Error; 30 | 31 | fn send( 32 | &mut self, 33 | item: TxJsonRpcMessage, 34 | ) -> impl Future> + Send + 'static { 35 | use futures::SinkExt; 36 | let lock = self.sink.clone(); 37 | async move { 38 | let mut write = lock.lock().await; 39 | write.send(item).await 40 | } 41 | } 42 | 43 | fn receive(&mut self) -> impl Future>> { 44 | use futures::StreamExt; 45 | self.stream.next() 46 | } 47 | 48 | async fn close(&mut self) -> Result<(), Self::Error> { 49 | Ok(()) 50 | } 51 | } 52 | 53 | pub enum TransportAdapterSinkStream {} 54 | 55 | impl IntoTransport for (Si, St) 56 | where 57 | Role: ServiceRole, 58 | Si: Send + Sink> + Unpin + 'static, 59 | St: Send + Stream> + Unpin + 'static, 60 | Si::Error: std::error::Error + Send + Sync + 'static, 61 | { 62 | fn into_transport(self) -> impl Transport + 'static { 63 | SinkStreamTransport::new(self.0, self.1) 64 | } 65 | } 66 | 67 | pub enum TransportAdapterAsyncCombinedRW {} 68 | impl IntoTransport for S 69 | where 70 | Role: ServiceRole, 71 | S: Sink> + Stream> + Send + 'static, 72 | S::Error: std::error::Error + Send + Sync + 'static, 73 | { 74 | fn into_transport(self) -> impl Transport + 'static { 75 | use futures::StreamExt; 76 | IntoTransport::::into_transport(self.split()) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crates/rmcp/src/transport/streamable_http_server.rs: -------------------------------------------------------------------------------- 1 | pub mod session; 2 | #[cfg(feature = "transport-streamable-http-server")] 3 | #[cfg_attr(docsrs, doc(cfg(feature = "transport-streamable-http-server")))] 4 | pub mod tower; 5 | pub use session::{SessionId, SessionManager}; 6 | #[cfg(feature = "transport-streamable-http-server")] 7 | #[cfg_attr(docsrs, doc(cfg(feature = "transport-streamable-http-server")))] 8 | pub use tower::{StreamableHttpServerConfig, StreamableHttpService}; 9 | -------------------------------------------------------------------------------- /crates/rmcp/src/transport/streamable_http_server/session.rs: -------------------------------------------------------------------------------- 1 | use futures::Stream; 2 | 3 | pub use crate::transport::common::server_side_http::SessionId; 4 | use crate::{ 5 | RoleServer, 6 | model::{ClientJsonRpcMessage, ServerJsonRpcMessage}, 7 | transport::common::server_side_http::ServerSseMessage, 8 | }; 9 | 10 | pub mod local; 11 | pub mod never; 12 | 13 | pub trait SessionManager: Send + Sync + 'static { 14 | type Error: std::error::Error + Send + 'static; 15 | type Transport: crate::transport::Transport; 16 | /// Create a new session with the given id and configuration. 17 | fn create_session( 18 | &self, 19 | ) -> impl Future> + Send; 20 | fn initialize_session( 21 | &self, 22 | id: &SessionId, 23 | message: ClientJsonRpcMessage, 24 | ) -> impl Future> + Send; 25 | fn has_session(&self, id: &SessionId) 26 | -> impl Future> + Send; 27 | fn close_session(&self, id: &SessionId) 28 | -> impl Future> + Send; 29 | fn create_stream( 30 | &self, 31 | id: &SessionId, 32 | message: ClientJsonRpcMessage, 33 | ) -> impl Future< 34 | Output = Result + Send + Sync + 'static, Self::Error>, 35 | > + Send; 36 | fn accept_message( 37 | &self, 38 | id: &SessionId, 39 | message: ClientJsonRpcMessage, 40 | ) -> impl Future> + Send; 41 | fn create_standalone_stream( 42 | &self, 43 | id: &SessionId, 44 | ) -> impl Future< 45 | Output = Result + Send + Sync + 'static, Self::Error>, 46 | > + Send; 47 | fn resume( 48 | &self, 49 | id: &SessionId, 50 | last_event_id: String, 51 | ) -> impl Future< 52 | Output = Result + Send + Sync + 'static, Self::Error>, 53 | > + Send; 54 | } 55 | -------------------------------------------------------------------------------- /crates/rmcp/src/transport/streamable_http_server/session/never.rs: -------------------------------------------------------------------------------- 1 | use futures::Stream; 2 | use thiserror::Error; 3 | 4 | use super::{ServerSseMessage, SessionId, SessionManager}; 5 | use crate::{ 6 | RoleServer, 7 | model::{ClientJsonRpcMessage, ServerJsonRpcMessage}, 8 | transport::Transport, 9 | }; 10 | 11 | #[derive(Debug, Clone, Error)] 12 | #[error("Session management is not supported")] 13 | pub struct ErrorSessionManagementNotSupported; 14 | #[derive(Debug, Clone, Default)] 15 | pub struct NeverSessionManager {} 16 | pub enum NeverTransport {} 17 | impl Transport for NeverTransport { 18 | type Error = ErrorSessionManagementNotSupported; 19 | 20 | fn send( 21 | &mut self, 22 | _item: ServerJsonRpcMessage, 23 | ) -> impl Future> + Send + 'static { 24 | futures::future::ready(Err(ErrorSessionManagementNotSupported)) 25 | } 26 | 27 | fn receive(&mut self) -> impl Future> { 28 | futures::future::ready(None) 29 | } 30 | 31 | async fn close(&mut self) -> Result<(), Self::Error> { 32 | Err(ErrorSessionManagementNotSupported) 33 | } 34 | } 35 | 36 | impl SessionManager for NeverSessionManager { 37 | type Error = ErrorSessionManagementNotSupported; 38 | type Transport = NeverTransport; 39 | 40 | fn create_session( 41 | &self, 42 | ) -> impl Future> + Send { 43 | futures::future::ready(Err(ErrorSessionManagementNotSupported)) 44 | } 45 | 46 | fn initialize_session( 47 | &self, 48 | _id: &SessionId, 49 | _message: ClientJsonRpcMessage, 50 | ) -> impl Future> + Send { 51 | futures::future::ready(Err(ErrorSessionManagementNotSupported)) 52 | } 53 | 54 | fn has_session( 55 | &self, 56 | _id: &SessionId, 57 | ) -> impl Future> + Send { 58 | futures::future::ready(Err(ErrorSessionManagementNotSupported)) 59 | } 60 | 61 | fn close_session( 62 | &self, 63 | _id: &SessionId, 64 | ) -> impl Future> + Send { 65 | futures::future::ready(Err(ErrorSessionManagementNotSupported)) 66 | } 67 | 68 | fn create_stream( 69 | &self, 70 | _id: &SessionId, 71 | _message: ClientJsonRpcMessage, 72 | ) -> impl Future< 73 | Output = Result + Send + 'static, Self::Error>, 74 | > + Send { 75 | futures::future::ready(Result::, _>::Err( 76 | ErrorSessionManagementNotSupported, 77 | )) 78 | } 79 | fn create_standalone_stream( 80 | &self, 81 | _id: &SessionId, 82 | ) -> impl Future< 83 | Output = Result + Send + 'static, Self::Error>, 84 | > + Send { 85 | futures::future::ready(Result::, _>::Err( 86 | ErrorSessionManagementNotSupported, 87 | )) 88 | } 89 | fn resume( 90 | &self, 91 | _id: &SessionId, 92 | _last_event_id: String, 93 | ) -> impl Future< 94 | Output = Result + Send + 'static, Self::Error>, 95 | > + Send { 96 | futures::future::ready(Result::, _>::Err( 97 | ErrorSessionManagementNotSupported, 98 | )) 99 | } 100 | fn accept_message( 101 | &self, 102 | _id: &SessionId, 103 | _message: ClientJsonRpcMessage, 104 | ) -> impl Future> + Send { 105 | futures::future::ready(Err(ErrorSessionManagementNotSupported)) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /crates/rmcp/src/transport/ws.rs: -------------------------------------------------------------------------------- 1 | // Maybe we don't really need a ws implementation? 2 | -------------------------------------------------------------------------------- /crates/rmcp/tests/common/calculator.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use rmcp::{ 3 | ServerHandler, 4 | handler::server::{router::tool::ToolRouter, wrapper::Parameters}, 5 | model::{ServerCapabilities, ServerInfo}, 6 | schemars, tool, tool_router, 7 | }; 8 | #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] 9 | pub struct SumRequest { 10 | #[schemars(description = "the left hand side number")] 11 | pub a: i32, 12 | pub b: i32, 13 | } 14 | 15 | #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] 16 | pub struct SubRequest { 17 | #[schemars(description = "the left hand side number")] 18 | pub a: i32, 19 | #[schemars(description = "the right hand side number")] 20 | pub b: i32, 21 | } 22 | #[derive(Debug, Clone)] 23 | pub struct Calculator { 24 | tool_router: ToolRouter, 25 | } 26 | 27 | impl Calculator { 28 | pub fn new() -> Self { 29 | Self { 30 | tool_router: Self::tool_router(), 31 | } 32 | } 33 | } 34 | 35 | impl Default for Calculator { 36 | fn default() -> Self { 37 | Self::new() 38 | } 39 | } 40 | 41 | #[tool_router] 42 | impl Calculator { 43 | #[tool(description = "Calculate the sum of two numbers")] 44 | fn sum(&self, Parameters(SumRequest { a, b }): Parameters) -> String { 45 | (a + b).to_string() 46 | } 47 | 48 | #[tool(description = "Calculate the sub of two numbers")] 49 | fn sub(&self, Parameters(SubRequest { a, b }): Parameters) -> String { 50 | (a - b).to_string() 51 | } 52 | } 53 | 54 | impl ServerHandler for Calculator { 55 | fn get_info(&self) -> ServerInfo { 56 | ServerInfo { 57 | instructions: Some("A simple calculator".into()), 58 | capabilities: ServerCapabilities::builder().enable_tools().build(), 59 | ..Default::default() 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/rmcp/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod calculator; 2 | pub mod handlers; 3 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_complex_schema.rs: -------------------------------------------------------------------------------- 1 | use rmcp::{ 2 | ErrorData as McpError, handler::server::wrapper::Parameters, model::*, schemars, tool, 3 | tool_router, 4 | }; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)] 8 | pub enum ChatRole { 9 | System, 10 | User, 11 | Assistant, 12 | Tool, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)] 16 | pub struct ChatMessage { 17 | pub role: ChatRole, 18 | pub content: String, 19 | } 20 | 21 | #[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)] 22 | pub struct ChatRequest { 23 | pub system: Option, 24 | pub messages: Vec, 25 | } 26 | 27 | #[derive(Clone, Default)] 28 | pub struct Demo; 29 | 30 | #[tool_router] 31 | impl Demo { 32 | pub fn new() -> Self { 33 | Self 34 | } 35 | 36 | #[tool(description = "LLM")] 37 | async fn chat( 38 | &self, 39 | chat_request: Parameters, 40 | ) -> Result { 41 | let content = Content::json(chat_request.0)?; 42 | Ok(CallToolResult::success(vec![content])) 43 | } 44 | } 45 | 46 | fn expected_schema() -> serde_json::Value { 47 | serde_json::json!({ 48 | "$defs": { 49 | "ChatMessage": { 50 | "properties": { 51 | "content": { 52 | "type": "string" 53 | }, 54 | "role": { 55 | "$ref": "#/$defs/ChatRole" 56 | } 57 | }, 58 | "required": [ 59 | "role", 60 | "content" 61 | ], 62 | "type": "object" 63 | }, 64 | "ChatRole": { 65 | "enum": [ 66 | "System", 67 | "User", 68 | "Assistant", 69 | "Tool" 70 | ], 71 | "type": "string" 72 | } 73 | }, 74 | "$schema": "https://json-schema.org/draft/2020-12/schema", 75 | "properties": { 76 | "messages": { 77 | "items": { 78 | "$ref": "#/$defs/ChatMessage" 79 | }, 80 | "type": "array" 81 | }, 82 | "system": { 83 | "nullable": true, 84 | "type": "string" 85 | } 86 | }, 87 | "required": [ 88 | "messages" 89 | ], 90 | "title": "ChatRequest", 91 | "type": "object" 92 | }) 93 | } 94 | 95 | #[test] 96 | fn test_complex_schema() { 97 | let attr = Demo::chat_tool_attr(); 98 | let input_schema = attr.input_schema; 99 | let expected = expected_schema(); 100 | let produced = serde_json::Value::Object(input_schema.as_ref().clone()); 101 | assert_eq!(produced, expected, "schema mismatch"); 102 | } 103 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_deserialization.rs: -------------------------------------------------------------------------------- 1 | use rmcp::model::{JsonRpcResponse, ServerJsonRpcMessage, ServerResult}; 2 | #[test] 3 | fn test_tool_list_result() { 4 | let json = std::fs::read("tests/test_deserialization/tool_list_result.json").unwrap(); 5 | let result: ServerJsonRpcMessage = serde_json::from_slice(&json).unwrap(); 6 | println!("{result:#?}"); 7 | 8 | assert!(matches!( 9 | result, 10 | ServerJsonRpcMessage::Response(JsonRpcResponse { 11 | result: ServerResult::ListToolsResult(_), 12 | .. 13 | }) 14 | )); 15 | } 16 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_deserialization/tool_list_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "result": { 3 | "tools": [ 4 | { 5 | "name": "add", 6 | "inputSchema": { 7 | "type": "object", 8 | "properties": { 9 | "a": { 10 | "type": "number" 11 | }, 12 | "b": { 13 | "type": "number" 14 | } 15 | }, 16 | "required": [ 17 | "a", 18 | "b" 19 | ], 20 | "additionalProperties": false, 21 | "$schema": "http://json-schema.org/draft-07/schema#" 22 | } 23 | } 24 | ] 25 | }, 26 | "jsonrpc": "2.0", 27 | "id": 2 28 | } -------------------------------------------------------------------------------- /crates/rmcp/tests/test_embedded_resource_meta.rs: -------------------------------------------------------------------------------- 1 | use rmcp::model::{AnnotateAble, Content, Meta, RawContent, ResourceContents}; 2 | use serde_json::json; 3 | 4 | #[test] 5 | fn serialize_embedded_text_resource_with_meta() { 6 | // Inner contents meta 7 | let mut resource_content_meta = Meta::new(); 8 | resource_content_meta.insert("inner".to_string(), json!(2)); 9 | 10 | // Top-level embedded resource meta 11 | let mut resource_meta = Meta::new(); 12 | resource_meta.insert("top".to_string(), json!(1)); 13 | 14 | let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource { 15 | meta: Some(resource_meta), 16 | resource: ResourceContents::TextResourceContents { 17 | uri: "str://example".to_string(), 18 | mime_type: Some("text/plain".to_string()), 19 | text: "hello".to_string(), 20 | meta: Some(resource_content_meta), 21 | }, 22 | }) 23 | .no_annotation(); 24 | 25 | let v = serde_json::to_value(&content).unwrap(); 26 | 27 | let expected = json!({ 28 | "type": "resource", 29 | "_meta": {"top": 1}, 30 | "resource": { 31 | "uri": "str://example", 32 | "mimeType": "text/plain", 33 | "text": "hello", 34 | "_meta": {"inner": 2} 35 | } 36 | }); 37 | 38 | assert_eq!(v, expected); 39 | } 40 | 41 | #[test] 42 | fn serialize_embedded_text_resource_without_meta_omits_fields() { 43 | let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource { 44 | meta: None, 45 | resource: ResourceContents::TextResourceContents { 46 | uri: "str://no-meta".to_string(), 47 | mime_type: Some("text/plain".to_string()), 48 | text: "hi".to_string(), 49 | meta: None, 50 | }, 51 | }) 52 | .no_annotation(); 53 | 54 | let v = serde_json::to_value(&content).unwrap(); 55 | 56 | assert_eq!(v.get("_meta"), None); 57 | let inner = v.get("resource").and_then(|r| r.as_object()).unwrap(); 58 | assert_eq!(inner.get("_meta"), None); 59 | } 60 | 61 | #[test] 62 | fn deserialize_embedded_text_resource_with_meta() { 63 | let raw = json!({ 64 | "type": "resource", 65 | "_meta": {"x": true}, 66 | "resource": { 67 | "uri": "str://from-json", 68 | "text": "ok", 69 | "_meta": {"y": 42} 70 | } 71 | }); 72 | 73 | let content: Content = serde_json::from_value(raw).unwrap(); 74 | 75 | let raw = match &content.raw { 76 | RawContent::Resource(er) => er, 77 | _ => panic!("expected resource"), 78 | }; 79 | 80 | // top-level _meta 81 | let top = raw.meta.as_ref().expect("top-level meta missing"); 82 | assert_eq!(top.get("x").unwrap(), &json!(true)); 83 | 84 | // inner contents _meta 85 | match &raw.resource { 86 | ResourceContents::TextResourceContents { 87 | meta, uri, text, .. 88 | } => { 89 | assert_eq!(uri, "str://from-json"); 90 | assert_eq!(text, "ok"); 91 | let inner = meta.as_ref().expect("inner meta missing"); 92 | assert_eq!(inner.get("y").unwrap(), &json!(42)); 93 | } 94 | _ => panic!("expected text resource contents"), 95 | } 96 | } 97 | 98 | #[test] 99 | fn serialize_embedded_blob_resource_with_meta() { 100 | let mut resource_content_meta = Meta::new(); 101 | resource_content_meta.insert("blob_inner".to_string(), json!(true)); 102 | 103 | let mut resource_meta = Meta::new(); 104 | resource_meta.insert("blob_top".to_string(), json!("t")); 105 | 106 | let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource { 107 | meta: Some(resource_meta), 108 | resource: ResourceContents::BlobResourceContents { 109 | uri: "str://blob".to_string(), 110 | mime_type: Some("application/octet-stream".to_string()), 111 | blob: "Zm9v".to_string(), 112 | meta: Some(resource_content_meta), 113 | }, 114 | }) 115 | .no_annotation(); 116 | 117 | let v = serde_json::to_value(&content).unwrap(); 118 | 119 | assert_eq!(v.get("_meta").unwrap(), &json!({"blob_top": "t"})); 120 | let inner = v.get("resource").unwrap(); 121 | assert_eq!(inner.get("_meta").unwrap(), &json!({"blob_inner": true})); 122 | } 123 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_json_schema_detection.rs: -------------------------------------------------------------------------------- 1 | //cargo test --test test_json_schema_detection --features "client server macros" 2 | use rmcp::{ 3 | Json, ServerHandler, handler::server::router::tool::ToolRouter, tool, tool_handler, tool_router, 4 | }; 5 | use schemars::JsonSchema; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Serialize, Deserialize, JsonSchema)] 9 | pub struct TestData { 10 | pub value: String, 11 | } 12 | 13 | #[tool_handler(router = self.tool_router)] 14 | impl ServerHandler for TestServer {} 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct TestServer { 18 | tool_router: ToolRouter, 19 | } 20 | 21 | impl Default for TestServer { 22 | fn default() -> Self { 23 | Self::new() 24 | } 25 | } 26 | 27 | #[tool_router(router = tool_router)] 28 | impl TestServer { 29 | pub fn new() -> Self { 30 | Self { 31 | tool_router: Self::tool_router(), 32 | } 33 | } 34 | 35 | /// Tool that returns Json - should have output schema 36 | #[tool(name = "with-json")] 37 | pub async fn with_json(&self) -> Result, String> { 38 | Ok(Json(TestData { 39 | value: "test".to_string(), 40 | })) 41 | } 42 | 43 | /// Tool that returns regular type - should NOT have output schema 44 | #[tool(name = "without-json")] 45 | pub async fn without_json(&self) -> Result { 46 | Ok("test".to_string()) 47 | } 48 | 49 | /// Tool that returns Result with inner Json - should have output schema 50 | #[tool(name = "result-with-json")] 51 | pub async fn result_with_json(&self) -> Result, rmcp::ErrorData> { 52 | Ok(Json(TestData { 53 | value: "test".to_string(), 54 | })) 55 | } 56 | 57 | /// Tool with explicit output_schema attribute - should have output schema 58 | #[tool(name = "explicit-schema", output_schema = rmcp::handler::server::tool::schema_for_type::())] 59 | pub async fn explicit_schema(&self) -> Result { 60 | Ok("test".to_string()) 61 | } 62 | } 63 | 64 | #[tokio::test] 65 | async fn test_json_type_generates_schema() { 66 | let server = TestServer::new(); 67 | let tools = server.tool_router.list_all(); 68 | 69 | // Find the with-json tool 70 | let json_tool = tools.iter().find(|t| t.name == "with-json").unwrap(); 71 | assert!( 72 | json_tool.output_schema.is_some(), 73 | "Json return type should generate output schema" 74 | ); 75 | } 76 | 77 | #[tokio::test] 78 | async fn test_non_json_type_no_schema() { 79 | let server = TestServer::new(); 80 | let tools = server.tool_router.list_all(); 81 | 82 | // Find the without-json tool 83 | let non_json_tool = tools.iter().find(|t| t.name == "without-json").unwrap(); 84 | assert!( 85 | non_json_tool.output_schema.is_none(), 86 | "Regular return type should NOT generate output schema" 87 | ); 88 | } 89 | 90 | #[tokio::test] 91 | async fn test_result_with_json_generates_schema() { 92 | let server = TestServer::new(); 93 | let tools = server.tool_router.list_all(); 94 | 95 | // Find the result-with-json tool 96 | let result_json_tool = tools.iter().find(|t| t.name == "result-with-json").unwrap(); 97 | assert!( 98 | result_json_tool.output_schema.is_some(), 99 | "Result, E> return type should generate output schema" 100 | ); 101 | } 102 | 103 | #[tokio::test] 104 | async fn test_explicit_schema_override() { 105 | let server = TestServer::new(); 106 | let tools = server.tool_router.list_all(); 107 | 108 | // Find the explicit-schema tool 109 | let explicit_tool = tools.iter().find(|t| t.name == "explicit-schema").unwrap(); 110 | assert!( 111 | explicit_tool.output_schema.is_some(), 112 | "Explicit output_schema attribute should work" 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_message_schema.rs: -------------------------------------------------------------------------------- 1 | mod tests { 2 | use rmcp::model::{ClientJsonRpcMessage, ServerJsonRpcMessage}; 3 | use schemars::generate::SchemaSettings; 4 | 5 | fn compare_schemas(name: &str, actual: &str, expected_file: &str) { 6 | let expected = match std::fs::read_to_string(expected_file) { 7 | Ok(content) => content, 8 | Err(e) => { 9 | panic!( 10 | "Failed to read expected schema file {}: {}", 11 | expected_file, e 12 | ); 13 | } 14 | }; 15 | 16 | let actual_json: serde_json::Value = 17 | serde_json::from_str(actual).expect("Failed to parse actual schema as JSON"); 18 | let expected_json: serde_json::Value = 19 | serde_json::from_str(&expected).expect("Failed to parse expected schema as JSON"); 20 | 21 | if actual_json == expected_json { 22 | println!("{} schema matches expected", name); 23 | return; 24 | } 25 | 26 | // Write current schema to file for comparison 27 | let current_file = expected_file.replace(".json", "_current.json"); 28 | std::fs::write(¤t_file, actual).expect("Failed to write current schema"); 29 | 30 | println!("{} schema differs from expected", name); 31 | println!("Expected: {}", expected_file); 32 | println!("Current: {}", current_file); 33 | println!( 34 | "Run 'diff {} {}' to see differences", 35 | expected_file, current_file 36 | ); 37 | 38 | // UPDATE_SCHEMA=1 cargo test -p rmcp --test test_message_schema --features="server client schemars" 39 | if std::env::var("UPDATE_SCHEMA").is_ok() { 40 | println!("UPDATE_SCHEMA is set, updating expected file"); 41 | std::fs::write(expected_file, actual).expect("Failed to update expected schema file"); 42 | println!("Updated {}", expected_file); 43 | } else { 44 | println!("Set UPDATE_SCHEMA=1 to auto-update expected schemas"); 45 | panic!("Schema validation failed"); 46 | } 47 | } 48 | 49 | #[test] 50 | fn test_client_json_rpc_message_schema() { 51 | let settings = SchemaSettings::draft07(); 52 | let schema = settings 53 | .into_generator() 54 | .into_root_schema_for::(); 55 | let schema_str = serde_json::to_string_pretty(&schema).expect("Failed to serialize schema"); 56 | 57 | compare_schemas( 58 | "ClientJsonRpcMessage", 59 | &schema_str, 60 | "tests/test_message_schema/client_json_rpc_message_schema.json", 61 | ); 62 | } 63 | 64 | #[test] 65 | fn test_server_json_rpc_message_schema() { 66 | let settings = SchemaSettings::draft07(); 67 | let schema = settings 68 | .into_generator() 69 | .into_root_schema_for::(); 70 | let schema_str = serde_json::to_string_pretty(&schema).expect("Failed to serialize schema"); 71 | 72 | compare_schemas( 73 | "ServerJsonRpcMessage", 74 | &schema_str, 75 | "tests/test_message_schema/server_json_rpc_message_schema.json", 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_progress_subscriber.rs: -------------------------------------------------------------------------------- 1 | use futures::StreamExt; 2 | use rmcp::{ 3 | ClientHandler, Peer, RoleServer, ServerHandler, ServiceExt, 4 | handler::{client::progress::ProgressDispatcher, server::tool::ToolRouter}, 5 | model::{CallToolRequestParam, ClientRequest, Meta, ProgressNotificationParam, Request}, 6 | service::PeerRequestOptions, 7 | tool, tool_handler, tool_router, 8 | }; 9 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 10 | 11 | pub struct MyClient { 12 | progress_handler: ProgressDispatcher, 13 | } 14 | 15 | impl MyClient { 16 | pub fn new() -> Self { 17 | Self { 18 | progress_handler: ProgressDispatcher::new(), 19 | } 20 | } 21 | } 22 | 23 | impl Default for MyClient { 24 | fn default() -> Self { 25 | Self::new() 26 | } 27 | } 28 | 29 | impl ClientHandler for MyClient { 30 | async fn on_progress( 31 | &self, 32 | params: rmcp::model::ProgressNotificationParam, 33 | _context: rmcp::service::NotificationContext, 34 | ) { 35 | tracing::info!("Received progress notification: {:?}", params); 36 | self.progress_handler.handle_notification(params).await; 37 | } 38 | } 39 | 40 | pub struct MyServer { 41 | tool_router: ToolRouter, 42 | } 43 | 44 | impl MyServer { 45 | pub fn new() -> Self { 46 | Self { 47 | tool_router: Self::tool_router(), 48 | } 49 | } 50 | } 51 | 52 | impl Default for MyServer { 53 | fn default() -> Self { 54 | Self::new() 55 | } 56 | } 57 | 58 | #[tool_router] 59 | impl MyServer { 60 | #[tool] 61 | pub async fn some_progress( 62 | meta: Meta, 63 | client: Peer, 64 | ) -> Result<(), rmcp::ErrorData> { 65 | let progress_token = meta 66 | .get_progress_token() 67 | .ok_or(rmcp::ErrorData::invalid_params( 68 | "Progress token is required for this tool", 69 | None, 70 | ))?; 71 | for step in 0..10 { 72 | let _ = client 73 | .notify_progress(ProgressNotificationParam { 74 | progress_token: progress_token.clone(), 75 | progress: (step as f64), 76 | total: Some(10.0), 77 | message: Some("Some message".into()), 78 | }) 79 | .await; 80 | tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; 81 | } 82 | Ok(()) 83 | } 84 | } 85 | 86 | #[tool_handler] 87 | impl ServerHandler for MyServer {} 88 | 89 | #[tokio::test] 90 | async fn test_progress_subscriber() -> anyhow::Result<()> { 91 | let _ = tracing_subscriber::registry() 92 | .with( 93 | tracing_subscriber::EnvFilter::try_from_default_env() 94 | .unwrap_or_else(|_| "debug".to_string().into()), 95 | ) 96 | .with(tracing_subscriber::fmt::layer()) 97 | .try_init(); 98 | let client = MyClient::new(); 99 | 100 | let server = MyServer::new(); 101 | let (transport_server, transport_client) = tokio::io::duplex(4096); 102 | tokio::spawn(async move { 103 | let service = server.serve(transport_server).await?; 104 | service.waiting().await?; 105 | anyhow::Ok(()) 106 | }); 107 | let client_service = client.serve(transport_client).await?; 108 | let handle = client_service 109 | .send_cancellable_request( 110 | ClientRequest::CallToolRequest(Request::new(CallToolRequestParam { 111 | name: "some_progress".into(), 112 | arguments: None, 113 | })), 114 | PeerRequestOptions::no_options(), 115 | ) 116 | .await?; 117 | let mut progress_subscriber = client_service 118 | .service() 119 | .progress_handler 120 | .subscribe(handle.progress_token.clone()) 121 | .await; 122 | tokio::spawn(async move { 123 | while let Some(notification) = progress_subscriber.next().await { 124 | tracing::info!("Progress notification: {:?}", notification); 125 | } 126 | }); 127 | let _response = handle.await_response().await?; 128 | 129 | // Simulate some delay to allow the async task to complete 130 | tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; 131 | Ok(()) 132 | } 133 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_prompt_handler.rs: -------------------------------------------------------------------------------- 1 | //cargo test --test test_prompt_handler --features "client server" 2 | // Tests for verifying that the #[prompt_handler] macro correctly generates 3 | // the ServerHandler trait implementation methods. 4 | #![allow(dead_code)] 5 | 6 | use rmcp::{ 7 | RoleServer, ServerHandler, 8 | handler::server::router::prompt::PromptRouter, 9 | model::{GetPromptRequestParam, GetPromptResult, ListPromptsResult, PaginatedRequestParam}, 10 | prompt_handler, 11 | service::RequestContext, 12 | }; 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct TestPromptServer { 16 | prompt_router: PromptRouter, 17 | } 18 | 19 | impl Default for TestPromptServer { 20 | fn default() -> Self { 21 | Self::new() 22 | } 23 | } 24 | 25 | impl TestPromptServer { 26 | pub fn new() -> Self { 27 | Self { 28 | prompt_router: PromptRouter::new(), 29 | } 30 | } 31 | } 32 | 33 | #[prompt_handler] 34 | impl ServerHandler for TestPromptServer {} 35 | 36 | #[derive(Debug, Clone)] 37 | pub struct CustomRouterServer { 38 | custom_router: PromptRouter, 39 | } 40 | 41 | impl Default for CustomRouterServer { 42 | fn default() -> Self { 43 | Self::new() 44 | } 45 | } 46 | 47 | impl CustomRouterServer { 48 | pub fn new() -> Self { 49 | Self { 50 | custom_router: PromptRouter::new(), 51 | } 52 | } 53 | 54 | pub fn get_custom_router(&self) -> &PromptRouter { 55 | &self.custom_router 56 | } 57 | } 58 | 59 | #[prompt_handler(router = self.custom_router)] 60 | impl ServerHandler for CustomRouterServer {} 61 | 62 | #[derive(Debug, Clone)] 63 | pub struct GenericPromptServer { 64 | prompt_router: PromptRouter, 65 | _marker: std::marker::PhantomData, 66 | } 67 | 68 | impl Default for GenericPromptServer { 69 | fn default() -> Self { 70 | Self::new() 71 | } 72 | } 73 | 74 | impl GenericPromptServer { 75 | pub fn new() -> Self { 76 | Self { 77 | prompt_router: PromptRouter::new(), 78 | _marker: std::marker::PhantomData, 79 | } 80 | } 81 | } 82 | 83 | #[prompt_handler] 84 | impl ServerHandler for GenericPromptServer {} 85 | 86 | #[test] 87 | fn test_prompt_handler_basic() { 88 | let server = TestPromptServer::new(); 89 | 90 | // Test that the server implements ServerHandler 91 | fn assert_server_handler(_: &T) {} 92 | assert_server_handler(&server); 93 | 94 | // Test that the prompt router is accessible 95 | assert_eq!(server.prompt_router.list_all().len(), 0); 96 | } 97 | 98 | #[test] 99 | fn test_prompt_handler_custom_router() { 100 | let server = CustomRouterServer::new(); 101 | 102 | // Test that the server implements ServerHandler 103 | fn assert_server_handler(_: &T) {} 104 | assert_server_handler(&server); 105 | 106 | // Test that the custom router is used 107 | assert_eq!(server.custom_router.list_all().len(), 0); 108 | } 109 | 110 | #[test] 111 | fn test_prompt_handler_with_generics() { 112 | let server = GenericPromptServer::::new(); 113 | 114 | // Test that generic server implements ServerHandler 115 | fn assert_server_handler(_: &T) {} 116 | assert_server_handler(&server); 117 | 118 | // Test with a different generic type 119 | let server2 = GenericPromptServer::::new(); 120 | assert_server_handler(&server2); 121 | } 122 | 123 | #[test] 124 | fn test_prompt_handler_trait_implementation() { 125 | // This test verifies that the prompt_handler macro generates proper ServerHandler implementation 126 | // The actual method signatures are tested through the ServerHandler trait bound 127 | fn compile_time_check() {} 128 | 129 | compile_time_check::(); 130 | compile_time_check::(); 131 | compile_time_check::>(); 132 | } 133 | 134 | // Test that the macro works with different server configurations 135 | mod nested { 136 | use super::*; 137 | 138 | #[derive(Debug, Clone)] 139 | pub struct NestedServer { 140 | prompt_router: PromptRouter, 141 | } 142 | 143 | impl NestedServer { 144 | pub fn new() -> Self { 145 | Self { 146 | prompt_router: PromptRouter::new(), 147 | } 148 | } 149 | } 150 | 151 | #[prompt_handler] 152 | impl ServerHandler for NestedServer {} 153 | 154 | #[test] 155 | fn test_nested_prompt_handler() { 156 | let server = NestedServer::new(); 157 | // Verify it implements ServerHandler 158 | fn assert_server_handler(_: &T) {} 159 | assert_server_handler(&server); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_prompt_routers.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use futures::future::BoxFuture; 4 | use rmcp::{ 5 | ServerHandler, 6 | handler::server::wrapper::Parameters, 7 | model::{GetPromptResult, PromptMessage, PromptMessageRole}, 8 | }; 9 | 10 | #[derive(Debug, Default)] 11 | pub struct TestHandler { 12 | pub _marker: std::marker::PhantomData, 13 | } 14 | 15 | impl ServerHandler for TestHandler {} 16 | 17 | #[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)] 18 | pub struct Request { 19 | pub fields: HashMap, 20 | } 21 | 22 | #[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)] 23 | pub struct Sum { 24 | pub a: i32, 25 | pub b: i32, 26 | } 27 | 28 | #[rmcp::prompt_router(router = "test_router")] 29 | impl TestHandler { 30 | #[rmcp::prompt] 31 | async fn async_method( 32 | &self, 33 | Parameters(Request { fields }): Parameters, 34 | ) -> Vec { 35 | drop(fields); 36 | vec![PromptMessage::new_text( 37 | PromptMessageRole::Assistant, 38 | "Async method response", 39 | )] 40 | } 41 | 42 | #[rmcp::prompt] 43 | fn sync_method( 44 | &self, 45 | Parameters(Request { fields }): Parameters, 46 | ) -> Vec { 47 | drop(fields); 48 | vec![PromptMessage::new_text( 49 | PromptMessageRole::Assistant, 50 | "Sync method response", 51 | )] 52 | } 53 | } 54 | 55 | #[rmcp::prompt] 56 | async fn async_function(Parameters(Request { fields }): Parameters) -> Vec { 57 | drop(fields); 58 | vec![PromptMessage::new_text( 59 | PromptMessageRole::Assistant, 60 | "Async function response", 61 | )] 62 | } 63 | 64 | #[rmcp::prompt] 65 | fn async_function2(_callee: &TestHandler) -> BoxFuture<'_, GetPromptResult> { 66 | Box::pin(async move { 67 | GetPromptResult { 68 | description: Some("Async function 2".to_string()), 69 | messages: vec![PromptMessage::new_text( 70 | PromptMessageRole::Assistant, 71 | "Async function 2 response", 72 | )], 73 | } 74 | }) 75 | } 76 | 77 | #[test] 78 | fn test_prompt_router() { 79 | let test_prompt_router = TestHandler::<()>::test_router() 80 | .with_route(rmcp::handler::server::router::prompt::PromptRoute::new_dyn( 81 | async_function_prompt_attr(), 82 | |mut context| { 83 | Box::pin(async move { 84 | use rmcp::handler::server::{ 85 | common::FromContextPart, prompt::IntoGetPromptResult, 86 | }; 87 | let params = Parameters::::from_context_part(&mut context)?; 88 | let result = async_function(params).await; 89 | result.into_get_prompt_result() 90 | }) 91 | }, 92 | )) 93 | .with_route(rmcp::handler::server::router::prompt::PromptRoute::new_dyn( 94 | async_function2_prompt_attr(), 95 | |context| { 96 | Box::pin(async move { 97 | use rmcp::handler::server::prompt::IntoGetPromptResult; 98 | let result = async_function2(context.server).await; 99 | result.into_get_prompt_result() 100 | }) 101 | }, 102 | )); 103 | let prompts = test_prompt_router.list_all(); 104 | assert_eq!(prompts.len(), 4); 105 | } 106 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_resource_link.rs: -------------------------------------------------------------------------------- 1 | use rmcp::model::{CallToolResult, Content, RawResource}; 2 | 3 | #[test] 4 | fn test_resource_link_in_tool_result() { 5 | // Test creating a tool result with resource links 6 | let resource = RawResource::new("file:///test/file.txt", "test.txt"); 7 | 8 | // Create a tool result with a resource link 9 | let result = CallToolResult::success(vec![ 10 | Content::text("Found a file"), 11 | Content::resource_link(resource), 12 | ]); 13 | 14 | // Serialize to JSON to verify format 15 | let json = serde_json::to_string_pretty(&result).unwrap(); 16 | println!("Tool result with resource link:\n{}", json); 17 | 18 | // Verify JSON contains expected structure 19 | assert!( 20 | json.contains("\"type\":\"resource_link\"") || json.contains("\"type\": \"resource_link\"") 21 | ); 22 | assert!( 23 | json.contains("\"uri\":\"file:///test/file.txt\"") 24 | || json.contains("\"uri\": \"file:///test/file.txt\"") 25 | ); 26 | assert!(json.contains("\"name\":\"test.txt\"") || json.contains("\"name\": \"test.txt\"")); 27 | 28 | // Test deserialization 29 | let deserialized: CallToolResult = serde_json::from_str(&json).unwrap(); 30 | assert_eq!(deserialized.content.len(), 2); 31 | 32 | // Check the text content 33 | assert!(deserialized.content[0].as_text().is_some()); 34 | 35 | // Check the resource link 36 | let resource_link = deserialized.content[1] 37 | .as_resource_link() 38 | .expect("Expected resource link in content[1]"); 39 | assert_eq!(resource_link.uri, "file:///test/file.txt"); 40 | assert_eq!(resource_link.name, "test.txt"); 41 | } 42 | 43 | #[test] 44 | fn test_resource_link_with_full_metadata() { 45 | let mut resource = RawResource::new("https://example.com/data.json", "API Data"); 46 | resource.description = Some("JSON data from external API".to_string()); 47 | resource.mime_type = Some("application/json".to_string()); 48 | resource.size = Some(1024); 49 | 50 | let result = CallToolResult::success(vec![Content::resource_link(resource)]); 51 | 52 | let json = serde_json::to_string(&result).unwrap(); 53 | let deserialized: CallToolResult = serde_json::from_str(&json).unwrap(); 54 | 55 | let resource_link = deserialized.content[0] 56 | .as_resource_link() 57 | .expect("Expected resource link"); 58 | assert_eq!(resource_link.uri, "https://example.com/data.json"); 59 | assert_eq!(resource_link.name, "API Data"); 60 | assert_eq!( 61 | resource_link.description, 62 | Some("JSON data from external API".to_string()) 63 | ); 64 | assert_eq!( 65 | resource_link.mime_type, 66 | Some("application/json".to_string()) 67 | ); 68 | assert_eq!(resource_link.size, Some(1024)); 69 | } 70 | 71 | #[test] 72 | fn test_mixed_content_types() { 73 | // Test that resource links can be mixed with other content types 74 | let resource = RawResource::new("file:///doc.pdf", "Document"); 75 | 76 | let result = CallToolResult::success(vec![ 77 | Content::text("Processing complete"), 78 | Content::resource_link(resource), 79 | Content::embedded_text("memo://result", "Analysis results here"), 80 | ]); 81 | 82 | assert_eq!(result.content.len(), 3); 83 | assert!(result.content[0].as_text().is_some()); 84 | assert!(result.content[1].as_resource_link().is_some()); 85 | assert!(result.content[2].as_resource().is_some()); 86 | } 87 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_tool_builder_methods.rs: -------------------------------------------------------------------------------- 1 | //cargo test --test test_tool_builder_methods --features "client server macros" 2 | use rmcp::model::{JsonObject, Tool}; 3 | use schemars::JsonSchema; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Serialize, Deserialize, JsonSchema)] 7 | pub struct InputData { 8 | pub name: String, 9 | pub age: u32, 10 | } 11 | 12 | #[derive(Serialize, Deserialize, JsonSchema)] 13 | pub struct OutputData { 14 | pub greeting: String, 15 | pub is_adult: bool, 16 | } 17 | 18 | #[test] 19 | fn test_with_output_schema() { 20 | let tool = Tool::new("test", "Test tool", JsonObject::new()).with_output_schema::(); 21 | 22 | assert!(tool.output_schema.is_some()); 23 | 24 | // Verify the schema contains expected fields 25 | let schema_str = serde_json::to_string(tool.output_schema.as_ref().unwrap()).unwrap(); 26 | assert!(schema_str.contains("greeting")); 27 | assert!(schema_str.contains("is_adult")); 28 | } 29 | 30 | #[test] 31 | fn test_with_input_schema() { 32 | let tool = Tool::new("test", "Test tool", JsonObject::new()).with_input_schema::(); 33 | 34 | // Verify the schema contains expected fields 35 | let schema_str = serde_json::to_string(&tool.input_schema).unwrap(); 36 | assert!(schema_str.contains("name")); 37 | assert!(schema_str.contains("age")); 38 | } 39 | 40 | #[test] 41 | fn test_chained_builder_methods() { 42 | let tool = Tool::new("test", "Test tool", JsonObject::new()) 43 | .with_input_schema::() 44 | .with_output_schema::() 45 | .annotate(rmcp::model::ToolAnnotations::new().read_only(true)); 46 | 47 | assert!(tool.output_schema.is_some()); 48 | assert!(tool.annotations.is_some()); 49 | assert_eq!( 50 | tool.annotations.as_ref().unwrap().read_only_hint, 51 | Some(true) 52 | ); 53 | 54 | // Verify both schemas are set correctly 55 | let input_schema_str = serde_json::to_string(&tool.input_schema).unwrap(); 56 | assert!(input_schema_str.contains("name")); 57 | assert!(input_schema_str.contains("age")); 58 | 59 | let output_schema_str = serde_json::to_string(tool.output_schema.as_ref().unwrap()).unwrap(); 60 | assert!(output_schema_str.contains("greeting")); 61 | assert!(output_schema_str.contains("is_adult")); 62 | } 63 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_tool_handler.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_tool_macro_annotations.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use rmcp::{ServerHandler, handler::server::router::tool::ToolRouter, tool, tool_handler}; 4 | 5 | #[derive(Debug, Clone, Default)] 6 | pub struct AnnotatedServer { 7 | tool_router: ToolRouter, 8 | } 9 | 10 | impl AnnotatedServer { 11 | // Tool with inline comments for documentation 12 | /// Direct annotation test tool 13 | /// This is used to test tool annotations 14 | #[tool( 15 | name = "direct-annotated-tool", 16 | annotations(title = "Annotated Tool", read_only_hint = true) 17 | )] 18 | pub async fn direct_annotated_tool(&self, input: String) -> String { 19 | format!("Direct: {}", input) 20 | } 21 | } 22 | #[tool_handler] 23 | impl ServerHandler for AnnotatedServer {} 24 | 25 | #[test] 26 | fn test_direct_tool_attributes() { 27 | // Get the tool definition 28 | let tool = AnnotatedServer::direct_annotated_tool_tool_attr(); 29 | 30 | // Verify basic properties 31 | assert_eq!(tool.name, "direct-annotated-tool"); 32 | 33 | // Verify description is extracted from doc comments 34 | assert!(tool.description.is_some()); 35 | assert!( 36 | tool.description 37 | .as_ref() 38 | .unwrap() 39 | .contains("Direct annotation test tool") 40 | ); 41 | 42 | let annotations = tool.annotations.unwrap(); 43 | assert_eq!(annotations.title.as_ref().unwrap(), "Annotated Tool"); 44 | assert_eq!(annotations.read_only_hint, Some(true)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_tool_result_meta.rs: -------------------------------------------------------------------------------- 1 | use rmcp::model::{CallToolResult, Content, Meta}; 2 | use serde_json::{Value, json}; 3 | 4 | #[test] 5 | fn serialize_tool_result_with_meta() { 6 | let content = vec![Content::text("ok")]; 7 | let mut meta = Meta::new(); 8 | meta.insert("foo".to_string(), json!("bar")); 9 | let result = CallToolResult { 10 | content, 11 | structured_content: None, 12 | is_error: Some(false), 13 | meta: Some(meta), 14 | }; 15 | let v = serde_json::to_value(&result).unwrap(); 16 | let expected = json!({ 17 | "content": [{"type":"text","text":"ok"}], 18 | "isError": false, 19 | "_meta": {"foo":"bar"} 20 | }); 21 | assert_eq!(v, expected); 22 | } 23 | 24 | #[test] 25 | fn deserialize_tool_result_with_meta() { 26 | let raw: Value = json!({ 27 | "content": [{"type":"text","text":"hello"}], 28 | "isError": true, 29 | "_meta": {"a": 1, "b": "two"} 30 | }); 31 | let result: CallToolResult = serde_json::from_value(raw).unwrap(); 32 | assert_eq!(result.is_error, Some(true)); 33 | assert_eq!(result.content.len(), 1); 34 | let meta = result.meta.expect("meta should exist"); 35 | assert_eq!(meta.get("a").unwrap(), &json!(1)); 36 | assert_eq!(meta.get("b").unwrap(), &json!("two")); 37 | } 38 | 39 | #[test] 40 | fn serialize_tool_result_without_meta_omits_field() { 41 | let result = CallToolResult::success(vec![Content::text("no meta")]); 42 | let v = serde_json::to_value(&result).unwrap(); 43 | // Ensure _meta is omitted 44 | assert!(v.get("_meta").is_none()); 45 | } 46 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_tool_routers.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use futures::future::BoxFuture; 4 | use rmcp::{ 5 | ServerHandler, 6 | handler::server::{router::tool::ToolRouter, tool::CallToolHandler, wrapper::Parameters}, 7 | }; 8 | 9 | #[derive(Debug, Default)] 10 | pub struct TestHandler { 11 | pub _marker: std::marker::PhantomData, 12 | } 13 | 14 | impl ServerHandler for TestHandler {} 15 | #[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)] 16 | pub struct Request { 17 | pub fields: HashMap, 18 | } 19 | 20 | #[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)] 21 | pub struct Sum { 22 | pub a: i32, 23 | pub b: i32, 24 | } 25 | 26 | #[rmcp::tool_router(router = test_router_1)] 27 | impl TestHandler { 28 | #[rmcp::tool] 29 | async fn async_method(&self, Parameters(Request { fields }): Parameters) { 30 | drop(fields) 31 | } 32 | } 33 | 34 | #[rmcp::tool_router(router = test_router_2)] 35 | impl TestHandler { 36 | #[rmcp::tool] 37 | fn sync_method(&self, Parameters(Request { fields }): Parameters) { 38 | drop(fields) 39 | } 40 | } 41 | 42 | #[rmcp::tool] 43 | async fn async_function(Parameters(Request { fields }): Parameters) { 44 | drop(fields) 45 | } 46 | 47 | #[rmcp::tool] 48 | fn async_function2(_callee: &TestHandler) -> BoxFuture<'_, ()> { 49 | Box::pin(async move {}) 50 | } 51 | 52 | #[test] 53 | fn test_tool_router() { 54 | let test_tool_router: ToolRouter> = ToolRouter::>::new() 55 | .with_route((async_function_tool_attr(), async_function)) 56 | .with_route((async_function2_tool_attr(), async_function2)) 57 | + TestHandler::<()>::test_router_1() 58 | + TestHandler::<()>::test_router_2(); 59 | let tools = test_tool_router.list_all(); 60 | assert_eq!(tools.len(), 4); 61 | assert_handler(TestHandler::<()>::async_method); 62 | } 63 | 64 | fn assert_handler(_handler: H) 65 | where 66 | H: CallToolHandler, 67 | { 68 | } 69 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_with_js/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /crates/rmcp/tests/test_with_js/client.js: -------------------------------------------------------------------------------- 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 2 | import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; 3 | 4 | const transport = new SSEClientTransport(new URL(`http://127.0.0.1:8000/sse`)); 5 | 6 | const client = new Client( 7 | { 8 | name: "example-client", 9 | version: "1.0.0" 10 | }, 11 | { 12 | capabilities: { 13 | prompts: {}, 14 | resources: {}, 15 | tools: {} 16 | } 17 | } 18 | ); 19 | await client.connect(transport); 20 | const tools = await client.listTools(); 21 | console.log(tools); 22 | const resources = await client.listResources(); 23 | console.log(resources); 24 | const templates = await client.listResourceTemplates(); 25 | console.log(templates); 26 | const prompts = await client.listPrompts(); 27 | console.log(prompts); 28 | await client.close(); 29 | await transport.close(); -------------------------------------------------------------------------------- /crates/rmcp/tests/test_with_js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@modelcontextprotocol/sdk": "^1.10", 4 | "eventsource-parser": "^3.0.1", 5 | "express": "^5.1.0" 6 | }, 7 | "type": "module", 8 | "name": "test_with_ts", 9 | "version": "1.0.0", 10 | "main": "index.js", 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "description": "" 17 | } 18 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_with_js/server.js: -------------------------------------------------------------------------------- 1 | import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { z } from "zod"; 4 | 5 | const server = new McpServer({ 6 | name: "Demo", 7 | version: "1.0.0" 8 | }); 9 | 10 | server.resource( 11 | "greeting", 12 | new ResourceTemplate("greeting://{name}", { list: undefined }), 13 | async (uri, { name }) => ({ 14 | contents: [{ 15 | uri: uri.href, 16 | text: `Hello, ${name}` 17 | }] 18 | }) 19 | ); 20 | 21 | server.tool( 22 | "add", 23 | { a: z.number(), b: z.number() }, 24 | async ({ a, b }) => ({ 25 | "content": [ 26 | { 27 | "type": "text", 28 | "text": `${a + b}` 29 | } 30 | ] 31 | }) 32 | ); 33 | 34 | const transport = new StdioServerTransport(); 35 | await server.connect(transport); -------------------------------------------------------------------------------- /crates/rmcp/tests/test_with_js/streamable_client.js: -------------------------------------------------------------------------------- 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 2 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; 3 | 4 | const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:8001/mcp/`)); 5 | 6 | const client = new Client( 7 | { 8 | name: "example-client", 9 | version: "1.0.0" 10 | }, 11 | { 12 | capabilities: { 13 | prompts: {}, 14 | resources: {}, 15 | tools: {} 16 | } 17 | } 18 | ); 19 | await client.connect(transport); 20 | const tools = await client.listTools(); 21 | console.log(tools); 22 | const resources = await client.listResources(); 23 | console.log(resources); 24 | const templates = await client.listResourceTemplates(); 25 | console.log(templates); 26 | const prompts = await client.listPrompts(); 27 | console.log(prompts); 28 | await client.close(); 29 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_with_js/streamable_server.js: -------------------------------------------------------------------------------- 1 | import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; 3 | import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js" 4 | import { randomUUID } from "node:crypto" 5 | import { z } from "zod"; 6 | import express from "express" 7 | 8 | const app = express(); 9 | app.use(express.json()); 10 | 11 | // Map to store transports by session ID 12 | const transports = {}; 13 | 14 | // Handle POST requests for client-to-server communication 15 | app.post('/mcp', async (req, res) => { 16 | // Check for existing session ID 17 | const sessionId = req.headers['mcp-session-id']; 18 | let transport; 19 | 20 | if (sessionId && transports[sessionId]) { 21 | // Reuse existing transport 22 | transport = transports[sessionId]; 23 | } else if (!sessionId && isInitializeRequest(req.body)) { 24 | // New initialization request 25 | transport = new StreamableHTTPServerTransport({ 26 | sessionIdGenerator: () => randomUUID().toString(), 27 | onsessioninitialized: (sessionId) => { 28 | // Store the transport by session ID 29 | transports[sessionId] = transport; 30 | } 31 | }); 32 | 33 | // Clean up transport when closed 34 | transport.onclose = () => { 35 | if (transport.sessionId) { 36 | delete transports[transport.sessionId]; 37 | } 38 | }; 39 | const server = new McpServer({ 40 | name: "example-server", 41 | version: "1.0.0" 42 | }); 43 | 44 | server.resource( 45 | "greeting", 46 | new ResourceTemplate("greeting://{name}", { list: undefined }), 47 | async (uri, { name }) => ({ 48 | contents: [{ 49 | uri: uri.href, 50 | text: `Hello, ${name}` 51 | }] 52 | }) 53 | ); 54 | 55 | server.tool( 56 | "add", 57 | { a: z.number(), b: z.number() }, 58 | async ({ a, b }) => ({ 59 | "content": [ 60 | { 61 | "type": "text", 62 | "text": `${a + b}` 63 | } 64 | ] 65 | }) 66 | ); 67 | 68 | // Connect to the MCP server 69 | await server.connect(transport); 70 | } else { 71 | // Invalid request 72 | res.status(400).json({ 73 | jsonrpc: '2.0', 74 | error: { 75 | code: -32000, 76 | message: 'Bad Request: No valid session ID provided', 77 | }, 78 | id: null, 79 | }); 80 | return; 81 | } 82 | 83 | // Handle the request 84 | await transport.handleRequest(req, res, req.body); 85 | }); 86 | 87 | // Reusable handler for GET and DELETE requests 88 | const handleSessionRequest = async (req, res) => { 89 | const sessionId = req.headers['mcp-session-id']; 90 | if (!sessionId || !transports[sessionId]) { 91 | res.status(400).send('Invalid or missing session ID'); 92 | return; 93 | } 94 | 95 | const transport = transports[sessionId]; 96 | await transport.handleRequest(req, res); 97 | }; 98 | 99 | // Handle GET requests for server-to-client notifications via SSE 100 | app.get('/mcp', handleSessionRequest); 101 | 102 | // Handle DELETE requests for session termination 103 | app.delete('/mcp', handleSessionRequest); 104 | console.log("Listening on port 8002"); 105 | app.listen(8002); -------------------------------------------------------------------------------- /crates/rmcp/tests/test_with_python.rs: -------------------------------------------------------------------------------- 1 | use std::process::Stdio; 2 | 3 | use rmcp::{ 4 | ServiceExt, 5 | transport::{ConfigureCommandExt, TokioChildProcess}, 6 | }; 7 | use tokio::io::AsyncReadExt; 8 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 9 | mod common; 10 | 11 | async fn init() -> anyhow::Result<()> { 12 | let _ = tracing_subscriber::registry() 13 | .with( 14 | tracing_subscriber::EnvFilter::try_from_default_env() 15 | .unwrap_or_else(|_| "debug".to_string().into()), 16 | ) 17 | .with(tracing_subscriber::fmt::layer()) 18 | .try_init(); 19 | tokio::process::Command::new("uv") 20 | .args(["sync"]) 21 | .current_dir("tests/test_with_python") 22 | .spawn()? 23 | .wait() 24 | .await?; 25 | Ok(()) 26 | } 27 | 28 | #[tokio::test] 29 | async fn test_with_python_server() -> anyhow::Result<()> { 30 | init().await?; 31 | 32 | let transport = TokioChildProcess::new(tokio::process::Command::new("uv").configure(|cmd| { 33 | cmd.arg("run") 34 | .arg("server.py") 35 | .current_dir("tests/test_with_python"); 36 | }))?; 37 | 38 | let client = ().serve(transport).await?; 39 | let resources = client.list_all_resources().await?; 40 | tracing::info!("{:#?}", resources); 41 | let tools = client.list_all_tools().await?; 42 | tracing::info!("{:#?}", tools); 43 | client.cancel().await?; 44 | Ok(()) 45 | } 46 | 47 | #[tokio::test] 48 | async fn test_with_python_server_stderr() -> anyhow::Result<()> { 49 | init().await?; 50 | 51 | let (transport, stderr) = 52 | TokioChildProcess::builder(tokio::process::Command::new("uv").configure(|cmd| { 53 | cmd.arg("run") 54 | .arg("server.py") 55 | .current_dir("tests/test_with_python"); 56 | })) 57 | .stderr(Stdio::piped()) 58 | .spawn()?; 59 | 60 | let mut stderr = stderr.expect("stderr must be piped"); 61 | 62 | let stderr_task = tokio::spawn(async move { 63 | let mut buffer = String::new(); 64 | stderr.read_to_string(&mut buffer).await?; 65 | Ok::<_, std::io::Error>(buffer) 66 | }); 67 | 68 | let client = ().serve(transport).await?; 69 | let _ = client.list_all_resources().await?; 70 | let _ = client.list_all_tools().await?; 71 | client.cancel().await?; 72 | 73 | let stderr_output = stderr_task.await??; 74 | assert!(stderr_output.contains("server starting up...")); 75 | 76 | Ok(()) 77 | } 78 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_with_python/.gitignore: -------------------------------------------------------------------------------- 1 | # Lock files 2 | *.lock 3 | 4 | # Python build artifacts 5 | *.egg-info/ 6 | build/ 7 | dist/ 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # Virtual environments 13 | venv/ 14 | env/ 15 | .venv/ 16 | .env/ 17 | 18 | # IDE 19 | .vscode/ 20 | .idea/ 21 | *.swp 22 | *.swo 23 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_with_python/client.py: -------------------------------------------------------------------------------- 1 | from mcp import ClientSession, StdioServerParameters, types 2 | from mcp.client.sse import sse_client 3 | import sys 4 | 5 | async def run(): 6 | url = sys.argv[1] 7 | async with sse_client(url) as (read, write): 8 | async with ClientSession( 9 | read, write 10 | ) as session: 11 | # Initialize the connection 12 | await session.initialize() 13 | 14 | # List available prompts 15 | prompts = await session.list_prompts() 16 | print(prompts) 17 | # List available resources 18 | resources = await session.list_resources() 19 | print(resources) 20 | 21 | # List available tools 22 | tools = await session.list_tools() 23 | print(tools) 24 | 25 | if __name__ == "__main__": 26 | import asyncio 27 | 28 | asyncio.run(run()) 29 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_with_python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "test_with_python" 7 | version = "0.1.0" 8 | description = "Test Python client for RMCP" 9 | dependencies = [ 10 | "fastmcp", 11 | ] 12 | 13 | [tool.setuptools] 14 | py-modules = ["client", "server"] 15 | -------------------------------------------------------------------------------- /crates/rmcp/tests/test_with_python/server.py: -------------------------------------------------------------------------------- 1 | from fastmcp import FastMCP 2 | 3 | import sys 4 | 5 | mcp = FastMCP("Demo") 6 | 7 | print("server starting up...", file=sys.stderr) 8 | 9 | 10 | @mcp.tool() 11 | def add(a: int, b: int) -> int: 12 | """Add two numbers""" 13 | return a + b 14 | 15 | 16 | # Add a dynamic greeting resource 17 | @mcp.resource("greeting://{name}") 18 | def get_greeting(name: str) -> str: 19 | """Get a personalized greeting""" 20 | return f"Hello, {name}!" 21 | 22 | 23 | 24 | if __name__ == "__main__": 25 | mcp.run() -------------------------------------------------------------------------------- /docs/CONTRIBUTE.MD: -------------------------------------------------------------------------------- 1 | # Discuss first 2 | If you have a idea, make sure it is discussed before you make a PR. 3 | 4 | # Fmt And Clippy 5 | You can use [just](https://github.com/casey/just) to help you fix your commit rapidly: 6 | ```shell 7 | just fix 8 | ``` 9 | 10 | # How Can I Rewrite My Commit Message? 11 | You can `git reset --soft upstream/main` and `git commit --forge`, this will merge your changes into one commit. 12 | 13 | Or you also can use git rebase. But we will still merge them into one commit when it is merged. 14 | 15 | # Check Code Coverage 16 | If you are developing on vscode, you can use vscode plugin [Coverage Gutters](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) 17 | 18 | And also need to install llvm-cov 19 | ```sh 20 | cargo install cargo-llvm-cov 21 | 22 | rustup component add llvm-tools-preview 23 | ``` 24 | 25 | If you are using coverage gutters plugin, add these config to let it know lcov output. 26 | ```json 27 | { 28 | "coverage-gutters.coverageFileNames": [ 29 | "coverage.lcov", 30 | ], 31 | "coverage-gutters.coverageBaseDir": "target/llvm-cov-target", 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/DEVCONTAINER.md: -------------------------------------------------------------------------------- 1 | ## Development with Dev Container and GitHub Codespaces 2 | 3 | This repository provides a Dev Container to easily set up a development environment. Using Dev Container allows you to work in a consistent development environment with pre-configured dependencies and tools, whether locally or in the cloud with GitHub Codespaces. 4 | 5 | ### Prerequisites 6 | 7 | **For Local Development:** 8 | 9 | * [Docker Desktop](https://www.docker.com/products/docker-desktop/) or any other compatible container runtime (e.g., Podman, OrbStack) installed. 10 | * [Visual Studio Code](https://code.visualstudio.com/) with the [Remote - Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed. 11 | 12 | **For GitHub Codespaces:** 13 | 14 | * A GitHub account. 15 | 16 | ### Starting Dev Container 17 | 18 | **Using Visual Studio Code (Local):** 19 | 20 | 1. Clone the repository. 21 | 2. Open the repository in Visual Studio Code. 22 | 3. Open the command palette in Visual Studio Code (`Ctrl + Shift + P` or `Cmd + Shift + P`) and execute `Dev Containers: Reopen in Container`. 23 | 24 | **Using GitHub Codespaces (Cloud):** 25 | 26 | 1. Navigate to the repository on GitHub. 27 | 2. Click the "<> Code" button. 28 | 3. Select the "Codespaces" tab. 29 | 4. Click "Create codespace on main" (or your desired branch). 30 | 31 | ### Dev Container Configuration 32 | 33 | Dev Container settings are configured in `.devcontainer/devcontainer.json`. In this file, you can set the Docker image to use, extensions to install, port forwarding, and more. This configuration is used both for local development and GitHub Codespaces. 34 | 35 | ### Development 36 | 37 | Once the Dev Container is started, you can proceed with development as usual. The container already has the necessary tools and libraries installed. In GitHub Codespaces, you will have a fully configured VS Code in your browser or desktop application. 38 | 39 | ### Stopping Dev Container 40 | 41 | **Using Visual Studio Code (Local):** 42 | 43 | To stop the Dev Container, open the command palette in Visual Studio Code and execute `Remote: Close Remote Connection`. 44 | 45 | **Using GitHub Codespaces (Cloud):** 46 | 47 | GitHub Codespaces will automatically stop after a period of inactivity. You can also manually stop the codespace from the Codespaces menu in GitHub. 48 | 49 | ### More Information 50 | 51 | * [Visual Studio Code Dev Containers](https://code.visualstudio.com/docs/remote/containers) 52 | * [Dev Container Specification](https://containers.dev/implementors/json_reference/) 53 | * [GitHub Codespaces](https://github.com/features/codespaces) 54 | 55 | This document describes the basic usage of Dev Container and GitHub Codespaces. Add project-specific settings and procedures as needed. -------------------------------------------------------------------------------- /docs/coverage.svg: -------------------------------------------------------------------------------- 1 | Coverage: 53%Coverage53% -------------------------------------------------------------------------------- /docs/readme/README.zh-cn.md: -------------------------------------------------------------------------------- 1 | # RMCP 2 | [![Crates.io Version](https://img.shields.io/crates/v/rmcp)](https://crates.io/crates/rmcp) 3 | ![Release status](https://github.commodelcontextprotocol/rust-sdk/actions/workflows/release.yml/badge.svg) 4 | [![docs.rs](https://img.shields.io/docsrs/rmcp)](https://docs.rs/rmcp/latest/rmcp) 5 | 6 | 一个基于 tokio 异步运行时的官方 Model Context Protocol SDK 实现。 7 | 8 | 本项目使用了以下开源库: 9 | 10 | - [rmcp](crates/rmcp): 实现 RMCP 协议的核心库 (详见:[rmcp](crates/rmcp/README.md)) 11 | - [rmcp-macros](crates/rmcp-macros): 一个用于生成 RMCP 工具实现的过程宏库。 (详见:[rmcp-macros](crates/rmcp-macros/README.md)) 12 | 13 | ## 使用 14 | 15 | ### 导入 16 | ```toml 17 | rmcp = { version = "0.2.0", features = ["server"] } 18 | ## 或使用最新开发版本 19 | rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", branch = "main" } 20 | ``` 21 | 22 | ### 第三方依赖库 23 | 基本依赖: 24 | - [tokio required](https://github.com/tokio-rs/tokio) 25 | - [serde required](https://github.com/serde-rs/serde) 26 | 27 | ### 构建客户端 28 |

29 | 构建客户端 30 | 31 | ```rust, ignore 32 | use rmcp::{ServiceExt, transport::{TokioChildProcess, ConfigureCommandExt}}; 33 | use tokio::process::Command; 34 | 35 | #[tokio::main] 36 | async fn main() -> Result<(), Box> { 37 | let client = ().serve(TokioChildProcess::new(Command::new("npx").configure(|cmd| { 38 | cmd.arg("-y").arg("@modelcontextprotocol/server-everything"); 39 | }))?).await?; 40 | Ok(()) 41 | } 42 | ``` 43 |
44 | 45 | ### 构建服务端 46 | 47 |
48 | 构建传输层 49 | 50 | ```rust, ignore 51 | use tokio::io::{stdin, stdout}; 52 | let transport = (stdin(), stdout()); 53 | ``` 54 | 55 |
56 | 57 |
58 | 构建服务 59 | 60 | You can easily build a service by using [`ServerHandler`](crates/rmcp/src/handler/server.rs) or [`ClientHandler`](crates/rmcp/src/handler/client.rs). 61 | 62 | ```rust, ignore 63 | let service = common::counter::Counter::new(); 64 | ``` 65 |
66 | 67 |
68 | 启动服务端 69 | 70 | ```rust, ignore 71 | // this call will finish the initialization process 72 | let server = service.serve(transport).await?; 73 | ``` 74 |
75 | 76 |
77 | 与服务端交互 78 | 79 | Once the server is initialized, you can send requests or notifications: 80 | 81 | ```rust, ignore 82 | // request 83 | let roots = server.list_roots().await?; 84 | 85 | // or send notification 86 | server.notify_cancelled(...).await?; 87 | ``` 88 |
89 | 90 |
91 | 等待服务停止 92 | 93 | ```rust, ignore 94 | let quit_reason = server.waiting().await?; 95 | // 或将其取消 96 | let quit_reason = server.cancel().await?; 97 | ``` 98 |
99 | 100 | ### 示例 101 | 查看 [examples](examples/README.md) 102 | 103 | ## OAuth 支持 104 | 105 | 查看 [oauth_support](docs/OAUTH_SUPPORT.md) 106 | 107 | ## 相关资源 108 | 109 | - [MCP Specification](https://spec.modelcontextprotocol.io/specification/2024-11-05/) 110 | - [Schema](https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.ts) 111 | 112 | ## 相关项目 113 | - [containerd-mcp-server](https://github.com/jokemanfire/mcp-containerd) - 基于 containerd 实现的 MCP 服务 114 | 115 | ## 开发 116 | 117 | ### 贡献指南 118 | 119 | 查看 [docs/CONTRIBUTE.MD](docs/CONTRIBUTE.MD) 120 | 121 | ### 使用 Dev Container 122 | 123 | 如果你想使用 Dev Container,查看 [docs/DEVCONTAINER.md](docs/DEVCONTAINER.md) 获取开发指南。 124 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Quick Start With Claude Desktop 2 | 3 | 1. **Build the Server (Counter Example)** 4 | 5 | ```sh 6 | cargo build --release --example servers_counter_stdio 7 | ``` 8 | 9 | This builds a standard input/output MCP server binary. 10 | 11 | 2. **Add or update this section in your** `PATH-TO/claude_desktop_config.json` 12 | 13 | Windows 14 | 15 | ```json 16 | { 17 | "mcpServers": { 18 | "counter": { 19 | "command": "PATH-TO/rust-sdk/target/release/examples/servers_counter_stdio.exe", 20 | "args": [] 21 | } 22 | } 23 | } 24 | ``` 25 | 26 | MacOS/Linux 27 | 28 | ```json 29 | { 30 | "mcpServers": { 31 | "counter": { 32 | "command": "PATH-TO/rust-sdk/target/release/examples/servers_counter_stdio", 33 | "args": [] 34 | } 35 | } 36 | } 37 | ``` 38 | 39 | 3. **Ensure that the MCP UI elements appear in Claude Desktop** 40 | The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured. It may require to restart Claude for Desktop. 41 | 42 | 4. **Once Claude Desktop is running, try chatting:** 43 | 44 | ```text 45 | counter.say_hello 46 | ``` 47 | 48 | Or test other tools like: 49 | 50 | ```texts 51 | counter.increment 52 | counter.get_value 53 | counter.sum {"a": 3, "b": 4} 54 | ``` 55 | 56 | # Client Examples 57 | 58 | see [clients/README.md](clients/README.md) 59 | 60 | # Server Examples 61 | 62 | see [servers/README.md](servers/README.md) 63 | 64 | # Transport Examples 65 | 66 | - [Tcp](transport/src/tcp.rs) 67 | - [Transport on http upgrade](transport/src/http_upgrade.rs) 68 | - [Unix Socket](transport/src/unix_socket.rs) 69 | - [Websocket](transport/src/websocket.rs) 70 | 71 | # Integration 72 | 73 | - [Rig](rig-integration) A stream chatbot with rig 74 | - [Simple Chat Client](simple-chat-client) A simple chat client implementation using the Model Context Protocol (MCP) SDK. 75 | 76 | # WASI 77 | 78 | - [WASI-P2 runtime](wasi) How it works with wasip2 79 | 80 | ## Use Mcp Inspector 81 | 82 | ```sh 83 | npx @modelcontextprotocol/inspector 84 | ``` 85 | -------------------------------------------------------------------------------- /examples/clients/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | 3 | [package] 4 | name = "mcp-client-examples" 5 | version = "0.1.5" 6 | edition = "2024" 7 | publish = false 8 | 9 | [dependencies] 10 | rmcp = { workspace = true, features = [ 11 | "client", 12 | "reqwest", 13 | "transport-streamable-http-client-reqwest", 14 | "transport-child-process", 15 | "tower", 16 | "auth" 17 | ] } 18 | tokio = { version = "1", features = ["full"] } 19 | serde = { version = "1.0", features = ["derive"] } 20 | serde_json = "1.0" 21 | tracing = "0.1" 22 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 23 | rand = "0.9" 24 | futures = "0.3" 25 | anyhow = "1.0" 26 | url = "2.4" 27 | tower = "0.5" 28 | axum = "0.8" 29 | reqwest = "0.12" 30 | clap = { version = "4.0", features = ["derive"] } 31 | 32 | [[example]] 33 | name = "clients_git_stdio" 34 | path = "src/git_stdio.rs" 35 | 36 | [[example]] 37 | name = "clients_streamable_http" 38 | path = "src/streamable_http.rs" 39 | 40 | [[example]] 41 | name = "clients_everything_stdio" 42 | path = "src/everything_stdio.rs" 43 | 44 | [[example]] 45 | name = "clients_collection" 46 | path = "src/collection.rs" 47 | 48 | [[example]] 49 | name = "clients_oauth_client" 50 | path = "src/auth/oauth_client.rs" 51 | 52 | [[example]] 53 | name = "clients_sampling_stdio" 54 | path = "src/sampling_stdio.rs" 55 | 56 | 57 | [[example]] 58 | name = "clients_progress_client" 59 | path = "src/progress_client.rs" 60 | -------------------------------------------------------------------------------- /examples/clients/README.md: -------------------------------------------------------------------------------- 1 | # MCP Client Examples 2 | 3 | This directory contains Model Context Protocol (MCP) client examples implemented in Rust. These examples demonstrate how to communicate with MCP servers using different transport methods and how to use various client APIs. 4 | 5 | ## Example List 6 | 7 | ### Git Standard I/O Client (`git_stdio.rs`) 8 | 9 | A client that communicates with a Git-related MCP server using standard input/output. 10 | 11 | - Launches the `uvx mcp-server-git` command as a child process 12 | - Retrieves server information and list of available tools 13 | - Calls the `git_status` tool to check the Git status of the current directory 14 | 15 | ### Streamable HTTP Client (`streamable_http.rs`) 16 | 17 | A client that communicates with an MCP server using HTTP streaming transport. 18 | - Connects to an MCP server running at `http://localhost:8000` 19 | - Retrieves server information and list of available tools 20 | - Calls a tool named "increment" 21 | 22 | ### Full-Featured Standard I/O Client (`everything_stdio.rs`) 23 | 24 | An example demonstrating all MCP client capabilities. 25 | 26 | - Launches `npx -y @modelcontextprotocol/server-everything` as a child process 27 | - Retrieves server information and list of available tools 28 | - Calls various tools, including "echo" and "longRunningOperation" 29 | - Lists and reads available resources 30 | - Lists and retrieves simple and complex prompts 31 | - Lists available resource templates 32 | 33 | ### Client Collection (`collection.rs`) 34 | 35 | An example showing how to manage multiple MCP clients. 36 | 37 | - Creates 10 clients connected to Git servers 38 | - Stores these clients in a HashMap 39 | - Performs the same sequence of operations on each client 40 | - Uses `into_dyn()` to convert services to dynamic services 41 | 42 | ### OAuth Client (`auth/oauth_client.rs`) 43 | 44 | A client demonstrating how to authenticate with an MCP server using OAuth. 45 | 46 | - Starts a local HTTP server to handle OAuth callbacks 47 | - Initializes the OAuth state machine and begins the authorization flow 48 | - Displays the authorization URL and waits for user authorization 49 | - Establishes an authorized connection to the MCP server using the acquired access token 50 | - Demonstrates how to use the authorized connection to retrieve available tools and prompts 51 | 52 | 53 | ### Sampling Standard I/O Client (`sampling_stdio.rs`) 54 | 55 | A client demonstrating how to use the sampling tool. 56 | 57 | - Launches the server example `servers_sampling_stdio` 58 | - Connects to the server 59 | - Retrieves server information and list of available tools 60 | - Calls the `ask_llm` tool 61 | 62 | ### Progress Test Client (`progress_client.rs`) 63 | 64 | A client that communicates with an MCP server using progress notifications. 65 | 66 | - Launches the `cargo run --example clients_progress_client -- --transport {stdio|http|all}` to test the progress notifications 67 | - Connects to the server using different transport methods 68 | - Tests the progress notifications 69 | - The http transport should run the server first 70 | 71 | 72 | ## How to Run 73 | 74 | Each example can be run using Cargo: 75 | 76 | ```bash 77 | # Run the Git standard I/O client example 78 | cargo run --example clients_git_stdio 79 | 80 | # Run the streamable HTTP client example 81 | cargo run --example clients_streamable_http 82 | 83 | # Run the full-featured standard I/O client example 84 | cargo run --example clients_everything_stdio 85 | 86 | # Run the client collection example 87 | cargo run --example clients_collection 88 | 89 | # Run the OAuth client example 90 | cargo run --example clients_oauth_client 91 | 92 | # Run the sampling standard I/O client example 93 | cargo run --example clients_sampling_stdio 94 | ``` 95 | 96 | ## Dependencies 97 | 98 | These examples use the following main dependencies: 99 | 100 | - `rmcp`: Rust implementation of the MCP client library 101 | - `tokio`: Asynchronous runtime 102 | - `serde` and `serde_json`: For JSON serialization and deserialization 103 | - `tracing` and `tracing-subscriber`: For logging, not must, only for logging 104 | - `anyhow`: Error handling, not must, only for error handling 105 | - `axum`: For the OAuth callback HTTP server (used only in the OAuth example) 106 | - `reqwest`: HTTP client library (used for OAuth and streamable HTTP transport) 107 | -------------------------------------------------------------------------------- /examples/clients/src/auth/callback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | OAuth Success 7 | 84 | 85 | 86 |
87 |
88 |

Authorization Success

89 |

You have successfully authorized the MCP client. You can now close this window and return to the application.

90 | 91 |
92 | 93 | -------------------------------------------------------------------------------- /examples/clients/src/collection.rs: -------------------------------------------------------------------------------- 1 | /// This example show how to store multiple clients in a map and call tools on them. 2 | /// into_dyn() is used to convert the service to a dynamic service. 3 | /// For example, you can use this to call tools on a service that is running in a different process. 4 | /// or a service that is running in a different machine. 5 | use std::collections::HashMap; 6 | 7 | use anyhow::Result; 8 | use rmcp::{ 9 | model::CallToolRequestParam, 10 | service::ServiceExt, 11 | transport::{ConfigureCommandExt, TokioChildProcess}, 12 | }; 13 | use tokio::process::Command; 14 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 15 | 16 | #[tokio::main] 17 | async fn main() -> Result<()> { 18 | // Initialize logging 19 | tracing_subscriber::registry() 20 | .with( 21 | tracing_subscriber::EnvFilter::try_from_default_env() 22 | .unwrap_or_else(|_| format!("info,{}=debug", env!("CARGO_CRATE_NAME")).into()), 23 | ) 24 | .with(tracing_subscriber::fmt::layer()) 25 | .init(); 26 | 27 | let mut clients_map = HashMap::new(); 28 | for idx in 0..10 { 29 | let client = () 30 | .into_dyn() 31 | .serve(TokioChildProcess::new(Command::new("uvx").configure( 32 | |cmd| { 33 | cmd.arg("mcp-client-git"); 34 | }, 35 | ))?) 36 | .await?; 37 | clients_map.insert(idx, client); 38 | } 39 | 40 | for (_, client) in clients_map.iter() { 41 | // Initialize 42 | let _server_info = client.peer_info(); 43 | 44 | // List tools 45 | let _tools = client.list_tools(Default::default()).await?; 46 | 47 | // Call tool 'git_status' with arguments = {"repo_path": "."} 48 | let _tool_result = client 49 | .call_tool(CallToolRequestParam { 50 | name: "git_status".into(), 51 | arguments: serde_json::json!({ "repo_path": "." }).as_object().cloned(), 52 | }) 53 | .await?; 54 | } 55 | for (_, service) in clients_map { 56 | service.cancel().await?; 57 | } 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /examples/clients/src/everything_stdio.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use rmcp::{ 3 | ServiceExt, 4 | model::{CallToolRequestParam, GetPromptRequestParam, ReadResourceRequestParam}, 5 | object, 6 | transport::{ConfigureCommandExt, TokioChildProcess}, 7 | }; 8 | use tokio::process::Command; 9 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 10 | 11 | #[tokio::main] 12 | async fn main() -> Result<()> { 13 | // Initialize logging 14 | tracing_subscriber::registry() 15 | .with( 16 | tracing_subscriber::EnvFilter::try_from_default_env() 17 | .unwrap_or_else(|_| format!("info,{}=debug", env!("CARGO_CRATE_NAME")).into()), 18 | ) 19 | .with(tracing_subscriber::fmt::layer()) 20 | .init(); 21 | 22 | let client = () 23 | .serve(TokioChildProcess::new(Command::new("npx").configure( 24 | |cmd| { 25 | cmd.arg("-y").arg("@modelcontextprotocol/server-everything"); 26 | }, 27 | ))?) 28 | .await?; 29 | 30 | // Initialize 31 | let server_info = client.peer_info(); 32 | tracing::info!("Connected to server: {server_info:#?}"); 33 | 34 | // List tools 35 | let tools = client.list_all_tools().await?; 36 | tracing::info!("Available tools: {tools:#?}"); 37 | 38 | // Call tool echo 39 | let tool_result = client 40 | .call_tool(CallToolRequestParam { 41 | name: "echo".into(), 42 | arguments: Some(object!({ "message": "hi from rmcp" })), 43 | }) 44 | .await?; 45 | tracing::info!("Tool result for echo: {tool_result:#?}"); 46 | 47 | // Call tool longRunningOperation 48 | let tool_result = client 49 | .call_tool(CallToolRequestParam { 50 | name: "longRunningOperation".into(), 51 | arguments: Some(object!({ "duration": 3, "steps": 1 })), 52 | }) 53 | .await?; 54 | tracing::info!("Tool result for longRunningOperation: {tool_result:#?}"); 55 | 56 | // List resources 57 | let resources = client.list_all_resources().await?; 58 | tracing::info!("Available resources: {resources:#?}"); 59 | 60 | // Read resource 61 | let resource = client 62 | .read_resource(ReadResourceRequestParam { 63 | uri: "test://static/resource/3".into(), 64 | }) 65 | .await?; 66 | tracing::info!("Resource: {resource:#?}"); 67 | 68 | // List prompts 69 | let prompts = client.list_all_prompts().await?; 70 | tracing::info!("Available prompts: {prompts:#?}"); 71 | 72 | // Get simple prompt 73 | let prompt = client 74 | .get_prompt(GetPromptRequestParam { 75 | name: "simple_prompt".into(), 76 | arguments: None, 77 | }) 78 | .await?; 79 | tracing::info!("Prompt - simple: {prompt:#?}"); 80 | 81 | // Get complex prompt (returns text & image) 82 | let prompt = client 83 | .get_prompt(GetPromptRequestParam { 84 | name: "complex_prompt".into(), 85 | arguments: Some(object!({ "temperature": "0.5", "style": "formal" })), 86 | }) 87 | .await?; 88 | tracing::info!("Prompt - complex: {prompt:#?}"); 89 | 90 | // List resource templates 91 | let resource_templates = client.list_all_resource_templates().await?; 92 | tracing::info!("Available resource templates: {resource_templates:#?}"); 93 | 94 | client.cancel().await?; 95 | 96 | Ok(()) 97 | } 98 | -------------------------------------------------------------------------------- /examples/clients/src/git_stdio.rs: -------------------------------------------------------------------------------- 1 | use rmcp::{ 2 | RmcpError, 3 | model::CallToolRequestParam, 4 | service::ServiceExt, 5 | transport::{ConfigureCommandExt, TokioChildProcess}, 6 | }; 7 | use tokio::process::Command; 8 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 9 | 10 | #[allow(clippy::result_large_err)] 11 | #[tokio::main] 12 | async fn main() -> Result<(), RmcpError> { 13 | // Initialize logging 14 | tracing_subscriber::registry() 15 | .with( 16 | tracing_subscriber::EnvFilter::try_from_default_env() 17 | .unwrap_or_else(|_| format!("info,{}=debug", env!("CARGO_CRATE_NAME")).into()), 18 | ) 19 | .with(tracing_subscriber::fmt::layer()) 20 | .init(); 21 | let client = () 22 | .serve( 23 | TokioChildProcess::new(Command::new("uvx").configure(|cmd| { 24 | cmd.arg("mcp-server-git"); 25 | })) 26 | .map_err(RmcpError::transport_creation::)?, 27 | ) 28 | .await?; 29 | 30 | // or serve_client((), TokioChildProcess::new(cmd)?).await?; 31 | 32 | // Initialize 33 | let server_info = client.peer_info(); 34 | tracing::info!("Connected to server: {server_info:#?}"); 35 | 36 | // List tools 37 | let tools = client.list_tools(Default::default()).await?; 38 | tracing::info!("Available tools: {tools:#?}"); 39 | 40 | // Call tool 'git_status' with arguments = {"repo_path": "."} 41 | let tool_result = client 42 | .call_tool(CallToolRequestParam { 43 | name: "git_status".into(), 44 | arguments: serde_json::json!({ "repo_path": "." }).as_object().cloned(), 45 | }) 46 | .await?; 47 | tracing::info!("Tool result: {tool_result:#?}"); 48 | client.cancel().await?; 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /examples/clients/src/sampling_stdio.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use rmcp::{ 3 | ClientHandler, ServiceExt, 4 | model::*, 5 | object, 6 | service::{RequestContext, RoleClient}, 7 | transport::{ConfigureCommandExt, TokioChildProcess}, 8 | }; 9 | use tokio::process::Command; 10 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 11 | /// Simple Sampling Demo Client 12 | /// 13 | /// This client demonstrates how to handle sampling requests from servers. 14 | /// It includes a mock LLM that generates simple responses. 15 | /// Run with: cargo run --example clients_sampling_stdio 16 | #[derive(Clone, Debug, Default)] 17 | pub struct SamplingDemoClient; 18 | 19 | impl SamplingDemoClient { 20 | /// Mock LLM function that generates responses based on the input 21 | /// In actual implementation, this would be replaced with a call to an LLM service 22 | fn mock_llm_response( 23 | &self, 24 | _messages: &[SamplingMessage], 25 | _system_prompt: Option<&str>, 26 | ) -> String { 27 | "It just a mock response".to_string() 28 | } 29 | } 30 | 31 | impl ClientHandler for SamplingDemoClient { 32 | async fn create_message( 33 | &self, 34 | params: CreateMessageRequestParam, 35 | _context: RequestContext, 36 | ) -> Result { 37 | tracing::info!("Received sampling request with {:?}", params); 38 | 39 | // Generate mock response using our simple LLM 40 | let response_text = 41 | self.mock_llm_response(¶ms.messages, params.system_prompt.as_deref()); 42 | 43 | Ok(CreateMessageResult { 44 | message: SamplingMessage { 45 | role: Role::Assistant, 46 | content: Content::text(response_text), 47 | }, 48 | model: "mock_llm".to_string(), 49 | stop_reason: Some(CreateMessageResult::STOP_REASON_END_TURN.to_string()), 50 | }) 51 | } 52 | } 53 | 54 | #[tokio::main] 55 | async fn main() -> Result<()> { 56 | // Initialize logging 57 | tracing_subscriber::registry() 58 | .with( 59 | tracing_subscriber::EnvFilter::try_from_default_env() 60 | .unwrap_or_else(|_| format!("info,{}=debug", env!("CARGO_CRATE_NAME")).into()), 61 | ) 62 | .with(tracing_subscriber::fmt::layer()) 63 | .init(); 64 | 65 | tracing::info!("Starting Sampling Demo Client"); 66 | 67 | let client = SamplingDemoClient; 68 | 69 | // Start the sampling server as a child process 70 | let servers_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) 71 | .parent() 72 | .expect("CARGO_MANIFEST_DIR is not set") 73 | .join("servers"); 74 | 75 | let client = client 76 | .serve(TokioChildProcess::new(Command::new("cargo").configure( 77 | |cmd| { 78 | cmd.arg("run") 79 | .arg("--example") 80 | .arg("servers_sampling_stdio") 81 | .current_dir(servers_dir); 82 | }, 83 | ))?) 84 | .await 85 | .inspect_err(|e| { 86 | tracing::error!("client error: {:?}", e); 87 | })?; 88 | 89 | // Wait for initialization 90 | tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; 91 | 92 | // Get server info 93 | let server_info = client.peer_info(); 94 | tracing::info!("Connected to server: {server_info:#?}"); 95 | 96 | // List available tools 97 | match client.list_all_tools().await { 98 | Ok(tools) => { 99 | tracing::info!("Available tools: {tools:#?}"); 100 | 101 | // Test the ask_llm tool 102 | tracing::info!("Testing ask_llm tool..."); 103 | match client 104 | .call_tool(CallToolRequestParam { 105 | name: "ask_llm".into(), 106 | arguments: Some(object!({ 107 | "question": "Hello world" 108 | })), 109 | }) 110 | .await 111 | { 112 | Ok(result) => tracing::info!("Ask LLM result: {result:#?}"), 113 | Err(e) => tracing::error!("Ask LLM error: {e}"), 114 | } 115 | } 116 | Err(e) => tracing::error!("Failed to list tools: {e}"), 117 | } 118 | 119 | tracing::info!("Sampling demo completed successfully!"); 120 | 121 | tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; 122 | client.cancel().await?; 123 | Ok(()) 124 | } 125 | -------------------------------------------------------------------------------- /examples/clients/src/streamable_http.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use rmcp::{ 3 | ServiceExt, 4 | model::{CallToolRequestParam, ClientCapabilities, ClientInfo, Implementation}, 5 | transport::StreamableHttpClientTransport, 6 | }; 7 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | // Initialize logging 12 | tracing_subscriber::registry() 13 | .with( 14 | tracing_subscriber::EnvFilter::try_from_default_env() 15 | .unwrap_or_else(|_| format!("info,{}=debug", env!("CARGO_CRATE_NAME")).into()), 16 | ) 17 | .with(tracing_subscriber::fmt::layer()) 18 | .init(); 19 | let transport = StreamableHttpClientTransport::from_uri("http://localhost:8000/mcp"); 20 | let client_info = ClientInfo { 21 | protocol_version: Default::default(), 22 | capabilities: ClientCapabilities::default(), 23 | client_info: Implementation { 24 | name: "test sse client".to_string(), 25 | title: None, 26 | version: "0.0.1".to_string(), 27 | website_url: None, 28 | icons: None, 29 | }, 30 | }; 31 | let client = client_info.serve(transport).await.inspect_err(|e| { 32 | tracing::error!("client error: {:?}", e); 33 | })?; 34 | 35 | // Initialize 36 | let server_info = client.peer_info(); 37 | tracing::info!("Connected to server: {server_info:#?}"); 38 | 39 | // List tools 40 | let tools = client.list_tools(Default::default()).await?; 41 | tracing::info!("Available tools: {tools:#?}"); 42 | 43 | let tool_result = client 44 | .call_tool(CallToolRequestParam { 45 | name: "increment".into(), 46 | arguments: serde_json::json!({}).as_object().cloned(), 47 | }) 48 | .await?; 49 | tracing::info!("Tool result: {tool_result:#?}"); 50 | client.cancel().await?; 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /examples/rig-integration/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rig-integration" 3 | edition = { workspace = true } 4 | version = { workspace = true } 5 | authors = { workspace = true } 6 | license = { workspace = true } 7 | repository = { workspace = true } 8 | description = { workspace = true } 9 | keywords = { workspace = true } 10 | homepage = { workspace = true } 11 | categories = { workspace = true } 12 | readme = { workspace = true } 13 | publish = false 14 | 15 | [dependencies] 16 | rig-core = "0.15.1" 17 | tokio = { version = "1", features = ["full"] } 18 | rmcp = { workspace = true, features = [ 19 | "client", 20 | "transport-child-process", 21 | "transport-streamable-http-client-reqwest" 22 | ] } 23 | anyhow = "1.0" 24 | serde_json = "1" 25 | serde = { version = "1", features = ["derive"] } 26 | toml = "0.9" 27 | futures = "0.3" 28 | tracing = "0.1" 29 | tracing-subscriber = { version = "0.3", features = [ 30 | "env-filter", 31 | "std", 32 | "fmt", 33 | ] } 34 | tracing-appender = "0.2" 35 | -------------------------------------------------------------------------------- /examples/rig-integration/config.toml: -------------------------------------------------------------------------------- 1 | deepseek_key = "" 2 | cohere_key = "" 3 | 4 | [mcp] 5 | 6 | [[mcp.server]] 7 | name = "git" 8 | protocol = "stdio" 9 | command = "uvx" 10 | args = ["mcp-server-git"] 11 | -------------------------------------------------------------------------------- /examples/rig-integration/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | pub mod mcp; 6 | 7 | #[derive(Debug, Deserialize, Serialize)] 8 | pub struct Config { 9 | pub mcp: mcp::McpConfig, 10 | pub deepseek_key: Option, 11 | pub cohere_key: Option, 12 | } 13 | 14 | impl Config { 15 | pub async fn retrieve(path: impl AsRef) -> anyhow::Result { 16 | let content = tokio::fs::read_to_string(path).await?; 17 | let config: Self = toml::from_str(&content)?; 18 | Ok(config) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/rig-integration/src/config/mcp.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, process::Stdio}; 2 | 3 | use rmcp::{RoleClient, ServiceExt, service::RunningService, transport::ConfigureCommandExt}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::mcp_adaptor::McpManager; 7 | #[derive(Debug, Serialize, Deserialize, Clone)] 8 | pub struct McpServerConfig { 9 | name: String, 10 | #[serde(flatten)] 11 | transport: McpServerTransportConfig, 12 | } 13 | 14 | #[derive(Debug, Serialize, Deserialize, Clone)] 15 | #[serde(tag = "protocol", rename_all = "lowercase")] 16 | pub enum McpServerTransportConfig { 17 | Streamable { 18 | url: String, 19 | }, 20 | Stdio { 21 | command: String, 22 | #[serde(default)] 23 | args: Vec, 24 | #[serde(default)] 25 | envs: HashMap, 26 | }, 27 | } 28 | 29 | #[derive(Debug, Serialize, Deserialize)] 30 | pub struct McpConfig { 31 | server: Vec, 32 | } 33 | 34 | impl McpConfig { 35 | pub async fn create_manager(&self) -> anyhow::Result { 36 | let mut clients = HashMap::new(); 37 | let mut task_set = tokio::task::JoinSet::>::new(); 38 | for server in &self.server { 39 | let server = server.clone(); 40 | task_set.spawn(async move { 41 | let client = server.transport.start().await?; 42 | anyhow::Result::Ok((server.name.clone(), client)) 43 | }); 44 | } 45 | let start_up_result = task_set.join_all().await; 46 | for result in start_up_result { 47 | match result { 48 | Ok((name, client)) => { 49 | clients.insert(name, client); 50 | } 51 | Err(e) => { 52 | eprintln!("Failed to start server: {:?}", e); 53 | } 54 | } 55 | } 56 | Ok(McpManager { clients }) 57 | } 58 | } 59 | 60 | impl McpServerTransportConfig { 61 | pub async fn start(&self) -> anyhow::Result> { 62 | let client = match self { 63 | McpServerTransportConfig::Streamable { url } => { 64 | let transport = 65 | rmcp::transport::StreamableHttpClientTransport::from_uri(url.to_string()); 66 | ().serve(transport).await? 67 | } 68 | McpServerTransportConfig::Stdio { 69 | command, 70 | args, 71 | envs, 72 | } => { 73 | let transport = rmcp::transport::TokioChildProcess::new( 74 | tokio::process::Command::new(command).configure(|cmd| { 75 | cmd.args(args).envs(envs).stderr(Stdio::null()); 76 | }), 77 | )?; 78 | ().serve(transport).await? 79 | } 80 | }; 81 | Ok(client) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /examples/rig-integration/src/main.rs: -------------------------------------------------------------------------------- 1 | use rig::{ 2 | client::{CompletionClient, ProviderClient}, 3 | embeddings::EmbeddingsBuilder, 4 | providers::{cohere, deepseek}, 5 | vector_store::in_memory_store::InMemoryVectorStore, 6 | }; 7 | use tracing_appender::rolling::{RollingFileAppender, Rotation}; 8 | pub mod chat; 9 | pub mod config; 10 | pub mod mcp_adaptor; 11 | 12 | #[tokio::main] 13 | async fn main() -> anyhow::Result<()> { 14 | let file_appender = RollingFileAppender::new( 15 | Rotation::DAILY, 16 | "logs", 17 | format!("{}.log", env!("CARGO_CRATE_NAME")), 18 | ); 19 | tracing_subscriber::fmt() 20 | .with_env_filter( 21 | tracing_subscriber::EnvFilter::from_default_env() 22 | .add_directive(tracing::Level::INFO.into()), 23 | ) 24 | .with_writer(file_appender) 25 | .with_file(false) 26 | .with_ansi(false) 27 | .init(); 28 | 29 | let config = config::Config::retrieve("config.toml").await?; 30 | let deepseek_client = { 31 | if let Some(key) = config.deepseek_key { 32 | deepseek::Client::new(&key) 33 | } else { 34 | deepseek::Client::from_env() 35 | } 36 | }; 37 | let cohere_client = { 38 | if let Some(key) = config.cohere_key { 39 | cohere::Client::new(&key) 40 | } else { 41 | cohere::Client::from_env() 42 | } 43 | }; 44 | let mcp_manager = config.mcp.create_manager().await?; 45 | tracing::info!( 46 | "MCP Manager created, {} servers started", 47 | mcp_manager.clients.len() 48 | ); 49 | let tool_set = mcp_manager.get_tool_set().await?; 50 | let embedding_model = 51 | cohere_client.embedding_model(cohere::EMBED_MULTILINGUAL_V3, "search_document"); 52 | let embeddings = EmbeddingsBuilder::new(embedding_model.clone()) 53 | .documents(tool_set.schemas()?)? 54 | .build() 55 | .await?; 56 | let store = InMemoryVectorStore::from_documents_with_id_f(embeddings, |f| { 57 | tracing::info!("store tool {}", f.name); 58 | f.name.clone() 59 | }); 60 | let index = store.index(embedding_model); 61 | let dpsk = deepseek_client 62 | .agent(deepseek::DEEPSEEK_CHAT) 63 | .dynamic_tools(4, index, tool_set) 64 | .build(); 65 | 66 | chat::cli_chatbot(dpsk).await?; 67 | 68 | Ok(()) 69 | } 70 | -------------------------------------------------------------------------------- /examples/rig-integration/src/mcp_adaptor.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use rig::tool::{ToolDyn as RigTool, ToolEmbeddingDyn, ToolSet}; 4 | use rmcp::{ 5 | RoleClient, 6 | model::{CallToolRequestParam, CallToolResult, Tool as McpTool}, 7 | service::{RunningService, ServerSink}, 8 | }; 9 | 10 | pub struct McpToolAdaptor { 11 | tool: McpTool, 12 | server: ServerSink, 13 | } 14 | 15 | impl RigTool for McpToolAdaptor { 16 | fn name(&self) -> String { 17 | self.tool.name.to_string() 18 | } 19 | 20 | fn definition( 21 | &self, 22 | _prompt: String, 23 | ) -> std::pin::Pin + Send + Sync + '_>> 24 | { 25 | Box::pin(std::future::ready(rig::completion::ToolDefinition { 26 | name: self.name(), 27 | description: self 28 | .tool 29 | .description 30 | .as_deref() 31 | .unwrap_or_default() 32 | .to_string(), 33 | parameters: self.tool.schema_as_json_value(), 34 | })) 35 | } 36 | 37 | fn call( 38 | &self, 39 | args: String, 40 | ) -> std::pin::Pin< 41 | Box> + Send + Sync + '_>, 42 | > { 43 | let server = self.server.clone(); 44 | Box::pin(async move { 45 | let call_mcp_tool_result = server 46 | .call_tool(CallToolRequestParam { 47 | name: self.tool.name.clone(), 48 | arguments: serde_json::from_str(&args) 49 | .map_err(rig::tool::ToolError::JsonError)?, 50 | }) 51 | .await 52 | .inspect(|result| tracing::info!(?result)) 53 | .inspect_err(|error| tracing::error!(%error)) 54 | .map_err(|e| rig::tool::ToolError::ToolCallError(Box::new(e)))?; 55 | 56 | Ok(convert_mcp_call_tool_result_to_string(call_mcp_tool_result)) 57 | }) 58 | } 59 | } 60 | 61 | impl ToolEmbeddingDyn for McpToolAdaptor { 62 | fn context(&self) -> serde_json::Result { 63 | serde_json::to_value(self.tool.clone()) 64 | } 65 | 66 | fn embedding_docs(&self) -> Vec { 67 | vec![ 68 | self.tool 69 | .description 70 | .as_deref() 71 | .unwrap_or_default() 72 | .to_string(), 73 | ] 74 | } 75 | } 76 | 77 | pub struct McpManager { 78 | pub clients: HashMap>, 79 | } 80 | 81 | impl McpManager { 82 | pub async fn get_tool_set(&self) -> anyhow::Result { 83 | let mut tool_set = ToolSet::default(); 84 | let mut task = tokio::task::JoinSet::>::new(); 85 | for client in self.clients.values() { 86 | let server = client.peer().clone(); 87 | task.spawn(get_tool_set(server)); 88 | } 89 | let results = task.join_all().await; 90 | for result in results { 91 | match result { 92 | Err(e) => { 93 | tracing::error!(error = %e, "Failed to get tool set"); 94 | } 95 | Ok(tools) => { 96 | tool_set.add_tools(tools); 97 | } 98 | } 99 | } 100 | Ok(tool_set) 101 | } 102 | } 103 | 104 | pub fn convert_mcp_call_tool_result_to_string(result: CallToolResult) -> String { 105 | serde_json::to_string(&result).unwrap() 106 | } 107 | 108 | pub async fn get_tool_set(server: ServerSink) -> anyhow::Result { 109 | let tools = server.list_all_tools().await?; 110 | let mut tool_builder = ToolSet::builder(); 111 | for tool in tools { 112 | tracing::info!("get tool: {}", tool.name); 113 | let adaptor = McpToolAdaptor { 114 | tool: tool.clone(), 115 | server: server.clone(), 116 | }; 117 | tool_builder = tool_builder.dynamic_tool(adaptor); 118 | } 119 | let tool_set = tool_builder.build(); 120 | Ok(tool_set) 121 | } 122 | -------------------------------------------------------------------------------- /examples/servers/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mcp-server-examples" 3 | version = "0.1.5" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | rmcp = { workspace = true, features = [ 9 | "server", 10 | "macros", 11 | "client", 12 | "transport-io", 13 | "transport-streamable-http-server", 14 | "auth", 15 | "elicitation", 16 | "schemars", 17 | ] } 18 | tokio = { version = "1", features = [ 19 | "macros", 20 | "rt", 21 | "rt-multi-thread", 22 | "io-std", 23 | "signal", 24 | ] } 25 | serde = { version = "1.0", features = ["derive"] } 26 | serde_json = "1.0" 27 | anyhow = "1.0" 28 | tracing = "0.1" 29 | tracing-subscriber = { version = "0.3", features = [ 30 | "env-filter", 31 | "std", 32 | "fmt", 33 | ] } 34 | futures = "0.3" 35 | rand = { version = "0.9", features = ["std"] } 36 | axum = { version = "0.8", features = ["macros"] } 37 | schemars = "1.0" 38 | reqwest = { version = "0.12", features = ["json"] } 39 | chrono = "0.4" 40 | uuid = { version = "1.6", features = ["v4", "serde"] } 41 | serde_urlencoded = "0.7" 42 | askama = { version = "0.14" } 43 | tower-http = { version = "0.6", features = ["cors"] } 44 | hyper = { version = "1" } 45 | hyper-util = { version = "0", features = ["server"] } 46 | tokio-util = { version = "0.7" } 47 | url = "2.5" 48 | 49 | [dev-dependencies] 50 | tokio-stream = { version = "0.1" } 51 | tokio-util = { version = "0.7", features = ["codec"] } 52 | 53 | [[example]] 54 | name = "servers_counter_stdio" 55 | path = "src/counter_stdio.rs" 56 | 57 | [[example]] 58 | name = "servers_memory_stdio" 59 | path = "src/memory_stdio.rs" 60 | 61 | [[example]] 62 | name = "servers_counter_streamhttp" 63 | path = "src/counter_streamhttp.rs" 64 | 65 | [[example]] 66 | name = "servers_prompt_stdio" 67 | path = "src/prompt_stdio.rs" 68 | 69 | [[example]] 70 | name = "counter_hyper_streamable_http" 71 | path = "src/counter_hyper_streamable_http.rs" 72 | 73 | [[example]] 74 | name = "servers_sampling_stdio" 75 | path = "src/sampling_stdio.rs" 76 | 77 | [[example]] 78 | name = "servers_structured_output" 79 | path = "src/structured_output.rs" 80 | 81 | [[example]] 82 | name = "servers_elicitation_stdio" 83 | path = "src/elicitation_stdio.rs" 84 | 85 | [[example]] 86 | name = "servers_completion_stdio" 87 | path = "src/completion_stdio.rs" 88 | 89 | [[example]] 90 | name = "servers_progress_demo" 91 | path = "src/progress_demo.rs" 92 | 93 | [[example]] 94 | name = "servers_simple_auth_streamhttp" 95 | path = "src/simple_auth_streamhttp.rs" 96 | 97 | [[example]] 98 | name = "servers_complex_auth_streamhttp" 99 | path = "src/complex_auth_streamhttp.rs" 100 | 101 | [[example]] 102 | name = "servers_cimd_auth_streamhttp" 103 | path = "src/cimd_auth_streamhttp.rs" 104 | 105 | [[example]] 106 | name = "servers_calculator_stdio" 107 | path = "src/calculator_stdio.rs" 108 | -------------------------------------------------------------------------------- /examples/servers/src/calculator_stdio.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use common::calculator::Calculator; 3 | use rmcp::{ServiceExt, transport::stdio}; 4 | use tracing_subscriber::{self, EnvFilter}; 5 | mod common; 6 | 7 | /// npx @modelcontextprotocol/inspector cargo run -p mcp-server-examples --example servers_calculator_stdio 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | // Initialize the tracing subscriber with file and stdout logging 11 | tracing_subscriber::fmt() 12 | .with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::DEBUG.into())) 13 | .with_writer(std::io::stderr) 14 | .with_ansi(false) 15 | .init(); 16 | 17 | tracing::info!("Starting Calculator MCP server"); 18 | 19 | // Create an instance of our calculator router 20 | let service = Calculator::new().serve(stdio()).await.inspect_err(|e| { 21 | tracing::error!("serving error: {:?}", e); 22 | })?; 23 | 24 | service.waiting().await?; 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /examples/servers/src/common/calculator.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use rmcp::{ 4 | ServerHandler, 5 | handler::server::{router::tool::ToolRouter, wrapper::Parameters}, 6 | model::{ServerCapabilities, ServerInfo}, 7 | schemars, tool, tool_handler, tool_router, 8 | }; 9 | 10 | #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] 11 | pub struct SumRequest { 12 | #[schemars(description = "the left hand side number")] 13 | pub a: i32, 14 | pub b: i32, 15 | } 16 | 17 | #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] 18 | pub struct SubRequest { 19 | #[schemars(description = "the left hand side number")] 20 | pub a: i32, 21 | #[schemars(description = "the right hand side number")] 22 | pub b: i32, 23 | } 24 | 25 | #[derive(Debug, Clone)] 26 | pub struct Calculator { 27 | tool_router: ToolRouter, 28 | } 29 | 30 | #[tool_router] 31 | impl Calculator { 32 | pub fn new() -> Self { 33 | Self { 34 | tool_router: Self::tool_router(), 35 | } 36 | } 37 | 38 | #[tool(description = "Calculate the sum of two numbers")] 39 | fn sum(&self, Parameters(SumRequest { a, b }): Parameters) -> String { 40 | (a + b).to_string() 41 | } 42 | 43 | #[tool(description = "Calculate the difference of two numbers")] 44 | fn sub(&self, Parameters(SubRequest { a, b }): Parameters) -> String { 45 | (a - b).to_string() 46 | } 47 | } 48 | 49 | #[tool_handler] 50 | impl ServerHandler for Calculator { 51 | fn get_info(&self) -> ServerInfo { 52 | ServerInfo { 53 | instructions: Some("A simple calculator".into()), 54 | capabilities: ServerCapabilities::builder().enable_tools().build(), 55 | ..Default::default() 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/servers/src/common/generic_service.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use rmcp::{ 4 | ServerHandler, 5 | handler::server::{router::tool::ToolRouter, wrapper::Parameters}, 6 | model::{ServerCapabilities, ServerInfo}, 7 | schemars, tool, tool_handler, tool_router, 8 | }; 9 | 10 | #[allow(dead_code)] 11 | pub trait DataService: Send + Sync + 'static { 12 | fn get_data(&self) -> String; 13 | fn set_data(&mut self, data: String); 14 | } 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct MemoryDataService { 18 | data: String, 19 | } 20 | 21 | impl MemoryDataService { 22 | #[allow(dead_code)] 23 | pub fn new(initial_data: impl Into) -> Self { 24 | Self { 25 | data: initial_data.into(), 26 | } 27 | } 28 | } 29 | 30 | impl DataService for MemoryDataService { 31 | fn get_data(&self) -> String { 32 | self.data.clone() 33 | } 34 | 35 | fn set_data(&mut self, data: String) { 36 | self.data = data; 37 | } 38 | } 39 | 40 | #[derive(Debug, Clone)] 41 | pub struct GenericService { 42 | #[allow(dead_code)] 43 | data_service: Arc, 44 | tool_router: ToolRouter, 45 | } 46 | 47 | #[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)] 48 | pub struct SetDataRequest { 49 | pub data: String, 50 | } 51 | 52 | #[tool_router] 53 | impl GenericService { 54 | #[allow(dead_code)] 55 | pub fn new(data_service: DS) -> Self { 56 | Self { 57 | data_service: Arc::new(data_service), 58 | tool_router: Self::tool_router(), 59 | } 60 | } 61 | 62 | #[tool(description = "get memory from service")] 63 | pub async fn get_data(&self) -> String { 64 | self.data_service.get_data() 65 | } 66 | 67 | #[tool(description = "set memory to service")] 68 | pub async fn set_data( 69 | &self, 70 | Parameters(SetDataRequest { data }): Parameters, 71 | ) -> String { 72 | let new_data = data.clone(); 73 | format!("Current memory: {}", new_data) 74 | } 75 | } 76 | 77 | #[tool_handler] 78 | impl ServerHandler for GenericService { 79 | fn get_info(&self) -> ServerInfo { 80 | ServerInfo { 81 | instructions: Some("generic data service".into()), 82 | capabilities: ServerCapabilities::builder().enable_tools().build(), 83 | ..Default::default() 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /examples/servers/src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod calculator; 2 | pub mod counter; 3 | pub mod generic_service; 4 | pub mod progress_demo; 5 | -------------------------------------------------------------------------------- /examples/servers/src/common/progress_demo.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | pin::Pin, 4 | task::{Context, Poll}, 5 | }; 6 | 7 | use futures::Stream; 8 | use rmcp::{ 9 | ErrorData as McpError, RoleServer, ServerHandler, handler::server::tool::ToolRouter, model::*, 10 | service::RequestContext, tool, tool_handler, tool_router, 11 | }; 12 | use serde_json::json; 13 | use tokio_stream::StreamExt; 14 | use tracing::debug; 15 | 16 | // a Stream data source that generates data in chunks 17 | #[derive(Clone)] 18 | struct StreamDataSource { 19 | data: Vec, 20 | chunk_size: usize, 21 | position: usize, 22 | } 23 | 24 | impl StreamDataSource { 25 | pub fn new(data: Vec, chunk_size: usize) -> Self { 26 | Self { 27 | data, 28 | chunk_size, 29 | position: 0, 30 | } 31 | } 32 | pub fn from_text(text: &str) -> Self { 33 | Self::new(text.as_bytes().to_vec(), 1) 34 | } 35 | } 36 | 37 | impl Stream for StreamDataSource { 38 | type Item = Result, io::Error>; 39 | 40 | fn poll_next(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { 41 | let this = self.get_mut(); 42 | if this.position >= this.data.len() { 43 | return Poll::Ready(None); 44 | } 45 | 46 | let start = this.position; 47 | let end = (start + this.chunk_size).min(this.data.len()); 48 | let chunk = this.data[start..end].to_vec(); 49 | this.position = end; 50 | Poll::Ready(Some(Ok(chunk))) 51 | } 52 | } 53 | 54 | #[derive(Clone)] 55 | pub struct ProgressDemo { 56 | data_source: StreamDataSource, 57 | tool_router: ToolRouter, 58 | } 59 | 60 | #[tool_router] 61 | impl ProgressDemo { 62 | #[allow(dead_code)] 63 | pub fn new() -> Self { 64 | Self { 65 | tool_router: Self::tool_router(), 66 | data_source: StreamDataSource::from_text("Hello, world!"), 67 | } 68 | } 69 | #[tool(description = "Process data stream with progress updates")] 70 | async fn stream_processor( 71 | &self, 72 | ctx: RequestContext, 73 | ) -> Result { 74 | let mut counter = 0; 75 | 76 | let mut data_source = self.data_source.clone(); 77 | loop { 78 | let chunk = data_source.next().await; 79 | if chunk.is_none() { 80 | break; 81 | } 82 | 83 | let chunk = chunk.unwrap().unwrap(); 84 | let chunk_str = String::from_utf8_lossy(&chunk); 85 | counter += 1; 86 | // create progress notification param 87 | let progress_param = ProgressNotificationParam { 88 | progress_token: ProgressToken(NumberOrString::Number(counter)), 89 | progress: counter as f64, 90 | total: None, 91 | message: Some(chunk_str.to_string()), 92 | }; 93 | 94 | match ctx.peer.notify_progress(progress_param).await { 95 | Ok(_) => { 96 | debug!("Processed record: {}", chunk_str); 97 | } 98 | Err(e) => { 99 | return Err(McpError::internal_error( 100 | format!("Failed to notify progress: {}", e), 101 | Some(json!({ 102 | "record": chunk_str, 103 | "progress": counter, 104 | "error": e.to_string() 105 | })), 106 | )); 107 | } 108 | } 109 | } 110 | 111 | Ok(CallToolResult::success(vec![Content::text(format!( 112 | "Processed {} records successfully", 113 | counter 114 | ))])) 115 | } 116 | } 117 | 118 | #[tool_handler] 119 | impl ServerHandler for ProgressDemo { 120 | fn get_info(&self) -> ServerInfo { 121 | ServerInfo { 122 | protocol_version: ProtocolVersion::V_2024_11_05, 123 | capabilities: ServerCapabilities::builder().enable_tools().build(), 124 | server_info: Implementation::from_build_env(), 125 | instructions: Some( 126 | "This server demonstrates progress notifications during long-running operations. \ 127 | Use the tools to see real-time progress updates for batch processing" 128 | .to_string(), 129 | ), 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /examples/servers/src/counter_hyper_streamable_http.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use common::counter::Counter; 3 | use hyper_util::{ 4 | rt::{TokioExecutor, TokioIo}, 5 | server::conn::auto::Builder, 6 | service::TowerToHyperService, 7 | }; 8 | use rmcp::transport::streamable_http_server::{ 9 | StreamableHttpService, session::local::LocalSessionManager, 10 | }; 11 | 12 | #[tokio::main] 13 | async fn main() -> anyhow::Result<()> { 14 | let service = TowerToHyperService::new(StreamableHttpService::new( 15 | || Ok(Counter::new()), 16 | LocalSessionManager::default().into(), 17 | Default::default(), 18 | )); 19 | let listener = tokio::net::TcpListener::bind("[::1]:8080").await?; 20 | loop { 21 | let io = tokio::select! { 22 | _ = tokio::signal::ctrl_c() => break, 23 | accept = listener.accept() => { 24 | TokioIo::new(accept?.0) 25 | } 26 | }; 27 | let service = service.clone(); 28 | tokio::spawn(async move { 29 | let _result = Builder::new(TokioExecutor::default()) 30 | .serve_connection(io, service) 31 | .await; 32 | }); 33 | } 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /examples/servers/src/counter_stdio.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use common::counter::Counter; 3 | use rmcp::{ServiceExt, transport::stdio}; 4 | use tracing_subscriber::{self, EnvFilter}; 5 | mod common; 6 | /// npx @modelcontextprotocol/inspector cargo run -p mcp-server-examples --example std_io 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | // Initialize the tracing subscriber with file and stdout logging 10 | tracing_subscriber::fmt() 11 | .with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::DEBUG.into())) 12 | .with_writer(std::io::stderr) 13 | .with_ansi(false) 14 | .init(); 15 | 16 | tracing::info!("Starting MCP server"); 17 | 18 | // Create an instance of our counter router 19 | let service = Counter::new().serve(stdio()).await.inspect_err(|e| { 20 | tracing::error!("serving error: {:?}", e); 21 | })?; 22 | 23 | service.waiting().await?; 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /examples/servers/src/counter_streamhttp.rs: -------------------------------------------------------------------------------- 1 | use rmcp::transport::streamable_http_server::{ 2 | StreamableHttpServerConfig, StreamableHttpService, session::local::LocalSessionManager, 3 | }; 4 | use tracing_subscriber::{ 5 | layer::SubscriberExt, 6 | util::SubscriberInitExt, 7 | {self}, 8 | }; 9 | mod common; 10 | use common::counter::Counter; 11 | 12 | const BIND_ADDRESS: &str = "127.0.0.1:8000"; 13 | 14 | #[tokio::main] 15 | async fn main() -> anyhow::Result<()> { 16 | tracing_subscriber::registry() 17 | .with( 18 | tracing_subscriber::EnvFilter::try_from_default_env() 19 | .unwrap_or_else(|_| "debug".to_string().into()), 20 | ) 21 | .with(tracing_subscriber::fmt::layer()) 22 | .init(); 23 | let ct = tokio_util::sync::CancellationToken::new(); 24 | 25 | let service = StreamableHttpService::new( 26 | || Ok(Counter::new()), 27 | LocalSessionManager::default().into(), 28 | StreamableHttpServerConfig { 29 | cancellation_token: ct.child_token(), 30 | ..Default::default() 31 | }, 32 | ); 33 | 34 | let router = axum::Router::new().nest_service("/mcp", service); 35 | let tcp_listener = tokio::net::TcpListener::bind(BIND_ADDRESS).await?; 36 | let _ = axum::serve(tcp_listener, router) 37 | .with_graceful_shutdown(async move { 38 | tokio::signal::ctrl_c().await.unwrap(); 39 | ct.cancel(); 40 | }) 41 | .await; 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /examples/servers/src/elicitation_stdio.rs: -------------------------------------------------------------------------------- 1 | //! Simple MCP Server with Elicitation 2 | //! 3 | //! Demonstrates user name collection via elicitation 4 | 5 | use std::sync::Arc; 6 | 7 | use anyhow::Result; 8 | use rmcp::{ 9 | ErrorData as McpError, ServerHandler, ServiceExt, elicit_safe, 10 | handler::server::{router::tool::ToolRouter, wrapper::Parameters}, 11 | model::*, 12 | schemars::JsonSchema, 13 | service::{RequestContext, RoleServer}, 14 | tool, tool_handler, tool_router, 15 | transport::stdio, 16 | }; 17 | use serde::{Deserialize, Serialize}; 18 | use tokio::sync::Mutex; 19 | use tracing_subscriber::{self, EnvFilter}; 20 | 21 | /// User information request 22 | #[derive(Debug, Serialize, Deserialize, JsonSchema)] 23 | #[schemars(description = "User information")] 24 | pub struct UserInfo { 25 | #[schemars(description = "User's name")] 26 | pub name: String, 27 | } 28 | 29 | // Mark as safe for elicitation 30 | elicit_safe!(UserInfo); 31 | 32 | /// Simple greeting message 33 | #[derive(Debug, Serialize, Deserialize)] 34 | pub struct GreetingMessage { 35 | pub text: String, 36 | } 37 | 38 | /// Simple tool request 39 | #[derive(Debug, Deserialize, JsonSchema)] 40 | pub struct GreetRequest { 41 | pub greeting: String, 42 | } 43 | 44 | /// Simple server with elicitation 45 | #[derive(Clone)] 46 | pub struct ElicitationServer { 47 | user_name: Arc>>, 48 | tool_router: ToolRouter, 49 | } 50 | 51 | impl ElicitationServer { 52 | pub fn new() -> Self { 53 | Self { 54 | user_name: Arc::new(Mutex::new(None)), 55 | tool_router: Self::tool_router(), 56 | } 57 | } 58 | } 59 | 60 | impl Default for ElicitationServer { 61 | fn default() -> Self { 62 | Self::new() 63 | } 64 | } 65 | 66 | #[tool_router] 67 | impl ElicitationServer { 68 | #[tool(description = "Greet user with name collection")] 69 | async fn greet_user( 70 | &self, 71 | context: RequestContext, 72 | Parameters(request): Parameters, 73 | ) -> Result { 74 | // Check if we have user name 75 | let current_name = self.user_name.lock().await.clone(); 76 | 77 | let user_name = if let Some(name) = current_name { 78 | name 79 | } else { 80 | // Request user name via elicitation 81 | match context 82 | .peer 83 | .elicit::("Please provide your name".to_string()) 84 | .await 85 | { 86 | Ok(Some(user_info)) => { 87 | let name = user_info.name.clone(); 88 | *self.user_name.lock().await = Some(name.clone()); 89 | name 90 | } 91 | Ok(None) => "Guest".to_string(), // Never happen if client checks schema 92 | Err(_) => "Unknown".to_string(), 93 | } 94 | }; 95 | 96 | Ok(CallToolResult::success(vec![Content::text(format!( 97 | "{} {}!", 98 | request.greeting, user_name 99 | ))])) 100 | } 101 | 102 | #[tool(description = "Reset stored user name")] 103 | async fn reset_name(&self) -> Result { 104 | *self.user_name.lock().await = None; 105 | Ok(CallToolResult::success(vec![Content::text( 106 | "User name reset. Next greeting will ask for name again.".to_string(), 107 | )])) 108 | } 109 | } 110 | 111 | #[tool_handler] 112 | impl ServerHandler for ElicitationServer { 113 | fn get_info(&self) -> ServerInfo { 114 | ServerInfo { 115 | capabilities: ServerCapabilities::builder().enable_tools().build(), 116 | server_info: Implementation::from_build_env(), 117 | instructions: Some( 118 | "Simple server demonstrating elicitation for user name collection".to_string(), 119 | ), 120 | ..Default::default() 121 | } 122 | } 123 | } 124 | 125 | #[tokio::main] 126 | async fn main() -> Result<()> { 127 | tracing_subscriber::fmt() 128 | .with_env_filter(EnvFilter::from_default_env()) 129 | .init(); 130 | 131 | println!("Simple MCP Elicitation Demo"); 132 | 133 | // Get current executable path for Inspector 134 | let current_exe = std::env::current_exe() 135 | .map(|path| path.display().to_string()) 136 | .unwrap(); 137 | 138 | println!("To test with MCP Inspector:"); 139 | println!("1. Run: npx @modelcontextprotocol/inspector"); 140 | println!("2. Enter server command: {}", current_exe); 141 | 142 | let service = ElicitationServer::new() 143 | .serve(stdio()) 144 | .await 145 | .inspect_err(|e| { 146 | tracing::error!("serving error: {:?}", e); 147 | })?; 148 | 149 | service.waiting().await?; 150 | Ok(()) 151 | } 152 | -------------------------------------------------------------------------------- /examples/servers/src/html/mcp_oauth_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MCP OAuth Server 5 | 12 | 13 | 14 |

MCP OAuth Server

15 |

This is an MCP server with OAuth 2.0 integration to a third-party authorization server.

16 | 17 |

Available Endpoints:

18 | 19 |
20 |

Authorization Endpoint

21 |

GET /oauth/authorize

22 |

Parameters:

23 |
    24 |
  • response_type - Must be "code"
  • 25 |
  • client_id - Client identifier (e.g., "mcp-client")
  • 26 |
  • redirect_uri - URI to redirect after authorization
  • 27 |
  • scope - Optional requested scope
  • 28 |
  • state - Optional state value for CSRF prevention
  • 29 |
30 |
31 | 32 |
33 |

Token Endpoint

34 |

POST /oauth/token

35 |

Parameters:

36 |
    37 |
  • grant_type - Must be "authorization_code"
  • 38 |
  • code - The authorization code
  • 39 |
  • client_id - Client identifier
  • 40 |
  • client_secret - Client secret
  • 41 |
  • redirect_uri - Redirect URI used in authorization request
  • 42 |
43 |
44 | 45 |
46 |

MCP SSE Endpoints

47 |

/mcp/sse - SSE connection endpoint (requires OAuth token)

48 |

/mcp/message - Message endpoint (requires OAuth token)

49 |
50 | 51 |
52 |

OAuth Flow:

53 |
    54 |
  1. MCP Client initiates OAuth flow with this MCP Server
  2. 55 |
  3. MCP Server redirects to Third-Party OAuth Server
  4. 56 |
  5. User authenticates with Third-Party Server
  6. 57 |
  7. Third-Party Server redirects back to MCP Server with auth code
  8. 58 |
  9. MCP Server exchanges the code for a third-party access token
  10. 59 |
  11. MCP Server generates its own token bound to the third-party session
  12. 60 |
  13. MCP Server completes the OAuth flow with the MCP Client
  14. 61 |
62 |
63 | 64 | -------------------------------------------------------------------------------- /examples/servers/src/memory_stdio.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | mod common; 3 | use common::generic_service::{GenericService, MemoryDataService}; 4 | use rmcp::serve_server; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<(), Box> { 8 | let memory_service = MemoryDataService::new("initial data"); 9 | 10 | let generic_service = GenericService::new(memory_service); 11 | 12 | println!("start server, connect to standard input/output"); 13 | 14 | let io = (tokio::io::stdin(), tokio::io::stdout()); 15 | 16 | serve_server(generic_service, io).await?; 17 | Ok(()) 18 | } 19 | -------------------------------------------------------------------------------- /examples/servers/src/progress_demo.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use rmcp::{ 4 | ServiceExt, 5 | transport::{ 6 | stdio, 7 | streamable_http_server::{StreamableHttpService, session::local::LocalSessionManager}, 8 | }, 9 | }; 10 | 11 | mod common; 12 | use common::progress_demo::ProgressDemo; 13 | 14 | const HTTP_BIND_ADDRESS: &str = "127.0.0.1:8001"; 15 | 16 | #[tokio::main] 17 | async fn main() -> anyhow::Result<()> { 18 | // Get transport mode from environment variable or command line argument 19 | let transport_mode = env::args() 20 | .nth(1) 21 | .unwrap_or_else(|| env::var("TRANSPORT_MODE").unwrap_or_else(|_| "stdio".to_string())); 22 | 23 | match transport_mode.as_str() { 24 | "stdio" => run_stdio().await, 25 | "http" | "streamhttp" => run_streamable_http().await, 26 | "all" => run_all_transports().await, 27 | _ => { 28 | eprintln!("Usage: {} [stdio|http|all]", env::args().next().unwrap()); 29 | std::process::exit(1); 30 | } 31 | } 32 | } 33 | 34 | async fn run_stdio() -> anyhow::Result<()> { 35 | let server = ProgressDemo::new(); 36 | let service = server.serve(stdio()).await.inspect_err(|e| { 37 | tracing::error!("stdio serving error: {:?}", e); 38 | })?; 39 | 40 | service.waiting().await?; 41 | Ok(()) 42 | } 43 | 44 | async fn run_streamable_http() -> anyhow::Result<()> { 45 | println!("Running Streamable HTTP server"); 46 | let service = StreamableHttpService::new( 47 | || Ok(ProgressDemo::new()), 48 | LocalSessionManager::default().into(), 49 | Default::default(), 50 | ); 51 | 52 | let router = axum::Router::new().nest_service("/mcp", service); 53 | let tcp_listener = tokio::net::TcpListener::bind(HTTP_BIND_ADDRESS).await?; 54 | 55 | tracing::info!( 56 | "Progress Demo HTTP server started at http://{}/mcp", 57 | HTTP_BIND_ADDRESS 58 | ); 59 | tracing::info!("Press Ctrl+C to shutdown"); 60 | 61 | let _ = axum::serve(tcp_listener, router) 62 | .with_graceful_shutdown(async { tokio::signal::ctrl_c().await.unwrap() }) 63 | .await; 64 | 65 | Ok(()) 66 | } 67 | 68 | async fn run_all_transports() -> anyhow::Result<()> { 69 | println!("Running all transports"); 70 | 71 | // Start Streamable HTTP server 72 | let http_service = StreamableHttpService::new( 73 | || Ok(ProgressDemo::new()), 74 | LocalSessionManager::default().into(), 75 | Default::default(), 76 | ); 77 | let http_router = axum::Router::new().nest_service("/mcp", http_service); 78 | let http_listener = tokio::net::TcpListener::bind(HTTP_BIND_ADDRESS).await?; 79 | 80 | // Start Streamable HTTP server 81 | tokio::spawn(async move { 82 | let _ = axum::serve(http_listener, http_router) 83 | .with_graceful_shutdown(async { tokio::signal::ctrl_c().await.unwrap() }) 84 | .await; 85 | }); 86 | 87 | tracing::info!( 88 | "Progress Demo HTTP server started at http://{}/mcp", 89 | HTTP_BIND_ADDRESS 90 | ); 91 | tracing::info!("Press Ctrl+C to shutdown"); 92 | 93 | tokio::signal::ctrl_c().await?; 94 | 95 | Ok(()) 96 | } 97 | -------------------------------------------------------------------------------- /examples/servers/templates/mcp_oauth_authorize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MCP OAuth 7 | 84 | 85 | 86 |
87 |

MCP OAuth

88 |
89 |

{{ client_id }} requests access to your account.

90 |

requested scopes: {{ scopes }}

91 |
92 | 93 |
94 | 95 | 96 | 97 | 98 | 99 |
100 | 101 | 102 |
103 |
104 |
105 | 106 | -------------------------------------------------------------------------------- /examples/simple-chat-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple-chat-client" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | tokio = { version = "1", features = ["full"] } 9 | serde = { version = "1.0", features = ["derive"] } 10 | serde_json = "1.0" 11 | reqwest = { version = "0.12", features = ["json"] } 12 | anyhow = "1.0" 13 | thiserror = "2.0" 14 | async-trait = "0.1" 15 | futures = "0.3" 16 | toml = "0.9" 17 | rmcp = { workspace = true, features = [ 18 | "client", 19 | "transport-child-process", 20 | "transport-streamable-http-client-reqwest" 21 | ] } 22 | clap = { version = "4.0", features = ["derive"] } 23 | -------------------------------------------------------------------------------- /examples/simple-chat-client/README.md: -------------------------------------------------------------------------------- 1 | # Simple Chat Client 2 | 3 | A simple chat client implementation using the Model Context Protocol (MCP) SDK. It just a example for developers to understand how to use the MCP SDK. This example use the easiest way to start a MCP server, and call the tool directly. No need embedding or complex third library or function call(because some models can't support function call).Just add tool in system prompt, and the client will call the tool automatically. 4 | 5 | 6 | ## Usage 7 | 8 | After configuring the config file, you can run the example: 9 | ```bash 10 | ./simple_chat --help # show help info 11 | ./simple_chat config > config.toml # output default config to file 12 | ./simple_chat --config my_config.toml chat # start chat with specified config 13 | ./simple_chat --config my_config.toml --model gpt-4o-mini chat # start chat with specified model 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /examples/simple-chat-client/src/client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use async_trait::async_trait; 3 | use reqwest::Client as HttpClient; 4 | 5 | use crate::model::{CompletionRequest, CompletionResponse}; 6 | 7 | #[async_trait] 8 | pub trait ChatClient: Send + Sync { 9 | async fn complete(&self, request: CompletionRequest) -> Result; 10 | } 11 | 12 | pub struct OpenAIClient { 13 | api_key: String, 14 | client: HttpClient, 15 | base_url: String, 16 | } 17 | 18 | impl OpenAIClient { 19 | pub fn new(api_key: String, url: Option, proxy: Option) -> Self { 20 | let base_url = url.unwrap_or("https://api.openai.com/v1/chat/completions".to_string()); 21 | let proxy = proxy.unwrap_or(false); 22 | let client = if proxy { 23 | HttpClient::new() 24 | } else { 25 | HttpClient::builder() 26 | .no_proxy() 27 | .build() 28 | .unwrap_or_else(|_| HttpClient::new()) 29 | }; 30 | 31 | Self { 32 | api_key, 33 | client, 34 | base_url, 35 | } 36 | } 37 | 38 | pub fn with_base_url(mut self, base_url: impl Into) -> Self { 39 | self.base_url = base_url.into(); 40 | self 41 | } 42 | } 43 | 44 | #[async_trait] 45 | impl ChatClient for OpenAIClient { 46 | async fn complete(&self, request: CompletionRequest) -> Result { 47 | let response = self 48 | .client 49 | .post(&self.base_url) 50 | .header("Authorization", format!("Bearer {}", self.api_key)) 51 | .header("Content-Type", "application/json") 52 | .json(&request) 53 | .send() 54 | .await?; 55 | 56 | if !response.status().is_success() { 57 | let error_text = response.text().await?; 58 | println!("API error: {}", error_text); 59 | return Err(anyhow::anyhow!("API Error: {}", error_text)); 60 | } 61 | let text_data = response.text().await?; 62 | println!("Received response: {}", text_data); 63 | let completion: CompletionResponse = serde_json::from_str(&text_data) 64 | .map_err(anyhow::Error::from) 65 | .unwrap(); 66 | Ok(completion) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/simple-chat-client/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, path::Path, process::Stdio}; 2 | 3 | use anyhow::Result; 4 | use rmcp::{RoleClient, ServiceExt, service::RunningService, transport::ConfigureCommandExt}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Debug, Serialize, Deserialize)] 8 | pub struct Config { 9 | pub openai_key: Option, 10 | pub chat_url: Option, 11 | pub mcp: Option, 12 | pub model_name: Option, 13 | pub proxy: Option, 14 | pub support_tool: Option, 15 | } 16 | 17 | #[derive(Debug, Serialize, Deserialize)] 18 | pub struct McpConfig { 19 | pub server: Vec, 20 | } 21 | 22 | #[derive(Debug, Serialize, Deserialize, Clone)] 23 | pub struct McpServerConfig { 24 | pub name: String, 25 | #[serde(flatten)] 26 | pub transport: McpServerTransportConfig, 27 | } 28 | 29 | #[derive(Debug, Serialize, Deserialize, Clone)] 30 | #[serde(tag = "protocol", rename_all = "lowercase")] 31 | pub enum McpServerTransportConfig { 32 | Streamable { 33 | url: String, 34 | }, 35 | Stdio { 36 | command: String, 37 | #[serde(default)] 38 | args: Vec, 39 | #[serde(default)] 40 | envs: HashMap, 41 | }, 42 | } 43 | 44 | impl McpServerTransportConfig { 45 | pub async fn start(&self) -> Result> { 46 | let client = match self { 47 | McpServerTransportConfig::Streamable { url } => { 48 | let transport = 49 | rmcp::transport::StreamableHttpClientTransport::from_uri(url.to_string()); 50 | ().serve(transport).await? 51 | } 52 | McpServerTransportConfig::Stdio { 53 | command, 54 | args, 55 | envs, 56 | } => { 57 | let transport = rmcp::transport::child_process::TokioChildProcess::new( 58 | tokio::process::Command::new(command).configure(|cmd| { 59 | cmd.args(args) 60 | .envs(envs) 61 | .stderr(Stdio::inherit()) 62 | .stdout(Stdio::inherit()); 63 | }), 64 | )?; 65 | ().serve(transport).await? 66 | } 67 | }; 68 | Ok(client) 69 | } 70 | } 71 | 72 | impl Config { 73 | pub async fn load(path: impl AsRef) -> Result { 74 | let content = tokio::fs::read_to_string(path).await?; 75 | let config: Self = toml::from_str(&content)?; 76 | Ok(config) 77 | } 78 | 79 | pub async fn create_mcp_clients( 80 | &self, 81 | ) -> Result>> { 82 | let mut clients = HashMap::new(); 83 | 84 | if let Some(mcp_config) = &self.mcp { 85 | for server in &mcp_config.server { 86 | let client = server.transport.start().await?; 87 | clients.insert(server.name.clone(), client); 88 | } 89 | } 90 | 91 | Ok(clients) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /examples/simple-chat-client/src/config.toml: -------------------------------------------------------------------------------- 1 | openai_key = "key" 2 | chat_url = "url" 3 | model_name = "model_name" 4 | proxy = false 5 | support_tool = true # if support tool call 6 | 7 | [mcp] 8 | [[mcp.server]] 9 | name = "MCP server name" 10 | protocol = "stdio" 11 | command = "MCP server path" 12 | args = [" "] -------------------------------------------------------------------------------- /examples/simple-chat-client/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use serde::Serialize; 4 | 5 | #[derive(Debug, Serialize)] 6 | pub struct McpError { 7 | pub message: String, 8 | } 9 | 10 | impl fmt::Display for McpError { 11 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 12 | write!(f, "{}", self.message) 13 | } 14 | } 15 | 16 | impl std::error::Error for McpError {} 17 | 18 | impl McpError { 19 | pub fn new(message: impl ToString) -> Self { 20 | Self { 21 | message: message.to_string(), 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/simple-chat-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod chat; 2 | pub mod client; 3 | pub mod config; 4 | pub mod error; 5 | pub mod model; 6 | pub mod tool; 7 | -------------------------------------------------------------------------------- /examples/simple-chat-client/src/model.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize, Clone)] 4 | pub struct Message { 5 | pub role: String, 6 | pub content: String, 7 | #[serde(skip_serializing_if = "Option::is_none")] 8 | pub tool_calls: Option>, 9 | } 10 | 11 | impl Message { 12 | pub fn system(content: impl ToString) -> Self { 13 | Self { 14 | role: "system".to_string(), 15 | content: content.to_string(), 16 | tool_calls: None, 17 | } 18 | } 19 | 20 | pub fn user(content: impl ToString) -> Self { 21 | Self { 22 | role: "user".to_string(), 23 | content: content.to_string(), 24 | tool_calls: None, 25 | } 26 | } 27 | 28 | pub fn assistant(content: impl ToString) -> Self { 29 | Self { 30 | role: "assistant".to_string(), 31 | content: content.to_string(), 32 | tool_calls: None, 33 | } 34 | } 35 | } 36 | 37 | #[derive(Debug, Serialize, Deserialize)] 38 | pub struct CompletionRequest { 39 | pub model: String, 40 | pub messages: Vec, 41 | #[serde(skip_serializing_if = "Option::is_none")] 42 | pub temperature: Option, 43 | #[serde(skip_serializing_if = "Option::is_none")] 44 | pub tools: Option>, 45 | } 46 | 47 | #[derive(Debug, Serialize, Deserialize)] 48 | pub struct Tool { 49 | pub name: String, 50 | pub description: String, 51 | pub parameters: serde_json::Value, 52 | } 53 | 54 | #[derive(Debug, Serialize, Deserialize)] 55 | pub struct CompletionResponse { 56 | pub id: String, 57 | pub object: String, 58 | pub created: u64, 59 | pub model: String, 60 | pub choices: Vec, 61 | } 62 | 63 | #[derive(Debug, Serialize, Deserialize)] 64 | pub struct Choice { 65 | pub index: u32, 66 | pub message: Message, 67 | pub finish_reason: String, 68 | } 69 | 70 | #[derive(Debug, Serialize, Deserialize, Clone)] 71 | pub struct ToolCall { 72 | pub id: String, 73 | #[serde(rename = "type")] 74 | pub _type: String, 75 | pub function: ToolFunction, 76 | } 77 | #[derive(Debug, Serialize, Deserialize, Clone)] 78 | pub struct ToolFunction { 79 | pub name: String, 80 | pub arguments: String, 81 | } 82 | 83 | #[derive(Debug, Serialize, Deserialize)] 84 | pub struct ToolResult { 85 | pub success: bool, 86 | pub contents: Vec, 87 | } 88 | 89 | #[derive(Debug, Serialize, Deserialize)] 90 | pub struct Content { 91 | pub content_type: String, 92 | pub body: String, 93 | } 94 | 95 | impl Content { 96 | pub fn text(content: impl ToString) -> Self { 97 | Self { 98 | content_type: "text/plain".to_string(), 99 | body: content.to_string(), 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /examples/simple-chat-client/src/tool.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use anyhow::Result; 4 | use async_trait::async_trait; 5 | use rmcp::{ 6 | RoleClient, 7 | model::{CallToolRequestParam, CallToolResult, Tool as McpTool}, 8 | service::{RunningService, ServerSink}, 9 | }; 10 | use serde_json::Value; 11 | 12 | use crate::{ 13 | error::McpError, 14 | model::{Content, ToolResult}, 15 | }; 16 | 17 | #[async_trait] 18 | pub trait Tool: Send + Sync { 19 | fn name(&self) -> String; 20 | fn description(&self) -> String; 21 | fn parameters(&self) -> Value; 22 | async fn call(&self, args: Value) -> Result; 23 | } 24 | 25 | pub struct McpToolAdapter { 26 | tool: McpTool, 27 | server: ServerSink, 28 | } 29 | 30 | impl McpToolAdapter { 31 | pub fn new(tool: McpTool, server: ServerSink) -> Self { 32 | Self { tool, server } 33 | } 34 | } 35 | 36 | #[async_trait] 37 | impl Tool for McpToolAdapter { 38 | fn name(&self) -> String { 39 | self.tool.name.clone().to_string() 40 | } 41 | 42 | fn description(&self) -> String { 43 | self.tool 44 | .description 45 | .clone() 46 | .unwrap_or_default() 47 | .to_string() 48 | } 49 | 50 | fn parameters(&self) -> Value { 51 | serde_json::to_value(&self.tool.input_schema).unwrap_or(serde_json::json!({})) 52 | } 53 | 54 | async fn call(&self, args: Value) -> Result { 55 | let arguments = match args { 56 | Value::Object(map) => Some(map), 57 | _ => None, 58 | }; 59 | println!("arguments: {:?}", arguments); 60 | let call_result = self 61 | .server 62 | .call_tool(CallToolRequestParam { 63 | name: self.tool.name.clone(), 64 | arguments, 65 | }) 66 | .await?; 67 | 68 | Ok(call_result) 69 | } 70 | } 71 | #[derive(Default)] 72 | pub struct ToolSet { 73 | tools: HashMap>, 74 | clients: HashMap>, 75 | } 76 | 77 | impl ToolSet { 78 | pub fn set_clients(&mut self, clients: HashMap>) { 79 | self.clients = clients; 80 | } 81 | 82 | pub fn add_tool(&mut self, tool: T) { 83 | self.tools.insert(tool.name(), Arc::new(tool)); 84 | } 85 | 86 | pub fn get_tool(&self, name: &str) -> Option> { 87 | self.tools.get(name).cloned() 88 | } 89 | 90 | pub fn tools(&self) -> Vec> { 91 | self.tools.values().cloned().collect() 92 | } 93 | } 94 | 95 | pub async fn get_mcp_tools(server: ServerSink) -> Result> { 96 | let tools = server.list_all_tools().await?; 97 | Ok(tools 98 | .into_iter() 99 | .map(|tool| McpToolAdapter::new(tool, server.clone())) 100 | .collect()) 101 | } 102 | 103 | pub trait IntoCallToolResult { 104 | fn into_call_tool_result(self) -> Result; 105 | } 106 | 107 | impl IntoCallToolResult for Result 108 | where 109 | T: serde::Serialize, 110 | { 111 | fn into_call_tool_result(self) -> Result { 112 | match self { 113 | Ok(response) => { 114 | let content = Content { 115 | content_type: "application/json".to_string(), 116 | body: serde_json::to_string(&response).unwrap_or_default(), 117 | }; 118 | Ok(ToolResult { 119 | success: true, 120 | contents: vec![content], 121 | }) 122 | } 123 | Err(error) => { 124 | let content = Content { 125 | content_type: "application/json".to_string(), 126 | body: serde_json::to_string(&error).unwrap_or_default(), 127 | }; 128 | Ok(ToolResult { 129 | success: false, 130 | contents: vec![content], 131 | }) 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /examples/transport/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "transport" 3 | edition = { workspace = true } 4 | version = { workspace = true } 5 | authors = { workspace = true } 6 | license = { workspace = true } 7 | repository = { workspace = true } 8 | description = { workspace = true } 9 | keywords = { workspace = true } 10 | homepage = { workspace = true } 11 | categories = { workspace = true } 12 | readme = { workspace = true } 13 | publish = false 14 | 15 | [package.metadata.docs.rs] 16 | all-features = true 17 | 18 | [dependencies] 19 | rmcp = { workspace = true, features = ["server", "client"] } 20 | tokio = { version = "1", features = [ 21 | "macros", 22 | "rt", 23 | "rt-multi-thread", 24 | "io-std", 25 | "net", 26 | "fs", 27 | "time", 28 | ] } 29 | serde = { version = "1.0", features = ["derive"] } 30 | serde_json = "1.0" 31 | anyhow = "1.0" 32 | tracing = "0.1" 33 | tracing-subscriber = { version = "0.3", features = [ 34 | "env-filter", 35 | "std", 36 | "fmt", 37 | ] } 38 | futures = "0.3" 39 | rand = { version = "0.9" } 40 | schemars = { version = "1.0", optional = true } 41 | hyper = { version = "1", features = ["client", "server", "http1"] } 42 | hyper-util = { version = "0.1", features = ["tokio"] } 43 | tokio-tungstenite = "0.28.0" 44 | reqwest = { version = "0.12" } 45 | pin-project-lite = "0.2" 46 | 47 | [[example]] 48 | name = "tcp" 49 | path = "src/tcp.rs" 50 | 51 | 52 | [[example]] 53 | name = "http_upgrade" 54 | path = "src/http_upgrade.rs" 55 | 56 | [[example]] 57 | name = "unix_socket" 58 | path = "src/unix_socket.rs" 59 | 60 | [[example]] 61 | name = "websocket" 62 | path = "src/websocket.rs" 63 | 64 | [[example]] 65 | name = "named-pipe" 66 | path = "src/named-pipe.rs" 67 | -------------------------------------------------------------------------------- /examples/transport/src/common/calculator.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use rmcp::{ 4 | ServerHandler, 5 | handler::server::{ 6 | router::tool::ToolRouter, 7 | wrapper::{Json, Parameters}, 8 | }, 9 | model::{ServerCapabilities, ServerInfo}, 10 | schemars, tool, tool_handler, tool_router, 11 | }; 12 | 13 | #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] 14 | pub struct SumRequest { 15 | #[schemars(description = "the left hand side number")] 16 | pub a: i32, 17 | pub b: i32, 18 | } 19 | 20 | #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] 21 | pub struct SubRequest { 22 | #[schemars(description = "the left hand side number")] 23 | pub a: i32, 24 | #[schemars(description = "the right hand side number")] 25 | pub b: i32, 26 | } 27 | 28 | #[derive(Debug, Clone)] 29 | pub struct Calculator { 30 | tool_router: ToolRouter, 31 | } 32 | 33 | impl Calculator { 34 | pub fn new() -> Self { 35 | Self { 36 | tool_router: Self::tool_router(), 37 | } 38 | } 39 | } 40 | 41 | #[tool_router] 42 | impl Calculator { 43 | #[tool(description = "Calculate the sum of two numbers")] 44 | fn sum(&self, Parameters(SumRequest { a, b }): Parameters) -> String { 45 | (a + b).to_string() 46 | } 47 | 48 | #[tool(description = "Calculate the difference of two numbers")] 49 | fn sub(&self, Parameters(SubRequest { a, b }): Parameters) -> Json { 50 | Json(a - b) 51 | } 52 | } 53 | #[tool_handler] 54 | impl ServerHandler for Calculator { 55 | fn get_info(&self) -> ServerInfo { 56 | ServerInfo { 57 | instructions: Some("A simple calculator".into()), 58 | capabilities: ServerCapabilities::builder().enable_tools().build(), 59 | ..Default::default() 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/transport/src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod calculator; 2 | -------------------------------------------------------------------------------- /examples/transport/src/http_upgrade.rs: -------------------------------------------------------------------------------- 1 | use common::calculator::Calculator; 2 | use hyper::{ 3 | Request, StatusCode, 4 | body::Incoming, 5 | header::{HeaderValue, UPGRADE}, 6 | }; 7 | use hyper_util::rt::TokioIo; 8 | use rmcp::{RoleClient, ServiceExt, service::RunningService}; 9 | use tracing_subscriber::EnvFilter; 10 | mod common; 11 | #[tokio::main] 12 | async fn main() -> anyhow::Result<()> { 13 | tracing_subscriber::fmt() 14 | .with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::INFO.into())) 15 | .init(); 16 | start_server().await?; 17 | let client = http_client("127.0.0.1:8001").await?; 18 | let tools = client.list_all_tools().await?; 19 | client.cancel().await?; 20 | tracing::info!("{:#?}", tools); 21 | Ok(()) 22 | } 23 | 24 | async fn http_server(req: Request) -> Result, hyper::Error> { 25 | tokio::spawn(async move { 26 | let upgraded = hyper::upgrade::on(req).await?; 27 | let service = Calculator::new().serve(TokioIo::new(upgraded)).await?; 28 | service.waiting().await?; 29 | anyhow::Result::<()>::Ok(()) 30 | }); 31 | let mut response = hyper::Response::new(String::new()); 32 | *response.status_mut() = StatusCode::SWITCHING_PROTOCOLS; 33 | response 34 | .headers_mut() 35 | .insert(UPGRADE, HeaderValue::from_static("mcp")); 36 | Ok(response) 37 | } 38 | 39 | async fn http_client(uri: &str) -> anyhow::Result> { 40 | let tcp_stream = tokio::net::TcpStream::connect(uri).await?; 41 | let (mut s, c) = 42 | hyper::client::conn::http1::handshake::<_, String>(TokioIo::new(tcp_stream)).await?; 43 | tokio::spawn(c.with_upgrades()); 44 | let mut req = Request::new(String::new()); 45 | req.headers_mut() 46 | .insert(UPGRADE, HeaderValue::from_static("mcp")); 47 | let response = s.send_request(req).await?; 48 | let upgraded = hyper::upgrade::on(response).await?; 49 | let client = ().serve(TokioIo::new(upgraded)).await?; 50 | Ok(client) 51 | } 52 | 53 | async fn start_server() -> anyhow::Result<()> { 54 | let tcp_listener = tokio::net::TcpListener::bind("127.0.0.1:8001").await?; 55 | let service = hyper::service::service_fn(http_server); 56 | tokio::spawn(async move { 57 | while let Ok((stream, addr)) = tcp_listener.accept().await { 58 | tracing::info!("accepted connection from: {}", addr); 59 | let conn = hyper::server::conn::http1::Builder::new() 60 | .serve_connection(TokioIo::new(stream), service) 61 | .with_upgrades(); 62 | tokio::spawn(conn); 63 | } 64 | }); 65 | 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /examples/transport/src/named-pipe.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | #[cfg(target_family = "windows")] 4 | #[tokio::main] 5 | async fn main() -> anyhow::Result<()> { 6 | use common::calculator::Calculator; 7 | use rmcp::{serve_client, serve_server}; 8 | use tokio::net::windows::named_pipe::{ClientOptions, ServerOptions}; 9 | const PIPE_NAME: &str = r"\\.\pipe\rmcp_example"; 10 | 11 | async fn server(name: &str) -> anyhow::Result<()> { 12 | let mut server = ServerOptions::new() 13 | .first_pipe_instance(true) 14 | .create(name)?; 15 | while server.connect().await.is_ok() { 16 | let stream = server; 17 | server = ServerOptions::new().create(name)?; 18 | tokio::spawn(async move { 19 | match serve_server(Calculator::new(), stream).await { 20 | Ok(server) => { 21 | println!("Server initialized successfully"); 22 | if let Err(e) = server.waiting().await { 23 | println!("Error while server waiting: {}", e); 24 | } 25 | } 26 | Err(e) => println!("Server initialization failed: {}", e), 27 | } 28 | 29 | anyhow::Ok(()) 30 | }); 31 | } 32 | Ok(()) 33 | } 34 | 35 | async fn client() -> anyhow::Result<()> { 36 | println!("Client connecting to {}", PIPE_NAME); 37 | let stream = ClientOptions::new().open(PIPE_NAME)?; 38 | 39 | let client = serve_client((), stream).await?; 40 | println!("Client connected and initialized successfully"); 41 | 42 | // List available tools 43 | let tools = client.peer().list_tools(Default::default()).await?; 44 | println!("Available tools: {:?}", tools); 45 | 46 | // Call the sum tool 47 | if let Some(sum_tool) = tools.tools.iter().find(|t| t.name.contains("sum")) { 48 | println!("Calling sum tool: {}", sum_tool.name); 49 | let result = client 50 | .peer() 51 | .call_tool(rmcp::model::CallToolRequestParam { 52 | name: sum_tool.name.clone(), 53 | arguments: Some(rmcp::object!({ 54 | "a": 10, 55 | "b": 20 56 | })), 57 | }) 58 | .await?; 59 | 60 | println!("Result: {:?}", result); 61 | } 62 | 63 | Ok(()) 64 | } 65 | tokio::spawn(server(PIPE_NAME)); 66 | let mut clients = vec![]; 67 | 68 | for _ in 0..100 { 69 | clients.push(client()); 70 | } 71 | for client in clients { 72 | client.await?; 73 | } 74 | Ok(()) 75 | } 76 | 77 | #[cfg(not(target_family = "windows"))] 78 | fn main() { 79 | println!("Unix socket example is not supported on this platform."); 80 | } 81 | -------------------------------------------------------------------------------- /examples/transport/src/tcp.rs: -------------------------------------------------------------------------------- 1 | use common::calculator::Calculator; 2 | use rmcp::{serve_client, serve_server}; 3 | 4 | mod common; 5 | #[tokio::main] 6 | async fn main() -> anyhow::Result<()> { 7 | tokio::spawn(server()); 8 | client().await?; 9 | Ok(()) 10 | } 11 | 12 | async fn server() -> anyhow::Result<()> { 13 | let tcp_listener = tokio::net::TcpListener::bind("127.0.0.1:8001").await?; 14 | while let Ok((stream, _)) = tcp_listener.accept().await { 15 | tokio::spawn(async move { 16 | let server = serve_server(Calculator::new(), stream).await?; 17 | server.waiting().await?; 18 | anyhow::Ok(()) 19 | }); 20 | } 21 | Ok(()) 22 | } 23 | 24 | async fn client() -> anyhow::Result<()> { 25 | let stream = tokio::net::TcpSocket::new_v4()? 26 | .connect("127.0.0.1:8001".parse()?) 27 | .await?; 28 | let client = serve_client((), stream).await?; 29 | let tools = client.peer().list_tools(Default::default()).await?; 30 | println!("{:?}", tools); 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /examples/transport/src/unix_socket.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | #[cfg(target_family = "unix")] 4 | #[tokio::main] 5 | async fn main() -> anyhow::Result<()> { 6 | use std::fs; 7 | 8 | use common::calculator::Calculator; 9 | use rmcp::{serve_client, serve_server}; 10 | use tokio::net::{UnixListener, UnixStream}; 11 | 12 | const SOCKET_PATH: &str = "/tmp/rmcp_example.sock"; 13 | async fn server(unix_listener: UnixListener) -> anyhow::Result<()> { 14 | while let Ok((stream, addr)) = unix_listener.accept().await { 15 | println!("Client connected: {:?}", addr); 16 | tokio::spawn(async move { 17 | match serve_server(Calculator::new(), stream).await { 18 | Ok(server) => { 19 | println!("Server initialized successfully"); 20 | if let Err(e) = server.waiting().await { 21 | println!("Error while server waiting: {}", e); 22 | } 23 | } 24 | Err(e) => println!("Server initialization failed: {}", e), 25 | } 26 | 27 | anyhow::Ok(()) 28 | }); 29 | } 30 | Ok(()) 31 | } 32 | 33 | async fn client() -> anyhow::Result<()> { 34 | println!("Client connecting to {}", SOCKET_PATH); 35 | let stream = UnixStream::connect(SOCKET_PATH).await?; 36 | 37 | let client = serve_client((), stream).await?; 38 | println!("Client connected and initialized successfully"); 39 | 40 | // List available tools 41 | let tools = client.peer().list_tools(Default::default()).await?; 42 | println!("Available tools: {:?}", tools); 43 | 44 | // Call the sum tool 45 | if let Some(sum_tool) = tools.tools.iter().find(|t| t.name.contains("sum")) { 46 | println!("Calling sum tool: {}", sum_tool.name); 47 | let result = client 48 | .peer() 49 | .call_tool(rmcp::model::CallToolRequestParam { 50 | name: sum_tool.name.clone(), 51 | arguments: Some(rmcp::object!({ 52 | "a": 10, 53 | "b": 20 54 | })), 55 | }) 56 | .await?; 57 | 58 | println!("Result: {:?}", result); 59 | } 60 | 61 | Ok(()) 62 | } 63 | 64 | // Remove any existing socket file 65 | let _ = fs::remove_file(SOCKET_PATH); 66 | match UnixListener::bind(SOCKET_PATH) { 67 | Ok(unix_listener) => { 68 | println!("Server successfully listening on {}", SOCKET_PATH); 69 | tokio::spawn(server(unix_listener)); 70 | } 71 | Err(e) => { 72 | println!("Unable to bind to {}: {}", SOCKET_PATH, e); 73 | } 74 | } 75 | 76 | client().await?; 77 | 78 | // Clean up socket file 79 | let _ = fs::remove_file(SOCKET_PATH); 80 | 81 | Ok(()) 82 | } 83 | 84 | #[cfg(not(target_family = "unix"))] 85 | fn main() { 86 | println!("Unix socket example is not supported on this platform."); 87 | } 88 | -------------------------------------------------------------------------------- /examples/wasi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasi-mcp-example" 3 | edition = { workspace = true } 4 | version = { workspace = true } 5 | authors = { workspace = true } 6 | license = { workspace = true } 7 | repository = { workspace = true } 8 | description = { workspace = true } 9 | keywords = { workspace = true } 10 | homepage = { workspace = true } 11 | categories = { workspace = true } 12 | readme = { workspace = true } 13 | publish = false 14 | 15 | [lib] 16 | crate-type = ["cdylib"] 17 | 18 | [dependencies] 19 | wasi = { version = "0.14.2"} 20 | tokio = { version = "1", features = ["rt", "io-util", "sync", "macros", "time"] } 21 | rmcp = { workspace = true, features = ["server", "macros"] } 22 | serde = { version = "1", features = ["derive"]} 23 | tracing-subscriber = { version = "0.3", features = [ 24 | "env-filter", 25 | "std", 26 | "fmt", 27 | ] } 28 | tracing = "0.1" 29 | -------------------------------------------------------------------------------- /examples/wasi/README.md: -------------------------------------------------------------------------------- 1 | # Example for WASI-p2 2 | 3 | Build: 4 | 5 | ```sh 6 | cargo build -p wasi-mcp-example --target wasm32-wasip2 7 | ``` 8 | 9 | Run: 10 | 11 | ``` 12 | npx @modelcontextprotocol/inspector wasmtime target/wasm32-wasip2/debug/wasi_mcp_example.wasm 13 | ``` 14 | 15 | *Note:* Change `wasmtime` to a different installed run time, if needed. 16 | 17 | The printed URL of the MCP inspector can be opened and a connection to the module established via `STDIO`. -------------------------------------------------------------------------------- /examples/wasi/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-wasip2" -------------------------------------------------------------------------------- /examples/wasi/src/calculator.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use rmcp::{ 4 | ServerHandler, 5 | handler::server::{ 6 | router::tool::ToolRouter, 7 | wrapper::{Json, Parameters}, 8 | }, 9 | model::{ServerCapabilities, ServerInfo}, 10 | schemars, tool, tool_handler, tool_router, 11 | }; 12 | 13 | #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] 14 | pub struct SumRequest { 15 | #[schemars(description = "the left hand side number")] 16 | pub a: i32, 17 | pub b: i32, 18 | } 19 | 20 | #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] 21 | pub struct SubRequest { 22 | #[schemars(description = "the left hand side number")] 23 | pub a: i32, 24 | #[schemars(description = "the right hand side number")] 25 | pub b: i32, 26 | } 27 | 28 | #[derive(Debug, Clone)] 29 | pub struct Calculator { 30 | tool_router: ToolRouter, 31 | } 32 | 33 | impl Calculator { 34 | pub fn new() -> Self { 35 | Self { 36 | tool_router: Self::tool_router(), 37 | } 38 | } 39 | } 40 | 41 | impl Default for Calculator { 42 | fn default() -> Self { 43 | Self::new() 44 | } 45 | } 46 | 47 | #[tool_router] 48 | impl Calculator { 49 | #[tool(description = "Calculate the sum of two numbers")] 50 | fn sum(&self, Parameters(SumRequest { a, b }): Parameters) -> String { 51 | (a + b).to_string() 52 | } 53 | 54 | #[tool(description = "Calculate the difference of two numbers")] 55 | fn sub(&self, Parameters(SubRequest { a, b }): Parameters) -> Json { 56 | Json(a - b) 57 | } 58 | } 59 | 60 | #[tool_handler] 61 | impl ServerHandler for Calculator { 62 | fn get_info(&self) -> ServerInfo { 63 | ServerInfo { 64 | instructions: Some("A simple calculator".into()), 65 | capabilities: ServerCapabilities::builder().enable_tools().build(), 66 | ..Default::default() 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/wasi/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod calculator; 2 | use std::task::{Poll, Waker}; 3 | 4 | use rmcp::ServiceExt; 5 | use tokio::io::{AsyncRead, AsyncWrite}; 6 | use tracing_subscriber::EnvFilter; 7 | use wasi::{ 8 | cli::{ 9 | stdin::{InputStream, get_stdin}, 10 | stdout::{OutputStream, get_stdout}, 11 | }, 12 | io::streams::Pollable, 13 | }; 14 | 15 | pub fn wasi_io() -> (AsyncInputStream, AsyncOutputStream) { 16 | let input = AsyncInputStream { inner: get_stdin() }; 17 | let output = AsyncOutputStream { 18 | inner: get_stdout(), 19 | }; 20 | (input, output) 21 | } 22 | 23 | pub struct AsyncInputStream { 24 | inner: InputStream, 25 | } 26 | 27 | impl AsyncRead for AsyncInputStream { 28 | fn poll_read( 29 | self: std::pin::Pin<&mut Self>, 30 | cx: &mut std::task::Context<'_>, 31 | buf: &mut tokio::io::ReadBuf<'_>, 32 | ) -> std::task::Poll> { 33 | let bytes = self 34 | .inner 35 | .read(buf.remaining() as u64) 36 | .map_err(std::io::Error::other)?; 37 | if bytes.is_empty() { 38 | let pollable = self.inner.subscribe(); 39 | let waker = cx.waker().clone(); 40 | runtime_poll(waker, pollable); 41 | return Poll::Pending; 42 | } 43 | buf.put_slice(&bytes); 44 | std::task::Poll::Ready(Ok(())) 45 | } 46 | } 47 | 48 | pub struct AsyncOutputStream { 49 | inner: OutputStream, 50 | } 51 | fn runtime_poll(waker: Waker, pollable: Pollable) { 52 | tokio::task::spawn(async move { 53 | loop { 54 | if pollable.ready() { 55 | waker.wake(); 56 | break; 57 | } else { 58 | tokio::task::yield_now().await; 59 | } 60 | } 61 | }); 62 | } 63 | impl AsyncWrite for AsyncOutputStream { 64 | fn poll_write( 65 | self: std::pin::Pin<&mut Self>, 66 | cx: &mut std::task::Context<'_>, 67 | buf: &[u8], 68 | ) -> Poll> { 69 | let writable_len = self.inner.check_write().map_err(std::io::Error::other)?; 70 | if writable_len == 0 { 71 | let pollable = self.inner.subscribe(); 72 | let waker = cx.waker().clone(); 73 | runtime_poll(waker, pollable); 74 | return Poll::Pending; 75 | } 76 | let bytes_to_write = buf.len().min(writable_len as usize); 77 | self.inner 78 | .write(&buf[0..bytes_to_write]) 79 | .map_err(std::io::Error::other)?; 80 | Poll::Ready(Ok(bytes_to_write)) 81 | } 82 | 83 | fn poll_flush( 84 | self: std::pin::Pin<&mut Self>, 85 | _cx: &mut std::task::Context<'_>, 86 | ) -> Poll> { 87 | self.inner.flush().map_err(std::io::Error::other)?; 88 | Poll::Ready(Ok(())) 89 | } 90 | 91 | fn poll_shutdown( 92 | self: std::pin::Pin<&mut Self>, 93 | cx: &mut std::task::Context<'_>, 94 | ) -> Poll> { 95 | self.poll_flush(cx) 96 | } 97 | } 98 | 99 | struct TokioCliRunner; 100 | 101 | impl wasi::exports::cli::run::Guest for TokioCliRunner { 102 | fn run() -> Result<(), ()> { 103 | let rt = tokio::runtime::Builder::new_current_thread() 104 | .enable_all() 105 | .build() 106 | .unwrap(); 107 | rt.block_on(async move { 108 | tracing_subscriber::fmt() 109 | .with_env_filter( 110 | EnvFilter::from_default_env().add_directive(tracing::Level::DEBUG.into()), 111 | ) 112 | .with_writer(std::io::stderr) 113 | .with_ansi(false) 114 | .init(); 115 | let server = calculator::Calculator::new() 116 | .serve(wasi_io()) 117 | .await 118 | .unwrap(); 119 | server.waiting().await.unwrap(); 120 | }); 121 | Ok(()) 122 | } 123 | } 124 | wasi::cli::command::export!(TokioCliRunner); 125 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | fmt: 2 | cargo +nightly fmt --all 3 | 4 | check: 5 | cargo clippy --all-targets --all-features -- -D warnings 6 | 7 | fix: fmt 8 | git add ./ 9 | cargo clippy --fix --all-targets --all-features --allow-staged 10 | 11 | test: 12 | cargo test --all-features 13 | 14 | cov: 15 | cargo llvm-cov --lcov --output-path {{justfile_directory()}}/target/llvm-cov-target/coverage.lcov -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.90" 3 | components = ["rustc", "rust-std", "cargo", "clippy", "rustfmt", "rust-docs"] 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | newline_style = "Unix" 2 | unstable_features = true # Cargo fmt now needs to be called with `cargo +nightly fmt` 3 | group_imports = "StdExternalCrate" # Create 3 groups: std, external crates, and self. 4 | imports_granularity = "Crate" # Merge imports from the same crate into a single use statement 5 | style_edition = "2024" 6 | max_width = 100 --------------------------------------------------------------------------------