Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
11 changes: 7 additions & 4 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -888,10 +888,13 @@ static void print_help(void) {
printf(" Manual/UI MCP boundaries: Qodo, Warp, JetBrains AI/ACP, Replit,\n");
printf(" Plandex, SWE-agent, BLACKBOX, GitHub cloud agents, Jules,\n");
printf(" CodeRabbit.\n");
printf("\nTools: index_repository, search_graph, query_graph, trace_path,\n");
printf(" get_code_snippet, get_graph_schema, get_architecture, search_code,\n");
printf(" list_projects, delete_project, index_status, detect_changes,\n");
printf(" manage_adr, ingest_traces\n");
/* Rendered from the MCP tool registry: a hand-maintained copy here
* omitted check_index_coverage (#1361) and could silently drift again. */
char *tools_help = cbm_mcp_tools_help_list();
if (tools_help) {
printf("\n%s", tools_help);
free(tools_help);
}
}

/* ── Main ───────────────────────────────────────────────────────── */
Expand Down
42 changes: 42 additions & 0 deletions src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
MCP_CONTENT_PREFIX = 15, /* strlen("Content-Length:") */
MCP_RETURN_2 = 2,
MCP_TOOLS_PAGE_SIZE = 8,
MCP_HELP_TOOLS_WRAP_COL = 74, /* --help tool list stays readable on 80-col terminals */
MCP_MAX_CROSS_REPO_TARGETS = 4096,
};
#define MCP_MS_TO_US 1000LL
Expand Down Expand Up @@ -890,6 +891,47 @@
return NULL;
}

int cbm_mcp_tool_count(void) {
return TOOL_COUNT;
}

const char *cbm_mcp_tool_name(int index) {
if (index < 0 || index >= TOOL_COUNT) {
return NULL;
}
return TOOLS[index].name;
}

/* Render the top-level --help "Tools:" block from the registry tools/list
* serves. The list used to be hand-maintained in the help text and drifted
* when check_index_coverage was added (#1361); deriving it here makes that
* divergence impossible. Heap-allocated; caller frees. */
char *cbm_mcp_tools_help_list(void) {
size_t cap = SLEN("Tools:") + 2; /* trailing newline + NUL */
for (int i = 0; i < TOOL_COUNT; i++) {
cap += strlen(TOOLS[i].name) + SLEN(" ,\n "); /* per-tool worst case incl. a wrap */
}
char *out = malloc(cap);
if (!out) {
return NULL;
}
size_t len = (size_t)snprintf(out, cap, "Tools:");
size_t col = len;
for (int i = 0; i < TOOL_COUNT; i++) {
const char *sep = (i + 1 < TOOL_COUNT) ? "," : "";
size_t item = SLEN(" ") + strlen(TOOLS[i].name) + strlen(sep);
if (i > 0 && col + item > MCP_HELP_TOOLS_WRAP_COL) {
len += (size_t)snprintf(out + len, cap - len, "\n ");

Check failure

Code scanning / CodeQL

Potentially overflowing call to snprintf High

The
size argument
of this snprintf call is derived from its return value, which may exceed the size of the buffer and overflow.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
col = 1;
}
size_t wrote = (size_t)snprintf(out + len, cap - len, " %s%s", TOOLS[i].name, sep);

Check failure

Code scanning / CodeQL

Potentially overflowing call to snprintf High

The
size argument
of this snprintf call is derived from its return value, which may exceed the size of the buffer and overflow.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
len += wrote;
col += wrote;
}
snprintf(out + len, cap - len, "\n");
return out;
}

static int mcp_tools_cursor_offset(const char *params_json, bool *has_cursor_out) {
if (has_cursor_out) {
*has_cursor_out = false;
Expand Down
9 changes: 9 additions & 0 deletions src/mcp/mcp.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ char *cbm_mcp_tools_list(void);
* NULL if the tool is unknown. Backs the CLI flag parser + per-tool --help. */
const char *cbm_mcp_tool_input_schema(const char *tool_name);

/* Registry accessors: the number of tools tools/list advertises, and the name
* of tool `index` (static, do not free; NULL when out of range). */
int cbm_mcp_tool_count(void);
const char *cbm_mcp_tool_name(int index);

/* Render the top-level --help "Tools:" block from the registry so the help
* text cannot drift from tools/list (#1361). Heap-allocated; caller frees. */
char *cbm_mcp_tools_help_list(void);

/* Format the initialize response. params_json is the raw initialize params
* (used for protocol version negotiation). Returns heap-allocated JSON. */
char *cbm_mcp_initialize_response(const char *params_json);
Expand Down
35 changes: 35 additions & 0 deletions tests/test_mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,40 @@ TEST(mcp_tools_list) {
PASS();
}

/* #1361: --help omitted check_index_coverage because its tool list was a
* hand-maintained copy. The list is now rendered from the registry; this pins
* the render so a formatter bug cannot reintroduce a silent omission. */
TEST(mcp_tools_help_list_matches_registry) {
char *help = cbm_mcp_tools_help_list();
ASSERT_NOT_NULL(help);
int count = cbm_mcp_tool_count();
ASSERT_GT(count, 0);
for (int i = 0; i < count; i++) {
const char *name = cbm_mcp_tool_name(i);
ASSERT_NOT_NULL(name);
ASSERT_NOT_NULL(strstr(help, name));
}
/* Exactly one comma between consecutive tools: the rendered cardinality
* equals the registry's, so truncation or duplication fails here. */
int commas = 0;
for (const char *p = help; *p; p++) {
if (*p == ',') {
commas++;
}
}
ASSERT_EQ(commas, count - 1);
/* Wrapped for an 80-column terminal. */
const char *line = help;
while (line && *line) {
const char *nl = strchr(line, '\n');
size_t line_len = nl ? (size_t)(nl - line) : strlen(line);
ASSERT_LT((int)line_len, 80);
line = nl ? nl + 1 : NULL;
}
free(help);
PASS();
}

TEST(mcp_tools_list_latest_metadata) {
char *json = cbm_mcp_tools_list();
ASSERT_NOT_NULL(json);
Expand Down Expand Up @@ -9517,6 +9551,7 @@ SUITE(mcp) {
/* MCP protocol helpers */
RUN_TEST(mcp_initialize_response);
RUN_TEST(mcp_tools_list);
RUN_TEST(mcp_tools_help_list_matches_registry);
RUN_TEST(mcp_tools_list_latest_metadata);
RUN_TEST(mcp_tools_have_behavior_annotations);
RUN_TEST(mcp_index_repository_declares_name_override_issue571);
Expand Down
Loading