@@ -10,12 +10,6 @@ use crate::pool::{format_invoke_permission_denied, format_server_inactive_error}
1010use crate :: services:: tool_discovery:: ToolDiscoveryService ;
1111use mcpmux_core:: FeatureType ;
1212
13- /// Default row cap for smart truncation when `filter` is omitted.
14- pub const DEFAULT_MAX_ROWS : usize = 50 ;
15-
16- /// Default byte cap for smart truncation when `filter` is omitted.
17- pub const DEFAULT_MAX_BYTES : usize = 65_536 ;
18-
1913/// Object keys that commonly hold large list payloads from backend tools.
2014const HEAVY_ARRAY_KEYS : & [ & str ] = & [
2115 "items" , "data" , "results" , "rows" , "records" , "issues" , "entries" , "values" , "list" ,
@@ -61,16 +55,6 @@ pub fn parse_invoke_filter(value: Option<&Value>) -> Option<InvokeResultFilter>
6155}
6256
6357impl InvokeResultFilter {
64- fn effective_max_rows ( & self , use_defaults : bool ) -> Option < usize > {
65- self . max_rows
66- . or_else ( || use_defaults. then_some ( DEFAULT_MAX_ROWS ) )
67- }
68-
69- fn effective_max_bytes ( & self , use_defaults : bool ) -> Option < usize > {
70- self . max_bytes
71- . or_else ( || use_defaults. then_some ( DEFAULT_MAX_BYTES ) )
72- }
73-
7458 fn is_summary ( & self ) -> bool {
7559 self . format . as_deref ( ) == Some ( "summary" )
7660 }
@@ -80,14 +64,12 @@ impl InvokeResultFilter {
8064pub fn apply_invoke_result_filter (
8165 content : Vec < Value > ,
8266 structured_content : Option < Value > ,
83- filter : Option < & InvokeResultFilter > ,
84- use_defaults : bool ,
67+ filter : & InvokeResultFilter ,
8568) -> ( Vec < Value > , Option < Value > ) {
86- let filter = filter. cloned ( ) . unwrap_or_default ( ) ;
87- let shaped_structured = structured_content. map ( |value| shape_json_value ( value, & filter, use_defaults) ) ;
69+ let shaped_structured = structured_content. map ( |value| shape_json_value ( value, filter) ) ;
8870 let shaped_content = content
8971 . into_iter ( )
90- . map ( |block| shape_content_block ( block, & filter, use_defaults ) )
72+ . map ( |block| shape_content_block ( block, filter) )
9173 . collect ( ) ;
9274 ( shaped_content, shaped_structured)
9375}
@@ -105,8 +87,8 @@ impl MetaTool for InvokeToolTool {
10587 "Invoke a backend MCP tool by server_id and tool name. Requires the \
10688 server to be active (binding or session enable) and the tool to be \
10789 in the current permission set. Use mcpmux_search_tools and \
108- mcpmux_get_tool_schema before calling. Large list payloads are \
109- truncated by default; pass an optional filter object to control shaping ."
90+ mcpmux_get_tool_schema before calling. Pass an optional filter object \
91+ to bound large payloads; omit filter to return the backend response as-is ."
11092 }
11193
11294 fn input_schema ( & self ) -> Value {
@@ -129,7 +111,7 @@ impl MetaTool for InvokeToolTool {
129111 } ,
130112 "filter" : {
131113 "type" : "object" ,
132- "description" : "Optional result shaping. When omitted, large arrays are truncated with returned/total/truncated metadata ." ,
114+ "description" : "Optional result shaping (max_rows, max_bytes, fields, format). Omit to return the backend response as-is ." ,
133115 "properties" : {
134116 "max_rows" : {
135117 "type" : "integer" ,
@@ -176,7 +158,6 @@ impl MetaTool for InvokeToolTool {
176158 . to_string ( ) ;
177159 let args = call. args . get ( "args" ) . cloned ( ) . unwrap_or_else ( || json ! ( { } ) ) ;
178160 let filter = parse_invoke_filter ( call. args . get ( "filter" ) ) ;
179- let use_defaults = call. args . get ( "filter" ) . is_none ( ) ;
180161
181162 let resolved = caller_resolution ( & call) . await ?;
182163 let space_id = caller_space_id ( & call) . await ?;
@@ -292,12 +273,11 @@ impl MetaTool for InvokeToolTool {
292273 return Ok ( mcp_result) ;
293274 }
294275
295- let ( content, structured_content) = apply_invoke_result_filter (
296- result. content ,
297- result. structured_content ,
298- filter. as_ref ( ) ,
299- use_defaults,
300- ) ;
276+ let ( content, structured_content) = if let Some ( ref filter) = filter {
277+ apply_invoke_result_filter ( result. content , result. structured_content , filter)
278+ } else {
279+ ( result. content , result. structured_content )
280+ } ;
301281 let parsed_content: Vec < Content > = content
302282 . into_iter ( )
303283 . filter_map ( |v| serde_json:: from_value ( v) . ok ( ) )
@@ -312,72 +292,69 @@ impl MetaTool for InvokeToolTool {
312292}
313293
314294/// Shape one MCP content block (typically `{ "type": "text", "text": "..." }`).
315- fn shape_content_block ( block : Value , filter : & InvokeResultFilter , use_defaults : bool ) -> Value {
295+ fn shape_content_block ( block : Value , filter : & InvokeResultFilter ) -> Value {
316296 let Some ( text) = block. get ( "text" ) . and_then ( |v| v. as_str ( ) ) else {
317297 return block;
318298 } ;
319299
320300 if let Ok ( parsed) = serde_json:: from_str :: < Value > ( text) {
321- let shaped = shape_json_value ( parsed, filter, use_defaults ) ;
301+ let shaped = shape_json_value ( parsed, filter) ;
322302 return json ! ( {
323303 "type" : "text" ,
324304 "text" : shaped. to_string( ) ,
325305 } ) ;
326306 }
327307
328- let max_bytes = filter. effective_max_bytes ( use_defaults) ;
329- let Some ( max_bytes) = max_bytes else {
308+ let Some ( max_bytes) = filter. max_bytes else {
330309 return block;
331310 } ;
332311 if text. len ( ) <= max_bytes {
333312 return block;
334313 }
335314
336- let mut truncated = text[ ..max_bytes] . to_string ( ) ;
337- truncated. push_str ( "\n ...[truncated]" ) ;
315+ let envelope = byte_truncation_envelope ( text, max_bytes) ;
338316 json ! ( {
339317 "type" : "text" ,
340- "text" : truncated ,
318+ "text" : envelope . to_string ( ) ,
341319 } )
342320}
343321
344- /// Shape a JSON value, applying smart truncation for large arrays when enabled .
345- pub fn shape_json_value ( value : Value , filter : & InvokeResultFilter , use_defaults : bool ) -> Value {
322+ /// Shape a JSON value, applying truncation when explicit filter limits are set .
323+ pub fn shape_json_value ( value : Value , filter : & InvokeResultFilter ) -> Value {
346324 match value {
347- Value :: Array ( items) => shape_array ( items, filter, use_defaults , "items" ) ,
348- Value :: Object ( map) => shape_object ( map, filter, use_defaults ) ,
349- other => enforce_byte_limit ( other, filter, use_defaults ) ,
325+ Value :: Array ( items) => shape_array ( items, filter, "items" ) ,
326+ Value :: Object ( map) => shape_object ( map, filter) ,
327+ other => enforce_byte_limit ( other, filter) ,
350328 }
351329}
352330
353- fn shape_object ( map : Map < String , Value > , filter : & InvokeResultFilter , use_defaults : bool ) -> Value {
331+ fn shape_object ( map : Map < String , Value > , filter : & InvokeResultFilter ) -> Value {
354332 for key in HEAVY_ARRAY_KEYS {
355333 if let Some ( Value :: Array ( items) ) = map. get ( * key) . cloned ( ) {
356- if should_truncate ( items. len ( ) , filter, use_defaults ) {
357- return shape_object_with_truncated_array ( map, key, items, filter, use_defaults ) ;
334+ if should_truncate ( items. len ( ) , filter) {
335+ return shape_object_with_truncated_array ( map, key, items, filter) ;
358336 }
359337 }
360338 }
361339
362340 for ( key, value) in & map {
363341 if let Value :: Array ( items) = value {
364- if should_truncate ( items. len ( ) , filter, use_defaults ) {
365- return shape_object_with_truncated_array ( map. clone ( ) , key, items. clone ( ) , filter, use_defaults ) ;
342+ if should_truncate ( items. len ( ) , filter) {
343+ return shape_object_with_truncated_array ( map. clone ( ) , key, items. clone ( ) , filter) ;
366344 }
367345 }
368346 }
369347
370- enforce_byte_limit ( Value :: Object ( map) , filter, use_defaults )
348+ enforce_byte_limit ( Value :: Object ( map) , filter)
371349}
372350
373351fn shape_object_with_truncated_array (
374352 mut map : Map < String , Value > ,
375353 array_key : & str ,
376354 items : Vec < Value > ,
377355 filter : & InvokeResultFilter ,
378- use_defaults : bool ,
379356) -> Value {
380- let shaped_array = shape_array ( items, filter, use_defaults , array_key) ;
357+ let shaped_array = shape_array ( items, filter, array_key) ;
381358 if let Value :: Object ( truncation) = & shaped_array {
382359 if truncation. get ( "truncated" ) == Some ( & Value :: Bool ( true ) ) {
383360 for ( meta_key, meta_value) in truncation {
@@ -388,25 +365,19 @@ fn shape_object_with_truncated_array(
388365 if let Some ( data) = truncation. get ( array_key) {
389366 map. insert ( array_key. to_string ( ) , data. clone ( ) ) ;
390367 }
391- return enforce_byte_limit ( Value :: Object ( map) , filter, use_defaults ) ;
368+ return enforce_byte_limit ( Value :: Object ( map) , filter) ;
392369 }
393370 }
394371
395372 map. insert ( array_key. to_string ( ) , shaped_array) ;
396- enforce_byte_limit ( Value :: Object ( map) , filter, use_defaults )
373+ enforce_byte_limit ( Value :: Object ( map) , filter)
397374}
398375
399- fn shape_array (
400- items : Vec < Value > ,
401- filter : & InvokeResultFilter ,
402- use_defaults : bool ,
403- data_key : & str ,
404- ) -> Value {
376+ fn shape_array ( items : Vec < Value > , filter : & InvokeResultFilter , data_key : & str ) -> Value {
405377 let total = items. len ( ) ;
406378 let filtered_items = apply_fields_filter ( items, filter) ;
407- let max_rows = filter. effective_max_rows ( use_defaults) ;
408379
409- let Some ( max_rows) = max_rows else {
380+ let Some ( max_rows) = filter . max_rows else {
410381 return Value :: Array ( filtered_items) ;
411382 } ;
412383
@@ -455,15 +426,15 @@ fn pick_fields(value: Value, fields: &[String]) -> Value {
455426 Value :: Object ( picked)
456427}
457428
458- fn should_truncate ( length : usize , filter : & InvokeResultFilter , use_defaults : bool ) -> bool {
459- match filter. effective_max_rows ( use_defaults ) {
429+ fn should_truncate ( length : usize , filter : & InvokeResultFilter ) -> bool {
430+ match filter. max_rows {
460431 Some ( max_rows) => length > max_rows,
461432 None => false ,
462433 }
463434}
464435
465- fn enforce_byte_limit ( value : Value , filter : & InvokeResultFilter , use_defaults : bool ) -> Value {
466- let Some ( max_bytes) = filter. effective_max_bytes ( use_defaults ) else {
436+ fn enforce_byte_limit ( value : Value , filter : & InvokeResultFilter ) -> Value {
437+ let Some ( max_bytes) = filter. max_bytes else {
467438 return value;
468439 } ;
469440
@@ -472,8 +443,13 @@ fn enforce_byte_limit(value: Value, filter: &InvokeResultFilter, use_defaults: b
472443 return value;
473444 }
474445
475- let total_bytes = serialized. len ( ) ;
476- let mut truncated = serialized;
446+ byte_truncation_envelope ( & serialized, max_bytes)
447+ }
448+
449+ /// Build a `{ returned, total, truncated, text }` envelope for byte-capped plain text or JSON.
450+ fn byte_truncation_envelope ( text : & str , max_bytes : usize ) -> Value {
451+ let total_bytes = text. len ( ) ;
452+ let mut truncated = text. to_string ( ) ;
477453 truncated. truncate ( max_bytes) ;
478454 truncated. push_str ( "...[truncated]" ) ;
479455 json ! ( {
@@ -498,22 +474,20 @@ mod tests {
498474 use super :: * ;
499475
500476 #[ test]
501- fn default_truncates_large_top_level_array ( ) {
477+ fn no_filter_passes_through_large_array ( ) {
502478 let items: Vec < Value > = ( 0 ..100 ) . map ( |i| json ! ( { "id" : i, "name" : format!( "n{i}" ) } ) ) . collect ( ) ;
503- let shaped = shape_json_value ( Value :: Array ( items) , & InvokeResultFilter :: default ( ) , true ) ;
504- assert_eq ! ( shaped. get( "returned" ) , Some ( & json!( 50 ) ) ) ;
505- assert_eq ! ( shaped. get( "total" ) , Some ( & json!( 100 ) ) ) ;
506- assert_eq ! ( shaped. get( "truncated" ) , Some ( & json!( true ) ) ) ;
479+ let shaped = shape_json_value ( Value :: Array ( items. clone ( ) ) , & InvokeResultFilter :: default ( ) ) ;
480+ assert_eq ! ( shaped, Value :: Array ( items) ) ;
507481 }
508482
509483 #[ test]
510- fn explicit_max_rows_overrides_default ( ) {
484+ fn explicit_max_rows_truncates ( ) {
511485 let items: Vec < Value > = ( 0 ..20 ) . map ( |i| json ! ( { "id" : i } ) ) . collect ( ) ;
512486 let filter = InvokeResultFilter {
513487 max_rows : Some ( 3 ) ,
514488 ..Default :: default ( )
515489 } ;
516- let shaped = shape_json_value ( Value :: Array ( items) , & filter, false ) ;
490+ let shaped = shape_json_value ( Value :: Array ( items) , & filter) ;
517491 assert_eq ! ( shaped. get( "returned" ) , Some ( & json!( 3 ) ) ) ;
518492 assert_eq ! ( shaped. get( "total" ) , Some ( & json!( 20 ) ) ) ;
519493 assert_eq ! ( shaped. get( "truncated" ) , Some ( & json!( true ) ) ) ;
@@ -529,9 +503,23 @@ mod tests {
529503 fields : Some ( vec ! [ "id" . into( ) , "name" . into( ) ] ) ,
530504 ..Default :: default ( )
531505 } ;
532- let shaped = shape_json_value ( Value :: Array ( items) , & filter, false ) ;
506+ let shaped = shape_json_value ( Value :: Array ( items) , & filter) ;
533507 let kept = shaped. as_array ( ) . unwrap ( ) ;
534508 assert_eq ! ( kept[ 0 ] , json!( { "id" : 1 , "name" : "a" } ) ) ;
535509 assert_eq ! ( kept[ 1 ] , json!( { "id" : 2 , "name" : "b" } ) ) ;
536510 }
511+
512+ #[ test]
513+ fn plain_text_byte_trunc_includes_metadata ( ) {
514+ let text = "x" . repeat ( 100 ) ;
515+ let filter = InvokeResultFilter {
516+ max_bytes : Some ( 50 ) ,
517+ ..Default :: default ( )
518+ } ;
519+ let block = json ! ( { "type" : "text" , "text" : text } ) ;
520+ let shaped = shape_content_block ( block, & filter) ;
521+ let parsed: Value = serde_json:: from_str ( shaped. get ( "text" ) . unwrap ( ) . as_str ( ) . unwrap ( ) ) . unwrap ( ) ;
522+ assert_eq ! ( parsed. get( "truncated" ) , Some ( & json!( true ) ) ) ;
523+ assert_eq ! ( parsed. get( "total" ) , Some ( & json!( 100 ) ) ) ;
524+ }
537525}
0 commit comments