Skip to content

Commit 8769d70

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 89caa5b commit 8769d70

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
@@ -885,6 +885,15 @@ static const tool_def_t TOOLS[] = {
885885
"\"description\":\"Include graph neighbors of touched symbols that have not been "
886886
"examined yet.\"},\"limit\":{\"type\":\"integer\",\"default\":10,"
887887
"\"description\":\"Max related_untouched items.\"}},\"required\":[]}"},
888+
889+
{"get_session_summary",
890+
"Compact markdown session summary for context recovery after compaction. "
891+
"Shows files touched, symbols investigated with PageRank, areas explored, "
892+
"and suggested next steps.",
893+
"{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\","
894+
"\"description\":\"Project name (needed for PageRank enrichment and next-step "
895+
"suggestions).\"},\"max_tokens\":{\"type\":\"integer\",\"default\":2000,"
896+
"\"description\":\"Maximum output size.\"}},\"required\":[]}"},
888897
};
889898

890899
static const int TOOL_COUNT = sizeof(TOOLS) / sizeof(TOOLS[0]);
@@ -5965,7 +5974,7 @@ static void maybe_add_session_hint(yyjson_mut_doc *doc, yyjson_mut_val *root, co
59655974
}
59665975
}
59675976

5968-
/* ── Session context (Phase 7A) ────────────────────────────────── */
5977+
/* ── Session helpers (shared by 7A + 7C) ──────────────────────── */
59695978

59705979
/* Callback: free a strdup'd hash table key (for temporary candidate sets). */
59715980
static void free_ht_key_cb(const char *key, void *value, void *userdata) {
@@ -5974,7 +5983,7 @@ static void free_ht_key_cb(const char *key, void *value, void *userdata) {
59745983
free((void *)key);
59755984
}
59765985

5977-
/* Callback: append key to a yyjson array. */
5986+
/* Callback: append key to a yyjson array (used by get_session_context). */
59785987
typedef struct {
59795988
yyjson_mut_doc *doc;
59805989
yyjson_mut_val *arr;
@@ -5985,7 +5994,7 @@ static void append_key_to_json_arr(const char *key, void *userdata) {
59855994
yyjson_mut_arr_add_strcpy(ctx->doc, ctx->arr, key);
59865995
}
59875996

5988-
/* Callback: collect symbol names into a list for related_untouched lookup. */
5997+
/* Callback: collect symbol names into a list for neighbor lookup. */
59895998
typedef struct {
59905999
const char **names;
59916000
int count;
@@ -5999,6 +6008,215 @@ static void collect_symbol_name(const char *key, void *userdata) {
59996008
}
60006009
}
60016010

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

62296450
char msg[256];
62306451
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)