mcp-steroid / ij-plugin
CLAUDE.md78 starsChanged 3 months agoApache-2.0View on GitHub
Guidance for working on the IntelliJ Platform plugin and standalone MCP server. Read this in addition to the root CLAUDE.md whenever you change files under ij-plugin/. IntelliJ Platform plugin with standalone MCP server (Kotlin MCP SDK + Ktor): Core flow: SteroidsMcpServer.kt → ExecuteCodeToolHandler.kt → ExecutionManager.kt → CodeEvalManager.kt (compile) → ScriptExecutor.kt (run with timeout) Key decisions: Standalone MCP server (no built-in MCP dependency). Synchronous request-response. Coroutines over blocking. Two-phase execution (compile then run). Fast failure on errors. Disposable lifecycle for cleanup.
- Deletes or force-pushes
1 other project uses a version of this.
Adopted by
Most recently taken up by
- JetBrains/mcp-steroidword for word, first seen Nov 8, 2026
Other versions1 project uses a version of this.
Word for word · 1
- JetBrains/mcp-steroid★ 82100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository16 other files it gives its agents.
- AGENTS.mdAGENTS.md
- docs/AGENTS.mdAGENTS.md
- ij-plugin/AGENTS.mdAGENTS.md
- prompts/AGENTS.mdAGENTS.md
- test-experiments/AGENTS.mdAGENTS.md
- test-integration/AGENTS.mdAGENTS.md
- website/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- docs/CLAUDE.mdCLAUDE.md
- installer-gen/CLAUDE.mdCLAUDE.md
- prompts/CLAUDE.mdCLAUDE.md
- test-experiments/CLAUDE.mdCLAUDE.md
- test-integration-agent-launch/CLAUDE.mdCLAUDE.md
- test-integration/CLAUDE.mdCLAUDE.md
- website/CLAUDE.mdCLAUDE.md
- website-gen/CLAUDE.mdCLAUDE.md
The file
Show its rules and text
We split this CLAUDE.md into the rules it gives an agent
19 rules. About 0 are common, 2 are distinctive, and 17 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
17
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Very specific: line 41
Verify it: Check the declaration in ~/Work/intellij (grep -n "@ApiStatus" <File>), or via steroid_execute_code + PSI on the open intellij …
- Very specific: line 60
Use IntelliJ services instead of object/singletons:
- Very specific: line 75
Never block EDT. Use Dispatchers.EDT for UI, Dispatchers.IO/Default for background.
- Very specific: line 88
Never catch ProcessCanceledException — rethrow it.
- Very specific: line 108
Use Logger.getInstance(MyClass::class.java) for logging.
- Very specific: line 121
Run Gradle tasks from the IDE via MCP, not shell.
- Very specific: line 172
Don't, in almost every case: Read docs/PHILOSOPHY.md (canonical) or mcp-steroid://skill/design-philosophy (runtime mirror) first.
- Distinctive: line 184
If the gates pass, the wiring itself is two lines:
- Very specific: line 186
Create @Service(Service.Level.APP) handler with a register(server: McpServerCore) method.
- Very specific: line 227
After touching ij-plugin production code: ./gradlew :ij-plugin:test.
- Very specific: line 228
After touching prompts/src/main/prompts/: ./gradlew :prompts:test --tests '<KtBlocksCompilationTest>'.
- Very specific: line 230
After touching buildSrc codegen: ./gradlew :prompt-generator:test :prompts:generatePrompts.
- Very specific: line 231
When a test fails under root test, re-run just that module first.
- Very specific: line 234
Run them explicitly: --tests '*CliClaudeIntegrationTest*' etc.
- Very specific: line 275
Prevention: avoid SIGKILL (kill -9) on the Gradle test JVM mid-run.
- Very specific: line 278
Critical: NEVER run two :ij-plugin:test tasks concurrently — both write to the same idea-sandbox/ and corrupt each other's Stubs.storage.
- Distinctive: line 284
When a test run hangs, do NOT stop it first — collect a thread dump:
- Very specific: line 292
Look for: BLOCKED threads (deadlock), DefaultDispatcher-worker-* waiting (stuck coroutines), AWT-EventQueue-0 deep-blocked (EDT violation), …
- Very specific: line 437
Additive only: Never remove, rename, or retype an existing param/field.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
ij-plugin — IntelliJ Plugin Code
Guidance for working on the IntelliJ Platform plugin and standalone MCP server. Read this in addition
to the root CLAUDE.md whenever you change files under ij-plugin/.
Architecture
IntelliJ Platform plugin with standalone MCP server (Kotlin MCP SDK + Ktor):
Core flow: SteroidsMcpServer.kt → ExecuteCodeToolHandler.kt → ExecutionManager.kt →
CodeEvalManager.kt (compile) → ScriptExecutor.kt (run with timeout)
Key decisions: Standalone MCP server (no built-in MCP dependency). Synchronous request-response. Coroutines over blocking. Two-phase execution (compile then run). Fast failure on errors. Disposable lifecycle for cleanup.
Source structure
src/main/kotlin/com/jonnyzzz/mcpSteroid/
├── server/ # MCP server, tool handlers, skills
├── mcp/ # Core MCP protocol, tool registry
├── execution/ # ExecutionManager, CodeEvalManager, ScriptExecutor, McpScriptContext
├── storage/ # Append-only file storage
├── vision/ # Screenshot, input dispatch
├── demo/ # Demo mode overlay
├── ocr/ # External OCR process
├── koltinc/ # Kotlin Build Tools API (BTA) snippet compilation
└── updates/ # Update checkerIntelliJ Platform coding principles
⚠️ Public, stable API only — NEVER @ApiStatus.Internal (or @ApiStatus.Experimental)
Every IntelliJ API this plugin calls — in production code AND in mcp-steroid:// prompt recipes —
must be public and stable. Before using a class/method/field/topic/extension point, confirm it is not
annotated @ApiStatus.Internal (and prefer not @ApiStatus.Experimental). Internal APIs break without
notice across IDE releases and are off-limits even when they look convenient.
- Verify it. Check the declaration in
~/Work/intellij(grep -n "@ApiStatus" <File>), or viasteroid_execute_code+ PSI on the openintellijproject. If it's@ApiStatus.Internal, find the public replacement; if none exists, that's a design constraint to surface, not to bypass. - Known traps:
LaterInvocator.isInModalContext()is@ApiStatus.Internal— use the publicModalityState.current() != ModalityState.nonModal()(Yuriy'sisModalEdt()) instead. Prefer public message-bus topics likeProjectLevelVcsManager.VCS_CONFIGURATION_CHANGEDover internal hooks. - Reflection into private fields /
setAccessible(true)to reach internals is not an escape hatch — it silently breaks on the next IDE release (see the reflection rule inprompts/CLAUDE.md). - Moving target — plugin enumeration / descriptor APIs. Across
261 → 262, IntelliJ is internalizingPluginManagerCore.getPlugin/getLoadedPlugins/getPluginsandPluginManager.findEnabledPlugin/getPlugins; the public successorPluginDetailsServiceexists only in262+, not261. So there is currently no typed public API that is clean on both targets —PluginDescriptorProviderandScriptClassLoader.orderedPluginDescriptors()therefore keep using the261-publicPluginManagerCorecalls and accept the262-EAP internal-API verifier hits (261 — the shipping target — stays at 0). Full analysis, the per-build@Internaltable, and the migration path:docs/262-plugin-manager-api-internalization.md(reported upstream as IJPL-246183).
Services
Use IntelliJ services instead of object/singletons:
inline val serviceX: ServiceX get() = service()
inline val Project.serviceY: ServiceY get() = service()
@Service(Service.Level.PROJECT)
class MyService(private val project: Project, coroutineScope: CoroutineScope)Get services: project.service<MyService>() or service<AppService>(). Use childScope() for cleanup,
job.cancelOnDispose(disposable) for cancellation, Disposer.register(parent, child) for lifecycle.
Threading
- Never block EDT. Use
Dispatchers.EDTfor UI,Dispatchers.IO/Defaultfor background. - PSI/VFS access needs read actions; modifications need write actions. Use
readAction { }/writeAction { }. - Dumb mode:
DumbService.isDumb(project);waitForSmartMode()handles this for scripts.
Coroutines
runBlockingCancellable— BGT, blocks thread, cancellation-aware.runWithModalProgressBlocking— EDT, shows modal progress.RunSuspend— low-levelObject.wait()/notifyAll().
Error handling
- Never catch
ProcessCanceledException— rethrow it. - Never log
CancellationException(or any subclass) as an error. On the hot script-execution path (mcp-http/,ij-plugin/.../execution/,ij-plugin/.../server/), everycatch (Throwable)/catch (Exception)must matchCancellationExceptionfirst and rethrow without logging — this is thec.i.openapi.diagnostic.LoggerJavadoc contract for control-flow exceptions. Audit completed in commitefcd3400(2026-05-19); see TASKS.md → "A2b" for the site-by-site list of fixed catches, sites already correct (do not retouch), and intentionally-deferred sites off the hot path. The user-visible failure from issue #46 (SEVERE: StandaloneCoroutine was cancelled+ dual 200/500 log lines) was driven by this rule being violated; A0's boundary catch-all (McpHttpTransport.handlePost, commit3a4e7c13) plus the A2b rethrows form the complete fix. - One exception:
ScriptExecutor.executeCodeBlocksdeliberately catchesTimeoutCancellationExceptionBEFORE the genericCancellationExceptioncatch and callsreportFailed("Execution timed out after $timeout seconds while running the script body (modal=…; pre-flight completed)"). TCE is a CE subclass; the script-timeout case is a domain error that needs to surface to the agent, not a control-flow signal to propagate. - Use
Logger.getInstance(MyClass::class.java)for logging.
Cancellation and the BTA compile
Snippets compile through the Kotlin Build Tools API
(KotlinBuildsSession.compileKotlin, kotlin-cli/). The blocking
executeOperation runs on Dispatchers.IO and has no compiler-specific
timeout or cooperative-cancellation path. A caller cancellation may discard
the result after executeOperation returns, but it deliberately does not call
BuildOperation.cancel() and tear through compiler state mid-operation.
Build
Run Gradle tasks from the IDE via MCP, not shell. Key tasks: build, test, buildPlugin, runIde,
verifyPlugin, deployPlugin.
Gradle run-config recipe (paste into steroid_execute_code):
import com.intellij.execution.ProgramRunnerUtil
import com.intellij.execution.RunManager
import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.openapi.externalSystem.service.execution.ExternalSystemRunConfiguration
import org.jetbrains.plugins.gradle.service.execution.GradleExternalTaskConfigurationType
val runManager = RunManager.getInstance(project)
val factory = GradleExternalTaskConfigurationType.getInstance().configurationFactories.single()
val runConfig = factory.createTemplateConfiguration(project) as ExternalSystemRunConfiguration
runConfig.name = "Gradle test (MCP)"
runConfig.settings.externalProjectPath = project.basePath
runConfig.settings.taskNames = listOf("test")
// runConfig.settings.scriptParameters = "--tests \"*ExecutionManagerTest*\""
val settings = runManager.createConfiguration(runConfig, factory)
runManager.addConfiguration(settings)
runManager.selectedConfiguration = settings
ProgramRunnerUtil.executeConfiguration(settings, DefaultRunExecutor.getRunExecutorInstance())Build notes
- IDE cached under
.intellijPlatform/ides/IU-2025.3. - Tests use
localPlugin(...)for Kotlin/Java plugins to align with IDE classpath. com.intellij.java+org.jetbrains.kotlinare declared viatestBundledPlugin(...)in the maindependencies.intellijPlatform { }block — they live on the project-level test classpath (intellijPlatformTestBundledPlugins), NOT on the main IDE classpath. Two safety nets enforce this:NoForbiddenPluginImportsTest(compile-time, scans every production Kotlin source root) andPluginRuntimeCompatibilityTest.runtime compat pycharm*(PyCharm doesn't bundle either plugin and the .zip still loads).- Latent footgun: the custom integrationTest source set wired via
intellijPlatformTesting.testIde.register("integrationTest")does NOT inherit project-leveltestBundledPlugindeclarations — IPGP creates a suffixedintellijPlatformTestBundledPlugins_integrationTestconfig that doesn'textendsFromthe project-level one. The Cli*IntegrationTest classes today don't import any Java/Kotlin plugin types so this is fine, but if a future integration test needs them, add them explicitly inside theregister("integrationTest") { ... }block.
- Latent footgun: the custom integrationTest source set wired via
Plugin deployment
./gradlew deployPlugin # Hot-reload (requires Plugin Hot Reload plugin)
./gradlew deployPluginLocallyTo253 # Cold deploy (requires restart)Adding new MCP tools
Don't, in almost every case. Read docs/PHILOSOPHY.md
(canonical) or mcp-steroid://skill/design-philosophy (runtime mirror)
first. The MCP tool surface is intentionally narrow (8 today); a new tool
ships only when all three of:
- The need cannot be met by
steroid_execute_code+ a direct IntelliJ API call. Document the specific IntelliJ API path you ruled out. - It cannot be met by a richer
mcp-steroid://recipe. - Three independent reviewers (
run-agent.sh codex/claude/gemini) agree, after reading PHILOSOPHY.md. One reviewer disagreeing kills the proposal — propose a recipe instead.
If the gates pass, the wiring itself is two lines:
- Create
@Service(Service.Level.APP)handler with aregister(server: McpServerCore)method. - Register in
SteroidsMcpServer.kt:service<MyToolHandler>().register(server).
Same gate applies to adding methods on McpScriptContext (Tenet 3 in
PHILOSOPHY.md): the IntelliJ API is the extension point; context methods
are last-resort. The removed applyPatch { } DSL is the cautionary
example — it earned its place for atomic multi-site edits, then lost it
when full-scale eval data showed 64% of real calls failed on exact-match
resolution (#206; tolerance-matching successor tracked in #208). New
context methods must clear both bars: justify the surface AND prove
agents can use it reliably.
IDE control via execute_code
Invoke IDE actions programmatically:
val action = ActionManager.getInstance().getAction("RestartIde")
val dataContext = SimpleDataContext.builder().add(CommonDataKeys.PROJECT, project).build()
ActionUtil.invokeAction(action, dataContext, "mcp", null, null)List actions: ActionManager.getInstance().getActionIds("").filter { it.contains("restart", ignoreCase = true) }.
Testing
fun testExample(): Unit = timeoutRunBlocking(30.seconds) { /* coroutine test code */ }
private fun testExecParams(code: String, timeout: Int = 30) = ExecCodeParams(
taskId = "test-task", code = code, reason = "test", timeout = timeout,
rawParams = buildJsonObject { }
)Per-module test scoping
This repo's per-module split (:ij-plugin, :prompts, :kotlin-cli, :prompts-api, :test-helper,
:test-integration, :test-experiments) is intentional. Prefer per-project :module:test over root
./gradlew test (banned at root — see root CLAUDE.md):
- After touching
ij-pluginproduction code:./gradlew :ij-plugin:test. - After touching
prompts/src/main/prompts/**:./gradlew :prompts:test --tests '<KtBlocksCompilationTest>'. - After touching
kotlin-cli:./gradlew :kotlin-cli:test. - After touching
buildSrccodegen:./gradlew :prompt-generator:test :prompts:generatePrompts. - When a test fails under root
test, re-run just that module first.
./gradlew :ij-plugin:test runs only unit/in-process tests; Docker CLI tests are excluded by default.
Run them explicitly: --tests '*CliClaudeIntegrationTest*' etc. (need Docker + API keys).
Test naming
- NO
@ParameterizedTest— create explicit@Testmethods with descriptive names (e.g.,`describeMcp claude`(),`describeMcp codex`()). - Truly dynamic cases:
@TestFactorywithDynamicTest.dynamicTest("descriptive-name") { ... }. - This ensures IDE test runner,
./gradlew --tests, and CI reports work optimally.
Key test files
- McpServerIntegrationTest — MCP protocol handshake, tool flows, session management (in-process).
- CliClaudeIntegrationTest / CliCodexIntegrationTest / CliGeminiIntegrationTest —
Docker-isolated CLI tests; excluded from default
:ij-plugin:test. - ScriptExecutorTest — Fast failure semantics, compilation/runtime errors.
- ExecutionManagerTest — Execution with progress reporting.
Build troubleshooting
Test suite runtimes
./gradlew :ij-plugin:test(full clean): ~13–14 min../gradlew :prompts:test --tests '*KtBlock*'(KtBlocks only): ~7 min.- Suspiciously fast results (e.g., 500 tests in 14 seconds): stale Gradle test cache. Use
--rerun-tasksor deleteij-plugin/build/test-results/.
IntelliJ index corruption
Symptom: many tests fail with PersistentEnumerator storage corrupted at
.../system-test/index/stubs/Stubs.storage. Failure is inside
IndexDataInitializer$Companion$submitGenesisTask$2.invokeSuspend on a CoroutineScheduler$Worker —
truncated write from a previous abrupt JVM kill.
Fix:
rm -rf ij-plugin/build/idea-sandbox/IU-2025.3/system-test/
# Or more broadly: rm -rf ij-plugin/build/idea-sandbox/Next test run will rebuild indexes (~30–60s extra startup).
Prevention: avoid SIGKILL (kill -9) on the Gradle test JVM mid-run; SIGTERM lets IntelliJ flush
index files cleanly.
Critical: NEVER run two :ij-plugin:test tasks concurrently — both write to the same
idea-sandbox/ and corrupt each other's Stubs.storage. Never delete idea-sandbox/ while a test JVM
is running — file-handle corruption causes the same result.
Live JVM thread/coroutine dumps
When a test run hangs, do NOT stop it first — collect a thread dump:
jps -l | grep GradleWorkerMain # find test JVM PID
jcmd <PID> Thread.print > /tmp/thread-dump.txt # full thread dump (incl. coroutines via DebugProbes)
jcmd <PID> Thread.print -l > /tmp/dump-with-locks.txtLook for: BLOCKED threads (deadlock), DefaultDispatcher-worker-* waiting (stuck coroutines),
AWT-EventQueue-0 deep-blocked (EDT violation), the currently executing test method.
When a dump isn't enough, attach a live debugger. Docker integration tests always start the
IDE JVM with a JDWP agent open (server=y,suspend=n, IDE_DEBUG_PORT=5005), and the in-container
devrig JVM too (DEVRIG_DEBUG_PORT=5006) — both Docker-mapped to host ports printed on the host as
Listening for transport dt_socket at address: <host-port> (+ session-info.txt). Attach
IntelliJ's "Remote JVM Debug" (module ij-plugin) to step through plugin code (DialogKiller,
DialogWindowsLookup, ScriptExecutor) live. Recipe to attach programmatically:
mcp-steroid://debugger/debug-attach-remote-jvm; full workflow in test-integration/AGENTS.md
→ "Remote-debugging the Dockerized IDE". The modal-dialog/EDT hang (resolved 2026-05-31) is written
up in docs/dialog-killer-modality-hang.md.
exec_code modality model — the modal enum (ScriptExecutor.executeWithProgress)
The MCP surface is one ModalMode enum (ExecuteCodeTool.kt), not booleans. The old
dialog_killer / cancel_on_modal / allow_modal params and the runtime
doNotCancelOnModalityStateChange() are GONE. Full design + behavior table + the 10-iteration review:
docs/exec-code-options-redesign.md (LOCKED). Modality-hang investigation that led here:
docs/dialog-killer-modality-hang.md.
Why a stance is needed: commitAllDocuments / saveAllDocuments / VfsRefreshService.awaitRefresh
run on the write-intent Dispatchers.EDT and hang under a modal (the dispatch is withheld), and
waitForSmartMode() / indexing cannot complete while a modal is up. So a script that touches PSI must
first guarantee a non-modal IDE. The single reliable modal check is Yuriy's isModalEdt()
(DialogWindowsLookup: Dispatchers.EDT + ModalityState.any(), current() != nonModal(); stable —
LaterInvocator is @Internal), shared by the gate and the dialog killer.
modal values (default smart_non_modal), each a when branch in executeWithProgressImpl that is
sugar over the McpScriptContext methods:
modal | Pre-flight | During body | Post-flight |
smart_non_modal (default) | closeModalDialogs() (sweep, deepest-first) → await dialog-less modal progress (bounded by mcp.steroid.execution.dialogless.modal.wait.ms, default 120s: the 2026.2 platform elevates modality with NO dialog while its freeze-protection pumps a write-action storm — SuvorovProgress, seen on PyCharm-EAP cold-start VFS refresh, #412; a wait is correct because dialog-less modality ends on its own, while a dialog window still fails fast) → require non-modal (requireNonModalOrFail → fail + screenshot + thread dump) → syncDocuments() (commit+save+VFS, bounded 60s) → waitForSmartMode() (bounded 60s) → monitorAndCloseModalDialogs() | monitor polls ~1s; a modal dialog gets closed + the run FAILS (screenshot + thread dump) | re-syncDocuments() iff !isModalEdt() |
non_modal | requireNonModalOrFail only (assert-only) | nothing | nothing |
unleashed | nothing | nothing | nothing |
Context methods (McpScriptContextImpl, callable from any mode): closeModalDialogs(): Int (sweep;
thread-dump+screenshot side effects, skipped on an empty sweep; does not fail), monitorAndCloseModalDialogs()
(1s-poll watcher; close + FAIL; launched in the execution scope so throwing fails the run; allowModalDialog()
cancels it for the rest of the run), syncDocuments() (commit+save+VFS; asserts non-modal), waitForSmartMode()
(asserts non-modal; bounded by WAIT_FOR_SMART_MODE_TIMEOUT). All bounded EDT ops use
withTimeout → ToolCallErrorException so a stuck modal fails fast with a thread dump instead of hanging.
Stage markers ([PRE]/[RUN]/[POST]) go to idea.log only — never into the tool result (#154: the
result is the execution_id: header plus the script's own output, plus explicit WARNING/ERROR/HINT
lines on anomalies, so a JSON-printing script stays machine-parseable after stripping that header).
The same separation applies to ALL in-flight progress (progress(...), indexing/compile waits,
multi-block progress): ExecutionManager.logProgress delivers it via MCP progress notifications
(when the client passed a progressToken) + idea.log + the execution event storage — never the
result content. A failing pre-flight step propagates the context API's own error untouched;
a stall is localized via the per-step [PRE] lines in idea.log.
Tests: unit ModalModeTest (wire/default/parse) + ExecutionManagerTest profile-pipeline cases;
integration DialogKillerIntegrationTest (Docker) — Step-1 opens a modal with modal=unleashed (no
monitor, so it survives), explicit/screenshot/nested close it with unleashed + closeModalDialogs(),
the automatic test uses smart_non_modal (pre-flight sweep). The test helper's mcpExecuteCode(modal=…)
and testExecParams(modal=…) carry the wire string.
Cleaning build artifacts
rm -rf ij-plugin/build/test-results/ ij-plugin/build/reports/ # force test re-run
rm -rf ij-plugin/build/idea-sandbox/ # corrupted indexes
./gradlew :ij-plugin:clean # full cleanCommon test failure patterns
| Symptom | Likely cause | Fix |
PersistentEnumerator storage corrupted | Corrupted index files | rm -rf ij-plugin/build/idea-sandbox/ |
| 500+ failures in <30s | Stale Gradle test cache | ./gradlew :ij-plugin:test --rerun-tasks |
KtCompilationTest fails with -Werror | Deprecated API used in .kt section | Replace the deprecated call with the non-deprecated API (no @Suppress("DEPRECATION") — see root CLAUDE.md) |
KtBlocksCompilationTest fails | Non-compilable code in ```kotlin ``` fence | Change fence to ```text ``` in .md |
MarkdownArticleContractTest fails | Title >80 chars, desc >200 chars, or bare code outside fences | Fix the article header/body |
NoHardcodedMcpSteroidUriUsageTest fails | Hardcoded mcp-steroid://... URI in production Kotlin | Replace with XxxPromptArticle().uri |
:test-integration hangs with Blocking modal dialog detected | Stale test-integration/src/test/docker/test-project/.idea/ pins project-jdk-name/gradleJvm to a name not in ProjectJdkTable | Sanitize .idea/ (gitignored) or add the name to mcpRegisterJdks aliases — see test-integration/AGENTS.md ("Configuring the IDE") |
:test-integration hangs with MODAL DIALOG DETECTED — Resolving SDKs… during ProjectTaskManager.build() | Missing -Dunknown.sdk.modal.jps=false (gates CompilerDriverUnknownSdkTracker.fixSdkSettings) | Add to test-integration/src/main/kotlin/com/jonnyzzz/mcpSteroid/integration/infra/intelliJ.kt generateVmOptions() — see test-integration/AGENTS.md |
unresolved reference 'JavaSdk' in PyCharm/GoLand/WebStorm/Rider tests | Factory's early-JDK hook fires for IDEs without com.intellij.java on script classpath | Verify IdeProduct.hasJavaSdk is true only for IntelliJIdea (see test-integration/AGENTS.md → "Non-Java IDEs skip JDK setup") |
ContentModuleClasspathTest fails with "JAR(s) on filesystem but not in classpath" and/or "in UNLOADED_CONTENT_MODULES allowlist but not found" | Rolling stable channel slid to a new IDE version and re-organized content modules (261→262 added 39 module jars, dropped 22 — issue #412; will recur at 263) | Transcribe the failure's add/drop lists into the per-major UNLOADED_CONTENT_MODULES_IU_<major> set; on a new major, add a set + dispatch branch in unloadedContentModules() |
Key types
- ExecCodeParams:
taskId,code,reason,timeout,rawParams. - ToolCallResult:
content(List<ContentItem>),isError. UseToolCallResult.builder(). - McpScriptContext: in-script Kotlin runtime context (
project,disposable,printJson(...),progress(...),findProjectFile(...),projectScope(), the inspection / highlighting helpers, etc.). Seeij-plugin/src/main/kotlin/com/jonnyzzz/mcpSteroid/execution/McpScriptContext.ktfor the current surface — not enumerated here on purpose, since growing the surface is gated by Tenet 3 indocs/PHILOSOPHY.md.
Inspections from steroid_execute_code: only the per-file driver works
runInspectionsDirectly (per-file InspectionEngine.inspectEx, the mcp-steroid://ide/inspect-and-fix
recipe) is the ONLY inspection driver that runs from steroid_execute_code. The whole-project drivers do
NOT: InspectionEngine.runInspectionOnFile, DaemonCodeAnalyzerImpl.runMainPasses (fails
assertUnderDaemonProgress() — no DaemonProgressIndicator), and MainPassesRunner.runMainPasses (pumps
the EDT internally while the suspend script holds resources that block forward progress — timed out at 240s
on 3 small files). For full-project inspection coverage, use the IDE's Code → Inspect Code… UI, not a
script.
Configuration
Registry keys: mcp.steroid.server.port, .host, .execution.timeout,
.execution.dialogless.modal.wait.ms (smart_non_modal's bounded wait for dialog-less modal progress,
default 120 s, 0 disables), .dialog.killer.enabled, .demo.enabled, .storage.path,
.kotlinc.parameters.
WSL-hosted projects and mcp.steroid.storage.path (#78)
Execution storage defaults to the IDE user's native ~/.mcp-steroid/runs/ directory, not the project.
That keeps BTA's per-execution compiler outputs off \\wsl$\… /
\\wsl.localhost\… when a Windows IDE opens a WSL-hosted project. If
mcp.steroid.storage.path is customized in this setup, keep the override on a native Windows volume;
pointing it back into WSL makes compiler I/O use the UNC path again. The BTA migration removed the old
cmd.exe /c kotlinc.bat environment-routing failure from #78, but direct BTA output on WSL storage
remains unverified.
Kotlin compiler version
The snippet compiler is the Kotlin Build Tools implementation pinned directly in
kotlin-cli/build.gradle.kts (bundled as the plugin's kotlinc/ jar folder). Its version is
intentionally independent from the Kotlin Gradle plugin used to build the rest of the repo.
prompts consumes kotlin-cli's kotlincDist artifact and reads the compiler version from the
loaded BTA; it must not resolve or version the RC Kotlin modules independently. Keep the direct
literals in kotlin-cli and this module's compatibility/distribution checks aligned. When an IDE
bundles a newer Kotlin than the snippet compiler can read (metadata is readable at most one minor
ahead), bump those BTA literals — there is no runtime override.
Script preprocessing (CodeButcher)
CodeButcher.wrapWithImports(): extracts user imports, adds defaults, merges, wraps body into a suspend
method.
devrig ↔ plugin wire contract (frozen, additive-only)
From the 0.96 release onward, the devrig binary must stay compatible with every plugin version it talks
to (and vice-versa). The wire surface is: the JSON-RPC tool-call params devrig sends
(DevrigBridgeToolHandlers put(...) blocks → steroid_execute_code, _execute_feedback,
_take_screenshot, _input, _open_project), the /windows + /projects/stream responses, and the
@Serializable DTOs they (de)serialize with (mcp-steroid-server: NpxBridge*, NpxStream*, PidMarker,
McpProtocol types; mcp-core: ToolCallResult).
Rules — never break these:
- Additive only. Never remove, rename, or retype an existing param/field. New params/fields must be
optional with a safe default (
= null/= ""/= false/ a defaulted enum), so an older peer that omits them still decodes and a newer peer that ignores them still works. Graceful degradation is fine; the protocol must still function end-to-end. - Tolerant decode is the baseline, not a license to break.
ignoreUnknownKeys = truetolerates unknown keys (additive on the sender) but a required field without a default still throws on a missing key — so the additive-only rule is what actually guarantees forward/back compat. The current required fields (e.g.NpxBridgeWindowsResponse's 8) are the v1 baseline: present in all versions, never to be removed. - Enums must not be parsed strictly against the wire — an unknown future value must degrade, not throw.
- Changing the contract requires a conscious, reviewed, additive-only edit + a contract-test update.
DevrigToolBridgeClientTestpins each tool's exact param names/types (golden-ish assertions); a diff fails the build. Add a pinned case for any new tool/param.
Wire ProjectInfo is pristine {name, path}. The wire DTO carries exactly two fields; there is no
backend field on it. (An earlier round added ProjectInfo.backend: String? = null; it was reverted — the
per-project backend reference now lives on the devrig-owned, MCP-surface-only ListedProject.backendName,
which never crosses the wire. ProjectsStreamService only ever builds ProjectInfo(name, path), so the
reversion changed zero emitted bytes.) A wire-pristineness guard test (WirePristinenessTest,
mcp-steroid-server) asserts NpxStreamEnvelope (/projects/stream) and NpxBridgeWindowsResponse
(/windows) never serialize the devrig-only backend_name / project_name / ListedProject
keys, nor the #155 MCP-only backends / intellij identity keys (BackendInfo and
ListedBackendInfo were deleted in the startable-backends release).
MCP-surface-only, never wire-crossing:
OpenProjectParams.backendName: String? = nullis MCP-surface only and is NOT forwarded to the IDE bridge. devrig resolves it locally to a target IDE and POSTs the byte-identicalsteroid_open_projectbody (project_path/trust_project/task_id/reason). The non-forwarding invariant is pinned inDevrigToolBridgeClientTest(forwardedarguments["backend_name"] == null).ListProjectsResponse/ListWindowsResponse/ListedProjectare devrig-computed MCP/CLI output types (built from devrig's routing snapshot, never fetched from the IDE) — devrig-owned and outside the wire contract. Note:BackendInfoandListedBackendInfowere deleted in the startable-backends release;backends[]was removed from both response types. The one-release additive-only waiver that permitted this is recorded indocs/PHILOSOPHY.mdTenet 5 anddocs/startable-backends-design.md. #155 re-introduced a deliberately slimmer, identity-onlybackends[]on both response types:BackendRef{backend_name, intellij{name, version, build}}(IntelliJInfois a dedicated presentation type — a projection of the markerIdeInfo, drift-gated byBackendRefSerializationTest, never wire-crossing). Membership: referenced-only on devrig (derived from the routing snapshot); the direct in-IDE surface always emits its single self entry, even with zero open projects.projects[]is sorted byproject_name,backends[]bybackend_name; windows/tasks keep their produced order.
PidMarker fields added in the startable-backends release (additive, nullable):
PidMarker.ideHome: String? = null— the IDE install home (PathManager.getHomePath()), written byServerUrlWriter. It is devrig's cross-process identity: it correlates a running IDE to its managed install (path-normalized on both sides) and replaces the old process/pid-file scanning. Its presence also signals plugin compatibility — a running marker withoutideHomemeans an old/incompatible plugin, shown in the "Other IDEs" group and never offered as anopen_projectcandidate.McpSteroidServerInfo.pluginPath: String? = null— the mcp-steroid plugin install folder (fromPluginDescriptorProvider). Plumbing for the deferred "update plugin before run" step (Finding A,TASKS.md#12); no logic reads it yet.
Later additive PidMarker fields:
PidMarker.remoteDevelopmentBackend: Boolean = false— written byServerUrlWriterfrom the publicIdeProductMode.isBackendsignal. It lets devrig and diagnostics identify a frontendless Remote Development backend directly from the marker; older markers that omit it decode asfalse.
Deferred (revisit with a baseline release): (a) a cross-version test (devrig HEAD ↔ an older plugin build);
(b) giving devrig its own copy of the marker/bridge DTOs so the two version independently and only the
JSON wire shape is shared (today devrig reuses mcp-steroid-server's classes, decoding tolerantly).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

