* 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
( plugin: LoadedPlugin, serverName: string, )
| 438 | * skips substituteUserConfigVariables in that case. |
| 439 | */ |
| 440 | function buildMcpUserConfig( |
| 441 | plugin: LoadedPlugin, |
| 442 | serverName: string, |
| 443 | ): UserConfigValues | undefined { |
| 444 | // Gate on manifest.userConfig. loadPluginOptions always returns at least {} |
| 445 | // (it spreads two `?? {}` fallbacks), so without this guard topLevel is never |
| 446 | // undefined — the `!topLevel` check below is dead, we return {} for |
| 447 | // unconfigured plugins, and resolvePluginMcpEnvironment runs |
| 448 | // substituteUserConfigVariables against an empty map → throws on any |
| 449 | // ${user_config.X} ref. The manifest check also skips the unconditional |
| 450 | // keychain read (~50-100ms on macOS) for plugins that don't use options. |
| 451 | const topLevel = plugin.manifest.userConfig |
| 452 | ? loadPluginOptions(getPluginStorageId(plugin)) |
| 453 | : undefined |
| 454 | const channelSpecific = loadChannelUserConfig(plugin, serverName) |
| 455 | |
| 456 | if (!topLevel && !channelSpecific) return undefined |
| 457 | return { ...topLevel, ...channelSpecific } |
| 458 | } |
| 459 | |
| 460 | /** |
| 461 | * Resolve environment variables for plugin MCP servers |
no test coverage detected