From f3127fba8bdbaf611328e2ca8be1959d0316dea9 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 3 Feb 2026 16:29:59 +0300 Subject: [PATCH] feat(mcp): replace name/contains with query and remove word streaming --- src/com/lsfusion/mcp/MCPSearchUtils.java | 59 +++++----------------- src/com/lsfusion/mcp/McpServerService.java | 10 ++-- src/com/lsfusion/mcp/McpToolset.kt | 39 +++++++------- 3 files changed, 33 insertions(+), 75 deletions(-) diff --git a/src/com/lsfusion/mcp/MCPSearchUtils.java b/src/com/lsfusion/mcp/MCPSearchUtils.java index b9e9cae2..8c44de59 100644 --- a/src/com/lsfusion/mcp/MCPSearchUtils.java +++ b/src/com/lsfusion/mcp/MCPSearchUtils.java @@ -14,8 +14,6 @@ import com.intellij.psi.PsiReference; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.search.ProjectScope; -import com.intellij.psi.search.PsiSearchHelper; -import com.intellij.psi.search.UsageSearchContext; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.MergeQuery; import com.intellij.util.Processor; @@ -110,8 +108,7 @@ private SelectedStatement(@NotNull LSFMCPDeclaration decl, GlobalSearchScope sea * // scope=project -> projectOnly scope (excludes libraries) * // otherwise, scope can be a CSV list of IDEA module names (e.g. "my-idea-module1,my-idea-module2") * requiredModules: true, // optional (default: true); if true, include REQUIRE-d modules for modules in `modules` - * name: "cust,Order", // optional, CSV; filters by element name - * contains: "(?i)cust.*", // optional, CSV; filters by element code + * query: "cust,Order", // optional, CSV; filters by element name and code (word or regex) * elementTypes: "class,property,action", // optional, CSV * classes: "MyNS.MyClass, MyOtherNS.OtherClass", // optional, CSV canonical names * relatedElements: "property:MyNS.myProp[MyNS.MyClass], MyModule(10:5)", // optional, CSV @@ -240,7 +237,7 @@ private static JSONObject assembleResult(JSONArray items, String meta, List createSearchProcessor(SearchState state, Set seen, List nameFilters, List containsFilters, Set elementTypes, Set classDecls, Map related, GlobalSearchScope searchScope, ConcurrentMap relatedCache) { + private static Processor createSearchProcessor(SearchState state, Set seen, List queryFilters, Set elementTypes, Set classDecls, Map related, GlobalSearchScope searchScope, ConcurrentMap relatedCache) { return st -> { if (state.stopRequested.get()) { return false; @@ -254,7 +251,7 @@ private static Processor createSearchProcessor(SearchState st // already processed - if (matchesAllFilters(st, nameFilters, containsFilters, elementTypes, classDecls, related, searchScope, relatedCache)) { + if (matchesAllFilters(st, queryFilters, elementTypes, classDecls, related, searchScope, relatedCache)) { SelectedStatement selSt = new SelectedStatement(st, searchScope); synchronized (state.statementsByPriority[p]) { @@ -371,19 +368,6 @@ private static void submitFileTasks(GlobalSearchScope searchScope, Processor filters, Processor processor, TaskSubmitter submit) { - if (filters.isEmpty()) return false; - boolean fullyStreamable = true; - for (NameFilter nf : filters) { - if (nf.isWordStreamable()) { - submit.submit(5, () -> ReadAction.run(() -> streamWord(project, nf, processor, searchScope))); - } else { - fullyStreamable = false; - } - } - return fullyStreamable; - } - private static @NotNull GlobalSearchScope run(@NotNull Project project, @NotNull JSONObject query, @NotNull Set seen, @@ -391,21 +375,14 @@ private static boolean submitWordTasks(Project project, GlobalSearchScope search @NotNull SearchState state, @NotNull TaskSubmitter submit) { GlobalSearchScope searchScope = ReadAction.compute(() -> buildSearchScope(project, query.optString("modules"), query.optString("scope"), query.optBoolean("requiredModules", true))); - List nameFilters = parseMatchersCsv(query.optString("name")); - List containsFilters = parseMatchersCsv(query.optString("contains")); + List queryFilters = parseMatchersCsv(query.optString("query")); Set elementTypes = parseElementTypes(query.optString("elementTypes")); Set classDecls = ReadAction.compute(() -> parseClasses(project, searchScope, query.optString("classes"))); Map related = ReadAction.compute(() -> parseRelated(project, searchScope, query.optString("relatedElements"), query.optString("relatedDirection"))); // Shared processor that applies all filters and returns false to stop the current iteration - final Processor processor = createSearchProcessor(state, seen, nameFilters, containsFilters, elementTypes, classDecls, related, searchScope, relatedCache); - - // Track which blocks are fully streamable while assembling iterations. - // Name/code filters are considered fully streamable only if ALL matchers are "word-only" with length >= 3. - // Name/code-based iterations (only for word length >= 3) - boolean nameFullyStreamable = submitWordTasks(project, searchScope, nameFilters, processor, submit); - boolean containsFullyStreamable = submitWordTasks(project, searchScope, containsFilters, processor, submit); + final Processor processor = createSearchProcessor(state, seen, queryFilters, elementTypes, classDecls, related, searchScope, relatedCache); // Element-type iterations (only for index-backed types and only if elementTypes filter provided) boolean typesFullyStreamable = submitTypeTasks(project, searchScope, elementTypes, processor, submit); @@ -417,7 +394,7 @@ private static boolean submitWordTasks(Project project, GlobalSearchScope search boolean relatedFullyStreamable = submitRelatedTasks(related, searchScope, processor, submit); // Per-file iterations (fallback) — run only if no block is fully streamable - if (!nameFullyStreamable && !containsFullyStreamable && !classesFullyStreamable && !relatedFullyStreamable && !typesFullyStreamable) { + if (!classesFullyStreamable && !relatedFullyStreamable && !typesFullyStreamable) { submitFileTasks(searchScope, processor, submit); } @@ -432,15 +409,6 @@ private static boolean isOnlyPropertiesClassesActions(Set processor, GlobalSearchScope searchScope) { - PsiSearchHelper helper = PsiSearchHelper.getInstance(project); - helper.processElementsWithWord((element, offsetInElement) -> processStatement(element, processor), - searchScope, - nf.word, - (short)(UsageSearchContext.IN_CODE | UsageSearchContext.IN_FOREIGN_LANGUAGES | UsageSearchContext.IN_COMMENTS), - true); - } - private static boolean isTimedOut(long deadlineMillis) { return System.currentTimeMillis() > deadlineMillis; } @@ -1012,8 +980,7 @@ private static GlobalSearchScope buildSearchScope(Project project, String module // region Matching private static boolean matchesAllFilters(LSFMCPDeclaration stmt, - List nameFilters, - List containsFilters, + List queryFilters, Set elementTypes, Set classDecls, Map related, @@ -1021,12 +988,16 @@ private static boolean matchesAllFilters(LSFMCPDeclaration stmt, ConcurrentMap relatedCache) { LSFMCPDeclaration.ElementType t; return (elementTypes.isEmpty() || ((t = stmt.getMCPType()) != null && elementTypes.contains(t))) && - (nameFilters.isEmpty() || matchesNameFilters(stmt, nameFilters, scope, false)) && - (containsFilters.isEmpty() || matchesNameFilters(stmt, containsFilters, scope, true)) && + (queryFilters.isEmpty() || matchesQueryFilters(stmt, queryFilters, scope)) && (classDecls.isEmpty() || matchesClassesFilter(stmt, classDecls, scope)) && (related.isEmpty() || matchesRelatedFilters(stmt, related, scope, relatedCache)); } + private static boolean matchesQueryFilters(LSFMCPDeclaration stmt, List queryFilters, GlobalSearchScope scope) { + return matchesNameFilters(stmt, queryFilters, scope, false) || + matchesNameFilters(stmt, queryFilters, scope, true); + } + private static boolean matchesClassesFilter(LSFMCPDeclaration stmt, Set classDecls, GlobalSearchScope scope) { if (classDecls.isEmpty()) return true; // Resolve candidate declarations (can be several) @@ -1262,10 +1233,6 @@ private static boolean matchesNameFilters(LSFMCPDeclaration stmt, List= 3 && regex == null; - } NameFilter(String word, Pattern regex) { this.word = word; diff --git a/src/com/lsfusion/mcp/McpServerService.java b/src/com/lsfusion/mcp/McpServerService.java index fb19b6bc..9655cc39 100644 --- a/src/com/lsfusion/mcp/McpServerService.java +++ b/src/com/lsfusion/mcp/McpServerService.java @@ -250,14 +250,10 @@ private JSONObject buildFindElementsToolDescriptor() { .put("type", "string") .put("description", "Scope filter (IDEA concept): omitted = project + libraries; `project` = project content only; otherwise, a CSV list of IDEA module names.")) - .put("name", new JSONObject() - .put("type", "string") - .put("description", - "Element name filter as CSV (comma-separated). Word if valid ID, else Java regex.")) - .put("contains", new JSONObject() + .put("query", new JSONObject() .put("type", "string") .put("description", - "Element code filter as CSV. Word if valid ID, else Java regex.")) + "Query filter as CSV (comma-separated). Word if valid ID, else Java regex. Matches against element names and code.")) .put("elementTypes", new JSONObject() .put("type", "string") .put("description", "Element type filter as CSV. Allowed values: `module`, `metacode`, `class`, `property`, `action`, `form`, `navigatorElement`, `window`, `group`, `table`, `event`, `calculatedEvent`, `constraint`, `index`.")) @@ -285,7 +281,7 @@ private JSONObject buildFindElementsToolDescriptor() { .put("moreFilters", new JSONObject() .put("type", "string") .put("description", - "Additional filter objects of the same structure as the root. JSON array string (e.g. `[{\"names\":\"Foo\", \"modules\" : \"MyModule\"},{\"names\":\"Bar\"}]`). Results are merged (OR).")) + "Additional filter objects of the same structure as the root. JSON array string (e.g. `[{\"query\":\"Foo\", \"modules\" : \"MyModule\"},{\"query\":\"Bar\"}]`). Results are merged (OR).")) .put("minSymbols", new JSONObject() .put("type", "integer") .put("minimum", 0) diff --git a/src/com/lsfusion/mcp/McpToolset.kt b/src/com/lsfusion/mcp/McpToolset.kt index 2797345b..2e75bcf4 100644 --- a/src/com/lsfusion/mcp/McpToolset.kt +++ b/src/com/lsfusion/mcp/McpToolset.kt @@ -193,13 +193,9 @@ class McpToolset : com.intellij.mcpserver.McpToolset { ) scope: String? = null, @McpDescription( - description = "Element name filter as CSV (comma-separated). Word if valid ID, else Java regex." + description = "Query filter as CSV (comma-separated). Word if valid ID, else Java regex. Matches against element names and code." ) - names: String? = null, - @McpDescription( - description = "Element code filter as CSV. Word if valid ID, else Java regex." - ) - contains: String? = null, + queryText: String? = null, @McpDescription( description = "Element type filter as CSV. Allowed values: `module`, `metacode`, `class`, `property`, `action`, `form`, `navigatorElement`, `window`, `group`, `table`, `event`, `calculatedEvent`, `constraint`, `index`." ) @@ -215,7 +211,7 @@ class McpToolset : com.intellij.mcpserver.McpToolset { @McpDescription(description = "Direction for ALL `relatedElements` seeds. Allowed values: `both`, `uses`, `used`. Default: `both`.") relatedDirection: String? = null, @McpDescription( - description = "Additional filter objects of the same structure as the root. JSON array string (e.g. `[{\"names\":\"Foo\", \"modules\" : \"MyModule\"},{\"names\":\"Bar\"}]`). Results are merged (OR)." + description = "Additional filter objects of the same structure as the root. JSON array string (e.g. `[{\"query\":\"Foo\", \"modules\" : \"MyModule\"},{\"query\":\"Bar\"}]`). Results are merged (OR)." ) moreFilters: String? = null, @McpDescription(description = "Best-effort minimum output size in JSON chars; server may append neighboring elements if too small (>= 0). Default: ${MCPSearchUtils.DEFAULT_MIN_SYMBOLS}.") @@ -235,24 +231,23 @@ class McpToolset : com.intellij.mcpserver.McpToolset { } try { - val query = JSONObject() - if (modules != null) query.put("modules", modules) - if (scope != null) query.put("scope", scope) - query.put("requiredModules", requiredModules) - if (names != null) query.put("name", names) - if (contains != null) query.put("contains", contains) - if (elementTypes != null) query.put("elementTypes", elementTypes) - if (classes != null) query.put("classes", classes) - if (relatedElements != null) query.put("relatedElements", relatedElements) - if (relatedDirection != null) query.put("relatedDirection", relatedDirection) - query.put("minSymbols", minSymbols) - query.put("maxSymbols", maxSymbols) - query.put("timeoutSeconds", timeoutSeconds) + val payload = JSONObject() + if (modules != null) payload.put("modules", modules) + if (scope != null) payload.put("scope", scope) + payload.put("requiredModules", requiredModules) + if (queryText != null) payload.put("query", queryText) + if (elementTypes != null) payload.put("elementTypes", elementTypes) + if (classes != null) payload.put("classes", classes) + if (relatedElements != null) payload.put("relatedElements", relatedElements) + if (relatedDirection != null) payload.put("relatedDirection", relatedDirection) + payload.put("minSymbols", minSymbols) + payload.put("maxSymbols", maxSymbols) + payload.put("timeoutSeconds", timeoutSeconds) if (moreFilters != null && !moreFilters.isEmpty()) { - query.put("moreFilters", JSONArray(moreFilters)) + payload.put("moreFilters", JSONArray(moreFilters)) } - val result = MCPSearchUtils.findElements(project, query) + val result = MCPSearchUtils.findElements(project, payload) val jsonElement = json.parseToJsonElement(result.toString()) return json.decodeFromJsonElement(jsonElement) } catch (e: McpExpectedError) {