diff --git a/Cargo.toml b/Cargo.toml index 48d967f7..9b56126d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,11 @@ indexmap = "2.0.0" reqwest = { version = "0.12.2", default-features = false } darling = "0.20.10" http = "1.0.0" +async-stream = "0.3.6" +tokio-util = "0.7.14" +rand = "0.9.0" +time = "0.3.39" +schemars = "0.8.22" # rustls, update together rustls = "0.23" diff --git a/poem-mcpserver-macros/Cargo.toml b/poem-mcpserver-macros/Cargo.toml index 1c6b86ca..e8acab02 100644 --- a/poem-mcpserver-macros/Cargo.toml +++ b/poem-mcpserver-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-mcpserver-macros" -version = "0.1.2" +version = "0.1.3" authors.workspace = true edition.workspace = true license.workspace = true @@ -14,8 +14,8 @@ description = "Macros for poem-mcpserver" proc-macro = true [dependencies] -darling = "0.20.10" -proc-macro-crate = "3.3.0" -proc-macro2 = "1.0.94" -quote = "1.0.40" -syn = "2.0.100" +darling.workspace = true +proc-macro-crate.workspace = true +proc-macro2.workspace = true +quote.workspace = true +syn.workspace = true diff --git a/poem-mcpserver-macros/src/tools.rs b/poem-mcpserver-macros/src/tools.rs index 70cb138d..c5d0a4af 100644 --- a/poem-mcpserver-macros/src/tools.rs +++ b/poem-mcpserver-macros/src/tools.rs @@ -36,7 +36,7 @@ pub(crate) fn generate(_args: ToolsArgs, mut item_impl: ItemImpl) -> Result name.clone(), None => method.sig.ident.to_string(), }; - let tool_description = get_description(&method.attrs); + let tool_description = get_description(&method.attrs).unwrap_or_default(); if method.sig.asyncness.is_none() { return Err(Error::custom("must be asynchronous").with_span(&method.sig.ident)); diff --git a/poem-mcpserver/Cargo.toml b/poem-mcpserver/Cargo.toml index b0368e1f..46706f17 100644 --- a/poem-mcpserver/Cargo.toml +++ b/poem-mcpserver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-mcpserver" -version = "0.1.2" +version = "0.1.3" authors.workspace = true edition.workspace = true license.workspace = true @@ -24,12 +24,13 @@ sse = ["poem"] [dependencies] poem-mcpserver-macros.workspace = true -schemars = "0.8.22" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" -time = { version = "0.3.39", features = ["macros", "formatting", "parsing"] } -tokio = { version = "1.44.1", features = ["io-std", "io-util"] } -poem = { version = "3.1.7", features = ["sse"], optional = true } -rand = "0.9.0" -tokio-util = { version = "0.7.14", features = ["io"] } -async-stream = "0.3.6" +schemars.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +time = { workspace = true, features = ["macros", "formatting", "parsing"] } +tokio = { workspace = true, features = ["io-std", "io-util"] } +poem = { workspace = true, features = ["sse"], optional = true } +rand.workspace = true +tokio-util = { workspace = true, features = ["io"] } +async-stream.workspace = true +tracing.workspace = true diff --git a/poem-mcpserver/src/sse.rs b/poem-mcpserver/src/sse.rs index aa538115..7f5084f2 100644 --- a/poem-mcpserver/src/sse.rs +++ b/poem-mcpserver/src/sse.rs @@ -41,11 +41,9 @@ where { let connections = data.connections.lock().await; let Some(sender) = connections.get(&session_id) else { - return StatusCode::BAD_REQUEST; + return StatusCode::NOT_FOUND; }; - if sender.send(request.0).await.is_err() { - return StatusCode::BAD_REQUEST; - } + _ = sender.send(request.0).await; StatusCode::OK } @@ -60,12 +58,15 @@ where let mut connections = data.connections.lock().await; let (tx, mut rx) = tokio::sync::mpsc::channel(32); + tracing::info!(session_id = session_id, "new mcp connection"); connections.insert(session_id.clone(), tx); SSE::new(async_stream::stream! { yield Event::message(format!("?session_id={}", session_id)).event_type("endpoint"); while let Some(req) = rx.recv().await { + tracing::info!(session_id = session_id, request = ?req, "received request"); if let Some(resp) = server.handle_request(req).await { + tracing::info!(session_id = session_id, response = ?resp, "sending response"); yield Event::message(serde_json::to_string(&resp).unwrap()).event_type("message"); } } @@ -77,7 +78,7 @@ where pub fn sse_endpoint(server_factory: F) -> impl IntoEndpoint where F: Fn() -> McpServer + Send + Sync + 'static, - ToolsType: Tools + 'static, + ToolsType: Tools + Send + Sync + 'static, { get(events_handler::::default()) .post(post_handler::::default()) diff --git a/poem-mcpserver/src/stdio.rs b/poem-mcpserver/src/stdio.rs index 99a944c9..1ac5a919 100644 --- a/poem-mcpserver/src/stdio.rs +++ b/poem-mcpserver/src/stdio.rs @@ -25,6 +25,8 @@ where let mut input = BufReader::new(tokio::io::stdin()).lines(); while let Some(line) = input.next_line().await? { + tracing::info!(request = &line, "received request"); + let Ok(request) = serde_json::from_str::(&line) else { continue; }; @@ -42,6 +44,7 @@ where } if let Some(resp) = server.handle_request(request).await { + tracing::info!(response = ?resp, "sending response"); print_response(resp); } } diff --git a/poem-mcpserver/src/tool.rs b/poem-mcpserver/src/tool.rs index e5908d21..f16fdb8e 100644 --- a/poem-mcpserver/src/tool.rs +++ b/poem-mcpserver/src/tool.rs @@ -2,6 +2,7 @@ use std::{fmt::Display, future::Future}; +use serde::Serialize; use serde_json::Value; use crate::protocol::{ @@ -9,25 +10,67 @@ use crate::protocol::{ tool::{Content, Tool as PTool, ToolsCallResponse}, }; -/// Represents the result of a tool call. -pub trait IntoToolResponse { - /// Consumes the object and converts it into a tool response. - fn into_tool_response(self) -> ToolsCallResponse; +/// Represents a type that can be converted into a content. +pub trait IntoContent { + /// Consumes the object and converts it into a content. + fn into_content(self) -> Vec; } /// A Text response. #[derive(Debug, Clone, Copy)] pub struct Text(pub T); -impl IntoToolResponse for Text +impl IntoContent for Text where T: Display, +{ + fn into_content(self) -> Vec { + vec![Content::Text { + text: self.0.to_string(), + }] + } +} + +/// A Json response. +#[derive(Debug, Clone, Copy)] +pub struct Json(pub T); + +impl IntoContent for Json +where + T: Serialize, +{ + fn into_content(self) -> Vec { + serde_json::to_string(&self.0) + .into_iter() + .map(|text| Content::Text { text }) + .collect() + } +} + +impl IntoContent for Vec +where + T: IntoContent, +{ + fn into_content(self) -> Vec { + self.into_iter() + .flat_map(IntoContent::into_content) + .collect() + } +} + +/// Represents the result of a tool call. +pub trait IntoToolResponse { + /// Consumes the object and converts it into a tool response. + fn into_tool_response(self) -> ToolsCallResponse; +} + +impl IntoToolResponse for T +where + T: IntoContent, { fn into_tool_response(self) -> ToolsCallResponse { ToolsCallResponse { - content: vec![Content::Text { - text: self.0.to_string(), - }], + content: self.into_content(), is_error: false, } } @@ -35,12 +78,15 @@ where impl IntoToolResponse for Result where - T: IntoToolResponse, + T: IntoContent, E: Display, { fn into_tool_response(self) -> ToolsCallResponse { match self { - Ok(value) => value.into_tool_response(), + Ok(value) => ToolsCallResponse { + content: value.into_content(), + is_error: false, + }, Err(error) => ToolsCallResponse { content: vec![Content::Text { text: error.to_string(), @@ -52,7 +98,7 @@ where } /// Represents a tools collection. -pub trait Tools: Send + Sync { +pub trait Tools { /// Returns the instructions for the tools. fn instructions() -> &'static str; diff --git a/poem-openapi/Cargo.toml b/poem-openapi/Cargo.toml index e2bc78d1..a46357b5 100644 --- a/poem-openapi/Cargo.toml +++ b/poem-openapi/Cargo.toml @@ -61,7 +61,7 @@ hostname-validator = { version = "1.1.0", optional = true } chrono = { workspace = true, optional = true, default-features = false, features = [ "clock", ] } -time = { version = "0.3.36", optional = true, features = [ +time = { workspace = true, optional = true, features = [ "parsing", "formatting", ] } diff --git a/poem/Cargo.toml b/poem/Cargo.toml index a4bce3ea..0970c1ac 100644 --- a/poem/Cargo.toml +++ b/poem/Cargo.toml @@ -79,7 +79,7 @@ hyper = { version = "1.0.0", features = ["http1", "http2"] } hyper-util = { version = "0.1.6", features = ["server-auto", "tokio"] } http-body-util = "0.1.0" tokio = { workspace = true, features = ["sync", "time", "macros", "net"] } -tokio-util = { version = "0.7.0", features = ["io"] } +tokio-util = { workspace = true, features = ["io"] } serde.workspace = true sonic-rs = { workspace = true, optional = true } serde_json.workspace = true