From 9ae51b01cb4be24aa71beae62cb7ae34f1621f8e Mon Sep 17 00:00:00 2001 From: Julien Doche Date: Thu, 2 Apr 2026 18:27:02 +0200 Subject: [PATCH 1/2] MCP Server: resolve files against content roots for Bazel/IJwB projects In Bazel projects using the IJwB plugin, `projectBasePath` points to the `.ijwb/` subdirectory, while all source files reside in the workspace root (the parent directory). This causes `resolveInProject()` to reject every source file as "outside of the project directory", breaking `get_symbol_info`, `get_file_problems`, and all other MCP tools that operate on files. Fix `resolveInProject()` to also try resolving paths against the project's content roots (via `ProjectRootManager`), and extend `findMostRelevantProject()` to match against content roots when looking up which project owns a given path. This unblocks MCP tool usage in large Bazel monorepos (e.g., via Claude Code, Cursor, or VS Code Copilot) where IJwB is the project type. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../com/intellij/mcpserver/util/fs.util.kt | 67 ++++++++++++++++++- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/plugins/mcp-server/src/com/intellij/mcpserver/util/fs.util.kt b/plugins/mcp-server/src/com/intellij/mcpserver/util/fs.util.kt index d00784834273c..ea840af8e02d1 100644 --- a/plugins/mcp-server/src/com/intellij/mcpserver/util/fs.util.kt +++ b/plugins/mcp-server/src/com/intellij/mcpserver/util/fs.util.kt @@ -38,13 +38,61 @@ val Project.projectDirectory: Path else mcpFail("The project directory cannot be determined.") } +/** + * Returns all root paths that should be considered "inside" the project. + * + * This includes both the [projectDirectory] and any content roots registered with the project. + * Content roots are important for project types like Bazel/IJwB where [projectDirectory] points + * to a subdirectory (e.g., `.ijwb/`) while source files reside in the workspace root. + */ +private fun Project.allProjectRoots(): List { + val roots = mutableListOf(projectDirectory) + try { + ProjectRootManager.getInstance(this).contentRoots + .mapNotNull { it.toNioPathOrNull()?.normalize() } + .forEach { roots.add(it) } + } + catch (_: Throwable) { + // ProjectRootManager might not be available in all contexts + } + return roots.distinct() +} + /** * Resolves a relative path against the project's directory. * - * When [throwWhenOutside] is true the method throws an McpExpectedException if the path is outside the project directory. + * When [throwWhenOutside] is true the method throws an McpExpectedException if the path is outside the project directory + * or any of its content roots. + * + * For project types like Bazel/IJwB where the project directory (e.g., `.ijwb/`) differs from the workspace root, + * this method also tries to resolve the path against each content root if the primary resolution does not find an + * existing file. */ fun Project.resolveInProject(pathInProject: String, throwWhenOutside: Boolean = true): Path { - return resolveInProject(pathInProject = pathInProject, projectDirectory = projectDirectory, throwWhenOutside = throwWhenOutside) + val roots = allProjectRoots() + val filePath = projectDirectory.resolve(pathInProject).normalize() + + // If the path resolves to an existing file under projectDirectory, use it directly + if (filePath.toFile().exists() && filePath.startsWith(projectDirectory)) { + return filePath + } + + // Try resolving against content roots (needed for Bazel/IJwB projects) + for (root in roots) { + val candidate = root.resolve(pathInProject).normalize() + if (candidate.toFile().exists()) { + if (throwWhenOutside && roots.none { candidate.startsWith(it) }) { + mcpFail("Specified path '$candidate' points to the location outside of the project directory") + } + return candidate + } + } + + // Fall back to the original resolution + if (throwWhenOutside && roots.none { filePath.startsWith(it) }) { + mcpFail("Specified path '$filePath' points to the location outside of the project directory") + } + return filePath } /** @@ -106,7 +154,20 @@ private suspend fun findMostRelevantProject(path: Path): Project? { // here we will have 2 project matches: `frontend/common` and `frontend` and better to prefer `frontend/common` val pairs = openProjects.mapNotNull { project -> val openProjectPath = if (project is ProjectStoreOwner) project.componentStore.projectBasePath.normalize() else return@mapNotNull null - if (targetNormalizedPath.startsWith(openProjectPath)) project to path else null + // Check against projectBasePath first + if (targetNormalizedPath.startsWith(openProjectPath)) return@mapNotNull project to path + // Also check against content roots (needed for Bazel/IJwB where projectBasePath is a subdirectory) + try { + val contentRoots = ProjectRootManager.getInstance(project).contentRoots + for (root in contentRoots) { + val rootPath = root.toNioPathOrNull()?.normalize() ?: continue + if (targetNormalizedPath.startsWith(rootPath)) return@mapNotNull project to path + } + } + catch (_: Throwable) { + // ProjectRootManager might not be available + } + null }.sortedByDescending { it.second.nameCount } logger.trace { "Found projects for path $path: ${pairs.joinToString { it.first.basePath ?: "null"}}" } return pairs.firstOrNull()?.first From a4abed116bc9e50ba474181139cb22f32dd0f74d Mon Sep 17 00:00:00 2001 From: Julien Doche Date: Thu, 16 Apr 2026 11:32:39 +0200 Subject: [PATCH 2/2] MCP Server: normalize projectDirectory in allProjectRoots and fix sorting in findMostRelevantProject - Normalize projectDirectory in allProjectRoots() so distinct() and startsWith() work consistently with the already-normalized content roots - Use the matched root path (not the input path) in findMostRelevantProject pairs so sortedByDescending { nameCount } correctly prefers inner directories Co-Authored-By: Claude Opus 4.6 (1M context) --- .../mcp-server/src/com/intellij/mcpserver/util/fs.util.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/mcp-server/src/com/intellij/mcpserver/util/fs.util.kt b/plugins/mcp-server/src/com/intellij/mcpserver/util/fs.util.kt index ea840af8e02d1..7f5f1344114a7 100644 --- a/plugins/mcp-server/src/com/intellij/mcpserver/util/fs.util.kt +++ b/plugins/mcp-server/src/com/intellij/mcpserver/util/fs.util.kt @@ -46,7 +46,7 @@ val Project.projectDirectory: Path * to a subdirectory (e.g., `.ijwb/`) while source files reside in the workspace root. */ private fun Project.allProjectRoots(): List { - val roots = mutableListOf(projectDirectory) + val roots = mutableListOf(projectDirectory.normalize()) try { ProjectRootManager.getInstance(this).contentRoots .mapNotNull { it.toNioPathOrNull()?.normalize() } @@ -155,13 +155,13 @@ private suspend fun findMostRelevantProject(path: Path): Project? { val pairs = openProjects.mapNotNull { project -> val openProjectPath = if (project is ProjectStoreOwner) project.componentStore.projectBasePath.normalize() else return@mapNotNull null // Check against projectBasePath first - if (targetNormalizedPath.startsWith(openProjectPath)) return@mapNotNull project to path + if (targetNormalizedPath.startsWith(openProjectPath)) return@mapNotNull project to openProjectPath // Also check against content roots (needed for Bazel/IJwB where projectBasePath is a subdirectory) try { val contentRoots = ProjectRootManager.getInstance(project).contentRoots for (root in contentRoots) { val rootPath = root.toNioPathOrNull()?.normalize() ?: continue - if (targetNormalizedPath.startsWith(rootPath)) return@mapNotNull project to path + if (targetNormalizedPath.startsWith(rootPath)) return@mapNotNull project to rootPath } } catch (_: Throwable) {