Repository navigation
mcp run expands ${PLUGIN_ROOT} and the AGENT_BUNDLE_PLUGIN_ROOT anchor to the artifact target root, fragmenting durable state per rebuild #59
Description
Package: agent-bundle (mcp run), at main da5df1d (PR #52, RFC #50 Phase 1). Found empirically by the movie-library Phase-2 migration onto that commit.
What happens
Under mcp run, the manifest's ${PLUGIN_ROOT} token — and with it the AGENT_BUNDLE_PLUGIN_ROOT env anchor that adapters inject on every stdio server since PR #49 (#47's fix) — expands to the artifact target root, not the project root:
resolveMcpStdioLaunch(src/services/mcp-run.ts) resolves tokens withpluginRoot: targetRoot, wheretargetRoot = joinArtifact(artifact, target)— e.g.artifact/portable.- Every emitted stdio server carries
env.AGENT_BUNDLE_PLUGIN_ROOT = "${PLUGIN_ROOT}"(withPluginRootEnvAnchor,src/adapters/types.ts), so undermcp runthe documented state anchor points into the build output directory. runMcpForegroundspawns withenv: { ...process.env, ...launch.env }— expanded manifest env last — so an operator'sexport AGENT_BUNDLE_PLUGIN_ROOT=…beforemcp runis silently overridden. (A config-declared anchor wins over the injected one by design, per the comment onwithPluginRootEnvAnchor, but the operator-shell layer has no such escape.)
Contrast with the same function one layer up: runMcp (src/api.ts) already computes workspaceRoot = resolve(options.root) and anchors ${PLUGIN_DATA} durably at <project>/.agent-bundle/mcp-run/<target>/<server>. The framework already chose project-root durability for plugin-data; the plugin-root anchor is the inconsistent one.
Empirical consequence
Movie-library's state resolution trusts the anchor exactly as #47's fix intends. A bare mcp run handshake against the checkout's artifact/ wrote persistent state to artifact/portable/.runtime — fragmenting per rebuild and dying with artifact regeneration. That is precisely the state-fragmentation defect (per-tracker policy ledger, live Trakt device auth) the anchor was introduced to prevent in host installs. The consumer's wrapper therefore stays, pinning MOVIE_LIBRARY_ROOT / MOVIE_LIBRARY_STATE_DIR (layer 1 of its state-root chain, which beats the anchor).
The semantics question
Both expansions are defensible, which is why this is filed as a question and not just a defect:
- Under a host install,
${PLUGIN_ROOT}= install root is correct — the artifact is the installation, and it is durable. - Under
mcp runagainst a checkout'sartifact/, the artifact is an ephemeral build product; the "plugin root" in the operator's mental model is the checkout.
One nuance constrains the fix: ${PLUGIN_ROOT} in args/cwd genuinely must remain the artifact target root — args[0] is the content-hashed bundle inside it. It is specifically the env-anchor semantics that mislead runtimes into anchoring durable state on a directory with build-product lifetime.
Suggested fix directions
- Under
mcp run, expand env-value occurrences of${PLUGIN_ROOT}(or at minimum the injectedAGENT_BUNDLE_PLUGIN_ROOT) to the resolved project root, keepingargs/cwdon the artifact target root. The machinery exists: the token resolver already supports per-field token→root maps (McpPathTokenMap,src/services/mcp-path-tokens.ts), andrunMcpalready holdsworkspaceRoot. Tradeoff: dev runs gain checkout-state continuity (matching wrapper behavior and host-install durability), at the cost ofmcp runno longer being a byte-faithful rehearsal of a copied-artifact launch — a--plugin-root <path>override restores that when wanted. - Keep artifact-root expansion as a documented default, add
--plugin-root <path>, and let the operator's environment win: todayexport AGENT_BUNDLE_PLUGIN_ROOT=…is silently beaten by the manifest anchor because of the env spread order. Weaker: every consumer learns the flag only after state has fragmented once. - Documentation-only: state that under
mcp runthe anchor points intoartifact/<target>and durable state should ride${PLUGIN_DATA}(already durably anchored at<project>/.agent-bundle/mcp-run/…). Weakest: it contradicts the claude adapter drops cwd for source-built stdio servers: no working-directory anchor on bare installs (portable and codex both provide one) #47-era guidance to trust the anchor, and plugins that share state between host installs and dev runs (movie-library does) cannot split roots per launch mode.
Related: #58 (companion mcp-run finding, missing .env loading), #50 (RFC #50 Phase 1 shipped mcp run), #47 (the anchor this expansion undermines was introduced there and fixed by PR #49) — empirically resolved by the movie-library Phase-2 migration against da5df1d.
Activity
ScriptedAlchemy commentedon Aug 31, 2026
Shipped in #61 (merge
1286f5b2), implementing fix direction 1. Undermcp run, plugin-root tokens in env values — including the injectedAGENT_BUNDLE_PLUGIN_ROOTanchor — expand to the resolved project root, whileargs/cwdstay artifact-rooted (args[0]is the content-hashed bundle inside the target root), exactly per the nuance in the issue. The per-field split is applied at themcp runseam by wrapping the adapter'sresolveValuewith an env-field-specificpluginRoot, so adapters andmcp list/invoke(deliberately ephemeral) are untouched. Codex's tokenless./-serialized anchor re-anchors through the target's own relative-argument rule.--plugin-root <path>restores the byte-faithful copied-artifact rehearsal; host-install semantics are unchanged.The env-spread ordering is fixed too: the operator's real
process.envnow composes last, soexport AGENT_BUNDLE_PLUGIN_ROOT=…beats the manifest anchor (canonical precedence table indocs/entry-conventions.md). A regression test drives a consumer entry that trusts the anchor through a bare CLImcp runand asserts state lands at<project>/.runtimewith nothing underartifact/portable/— the movie-libraryMOVIE_LIBRARY_ROOT/MOVIE_LIBRARY_STATE_DIRpins are no longer needed for dev runs.