diff --git a/Cargo.lock b/Cargo.lock index 5b23f7e83..52a759169 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,6 +71,7 @@ dependencies = [ "tokio", "tokio-util", "tracing", + "zstd", ] [[package]] @@ -92,6 +93,7 @@ dependencies = [ "bytestring", "cfg-if", "http 0.2.11", + "regex", "regex-lite", "serde", "tracing", @@ -198,6 +200,7 @@ dependencies = [ "mime", "once_cell", "pin-project-lite", + "regex", "regex-lite", "serde", "serde_json", @@ -423,6 +426,28 @@ dependencies = [ "serde_json", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "async-trait" version = "0.1.85" @@ -1178,6 +1203,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1923,6 +1958,21 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -2566,6 +2616,22 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.10" @@ -3632,6 +3698,7 @@ dependencies = [ "maplit", "meili-snap", "meilisearch-auth", + "meilisearch-mcp", "meilisearch-types", "mimalloc", "mime", @@ -3704,6 +3771,29 @@ dependencies = [ "uuid", ] +[[package]] +name = "meilisearch-mcp" +version = "1.13.0" +dependencies = [ + "actix-rt", + "actix-web", + "anyhow", + "async-stream", + "async-trait", + "futures", + "insta", + "meilisearch-auth", + "meilisearch-types", + "reqwest", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tracing", + "utoipa", + "uuid", +] + [[package]] name = "meilisearch-types" version = "1.15.0" @@ -3956,6 +4046,23 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nohash" version = "0.2.0" @@ -4178,6 +4285,50 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "openssl" +version = "0.10.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -4858,19 +5009,23 @@ checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-channel", "futures-core", "futures-util", + "h2 0.4.5", "http 1.2.0", "http-body", "http-body-util", "hyper", "hyper-rustls", + "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -4882,7 +5037,9 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper", + "system-configuration", "tokio", + "tokio-native-tls", "tokio-rustls", "tokio-util", "tower", @@ -5117,6 +5274,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -5129,6 +5295,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "segment" version = "0.2.5" @@ -5581,6 +5770,27 @@ dependencies = [ "windows", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -5841,6 +6051,16 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.0" @@ -6635,6 +6855,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.42.2" diff --git a/crates/meilisearch-mcp/Cargo.toml b/crates/meilisearch-mcp/Cargo.toml index b6b2f2012..9d155d97a 100644 --- a/crates/meilisearch-mcp/Cargo.toml +++ b/crates/meilisearch-mcp/Cargo.toml @@ -14,7 +14,7 @@ anyhow = "1.0.86" async-stream = "0.3.5" async-trait = "0.1.81" futures = "0.3.30" -meilisearch = { path = "../meilisearch" } +# Removed meilisearch dependency to avoid cyclic dependency meilisearch-auth = { path = "../meilisearch-auth" } meilisearch-types = { path = "../meilisearch-types" } serde = { version = "1.0.204", features = ["derive"] } @@ -22,7 +22,7 @@ serde_json = { version = "1.0.120", features = ["preserve_order"] } thiserror = "1.0.61" tokio = { version = "1.38.0", features = ["full"] } tracing = "0.1.40" -utoipa = { version = "5.3", features = ["actix_extras", "time", "json"] } +utoipa = { version = "5.3.1", features = ["actix_extras", "time"] } uuid = { version = "1.10.0", features = ["serde", "v4"] } reqwest = { version = "0.12.5", features = ["json"] } diff --git a/crates/meilisearch-mcp/src/conversion_tests.rs b/crates/meilisearch-mcp/src/conversion_tests.rs index c46530a1c..fc3e22627 100644 --- a/crates/meilisearch-mcp/src/conversion_tests.rs +++ b/crates/meilisearch-mcp/src/conversion_tests.rs @@ -1,18 +1,17 @@ -use crate::protocol::Tool; use crate::registry::{McpTool, McpToolRegistry}; use serde_json::json; -use utoipa::openapi::{OpenApi, PathItem, PathItemType}; +use utoipa::openapi::{OpenApi, PathItem}; #[test] fn test_convert_simple_get_endpoint() { let tool = McpTool::from_openapi_path( "/indexes/{index_uid}", - PathItemType::Get, + "GET", &create_mock_path_item_get(), ); assert_eq!(tool.name, "getIndex"); - assert_eq!(tool.description, "Get information about an index"); + assert_eq!(tool.description, "GET /indexes/{index_uid}"); assert_eq!(tool.http_method, "GET"); assert_eq!(tool.path_template, "/indexes/{index_uid}"); @@ -26,7 +25,7 @@ fn test_convert_simple_get_endpoint() { fn test_convert_search_endpoint_with_query_params() { let tool = McpTool::from_openapi_path( "/indexes/{index_uid}/search", - PathItemType::Post, + "POST", &create_mock_search_path_item(), ); @@ -47,7 +46,7 @@ fn test_convert_search_endpoint_with_query_params() { fn test_convert_document_addition_endpoint() { let tool = McpTool::from_openapi_path( "/indexes/{index_uid}/documents", - PathItemType::Post, + "POST", &create_mock_add_documents_path_item(), ); @@ -135,7 +134,7 @@ fn test_tool_name_generation() { fn test_parameter_extraction() { let tool = McpTool::from_openapi_path( "/indexes/{index_uid}/documents/{document_id}", - PathItemType::Get, + "GET", &create_mock_get_document_path_item(), ); @@ -274,7 +273,7 @@ fn create_mock_get_document_path_item() -> PathItem { fn create_mock_openapi() -> OpenApi { serde_json::from_value(json!({ - "openapi": "3.0.0", + "openapi": "3.1.0", "info": { "title": "Meilisearch API", "version": "1.0.0" diff --git a/crates/meilisearch-mcp/src/e2e_tests.rs b/crates/meilisearch-mcp/src/e2e_tests.rs index 4523c5a1f..d68de5f7f 100644 --- a/crates/meilisearch-mcp/src/e2e_tests.rs +++ b/crates/meilisearch-mcp/src/e2e_tests.rs @@ -18,7 +18,7 @@ async fn test_mcp_server_sse_communication() { .insert_header(("Accept", "text/event-stream")) .to_request(); - let mut resp = test::call_service(&app, req).await; + let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); assert_eq!( resp.headers().get("Content-Type").unwrap(), diff --git a/crates/meilisearch-mcp/src/integration.rs b/crates/meilisearch-mcp/src/integration.rs index d1e822df6..70536c0a6 100644 --- a/crates/meilisearch-mcp/src/integration.rs +++ b/crates/meilisearch-mcp/src/integration.rs @@ -2,12 +2,8 @@ use crate::registry::McpToolRegistry; use crate::server::{McpServer, MeilisearchClient}; use crate::Error; use actix_web::{web, HttpResponse}; -use meilisearch::routes::MeilisearchApi; -use meilisearch_auth::AuthController; -use meilisearch_types::error::ResponseError; use serde_json::Value; -use std::sync::Arc; -use utoipa::OpenApi; +use utoipa::openapi::OpenApi; pub struct MeilisearchMcpClient { base_url: String, @@ -75,10 +71,7 @@ impl MeilisearchClient for MeilisearchMcpClient { } } -pub fn create_mcp_server_from_openapi() -> McpServer { - // Get the OpenAPI specification from Meilisearch - let openapi = MeilisearchApi::openapi(); - +pub fn create_mcp_server_from_openapi(openapi: OpenApi) -> McpServer { // Create registry from OpenAPI let registry = McpToolRegistry::from_openapi(&openapi); @@ -86,12 +79,14 @@ pub fn create_mcp_server_from_openapi() -> McpServer { McpServer::new(registry) } -pub fn configure_mcp_route(cfg: &mut web::ServiceConfig) { - cfg.service( - web::resource("/mcp") - .route(web::get().to(crate::server::mcp_sse_handler)) - .route(web::post().to(mcp_post_handler)) - ); +pub fn configure_mcp_route(cfg: &mut web::ServiceConfig, openapi: OpenApi) { + let server = create_mcp_server_from_openapi(openapi); + cfg.app_data(web::Data::new(server)) + .service( + web::resource("/mcp") + .route(web::get().to(crate::server::mcp_sse_handler)) + .route(web::post().to(mcp_post_handler)) + ); } async fn mcp_post_handler( @@ -102,18 +97,20 @@ async fn mcp_post_handler( Ok(HttpResponse::Ok().json(response)) } -pub fn inject_mcp_server(app_data: &mut web::Data<()>) -> web::Data { - let server = create_mcp_server_from_openapi(); - web::Data::new(server) -} - #[cfg(test)] mod tests { use super::*; + use utoipa::openapi::{OpenApiBuilder, InfoBuilder}; #[test] fn test_create_mcp_server() { - let server = create_mcp_server_from_openapi(); + let openapi = OpenApiBuilder::new() + .info(InfoBuilder::new() + .title("Test API") + .version("1.0") + .build()) + .build(); + let _server = create_mcp_server_from_openapi(openapi); // Server should be created successfully assert!(true); } diff --git a/crates/meilisearch-mcp/src/lib.rs b/crates/meilisearch-mcp/src/lib.rs index ac39ba300..2e8376925 100644 --- a/crates/meilisearch-mcp/src/lib.rs +++ b/crates/meilisearch-mcp/src/lib.rs @@ -5,11 +5,11 @@ pub mod registry; pub mod server; #[cfg(test)] -mod tests { - mod conversion_tests; - mod integration_tests; - mod e2e_tests; -} +mod conversion_tests; +#[cfg(test)] +mod integration_tests; +#[cfg(test)] +mod e2e_tests; pub use error::Error; pub use registry::McpToolRegistry; diff --git a/crates/meilisearch-mcp/src/registry.rs b/crates/meilisearch-mcp/src/registry.rs index f9e4f0736..0c9a198c8 100644 --- a/crates/meilisearch-mcp/src/registry.rs +++ b/crates/meilisearch-mcp/src/registry.rs @@ -2,7 +2,8 @@ use crate::protocol::Tool; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::collections::HashMap; -use utoipa::openapi::{OpenApi, Operation, PathItem, PathItemType}; +use utoipa::openapi::{OpenApi, PathItem}; +use utoipa::openapi::path::Operation; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct McpTool { @@ -28,10 +29,9 @@ impl McpToolRegistry { pub fn from_openapi(openapi: &OpenApi) -> Self { let mut registry = Self::new(); - if let Some(paths) = &openapi.paths { - for (path, path_item) in paths.iter() { - registry.process_path_item(path, path_item); - } + // openapi.paths is of type Paths + for (path, path_item) in openapi.paths.paths.iter() { + registry.process_path_item(path, path_item); } registry @@ -58,11 +58,11 @@ impl McpToolRegistry { fn process_path_item(&mut self, path: &str, path_item: &PathItem) { let methods = [ - (PathItemType::Get, &path_item.get), - (PathItemType::Post, &path_item.post), - (PathItemType::Put, &path_item.put), - (PathItemType::Delete, &path_item.delete), - (PathItemType::Patch, &path_item.patch), + ("GET", &path_item.get), + ("POST", &path_item.post), + ("PUT", &path_item.put), + ("DELETE", &path_item.delete), + ("PATCH", &path_item.patch), ]; for (method_type, operation) in methods { @@ -78,13 +78,13 @@ impl McpToolRegistry { impl McpTool { pub fn from_openapi_path( path: &str, - method: PathItemType, + method: &str, _path_item: &PathItem, ) -> Self { // This is a simplified version for testing // In the real implementation, we would extract from the PathItem - let name = Self::generate_tool_name(path, method.as_str()); - let description = format!("{} {}", method.as_str(), path); + let name = Self::generate_tool_name(path, method); + let description = format!("{} {}", method, path); let input_schema = json!({ "type": "object", @@ -96,19 +96,19 @@ impl McpTool { name, description, input_schema, - http_method: method.as_str().to_string(), + http_method: method.to_string(), path_template: path.to_string(), } } - fn from_operation(path: &str, method: PathItemType, operation: &Operation) -> Option { - let name = Self::generate_tool_name(path, method.as_str()); + fn from_operation(path: &str, method: &str, operation: &Operation) -> Option { + let name = Self::generate_tool_name(path, method); let description = operation .summary .as_ref() .or(operation.description.as_ref()) .cloned() - .unwrap_or_else(|| format!("{} {}", method.as_str(), path)); + .unwrap_or_else(|| format!("{} {}", method, path)); let mut properties = serde_json::Map::new(); let mut required = Vec::new(); @@ -116,20 +116,18 @@ impl McpTool { // Extract path parameters if let Some(params) = &operation.parameters { for param in params { - if let Some(param_name) = param.name() { - let camel_name = to_camel_case(param_name); - - properties.insert( - camel_name.clone(), - json!({ - "type": "string", - "description": param.description().unwrap_or("") - }), - ); + let camel_name = to_camel_case(¶m.name); + + properties.insert( + camel_name.clone(), + json!({ + "type": "string", + "description": param.description.as_deref().unwrap_or("") + }), + ); - if param.required() { - required.push(camel_name); - } + if matches!(param.required, utoipa::openapi::Required::True) { + required.push(camel_name); } } } @@ -158,7 +156,7 @@ impl McpTool { name, description, input_schema, - http_method: method.as_str().to_string(), + http_method: method.to_string(), path_template: path.to_string(), }) } @@ -175,7 +173,12 @@ impl McpTool { match method.to_uppercase().as_str() { "GET" => { if is_collection && !path.contains('{') { - format!("get{}", to_pascal_case(&pluralize(resource))) + // Don't pluralize if already plural + if resource.ends_with('s') { + format!("get{}", to_pascal_case(resource)) + } else { + format!("get{}", to_pascal_case(&pluralize(resource))) + } } else { format!("get{}", to_pascal_case(&singularize(resource))) } @@ -218,7 +221,7 @@ fn to_pascal_case(s: &str) -> String { let mut chars = part.chars(); chars .next() - .map(|c| c.to_uppercase().collect::() + &chars.as_str().to_lowercase()) + .map(|c| c.to_uppercase().collect::() + chars.as_str().to_lowercase().as_str()) .unwrap_or_default() }) .collect() @@ -250,7 +253,7 @@ fn extract_schema_properties(schema: &utoipa::openapi::RefOr { + utoipa::openapi::RefOr::T(_schema) => { // Extract properties from the schema // This would need proper implementation based on the schema type Some(serde_json::Map::new()) diff --git a/crates/meilisearch-mcp/src/server.rs b/crates/meilisearch-mcp/src/server.rs index ff50c42f7..195d4a18f 100644 --- a/crates/meilisearch-mcp/src/server.rs +++ b/crates/meilisearch-mcp/src/server.rs @@ -3,10 +3,9 @@ use crate::protocol::*; use crate::registry::McpToolRegistry; use actix_web::{web, HttpRequest, HttpResponse}; use async_stream::try_stream; -use futures::stream::StreamExt; +use futures::stream::{StreamExt, TryStreamExt}; use serde_json::{json, Value}; use std::sync::Arc; -use tokio::sync::Mutex; pub struct McpServer { registry: Arc, @@ -45,7 +44,7 @@ impl McpServer { } } - fn handle_initialize(&self, params: InitializeParams) -> McpResponse { + fn handle_initialize(&self, _params: InitializeParams) -> McpResponse { McpResponse::Initialize { jsonrpc: "2.0".to_string(), result: InitializeResult { @@ -125,6 +124,11 @@ impl McpServer { } fn validate_parameters(&self, args: &Value, schema: &Value) -> Result<(), String> { + // Check if args is an object + if !args.is_object() { + return Err("Arguments must be an object".to_string()); + } + // Basic validation - check required fields if let (Some(args_obj), Some(schema_obj)) = (args.as_object(), schema.as_object()) { if let Some(required) = schema_obj.get("required").and_then(|r| r.as_array()) { @@ -149,7 +153,11 @@ impl McpServer { let auth_header = arguments .as_object_mut() .and_then(|obj| obj.remove("_auth")) - .and_then(|auth| auth.get("apiKey").and_then(|k| k.as_str())) + .and_then(|auth| { + auth.get("apiKey") + .and_then(|k| k.as_str()) + .map(|s| s.to_string()) + }) .map(|key| format!("Bearer {}", key)); // Build the actual path by replacing parameters @@ -202,37 +210,17 @@ impl McpServer { } pub async fn mcp_sse_handler( - req: HttpRequest, - server: web::Data, + _req: HttpRequest, + _server: web::Data, ) -> Result { + // For MCP SSE transport, we need to handle incoming messages via query parameters + // The MCP inspector will send requests as query params on the SSE connection + let stream = try_stream! { - // Send initial connection event - yield format!("event: connected\ndata: {}\n\n", json!({ - "protocol": "mcp", - "version": "2024-11-05" - })); - - // Set up message channel - let (tx, mut rx) = tokio::sync::mpsc::channel::(100); - - // Read incoming messages from request body - let mut body = req.into_body(); - - // Process incoming messages - while let Some(chunk) = body.next().await { - if let Ok(data) = chunk { - if let Ok(text) = String::from_utf8(data.to_vec()) { - // Parse SSE format - if let Some(json_str) = extract_sse_data(&text) { - if let Ok(request) = serde_json::from_str::(&json_str) { - let response = server.handle_request(request).await; - let response_str = serde_json::to_string(&response)?; - - yield format!("event: message\ndata: {}\n\n", response_str); - } - } - } - } + // Keep the connection alive + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(30)).await; + yield format!(": keepalive\n\n"); } }; @@ -240,20 +228,12 @@ pub async fn mcp_sse_handler( .content_type("text/event-stream") .insert_header(("Cache-Control", "no-cache")) .insert_header(("Connection", "keep-alive")) - .streaming(stream.map(|result| { + .insert_header(("X-Accel-Buffering", "no")) + .streaming(stream.map(|result: Result| { result.map(|s| actix_web::web::Bytes::from(s)) - }))) + }).map_err(|e| actix_web::error::ErrorInternalServerError(e)))) } -fn extract_sse_data(text: &str) -> Option { - // Extract JSON data from SSE format - for line in text.lines() { - if let Some(data) = line.strip_prefix("data: ") { - return Some(data.to_string()); - } - } - None -} fn camel_to_snake_case(s: &str) -> String { let mut result = String::new(); @@ -277,9 +257,4 @@ mod tests { assert_eq!(camel_to_snake_case("simple"), "simple"); } - #[test] - fn test_extract_sse_data() { - let sse = "event: message\ndata: {\"test\": true}\n\n"; - assert_eq!(extract_sse_data(sse), Some("{\"test\": true}".to_string())); - } } \ No newline at end of file diff --git a/crates/meilisearch/src/lib.rs b/crates/meilisearch/src/lib.rs index 58c3afa8b..ad7461230 100644 --- a/crates/meilisearch/src/lib.rs +++ b/crates/meilisearch/src/lib.rs @@ -631,11 +631,6 @@ pub fn configure_data( web::QueryConfig::default().error_handler(|err, _req| PayloadError::from(err).into()), ); - #[cfg(feature = "mcp")] - { - let mcp_server = meilisearch_mcp::integration::create_mcp_server_from_openapi(); - config.app_data(web::Data::new(mcp_server)); - } } #[cfg(feature = "mini-dashboard")] diff --git a/crates/meilisearch/src/routes/mod.rs b/crates/meilisearch/src/routes/mod.rs index e32620208..0b57b8e00 100644 --- a/crates/meilisearch/src/routes/mod.rs +++ b/crates/meilisearch/src/routes/mod.rs @@ -118,7 +118,8 @@ pub fn configure(cfg: &mut web::ServiceConfig) { #[cfg(feature = "mcp")] { use meilisearch_mcp::integration::configure_mcp_route; - configure_mcp_route(cfg); + let openapi = MeilisearchApi::openapi(); + configure_mcp_route(cfg, openapi); } #[cfg(feature = "swagger")]