Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 58 additions & 5 deletions src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,11 @@ static const tool_def_t TOOLS[] = {
"representative top_nodes, and the packages/edge_types that bind it) — use these to grasp "
"the real architectural seams, which often cut across the folder layout.",
"{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\"},\"aspects\":{\"type\":"
"\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"project\"]}"},
"\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"all\",\"overview\",\"structure\","
"\"dependencies\",\"routes\",\"languages\",\"packages\",\"entry_points\",\"hotspots\","
"\"boundaries\",\"layers\",\"file_tree\",\"clusters\"]},\"description\":\"Aspects to "
"include. 'all' = everything; 'overview' = compact summary (all except file_tree); "
"omit = all.\"}},\"required\":[\"project\"]}"},

{"search_code",
"Graph-augmented code search. Finds text patterns via grep, then enriches results with "
Expand Down Expand Up @@ -1831,7 +1835,36 @@ static char *handle_delete_project(cbm_mcp_server_t *srv, const char *args) {
return result;
}

/* Check if an aspect is requested (NULL aspects = all, or array contains "all" or the name). */
/* Canonical list of valid aspect tokens for get_architecture.
* SSOT consumed by both the JSON-Schema enum (advisory, client-side)
* and the server-side validation (authoritative). Update both together
* if the aspect set changes. */
static const char *VALID_ASPECTS[] = {
"all", "overview",
"structure", "dependencies", "routes",
"languages", "packages", "entry_points",
"hotspots", "boundaries", "layers",
"file_tree", "clusters",
NULL
};

static bool aspect_is_valid(const char *name) {
if (!name) return false;
for (int i = 0; VALID_ASPECTS[i]; i++) {
if (strcmp(name, VALID_ASPECTS[i]) == 0) return true;
}
return false;
}

/* "overview" = compact architecture summary: every aspect EXCEPT the large
per-file listing (file_tree), which alone dominates the payload (~275 entries)
and pushes the response past MAX_MCP_OUTPUT_TOKENS. */
static bool aspect_in_overview(const char *name) {
return strcmp(name, "file_tree") != 0;
}

/* Check if an aspect is requested. NULL aspects = all. Array can contain "all"
* (everything), "overview" (everything except file_tree), or the aspect name. */
static bool aspect_wanted(yyjson_doc *aspects_doc, yyjson_val *aspects_arr, const char *name) {
if (!aspects_arr) {
return true; /* no filter = all */
Expand All @@ -1841,9 +1874,10 @@ static bool aspect_wanted(yyjson_doc *aspects_doc, yyjson_val *aspects_arr, cons
yyjson_val *val;
while ((val = yyjson_arr_iter_next(&iter)) != NULL) {
const char *s = yyjson_get_str(val);
if (s && (strcmp(s, "all") == 0 || strcmp(s, name) == 0)) {
return true;
}
if (!s) continue;
if (strcmp(s, "all") == 0) return true;
if (strcmp(s, "overview") == 0 && aspect_in_overview(name)) return true;
if (strcmp(s, name) == 0) return true;
}
(void)aspects_doc;
return false;
Expand Down Expand Up @@ -1916,6 +1950,25 @@ static char *handle_get_architecture(cbm_mcp_server_t *srv, const char *args) {
}
}

/* Server-side validation: reject unknown aspect tokens with a model-friendly
* error listing the valid values. The JSON Schema enum on this tool is
* advisory — not every MCP client validates client-side (Claude Code does
* not), so silent-empty was the failure mode before this check. */
for (int i = 0; i < aspects_strs_count; i++) {
if (!aspect_is_valid(aspects_strs[i])) {
char msg[512];
snprintf(msg, sizeof(msg),
"Unknown aspect '%s'. Valid: all, overview, structure, "
"dependencies, routes, languages, packages, entry_points, "
"hotspots, boundaries, layers, file_tree, clusters.",
aspects_strs[i]);
char *err = cbm_mcp_text_result(msg, true);
free(project);
if (aspects_doc) yyjson_doc_free(aspects_doc);
return err;
}
}

cbm_schema_info_t schema = {0};
/* Counts-only: this handler renders label/type counts but never property
* keys, and full key discovery json_each-scans every row (seconds-to-
Expand Down
16 changes: 10 additions & 6 deletions src/store/store.c
Original file line number Diff line number Diff line change
Expand Up @@ -4931,17 +4931,21 @@ static int arch_clusters(cbm_store_t *s, const char *project, cbm_architecture_i

/* ── GetArchitecture dispatch ──────────────────────────────────── */

/* "overview" = compact architecture summary: every aspect EXCEPT the large
per-file listing (file_tree), which alone dominates the payload (~275 entries)
and pushes the response past MAX_MCP_OUTPUT_TOKENS. */
static bool aspect_in_overview(const char *name) {
return strcmp(name, "file_tree") != 0;
}

static bool want_aspect(const char **aspects, int aspect_count, const char *name) {
if (!aspects || aspect_count == 0) {
return true;
}
for (int i = 0; i < aspect_count; i++) {
if (strcmp(aspects[i], "all") == 0) {
return true;
}
if (strcmp(aspects[i], name) == 0) {
return true;
}
if (strcmp(aspects[i], "all") == 0) return true;
if (strcmp(aspects[i], "overview") == 0 && aspect_in_overview(name)) return true;
if (strcmp(aspects[i], name) == 0) return true;
}
return false;
}
Expand Down
Loading