Skip to content

fix(mcp): a metadata outage stops being answered as Agent "X" not found (#6055) - #6507

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-6055-mcp-metadata-outage-diagnosed
Aug 8, 2026
Merged

fix(mcp): a metadata outage stops being answered as Agent "X" not found (#6055)#6507
os-project-manager merged 1 commit into
mainfrom
claude/issue-6055-mcp-metadata-outage-diagnosed

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #6055

An MCP client asking for an agent prompt during a metadata outage was told Error: Agent "X" not found — an availability failure reported as a fact about what the author declared. This PR makes the two answers different. It does not change what is served: the surface was fail-closed before and is fail-closed after.


1. Premise re-check (first action, against origin/main)

Holds, with the line drift the dispatch predicted. Located by content, not by line number:

Claim Verdict on origin/main
mcp-server-runtime.ts:443 reads metadataService.get('agent', agentName) True, now at :462
the undefined is answered as Agent "X" not found True, verbatim
fail-closed, not a security defect True — no instructions are served on either branch
getDiagnosed landed and is consumable TrueIMetadataService.getDiagnosed? at packages/spec/src/contracts/metadata-service.ts:262, MetadataManager.getDiagnosed at packages/metadata/src/metadata-manager.ts:836

2. The PM's mechanism assumptions, verified rather than assumed

getDiagnosed is OPTIONAL — confirmed, and handled explicitly. The contract declares it getDiagnosed?(…) and says so in prose: "Optional: implementations that predate it simply cannot report the distinction, and a consumer that probes for it must keep reading get when it is absent." Both new read paths probe with typeof … === 'function' and fall back; a legacy service is pinned by two tests (a service that predates getDiagnosed behaves exactly as it did), one per surface.

Both landed consumer samples were read; this surface takes the shape of the 503 one and the level of the warn one. They are not two styles of the same choice — they answer two different questions, and this surface answers both:

  • packages/metadata-protocol/src/protocol.ts (503) — its caller is a REST boundary that will render the answer. The MCP surfaces are the same: an external caller is waiting and will be told something, so it must be told the truth. This PR takes that shape, and narrows it exactly as PR fix(metadata,metadata-protocol,objectql): getDiagnosed —— get() 不再把 loader outage 答成「这一项没声明」 (#5840) #6051 narrowed getMetaItem: degraded changes the answer only after the read has resolved nothing, i.e. only when the answer would otherwise have been the unfounded "not found". A read that answered a body is served exactly as before.
  • packages/objectql/src/plugin.ts (warn) — its subject is the log level, decided by AGENTS.md "Degradation log levels" rather than by analogy. Asked honestly here: does something this code claims is persisted fail to land while the system looks normal? No — nothing was written, a read failed, and the caller is told in the same breath. So warn, carrying the consequence and the fix, exactly as that sample does.

Sibling-occurrence sweep inside packages/mcp — one more found, and it is in the same file. Every metadata read in the package:

Site Read Verdict
mcp-server-runtime.ts:462 agent_prompt get('agent', n) the issue's subject — fixed
mcp-server-runtime.ts:296 object_schema resource getObject(n) same family, same file — fixed
mcp-server-runtime.ts:268 objectstack://objects listObjects() plural read; no diagnosed counterpart exists — filed, not fixed (§6)
mcp-server-runtime.ts:499 skill prompts list('skill') same as above — filed, not fixed (§6)
mcp-server-runtime.ts:398 metadata types getRegisteredTypes() not the family (no absence claim)

The one sibling PR #6051 named outside this package — service-datasource/src/plugin.ts:82 — is untouched, per the dispatch.

3. What changed

Two call sites, both in packages/mcp/src/mcp-server-runtime.ts, each extracted into an exported builder so the decision can be pinned directly (the handlers are registered on a private McpServer; driving them over a transport would test the SDK, not this decision).

agent_prompt reads through getDiagnosed('agent', name) when the service offers it. Three answers:

  • present → the instructions, unchanged;
  • genuinely absent → Error: Agent "X" not found, byte-identical to before;
  • degraded → Error: SERVICE_UNAVAILABLE — the metadata service could not be read, so whether agent "X" exists is unknown. No agent instruction is being served for this call. Retry once the metadata service is reachable.

object_schema keeps getObject(name) as its resolver and consults getDiagnosed('object', name) as a verdict-only probe on the miss path. Why not just swap the resolver: getObject is its own contract member with no documented equivalence to get('object', name), and the equivalence does not hold — MetadataManager.getObject delegates to get('object', name), but MetadataFacade.getObject (objectql) returns registry.getObject(name), a different lookup returning a different shape. Presuming that equivalence at a consumer is the private dialect Prime Directive #12 forbids. The trade is one extra read on the miss path only (never on a hit), and on a hypothetical host with both a getDiagnosed and a divergent getObject the probe can only make the answer more conservative — it withholds, it never admits. Written up in diagnoseEmptyRead's TSDoc, and the contract gap itself is filed (§6).

Wording and classification

SERVICE_UNAVAILABLE / 503 and RESOURCE_NOT_FOUND / 404 are the standard catalog's own codes for their statuses (ADR-0112), the same spelling the sys_metadata half of this family already emits (#5532 / #5705 / #5843) — no vocabulary invented for MCP. The sentence is modelled on METADATA_STORE_UNAVAILABLE_MESSAGE (whether it exists is unknown, plus retry advice) with two deliberate departures, both argued in the code: it names the metadata service, not "the metadata store", because degraded is a loader-set fact and PR #6051 explicitly declined #5897's single-store storeUnavailable spelling for that reason; and it states what is not being served, so the next reader does not "restore" a body here.

4. Where the ADR-0112 envelope could not go, and what stands in for it

Reported as the dispatch asked. This transport has no error envelope on either surface. MCP answers prompts/get with a GetPromptResult and resources/read with a ReadResourceResult; neither type carries an error channel — only CallToolResult has isError. Neither handler threw before this change and neither throws after, so there is no code + status on the wire to pin.

The channel was deliberately not changed to a thrown JSON-RPC error. Both surfaces answer their existing error convention (a user-role Error: … message; an { error } JSON body), and converting one of the two branches to a throw would leave the surface answering a miss one way and an outage another — a larger behavioural change than the diagnosis fix the issue asks for, and one that lands on every MCP client. If the maintainer wants these surfaces to fail loudly at the transport level, that is a separate contract decision.

So the strongest available discriminator is pinned instead, per surface:

  • the resource body is JSON, so the classification travels structurally and both answers carry it (SERVICE_UNAVAILABLE/503 vs RESOURCE_NOT_FOUND/404). The miss's error sentence is unchanged; code/status are additive;
  • the prompt body is plain text, so it is pinned as: carries SERVICE_UNAVAILABLE, says "unknown", does not say "not found".

On top of both, each pair is pinned as a pair: the outage answer and the miss answer must not be equal. That is the fact the defect was — before this they were byte-identical — and it is the one assertion a merely mis-worded improvement cannot satisfy.

5. Tests and reverse verification

New: packages/mcp/src/mcp-server-runtime.metadata-outage.test.ts, 17 cases covering all three states on both surfaces — present (resolves), genuinely absent (not-found preserved), degraded (unavailable, and access still refused) — plus the legacy-service path, the "reads through getDiagnosed, not get" pin, and the log line. The doubles fill every REQUIRED member of IMetadataService with a throwing stub, so a path that reaches a read it should not fails loudly instead of resolving undefined and looking like the very absence under test. No engine write verb is declared, so there is no delete dispatch for check:engine-double-contract to scan and no guard to hand-mirror.

Reverse verification — direction and counts written down before running. Reversion defined as restoring the pre-#6055 reads (get('agent', name) with the single if (!raw); getObject() with the bare { error: 'Object "X" not found' }). Predicted 8 red / 9 green, split 4/6 on the prompt and 4/3 on the resource. Measured, exactly the eight named:

× DEGRADED: answers SERVICE_UNAVAILABLE, and never the not-found claim
× the outage and the miss no longer collapse to the same answer          (prompt)
× reads through getDiagnosed, not get, when the service offers it
× logs the outage once, with the consequence and the fix
× GENUINELY ABSENT: not-found, classified 404 / RESOURCE_NOT_FOUND
× DEGRADED: unavailable, classified 503 / SERVICE_UNAVAILABLE, no schema served
× the outage and the miss no longer collapse to the same answer          (resource)
× probes the object type by name when the resolver came back empty
Test Files  1 failed (1)     Tests  8 failed | 9 passed (17)

⚠️ One case is green in both directions, and that is reported rather than counted as coverage. DEGRADED: access is still refused — no instructions are served cannot go red on this fix's reversion: the pre-fix code served no instructions during an outage either. It is an invariant pin — the thing that must not change, and what would go red if a future edit here started serving a body. Folding it into the red count would have been a fabricated number.

6. Out-of-scope findings (filed, not fixed here)

7. Commands and real output

pnpm --filter @objectstack/mcp test
  Test Files  10 passed (10)        Tests  114 passed (114)

pnpm --filter @objectstack/mcp exec vitest run mcp-server-runtime.metadata-outage
  Test Files  1 passed (1)          Tests  17 passed (17)

pnpm --filter @objectstack/mcp typecheck            (tsc --noEmit)  → clean, no output
pnpm lint                                            → clean, no output
pnpm check:type-check-coverage                       → OK, 62/77 + root, 15 ledgered

⚠️ packages/mcp/tsconfig.json excludes **/*.test.ts (pre-existing TEST_DEBT entry, recorded 63), so pnpm typecheck does not read the new test file. Rather than report a green it did not earn, the ledger measurement was reproduced by hand the way --re-measure does it (sibling config, test globs dropped from exclude): 53 errors, none in the new file — below the recorded 63, i.e. informational, never a failure. The exclusion was not touched; lifting it is not this PR's business.

Every check:* step enumerated from .github/workflows/lint.yml, run one by one, all green:

slot-lookup query-options-erasure nul-bytes doc-authoring docs-audit-scope role-word
quick-reference-counts adr-anchors org-identifier authz-resolver service-providers
route-envelope error-code-casing wildcard-fallthrough meta-type-normalized
init-service-contract durability-log-level startup-registry-verdict objectui-changeset
release-notes release-body node-version workflow-status-functions shard-attestation
published-files engine-double-contract resume-authority-declared merge-driver
spec-parsed-alias                                                        → all OK

node scripts/check-nul-bytes.mjs → OK (6106 tracked text files), plus a direct
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' over the three changed files → clean.

Changeset added: .changeset/mcp-metadata-outage-vs-miss.md (@objectstack/mcp: patch) — the change is user-visible (an MCP client's answer during an outage changes, and the resource body gains code/status).


Generated by Claude Code

…d" (#6055)

`agent_prompt` read `metadataService.get('agent', name)` and answered its
`undefined` with `Error: Agent "X" not found`. That `undefined` carries two
opposite facts (#5840, ADR-0110 D3) — never declared, or every loader down —
so an availability failure was reported to an MCP client as a declaration
fact. The `objectstack://objects/{objectName}` resource had the same shape on
`getObject()`.

Both now separate the two, keeping the surface fail-closed: a degraded read
answers SERVICE_UNAVAILABLE (the #5532/#5843 spelling), a genuine miss keeps
its not-found answer. MCP's prompt/resource results carry no error envelope,
so the classification travels in each surface's existing payload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uFVNMmTxLpmfQYiuKM1Yx
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 2:41am

Request Review

@github-actions github-actions Bot added the size/l label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/mcp.

11 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via @objectstack/mcp)
  • content/docs/ai/agents.mdx (via @objectstack/mcp)
  • content/docs/ai/connect-mcp.mdx (via @objectstack/mcp)
  • content/docs/ai/index.mdx (via @objectstack/mcp)
  • content/docs/ai/natural-language-queries.mdx (via @objectstack/mcp)
  • content/docs/api/index.mdx (via @objectstack/mcp)
  • content/docs/deployment/environment-variables.mdx (via @objectstack/mcp)
  • content/docs/permissions/authorization.mdx (via @objectstack/mcp)
  • content/docs/plugins/packages.mdx (via @objectstack/mcp)
  • content/docs/protocol/knowledge.mdx (via @objectstack/mcp)
  • content/docs/releases/implementation-status.mdx (via @objectstack/mcp)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 8, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 8, 2026 04:41
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 5c2716b Aug 8, 2026
25 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-6055-mcp-metadata-outage-diagnosed branch August 8, 2026 05:01
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mcp-server-runtime:443 把 metadata outage 答成 'Agent "X" not found' —— fail-closed 但误述,#5840 定性后的 cli 侧跟进

2 participants