Repository navigation
os dev's MCP connect hint builds all three URLs from the listen origin, so under OS_AUTH_URL=https://… it prints a claude mcp add … http://localhost:<port>/… line the reader is meant to paste #16734
Description
Activity
- addedbugSomething isn't workingSomething isn't workingand removed
on Sep 8, 2026 os-project-manager commented
on Sep 8, 2026 CollaboratorMore actionsClaim: PM loop round 71
Session:session_015QE8qk46e5CHJxyQEUjbf8—domain:cliexecution PM seat (#6024), R71
Branch:claude/issue-16734-mcp-connect-hint-origin
Worktree:objectstack-issue-16734
Domain:domain:cli
File surface:packages/cli/src/commands/dev.ts(thebaseexpression at:503-508and the block it feeds) + a test inpackages/cli/src/commands/**.⚠️ packages/cli/src/utils/format.tsis the reference implementation, not a second thing to change — the card says so and it is right.
Container & model:M,mode:subagent,model: opus— cited from THIS round'snode scripts/pm/dispatch-gates.mjs --tier packages/cli/src/commands/dev.ts packages/cli/src/utils/format.ts, derived from a worktree atorigin/maindd2184ae25with noSTALE TREEbanner: "no path-derived mandate — the surface hits none of the 3 declared glob(s) … floor sonnet · default opus · ceiling fable".⚠️ claude-fable-5-1is measured exhausted by attempt twice this session (req_011CeqFjyy9jEa2RDJqpftnv,req_011CeqRGUZzRcHQvL6vZ5QEy, HTTP 429), soopusis also the ceiling the 额度耗尽豁免 permits — ⛔ 不再往下.
Clause-②: no
Thread-read: card body only — this card carries no comments.Why
Clause-②: no, judged from content⛔ Not from paths (the tier line is a floor, never a clearance). The act changes which origin a printed hint is built from — a correctness fix that makes two printers in the same boot output agree. No accept set is relaxed, no published surface is widened, no member is added to a closed set; the card explicitly scopes the
Endpoint/Skill/Connecttext as already correct and out of scope.⚠️ Rebuttable by your diff. If what you write widens an accept set — for instance if reusing the resolver means teaching it a new precedence entry, or the fix adds a new env var to a published chain — ⛔ stop and say so in your report; do not re-declare it yourself. ⛔ And do not narrow the fix to keep theno: the declaration follows the right fix, never the reverse.⚠️ Ayesmeans the PR is held, not reviewed, because the review tier is unbuyable this session — that is the correct outcome and still ⛔ not a reason to shade the declaration.⭐ Carry
Clause-②: noverbatim into the PR body as its own line. Two gates read two different documents —check-clause2-carriers --pairreads THIS comment,check-changeset-no-majorreads the PR body — and nothing cross-checks them (measured this round, #16770). A split between them is silent and picks the weaker branch.Serial constraints cleared
Measured at 2026-09-08T06:1xZ over 25 of 25 open PRs, per-PR file lists, 0 unmeasured, 0 empty file lists, 582 distinct files:
packages/cli/src/commands/dev.ts— 0 holderspackages/cli/src/utils/format.ts— 0 holders
⭐ Positive control fired (a bare zero is not a reading):
packages/metadata-protocol/src/protocol.ts2 holders (#16796, #16783);packages/objectql/src/plugin.integration.test.ts2 (#16796, #16783);packages/plugins/plugin-auth/src/auth-manager.ts2 (#16780, #16730).Acceptance — the card's own three, ⛔ none optional
Reproduce them in the PR under
## 验收备注with your evidence against each:- With
OS_AUTH_URLset, all three lines of the connect block print the canonical origin, and the➜ MCP:row agrees with the block. ⛔ Assert on both rows of one captured boot output — the card is explicit: "Two printers that disagree cannot be pinned by reading one of them." Asserting only on the block would leave the actual defect untested. - ⭐ With
OS_AUTH_URLunset, the block still prints the listen origin — including the auto-shifted-port case (3000 busy → 3001) the surrounding code exists to handle. This is the negative control, and it is the direction a fix most easily breaks: hardcoding the canonical origin makes item 1 green while destroying the ordinary local case. ⛔ Do not skip it. - Reuse the precedence chain from the existing resolver, ⛔ do not reimplement it. In particular the set-but-empty
OS_AUTH_URLbehaviour already pinned inpackages/cli/src/commands/serve-auth-base-url-diagnostic.test.tsmust not change — run those pins and show them still green.
Notes for the taker
⚠️ Locate by symbol, never by line number. The card's:503-508and:816anchors were written against5e53d73d;origin/mainis nowdd2184ae25. This lane has measured triage anchors rotting by 19 lines (runtime: two dispatcher domain gates read the tenancy posture through the collapsingresolveServiceprobe, so a tenancy service that failed to build reads as "no wall" at/keysmint and at the activation-write refusal #15900) and 491 ([finding] fourpackages/clisites cite driver-sql's lookup arm for "a platform id is 26 characters" — the platform mints 16, and stores a supplied id at any width #16114). Re-derive before you edit, and say what you found.- The
Connectline is a command the reader pastes, not documentation — which is why this is a bug and not a cosmetic mismatch. - ⛔ Never edit
content/docs/releases/; your PR's input to release notes is its changeset.
Generated by Claude Code
os-dev-report
{ "issue": 16734, "status": "done", "branch": "claude/issue-16734-mcp-connect-hint-origin", "pr": "https://github.com/objectstack-ai/objectstack/pull/16812", "premise_still_valid": true, "summary": "The card's premise held on dd2184ae25 and the defect reproduced exactly as described. `os dev`'s connect block now resolves its origin through `resolveAuthBaseUrl` — the same function `serve`'s ready-banner call site uses — with the port the child ACTUALLY bound, instead of building a base from the `objectstack:listening` `url` (the socket the child bound, which `OS_AUTH_URL` never reaches). The block moved into an exported `printMcpConnectHint({ boundPort, name })` in dev.ts so both printers could be driven into one captured boot output by the new pin; the call site now passes `boundPort: actual` and nothing else. When the chain yields no parseable origin (set-but-empty `OS_AUTH_URL`, or a value with no scheme) the block is omitted rather than reprinting the address the banner just refused to print — the one judgement call in the diff, argued in the function header and in the PR body. format.ts is untouched; resolveAuthBaseUrl is untouched; the Endpoint/Skill/Connect wording is untouched. Anchor rot, re-derived on the current base: the card's `dev.ts:504-505` base expression was at 500-501 (4 lines), the card's `dev.ts:503-508` block was exactly at 503-508 (0 lines), and `format.ts:816` was exact. Clause-② stays `no`: no accept set relaxed, no published surface widened, no new entry in any precedence chain — the diff adds an import of an existing exported resolver and changes no line of serve.ts. `check-clause2-carriers --pair 16812` exit 0, both carriers agree. Assignee untouched, no second Claim posted. Docs-drift advisory on the PR answered without widening it: re-derived locally (34 docs, 4 anchors), the 4 release-owned pages left untouched, the other 30 verified unaffected, and a hand-read of content/ for the rule's own vocabulary found no page that states which origin the hint is built from. 0 of 34 pages edited; the PR is still 3 files.", "tests": "All at b466fc5d90 unless noted; exit codes captured before any pipe, verdicts read from each tool's own line. (1) `pnpm --filter '@objectstack/cli^...' build` exit 0. (2) NEW `packages/cli/src/commands/dev-mcp-connect-hint-origin.test.ts`, 11 cases, every case capturing BOTH printers (console.error banner + console.log hint) into one ordered buffer: acceptance 1 = the measured repro (dev -p 4001 under OS_AUTH_URL=https://localhost:4443) asserting the banner row, Endpoint, Skill and Connect all carry https://localhost:4443, that `localhost:4001` appears nowhere, and that the set of distinct MCP origins in the capture is exactly one; plus public origin with/without port and the rest of the chain (BETTER_AUTH_URL then OS_BASE_URL). acceptance 2 = unset, three cases: 3000, the auto-shifted 3001 (also asserting `3000` appears nowhere), and an ephemeral 45064. acceptance 3 = set-but-empty OS_AUTH_URL and a scheme-less value: banner paths-only, hint silent, `never-consulted.example.com` absent. Plus three source pins (call site passes the bound port; no `msg.url` remains; dev's child-env literal sets no chain variable). (3) Existing pins re-run UNCHANGED: serve-auth-base-url-diagnostic.test.ts 25 passed, serve-banner-external-base-url.test.ts 6 passed, serve-bound-port-publication.test.ts 21 passed, test/vitest-tiers-partition.test.ts 22 passed, new file 11 passed = 85 of 85 in one run, exit 0. (4) Full unit tier `pnpm --filter @objectstack/cli exec vitest run --project unit`: 186 files, 2555 passed + 6 expected-fail, exit 0. A first attempt had 2 suites fail to COLLECT with the gate's own text `packages/cli is not built (./dist/index.js is absent)` — a stated PREREQUISITE, not a finding; green after `pnpm --filter @objectstack/cli build` and in the full re-run. (5) `pnpm --filter @objectstack/cli typecheck` exit 0. (6) `pnpm lint` (eslint . --no-inline-config, WHOLE repo, no narrowing) exit 0 in 1m22s. (7) Gate families derived, not guessed: `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, reconciled with --ran: 57 derived, 57 run, 0 NOT-MEASURED, 0 UNRUN; 54 exit 0; 3 exit 3 PREREQUISITE NOT MET (each needs a full workspace build, declared to CI): check:dual-build-cjs-loads, check:i18n-coverage, check:type-check-debt — its first half check:type-check-coverage ran and passed. The 5 roster-flagged gates whose silence is not evidence were RUN, all exit 0: check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:scaffold-emission-policy. check:dts-closure read 0 during the sweep, then 1 after the unit tier had run, naming 7 packages with JS-only dist and no declarations, none in this diff nor downstream of it; rebuilding exactly those 7 restored 63 packages swept / 155 of 155 declarations present, exit 0 — container tree state, not this change. check:nul-bytes exit 0 plus a direct grep -naP control-character scan over both edited files, no hits. dispatch-gates warned STALE TREE (origin/main 78bc4ad587) naming publish-smoke.yml and check-skill-line-ratchet.mjs; re-deriving after `git fetch origin main` produced a byte-identical command list, and neither file can add a family for these paths (workflow_run/schedule with no PR path filter; population is skills/**, untouched). Integration tier: the derived include list names no file this diff touches, so it is declared to CI. ABLATION — two legs, fix committed first, mutation proven ON DISK by occurrence counts for both the removed text and the injected marker plus `git hash-object`, restored with `git checkout HEAD --` and re-proven (`git diff HEAD` empty, blob equal to the HEAD blob), each leg under a `trap ... EXIT INT TERM`. Leg 1 (origin taken from the listen socket again, blob 8bb27f19db then a2980ca600): 5 failed / 6 passed — the three OS_AUTH_URL-set cases and both unusable-value cases red, the three unset cases green, as they must be. Leg 2 (resolveAuthBaseUrl(3000) instead of the bound port, blob then 56d7bfd6e4): 2 failed / 9 passed — only the shifted-port and ephemeral-port cases red, acceptance 1 green. Direction as predicted in both legs; no rebuild sits between mutation and reading because dev.js/serve.js resolve to SOURCE inside packages/cli, not through any dist. DOCS-DRIFT ADVISORY (mid-task, from the PM): re-derived rather than read off the truncated bot comment — `node scripts/docs-audit/affected-docs.mjs --json 56ee7aef1d49d7583e0cb268dc6cbd1c34ee8fd0` exit 0: 34 docs, 1 changed package, 4 anchors (printMcpConnectHint symbol, `os dev` command, /api/v1/mcp and /api/v1/mcp/skill routes). Provenance kept beside the bot's: the bot computed on merge commit 8652f200e599 and itself said that checkout carried uncommitted changes; this run reports head b466fc5d90ec, diffBase dd2184ae25, dirty false, same sinceRef. The 4 release-owned pages (content/docs/releases v13, v14, v16, v17) were NOT edited — release notes are written centrally at release time and this PR's only input to them is its changeset; for the record v16 lines 75 and 468, the two rows that actually describe this block, remain true after the change. The other 30 were each listed through a generic anchor — the `os dev` command name, the two MCP path literals this diff moves but does not change, or the brand-new symbol no page can be naming — so all 30 are verified unaffected and none was edited. HAND-READ for the bot's declared blind spot (a page stating the rule by its inputs while the diff changed the emitter): `grep -rn 'objectstack:listening|listen origin|bound socket' content/` returned ZERO hits — no page anywhere states which origin the hint is built from. The two pages that print a `claude mcp add` line (content/docs/ai/connect-mcp.mdx and content/docs/getting-started/build-with-claude-code.mdx) show http://localhost:3000/api/v1/mcp for a local dev server and the deployment origin for a deployed instance, which is exactly what the fixed hint now emits in each case — the change moves the code toward those pages, since before it only the local form could ever be printed. content/docs/deployment/cli.mdx:45 names the PATH with no origin and is about the ready banner this diff does not touch. The one behaviour change that could falsify a promise — the block being omitted when no origin resolves — is promised by no page. Outcome: 0 of 34 pages needed a change, 0 edited, PR still 3 files. Gates re-run after the PR body was updated to record this: scripts/docs-audit/check-affected-docs.mjs exit 0, scripts/docs-audit/check-drift-comment.mjs exit 0, check-clause2-carriers --pair 16812 exit 0.", "mcp_calls": "0 — every GitHub read and write on this run went through repo-scoped REST (probed 200 first) and git; no MCP GitHub tool was called", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: `ListeningMessage.url` now has no reader in packages/cli/src — `dev` was its last consumer and `start` declines it on purpose (its own comment forbids rebuilding an address from that message). The field is still published, still pinned by serve-bound-port-publication.test.ts, and the same `url` is the right value in the runtime state file. Dead-ish code, which is on the never-file list. Carrier: the next PR touching publishBoundPort / ListeningMessage.", "noted, not filed: the PR body carries two attribution footers — the one this session is required to write, plus a session-URL footer the platform appended below it on creation. Cosmetic; read back and confirmed the rest of the body survived byte-for-byte.", "noted, not filed: the docs-drift bot's own output declares two precision limits on this run — 1 name too generic to anchor anything, and the SDK route bridge reaching only 61 of 219 client-bound ledger rows (158 unreachable), so the list is neither exhaustive nor fully precise. That is the tool's stated behaviour, not a defect of this change; the hand-read above is the compensating control." ] }
Generated by Claude Code
- added a commit that references this issue
on Sep 17, 2026
Split out of #16530 (Side observations, first bullet). That card is a
domain:servicesp1 aboutplugin-authpassing an option@better-auth/oauth-provider1.7.2 no longer reads; this is an unrelatedpackages/cliprinting face, and making the p1 carry it would pull a whole CLI package verification run into an auth-plugin PR.Reproducible defect
os devprints two MCP blocks from two different origin resolvers, and only one of them knows about the canonical origin. Measured onorigin/main5e53d73d:packages/cli/src/utils/format.ts:816— the➜ MCP:row of the ready bannerlink()), which honoursOS_AUTH_URLpackages/cli/src/commands/dev.ts:503-508— the🤖 MCP server — connect a coding agentblockdev.ts:504-505:msg.urlis theobjectstack:listeningmessage the serve child emits once its HTTP server is up — the bound socket, by construction.OS_AUTH_URLnever enters this expression, and the whole block (Endpoint,Skill,Connect) is built from that onebase.Reported observation from #16530, on hotcrm
789a7324withobjectstack dev -p 4001andOS_AUTH_URL=https://localhost:4443behind a TLS reverse proxy: the➜ MCP:line sayshttps://localhost:4443/…and the block below it sayshttp://localhost:4001/…, in the same boot output.Why it matters
The
Connectline is not documentation, it is a command the reader is meant to paste. Pasting it registers an MCP entry against an origin that (a) is not the origin the discovery documents advertise and (b) on a TLS-fronted deployment is not reachable from the agent at all. The two blocks disagreeing in the same output also makes the correct line look like the typo.Scoped deliberately narrow: this is about which origin the hint is built from. The
Endpoint/Skill/Connecttext itself is right.Where
packages/cli/src/commands/dev.ts:503-508— the block, and thebaseexpression above it.packages/cli/src/utils/format.ts:816and itslink()base — the resolver that already gets this right; the reference implementation, not a second thing to change.packages/cli/src/commands/serve-auth-base-url-diagnostic.test.ts— the existing pins on theOS_AUTH_URL→BETTER_AUTH_URL→OS_BASE_URLprecedence chain, including the set-but-empty case. Whatever this fix reuses must not re-derive that chain by hand.Acceptance
OS_AUTH_URLset, all three lines of the connect block print the canonical origin, and the➜ MCP:row and the block agree — assert on both rows of one captured boot output, not on the block alone. Two printers that disagree cannot be pinned by reading one of them.OS_AUTH_URLunset, the block still prints the listen origin (including the auto-shifted-port case3000 busy → 3001the surrounding code exists to handle). This is the control: a fix that just hardcodes the canonical origin breaks the common local case.OS_AUTH_URLbehaviour already pinned inserve-auth-base-url-diagnostic.test.tsmust not change.Filed by the triage seat, unassigned. Related: #16530 (the p1 this came out of), #16399 (URL normalisation in
getAuthIssuer/getMcpResourceUrl— a different layer, and the values in #16530 were well-formed).