Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
20 kB · 634 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635import { join } from 'path'import { expandEnvVarsInString } from '../../services/mcp/envExpansion.js'import { type McpServerConfig, McpServerConfigSchema, type ScopedMcpServerConfig,} from '../../services/mcp/types.js'import type { LoadedPlugin, PluginError } from '../../types/plugin.js'import { logForDebugging } from '../debug.js'import { errorMessage, isENOENT } from '../errors.js'import { getFsImplementation } from '../fsOperations.js'import { jsonParse } from '../slowOperations.js'import { isMcpbSource, loadMcpbFile, loadMcpServerUserConfig, type McpbLoadResult, type UserConfigSchema, type UserConfigValues, validateUserConfig,} from './mcpbHandler.js'import { getPluginDataDir } from './pluginDirectories.js'import { getPluginStorageId, loadPluginOptions, substitutePluginVariables, substituteUserConfigVariables,} from './pluginOptionsStorage.js'
/** * Load MCP servers from an MCPB file * Handles downloading, extracting, and converting DXT manifest to MCP config */async function loadMcpServersFromMcpb( plugin: LoadedPlugin, mcpbPath: string, errors: PluginError[],): Promise<Record<string, McpServerConfig> | null> { try { logForDebugging(`Loading MCP servers from MCPB: ${mcpbPath}`)
// Use plugin.repository directly - it's already in "plugin@marketplace" format const pluginId = plugin.repository
const result = await loadMcpbFile( mcpbPath, plugin.path, pluginId, status => { logForDebugging(`MCPB [${plugin.name}]: ${status}`) }, )
// Check if MCPB needs user configuration if ('status' in result && result.status === 'needs-config') { // User config needed - this is normal for unconfigured plugins // Don't load the MCP server yet - user can configure via /plugin menu logForDebugging( `MCPB ${mcpbPath} requires user configuration. ` + `User can configure via: /plugin → Manage plugins → ${plugin.name} → Configure`, ) // Return null to skip this server for now (not an error) return null }
// Type guard passed - result is success type const successResult = result as McpbLoadResult
// Use the DXT manifest name as the server name const serverName = successResult.manifest.name
// Check for server name conflicts with existing servers // This will be checked later when merging all servers, but we log here for debugging logForDebugging( `Loaded MCP server "${serverName}" from MCPB (extracted to ${successResult.extractedPath})`, )
return { [serverName]: successResult.mcpConfig } } catch (error) { const errorMsg = errorMessage(error) logForDebugging(`Failed to load MCPB ${mcpbPath}: ${errorMsg}`, { level: 'error', })
// Use plugin@repository as source (consistent with other plugin errors) const source = `${plugin.name}@${plugin.repository}`
// Determine error type based on error message const isUrl = mcpbPath.startsWith('http') if ( isUrl && (errorMsg.includes('download') || errorMsg.includes('network')) ) { errors.push({ type: 'mcpb-download-failed', source, plugin: plugin.name, url: mcpbPath, reason: errorMsg, }) } else if ( errorMsg.includes('manifest') || errorMsg.includes('user configuration') ) { errors.push({ type: 'mcpb-invalid-manifest', source, plugin: plugin.name, mcpbPath, validationError: errorMsg, }) } else { errors.push({ type: 'mcpb-extract-failed', source, plugin: plugin.name, mcpbPath, reason: errorMsg, }) }
return null }}
/** * Load MCP servers from a plugin's manifest * This function loads MCP server configurations from various sources within the plugin * including manifest entries, .mcp.json files, and .mcpb files */export async function loadPluginMcpServers( plugin: LoadedPlugin, errors: PluginError[] = [],): Promise<Record<string, McpServerConfig> | undefined> { let servers: Record<string, McpServerConfig> = {}
// Check for .mcp.json in plugin directory first (lowest priority) const defaultMcpServers = await loadMcpServersFromFile( plugin.path, '.mcp.json', ) if (defaultMcpServers) { servers = { ...servers, ...defaultMcpServers } }
// Handle manifest mcpServers if present (higher priority) if (plugin.manifest.mcpServers) { const mcpServersSpec = plugin.manifest.mcpServers
// Handle different mcpServers formats if (typeof mcpServersSpec === 'string') { // Check if it's an MCPB file if (isMcpbSource(mcpServersSpec)) { const mcpbServers = await loadMcpServersFromMcpb( plugin, mcpServersSpec, errors, ) if (mcpbServers) { servers = { ...servers, ...mcpbServers } } } else { // Path to JSON file const mcpServers = await loadMcpServersFromFile( plugin.path, mcpServersSpec, ) if (mcpServers) { servers = { ...servers, ...mcpServers } } } } else if (Array.isArray(mcpServersSpec)) { // Array of paths or inline configs. // Load all specs in parallel, then merge in original order so // last-wins collision semantics are preserved. const results = await Promise.all( mcpServersSpec.map(async spec => { try { if (typeof spec === 'string') { // Check if it's an MCPB file if (isMcpbSource(spec)) { return await loadMcpServersFromMcpb(plugin, spec, errors) } // Path to JSON file return await loadMcpServersFromFile(plugin.path, spec) } // Inline MCP server configs (sync) return spec } catch (e) { // Defensive: if one spec throws, don't lose results from the // others. The previous serial loop implicitly tolerated this. logForDebugging( `Failed to load MCP servers from spec for plugin ${plugin.name}: ${e}`, { level: 'error' }, ) return null } }), ) for (const result of results) { if (result) { servers = { ...servers, ...result } } } } else { // Direct MCP server configs servers = { ...servers, ...mcpServersSpec } } }
return Object.keys(servers).length > 0 ? servers : undefined}
/** * Load MCP servers from a JSON file within a plugin * This is a simplified version that doesn't expand environment variables * and is specifically for plugin MCP configs */async function loadMcpServersFromFile( pluginPath: string, relativePath: string,): Promise<Record<string, McpServerConfig> | null> { const fs = getFsImplementation() const filePath = join(pluginPath, relativePath)
let content: string try { content = await fs.readFile(filePath, { encoding: 'utf-8' }) } catch (e: unknown) { if (isENOENT(e)) { return null } logForDebugging(`Failed to load MCP servers from ${filePath}: ${e}`, { level: 'error', }) return null }
try { const parsed = jsonParse(content)
// Check if it's in the .mcp.json format with mcpServers key const mcpServers = parsed.mcpServers || parsed
// Validate each server config const validatedServers: Record<string, McpServerConfig> = {} for (const [name, config] of Object.entries(mcpServers)) { const result = McpServerConfigSchema().safeParse(config) if (result.success) { validatedServers[name] = result.data } else { logForDebugging( `Invalid MCP server config for ${name} in ${filePath}: ${result.error.message}`, { level: 'error' }, ) } }
return validatedServers } catch (error) { logForDebugging(`Failed to load MCP servers from ${filePath}: ${error}`, { level: 'error', }) return null }}
/** * A channel entry from a plugin's manifest whose userConfig has not yet been * filled in (required fields are missing from saved settings). */export type UnconfiguredChannel = { server: string displayName: string configSchema: UserConfigSchema}
/** * Find channel entries in a plugin's manifest whose required userConfig * fields are not yet saved. Pure function — no React, no prompting. * ManagePlugins.tsx calls this after a plugin is enabled to decide whether * to show the config dialog. * * Entries without a `userConfig` schema are skipped (nothing to prompt for). * Entries whose saved config already satisfies `validateUserConfig` are * skipped. The `configSchema` in the return value is structurally a * `UserConfigSchema` because the Zod schema in schemas.ts matches * `McpbUserConfigurationOption` field-for-field. */export function getUnconfiguredChannels( plugin: LoadedPlugin,): UnconfiguredChannel[] { const channels = plugin.manifest.channels if (!channels || channels.length === 0) { return [] }
// plugin.repository is already in "plugin@marketplace" format — same key // loadMcpServerUserConfig / saveMcpServerUserConfig use. const pluginId = plugin.repository
const unconfigured: UnconfiguredChannel[] = [] for (const channel of channels) { if (!channel.userConfig || Object.keys(channel.userConfig).length === 0) { continue } const saved = loadMcpServerUserConfig(pluginId, channel.server) ?? {} const validation = validateUserConfig(saved, channel.userConfig) if (!validation.valid) { unconfigured.push({ server: channel.server, displayName: channel.displayName ?? channel.server, configSchema: channel.userConfig, }) } } return unconfigured}
/** * Look up saved user config for a server, if this server is declared as a * channel in the plugin's manifest. Returns undefined for non-channel servers * or channels without a userConfig schema — resolvePluginMcpEnvironment will * then skip ${user_config.X} substitution for that server. */function loadChannelUserConfig( plugin: LoadedPlugin, serverName: string,): UserConfigValues | undefined { const channel = plugin.manifest.channels?.find(c => c.server === serverName) if (!channel?.userConfig) { return undefined } return loadMcpServerUserConfig(plugin.repository, serverName) ?? undefined}
/** * Add plugin scope to MCP server configs * This adds a prefix to server names to avoid conflicts between plugins */export function addPluginScopeToServers( servers: Record<string, McpServerConfig>, pluginName: string, pluginSource: string,): Record<string, ScopedMcpServerConfig> { const scopedServers: Record<string, ScopedMcpServerConfig> = {}
for (const [name, config] of Object.entries(servers)) { // Add plugin prefix to server name to avoid conflicts const scopedName = `plugin:${pluginName}:${name}` const scoped: ScopedMcpServerConfig = { ...config, scope: 'dynamic', // Use dynamic scope for plugin servers pluginSource, } scopedServers[scopedName] = scoped }
return scopedServers}
/** * Extract all MCP servers from loaded plugins * NOTE: Resolves environment variables for all servers before returning */export async function extractMcpServersFromPlugins( plugins: LoadedPlugin[], errors: PluginError[] = [],): Promise<Record<string, ScopedMcpServerConfig>> { const allServers: Record<string, ScopedMcpServerConfig> = {}
const scopedResults = await Promise.all( plugins.map(async plugin => { if (!plugin.enabled) return null
const servers = await loadPluginMcpServers(plugin, errors) if (!servers) return null
// Resolve environment variables before scoping. When a saved channel // config is missing a key (plugin update added a required field, or a // hand-edited settings.json), substituteUserConfigVariables throws // inside resolvePluginMcpEnvironment — catch per-server so one bad // config doesn't crash the whole plugin load via Promise.all. const resolvedServers: Record<string, McpServerConfig> = {} for (const [name, config] of Object.entries(servers)) { const userConfig = buildMcpUserConfig(plugin, name) try { resolvedServers[name] = resolvePluginMcpEnvironment( config, plugin, userConfig, errors, plugin.name, name, ) } catch (err) { errors?.push({ type: 'generic-error', source: name, plugin: plugin.name, error: errorMessage(err), }) } }
// Store the UNRESOLVED servers on the plugin for caching // (Environment variables will be resolved fresh each time they're needed) plugin.mcpServers = servers
logForDebugging( `Loaded ${Object.keys(servers).length} MCP servers from plugin ${plugin.name}`, )
return addPluginScopeToServers( resolvedServers, plugin.name, plugin.source, ) }), )
for (const scopedServers of scopedResults) { if (scopedServers) { Object.assign(allServers, scopedServers) } }
return allServers}
/** * Build the userConfig map for a single MCP server by merging the plugin's * top-level manifest.userConfig values with the channel-specific per-server * config (assistant-mode channels). Channel-specific wins on collision so * plugins that declare the same key at both levels get the more specific value. * * Returns undefined when neither source has anything — resolvePluginMcpEnvironment * skips substituteUserConfigVariables in that case. */function buildMcpUserConfig( plugin: LoadedPlugin, serverName: string,): UserConfigValues | undefined { // Gate on manifest.userConfig. loadPluginOptions always returns at least {} // (it spreads two `?? {}` fallbacks), so without this guard topLevel is never // undefined — the `!topLevel` check below is dead, we return {} for // unconfigured plugins, and resolvePluginMcpEnvironment runs // substituteUserConfigVariables against an empty map → throws on any // ${user_config.X} ref. The manifest check also skips the unconditional // keychain read (~50-100ms on macOS) for plugins that don't use options. const topLevel = plugin.manifest.userConfig ? loadPluginOptions(getPluginStorageId(plugin)) : undefined const channelSpecific = loadChannelUserConfig(plugin, serverName)
if (!topLevel && !channelSpecific) return undefined return { ...topLevel, ...channelSpecific }}
/** * Resolve environment variables for plugin MCP servers * Handles ${CLAUDE_PLUGIN_ROOT}, ${user_config.X}, and general ${VAR} substitution * Tracks missing environment variables for error reporting */export function resolvePluginMcpEnvironment( config: McpServerConfig, plugin: { path: string; source: string }, userConfig?: UserConfigValues, errors?: PluginError[], pluginName?: string, serverName?: string,): McpServerConfig { const allMissingVars: string[] = []
const resolveValue = (value: string): string => { // First substitute plugin-specific variables let resolved = substitutePluginVariables(value, plugin)
// Then substitute user config variables if provided if (userConfig) { resolved = substituteUserConfigVariables(resolved, userConfig) }
// Finally expand general environment variables // This is done last so plugin-specific and user config vars take precedence const { expanded, missingVars } = expandEnvVarsInString(resolved) allMissingVars.push(...missingVars)
return expanded }
let resolved: McpServerConfig
// Handle different server types switch (config.type) { case undefined: case 'stdio': { const stdioConfig = { ...config }
// Resolve command path if (stdioConfig.command) { stdioConfig.command = resolveValue(stdioConfig.command) }
// Resolve args if (stdioConfig.args) { stdioConfig.args = stdioConfig.args.map(arg => resolveValue(arg)) }
// Resolve environment variables and add CLAUDE_PLUGIN_ROOT / CLAUDE_PLUGIN_DATA const resolvedEnv: Record<string, string> = { CLAUDE_PLUGIN_ROOT: plugin.path, CLAUDE_PLUGIN_DATA: getPluginDataDir(plugin.source), ...(stdioConfig.env || {}), } for (const [key, value] of Object.entries(resolvedEnv)) { if (key !== 'CLAUDE_PLUGIN_ROOT' && key !== 'CLAUDE_PLUGIN_DATA') { resolvedEnv[key] = resolveValue(value) } } stdioConfig.env = resolvedEnv
resolved = stdioConfig break }
case 'sse': case 'http': case 'ws': { const remoteConfig = { ...config }
// Resolve URL if (remoteConfig.url) { remoteConfig.url = resolveValue(remoteConfig.url) }
// Resolve headers if (remoteConfig.headers) { const resolvedHeaders: Record<string, string> = {} for (const [key, value] of Object.entries(remoteConfig.headers)) { resolvedHeaders[key] = resolveValue(value) } remoteConfig.headers = resolvedHeaders }
resolved = remoteConfig break }
// For other types (sse-ide, ws-ide, sdk, claudeai-proxy), pass through unchanged case 'sse-ide': case 'ws-ide': case 'sdk': case 'claudeai-proxy': resolved = config break }
// Log and track missing variables if any were found and errors array provided if (errors && allMissingVars.length > 0) { const uniqueMissingVars = [...new Set(allMissingVars)] const varList = uniqueMissingVars.join(', ')
logForDebugging( `Missing environment variables in plugin MCP config: ${varList}`, { level: 'warn' }, )
// Add error to the errors array if plugin and server names are provided if (pluginName && serverName) { errors.push({ type: 'mcp-config-invalid', source: `plugin:${pluginName}`, plugin: pluginName, serverName, validationError: `Missing environment variables: ${varList}`, }) } }
return resolved}
/** * Get MCP servers from a specific plugin with environment variable resolution and scoping * This function is called when the MCP servers need to be activated and ensures they have * the proper environment variables and scope applied */export async function getPluginMcpServers( plugin: LoadedPlugin, errors: PluginError[] = [],): Promise<Record<string, ScopedMcpServerConfig> | undefined> { if (!plugin.enabled) { return undefined }
// Use cached servers if available const servers = plugin.mcpServers || (await loadPluginMcpServers(plugin, errors)) if (!servers) { return undefined }
// Resolve environment variables. Same per-server try/catch as // extractMcpServersFromPlugins above: a partial saved channel config // (plugin update added a required field) would make // substituteUserConfigVariables throw inside resolvePluginMcpEnvironment, // and this function runs inside Promise.all at config.ts:911 — one // uncaught throw crashes all plugin MCP loading. const resolvedServers: Record<string, McpServerConfig> = {} for (const [name, config] of Object.entries(servers)) { const userConfig = buildMcpUserConfig(plugin, name) try { resolvedServers[name] = resolvePluginMcpEnvironment( config, plugin, userConfig, errors, plugin.name, name, ) } catch (err) { errors?.push({ type: 'generic-error', source: name, plugin: plugin.name, error: errorMessage(err), }) } }
// Add plugin scope return addPluginScopeToServers(resolvedServers, plugin.name, plugin.source)}