Skip to content
Closed
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
43 changes: 27 additions & 16 deletions src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -8744,12 +8744,16 @@ static int search_result_cmp(const void *a, const void *b) {
return rb->score - ra->score; /* descending */
}

enum { SEARCH_CODE_MAX_GREP_MATCHES = 500 };

/* Build the grep/search command string based on scoped vs recursive mode.
* On Windows, uses PowerShell Select-String with tab-delimited output.
* On POSIX, uses grep with colon-delimited output. */
static void build_grep_cmd(char *cmd, size_t cmd_sz, bool use_regex, bool scoped,
const char *file_pattern, const char *tmpfile, const char *filelist,
const char *root_path) {
* On Windows, uses
* PowerShell Select-String with tab-delimited output.
* On POSIX, uses grep with colon-delimited
* output. */
void cbm_search_code_build_grep_cmd(char *cmd, size_t cmd_sz, bool use_regex, bool scoped,
const char *file_pattern, const char *tmpfile,
const char *filelist, const char *root_path) {
#ifdef _WIN32
const char *sm = use_regex ? "" : " -SimpleMatch";
if (scoped) {
Expand All @@ -8761,17 +8765,19 @@ static void build_grep_cmd(char *cmd, size_t cmd_sz, bool use_regex, bool scoped
"-LiteralPath $_ -Pattern $pat%s "
"-ErrorAction SilentlyContinue }"
" | Where-Object { $_.Path -like '*%s' }"
" | Select-Object -First %d"
" | ForEach-Object { $_.Path + [char]9 + $_.LineNumber + [char]9 + $_.Line }\"",
tmpfile, filelist, sm, file_pattern);
tmpfile, filelist, sm, file_pattern, SEARCH_CODE_MAX_GREP_MATCHES);
} else {
snprintf(
cmd, cmd_sz,
"powershell -Command \"$pat = Get-Content -Encoding UTF8 -LiteralPath '%s'; "
"Get-Content -Encoding UTF8 -LiteralPath '%s' | ForEach-Object { Select-String "
"-LiteralPath $_ -Pattern $pat%s "
"-ErrorAction SilentlyContinue }"
" | Select-Object -First %d"
" | ForEach-Object { $_.Path + [char]9 + $_.LineNumber + [char]9 + $_.Line }\"",
tmpfile, filelist, sm);
tmpfile, filelist, sm, SEARCH_CODE_MAX_GREP_MATCHES);
}
} else {
if (file_pattern) {
Expand All @@ -8781,17 +8787,19 @@ static void build_grep_cmd(char *cmd, size_t cmd_sz, bool use_regex, bool scoped
"-ErrorAction SilentlyContinue"
" | Select-String -Pattern (Get-Content -Encoding UTF8 -LiteralPath '%s')%s "
"-ErrorAction SilentlyContinue"
" | Select-Object -First %d"
" | ForEach-Object { $_.Path + [char]9 + $_.LineNumber + [char]9 + $_.Line }\"",
root_path, file_pattern, tmpfile, sm);
root_path, file_pattern, tmpfile, sm, SEARCH_CODE_MAX_GREP_MATCHES);
} else {
snprintf(
cmd, cmd_sz,
"powershell -Command \"Get-ChildItem -Recurse -Path '%s\\*' -File -ErrorAction "
"SilentlyContinue"
" | Select-String -Pattern (Get-Content -Encoding UTF8 -LiteralPath '%s')%s "
"-ErrorAction SilentlyContinue"
" | Select-Object -First %d"
" | ForEach-Object { $_.Path + [char]9 + $_.LineNumber + [char]9 + $_.Line }\"",
root_path, tmpfile, sm);
root_path, tmpfile, sm, SEARCH_CODE_MAX_GREP_MATCHES);
}
}
#else
Expand Down Expand Up @@ -9767,11 +9775,14 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
const char *tmpfile = scratch.pattern_path;
const char *filelist = scratch.filelist_path;

/* No grep-level match limit — let grep find all matches, then dedup and
* cap in our code. The -m flag caused results from large vendored files
* to exhaust the quota before reaching project source files. */
enum { GREP_MAX_MATCHES = 500 };
int grep_limit = GREP_MAX_MATCHES;
/* Bound accepted scanner records in C on every platform. Windows also
* applies this limit
* after its producer filters to avoid an unbounded
* PowerShell stream. POSIX deliberately
* avoids grep's per-file -m flag,
* which let large vendored files exhaust the quota
* before project source. */
int grep_limit = SEARCH_CODE_MAX_GREP_MATCHES;

/* Scope grep to indexed files only — avoids scanning vendored/generated code.
* Query the graph for distinct file paths, write them to a temp file,
Expand Down Expand Up @@ -9800,8 +9811,8 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
search_scratch_close(&scratch);
} else {
char cmd[CBM_SZ_4K];
build_grep_cmd(cmd, sizeof(cmd), use_regex, scoped, file_pattern, tmpfile, filelist,
root_path);
cbm_search_code_build_grep_cmd(cmd, sizeof(cmd), use_regex, scoped, file_pattern, tmpfile,
filelist, root_path);

FILE *fp = cbm_popen(cmd, "r");
if (!fp) {
Expand Down
7 changes: 7 additions & 0 deletions src/mcp/mcp_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ void cbm_mcp_server_set_command_test_hook(cbm_mcp_server_t *srv, cbm_mcp_command
* this immediately before publication so idle sessions retain no SQLite DB. */
bool cbm_mcp_server_release_pristine_memory_store(cbm_mcp_server_t *srv);

/* Build search_code's platform command. Exposed only for white-box tests that
* pin producer-side
* filtering and output limits. */
void cbm_search_code_build_grep_cmd(char *cmd, size_t cmd_sz, bool use_regex, bool scoped,
const char *file_pattern, const char *tmpfile,
const char *filelist, const char *root_path);

/* Prepend one daemon-owned notice to a successful JSON-RPC tool response.
* On success replaces and frees *response_io; on failure it is unchanged. */
bool cbm_mcp_jsonrpc_response_prepend_notice(char **response_io, const char *notice);
Expand Down
29 changes: 26 additions & 3 deletions tests/test_mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -4190,10 +4190,32 @@ TEST(search_code_scoped_path_with_spaces_issue687) {
}

#ifdef _WIN32
static bool search_code_command_has_producer_cap(bool scoped, const char *file_pattern) {
char cmd[CBM_SZ_4K];
cbm_search_code_build_grep_cmd(cmd, sizeof(cmd), false, scoped, file_pattern,
"C:\\tmp\\pattern", "C:\\tmp\\files", "C:\\repo");

const char *filter =
scoped && file_pattern ? strstr(cmd, " | Where-Object ") : strstr(cmd, "Select-String ");
const char *cap = strstr(cmd, " | Select-Object -First 500");
const char *format = strstr(cmd, " | ForEach-Object { $_.Path +");
return filter && cap && format && filter < cap && cap < format &&
strstr(cmd, "Select-Object -First 4096") == NULL &&
strstr(cap + 1, " | Select-Object -First 500") == NULL;
}

TEST(search_code_windows_producer_cap_follows_filters) {
ASSERT_TRUE(search_code_command_has_producer_cap(true, "*.pas"));
ASSERT_TRUE(search_code_command_has_producer_cap(true, NULL));
ASSERT_TRUE(search_code_command_has_producer_cap(false, "*.pas"));
ASSERT_TRUE(search_code_command_has_producer_cap(false, NULL));
PASS();
}

/* Issue #903 follow-up: scoped search_code on Windows writes a UTF-8 filelist
* containing absolute source paths, then reads it back through PowerShell.
* Windows PowerShell 5.1 treats UTF-8 without BOM as ANSI unless told
* otherwise, so a non-ASCII project root can be mojibaked before
* containing
* absolute source paths, then reads it back through PowerShell. Windows PowerShell 5.1 treats UTF-8
* without BOM as ANSI unless told otherwise, so a non-ASCII project root can be mojibaked before
* Select-String sees the LiteralPath. */
TEST(search_code_scoped_path_with_cjk_root_issue903) {
char tmp[512];
Expand Down Expand Up @@ -10651,6 +10673,7 @@ SUITE(mcp) {
RUN_TEST(search_code_multi_word);
RUN_TEST(search_code_scoped_path_with_spaces_issue687);
#ifdef _WIN32
RUN_TEST(search_code_windows_producer_cap_follows_filters);
RUN_TEST(search_code_scoped_path_with_cjk_root_issue903);
#endif
RUN_TEST(search_code_path_filter_prefilter_keeps_matches);
Expand Down
Loading