Skip to content

Commit 26d682c

Browse files
committed
Search: Add MCP tools so agents can use this
It was scoped out of the original effort, now it's in. - `search` tool: structured file search with pattern, size, date, type filters - `ai_search` tool: natural language search via configured LLM, shows interpreted query - Make `execute_tool` async to support index loading and AI calls - Parse human-readable size params ("1 MB", "500 KB") into bytes - Format results as aligned columns with name, path, size, date - Bump visibility of helpers in `commands::search` and `indexing::search` to `pub(crate)`
1 parent 3779198 commit 26d682c

7 files changed

Lines changed: 556 additions & 26 deletions

File tree

apps/desktop/src-tauri/src/commands/search.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -257,14 +257,14 @@ pub async fn release_search_index() -> Result<(), String> {
257257
/// Intermediate struct for LLM output — uses ISO date strings.
258258
#[derive(Debug, Clone, Deserialize)]
259259
#[serde(rename_all = "camelCase")]
260-
struct AiSearchQuery {
261-
name_pattern: Option<String>,
262-
pattern_type: Option<String>,
263-
min_size: Option<u64>,
264-
max_size: Option<u64>,
265-
modified_after: Option<String>,
266-
modified_before: Option<String>,
267-
is_directory: Option<bool>,
260+
pub(crate) struct AiSearchQuery {
261+
pub(crate) name_pattern: Option<String>,
262+
pub(crate) pattern_type: Option<String>,
263+
pub(crate) min_size: Option<u64>,
264+
pub(crate) max_size: Option<u64>,
265+
pub(crate) modified_after: Option<String>,
266+
pub(crate) modified_before: Option<String>,
267+
pub(crate) is_directory: Option<bool>,
268268
}
269269

270270
/// Human-readable field values returned alongside the structured query.
@@ -302,7 +302,7 @@ pub struct TranslateDisplay {
302302
}
303303

304304
/// Converts an ISO date string (YYYY-MM-DD) to a unix timestamp (seconds since epoch).
305-
fn iso_date_to_timestamp(date_str: &str) -> Result<u64, String> {
305+
pub(crate) fn iso_date_to_timestamp(date_str: &str) -> Result<u64, String> {
306306
let format = time::macros::format_description!("[year]-[month]-[day]");
307307
let date = time::Date::parse(date_str, &format).map_err(|e| format!("Invalid date '{date_str}': {e}"))?;
308308
let datetime = date.with_hms(0, 0, 0).expect("midnight is always valid");
@@ -313,7 +313,7 @@ fn iso_date_to_timestamp(date_str: &str) -> Result<u64, String> {
313313
Ok(timestamp as u64)
314314
}
315315

316-
fn build_search_system_prompt() -> String {
316+
pub(crate) fn build_search_system_prompt() -> String {
317317
let today = time::OffsetDateTime::now_utc().date();
318318
let format = time::macros::format_description!("[year]-[month]-[day]");
319319
let today_str = today.format(&format).expect("date format always succeeds");
@@ -345,7 +345,7 @@ fn build_search_system_prompt() -> String {
345345
}
346346

347347
/// Strips markdown code fences from an LLM response and parses it as JSON.
348-
fn parse_ai_response(response: &str) -> Result<AiSearchQuery, String> {
348+
pub(crate) fn parse_ai_response(response: &str) -> Result<AiSearchQuery, String> {
349349
let json_str = response.trim();
350350
let json_str = json_str
351351
.strip_prefix("```json")
@@ -358,7 +358,7 @@ fn parse_ai_response(response: &str) -> Result<AiSearchQuery, String> {
358358
}
359359

360360
/// Converts a parsed `AiSearchQuery` into the final `TranslateResult`.
361-
fn build_translate_result(ai_query: AiSearchQuery) -> Result<TranslateResult, String> {
361+
pub(crate) fn build_translate_result(ai_query: AiSearchQuery) -> Result<TranslateResult, String> {
362362
let modified_after_ts = ai_query
363363
.modified_after
364364
.as_deref()
@@ -396,7 +396,7 @@ fn build_translate_result(ai_query: AiSearchQuery) -> Result<TranslateResult, St
396396

397397
/// If the AI returned a regex pattern, validates it against the `regex` crate.
398398
/// Returns `Ok(())` if valid or not a regex, `Err(message)` with the compile error otherwise.
399-
fn validate_regex_pattern(ai_query: &AiSearchQuery) -> Result<(), String> {
399+
pub(crate) fn validate_regex_pattern(ai_query: &AiSearchQuery) -> Result<(), String> {
400400
let is_regex = ai_query.pattern_type.as_deref().is_some_and(|t| t == "regex");
401401
if !is_regex {
402402
return Ok(());

apps/desktop/src-tauri/src/indexing/search.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ pub fn summarize_query(query: &SearchQuery) -> String {
259259
}
260260
}
261261

262-
fn format_size(bytes: u64) -> String {
262+
pub(crate) fn format_size(bytes: u64) -> String {
263263
const KB: u64 = 1_024;
264264
const MB: u64 = 1_024 * KB;
265265
const GB: u64 = 1_024 * MB;
@@ -298,7 +298,7 @@ fn format_size(bytes: u64) -> String {
298298
}
299299
}
300300

301-
fn format_timestamp(ts: u64) -> String {
301+
pub(crate) fn format_timestamp(ts: u64) -> String {
302302
let format = time::macros::format_description!("[year]-[month]-[day]");
303303
time::OffsetDateTime::from_unix_timestamp(ts as i64)
304304
.map(|dt| dt.format(&format).unwrap_or_else(|_| ts.to_string()))

apps/desktop/src-tauri/src/mcp/CLAUDE.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ Expose Cmdr functionality to AI agents via the Model Context Protocol (MCP). Age
2121

2222
### Tools (`tools.rs`)
2323

24-
22 semantic tools grouped by category:
24+
24 semantic tools grouped by category:
2525
- Navigation (6): `select_volume`, `nav_to_path`, `nav_to_parent`, `nav_back`, `nav_forward`, `scroll_to`
2626
- Cursor/Selection (3): `move_cursor`, `open_under_cursor`, `select`
2727
- File operations (4): `copy`, `delete`, `mkdir`, `refresh`
2828
- View (3): `sort`, `toggle_hidden`, `set_view_mode`
2929
- Tabs (2): `activate_tab` (switch to a specific tab by pane + tab ID), `pin_tab` (pin/unpin a tab)
3030
- Dialogs (1): `dialog` (unified open/focus/close)
3131
- App (3): `switch_pane`, `swap_panes`, `quit`
32+
- Search (2): `search` (structured file search across the drive index), `ai_search` (natural language search using configured LLM)
3233

3334
### Resources (`resources.rs`)
3435

@@ -60,7 +61,7 @@ Frontend syncs state to these stores via Tauri commands (`update_left_pane_state
6061

6162
### Why agent-centric API?
6263

63-
The original design mirrored keyboard shortcuts (43 tools like `nav_up`, `nav_down`). This forced agents to make dozens of calls to find a file. The agent-centric redesign (Jan 2026) consolidated to 22 semantic tools (`move_cursor(index=42)`, `nav_to_path("/Users")`). This reduced round-trips from 6+ reads to 1 (`cmdr://state` resource).
64+
The original design mirrored keyboard shortcuts (43 tools like `nav_up`, `nav_down`). This forced agents to make dozens of calls to find a file. The agent-centric redesign (Jan 2026) consolidated to 24 semantic tools (`move_cursor(index=42)`, `nav_to_path("/Users")`). This reduced round-trips from 6+ reads to 1 (`cmdr://state` resource).
6465

6566
### Why YAML over JSON for resources?
6667

@@ -130,9 +131,9 @@ Settings window: full bridge (`mcp-settings-bridge.ts`) syncs all state and hand
130131
Main window: lightweight listener (`mcp-shortcuts-listener.ts`) only handles shortcut changes.
131132
This separation keeps main window overhead minimal.
132133

133-
### Tool execution is synchronous
134+
### Tool execution is async but mostly synchronous
134135

135-
`execute_tool()` is a synchronous function. Tools that trigger async operations (like `copy`, `mkdir`) return immediately after emitting the event. The tool result doesn't wait for the operation to complete. This is intentional—tools return "OK: Copy dialog opened" not "OK: Files copied".
136+
`execute_tool()` is an async function. Most tools are synchronous — they emit a Tauri event and return immediately (for example, "OK: Copy dialog opened" not "OK: Files copied"). The `search` and `ai_search` tools are truly async: they load the search index via `spawn_blocking` and (for `ai_search`) call the LLM API.
136137

137138
### Error codes are JSON-RPC standard
138139

0 commit comments

Comments
 (0)