From fb3c258c5d964b89670fcd22311f0d4594ada8cb Mon Sep 17 00:00:00 2001 From: "Ragdoll-Opus-4.6" Date: Wed, 8 Jul 2026 00:55:34 +0800 Subject: [PATCH 1/2] fix: add mcpPromptMode capability to stop injecting dead MCP docs into CatAgent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CatAgent had mcpSupport=true but couldn't actually use MCP tools (no MCP client) or HTTP callbacks (no shell/curl). Both config states produced dead documentation in the prompt: - mcpSupport=true → S13 MCP_TOOLS_SECTION injected (unusable tools) - mcpSupport=false → C1 HTTP callback injected (no curl available) Add mcpPromptMode() to AgentService interface with three modes: - 'native-mcp': inject S13 docs (Claude) - 'http-callback': inject C1 callback (Codex/Gemini) - 'none': inject neither (CatAgent pre-bridge) Centralized resolveMcpPromptInjection() helper replaces scattered boolean logic in both route-serial and route-parallel. Legacy providers without the new capability fall back to existing mcpSupport && mcpServerPath logic (zero back-compat break). Closes #59 Co-Authored-By: Claude Opus 4.6 --- .../agents/invocation/McpPromptInjector.ts | 41 +++++++ .../providers/catagent/CatAgentService.ts | 9 ++ .../services/agents/routing/route-parallel.ts | 24 ++-- .../services/agents/routing/route-serial.ts | 25 ++-- .../api/src/domains/cats/services/index.ts | 7 +- .../api/src/domains/cats/services/types.ts | 16 +++ packages/api/test/mcp-prompt-injector.test.js | 109 ++++++++++++++++++ 7 files changed, 203 insertions(+), 28 deletions(-) diff --git a/packages/api/src/domains/cats/services/agents/invocation/McpPromptInjector.ts b/packages/api/src/domains/cats/services/agents/invocation/McpPromptInjector.ts index 374d947ee0..797f086377 100644 --- a/packages/api/src/domains/cats/services/agents/invocation/McpPromptInjector.ts +++ b/packages/api/src/domains/cats/services/agents/invocation/McpPromptInjector.ts @@ -9,8 +9,49 @@ * HTTP endpoints preserved as fallback only. */ +import type { CatConfig } from '@cat-cafe/shared'; +import type { AgentService } from '../../types.js'; import { renderSegment } from '../../context/prompt-template-loader.js'; +/** + * Issue #59 — centralized MCP prompt injection decision. + * + * Resolves whether to inject native MCP docs (S13 MCP_TOOLS_SECTION) or + * HTTP callback instructions (C1) based on the provider's capability. + * + * Priority: service.mcpPromptMode() > legacy boolean fallback. + */ +export interface McpPromptInjectionResult { + /** Inject MCP_TOOLS_SECTION into static identity (for Claude-style native MCP) */ + injectNativeMcpDocs: boolean; + /** Inject C1 HTTP callback instructions per-message (for Codex/Gemini fallback) */ + injectHttpCallbackDocs: boolean; +} + +export function resolveMcpPromptInjection( + service: AgentService, + catConfig: CatConfig | null | undefined, + mcpServerPath: string | undefined, +): McpPromptInjectionResult { + const mode = service.mcpPromptMode?.(); + if (mode !== undefined) { + return { + injectNativeMcpDocs: mode === 'native-mcp', + injectHttpCallbackDocs: mode === 'http-callback', + }; + } + // Legacy fallback: mcpSupport && mcpServerPath → native docs; else http callback. + // Antigravity always skips (LS persistent process can't receive callback env). + if (catConfig?.clientId === 'antigravity') { + return { injectNativeMcpDocs: false, injectHttpCallbackDocs: false }; + } + const mcpAvailable = (catConfig?.mcpSupport ?? false) && !!mcpServerPath; + return { + injectNativeMcpDocs: mcpAvailable, + injectHttpCallbackDocs: !mcpAvailable, + }; +} + export interface McpCallbackOptions { /** * Example unique handle to show in documentation snippets. diff --git a/packages/api/src/domains/cats/services/agents/providers/catagent/CatAgentService.ts b/packages/api/src/domains/cats/services/agents/providers/catagent/CatAgentService.ts index 574fb774c0..ed85783a14 100644 --- a/packages/api/src/domains/cats/services/agents/providers/catagent/CatAgentService.ts +++ b/packages/api/src/domains/cats/services/agents/providers/catagent/CatAgentService.ts @@ -137,6 +137,15 @@ export class CatAgentService implements AgentService { this.adapter = createCatAgentProtocolAdapter(options.catConfig); } + /** + * Issue #59 — CatAgent has no MCP client and no shell access for HTTP + * callbacks. Pre-bridge: suppress both S13 native MCP docs and C1 HTTP + * callback instructions so the model doesn't see tools it cannot call. + */ + mcpPromptMode(): 'native-mcp' | 'http-callback' | 'none' { + return 'none'; + } + async *invoke(prompt: string, options?: AgentServiceOptions): AsyncIterable { const now = Date.now(); let model: string; diff --git a/packages/api/src/domains/cats/services/agents/routing/route-parallel.ts b/packages/api/src/domains/cats/services/agents/routing/route-parallel.ts index 4ad65984a6..729e28c690 100644 --- a/packages/api/src/domains/cats/services/agents/routing/route-parallel.ts +++ b/packages/api/src/domains/cats/services/agents/routing/route-parallel.ts @@ -50,7 +50,7 @@ import { getVoiceBlockSynthesizer } from '../../tts/VoiceBlockSynthesizer.js'; import type { AgentMessage, AgentMessageType, MessageMetadata } from '../../types.js'; import { buildCapsuleFromRouteState } from '../invocation/CollaborationContinuityCapsule.js'; import { invokeSingleCat } from '../invocation/invoke-single-cat.js'; -import { buildMcpCallbackInstructions, needsMcpInjection } from '../invocation/McpPromptInjector.js'; +import { buildMcpCallbackInstructions, resolveMcpPromptInjection } from '../invocation/McpPromptInjector.js'; import { getRichBlockBuffer } from '../invocation/RichBlockBuffer.js'; import { mergeStreams } from '../invocation/stream-merge.js'; import { resolveDefaultClaudeMcpServerPath } from '../providers/ClaudeAgentService.js'; @@ -222,15 +222,6 @@ export async function* routeParallel( targetCats.map(async (catId) => { const catConfig: CatConfig | undefined = catRegistry.tryGet(catId as string)?.config; const teammates = targetCats.filter((id) => id !== catId); - // F203 Phase C: non-pack identity/家规/MCP docs travel via the - // compression-immune native system role (--system-prompt-file / -c) - // ONLY for providers with native L0 injection (ClaudeAgentService -p, - // ClaudeBgCarrierService, CodexAgent). Non-native providers (Gemini, - // Antigravity, CatAgent, A2A, OpenCode, Kimi…) still need full - // identity via user-message systemPrompt prepend, else they - // lose identity/家规 entirely (云端 Codex P1-cloud-1, 2026-05-16). - // mcpAvailable still gates the per-message HTTP callback fallback. - const mcpAvailable = (catConfig?.mcpSupport ?? false) && !!mcpServerPath; // F129: Load active pack blocks (best-effort) let packBlocks: import('@cat-cafe/shared').CompiledPackBlocks | null = null; if (deps.packStore) { @@ -239,13 +230,18 @@ export async function* routeParallel( } const service = getService(deps.services, catId); const hasNativeL0 = service.injectsL0Natively?.() ?? false; - // Staging is injected in invoke-single-cat independently of staticIdentity - // (Cloud R2 P1 #2237 L1099). See route-serial.ts for the architecture rationale. + // Issue #59: MCP prompt injection resolved from service.mcpPromptMode() + // capability. Falls back to legacy mcpSupport && mcpServerPath for + // providers that haven't adopted the new capability yet. + // F203 Phase C: non-pack identity/家規/MCP docs travel via the + // compression-immune native system role only for native-L0 providers. + const mcpInjection = resolveMcpPromptInjection(service, catConfig, mcpServerPath); + const mcpAvailable = mcpInjection.injectNativeMcpDocs; const staticIdentity = hasNativeL0 ? buildStaticIdentityPackOnly(catId, { packBlocks }) : buildStaticIdentity(catId, { mcpAvailable, packBlocks }); - // F041: inject HTTP callback only when MCP is NOT actually available (fallback) - const mcpInstructions = needsMcpInjection(mcpAvailable, catConfig?.clientId) + // Issue #59: HTTP callback injection from resolved mode (not boolean flip). + const mcpInstructions = mcpInjection.injectHttpCallbackDocs ? buildMcpCallbackInstructions({ currentCatId: catId as string, teammates: teammates.map((id) => id as string), diff --git a/packages/api/src/domains/cats/services/agents/routing/route-serial.ts b/packages/api/src/domains/cats/services/agents/routing/route-serial.ts index ba7087a281..e7cd8aaa77 100644 --- a/packages/api/src/domains/cats/services/agents/routing/route-serial.ts +++ b/packages/api/src/domains/cats/services/agents/routing/route-serial.ts @@ -103,7 +103,7 @@ import { getVoiceBlockSynthesizer } from '../../tts/VoiceBlockSynthesizer.js'; import type { AgentMessage, AgentMessageType, MessageMetadata } from '../../types.js'; import { buildCapsuleFromRouteState } from '../invocation/CollaborationContinuityCapsule.js'; import { invokeSingleCat } from '../invocation/invoke-single-cat.js'; -import { buildMcpCallbackInstructions, needsMcpInjection } from '../invocation/McpPromptInjector.js'; +import { buildMcpCallbackInstructions, resolveMcpPromptInjection } from '../invocation/McpPromptInjector.js'; import { getRichBlockBuffer } from '../invocation/RichBlockBuffer.js'; import { resolveDefaultClaudeMcpServerPath } from '../providers/ClaudeAgentService.js'; import { detectInlineActionMentionsWithShadow, getMaxA2ADepth, parseA2AMentions } from '../routing/a2a-mentions.js'; @@ -694,16 +694,6 @@ export async function* routeSerial( log.warn({ catId: catId as string, err: feedbackErr }, 'consumeMentionRoutingFeedback failed'); } } - // mcpAvailable still gates the per-message HTTP callback fallback below - // (needsMcpInjection). F203 Phase C: the non-pack identity/家规/MCP docs - // travel via the compression-immune native system role - // (--system-prompt-file / -c) ONLY for providers that inject L0 natively - // (ClaudeAgentService -p, ClaudeBgCarrierService, CodexAgent). Other - // providers (Gemini, Antigravity, CatAgent, A2A, OpenCode, Kimi…) - // have no native L0 channel, so they MUST still receive the full static - // identity via the user-message systemPrompt prepend — otherwise they - // lose identity/家规 entirely (云端 Codex P1-cloud-1, 2026-05-16). - const mcpAvailable = (catConfig?.mcpSupport ?? false) && !!mcpServerPath; // F129: Load active pack blocks (best-effort, failure does not block invocation) let packBlocks: import('@cat-cafe/shared').CompiledPackBlocks | null = null; if (deps.packStore) { @@ -713,6 +703,15 @@ export async function* routeSerial( const service = getService(deps.services, catId); const needsServerRoutingGuard = service.needsServerRoutingGuard?.() ?? false; const hasNativeL0 = service.injectsL0Natively?.() ?? false; + // Issue #59: MCP prompt injection resolved from service.mcpPromptMode() + // capability. Falls back to legacy mcpSupport && mcpServerPath for + // providers that haven't adopted the new capability yet. + // F203 Phase C: non-pack identity/家規/MCP docs travel via the + // compression-immune native system role (--system-prompt-file / -c) + // ONLY for providers that inject L0 natively. Others receive full + // static identity via user-message systemPrompt prepend. + const mcpInjection = resolveMcpPromptInjection(service, catConfig, mcpServerPath); + const mcpAvailable = mcpInjection.injectNativeMcpDocs; const staticIdentity = hasNativeL0 ? buildStaticIdentityPackOnly(catId, { packBlocks }) : buildStaticIdentity(catId, { mcpAvailable, packBlocks }); @@ -722,8 +721,8 @@ export async function* routeSerial( // session-chain turns, because invoke-single-cat skips systemPrompt // injection on those resumes. Staging is now injected in invoke-single-cat // independently (mirrors F225 contextHintPrefix pattern). - // F041: inject HTTP callback only when MCP is NOT actually available (fallback) - const mcpInstructions = needsMcpInjection(mcpAvailable, catConfig?.clientId) + // Issue #59: HTTP callback injection from resolved mode (not boolean flip). + const mcpInstructions = mcpInjection.injectHttpCallbackDocs ? buildMcpCallbackInstructions({ currentCatId: catId as string, teammates: teammates.map((id) => id as string), diff --git a/packages/api/src/domains/cats/services/index.ts b/packages/api/src/domains/cats/services/index.ts index 6b36e5f1f5..851c80cac1 100644 --- a/packages/api/src/domains/cats/services/index.ts +++ b/packages/api/src/domains/cats/services/index.ts @@ -7,7 +7,12 @@ export { InvocationRegistry } from './agents/invocation/InvocationRegistry.js'; export { InvocationTracker } from './agents/invocation/InvocationTracker.js'; export type { InvocationDeps, InvocationParams } from './agents/invocation/invoke-single-cat.js'; export { invokeSingleCat } from './agents/invocation/invoke-single-cat.js'; -export { buildMcpCallbackInstructions, needsMcpInjection } from './agents/invocation/McpPromptInjector.js'; +export type { McpPromptInjectionResult } from './agents/invocation/McpPromptInjector.js'; +export { + buildMcpCallbackInstructions, + needsMcpInjection, + resolveMcpPromptInjection, +} from './agents/invocation/McpPromptInjector.js'; export { ClaudeAgentService } from './agents/providers/ClaudeAgentService.js'; export { CodexAgentService } from './agents/providers/CodexAgentService.js'; export { GeminiAgentService } from './agents/providers/GeminiAgentService.js'; diff --git a/packages/api/src/domains/cats/services/types.ts b/packages/api/src/domains/cats/services/types.ts index 2367f5e49c..7d0d80ca9a 100644 --- a/packages/api/src/domains/cats/services/types.ts +++ b/packages/api/src/domains/cats/services/types.ts @@ -362,6 +362,22 @@ export interface AgentService { * covered by the Stop hook). */ needsServerRoutingGuard?(): boolean; + + /** + * Issue #59 — what kind of MCP prompt injection this provider supports. + * + * - `'native-mcp'`: Provider has a native MCP client (e.g. Claude CLI + * `--mcp-config`). Inject MCP_TOOLS_SECTION into static identity. + * - `'http-callback'`: Provider can use HTTP callbacks via shell/curl + * (e.g. Codex, Gemini). Inject C1 HTTP callback instructions. + * - `'none'`: Provider cannot consume MCP tools or HTTP callbacks + * (e.g. CatAgent pre-bridge — no MCP client, no shell access). + * Neither S13 nor C1 is injected. + * + * Optional — when absent, falls back to the legacy boolean + * `mcpSupport && !!mcpServerPath` logic for back-compat. + */ + mcpPromptMode?(): 'native-mcp' | 'http-callback' | 'none'; } /** diff --git a/packages/api/test/mcp-prompt-injector.test.js b/packages/api/test/mcp-prompt-injector.test.js index 874a02d15b..91c02db72b 100644 --- a/packages/api/test/mcp-prompt-injector.test.js +++ b/packages/api/test/mcp-prompt-injector.test.js @@ -117,3 +117,112 @@ describe('McpPromptInjector', () => { assert.ok(instructions.includes('@codex'), 'should provide a routable handle example'); }); }); + +describe('resolveMcpPromptInjection (Issue #59)', () => { + /** @param {string} mode */ + function mockService(mode) { + return /** @type {import('../dist/domains/cats/services/types.js').AgentService} */ ({ + invoke: async function* () {}, + mcpPromptMode: () => mode, + }); + } + + /** Service without mcpPromptMode (legacy path) */ + function legacyService() { + return /** @type {import('../dist/domains/cats/services/types.js').AgentService} */ ({ + invoke: async function* () {}, + }); + } + + it("CatAgent mcpPromptMode='none' → neither S13 nor C1 injected", async () => { + const { resolveMcpPromptInjection } = await import( + '../dist/domains/cats/services/agents/invocation/McpPromptInjector.js' + ); + const result = resolveMcpPromptInjection( + mockService('none'), + { mcpSupport: true }, + '/some/mcp/server/path', + ); + assert.equal(result.injectNativeMcpDocs, false, 'should NOT inject S13 native MCP docs'); + assert.equal(result.injectHttpCallbackDocs, false, 'should NOT inject C1 HTTP callback'); + }); + + it("Claude mcpPromptMode='native-mcp' → S13 injected, C1 not", async () => { + const { resolveMcpPromptInjection } = await import( + '../dist/domains/cats/services/agents/invocation/McpPromptInjector.js' + ); + const result = resolveMcpPromptInjection( + mockService('native-mcp'), + { mcpSupport: true }, + '/some/mcp/server/path', + ); + assert.equal(result.injectNativeMcpDocs, true, 'should inject S13 native MCP docs'); + assert.equal(result.injectHttpCallbackDocs, false, 'should NOT inject C1'); + }); + + it("Codex mcpPromptMode='http-callback' → C1 injected, S13 not", async () => { + const { resolveMcpPromptInjection } = await import( + '../dist/domains/cats/services/agents/invocation/McpPromptInjector.js' + ); + const result = resolveMcpPromptInjection( + mockService('http-callback'), + { mcpSupport: false }, + undefined, + ); + assert.equal(result.injectNativeMcpDocs, false, 'should NOT inject S13'); + assert.equal(result.injectHttpCallbackDocs, true, 'should inject C1 HTTP callback'); + }); + + it('legacy fallback: mcpSupport=true + mcpServerPath → S13 (back-compat)', async () => { + const { resolveMcpPromptInjection } = await import( + '../dist/domains/cats/services/agents/invocation/McpPromptInjector.js' + ); + const result = resolveMcpPromptInjection( + legacyService(), + { mcpSupport: true }, + '/some/mcp/server/path', + ); + assert.equal(result.injectNativeMcpDocs, true, 'legacy: mcpAvailable=true → S13'); + assert.equal(result.injectHttpCallbackDocs, false, 'legacy: mcpAvailable=true → no C1'); + }); + + it('legacy fallback: mcpSupport=false → C1 (back-compat)', async () => { + const { resolveMcpPromptInjection } = await import( + '../dist/domains/cats/services/agents/invocation/McpPromptInjector.js' + ); + const result = resolveMcpPromptInjection( + legacyService(), + { mcpSupport: false }, + '/some/mcp/server/path', + ); + assert.equal(result.injectNativeMcpDocs, false, 'legacy: mcpAvailable=false → no S13'); + assert.equal(result.injectHttpCallbackDocs, true, 'legacy: mcpAvailable=false → C1'); + }); + + it('legacy fallback: antigravity → neither (special case preserved)', async () => { + const { resolveMcpPromptInjection } = await import( + '../dist/domains/cats/services/agents/invocation/McpPromptInjector.js' + ); + const result = resolveMcpPromptInjection( + legacyService(), + { clientId: 'antigravity', mcpSupport: false }, + undefined, + ); + assert.equal(result.injectNativeMcpDocs, false, 'antigravity: no S13'); + assert.equal(result.injectHttpCallbackDocs, false, 'antigravity: no C1'); + }); + + it("mcpPromptMode='none' overrides even when mcpSupport=true (capability > config)", async () => { + const { resolveMcpPromptInjection } = await import( + '../dist/domains/cats/services/agents/invocation/McpPromptInjector.js' + ); + // CatAgent scenario: config says mcpSupport=true but service knows better + const result = resolveMcpPromptInjection( + mockService('none'), + { mcpSupport: true, clientId: 'catagent' }, + '/some/mcp/server/path', + ); + assert.equal(result.injectNativeMcpDocs, false, 'capability should override config'); + assert.equal(result.injectHttpCallbackDocs, false, 'capability should override config'); + }); +}); From 1f772d68b5ac11c844dd7d75d3b788ce451b135d Mon Sep 17 00:00:00 2001 From: "Ragdoll-Opus-4.6" Date: Wed, 8 Jul 2026 01:00:25 +0800 Subject: [PATCH 2/2] chore: biome format fix for mcp-prompt-injector tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address codex review P2 — new test block formatter compliance. Co-Authored-By: Claude Opus 4.6 --- .../agents/invocation/McpPromptInjector.ts | 2 +- packages/api/test/mcp-prompt-injector.test.js | 30 ++++--------------- 2 files changed, 6 insertions(+), 26 deletions(-) diff --git a/packages/api/src/domains/cats/services/agents/invocation/McpPromptInjector.ts b/packages/api/src/domains/cats/services/agents/invocation/McpPromptInjector.ts index 797f086377..0f5cd4f018 100644 --- a/packages/api/src/domains/cats/services/agents/invocation/McpPromptInjector.ts +++ b/packages/api/src/domains/cats/services/agents/invocation/McpPromptInjector.ts @@ -10,8 +10,8 @@ */ import type { CatConfig } from '@cat-cafe/shared'; -import type { AgentService } from '../../types.js'; import { renderSegment } from '../../context/prompt-template-loader.js'; +import type { AgentService } from '../../types.js'; /** * Issue #59 — centralized MCP prompt injection decision. diff --git a/packages/api/test/mcp-prompt-injector.test.js b/packages/api/test/mcp-prompt-injector.test.js index 91c02db72b..8c4898cce7 100644 --- a/packages/api/test/mcp-prompt-injector.test.js +++ b/packages/api/test/mcp-prompt-injector.test.js @@ -138,11 +138,7 @@ describe('resolveMcpPromptInjection (Issue #59)', () => { const { resolveMcpPromptInjection } = await import( '../dist/domains/cats/services/agents/invocation/McpPromptInjector.js' ); - const result = resolveMcpPromptInjection( - mockService('none'), - { mcpSupport: true }, - '/some/mcp/server/path', - ); + const result = resolveMcpPromptInjection(mockService('none'), { mcpSupport: true }, '/some/mcp/server/path'); assert.equal(result.injectNativeMcpDocs, false, 'should NOT inject S13 native MCP docs'); assert.equal(result.injectHttpCallbackDocs, false, 'should NOT inject C1 HTTP callback'); }); @@ -151,11 +147,7 @@ describe('resolveMcpPromptInjection (Issue #59)', () => { const { resolveMcpPromptInjection } = await import( '../dist/domains/cats/services/agents/invocation/McpPromptInjector.js' ); - const result = resolveMcpPromptInjection( - mockService('native-mcp'), - { mcpSupport: true }, - '/some/mcp/server/path', - ); + const result = resolveMcpPromptInjection(mockService('native-mcp'), { mcpSupport: true }, '/some/mcp/server/path'); assert.equal(result.injectNativeMcpDocs, true, 'should inject S13 native MCP docs'); assert.equal(result.injectHttpCallbackDocs, false, 'should NOT inject C1'); }); @@ -164,11 +156,7 @@ describe('resolveMcpPromptInjection (Issue #59)', () => { const { resolveMcpPromptInjection } = await import( '../dist/domains/cats/services/agents/invocation/McpPromptInjector.js' ); - const result = resolveMcpPromptInjection( - mockService('http-callback'), - { mcpSupport: false }, - undefined, - ); + const result = resolveMcpPromptInjection(mockService('http-callback'), { mcpSupport: false }, undefined); assert.equal(result.injectNativeMcpDocs, false, 'should NOT inject S13'); assert.equal(result.injectHttpCallbackDocs, true, 'should inject C1 HTTP callback'); }); @@ -177,11 +165,7 @@ describe('resolveMcpPromptInjection (Issue #59)', () => { const { resolveMcpPromptInjection } = await import( '../dist/domains/cats/services/agents/invocation/McpPromptInjector.js' ); - const result = resolveMcpPromptInjection( - legacyService(), - { mcpSupport: true }, - '/some/mcp/server/path', - ); + const result = resolveMcpPromptInjection(legacyService(), { mcpSupport: true }, '/some/mcp/server/path'); assert.equal(result.injectNativeMcpDocs, true, 'legacy: mcpAvailable=true → S13'); assert.equal(result.injectHttpCallbackDocs, false, 'legacy: mcpAvailable=true → no C1'); }); @@ -190,11 +174,7 @@ describe('resolveMcpPromptInjection (Issue #59)', () => { const { resolveMcpPromptInjection } = await import( '../dist/domains/cats/services/agents/invocation/McpPromptInjector.js' ); - const result = resolveMcpPromptInjection( - legacyService(), - { mcpSupport: false }, - '/some/mcp/server/path', - ); + const result = resolveMcpPromptInjection(legacyService(), { mcpSupport: false }, '/some/mcp/server/path'); assert.equal(result.injectNativeMcpDocs, false, 'legacy: mcpAvailable=false → no S13'); assert.equal(result.injectHttpCallbackDocs, true, 'legacy: mcpAvailable=false → C1'); });