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
36 changes: 36 additions & 0 deletions src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -3176,6 +3176,42 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
return cbm_mcp_text_result("path or file_pattern contains invalid characters", true);
}

/* ── Phase 0.5: Multi-word → regex conversion ───────────── */
/* If pattern contains whitespace and is not already a regex, convert to a
* regex that matches all words in order: "foo bar baz" → "foo.*bar.*baz".
* This avoids requiring the exact phrase as a contiguous substring. */
if (!use_regex && strchr(pattern, ' ')) {
size_t plen = strlen(pattern);
/* Worst case: every char is a space → ".*" between each char */
char *regex_pat = malloc(plen * 3 + 1);
if (regex_pat) {
char *dst = regex_pat;
const char *src = pattern;
bool in_space = false;
while (*src) {
if (*src == ' ' || *src == '\t') {
if (!in_space) {
*dst++ = '.';
*dst++ = '*';
in_space = true;
}
} else {
/* Escape regex metacharacters from user input */
if (strchr("\\^$.|?*+()[]{}", *src)) {
*dst++ = '\\';
}
*dst++ = *src;
in_space = false;
}
src++;
}
*dst = '\0';
free(pattern);
pattern = regex_pat;
use_regex = true;
}
}

/* ── Phase 1: Grep scan ──────────────────────────────────── */
char tmpfile[CBM_SZ_256];
if (!write_pattern_file(tmpfile, sizeof(tmpfile), pattern)) {
Expand Down
28 changes: 28 additions & 0 deletions tests/test_mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,33 @@ TEST(tool_search_code_no_project) {
PASS();
}

TEST(search_code_multi_word) {
char tmp[512];
cbm_mcp_server_t *srv = setup_snippet_server(tmp, sizeof(tmp));
ASSERT_NOT_NULL(srv);

/* Multi-word query "HandleRequest error" — should find the line
* "func HandleRequest() error {" via regex conversion. */
char req[512];
snprintf(req, sizeof(req),
"{\"jsonrpc\":\"2.0\",\"id\":90,\"method\":\"tools/call\","
"\"params\":{\"name\":\"search_code\","
"\"arguments\":{\"pattern\":\"HandleRequest error\","
"\"project\":\"test-project\"}}}");

char *resp = cbm_mcp_server_handle(srv, req);
ASSERT_NOT_NULL(resp);
/* Should find at least one result (not zero) */
ASSERT_TRUE(strstr(resp, "HandleRequest") != NULL);
/* Should NOT contain an error about "not found" */
ASSERT_TRUE(strstr(resp, "\"isError\":true") == NULL);
free(resp);

cleanup_snippet_dir(tmp);
cbm_mcp_server_free(srv);
PASS();
}

TEST(tool_detect_changes_no_project) {
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);

Expand Down Expand Up @@ -1711,6 +1738,7 @@ SUITE(mcp) {
RUN_TEST(tool_get_code_snippet_not_found);
RUN_TEST(tool_search_code_missing_pattern);
RUN_TEST(tool_search_code_no_project);
RUN_TEST(search_code_multi_word);
RUN_TEST(tool_detect_changes_no_project);
RUN_TEST(tool_manage_adr_no_project);
RUN_TEST(tool_manage_adr_get_with_existing_adr);
Expand Down