source dump of claude code
at main 130 lines 4.8 kB view raw
1import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' 2import { logEvent } from '../services/analytics/index.js' 3import type { 4 ConnectedMCPServer, 5 MCPServerConnection, 6} from '../services/mcp/types.js' 7import type { Message } from '../types/message.js' 8import { isEnvDefinedFalsy, isEnvTruthy } from './envUtils.js' 9 10export type McpInstructionsDelta = { 11 /** Server names — for stateless-scan reconstruction. */ 12 addedNames: string[] 13 /** Rendered "## {name}\n{instructions}" blocks for addedNames. */ 14 addedBlocks: string[] 15 removedNames: string[] 16} 17 18/** 19 * Client-authored instruction block to announce when a server connects, 20 * in addition to (or instead of) the server's own `InitializeResult.instructions`. 21 * Lets first-party servers (e.g., claude-in-chrome) carry client-side 22 * context the server itself doesn't know about. 23 */ 24export type ClientSideInstruction = { 25 serverName: string 26 block: string 27} 28 29/** 30 * True → announce MCP server instructions via persisted delta attachments. 31 * False → prompts.ts keeps its DANGEROUS_uncachedSystemPromptSection 32 * (rebuilt every turn; cache-busts on late connect). 33 * 34 * Env override for local testing: CLAUDE_CODE_MCP_INSTR_DELTA=true/false 35 * wins over both ant bypass and the GrowthBook gate. 36 */ 37export function isMcpInstructionsDeltaEnabled(): boolean { 38 if (isEnvTruthy(process.env.CLAUDE_CODE_MCP_INSTR_DELTA)) return true 39 if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_MCP_INSTR_DELTA)) return false 40 return ( 41 process.env.USER_TYPE === 'ant' || 42 getFeatureValue_CACHED_MAY_BE_STALE('tengu_basalt_3kr', false) 43 ) 44} 45 46/** 47 * Diff the current set of connected MCP servers that have instructions 48 * (server-authored via InitializeResult, or client-side synthesized) 49 * against what's already been announced in this conversation. Null if 50 * nothing changed. 51 * 52 * Instructions are immutable for the life of a connection (set once at 53 * handshake), so the scan diffs on server NAME, not on content. 54 */ 55export function getMcpInstructionsDelta( 56 mcpClients: MCPServerConnection[], 57 messages: Message[], 58 clientSideInstructions: ClientSideInstruction[], 59): McpInstructionsDelta | null { 60 const announced = new Set<string>() 61 let attachmentCount = 0 62 let midCount = 0 63 for (const msg of messages) { 64 if (msg.type !== 'attachment') continue 65 attachmentCount++ 66 if (msg.attachment.type !== 'mcp_instructions_delta') continue 67 midCount++ 68 for (const n of msg.attachment.addedNames) announced.add(n) 69 for (const n of msg.attachment.removedNames) announced.delete(n) 70 } 71 72 const connected = mcpClients.filter( 73 (c): c is ConnectedMCPServer => c.type === 'connected', 74 ) 75 const connectedNames = new Set(connected.map(c => c.name)) 76 77 // Servers with instructions to announce (either channel). A server can 78 // have both: server-authored instructions + a client-side block appended. 79 const blocks = new Map<string, string>() 80 for (const c of connected) { 81 if (c.instructions) blocks.set(c.name, `## ${c.name}\n${c.instructions}`) 82 } 83 for (const ci of clientSideInstructions) { 84 if (!connectedNames.has(ci.serverName)) continue 85 const existing = blocks.get(ci.serverName) 86 blocks.set( 87 ci.serverName, 88 existing 89 ? `${existing}\n\n${ci.block}` 90 : `## ${ci.serverName}\n${ci.block}`, 91 ) 92 } 93 94 const added: Array<{ name: string; block: string }> = [] 95 for (const [name, block] of blocks) { 96 if (!announced.has(name)) added.push({ name, block }) 97 } 98 99 // A previously-announced server that is no longer connected → removed. 100 // There is no "announced but now has no instructions" case for a still- 101 // connected server: InitializeResult is immutable, and client-side 102 // instruction gates are session-stable in practice. (/model can flip 103 // the model gate, but deferred_tools_delta has the same property and 104 // we treat history as historical — no retroactive retractions.) 105 const removed: string[] = [] 106 for (const n of announced) { 107 if (!connectedNames.has(n)) removed.push(n) 108 } 109 110 if (added.length === 0 && removed.length === 0) return null 111 112 // Same diagnostic fields as tengu_deferred_tools_pool_change — same 113 // scan-fails-in-prod bug, same attachment persistence path. 114 logEvent('tengu_mcp_instructions_pool_change', { 115 addedCount: added.length, 116 removedCount: removed.length, 117 priorAnnouncedCount: announced.size, 118 clientSideCount: clientSideInstructions.length, 119 messagesLength: messages.length, 120 attachmentCount, 121 midCount, 122 }) 123 124 added.sort((a, b) => a.name.localeCompare(b.name)) 125 return { 126 addedNames: added.map(a => a.name), 127 addedBlocks: added.map(a => a.block), 128 removedNames: removed.sort(), 129 } 130}