mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-238874 Execution toolset should allow adjustments for parameters, env vars, working dir
(cherry picked from commit 48b5c2c7ca109993c9fa57cd8541ea8b22688119) GitOrigin-RevId: 5d18bdea2f8c1c4cd7f1b9efa373050a1372e5e8
This commit is contained in:
committed by
intellij-monorepo-bot
parent
1b1917e342
commit
6cb0043247
@@ -4,7 +4,9 @@
|
||||
package com.intellij.mcpserver.toolsets.general
|
||||
|
||||
import com.intellij.execution.CommonProgramRunConfigurationParameters
|
||||
import com.intellij.execution.Executor
|
||||
import com.intellij.execution.RunManager
|
||||
import com.intellij.execution.configurations.RunConfiguration
|
||||
import com.intellij.execution.executors.DefaultRunExecutor
|
||||
import com.intellij.execution.process.ProcessEvent
|
||||
import com.intellij.execution.process.ProcessListener
|
||||
@@ -22,6 +24,7 @@ import com.intellij.mcpserver.reportToolActivity
|
||||
import com.intellij.mcpserver.toolsets.Constants
|
||||
import com.intellij.mcpserver.util.TruncateMode
|
||||
import com.intellij.mcpserver.util.checkUserConfirmationIfNeeded
|
||||
import com.intellij.mcpserver.util.prepareRunConfigurationForExecution
|
||||
import com.intellij.mcpserver.util.truncateText
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.PathManager
|
||||
@@ -50,7 +53,8 @@ class ExecutionToolset : McpToolset {
|
||||
|Returns a list of run configurations for the current project.
|
||||
|Run configurations are usually used to define user the way how to run a user application, task or test suite from sources.
|
||||
|
|
||||
|This tool provides additional info like command line, working directory, and environment variables if they are available.
|
||||
|This tool provides additional info like command line, working directory, environment variables,
|
||||
|and whether the configuration supports dynamic launch overrides (`programArguments`, `workingDirectory`, `envs`).
|
||||
|
|
||||
|Use this tool to query the list of available run configurations in the current project.
|
||||
""")
|
||||
@@ -61,11 +65,13 @@ class ExecutionToolset : McpToolset {
|
||||
|
||||
val configurations = readAction {
|
||||
runManager.allSettings.map { configurationSettings ->
|
||||
val runConfigurationParameters = configurationSettings.configuration as? CommonProgramRunConfigurationParameters
|
||||
val configuration = configurationSettings.configuration
|
||||
val runConfigurationParameters = configuration as? CommonProgramRunConfigurationParameters
|
||||
// TODO: render other details of other types of run configurations
|
||||
RunConfigurationInfo(
|
||||
name = configurationSettings.name,
|
||||
description = (configurationSettings.type.configurationTypeDescription ?: configurationSettings.type.displayName).ifBlank { null },
|
||||
supportsDynamicLaunchOverrides = runConfigurationParameters != null,
|
||||
commandLine = runConfigurationParameters?.programParameters?.ifBlank { null },
|
||||
workingDirectory = runConfigurationParameters?.workingDirectory?.ifBlank { null },
|
||||
environment = runConfigurationParameters?.envs?.ifEmpty { null },
|
||||
@@ -79,6 +85,12 @@ class ExecutionToolset : McpToolset {
|
||||
@McpDescription("""
|
||||
|Run a specific run configuration in the current project and wait up to specified timeout for it to finish.
|
||||
|Use this tool to run a run configuration that you have found from the "get_run_configurations" tool.
|
||||
|
|
||||
|Optional launch overrides (`programArguments`, `workingDirectory`, `envs`) are applied only for this run and are not persisted.
|
||||
|Do not pass these override parameters unless you explicitly need to change the configured launch values for this run.
|
||||
|Missing/null override parameters keep existing run configuration values unchanged.
|
||||
|For string overrides (`programArguments`, `workingDirectory`), only explicit empty string (`""`) clears an existing value.
|
||||
|
|
||||
|Returns the execution result including exit code, output, and success status.
|
||||
""")
|
||||
suspend fun execute_run_configuration(
|
||||
@@ -90,14 +102,28 @@ class ExecutionToolset : McpToolset {
|
||||
maxLinesCount: Int = Constants.MAX_LINES_COUNT_VALUE,
|
||||
@McpDescription(Constants.TRUNCATE_MODE_DESCRIPTION)
|
||||
truncateMode: TruncateMode = Constants.TRUCATE_MODE_VALUE,
|
||||
@McpDescription("Optional program arguments override for this launch only. Missing/null keeps existing value; explicit empty string clears it.")
|
||||
programArguments: String? = null,
|
||||
@McpDescription("Optional working directory override for this launch only. Missing/null keeps existing value; explicit empty string clears it.")
|
||||
workingDirectory: String? = null,
|
||||
@McpDescription("Optional environment variable overrides for this launch only. Missing/null keeps existing env unchanged; when provided, values are merged over existing env.")
|
||||
envs: Map<String, String>? = null,
|
||||
): RunConfigurationResult {
|
||||
currentCoroutineContext().reportToolActivity(McpServerBundle.message("tool.activity.executing.run.configuration", configurationName))
|
||||
val project = currentCoroutineContext().project
|
||||
val runManager = RunManager.getInstance(project)
|
||||
|
||||
val runnerAndConfigurationSettings = readAction { runManager.allSettings.find { it.name == configurationName } } ?: mcpFail("Run configuration with name '$configurationName' not found.")
|
||||
val runConfiguration = prepareRunConfigurationForExecution(
|
||||
configurationName = configurationName,
|
||||
configuration = runnerAndConfigurationSettings.configuration,
|
||||
programArguments = programArguments,
|
||||
workingDirectory = workingDirectory,
|
||||
envs = envs,
|
||||
)
|
||||
val useOriginalSettings = runConfiguration === runnerAndConfigurationSettings.configuration
|
||||
|
||||
val runConfigurationParameters = (runnerAndConfigurationSettings.configuration as? CommonProgramRunConfigurationParameters)?.programParameters
|
||||
val runConfigurationParameters = (runConfiguration as? CommonProgramRunConfigurationParameters)?.programParameters
|
||||
val notificationText = if (runConfigurationParameters != null) {
|
||||
McpServerBundle.message("label.do.you.want.to.execute.run.configuration.with.command", configurationName)
|
||||
}
|
||||
@@ -111,7 +137,7 @@ class ExecutionToolset : McpToolset {
|
||||
val outputBuilder = StringBuilder()
|
||||
|
||||
withContext(Dispatchers.EDT) {
|
||||
val runner: ProgramRunner<*>? = ProgramRunner.getRunner(executor.id, runnerAndConfigurationSettings.configuration)
|
||||
val runner: ProgramRunner<*>? = ProgramRunner.getRunner(executor.id, runConfiguration)
|
||||
if (runner == null) mcpFail("No suitable runner found for configuration '${runnerAndConfigurationSettings.name}'")
|
||||
|
||||
val callback = object : ProgramRunner.Callback {
|
||||
@@ -144,7 +170,13 @@ class ExecutionToolset : McpToolset {
|
||||
}
|
||||
}
|
||||
|
||||
val environment = ExecutionEnvironmentBuilder.create(project, executor, runnerAndConfigurationSettings.configuration).build()
|
||||
val environment = createExecutionEnvironment(
|
||||
project = project,
|
||||
executor = executor,
|
||||
runConfiguration = runConfiguration,
|
||||
useOriginalSettings = useOriginalSettings,
|
||||
runnerAndConfigurationSettings = runnerAndConfigurationSettings,
|
||||
)
|
||||
environment.callback = callback
|
||||
runner.execute(environment)
|
||||
}
|
||||
@@ -184,6 +216,19 @@ class ExecutionToolset : McpToolset {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createExecutionEnvironment(
|
||||
project: com.intellij.openapi.project.Project,
|
||||
executor: Executor,
|
||||
runConfiguration: RunConfiguration,
|
||||
useOriginalSettings: Boolean,
|
||||
runnerAndConfigurationSettings: com.intellij.execution.RunnerAndConfigurationSettings,
|
||||
) = if (useOriginalSettings) {
|
||||
ExecutionEnvironmentBuilder.create(executor, runnerAndConfigurationSettings).build()
|
||||
}
|
||||
else {
|
||||
ExecutionEnvironmentBuilder.create(project, executor, runConfiguration).build()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class RunConfigurationsList(
|
||||
val configurations: List<RunConfigurationInfo>,
|
||||
@@ -194,6 +239,8 @@ class ExecutionToolset : McpToolset {
|
||||
val name: String,
|
||||
@EncodeDefault(mode = EncodeDefault.Mode.NEVER)
|
||||
val description: String? = null,
|
||||
@property:McpDescription("Whether this run configuration supports one-time dynamic launch overrides for programArguments, workingDirectory, and envs.")
|
||||
val supportsDynamicLaunchOverrides: Boolean,
|
||||
@EncodeDefault(mode = EncodeDefault.Mode.NEVER)
|
||||
val commandLine: String? = null,
|
||||
@EncodeDefault(mode = EncodeDefault.Mode.NEVER)
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.intellij.mcpserver.util
|
||||
|
||||
import com.intellij.execution.CommonProgramRunConfigurationParameters
|
||||
import com.intellij.execution.configurations.RunConfiguration
|
||||
import com.intellij.mcpserver.McpExpectedError
|
||||
import com.intellij.mcpserver.McpServerBundle
|
||||
import com.intellij.mcpserver.impl.McpServerService
|
||||
import com.intellij.mcpserver.mcpCallInfo
|
||||
import com.intellij.mcpserver.mcpFail
|
||||
import com.intellij.mcpserver.settings.McpServerSettings
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.editor.colors.EditorFontType
|
||||
@@ -82,3 +85,36 @@ suspend fun askConfirmation(project: Project, @NlsContexts.Label notificationTex
|
||||
return@withContext confirmationDialog.isOK
|
||||
}
|
||||
}
|
||||
|
||||
fun prepareRunConfigurationForExecution(
|
||||
configurationName: String,
|
||||
configuration: RunConfiguration,
|
||||
programArguments: String?,
|
||||
workingDirectory: String?,
|
||||
envs: Map<String, String>?,
|
||||
): RunConfiguration {
|
||||
if (programArguments == null && workingDirectory == null && envs == null) {
|
||||
return configuration
|
||||
}
|
||||
|
||||
val copiedConfiguration = configuration.clone()
|
||||
val configurable = copiedConfiguration as? CommonProgramRunConfigurationParameters
|
||||
?: mcpFail(
|
||||
"Run configuration '$configurationName' of type '${configuration.type.displayName}' doesn't support dynamic launch overrides " +
|
||||
"(programArguments, workingDirectory, envs)."
|
||||
)
|
||||
|
||||
if (programArguments != null) {
|
||||
configurable.programParameters = programArguments.ifEmpty { null }
|
||||
}
|
||||
if (workingDirectory != null) {
|
||||
configurable.workingDirectory = workingDirectory.ifEmpty { null }
|
||||
}
|
||||
if (envs != null) {
|
||||
val mergedEnvs = LinkedHashMap(configurable.envs)
|
||||
mergedEnvs.putAll(envs)
|
||||
configurable.envs = mergedEnvs
|
||||
}
|
||||
|
||||
return copiedConfiguration
|
||||
}
|
||||
|
||||
+153
-3
@@ -4,19 +4,57 @@ package com.intellij.mcpserver.toolsets
|
||||
|
||||
import com.intellij.mcpserver.McpToolsetTestBase
|
||||
import com.intellij.mcpserver.toolsets.general.ExecutionToolset
|
||||
import com.intellij.openapi.application.runWriteAction
|
||||
import io.kotest.common.runBlocking
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
import java.lang.reflect.InvocationHandler
|
||||
import java.lang.reflect.Proxy
|
||||
|
||||
class ExecutionToolsetTest : McpToolsetTestBase() {
|
||||
@Test
|
||||
fun get_run_configurations() = runBlocking {
|
||||
val runManager = getRunManager()
|
||||
val editableSettings = createEditableConfiguration(runManager)
|
||||
val editableConfiguration = getConfiguration(editableSettings)
|
||||
setDynamicLaunchOverrides(editableConfiguration)
|
||||
val nonEditableSettings = createNonEditableConfiguration(runManager)
|
||||
|
||||
runWriteAction {
|
||||
addConfiguration(runManager, editableSettings)
|
||||
addConfiguration(runManager, nonEditableSettings)
|
||||
}
|
||||
|
||||
testMcpTool(
|
||||
ExecutionToolset::get_run_configurations.name,
|
||||
buildJsonObject {},
|
||||
"""{"configurations":[]}"""
|
||||
)
|
||||
) { result ->
|
||||
val configurations = Json.parseToJsonElement(result.textContent.text)
|
||||
.jsonObject
|
||||
.getValue("configurations")
|
||||
.jsonArray
|
||||
.associateBy { it.jsonObject.getValue("name").jsonPrimitive.content }
|
||||
|
||||
val editable = configurations.getValue("editable-config").jsonObject
|
||||
assertTrue(editable.getValue("supportsDynamicLaunchOverrides").jsonPrimitive.content.toBoolean())
|
||||
assertEquals("--sample", editable.getValue("commandLine").jsonPrimitive.content)
|
||||
assertEquals(project.basePath, editable.getValue("workingDirectory").jsonPrimitive.content)
|
||||
assertEquals("bar", editable.getValue("environment").jsonObject.getValue("FOO").jsonPrimitive.content)
|
||||
|
||||
val nonEditable = configurations.getValue("compound-config").jsonObject
|
||||
assertFalse(nonEditable.getValue("supportsDynamicLaunchOverrides").jsonPrimitive.content.toBoolean())
|
||||
assertFalse("commandLine" in nonEditable)
|
||||
assertFalse("workingDirectory" in nonEditable)
|
||||
assertFalse("environment" in nonEditable)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -29,4 +67,116 @@ class ExecutionToolsetTest : McpToolsetTestBase() {
|
||||
"Run configuration with name 'test-config' not found."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun execute_run_configuration_with_dynamic_launch_overrides_on_unsupported_configuration() = runBlocking {
|
||||
val runManager = getRunManager()
|
||||
val nonEditableSettings = createNonEditableConfiguration(runManager)
|
||||
|
||||
runWriteAction {
|
||||
addConfiguration(runManager, nonEditableSettings)
|
||||
}
|
||||
|
||||
testMcpTool(
|
||||
ExecutionToolset::execute_run_configuration.name,
|
||||
buildJsonObject {
|
||||
put("configurationName", JsonPrimitive("compound-config"))
|
||||
put("programArguments", JsonPrimitive("--sample"))
|
||||
},
|
||||
"Run configuration 'compound-config' of type 'Compound' doesn't support dynamic launch overrides (programArguments, workingDirectory, envs)."
|
||||
)
|
||||
}
|
||||
|
||||
private fun createEditableConfiguration(runManager: Any): Any {
|
||||
val name = "editable-config"
|
||||
val baseSettings = createNonEditableConfiguration(runManager)
|
||||
val baseConfiguration = getConfiguration(baseSettings)
|
||||
val factory = baseConfiguration.javaClass.getMethod("getFactory").invoke(baseConfiguration)
|
||||
val editableConfiguration = createEditableConfigurationProxy(baseConfiguration, name)
|
||||
return runManager.javaClass.methods
|
||||
.first { it.name == "createConfiguration" && it.parameterCount == 2 && it.parameterTypes[0].name == "com.intellij.execution.configurations.RunConfiguration" }
|
||||
.invoke(runManager, editableConfiguration, factory)
|
||||
}
|
||||
|
||||
private fun getRunManager(): Any {
|
||||
val projectClass = Class.forName("com.intellij.openapi.project.Project")
|
||||
val runManagerClass = Class.forName("com.intellij.execution.RunManager")
|
||||
return runManagerClass.getMethod("getInstance", projectClass).invoke(null, project)
|
||||
}
|
||||
|
||||
private fun createNonEditableConfiguration(runManager: Any): Any {
|
||||
val name = "compound-config"
|
||||
val configurationTypeClassName = "com.intellij.execution.compound.CompoundRunConfigurationType"
|
||||
val createConfigurationMethod = runManager.javaClass.methods.first {
|
||||
it.name == "createConfiguration" &&
|
||||
it.parameterCount == 2 &&
|
||||
it.parameterTypes[0] == String::class.java &&
|
||||
it.parameterTypes[1] == Class::class.java
|
||||
}
|
||||
return createConfigurationMethod.invoke(runManager, name, Class.forName(configurationTypeClassName))
|
||||
}
|
||||
|
||||
private fun getConfiguration(settings: Any): Any = settings.javaClass.getMethod("getConfiguration").invoke(settings)
|
||||
|
||||
private fun setDynamicLaunchOverrides(configuration: Any) {
|
||||
val parametersClass = Class.forName("com.intellij.execution.CommonProgramRunConfigurationParameters")
|
||||
parametersClass.getMethod("setProgramParameters", String::class.java).invoke(configuration, "--sample")
|
||||
parametersClass.getMethod("setWorkingDirectory", String::class.java).invoke(configuration, project.basePath)
|
||||
parametersClass.getMethod("setEnvs", Map::class.java).invoke(configuration, mapOf("FOO" to "bar"))
|
||||
}
|
||||
|
||||
private fun addConfiguration(runManager: Any, settings: Any) {
|
||||
runManager.javaClass.methods
|
||||
.first { it.name == "addConfiguration" && it.parameterCount == 1 }
|
||||
.invoke(runManager, settings)
|
||||
}
|
||||
|
||||
private fun createEditableConfigurationProxy(baseConfiguration: Any, name: String): Any {
|
||||
val runConfigurationClass = Class.forName("com.intellij.execution.configurations.RunConfiguration")
|
||||
val parametersClass = Class.forName("com.intellij.execution.CommonProgramRunConfigurationParameters")
|
||||
val envMap = linkedMapOf<String, String>()
|
||||
var programParameters: String? = null
|
||||
var workingDirectory: String? = null
|
||||
var passParentEnvs = true
|
||||
|
||||
val invocationHandler = InvocationHandler { _, method, args ->
|
||||
when (method.name) {
|
||||
"setProgramParameters" -> {
|
||||
programParameters = args?.firstOrNull() as String?
|
||||
null
|
||||
}
|
||||
"getProgramParameters" -> programParameters
|
||||
"setWorkingDirectory" -> {
|
||||
workingDirectory = args?.firstOrNull() as String?
|
||||
null
|
||||
}
|
||||
"getWorkingDirectory" -> workingDirectory
|
||||
"setEnvs" -> {
|
||||
envMap.clear()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
envMap.putAll(args?.firstOrNull() as Map<String, String>)
|
||||
null
|
||||
}
|
||||
"getEnvs" -> LinkedHashMap(envMap)
|
||||
"setPassParentEnvs" -> {
|
||||
passParentEnvs = args?.firstOrNull() as Boolean
|
||||
null
|
||||
}
|
||||
"isPassParentEnvs" -> passParentEnvs
|
||||
"getName" -> name
|
||||
"setName" -> null
|
||||
"clone" -> createEditableConfigurationProxy(baseConfiguration, name)
|
||||
"equals" -> args?.firstOrNull() === baseConfiguration
|
||||
"hashCode" -> System.identityHashCode(baseConfiguration)
|
||||
"toString" -> "EditableTestRunConfigurationProxy($name)"
|
||||
else -> method.invoke(baseConfiguration, *(args ?: emptyArray()))
|
||||
}
|
||||
}
|
||||
|
||||
return Proxy.newProxyInstance(
|
||||
javaClass.classLoader,
|
||||
arrayOf(runConfigurationClass, parametersClass),
|
||||
invocationHandler,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user