Format massive JSON arrays into Markdown natively to avoid context blowouts
This commit is contained in:
1 parent
760ebfcd40
commit
3ee95f5c39
4 files changed
+157
-42
No files matched your search
+76
-18
@@ -498,15 +498,37 @@ impl McpTool for FindSymbolReferencesHandler {
|
|||||||
|
|
||||||
if matches.len() < limit {
|
if matches.len() < limit {
|
||||||
let remaining = limit - matches.len();
|
let remaining = limit - matches.len();
|
||||||
|
let target_sym_clone = target_sym.clone();
|
||||||
let disk_matches = tokio::task::spawn_blocking(move || {
|
let disk_matches = tokio::task::spawn_blocking(move || {
|
||||||
scan_workspace_for_symbol(&target_sym, remaining, false, custom_dir)
|
scan_workspace_for_symbol(&target_sym_clone, remaining, false, custom_dir)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
matches.extend(disk_matches);
|
matches.extend(disk_matches);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(serde_yaml::to_string(&matches)?)
|
let mut out = String::new();
|
||||||
|
out.push_str(&format!("## Symbol References for `{}`\n\n", target_sym));
|
||||||
|
for match_item in &matches {
|
||||||
|
if let Some(source) = match_item.get("source").and_then(|s| s.as_str()) {
|
||||||
|
if source == "snippet" {
|
||||||
|
let name = match_item.get("name").and_then(|n| n.as_str()).unwrap_or("Unknown");
|
||||||
|
let code = match_item.get("code").and_then(|c| c.as_str()).unwrap_or("");
|
||||||
|
out.push_str(&format!("### Snippet: {}\n```rust\n{}\n```\n\n", name, code));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let file = match_item.get("file_path").and_then(|f| f.as_str()).unwrap_or("Unknown");
|
||||||
|
let line = match_item.get("line").and_then(|l| l.as_u64()).unwrap_or(0);
|
||||||
|
let content = match_item.get("content").and_then(|c| c.as_str()).unwrap_or("");
|
||||||
|
out.push_str(&format!("- `{}:{}`: `{}`\n", file, line, content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches.is_empty() {
|
||||||
|
out.push_str("No references found.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,15 +576,37 @@ impl McpTool for GetCallersHandler {
|
|||||||
|
|
||||||
if callers.len() < limit {
|
if callers.len() < limit {
|
||||||
let remaining = limit - callers.len();
|
let remaining = limit - callers.len();
|
||||||
|
let target_fn_clone = target_fn.clone();
|
||||||
let disk_callers = tokio::task::spawn_blocking(move || {
|
let disk_callers = tokio::task::spawn_blocking(move || {
|
||||||
scan_workspace_for_symbol(&target_fn, remaining, true, custom_dir)
|
scan_workspace_for_symbol(&target_fn_clone, remaining, true, custom_dir)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
callers.extend(disk_callers);
|
callers.extend(disk_callers);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(serde_yaml::to_string(&callers)?)
|
let mut out = String::new();
|
||||||
|
out.push_str(&format!("## Callers for `{}`\n\n", target_fn));
|
||||||
|
for caller in &callers {
|
||||||
|
if let Some(source) = caller.get("source").and_then(|s| s.as_str()) {
|
||||||
|
if source == "snippet" {
|
||||||
|
let name = caller.get("name").and_then(|n| n.as_str()).unwrap_or("Unknown");
|
||||||
|
let code = caller.get("code").and_then(|c| c.as_str()).unwrap_or("");
|
||||||
|
out.push_str(&format!("### Snippet: {}\n```rust\n{}\n```\n\n", name, code));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let file = caller.get("file_path").and_then(|f| f.as_str()).unwrap_or("Unknown");
|
||||||
|
let line = caller.get("line").and_then(|l| l.as_u64()).unwrap_or(0);
|
||||||
|
let content = caller.get("content").and_then(|c| c.as_str()).unwrap_or("");
|
||||||
|
out.push_str(&format!("- `{}:{}`: `{}`\n", file, line, content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if callers.is_empty() {
|
||||||
|
out.push_str("No callers found.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,21 +681,35 @@ impl McpTool for AnalyzeImpactHandler {
|
|||||||
"LOW"
|
"LOW"
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = serde_json::json!({
|
let mut out = String::new();
|
||||||
"target_symbol": sym,
|
out.push_str(&format!("## Impact Analysis for `{}`\n\n", sym));
|
||||||
"risk_level": risk_level,
|
out.push_str(&format!("**Risk Level:** {}\n\n", risk_level));
|
||||||
"ast_callers_count": caller_count,
|
|
||||||
"ast_callers_sample": callers.into_iter().take(10).collect::<Vec<_>>(),
|
|
||||||
"graph_relations_count": graph_count,
|
|
||||||
"graph_relations": kg_connected,
|
|
||||||
"recommendation": match risk_level {
|
|
||||||
"CRITICAL" | "HIGH" => "Requires comprehensive unit test verification and backwards compatibility checks before modifying.",
|
|
||||||
"MEDIUM" => "Verify direct call sites and run affected module tests.",
|
|
||||||
_ => "Safe to modify with standard unit test verification.",
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(serde_yaml::to_string(&result)?)
|
let rec = match risk_level {
|
||||||
|
"CRITICAL" | "HIGH" => "Requires comprehensive unit test verification and backwards compatibility checks before modifying.",
|
||||||
|
"MEDIUM" => "Verify direct call sites and run affected module tests.",
|
||||||
|
_ => "Safe to modify with standard unit test verification.",
|
||||||
|
};
|
||||||
|
out.push_str(&format!("**Recommendation:** {}\n\n", rec));
|
||||||
|
|
||||||
|
out.push_str(&format!("### AST Callers ({} total, showing up to 10)\n", caller_count));
|
||||||
|
for c in callers.into_iter().take(10) {
|
||||||
|
out.push_str(&format!("- `{}`\n", c));
|
||||||
|
}
|
||||||
|
if caller_count == 0 {
|
||||||
|
out.push_str("No callers found.\n");
|
||||||
|
}
|
||||||
|
out.push_str("\n");
|
||||||
|
|
||||||
|
out.push_str(&format!("### Graph Relations ({})\n", graph_count));
|
||||||
|
for g in kg_connected {
|
||||||
|
out.push_str(&format!("- {}\n", g));
|
||||||
|
}
|
||||||
|
if graph_count == 0 {
|
||||||
|
out.push_str("No graph relations found.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+49
-14
@@ -27,7 +27,7 @@ impl McpTool for GetActiveWorktreeContextHandler {
|
|||||||
_state: Arc<MemoryState>,
|
_state: Arc<MemoryState>,
|
||||||
) -> crate::error::Result<String> {
|
) -> crate::error::Result<String> {
|
||||||
let result =
|
let result =
|
||||||
tokio::task::spawn_blocking(move || -> crate::error::Result<serde_json::Value> {
|
tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
|
||||||
let cwd = env::current_dir().map_err(|e| crate::error::AppError::Internal(format!("Failed to get current directory: {}", e)))?;
|
let cwd = env::current_dir().map_err(|e| crate::error::AppError::Internal(format!("Failed to get current directory: {}", e)))?;
|
||||||
let repo = git2::Repository::discover(&cwd).map_err(|e| {
|
let repo = git2::Repository::discover(&cwd).map_err(|e| {
|
||||||
crate::error::AppError::Internal(format!("Not in a git repository: {}", e))
|
crate::error::AppError::Internal(format!("Not in a git repository: {}", e))
|
||||||
@@ -79,16 +79,34 @@ impl McpTool for GetActiveWorktreeContextHandler {
|
|||||||
diff_output.push_str("\n... [Diff truncated due to size]");
|
diff_output.push_str("\n... [Diff truncated due to size]");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(json!({
|
let mut out = String::new();
|
||||||
"branch": branch_name,
|
out.push_str("## Active Worktree Context\n\n");
|
||||||
"modified_files": files,
|
out.push_str(&format!("**Branch:** `{}`\n\n", branch_name));
|
||||||
"diff": diff_output
|
|
||||||
}))
|
out.push_str("### Modified Files\n");
|
||||||
|
if files.is_empty() {
|
||||||
|
out.push_str("No modified files.\n");
|
||||||
|
} else {
|
||||||
|
for f in files {
|
||||||
|
out.push_str(&format!("- `{}`\n", f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push_str("\n### Diff\n");
|
||||||
|
if diff_output.is_empty() {
|
||||||
|
out.push_str("No changes.\n");
|
||||||
|
} else {
|
||||||
|
out.push_str("```diff\n");
|
||||||
|
out.push_str(&diff_output);
|
||||||
|
out.push_str("\n```\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
||||||
|
|
||||||
Ok::<String, crate::error::AppError>(serde_yaml::to_string(&result)?)
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +188,25 @@ impl McpTool for QueryGitDiffsHandler {
|
|||||||
matches.extend(git_matches);
|
matches.extend(git_matches);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(serde_yaml::to_string(&matches)?)
|
let mut out = String::new();
|
||||||
|
out.push_str(&format!("## Git Diffs for `{}`\n\n", req.query));
|
||||||
|
for m in &matches {
|
||||||
|
if let Some(file) = m.get("file_path").and_then(|f| f.as_str()) {
|
||||||
|
let desc = m.get("description").and_then(|d| d.as_str()).unwrap_or("");
|
||||||
|
let commit = m.get("commit").and_then(|c| c.as_str()).unwrap_or("");
|
||||||
|
out.push_str(&format!("- **File:** `{}` | **Commit:** `{}`\n - {}\n", file, commit, desc));
|
||||||
|
} else {
|
||||||
|
let commit = m.get("commit_id").and_then(|c| c.as_str()).unwrap_or("");
|
||||||
|
let author = m.get("author").and_then(|a| a.as_str()).unwrap_or("");
|
||||||
|
let msg = m.get("message").and_then(|m| m.as_str()).unwrap_or("");
|
||||||
|
out.push_str(&format!("- **Commit:** `{}` | **Author:** {}\n - {}\n", commit, author, msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches.is_empty() {
|
||||||
|
out.push_str("No git diffs found.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,10 +229,10 @@ mod tests {
|
|||||||
.map_err(|e| format!("Failed to get worktree context: {}", e))
|
.map_err(|e| format!("Failed to get worktree context: {}", e))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let parsed: serde_json::Value = serde_yaml::from_str(&result).unwrap();
|
assert!(result.contains("## Active Worktree Context"));
|
||||||
assert!(parsed.get("branch").is_some());
|
assert!(result.contains("**Branch:**"));
|
||||||
assert!(parsed.get("modified_files").is_some());
|
assert!(result.contains("### Modified Files"));
|
||||||
assert!(parsed.get("diff").is_some());
|
assert!(result.contains("### Diff"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -212,7 +248,6 @@ mod tests {
|
|||||||
.map_err(|e| format!("Failed to get worktree context: {}", e))
|
.map_err(|e| format!("Failed to get worktree context: {}", e))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let parsed: serde_json::Value = serde_yaml::from_str(&result).unwrap();
|
assert!(result.contains("## Active Worktree Context"));
|
||||||
assert!(parsed.get("branch").is_some() || parsed.is_object());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1279,16 +1279,38 @@ impl McpTool for SweepGraphHealthHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let report = serde_json::json!({
|
let mut out = String::new();
|
||||||
"orphaned_entities": orphans,
|
out.push_str("## Graph Health Report\n\n");
|
||||||
"orphans_pruned": auto_prune,
|
let score = if orphans.is_empty() && duplicates.is_empty() && stale_entities.is_empty() { "100% (Healthy)" } else { "Needs Maintenance" };
|
||||||
"stale_entities": stale_entities,
|
out.push_str(&format!("**Health Status:** {}\n\n", score));
|
||||||
"stale_pruned": auto_prune_stale,
|
|
||||||
"potential_duplicates": duplicates,
|
|
||||||
"health_score": if orphans.is_empty() && duplicates.is_empty() && stale_entities.is_empty() { "100%" } else { "Needs Maintenance" }
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(serde_yaml::to_string(&report)?)
|
out.push_str(&format!("### Orphaned Entities ({})\n", orphans.len()));
|
||||||
|
if auto_prune {
|
||||||
|
out.push_str("*These orphans were automatically pruned.*\n");
|
||||||
|
}
|
||||||
|
for o in orphans {
|
||||||
|
out.push_str(&format!("- `{}`\n", o));
|
||||||
|
}
|
||||||
|
out.push_str("\n");
|
||||||
|
|
||||||
|
out.push_str(&format!("### Stale Entities ({})\n", stale_entities.len()));
|
||||||
|
if auto_prune_stale {
|
||||||
|
out.push_str("*These stale entities were automatically pruned.*\n");
|
||||||
|
}
|
||||||
|
for s in stale_entities {
|
||||||
|
out.push_str(&format!("- `{}`\n", s));
|
||||||
|
}
|
||||||
|
out.push_str("\n");
|
||||||
|
|
||||||
|
out.push_str(&format!("### Potential Duplicates ({})\n", duplicates.len()));
|
||||||
|
for d in duplicates {
|
||||||
|
let a = d.get("entity_a").and_then(|x| x.as_str()).unwrap_or("?");
|
||||||
|
let b = d.get("entity_b").and_then(|x| x.as_str()).unwrap_or("?");
|
||||||
|
let act = d.get("suggested_action").and_then(|x| x.as_str()).unwrap_or("");
|
||||||
|
out.push_str(&format!("- `{}` and `{}`\n - Suggested: `{}`\n", a, b, act));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2336,7 +2336,7 @@ mod tests {
|
|||||||
.execute(serde_json::json!({}), state.clone())
|
.execute(serde_json::json!({}), state.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(sweep_h_res.contains("orphaned_entities"));
|
assert!(sweep_h_res.contains("Orphaned Entities"));
|
||||||
|
|
||||||
// AutoSessionCheckpoint
|
// AutoSessionCheckpoint
|
||||||
let chk = AutoSessionCheckpointHandler;
|
let chk = AutoSessionCheckpointHandler;
|
||||||
|
|||||||
Reference in new issue
Block a user