diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index ae5b5c31..ab782c6e 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -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)) { diff --git a/tests/test_mcp.c b/tests/test_mcp.c index 72729f11..32db0b8c 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -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); @@ -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);