You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(runtime,rest): advertise mcp only when it is actually serveable (#4032)
* fix(runtime,rest): advertise `mcp` only when it is actually serveable
Fixes#4024.
Both discovery producers gated the `/mcp` route on `isMcpServerEnabled()`
alone. The dispatcher documented why that was safe: `os serve` auto-loads
plugin-mcp from the same flag, so on that path advertised did imply mounted.
That reasoning is sound but it describes the CLI, not the dispatcher — and
the same comment claimed `@objectstack/rest` stayed "symmetric", which it
did not. `@objectstack/rest` has no `@objectstack/mcp` dependency, mounts no
`/mcp` route and performs no auto-load, so a host embedding it without
plugin-mcp advertised `/mcp` in `/discovery` and then answered 501 on it.
Every OTHER optional route in that discovery table is service-presence
gated (hasUi, hasFiles, hasAnalytics, hasI18n, …). `mcp` was the lone
exception, and the exception is what broke.
That matters beyond tidiness: `/discovery` is the contract a third-party
client reads to decide what exists, and the objectui Integrations page
renders its MCP connect card from this field. Advertising an endpoint that
501s makes discovery unreliable for exactly the consumers that depend on it.
Now: flag AND serveable, on both producers.
- Dispatcher gates on the handler's OWN predicate — `typeof
mcpSvc?.handleHttpRequest === 'function'`, byte-identical to what
handleMcpRequest 501s on. Gating on mere service presence would still
over-promise when a wrong-shaped service is registered.
- `@objectstack/rest` probes via the per-request kernel (shape) or the
single-env `serviceExistsProvider` (existence) — the same two paths
`resolveRegisteredServices` already uses. `rest-api-plugin` always wires
the latter, so the gate is live in real deployments rather than
decorative. When it genuinely cannot probe it keeps the prior flag-only
answer: "unknown" is not "absent", and hiding a working endpoint would be
the opposite over-correction (fail-open, ADR-0057 D10).
`os serve` / `os dev` are unaffected — they load the plugin, so the service
resolves and `/mcp` is still advertised.
## Reproduced, then fixed#4024 was filed as static analysis with the repro explicitly listed as not
done. It reproduces. With the flag on and the service absent, the old gate
failed the new pairing assertion with the defect stated verbatim:
AssertionError: discovery advertised mcp (true) while POST /mcp
returned 501 — declared ≠ enforced
## The seam that should have caught it
`route-parity.integration.test.ts` has carried an `mcp` opt-out in
`stubServicesPlugin` since it was written, and nothing ever passed it —
`bootServe()` was called with no args and with `{ notification: false }`,
never `{ mcp: false }`. Its MCP test ("MCP is advertised AND reachable")
stubs the service in unconditionally, proving the lockstep only under the
one condition where it cannot fail. So the single capability whose
advertisement was not service-presence gated was also the single one whose
absence was never tested.
The seam is now used. Also added rest-side coverage for absent / present /
unprobeable, since the three existing `routes.mcp` tests construct
RestServer with three args and therefore all take the unprobeable path —
without the new cases the rest half would have shipped with no coverage at
all. Verified each new assertion fails against the old gate and passes
against the new one.
runtime + rest: 906 passed (65 files); rest alone 494 (34 files); repo
eslint clean; 0 type errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011AvZj6cLX7APd7roh2eK4F
* docs(ai): record the enabled-but-unserveable MCP symptom
The connect-MCP troubleshooting table had a `501 Not Implemented` row
("the MCP plugin isn't part of this build"). That row is still accurate —
the route stays mounted and still 501s when called directly; only the
advertisement changed. But it is now incomplete: after the #4024 fix the
first signal an integrator sees is usually the *absence* of `mcp` from
`/discovery` and a missing Connect-an-Agent card, not a 501, because
discovery no longer points at a route it cannot serve.
Added the other-side-of-the-same-cause row, including the distinction that
matters for embedders: `os serve` / `os dev` load the plugin for you, while
a host embedding `@objectstack/rest` directly must add `@objectstack/mcp`
itself.
Note: the docs-drift advisory on #4032 listed 22 files derived from the
dependency graph and did NOT include this one — the doc that actually
needed the change. Recording that here rather than filing it; the advisory
is explicitly best-effort.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011AvZj6cLX7APd7roh2eK4F
---------
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: content/docs/ai/connect-mcp.mdx
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -191,6 +191,7 @@ skill and a guided `/objectstack:connect` command.
191
191
|:---|:---|
192
192
|`404` on `/api/v1/mcp`| The HTTP surface is disabled — unset `OS_MCP_SERVER_ENABLED` (default is on) |
193
193
|`501 Not Implemented`| The MCP plugin isn't part of this build — check your stack's plugins |
194
+
|`mcp` missing from `GET /api/v1/discovery` and no Connect-an-Agent card, but `OS_MCP_SERVER_ENABLED` is on | The same cause as the `501` above, seen from the other side: the surface is *enabled* but not *serveable*, so discovery declines to advertise a route that would 501 rather than over-promising it (`declared === enforced`). Load the MCP plugin — `os serve` / `os dev` do it for you; a host that embeds `@objectstack/rest` directly must add `@objectstack/mcp` itself |
194
195
| stdio won't start / boot fails closed |`OS_MCP_STDIO_ENABLED=true` but `OS_MCP_STDIO_API_KEY` is missing, unknown, revoked, or expired — fail-closed by design (ADR-0101). Set a valid `osk_` key; there is no unscoped or `system` fallback |
195
196
|`401` on every call | Anonymous or invalid credentials. Interactive clients: complete the browser login (the `WWW-Authenticate` header advertises the OAuth metadata). Headless: check the `osk_` key and header spelling |
196
197
|`403 insufficient_scope`| The OAuth token lacks the scope for that tool family (e.g. writes without `data:write`) — reconnect and grant the scope |
0 commit comments