Skip to content

Commit 1cf1877

Browse files
authored
feat: add sse transport support (#32)
* feat: add sse transport support * chore: fmt * chore: update doc * chore: v * chore: update readme * chore: update docs * update assets * chore: readme
1 parent c2854e1 commit 1cf1877

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+6487
-337
lines changed

Cargo.lock

Lines changed: 2411 additions & 228 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ members = [
88
"examples/simple-mcp-client-core",
99
"examples/hello-world-mcp-server",
1010
"examples/hello-world-mcp-server-core",
11+
"examples/hello-world-server-sse",
12+
"examples/hello-world-server-core-sse",
13+
"examples/simple-mcp-client-sse",
14+
"examples/simple-mcp-client-core-sse",
1115
]
1216

1317
[workspace.dependencies]
@@ -26,13 +30,20 @@ async-trait = { version = "0.1" }
2630
strum = { version = "0.27", features = ["derive"] }
2731
thiserror = { version = "2.0" }
2832
tokio-stream = { version = "0.1" }
33+
uuid = { version = "1" }
2934
tracing = "0.1"
3035
tracing-subscriber = { version = "0.3", features = [
3136
"env-filter",
3237
"std",
3338
"fmt",
3439
] }
3540

41+
axum = "0.8"
42+
rustls = "0.23"
43+
tokio-rustls = "0.26"
44+
axum-server = { version = "0.7" }
45+
reqwest = "0.12"
46+
bytes = "1.10.1"
3647

3748
# [workspace.dependencies.windows]
3849

Makefile.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ args = ["fmt", "--all", "--", "--check"]
88

99
[tasks.clippy]
1010
command = "cargo"
11-
args = ["clippy"]
11+
args = ["clippy", "--workspace", "--all-targets", "--all-features"]
1212

1313
[tasks.test]
1414
install_crate = "nextest"

README.md

Lines changed: 141 additions & 28 deletions
Large diffs are not rendered by default.
504 KB
Loading
283 KB
Loading

crates/rust-mcp-sdk/Cargo.toml

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,35 @@ async-trait = { workspace = true }
2222
futures = { workspace = true }
2323
thiserror = { workspace = true }
2424

25+
axum = { workspace = true, optional = true }
26+
uuid = { workspace = true, features = ["v4"], optional = true }
27+
tokio-stream = { workspace = true, optional = true }
28+
axum-server = { version = "0.7", features = [], optional = true }
29+
tracing.workspace = true
30+
31+
# rustls = { workspace = true, optional = true }
32+
hyper = { version = "1.6.0" }
33+
34+
[dev-dependencies]
35+
tracing-subscriber = { workspace = true, features = [
36+
"env-filter",
37+
"std",
38+
"fmt",
39+
] }
40+
2541
[features]
26-
default = ["client", "server", "macros"] # All features enabled by default
27-
server = [] # Server feature
28-
client = [] # Client feature
42+
default = [
43+
"client",
44+
"server",
45+
"macros",
46+
"hyper-server",
47+
"ssl",
48+
] # All features enabled by default
49+
server = [] # Server feature
50+
client = [] # Client feature
51+
hyper-server = ["axum", "axum-server", "uuid", "tokio-stream"]
52+
ssl = ["axum-server/tls-rustls"]
2953
macros = ["rust-mcp-macros"]
3054

31-
3255
[lints]
3356
workspace = true

crates/rust-mcp-sdk/README.md

Lines changed: 142 additions & 30 deletions
Large diffs are not rendered by default.
Loading
Loading

crates/rust-mcp-sdk/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ use rust_mcp_schema::RpcError;
22
use rust_mcp_transport::error::TransportError;
33
use thiserror::Error;
44

5+
#[cfg(feature = "hyper-server")]
6+
use crate::hyper_servers::error::TransportServerError;
7+
58
pub type SdkResult<T> = core::result::Result<T, McpSdkError>;
69

710
#[derive(Debug, Error)]
@@ -18,6 +21,9 @@ pub enum McpSdkError {
1821
AnyError(Box<(dyn std::error::Error + Send + Sync)>),
1922
#[error("{0}")]
2023
SdkError(#[from] rust_mcp_schema::schema_utils::SdkError),
24+
#[cfg(feature = "hyper-server")]
25+
#[error("{0}")]
26+
TransportServerError(#[from] TransportServerError),
2127
}
2228

2329
#[deprecated(since = "0.2.0", note = "Use `McpSdkError` instead.")]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
mod app_state;
2+
pub mod error;
3+
pub mod hyper_server;
4+
pub mod hyper_server_core;
5+
mod routes;
6+
mod server;
7+
mod session_store;
8+
9+
pub use server::*;
10+
pub use session_store::*;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use std::{sync::Arc, time::Duration};
2+
3+
use rust_mcp_schema::InitializeResult;
4+
use rust_mcp_transport::TransportOptions;
5+
6+
use crate::mcp_traits::mcp_handler::McpServerHandler;
7+
8+
use super::{session_store::SessionStore, IdGenerator};
9+
10+
/// Application state struct for the Hyper server
11+
///
12+
/// Holds shared, thread-safe references to session storage, ID generator,
13+
/// server details, handler, ping interval, and transport options.
14+
#[derive(Clone)]
15+
pub struct AppState {
16+
pub session_store: Arc<dyn SessionStore>,
17+
pub id_generator: Arc<dyn IdGenerator>,
18+
pub server_details: Arc<InitializeResult>,
19+
pub handler: Arc<dyn McpServerHandler>,
20+
pub ping_interval: Duration,
21+
pub transport_options: Arc<TransportOptions>,
22+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use std::net::AddrParseError;
2+
3+
use axum::{http::StatusCode, response::IntoResponse};
4+
use thiserror::Error;
5+
6+
pub type TransportServerResult<T> = core::result::Result<T, TransportServerError>;
7+
8+
#[derive(Debug, Error, Clone)]
9+
pub enum TransportServerError {
10+
#[error("'sessionId' query string is missing!")]
11+
SessionIdMissing,
12+
#[error("No session found for the given ID: {0}.")]
13+
SessionIdInvalid(String),
14+
#[error("Stream IO Error: {0}.")]
15+
StreamIoError(String),
16+
#[error("{0}")]
17+
AddrParseError(#[from] AddrParseError),
18+
#[error("Server start error: {0}")]
19+
ServerStartError(String),
20+
#[error("Invalid options: {0}")]
21+
InvalidServerOptions(String),
22+
#[error("{0}")]
23+
SslCertError(String),
24+
}
25+
26+
impl IntoResponse for TransportServerError {
27+
//consume self and returns a Response
28+
fn into_response(self) -> axum::response::Response {
29+
let mut response = StatusCode::INTERNAL_SERVER_ERROR.into_response();
30+
response.extensions_mut().insert(self);
31+
response
32+
}
33+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use std::sync::Arc;
2+
3+
use rust_mcp_schema::InitializeResult;
4+
5+
use crate::mcp_server::{server_runtime::ServerRuntimeInternalHandler, ServerHandler};
6+
7+
use super::{HyperServer, HyperServerOptions};
8+
9+
/// Creates a new HyperServer instance with the provided handler and options
10+
/// The handler must implement ServerHandler.
11+
///
12+
/// # Arguments
13+
/// * `server_details` - Initialization result from the MCP schema
14+
/// * `handler` - Implementation of the ServerHandlerCore trait
15+
/// * `server_options` - Configuration options for the HyperServer
16+
///
17+
/// # Returns
18+
/// * `HyperServer` - A configured HyperServer instance ready to start
19+
pub fn create_server(
20+
server_details: InitializeResult,
21+
handler: impl ServerHandler,
22+
server_options: HyperServerOptions,
23+
) -> HyperServer {
24+
HyperServer::new(
25+
server_details,
26+
Arc::new(ServerRuntimeInternalHandler::new(Box::new(handler))),
27+
server_options,
28+
)
29+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use super::{HyperServer, HyperServerOptions};
2+
use crate::mcp_server::{server_runtime_core::RuntimeCoreInternalHandler, ServerHandlerCore};
3+
use rust_mcp_schema::InitializeResult;
4+
use std::sync::Arc;
5+
6+
/// Creates a new HyperServer instance with the provided handler and options
7+
/// The handler must implement ServerHandlerCore.
8+
///
9+
/// # Arguments
10+
/// * `server_details` - Initialization result from the MCP schema
11+
/// * `handler` - Implementation of the ServerHandlerCore trait
12+
/// * `server_options` - Configuration options for the HyperServer
13+
///
14+
/// # Returns
15+
/// * `HyperServer` - A configured HyperServer instance ready to start
16+
pub fn create_server(
17+
server_details: InitializeResult,
18+
handler: impl ServerHandlerCore,
19+
server_options: HyperServerOptions,
20+
) -> HyperServer {
21+
HyperServer::new(
22+
server_details,
23+
Arc::new(RuntimeCoreInternalHandler::new(Box::new(handler))),
24+
server_options,
25+
)
26+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
pub mod fallback_routes;
2+
pub mod messages_routes;
3+
pub mod sse_routes;
4+
5+
use super::{app_state::AppState, HyperServerOptions};
6+
use axum::Router;
7+
use std::sync::Arc;
8+
9+
/// Constructs the Axum router with all application routes
10+
///
11+
/// Combines routes for Server-Sent Events, message handling, and fallback routes,
12+
/// attaching the shared application state to the router.
13+
///
14+
/// # Arguments
15+
/// * `state` - Shared application state wrapped in an Arc
16+
/// * `server_options` - Reference to the HyperServer configuration options
17+
///
18+
/// # Returns
19+
/// * `Router` - An Axum router configured with all application routes and state
20+
pub fn app_routes(state: Arc<AppState>, server_options: &HyperServerOptions) -> Router {
21+
Router::new()
22+
.merge(sse_routes::routes(
23+
state.clone(),
24+
server_options.sse_endpoint(),
25+
))
26+
.merge(messages_routes::routes(state.clone()))
27+
.with_state(state)
28+
.merge(fallback_routes::routes())
29+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use axum::{
2+
http::{StatusCode, Uri},
3+
Router,
4+
};
5+
6+
pub fn routes() -> Router {
7+
Router::new().fallback(not_found)
8+
}
9+
10+
pub async fn not_found(uri: Uri) -> (StatusCode, String) {
11+
(
12+
StatusCode::INTERNAL_SERVER_ERROR,
13+
format!("Server Error!\r\n uri: {}", uri),
14+
)
15+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use crate::hyper_servers::{
2+
app_state::AppState,
3+
error::{TransportServerError, TransportServerResult},
4+
};
5+
use axum::{
6+
extract::{Query, State},
7+
response::IntoResponse,
8+
routing::post,
9+
Router,
10+
};
11+
use std::{collections::HashMap, sync::Arc};
12+
use tokio::io::AsyncWriteExt;
13+
14+
const SSE_MESSAGES_PATH: &str = "/messages";
15+
16+
pub fn routes(_state: Arc<AppState>) -> Router<Arc<AppState>> {
17+
Router::new().route(SSE_MESSAGES_PATH, post(handle_messages))
18+
}
19+
20+
pub async fn handle_messages(
21+
State(state): State<Arc<AppState>>,
22+
Query(params): Query<HashMap<String, String>>,
23+
message: String,
24+
) -> TransportServerResult<impl IntoResponse> {
25+
let session_id = params
26+
.get("sessionId")
27+
.ok_or(TransportServerError::SessionIdMissing)?;
28+
29+
let transmit =
30+
state
31+
.session_store
32+
.get(session_id)
33+
.await
34+
.ok_or(TransportServerError::SessionIdInvalid(
35+
session_id.to_string(),
36+
))?;
37+
let mut transmit = transmit.lock().await;
38+
39+
transmit
40+
.write_all(format!("{message}\n").as_bytes())
41+
.await
42+
.map_err(|err| TransportServerError::StreamIoError(err.to_string()))?;
43+
44+
transmit
45+
.flush()
46+
.await
47+
.map_err(|err| TransportServerError::StreamIoError(err.to_string()))?;
48+
49+
Ok(axum::http::StatusCode::OK)
50+
}

0 commit comments

Comments
 (0)