diff --git a/crates/kebab-mcp/src/lib.rs b/crates/kebab-mcp/src/lib.rs index 009a1d3..1bca86b 100644 --- a/crates/kebab-mcp/src/lib.rs +++ b/crates/kebab-mcp/src/lib.rs @@ -96,7 +96,15 @@ impl ServerHandler for KebabHandler { return Ok(error::to_tool_error(&anyhow::Error::from(e))); } }; - Ok(tools::search::handle(&self.state, input)) + let state = self.state.clone(); + let result = tokio::task::spawn_blocking(move || { + tools::search::handle(&state, input) + }) + .await + .map_err(|e| { + ErrorData::internal_error(e.to_string(), None) + })?; + Ok(result) } "ask" => { let args = request.arguments.unwrap_or_default(); @@ -107,7 +115,15 @@ impl ServerHandler for KebabHandler { return Ok(error::to_tool_error(&anyhow::Error::from(e))); } }; - Ok(tools::ask::handle(&self.state, input)) + let state = self.state.clone(); + let result = tokio::task::spawn_blocking(move || { + tools::ask::handle(&state, input) + }) + .await + .map_err(|e| { + ErrorData::internal_error(e.to_string(), None) + })?; + Ok(result) } _other => Err(ErrorData::method_not_found::< rmcp::model::CallToolRequestMethod, diff --git a/crates/kebab-mcp/src/tools/ask.rs b/crates/kebab-mcp/src/tools/ask.rs index 5f6eebd..96c74f7 100644 --- a/crates/kebab-mcp/src/tools/ask.rs +++ b/crates/kebab-mcp/src/tools/ask.rs @@ -1,6 +1,6 @@ //! `ask` tool — wraps `kebab_app::ask_with_config` (single-shot) or //! `kebab_app::ask_with_session_with_config` when `session_id` is provided. -//! Input: { query, session_id? }. Output: answer.v1 JSON. +//! Input: { query, session_id?, mode? }. Output: answer.v1 JSON. //! //! `Answer` (kebab-core) does NOT carry a `schema_version` field; we tag //! it inline here, matching the pattern from `search.rs`. @@ -18,18 +18,20 @@ pub struct AskInput { pub query: String, /// Optional session id for multi-turn RAG context. pub session_id: Option, + /// Optional retrieval mode override ("lexical" / "vector" / "hybrid"). Default "hybrid". + pub mode: Option, } pub fn handle(state: &KebabAppState, input: AskInput) -> CallToolResult { - // Default to Lexical mode — the MCP server is typically called by - // agent hosts that may not have an embedding provider configured. - // Hybrid/vector retrieval would hard-error when embeddings are - // disabled; lexical FTS is always available and covers the common - // RAG case well. + let mode = match input.mode.as_deref() { + Some("lexical") => kebab_core::SearchMode::Lexical, + Some("vector") => kebab_core::SearchMode::Vector, + _ => kebab_core::SearchMode::Hybrid, // default + "hybrid" + unknown + }; let opts = kebab_app::AskOpts { k: 10, explain: false, - mode: kebab_core::SearchMode::Lexical, + mode, temperature: None, seed: None, stream_sink: None, diff --git a/crates/kebab-mcp/tests/tools_call_ask.rs b/crates/kebab-mcp/tests/tools_call_ask.rs index 9b40676..09657d1 100644 --- a/crates/kebab-mcp/tests/tools_call_ask.rs +++ b/crates/kebab-mcp/tests/tools_call_ask.rs @@ -52,6 +52,9 @@ async fn ask_tool_returns_answer_v1_with_refusal_on_empty_kb() { kebab_mcp::tools::ask::AskInput { query: "what is the meaning of life".to_string(), session_id: None, + // Test env uses provider="none" — Hybrid would hard-error on embedding. + // Pass Lexical explicitly so the test stays functional. + mode: Some("lexical".to_string()), }, ) })