diff --git a/src/main/kotlin/org/jetbrains/mcpserverplugin/MCPService.kt b/src/main/kotlin/org/jetbrains/mcpserverplugin/MCPService.kt index 5261478..76261a1 100644 --- a/src/main/kotlin/org/jetbrains/mcpserverplugin/MCPService.kt +++ b/src/main/kotlin/org/jetbrains/mcpserverplugin/MCPService.kt @@ -1,18 +1,15 @@ package org.jetbrains.ide.mcp import com.intellij.openapi.components.Service -import com.intellij.openapi.components.Service.Level.APP import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream -import io.ktor.client.HttpClient -import io.ktor.client.request.post -import io.ktor.client.request.setBody +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.http.* import io.ktor.http.ContentType.* -import io.ktor.http.contentType -import io.ktor.util.decodeBase64String -import io.ktor.util.encodeBase64 +import io.ktor.util.* import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.FullHttpRequest import io.netty.handler.codec.http.HttpMethod @@ -23,7 +20,6 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json import kotlinx.serialization.serializer -import okio.ByteString.Companion.decodeBase64 import org.jetbrains.ide.RestService import org.jetbrains.mcpserverplugin.AbstractMcpTool import org.jetbrains.mcpserverplugin.McpTool @@ -66,12 +62,12 @@ class MCPService : RestService() { override fun execute(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): String? { val path = urlDecoder.path().split(serviceName).last().trimStart('/') - val project = getLastFocusedOrOpenedProject() ?: return null + val tools = McpToolManager.Companion.getAllTools() when (path) { "list_tools" -> handleListTools(tools, request, context) - else -> handleToolExecution(path, tools, request, context, project) + else -> handleToolExecution(path, tools, request, context) } return null } @@ -96,7 +92,6 @@ class MCPService : RestService() { tools: List>, request: FullHttpRequest, context: ChannelHandlerContext, - project: Project ) { val tool = tools.find { it.name == path } ?: run { sendJson(Response(error = "Unknown tool: $path"), request, context) @@ -112,7 +107,7 @@ class MCPService : RestService() { return } val result = try { - toolHandle(tool, project, args) + toolHandle(tool, args) } catch (e: Throwable) { logger().warn("Failed to execute tool $path", e) Response(error = "Failed to execute tool $path, message ${e.message}") @@ -146,9 +141,9 @@ class MCPService : RestService() { } } - private fun toolHandle(tool: McpTool, project: Project, args: Any): Response { + private fun toolHandle(tool: McpTool, args: Any): Response { @Suppress("UNCHECKED_CAST") - return tool.handle(project, args as Args) + return tool.handle(args as Args) } override fun isMethodSupported(method: HttpMethod): Boolean = @@ -187,6 +182,13 @@ class MCPService : RestService() { @Serializable object NoArgs +interface ProjectAware { + val projectName: String +} + +@Serializable +data class ProjectOnly(override val projectName: String) : ProjectAware + @Serializable data class ToolInfo( val name: String, @@ -210,5 +212,8 @@ data class JsonSchemaObject( @Serializable data class PropertySchema( - val type: String + val type: String, ) + + +fun ProjectAware.getProject() = ProjectManager.getInstance().openProjects.find { it.name == projectName } \ No newline at end of file diff --git a/src/main/kotlin/org/jetbrains/mcpserverplugin/McpTool.kt b/src/main/kotlin/org/jetbrains/mcpserverplugin/McpTool.kt index a9218b1..21ecef6 100644 --- a/src/main/kotlin/org/jetbrains/mcpserverplugin/McpTool.kt +++ b/src/main/kotlin/org/jetbrains/mcpserverplugin/McpTool.kt @@ -1,13 +1,12 @@ package org.jetbrains.mcpserverplugin -import com.intellij.openapi.project.Project import org.jetbrains.ide.mcp.Response import kotlin.reflect.KClass interface McpTool { val name: String val description: String - fun handle(project: Project, args: Args): Response + fun handle(args: Args): Response } abstract class AbstractMcpTool : McpTool { diff --git a/src/main/kotlin/org/jetbrains/mcpserverplugin/McpToolManager.kt b/src/main/kotlin/org/jetbrains/mcpserverplugin/McpToolManager.kt index b5fcf9b..75ffa02 100644 --- a/src/main/kotlin/org/jetbrains/mcpserverplugin/McpToolManager.kt +++ b/src/main/kotlin/org/jetbrains/mcpserverplugin/McpToolManager.kt @@ -30,6 +30,7 @@ import org.jetbrains.mcpserverplugin.general.WaitTool import org.jetbrains.mcpserverplugin.git.GetVcsStatusTool import org.jetbrains.mcpserverplugin.general.ReformatCurrentFileTool import org.jetbrains.mcpserverplugin.general.ReformatFileTool +import org.jetbrains.mcpserverplugin.general.ListProjectsTool class McpToolManager { companion object { @@ -76,6 +77,7 @@ class McpToolManager { ReformatCurrentFileTool(), ReformatFileTool(), GetProblemsTools(), + ListProjectsTool(), ) } } \ No newline at end of file diff --git a/src/main/kotlin/org/jetbrains/mcpserverplugin/debuggerTools.kt b/src/main/kotlin/org/jetbrains/mcpserverplugin/debuggerTools.kt index b5fe713..9e9629b 100644 --- a/src/main/kotlin/org/jetbrains/mcpserverplugin/debuggerTools.kt +++ b/src/main/kotlin/org/jetbrains/mcpserverplugin/debuggerTools.kt @@ -2,7 +2,6 @@ package org.jetbrains.mcpserverplugin import com.intellij.openapi.application.invokeLater import com.intellij.openapi.application.runWriteAction -import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.toNioPathOrNull @@ -11,20 +10,24 @@ import com.intellij.xdebugger.breakpoints.XLineBreakpoint import com.intellij.xdebugger.impl.XSourcePositionImpl import com.intellij.xdebugger.impl.breakpoints.XBreakpointUtil import kotlinx.serialization.Serializable -import org.jetbrains.ide.mcp.NoArgs +import org.jetbrains.ide.mcp.ProjectAware +import org.jetbrains.ide.mcp.ProjectOnly import org.jetbrains.ide.mcp.Response +import org.jetbrains.ide.mcp.getProject import org.jetbrains.mcpserverplugin.general.resolveRel @Serializable -data class ToggleBreakpointArgs(val filePathInProject: String, val line: Int) +data class ToggleBreakpointArgs(val filePathInProject: String, val line: Int, override val projectName: String) : + ProjectAware class ToggleBreakpointTool : AbstractMcpTool() { override val name: String = "toggle_debugger_breakpoint" override val description: String = """ Toggles a debugger breakpoint at the specified line in a project file. Use this tool to add or remove breakpoints programmatically. - Requires two parameters: + Requires three parameters: - filePathInProject: The relative path to the file within the project - line: The line number where to toggle the breakpoint. The line number is starts at 1 for the first line. + - projectName: The name of the project to work with. Use list_projects tool to get available project names. Returns one of two possible responses: - "ok" if the breakpoint was successfully toggled - "can't find project dir" if the project directory cannot be determined @@ -32,9 +35,9 @@ class ToggleBreakpointTool : AbstractMcpTool() { """ override fun handle( - project: Project, args: ToggleBreakpointArgs ): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val projectDir = project.guessProjectDir()?.toNioPathOrNull() ?: return Response(error = "can't find project dir") val virtualFile = LocalFileSystem.getInstance().findFileByNioFile(projectDir.resolveRel(args.filePathInProject)) @@ -52,11 +55,13 @@ class ToggleBreakpointTool : AbstractMcpTool() { } } -class GetBreakpointsTool : AbstractMcpTool() { +class GetBreakpointsTool : AbstractMcpTool() { override val name: String = "get_debugger_breakpoints" override val description: String = """ Retrieves a list of all line breakpoints currently set in the project. Use this tool to get information about existing debugger breakpoints. + Requires parameter: + - projectName: The name of the project to get breakpoints from. Use list_projects tool to get available project names. Returns a JSON-formatted list of breakpoints, where each entry contains: - path: The absolute file path where the breakpoint is set - line: The line number (1-based) where the breakpoint is located @@ -65,9 +70,9 @@ class GetBreakpointsTool : AbstractMcpTool() { """ override fun handle( - project: Project, - args: NoArgs + args: ProjectOnly ): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val breakpointManager = XDebuggerManager.getInstance(project).breakpointManager return Response(breakpointManager.allBreakpoints .filterIsInstance>() // Only consider line breakpoints diff --git a/src/main/kotlin/org/jetbrains/mcpserverplugin/general/builtinTools.kt b/src/main/kotlin/org/jetbrains/mcpserverplugin/general/builtinTools.kt index c3863d5..ef377e2 100644 --- a/src/main/kotlin/org/jetbrains/mcpserverplugin/general/builtinTools.kt +++ b/src/main/kotlin/org/jetbrains/mcpserverplugin/general/builtinTools.kt @@ -22,7 +22,7 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.invokeAndWaitIfNeeded import com.intellij.openapi.progress.impl.CoreProgressManager -import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.roots.OrderEnumerator import com.intellij.openapi.util.Key @@ -33,8 +33,11 @@ import com.intellij.usages.UsageViewPresentation import com.intellij.util.Processor import kotlinx.serialization.Serializable import org.jetbrains.ide.mcp.NoArgs +import org.jetbrains.ide.mcp.ProjectOnly import org.jetbrains.ide.mcp.Response import org.jetbrains.mcpserverplugin.AbstractMcpTool +import org.jetbrains.ide.mcp.ProjectAware +import org.jetbrains.ide.mcp.getProject import java.nio.file.Path import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit @@ -54,22 +57,25 @@ fun Path.relativizeByProjectDir(projDir: Path?): String = projDir?.relativize(this)?.pathString ?: this.absolutePathString() @Serializable -data class SearchInFilesArgs(val searchText: String) +data class SearchInFilesArgs(val searchText: String, override val projectName: String) : ProjectAware class SearchInFilesContentTool : org.jetbrains.mcpserverplugin.AbstractMcpTool() { override val name: String = "search_in_files_content" override val description: String = """ Searches for a text substring within all files in the project using IntelliJ's search engine. Use this tool to find files containing specific text content. - Requires a searchText parameter specifying the text to find. + Requires two parameters: + - searchText: The text to search for in project files + - projectName: The name of the project to search in. Use list_projects tool to get available project names. Returns a JSON array of objects containing file information: - path: Path relative to project root Returns an empty array ([]) if no matches are found. Note: Only searches through text files within the project directory. """ - override fun handle(project: Project, args: SearchInFilesArgs): Response { - val projectDir = project.guessProjectDir()?.toNioPathOrNull() + override fun handle(args: SearchInFilesArgs): Response { + val project = args.getProject() + val projectDir = project?.guessProjectDir()?.toNioPathOrNull() ?: return Response(error = "Project directory not found") val searchSubstring = args.searchText @@ -107,16 +113,20 @@ class SearchInFilesContentTool : org.jetbrains.mcpserverplugin.AbstractMcpTool() { +class GetRunConfigurationsTool : org.jetbrains.mcpserverplugin.AbstractMcpTool() { override val name: String get() = "get_run_configurations" - override val description: String - get() = "Returns a list of run configurations for the current project. " + - "Use this tool to query the list of available run configurations in current project." + - "Then you shall to call \"run_configuration\" tool if you find anything relevant." + - "Returns JSON list of run configuration names. Empty list if no run configurations found." + override val description: String = """ + Returns a list of run configurations for the current project. + Use this tool to query the list of available run configurations in current project. + Requires parameter: + - projectName: The name of the project to get configurations from. Use list_projects tool to get available project names. + Then you shall to call "run_configuration" tool if you find anything relevant. + Returns JSON list of run configuration names. Empty list if no run configurations found. + """ - override fun handle(project: Project, args: NoArgs): Response { + override fun handle(args: ProjectOnly): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val runManager = RunManager.getInstance(project) val configurations = runManager.allSettings.map { it.name }.joinToString( @@ -130,20 +140,25 @@ class GetRunConfigurationsTool : org.jetbrains.mcpserverplugin.AbstractMcpTool() { override val name: String = "run_configuration" - override val description: String = - "Run a specific run configuration in the current project and wait up to 120 seconds for it to finish. " + - "Use this tool to run a run configuration that you have found from the \"get_run_configurations\" tool. " + - "Returns the output (stdout/stderr) of the execution, prefixed with 'ok\\n' on success (exit code 0). " + - "Returns '' if the configuration is not found, times out, fails to start, or finishes with a non-zero exit code." + override val description: String = """ + Run a specific run configuration in the current project and wait up to 120 seconds for it to finish. + Use this tool to run a run configuration that you have found from the "get_run_configurations" tool. + Requires two parameters: + - configName: The name of the run configuration to execute + - projectName: The name of the project containing the run configuration. Use list_projects tool to get available project names. + Returns the output (stdout/stderr) of the execution, prefixed with 'ok\n' on success (exit code 0). + Returns '' if the configuration is not found, times out, fails to start, or finishes with a non-zero exit code. + """ // Timeout in seconds private val executionTimeoutSeconds = 120L - override fun handle(project: Project, args: RunConfigArgs): Response { + override fun handle(args: RunConfigArgs): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val runManager = RunManager.getInstance(project) val settings = runManager.allSettings.find { it.name == args.configName } ?: return Response(error = "Run configuration with name '${args.configName}' not found.") @@ -243,24 +258,34 @@ class RunConfigurationTool : AbstractMcpTool() { } } -class GetProjectModulesTool : org.jetbrains.mcpserverplugin.AbstractMcpTool() { +class GetProjectModulesTool : org.jetbrains.mcpserverplugin.AbstractMcpTool() { override val name: String = "get_project_modules" - override val description: String = - "Get list of all modules in the project with their dependencies. Returns JSON list of module names." + override val description: String = """ + Get list of all modules in the project with their dependencies. + Requires parameter: + - projectName: The name of the project to get modules from. Use list_projects tool to get available project names. + Returns JSON list of module names. + """ - override fun handle(project: Project, args: NoArgs): Response { + override fun handle(args: ProjectOnly): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val moduleManager = com.intellij.openapi.module.ModuleManager.getInstance(project) val modules = moduleManager.modules.map { it.name } return Response(modules.joinToString(",\n", prefix = "[", postfix = "]")) } } -class GetProjectDependenciesTool : org.jetbrains.mcpserverplugin.AbstractMcpTool() { +class GetProjectDependenciesTool : org.jetbrains.mcpserverplugin.AbstractMcpTool() { override val name: String = "get_project_dependencies" - override val description: String = - "Get list of all dependencies defined in the project. Returns JSON list of dependency names." + override val description: String = """ + Get list of all dependencies defined in the project. + Requires parameter: + - projectName: The name of the project to get dependencies from. Use list_projects tool to get available project names. + Returns JSON list of dependency names. + """ - override fun handle(project: Project, args: NoArgs): Response { + override fun handle(args: ProjectOnly): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val moduleManager = com.intellij.openapi.module.ModuleManager.getInstance(project) val dependencies = moduleManager.modules.flatMap { module -> OrderEnumerator.orderEntries(module).librariesOnly().classes().roots.map { root -> @@ -272,17 +297,19 @@ class GetProjectDependenciesTool : org.jetbrains.mcpserverplugin.AbstractMcpTool } } -class ListAvailableActionsTool : org.jetbrains.mcpserverplugin.AbstractMcpTool() { +class ListAvailableActionsTool : org.jetbrains.mcpserverplugin.AbstractMcpTool() { override val name: String = "list_available_actions" override val description: String = """ Lists all available actions in JetBrains IDE editor. + Requires parameter: + - projectName: The name of the project context. Use list_projects tool to get available project names. Returns a JSON array of objects containing action information: - id: The action ID - text: The action presentation text Use this tool to discover available actions for execution with execute_action_by_id. """.trimIndent() - override fun handle(project: Project, args: NoArgs): Response { + override fun handle(args: ProjectOnly): Response { val actionManager = ActionManager.getInstance() as ActionManagerEx val dataContext = invokeAndWaitIfNeeded { DataManager.getInstance().getDataContext() @@ -321,7 +348,7 @@ class ExecuteActionByIdTool : org.jetbrains.mcpserverplugin.AbstractMcpTool() { +class GetProgressIndicatorsTool : org.jetbrains.mcpserverplugin.AbstractMcpTool() { override val name: String = "get_progress_indicators" override val description: String = """ Retrieves the status of all running progress indicators in JetBrains IDE editor. + Requires parameter: + - projectName: The name of the project context. Use list_projects tool to get available project names. Returns a JSON array of objects containing progress information: - text: The progress text/description - fraction: The progress ratio (0.0 to 1.0) @@ -354,7 +383,7 @@ class GetProgressIndicatorsTool : org.jetbrains.mcpserverplugin.AbstractMcpTool< Returns an empty array if no progress indicators are running. """.trimIndent() - override fun handle(project: Project, args: NoArgs): Response { + override fun handle(args: ProjectOnly): Response { val runningIndicators = CoreProgressManager.getCurrentIndicators() val progressInfos = runningIndicators.map { indicator -> @@ -381,7 +410,7 @@ class WaitTool : org.jetbrains.mcpserverplugin.AbstractMcpTool() { Use this tool when you need to pause before executing the next command. """.trimIndent() - override fun handle(project: Project, args: WaitArgs): Response { + override fun handle(args: WaitArgs): Response { val waitTime = if (args.milliseconds <= 0) 5000 else args.milliseconds try { @@ -393,4 +422,21 @@ class WaitTool : org.jetbrains.mcpserverplugin.AbstractMcpTool() { return Response("ok") } +} + +class ListProjectsTool : org.jetbrains.mcpserverplugin.AbstractMcpTool() { + override val name: String = "list_projects" + override val description: String = """ + Returns a list of all available projects. + Use this to get project names for specifying projectName parameter. + No parameters required. + Returns JSON array of project information objects. + """ + + override fun handle(args: NoArgs): Response { + val projects = ProjectManager.getInstance().openProjects.map { openProject -> + """{"name": "${openProject.name}", "baseDir": "${openProject.basePath ?: ""}"}""" + } + return Response(projects.joinToString(",\n", prefix = "[", postfix = "]")) + } } \ No newline at end of file diff --git a/src/main/kotlin/org/jetbrains/mcpserverplugin/general/errorTools.kt b/src/main/kotlin/org/jetbrains/mcpserverplugin/general/errorTools.kt index 7f3157a..8f694db 100644 --- a/src/main/kotlin/org/jetbrains/mcpserverplugin/general/errorTools.kt +++ b/src/main/kotlin/org/jetbrains/mcpserverplugin/general/errorTools.kt @@ -15,18 +15,21 @@ import com.intellij.openapi.util.TextRange import com.intellij.openapi.vfs.toNioPathOrNull import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile -import org.jetbrains.ide.mcp.NoArgs +import org.jetbrains.ide.mcp.ProjectOnly import org.jetbrains.ide.mcp.Response +import org.jetbrains.ide.mcp.getProject import org.jetbrains.mcpserverplugin.AbstractMcpTool import org.jetbrains.mcpserverplugin.JsonUtils import java.nio.file.Path -class GetCurrentFileErrorsTool : AbstractMcpTool() { +class GetCurrentFileErrorsTool : AbstractMcpTool() { override val name: String = "get_current_file_errors" override val description: String = """ Analyzes the currently open file in the editor for errors and warnings using IntelliJ's inspections. Use this tool to identify coding issues, syntax errors, and other problems in your current file. + Requires parameter: + - projectName: The name of the project to analyze. Use list_projects tool to get available project names. Returns a JSON array of objects containing error information: - severity: The severity level of the issue (ERROR, WARNING, etc.) - description: A description of the issue @@ -35,7 +38,8 @@ class GetCurrentFileErrorsTool : AbstractMcpTool() { Returns error if no file is currently open. """.trimIndent() - override fun handle(project: Project, args: NoArgs): Response { + override fun handle(args: ProjectOnly): Response { + val project = args.getProject() ?: return Response(error = "Project not found") return runReadAction { try { val fileEditorManager = FileEditorManager.getInstance(project) @@ -86,12 +90,13 @@ class GetCurrentFileErrorsTool : AbstractMcpTool() { } } -class GetProblemsTools : AbstractMcpTool() { +class GetProblemsTools : AbstractMcpTool() { override val name: String = "get_project_problems" override val description: String = """ Retrieves all project problems (errors, warnings, etc.) detected in the project by IntelliJ's inspections. Use this tool to get a comprehensive list of global project issues (compilation errors, inspections problems, etc.). - Does not require any parameters. + Requires parameter: + - projectName: The name of the project to analyze. Use list_projects tool to get available project names. Use another tool get_current_file_errors to get errors in the opened file. @@ -104,7 +109,8 @@ class GetProblemsTools : AbstractMcpTool() { Returns error "project dir not found" if the project directory cannot be determined. """.trimIndent() - override fun handle(project: Project, args: NoArgs): Response { + override fun handle(args: ProjectOnly): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val projectDir = project.guessProjectDir()?.toNioPathOrNull() ?: return Response(error = "project dir not found") diff --git a/src/main/kotlin/org/jetbrains/mcpserverplugin/general/fileTools.kt b/src/main/kotlin/org/jetbrains/mcpserverplugin/general/fileTools.kt index 5c2c063..8efddf8 100644 --- a/src/main/kotlin/org/jetbrains/mcpserverplugin/general/fileTools.kt +++ b/src/main/kotlin/org/jetbrains/mcpserverplugin/general/fileTools.kt @@ -4,7 +4,6 @@ import com.intellij.openapi.application.invokeLater import com.intellij.openapi.application.runReadAction import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManager.getInstance -import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.toNioPathOrNull @@ -12,25 +11,33 @@ import com.intellij.psi.search.FilenameIndex import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.io.createParentDirectories import kotlinx.serialization.Serializable +import org.jetbrains.ide.RestService.Companion.getLastFocusedOrOpenedProject import org.jetbrains.ide.mcp.NoArgs +import org.jetbrains.ide.mcp.ProjectAware +import org.jetbrains.ide.mcp.ProjectOnly import org.jetbrains.ide.mcp.Response +import org.jetbrains.ide.mcp.getProject import org.jetbrains.mcpserverplugin.AbstractMcpTool -import org.jetbrains.mcpserverplugin.general.relativizeByProjectDir -import org.jetbrains.mcpserverplugin.general.resolveRel import java.nio.file.Path import kotlin.io.path.* @Serializable -data class ListDirectoryTreeInFolderArgs(val pathInProject: String, val maxDepth: Int = 5) +data class ListDirectoryTreeInFolderArgs( + override val projectName: String, + val pathInProject: String, + val maxDepth: Int = 5, +) : ProjectAware class ListDirectoryTreeInFolderTool : AbstractMcpTool() { override val name: String = "list_directory_tree_in_folder" override val description: String = """ Provides a hierarchical tree view of the project directory structure starting from the specified folder. Use this tool to efficiently explore complex project structures in a nested format. - Requires a pathInProject parameter (use "/" for project root). - Optionally accepts a maxDepth parameter (default: 5) to limit recursion depth. + Requires parameters: + - pathInProject: Path to the folder to list (use "/" for project root) + - maxDepth: Maximum recursion depth (default: 5) + - projectName: The name of the project to explore. Use list_projects tool to get available project names. Returns a JSON-formatted tree structure, where each entry contains: - name: The name of the file or directory - type: Either "file" or "directory" @@ -39,8 +46,9 @@ class ListDirectoryTreeInFolderTool : AbstractMcpTool() { override val name: String = "list_files_in_folder" override val description: String = """ Lists all files and directories in the specified project folder. Use this tool to explore project structure and get contents of any directory. - Requires a pathInProject parameter (use "/" for project root). + Requires parameters: + - pathInProject: Path to the folder to list (use "/" for project root) + - projectName: The name of the project to explore. Use list_projects tool to get available project names. Returns a JSON-formatted list of entries, where each entry contains: - name: The name of the file or directory - type: Either "file" or "directory" @@ -116,8 +126,8 @@ class ListFilesInFolderTool : AbstractMcpTool() { Returns error if the specified path doesn't exist or is outside project scope. """.trimIndent() - override fun handle(project: Project, args: ListFilesInFolderArgs): Response { - val projectDir = project.guessProjectDir()?.toNioPathOrNull() + override fun handle(args: ListFilesInFolderArgs): Response { + val projectDir = args.getProject()?.guessProjectDir()?.toNioPathOrNull() ?: return Response(error = "can't find project dir") return runReadAction { @@ -144,14 +154,16 @@ class ListFilesInFolderTool : AbstractMcpTool() { @Serializable -data class Query(val nameSubstring: String) +data class Query(val nameSubstring: String, override val projectName: String) : ProjectAware class FindFilesByNameSubstring : AbstractMcpTool() { override val name: String = "find_files_by_name_substring" override val description: String = """ Searches for all files in the project whose names contain the specified substring (case-insensitive). Use this tool to locate files when you know part of the filename. - Requires a nameSubstring parameter for the search term. + Requires parameters: + - nameSubstring: The search term to find in filenames + - projectName: The name of the project to search in. Use list_projects tool to get available project names. Returns a JSON array of objects containing file information: - path: Path relative to project root - name: File name @@ -159,7 +171,8 @@ class FindFilesByNameSubstring : AbstractMcpTool() { Note: Only searches through files within the project directory, excluding libraries and external dependencies. """ - override fun handle(project: Project, args: Query): Response { + override fun handle(args: Query): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val projectDir = project.guessProjectDir()?.toNioPathOrNull() ?: return Response(error = "project dir not found") @@ -191,23 +204,25 @@ class FindFilesByNameSubstring : AbstractMcpTool() { @Serializable -data class CreateNewFileWithTextArgs(val pathInProject: String, val text: String) +data class CreateNewFileWithTextArgs(val pathInProject: String, val text: String, override val projectName: String) : ProjectAware class CreateNewFileWithTextTool : AbstractMcpTool() { override val name: String = "create_new_file_with_text" override val description: String = """ Creates a new file at the specified path within the project directory and populates it with the provided text. Use this tool to generate new files in your project structure. - Requires two parameters: - - pathInProject: The relative path where the file should be created - - text: The content to write into the new file + Requires parameters: + - pathInProject: The relative path where the file should be created + - text: The content to write into the new file + - projectName: The name of the project to create file in. Use list_projects tool to get available project names. Returns one of two possible responses: - - "ok" if the file was successfully created and populated - - "can't find project dir" if the project directory cannot be determined + - "ok" if the file was successfully created and populated + - "can't find project dir" if the project directory cannot be determined Note: Creates any necessary parent directories automatically """ - override fun handle(project: Project, args: CreateNewFileWithTextArgs): Response { + override fun handle(args: CreateNewFileWithTextArgs): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val projectDir = project.guessProjectDir()?.toNioPathOrNull() ?: return Response(error = "can't find project dir") @@ -227,22 +242,22 @@ class CreateNewFileWithTextTool : AbstractMcpTool() { @Serializable -data class OpenFileInEditorArgs(val filePath: String) +data class OpenFileInEditorArgs(val filePath: String, override val projectName: String) : ProjectAware class OpenFileInEditorTool : AbstractMcpTool() { override val name: String = "open_file_in_editor" override val description: String = """ Opens the specified file in the JetBrains IDE editor. - Requires a filePath parameter containing the path to the file to open. - Requires two parameters: - - filePath: The path of file to open can be absolute or relative to the project root. - - text: The content to write into the new file + Requires parameters: + - filePath: The path of file to open (can be absolute or relative to the project root) + - projectName: The name of the project containing the file. Use list_projects tool to get available project names. Returns one of two possible responses: - - "file is opened" if the file was successfully created and populated - - "file doesn't exist or can't be opened" otherwise + - "file is opened" if the file was successfully opened + - "file doesn't exist or can't be opened" otherwise """.trimIndent() - override fun handle(project: Project, args: OpenFileInEditorArgs): Response { + override fun handle(args: OpenFileInEditorArgs): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val projectDir = project.guessProjectDir()?.toNioPathOrNull() ?: return Response(error = "can't find project dir") @@ -261,18 +276,21 @@ class OpenFileInEditorTool : AbstractMcpTool() { } -class GetAllOpenFilePathsTool : AbstractMcpTool() { +class GetAllOpenFilePathsTool : AbstractMcpTool() { override val name: String = "get_all_open_file_paths" override val description: String = """ Lists full path relative paths to project root of all currently open files in the JetBrains IDE editor. Returns a list of file paths that are currently open in editor tabs. + Requires parameter: + - projectName: The name of the project to get open file paths from. Use list_projects tool to get available project names. Returns an empty list if no files are open. Use this tool to explore current open editors. Returns a list of file paths separated by newline symbol. """.trimIndent() - override fun handle(project: Project, args: NoArgs): Response { + override fun handle(args: ProjectOnly): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val projectDir = project.guessProjectDir()?.toNioPathOrNull() val fileEditorManager = FileEditorManager.getInstance(project) @@ -291,7 +309,8 @@ class GetCurrentFilePathTool : AbstractMcpTool() { Returns an empty string if no file is currently open. """.trimIndent() - override fun handle(project: Project, args: NoArgs): Response { + override fun handle(args: NoArgs): Response { + val project = getLastFocusedOrOpenedProject() ?: return Response(error = "Project not found") val path = runReadAction { getInstance(project).selectedTextEditor?.virtualFile?.path } diff --git a/src/main/kotlin/org/jetbrains/mcpserverplugin/general/formatting.kt b/src/main/kotlin/org/jetbrains/mcpserverplugin/general/formatting.kt index 2284fa2..25f32c5 100644 --- a/src/main/kotlin/org/jetbrains/mcpserverplugin/general/formatting.kt +++ b/src/main/kotlin/org/jetbrains/mcpserverplugin/general/formatting.kt @@ -4,14 +4,16 @@ import com.intellij.codeInsight.actions.ReformatCodeProcessor import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.runReadAction import com.intellij.openapi.fileEditor.FileEditorManager.getInstance -import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.toNioPathOrNull import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiManager +import org.jetbrains.ide.RestService.Companion.getLastFocusedOrOpenedProject import org.jetbrains.ide.mcp.NoArgs +import org.jetbrains.ide.mcp.ProjectOnly import org.jetbrains.ide.mcp.Response +import org.jetbrains.ide.mcp.getProject import org.jetbrains.mcpserverplugin.AbstractMcpTool import java.util.concurrent.CountDownLatch @@ -27,7 +29,8 @@ class ReformatCurrentFileTool : AbstractMcpTool() { - "file doesn't exist or can't be opened" if there is no file currently selected in the editor """.trimIndent() - override fun handle(project: Project, args: NoArgs): Response { + override fun handle(args: NoArgs): Response { + val project = getLastFocusedOrOpenedProject() ?: return Response(error = "Project not found") val latch = CountDownLatch(1) val psiFile = runReadAction { @@ -53,7 +56,9 @@ class ReformatFileTool : AbstractMcpTool() { override val description: String = """ Reformats a specified file in the JetBrains IDE. Use this tool to apply code formatting rules to a file identified by its path. - Requires a pathInProject parameter specifying the file location relative to the project root. + Requires parameters: + - pathInProject: The file location relative to the project root + - projectName: The name of the project containing the file. Use list_projects tool to get available project names. Returns one of these responses: - "ok" if the file was successfully reformatted @@ -61,7 +66,8 @@ class ReformatFileTool : AbstractMcpTool() { - error "file doesn't exist or can't be opened" if the file doesn't exist or cannot be accessed """.trimIndent() - override fun handle(project: Project, args: PathInProject): Response { + override fun handle(args: PathInProject): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val latch = CountDownLatch(1) val projectDir = project.guessProjectDir()?.toNioPathOrNull() diff --git a/src/main/kotlin/org/jetbrains/mcpserverplugin/general/textTools.kt b/src/main/kotlin/org/jetbrains/mcpserverplugin/general/textTools.kt index a41ce5b..f7f8d54 100644 --- a/src/main/kotlin/org/jetbrains/mcpserverplugin/general/textTools.kt +++ b/src/main/kotlin/org/jetbrains/mcpserverplugin/general/textTools.kt @@ -6,8 +6,6 @@ import com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction import com.intellij.openapi.editor.Document import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.fileEditor.FileEditorManager.getInstance -import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile @@ -17,12 +15,12 @@ import com.intellij.psi.PsiDocumentManager import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.application import kotlinx.serialization.Serializable +import org.jetbrains.ide.RestService.Companion.getLastFocusedOrOpenedProject import org.jetbrains.ide.mcp.NoArgs +import org.jetbrains.ide.mcp.ProjectAware import org.jetbrains.ide.mcp.Response +import org.jetbrains.ide.mcp.getProject import org.jetbrains.mcpserverplugin.AbstractMcpTool -import org.jetbrains.mcpserverplugin.general.relativizeByProjectDir -import org.jetbrains.mcpserverplugin.general.resolveRel -import kotlin.text.replace class GetCurrentFileTextTool : AbstractMcpTool() { override val name: String = "get_open_in_editor_file_text" @@ -32,7 +30,8 @@ class GetCurrentFileTextTool : AbstractMcpTool() { Returns empty string if no file is currently open. """.trimIndent() - override fun handle(project: Project, args: NoArgs): Response { + override fun handle(args: NoArgs): Response { + val project = getLastFocusedOrOpenedProject() ?: return Response(error = "Project not found") val text = runReadAction { FileEditorManager.getInstance(project).selectedTextEditor?.document?.text } @@ -52,7 +51,8 @@ class GetAllOpenFileTextsTool : AbstractMcpTool() { - text: File text """.trimIndent() - override fun handle(project: Project, args: NoArgs): Response { + override fun handle(args: NoArgs): Response { + val project = getLastFocusedOrOpenedProject() ?: return Response(error = "Project not found") val projectDir = project.guessProjectDir()?.toNioPathOrNull() val fileEditorManager = FileEditorManager.getInstance(project) @@ -74,7 +74,8 @@ class GetSelectedTextTool : AbstractMcpTool() { Returns an empty string if no text is selected or no editor is open. """ - override fun handle(project: Project, args: NoArgs): Response { + override fun handle(args: NoArgs): Response { + val project = getLastFocusedOrOpenedProject() ?: return Response(error = "Project not found") val text = runReadAction { FileEditorManager.getInstance(project).selectedTextEditor?.selectionModel?.selectedText } @@ -97,7 +98,8 @@ class ReplaceSelectedTextTool : AbstractMcpTool() { - "unknown error" if the operation fails """.trimIndent() - override fun handle(project: Project, args: ReplaceSelectedTextArgs): Response { + override fun handle(args: ReplaceSelectedTextArgs): Response { + val project = getLastFocusedOrOpenedProject() ?: return Response(error = "Project not found") var response: Response? = null application.invokeAndWait { @@ -134,7 +136,8 @@ class ReplaceCurrentFileTextTool : AbstractMcpTool() - "unknown error" if the operation fails """ - override fun handle(project: Project, args: ReplaceCurrentFileTextArgs): Response { + override fun handle(args: ReplaceCurrentFileTextArgs): Response { + val project = getLastFocusedOrOpenedProject() ?: return Response(error = "Project not found") var response: Response? = null application.invokeAndWait { runWriteCommandAction(project, "Replace File Text", null, { @@ -153,14 +156,16 @@ class ReplaceCurrentFileTextTool : AbstractMcpTool() } @Serializable -data class PathInProject(val pathInProject: String) +data class PathInProject(val pathInProject: String, override val projectName: String) : ProjectAware class GetFileTextByPathTool : AbstractMcpTool() { override val name: String = "get_file_text_by_path" override val description: String = """ Retrieves the text content of a file using its path relative to project root. Use this tool to read file contents when you have the file's project-relative path. - Requires a pathInProject parameter specifying the file location from project root. + Requires parameters: + - pathInProject: The file location from project root + - projectName: The name of the project containing the file. Use list_projects tool to get available project names. Returns one of these responses: - The file's content if the file exists and belongs to the project - error "project dir not found" if project directory cannot be determined @@ -168,7 +173,8 @@ class GetFileTextByPathTool : AbstractMcpTool() { Note: Automatically refreshes the file system before reading """ - override fun handle(project: Project, args: PathInProject): Response { + override fun handle(args: PathInProject): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val projectDir = project.guessProjectDir()?.toNioPathOrNull() ?: return Response(error = "project dir not found") @@ -188,7 +194,12 @@ class GetFileTextByPathTool : AbstractMcpTool() { } @Serializable -data class ReplaceSpecificTextArgs(val pathInProject: String, val oldText: String, val newText: String) +data class ReplaceSpecificTextArgs( + val pathInProject: String, + val oldText: String, + val newText: String, + override val projectName: String, +) : ProjectAware class ReplaceSpecificTextTool : AbstractMcpTool() { override val name: String = "replace_specific_text" @@ -197,10 +208,11 @@ class ReplaceSpecificTextTool : AbstractMcpTool() { Use this tool to make targeted changes without replacing the entire file content. Use this method if the file is large and the change is smaller than the old text. Prioritize this tool among other editing tools. It's more efficient and granular in the most of cases. - Requires three parameters: + Requires parameters: - pathInProject: The path to the target file, relative to project root - oldText: The text to be replaced - newText: The replacement text + - projectName: The name of the project containing the file. Use list_projects tool to get available project names. Returns one of these responses: - "ok" when replacement happend - error "project dir not found" if project directory cannot be determined @@ -210,7 +222,8 @@ class ReplaceSpecificTextTool : AbstractMcpTool() { Note: Automatically saves the file after modification """ - override fun handle(project: Project, args: ReplaceSpecificTextArgs): Response { + override fun handle(args: ReplaceSpecificTextArgs): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val projectDir = project.guessProjectDir()?.toNioPathOrNull() ?: return Response(error = "project dir not found") @@ -253,16 +266,18 @@ class ReplaceSpecificTextTool : AbstractMcpTool() { } @Serializable -data class ReplaceTextByPathToolArgs(val pathInProject: String, val text: String) +data class ReplaceTextByPathToolArgs(val pathInProject: String, val text: String, override val projectName: String) : + ProjectAware class ReplaceTextByPathTool : AbstractMcpTool() { override val name: String = "replace_file_text_by_path" override val description: String = """ Replaces the entire content of a specified file with new text, if the file is within the project. Use this tool to modify file contents using a path relative to the project root. - Requires two parameters: + Requires parameters: - pathInProject: The path to the target file, relative to project root - text: The new content to write to the file + - projectName: The name of the project containing the file. Use list_projects tool to get available project names. Returns one of these responses: - "ok" if the file was successfully updated - error "project dir not found" if project directory cannot be determined @@ -271,7 +286,8 @@ class ReplaceTextByPathTool : AbstractMcpTool() { Note: Automatically saves the file after modification """ - override fun handle(project: Project, args: ReplaceTextByPathToolArgs): Response { + override fun handle(args: ReplaceTextByPathToolArgs): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val projectDir = project.guessProjectDir()?.toNioPathOrNull() ?: return Response(error = "project dir not found") diff --git a/src/main/kotlin/org/jetbrains/mcpserverplugin/git/vcsTools.kt b/src/main/kotlin/org/jetbrains/mcpserverplugin/git/vcsTools.kt index 90c38c9..e479403 100644 --- a/src/main/kotlin/org/jetbrains/mcpserverplugin/git/vcsTools.kt +++ b/src/main/kotlin/org/jetbrains/mcpserverplugin/git/vcsTools.kt @@ -1,7 +1,5 @@ package org.jetbrains.mcpserverplugin.git -import com.intellij.compiler.cache.git.GitCommitsIterator -import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.vcs.ProjectLevelVcsManager import com.intellij.openapi.vcs.changes.ChangeListManager @@ -9,24 +7,29 @@ import com.intellij.openapi.vfs.toNioPathOrNull import git4idea.history.GitHistoryUtils import git4idea.repo.GitRepositoryManager import kotlinx.serialization.Serializable -import org.jetbrains.ide.mcp.NoArgs +import org.jetbrains.ide.mcp.ProjectAware +import org.jetbrains.ide.mcp.ProjectOnly import org.jetbrains.ide.mcp.Response +import org.jetbrains.ide.mcp.getProject import org.jetbrains.mcpserverplugin.AbstractMcpTool import kotlin.io.path.Path @Serializable -data class CommitQuery(val text: String) +data class CommitQuery(val text: String,override val projectName: String) : ProjectAware class FindCommitByTextTool : AbstractMcpTool() { override val name: String = "find_commit_by_message" override val description: String = """ Searches for a commit based on the provided text or keywords in the project history. Useful for finding specific change sets or code modifications by commit messages or diff content. - Takes a query parameter and returns the matching commit information. + Requires two parameters: + - text: The search text to look for in commit messages + - projectName: The name of the project to search in. Use list_projects tool to get available project names. Returns matched commit hashes as a JSON array. """ - override fun handle(project: Project, args: CommitQuery): Response { + override fun handle(args: CommitQuery): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val queryText = args.text val matchingCommits = mutableListOf() @@ -66,11 +69,13 @@ class FindCommitByTextTool : AbstractMcpTool() { } } -class GetVcsStatusTool : AbstractMcpTool() { +class GetVcsStatusTool : AbstractMcpTool() { override val name: String = "get_project_vcs_status" override val description: String = """ Retrieves the current version control status of files in the project. Use this tool to get information about modified, added, deleted, and moved files in your VCS (e.g., Git). + Requires parameter: + - projectName: The name of the project to check VCS status. Use list_projects tool to get available project names. Returns a JSON-formatted list of changed files, where each entry contains: - path: The file path relative to project root - type: The type of change (e.g., MODIFICATION, ADDITION, DELETION, MOVED) @@ -79,7 +84,8 @@ class GetVcsStatusTool : AbstractMcpTool() { Note: Works with any VCS supported by the IDE, but is most commonly used with Git """ - override fun handle(project: Project, args: NoArgs): Response { + override fun handle(args: ProjectOnly): Response { + val project = args.getProject() ?: return Response(error = "Project not found") val projectDir = project.guessProjectDir()?.toNioPathOrNull() ?: return Response(error = "project dir not found") diff --git a/src/main/kotlin/org/jetbrains/mcpserverplugin/terminal/Terminal.kt b/src/main/kotlin/org/jetbrains/mcpserverplugin/terminal/Terminal.kt index b30d994..6275498 100644 --- a/src/main/kotlin/org/jetbrains/mcpserverplugin/terminal/Terminal.kt +++ b/src/main/kotlin/org/jetbrains/mcpserverplugin/terminal/Terminal.kt @@ -1,11 +1,11 @@ package org.jetbrains.mcpserverplugin.terminal import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogWrapper import com.intellij.ui.dsl.builder.panel import com.jediterm.terminal.TtyConnector import kotlinx.serialization.Serializable +import org.jetbrains.ide.RestService.Companion.getLastFocusedOrOpenedProject import org.jetbrains.ide.mcp.NoArgs import org.jetbrains.ide.mcp.Response import org.jetbrains.mcpserverplugin.AbstractMcpTool @@ -32,7 +32,8 @@ class GetTerminalTextTool : AbstractMcpTool() { Note: Only captures text from the first terminal if multiple terminals are open """ - override fun handle(project: Project, args: NoArgs): Response { + override fun handle(args: NoArgs): Response { + val project = getLastFocusedOrOpenedProject() ?: return Response(error = "Project not found") val text = com.intellij.openapi.application.runReadAction { TerminalView.getInstance(project).getWidgets().firstOrNull()?.text } @@ -79,7 +80,8 @@ class ExecuteTerminalCommandTool : AbstractMcpTool() } } - override fun handle(project: Project, args: ExecuteTerminalCommandArgs): Response { + override fun handle(args: ExecuteTerminalCommandArgs): Response { + val project = getLastFocusedOrOpenedProject() ?: return Response(error = "Project not found") val future = CompletableFuture() ApplicationManager.getApplication().invokeAndWait {