diff --git a/mcp/http-stream-mcp/Cargo.toml b/mcp/http-stream-mcp/Cargo.toml new file mode 100644 index 00000000..5745248a --- /dev/null +++ b/mcp/http-stream-mcp/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "http-stream-mcp-server" +version = "0.1.0" +edition = "2024" + +[dependencies] +shuttle-axum = "0.57.0" +shuttle-runtime = "0.57.0" +tokio = { version = "1", features = ["full"] } +rmcp = { version = "0.8", features = [ + "server", + "macros", + "transport-streamable-http-server", +] } +axum = "0.8" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +schemars = "1.0" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +anyhow = "1.0" diff --git a/mcp/http-stream-mcp/src/main.rs b/mcp/http-stream-mcp/src/main.rs new file mode 100644 index 00000000..e6d520be --- /dev/null +++ b/mcp/http-stream-mcp/src/main.rs @@ -0,0 +1,185 @@ +use rmcp::{ + ErrorData as McpError, ServerHandler, + handler::server::{router::tool::ToolRouter, wrapper::Parameters}, + model::*, + schemars, tool, tool_handler, tool_router, +}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tokio::sync::Mutex; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Task { + id: usize, + title: String, + description: String, + completed: bool, +} + +#[derive(Debug, Clone)] +struct TaskManager { + tasks: Arc>>, + next_id: Arc>, + tool_router: ToolRouter, +} + +#[derive(Debug, Deserialize, schemars::JsonSchema)] +struct AddTaskRequest { + #[schemars(description = "The title of the task")] + title: String, + #[schemars(description = "A detailed description of the task")] + description: String, +} + +#[derive(Debug, Deserialize, schemars::JsonSchema)] +struct CompleteTaskRequest { + #[schemars(description = "The ID of the task to mark as completed")] + id: usize, +} + +#[derive(Debug, Deserialize, schemars::JsonSchema)] +struct GetTaskRequest { + #[schemars(description = "The ID of the task to retrieve")] + id: usize, +} + +#[tool_router] +impl TaskManager { + fn new() -> Self { + Self { + tasks: Arc::new(Mutex::new(Vec::new())), + next_id: Arc::new(Mutex::new(1)), + tool_router: Self::tool_router(), + } + } + + #[tool(description = "Add a new task to the task manager")] + async fn add_task( + &self, + Parameters(AddTaskRequest { title, description }): Parameters, + ) -> Result { + let mut tasks = self.tasks.lock().await; + let mut next_id = self.next_id.lock().await; + + let task = Task { + id: *next_id, + title, + description, + completed: false, + }; + + *next_id += 1; + tasks.push(task.clone()); + + let response = serde_json::json!({ + "success": true, + "task": task, + "message": format!("Task '{}' added successfully with ID {}", task.title, task.id) + }); + + Ok(CallToolResult::success(vec![Content::text( + serde_json::to_string_pretty(&response).unwrap(), + )])) + } + + #[tool(description = "Mark a task as completed")] + async fn complete_task( + &self, + Parameters(CompleteTaskRequest { id }): Parameters, + ) -> Result { + let mut tasks = self.tasks.lock().await; + + if let Some(task) = tasks.iter_mut().find(|t| t.id == id) { + task.completed = true; + let response = serde_json::json!({ + "success": true, + "task": task, + "message": format!("Task '{}' marked as completed", task.title) + }); + + Ok(CallToolResult::success(vec![Content::text( + serde_json::to_string_pretty(&response).unwrap(), + )])) + } else { + Err(McpError::invalid_params( + format!("Task with ID {} not found", id), + None, + )) + } + } + + #[tool(description = "List all tasks in the task manager")] + async fn list_tasks(&self) -> Result { + let tasks = self.tasks.lock().await; + + let response = serde_json::json!({ + "total": tasks.len(), + "tasks": tasks.clone() + }); + + Ok(CallToolResult::success(vec![Content::text( + serde_json::to_string_pretty(&response).unwrap(), + )])) + } + + #[tool(description = "Get a specific task by ID")] + async fn get_task( + &self, + Parameters(GetTaskRequest { id }): Parameters, + ) -> Result { + let tasks = self.tasks.lock().await; + + if let Some(task) = tasks.iter().find(|t| t.id == id) { + let response = serde_json::json!({ + "success": true, + "task": task + }); + + Ok(CallToolResult::success(vec![Content::text( + serde_json::to_string_pretty(&response).unwrap(), + )])) + } else { + Err(McpError::invalid_params( + format!("Task with ID {} not found", id), + None, + )) + } + } +} + +#[tool_handler] +impl ServerHandler for TaskManager { + fn get_info(&self) -> ServerInfo { + ServerInfo { + protocol_version: ProtocolVersion::V_2024_11_05, + capabilities: ServerCapabilities::builder().enable_tools().build(), + server_info: Implementation { + name: "task-manager".to_string(), + version: "0.1.0".to_string(), + title: None, + website_url: None, + icons: None, + }, + instructions: Some( + "A task manager MCP server that allows you to add, complete, list, and retrieve tasks with real-time updates." + .to_string(), + ), + } + } +} + +#[shuttle_runtime::main] +async fn main() -> shuttle_axum::ShuttleAxum { + tracing::info!("Starting Task Manager MCP Server"); + + let service = rmcp::transport::streamable_http_server::StreamableHttpService::new( + || Ok(TaskManager::new()), + rmcp::transport::streamable_http_server::session::local::LocalSessionManager::default() + .into(), + Default::default(), + ); + + let router = axum::Router::new().nest_service("/mcp", service); + + Ok(router.into()) +} diff --git a/templates.toml b/templates.toml index b8a3c74d..126d5cb5 100644 --- a/templates.toml +++ b/templates.toml @@ -390,6 +390,13 @@ path = "mcp/mcp-sse-oauth" use_cases = ["MCP", "AI", "AI Agents"] tags = ["axum", "mcp", "sse", "oauth"] +[templates.mcp-http-stream] +title = "Streamable HTTP MCP Server" +description = "Model Context Protocol server with HTTP streaming transport" +path = "mcp/http-stream-mcp" +use_cases = ["MCP", "AI", "AI Agents"] +tags = ["axum", "mcp", "http-stream"] + ## EXAMPLES ## [examples.metadata]