From 867c71bc95928e413ed07f89df5edcce909dd092 Mon Sep 17 00:00:00 2001 From: "Artem.Bukhonov" Date: Wed, 9 Jul 2025 22:24:35 +0200 Subject: [PATCH] [MCP Server] Truncate output in execution tools GitOrigin-RevId: 429d3e6ff370d9f9d341906a641bb83385be9387 --- .../intellij/mcpserver/toolsets/Constants.kt | 6 ++++ .../toolsets/general/ExecutionToolset.kt | 9 +++++- .../toolsets/terminal/TerminalToolset.kt | 29 ++++++++++++------- .../toolsets/terminal/terminalToolsetUtil.kt | 18 ++++++++++-- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/Constants.kt b/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/Constants.kt index 152cf360765e..5368294cccfb 100644 --- a/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/Constants.kt +++ b/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/Constants.kt @@ -1,9 +1,15 @@ package com.intellij.mcpserver.toolsets +import com.intellij.mcpserver.util.TruncateMode + object Constants { const val RELATIVE_PATH_IN_PROJECT_DESCRIPTION: String = "Path relative to the project root" const val TIMEOUT_MILLISECONDS_DESCRIPTION: String = "Timeout in milliseconds" const val LONG_TIMEOUT_MILLISECONDS_VALUE: Int = 60 * 1000 const val MEDIUM_TIMEOUT_MILLISECONDS_VALUE: Int = 10 * 1000 const val SHORT_TIMEOUT_MILLISECONDS_VALUE: Int = 1 * 1000 + const val MAX_LINES_COUNT_DESCRIPTION: String = "Maximum number of lines to return" + const val MAX_LINES_COUNT_VALUE: Int = 1000 + const val TRUNCATE_MODE_DESCRIPTION: String = "How to truncate the text: from the start, in the middle, at the end, or don't truncate at all" + val TRUCATE_MODE_VALUE: TruncateMode = TruncateMode.START } \ No newline at end of file diff --git a/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/general/ExecutionToolset.kt b/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/general/ExecutionToolset.kt index 53b890369fc7..a35d40db2ddd 100644 --- a/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/general/ExecutionToolset.kt +++ b/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/general/ExecutionToolset.kt @@ -19,7 +19,9 @@ import com.intellij.mcpserver.annotations.McpTool import com.intellij.mcpserver.mcpFail import com.intellij.mcpserver.project import com.intellij.mcpserver.toolsets.Constants +import com.intellij.mcpserver.util.TruncateMode import com.intellij.mcpserver.util.checkUserConfirmationIfNeeded +import com.intellij.mcpserver.util.truncateText import com.intellij.openapi.application.EDT import com.intellij.openapi.application.readAction import com.intellij.openapi.util.Key @@ -71,6 +73,10 @@ class ExecutionToolset : McpToolset { configurationName: String, @McpDescription(Constants.TIMEOUT_MILLISECONDS_DESCRIPTION) timeout: Int = Constants.LONG_TIMEOUT_MILLISECONDS_VALUE, + @McpDescription(Constants.MAX_LINES_COUNT_DESCRIPTION) + maxLinesCount: Int = Constants.MAX_LINES_COUNT_VALUE, + @McpDescription(Constants.TRUNCATE_MODE_DESCRIPTION) + truncateMode: TruncateMode = Constants.TRUCATE_MODE_VALUE, ): RunConfigurationResult { val project = currentCoroutineContext().project val runManager = RunManager.getInstance(project) @@ -128,10 +134,11 @@ class ExecutionToolset : McpToolset { val exitCode = withTimeoutOrNull(timeout.milliseconds) { exitCodeDeferred.await() } + val output = truncateText(outputBuilder.toString(), maxLinesCount = maxLinesCount, truncateMode = truncateMode) return RunConfigurationResult( exitCode = exitCode, timedOut = exitCode == null, - output = outputBuilder.toString() + output = output ) } diff --git a/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/terminal/TerminalToolset.kt b/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/terminal/TerminalToolset.kt index 3c19de7d5dad..d72f1dbf0fd1 100644 --- a/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/terminal/TerminalToolset.kt +++ b/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/terminal/TerminalToolset.kt @@ -8,6 +8,7 @@ import com.intellij.mcpserver.annotations.McpDescription import com.intellij.mcpserver.annotations.McpTool import com.intellij.mcpserver.project import com.intellij.mcpserver.toolsets.Constants +import com.intellij.mcpserver.util.TruncateMode import com.intellij.mcpserver.util.checkUserConfirmationIfNeeded import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.wm.ToolWindowManager @@ -16,14 +17,11 @@ import kotlinx.serialization.EncodeDefault import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import org.jetbrains.plugins.terminal.TerminalToolWindowFactory -import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.milliseconds private val logger = logger() class TerminalToolset : McpToolset { - private val maxLineCount = 2000 - private val timeout = 2.minutes - @McpTool @McpDescription(""" Executes a specified shell command in the IDE's integrated terminal. @@ -42,17 +40,18 @@ class TerminalToolset : McpToolset { suspend fun execute_terminal_command( @McpDescription("Shell command to execute") command: String, - @McpDescription("""Whether to execute the command in a default user's shell (bash, zsh, etc.). |Useful if the command is not a commandline but a shell script, or if it's important to preserve real environment of the user's terminal. |In the case of 'false' value the command will be started as a process""") - execute_in_shell: Boolean = false, - + executeInShell: Boolean = false, @McpDescription("Whether to reuse an existing terminal window. Allows to avoid creating multiple terminals") - reuse_existing_terminal_window: Boolean = true, - + reuseExistingTerminalWindow: Boolean = true, @McpDescription(Constants.TIMEOUT_MILLISECONDS_DESCRIPTION) - timeout_milliseconds: Int = Constants.LONG_TIMEOUT_MILLISECONDS_VALUE, + timeout: Int = Constants.LONG_TIMEOUT_MILLISECONDS_VALUE, + @McpDescription(Constants.MAX_LINES_COUNT_DESCRIPTION) + maxLinesCount: Int = Constants.MAX_LINES_COUNT_VALUE, + @McpDescription(Constants.TRUNCATE_MODE_DESCRIPTION) + truncateMode: TruncateMode = Constants.TRUCATE_MODE_VALUE, ): CommandExecutionResult { val project = currentCoroutineContext().project checkUserConfirmationIfNeeded(McpServerBundle.message("label.do.you.want.to.execute.command.in.terminal"), command, project) @@ -60,7 +59,15 @@ class TerminalToolset : McpToolset { // TODO pass from http request later (MCP Client name or something else) val id = "mcp_session" val window = ToolWindowManager.getInstance(project).getToolWindow(TerminalToolWindowFactory.TOOL_WINDOW_ID) - return executeShellCommand(window = window, project = project, command = command, executeInShell = execute_in_shell, sessionId = if (reuse_existing_terminal_window) id else null, timeout = timeout) + return executeShellCommand(window = window, + project = project, + command = command, + executeInShell = executeInShell, + sessionId = if (reuseExistingTerminalWindow) id else null, + timeout = Constants.LONG_TIMEOUT_MILLISECONDS_VALUE.milliseconds, + maxLinesCount = maxLinesCount, + truncateMode = truncateMode + ) } @OptIn(ExperimentalSerializationApi::class) diff --git a/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/terminal/terminalToolsetUtil.kt b/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/terminal/terminalToolsetUtil.kt index 47d916721fc2..dc8ba197594f 100644 --- a/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/terminal/terminalToolsetUtil.kt +++ b/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/terminal/terminalToolsetUtil.kt @@ -9,6 +9,8 @@ import com.intellij.execution.process.ProcessOutputTypes import com.intellij.mcpserver.McpServerBundle import com.intellij.mcpserver.mcpFail import com.intellij.mcpserver.toolsets.terminal.TerminalToolset.CommandExecutionResult +import com.intellij.mcpserver.util.TruncateMode +import com.intellij.mcpserver.util.truncateText import com.intellij.openapi.application.EDT import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key @@ -28,7 +30,16 @@ import kotlin.time.Duration class CommandSession(val sessionId: String, val console: TerminalExecutionConsole) val MCP_TERMINAL_KEY: Key = Key.create("MCP_TERMINAL_KEY") -suspend fun executeShellCommand(window: ToolWindow?, project: Project, command: String, executeInShell: Boolean, sessionId: String?, timeout: Duration): CommandExecutionResult { +suspend fun executeShellCommand( + window: ToolWindow?, + project: Project, + command: String, + executeInShell: Boolean, + sessionId: String?, + timeout: Duration, + maxLinesCount: Int, + truncateMode: TruncateMode = TruncateMode.START, +): CommandExecutionResult { val defaultShell = ShConfigurationType.getDefaultShell(project) val commandLine = if (executeInShell) { @@ -95,8 +106,9 @@ suspend fun executeShellCommand(window: ToolWindow?, project: Project, command: val exitCodeValue = withTimeoutOrNull(timeout) { exitCode.await() } + val truncateText = truncateText(text = output.toString(), maxLinesCount = maxLinesCount, truncateMode = truncateMode) if (exitCodeValue == null) { - return CommandExecutionResult(is_timed_out = true, command_output = output.toString()) + return CommandExecutionResult(is_timed_out = true, command_output = truncateText) } - return CommandExecutionResult(command_exit_code = exitCodeValue, command_output = output.toString()) + return CommandExecutionResult(command_exit_code = exitCodeValue, command_output = truncateText) } \ No newline at end of file