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
{{ message }}
Repository navigation
mcp-http-surface (b): the stock showcase cannot serve MCP at all — capability resolver substring-matches the outbound MCP client connector #7652
The stock showcase boot banner advertises "MCP server — connect a coding agent: Endpoint …/api/v1/mcp", yet both /mcp and /mcp/skill answer 501 "MCP server is not available".
Observed: banner promises the MCP surface; /mcp and /mcp/skill both 501.
Expected: the MCP surface the banner promises (keyed initialize/tools/list, /mcp/skill 200).
Root cause
serve.ts auto-adds mcp to requires when the MCP server is enabled (packages/cli/src/commands/serve.ts:884), and CAPABILITY_PROVIDERS.mcp declares nameMatch: ['mcp-server', 'MCPServerPlugin', 'mcp'] (serve.ts:427). hasPluginMatching (serve.ts:2327) does substring matching over loaded plugin names:
The showcase loads com.objectstack.connector.mcp — the outbound MCP client connector — whose name contains the substring mcp. So the resolver concludes the mcp capability is already provided and never loads MCPServerPlugin.
Proven: removing only ConnectorMcpPlugin from a scratch copy makes [MCP] Plugin initialized appear and /mcp/skill answer 200; @objectstack/mcp was resolvable from the CLI the whole time.
The general hazard is a substring match between a provider plugin name and a consumer plugin name: a consumer whose name contains the provider's fragment satisfies the provider's nameMatch, suppressing the real provider. Still present on origin/main (serve.ts:427 and the .includes(f) in hasPluginMatching at 2327-2332).
Reproduction
Boot the stock showcase; curl the advertised endpoint:
curl http://<host>/api/v1/mcp/skill
Expected the MCP surface the banner promises. Actual 501 "MCP server is not available".
Source
Extracted from the QA run #7627 (framework 92f26f7, console 6314e87f).
File surface: packages/cli/src/commands/serve.ts (hasPluginMatching ~:2327 and the CAPABILITY_PROVIDERS entries ~:427) + its tests. Stop on breach; explain in the report.
Container & model: M, mode:cloud, model: claude-opus-5.
The card names the instance (com.objectstack.connector.mcp contains mcp, satisfies CAPABILITY_PROVIDERS.mcp.nameMatch, so MCPServerPlugin never loads) and the class: a substring match between a provider plugin name and a consumer plugin name lets a consumer suppress the provider it consumes. The class is the fix. ⛔ Special-casing the connector's name — an exclusion list, a longer fragment, reordering — leaves the next consumer to rediscover this, and mcp is not the only fragment short enough to collide.
⚠️Both directions, or it is not a fix. Tightening the match must not stop a legitimate provider from being recognised: whatever nameMatch currently satisfies correctly has to keep satisfying. A resolver that fixes this by matching nothing would make every capability load its default provider and look green in exactly this repro. Pin a provider that should match alongside the consumer that should not.
Pin the consequence
The acceptance criterion is the card's own repro: on a stock showcase boot, /api/v1/mcp/skill answers 200 (and /mcp keys initialize/tools/list) with ConnectorMcpPlugin still loaded. ⛔ Not "the resolver returns false for this name" — that pins the mechanism and would pass over a build where the plugin still fails to load for some other reason. The dev proved causality by removal (dropping ConnectorMcpPlugin makes MCP work); the fix must make it work without dropping it.
Context worth having, not in scope
This is the third MCP-surface defect this shift: #7645 (stdio transport started but deaf — merged) and #7915 (os serve writes its banner to the stdout the stdio transport owns — filed, ungraded). ⛔ Do not fold either in. But if the measurement here turns up something that changes the reading of those, say so in the report.
{
"issue": 7652,
"status": "done",
"branch": "claude/issue-7652-mcp-capability-substring-match",
"pr": "https://github.com/objectstack-ai/objectstack/pull/7935",
"premise_still_valid": true,
"summary": "Verified the premise on current origin/main (3c9a67e) by text, not line number — the card's cites had drifted (auto-add 884->888, CAPABILITY_PROVIDERS.mcp 427->431, hasPluginMatching 2327->2331) but the mechanism was exactly as described. Fixed the CLASS: Serve.providesCapability now compares a loaded plugin's `name` and constructor name against the registry's declared identities by EQUALITY, and every entry declares the provider's real registered plugin id instead of a fragment of it. `nameMatch` renamed to `identities` so the semantic change breaks any caller loudly. No exclusion list for the connector, no lengthened fragment, no reordering. BOTH DIRECTIONS MEASURED, NOT ASSUMED: reading the provider packages showed most old name fragments were ALREADY DEAD — `service-cache` never matched `com.objectstack.service.cache` (dash vs dot), `plugin-email` never matched `com.objectstack.service.email`, `trigger-record-change` never matched `com.objectstack.trigger.record-change`; 18 of 23 entries were carried entirely by their class name, and only service-automation/service-analytics/audit/plugin-webhook-outbox/mcp had a live name fragment. The registry now carries the ids those packages actually register (strictly more correct than before), and a drift test imports every provider package, constructs the exported class, and asserts the name it registers is declared — so a rename fails loudly instead of quietly restoring double-loading. Two dead aliases dropped: 'mcp-server' (plugin is com.objectstack.mcp) and 'SharingPlugin' (class is SharingServicePlugin); neither matched anything.",
"tests": {
"acceptance_pin": "packages/cli/test/serve-mcp-capability-collision.e2e.test.ts — spawns the shipped bin/run.js, boots an app loading the consumer plugin that never declares `mcp`, asks the card's own question. NOT the resolver's return value.",
"measured_before_after": {
"GET /api/v1/mcp/skill": "501 -> 200",
"POST /api/v1/mcp initialize": "501 -> 200 with real protocolVersion + serverInfo",
"POST /api/v1/mcp tools/list": "501 -> 200 with a non-empty tool array"
},
"reverse_verification": "PASSED — with the substring match restored in hasPluginMatching (fix reverted via patch, tests kept), the new e2e fails 'expected 501 to be 200' on BOTH /mcp/skill and /mcp. The card's exact symptom. Fix re-applied and re-confirmed green.",
"new_files": [
"packages/cli/test/serve-capability-identity.test.ts (53 tests) — both directions: the consumer must not satisfy `mcp`; every registered provider must STILL satisfy its own capability by name and by class; near-misses substring matching used to accept are rejected; the four real connectors satisfy no capability they merely consume; plus the package-import drift guard and a source-level pin on the connector's own name.",
"packages/cli/test/serve-mcp-capability-collision.e2e.test.ts (3 tests) — the acceptance pin above."
],
"gates": {
"packages/cli suite": "PASS — 113 files, 1247 tests",
"pnpm build (turbo, 71 tasks)": "PASS",
"dispatch-gates matched: pnpm check:startup-registry-verdict": "PASS",
"pnpm check:nul-bytes": "PASS",
"pnpm check:type-check-debt": "PASS — no `•` error line for packages/cli; NO ledger entry raised",
"standalone typecheck of both new test files": "PASS — zero errors under packages/cli's strict config (its tsconfig excludes test/, so the ratchet does not measure them)",
"eslint --no-inline-config on all 3 changed files": "PASS"
},
"changeset": ".changeset/serve-capability-identity-match.md (@objectstack/cli patch)"
},
"sweep_for_same_exposure": {
"method": "All 26 fragment lists (23 CAPABILITY_PROVIDERS entries + 3 extras + 3 inline hasPluginMatching call sites) tested against a corpus of 59 plugin names and 64 plugin classes in this repo, asking which fragments were satisfiable by a plugin that is NOT the provider.",
"realized_collisions": "Exactly one: 'mcp' contained in com.objectstack.connector.mcp — the reported bug.",
"latent_exposure": "'audit' was the only other single-word fragment, one consumer away from the same fate (any plugin named com.*.audit-* or a class ending in AuditPlugin would have satisfied it). Every class-name fragment was exposed the same way — 'AuditPlugin' is contained in 'MyAuditPlugin' — but no in-repo class collides today.",
"action": "Reported, not separately special-cased. The uniform equality fix covers all of them; nothing measured justifies widening the change further."
},
"open_questions": [
"Resolved myself, not asked: the e2e declares the consumer plugin in its fixture rather than importing @objectstack/connector-mcp, because that package is NOT a dependency of @objectstack/cli — turbo's `test` dependsOn ^build would never build it, so it would be absent in CI. Importing it would have required adding a devDependency, i.e. touching packages/cli/package.json + the lockfile, outside the declared file surface. The resolver reads exactly two fields off a loaded plugin (name, constructor.name), so a plugin declaring the connector's real identity reproduces the defect with full fidelity; serve-capability-identity.test.ts additionally pins that identity against the connector's actual source so the fixture cannot drift into testing a name nobody registers. If the PM would rather the e2e import the real class, that is a one-line devDependency add.",
"Resolved myself: renamed the CapabilitySpec field `nameMatch` -> `identities`. Grepped the whole repo — the field is referenced only inside serve.ts (packages/spec owns a separate PLATFORM_CAPABILITY_PROVIDERS with a different shape). A rename makes any out-of-repo consumer of the public static `Serve.CAPABILITY_PROVIDERS` (e.g. cloud's objectos-runtime) fail at compile time rather than silently inherit substring semantics — that seemed the safer failure mode, but it is a visible API change on a public static and worth a reviewer's eye."
],
"out_of_scope_findings": [],
"file_surface": {
"declared": "packages/cli/src/commands/serve.ts + its tests",
"actual": "packages/cli/src/commands/serve.ts, packages/cli/test/serve-capability-identity.test.ts (new), packages/cli/test/serve-mcp-capability-collision.e2e.test.ts (new), .changeset/serve-capability-identity-match.md (new)",
"breach": "None beyond the changeset, which CLAUDE.md requires of every code PR."
},
"sibling_mcp_defects": "#7645 (stdio transport deaf — merged) and #7915 (os serve banner pollutes the stdio transport's stdout — filed) were NOT folded in. Nothing measured here changes how either should be read: both are transport-layer defects occurring AFTER MCPServerPlugin loads, whereas this one prevented it from loading at all. Worth noting for the PM that the three compound — on the stock showcase the plugin never loaded, so the surfaces #7645 and #7915 describe were unreachable there regardless."
}
Reviewed by the domain:cli seat (hotlong, session session_01B3Kurx8qufrDzNjk4rag7V) against the diff. The ruling was fix-the-class, and that is what landed: nameMatch fragments tested with String.includes() become identities compared by equality, and each entry declares the provider's real registered plugin id. ⛔ No exclusion list, no lengthened fragment, no load-order luck — a plugin either is the provider or it is not.
The comment states the general hazard in the form that will actually help the next reader: a plugin that CONSUMES a capability is conventionally named after the capability it consumes, so substring matching cannot tell a provider from its consumer — and mcp was not the only exposure (audit was one consumer away, and every class-name fragment was satisfied by any class merely ending in it, e.g. MyAuditPlugin for AuditPlugin).
The measurement found something worth more than the fix
Tightening the comparison could have broken legitimate providers, so the identities were measured against the provider packages instead of assumed — and that measurement found the registry was already substantially broken:
service-cache never matched com.objectstack.service.cache — dash versus dot. It has presumably never matched.
Eighteen of twenty-three entries were carried entirely by their class name.
So the "provider already supplied?" check had been running on one working limb for an unknown length of time, and nobody could see it because class-name containment quietly covered for every dead id. That is the kind of thing only a both-directions measurement surfaces; a fix that just special-cased the connector would have left all of it in place.
Both directions are pinned, and the vacuity traps are closed
The test file states the trap explicitly — "a resolver that matches nothing would make every capability load its default provider and look green in the repro" — and pins the negative half accordingly: every registered provider must still satisfy its own capability, by name and by class, across the whole CAPABILITY_PROVIDERS table, plus a coverage assertion so the expectation table cannot silently fall behind the registry.
Near-misses that substring matching used to accept are pinned as rejections (com.objectstack.mcp-inspector, com.acme.mcp, FakeMCPServerPlugin), as are the degenerate inputs (empty identity list, null-prototype plugin, nameless plugin).
Two guards I did not ask for and would have wanted
A drift guard for the new brittleness. Exact ids are precise but rename-fragile, and a provider renaming itself would silently return the resolver to double-loading. The new test imports each provider package, constructs it, and asserts the name it registers is one the registry declares — so the fix cannot rot the way the thing it replaced did.
The fixture's consumer identity is pinned against the real connector source. The e2e declares the consumer inline rather than importing @objectstack/connector-mcp (not a cli dependency, so turbo run test's ^build never builds it), which creates an obvious drift risk — the fixture could end up testing a name nobody registers. They closed it by reading connector-mcp-plugin.ts and asserting name = 'com.objectstack.connector.mcp'. That is the failure mode of their own workaround, found and fenced.
The acceptance is the real repro, not the mechanism
The e2e boots the shippedbin/run.js with the consumer plugin loaded and asserts GET /api/v1/mcp/skill → 200 and POST /api/v1/mcp → real JSON-RPC results for initialize and tools/list — the endpoints the card names, with a minted osk_ key because /mcp is authenticated. A providesCapability(...) === false assertion would have stayed green on a build where MCPServerPlugin fails to load for an unrelated reason; the test file says exactly that and rejects it as the criterion. Reverse-verified: restore the substring match and /mcp/skill returns to 501.
It also carries a vacuity check — "the boot really did load the consumer plugin — otherwise this file proves nothing" — because a fixture that stopped loading the connector would pass for the wrong reason.
Scope
4 files: packages/cli/src/commands/serve.ts, two new tests under packages/cli/test/, and the changeset. No docs/adr/**, no skills paths, no content/docs/releases/.
⛔ Not flipped yet: ESLint and TypeScript Type Check must both read conclusion: success on the head commit. ⚠️ Note packages/cli's tsconfig excludes test/, so the ratchet may not cover the two new test files — if the job trips anyway, the fix is to type them, ⛔ never to raise the ledger.
Activity
hotlong commentedon Aug 12, 2026
Claim: PM loop round 5 —
domain:cliseat.session_01B3Kurx8qufrDzNjk4rag7V(accounthotlong)claude/issue-7652-mcp-capability-substring-matchdomain:clipackages/cli/src/commands/serve.ts(hasPluginMatching~:2327 and theCAPABILITY_PROVIDERSentries ~:427) + its tests. Stop on breach; explain in the report.mode:cloud,model: claude-opus-5.serve.tswas untouched by every one of them (fix(mcp): the stdio transport answers again — resume the stdin it owns (#7645) #7914 touchedpackages/mcp/src/mcp-server-runtime.tsand addedpackages/cli/test/serve-mcp-stdio-answers.e2e.test.ts, notsrc/commands/serve.ts). This card was deliberately held behind mcp-stdio-fail-closed c3: the stdio MCP transport starts but is permanently deaf (oclif arg parser pauses process.stdin) #7645 for thepackages/mcpsurface; that surface is now free.Scope: fix the resolver, not the one collision
The card names the instance (
com.objectstack.connector.mcpcontainsmcp, satisfiesCAPABILITY_PROVIDERS.mcp.nameMatch, soMCPServerPluginnever loads) and the class: a substring match between a provider plugin name and a consumer plugin name lets a consumer suppress the provider it consumes. The class is the fix. ⛔ Special-casing the connector's name — an exclusion list, a longer fragment, reordering — leaves the next consumer to rediscover this, andmcpis not the only fragment short enough to collide.nameMatchcurrently satisfies correctly has to keep satisfying. A resolver that fixes this by matching nothing would make every capability load its default provider and look green in exactly this repro. Pin a provider that should match alongside the consumer that should not.Pin the consequence
The acceptance criterion is the card's own repro: on a stock showcase boot,
/api/v1/mcp/skillanswers 200 (and/mcpkeysinitialize/tools/list) withConnectorMcpPluginstill loaded. ⛔ Not "the resolver returns false for this name" — that pins the mechanism and would pass over a build where the plugin still fails to load for some other reason. The dev proved causality by removal (droppingConnectorMcpPluginmakes MCP work); the fix must make it work without dropping it.Context worth having, not in scope
This is the third MCP-surface defect this shift: #7645 (stdio transport started but deaf — merged) and #7915 (
os servewrites its banner to the stdout the stdio transport owns — filed, ungraded). ⛔ Do not fold either in. But if the measurement here turns up something that changes the reading of those, say so in the report.Generated by Claude Code
hotlong commentedon Aug 12, 2026
{ "issue": 7652, "status": "done", "branch": "claude/issue-7652-mcp-capability-substring-match", "pr": "https://github.com/objectstack-ai/objectstack/pull/7935", "premise_still_valid": true, "summary": "Verified the premise on current origin/main (3c9a67e) by text, not line number — the card's cites had drifted (auto-add 884->888, CAPABILITY_PROVIDERS.mcp 427->431, hasPluginMatching 2327->2331) but the mechanism was exactly as described. Fixed the CLASS: Serve.providesCapability now compares a loaded plugin's `name` and constructor name against the registry's declared identities by EQUALITY, and every entry declares the provider's real registered plugin id instead of a fragment of it. `nameMatch` renamed to `identities` so the semantic change breaks any caller loudly. No exclusion list for the connector, no lengthened fragment, no reordering. BOTH DIRECTIONS MEASURED, NOT ASSUMED: reading the provider packages showed most old name fragments were ALREADY DEAD — `service-cache` never matched `com.objectstack.service.cache` (dash vs dot), `plugin-email` never matched `com.objectstack.service.email`, `trigger-record-change` never matched `com.objectstack.trigger.record-change`; 18 of 23 entries were carried entirely by their class name, and only service-automation/service-analytics/audit/plugin-webhook-outbox/mcp had a live name fragment. The registry now carries the ids those packages actually register (strictly more correct than before), and a drift test imports every provider package, constructs the exported class, and asserts the name it registers is declared — so a rename fails loudly instead of quietly restoring double-loading. Two dead aliases dropped: 'mcp-server' (plugin is com.objectstack.mcp) and 'SharingPlugin' (class is SharingServicePlugin); neither matched anything.", "tests": { "acceptance_pin": "packages/cli/test/serve-mcp-capability-collision.e2e.test.ts — spawns the shipped bin/run.js, boots an app loading the consumer plugin that never declares `mcp`, asks the card's own question. NOT the resolver's return value.", "measured_before_after": { "GET /api/v1/mcp/skill": "501 -> 200", "POST /api/v1/mcp initialize": "501 -> 200 with real protocolVersion + serverInfo", "POST /api/v1/mcp tools/list": "501 -> 200 with a non-empty tool array" }, "reverse_verification": "PASSED — with the substring match restored in hasPluginMatching (fix reverted via patch, tests kept), the new e2e fails 'expected 501 to be 200' on BOTH /mcp/skill and /mcp. The card's exact symptom. Fix re-applied and re-confirmed green.", "new_files": [ "packages/cli/test/serve-capability-identity.test.ts (53 tests) — both directions: the consumer must not satisfy `mcp`; every registered provider must STILL satisfy its own capability by name and by class; near-misses substring matching used to accept are rejected; the four real connectors satisfy no capability they merely consume; plus the package-import drift guard and a source-level pin on the connector's own name.", "packages/cli/test/serve-mcp-capability-collision.e2e.test.ts (3 tests) — the acceptance pin above." ], "gates": { "packages/cli suite": "PASS — 113 files, 1247 tests", "pnpm build (turbo, 71 tasks)": "PASS", "dispatch-gates matched: pnpm check:startup-registry-verdict": "PASS", "pnpm check:nul-bytes": "PASS", "pnpm check:type-check-debt": "PASS — no `•` error line for packages/cli; NO ledger entry raised", "standalone typecheck of both new test files": "PASS — zero errors under packages/cli's strict config (its tsconfig excludes test/, so the ratchet does not measure them)", "eslint --no-inline-config on all 3 changed files": "PASS" }, "changeset": ".changeset/serve-capability-identity-match.md (@objectstack/cli patch)" }, "sweep_for_same_exposure": { "method": "All 26 fragment lists (23 CAPABILITY_PROVIDERS entries + 3 extras + 3 inline hasPluginMatching call sites) tested against a corpus of 59 plugin names and 64 plugin classes in this repo, asking which fragments were satisfiable by a plugin that is NOT the provider.", "realized_collisions": "Exactly one: 'mcp' contained in com.objectstack.connector.mcp — the reported bug.", "latent_exposure": "'audit' was the only other single-word fragment, one consumer away from the same fate (any plugin named com.*.audit-* or a class ending in AuditPlugin would have satisfied it). Every class-name fragment was exposed the same way — 'AuditPlugin' is contained in 'MyAuditPlugin' — but no in-repo class collides today.", "action": "Reported, not separately special-cased. The uniform equality fix covers all of them; nothing measured justifies widening the change further." }, "open_questions": [ "Resolved myself, not asked: the e2e declares the consumer plugin in its fixture rather than importing @objectstack/connector-mcp, because that package is NOT a dependency of @objectstack/cli — turbo's `test` dependsOn ^build would never build it, so it would be absent in CI. Importing it would have required adding a devDependency, i.e. touching packages/cli/package.json + the lockfile, outside the declared file surface. The resolver reads exactly two fields off a loaded plugin (name, constructor.name), so a plugin declaring the connector's real identity reproduces the defect with full fidelity; serve-capability-identity.test.ts additionally pins that identity against the connector's actual source so the fixture cannot drift into testing a name nobody registers. If the PM would rather the e2e import the real class, that is a one-line devDependency add.", "Resolved myself: renamed the CapabilitySpec field `nameMatch` -> `identities`. Grepped the whole repo — the field is referenced only inside serve.ts (packages/spec owns a separate PLATFORM_CAPABILITY_PROVIDERS with a different shape). A rename makes any out-of-repo consumer of the public static `Serve.CAPABILITY_PROVIDERS` (e.g. cloud's objectos-runtime) fail at compile time rather than silently inherit substring semantics — that seemed the safer failure mode, but it is a visible API change on a public static and worth a reviewer's eye." ], "out_of_scope_findings": [], "file_surface": { "declared": "packages/cli/src/commands/serve.ts + its tests", "actual": "packages/cli/src/commands/serve.ts, packages/cli/test/serve-capability-identity.test.ts (new), packages/cli/test/serve-mcp-capability-collision.e2e.test.ts (new), .changeset/serve-capability-identity-match.md (new)", "breach": "None beyond the changeset, which CLAUDE.md requires of every code PR." }, "sibling_mcp_defects": "#7645 (stdio transport deaf — merged) and #7915 (os serve banner pollutes the stdio transport's stdout — filed) were NOT folded in. Nothing measured here changes how either should be read: both are transport-layer defects occurring AFTER MCPServerPlugin loads, whereas this one prevented it from loading at all. Worth noting for the PM that the three compound — on the stock showcase the plugin never loaded, so the surfaces #7645 and #7915 describe were unreachable there regardless." }Generated by Claude Code
hotlong commentedon Aug 12, 2026
ACCEPTED — PR #7935. Landing on gate-green.
Reviewed by the
domain:cliseat (hotlong, sessionsession_01B3Kurx8qufrDzNjk4rag7V) against the diff. The ruling was fix-the-class, and that is what landed:nameMatchfragments tested withString.includes()becomeidentitiescompared by equality, and each entry declares the provider's real registered plugin id. ⛔ No exclusion list, no lengthened fragment, no load-order luck — a plugin either is the provider or it is not.The comment states the general hazard in the form that will actually help the next reader: a plugin that CONSUMES a capability is conventionally named after the capability it consumes, so substring matching cannot tell a provider from its consumer — and
mcpwas not the only exposure (auditwas one consumer away, and every class-name fragment was satisfied by any class merely ending in it, e.g.MyAuditPluginforAuditPlugin).The measurement found something worth more than the fix
Tightening the comparison could have broken legitimate providers, so the identities were measured against the provider packages instead of assumed — and that measurement found the registry was already substantially broken:
service-cachenever matchedcom.objectstack.service.cache— dash versus dot. It has presumably never matched.So the "provider already supplied?" check had been running on one working limb for an unknown length of time, and nobody could see it because class-name containment quietly covered for every dead id. That is the kind of thing only a both-directions measurement surfaces; a fix that just special-cased the connector would have left all of it in place.
Both directions are pinned, and the vacuity traps are closed
The test file states the trap explicitly — "a resolver that matches nothing would make every capability load its default provider and look green in the repro" — and pins the negative half accordingly: every registered provider must still satisfy its own capability, by name and by class, across the whole
CAPABILITY_PROVIDERStable, plus a coverage assertion so the expectation table cannot silently fall behind the registry.Near-misses that substring matching used to accept are pinned as rejections (
com.objectstack.mcp-inspector,com.acme.mcp,FakeMCPServerPlugin), as are the degenerate inputs (empty identity list, null-prototype plugin, nameless plugin).Two guards I did not ask for and would have wanted
@objectstack/connector-mcp(not aclidependency, soturbo run test's^buildnever builds it), which creates an obvious drift risk — the fixture could end up testing a name nobody registers. They closed it by readingconnector-mcp-plugin.tsand assertingname = 'com.objectstack.connector.mcp'. That is the failure mode of their own workaround, found and fenced.The acceptance is the real repro, not the mechanism
The e2e boots the shipped
bin/run.jswith the consumer plugin loaded and assertsGET /api/v1/mcp/skill→ 200 andPOST /api/v1/mcp→ real JSON-RPC results forinitializeandtools/list— the endpoints the card names, with a mintedosk_key because/mcpis authenticated. AprovidesCapability(...) === falseassertion would have stayed green on a build whereMCPServerPluginfails to load for an unrelated reason; the test file says exactly that and rejects it as the criterion. Reverse-verified: restore the substring match and/mcp/skillreturns to 501.It also carries a vacuity check — "the boot really did load the consumer plugin — otherwise this file proves nothing" — because a fixture that stopped loading the connector would pass for the wrong reason.
Scope
4 files:
packages/cli/src/commands/serve.ts, two new tests underpackages/cli/test/, and the changeset. Nodocs/adr/**, no skills paths, nocontent/docs/releases/.⛔ Not flipped yet: ESLint and TypeScript Type Check must both read⚠️ Note
conclusion: successon the head commit.packages/cli's tsconfig excludestest/, so the ratchet may not cover the two new test files — if the job trips anyway, the fix is to type them, ⛔ never to raise the ledger.Generated by Claude Code
fix(cli): capability resolver matches provider identities, not name f…