Skip to content

Commit 6bb77e6

Browse files
anvansterclaude
andcommitted
feat(mcp): compact tool-result serialization — phase 1 output compaction
The MCP tool-call choke point pretty-printed every result; the consumer is an LLM agent that parses JSON identically with or without indentation, so the indentation was pure cost. Default the tool-call text channel to compact JSON. Measured on a representative symbol_search payload (25 hits): 8742 → 6676 chars, ~24% smaller, ~517 tokens/call — lossless, zero schema change, no parseability risk. symbol_search is the most-called tool fleet-wide (~3k installs), so this compounds. Scope: only the MCP tool-call hot path. The --run-tool CLI (human output piped to PR comments) and resource stats stay pretty. CODEGRAPH_MCP_PRETTY=1 restores indentation for inspection. Split into serialize_tool_result (env) + serialize_tool_result_mode (pure) so the win is unit-tested deterministically. Phase 2 (per-tool structural: columnar rows + path legend) measured at a further ~48% on top (60% total vs pretty) but carries agent-comprehension cost — deferred until phase 1's resultSizeBucket delta is read. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8bc90ec commit 6bb77e6

1 file changed

Lines changed: 51 additions & 2 deletions

File tree

crates/codegraph-server/src/mcp/server.rs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,29 @@ fn safe_tool_name(name: &str) -> &str {
5959
}
6060
}
6161

62+
/// Serialize a tool result for the MCP text channel. Defaults to COMPACT JSON
63+
/// (no pretty-print indentation) — the consumer is an LLM agent that parses
64+
/// JSON identically either way, while pretty-printing is ~25-40% pure
65+
/// whitespace on codegraph's nested results. At ~3k installs with
66+
/// `symbol_search` alone returning thousands of multi-KB results per day, this
67+
/// is a lossless, zero-schema, fleet-wide token cut.
68+
///
69+
/// Set `CODEGRAPH_MCP_PRETTY=1` to restore the indented form for human
70+
/// inspection. This is phase 1 of MCP output compaction; per-tool structural
71+
/// compaction (columnar rows, path legends, default caps) layers on top later,
72+
/// each validated against the `resultSizeBucket` telemetry.
73+
fn serialize_tool_result(result: &Value) -> String {
74+
serialize_tool_result_mode(result, std::env::var_os("CODEGRAPH_MCP_PRETTY").is_some())
75+
}
76+
77+
fn serialize_tool_result_mode(result: &Value, pretty: bool) -> String {
78+
if pretty {
79+
serde_json::to_string_pretty(result).unwrap_or_else(|_| result.to_string())
80+
} else {
81+
serde_json::to_string(result).unwrap_or_else(|_| result.to_string())
82+
}
83+
}
84+
6285
use super::protocol::*;
6386
use super::resources::get_all_resources;
6487
use super::tools::{get_all_tools, tool_in_profile, ToolProfile};
@@ -1638,8 +1661,7 @@ impl McpServer {
16381661
}));
16391662
let tool_result = ToolCallResult {
16401663
content: vec![ToolResultContent::Text {
1641-
text: serde_json::to_string_pretty(&result)
1642-
.unwrap_or_else(|_| result.to_string()),
1664+
text: serialize_tool_result(&result),
16431665
}],
16441666
is_error: None,
16451667
};
@@ -4857,6 +4879,33 @@ fn parse_node_id(s: &str) -> Option<codegraph::NodeId> {
48574879
mod quarantine_tests {
48584880
use super::McpBackend;
48594881

4882+
#[test]
4883+
fn compact_serialization_drops_whitespace_losslessly() {
4884+
let v = serde_json::json!({
4885+
"results": [
4886+
{"path": "src/a.rs", "name": "foo", "line": 12},
4887+
{"path": "src/b.rs", "name": "bar", "line": 34}
4888+
],
4889+
"total": 2
4890+
});
4891+
let compact = super::serialize_tool_result_mode(&v, false);
4892+
let pretty = super::serialize_tool_result_mode(&v, true);
4893+
4894+
// Compact has no indentation newlines; pretty does.
4895+
assert!(!compact.contains('\n'), "compact must be single-line");
4896+
assert!(pretty.contains('\n'), "pretty must be multi-line");
4897+
assert!(compact.len() < pretty.len(), "compact must be smaller");
4898+
// Lossless: both parse back to the identical value.
4899+
assert_eq!(
4900+
serde_json::from_str::<serde_json::Value>(&compact).unwrap(),
4901+
serde_json::from_str::<serde_json::Value>(&pretty).unwrap()
4902+
);
4903+
assert_eq!(
4904+
serde_json::from_str::<serde_json::Value>(&compact).unwrap(),
4905+
v
4906+
);
4907+
}
4908+
48604909
#[test]
48614910
fn quarantine_moves_db_dir_aside_to_fixed_name() {
48624911
let tmp = tempfile::tempdir().unwrap();

0 commit comments

Comments
 (0)