fix(plugins): load Codex MCP server from root config

This commit is contained in:
morluto 2026-10-13 23:13:34 +08:00
parent 987cb3b96b
commit 45bae55787
9 changed files with 120 additions and 41 deletions

View File

@ -16,17 +16,7 @@
"mcp-server"
],
"skills": "./skills",
"mcpServers": {
"rea": {
"command": "node",
"args": [
"./scripts/plugin-mcp-launcher.mjs",
"rea-agents@6.4.0"
],
"startup_timeout_sec": 30,
"cwd": "."
}
},
"mcpServers": "./.mcp.json",
"interface": {
"displayName": "REA",
"shortDescription": "Reverse engineer anything with agents",

12
.mcp.json Normal file
View File

@ -0,0 +1,12 @@
{
"mcpServers": {
"rea": {
"command": "node",
"args": [
"./scripts/plugin-mcp-launcher.mjs",
"rea-agents@6.4.0"
],
"cwd": "."
}
}
}

View File

@ -242,7 +242,9 @@ tag remains in main's ancestry. Keep the released tag unchanged.
The synchronization must include `.release-please-manifest.json`,
`package.json`, both root versions in `package-lock.json`, `CHANGELOG.md`,
`server.json`, both plugin manifests (`.claude-plugin/plugin.json` and
`.codex-plugin/plugin.json`), and the versioned documentation examples. Run
`.codex-plugin/plugin.json`), the Codex server declaration (`.mcp.json`), both
marketplaces (`.claude-plugin/marketplace.json` and
`.agents/plugins/marketplace.json`), and the versioned documentation examples. Run
`npm run docs:generate`, `npm run docs:check`, and the release configuration
tests against the combined tree. After the merge, verify that the released
tag is an ancestor of main:

View File

@ -72,6 +72,7 @@
"scripts/prepare.mjs",
".claude-plugin/plugin.json",
".codex-plugin/plugin.json",
".mcp.json",
"skills",
"README.md",
"DISCLAIMER.md",

View File

@ -49,7 +49,7 @@
},
{
"type": "json",
"path": ".codex-plugin/plugin.json",
"path": ".mcp.json",
"jsonpath": "$.mcpServers.rea.args[1]"
},
{

View File

@ -1,9 +1,10 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { mkdtemp, rm } from "node:fs/promises";
import { access, mkdtemp, rm } from "node:fs/promises";
import { constants, tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { packageRunnerCommand } from "./lib/packageRunnerCommand.mjs";
@ -26,6 +27,32 @@ if (
}
async function launch(packageSpecifier) {
// npm installs already carry the release-pinned runtime. Run it in this
// process so the plugin keeps the caller's working directory and the
// runtime retains ownership of provider and worker cleanup.
const { default: bundledPackage } = await import("../package.json", {
with: { type: "json" },
});
let bundledRuntimeMatchesPin =
packageSpecifier === `${bundledPackage.name}@${bundledPackage.version}`;
if (bundledRuntimeMatchesPin) {
try {
await access(new URL("../dist/mcp/main.js", import.meta.url));
} catch (cause) {
if (!isMissing(cause)) throw cause;
bundledRuntimeMatchesPin = false;
}
}
if (bundledRuntimeMatchesPin) {
process.argv = [
process.execPath,
fileURLToPath(new URL("./rea.mjs", import.meta.url)),
"mcp",
];
await import("./rea.mjs");
return;
}
const [command, ...args] = packageRunnerCommand(
"npx",
packageSpecifier,
@ -93,3 +120,12 @@ async function launch(packageSpecifier) {
}
});
}
function isMissing(cause) {
return (
typeof cause === "object" &&
cause !== null &&
"code" in cause &&
cause.code === "ENOENT"
);
}

View File

@ -71,21 +71,38 @@ export async function verifyPackagePack({ root, workspace }) {
const manifest = JSON.parse(
(await exec("tar", ["-xOf", tarball, `package/${path}`])).stdout,
);
const serverConfig = path.startsWith(".codex")
? JSON.parse(
(
await exec("tar", [
"-xOf",
tarball,
"package/.mcp.json",
])
).stdout,
)
: manifest;
const expectedArgs = [
`${path.startsWith(".claude") ? "${CLAUDE_PLUGIN_ROOT}/" : "./"}scripts/plugin-mcp-launcher.mjs`,
`${packedManifest.name}@${packedManifest.version}`,
];
if (
manifest.version !== packedManifest.version ||
manifest.skills !== "./skills" ||
manifest.mcpServers?.rea?.command !== "node" ||
(path.startsWith(".codex") && manifest.mcpServers?.rea?.cwd !== ".") ||
JSON.stringify(manifest.mcpServers?.rea?.args) !==
JSON.stringify([
`${path.startsWith(".claude") ? "${CLAUDE_PLUGIN_ROOT}/" : "./"}scripts/plugin-mcp-launcher.mjs`,
`${packedManifest.name}@${packedManifest.version}`,
])
(path.startsWith(".codex")
? manifest.mcpServers !== "./.mcp.json" ||
serverConfig.mcpServers?.rea?.cwd !== "."
: false) ||
serverConfig.mcpServers?.rea?.command !== "node" ||
JSON.stringify(serverConfig.mcpServers?.rea?.args) !==
JSON.stringify(expectedArgs)
)
throw new Error(
`Packaged plugin ${path} does not match the exact runtime and canonical skill release`,
);
}
if (!packedFiles.includes("package/.mcp.json"))
throw new Error("Packaged Codex MCP server declaration is missing");
for (const path of [
"scripts/plugin-mcp-launcher.mjs",
"scripts/lib/packageRunnerCommand.mjs",

View File

@ -10,7 +10,7 @@ const launcher = fileURLToPath(
new URL("../../../scripts/plugin-mcp-launcher.mjs", import.meta.url),
);
const start = (path: string, packageSpecifier = "rea-agents@6.4.0") => {
const start = (path: string, packageSpecifier = "rea-agents@6.4.0-rc.1") => {
const child = spawn(process.execPath, [launcher, packageSpecifier], {
env: {
...process.env,

View File

@ -20,6 +20,7 @@ const pluginManifests = [
".claude-plugin/plugin.json",
".codex-plugin/plugin.json",
] as const;
const pluginServerConfig = ".mcp.json";
describe("release configuration", () => {
it("keeps doctor and setup runtime support aligned with package engines", async () => {
@ -54,13 +55,16 @@ describe("release configuration", () => {
path: "server.json",
jsonpath: "$.packages[0].version",
},
...pluginManifests.flatMap((path) =>
["$.version", "$.mcpServers.rea.args[1]"].map((jsonpath) => ({
type: "json",
path,
jsonpath,
})),
),
...pluginManifests.map((path) => ({
type: "json",
path,
jsonpath: "$.version",
})),
{
type: "json",
path: pluginServerConfig,
jsonpath: "$.mcpServers.rea.args[1]",
},
...[
".claude-plugin/marketplace.json",
".agents/plugins/marketplace.json",
@ -164,7 +168,11 @@ describe("plugin manifests", () => {
files: string[];
};
expect(pkg.files).toEqual(
expect.arrayContaining([...pluginManifests, "skills"]),
expect.arrayContaining([
...pluginManifests,
pluginServerConfig,
"skills",
]),
);
for (const path of pluginManifests)
expect(await readJson(path)).toMatchObject({ skills: "./skills" });
@ -186,7 +194,7 @@ describe("plugin manifests", () => {
});
it.each(pluginManifests)(
"pins the plugin manifest %s and its server launch to the package version",
"pins the plugin manifest %s and Codex's server config path",
async (path) => {
const packageJson = (await readJson("package.json")) as {
name: string;
@ -195,20 +203,33 @@ describe("plugin manifests", () => {
expect(await readJson(path)).toMatchObject({
version: packageJson.version,
mcpServers: {
rea: {
command: "node",
args: [
`${path.startsWith(".claude") ? "${CLAUDE_PLUGIN_ROOT}/" : "./"}scripts/plugin-mcp-launcher.mjs`,
`${packageJson.name}@${packageJson.version}`,
],
...(path.startsWith(".codex") ? { cwd: "." } : {}),
},
},
...(path.startsWith(".codex")
? { mcpServers: `./${pluginServerConfig}` }
: {}),
});
},
);
it("pins the plugin server launch from the shared MCP declaration", async () => {
const packageJson = (await readJson("package.json")) as {
name: string;
version: string;
};
expect(await readJson(pluginServerConfig)).toMatchObject({
mcpServers: {
rea: {
command: "node",
args: [
"./scripts/plugin-mcp-launcher.mjs",
`${packageJson.name}@${packageJson.version}`,
],
cwd: ".",
},
},
});
});
it.each(pluginManifests)(
"stores %s in the form Release Please rewrites it to",
async (path) => {