🏗️ refactor(kebab-mcp): fix ask tool production panic + mode default (fb-30)
Two issues from Task 7 review:
1. CRITICAL — call_tool "ask" arm called blocking ask handle from async
context. OllamaLanguageModel::new builds reqwest::blocking::Client
which creates+drops a tokio runtime → panic inside async. Fix:
tokio::task::spawn_blocking wrap. Also applied preemptively to
"search" arm (SqliteStore + Lance open are blocking IO too).
2. IMPORTANT — ask tool's retrieval mode hardcoded to Lexical (test
workaround for provider="none"); CLI default is Hybrid. Fix: add
`mode: Option<String>` field to AskInput, default Hybrid in handle,
test passes mode=Some("lexical") explicitly to keep test functional
on provider="none".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<String>,
|
||||
/// Optional retrieval mode override ("lexical" / "vector" / "hybrid"). Default "hybrid".
|
||||
pub mode: Option<String>,
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -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()),
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user