The original mcp-5 notes below describe the history of the npx-kt monitor
work. Current code has since moved on:
- The user-facing tool is
devrig; Gradle module names remain:npx-ktand:npx. - The active Kotlin package is
com.jonnyzzz.mcpSteroid.devrig. - The npm TypeScript MCP proxy and Kotlin attic implementation were removed.
- Remaining
Npx*names are bridge protocol names for the IDE-side/npx/v1/*routes, not active npm proxy code.
Process notes and friction encountered while implementing the npx-kt
project-monitoring service (push-style HTTP, JSON PID markers).
- PID file format: replace the existing
.<pid>.mcp-steroidtext file with a single-file JSON document (schema=1,pid,mcpUrl,ide,plugin,createdAt). Rejected: sibling-file or hybrid layouts (extra surfaces to keep in sync). - Streaming:
application/x-ndjson, one complete JSON object per line. Rejected: SSE (extra framing for no benefit; we don't need named event types here). - Event payload: full snapshot every emit
(
{type:"snapshot", seq, projects:[...]}). Consumer state stays trivial — replace, don't merge. Rejected: delta events (more consumer state, more edge cases on missed messages / reconnect). - npx-kt wiring: instantiate the new monitoring services in
Main.ktalongside the new stdio MCP server. Legacy proxy path (legacyProxyMain,ServerRegistry,NpxBeacon) is left alone.
- Existing IDE marker file is consumed by three readers
(
npx-kt::Utils.kt::scanMarkers, the npm-distributednpx/TS proxy,:test-helper:NpxProxyInstaller). The TS reader is out of scope for this branch; the Kotlin readers are updated in lockstep with the writer. NoLargeInlineStringsTestand themcp-steroid://URI lint rules don't apply here — no prompt content, nomcp-steroid://URIs added.
Eight commits on top of main, intentionally split so each is reviewable in
isolation:
mcp-5: seed branch with IMPROVEMENTS.mdmcp-5: pid marker file is now schema-versioned JSONmcp-5: legacy npx-kt + test-helper consume the JSON pid markerij-plugin: NDJSON projects-stream endpoint + ProjectsStreamServicenpx-kt: IDE monitoring stack — discovery + per-IDE NDJSON consumernpx-kt: tests for IdeDiscoveryService + IdeMonitorService roundtripmcp-5: close out IMPROVEMENTS.md with test summary + follow-up listmcp-5: pid marker carries the IDE's MCP port + bearer tokenmcp-5: log self-review findings + port/token addition in IMPROVEMENTSmcp-5: attach IntelliJ's bundled MCP server to pid marker via optional descriptormcp-5: log IntelliJ HTTP-server research + optional-descriptor pattern in IMPROVEMENTSnpx-kt: active port-scan discovery of IntelliJ-family IDEs
Test coverage:
:mcp-steroid-server:test—PidMarkerTest(6: roundtrip, pretty-print includes new port + token fields, forward-compat unknown fields, required-field rejection, filename contract, legacy marker without port/token falls back to defaults).:npx-kt:test—MarkerScanTest(7),IdeDiscoveryServiceTest(4),IdeMonitorServiceTest(3: roundtrip snapshots, multi-snapshot updates, Authorization header sent / suppressed), legacyStdioServerProtocolTest(61, untouched).:ij-plugin:test—NpxProjectsStreamRouteTest(4: initial snapshot, flow update, periodic ping, client-info parse with future-field tolerance), full pre-existing suite still green.
- PidMarker omits the MCP server's port and bearer token. The IDE
already owns both (
SteroidsMcpServer.portandNpxBridgeService.token); without them on the marker, npx-kt must parse the URL and has no way to authenticate. Addressed by commit 8 — the newport: Int+token: Stringfields are optional (defaults0/"") so older markers still decode. npx-kt'sIdeMonitorServicenow setsAuthorization: Bearer <token>when the marker carries a non-empty token. IdeMonitorServicedoes not detect when a marker is rewritten with a differentmcpUrlfor the same pid. Workers are keyed by pid; if an IDE restarts its MCP server on a different port within the same process, the worker keeps reconnecting to the old URL. Discovery polls the file every 2 s and picks up the newDiscoveredIdevalue, but the orchestrator'sif (workers.containsKey(pid)) continueskips respawn. Filed as a follow-up; not load-bearing for the current open/close push goal.- The
/projects/streamroute is not yet auth-gated. With the token now on the marker, the IDE can enforceNpxBridgeService.isAuthorized()on the projects-stream route whenever it wants —IdeMonitorServicealready sends the header. Not in this branch to keep the behaviour change focused.
docs/intellij-builtin-servers.mdcatalogues both the platform's always-on Netty HTTP server (REST under/api/*—about,file,settings,installPlugin,toolbox,projectSet,logs,startUpMeasurement, plus plugin-provided handlers) and the optional MCP Server plugin (com.intellij.mcpServer). Use the doc before adding any cross-process integration that talks to the IDE outside of themcp-steroidktor server.- MCP Server plugin is bundled in IDEA 2025.3+ but off by
default. Default port 64342, bound to 127.0.0.1, exposes
/sse(and/streamin 2026.1+). Force-enable system properties:-Didea.mcp.server.force.enable=true,-Didea.mcp.server.force.port=<int>. - Optional dependency wiring (no reflection). We expose the
bundled MCP server's endpoint shape on
PidMarker.intellijMcpServervia the canonical IntelliJ optional-plugin pattern:bundledPlugin("com.intellij.mcpServer")in Gradle for compile access,<depends optional="true" config-file="mcpServer-integration.xml">inplugin.xml, andmcpServer-integration.xmlregisteringIntelliJMcpServerProbeImplonly when the dep is satisfied. When the dep is missing the class is never loaded, so there's noNoClassDefFoundErrorwindow and no reflection involved. - API version skew. The 253 bundle of
McpServerServiceexposesisRunning,getPort,getServerSseUrl;getServerStreamUrlwas added later. The probe derives the streamable HTTP URL from the SSE URL (same listener, sibling path) so the marker carries both. If the/streamendpoint isn't live on an older bundle, the client observes that and falls back to SSE.
- Why active scan, on top of marker discovery? The
.<pid>.mcp-steroidmarker only fires for IDEs that have themcp-steroidplugin installed and started. Active port scanning finds any JetBrains IDE running on localhost (vanilla IntelliJ, PyCharm without our plugin, etc.) by probing/api/abouton the IntelliJ Platform's known port ranges. - Default scan ranges:
63342..63361(Netty built-in HTTP server, the platform picks the first free port in that 20-port window) and64342..64361(bundled MCP Server plugin'sDEFAULT_MCP_PORT + 19fallback range). - Threading model: a fixed-size daemon-thread pool named
mcp-steroid-port-scan-<n>is wrapped as aCoroutineDispatcherviaExecutors.asCoroutineDispatcher(). Probes are launched withasync(scanDispatcher)+awaitAll(). This keeps a slow TCP connect on one port from stalling the stdio MCP server's dispatcher or the marker discovery's polling. - Failure modes are normal: connection-refused on a port (no IDE
listening) and JSON-200-without-IDE-fields (a non-IDE web server
happens to share the port) both filter to
nullwithout propagating. The scan is a probe, not a contract — a non-IDE port is not an error. - Shutdown discipline:
IntelliJPortDiscoveryimplementsCloseable.Main.ktcallsclose()after cancelling the scan loop; the executor is also drained inside thestart { … }job'sfinallyblock (onNonCancellable) so in-flight probes don't leak when the parent scope is cancelled.
- The port-discovery output is currently informational only — it
isn't consumed by
IdeMonitorService(which still streams projects only frommcp-steroid-aware IDEs). Future work: cross-reference the two flows so the monitor can also surface "IntelliJ detected at :63344 but nomcp-steroidplugin loaded" states. - We don't yet probe the MCP server plugin's
/sseendpoint directly to confirm it's enabled. The/api/aboutprobe only tells us the IDE itself is alive. A second pass on the bundled MCP server port range that does a HEAD on/ssewould close that gap.
- The npm-distributed
npx/TypeScript proxy still parses the legacy text format. Updating it to consume the JSON marker (and the new streaming endpoint) is a separate piece of work — different language, different deploy pipeline. - The monitoring stack does not yet feed back into
legacyProxyMain'sServerRegistry. Replacing the polling refresh loop with the push-based state fromIdeMonitorServiceis the natural next step but was kept out of this branch to keep changesets small. - Reconnect-on-half-open:
IdeMonitorServicereconnects on stream close, but does not yet treat "no envelope received in N×ping" as a hint to proactively drop and reconnect. Trivial to add once we have telemetry on how often the IDE actually pings under load.
- Forward/backward compat is universal:
ignoreUnknownKeys = trueapplies to every decoder we touch in this branch — JSON marker file, NDJSON wire frames, any request/response body. PidMarker already does this; the npx-kt monitor and the IDE-side stream parsers must follow suit. - Liveness: IDE emits a
pingenvelope on the projects stream every N seconds (target 5s) so the monitor can distinguish "no project changes" from "TCP socket silently dead". Reading apingresets a stale-watchdog on the consumer; missing it pastN * 3triggers a reconnect. - Client identification: npx-kt announces itself to the IDE on connect
(clientId, clientPid, clientVersion, platform/arch). Cleanest fit is a
POST /npx/v1/projects/streamwhose request body carries the client-info JSON; the response keeps the streaming NDJSON shape. IDE logs the announcement and includesclientInstanceIdon the streamed envelopes for traceability.