├── .github └── workflows │ └── crate-tests.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── imgs ├── mcp_logo.png ├── plus.svg └── rust_logo.png ├── mcp-core-macros ├── Cargo.toml ├── README.md ├── src │ └── lib.rs └── tests │ └── annotation_tests.rs └── mcp-core ├── Cargo.toml ├── examples ├── echo_client.rs └── echo_server.rs └── src ├── client.rs ├── lib.rs ├── protocol.rs ├── server.rs ├── tools.rs ├── transport ├── client │ ├── mod.rs │ ├── sse.rs │ └── stdio.rs ├── mod.rs └── server │ ├── mod.rs │ ├── sse.rs │ └── stdio.rs └── types.rs /.github/workflows/crate-tests.yml: -------------------------------------------------------------------------------- 1 | name: Crate Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | test-mcp-core: 14 | name: Test mcp-core 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Install Rust toolchain 20 | uses: dtolnay/rust-toolchain@stable 21 | 22 | - name: Cache dependencies 23 | uses: Swatinem/rust-cache@v2 24 | 25 | - name: Run tests for mcp-core 26 | run: cargo test -p mcp-core --verbose 27 | 28 | test-mcp-core-macros: 29 | name: Test mcp-core-macros 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - name: Install Rust toolchain 35 | uses: dtolnay/rust-toolchain@stable 36 | 37 | - name: Cache dependencies 38 | uses: Swatinem/rust-cache@v2 39 | 40 | - name: Run tests for mcp-core-macros 41 | run: cargo test -p mcp-core-macros --verbose 42 | 43 | docs-test: 44 | name: Documentation Tests 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v4 48 | 49 | - name: Install Rust toolchain 50 | uses: dtolnay/rust-toolchain@stable 51 | 52 | - name: Cache dependencies 53 | uses: Swatinem/rust-cache@v2 54 | 55 | - name: Run documentation tests 56 | run: cargo test --doc --workspace --verbose 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | # Generated by Cargo 4 | # will have compiled files and executables 5 | debug/ 6 | target/ 7 | 8 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 9 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 10 | Cargo.lock 11 | 12 | # These are backup files generated by rustfmt 13 | **/*.rs.bk 14 | 15 | # MSVC Windows builds of rustc generate these, which store debugging information 16 | *.pdb -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["mcp-core", "mcp-core-macros"] 4 | 5 | [patch.crates-io] 6 | mcp-core = { path = "./mcp-core" } 7 | mcp-core-macros = { path = "./mcp-core-macros" } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | mcp_logo 3 | plus_svg 4 | rust_logo 5 |

6 |

7 |

MCP Core

8 |

9 | A Rust library implementing the Modern Context Protocol (MCP) 10 |

11 |

12 | stars 13 |   14 | Crates.io 15 |   16 |

17 | 18 | ## Project Goals 19 | Combine efforts with [Offical MCP Rust SDK](https://github.com/modelcontextprotocol/rust-sdk). The offical SDK repo is new and collaborations are in works to bring these features to the adopted platform. 20 | - **Efficiency & Scalability** 21 | - Handles many concurrent connections with low overhead. 22 | - Scales easily across multiple nodes. 23 | - **Security** 24 | - Strong authentication and authorization. 25 | - Built-in rate limiting and quota management. 26 | - **Rust Advantages** 27 | - High performance and predictable latency. 28 | - Memory safety with no runtime overhead. 29 | 30 | ## Installation 31 | 32 | Use the `cargo add` command to automatically add it to your `Cargo.toml` 33 | ```bash 34 | cargo add mcp-core 35 | ``` 36 | Or add `mcp-core` to your `Cargo.toml` dependencies directly 37 | ```toml 38 | [dependencies] 39 | mcp-core = "0.1.50" 40 | ``` 41 | 42 | ## Server Implementation 43 | Easily start your own local SSE MCP Servers with tooling capabilities. To use SSE functionality, make sure to enable the "http" feature in your Cargo.toml `mcp-core = { version = "0.1.50", features = ["sse"] }` 44 | ```rust 45 | use anyhow::Result; 46 | use clap::{Parser, ValueEnum}; 47 | use mcp_core::{ 48 | server::Server, 49 | tool_text_response, 50 | tools::ToolHandlerFn, 51 | transport::{ServerSseTransport, ServerStdioTransport}, 52 | types::{CallToolRequest, ServerCapabilities, Tool, ToolCapabilities}, 53 | }; 54 | use serde_json::json; 55 | 56 | #[derive(Parser)] 57 | #[command(author, version, about, long_about = None)] 58 | struct Cli { 59 | /// Transport type to use 60 | #[arg(value_enum, default_value_t = TransportType::Stdio)] 61 | transport: TransportType, 62 | } 63 | 64 | #[derive(Copy, Clone, PartialEq, Eq, ValueEnum)] 65 | enum TransportType { 66 | Stdio, 67 | Sse, 68 | } 69 | 70 | struct EchoTool; 71 | 72 | impl EchoTool { 73 | fn tool() -> Tool { 74 | Tool { 75 | name: "echo".to_string(), 76 | description: Some("Echo back the message you send".to_string()), 77 | input_schema: json!({ 78 | "type": "object", 79 | "properties": { 80 | "message": { 81 | "type": "string", 82 | "description": "The message to echo back" 83 | } 84 | }, 85 | "required": ["message"] 86 | }), 87 | annotations: None, 88 | } 89 | } 90 | 91 | fn call() -> ToolHandlerFn { 92 | move |request: CallToolRequest| { 93 | Box::pin(async move { 94 | let message = request 95 | .arguments 96 | .as_ref() 97 | .and_then(|args| args.get("message")) 98 | .and_then(|v| v.as_str()) 99 | .unwrap_or("") 100 | .to_string(); 101 | 102 | tool_text_response!(message) 103 | }) 104 | } 105 | } 106 | } 107 | 108 | #[tokio::main] 109 | async fn main() -> Result<()> { 110 | tracing_subscriber::fmt() 111 | .with_max_level(tracing::Level::DEBUG) 112 | .with_writer(std::io::stderr) 113 | .init(); 114 | 115 | let cli = Cli::parse(); 116 | 117 | let server_protocol = Server::builder( 118 | "echo".to_string(), 119 | "1.0".to_string(), 120 | mcp_core::types::ProtocolVersion::V2024_11_05 121 | ) 122 | .set_capabilities(ServerCapabilities { 123 | tools: Some(ToolCapabilities::default()), 124 | ..Default::default() 125 | }) 126 | .register_tool(EchoTool::tool(), EchoTool::call()) 127 | .build(); 128 | 129 | match cli.transport { 130 | TransportType::Stdio => { 131 | let transport = ServerStdioTransport::new(server_protocol); 132 | Server::start(transport).await 133 | } 134 | TransportType::Sse => { 135 | let transport = ServerSseTransport::new("127.0.0.1".to_string(), 3000, server_protocol); 136 | Server::start(transport).await 137 | } 138 | } 139 | } 140 | ``` 141 | 142 | ## Creating MCP Tools 143 | There are two ways to create tools in MCP Core: using macros (recommended) or manually implementing the tool trait. 144 | 145 | ### Using Macros (Recommended) 146 | The easiest way to create a tool is using the `mcp-core-macros` crate. First, add it to your dependencies: 147 | ```toml 148 | [dependencies] 149 | mcp-core-macros = "0.1.30" 150 | ``` 151 | 152 | Then create your tool using the `#[tool]` macro: 153 | ```rust 154 | use mcp_core::{tool_text_content, types::ToolResponseContent}; 155 | use mcp_core_macros::{tool, tool_param}; 156 | use anyhow::Result; 157 | 158 | #[tool( 159 | name = "echo", 160 | description = "Echo back the message you send", 161 | annotations( 162 | title = "Echo Tool", 163 | read_only_hint = true, 164 | destructive_hint = false 165 | ) 166 | )] 167 | async fn echo_tool( 168 | message: tool_param!(String, description = "The message to echo back") 169 | ) -> Result { 170 | Ok(tool_text_content!(message)) 171 | } 172 | ``` 173 | 174 | The macro automatically generates all the necessary boilerplate code for your tool. You can then register it with your server: 175 | 176 | ```rust 177 | let server_protocol = Server::builder( 178 | "echo".to_string(), 179 | "1.0".to_string(), 180 | mcp_core::types::ProtocolVersion::V2024_11_05 181 | ) 182 | .set_capabilities(ServerCapabilities { 183 | tools: Some(ToolCapabilities::default()), 184 | ..Default::default() 185 | }) 186 | .register_tool(EchoTool::tool(), EchoTool::call()) 187 | .build(); 188 | ``` 189 | 190 | ### Tool Parameters 191 | Tools can have various parameter types that are automatically deserialized from the client's JSON input: 192 | - Basic types (String, f64, bool) 193 | - Optional types (Option) 194 | - Custom parameter attributes 195 | 196 | For example: 197 | ```rust 198 | #[tool( 199 | name = "complex_tool", 200 | description = "A tool with complex parameters" 201 | )] 202 | async fn complex_tool( 203 | // A required parameter with description 204 | text: tool_param!(String, description = "A text parameter"), 205 | 206 | // An optional parameter 207 | number: tool_param!(Option, description = "An optional number parameter"), 208 | 209 | // A hidden parameter that won't appear in the schema 210 | internal_param: tool_param!(String, hidden) 211 | ) -> Result { 212 | // Tool implementation 213 | Ok(tool_text_content!("Tool executed successfully")) 214 | } 215 | ``` 216 | 217 | ## SSE Client Connection 218 | Connect to an SSE MCP Server using the `ClientSseTransport`. Here is an example of connecting to one and listing the tools from that server. 219 | ```rust 220 | use std::time::Duration; 221 | 222 | use anyhow::Result; 223 | use clap::{Parser, ValueEnum}; 224 | use mcp_core::{ 225 | client::ClientBuilder, 226 | protocol::RequestOptions, 227 | transport::{ClientSseTransportBuilder, ClientStdioTransport}, 228 | }; 229 | use serde_json::json; 230 | use tracing::info; 231 | 232 | #[derive(Parser)] 233 | #[command(author, version, about, long_about = None)] 234 | struct Cli { 235 | /// Transport type to use 236 | #[arg(value_enum, default_value_t = TransportType::Sse)] 237 | transport: TransportType, 238 | } 239 | 240 | #[derive(Copy, Clone, PartialEq, Eq, ValueEnum)] 241 | enum TransportType { 242 | Stdio, 243 | Sse, 244 | } 245 | 246 | #[tokio::main] 247 | async fn main() -> Result<()> { 248 | tracing_subscriber::fmt() 249 | .with_max_level(tracing::Level::DEBUG) 250 | .with_writer(std::io::stderr) 251 | .init(); 252 | 253 | let cli = Cli::parse(); 254 | 255 | let response = match cli.transport { 256 | TransportType::Stdio => { 257 | // Build the server first 258 | // cargo run --example echo_server --features="sse" 259 | let transport = ClientStdioTransport::new("./target/debug/examples/echo_server", &[])?; 260 | let client = ClientBuilder::new(transport.clone()) 261 | .set_protocol_version(mcp_core::types::ProtocolVersion::V2024_11_05) 262 | .set_client_info("echo_client".to_string(), "0.1.0".to_string()) 263 | .build(); 264 | tokio::time::sleep(Duration::from_millis(100)).await; 265 | client.open().await?; 266 | 267 | client.initialize().await?; 268 | 269 | client 270 | .call_tool( 271 | "echo", 272 | Some(json!({ 273 | "message": "Hello, world!" 274 | })), 275 | ) 276 | .await? 277 | } 278 | TransportType::Sse => { 279 | let client = ClientBuilder::new( 280 | ClientSseTransportBuilder::new("http://localhost:3000/sse".to_string()).build(), 281 | ) 282 | .set_protocol_version(mcp_core::types::ProtocolVersion::V2024_11_05) 283 | .set_client_info("echo_client".to_string(), "0.1.0".to_string()) 284 | .build(); 285 | client.open().await?; 286 | 287 | client.initialize().await?; 288 | 289 | client 290 | .request( 291 | "tools/list", 292 | None, 293 | RequestOptions::default().timeout(Duration::from_secs(5)), 294 | ) 295 | .await?; 296 | 297 | client 298 | .call_tool( 299 | "echo", 300 | Some(json!({ 301 | "message": "Hello, world!" 302 | })), 303 | ) 304 | .await? 305 | } 306 | }; 307 | info!("response: {:?}", response); 308 | Ok(()) 309 | } 310 | ``` 311 | 312 | ### Setting `SecureValues` to your SSE MCP Client 313 | Have API Keys or Secrets needed to be passed to MCP Tool Calls, but you don't want to pass this information to the LLM you are prompting? Use `mcp_core::client::SecureValue`! 314 | ```rust 315 | ClientBuilder::new( 316 | ClientSseTransportBuilder::new("http://localhost:3000/sse".to_string()).build(), 317 | ) 318 | .with_secure_value( 319 | "discord_token", 320 | mcp_core::client::SecureValue::Static(discord_token), 321 | ) 322 | .with_secure_value( 323 | "anthropic_api_key", 324 | mcp_core::client::SecureValue::Env("ANTHROPIC_API_KEY".to_string()), 325 | ) 326 | .use_strict() 327 | .build() 328 | ``` 329 | #### mcp_core::client::SecureValue::Static 330 | Automatically have **MCP Tool Call Parameters** be replaced by the string value set to it. 331 | #### mcp_core::client::SecureValue::Env 332 | Automatically have **MCP Tool Call Parameters** be replaced by the value in your `.env` from the string set to it. -------------------------------------------------------------------------------- /imgs/mcp_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevohuncho/mcp-core/846ce00239e72fe8c60fb46132fbce2680d0c3df/imgs/mcp_logo.png -------------------------------------------------------------------------------- /imgs/plus.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /imgs/rust_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevohuncho/mcp-core/846ce00239e72fe8c60fb46132fbce2680d0c3df/imgs/rust_logo.png -------------------------------------------------------------------------------- /mcp-core-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mcp-core-macros" 3 | version = "0.1.30" 4 | edition = "2021" 5 | description = "A Rust Macros library for mcp-core" 6 | repository = "https://github.com/stevohuncho/mcp-core" 7 | license = "Apache-2.0" 8 | authors = ["https://github.com/stevohuncho"] 9 | documentation = "https://github.com/stevohuncho/mcp-core/tree/main/mcp-core-macros" 10 | homepage = "https://github.com/stevohuncho/mcp-core" 11 | readme = "README.md" 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | syn = { version = "2.0", features = ["full", "extra-traits"] } 18 | quote = "1.0" 19 | proc-macro2 = "1.0" 20 | serde = { version = "1.0", features = ["derive"] } 21 | serde_json = "1.0" 22 | mcp-core = "0.1.50" 23 | async-trait = "0.1" 24 | schemars = "0.8" 25 | convert_case = "0.6.0" 26 | 27 | [dev-dependencies] 28 | tokio = { version = "1.0", features = ["full"] } 29 | async-trait = "0.1" 30 | serde_json = "1.0" 31 | anyhow = "1.0" 32 | -------------------------------------------------------------------------------- /mcp-core-macros/README.md: -------------------------------------------------------------------------------- 1 | # MCP Core Macros 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/mcp-core-macros.svg)](https://crates.io/crates/mcp-core-macros) 4 | [![Documentation](https://docs.rs/mcp-core-macros/badge.svg)](https://docs.rs/mcp-core-macros) 5 | 6 | A Rust library providing procedural macros for the MCP Core system. 7 | 8 | ## Overview 9 | 10 | This crate provides procedural macros that simplify the process of creating tool definitions for the MCP system. The macros handle generating tool metadata, parameter schemas, and the necessary boilerplate code for tool registration. 11 | 12 | ## Macros 13 | 14 | ### `#[tool]` Attribute Macro 15 | 16 | The `tool` attribute macro transforms an async function into a tool that can be registered with the MCP system. It automatically generates: 17 | 18 | - A struct named after the function (e.g., `web_search_tool` → `WebSearchTool`) 19 | - Tool definitions with proper metadata 20 | - JSON schema for input parameters 21 | - Methods to handle tool invocation 22 | 23 | #### Arguments 24 | 25 | - `name` - The name of the tool (optional, defaults to the function name) 26 | - `description` - A description of what the tool does 27 | - `annotations` - Additional metadata for the tool: 28 | - `title` - Display title for the tool (defaults to function name) 29 | - `read_only_hint` - Whether the tool only reads data (defaults to false) 30 | - `destructive_hint` - Whether the tool makes destructive changes (defaults to true) 31 | - `idempotent_hint` - Whether the tool is idempotent (defaults to false) 32 | - `open_world_hint` - Whether the tool can access resources outside the system (defaults to true) 33 | 34 | #### Example 35 | 36 | ```rust 37 | use mcp_core_macros::{tool, tool_param}; 38 | use mcp_core::types::ToolResponseContent; 39 | use mcp_core::tool_text_content; 40 | use anyhow::Result; 41 | 42 | #[tool( 43 | name = "web_search", 44 | description = "Search the web for information", 45 | annotations( 46 | title = "Web Search", 47 | read_only_hint = true, 48 | open_world_hint = true 49 | ) 50 | )] 51 | async fn web_search_tool(query: String) -> Result { 52 | // Tool implementation 53 | Ok(tool_text_content!("Results for: ".to_string() + &query)) 54 | } 55 | ``` 56 | 57 | ### `tool_param!` Macro 58 | 59 | The `tool_param!` macro allows specifying parameter attributes such as descriptions and visibility in the generated schema. 60 | 61 | #### Arguments 62 | 63 | - `hidden` - Excludes the parameter from the generated schema 64 | - `description` - Adds a description to the parameter in the schema 65 | 66 | #### Example 67 | 68 | ```rust 69 | use mcp_core_macros::{tool, tool_param}; 70 | use mcp_core::types::ToolResponseContent; 71 | use mcp_core::tool_text_content; 72 | use anyhow::Result; 73 | 74 | #[tool(name = "my_tool", description = "A tool with documented parameters", annotations(title = "My Tool"))] 75 | async fn my_tool( 76 | // A required parameter with description 77 | required_param: tool_param!(String, description = "A required parameter"), 78 | 79 | // An optional parameter 80 | optional_param: tool_param!(Option, description = "An optional parameter"), 81 | 82 | // A hidden parameter that won't appear in the schema 83 | internal_param: tool_param!(String, hidden) 84 | ) -> Result { 85 | // Implementation 86 | Ok(tool_text_content!("Tool executed".to_string())) 87 | } 88 | ``` 89 | 90 | ## Generated Code 91 | 92 | The `tool` macro generates a structure with methods to handle tool registration and invocation. For example, the function: 93 | 94 | ```rust 95 | #[tool(name = "example", description = "An example tool")] 96 | async fn example_tool(param: String) -> Result { 97 | // Implementation 98 | } 99 | ``` 100 | 101 | Will generate code equivalent to: 102 | 103 | ```rust 104 | struct ExampleTool; 105 | 106 | impl ExampleTool { 107 | pub fn tool() -> Tool { 108 | Tool { 109 | name: "example".to_string(), 110 | description: Some("An example tool".to_string()), 111 | input_schema: json!({ 112 | "type": "object", 113 | "properties": { 114 | "param": { 115 | "type": "string" 116 | } 117 | }, 118 | "required": ["param"] 119 | }), 120 | annotations: Some(json!({ 121 | "title": "example", 122 | "readOnlyHint": false, 123 | "destructiveHint": true, 124 | "idempotentHint": false, 125 | "openWorldHint": true 126 | })), 127 | } 128 | } 129 | 130 | pub async fn call(params: serde_json::Value) -> Result { 131 | // Deserialize parameters and call the implementation 132 | let param: String = serde_json::from_value(params["param"].clone())?; 133 | example_tool(param).await 134 | } 135 | } 136 | ``` 137 | 138 | ## License 139 | 140 | This project is licensed under the Apache-2.0 License. -------------------------------------------------------------------------------- /mcp-core-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A library of procedural macros for defining MCP core tools. 2 | //! 3 | //! This crate provides macros for defining tools that can be used with the MCP system. 4 | //! The main macro is `tool`, which is used to define a tool function that can be called 5 | //! by the system. 6 | 7 | use convert_case::{Case, Casing}; 8 | use proc_macro::TokenStream; 9 | use quote::{format_ident, quote}; 10 | use syn::{ 11 | parse::{Parse, ParseStream}, 12 | punctuated::Punctuated, 13 | Expr, ExprLit, FnArg, ItemFn, Lit, Meta, Pat, PatType, Token, Type, 14 | }; 15 | 16 | #[derive(Debug)] 17 | struct ToolArgs { 18 | name: Option, 19 | description: Option, 20 | annotations: ToolAnnotations, 21 | } 22 | 23 | #[derive(Debug)] 24 | struct ToolAnnotations { 25 | title: Option, 26 | read_only_hint: Option, 27 | destructive_hint: Option, 28 | idempotent_hint: Option, 29 | open_world_hint: Option, 30 | } 31 | 32 | impl Default for ToolAnnotations { 33 | fn default() -> Self { 34 | Self { 35 | title: None, 36 | read_only_hint: None, 37 | destructive_hint: None, 38 | idempotent_hint: None, 39 | open_world_hint: None, 40 | } 41 | } 42 | } 43 | 44 | impl Parse for ToolArgs { 45 | fn parse(input: ParseStream) -> syn::Result { 46 | let mut name = None; 47 | let mut description = None; 48 | let mut annotations = ToolAnnotations::default(); 49 | 50 | let meta_list: Punctuated = Punctuated::parse_terminated(input)?; 51 | 52 | for meta in meta_list { 53 | match meta { 54 | Meta::NameValue(nv) => { 55 | let ident = nv.path.get_ident().unwrap().to_string(); 56 | if let Expr::Lit(ExprLit { 57 | lit: Lit::Str(lit_str), 58 | .. 59 | }) = nv.value 60 | { 61 | match ident.as_str() { 62 | "name" => name = Some(lit_str.value()), 63 | "description" => description = Some(lit_str.value()), 64 | _ => { 65 | return Err(syn::Error::new_spanned( 66 | nv.path, 67 | format!("Unknown attribute: {}", ident), 68 | )) 69 | } 70 | } 71 | } else { 72 | return Err(syn::Error::new_spanned(nv.value, "Expected string literal")); 73 | } 74 | } 75 | Meta::List(list) if list.path.is_ident("annotations") => { 76 | let nested: Punctuated = 77 | list.parse_args_with(Punctuated::parse_terminated)?; 78 | 79 | for meta in nested { 80 | if let Meta::NameValue(nv) = meta { 81 | let key = nv.path.get_ident().unwrap().to_string(); 82 | 83 | if let Expr::Lit(ExprLit { 84 | lit: Lit::Str(lit_str), 85 | .. 86 | }) = nv.value 87 | { 88 | if key == "title" { 89 | annotations.title = Some(lit_str.value()); 90 | } else { 91 | return Err(syn::Error::new_spanned( 92 | nv.path, 93 | format!("Unknown string annotation: {}", key), 94 | )); 95 | } 96 | } else if let Expr::Lit(ExprLit { 97 | lit: Lit::Bool(lit_bool), 98 | .. 99 | }) = nv.value 100 | { 101 | match key.as_str() { 102 | "read_only_hint" | "readOnlyHint" => { 103 | annotations.read_only_hint = Some(lit_bool.value) 104 | } 105 | "destructive_hint" | "destructiveHint" => { 106 | annotations.destructive_hint = Some(lit_bool.value) 107 | } 108 | "idempotent_hint" | "idempotentHint" => { 109 | annotations.idempotent_hint = Some(lit_bool.value) 110 | } 111 | "open_world_hint" | "openWorldHint" => { 112 | annotations.open_world_hint = Some(lit_bool.value) 113 | } 114 | _ => { 115 | return Err(syn::Error::new_spanned( 116 | nv.path, 117 | format!("Unknown boolean annotation: {}", key), 118 | )) 119 | } 120 | } 121 | } else { 122 | return Err(syn::Error::new_spanned( 123 | nv.value, 124 | "Expected string or boolean literal for annotation value", 125 | )); 126 | } 127 | } else { 128 | return Err(syn::Error::new_spanned( 129 | meta, 130 | "Expected name-value pair for annotation", 131 | )); 132 | } 133 | } 134 | } 135 | _ => { 136 | return Err(syn::Error::new_spanned( 137 | meta, 138 | "Expected name-value pair or list", 139 | )) 140 | } 141 | } 142 | } 143 | 144 | Ok(ToolArgs { 145 | name, 146 | description, 147 | annotations, 148 | }) 149 | } 150 | } 151 | 152 | /// Defines a tool function that can be called by the MCP system. 153 | /// 154 | /// This macro transforms an async function into a tool that can be registered with the MCP system. 155 | /// It generates a corresponding structure with methods to get the tool definition and to handle 156 | /// calls to the tool. 157 | /// 158 | /// # Arguments 159 | /// 160 | /// * `name` - The name of the tool (optional, defaults to the function name) 161 | /// * `description` - A description of what the tool does 162 | /// * `annotations` - Additional metadata for the tool: 163 | /// * `title` - Display title for the tool (defaults to function name) 164 | /// * `read_only_hint` - Whether the tool only reads data (defaults to false) 165 | /// * `destructive_hint` - Whether the tool makes destructive changes (defaults to true) 166 | /// * `idempotent_hint` - Whether the tool is idempotent (defaults to false) 167 | /// * `open_world_hint` - Whether the tool can access resources outside the system (defaults to true) 168 | /// 169 | /// # Example 170 | /// 171 | /// ```rust 172 | /// use mcp_core_macros::{tool, tool_param}; 173 | /// use mcp_core::types::ToolResponseContent; 174 | /// use mcp_core::tool_text_content; 175 | /// use anyhow::Result; 176 | /// 177 | /// #[tool(name = "my_tool", description = "A tool with documented parameters", annotations(title = "My Tool"))] 178 | /// async fn my_tool( 179 | /// // A required parameter with description 180 | /// required_param: tool_param!(String, description = "A required parameter"), 181 | /// 182 | /// // An optional parameter 183 | /// optional_param: tool_param!(Option, description = "An optional parameter"), 184 | /// 185 | /// // A hidden parameter that won't appear in the schema 186 | /// internal_param: tool_param!(String, hidden) 187 | /// ) -> Result { 188 | /// // Implementation 189 | /// Ok(tool_text_content!("Tool executed".to_string())) 190 | /// } 191 | /// ``` 192 | #[proc_macro_attribute] 193 | pub fn tool(args: TokenStream, input: TokenStream) -> TokenStream { 194 | let args = match syn::parse::(args) { 195 | Ok(args) => args, 196 | Err(e) => return e.to_compile_error().into(), 197 | }; 198 | 199 | let input_fn = match syn::parse::(input.clone()) { 200 | Ok(input_fn) => input_fn, 201 | Err(e) => return e.to_compile_error().into(), 202 | }; 203 | 204 | let fn_name = &input_fn.sig.ident; 205 | let fn_name_str = fn_name.to_string(); 206 | let struct_name = format_ident!("{}", fn_name_str.to_case(Case::Pascal)); 207 | let tool_name = args.name.unwrap_or(fn_name_str.clone()); 208 | let tool_description = args.description.unwrap_or_default(); 209 | 210 | // Tool annotations 211 | let title = args.annotations.title.unwrap_or(fn_name_str.clone()); 212 | let read_only_hint = args.annotations.read_only_hint.unwrap_or(false); 213 | let destructive_hint = args.annotations.destructive_hint.unwrap_or(true); 214 | let idempotent_hint = args.annotations.idempotent_hint.unwrap_or(false); 215 | let open_world_hint = args.annotations.open_world_hint.unwrap_or(true); 216 | 217 | let mut param_defs = Vec::new(); 218 | let mut param_names = Vec::new(); 219 | let mut required_params = Vec::new(); 220 | let mut hidden_params: Vec = Vec::new(); 221 | let mut param_descriptions = Vec::new(); 222 | 223 | for arg in input_fn.sig.inputs.iter() { 224 | if let FnArg::Typed(PatType { pat, ty, .. }) = arg { 225 | let mut is_hidden = false; 226 | let mut description: Option = None; 227 | let mut is_optional = false; 228 | 229 | // Check for tool_type macro usage 230 | if let Type::Macro(type_macro) = &**ty { 231 | if let Some(ident) = type_macro.mac.path.get_ident() { 232 | if ident == "tool_param" { 233 | if let Ok(args) = 234 | syn::parse2::(type_macro.mac.tokens.clone()) 235 | { 236 | is_hidden = args.hidden; 237 | description = args.description; 238 | 239 | // Check if the parameter type is Option 240 | if let Type::Path(type_path) = &args.ty { 241 | is_optional = type_path 242 | .path 243 | .segments 244 | .last() 245 | .map_or(false, |segment| segment.ident == "Option"); 246 | } 247 | } 248 | } 249 | } 250 | } 251 | 252 | if is_hidden { 253 | if let Pat::Ident(ident) = &**pat { 254 | hidden_params.push(ident.ident.to_string()); 255 | } 256 | } 257 | 258 | if let Pat::Ident(param_ident) = &**pat { 259 | let param_name = ¶m_ident.ident; 260 | let param_name_str = param_name.to_string(); 261 | 262 | param_names.push(param_name.clone()); 263 | 264 | // Check if the parameter type is Option 265 | if !is_optional { 266 | is_optional = if let Type::Path(type_path) = &**ty { 267 | type_path 268 | .path 269 | .segments 270 | .last() 271 | .map_or(false, |segment| segment.ident == "Option") 272 | } else { 273 | false 274 | } 275 | } 276 | 277 | // Only require non-optional, non-hidden 278 | if !is_optional && !is_hidden { 279 | required_params.push(param_name_str.clone()); 280 | } 281 | 282 | if let Some(desc) = description { 283 | param_descriptions.push(quote! { 284 | if name == #param_name_str { 285 | prop_obj.insert("description".to_string(), serde_json::Value::String(#desc.to_string())); 286 | } 287 | }); 288 | } 289 | 290 | param_defs.push(quote! { 291 | #param_name: #ty 292 | }); 293 | } 294 | } 295 | } 296 | 297 | let params_struct_name = format_ident!("{}Parameters", struct_name); 298 | let expanded = quote! { 299 | #[derive(serde::Deserialize, schemars::JsonSchema)] 300 | struct #params_struct_name { 301 | #(#param_defs,)* 302 | } 303 | 304 | #input_fn 305 | 306 | #[derive(Default)] 307 | pub struct #struct_name; 308 | 309 | impl #struct_name { 310 | pub fn tool() -> mcp_core::types::Tool { 311 | let schema = schemars::schema_for!(#params_struct_name); 312 | let mut schema = serde_json::to_value(schema.schema).unwrap_or_default(); 313 | if let serde_json::Value::Object(ref mut map) = schema { 314 | // Add required fields 315 | map.insert("required".to_string(), serde_json::Value::Array( 316 | vec![#(serde_json::Value::String(#required_params.to_string())),*] 317 | )); 318 | map.remove("title"); 319 | 320 | // Normalize property types 321 | if let Some(serde_json::Value::Object(props)) = map.get_mut("properties") { 322 | for (name, prop) in props.iter_mut() { 323 | if let serde_json::Value::Object(prop_obj) = prop { 324 | // Fix number types 325 | if let Some(type_val) = prop_obj.get("type") { 326 | if type_val == "integer" || type_val == "number" || prop_obj.contains_key("format") { 327 | // Convert any numeric type to "number" 328 | prop_obj.insert("type".to_string(), serde_json::Value::String("number".to_string())); 329 | prop_obj.remove("format"); 330 | prop_obj.remove("minimum"); 331 | prop_obj.remove("maximum"); 332 | } 333 | } 334 | 335 | // Fix optional types (array with null) 336 | if let Some(serde_json::Value::Array(types)) = prop_obj.get("type") { 337 | if types.len() == 2 && types.contains(&serde_json::Value::String("null".to_string())) { 338 | let mut main_type = types.iter() 339 | .find(|&t| t != &serde_json::Value::String("null".to_string())) 340 | .cloned() 341 | .unwrap_or(serde_json::Value::String("string".to_string())); 342 | 343 | // If the main type is "integer", convert it to "number" 344 | if main_type == serde_json::Value::String("integer".to_string()) { 345 | main_type = serde_json::Value::String("number".to_string()); 346 | } 347 | 348 | prop_obj.insert("type".to_string(), main_type); 349 | } 350 | } 351 | 352 | // Add descriptions if they exist 353 | #(#param_descriptions)* 354 | } 355 | } 356 | 357 | #(props.remove(#hidden_params);)* 358 | } 359 | } 360 | 361 | let annotations = serde_json::json!({ 362 | "title": #title, 363 | "readOnlyHint": #read_only_hint, 364 | "destructiveHint": #destructive_hint, 365 | "idempotentHint": #idempotent_hint, 366 | "openWorldHint": #open_world_hint 367 | }); 368 | 369 | mcp_core::types::Tool { 370 | name: #tool_name.to_string(), 371 | description: Some(#tool_description.to_string()), 372 | input_schema: schema, 373 | annotations: Some(mcp_core::types::ToolAnnotations { 374 | title: Some(#title.to_string()), 375 | read_only_hint: Some(#read_only_hint), 376 | destructive_hint: Some(#destructive_hint), 377 | idempotent_hint: Some(#idempotent_hint), 378 | open_world_hint: Some(#open_world_hint), 379 | }), 380 | } 381 | } 382 | 383 | pub fn call() -> mcp_core::tools::ToolHandlerFn { 384 | move |req: mcp_core::types::CallToolRequest| { 385 | Box::pin(async move { 386 | let params = match req.arguments { 387 | Some(args) => serde_json::to_value(args).unwrap_or_default(), 388 | None => serde_json::Value::Null, 389 | }; 390 | 391 | let params: #params_struct_name = match serde_json::from_value(params) { 392 | Ok(p) => p, 393 | Err(e) => return mcp_core::types::CallToolResponse { 394 | content: vec![mcp_core::types::ToolResponseContent::Text( 395 | mcp_core::types::TextContent { 396 | content_type: "text".to_string(), 397 | text: format!("Invalid parameters: {}", e), 398 | annotations: None, 399 | } 400 | )], 401 | is_error: Some(true), 402 | meta: None, 403 | }, 404 | }; 405 | 406 | match #fn_name(#(params.#param_names,)*).await { 407 | Ok(response) => { 408 | let content = if let Ok(vec_content) = serde_json::from_value::>(serde_json::to_value(&response).unwrap_or_default()) { 409 | vec_content 410 | } else if let Ok(single_content) = serde_json::from_value::(serde_json::to_value(&response).unwrap_or_default()) { 411 | vec![single_content] 412 | } else { 413 | vec![mcp_core::types::ToolResponseContent::Text( 414 | mcp_core::types::TextContent { 415 | content_type: "text".to_string(), 416 | text: format!("Invalid response type: {:?}", response), 417 | annotations: None, 418 | } 419 | )] 420 | }; 421 | 422 | mcp_core::types::CallToolResponse { 423 | content, 424 | is_error: None, 425 | meta: None, 426 | } 427 | } 428 | Err(e) => mcp_core::types::CallToolResponse { 429 | content: vec![mcp_core::types::ToolResponseContent::Text( 430 | mcp_core::types::TextContent { 431 | content_type: "text".to_string(), 432 | text: format!("Tool execution error: {}", e), 433 | annotations: None, 434 | } 435 | )], 436 | is_error: Some(true), 437 | meta: None, 438 | }, 439 | } 440 | }) 441 | } 442 | } 443 | } 444 | }; 445 | 446 | TokenStream::from(expanded) 447 | } 448 | 449 | #[derive(Debug)] 450 | struct ToolParamArgs { 451 | ty: Type, 452 | hidden: bool, 453 | description: Option, 454 | } 455 | 456 | impl Parse for ToolParamArgs { 457 | fn parse(input: ParseStream) -> syn::Result { 458 | let mut hidden = false; 459 | let mut description = None; 460 | let ty = input.parse()?; 461 | 462 | if input.peek(Token![,]) { 463 | input.parse::()?; 464 | let meta_list: Punctuated = Punctuated::parse_terminated(input)?; 465 | 466 | for meta in meta_list { 467 | match meta { 468 | Meta::Path(path) if path.is_ident("hidden") => { 469 | hidden = true; 470 | } 471 | Meta::NameValue(nv) if nv.path.is_ident("description") => { 472 | if let Expr::Lit(ExprLit { 473 | lit: Lit::Str(lit_str), 474 | .. 475 | }) = &nv.value 476 | { 477 | description = Some(lit_str.value().to_string()); 478 | } 479 | } 480 | _ => {} 481 | } 482 | } 483 | } 484 | 485 | Ok(ToolParamArgs { 486 | ty, 487 | hidden, 488 | description, 489 | }) 490 | } 491 | } 492 | 493 | /// Defines a parameter for a tool function with additional metadata. 494 | /// 495 | /// This macro allows specifying parameter attributes such as: 496 | /// * `hidden` - Excludes the parameter from the generated schema 497 | /// * `description` - Adds a description to the parameter in the schema 498 | /// 499 | /// # Example 500 | /// 501 | /// ```rust 502 | /// use mcp_core_macros::{tool, tool_param}; 503 | /// use mcp_core::types::ToolResponseContent; 504 | /// use mcp_core::tool_text_content; 505 | /// use anyhow::Result; 506 | /// 507 | /// #[tool(name = "my_tool", description = "A tool with documented parameters")] 508 | /// async fn my_tool( 509 | /// // A required parameter with description 510 | /// required_param: tool_param!(String, description = "A required parameter"), 511 | /// 512 | /// // An optional parameter 513 | /// optional_param: tool_param!(Option, description = "An optional parameter"), 514 | /// 515 | /// // A hidden parameter that won't appear in the schema 516 | /// internal_param: tool_param!(String, hidden) 517 | /// ) -> Result { 518 | /// // Implementation 519 | /// Ok(tool_text_content!("Tool executed".to_string())) 520 | /// } 521 | /// ``` 522 | #[proc_macro] 523 | pub fn tool_param(input: TokenStream) -> TokenStream { 524 | let args = syn::parse_macro_input!(input as ToolParamArgs); 525 | let ty = args.ty; 526 | 527 | TokenStream::from(quote! { 528 | #ty 529 | }) 530 | } 531 | -------------------------------------------------------------------------------- /mcp-core-macros/tests/annotation_tests.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use mcp_core::{tool_text_content, types::ToolResponseContent}; 3 | use mcp_core_macros::{tool, tool_param}; 4 | use serde_json::json; 5 | 6 | #[tokio::test] 7 | async fn test_readonly_tool_annotations() { 8 | #[tool( 9 | name = "web_search", 10 | description = "Search the web for information", 11 | annotations(title = "web_search", read_only_hint = true, open_world_hint = true) 12 | )] 13 | async fn web_search_tool(query: String) -> Result { 14 | Ok(tool_text_content!("Success")) 15 | } 16 | 17 | let tool = WebSearchTool::tool(); 18 | 19 | assert_eq!(tool.name, "web_search"); 20 | assert_eq!( 21 | tool.description, 22 | Some("Search the web for information".to_string()) 23 | ); 24 | 25 | let expected_schema = json!({ 26 | "type": "object", 27 | "properties": { 28 | "query": { 29 | "type": "string" 30 | } 31 | }, 32 | "required": ["query"] 33 | }); 34 | 35 | assert_eq!(tool.input_schema, expected_schema); 36 | 37 | let annotations = tool.annotations.unwrap(); 38 | assert_eq!(annotations.title, Some("web_search".to_string())); 39 | assert_eq!(annotations.read_only_hint, Some(true)); 40 | assert_eq!(annotations.destructive_hint, Some(true)); // Default value 41 | assert_eq!(annotations.idempotent_hint, Some(false)); // Default value 42 | assert_eq!(annotations.open_world_hint, Some(true)); 43 | } 44 | 45 | #[tokio::test] 46 | async fn test_destructive_tool_annotations() { 47 | #[tool( 48 | name = "delete_file", 49 | description = "Delete a file from the filesystem", 50 | annotations( 51 | read_only_hint = false, 52 | destructive_hint = true, 53 | idempotent_hint = true, 54 | open_world_hint = false, 55 | title = "Delete File" 56 | ) 57 | )] 58 | async fn delete_file_tool(path: String) -> Result { 59 | Ok(tool_text_content!("Success")) 60 | } 61 | 62 | let tool = DeleteFileTool::tool(); 63 | 64 | assert_eq!(tool.name, "delete_file"); 65 | assert_eq!( 66 | tool.description, 67 | Some("Delete a file from the filesystem".to_string()) 68 | ); 69 | 70 | let expected_schema = json!({ 71 | "type": "object", 72 | "properties": { 73 | "path": { 74 | "type": "string" 75 | } 76 | }, 77 | "required": ["path"] 78 | }); 79 | 80 | assert_eq!(tool.input_schema, expected_schema); 81 | 82 | let annotations = tool.annotations.unwrap(); 83 | assert_eq!(annotations.title, Some("Delete File".to_string())); 84 | assert_eq!(annotations.read_only_hint, Some(false)); 85 | assert_eq!(annotations.destructive_hint, Some(true)); 86 | assert_eq!(annotations.idempotent_hint, Some(true)); 87 | assert_eq!(annotations.open_world_hint, Some(false)); 88 | } 89 | 90 | #[tokio::test] 91 | async fn test_annotation_nested_syntax() { 92 | #[tool( 93 | name = "create_record", 94 | description = "Create a new record in the database", 95 | annotations( 96 | title = "Create Database Record", 97 | readOnlyHint = false, 98 | destructiveHint = false, 99 | idempotentHint = false, 100 | openWorldHint = false 101 | ) 102 | )] 103 | async fn create_record_tool(table: String, data: String) -> Result { 104 | Ok(tool_text_content!("Success")) 105 | } 106 | 107 | let tool = CreateRecordTool::tool(); 108 | 109 | assert_eq!(tool.name, "create_record"); 110 | assert_eq!( 111 | tool.description, 112 | Some("Create a new record in the database".to_string()) 113 | ); 114 | 115 | let expected_schema = json!({ 116 | "type": "object", 117 | "properties": { 118 | "table": { 119 | "type": "string" 120 | }, 121 | "data": { 122 | "type": "string" 123 | } 124 | }, 125 | "required": ["table", "data"] 126 | }); 127 | 128 | assert_eq!(tool.input_schema, expected_schema); 129 | 130 | let annotations = tool.annotations.unwrap(); 131 | assert_eq!( 132 | annotations.title, 133 | Some("Create Database Record".to_string()) 134 | ); 135 | assert_eq!(annotations.read_only_hint, Some(false)); 136 | assert_eq!(annotations.destructive_hint, Some(false)); 137 | assert_eq!(annotations.idempotent_hint, Some(false)); 138 | assert_eq!(annotations.open_world_hint, Some(false)); 139 | } 140 | 141 | #[tokio::test] 142 | async fn test_numeric_parameters() { 143 | #[tool(name = "calculate", description = "Perform a calculation")] 144 | async fn calculate_tool( 145 | value1: f64, 146 | value2: i32, 147 | operation: String, 148 | ) -> Result { 149 | Ok(tool_text_content!("Success")) 150 | } 151 | 152 | let tool = CalculateTool::tool(); 153 | 154 | assert_eq!(tool.name, "calculate"); 155 | 156 | let expected_schema = json!({ 157 | "type": "object", 158 | "properties": { 159 | "value1": { 160 | "type": "number" 161 | }, 162 | "value2": { 163 | "type": "number" 164 | }, 165 | "operation": { 166 | "type": "string" 167 | } 168 | }, 169 | "required": ["value1", "value2", "operation"] 170 | }); 171 | 172 | assert_eq!(tool.input_schema, expected_schema); 173 | } 174 | 175 | #[tokio::test] 176 | async fn test_optional_parameters() { 177 | #[tool( 178 | name = "optional_params_tool", 179 | description = "Tool with optional parameters" 180 | )] 181 | async fn optional_params_tool( 182 | required_param: tool_param!(String, description = "A required parameter"), 183 | optional_string: tool_param!(Option, description = "An optional string parameter"), 184 | optional_number: tool_param!(Option, description = "An optional number parameter"), 185 | ) -> Result { 186 | Ok(tool_text_content!("Success")) 187 | } 188 | 189 | let tool = OptionalParamsTool::tool(); 190 | 191 | let expected_schema = json!({ 192 | "type": "object", 193 | "properties": { 194 | "required_param": { 195 | "type": "string", 196 | "description": "A required parameter" 197 | }, 198 | "optional_string": { 199 | "type": "string", 200 | "description": "An optional string parameter" 201 | }, 202 | "optional_number": { 203 | "type": "number", 204 | "description": "An optional number parameter" 205 | } 206 | }, 207 | "required": ["required_param"] 208 | }); 209 | 210 | assert_eq!(tool.input_schema, expected_schema); 211 | } 212 | 213 | #[tokio::test] 214 | async fn test_parameter_descriptions() { 215 | #[tool( 216 | name = "QueryDatabase", 217 | description = "Query a database with parameters" 218 | )] 219 | async fn query_database_tool( 220 | db_name: tool_param!(String, description = "Name of the database to query"), 221 | query: tool_param!(String, description = "SQL query to execute"), 222 | timeout_ms: tool_param!(Option, description = "Query timeout in milliseconds"), 223 | ) -> Result { 224 | Ok(tool_text_content!("Success")) 225 | } 226 | 227 | let tool = QueryDatabaseTool::tool(); 228 | 229 | assert_eq!(tool.name, "QueryDatabase"); 230 | assert_eq!( 231 | tool.description, 232 | Some("Query a database with parameters".to_string()) 233 | ); 234 | 235 | // Validate schema structure 236 | let expected_schema = json!({ 237 | "type": "object", 238 | "properties": { 239 | "db_name": { 240 | "type": "string", 241 | "description": "Name of the database to query" 242 | }, 243 | "query": { 244 | "type": "string", 245 | "description": "SQL query to execute" 246 | }, 247 | "timeout_ms": { 248 | "type": "number", 249 | "description": "Query timeout in milliseconds" 250 | } 251 | }, 252 | "required": ["db_name", "query"] 253 | }); 254 | 255 | assert_eq!(tool.input_schema, expected_schema); 256 | } 257 | 258 | #[tokio::test] 259 | async fn test_parameter_hidden() { 260 | #[tool( 261 | name = "HideParameter", 262 | description = "Demo tool to show how to hide parameters" 263 | )] 264 | async fn hide_parameter_tool( 265 | shown: tool_param!(String, description = "Name of the shown parameter"), 266 | hidden: tool_param!(String, description = "Name of the hidden parameter", hidden), 267 | ) -> Result { 268 | Ok(tool_text_content!("Success")) 269 | } 270 | 271 | let tool = HideParameterTool::tool(); 272 | 273 | assert_eq!(tool.name, "HideParameter"); 274 | assert_eq!( 275 | tool.description, 276 | Some("Demo tool to show how to hide parameters".to_string()) 277 | ); 278 | 279 | // Validate schema structure 280 | let expected_schema = json!({ 281 | "type": "object", 282 | "properties": { 283 | "shown": { 284 | "type": "string", 285 | "description": "Name of the shown parameter" 286 | }, 287 | }, 288 | "required": ["shown"] 289 | }); 290 | 291 | assert_eq!(tool.input_schema, expected_schema); 292 | } 293 | -------------------------------------------------------------------------------- /mcp-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mcp-core" 3 | version = "0.1.50" 4 | edition = "2021" 5 | description = "A Rust library implementing the Modern Context Protocol (MCP)" 6 | repository = "https://github.com/stevohuncho/mcp-core" 7 | license = "Apache-2.0" 8 | authors = ["https://github.com/stevohuncho"] 9 | documentation = "https://github.com/stevohuncho/mcp-core#readme" 10 | homepage = "https://github.com/stevohuncho/mcp-core" 11 | readme = "../README.md" 12 | 13 | [dependencies] 14 | tokio = { version = "1.0", features = ["time", "sync", "rt"] } 15 | serde = { version = "1.0", features = ["derive"] } 16 | serde_json = "1.0" 17 | anyhow = "1.0" 18 | async-trait = "0.1" 19 | url = { version = "2.5", features = ["serde"] } 20 | tracing = "0.1" 21 | futures = "0.3" 22 | libc = "0.2.170" 23 | # sse dependencies 24 | uuid = { version = "1.0", features = ["v4"], optional = true } 25 | actix-web = { version = "4", optional = true } 26 | reqwest = { version = "0.12.12", features = ["json"], optional = true } 27 | reqwest-eventsource = { version = "0.6.0", optional = true } 28 | 29 | [features] 30 | sse = ["actix-web", "uuid", "reqwest", "reqwest-eventsource"] 31 | 32 | 33 | [dev-dependencies] 34 | schemars = "0.8" 35 | tokio = { version = "1.0", features = ["full"] } 36 | tracing-subscriber = "0.3" 37 | dotenv = "0.15.0" 38 | thiserror = "2.0.11" 39 | serde = { version = "1.0", features = ["derive"] } 40 | serde_json = "1.0" 41 | anyhow = "1.0" 42 | tracing = "0.1" 43 | home = "0.5.9" 44 | clap = { version = "4.4", features = ["derive"] } 45 | 46 | [[example]] 47 | name = "echo_server" 48 | required-features = ["sse"] 49 | 50 | [[example]] 51 | name = "echo_client" 52 | required-features = ["sse"] 53 | -------------------------------------------------------------------------------- /mcp-core/examples/echo_client.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::Result; 4 | use clap::{Parser, ValueEnum}; 5 | use mcp_core::{ 6 | client::ClientBuilder, 7 | protocol::RequestOptions, 8 | transport::{ClientSseTransportBuilder, ClientStdioTransport}, 9 | }; 10 | use serde_json::json; 11 | use tracing::info; 12 | 13 | #[derive(Parser)] 14 | #[command(author, version, about, long_about = None)] 15 | struct Cli { 16 | /// Transport type to use 17 | #[arg(value_enum, default_value_t = TransportType::Sse)] 18 | transport: TransportType, 19 | } 20 | 21 | #[derive(Copy, Clone, PartialEq, Eq, ValueEnum)] 22 | enum TransportType { 23 | Stdio, 24 | Sse, 25 | } 26 | 27 | #[tokio::main] 28 | async fn main() -> Result<()> { 29 | tracing_subscriber::fmt() 30 | .with_max_level(tracing::Level::DEBUG) 31 | .with_writer(std::io::stderr) 32 | .init(); 33 | 34 | let cli = Cli::parse(); 35 | 36 | let response = match cli.transport { 37 | TransportType::Stdio => { 38 | // Build the server first 39 | // cargo run --example echo_server --features="sse" 40 | let transport = ClientStdioTransport::new("./target/debug/examples/echo_server", &[])?; 41 | let client: mcp_core::client::Client = 42 | ClientBuilder::new(transport.clone()) 43 | .set_protocol_version(mcp_core::types::ProtocolVersion::V2024_11_05) 44 | .set_client_info("echo_client".to_string(), "0.1.0".to_string()) 45 | .build(); 46 | tokio::time::sleep(Duration::from_millis(100)).await; 47 | client.open().await?; 48 | 49 | client.initialize().await?; 50 | 51 | client 52 | .call_tool( 53 | "echo", 54 | Some(json!({ 55 | "message": "Hello, world!" 56 | })), 57 | ) 58 | .await? 59 | } 60 | TransportType::Sse => { 61 | let client = ClientBuilder::new( 62 | ClientSseTransportBuilder::new("http://localhost:3000/sse".to_string()).build(), 63 | ) 64 | .set_protocol_version(mcp_core::types::ProtocolVersion::V2024_11_05) 65 | .set_client_info("echo_client".to_string(), "0.1.0".to_string()) 66 | .build(); 67 | client.open().await?; 68 | 69 | client.initialize().await?; 70 | 71 | client 72 | .request( 73 | "tools/list", 74 | None, 75 | RequestOptions::default().timeout(Duration::from_secs(5)), 76 | ) 77 | .await?; 78 | 79 | client 80 | .call_tool( 81 | "echo", 82 | Some(json!({ 83 | "message": "Hello, world!" 84 | })), 85 | ) 86 | .await? 87 | } 88 | }; 89 | info!("response: {:?}", response); 90 | Ok(()) 91 | } 92 | -------------------------------------------------------------------------------- /mcp-core/examples/echo_server.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::{Parser, ValueEnum}; 3 | use mcp_core::{ 4 | server::Server, 5 | tool_text_response, 6 | tools::ToolHandlerFn, 7 | transport::{ServerSseTransport, ServerStdioTransport}, 8 | types::{CallToolRequest, ServerCapabilities, Tool, ToolCapabilities}, 9 | }; 10 | use serde_json::json; 11 | 12 | #[derive(Parser)] 13 | #[command(author, version, about, long_about = None)] 14 | struct Cli { 15 | /// Transport type to use 16 | #[arg(value_enum, default_value_t = TransportType::Stdio)] 17 | transport: TransportType, 18 | } 19 | 20 | #[derive(Copy, Clone, PartialEq, Eq, ValueEnum)] 21 | enum TransportType { 22 | Stdio, 23 | Sse, 24 | } 25 | 26 | struct EchoTool; 27 | 28 | impl EchoTool { 29 | fn tool() -> Tool { 30 | Tool { 31 | name: "echo".to_string(), 32 | description: Some("Echo back the message you send".to_string()), 33 | input_schema: json!({ 34 | "type": "object", 35 | "properties": { 36 | "message": { 37 | "type": "string", 38 | "description": "The message to echo back" 39 | } 40 | }, 41 | "required": ["message"] 42 | }), 43 | annotations: None, 44 | } 45 | } 46 | 47 | fn call() -> ToolHandlerFn { 48 | move |request: CallToolRequest| { 49 | Box::pin(async move { 50 | let message = request 51 | .arguments 52 | .as_ref() 53 | .and_then(|args| args.get("message")) 54 | .and_then(|v| v.as_str()) 55 | .unwrap_or("") 56 | .to_string(); 57 | 58 | tool_text_response!(message) 59 | }) 60 | } 61 | } 62 | } 63 | 64 | #[tokio::main] 65 | async fn main() -> Result<()> { 66 | tracing_subscriber::fmt() 67 | .with_max_level(tracing::Level::DEBUG) 68 | .with_writer(std::io::stderr) 69 | .init(); 70 | 71 | let cli = Cli::parse(); 72 | 73 | let server_protocol = Server::builder( 74 | "echo".to_string(), 75 | "1.0".to_string(), 76 | mcp_core::types::ProtocolVersion::V2024_11_05, 77 | ) 78 | .set_capabilities(ServerCapabilities { 79 | tools: Some(ToolCapabilities::default()), 80 | ..Default::default() 81 | }) 82 | .register_tool(EchoTool::tool(), EchoTool::call()) 83 | .build(); 84 | 85 | match cli.transport { 86 | TransportType::Stdio => { 87 | let transport = ServerStdioTransport::new(server_protocol); 88 | Server::start(transport).await 89 | } 90 | TransportType::Sse => { 91 | let transport = ServerSseTransport::new("127.0.0.1".to_string(), 3000, server_protocol); 92 | Server::start(transport).await 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /mcp-core/src/client.rs: -------------------------------------------------------------------------------- 1 | //! # MCP Client 2 | //! 3 | //! This module provides the client-side implementation of the Model Context Protocol (MCP). 4 | //! The client can connect to MCP servers, initialize the connection, and invoke tools 5 | //! provided by the server. 6 | //! 7 | //! The core functionality includes: 8 | //! - Establishing connections to MCP servers 9 | //! - Managing the protocol handshake 10 | //! - Discovering available tools 11 | //! - Invoking tools with parameters 12 | //! - Handling server resources 13 | 14 | use std::{collections::HashMap, env, sync::Arc}; 15 | 16 | use crate::{ 17 | protocol::RequestOptions, 18 | transport::Transport, 19 | types::{ 20 | CallToolRequest, CallToolResponse, ClientCapabilities, Implementation, InitializeRequest, 21 | InitializeResponse, ListRequest, ProtocolVersion, ReadResourceRequest, Resource, 22 | ResourcesListResponse, ToolsListResponse, LATEST_PROTOCOL_VERSION, 23 | }, 24 | }; 25 | 26 | use anyhow::Result; 27 | use serde_json::Value; 28 | use tokio::sync::RwLock; 29 | use tracing::debug; 30 | 31 | /// An MCP client for connecting to MCP servers and invoking their tools. 32 | /// 33 | /// The `Client` provides a high-level API for interacting with MCP servers, 34 | /// including initialization, tool discovery, and tool invocation. 35 | #[derive(Clone)] 36 | pub struct Client { 37 | transport: T, 38 | strict: bool, 39 | protocol_version: ProtocolVersion, 40 | initialize_res: Arc>>, 41 | env: Option>, 42 | client_info: Implementation, 43 | capabilities: ClientCapabilities, 44 | } 45 | 46 | impl Client { 47 | /// Creates a new client builder. 48 | /// 49 | /// # Arguments 50 | /// 51 | /// * `transport` - The transport to use for communication with the server 52 | /// 53 | /// # Returns 54 | /// 55 | /// A `ClientBuilder` for configuring and building the client 56 | pub fn builder(transport: T) -> ClientBuilder { 57 | ClientBuilder::new(transport) 58 | } 59 | 60 | /// Sets the protocol version for the client. 61 | /// 62 | /// # Arguments 63 | /// 64 | /// * `protocol_version` - The protocol version to use 65 | /// 66 | /// # Returns 67 | /// 68 | /// The modified client instance 69 | pub fn set_protocol_version(mut self, protocol_version: ProtocolVersion) -> Self { 70 | self.protocol_version = protocol_version; 71 | self 72 | } 73 | 74 | /// Opens the transport connection. 75 | /// 76 | /// # Returns 77 | /// 78 | /// A `Result` indicating success or failure 79 | pub async fn open(&self) -> Result<()> { 80 | self.transport.open().await 81 | } 82 | 83 | /// Initializes the connection with the MCP server. 84 | /// 85 | /// This sends the initialize request to the server, negotiates protocol 86 | /// version and capabilities, and establishes the session. 87 | /// 88 | /// # Returns 89 | /// 90 | /// A `Result` containing the server's initialization response if successful 91 | pub async fn initialize(&self) -> Result { 92 | let request = InitializeRequest { 93 | protocol_version: self.protocol_version.as_str().to_string(), 94 | capabilities: self.capabilities.clone(), 95 | client_info: self.client_info.clone(), 96 | }; 97 | let response = self 98 | .request( 99 | "initialize", 100 | Some(serde_json::to_value(request)?), 101 | RequestOptions::default(), 102 | ) 103 | .await?; 104 | let response: InitializeResponse = serde_json::from_value(response) 105 | .map_err(|e| anyhow::anyhow!("Failed to parse response: {}", e))?; 106 | 107 | if response.protocol_version != self.protocol_version.as_str() { 108 | return Err(anyhow::anyhow!( 109 | "Unsupported protocol version: {}", 110 | response.protocol_version 111 | )); 112 | } 113 | 114 | // Save the response for later use 115 | let mut writer = self.initialize_res.write().await; 116 | *writer = Some(response.clone()); 117 | 118 | debug!( 119 | "Initialized with protocol version: {}", 120 | response.protocol_version 121 | ); 122 | self.transport 123 | .send_notification("notifications/initialized", None) 124 | .await?; 125 | 126 | Ok(response) 127 | } 128 | 129 | /// Checks if the client has been initialized. 130 | /// 131 | /// # Returns 132 | /// 133 | /// A `Result` indicating if the client is initialized 134 | pub async fn assert_initialized(&self) -> Result<(), anyhow::Error> { 135 | let reader = self.initialize_res.read().await; 136 | match &*reader { 137 | Some(_) => Ok(()), 138 | None => Err(anyhow::anyhow!("Not initialized")), 139 | } 140 | } 141 | 142 | /// Sends a request to the server. 143 | /// 144 | /// # Arguments 145 | /// 146 | /// * `method` - The method name 147 | /// * `params` - Optional parameters for the request 148 | /// * `options` - Request options (like timeout) 149 | /// 150 | /// # Returns 151 | /// 152 | /// A `Result` containing the server's response if successful 153 | pub async fn request( 154 | &self, 155 | method: &str, 156 | params: Option, 157 | options: RequestOptions, 158 | ) -> Result { 159 | let response = self.transport.request(method, params, options).await?; 160 | response 161 | .result 162 | .ok_or_else(|| anyhow::anyhow!("Request failed: {:?}", response.error)) 163 | } 164 | 165 | /// Lists tools available on the server. 166 | /// 167 | /// # Arguments 168 | /// 169 | /// * `cursor` - Optional pagination cursor 170 | /// * `request_options` - Optional request options 171 | /// 172 | /// # Returns 173 | /// 174 | /// A `Result` containing the list of tools if successful 175 | pub async fn list_tools( 176 | &self, 177 | cursor: Option, 178 | request_options: Option, 179 | ) -> Result { 180 | if self.strict { 181 | self.assert_initialized().await?; 182 | } 183 | 184 | let list_request = ListRequest { cursor, meta: None }; 185 | 186 | let response = self 187 | .request( 188 | "tools/list", 189 | Some(serde_json::to_value(list_request)?), 190 | request_options.unwrap_or_else(RequestOptions::default), 191 | ) 192 | .await?; 193 | 194 | Ok(serde_json::from_value(response) 195 | .map_err(|e| anyhow::anyhow!("Failed to parse response: {}", e))?) 196 | } 197 | 198 | /// Calls a tool on the server. 199 | /// 200 | /// # Arguments 201 | /// 202 | /// * `name` - The name of the tool to call 203 | /// * `arguments` - Optional arguments for the tool 204 | /// 205 | /// # Returns 206 | /// 207 | /// A `Result` containing the tool's response if successful 208 | pub async fn call_tool( 209 | &self, 210 | name: &str, 211 | arguements: Option, 212 | ) -> Result { 213 | if self.strict { 214 | self.assert_initialized().await?; 215 | } 216 | 217 | let arguments = if let Some(env) = &self.env { 218 | arguements 219 | .as_ref() 220 | .map(|args| apply_secure_replacements(args, env)) 221 | } else { 222 | arguements 223 | }; 224 | 225 | let arguments = 226 | arguments.map(|value| serde_json::from_value(value).unwrap_or_else(|_| HashMap::new())); 227 | 228 | let request = CallToolRequest { 229 | name: name.to_string(), 230 | arguments, 231 | meta: None, 232 | }; 233 | 234 | let response = self 235 | .request( 236 | "tools/call", 237 | Some(serde_json::to_value(request)?), 238 | RequestOptions::default(), 239 | ) 240 | .await?; 241 | 242 | Ok(serde_json::from_value(response) 243 | .map_err(|e| anyhow::anyhow!("Failed to parse response: {}", e))?) 244 | } 245 | 246 | /// Lists resources available on the server. 247 | /// 248 | /// # Arguments 249 | /// 250 | /// * `cursor` - Optional pagination cursor 251 | /// * `request_options` - Optional request options 252 | /// 253 | /// # Returns 254 | /// 255 | /// A `Result` containing the list of resources if successful 256 | pub async fn list_resources( 257 | &self, 258 | cursor: Option, 259 | request_options: Option, 260 | ) -> Result { 261 | if self.strict { 262 | self.assert_initialized().await?; 263 | } 264 | 265 | let list_request = ListRequest { cursor, meta: None }; 266 | 267 | let response = self 268 | .request( 269 | "resources/list", 270 | Some(serde_json::to_value(list_request)?), 271 | request_options.unwrap_or_else(RequestOptions::default), 272 | ) 273 | .await?; 274 | 275 | Ok(serde_json::from_value(response) 276 | .map_err(|e| anyhow::anyhow!("Failed to parse response: {}", e))?) 277 | } 278 | 279 | /// Reads a resource from the server. 280 | /// 281 | /// # Arguments 282 | /// 283 | /// * `uri` - The URI of the resource to read 284 | /// 285 | /// # Returns 286 | /// 287 | /// A `Result` containing the resource if successful 288 | pub async fn read_resource(&self, uri: url::Url) -> Result { 289 | if self.strict { 290 | self.assert_initialized().await?; 291 | } 292 | 293 | let read_request = ReadResourceRequest { uri }; 294 | 295 | let response = self 296 | .request( 297 | "resources/read", 298 | Some(serde_json::to_value(read_request)?), 299 | RequestOptions::default(), 300 | ) 301 | .await?; 302 | 303 | Ok(serde_json::from_value(response) 304 | .map_err(|e| anyhow::anyhow!("Failed to parse response: {}", e))?) 305 | } 306 | 307 | pub async fn subscribe_to_resource(&self, uri: url::Url) -> Result<()> { 308 | if self.strict { 309 | self.assert_initialized().await?; 310 | } 311 | 312 | let subscribe_request = ReadResourceRequest { uri }; 313 | 314 | self.request( 315 | "resources/subscribe", 316 | Some(serde_json::to_value(subscribe_request)?), 317 | RequestOptions::default(), 318 | ) 319 | .await?; 320 | 321 | Ok(()) 322 | } 323 | 324 | pub async fn unsubscribe_to_resource(&self, uri: url::Url) -> Result<()> { 325 | if self.strict { 326 | self.assert_initialized().await?; 327 | } 328 | 329 | let unsubscribe_request = ReadResourceRequest { uri }; 330 | 331 | self.request( 332 | "resources/unsubscribe", 333 | Some(serde_json::to_value(unsubscribe_request)?), 334 | RequestOptions::default(), 335 | ) 336 | .await?; 337 | 338 | Ok(()) 339 | } 340 | } 341 | 342 | /// Represents a value that may contain sensitive information. 343 | /// 344 | /// Secure values can be either static strings or environment variables. 345 | #[derive(Clone, Debug)] 346 | pub enum SecureValue { 347 | /// A static string value 348 | Static(String), 349 | /// An environment variable reference 350 | Env(String), 351 | } 352 | 353 | /// Builder for creating configured `Client` instances. 354 | /// 355 | /// The `ClientBuilder` provides a fluent API for configuring and creating 356 | /// MCP clients with specific settings. 357 | pub struct ClientBuilder { 358 | transport: T, 359 | strict: bool, 360 | env: Option>, 361 | protocol_version: ProtocolVersion, 362 | client_info: Implementation, 363 | capabilities: ClientCapabilities, 364 | } 365 | 366 | impl ClientBuilder { 367 | /// Creates a new client builder. 368 | /// 369 | /// # Arguments 370 | /// 371 | /// * `transport` - The transport to use for communication with the server 372 | /// 373 | /// # Returns 374 | /// 375 | /// A new `ClientBuilder` instance 376 | pub fn new(transport: T) -> Self { 377 | Self { 378 | transport, 379 | strict: false, 380 | env: None, 381 | protocol_version: LATEST_PROTOCOL_VERSION, 382 | client_info: Implementation { 383 | name: env::var("CARGO_PKG_NAME").unwrap_or_else(|_| "mcp-client".to_string()), 384 | version: env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.1.0".to_string()), 385 | }, 386 | capabilities: ClientCapabilities::default(), 387 | } 388 | } 389 | 390 | /// Sets the protocol version for the client. 391 | /// 392 | /// # Arguments 393 | /// 394 | /// * `protocol_version` - The protocol version to use 395 | /// 396 | /// # Returns 397 | /// 398 | /// The modified builder instance 399 | pub fn set_protocol_version(mut self, protocol_version: ProtocolVersion) -> Self { 400 | self.protocol_version = protocol_version; 401 | self 402 | } 403 | 404 | /// Sets the client information. 405 | /// 406 | /// # Arguments 407 | /// 408 | /// * `name` - The client name 409 | /// * `version` - The client version 410 | /// 411 | /// # Returns 412 | /// 413 | /// The modified builder instance 414 | pub fn set_client_info(mut self, name: impl Into, version: impl Into) -> Self { 415 | self.client_info = Implementation { 416 | name: name.into(), 417 | version: version.into(), 418 | }; 419 | self 420 | } 421 | 422 | /// Sets the client capabilities. 423 | /// 424 | /// # Arguments 425 | /// 426 | /// * `capabilities` - The client capabilities 427 | /// 428 | /// # Returns 429 | /// 430 | /// The modified builder instance 431 | pub fn set_capabilities(mut self, capabilities: ClientCapabilities) -> Self { 432 | self.capabilities = capabilities; 433 | self 434 | } 435 | 436 | /// Adds a secure value for substitution in tool arguments. 437 | /// 438 | /// # Arguments 439 | /// 440 | /// * `key` - The key for the secure value 441 | /// * `value` - The secure value 442 | /// 443 | /// # Returns 444 | /// 445 | /// The modified builder instance 446 | pub fn with_secure_value(mut self, key: impl Into, value: SecureValue) -> Self { 447 | if self.env.is_none() { 448 | self.env = Some(HashMap::new()); 449 | } 450 | 451 | if let Some(env) = &mut self.env { 452 | env.insert(key.into(), value); 453 | } 454 | 455 | self 456 | } 457 | 458 | /// Enables strict mode, which requires initialization before operations. 459 | /// 460 | /// # Returns 461 | /// 462 | /// The modified builder instance 463 | pub fn use_strict(mut self) -> Self { 464 | self.strict = true; 465 | self 466 | } 467 | 468 | /// Sets the strict mode flag. 469 | /// 470 | /// # Arguments 471 | /// 472 | /// * `strict` - Whether to enable strict mode 473 | /// 474 | /// # Returns 475 | /// 476 | /// The modified builder instance 477 | pub fn with_strict(mut self, strict: bool) -> Self { 478 | self.strict = strict; 479 | self 480 | } 481 | 482 | /// Builds the client with the configured settings. 483 | /// 484 | /// # Returns 485 | /// 486 | /// A new `Client` instance 487 | pub fn build(self) -> Client { 488 | Client { 489 | transport: self.transport, 490 | strict: self.strict, 491 | env: self.env, 492 | protocol_version: self.protocol_version, 493 | initialize_res: Arc::new(RwLock::new(None)), 494 | client_info: self.client_info, 495 | capabilities: self.capabilities, 496 | } 497 | } 498 | } 499 | 500 | /// Recursively walk through the JSON value. If a JSON string exactly matches 501 | /// one of the keys in the secure values map, replace it with the corresponding secure value. 502 | pub fn apply_secure_replacements( 503 | value: &Value, 504 | secure_values: &HashMap, 505 | ) -> Value { 506 | match value { 507 | Value::Object(map) => { 508 | let mut new_map = serde_json::Map::new(); 509 | for (k, v) in map.iter() { 510 | let new_value = if let Value::String(_) = v { 511 | if let Some(secure_val) = secure_values.get(k) { 512 | let replacement = match secure_val { 513 | SecureValue::Static(val) => val.clone(), 514 | SecureValue::Env(env_key) => env::var(env_key) 515 | .unwrap_or_else(|_| v.as_str().unwrap().to_string()), 516 | }; 517 | Value::String(replacement) 518 | } else { 519 | apply_secure_replacements(v, secure_values) 520 | } 521 | } else { 522 | apply_secure_replacements(v, secure_values) 523 | }; 524 | new_map.insert(k.clone(), new_value); 525 | } 526 | Value::Object(new_map) 527 | } 528 | Value::Array(arr) => { 529 | let new_arr: Vec = arr 530 | .iter() 531 | .map(|v| apply_secure_replacements(v, secure_values)) 532 | .collect(); 533 | Value::Array(new_arr) 534 | } 535 | _ => value.clone(), 536 | } 537 | } 538 | -------------------------------------------------------------------------------- /mcp-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Model Context Protocol (MCP) Core Library 2 | //! 3 | //! `mcp-core` is a Rust implementation of the Model Context Protocol (MCP), an open 4 | //! protocol for interaction between AI models and tools/external systems. 5 | //! 6 | //! This library provides a comprehensive framework for building both MCP servers (tool providers) 7 | //! and MCP clients (model interfaces), with support for: 8 | //! 9 | //! - Bidirectional communication between AI models and external tools 10 | //! - Tool registration, discovery, and invocation 11 | //! - Resource management 12 | //! - Transport-agnostic design (supporting both SSE and stdio) 13 | //! - Standardized request/response formats using JSON-RPC 14 | //! 15 | //! ## Architecture 16 | //! 17 | //! The library is organized into several main components: 18 | //! 19 | //! - **Client**: Implementation of the MCP client for connecting to servers 20 | //! - **Server**: Implementation of the MCP server for exposing tools to clients 21 | //! - **Protocol**: Core protocol implementation using JSON-RPC 22 | //! - **Types**: Data structures representing MCP concepts 23 | //! - **Transport**: Network transport abstraction (SSE, stdio) 24 | //! - **Tools**: Framework for registering and invoking tools 25 | //! 26 | //! ## Usage 27 | //! 28 | //! For examples of how to use this library, see the `examples/` directory: 29 | //! - `echo_server.rs`: A simple MCP server implementation 30 | //! - `echo_server_macro.rs`: Using the `#[tool]` macro for simpler integration 31 | //! - `echo_client.rs`: A client connecting to an MCP server 32 | //! 33 | //! ## Macros 34 | //! 35 | //! This library includes a set of utility macros to make working with the MCP protocol 36 | //! easier, including helpers for creating various types of tool responses. 37 | 38 | pub mod client; 39 | pub mod protocol; 40 | pub mod server; 41 | pub mod tools; 42 | pub mod transport; 43 | pub mod types; 44 | 45 | /// Creates a tool response with error information. 46 | /// 47 | /// This macro generates a `CallToolResponse` containing a text error message 48 | /// and sets the `is_error` flag to `true`. 49 | /// 50 | /// # Examples 51 | /// 52 | /// ``` 53 | /// use mcp_core::tool_error_response; 54 | /// use anyhow::Error; 55 | /// 56 | /// let error = Error::msg("Something went wrong"); 57 | /// let response = tool_error_response!(error); 58 | /// assert_eq!(response.is_error, Some(true)); 59 | /// ``` 60 | #[macro_export] 61 | macro_rules! tool_error_response { 62 | ($e:expr) => {{ 63 | let error_message = $e.to_string(); 64 | $crate::types::CallToolResponse { 65 | content: vec![$crate::types::ToolResponseContent::Text( 66 | $crate::types::TextContent { 67 | content_type: "text".to_string(), 68 | text: error_message, 69 | annotations: None, 70 | }, 71 | )], 72 | is_error: Some(true), 73 | meta: None, 74 | } 75 | }}; 76 | } 77 | 78 | /// Creates a tool response with text content. 79 | /// 80 | /// This macro generates a `CallToolResponse` containing the provided text. 81 | /// 82 | /// # Examples 83 | /// 84 | /// ``` 85 | /// use mcp_core::tool_text_response; 86 | /// 87 | /// let response = tool_text_response!("Hello, world!"); 88 | /// ``` 89 | #[macro_export] 90 | macro_rules! tool_text_response { 91 | ($e:expr) => {{ 92 | $crate::types::CallToolResponse { 93 | content: vec![$crate::types::ToolResponseContent::Text( 94 | $crate::types::TextContent { 95 | content_type: "text".to_string(), 96 | text: $e.to_string(), 97 | annotations: None, 98 | }, 99 | )], 100 | is_error: None, 101 | meta: None, 102 | } 103 | }}; 104 | } 105 | 106 | /// Creates a text content object for tool responses. 107 | /// 108 | /// This macro generates a `ToolResponseContent::Text` object with the provided text. 109 | /// 110 | /// # Examples 111 | /// 112 | /// ``` 113 | /// use mcp_core::tool_text_content; 114 | /// 115 | /// let content = tool_text_content!("Hello, world!"); 116 | /// ``` 117 | #[macro_export] 118 | macro_rules! tool_text_content { 119 | ($e:expr) => {{ 120 | $crate::types::ToolResponseContent::Text($crate::types::TextContent { 121 | content_type: "text".to_string(), 122 | text: $e.to_string(), 123 | annotations: None, 124 | }) 125 | }}; 126 | } 127 | 128 | /// Creates an image content object for tool responses. 129 | /// 130 | /// This macro generates a `ToolResponseContent::Image` object with the provided data and MIME type. 131 | /// 132 | /// # Examples 133 | /// 134 | /// ``` 135 | /// use mcp_core::tool_image_content; 136 | /// 137 | /// let image_data = "base64_encoded_data".to_string(); 138 | /// let content = tool_image_content!(image_data, "image/jpeg".to_string()); 139 | /// ``` 140 | #[macro_export] 141 | macro_rules! tool_image_content { 142 | ($data:expr, $mime_type:expr) => {{ 143 | $crate::types::ToolResponseContent::Image($crate::types::ImageContent { 144 | content_type: "image".to_string(), 145 | data: $data, 146 | mime_type: $mime_type, 147 | annotations: None, 148 | }) 149 | }}; 150 | } 151 | 152 | /// Creates an audio content object for tool responses. 153 | /// 154 | /// This macro generates a `ToolResponseContent::Audio` object with the provided data and MIME type. 155 | /// 156 | /// # Examples 157 | /// 158 | /// ``` 159 | /// use mcp_core::tool_audio_content; 160 | /// 161 | /// let audio_data = "base64_encoded_audio".to_string(); 162 | /// let content = tool_audio_content!(audio_data, "audio/mp3".to_string()); 163 | /// ``` 164 | #[macro_export] 165 | macro_rules! tool_audio_content { 166 | ($data:expr, $mime_type:expr) => {{ 167 | $crate::types::ToolResponseContent::Audio($crate::types::AudioContent { 168 | content_type: "audio".to_string(), 169 | data: $data, 170 | mime_type: $mime_type, 171 | annotations: None, 172 | }) 173 | }}; 174 | } 175 | 176 | /// Creates a resource content object for tool responses. 177 | /// 178 | /// This macro generates a `ToolResponseContent::Resource` object with the provided URI and optional MIME type. 179 | /// 180 | /// # Examples 181 | /// 182 | /// ``` 183 | /// use mcp_core::tool_resource_content; 184 | /// use url::Url; 185 | /// 186 | /// let uri = Url::parse("https://example.com/resource.png").unwrap(); 187 | /// let content = tool_resource_content!(uri, "image/png".to_string()); 188 | /// ``` 189 | #[macro_export] 190 | macro_rules! tool_resource_content { 191 | ($uri:expr, $mime_type:expr) => {{ 192 | $crate::types::ToolResponseContent::Resource($crate::types::EmbeddedResource { 193 | content_type: "resource".to_string(), 194 | resource: $crate::types::ResourceContents { 195 | uri: $uri, 196 | mime_type: Some($mime_type), 197 | text: None, 198 | blob: None, 199 | }, 200 | annotations: None, 201 | }) 202 | }}; 203 | ($uri:expr) => {{ 204 | $crate::types::ToolResponseContent::Resource($crate::types::EmbeddedResource { 205 | content_type: "resource".to_string(), 206 | resource: $crate::types::ResourceContents { 207 | uri: $uri, 208 | mime_type: None, 209 | text: None, 210 | blob: None, 211 | }, 212 | annotations: None, 213 | }) 214 | }}; 215 | } 216 | -------------------------------------------------------------------------------- /mcp-core/src/protocol.rs: -------------------------------------------------------------------------------- 1 | //! # MCP Protocol Implementation 2 | //! 3 | //! This module implements the core JSON-RPC protocol layer used by the MCP system. 4 | //! It provides the infrastructure for sending and receiving JSON-RPC requests, 5 | //! notifications, and responses between MCP clients and servers. 6 | //! 7 | //! The protocol layer is transport-agnostic and can work with any transport 8 | //! implementation that conforms to the `Transport` trait. 9 | //! 10 | //! Key components include: 11 | //! - `Protocol`: The main protocol handler 12 | //! - `ProtocolBuilder`: A builder for configuring protocols 13 | //! - Request and notification handlers 14 | //! - Timeout and error handling 15 | 16 | use super::transport::{JsonRpcError, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse}; 17 | use super::types::ErrorCode; 18 | use anyhow::Result; 19 | use async_trait::async_trait; 20 | use serde::de::DeserializeOwned; 21 | use serde::Serialize; 22 | use serde_json::json; 23 | use std::pin::Pin; 24 | 25 | use std::sync::atomic::{AtomicU64, Ordering}; 26 | use std::time::Duration; 27 | use std::{collections::HashMap, sync::Arc}; 28 | use tokio::sync::{oneshot, Mutex}; 29 | 30 | /// The core protocol handler for MCP. 31 | /// 32 | /// The `Protocol` struct manages the lifecycle of JSON-RPC requests and responses, 33 | /// dispatches incoming requests to the appropriate handlers, and manages 34 | /// pending requests and their responses. 35 | #[derive(Clone)] 36 | pub struct Protocol { 37 | request_id: Arc, 38 | pending_requests: Arc>>>, 39 | request_handlers: Arc>>>, 40 | notification_handlers: Arc>>>, 41 | } 42 | 43 | impl Protocol { 44 | /// Creates a new protocol builder. 45 | /// 46 | /// # Returns 47 | /// 48 | /// A `ProtocolBuilder` for configuring the protocol 49 | pub fn builder() -> ProtocolBuilder { 50 | ProtocolBuilder::new() 51 | } 52 | 53 | /// Handles an incoming JSON-RPC request. 54 | /// 55 | /// This method dispatches the request to the appropriate handler based on 56 | /// the request method, and returns the handler's response. 57 | /// 58 | /// # Arguments 59 | /// 60 | /// * `request` - The incoming JSON-RPC request 61 | /// 62 | /// # Returns 63 | /// 64 | /// A `JsonRpcResponse` containing the handler's response or an error 65 | pub async fn handle_request(&self, request: JsonRpcRequest) -> JsonRpcResponse { 66 | let handlers = self.request_handlers.lock().await; 67 | if let Some(handler) = handlers.get(&request.method) { 68 | match handler.handle(request.clone()).await { 69 | Ok(response) => response, 70 | Err(e) => JsonRpcResponse { 71 | id: request.id, 72 | result: None, 73 | error: Some(JsonRpcError { 74 | code: ErrorCode::InternalError as i32, 75 | message: e.to_string(), 76 | data: None, 77 | }), 78 | ..Default::default() 79 | }, 80 | } 81 | } else { 82 | JsonRpcResponse { 83 | id: request.id, 84 | error: Some(JsonRpcError { 85 | code: ErrorCode::MethodNotFound as i32, 86 | message: format!("Method not found: {}", request.method), 87 | data: None, 88 | }), 89 | ..Default::default() 90 | } 91 | } 92 | } 93 | 94 | /// Handles an incoming JSON-RPC notification. 95 | /// 96 | /// This method dispatches the notification to the appropriate handler based on 97 | /// the notification method. 98 | /// 99 | /// # Arguments 100 | /// 101 | /// * `request` - The incoming JSON-RPC notification 102 | pub async fn handle_notification(&self, request: JsonRpcNotification) { 103 | let handlers = self.notification_handlers.lock().await; 104 | if let Some(handler) = handlers.get(&request.method) { 105 | match handler.handle(request.clone()).await { 106 | Ok(_) => tracing::info!("Received notification: {:?}", request.method), 107 | Err(e) => tracing::error!("Error handling notification: {}", e), 108 | } 109 | } else { 110 | tracing::debug!("No handler for notification: {}", request.method); 111 | } 112 | } 113 | 114 | /// Generates a new unique message ID for requests. 115 | /// 116 | /// # Returns 117 | /// 118 | /// A unique message ID 119 | pub fn new_message_id(&self) -> u64 { 120 | self.request_id.fetch_add(1, Ordering::SeqCst) 121 | } 122 | 123 | /// Creates a new request ID and channel for receiving the response. 124 | /// 125 | /// # Returns 126 | /// 127 | /// A tuple containing the request ID and a receiver for the response 128 | pub async fn create_request(&self) -> (u64, oneshot::Receiver) { 129 | let id = self.new_message_id(); 130 | let (tx, rx) = oneshot::channel(); 131 | 132 | { 133 | let mut pending = self.pending_requests.lock().await; 134 | pending.insert(id, tx); 135 | } 136 | 137 | (id, rx) 138 | } 139 | 140 | /// Handles an incoming JSON-RPC response. 141 | /// 142 | /// This method delivers the response to the appropriate waiting request, 143 | /// if any. 144 | /// 145 | /// # Arguments 146 | /// 147 | /// * `response` - The incoming JSON-RPC response 148 | pub async fn handle_response(&self, response: JsonRpcResponse) { 149 | if let Some(tx) = self.pending_requests.lock().await.remove(&response.id) { 150 | let _ = tx.send(response); 151 | } 152 | } 153 | 154 | /// Cancels a pending request and sends an error response. 155 | /// 156 | /// # Arguments 157 | /// 158 | /// * `id` - The ID of the request to cancel 159 | pub async fn cancel_response(&self, id: u64) { 160 | if let Some(tx) = self.pending_requests.lock().await.remove(&id) { 161 | let _ = tx.send(JsonRpcResponse { 162 | id, 163 | result: None, 164 | error: Some(JsonRpcError { 165 | code: ErrorCode::RequestTimeout as i32, 166 | message: "Request cancelled".to_string(), 167 | data: None, 168 | }), 169 | ..Default::default() 170 | }); 171 | } 172 | } 173 | } 174 | 175 | /// The default request timeout, in milliseconds 176 | pub const DEFAULT_REQUEST_TIMEOUT_MSEC: u64 = 60000; 177 | 178 | /// Options for customizing requests. 179 | /// 180 | /// This struct allows configuring various aspects of request handling, 181 | /// such as timeouts. 182 | pub struct RequestOptions { 183 | /// The timeout duration for the request 184 | pub timeout: Duration, 185 | } 186 | 187 | impl RequestOptions { 188 | /// Sets the timeout for the request. 189 | /// 190 | /// # Arguments 191 | /// 192 | /// * `timeout` - The timeout duration 193 | /// 194 | /// # Returns 195 | /// 196 | /// The modified options instance 197 | pub fn timeout(self, timeout: Duration) -> Self { 198 | Self { timeout } 199 | } 200 | } 201 | 202 | impl Default for RequestOptions { 203 | fn default() -> Self { 204 | Self { 205 | timeout: Duration::from_millis(DEFAULT_REQUEST_TIMEOUT_MSEC), 206 | } 207 | } 208 | } 209 | 210 | /// Builder for creating configured protocols. 211 | /// 212 | /// The `ProtocolBuilder` provides a fluent API for configuring and creating 213 | /// protocols with specific request and notification handlers. 214 | #[derive(Clone)] 215 | pub struct ProtocolBuilder { 216 | request_handlers: Arc>>>, 217 | notification_handlers: Arc>>>, 218 | } 219 | 220 | impl ProtocolBuilder { 221 | /// Creates a new protocol builder. 222 | /// 223 | /// # Returns 224 | /// 225 | /// A new `ProtocolBuilder` instance 226 | pub fn new() -> Self { 227 | Self { 228 | request_handlers: Arc::new(Mutex::new(HashMap::new())), 229 | notification_handlers: Arc::new(Mutex::new(HashMap::new())), 230 | } 231 | } 232 | 233 | /// Registers a typed request handler. 234 | /// 235 | /// # Arguments 236 | /// 237 | /// * `method` - The method name to handle 238 | /// * `handler` - The handler function 239 | /// 240 | /// # Returns 241 | /// 242 | /// The modified builder instance 243 | pub fn request_handler( 244 | self, 245 | method: &str, 246 | handler: impl Fn(Req) -> Pin> + Send>> 247 | + Send 248 | + Sync 249 | + 'static, 250 | ) -> Self 251 | where 252 | Req: DeserializeOwned + Send + Sync + 'static, 253 | Resp: Serialize + Send + Sync + 'static, 254 | { 255 | let handler = TypedRequestHandler { 256 | handler: Box::new(handler), 257 | _phantom: std::marker::PhantomData, 258 | }; 259 | 260 | if let Ok(mut handlers) = self.request_handlers.try_lock() { 261 | handlers.insert(method.to_string(), Box::new(handler)); 262 | } 263 | self 264 | } 265 | 266 | /// Checks if a request handler exists for a method. 267 | /// 268 | /// # Arguments 269 | /// 270 | /// * `method` - The method name to check 271 | /// 272 | /// # Returns 273 | /// 274 | /// `true` if a handler exists, `false` otherwise 275 | pub fn has_request_handler(&self, method: &str) -> bool { 276 | self.request_handlers 277 | .try_lock() 278 | .map(|handlers| handlers.contains_key(method)) 279 | .unwrap_or(false) 280 | } 281 | 282 | /// Registers a typed notification handler. 283 | /// 284 | /// # Arguments 285 | /// 286 | /// * `method` - The method name to handle 287 | /// * `handler` - The handler function 288 | /// 289 | /// # Returns 290 | /// 291 | /// The modified builder instance 292 | pub fn notification_handler( 293 | self, 294 | method: &str, 295 | handler: impl Fn(N) -> Pin> + Send>> 296 | + Send 297 | + Sync 298 | + 'static, 299 | ) -> Self 300 | where 301 | N: DeserializeOwned + Send + Sync + 'static, 302 | { 303 | let handler = TypedNotificationHandler { 304 | handler: Box::new(handler), 305 | _phantom: std::marker::PhantomData, 306 | }; 307 | 308 | if let Ok(mut handlers) = self.notification_handlers.try_lock() { 309 | handlers.insert(method.to_string(), Box::new(handler)); 310 | } 311 | self 312 | } 313 | 314 | /// Checks if a notification handler exists for a method. 315 | /// 316 | /// # Arguments 317 | /// 318 | /// * `method` - The method name to check 319 | /// 320 | /// # Returns 321 | /// 322 | /// `true` if a handler exists, `false` otherwise 323 | pub fn has_notification_handler(&self, method: &str) -> bool { 324 | self.notification_handlers 325 | .try_lock() 326 | .map(|handlers| handlers.contains_key(method)) 327 | .unwrap_or(false) 328 | } 329 | 330 | /// Builds the protocol with the configured handlers. 331 | /// 332 | /// # Returns 333 | /// 334 | /// A new `Protocol` instance 335 | pub fn build(self) -> Protocol { 336 | Protocol { 337 | request_id: Arc::new(AtomicU64::new(0)), 338 | pending_requests: Arc::new(Mutex::new(HashMap::new())), 339 | request_handlers: self.request_handlers, 340 | notification_handlers: self.notification_handlers, 341 | } 342 | } 343 | } 344 | 345 | /// Trait for handling JSON-RPC requests. 346 | /// 347 | /// Implementors of this trait can handle incoming JSON-RPC requests 348 | /// and produce responses. 349 | #[async_trait] 350 | trait RequestHandler: Send + Sync { 351 | /// Handles an incoming JSON-RPC request. 352 | /// 353 | /// # Arguments 354 | /// 355 | /// * `request` - The incoming JSON-RPC request 356 | /// 357 | /// # Returns 358 | /// 359 | /// A `Result` containing the response or an error 360 | async fn handle(&self, request: JsonRpcRequest) -> Result; 361 | } 362 | 363 | /// Trait for handling JSON-RPC notifications. 364 | /// 365 | /// Implementors of this trait can handle incoming JSON-RPC notifications. 366 | #[async_trait] 367 | trait NotificationHandler: Send + Sync { 368 | /// Handles an incoming JSON-RPC notification. 369 | /// 370 | /// # Arguments 371 | /// 372 | /// * `notification` - The incoming JSON-RPC notification 373 | /// 374 | /// # Returns 375 | /// 376 | /// A `Result` indicating success or failure 377 | async fn handle(&self, notification: JsonRpcNotification) -> Result<()>; 378 | } 379 | 380 | /// A typed request handler. 381 | /// 382 | /// This struct adapts a typed handler function to the `RequestHandler` trait, 383 | /// handling the deserialization of the request and serialization of the response. 384 | struct TypedRequestHandler 385 | where 386 | Req: DeserializeOwned + Send + Sync + 'static, 387 | Resp: Serialize + Send + Sync + 'static, 388 | { 389 | handler: Box< 390 | dyn Fn(Req) -> std::pin::Pin> + Send>> 391 | + Send 392 | + Sync, 393 | >, 394 | _phantom: std::marker::PhantomData<(Req, Resp)>, 395 | } 396 | 397 | #[async_trait] 398 | impl RequestHandler for TypedRequestHandler 399 | where 400 | Req: DeserializeOwned + Send + Sync + 'static, 401 | Resp: Serialize + Send + Sync + 'static, 402 | { 403 | async fn handle(&self, request: JsonRpcRequest) -> Result { 404 | let params: Req = if request.params.is_none() || request.params.as_ref().unwrap().is_null() 405 | { 406 | serde_json::from_value(json!({}))? 407 | } else { 408 | serde_json::from_value(request.params.unwrap())? 409 | }; 410 | let result = (self.handler)(params).await?; 411 | Ok(JsonRpcResponse { 412 | id: request.id, 413 | result: Some(serde_json::to_value(result)?), 414 | error: None, 415 | ..Default::default() 416 | }) 417 | } 418 | } 419 | 420 | /// A typed notification handler. 421 | /// 422 | /// This struct adapts a typed handler function to the `NotificationHandler` trait, 423 | /// handling the deserialization of the notification. 424 | struct TypedNotificationHandler 425 | where 426 | N: DeserializeOwned + Send + Sync + 'static, 427 | { 428 | handler: Box< 429 | dyn Fn(N) -> std::pin::Pin> + Send>> 430 | + Send 431 | + Sync, 432 | >, 433 | _phantom: std::marker::PhantomData, 434 | } 435 | 436 | #[async_trait] 437 | impl NotificationHandler for TypedNotificationHandler 438 | where 439 | N: DeserializeOwned + Send + Sync + 'static, 440 | { 441 | async fn handle(&self, notification: JsonRpcNotification) -> Result<()> { 442 | let params: N = 443 | if notification.params.is_none() || notification.params.as_ref().unwrap().is_null() { 444 | serde_json::from_value(serde_json::Value::Null)? 445 | } else { 446 | serde_json::from_value(notification.params.unwrap())? 447 | }; 448 | (self.handler)(params).await 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /mcp-core/src/server.rs: -------------------------------------------------------------------------------- 1 | //! # MCP Server 2 | //! 3 | //! This module provides the server-side implementation of the Model Context Protocol (MCP). 4 | //! It allows creating MCP servers that expose tools for clients to discover and invoke. 5 | //! 6 | //! The core components include: 7 | //! - The `Server` for managing server lifetime 8 | //! - The `ServerProtocolBuilder` for configuring servers 9 | //! - Client connection tracking 10 | //! 11 | //! Servers expose tools that can be discovered and called by clients, with 12 | //! customizable capabilities and metadata. 13 | 14 | use std::{ 15 | collections::HashMap, 16 | sync::{Arc, RwLock}, 17 | }; 18 | 19 | use crate::{ 20 | protocol::Protocol, 21 | tools::{ToolHandler, ToolHandlerFn, Tools}, 22 | types::{ 23 | CallToolRequest, ListRequest, ProtocolVersion, Tool, ToolsListResponse, 24 | LATEST_PROTOCOL_VERSION, 25 | }, 26 | }; 27 | 28 | use super::{ 29 | protocol::ProtocolBuilder, 30 | transport::Transport, 31 | types::{ 32 | ClientCapabilities, Implementation, InitializeRequest, InitializeResponse, 33 | ServerCapabilities, 34 | }, 35 | }; 36 | use anyhow::Result; 37 | use std::pin::Pin; 38 | 39 | /// Represents a connected MCP client. 40 | /// 41 | /// Tracks information about a client that has connected to the server, 42 | /// including its capabilities, info, and initialization state. 43 | #[derive(Clone)] 44 | pub struct ClientConnection { 45 | /// The capabilities reported by the client 46 | pub client_capabilities: Option, 47 | /// Information about the client implementation 48 | pub client_info: Option, 49 | /// Whether the client has completed initialization 50 | pub initialized: bool, 51 | } 52 | 53 | /// The main MCP server type. 54 | /// 55 | /// Provides static methods for creating and starting MCP servers. 56 | #[derive(Clone)] 57 | pub struct Server; 58 | 59 | impl Server { 60 | /// Creates a new server builder with the specified server information. 61 | /// 62 | /// # Arguments 63 | /// 64 | /// * `name` - The server name 65 | /// * `version` - The server version 66 | /// * `protocol_version` - The protocol version to use 67 | /// 68 | /// # Returns 69 | /// 70 | /// A `ServerProtocolBuilder` for configuring the server 71 | pub fn builder( 72 | name: String, 73 | version: String, 74 | protocol_version: ProtocolVersion, 75 | ) -> ServerProtocolBuilder { 76 | ServerProtocolBuilder::new(name, version).set_protocol_version(protocol_version) 77 | } 78 | 79 | /// Starts the server with the given transport. 80 | /// 81 | /// # Arguments 82 | /// 83 | /// * `transport` - The transport to use for communication with clients 84 | /// 85 | /// # Returns 86 | /// 87 | /// A `Result` indicating success or failure 88 | pub async fn start(transport: T) -> Result<()> { 89 | transport.open().await 90 | } 91 | } 92 | 93 | /// Builder for creating configured server protocols. 94 | /// 95 | /// The `ServerProtocolBuilder` provides a fluent API for configuring and creating 96 | /// MCP server protocols with specific settings, tools, and capabilities. 97 | pub struct ServerProtocolBuilder { 98 | protocol_version: ProtocolVersion, 99 | protocol_builder: ProtocolBuilder, 100 | server_info: Implementation, 101 | capabilities: ServerCapabilities, 102 | instructions: Option, 103 | tools: HashMap, 104 | client_connection: Arc>, 105 | } 106 | 107 | impl ServerProtocolBuilder { 108 | /// Creates a new server protocol builder. 109 | /// 110 | /// # Arguments 111 | /// 112 | /// * `name` - The server name 113 | /// * `version` - The server version 114 | /// 115 | /// # Returns 116 | /// 117 | /// A new `ServerProtocolBuilder` instance 118 | pub fn new(name: String, version: String) -> Self { 119 | ServerProtocolBuilder { 120 | protocol_version: LATEST_PROTOCOL_VERSION, 121 | protocol_builder: ProtocolBuilder::new(), 122 | server_info: Implementation { name, version }, 123 | capabilities: ServerCapabilities::default(), 124 | instructions: None, 125 | tools: HashMap::new(), 126 | client_connection: Arc::new(RwLock::new(ClientConnection { 127 | client_capabilities: None, 128 | client_info: None, 129 | initialized: false, 130 | })), 131 | } 132 | } 133 | 134 | /// Sets the protocol version for the server. 135 | /// 136 | /// # Arguments 137 | /// 138 | /// * `protocol_version` - The protocol version to use 139 | /// 140 | /// # Returns 141 | /// 142 | /// The modified builder instance 143 | pub fn set_protocol_version(mut self, protocol_version: ProtocolVersion) -> Self { 144 | self.protocol_version = protocol_version; 145 | self 146 | } 147 | 148 | /// Sets the server capabilities. 149 | /// 150 | /// # Arguments 151 | /// 152 | /// * `capabilities` - The server capabilities 153 | /// 154 | /// # Returns 155 | /// 156 | /// The modified builder instance 157 | pub fn set_capabilities(mut self, capabilities: ServerCapabilities) -> Self { 158 | self.capabilities = capabilities; 159 | self 160 | } 161 | 162 | /// Sets the server instructions. 163 | /// 164 | /// Instructions provide guidance for AI models on how to use the server's tools. 165 | /// 166 | /// # Arguments 167 | /// 168 | /// * `instructions` - The instructions for using the server 169 | /// 170 | /// # Returns 171 | /// 172 | /// The modified builder instance 173 | pub fn set_instructions(mut self, instructions: String) -> Self { 174 | self.instructions = Some(instructions); 175 | self 176 | } 177 | 178 | /// Removes the server instructions. 179 | /// 180 | /// # Returns 181 | /// 182 | /// The modified builder instance 183 | pub fn remove_instructions(mut self) -> Self { 184 | self.instructions = None; 185 | self 186 | } 187 | 188 | /// Registers a tool with the server. 189 | /// 190 | /// # Arguments 191 | /// 192 | /// * `tool` - The tool definition 193 | /// * `f` - The handler function for the tool 194 | /// 195 | /// # Returns 196 | /// 197 | /// The modified builder instance 198 | pub fn register_tool(mut self, tool: Tool, f: ToolHandlerFn) -> Self { 199 | self.tools.insert( 200 | tool.name.clone(), 201 | ToolHandler { 202 | tool, 203 | f: Box::new(f), 204 | }, 205 | ); 206 | self 207 | } 208 | 209 | /// Helper function for creating an initialize request handler. 210 | /// 211 | /// # Arguments 212 | /// 213 | /// * `protocol_version` - The protocol version to use 214 | /// * `state` - The client connection state 215 | /// * `server_info` - The server information 216 | /// * `capabilities` - The server capabilities 217 | /// * `instructions` - Optional server instructions 218 | /// 219 | /// # Returns 220 | /// 221 | /// A handler function for initialize requests 222 | fn handle_init( 223 | protocol_version: ProtocolVersion, 224 | state: Arc>, 225 | server_info: Implementation, 226 | capabilities: ServerCapabilities, 227 | instructions: Option, 228 | ) -> impl Fn( 229 | InitializeRequest, 230 | ) 231 | -> Pin> + Send>> { 232 | move |req| { 233 | let state = state.clone(); 234 | let server_info = server_info.clone(); 235 | let capabilities = capabilities.clone(); 236 | let instructions = instructions.clone(); 237 | let protocol_version = protocol_version.clone(); 238 | 239 | Box::pin(async move { 240 | let mut state = state 241 | .write() 242 | .map_err(|_| anyhow::anyhow!("Lock poisoned"))?; 243 | state.client_capabilities = Some(req.capabilities); 244 | state.client_info = Some(req.client_info); 245 | 246 | Ok(InitializeResponse { 247 | protocol_version: protocol_version.as_str().to_string(), 248 | capabilities, 249 | server_info, 250 | instructions, 251 | }) 252 | }) 253 | } 254 | } 255 | 256 | /// Helper function for creating an initialized notification handler. 257 | /// 258 | /// # Arguments 259 | /// 260 | /// * `state` - The client connection state 261 | /// 262 | /// # Returns 263 | /// 264 | /// A handler function for initialized notifications 265 | fn handle_initialized( 266 | state: Arc>, 267 | ) -> impl Fn(()) -> Pin> + Send>> { 268 | move |_| { 269 | let state = state.clone(); 270 | Box::pin(async move { 271 | let mut state = state 272 | .write() 273 | .map_err(|_| anyhow::anyhow!("Lock poisoned"))?; 274 | state.initialized = true; 275 | Ok(()) 276 | }) 277 | } 278 | } 279 | 280 | /// Gets the client capabilities, if available. 281 | /// 282 | /// # Returns 283 | /// 284 | /// An `Option` containing the client capabilities if available 285 | pub fn get_client_capabilities(&self) -> Option { 286 | self.client_connection 287 | .read() 288 | .ok()? 289 | .client_capabilities 290 | .clone() 291 | } 292 | 293 | /// Gets the client information, if available. 294 | /// 295 | /// # Returns 296 | /// 297 | /// An `Option` containing the client information if available 298 | pub fn get_client_info(&self) -> Option { 299 | self.client_connection.read().ok()?.client_info.clone() 300 | } 301 | 302 | /// Checks if the client has completed initialization. 303 | /// 304 | /// # Returns 305 | /// 306 | /// `true` if the client is initialized, `false` otherwise 307 | pub fn is_initialized(&self) -> bool { 308 | self.client_connection 309 | .read() 310 | .ok() 311 | .map(|client_connection| client_connection.initialized) 312 | .unwrap_or(false) 313 | } 314 | 315 | /// Builds the server protocol. 316 | /// 317 | /// # Returns 318 | /// 319 | /// A `Protocol` instance configured with the server's settings 320 | pub fn build(self) -> Protocol { 321 | let tools = Arc::new(Tools::new(self.tools)); 322 | let tools_clone = tools.clone(); 323 | let tools_list = tools.clone(); 324 | let tools_call = tools_clone.clone(); 325 | 326 | let conn_for_list = self.client_connection.clone(); 327 | let conn_for_call = self.client_connection.clone(); 328 | 329 | self.protocol_builder 330 | .request_handler( 331 | "initialize", 332 | Self::handle_init( 333 | self.protocol_version.clone(), 334 | self.client_connection.clone(), 335 | self.server_info, 336 | self.capabilities, 337 | self.instructions, 338 | ), 339 | ) 340 | .notification_handler( 341 | "notifications/initialized", 342 | Self::handle_initialized(self.client_connection), 343 | ) 344 | .request_handler("tools/list", move |_req: ListRequest| { 345 | let tools_list = tools_list.clone(); 346 | let conn = conn_for_list.clone(); 347 | Box::pin(async move { 348 | match conn.read() { 349 | Ok(conn) => { 350 | if !conn.initialized { 351 | return Err(anyhow::anyhow!("Client not initialized")); 352 | } 353 | } 354 | Err(_) => return Err(anyhow::anyhow!("Lock poisoned")), 355 | } 356 | 357 | let tools = tools_list.list_tools(); 358 | 359 | Ok(ToolsListResponse { 360 | tools, 361 | next_cursor: None, 362 | meta: None, 363 | }) 364 | }) 365 | }) 366 | .request_handler("tools/call", move |req: CallToolRequest| { 367 | let tools_call = tools_call.clone(); 368 | let conn = conn_for_call.clone(); 369 | Box::pin(async move { 370 | match conn.read() { 371 | Ok(conn) => { 372 | if !conn.initialized { 373 | return Err(anyhow::anyhow!("Client not initialized")); 374 | } 375 | } 376 | Err(_) => return Err(anyhow::anyhow!("Lock poisoned")), 377 | } 378 | 379 | match tools_call.call_tool(req).await { 380 | Ok(resp) => Ok(resp), 381 | Err(e) => Err(e), 382 | } 383 | }) 384 | }) 385 | .build() 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /mcp-core/src/tools.rs: -------------------------------------------------------------------------------- 1 | //! # MCP Tools Management 2 | //! 3 | //! This module provides the infrastructure for registering, managing, and invoking 4 | //! MCP tools. Tools are the primary way for clients to interact with server capabilities. 5 | //! 6 | //! The module implements a registry for tools and handlers that process tool invocations. 7 | 8 | use crate::types::{CallToolRequest, CallToolResponse, Tool}; 9 | use anyhow::Result; 10 | use std::collections::HashMap; 11 | use std::future::Future; 12 | use std::pin::Pin; 13 | 14 | /// Registry and dispatcher for MCP tools. 15 | /// 16 | /// The `Tools` struct manages a collection of tools and their associated handlers, 17 | /// providing methods to register, list, and invoke tools. 18 | pub struct Tools { 19 | tool_handlers: HashMap, 20 | } 21 | 22 | impl Tools { 23 | /// Creates a new tool registry with the given tool handlers. 24 | pub(crate) fn new(map: HashMap) -> Self { 25 | Self { tool_handlers: map } 26 | } 27 | 28 | /// Retrieves a tool definition by name. 29 | /// 30 | /// # Arguments 31 | /// 32 | /// * `name` - The name of the tool to retrieve 33 | /// 34 | /// # Returns 35 | /// 36 | /// An `Option` containing the tool if found, or `None` if not found. 37 | pub fn get_tool(&self, name: &str) -> Option { 38 | self.tool_handlers 39 | .get(name) 40 | .map(|tool_handler| tool_handler.tool.clone()) 41 | } 42 | 43 | /// Invokes a tool with the given request. 44 | /// 45 | /// # Arguments 46 | /// 47 | /// * `req` - The request containing the tool name and arguments 48 | /// 49 | /// # Returns 50 | /// 51 | /// A `Result` containing the tool response if successful, or an error if 52 | /// the tool is not found or the invocation fails. 53 | pub async fn call_tool(&self, req: CallToolRequest) -> Result { 54 | let handler = self 55 | .tool_handlers 56 | .get(&req.name) 57 | .ok_or_else(|| anyhow::anyhow!("Tool not found: {}", req.name))?; 58 | 59 | Ok((handler.f)(req).await) 60 | } 61 | 62 | /// Lists all registered tools. 63 | /// 64 | /// # Returns 65 | /// 66 | /// A vector containing all registered tools. 67 | pub fn list_tools(&self) -> Vec { 68 | self.tool_handlers 69 | .values() 70 | .map(|tool_handler| tool_handler.tool.clone()) 71 | .collect() 72 | } 73 | } 74 | 75 | /// Type alias for a tool handler function. 76 | /// 77 | /// A tool handler is a function that takes a `CallToolRequest` and returns a 78 | /// future that resolves to a `CallToolResponse`. 79 | pub type ToolHandlerFn = 80 | fn(CallToolRequest) -> Pin + Send>>; 81 | 82 | /// Container for a tool definition and its handler function. 83 | /// 84 | /// The `ToolHandler` struct couples a tool definition with the function 85 | /// that implements the tool's behavior. 86 | pub(crate) struct ToolHandler { 87 | /// The tool definition (name, description, parameters, etc.) 88 | pub tool: Tool, 89 | /// The handler function that implements the tool 90 | pub f: Box, 91 | } 92 | -------------------------------------------------------------------------------- /mcp-core/src/transport/client/mod.rs: -------------------------------------------------------------------------------- 1 | //! # MCP Client Transports 2 | //! 3 | //! This module provides different transport implementations for MCP clients. 4 | //! 5 | //! Available transports include: 6 | //! - `ClientStdioTransport`: Communicates with an MCP server over standard I/O 7 | //! - `ClientSseTransport`: Communicates with an MCP server over Server-Sent Events (SSE) 8 | //! 9 | //! Each transport implements the `Transport` trait and provides client-specific 10 | //! functionality for connecting to MCP servers. 11 | 12 | #[cfg(feature = "sse")] 13 | mod sse; 14 | mod stdio; 15 | 16 | #[cfg(feature = "sse")] 17 | pub use sse::{ClientSseTransport, ClientSseTransportBuilder}; 18 | pub use stdio::ClientStdioTransport; 19 | -------------------------------------------------------------------------------- /mcp-core/src/transport/client/sse.rs: -------------------------------------------------------------------------------- 1 | use crate::protocol::{Protocol, ProtocolBuilder, RequestOptions}; 2 | use crate::transport::{ 3 | JsonRpcError, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, Message, RequestId, 4 | Transport, 5 | }; 6 | use crate::types::ErrorCode; 7 | use anyhow::Result; 8 | use async_trait::async_trait; 9 | use futures::TryStreamExt; 10 | use reqwest_eventsource::{Event, EventSource}; 11 | use std::collections::HashMap; 12 | use std::future::Future; 13 | use std::pin::Pin; 14 | use std::sync::Arc; 15 | use tokio::sync::Mutex; 16 | use tokio::time::timeout; 17 | use tracing::debug; 18 | 19 | /// Client transport that communicates with an MCP server over Server-Sent Events (SSE). 20 | /// 21 | /// The `ClientSseTransport` establishes a connection to an MCP server using Server-Sent 22 | /// Events (SSE) for receiving messages from the server, and HTTP for sending messages 23 | /// to the server. This transport is suitable for web-based applications and environments 24 | /// where network communication is required. 25 | /// 26 | /// Features: 27 | /// - Uses SSE for efficient one-way server-to-client communication 28 | /// - Uses HTTP for client-to-server communication 29 | /// - Supports authentication with bearer tokens 30 | /// - Allows custom HTTP headers 31 | /// - Automatically manages session state 32 | /// 33 | /// # Example 34 | /// 35 | /// ``` 36 | /// use mcp_core::transport::ClientSseTransport; 37 | /// 38 | /// async fn example() { 39 | /// let transport = ClientSseTransport::builder("https://example.com/sse".to_string()) 40 | /// .with_bearer_token("my-token".to_string()) 41 | /// .with_header("User-Agent", "My MCP Client") 42 | /// .build(); 43 | /// 44 | /// transport.open().await.expect("Failed to open SSE connection"); 45 | /// // Use transport... 46 | /// transport.close().await.expect("Failed to close SSE connection"); 47 | /// } 48 | /// ``` 49 | #[derive(Clone)] 50 | pub struct ClientSseTransport { 51 | protocol: Protocol, 52 | server_url: String, 53 | client: reqwest::Client, 54 | bearer_token: Option, 55 | session_endpoint: Arc>>, 56 | headers: HashMap, 57 | event_source: Arc>>, 58 | } 59 | 60 | /// Builder for configuring and creating `ClientSseTransport` instances. 61 | /// 62 | /// This builder allows customizing the SSE transport with options like: 63 | /// - Server URL 64 | /// - Authentication tokens 65 | /// - Custom HTTP headers 66 | /// 67 | /// Use this builder to create a new `ClientSseTransport` with the desired configuration. 68 | pub struct ClientSseTransportBuilder { 69 | server_url: String, 70 | bearer_token: Option, 71 | headers: HashMap, 72 | protocol_builder: ProtocolBuilder, 73 | } 74 | 75 | impl ClientSseTransportBuilder { 76 | /// Creates a new builder with the specified server URL. 77 | /// 78 | /// # Arguments 79 | /// 80 | /// * `server_url` - The URL of the SSE endpoint on the MCP server 81 | /// 82 | /// # Returns 83 | /// 84 | /// A new `ClientSseTransportBuilder` instance 85 | pub fn new(server_url: String) -> Self { 86 | Self { 87 | server_url, 88 | bearer_token: None, 89 | headers: HashMap::new(), 90 | protocol_builder: ProtocolBuilder::new(), 91 | } 92 | } 93 | 94 | /// Adds a bearer token for authentication. 95 | /// 96 | /// This token will be included in the `Authorization` header as `Bearer {token}`. 97 | /// 98 | /// # Arguments 99 | /// 100 | /// * `token` - The bearer token to use for authentication 101 | /// 102 | /// # Returns 103 | /// 104 | /// The modified builder instance 105 | pub fn with_bearer_token(mut self, token: String) -> Self { 106 | self.bearer_token = Some(token); 107 | self 108 | } 109 | 110 | /// Adds a custom HTTP header to the SSE request. 111 | /// 112 | /// # Arguments 113 | /// 114 | /// * `key` - The header name 115 | /// * `value` - The header value 116 | /// 117 | /// # Returns 118 | /// 119 | /// The modified builder instance 120 | pub fn with_header(mut self, key: impl Into, value: impl Into) -> Self { 121 | self.headers.insert(key.into(), value.into()); 122 | self 123 | } 124 | 125 | /// Builds the `ClientSseTransport` with the configured options. 126 | /// 127 | /// # Returns 128 | /// 129 | /// A new `ClientSseTransport` instance 130 | pub fn build(self) -> ClientSseTransport { 131 | ClientSseTransport { 132 | protocol: self.protocol_builder.build(), 133 | server_url: self.server_url, 134 | client: reqwest::Client::new(), 135 | bearer_token: self.bearer_token, 136 | session_endpoint: Arc::new(Mutex::new(None)), 137 | headers: self.headers, 138 | event_source: Arc::new(Mutex::new(None)), 139 | } 140 | } 141 | } 142 | 143 | impl ClientSseTransport { 144 | /// Creates a new builder for configuring the transport. 145 | /// 146 | /// # Arguments 147 | /// 148 | /// * `url` - The URL of the SSE endpoint on the MCP server 149 | /// 150 | /// # Returns 151 | /// 152 | /// A new `ClientSseTransportBuilder` instance 153 | pub fn builder(url: String) -> ClientSseTransportBuilder { 154 | ClientSseTransportBuilder::new(url) 155 | } 156 | } 157 | 158 | #[async_trait()] 159 | impl Transport for ClientSseTransport { 160 | /// Opens the transport by establishing an SSE connection to the server. 161 | /// 162 | /// This method: 163 | /// 1. Creates an SSE connection to the server URL 164 | /// 2. Adds configured headers and authentication 165 | /// 3. Starts a background task for handling incoming messages 166 | /// 4. Waits for the session endpoint to be received 167 | /// 168 | /// # Returns 169 | /// 170 | /// A `Result` indicating success or failure 171 | async fn open(&self) -> Result<()> { 172 | debug!("ClientSseTransport: Opening transport"); 173 | 174 | let mut request = self.client.get(self.server_url.clone()); 175 | 176 | // Add custom headers 177 | for (key, value) in &self.headers { 178 | request = request.header(key, value); 179 | } 180 | 181 | // Add auth header if configured 182 | if let Some(bearer_token) = &self.bearer_token { 183 | request = request.header("Authorization", format!("Bearer {}", bearer_token)); 184 | } 185 | 186 | let event_source = EventSource::new(request)?; 187 | 188 | { 189 | let mut es_lock = self.event_source.lock().await; 190 | *es_lock = Some(event_source); 191 | } 192 | 193 | // Spawn a background task to continuously poll messages 194 | let transport_clone = self.clone(); 195 | tokio::task::spawn(async move { 196 | loop { 197 | match transport_clone.poll_message().await { 198 | Ok(Some(message)) => match message { 199 | Message::Request(request) => { 200 | let response = transport_clone.protocol.handle_request(request).await; 201 | let _ = transport_clone 202 | .send_response(response.id, response.result, response.error) 203 | .await; 204 | } 205 | Message::Notification(notification) => { 206 | let _ = transport_clone 207 | .protocol 208 | .handle_notification(notification) 209 | .await; 210 | } 211 | Message::Response(response) => { 212 | transport_clone.protocol.handle_response(response).await; 213 | } 214 | }, 215 | Ok(None) => continue, // No message or control message, continue polling 216 | Err(e) => { 217 | debug!("ClientSseTransport: Error polling message: {:?}", e); 218 | // Maybe add some backoff or retry logic here 219 | tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; 220 | } 221 | } 222 | } 223 | }); 224 | 225 | // Wait for the session URL to be set 226 | let mut attempts = 0; 227 | while attempts < 10 { 228 | if self.session_endpoint.lock().await.is_some() { 229 | return Ok(()); 230 | } 231 | tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; 232 | attempts += 1; 233 | } 234 | 235 | Err(anyhow::anyhow!("Timeout waiting for initial SSE message")) 236 | } 237 | 238 | /// Closes the transport by terminating the SSE connection. 239 | /// 240 | /// This method: 241 | /// 1. Closes the EventSource connection 242 | /// 2. Clears the session endpoint 243 | /// 244 | /// # Returns 245 | /// 246 | /// A `Result` indicating success or failure 247 | async fn close(&self) -> Result<()> { 248 | debug!("ClientSseTransport: Closing transport"); 249 | // Close the event source 250 | *self.event_source.lock().await = None; 251 | 252 | // Clear the session URL 253 | *self.session_endpoint.lock().await = None; 254 | 255 | Ok(()) 256 | } 257 | 258 | /// Polls for incoming messages from the SSE connection. 259 | /// 260 | /// This method processes SSE events and: 261 | /// - Handles control messages (like endpoint information) 262 | /// - Parses JSON-RPC messages 263 | /// 264 | /// # Returns 265 | /// 266 | /// A `Result` containing an `Option` if a message is available 267 | async fn poll_message(&self) -> Result> { 268 | let mut event_source_guard = self.event_source.lock().await; 269 | let event_source = event_source_guard 270 | .as_mut() 271 | .ok_or_else(|| anyhow::anyhow!("Transport not opened"))?; 272 | 273 | match event_source.try_next().await { 274 | Ok(Some(event)) => match event { 275 | Event::Message(m) => { 276 | if &m.event[..] == "endpoint" { 277 | let endpoint = m 278 | .data 279 | .trim_start_matches("http://") 280 | .trim_start_matches("https://") 281 | .split_once('/') 282 | .map(|(_, path)| format!("/{}", path)) 283 | .unwrap_or(m.data); 284 | debug!("Received session endpoint: {}", endpoint); 285 | *self.session_endpoint.lock().await = Some(endpoint); 286 | return Ok(None); // This is a control message, not a JSON-RPC message 287 | } else { 288 | debug!("Received SSE message: {}", m.data); 289 | let message: Message = serde_json::from_str(&m.data)?; 290 | return Ok(Some(message)); 291 | } 292 | } 293 | _ => return Ok(None), 294 | }, 295 | Ok(None) => return Ok(None), // Stream ended 296 | Err(e) => { 297 | debug!("Error receiving SSE message: {:?}", e); 298 | return Err(anyhow::anyhow!("Failed to parse SSE message: {:?}", e)); 299 | } 300 | } 301 | } 302 | 303 | /// Sends a request to the server via HTTP and waits for a response. 304 | /// 305 | /// This method: 306 | /// 1. Creates a JSON-RPC request 307 | /// 2. Sends it to the session endpoint via HTTP POST 308 | /// 3. Waits for a response with the same ID through the SSE connection 309 | /// 310 | /// # Arguments 311 | /// 312 | /// * `method` - The method name for the request 313 | /// * `params` - Optional parameters for the request 314 | /// * `options` - Request options (like timeout) 315 | /// 316 | /// # Returns 317 | /// 318 | /// A `Future` that resolves to a `Result` containing the response 319 | fn request( 320 | &self, 321 | method: &str, 322 | params: Option, 323 | options: RequestOptions, 324 | ) -> Pin> + Send + Sync>> { 325 | let protocol = self.protocol.clone(); 326 | let client = self.client.clone(); 327 | let server_url = self.server_url.clone(); 328 | let session_endpoint = self.session_endpoint.clone(); 329 | let bearer_token = self.bearer_token.clone(); 330 | let method = method.to_owned(); 331 | let headers = self.headers.clone(); 332 | 333 | Box::pin(async move { 334 | let (id, rx) = protocol.create_request().await; 335 | let request = JsonRpcRequest { 336 | id, 337 | method, 338 | jsonrpc: Default::default(), 339 | params, 340 | }; 341 | 342 | // Get the session URL 343 | let session_url = { 344 | let url = session_endpoint.lock().await; 345 | url.as_ref() 346 | .ok_or_else(|| anyhow::anyhow!("No session URL available"))? 347 | .clone() 348 | }; 349 | 350 | let base_url = if let Some(idx) = server_url.find("://") { 351 | let domain_start = idx + 3; 352 | let domain_end = server_url[domain_start..] 353 | .find('/') 354 | .map(|i| domain_start + i) 355 | .unwrap_or(server_url.len()); 356 | &server_url[..domain_end] 357 | } else { 358 | let domain_end = server_url.find('/').unwrap_or(server_url.len()); 359 | &server_url[..domain_end] 360 | } 361 | .to_string(); 362 | 363 | debug!("ClientSseTransport: Base URL: {}", base_url); 364 | 365 | let full_url = format!("{}{}", base_url, session_url); 366 | debug!( 367 | "ClientSseTransport: Sending request to {}: {:?}", 368 | full_url, request 369 | ); 370 | 371 | let mut req_builder = client.post(&full_url).json(&request); 372 | 373 | for (key, value) in headers { 374 | req_builder = req_builder.header(key, value); 375 | } 376 | 377 | if let Some(token) = bearer_token { 378 | req_builder = req_builder.header("Authorization", format!("Bearer {}", token)); 379 | } 380 | 381 | let response = req_builder.send().await?; 382 | 383 | if !response.status().is_success() { 384 | let status = response.status(); 385 | let text = response.text().await?; 386 | return Err(anyhow::anyhow!( 387 | "Failed to send request, status: {status}, body: {text}" 388 | )); 389 | } 390 | 391 | debug!("ClientSseTransport: Request sent successfully"); 392 | 393 | // Wait for the response with a timeout 394 | let result = timeout(options.timeout, rx).await; 395 | match result { 396 | Ok(inner_result) => match inner_result { 397 | Ok(response) => Ok(response), 398 | Err(_) => { 399 | protocol.cancel_response(id).await; 400 | Ok(JsonRpcResponse { 401 | id, 402 | result: None, 403 | error: Some(JsonRpcError { 404 | code: ErrorCode::RequestTimeout as i32, 405 | message: "Request cancelled".to_string(), 406 | data: None, 407 | }), 408 | ..Default::default() 409 | }) 410 | } 411 | }, 412 | Err(_) => { 413 | protocol.cancel_response(id).await; 414 | Ok(JsonRpcResponse { 415 | id, 416 | result: None, 417 | error: Some(JsonRpcError { 418 | code: ErrorCode::RequestTimeout as i32, 419 | message: "Request timed out".to_string(), 420 | data: None, 421 | }), 422 | ..Default::default() 423 | }) 424 | } 425 | } 426 | }) 427 | } 428 | 429 | /// Sends a response to a request previously received from the server. 430 | /// 431 | /// # Arguments 432 | /// 433 | /// * `id` - The ID of the request being responded to 434 | /// * `result` - Optional successful result 435 | /// * `error` - Optional error information 436 | /// 437 | /// # Returns 438 | /// 439 | /// A `Result` indicating success or failure 440 | async fn send_response( 441 | &self, 442 | id: RequestId, 443 | result: Option, 444 | error: Option, 445 | ) -> Result<()> { 446 | let response = JsonRpcResponse { 447 | id, 448 | result, 449 | error, 450 | jsonrpc: Default::default(), 451 | }; 452 | 453 | // Get the session URL 454 | let session_url = { 455 | let url = self.session_endpoint.lock().await; 456 | url.as_ref() 457 | .ok_or_else(|| anyhow::anyhow!("No session URL available"))? 458 | .clone() 459 | }; 460 | 461 | let server_url = self.server_url.clone(); 462 | let base_url = if let Some(idx) = server_url.find("://") { 463 | let domain_start = idx + 3; 464 | let domain_end = server_url[domain_start..] 465 | .find('/') 466 | .map(|i| domain_start + i) 467 | .unwrap_or(server_url.len()); 468 | &server_url[..domain_end] 469 | } else { 470 | let domain_end = server_url.find('/').unwrap_or(server_url.len()); 471 | &server_url[..domain_end] 472 | } 473 | .to_string(); 474 | 475 | debug!("ClientSseTransport: Base URL: {}", base_url); 476 | 477 | let full_url = format!("{}{}", base_url, session_url); 478 | debug!( 479 | "ClientSseTransport: Sending response to {}: {:?}", 480 | full_url, response 481 | ); 482 | 483 | let mut req_builder = self.client.post(&full_url).json(&response); 484 | 485 | for (key, value) in &self.headers { 486 | req_builder = req_builder.header(key, value); 487 | } 488 | 489 | if let Some(token) = &self.bearer_token { 490 | req_builder = req_builder.header("Authorization", format!("Bearer {}", token)); 491 | } 492 | 493 | let response = req_builder.send().await?; 494 | 495 | if !response.status().is_success() { 496 | let status = response.status(); 497 | let text = response.text().await?; 498 | return Err(anyhow::anyhow!( 499 | "Failed to send response, status: {status}, body: {text}" 500 | )); 501 | } 502 | 503 | Ok(()) 504 | } 505 | 506 | /// Sends a notification to the server via HTTP. 507 | /// 508 | /// Unlike requests, notifications do not expect a response. 509 | /// 510 | /// # Arguments 511 | /// 512 | /// * `method` - The method name for the notification 513 | /// * `params` - Optional parameters for the notification 514 | /// 515 | /// # Returns 516 | /// 517 | /// A `Result` indicating success or failure 518 | async fn send_notification( 519 | &self, 520 | method: &str, 521 | params: Option, 522 | ) -> Result<()> { 523 | let notification = JsonRpcNotification { 524 | jsonrpc: Default::default(), 525 | method: method.to_owned(), 526 | params, 527 | }; 528 | 529 | // Get the session URL 530 | let session_url = { 531 | let url = self.session_endpoint.lock().await; 532 | url.as_ref() 533 | .ok_or_else(|| anyhow::anyhow!("No session URL available"))? 534 | .clone() 535 | }; 536 | 537 | let server_url = self.server_url.clone(); 538 | let base_url = if let Some(idx) = server_url.find("://") { 539 | let domain_start = idx + 3; 540 | let domain_end = server_url[domain_start..] 541 | .find('/') 542 | .map(|i| domain_start + i) 543 | .unwrap_or(server_url.len()); 544 | &server_url[..domain_end] 545 | } else { 546 | let domain_end = server_url.find('/').unwrap_or(server_url.len()); 547 | &server_url[..domain_end] 548 | } 549 | .to_string(); 550 | 551 | debug!("ClientSseTransport: Base URL: {}", base_url); 552 | 553 | let full_url = format!("{}{}", base_url, session_url); 554 | debug!( 555 | "ClientSseTransport: Sending notification to {}: {:?}", 556 | full_url, notification 557 | ); 558 | 559 | let mut req_builder = self.client.post(&full_url).json(¬ification); 560 | 561 | for (key, value) in &self.headers { 562 | req_builder = req_builder.header(key, value); 563 | } 564 | 565 | if let Some(token) = &self.bearer_token { 566 | req_builder = req_builder.header("Authorization", format!("Bearer {}", token)); 567 | } 568 | 569 | let response = req_builder.send().await?; 570 | 571 | if !response.status().is_success() { 572 | let status = response.status(); 573 | let text = response.text().await?; 574 | return Err(anyhow::anyhow!( 575 | "Failed to send notification, status: {status}, body: {text}" 576 | )); 577 | } 578 | 579 | Ok(()) 580 | } 581 | } 582 | -------------------------------------------------------------------------------- /mcp-core/src/transport/client/stdio.rs: -------------------------------------------------------------------------------- 1 | use crate::protocol::{Protocol, ProtocolBuilder, RequestOptions}; 2 | use crate::transport::{ 3 | JsonRpcError, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, Message, RequestId, 4 | Transport, 5 | }; 6 | use crate::types::ErrorCode; 7 | use anyhow::Result; 8 | use async_trait::async_trait; 9 | use std::future::Future; 10 | use std::io::{BufRead, BufReader, BufWriter, Write}; 11 | use std::pin::Pin; 12 | use std::process::Command; 13 | use std::sync::Arc; 14 | use tokio::sync::Mutex; 15 | use tokio::time::timeout; 16 | use tracing::debug; 17 | 18 | /// Client transport that communicates with an MCP server over standard I/O. 19 | /// 20 | /// The `ClientStdioTransport` launches a child process specified by the provided 21 | /// program and arguments, then communicates with it using the standard input and output 22 | /// streams. It implements the `Transport` trait to send requests and receive responses 23 | /// over these streams. 24 | /// 25 | /// This transport is useful for: 26 | /// - Running local MCP servers as child processes 27 | /// - Command-line tools that need to communicate with MCP servers 28 | /// - Testing and development scenarios 29 | /// 30 | /// # Example 31 | /// 32 | /// ``` 33 | /// use mcp_core::transport::{ClientStdioTransport, Transport}; 34 | /// use anyhow::Result; 35 | /// 36 | /// async fn example() -> Result<()> { 37 | /// let transport = ClientStdioTransport::new("my-mcp-server", &["--flag"])?; 38 | /// transport.open().await?; 39 | /// // Use transport... 40 | /// transport.close().await?; 41 | /// Ok(()) 42 | /// } 43 | /// ``` 44 | #[derive(Clone)] 45 | pub struct ClientStdioTransport { 46 | protocol: Protocol, 47 | stdin: Arc>>>, 48 | stdout: Arc>>>, 49 | child: Arc>>, 50 | program: String, 51 | args: Vec, 52 | } 53 | 54 | impl ClientStdioTransport { 55 | /// Creates a new `ClientStdioTransport` instance. 56 | /// 57 | /// # Arguments 58 | /// 59 | /// * `program` - The path or name of the program to execute 60 | /// * `args` - Command-line arguments to pass to the program 61 | /// 62 | /// # Returns 63 | /// 64 | /// A `Result` containing the new transport instance if successful 65 | pub fn new(program: &str, args: &[&str]) -> Result { 66 | Ok(ClientStdioTransport { 67 | protocol: ProtocolBuilder::new().build(), 68 | stdin: Arc::new(Mutex::new(None)), 69 | stdout: Arc::new(Mutex::new(None)), 70 | child: Arc::new(Mutex::new(None)), 71 | program: program.to_string(), 72 | args: args.iter().map(|&s| s.to_string()).collect(), 73 | }) 74 | } 75 | } 76 | 77 | #[async_trait()] 78 | impl Transport for ClientStdioTransport { 79 | /// Opens the transport by launching the child process and setting up the communication channels. 80 | /// 81 | /// This method: 82 | /// 1. Spawns the child process with the configured program and arguments 83 | /// 2. Sets up pipes for stdin and stdout 84 | /// 3. Starts a background task for handling incoming messages 85 | /// 86 | /// # Returns 87 | /// 88 | /// A `Result` indicating success or failure 89 | async fn open(&self) -> Result<()> { 90 | debug!("ClientStdioTransport: Opening transport"); 91 | let mut child = Command::new(&self.program) 92 | .args(&self.args) 93 | .stdin(std::process::Stdio::piped()) 94 | .stdout(std::process::Stdio::piped()) 95 | .spawn()?; 96 | 97 | let stdin = child 98 | .stdin 99 | .take() 100 | .ok_or_else(|| anyhow::anyhow!("Child process stdin not available"))?; 101 | let stdout = child 102 | .stdout 103 | .take() 104 | .ok_or_else(|| anyhow::anyhow!("Child process stdout not available"))?; 105 | 106 | { 107 | let mut stdin_lock = self.stdin.lock().await; 108 | *stdin_lock = Some(BufWriter::new(stdin)); 109 | } 110 | { 111 | let mut stdout_lock = self.stdout.lock().await; 112 | *stdout_lock = Some(BufReader::new(stdout)); 113 | } 114 | { 115 | let mut child_lock = self.child.lock().await; 116 | *child_lock = Some(child); 117 | } 118 | 119 | // Spawn a background task to continuously poll messages. 120 | let transport_clone = self.clone(); 121 | tokio::spawn(async move { 122 | loop { 123 | match transport_clone.poll_message().await { 124 | Ok(Some(message)) => match message { 125 | Message::Request(request) => { 126 | let response = transport_clone.protocol.handle_request(request).await; 127 | let _ = transport_clone 128 | .send_response(response.id, response.result, response.error) 129 | .await; 130 | } 131 | Message::Notification(notification) => { 132 | let _ = transport_clone 133 | .protocol 134 | .handle_notification(notification) 135 | .await; 136 | } 137 | Message::Response(response) => { 138 | transport_clone.protocol.handle_response(response).await; 139 | } 140 | }, 141 | Ok(None) => break, // EOF encountered. 142 | Err(e) => { 143 | debug!("ClientStdioTransport: Error polling message: {:?}", e); 144 | break; 145 | } 146 | } 147 | } 148 | }); 149 | Ok(()) 150 | } 151 | 152 | /// Closes the transport by terminating the child process and cleaning up resources. 153 | /// 154 | /// This method: 155 | /// 1. Kills the child process 156 | /// 2. Clears the stdin and stdout handles 157 | /// 158 | /// # Returns 159 | /// 160 | /// A `Result` indicating success or failure 161 | async fn close(&self) -> Result<()> { 162 | let mut child_lock = self.child.lock().await; 163 | if let Some(child) = child_lock.as_mut() { 164 | let _ = child.kill(); 165 | } 166 | *child_lock = None; 167 | 168 | // Clear stdin and stdout 169 | *self.stdin.lock().await = None; 170 | *self.stdout.lock().await = None; 171 | 172 | Ok(()) 173 | } 174 | 175 | /// Polls for incoming messages from the child process's stdout. 176 | /// 177 | /// This method reads a line from the child process's stdout and parses it 178 | /// as a JSON-RPC message. 179 | /// 180 | /// # Returns 181 | /// 182 | /// A `Result` containing an `Option`. `None` indicates EOF. 183 | async fn poll_message(&self) -> Result> { 184 | debug!("ClientStdioTransport: Starting to receive message"); 185 | 186 | // Take ownership of stdout temporarily 187 | let mut stdout_guard = self.stdout.lock().await; 188 | let mut stdout = stdout_guard 189 | .take() 190 | .ok_or_else(|| anyhow::anyhow!("Transport not opened"))?; 191 | 192 | // Drop the lock before spawning the blocking task 193 | drop(stdout_guard); 194 | 195 | // Use a blocking operation in a spawn_blocking task 196 | let (line_result, stdout) = tokio::task::spawn_blocking(move || { 197 | let mut line = String::new(); 198 | let result = match stdout.read_line(&mut line) { 199 | Ok(0) => Ok(None), // EOF 200 | Ok(_) => Ok(Some(line)), 201 | Err(e) => Err(anyhow::anyhow!("Error reading line: {}", e)), 202 | }; 203 | // Return both the result and the stdout so we can put it back 204 | (result, stdout) 205 | }) 206 | .await?; 207 | 208 | // Put stdout back 209 | let mut stdout_guard = self.stdout.lock().await; 210 | *stdout_guard = Some(stdout); 211 | 212 | // Process the result 213 | match line_result? { 214 | Some(line) => { 215 | debug!( 216 | "ClientStdioTransport: Received from process: {}", 217 | line.trim() 218 | ); 219 | let message: Message = serde_json::from_str(&line)?; 220 | debug!("ClientStdioTransport: Successfully parsed message"); 221 | Ok(Some(message)) 222 | } 223 | None => { 224 | debug!("ClientStdioTransport: Received EOF from process"); 225 | Ok(None) 226 | } 227 | } 228 | } 229 | 230 | /// Sends a request to the child process and waits for a response. 231 | /// 232 | /// This method: 233 | /// 1. Creates a new request ID 234 | /// 2. Constructs a JSON-RPC request 235 | /// 3. Sends it to the child process's stdin 236 | /// 4. Waits for a response with the same ID 237 | /// 238 | /// # Arguments 239 | /// 240 | /// * `method` - The method name for the request 241 | /// * `params` - Optional parameters for the request 242 | /// * `options` - Request options (like timeout) 243 | /// 244 | /// # Returns 245 | /// 246 | /// A `Future` that resolves to a `Result` containing the response 247 | fn request( 248 | &self, 249 | method: &str, 250 | params: Option, 251 | options: RequestOptions, 252 | ) -> Pin> + Send + Sync>> { 253 | let protocol = self.protocol.clone(); 254 | let stdin_arc = self.stdin.clone(); 255 | let method = method.to_owned(); 256 | Box::pin(async move { 257 | let (id, rx) = protocol.create_request().await; 258 | let request = JsonRpcRequest { 259 | id, 260 | method, 261 | jsonrpc: Default::default(), 262 | params, 263 | }; 264 | let serialized = serde_json::to_string(&request)?; 265 | debug!("ClientStdioTransport: Sending request: {}", serialized); 266 | 267 | // Get the stdin writer 268 | let mut stdin_guard = stdin_arc.lock().await; 269 | let mut stdin = stdin_guard 270 | .take() 271 | .ok_or_else(|| anyhow::anyhow!("Transport not opened"))?; 272 | 273 | // Use a blocking operation in a spawn_blocking task 274 | let stdin_result = tokio::task::spawn_blocking(move || { 275 | stdin.write_all(serialized.as_bytes())?; 276 | stdin.write_all(b"\n")?; 277 | stdin.flush()?; 278 | Ok::<_, anyhow::Error>(stdin) 279 | }) 280 | .await??; 281 | 282 | // Put the writer back 283 | *stdin_guard = Some(stdin_result); 284 | 285 | debug!("ClientStdioTransport: Request sent successfully"); 286 | let result = timeout(options.timeout, rx).await; 287 | match result { 288 | Ok(inner_result) => match inner_result { 289 | Ok(response) => Ok(response), 290 | Err(_) => { 291 | protocol.cancel_response(id).await; 292 | Ok(JsonRpcResponse { 293 | id, 294 | result: None, 295 | error: Some(JsonRpcError { 296 | code: ErrorCode::RequestTimeout as i32, 297 | message: "Request cancelled".to_string(), 298 | data: None, 299 | }), 300 | ..Default::default() 301 | }) 302 | } 303 | }, 304 | Err(_) => { 305 | protocol.cancel_response(id).await; 306 | Ok(JsonRpcResponse { 307 | id, 308 | result: None, 309 | error: Some(JsonRpcError { 310 | code: ErrorCode::RequestTimeout as i32, 311 | message: "Request timed out".to_string(), 312 | data: None, 313 | }), 314 | ..Default::default() 315 | }) 316 | } 317 | } 318 | }) 319 | } 320 | 321 | /// Sends a response to a request previously received from the child process. 322 | /// 323 | /// # Arguments 324 | /// 325 | /// * `id` - The ID of the request being responded to 326 | /// * `result` - Optional successful result 327 | /// * `error` - Optional error information 328 | /// 329 | /// # Returns 330 | /// 331 | /// A `Result` indicating success or failure 332 | async fn send_response( 333 | &self, 334 | id: RequestId, 335 | result: Option, 336 | error: Option, 337 | ) -> Result<()> { 338 | let response = JsonRpcResponse { 339 | id, 340 | result, 341 | error, 342 | jsonrpc: Default::default(), 343 | }; 344 | let serialized = serde_json::to_string(&response)?; 345 | debug!("ClientStdioTransport: Sending response: {}", serialized); 346 | 347 | // Get the stdin writer 348 | let mut stdin_guard = self.stdin.lock().await; 349 | let mut stdin = stdin_guard 350 | .take() 351 | .ok_or_else(|| anyhow::anyhow!("Transport not opened"))?; 352 | 353 | // Use a blocking operation in a spawn_blocking task 354 | let stdin_result = tokio::task::spawn_blocking(move || { 355 | stdin.write_all(serialized.as_bytes())?; 356 | stdin.write_all(b"\n")?; 357 | stdin.flush()?; 358 | Ok::<_, anyhow::Error>(stdin) 359 | }) 360 | .await??; 361 | 362 | // Put the writer back 363 | *stdin_guard = Some(stdin_result); 364 | 365 | Ok(()) 366 | } 367 | 368 | /// Sends a notification to the child process. 369 | /// 370 | /// Unlike requests, notifications do not expect a response. 371 | /// 372 | /// # Arguments 373 | /// 374 | /// * `method` - The method name for the notification 375 | /// * `params` - Optional parameters for the notification 376 | /// 377 | /// # Returns 378 | /// 379 | /// A `Result` indicating success or failure 380 | async fn send_notification( 381 | &self, 382 | method: &str, 383 | params: Option, 384 | ) -> Result<()> { 385 | let notification = JsonRpcNotification { 386 | jsonrpc: Default::default(), 387 | method: method.to_owned(), 388 | params, 389 | }; 390 | let serialized = serde_json::to_string(¬ification)?; 391 | debug!("ClientStdioTransport: Sending notification: {}", serialized); 392 | 393 | // Get the stdin writer 394 | let mut stdin_guard = self.stdin.lock().await; 395 | let mut stdin = stdin_guard 396 | .take() 397 | .ok_or_else(|| anyhow::anyhow!("Transport not opened"))?; 398 | 399 | // Use a blocking operation in a spawn_blocking task 400 | let stdin_result = tokio::task::spawn_blocking(move || { 401 | stdin.write_all(serialized.as_bytes())?; 402 | stdin.write_all(b"\n")?; 403 | stdin.flush()?; 404 | Ok::<_, anyhow::Error>(stdin) 405 | }) 406 | .await??; 407 | 408 | // Put the writer back 409 | *stdin_guard = Some(stdin_result); 410 | 411 | Ok(()) 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /mcp-core/src/transport/mod.rs: -------------------------------------------------------------------------------- 1 | //! # MCP Transport Layer 2 | //! 3 | //! This module implements the transport layer for the Model Context Protocol (MCP). 4 | //! It provides abstractions for sending and receiving JSON-RPC messages between 5 | //! clients and servers using different transport mechanisms. 6 | //! 7 | //! The transport layer: 8 | //! - Handles serialization and deserialization of messages 9 | //! - Provides interfaces for sending and receiving messages 10 | //! - Defines transport-specific implementations (SSE, stdio) 11 | //! - Abstracts the underlying communication protocol 12 | //! 13 | //! The core component is the `Transport` trait, which defines the operations that 14 | //! any MCP transport must support, regardless of the underlying mechanism. 15 | 16 | use std::{future::Future, pin::Pin}; 17 | 18 | use anyhow::Result; 19 | use async_trait::async_trait; 20 | use serde::{Deserialize, Serialize}; 21 | 22 | mod client; 23 | pub use client::*; 24 | 25 | mod server; 26 | pub use server::*; 27 | 28 | use crate::protocol::RequestOptions; 29 | 30 | /// A message in the MCP protocol. 31 | /// 32 | /// Currently, only JSON-RPC messages are supported, as defined in the 33 | /// [MCP specification](https://spec.modelcontextprotocol.io/specification/basic/messages/). 34 | pub type Message = JsonRpcMessage; 35 | 36 | /// Core trait that defines operations for MCP transports. 37 | /// 38 | /// This trait abstracts the transport layer, allowing the protocol to work 39 | /// with different communication mechanisms (SSE, stdio, etc.). 40 | #[async_trait()] 41 | pub trait Transport: Send + Sync + 'static { 42 | /// Opens the transport connection. 43 | /// 44 | /// This initializes the transport and prepares it for communication. 45 | /// 46 | /// # Returns 47 | /// 48 | /// A `Result` indicating success or failure 49 | async fn open(&self) -> Result<()>; 50 | 51 | /// Closes the transport connection. 52 | /// 53 | /// This terminates the transport and releases any resources. 54 | /// 55 | /// # Returns 56 | /// 57 | /// A `Result` indicating success or failure 58 | async fn close(&self) -> Result<()>; 59 | 60 | /// Polls for incoming messages. 61 | /// 62 | /// This checks for any new messages from the other endpoint. 63 | /// 64 | /// # Returns 65 | /// 66 | /// A `Result` containing an `Option` if a message is available 67 | async fn poll_message(&self) -> Result>; 68 | 69 | /// Sends a request and waits for the response. 70 | /// 71 | /// # Arguments 72 | /// 73 | /// * `method` - The method name for the request 74 | /// * `params` - Optional parameters for the request 75 | /// * `options` - Request options (like timeout) 76 | /// 77 | /// # Returns 78 | /// 79 | /// A `Future` that resolves to a `Result` containing the response 80 | fn request( 81 | &self, 82 | method: &str, 83 | params: Option, 84 | options: RequestOptions, 85 | ) -> Pin> + Send + Sync>>; 86 | 87 | /// Sends a notification. 88 | /// 89 | /// Unlike requests, notifications do not expect a response. 90 | /// 91 | /// # Arguments 92 | /// 93 | /// * `method` - The method name for the notification 94 | /// * `params` - Optional parameters for the notification 95 | /// 96 | /// # Returns 97 | /// 98 | /// A `Result` indicating success or failure 99 | async fn send_notification( 100 | &self, 101 | method: &str, 102 | params: Option, 103 | ) -> Result<()>; 104 | 105 | /// Sends a response to a request. 106 | /// 107 | /// # Arguments 108 | /// 109 | /// * `id` - The ID of the request being responded to 110 | /// * `result` - Optional successful result 111 | /// * `error` - Optional error information 112 | /// 113 | /// # Returns 114 | /// 115 | /// A `Result` indicating success or failure 116 | async fn send_response( 117 | &self, 118 | id: RequestId, 119 | result: Option, 120 | error: Option, 121 | ) -> Result<()>; 122 | } 123 | 124 | /// Type representing a JSON-RPC request ID. 125 | /// 126 | /// Request IDs are used to match responses to their corresponding requests. 127 | pub type RequestId = u64; 128 | 129 | /// Represents a JSON-RPC protocol version. 130 | /// 131 | /// The JSON-RPC version is included in all JSON-RPC messages and 132 | /// is typically "2.0" for the current version of the protocol. 133 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 134 | #[serde(transparent)] 135 | pub struct JsonRpcVersion(String); 136 | 137 | impl Default for JsonRpcVersion { 138 | /// Creates a default JSON-RPC version (2.0). 139 | /// 140 | /// # Returns 141 | /// 142 | /// A new `JsonRpcVersion` with value "2.0" 143 | fn default() -> Self { 144 | JsonRpcVersion("2.0".to_owned()) 145 | } 146 | } 147 | 148 | impl JsonRpcVersion { 149 | /// Returns the version as a string slice. 150 | /// 151 | /// # Returns 152 | /// 153 | /// A string slice containing the version 154 | pub fn as_str(&self) -> &str { 155 | &self.0 156 | } 157 | } 158 | 159 | /// Represents a JSON-RPC message. 160 | /// 161 | /// This enum can be a request, a response, or a notification. 162 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 163 | #[serde(deny_unknown_fields)] 164 | #[serde(untagged)] 165 | pub enum JsonRpcMessage { 166 | /// A response to a request 167 | Response(JsonRpcResponse), 168 | /// A request that expects a response 169 | Request(JsonRpcRequest), 170 | /// A notification that does not expect a response 171 | Notification(JsonRpcNotification), 172 | } 173 | 174 | /// Represents a JSON-RPC request. 175 | /// 176 | /// A request is a message that expects a response with the same ID. 177 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] 178 | #[serde(deny_unknown_fields)] 179 | pub struct JsonRpcRequest { 180 | /// The request ID, used to match with the response 181 | pub id: RequestId, 182 | /// The method name to call 183 | pub method: String, 184 | /// Optional parameters for the method 185 | #[serde(skip_serializing_if = "Option::is_none")] 186 | pub params: Option, 187 | /// The JSON-RPC version 188 | pub jsonrpc: JsonRpcVersion, 189 | } 190 | 191 | /// Represents a JSON-RPC notification. 192 | /// 193 | /// A notification is a message that does not expect a response. 194 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] 195 | #[serde(rename_all = "camelCase")] 196 | #[serde(deny_unknown_fields)] 197 | #[serde(default)] 198 | pub struct JsonRpcNotification { 199 | /// The method name for the notification 200 | pub method: String, 201 | /// Optional parameters for the notification 202 | #[serde(skip_serializing_if = "Option::is_none")] 203 | pub params: Option, 204 | /// The JSON-RPC version 205 | pub jsonrpc: JsonRpcVersion, 206 | } 207 | 208 | /// Represents a JSON-RPC response. 209 | /// 210 | /// A response is a message sent in reply to a request with the same ID. 211 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] 212 | #[serde(deny_unknown_fields)] 213 | #[serde(rename_all = "camelCase")] 214 | #[serde(default)] 215 | pub struct JsonRpcResponse { 216 | /// The request ID this response corresponds to 217 | pub id: RequestId, 218 | /// The result of the request, if successful 219 | #[serde(skip_serializing_if = "Option::is_none")] 220 | pub result: Option, 221 | /// The error, if the request failed 222 | #[serde(skip_serializing_if = "Option::is_none")] 223 | pub error: Option, 224 | /// The JSON-RPC version 225 | pub jsonrpc: JsonRpcVersion, 226 | } 227 | 228 | /// Represents a JSON-RPC error. 229 | /// 230 | /// An error is included in a response when the request fails. 231 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] 232 | #[serde(rename_all = "camelCase")] 233 | #[serde(default)] 234 | pub struct JsonRpcError { 235 | /// Error code 236 | pub code: i32, 237 | /// Error message 238 | pub message: String, 239 | /// Optional additional error data 240 | #[serde(skip_serializing_if = "Option::is_none")] 241 | pub data: Option, 242 | } 243 | -------------------------------------------------------------------------------- /mcp-core/src/transport/server/mod.rs: -------------------------------------------------------------------------------- 1 | //! # MCP Server Transports 2 | //! 3 | //! This module provides different transport implementations for MCP servers. 4 | //! 5 | //! Available transports include: 6 | //! - `ServerStdioTransport`: Communicates with MCP clients over standard I/O 7 | //! - `ServerSseTransport`: Communicates with MCP clients over Server-Sent Events (SSE) 8 | //! 9 | //! Each transport implements the `Transport` trait and provides server-specific 10 | //! functionality for accepting connections from MCP clients and handling 11 | //! communication. 12 | 13 | mod stdio; 14 | pub use stdio::ServerStdioTransport; 15 | 16 | #[cfg(feature = "sse")] 17 | mod sse; 18 | #[cfg(feature = "sse")] 19 | pub use sse::ServerSseTransport; 20 | -------------------------------------------------------------------------------- /mcp-core/src/transport/server/sse.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | protocol::{Protocol, RequestOptions}, 3 | transport::{ 4 | JsonRpcError, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, 5 | Message, RequestId, Transport, 6 | }, 7 | types::ErrorCode, 8 | }; 9 | use actix_web::{ 10 | middleware::Logger, 11 | web::{self, Query}, 12 | App, HttpResponse, HttpServer, 13 | }; 14 | use anyhow::Result; 15 | use async_trait::async_trait; 16 | use futures::StreamExt; 17 | use serde::Deserialize; 18 | use std::sync::Arc; 19 | use std::{collections::HashMap, future::Future}; 20 | use std::{pin::Pin, time::Duration}; 21 | use tokio::{ 22 | sync::{mpsc, Mutex}, 23 | time::timeout, 24 | }; 25 | use uuid::Uuid; 26 | 27 | /// Server transport that communicates with MCP clients over Server-Sent Events (SSE). 28 | /// 29 | /// The `ServerSseTransport` runs an HTTP server that accepts connections from clients 30 | /// using Server-Sent Events (SSE) for sending messages to clients and receiving messages 31 | /// via HTTP POST requests. This transport is suitable for web-based MCP implementations 32 | /// and applications that need to communicate across network boundaries. 33 | /// 34 | /// Features: 35 | /// - Supports multiple concurrent client connections 36 | /// - Uses SSE for efficient server-to-client messaging 37 | /// - Manages client sessions with unique IDs 38 | /// - Provides heartbeat/ping functionality to maintain connections 39 | /// 40 | /// # Example 41 | /// 42 | /// ``` 43 | /// use mcp_core::{protocol::Protocol, transport::ServerSseTransport}; 44 | /// 45 | /// async fn example() { 46 | /// let protocol = Protocol::builder().build(); 47 | /// let transport = ServerSseTransport::new("127.0.0.1".to_string(), 3000, protocol); 48 | /// // Start the server 49 | /// transport.open().await.expect("Failed to start SSE server"); 50 | /// } 51 | /// ``` 52 | #[derive(Clone)] 53 | pub struct ServerSseTransport { 54 | protocol: Protocol, 55 | sessions: Arc>>, 56 | host: String, 57 | port: u16, 58 | } 59 | 60 | impl ServerSseTransport { 61 | /// Creates a new `ServerSseTransport` instance. 62 | /// 63 | /// # Arguments 64 | /// 65 | /// * `host` - The host address to bind the HTTP server to (e.g., "127.0.0.1") 66 | /// * `port` - The port to listen on 67 | /// * `protocol` - The MCP protocol instance to use for handling messages 68 | /// 69 | /// # Returns 70 | /// 71 | /// A new `ServerSseTransport` instance 72 | pub fn new(host: String, port: u16, protocol: Protocol) -> Self { 73 | Self { 74 | protocol, 75 | sessions: Arc::new(Mutex::new(HashMap::new())), 76 | host, 77 | port, 78 | } 79 | } 80 | 81 | /// Creates a new session with the given ID. 82 | /// 83 | /// This sets up the communication channels needed for the session. 84 | /// 85 | /// # Arguments 86 | /// 87 | /// * `session_id` - The unique ID for the session 88 | async fn create_session(&self, session_id: String) { 89 | let (tx, rx) = mpsc::channel::(100); 90 | let session = ServerSseTransportSession { 91 | protocol: self.protocol.clone(), 92 | tx, 93 | rx: Arc::new(Mutex::new(rx)), 94 | }; 95 | self.sessions.lock().await.insert(session_id, session); 96 | } 97 | 98 | /// Retrieves a session by its ID. 99 | /// 100 | /// # Arguments 101 | /// 102 | /// * `session_id` - The ID of the session to retrieve 103 | /// 104 | /// # Returns 105 | /// 106 | /// An `Option` containing the session if found, or `None` if not found 107 | async fn get_session(&self, session_id: &str) -> Option { 108 | let sessions = self.sessions.lock().await; 109 | sessions.get(session_id).cloned() 110 | } 111 | } 112 | 113 | #[async_trait()] 114 | impl Transport for ServerSseTransport { 115 | /// Opens the transport by starting the HTTP server. 116 | /// 117 | /// This method: 118 | /// 1. Creates an Actix Web HTTP server 119 | /// 2. Sets up routes for SSE connections and message handling 120 | /// 3. Binds to the configured host and port 121 | /// 4. Starts the server 122 | /// 123 | /// # Returns 124 | /// 125 | /// A `Result` indicating success or failure 126 | async fn open(&self) -> Result<()> { 127 | let transport = self.clone(); 128 | let server = HttpServer::new(move || { 129 | App::new() 130 | .wrap(Logger::default()) 131 | .app_data(web::Data::new(transport.clone())) 132 | .route("/sse", web::get().to(sse_handler)) 133 | .route("/message", web::post().to(message_handler)) 134 | }) 135 | .bind((self.host.clone(), self.port))? 136 | .run(); 137 | 138 | server 139 | .await 140 | .map_err(|e| anyhow::anyhow!("Server error: {:?}", e)) 141 | } 142 | 143 | /// Closes the transport. 144 | /// 145 | /// This is a no-op for the SSE transport as the HTTP server is managed by Actix Web. 146 | /// 147 | /// # Returns 148 | /// 149 | /// A `Result` indicating success 150 | async fn close(&self) -> Result<()> { 151 | Ok(()) 152 | } 153 | 154 | /// Polls for incoming messages. 155 | /// 156 | /// This is a no-op for the SSE transport as messages are handled via HTTP routes. 157 | /// 158 | /// # Returns 159 | /// 160 | /// A `Result` containing `None` 161 | async fn poll_message(&self) -> Result> { 162 | Ok(None) 163 | } 164 | 165 | /// Sends a request. 166 | /// 167 | /// This is a no-op for the SSE transport as it doesn't directly send requests. 168 | /// 169 | /// # Returns 170 | /// 171 | /// A `Future` that resolves to a `Result` containing a default response 172 | fn request( 173 | &self, 174 | _method: &str, 175 | _params: Option, 176 | _options: RequestOptions, 177 | ) -> Pin> + Send + Sync>> { 178 | Box::pin(async move { Ok(JsonRpcResponse::default()) }) 179 | } 180 | 181 | /// Sends a notification. 182 | /// 183 | /// This is a no-op for the SSE transport as it doesn't directly send notifications. 184 | /// 185 | /// # Returns 186 | /// 187 | /// A `Result` indicating success 188 | async fn send_notification( 189 | &self, 190 | _method: &str, 191 | _params: Option, 192 | ) -> Result<()> { 193 | Ok(()) 194 | } 195 | 196 | /// Sends a response. 197 | /// 198 | /// This is a no-op for the SSE transport as responses are handled by individual sessions. 199 | /// 200 | /// # Returns 201 | /// 202 | /// A `Result` indicating success 203 | async fn send_response( 204 | &self, 205 | _id: RequestId, 206 | _result: Option, 207 | _error: Option, 208 | ) -> Result<()> { 209 | Ok(()) 210 | } 211 | } 212 | 213 | /// Handles SSE connection requests. 214 | /// 215 | /// This function: 216 | /// 1. Creates a new session for the client 217 | /// 2. Establishes an SSE stream 218 | /// 3. Sends the endpoint info event 219 | /// 4. Sets up a ping mechanism to keep the connection alive 220 | /// 5. Streams messages to the client 221 | /// 222 | /// # Arguments 223 | /// 224 | /// * `req` - The HTTP request 225 | /// * `transport` - The `ServerSseTransport` instance 226 | /// 227 | /// # Returns 228 | /// 229 | /// An `HttpResponse` with the SSE stream 230 | pub async fn sse_handler( 231 | req: actix_web::HttpRequest, 232 | transport: web::Data, 233 | ) -> HttpResponse { 234 | let client_ip = req 235 | .peer_addr() 236 | .map(|addr| addr.ip().to_string()) 237 | .unwrap_or_else(|| "unknown".to_string()); 238 | tracing::info!("New SSE connection request from {}", client_ip); 239 | 240 | // Create new session 241 | let session_id = Uuid::new_v4().to_string(); 242 | 243 | transport.create_session(session_id.clone()).await; 244 | 245 | tracing::info!( 246 | "SSE connection established for {} with session_id {}", 247 | client_ip, 248 | session_id 249 | ); 250 | 251 | // Create initial endpoint info event 252 | let endpoint_info = format!( 253 | "event: endpoint\ndata: /message?sessionId={}\n\n", 254 | session_id 255 | ); 256 | 257 | // Spawn a task to handle ping notifications separately 258 | let transport_ping = transport.clone(); 259 | let session_id_ping = session_id.clone(); 260 | tokio::spawn(async move { 261 | loop { 262 | tokio::time::sleep(Duration::from_secs(15)).await; 263 | if let Some(session) = transport_ping.get_session(&session_id_ping).await { 264 | if let Err(e) = session.send_notification("ping", None).await { 265 | tracing::error!( 266 | "Failed to send ping to session {}: {:?}", 267 | session_id_ping, 268 | e 269 | ); 270 | } 271 | } else { 272 | break; 273 | } 274 | } 275 | }); 276 | 277 | let stream = futures::stream::once(async move { 278 | Ok::<_, std::convert::Infallible>(web::Bytes::from(endpoint_info)) 279 | }) 280 | .chain(futures::stream::unfold( 281 | (transport.clone(), session_id.clone(), client_ip.clone()), 282 | move |state| async move { 283 | let (transport, session_id, client_ip) = state; 284 | let session = transport.get_session(&session_id).await; 285 | 286 | if let Some(session) = session { 287 | match session.poll_message().await { 288 | Ok(Some(msg)) => { 289 | tracing::debug!("Sending SSE message to Session {}: {:?}", session_id, msg); 290 | let json = serde_json::to_string(&msg).unwrap(); 291 | let sse_data = format!("event: message\ndata: {}\n\n", json); 292 | let response = 293 | Ok::<_, std::convert::Infallible>(web::Bytes::from(sse_data)); 294 | Some((response, (transport, session_id, client_ip))) 295 | } 296 | Ok(None) => None, 297 | Err(e) => { 298 | tracing::error!("Error polling message for Session {}: {:?}", client_ip, e); 299 | None 300 | } 301 | } 302 | } else { 303 | tracing::warn!("Session {} not found, closing stream", session_id); 304 | None 305 | } 306 | }, 307 | )); 308 | 309 | HttpResponse::Ok() 310 | .append_header(("X-Session-Id", session_id)) 311 | .content_type("text/event-stream") 312 | .streaming(stream) 313 | } 314 | 315 | /// Query parameters for message handling. 316 | #[derive(Deserialize)] 317 | pub struct MessageQuery { 318 | /// The session ID that identifies the client 319 | #[serde(rename = "sessionId")] 320 | session_id: Option, 321 | } 322 | 323 | /// Handles incoming messages from clients. 324 | /// 325 | /// This function: 326 | /// 1. Extracts the session ID from the query parameters 327 | /// 2. Retrieves the session 328 | /// 3. Passes the message to the protocol for processing 329 | /// 4. Returns a response to the client 330 | /// 331 | /// # Arguments 332 | /// 333 | /// * `query` - The query parameters containing the session ID 334 | /// * `message` - The JSON-RPC message 335 | /// * `transport` - The `ServerSseTransport` instance 336 | /// 337 | /// # Returns 338 | /// 339 | /// An `HttpResponse` with the operation result 340 | pub async fn message_handler( 341 | query: Query, 342 | message: web::Json, 343 | transport: web::Data, 344 | ) -> HttpResponse { 345 | if let Some(session_id) = &query.session_id { 346 | let sessions = transport.sessions.lock().await; 347 | if let Some(transport) = sessions.get(session_id) { 348 | match message.into_inner() { 349 | JsonRpcMessage::Request(request) => { 350 | tracing::debug!( 351 | "Received request from session {}: {:?}", 352 | session_id, 353 | request 354 | ); 355 | let response = transport.protocol.handle_request(request).await; 356 | match transport 357 | .send_response(response.id, response.result, response.error) 358 | .await 359 | { 360 | Ok(_) => { 361 | tracing::debug!("Successfully sent message to session {}", session_id); 362 | HttpResponse::Accepted().finish() 363 | } 364 | Err(e) => { 365 | tracing::error!( 366 | "Failed to send message to session {}: {:?}", 367 | session_id, 368 | e 369 | ); 370 | HttpResponse::InternalServerError().finish() 371 | } 372 | } 373 | } 374 | JsonRpcMessage::Response(response) => { 375 | tracing::debug!( 376 | "Received response from session {}: {:?}", 377 | session_id, 378 | response 379 | ); 380 | transport.protocol.handle_response(response).await; 381 | HttpResponse::Accepted().finish() 382 | } 383 | JsonRpcMessage::Notification(notification) => { 384 | tracing::debug!( 385 | "Received notification from session {}: {:?}", 386 | session_id, 387 | notification 388 | ); 389 | transport.protocol.handle_notification(notification).await; 390 | HttpResponse::Accepted().finish() 391 | } 392 | } 393 | } else { 394 | HttpResponse::NotFound().body(format!("Session {} not found", session_id)) 395 | } 396 | } else { 397 | HttpResponse::BadRequest().body("Session ID not specified") 398 | } 399 | } 400 | 401 | /// Represents a client session in the SSE transport. 402 | /// 403 | /// Each `ServerSseTransportSession` handles communication with a specific client, 404 | /// processing incoming messages and sending outgoing messages. 405 | #[derive(Clone)] 406 | pub struct ServerSseTransportSession { 407 | protocol: Protocol, 408 | rx: Arc>>, 409 | tx: mpsc::Sender, 410 | } 411 | 412 | #[async_trait()] 413 | impl Transport for ServerSseTransportSession { 414 | async fn open(&self) -> Result<()> { 415 | Ok(()) 416 | } 417 | 418 | async fn close(&self) -> Result<()> { 419 | Ok(()) 420 | } 421 | 422 | async fn poll_message(&self) -> Result> { 423 | let mut rx = self.rx.lock().await; 424 | match rx.recv().await { 425 | Some(message) => { 426 | tracing::debug!("Received message from SSE: {:?}", message); 427 | Ok(Some(message)) 428 | } 429 | None => Ok(None), 430 | } 431 | } 432 | 433 | fn request( 434 | &self, 435 | method: &str, 436 | params: Option, 437 | options: RequestOptions, 438 | ) -> Pin> + Send + Sync>> { 439 | let protocol = self.protocol.clone(); 440 | let tx = self.tx.clone(); 441 | 442 | let method = method.to_owned(); 443 | let params = params.clone(); 444 | 445 | Box::pin(async move { 446 | let (id, rx) = protocol.create_request().await; 447 | let message = JsonRpcMessage::Request(JsonRpcRequest { 448 | id, 449 | method: method.clone(), 450 | jsonrpc: Default::default(), 451 | params, 452 | }); 453 | 454 | if let Err(e) = tx.send(message).await { 455 | return Ok(JsonRpcResponse { 456 | id, 457 | result: None, 458 | error: Some(JsonRpcError { 459 | code: ErrorCode::InternalError as i32, 460 | message: format!("Failed to send request: {}", e), 461 | data: None, 462 | }), 463 | ..Default::default() 464 | }); 465 | } 466 | 467 | let result = timeout(options.timeout, rx).await; 468 | match result { 469 | Ok(inner_result) => match inner_result { 470 | Ok(response) => Ok(response), 471 | Err(_) => { 472 | protocol.cancel_response(id).await; 473 | Ok(JsonRpcResponse { 474 | id, 475 | result: None, 476 | error: Some(JsonRpcError { 477 | code: ErrorCode::RequestTimeout as i32, 478 | message: "Request cancelled".to_string(), 479 | data: None, 480 | }), 481 | ..Default::default() 482 | }) 483 | } 484 | }, 485 | Err(_) => { 486 | protocol.cancel_response(id).await; 487 | Ok(JsonRpcResponse { 488 | id, 489 | result: None, 490 | error: Some(JsonRpcError { 491 | code: ErrorCode::RequestTimeout as i32, 492 | message: "Request cancelled".to_string(), 493 | data: None, 494 | }), 495 | ..Default::default() 496 | }) 497 | } 498 | } 499 | }) 500 | } 501 | 502 | async fn send_notification( 503 | &self, 504 | method: &str, 505 | params: Option, 506 | ) -> Result<()> { 507 | let message = JsonRpcMessage::Notification(JsonRpcNotification { 508 | method: method.to_owned(), 509 | params, 510 | jsonrpc: Default::default(), 511 | }); 512 | self.tx 513 | .send(message) 514 | .await 515 | .map_err(|e| anyhow::anyhow!("Send notification error: {:?}", e)) 516 | } 517 | 518 | async fn send_response( 519 | &self, 520 | id: RequestId, 521 | result: Option, 522 | error: Option, 523 | ) -> Result<()> { 524 | let message = JsonRpcMessage::Response(JsonRpcResponse { 525 | id, 526 | result, 527 | error, 528 | jsonrpc: Default::default(), 529 | }); 530 | self.tx 531 | .send(message) 532 | .await 533 | .map_err(|e| anyhow::anyhow!("Send response error: {:?}", e)) 534 | } 535 | } 536 | -------------------------------------------------------------------------------- /mcp-core/src/transport/server/stdio.rs: -------------------------------------------------------------------------------- 1 | use crate::protocol::{Protocol, RequestOptions}; 2 | use crate::transport::{ 3 | JsonRpcError, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, Message, RequestId, 4 | Transport, 5 | }; 6 | use crate::types::ErrorCode; 7 | use anyhow::Result; 8 | use async_trait::async_trait; 9 | use std::future::Future; 10 | use std::io::{self, BufRead, Write}; 11 | use std::pin::Pin; 12 | use tokio::time::timeout; 13 | use tracing::debug; 14 | 15 | /// Server transport that communicates with MCP clients over standard I/O. 16 | /// 17 | /// The `ServerStdioTransport` uses standard input and output streams (stdin/stdout) 18 | /// to send and receive MCP messages. This transport is ideal for command-line 19 | /// applications, where the server needs to communicate with a client that launched 20 | /// it as a child process. 21 | /// 22 | /// Use cases include: 23 | /// - CLI tools that implement MCP 24 | /// - Embedding MCP in existing command-line applications 25 | /// - Testing and development scenarios 26 | /// 27 | /// # Example 28 | /// 29 | /// ``` 30 | /// use mcp_core::{protocol::Protocol, transport::{ServerStdioTransport, Transport}}; 31 | /// 32 | /// async fn example() { 33 | /// let protocol = Protocol::builder().build(); 34 | /// let transport = ServerStdioTransport::new(protocol); 35 | /// // Start handling messages 36 | /// transport.open().await.expect("Failed to start stdio server"); 37 | /// } 38 | /// ``` 39 | #[derive(Clone)] 40 | pub struct ServerStdioTransport { 41 | protocol: Protocol, 42 | } 43 | 44 | impl ServerStdioTransport { 45 | /// Creates a new `ServerStdioTransport` instance. 46 | /// 47 | /// # Arguments 48 | /// 49 | /// * `protocol` - The MCP protocol instance to use for handling messages 50 | /// 51 | /// # Returns 52 | /// 53 | /// A new `ServerStdioTransport` instance 54 | pub fn new(protocol: Protocol) -> Self { 55 | Self { protocol } 56 | } 57 | } 58 | 59 | #[async_trait()] 60 | impl Transport for ServerStdioTransport { 61 | /// Opens the transport and starts processing messages. 62 | /// 63 | /// This method enters a loop that: 64 | /// 1. Polls for incoming messages from stdin 65 | /// 2. Processes each message according to its type (request, notification, response) 66 | /// 3. Sends responses as needed 67 | /// 4. Continues until EOF is received on stdin 68 | /// 69 | /// # Returns 70 | /// 71 | /// A `Result` indicating success or failure 72 | async fn open(&self) -> Result<()> { 73 | loop { 74 | match self.poll_message().await { 75 | Ok(Some(message)) => match message { 76 | Message::Request(request) => { 77 | let response = self.protocol.handle_request(request).await; 78 | self.send_response(response.id, response.result, response.error) 79 | .await?; 80 | } 81 | Message::Notification(notification) => { 82 | self.protocol.handle_notification(notification).await; 83 | } 84 | Message::Response(response) => { 85 | self.protocol.handle_response(response).await; 86 | } 87 | }, 88 | Ok(None) => { 89 | break; 90 | } 91 | Err(e) => { 92 | tracing::error!("Error receiving message: {:?}", e); 93 | } 94 | } 95 | } 96 | Ok(()) 97 | } 98 | 99 | /// Closes the transport. 100 | /// 101 | /// This is a no-op for the stdio transport as standard I/O streams are managed by the OS. 102 | /// 103 | /// # Returns 104 | /// 105 | /// A `Result` indicating success 106 | async fn close(&self) -> Result<()> { 107 | Ok(()) 108 | } 109 | 110 | /// Polls for incoming messages from stdin. 111 | /// 112 | /// This method reads a line from stdin and parses it as a JSON-RPC message. 113 | /// 114 | /// # Returns 115 | /// 116 | /// A `Result` containing an `Option`. `None` indicates EOF. 117 | async fn poll_message(&self) -> Result> { 118 | let stdin = io::stdin(); 119 | let mut reader = stdin.lock(); 120 | let mut line = String::new(); 121 | reader.read_line(&mut line)?; 122 | if line.is_empty() { 123 | return Ok(None); 124 | } 125 | 126 | debug!("Received: {line}"); 127 | let message: Message = serde_json::from_str(&line)?; 128 | Ok(Some(message)) 129 | } 130 | 131 | /// Sends a request to the client and waits for a response. 132 | /// 133 | /// This method: 134 | /// 1. Creates a new request ID 135 | /// 2. Constructs a JSON-RPC request 136 | /// 3. Sends it to stdout 137 | /// 4. Waits for a response with the same ID, with a timeout 138 | /// 139 | /// # Arguments 140 | /// 141 | /// * `method` - The method name for the request 142 | /// * `params` - Optional parameters for the request 143 | /// * `options` - Request options (like timeout) 144 | /// 145 | /// # Returns 146 | /// 147 | /// A `Future` that resolves to a `Result` containing the response 148 | fn request( 149 | &self, 150 | method: &str, 151 | params: Option, 152 | options: RequestOptions, 153 | ) -> Pin> + Send + Sync>> { 154 | let protocol = self.protocol.clone(); 155 | let method = method.to_owned(); 156 | Box::pin(async move { 157 | let (id, rx) = protocol.create_request().await; 158 | let request = JsonRpcRequest { 159 | id, 160 | method, 161 | jsonrpc: Default::default(), 162 | params, 163 | }; 164 | let serialized = serde_json::to_string(&request).unwrap_or_default(); 165 | debug!("Sending: {serialized}"); 166 | 167 | // Use Tokio's async stdout to perform thread-safe, nonblocking writes. 168 | let mut stdout = io::stdout(); 169 | stdout.write_all(serialized.as_bytes())?; 170 | stdout.write_all(b"\n")?; 171 | stdout.flush()?; 172 | 173 | let result = timeout(options.timeout, rx).await; 174 | match result { 175 | // The request future completed before the timeout. 176 | Ok(inner_result) => match inner_result { 177 | Ok(response) => Ok(response), 178 | Err(_) => { 179 | protocol.cancel_response(id).await; 180 | Ok(JsonRpcResponse { 181 | id, 182 | result: None, 183 | error: Some(JsonRpcError { 184 | code: ErrorCode::RequestTimeout as i32, 185 | message: "Request cancelled".to_string(), 186 | data: None, 187 | }), 188 | ..Default::default() 189 | }) 190 | } 191 | }, 192 | // The timeout expired. 193 | Err(_) => { 194 | protocol.cancel_response(id).await; 195 | Ok(JsonRpcResponse { 196 | id, 197 | result: None, 198 | error: Some(JsonRpcError { 199 | code: ErrorCode::RequestTimeout as i32, 200 | message: "Request cancelled".to_string(), 201 | data: None, 202 | }), 203 | ..Default::default() 204 | }) 205 | } 206 | } 207 | }) 208 | } 209 | 210 | /// Sends a notification to the client. 211 | /// 212 | /// This method constructs a JSON-RPC notification and writes it to stdout. 213 | /// Unlike requests, notifications do not expect a response. 214 | /// 215 | /// # Arguments 216 | /// 217 | /// * `method` - The method name for the notification 218 | /// * `params` - Optional parameters for the notification 219 | /// 220 | /// # Returns 221 | /// 222 | /// A `Result` indicating success or failure 223 | async fn send_notification( 224 | &self, 225 | method: &str, 226 | params: Option, 227 | ) -> Result<()> { 228 | let notification = JsonRpcNotification { 229 | jsonrpc: Default::default(), 230 | method: method.to_owned(), 231 | params, 232 | }; 233 | let serialized = serde_json::to_string(¬ification).unwrap_or_default(); 234 | let stdout = io::stdout(); 235 | let mut writer = stdout.lock(); 236 | debug!("Sending: {serialized}"); 237 | writer.write_all(serialized.as_bytes())?; 238 | writer.write_all(b"\n")?; 239 | writer.flush()?; 240 | Ok(()) 241 | } 242 | 243 | /// Sends a response to the client. 244 | /// 245 | /// This method constructs a JSON-RPC response and writes it to stdout. 246 | /// 247 | /// # Arguments 248 | /// 249 | /// * `id` - The ID of the request being responded to 250 | /// * `result` - Optional successful result 251 | /// * `error` - Optional error information 252 | /// 253 | /// # Returns 254 | /// 255 | /// A `Result` indicating success or failure 256 | async fn send_response( 257 | &self, 258 | id: RequestId, 259 | result: Option, 260 | error: Option, 261 | ) -> Result<()> { 262 | let response = JsonRpcResponse { 263 | id, 264 | result, 265 | error, 266 | jsonrpc: Default::default(), 267 | }; 268 | let serialized = serde_json::to_string(&response).unwrap_or_default(); 269 | let stdout = io::stdout(); 270 | let mut writer = stdout.lock(); 271 | debug!("Sending: {serialized}"); 272 | writer.write_all(serialized.as_bytes())?; 273 | writer.write_all(b"\n")?; 274 | writer.flush()?; 275 | Ok(()) 276 | } 277 | } 278 | --------------------------------------------------------------------------------