Skip to content
Merged
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
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
70 changes: 70 additions & 0 deletions src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ enum {
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 @@ -88,6 +89,7 @@ enum {
#include <yyjson/yyjson.h>
#include <ctype.h>
#include <limits.h>
#include <stdarg.h> // va_list, for the bounded help-list appender
#include <stdint.h> // int64_t
#include <stdio.h>
#include <stdlib.h>
Expand Down Expand Up @@ -890,6 +892,74 @@ const char *cbm_mcp_tool_input_schema(const char *tool_name) {
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. */
/* Append at out[len] and return the bytes ACTUALLY written.
*
* snprintf returns the length it WOULD have written, so accumulating that value
* lets len run past cap; the next `cap - len` then underflows to a huge size_t
* and the following write lands outside the buffer. CodeQL flagged exactly that
* shape here, and it is the same class #1173 just fixed in the Cypher list
* builder. The capacity computed below does happen to be sufficient today —
* which makes this the more dangerous version, not the safer one: the code is
* correct only by an argument made ten lines away, so renaming a tool or
* changing the wrap rule would turn it into an overflow with nothing to notice.
* Clamping makes `len <= cap - 1` a local invariant no later edit can void. */
static size_t help_append(char *out, size_t cap, size_t len, const char *fmt, ...) {
if (len + 1 >= cap) {
return 0;
}
va_list args;
va_start(args, fmt);
int written = vsnprintf(out + len, cap - len, fmt, args);
va_end(args);
if (written <= 0) {
return 0;
}
size_t room = cap - len - 1;
return (size_t)written > room ? room : (size_t)written;
}

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 = help_append(out, cap, 0, "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 += help_append(out, cap, len, "\n ");
col = 1;
}
size_t wrote = help_append(out, cap, len, " %s%s", TOOLS[i].name, sep);
len += wrote;
col += wrote;
}
len += help_append(out, cap, len, "\n");
(void)len; /* final length is not needed; the buffer is NUL-terminated */
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