Skip to content

feat(connector-mcp): a declarative stdio MCP transport runs in the declaring app's root - #22501

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22423-mcp-stdio-package-cwd
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22423-mcp-stdio-package-cwd

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22423
Clause-②: yes (widening)

What changes

This is the connector-mcp half of the split that triage ruled on the card (comment 6076155500: "(a), not (b)."). The host half landed in PR #22465: ConnectorProviderContext.resolvePackagePath, which service-automation hands to every provider factory, anchored at its packageRoot.

  • mcp-provider.ts, createMcpProviderFactory. For a stdio transport, the declarativeStdio policy check runs first, unchanged. Then, when the host provides resolvePackagePath, the factory sets the transport's cwd to await ctx.resolvePackagePath('.'). When the host does not provide it, the transport is built exactly as before, with no cwd key. A resolver that throws propagates as a plain error. It is a configuration fault, fatal at boot, and never upstream-unavailable.
  • mcp-connector.ts. The stdio variant of the exported McpTransport gains an optional cwd. defaultClientFactory passes it to the MCP SDK's StdioClientTransport (installed @modelcontextprotocol/sdk 1.31.0). The SDK hands it to cross-spawn as the spawn cwd. Left undefined, the child inherits the host's directory, as before.
  • The anchor is the host's alone. normalizeTransport still reads only command, args and env from providerConfig.transport, so an authored cwd there is never used. A pin below holds that. The declarative accept set is unchanged, and so is the declarativeStdio policy.
  • Hand-wired transports (new ConnectorMcpPlugin({ transport }), createMcpConnector) get a cwd only if their author passes one.

Clause-②: yes (widening): McpTransport is re-exported from index.ts, and its stdio variant gains an optional key. The changeset is minor for @objectstack/connector-mcp.

Reproduction: the os verify door (CI's Dogfood Verify CLI command)

node packages/cli/bin/run.js verify --app examples/app-showcase/objectstack.config.ts --rls. The CLI and showcase closures were built with pnpm turbo run build --filter=@objectstack/cli... --filter=@objectstack/example-showcase.... Counts are log lines. "materialized" counts [Automation] materialized connector instance 'showcase_mcp_tools' via provider 'mcp' (1 action(s)). "degraded" counts connector instance 'showcase_mcp_tools' (provider 'mcp') upstream unavailable. Verify boots the composition twice, so a clean run prints 2 materialized lines.

tree process cwd materialized degraded Cannot find module .../scripts/mcp-fixture.mjs verdict line
base 35ef501e1 repository root 0 6 6 (resolved at the repository root) verify passed
base 35ef501e1 examples/app-showcase 2 0 0 verify passed
b75c4acfb repository root 2 0 0 verify passed
b75c4acfb a scratch directory outside the repository (absolute --app) 2 0 0 verify passed
b75c4acfb examples/app-showcase 2 0 0 verify passed
head f9cf8b4df (after merging origin/main e148ca984) repository root 2 0 0 verify passed

Wall time per run was 66 to 73 s on the shared box.

Pins

file pin what it holds
mcp-provider.test.ts sets the stdio cwd to the host's resolvePackagePath('.') the resolver is asked '.' once; cwd is its answer; command and args reach the transport as written
mcp-provider.test.ts keeps the transport exactly as before without resolvePackagePath the transport has no cwd key
mcp-provider.test.ts never honours an authored providerConfig.transport.cwd with a host the cwd is the host's answer; without one there is no cwd key
mcp-provider.test.ts does not consult the resolver for an http transport the resolver is not called
mcp-provider.test.ts judges the declarativeStdio policy first a denied command is rejected and the resolver is never called
mcp-provider.test.ts a resolver failure is a configuration fault a plain error, not upstream-unavailable, and no client built
mcp-stdio-cwd.test.ts (real spawns through the SDK, no injected client) a relative script arg resolves against the given cwd node ./scripts/server.mjs, with cwd set to the fixture app, connects; the child reports its cwd as the app
same the same relative arg with no cwd rejects with ErrorCode.ConnectionClosed: the child exits before the handshake
same a relative command path resolves against the given cwd ./bin/node-here (a symlink to this node) connects
same control: an absolute command and an absolute script launch with cwd set to another directory, and the child reports that directory; launch without it and the child reports the process cwd
same control: a bare executable with a cwd node is still found on PATH
same a declarative instance booted from another directory LiteKernel + AutomationServicePlugin({ packageRoot }) + ConnectorMcpPlugin({ declarativeStdio: ['node'] }), with a declared provider: 'mcp' entry whose args are relative. The connector registers ready with its action, and the child runs in packageRoot. Test body 62 ms when green.

The fixture is a dependency-free newline-delimited JSON-RPC MCP server. The test writes it into a fresh mkdtemp directory, which is never the test process's cwd, and its one tool reports process.cwd(). No file outside connector-mcp is read.

Ablations (each committed first, mutated through scripts/ablation-replace.mjs, restored to a blob equal to HEAD with git diff HEAD empty)

All legs ran at b75c4acfb (the merge brought nothing under connector-mcp). The subjects are imported relatively from src, so no build or dist/ leg applies.

leg mutation red still green
A factory sets no cwd (resolver still called) sets-cwd, authored-cwd (with-host leg), boot from another directory 23 others
B defaultClientFactory drops cwd: transport.cwd relative arg, relative command, both controls (they launch, but report the process cwd), boot from another directory 21 others
C normalizeTransport passes an authored cwd through authored-cwd 18 others in mcp-provider.test.ts
D no resolver falls back to cwd: process.cwd() no-resolver, authored-cwd 17 others
E resolver called before the policy check policy-first, plus sets-cwd (the resolver is asked twice) 17 others
F resolver called for an http transport http 18 others
G a resolver failure rethrown as ConnectorUpstreamUnavailableError resolver-failure 18 others

The first attempt at C through F was a no-op: each replacement contained its own anchor, so the anchor count did not fall, and the tool refused with exit 1 before running anything. Those readings were discarded, and the legs were re-run with anchors their replacements do not contain. The table shows the re-run.

Local verification (head f9cf8b4df)

  • pnpm --filter @objectstack/connector-mcp typecheck: tsc --noEmit clean, and check:test-typecheck: OK, with the ledger unchanged (1 file, 5 errors, 1 signature). The test program compiles 4 of 4 test files, measured with --listFiles.
  • pnpm --filter @objectstack/connector-mcp test: 4 files, 36 tests passed.
  • pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/showcase-declarative-mcp.dogfood.test.ts: 1 file, 2 tests passed. That test still chdirs into the showcase, so it reads the same as before.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran: 61 derived, 61 run, 0 NOT MEASURED, 0 unrun, every one exit 0. Also run, exit 0: the 11 declared wide-population families, and the 4 roster gates whose roster sits in a directory this diff touches (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity).
  • Lint, narrowed to the 4 changed .ts files: eslint --no-inline-config --format json reads 4 files, 0 errors, 0 warnings. All 4 fall under the packages/** config block. The narrowing cannot move a verdict on an untouched file, because eslint.config.mjs enables no type-aware linting (--print-config shows parserOptions { ecmaVersion, sourceType }, no project). The repository-wide pnpm lint is CI's.
  • Not run locally: the packages/cli integration tier and the rest of the dogfood suite. They are declared to CI.

Acceptance notes

  • A showcase comment goes stale. examples/app-showcase/src/system/connectors/index.ts, above the showcase_mcp_tools transport, says the path "is relative to the app's working directory (os dev/serve run from the app root)". It now resolves against the app root whatever the working directory. examples/** is outside this card's surface. Carrier: none.
  • A dogfood workaround becomes unnecessary. packages/qa/dogfood/test/showcase-declarative-mcp.dogfood.test.ts chdirs into the showcase so that the fixture command resolves. With a packageRoot handed to the automation plugin, that chdir is no longer needed for the MCP half. This was not measured, because the harness's packageRoot wiring was not read. Carrier: none.
  • The docs say nothing about where a relative path resolves. content/docs/automation/connectors.mdx documents { kind: 'stdio', command, args?, env? } and does not say where a declarative stdio transport's relative paths resolve. Carrier: none, docs lane.
  • Boundary: relative PATH entries. A PATH entry that is relative (empty or .) is searched from the child's cwd, so for a declarative stdio transport such a lookup now searches the app root instead of the host's directory. No producer was measured.
  • Boundary: a root that does not exist. resolvePackagePath does not check that the root exists. A spawn whose cwd is missing reports spawn ... ENOENT, which reads like a missing command. The CLI and serve hosts pass the directory that holds objectstack.config.ts, which exists. A host passing a missing root was not measured.
  • Unchanged: unknown providerConfig.transport keys are dropped silently. That is today's behaviour for every unknown key, and an authored cwd is one of them (pinned). No producer was measured writing one.
  • Windows was not measured; CI runs Linux.

Generated by Claude Code

claude added 3 commits October 9, 2026 14:28
…ng app's root

The mcp provider factory sets the stdio child's working directory to the
host's ConnectorProviderContext.resolvePackagePath('.'), so an app's
relative command and args resolve against the same root as its other
relative refs instead of the directory the server was started from. A
host without the member keeps today's behaviour (no cwd). McpTransport's
stdio variant gains an optional cwd, passed to the SDK's
StdioClientTransport.

Claude-Session: https://claude.ai/code/session_01WYYhVJ78u7PhwFViWo1EmQ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m label Oct 9, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e148ca984258c7c0d162eefd04b1f0f77c3fa9fd → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c87c050c7eaa6529a0ecb8a8e42ef27d820e755e — the merge of head f9cf8b4df941d0a0db3fc12406db9cd0d5604605 into base e148ca984258c7c0d162eefd04b1f0f77c3fa9fd, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c87c050c7eaa6529a0ecb8a8e42ef27d820e755e && git checkout c87c050c7eaa6529a0ecb8a8e42ef27d820e755e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e148ca984258c7c0d162eefd04b1f0f77c3fa9fd f9cf8b4df941d0a0db3fc12406db9cd0d5604605 && git checkout -B drift-repro e148ca984258c7c0d162eefd04b1f0f77c3fa9fd && git merge --no-ff f9cf8b4df941d0a0db3fc12406db9cd0d5604605

node scripts/docs-audit/affected-docs.mjs --json e148ca984258c7c0d162eefd04b1f0f77c3fa9fd

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f9cf8b4df941d0a0db3fc12406db9cd0d5604605
Local-runs: none

Inputs: card #22423 (body and all six comments, the triage grade 6075273376, the retriage 6075485437, the triage answer 6076155500, the unlock 6082515147, the claim 6082624230, the dev report 6083813153), PR #22501 (body, file list), the net diff against main at merge base e148ca984 (3 commits, 5 files, +311/-1), and the head's check-runs (33, latest run per name, read twice; last read at posting time). Read-only: nothing built, run or re-run.

① Derived judgments

Public surface. @objectstack/connector-mcp's exports map has one entry (., dist/index.d.ts), and index.ts is not in the diff, so the published surface is the twelve names it already re-exported. Exactly one declaration changes: the stdio variant of McpTransport gains cwd?: string (mcp-connector.ts). It is reachable from the entry in five positions, all the same declaration: McpTransport, McpConnectorOptions.transport, the transport parameter of McpConnectorOptions.clientFactory, ConnectorMcpPluginOptions (which extends Partial of McpConnectorOptions) and McpProviderDeps.clientFactory. Widening, right; the only one. The contravariant position is not a narrowing: a consumer's clientFactory typed on the previous shape stays assignable, because the new stdio variant is assignable to the old (an optional extra key on a non-literal is not an excess property). No export is added, removed or renamed; McpDeclarativeStdioPolicy, McpProviderDeps, ConnectorMcpPluginOptions, McpConnectorBundle and McpClientLike are byte-unchanged. No other package in the repo names McpTransport (git grep at the head: only the changeset). So the PR body's Clause-②: yes (widening) names every published type change there is. Right.

Behavioural surface, each path.

  • defaultClientFactory (not exported) now passes cwd: transport.cwd to the SDK's StdioClientTransport (locked @modelcontextprotocol/sdk 1.31.0, pnpm-lock.yaml line 4339). A hand-wired transport (createMcpConnector, new ConnectorMcpPlugin({ transport })) that sets cwd is now honoured; one that does not passes undefined, which the spawn inherits as before. Right. The CI TypeScript Type Check family is green, which is the proof that cwd is an accepted StdioServerParameters key (an unknown key in that object literal fails excess-property checking); the five real-spawn pins in mcp-stdio-cwd.test.ts are the behaviour proof (relative arg, relative command, absolute control, bare-executable control, and the no-cwd leg rejecting with ErrorCode.ConnectionClosed). The http branch is untouched.
  • createMcpProviderFactory (exported), stdio branch: normalizeTransport then assertDeclarativeStdioAllowed then, only if ctx.resolvePackagePath is present, transport = { ...transport, cwd: await ctx.resolvePackagePath('.') }, then the connect try. Order right: the declarativeStdio policy runs before the resolver and judges transport.command exactly as written, and the policy function and McpDeclarativeStdioPolicy are unchanged by the diff (pin: a denied command never reaches the resolver; the resolver is asked '.' exactly once).
  • Resolver failure: the await sits outside the try that wraps createMcpConnector, so a throwing resolver propagates as a plain Error, which the materializer's reconcile path treats as a configuration fault (fatal at boot, skipped on reload), never ConnectorUpstreamUnavailableError. Right (pin: isConnectorUpstreamUnavailable(err) is false and no client is built).
  • Host without the member: the spread is skipped and the transport object carries no cwd key at all (pin: Object.keys(...) does not contain cwd). Right. Scope note for the owning seat, not a defect: service-automation at this base always hands resolvePackagePath: createPackagePathResolver(this.options.packageRoot) (plugin.ts line 2022), and with packageRoot undefined that resolver answers path.resolve(process.cwd()), so on that host a declarative stdio child gets an explicit cwd equal to the directory it would have inherited. The "no key" path is reached only by a host that predates spec(integration): ConnectorProviderContext gains a host-provided package anchor, so a provider resolves an app-relative path against the app's root (#22423's spec stage) #22434 or has no filesystem. Behaviour is equal either way.
  • http transport: the resolver is not consulted (pin). Right.

Declarative providerConfig accept set: unchanged, and the authored key is ignored on every path. normalizeTransport is not in the diff; for stdio it still copies only kind, command, args, env, and for http only kind, url, headers. An authored providerConfig.transport.cwd therefore never survives normalisation: with a host resolver present the transport's cwd is the host's answer (pin: /somewhere/else authored, ROOT observed); without one there is no cwd key (pin). The two hand-wired entry points take a McpTransport written in host code, not metadata, so they are not the declarative accept set. The anchor is the host's alone, which is what triage 6076155500 ruled ("(a), not (b)"). Right.

Security reading. The working directory is resolvePackagePath('.'), which resolveInsidePackageRoot resolves to the package root itself (relative '', inside). The child still runs with the host process's uid, environment and PATH; the command string is still gated by the same policy with the same default deny; the transport is not serialised into the discovery def, so cwd is not exposed. What changes is only which directory anchors a relative command, a relative args entry and a relative PATH entry: the declaring app's root instead of the directory the server happened to be started from. The app root was already the reachable anchor in the documented case (server started from the app root), and a start directory elsewhere was never a confinement, so the change gives a declarative entry no location it could not reach before, and removes the start-location dependence. No new reach; right. The dev's relative-PATH boundary is inside the same trust.

Tests. 6 factory pins (mcp-provider.test.ts), 5 real-spawn pins and 1 boot-level pin (mcp-stdio-cwd.test.ts: LiteKernel + AutomationServicePlugin({ packageRoot }) + ConnectorMcpPlugin({ declarativeStdio: ['node'] }), process cwd not the app, connector ready with its action and the child reporting packageRoot). The spawn file writes only to a fresh mkdtemp directory and reads no path outside the package; its imports (@objectstack/core, @objectstack/service-automation) are declared in package.json (dependency and devDependency). The card's three pins (another directory registers actions; absolute command and bare executable as today; no-resolver host as today) are each held. The seven ablation legs reported in the PR body each red the pin they target and nothing else; the first no-op attempt at C through F was disclosed and re-run.

File list. 5 files: .changeset/22423-mcp-stdio-package-cwd.md, packages/connectors/connector-mcp/src/mcp-connector.ts, mcp-provider.ts, mcp-provider.test.ts, mcp-stdio-cwd.test.ts (new). Every one sits inside the claim 6082624230's surface (the two source files, tests in connector-mcp, .changeset/22423-*.md); none outside; identical to the report's files_changed. No packages/spec, no service-automation, no examples/**, no content/docs, no governed path (Governed Surface Queue Guard green). 312 changed lines, under the 3,000-line human-merge threshold. A model-identifier sweep over the diff finds none; both authored commits end with the model-free trailer pair.

Check-runs on the head (latest per name, read at posting). Green: Build Core, TypeScript Type Check and the four Type Check · gates, Test Core shards 1, 3, 4, 5, 6, Dogfood Regression Gate 1/3, 2/3, 3/3 and the aggregate, Dogfood Verify CLI (the card's door), Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard, Check Changeset, Check PR Size, Check Documentation Links, the four PR-policy gates, Auto Label, filter. Skipped by path filter or opt-in: Build Docs, Console Pin Gate, Packed-tarball smoke. Not yet reported at posting: Lint & Repo Gates (required; carries the check:* gates, cross-package-test-inputs included) and Test Core (2/6) (required). Their conclusions are the gate verdicts; this record does not stand in for them, and the landing waits on both.

② Semver level

@objectstack/connector-mcp at minor, Clause-②: yes (widening) in both the PR body and the changeset body (the ADR-0087 gate reads it there; Check Changeset green). Right. yes takes at least minor; nothing is removed, renamed or narrowed, so nothing lifts it to breaking; a patch would be malformed against a yes. The behaviour change for a declarative stdio entry is the fix the card and triage ruled on (the process-cwd anchor was the outlier against the packageRoot anchor every other app-relative ref uses), the previous anchor was never a documented contract, and the changeset states before and after plainly.

Changeset prose against the head, sentence by sentence: the title (runs in the app's root; relative command and args resolve there wherever the server was started) holds for every host that hands resolvePackagePath, which service-automation does. "Sets the child process's working directory to the app's root": right. "Reads that root from the host through ConnectorProviderContext.resolvePackagePath('.')": right, the literal '.'. "Same root as the app's other relative refs, such as the openapi provider's providerConfig.spec": right, loadPackageFile and resolvePackagePath are built from one packageRoot by one confinement function. "Covers a relative command path and a script the launched program opens itself": right, both pinned on real spawns. "Before this change they resolved against the directory the server was started from" and the os verify example: right, the card's measurement. "A bare executable is still looked up on PATH, and an absolute command or argument resolves as before": right, both controls pinned. "The declarativeStdio policy judges the command string exactly as written, and it runs before the root is resolved": right, code order. "providerConfig.transport gains no key ... an authored cwd there is not used": right, pinned on both paths. "A host that does not provide resolvePackagePath starts the child exactly as before, in the host's current directory": right, no key, spawn inherits. "A server that opens relative files of its own now opens them under the app's root": right, a consequence of the cwd. "The stdio variant of McpTransport gains an optional cwd, which the default client passes to the MCP SDK's StdioClientTransport": right. "A hand-wired transport may set it. Left out, the child inherits the host's current directory, as before": right. "Nothing to migrate. An app that started its server from its own directory sees no difference": right as scoped; an app that relied on a start directory other than its own root as the anchor rewrites that relative path against its root, and the FROM (start directory) and TO (app root) are already the body's second bullet, so the migration text is present where a reader greps.

③ Boundary flags

Dev deviations (report 6083813153), each answered:

  • Merged origin/main e148ca984 into the branch as the head before opening the PR: the merge is clean, brings nothing under connector-mcp, and the net diff against the merge base is the three-commit change above. The merge commit carries git's default message and no trailer pair; the authored commits carry the model-free pair. Disclosed; a squash landing does not carry it to main. Accepted.
  • Ablations C through F were a no-op on the first attempt and re-run with disjoint anchors; only the re-run is reported. Accepted; the table reads consistently against the pins.
  • Attribution per AGENTS.md (model-free trailer pair, session-URL PR footer) over the harness reminder: AGENTS.md is the authority here and the diff carries no model identifier. Accepted.
  • Pins beyond the PM's list (authored cwd not honoured, http does not consult the resolver, policy first, resolver failure is configuration): each inside the card's surface and each is what ② above leans on. Accepted.

open_questions: empty. Nothing to escalate from it.

out_of_scope_findings and acceptance notes, each answered:

  • Showcase comment above showcase_mcp_tools ("relative to the app's working directory") goes stale: prose in examples/**, outside the claim's surface, not a defect; noted in the PR's acceptance notes, which Prime Directive chore: version packages #10 asks for. Answered; a prose fix rides the next examples/** touch.
  • showcase-declarative-mcp.dogfood.test.ts still chdirs into the showcase: not changed, not measured, still green (Dogfood Regression Gate green). Benign. Answered.
  • content/docs/automation/connectors.mdx says nothing about where a relative path resolves: it advertises nothing false; the changeset is the consumer-facing sentence. Answered; docs lane if wanted.
  • Unknown providerConfig.transport keys, an authored cwd included, are dropped silently: pre-existing for every unknown key, the accept set is unchanged, no producer measured. A strict transport shape would narrow the de facto accept set and is its own card, not this one. Answered, not escalated.
  • Relative PATH entries now searched from the app root: inside the declared trust (same uid, same policy). Answered.
  • A root that does not exist: resolvePackagePath does not check existence (its spec contract leaves that to the factory), and a spawn with a missing cwd fails inside the connect try, so it is classified upstream-unavailable with an ENOENT message, the same classification a missing relative command had before this PR. The CLI and serve hosts pass the directory holding objectstack.config.ts, which exists; no producer. Answered, not escalated; named so the owning seat sees the seam.
  • Windows not measured: CI runs Linux; nothing platform-specific is in the diff. Answered.

Implemented-by: claude/issue-22423-mcp-stdio-package-cwd
Reviewed-by: session_01WYYhVJ78u7PhwFViWo1EmQ

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 9, 2026 15:35
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 9, 2026 15:35
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit f8566a0 Oct 9, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22423-mcp-stdio-package-cwd branch October 9, 2026 16:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants