├── .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 |
3 |
4 |
5 |
6 |
7 |
MCP Core
8 |
9 | A Rust library implementing the Modern Context Protocol (MCP)
10 |
11 |
12 |
13 |
14 |
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 | [](https://crates.io/crates/mcp-core-macros)
4 | [](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 |
--------------------------------------------------------------------------------