Files
kebab/crates/kebab-mcp/src/tools/bulk_search.rs
th-kim0823 6ab0d782ef feat(mcp): kebab__bulk_search tool (fb-42)
Exposes bulk multi-query search via MCP `bulk_search` tool:
- Input: { queries: [SearchInput shapes...] }, capped at 100
- Output: bulk_search_response.v1 with per-query results + summary
- Sequential execution reuses App instance for cache amortization
- Per-query errors embed error.v1 JSON; never aborts bulk call

Updates tool count from 7 to 8 in lib.rs comment + tools_list test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:31:20 +09:00

56 lines
2.2 KiB
Rust

//! `bulk_search` tool — wraps `kebab_app::bulk_search_with_config`.
//! Input: `{ queries: [<SearchInput shape>, ...] }`.
//! 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<serde_json::Value>,
}
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<serde_json::Value> = 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),
}
}