feat(cursor): ship a Marketplace plugin with the skill and pinned MCP server (#9878)

Cursor Marketplace installs are Git checkouts, so the plugin manifest
points at a synced copy of the authored skill and starts
npx -y rea-agents@<release> mcp. Release Please and the release
checkpoint keep that pin aligned with the package version.

The manifest, skill, and icon checks follow the local plugin bundle
contract from #7619, adapted for a checkout that has no dist/.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: GamerDubz <105694977+GamerDubz@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-11-11 00:16:44 -05:00
1 parent 6a2b690440
commit a54e848302
29 files changed
+4529 -95

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { syncCursorPluginSkill } from "./lib/cursor-plugin-bundle.mjs";
const arguments_ = new Set(process.argv.slice(2));
for (const argument of arguments_)
if (argument !== "--check")
throw new Error(`Unknown Cursor plugin skill option: ${argument}`);
await syncCursorPluginSkill({
root: join(dirname(fileURLToPath(import.meta.url)), ".."),
check: arguments_.has("--check"),
});
+79
View File
@@ -0,0 +1,79 @@
export const CURSOR_PLUGIN_MANIFEST: ".cursor-plugin/plugin.json";
export const CURSOR_PLUGIN_MCP: "mcp.json";
export const AUTHORED_SKILL_DIRECTORY: ".agents/skills/reverse-engineer-anything";
export const CURSOR_PLUGIN_SKILLS: ".cursor-plugin/skills";
export const CURSOR_PLUGIN_SKILL: "reverse-engineer-anything";
export const CURSOR_PLUGIN_LOGO: "assets/rea-plugin.png";
export const CURSOR_PLUGIN_REPOSITORY: "https://github.com/morluto/rea";
export interface CursorPluginBundleIssue {
readonly field: string;
}
export interface CursorPluginBundleInput {
readonly manifest: {
readonly name?: string;
readonly version?: string;
readonly description?: string;
readonly author?: { readonly name?: string };
readonly repository?: string;
readonly license?: string;
readonly keywords?: readonly string[];
readonly skills?: string;
readonly logo?: string;
readonly mcpServers?: unknown;
};
readonly mcp: {
readonly mcpServers?: {
readonly rea?: {
readonly command?: string;
readonly args?: readonly string[];
};
};
};
readonly packageVersion: string;
readonly logoExists: boolean;
readonly skillNames: readonly string[];
readonly skillFrontmatter: {
readonly name?: string;
readonly description?: string;
} | null;
}
export interface CursorPluginInspection {
readonly manifest: CursorPluginBundleInput["manifest"] & {
readonly version: string;
};
readonly mcp: {
readonly mcpServers: {
readonly rea: {
readonly command: string;
readonly args: readonly string[];
};
};
};
readonly packageVersion: string;
readonly issues: readonly CursorPluginBundleIssue[];
}
export function cursorPluginSkillPaths(
directory: string,
prefix?: string,
): Promise<readonly string[] | null>;
export function cursorPluginBundleIssues(
input: CursorPluginBundleInput,
): readonly CursorPluginBundleIssue[];
export function syncCursorPluginSkill(options: {
readonly root: string;
readonly check: boolean;
}): Promise<void>;
export function inspectCursorPluginBundle(
root: string,
): Promise<CursorPluginInspection>;
export function cursorMarketplaceInstallLinkIssues(
documents: readonly { readonly path: string; readonly text: string }[],
): readonly { readonly path: string; readonly marker: string }[];
+216
View File
@@ -0,0 +1,216 @@
import { mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
import { dirname, join } from "node:path";
import { ensureGeneratedFile } from "./generated-file.mjs";
// Identity fields follow the shared plugin manifest checked for local Cursor
// publication (name, release version, repository, skill, and icon). The
// Marketplace checkout has no dist/, so the server is the pinned npm command
// instead of the bundled launcher.
export const CURSOR_PLUGIN_MANIFEST = ".cursor-plugin/plugin.json";
export const CURSOR_PLUGIN_MCP = "mcp.json";
export const AUTHORED_SKILL_DIRECTORY =
".agents/skills/reverse-engineer-anything";
export const CURSOR_PLUGIN_SKILLS = ".cursor-plugin/skills";
export const CURSOR_PLUGIN_SKILL = "reverse-engineer-anything";
export const CURSOR_PLUGIN_LOGO = "assets/rea-plugin.png";
export const CURSOR_PLUGIN_REPOSITORY = "https://github.com/morluto/rea";
const GENERATE_COMMAND = "node scripts/generate-cursor-plugin-skill.mjs";
const INSTALL_LINK_MARKERS = ["install-mcp", "deeplink/mcp", "mcp-install"];
const failure = (code, detail) => {
const error = new Error(`${code}: ${detail}`);
error.code = code;
return error;
};
const cursorServerArgs = (version) => ["-y", `rea-agents@${version}`, "mcp"];
/** Relative file inventory for one skill directory, in stable order. */
export const cursorPluginSkillPaths = async (directory, prefix = "") => {
const paths = [];
let entries;
try {
entries = await readdir(directory, { withFileTypes: true });
} catch (cause) {
if (cause?.code === "ENOENT") return null;
throw cause;
}
for (const entry of entries.sort((left, right) =>
left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
)) {
const relativePath = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
if (entry.isDirectory())
paths.push(
...(await cursorPluginSkillPaths(
join(directory, entry.name),
relativePath,
)),
);
else if (entry.isFile()) paths.push(relativePath);
else
throw failure(
"cursor_plugin_skill_inventory",
`Skill bundle requires regular files or directories: ${relativePath}`,
);
}
return paths;
};
const skillFrontmatter = (content) => {
const normalized = content.replaceAll("\r\n", "\n");
if (!normalized.startsWith("---\n")) return null;
const closing = normalized.indexOf("\n---\n", 4);
if (closing === -1) return null;
const fields = {};
for (const line of normalized.slice(4, closing).split("\n")) {
const separator = line.indexOf(":");
if (separator === -1) continue;
const key = line.slice(0, separator).trim();
const value = line.slice(separator + 1).trim();
if (key !== "" && fields[key] === undefined) fields[key] = value;
}
return fields;
};
/**
* Structured differences between the Cursor plugin declaration and the
* package version it must launch. Empty means the manifest, server pin,
* logo, and skill frontmatter agree.
*/
export const cursorPluginBundleIssues = ({
manifest,
mcp,
packageVersion,
logoExists,
skillNames,
skillFrontmatter: frontmatter,
}) => {
const issues = [];
const add = (field) => issues.push({ field });
if (manifest?.name !== "rea") add("name");
if (manifest?.version !== packageVersion) add("version");
if (typeof manifest?.description !== "string" || manifest.description === "")
add("description");
if (typeof manifest?.author?.name !== "string" || manifest.author.name === "")
add("author.name");
if (manifest?.repository !== CURSOR_PLUGIN_REPOSITORY) add("repository");
if (typeof manifest?.license !== "string" || manifest.license === "")
add("license");
if (
!Array.isArray(manifest?.keywords) ||
manifest.keywords.length === 0 ||
manifest.keywords.some((keyword) => typeof keyword !== "string")
)
add("keywords");
if (manifest?.skills !== CURSOR_PLUGIN_SKILLS) add("skills");
if (manifest?.logo !== CURSOR_PLUGIN_LOGO) add("logo");
if (Object.hasOwn(manifest ?? {}, "mcpServers")) add("mcpServers");
if (logoExists !== true) add("logo.file");
const server = mcp?.mcpServers?.rea;
if (
server?.command !== "npx" ||
JSON.stringify(server?.args) !==
JSON.stringify(cursorServerArgs(packageVersion))
)
add("mcp.args");
if (
!Array.isArray(skillNames) ||
skillNames.length !== 1 ||
skillNames[0] !== CURSOR_PLUGIN_SKILL
)
add("skills.entries");
if (frontmatter?.name !== CURSOR_PLUGIN_SKILL) add("skill.name");
if (
typeof frontmatter?.description !== "string" ||
frontmatter.description === ""
)
add("skill.description");
return issues;
};
/** Copy the authored skill into the Marketplace plugin tree, or fail when it drifted. */
export const syncCursorPluginSkill = async ({ root, check }) => {
const sourceRoot = join(root, AUTHORED_SKILL_DIRECTORY);
const outputRoot = join(root, CURSOR_PLUGIN_SKILLS, CURSOR_PLUGIN_SKILL);
const sourcePaths = await cursorPluginSkillPaths(sourceRoot);
if (sourcePaths === null)
throw failure(
"cursor_plugin_skill_inventory",
`Authored skill is missing: ${AUTHORED_SKILL_DIRECTORY}`,
);
if (!check) await rm(outputRoot, { recursive: true, force: true });
for (const relativePath of sourcePaths) {
const source = (
await readFile(join(sourceRoot, relativePath), "utf8")
).replaceAll("\r\n", "\n");
if (!check)
await mkdir(dirname(join(outputRoot, relativePath)), { recursive: true });
try {
await ensureGeneratedFile({
path: join(outputRoot, relativePath),
source,
check,
generateCommand: GENERATE_COMMAND,
});
} catch (cause) {
throw failure(
"cursor_plugin_skill_stale",
cause instanceof Error ? cause.message : String(cause),
);
}
}
const outputPaths = await cursorPluginSkillPaths(outputRoot);
if (
outputPaths === null ||
JSON.stringify(outputPaths) !== JSON.stringify(sourcePaths)
)
throw failure(
"cursor_plugin_skill_inventory",
"Cursor plugin skill inventory drifted; run node scripts/generate-cursor-plugin-skill.mjs",
);
};
/** Read the committed plugin and report structured contract issues. */
export const inspectCursorPluginBundle = async (root) => {
const manifest = JSON.parse(
await readFile(join(root, CURSOR_PLUGIN_MANIFEST), "utf8"),
);
const mcp = JSON.parse(await readFile(join(root, CURSOR_PLUGIN_MCP), "utf8"));
const packageJson = JSON.parse(
await readFile(join(root, "package.json"), "utf8"),
);
const logoExists = await stat(join(root, CURSOR_PLUGIN_LOGO))
.then((metadata) => metadata.isFile())
.catch((cause) => {
if (cause?.code === "ENOENT") return false;
throw cause;
});
const skillDirectory = join(root, CURSOR_PLUGIN_SKILLS);
const skillNames = await readdir(skillDirectory);
const skillContent = await readFile(
join(skillDirectory, CURSOR_PLUGIN_SKILL, "SKILL.md"),
"utf8",
);
return {
manifest,
mcp,
packageVersion: packageJson.version,
issues: cursorPluginBundleIssues({
manifest,
mcp,
packageVersion: packageJson.version,
logoExists,
skillNames,
skillFrontmatter: skillFrontmatter(skillContent),
}),
};
};
/** MCP-only install links omit the skill, so Marketplace docs must not carry them. */
export const cursorMarketplaceInstallLinkIssues = (documents) =>
documents.flatMap(({ path, text }) =>
INSTALL_LINK_MARKERS.filter((marker) => text.includes(marker)).map(
(marker) => ({ path, marker }),
),
);
+31
View File
@@ -35,6 +35,16 @@ const codexServerConfigSchema = z.object({
}),
}),
});
const cursorPluginManifest = ".cursor-plugin/plugin.json";
const cursorPluginServer = "mcp.json";
const cursorServerConfigSchema = z.object({
mcpServers: z.object({
rea: z.object({
command: z.literal("npx"),
args: z.tuple([z.literal("-y"), z.string(), z.literal("mcp")]),
}),
}),
});
const sectionsSchema = z.object({
versioning: z.string().optional(),
"changelog-sections": z.array(
@@ -240,6 +250,20 @@ async function validateCandidate(git, sha, version, commits) {
: manifest;
launches.push([path, server.mcpServers.rea.args[1]]);
}
const cursorManifest = await readJson(
git,
sha,
cursorPluginManifest,
versionSchema,
);
versions.push([cursorPluginManifest, cursorManifest.version]);
const cursorServer = await readJson(
git,
sha,
cursorPluginServer,
cursorServerConfigSchema,
);
const cursorPin = cursorServer.mcpServers.rea.args[1];
// Catalogs are generated and checked from this candidate by the release job;
// they are not version authority stored in the immutable source tree.
const generated = await git([
@@ -262,6 +286,13 @@ async function validateCandidate(git, sha, version, commits) {
}
}
// The launcher checks the scalar pin against the runtime in its own package.
// Compare manifest versions first so a wholly stale candidate reports the
// shared version authority before this Marketplace package specifier.
if (cursorPin !== `rea-agents@${version}`) {
throw new Error(
`${cursorPluginServer}@${sha} launches ${cursorPin}; release branch requires rea-agents@${version}.`,
);
}
for (const [location, launched] of launches) {
if (launched !== version) {
throw new Error(
+10 -3
View File
@@ -373,6 +373,8 @@ const VERSION_ONLY_PLUGIN_METADATA = new Set([
".claude-plugin/plugin.json",
".codex-plugin/plugin.json",
".codex-plugin/mcp.json",
".cursor-plugin/plugin.json",
"mcp.json",
".claude-plugin/marketplace.json",
".agents/plugins/marketplace.json",
]);
@@ -395,9 +397,14 @@ function pluginMetadataWithoutVersions(path, text) {
}
if ("version" in manifest) manifest.version = "";
const launch = manifest.mcpServers?.rea?.args;
// Only the scalar pinned version is release-owned; the launcher owns the
// fixed package name and the executable/path remain product behavior.
if (Array.isArray(launch) && typeof launch[1] === "string") launch[1] = "";
// Only the scalar pinned version is release-owned. Blank that version and
// keep a package name such as rea-agents@ so a renamed package still counts
// as a product change. The executable and path remain product behavior.
if (Array.isArray(launch) && typeof launch[1] === "string")
launch[1] = launch[1].replace(
/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/u,
"",
);
return JSON.stringify(manifest);
}
+4
View File
@@ -43,6 +43,10 @@ if (await pathExists(join(root, "src"))) {
throw new Error("Prepack could not resolve the active package manager");
run(process.execPath, [packageManagerPath, "run", "build:cached"]);
run(process.execPath, ["scripts/generate-skill-metadata.mjs", "--check"]);
run(process.execPath, [
"scripts/generate-cursor-plugin-skill.mjs",
"--check",
]);
} else {
for (const path of [
"scripts/lib/cli-process-environment.mjs",
+33
View File
@@ -266,6 +266,39 @@ export async function verifyPackagePack({ root, workspace, commands }) {
)
throw new Error(`Packaged plugin ${path} is missing its shared icon`);
}
const cursorManifest = JSON.parse(
(
await commands.exec("tar", [
"-xOf",
tarball,
"package/.cursor-plugin/plugin.json",
])
).stdout,
);
const cursorServer = JSON.parse(
(await commands.exec("tar", ["-xOf", tarball, "package/mcp.json"])).stdout,
);
const cursorLogo = String(cursorManifest.logo ?? "").replace(/^\.\//u, "");
const cursorSkill =
"package/.cursor-plugin/skills/reverse-engineer-anything/SKILL.md";
if (
cursorManifest.version !== packedManifest.version ||
cursorManifest.skills !== ".cursor-plugin/skills" ||
cursorLogo !== "assets/rea-plugin.png" ||
cursorManifest.mcpServers !== undefined ||
cursorServer.mcpServers?.rea?.command !== "npx" ||
JSON.stringify(cursorServer.mcpServers?.rea?.args) !==
JSON.stringify([
"-y",
`${packedManifest.name}@${packedManifest.version}`,
"mcp",
]) ||
!packedFiles.includes(`package/${cursorLogo}`) ||
!packedFiles.includes(cursorSkill)
)
throw new Error(
"Packaged Cursor plugin does not ship the pinned server, skill, and logo",
);
if (packedFiles.includes("package/.mcp.json"))
throw new Error("Plugin metadata must not register a project MCP server");
if (!packedFiles.includes("package/.codex-plugin/mcp.json"))