Skip to content

Commit 36f05cf

Browse files
maplenkclaude
andcommitted
Add get_session_summary markdown tool for context recovery (Phase 7C)
New MCP tool returns a compact markdown summary of the session: files touched (read/edited), symbols investigated with PageRank enrichment, impact analyses run, areas explored, and suggested next steps (unexamined graph neighbors ranked by PageRank). Designed for context recovery after Claude Code compaction — call get_session_summary to instantly restore the full session picture. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9bd695d commit 36f05cf

2 files changed

Lines changed: 315 additions & 3 deletions

File tree

src/mcp/mcp.c

Lines changed: 224 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,15 @@ static const tool_def_t TOOLS[] = {
884884
"\"description\":\"Include graph neighbors of touched symbols that have not been "
885885
"examined yet.\"},\"limit\":{\"type\":\"integer\",\"default\":10,"
886886
"\"description\":\"Max related_untouched items.\"}},\"required\":[]}"},
887+
888+
{"get_session_summary",
889+
"Compact markdown session summary for context recovery after compaction. "
890+
"Shows files touched, symbols investigated with PageRank, areas explored, "
891+
"and suggested next steps.",
892+
"{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\","
893+
"\"description\":\"Project name (needed for PageRank enrichment and next-step "
894+
"suggestions).\"},\"max_tokens\":{\"type\":\"integer\",\"default\":2000,"
895+
"\"description\":\"Maximum output size.\"}},\"required\":[]}"},
887896
};
888897

889898
static const int TOOL_COUNT = sizeof(TOOLS) / sizeof(TOOLS[0]);
@@ -5962,7 +5971,7 @@ static void maybe_add_session_hint(yyjson_mut_doc *doc, yyjson_mut_val *root, co
59625971
}
59635972
}
59645973

5965-
/* ── Session context (Phase 7A) ────────────────────────────────── */
5974+
/* ── Session helpers (shared by 7A + 7C) ──────────────────────── */
59665975

59675976
/* Callback: free a strdup'd hash table key (for temporary candidate sets). */
59685977
static void free_ht_key_cb(const char *key, void *value, void *userdata) {
@@ -5971,7 +5980,7 @@ static void free_ht_key_cb(const char *key, void *value, void *userdata) {
59715980
free((void *)key);
59725981
}
59735982

5974-
/* Callback: append key to a yyjson array. */
5983+
/* Callback: append key to a yyjson array (used by get_session_context). */
59755984
typedef struct {
59765985
yyjson_mut_doc *doc;
59775986
yyjson_mut_val *arr;
@@ -5982,7 +5991,7 @@ static void append_key_to_json_arr(const char *key, void *userdata) {
59825991
yyjson_mut_arr_add_strcpy(ctx->doc, ctx->arr, key);
59835992
}
59845993

5985-
/* Callback: collect symbol names into a list for related_untouched lookup. */
5994+
/* Callback: collect symbol names into a list for neighbor lookup. */
59865995
typedef struct {
59875996
const char **names;
59885997
int count;
@@ -5996,6 +6005,215 @@ static void collect_symbol_name(const char *key, void *userdata) {
59966005
}
59976006
}
59986007

6008+
/* ── Session summary (Phase 7C) ────────────────────────────────── */
6009+
6010+
/* Callback context for iterating session sets into markdown. */
6011+
typedef struct {
6012+
markdown_builder_t *md;
6013+
int count; /* items emitted so far */
6014+
} md_list_ctx_t;
6015+
6016+
static void append_key_comma_separated(const char *key, void *userdata) {
6017+
md_list_ctx_t *ctx = (md_list_ctx_t *)userdata;
6018+
if (ctx->count > 0) {
6019+
(void)markdown_builder_append_raw(ctx->md, ", ");
6020+
}
6021+
(void)markdown_builder_append_raw(ctx->md, key);
6022+
ctx->count++;
6023+
}
6024+
6025+
static void append_key_bullet(const char *key, void *userdata) {
6026+
md_list_ctx_t *ctx = (md_list_ctx_t *)userdata;
6027+
(void)markdown_builder_appendf(ctx->md, "- %s\n", key);
6028+
ctx->count++;
6029+
}
6030+
6031+
static char *handle_get_session_summary(cbm_mcp_server_t *srv, const char *args) {
6032+
char *project = cbm_mcp_get_string_arg(args, "project");
6033+
int max_tokens = cbm_mcp_get_int_arg(args, "max_tokens", DEFAULT_MAX_TOKENS);
6034+
6035+
cbm_session_state_t *ss = ensure_session(srv);
6036+
cbm_store_t *store = project ? resolve_store(srv, project) : NULL;
6037+
6038+
size_t char_budget = max_tokens_to_char_budget(max_tokens);
6039+
markdown_builder_t md;
6040+
markdown_builder_init(&md, char_budget);
6041+
6042+
/* ── Header ──────────────────────────────────────────────── */
6043+
time_t start = cbm_session_start_time(ss);
6044+
time_t now = time(NULL);
6045+
int elapsed = (int)(now - start);
6046+
if (elapsed < 0) elapsed = 0;
6047+
int minutes = elapsed / 60;
6048+
int seconds = elapsed % 60;
6049+
int qc = cbm_session_query_count(ss);
6050+
6051+
if (minutes > 0) {
6052+
(void)markdown_builder_appendf(&md, "## Session Summary (%d queries, %dm%ds)\n\n",
6053+
qc, minutes, seconds);
6054+
} else {
6055+
(void)markdown_builder_appendf(&md, "## Session Summary (%d queries, %ds)\n\n",
6056+
qc, seconds);
6057+
}
6058+
6059+
/* ── Files touched ───────────────────────────────────────── */
6060+
int read_count = cbm_session_files_read_count(ss);
6061+
int edited_count = cbm_session_files_edited_count(ss);
6062+
6063+
if (read_count > 0 || edited_count > 0) {
6064+
(void)markdown_builder_append_raw(&md, "### Files touched\n");
6065+
if (read_count > 0) {
6066+
(void)markdown_builder_append_raw(&md, "- **Read:** ");
6067+
md_list_ctx_t ctx = {.md = &md, .count = 0};
6068+
cbm_session_foreach_file_read(ss, append_key_comma_separated, &ctx);
6069+
(void)markdown_builder_append_raw(&md, "\n");
6070+
}
6071+
if (edited_count > 0) {
6072+
(void)markdown_builder_append_raw(&md, "- **Edited:** ");
6073+
md_list_ctx_t ctx = {.md = &md, .count = 0};
6074+
cbm_session_foreach_file_edited(ss, append_key_comma_separated, &ctx);
6075+
(void)markdown_builder_append_raw(&md, "\n");
6076+
}
6077+
(void)markdown_builder_append_raw(&md, "\n");
6078+
}
6079+
6080+
/* ── Symbols investigated ────────────────────────────────── */
6081+
int sym_count = cbm_session_symbols_count(ss);
6082+
int impact_count = cbm_session_impacts_count(ss);
6083+
6084+
if (sym_count > 0 || impact_count > 0) {
6085+
(void)markdown_builder_append_raw(&md, "### Symbols investigated\n");
6086+
6087+
/* Collect queried symbol names */
6088+
const char *sym_names[30];
6089+
name_collector_t sc = {.names = sym_names, .count = 0, .cap = 30};
6090+
cbm_session_foreach_symbol(ss, collect_symbol_name, &sc);
6091+
6092+
for (int i = 0; i < sc.count; i++) {
6093+
const char *name = sc.names[i];
6094+
6095+
/* Look up PageRank if store available */
6096+
if (store) {
6097+
cbm_key_symbol_t *ks = NULL;
6098+
int ks_count = 0;
6099+
cbm_store_get_key_symbols(store, project, name, 1, &ks, &ks_count);
6100+
if (ks_count > 0 && ks[0].name && strcmp(ks[0].name, name) == 0) {
6101+
(void)markdown_builder_appendf(&md, "- %s (%d callers, PageRank %.4f)",
6102+
name, ks[0].in_degree, ks[0].pagerank);
6103+
} else {
6104+
(void)markdown_builder_appendf(&md, "- %s", name);
6105+
}
6106+
cbm_store_key_symbols_free(ks, ks_count);
6107+
} else {
6108+
(void)markdown_builder_appendf(&md, "- %s", name);
6109+
}
6110+
(void)markdown_builder_append_raw(&md, "\n");
6111+
}
6112+
(void)markdown_builder_append_raw(&md, "\n");
6113+
}
6114+
6115+
/* ── Impact analyses ─────────────────────────────────────── */
6116+
if (impact_count > 0) {
6117+
(void)markdown_builder_append_raw(&md, "### Impact analyses run\n");
6118+
md_list_ctx_t ctx = {.md = &md, .count = 0};
6119+
cbm_session_foreach_impact(ss, append_key_bullet, &ctx);
6120+
(void)markdown_builder_append_raw(&md, "\n");
6121+
}
6122+
6123+
/* ── Areas explored ──────────────────────────────────────── */
6124+
int area_count = cbm_session_areas_count(ss);
6125+
if (area_count > 0) {
6126+
(void)markdown_builder_append_raw(&md, "### Areas explored\n");
6127+
md_list_ctx_t ctx = {.md = &md, .count = 0};
6128+
cbm_session_foreach_area(ss, append_key_bullet, &ctx);
6129+
(void)markdown_builder_append_raw(&md, "\n");
6130+
}
6131+
6132+
/* ── Suggested next steps ────────────────────────────────── */
6133+
if (store && sym_count > 0) {
6134+
{
6135+
/* Collect symbols for neighbor lookup */
6136+
const char *lookup_names[20];
6137+
name_collector_t nc = {.names = lookup_names, .count = 0, .cap = 20};
6138+
cbm_session_foreach_symbol(ss, collect_symbol_name, &nc);
6139+
cbm_session_foreach_impact(ss, collect_symbol_name, &nc);
6140+
6141+
/* Temporary dedup set for candidates */
6142+
CBMHashTable *candidates = cbm_ht_create(64);
6143+
for (int i = 0; i < nc.count; i++) {
6144+
cbm_node_t *nodes = NULL;
6145+
int ncount = 0;
6146+
cbm_store_find_nodes_by_name(store, project, lookup_names[i], &nodes, &ncount);
6147+
for (int j = 0; j < ncount; j++) {
6148+
char **callers = NULL;
6149+
char **callees = NULL;
6150+
int caller_count = 0, callee_count = 0;
6151+
cbm_store_node_neighbor_names(store, nodes[j].id, 10, &callers, &caller_count,
6152+
&callees, &callee_count);
6153+
for (int k = 0; k < caller_count; k++) {
6154+
if (callers[k] && !cbm_session_has_symbol(ss, callers[k]) &&
6155+
!cbm_ht_has(candidates, callers[k])) {
6156+
char *key = strdup(callers[k]);
6157+
if (key) cbm_ht_set(candidates, key, (void *)lookup_names[i]);
6158+
}
6159+
}
6160+
for (int k = 0; k < callee_count; k++) {
6161+
if (callees[k] && !cbm_session_has_symbol(ss, callees[k]) &&
6162+
!cbm_ht_has(candidates, callees[k])) {
6163+
char *key = strdup(callees[k]);
6164+
if (key) cbm_ht_set(candidates, key, (void *)lookup_names[i]);
6165+
}
6166+
}
6167+
for (int k = 0; k < caller_count; k++) free(callers[k]);
6168+
free(callers);
6169+
for (int k = 0; k < callee_count; k++) free(callees[k]);
6170+
free(callees);
6171+
}
6172+
cbm_store_free_nodes(nodes, ncount);
6173+
}
6174+
6175+
if (cbm_ht_count(candidates) > 0) {
6176+
cbm_key_symbol_t *key_syms = NULL;
6177+
int ks_count = 0;
6178+
cbm_store_get_key_symbols(store, project, NULL, 200, &key_syms, &ks_count);
6179+
6180+
bool header_emitted = false;
6181+
int emitted = 0;
6182+
for (int i = 0; i < ks_count && emitted < 5; i++) {
6183+
if (key_syms[i].name && cbm_ht_has(candidates, key_syms[i].name)) {
6184+
if (!header_emitted) {
6185+
(void)markdown_builder_append_raw(&md, "### Suggested next steps\n");
6186+
header_emitted = true;
6187+
}
6188+
const char *reason =
6189+
(const char *)cbm_ht_get(candidates, key_syms[i].name);
6190+
(void)markdown_builder_appendf(
6191+
&md, "- Examine %s%s%s (neighbor of %s, not yet examined)\n",
6192+
key_syms[i].name,
6193+
key_syms[i].file_path ? " in " : "",
6194+
key_syms[i].file_path ? key_syms[i].file_path : "",
6195+
reason ? reason : "queried symbol");
6196+
emitted++;
6197+
}
6198+
}
6199+
cbm_store_key_symbols_free(key_syms, ks_count);
6200+
}
6201+
6202+
cbm_ht_foreach(candidates, free_ht_key_cb, NULL);
6203+
cbm_ht_free(candidates);
6204+
}
6205+
}
6206+
6207+
char *markdown = markdown_builder_finish(&md);
6208+
free(project);
6209+
6210+
char *result = cbm_mcp_text_result(markdown ? markdown : "", false);
6211+
free(markdown);
6212+
return result;
6213+
}
6214+
6215+
/* ── Session context (Phase 7A) ────────────────────────────────── */
6216+
59996217
static char *handle_get_session_context(cbm_mcp_server_t *srv, const char *args) {
60006218
char *project = cbm_mcp_get_string_arg(args, "project");
60016219
bool include_related = cbm_mcp_get_bool_arg_default(args, "include_related", true);
@@ -6222,6 +6440,9 @@ char *cbm_mcp_handle_tool(cbm_mcp_server_t *srv, const char *tool_name, const ch
62226440
if (strcmp(tool_name, "get_session_context") == 0) {
62236441
return handle_get_session_context(srv, args_json);
62246442
}
6443+
if (strcmp(tool_name, "get_session_summary") == 0) {
6444+
return handle_get_session_summary(srv, args_json);
6445+
}
62256446

62266447
char msg[256];
62276448
snprintf(msg, sizeof(msg), "unknown tool: %s", tool_name);

tests/test_mcp.c

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2957,6 +2957,91 @@ TEST(session_has_area_membership) {
29572957
PASS();
29582958
}
29592959

2960+
/* ══════════════════════════════════════════════════════════════════
2961+
* SESSION SUMMARY (Phase 7C)
2962+
* ══════════════════════════════════════════════════════════════════ */
2963+
2964+
TEST(session_summary_empty) {
2965+
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
2966+
ASSERT_NOT_NULL(srv);
2967+
2968+
char *raw = cbm_mcp_handle_tool(srv, "get_session_summary", "{}");
2969+
ASSERT_NOT_NULL(raw);
2970+
char *text = extract_text_content(raw);
2971+
ASSERT_NOT_NULL(text);
2972+
ASSERT_NOT_NULL(strstr(text, "Session Summary"));
2973+
ASSERT_NOT_NULL(strstr(text, "0 queries"));
2974+
free(text);
2975+
free(raw);
2976+
cbm_mcp_server_free(srv);
2977+
PASS();
2978+
}
2979+
2980+
TEST(session_summary_after_tools) {
2981+
cbm_mcp_server_t *srv = setup_impact_server();
2982+
ASSERT_NOT_NULL(srv);
2983+
2984+
/* Call explore + understand to populate session */
2985+
char *r1 = cbm_mcp_handle_tool(srv, "explore",
2986+
"{\"project\":\"impact\",\"area\":\"Order\"}");
2987+
free(r1);
2988+
char *r2 = cbm_mcp_handle_tool(srv, "understand",
2989+
"{\"project\":\"impact\",\"symbol\":\"ProcessOrder\"}");
2990+
free(r2);
2991+
2992+
char *raw = cbm_mcp_handle_tool(srv, "get_session_summary",
2993+
"{\"project\":\"impact\"}");
2994+
ASSERT_NOT_NULL(raw);
2995+
char *text = extract_text_content(raw);
2996+
ASSERT_NOT_NULL(text);
2997+
2998+
/* Should contain markdown structure */
2999+
ASSERT_NOT_NULL(strstr(text, "Session Summary"));
3000+
/* Should mention areas explored */
3001+
ASSERT_NOT_NULL(strstr(text, "Areas explored"));
3002+
ASSERT_NOT_NULL(strstr(text, "Order"));
3003+
/* Should mention symbols */
3004+
ASSERT_NOT_NULL(strstr(text, "Symbols investigated"));
3005+
ASSERT_NOT_NULL(strstr(text, "ProcessOrder"));
3006+
3007+
free(text);
3008+
free(raw);
3009+
cbm_mcp_server_free(srv);
3010+
PASS();
3011+
}
3012+
3013+
TEST(session_summary_with_impact) {
3014+
cbm_mcp_server_t *srv = setup_impact_server();
3015+
ASSERT_NOT_NULL(srv);
3016+
3017+
/* Run impact analysis to populate session */
3018+
char *r1 = cbm_mcp_handle_tool(srv, "get_impact_analysis",
3019+
"{\"project\":\"impact\",\"symbol\":\"ProcessOrder\"}");
3020+
free(r1);
3021+
3022+
char *raw = cbm_mcp_handle_tool(srv, "get_session_summary",
3023+
"{\"project\":\"impact\"}");
3024+
ASSERT_NOT_NULL(raw);
3025+
char *text = extract_text_content(raw);
3026+
ASSERT_NOT_NULL(text);
3027+
ASSERT_NOT_NULL(strstr(text, "Session Summary"));
3028+
ASSERT_NOT_NULL(strstr(text, "Impact analyses"));
3029+
ASSERT_NOT_NULL(strstr(text, "ProcessOrder"));
3030+
3031+
free(text);
3032+
free(raw);
3033+
cbm_mcp_server_free(srv);
3034+
PASS();
3035+
}
3036+
3037+
TEST(session_summary_tools_list) {
3038+
char *tools_json = cbm_mcp_tools_list();
3039+
ASSERT_NOT_NULL(tools_json);
3040+
ASSERT_NOT_NULL(strstr(tools_json, "get_session_summary"));
3041+
free(tools_json);
3042+
PASS();
3043+
}
3044+
29603045
/* ══════════════════════════════════════════════════════════════════
29613046
* SUITE
29623047
* ══════════════════════════════════════════════════════════════════ */
@@ -3136,4 +3221,10 @@ SUITE(mcp) {
31363221
RUN_TEST(session_hint_prepare_change_edited_file);
31373222
RUN_TEST(session_hint_not_present_first_call);
31383223
RUN_TEST(session_has_area_membership);
3224+
3225+
/* Session summary (Phase 7C) */
3226+
RUN_TEST(session_summary_empty);
3227+
RUN_TEST(session_summary_after_tools);
3228+
RUN_TEST(session_summary_with_impact);
3229+
RUN_TEST(session_summary_tools_list);
31393230
}

0 commit comments

Comments
 (0)