Expose exact music filters in desktop and local agent clients

This commit is contained in:
karl committed 2026-10-13 18:37:51 +02:00
1 parent 2eace15a33
commit c33047998b
6 files changed
+263 -35

No files matched your search

+77 -5
View File
@@ -29,7 +29,8 @@ use tokio_util::codec::{FramedRead, FramedWrite};
#[derive(Debug, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
struct SearchInput {
/// Filename, content or remembered meaning. At most 4096 UTF-8 bytes.
/// Filename, content or remembered meaning. At most 4096 UTF-8 bytes;
/// may be empty when an exact media_artist/media_title constraint is supplied.
query: String,
/// Maximum returned files, 1–100; default 10.
@@ -43,6 +44,10 @@ struct SearchInput {
code_only: bool,
extension: Option<String>,
/// Exact full artist tag, case-insensitive; at most 1024 UTF-8 bytes.
media_artist: Option<String>,
/// Exact full media title tag, case-insensitive; at most 1024 UTF-8 bytes.
media_title: Option<String>,
min_size: Option<u64>,
@@ -132,9 +137,10 @@ impl AgentServer {
ensure!(
!input.query.trim().is_empty() && input.query.len() <= 4096,
input.query.len() <= 4096 && (!input.query.trim().is_empty()
|| input.media_artist.is_some() || input.media_title.is_some()),
"Query must contain 1–4096 UTF-8 bytes"
"Supply search text (at most 4096 UTF-8 bytes) or an exact media artist/title constraint"
);
@@ -174,6 +180,8 @@ impl AgentServer {
code_only: input.code_only,
extension: input.extension,
media_artist: input.media_artist,
media_title: input.media_title,
min_size: input.min_size,
@@ -189,7 +197,12 @@ impl AgentServer {
};
let (mode, response) = if let Some(hybrid) = state.hybrid.as_mut() {
let semantic_available = state.hybrid.is_some();
let (mode, response) = if input.query.trim().is_empty() {
("metadata_only", state.index.search_with_options_measured(
&input.query, usize::from(input.limit), &options, false,
)?)
} else if let Some(hybrid) = state.hybrid.as_mut() {
("hybrid", hybrid.search_measured(
@@ -278,7 +291,7 @@ impl AgentServer {
"mode": mode,
"code_only": input.code_only,
"semantic_available": mode == "hybrid",
"semantic_available": semantic_available,
"results": results,
@@ -513,3 +526,62 @@ pub fn serve(
Ok(())
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn metadata_only_mcp_filters_real_fields_and_rejects_empty_constraints() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("track.ogg");
std::fs::write(&file, b"metadata storage fixture").unwrap();
let mut index = SearchIndex::open(":memory:").unwrap();
index.index_folder(dir.path()).unwrap();
let path = file.canonicalize().unwrap().to_string_lossy().into_owned();
let (bytes, modified) = index.file_signature(&path).unwrap().unwrap();
let revision = index.media_change_revision(&path, bytes, modified).unwrap();
let tags = std::collections::BTreeMap::from([
("artist".to_owned(), "Björk".to_owned()),
("title".to_owned(), "Jóga".to_owned()),
("comment".to_owned(), "artist: Other Artist".to_owned()),
]);
index
.set_structured_media_metadata_at_revision(
&path, &tags, 1000, bytes, modified, revision,
)
.unwrap();
let server = AgentServer {
state: Arc::new(Mutex::new(State {
index,
hybrid: None,
handles: BTreeMap::new(),
recent: VecDeque::new(),
})),
gate: Arc::new(tokio::sync::Semaphore::new(1)),
};
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
for (input, expected) in [
(
json!({"query":"", "media_artist":" BJÖRK ", "media_title":"JÓGA"}),
Some(1),
),
(json!({"query":"", "media_artist":"Other Artist"}), Some(0)),
(json!({"query":"", "media_artist":" "}), None),
(json!({"query":""}), None),
] {
let response =
runtime.block_on(server.search(Parameters(serde_json::from_value(input).unwrap())));
if let Some(count) = expected {
let data = response.structured_content.unwrap();
assert_eq!(data["mode"], "metadata_only");
assert_eq!(data["semantic_available"], false);
assert_eq!(data["results"].as_array().unwrap().len(), count);
} else {
assert_eq!(response.is_error, Some(true));
}
}
}
}