Skip to content

Commit bcb9438

Browse files
committed
feat: Add pagination and filtering to MCP analyze_audio tool
- Implement cursor-based pagination for large data arrays - Add filtering parameters (include_visuals, include_spectral, include_temporal) - Change default return_format to "summary" for MCP compatibility - Add max_data_points parameter to limit response sizes - Update documentation with new parameter examples - Prevent token limit errors by keeping responses under 25k tokens
1 parent a468179 commit bcb9438

3 files changed

Lines changed: 267 additions & 22 deletions

File tree

README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,25 @@ ferrous-waves serve
8282
```
8383

8484
The server exposes three tools:
85-
- `analyze_audio` - Full audio analysis with spectral and temporal features
86-
- `compare_audio` - Compare two audio files
87-
- `get_job_status` - Check analysis job status
85+
86+
#### `analyze_audio` - Comprehensive audio analysis with filtering and pagination
87+
Parameters:
88+
- `file_path` (required): Path to audio file
89+
- `return_format`: "summary" (default), "full", or "visual_only"
90+
- `include_visuals`: Include base64-encoded images (default: false, WARNING: very large)
91+
- `include_spectral`: Include spectral data arrays (default: false)
92+
- `include_temporal`: Include temporal data arrays (default: false)
93+
- `max_data_points`: Limit array sizes for pagination (default: 1000)
94+
- `cursor`: Continue from previous response's next_cursor
95+
96+
#### `compare_audio` - Compare two audio files
97+
Parameters:
98+
- `file_a`, `file_b` (required): Paths to audio files
99+
- `metrics`: Optional comparison metrics to calculate
100+
101+
#### `get_job_status` - Check analysis job status
102+
Parameters:
103+
- `job_id` (required): Job ID from previous analysis
88104

89105
## Architecture
90106

examples/mcp_client.rs

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,80 @@ fn main() {
77
println!("Example MCP Tool Calls:");
88
println!();
99

10-
// Example 1: Analyze a single audio file
11-
let analyze_request = json!({
10+
// Example 1: Analyze with default settings (returns compact summary)
11+
let analyze_summary = json!({
12+
"tool": "analyze_audio",
13+
"arguments": {
14+
"file_path": "/path/to/audio.wav"
15+
}
16+
});
17+
println!("1. Analyze Audio (Summary - Default):");
18+
println!(
19+
"{}",
20+
serde_json::to_string_pretty(&analyze_summary).unwrap()
21+
);
22+
println!();
23+
24+
// Example 2: Analyze with spectral data and pagination
25+
let analyze_detailed = json!({
26+
"tool": "analyze_audio",
27+
"arguments": {
28+
"file_path": "/path/to/audio.wav",
29+
"return_format": "full",
30+
"include_spectral": true,
31+
"include_temporal": true,
32+
"max_data_points": 100,
33+
"cursor": null
34+
}
35+
});
36+
println!("2. Analyze Audio (Full with Pagination):");
37+
println!(
38+
"{}",
39+
serde_json::to_string_pretty(&analyze_detailed).unwrap()
40+
);
41+
println!();
42+
43+
// Example 3: Continue pagination with cursor
44+
let analyze_next_page = json!({
1245
"tool": "analyze_audio",
1346
"arguments": {
1447
"file_path": "/path/to/audio.wav",
15-
"return_format": "summary"
48+
"return_format": "full",
49+
"include_spectral": true,
50+
"max_data_points": 100,
51+
"cursor": "100" // From previous response's next_cursor
1652
}
1753
});
18-
println!("1. Analyze Audio:");
54+
println!("3. Continue Pagination:");
1955
println!(
2056
"{}",
21-
serde_json::to_string_pretty(&analyze_request).unwrap()
57+
serde_json::to_string_pretty(&analyze_next_page).unwrap()
2258
);
2359
println!();
2460

25-
// Example 2: Compare two audio files
61+
// Example 4: Compare two audio files
2662
let compare_request = json!({
2763
"tool": "compare_audio",
2864
"arguments": {
2965
"file_a": "/path/to/original.wav",
3066
"file_b": "/path/to/processed.wav"
3167
}
3268
});
33-
println!("2. Compare Audio:");
69+
println!("4. Compare Audio:");
3470
println!(
3571
"{}",
3672
serde_json::to_string_pretty(&compare_request).unwrap()
3773
);
3874
println!();
3975

40-
// Example 3: Check job status
76+
// Example 5: Check job status
4177
let status_request = json!({
4278
"tool": "get_job_status",
4379
"arguments": {
4480
"job_id": "550e8400-e29b-41d4-a716-446655440000"
4581
}
4682
});
47-
println!("3. Get Job Status:");
83+
println!("5. Get Job Status:");
4884
println!("{}", serde_json::to_string_pretty(&status_request).unwrap());
4985
println!();
5086

src/mcp/server.rs

Lines changed: 203 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,42 @@ pub struct AnalyzeAudioParams {
3131
/// Return format: "full" | "summary" | "visual_only"
3232
#[serde(default = "default_return_format")]
3333
pub return_format: String,
34+
35+
/// Maximum number of data points in arrays (for pagination)
36+
#[serde(skip_serializing_if = "Option::is_none")]
37+
pub max_data_points: Option<usize>,
38+
39+
/// Pagination cursor for continuing from previous results
40+
#[serde(skip_serializing_if = "Option::is_none")]
41+
pub cursor: Option<String>,
42+
43+
/// Include visual data (base64 images) - defaults to false for MCP
44+
#[serde(default = "default_include_visuals")]
45+
pub include_visuals: bool,
46+
47+
/// Include raw spectral data arrays
48+
#[serde(default = "default_include_spectral")]
49+
pub include_spectral: bool,
50+
51+
/// Include temporal data arrays (beats, onsets)
52+
#[serde(default = "default_include_temporal")]
53+
pub include_temporal: bool,
3454
}
3555

3656
fn default_return_format() -> String {
37-
"full".to_string()
57+
"summary".to_string()
58+
}
59+
60+
fn default_include_visuals() -> bool {
61+
false // Never include base64 images by default in MCP
62+
}
63+
64+
fn default_include_spectral() -> bool {
65+
false // Don't include large arrays by default
66+
}
67+
68+
fn default_include_temporal() -> bool {
69+
false // Don't include large arrays by default
3870
}
3971

4072
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -120,22 +152,159 @@ impl FerrousWavesMcp {
120152
status.message = Some("Analysis complete".to_string());
121153
}
122154

155+
// Apply filtering based on parameters
156+
let mut filtered_result = analysis_result;
157+
158+
// Filter visuals unless explicitly requested
159+
if !params.include_visuals {
160+
filtered_result.visuals.waveform = None;
161+
filtered_result.visuals.spectrogram = None;
162+
filtered_result.visuals.mel_spectrogram = None;
163+
filtered_result.visuals.power_curve = None;
164+
}
165+
166+
// Filter spectral data unless requested
167+
if !params.include_spectral {
168+
filtered_result.spectral.spectral_centroid.clear();
169+
filtered_result.spectral.spectral_rolloff.clear();
170+
filtered_result.spectral.spectral_flux.clear();
171+
filtered_result.spectral.mfcc.clear();
172+
filtered_result.spectral.dominant_frequencies.clear();
173+
}
174+
175+
// Filter temporal data unless requested
176+
if !params.include_temporal {
177+
filtered_result.temporal.beats.clear();
178+
filtered_result.temporal.onsets.clear();
179+
}
180+
181+
// Parse cursor for pagination
182+
let offset = if let Some(cursor) = &params.cursor {
183+
cursor.parse::<usize>().unwrap_or(0)
184+
} else {
185+
0
186+
};
187+
188+
// Apply pagination with max_data_points
189+
let mut next_cursor = None;
190+
if let Some(max_points) = params.max_data_points {
191+
if params.include_spectral {
192+
// Paginate spectral data
193+
let total_len = filtered_result.spectral.spectral_centroid.len();
194+
if offset < total_len {
195+
let end = (offset + max_points).min(total_len);
196+
filtered_result.spectral.spectral_centroid =
197+
filtered_result.spectral.spectral_centroid[offset..end].to_vec();
198+
199+
if end < total_len {
200+
next_cursor = Some(end.to_string());
201+
}
202+
}
203+
204+
// Apply same pagination to other spectral arrays
205+
if offset < filtered_result.spectral.spectral_flux.len() {
206+
let end =
207+
(offset + max_points).min(filtered_result.spectral.spectral_flux.len());
208+
filtered_result.spectral.spectral_flux =
209+
filtered_result.spectral.spectral_flux[offset..end].to_vec();
210+
}
211+
212+
if offset < filtered_result.spectral.spectral_rolloff.len() {
213+
let end =
214+
(offset + max_points).min(filtered_result.spectral.spectral_rolloff.len());
215+
filtered_result.spectral.spectral_rolloff =
216+
filtered_result.spectral.spectral_rolloff[offset..end].to_vec();
217+
}
218+
219+
// MFCC is special - limit number of frames
220+
if offset < filtered_result.spectral.mfcc.len() {
221+
let end = (offset + max_points).min(filtered_result.spectral.mfcc.len());
222+
filtered_result.spectral.mfcc =
223+
filtered_result.spectral.mfcc[offset..end].to_vec();
224+
}
225+
}
226+
227+
if params.include_temporal {
228+
// Paginate temporal data
229+
if offset < filtered_result.temporal.beats.len() {
230+
let end = (offset + max_points).min(filtered_result.temporal.beats.len());
231+
filtered_result.temporal.beats =
232+
filtered_result.temporal.beats[offset..end].to_vec();
233+
234+
if end < filtered_result.temporal.beats.len() && next_cursor.is_none() {
235+
next_cursor = Some(end.to_string());
236+
}
237+
}
238+
239+
if offset < filtered_result.temporal.onsets.len() {
240+
let end = (offset + max_points).min(filtered_result.temporal.onsets.len());
241+
filtered_result.temporal.onsets =
242+
filtered_result.temporal.onsets[offset..end].to_vec();
243+
244+
if end < filtered_result.temporal.onsets.len() && next_cursor.is_none() {
245+
next_cursor = Some(end.to_string());
246+
}
247+
}
248+
}
249+
}
250+
123251
// Format response based on return_format
124252
let response_data = match params.return_format.as_str() {
125-
"summary" => json!({
126-
"job_id": job_id,
127-
"status": "success",
128-
"summary": analysis_result.get_summary(),
129-
}),
253+
"summary" => {
254+
let mut response = json!({
255+
"job_id": job_id,
256+
"status": "success",
257+
"summary": filtered_result.get_summary(),
258+
"insights": filtered_result.insights.iter().take(5).cloned().collect::<Vec<_>>(),
259+
});
260+
if let Some(cursor) = next_cursor {
261+
response["next_cursor"] = json!(cursor);
262+
}
263+
response
264+
}
130265
"visual_only" => json!({
131266
"job_id": job_id,
132267
"status": "success",
133-
"visuals": analysis_result.get_visuals(),
268+
"visuals": filtered_result.get_visuals(),
134269
}),
270+
"full" => {
271+
let mut response = json!({
272+
"job_id": job_id,
273+
"status": "success",
274+
"data": {
275+
"summary": filtered_result.summary,
276+
"spectral": if params.include_spectral {
277+
Some(filtered_result.spectral)
278+
} else {
279+
None
280+
},
281+
"temporal": if params.include_temporal {
282+
Some(filtered_result.temporal)
283+
} else {
284+
None
285+
},
286+
"visuals": if params.include_visuals {
287+
Some(filtered_result.visuals)
288+
} else {
289+
None
290+
},
291+
"insights": filtered_result.insights,
292+
"recommendations": filtered_result.recommendations,
293+
}
294+
});
295+
if let Some(cursor) = next_cursor {
296+
response["next_cursor"] = json!(cursor);
297+
response["has_more"] = json!(true);
298+
} else {
299+
response["has_more"] = json!(false);
300+
}
301+
response
302+
}
135303
_ => json!({
136304
"job_id": job_id,
137305
"status": "success",
138-
"data": analysis_result,
306+
"summary": filtered_result.get_summary(),
307+
"insights": filtered_result.insights.iter().take(5).cloned().collect::<Vec<_>>(),
139308
}),
140309
};
141310

@@ -251,8 +420,32 @@ impl ServerHandler for FerrousWavesMcp {
251420
"return_format": {
252421
"type": "string",
253422
"enum": ["full", "summary", "visual_only"],
254-
"description": "Return format",
255-
"default": "full"
423+
"description": "Return format (default: summary for MCP compatibility)",
424+
"default": "summary"
425+
},
426+
"max_data_points": {
427+
"type": "integer",
428+
"description": "Maximum number of data points in arrays (for pagination)",
429+
"default": 1000
430+
},
431+
"cursor": {
432+
"type": "string",
433+
"description": "Pagination cursor from previous response's next_cursor field"
434+
},
435+
"include_visuals": {
436+
"type": "boolean",
437+
"description": "Include visual data (base64 images) - WARNING: very large",
438+
"default": false
439+
},
440+
"include_spectral": {
441+
"type": "boolean",
442+
"description": "Include raw spectral data arrays",
443+
"default": false
444+
},
445+
"include_temporal": {
446+
"type": "boolean",
447+
"description": "Include temporal data arrays (beats, onsets)",
448+
"default": false
256449
}
257450
},
258451
"required": ["file_path"]

0 commit comments

Comments
 (0)