()
| 158 | * Results are cached for 30 seconds to avoid repeated filesystem scans |
| 159 | */ |
| 160 | export async function discoverPluginMcpServers(): Promise<PluginMcpConfig[]> { |
| 161 | // Return cached result if still valid |
| 162 | if (mcpCache && Date.now() - mcpCache.timestamp < CACHE_TTL_MS) { |
| 163 | return mcpCache.configs |
| 164 | } |
| 165 | |
| 166 | const plugins = await discoverInstalledPlugins() |
| 167 | const configs: PluginMcpConfig[] = [] |
| 168 | |
| 169 | for (const plugin of plugins) { |
| 170 | const mcpJsonPath = path.join(plugin.path, ".mcp.json") |
| 171 | try { |
| 172 | const content = await fs.readFile(mcpJsonPath, "utf-8") |
| 173 | let parsed: Record<string, unknown> |
| 174 | try { |
| 175 | parsed = JSON.parse(content) |
| 176 | } catch { |
| 177 | continue |
| 178 | } |
| 179 | |
| 180 | // Support two formats: |
| 181 | // Format A (flat): { "server-name": { "command": "...", ... } } |
| 182 | // Format B (nested): { "mcpServers": { "server-name": { ... } } } |
| 183 | const serversObj = |
| 184 | parsed.mcpServers && |
| 185 | typeof parsed.mcpServers === "object" && |
| 186 | !Array.isArray(parsed.mcpServers) |
| 187 | ? (parsed.mcpServers as Record<string, unknown>) |
| 188 | : parsed |
| 189 | |
| 190 | const validServers: Record<string, McpServerConfig> = {} |
| 191 | for (const [name, config] of Object.entries(serversObj)) { |
| 192 | if (config && typeof config === "object" && !Array.isArray(config)) { |
| 193 | validServers[name] = config as McpServerConfig |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | if (Object.keys(validServers).length > 0) { |
| 198 | configs.push({ |
| 199 | pluginSource: plugin.source, |
| 200 | mcpServers: validServers, |
| 201 | }) |
| 202 | } |
| 203 | } catch { |
| 204 | // No .mcp.json file, skip silently (this is expected for most plugins) |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | // Cache the result |
| 209 | mcpCache = { configs, timestamp: Date.now() } |
| 210 | return configs |
| 211 | } |
no test coverage detected