Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,7 +45,6 @@ Marketplace plugins for Reforma. Each plugin is a folder with `.reforma-plugin/p
| heygen | [HeyGen](marketplace/media/heygen) | HeyGen | Media | Generate avatar videos with HeyGen. |
| fal | [fal](marketplace/media/fal) | fal | Media | Generate images, video, and audio from fal models. Needs an API key. |
| elevenlabs | [ElevenLabs](marketplace/media/elevenlabs) | ElevenLabs | Media | Generate speech and manage voices in ElevenLabs. |
| cartesia | [Cartesia](marketplace/media/cartesia) | Cartesia | Media | Generate speech with Cartesia voices. |
| sentry | [Sentry](https://github.com/getsentry/plugin-cursor) | Sentry | Analytics | Errors and traces from the Sentry org. |
| gsap | [GSAP](https://github.com/greensock/gsap-skills) | GreenSock | UI | Animation timelines, ScrollTrigger, and GSAP plugins. |
| motion | [Motion](https://github.com/motiondivision/cursor-plugin) | Motion | UI | Web animation, springs, and Motion docs. |
Expand DownExpand Up@@ -78,7 +77,7 @@ Author is `plugin.json` `author.name` when present, else Reforma. Shelf is the p

## Repository structure

Root `marketplace.json` lists plugins under `categories[].plugins`. `source` is a path in this repo or a GitHub `tree/…` URL. `"disabled": true` skips pack (folder stays); delete that line to ship after the vendor allowlists us.
Root `marketplace.json` lists plugins under `categories[].plugins`. `source` is a path in this repo or a GitHub `tree/…` URL. `"disabled": true` skips pack (folder stays) when the hosted MCP only accepts catalog clients (Figma waitlist). OAuth apps you register yourself (Drive, Dropbox, Slack, GitHub) stay enabled — credentials go in IN → OAuth clients, not here.

Local plugins live under `marketplace/<category>/<name>/`. Origin (first-party vs vendored) is `plugin.json` `author`, not the folder tree. Remote pins stay as GitHub URLs. Array order is section order; empty shelves omit `plugins`.

Expand Down
20 changes: 5 additions & 15 deletions marketplace.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -142,8 +142,7 @@
"plugins": [
{
"name": "clerk",
"source": "./marketplace/auth/clerk",
"disabled": true
"source": "./marketplace/auth/clerk"
},
{
"name": "auth0",
Expand DownExpand Up@@ -203,13 +202,11 @@
"plugins": [
{
"name": "google-drive",
"source": "./marketplace/files/google-drive",
"disabled": true
"source": "./marketplace/files/google-drive"
},
{
"name": "dropbox",
"source": "https://github.com/dropbox/dropbox-ai-plugins/tree/4900430d6afa76fc2504e00e006895de424f3637/codex",
"disabled": true
"source": "https://github.com/dropbox/dropbox-ai-plugins/tree/4900430d6afa76fc2504e00e006895de424f3637/codex"
},
{
"name": "cloudinary",
Expand DownExpand Up@@ -281,11 +278,6 @@
{
"name": "elevenlabs",
"source": "./marketplace/media/elevenlabs"
},
{
"name": "cartesia",
"source": "./marketplace/media/cartesia",
"disabled": true
}
]
},
Expand DownExpand Up@@ -335,13 +327,11 @@
},
{
"name": "slack",
"source": "./marketplace/workspace/slack",
"disabled": true
"source": "./marketplace/workspace/slack"
},
{
"name": "github-mcp",
"source": "./marketplace/workspace/github-mcp",
"disabled": true
"source": "./marketplace/workspace/github-mcp"
}
]
},
Expand Down
9 changes: 0 additions & 9 deletions marketplace/media/cartesia/.reforma-plugin/plugin.json

This file was deleted.

7 changes: 0 additions & 7 deletions marketplace/media/cartesia/mcp.json

This file was deleted.

26 changes: 26 additions & 0 deletions scripts/pack/content.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,32 @@ describe("normalizePackedMcpServerId", () => {
});
});

it("lowercases every server key when a plugin ships several", () => {
const dir = mkdtempSync(join(tmpdir(), "motion-"));
const pluginDir = join(dir, "motion");

dirs.push(dir);
mkdirSync(pluginDir, { recursive: true });
writeFileSync(
join(pluginDir, "mcp.json"),
`${JSON.stringify({
mcpServers: {
Motion: { url: "https://mcp.motion.dev" },
"Motion+": { url: "https://mcp.motion.dev/plus" },
},
})}\n`,
);

normalizePackedMcpServerId(pluginDir);

expect(JSON.parse(readFileSync(join(pluginDir, "mcp.json"), "utf8"))).toEqual({
mcpServers: {
motion: { url: "https://mcp.motion.dev" },
"motion+": { url: "https://mcp.motion.dev/plus" },
},
});
});

it("wraps a Cursor top-level map and renames the lone server key", () => {
const dir = mkdtempSync(join(tmpdir(), "notion-"));
const pluginDir = join(dir, "notion-workspace");
Expand Down
36 changes: 36 additions & 0 deletions scripts/pack/content.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -145,6 +145,11 @@ function isMcpServerEntry(value: unknown): value is Record<string, unknown> {
return url.length > 0 !== command.length > 0;
}

/** Same rule as reforma `normalizeMcpServerId` — trim, lowercase, spaces → `_`. */
function normalizeMcpServerId(raw: string): string {
return raw.trim().toLowerCase().replace(/\s+/g, "_");
}

function renameLoneKey(
servers: Record<string, unknown>,
pluginName: string,
Expand All@@ -164,6 +169,35 @@ function renameLoneKey(
return { next: { [pluginName]: servers[from] }, from };
}

function normalizeServerKeys(
servers: Record<string, unknown>,
pluginName: string,
): Record<string, unknown> {
const next: Record<string, unknown> = {};

for (const [id, entry] of Object.entries(servers)) {
const key = normalizeMcpServerId(id);

if (!key) {
throw new Error(`${pluginName}: mcp server id is empty`);
}

if (key in next) {
throw new Error(
`${pluginName}: mcp server id ${key} collides after normalize`,
);
}

if (key !== id) {
console.log(` mcp ${id} → ${key}`);
}

next[key] = entry;
}

return next;
}

/** One MCP server → marketplace plugin name. Vendor keys like `chatgpt_app_mcp` go away. */
export function renameLoneMcpServerKey(
raw: unknown,
Expand DownExpand Up@@ -239,6 +273,8 @@ export function normalizePackedMcpServerId(pluginDir: string): void {
console.log(` mcp ${renamed.from} → ${pluginName}`);
}

servers = normalizeServerKeys(servers, pluginName);

const next = { mcpServers: servers };
const current = isRecord(raw) && isRecord(raw.mcpServers) ? raw : undefined;

Expand Down