Skip to content
Open
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
67 changes: 64 additions & 3 deletions plugins/mcp-server/src/com/intellij/mcpserver/util/fs.util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Path> {
val roots = mutableListOf(projectDirectory.normalize())
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
}

/**
Expand Down Expand Up @@ -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 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 rootPath
}
}
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
Expand Down