diff --git a/crates/kebab-mcp/src/lib.rs b/crates/kebab-mcp/src/lib.rs index 93f39df..2475d39 100644 --- a/crates/kebab-mcp/src/lib.rs +++ b/crates/kebab-mcp/src/lib.rs @@ -1,7 +1,7 @@ -//! MCP (Model Context Protocol) server over stdio. Exposes 7 tools +//! MCP (Model Context Protocol) server over stdio. Exposes 8 tools //! (`search` / `ask` / `schema` / `doctor` / `ingest_file` / `ingest_stdin` -//! / `fetch`) backed by `kebab-app` facade methods. Used by `kebab-cli`'s -//! `Cmd::Mcp` arm. +//! / `fetch` / `bulk_search`) backed by `kebab-app` facade methods. Used by +//! `kebab-cli`'s `Cmd::Mcp` arm. //! //! See spec `docs/superpowers/specs/2026-05-07-p9-fb-30-mcp-server-design.md`. @@ -67,6 +67,11 @@ pub fn build_tools_vec() -> Vec { "Verbatim fetch — chunk / doc / span modes. Returns fetch_result.v1 with the indexed text (no LLM rewrite).", schema_for_type::(), ), + Tool::new( + "bulk_search", + "Bulk multi-query search — N queries per call (cap 100). Each query mirrors the `search` input shape; returns `bulk_search_response.v1` with per-query results + summary. Sequential execution reuses one App instance so cache / embedder cold-start cost amortizes.", + schema_for_type::(), + ), ] } @@ -170,6 +175,13 @@ impl ServerHandler for KebabHandler { }) .await } + "bulk_search" => { + let args = request.arguments.unwrap_or_default(); + self.spawn_tool(args, |state, input| { + tools::bulk_search::handle(&state, input) + }) + .await + } _other => Err(ErrorData::method_not_found::< rmcp::model::CallToolRequestMethod, >()), diff --git a/crates/kebab-mcp/src/tools/bulk_search.rs b/crates/kebab-mcp/src/tools/bulk_search.rs new file mode 100644 index 0000000..8be8a13 --- /dev/null +++ b/crates/kebab-mcp/src/tools/bulk_search.rs @@ -0,0 +1,55 @@ +//! `bulk_search` tool — wraps `kebab_app::bulk_search_with_config`. +//! Input: `{ queries: [, ...] }`. +//! Output: `bulk_search_response.v1` envelope (results + summary). + +use rmcp::model::CallToolResult; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::error::{to_tool_error, to_tool_success}; +use crate::state::KebabAppState; + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct BulkSearchInput { + /// Per-query inputs. Each item mirrors the single-query `search` + /// tool's input shape — `query` is required, all other fields are + /// optional and default to single-search defaults. Capped at 100 + /// items; exceeding returns an `invalid_input` tool error without + /// running any query. + pub queries: Vec, +} + +pub fn handle(state: &KebabAppState, input: BulkSearchInput) -> CallToolResult { + let cfg_clone = (*state.config).clone(); + match kebab_app::bulk_search_with_config(cfg_clone, input.queries) { + Ok((items, summary)) => { + let tagged_items: Vec = items + .iter() + .map(|it| { + let mut v = serde_json::to_value(it).unwrap_or(serde_json::Value::Null); + if let serde_json::Value::Object(ref mut map) = v { + map.insert( + "schema_version".to_string(), + serde_json::Value::String("bulk_search_item.v1".to_string()), + ); + } + v + }) + .collect(); + let envelope = serde_json::json!({ + "schema_version": "bulk_search_response.v1", + "results": tagged_items, + "summary": { + "total": summary.total, + "succeeded": summary.succeeded, + "failed": summary.failed, + }, + }); + match serde_json::to_string(&envelope) { + Ok(json) => to_tool_success(json), + Err(e) => to_tool_error(&anyhow::anyhow!(e)), + } + } + Err(e) => to_tool_error(&e), + } +} diff --git a/crates/kebab-mcp/src/tools/mod.rs b/crates/kebab-mcp/src/tools/mod.rs index 22b5569..f06f91b 100644 --- a/crates/kebab-mcp/src/tools/mod.rs +++ b/crates/kebab-mcp/src/tools/mod.rs @@ -7,3 +7,4 @@ pub mod ask; pub mod ingest_file; pub mod ingest_stdin; pub mod fetch; +pub mod bulk_search; diff --git a/crates/kebab-mcp/tests/tools_list.rs b/crates/kebab-mcp/tests/tools_list.rs index c56c4a3..5746bdf 100644 --- a/crates/kebab-mcp/tests/tools_list.rs +++ b/crates/kebab-mcp/tests/tools_list.rs @@ -1,13 +1,13 @@ -//! Integration: `build_tools_vec` returns 7 tools with correct names and +//! Integration: `build_tools_vec` returns 8 tools with correct names and //! inputSchema. Uses the extracted `pub fn build_tools_vec()` helper — no //! transport or RequestContext needed. use kebab_mcp::build_tools_vec; #[test] -fn tools_list_returns_seven_tools() { +fn tools_list_returns_eight_tools() { let tools = build_tools_vec(); - assert_eq!(tools.len(), 7, "expected exactly 7 tools, got {}", tools.len()); + assert_eq!(tools.len(), 8, "expected exactly 8 tools, got {}", tools.len()); let names: Vec<&str> = tools.iter().map(|t| t.name.as_ref()).collect(); assert!(names.contains(&"schema"), "missing 'schema' tool"); @@ -17,6 +17,7 @@ fn tools_list_returns_seven_tools() { assert!(names.contains(&"ingest_file"), "missing 'ingest_file' tool"); assert!(names.contains(&"ingest_stdin"), "missing 'ingest_stdin' tool"); assert!(names.contains(&"fetch"), "missing 'fetch' tool"); + assert!(names.contains(&"bulk_search"), "missing 'bulk_search' tool"); } #[test]