Skip to content

fix(mcp): stdio lists agent_prompt only on a boot that holds an agent (#28940) - #29201

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-4hn9ef
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-4hn9ef

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #28940

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:stdio 通道的 MCP 服务器只在本次启动确实注册了 agent 时才在提示列表里列出 agent_prompt;参数说明里的示例名改为取自已注册的 agent,不再写死 "ask" / "build"。
  • 为什么改:开源框架启动时一个 agent 都没有,客户端却能看到 agent_prompt,照说明填 "ask" 得到一条「成功」的提示,内容是 Error: Agent "ask" not found,模型会把它当指令读。
  • 风险与回滚:有 agent 的启动对客户端无变化(照样列出,未命中文字逐字不变);agent 元数据读不全时照旧列出,不把故障当成「没有」。回滚即 revert 本 PR。
  • 先看:packages/mcp/src/mcp-server-runtime.ts 的 bridgePrompts 开头(是否注册的分支)与 readAgentRoster;packages/mcp/src/mcp-server-runtime.agent-prompt-advertised.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/mcp exec vitest run src/mcp-server-runtime.agent-prompt-advertised.test.ts,预期 8/8 通过。
  • 需要你做的:无,席位落地。

What changes

Why

The same file's skill projection is built on "declared IS served": a skill that cannot be served is not listed. agent_prompt was the opposite shape on a boot with no agent. It was a prompt advertised where nothing can serve it, with an argument teaching two names that resolve to nothing, and a miss answered as a success.

Route check against recorded decisions (the delta asked for it):

The two transports after this change:

  • stdio serves skill prompts, plus agent_prompt only on a boot holding an agent.
  • HTTP (/api/v1/mcp, handleHttpRequest → registerSkillPrompts) serves skill prompts only and never agent_prompt. That is unchanged.

On a boot with no agent, the open-framework case the card measured, the two transports now agree.

Tests

  • New packages/mcp/src/mcp-server-runtime.agent-prompt-advertised.test.ts, 8 cases over the real wire (an in-memory transport into the same McpServer the stdio transport attaches to). Red first on unmodified code: 7/8 failed, and each failure was the defect itself (expected [ 'agent_prompt', 'case_management' ] to not include 'agent_prompt'; a get on the no-agent boot returned Error: Agent "ask" not found as a result; the description read (e.g. "ask"…; capabilities.prompts was { listChanged: true } with nothing to list). The 8th case, served body plus byte-stable miss, is the preservation pin and passes on both trees.
  • Forced test edits (named per the order):
  • pnpm --filter @objectstack/mcp exec vitest run: 54 files, 652/652 passed. pnpm --filter @objectstack/mcp typecheck: exit 0.
Ablations (one per negative pin, each restored with a blob-hash match)

Every mutation went through scripts/ablation-replace.mjs (the anchor hit once, the replacement landed, then a restore). The suite imports ./mcp-server-runtime.js from source, so no dist rebuild was involved. After the run, git diff HEAD was empty and the file blob was back at 09f63b589939.

  • A1, withholding disabled (if (roster.state === 'none' && false)) turned 4 red: no-agent list, get is -32602, boot line, no-capability case.
  • A2, the fixed e.g. "ask", "build" description put back turned 2 red: agents-boot description and degraded description.
  • A3, the degraded branch deleted (a degraded empty read becomes none) turned 1 red: degraded keeps the prompt.
  • A4, a thrown read mapped to none turned 1 red: throw keeps the prompt.

Acceptance notes

  • With agents registered, stdio lists agent_prompt and HTTP still does not. That divergence predates this card and is not in its acceptance. Noted, not filed.
  • The roster is a snapshot taken at MCP plugin start(), beside the skill snapshot. An agent provider that registers after that point in the same boot would leave agent_prompt withheld on stdio until restart. No such provider exists in this repository (no open package registers an agent). Whether a cloud host registers its platform agents later than start() cannot be measured from here, and the stdio transport is a local-host surface. Raised as an open question in the report, not worked around.

Verification

All readings at head 9126fc31.

Derived gate list and lint
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 59 commands. On the first sweep 56 exited 0. Three exited 3 because a prerequisite was missing, which is NOT MEASURED, not a pass:
    • node scripts/check-plugin-teardown-shape.mjs --self-test needed its pinned positive-control commit. After git fetch origin 621a4876… it exited 0 (48 cases).
    • pnpm check:dual-build-cjs-loads and pnpm check:lean-entry-closure read built output. After a full pnpm build (117/117 tasks) both exited 0. pnpm check:dts-closure was rerun on the same build and exited 0 (293/293 declaration files present).
  • --ran reconciliation: 59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN.
  • Not in the derived list but run anyway: pnpm check:startup-registry-verdict exited 0, reporting none recording a verdict the boot can contradict.
  • pnpm lint (eslint . --no-inline-config, whole repo) printed no diagnostic. The four changed .ts files linted alone also exited 0.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

The long-lived server registered agent_prompt unconditionally, with an
agentName argument suggesting "ask" / "build". On a boot that registers
no agent (every open-framework boot) prompts/list advertised a prompt
nothing could serve, and each get answered a successful prompt whose one
message was `Error: Agent "X" not found`.

The agent roster is now read at bridge time, beside the skill snapshot:
no agent and a complete read => not advertised (a get is the SDK's
-32602 unknown-prompt error) and one boot info line says why; agents =>
advertised, argument examples drawn from the registered names, a miss
byte-identical to before (#6055); degraded or thrown read => advertised
as before with no example names, plus a warn.

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

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

8 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/mcp/README.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/mcp/README.md) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 12 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 692e0dc8b936ad37311352868083d78745c76a21 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from eead7f4b5d932036e53b280241d86c80a7f7a031 — the merge of head 9126fc31d4e306ffa339b9e6dc02984c350d0c1d into base 692e0dc8b936ad37311352868083d78745c76a21, 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 eead7f4b5d932036e53b280241d86c80a7f7a031 && git checkout eead7f4b5d932036e53b280241d86c80a7f7a031
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 692e0dc8b936ad37311352868083d78745c76a21 9126fc31d4e306ffa339b9e6dc02984c350d0c1d && git checkout -B drift-repro 692e0dc8b936ad37311352868083d78745c76a21 && git merge --no-ff 9126fc31d4e306ffa339b9e6dc02984c350d0c1d

node scripts/docs-audit/affected-docs.mjs --json 692e0dc8b936ad37311352868083d78745c76a21

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

席位核对 · ACCEPT · PR #29201 · 2026-11-02T09:18Z

Head-sha: 9126fc31d4e306ffa339b9e6dc02984c350d0c1d

维护者速读(席位终稿)

核对六项

  1. 形态:draft → main;Fixes #28940,与认领 6572588582 一致;6 个文件(运行时、新测试、两处测试替身、README、changeset),均在申报面内。
  2. 前提实测:席位读 diff:三态 agents / none / unknown,只有读数完整且为空才不注册;buildAgentPromptResult 未动(mcp-server-runtime:443 把 metadata outage 答成 'Agent "X" not found' —— fail-closed 但误述,#5840 定性后的 cli 侧跟进 #6055 的未命中与停摆文字逐字保留)。dev 红先 7/8 失败;4 处消融转红并按哈希复原;mcp 652/652,typecheck 0。
  3. 门禁:dev 本地 dispatch-gates 59/59,0 NOT-MEASURED;pnpm lint 无诊断(退出码未留,CI 作答)。CI 在跑,入队前席位读本 head 全部 check-runs。
  4. 接受的偏差:open_question 取 A(启动时快照,与技能快照同一时刻同一寿命)。席位读数:bridgePrompts 只在 plugin.ts 的 stdio 分支、start() 之前调用一处;agent 是只能由代码声明的类型,save 门拒运行时新建,本仓无生产者在其后注册。B 需要第二条注册路径且 SDK 在传输接上后拒新能力,无实测拉动,不做。另两处被迫改的测试替身(给 list('agent') 返回 agent)已在正文点名。
  5. 范围外去向:无。Metadata-protocol: none。
  6. 落地条款:Clause-②: no — 有 agent 的启动对客户端无变化;无 agent 的启动不再列出一个答不了的提示,非已发布契约面(无 packages/spec/src/**),免契约复审。semver:@objectstack/mcp patch,与 changeset 一致。check-governed-merges --pr 29201:NOT governed。落地授权,维护者原话:「按照 pm-dispatch 规则复核过的都授权」(2026-10-25T11:35Z)。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 09:35
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 09:35
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 31670f5 Nov 2, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-4hn9ef branch November 2, 2026 10:40
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

1 participant