[MCP Server] Truncate output in execution tools

GitOrigin-RevId: 429d3e6ff370d9f9d341906a641bb83385be9387
This commit is contained in:
Artem.Bukhonov
2025-07-10 19:09:56 +00:00
committed by intellij-monorepo-bot
parent 195e624fa6
commit 867c71bc95
4 changed files with 47 additions and 15 deletions
@@ -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
}
@@ -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
)
}
@@ -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<TerminalToolset>()
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)
@@ -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<CommandSession> = 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)
}