( codexHome: string, pluginDir: string, pluginName: string, )
| 295 | // --------------------------------------------------------------------------- |
| 296 | |
| 297 | const scanCachedPlugin = ( |
| 298 | codexHome: string, |
| 299 | pluginDir: string, |
| 300 | pluginName: string, |
| 301 | ): readonly CodexPluginEntry[] => { |
| 302 | const versions = [...listDirs(pluginDir)].sort(compareVersionsDesc); |
| 303 | const version = versions[0]; |
| 304 | if (version === undefined) return []; |
| 305 | const versionDir = path.join(pluginDir, version); |
| 306 | |
| 307 | const manifest = Option.getOrUndefined( |
| 308 | decodeManifestJson(readText(path.join(versionDir, ".codex-plugin", "plugin.json"))), |
| 309 | ); |
| 310 | if (manifest?.mcpServers === undefined) return []; |
| 311 | |
| 312 | // `mcpServers` is a relative path to an `.mcp.json` in every known manifest; |
| 313 | // tolerate an inline object of the same shape. |
| 314 | const servers = Option.getOrUndefined( |
| 315 | typeof manifest.mcpServers === "string" |
| 316 | ? decodeServersFileJson(readText(path.resolve(versionDir, manifest.mcpServers))) |
| 317 | : decodeServersFile({ mcpServers: manifest.mcpServers }), |
| 318 | ); |
| 319 | if (servers === undefined) return []; |
| 320 | |
| 321 | const displayName = manifest.interface?.displayName ?? manifest.name; |
| 322 | const summary = |
| 323 | manifest.interface?.shortDescription ?? |
| 324 | manifest.description?.split("\n")[0] ?? |
| 325 | `Local MCP server from the Codex plugin "${manifest.name}".`; |
| 326 | const icon = |
| 327 | manifest.interface?.logo === undefined |
| 328 | ? undefined |
| 329 | : readIconDataUri(path.resolve(versionDir, manifest.interface.logo)); |
| 330 | const display: CodexPluginDisplay = { |
| 331 | ...(icon === undefined ? {} : { icon }), |
| 332 | ...(manifest.interface?.displayName === undefined |
| 333 | ? {} |
| 334 | : { displayName: manifest.interface.displayName }), |
| 335 | ...(manifest.interface?.shortDescription === undefined |
| 336 | ? {} |
| 337 | : { tagline: manifest.interface.shortDescription }), |
| 338 | ...(manifest.interface?.longDescription === undefined |
| 339 | ? {} |
| 340 | : { description: manifest.interface.longDescription }), |
| 341 | }; |
| 342 | |
| 343 | const localServers = Object.entries(servers.mcpServers).flatMap(([serverKey, spec]) => |
| 344 | spec.command !== undefined && spec.type !== "http" && spec.url === undefined |
| 345 | ? [{ serverKey, command: spec.command, args: spec.args, cwd: spec.cwd }] |
| 346 | : [], |
| 347 | ); |
| 348 | |
| 349 | return localServers.map(({ serverKey, ...spec }) => { |
| 350 | // Manifest paths are relative to the versioned plugin dir. The versioned |
| 351 | // dir moves on plugin updates — the connection health check surfaces that |
| 352 | // as "command missing" and the card re-adds against the new path. |
| 353 | const command = path.resolve(versionDir, spec.command); |
| 354 | const cwd = path.resolve(versionDir, spec.cwd ?? "."); |
no test coverage detected