The directory of plugins from the Cursor community.
├── apps/
│ └── cursor/ # Next.js app
├── supabase/
│ └── migrations/ # Database migrations
└── package.json # Bun workspace config
All data lives in the database — there is no local data in the repo.
- Clone the repo
git clone https://github.com/cursor/community-plugins.git
cd community-plugins- Install dependencies
bun install- Configure environment variables
cp apps/cursor/.env.example apps/cursor/.envFill in the required values:
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Yes | Supabase project URL |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY |
Yes | Supabase Publishable key (sb_publishable_..., replaces the legacy anon key) |
SUPABASE_SECRET_KEY |
Yes | Supabase Secret key (sb_secret_..., replaces the legacy service role key) |
NEXT_PUBLIC_APP_URL |
No | Defaults to http://localhost:3000 |
- Run the database migrations
Apply the migrations in supabase/migrations/ to your Supabase project.
- Start the dev server
bun devOpen http://localhost:3000.
All content is submitted through the website — no pull requests needed for data.
- Go to cursor.directory/plugins/new
- Sign in with GitHub or Google
- Paste a GitHub repo URL — we auto-detect components following the Open Plugins standard
- Click Submit
Auto-detected components:
| Component | Path |
|---|---|
| Rules | rules/*.mdc |
| MCP Servers | .mcp.json |
| Skills | skills/*/SKILL.md |
| Agents | agents/*.md |
| Hooks | hooks/hooks.json |
| LSP Servers | .lsp.json |
See the Open Plugins specification and plugin template for details.
- Framework: Next.js (App Router, Turbopack)
- Runtime: Bun
- Database: Supabase (PostgreSQL)
- Job Queue: Supabase Queues (
pgmq) drained by a 1-min Vercel cron - Styling: Tailwind CSS
- UI: Radix UI + shadcn/ui
- Search: Fuse.js (client-side fuzzy search)
- URL State: nuqs
- Linting: Biome
Submitted plugins are auto-reviewed by a Cursor SDK agent (composer-2) running
in local mode against a fresh clone of the plugin's repo plus its inline
component content. The verdict (safe / suspicious / malicious) is written
back to plugins.scan_status and surfaces in the admin queue.
The scan is asynchronous and runs out of the request lifecycle:
- Enqueue — server actions and the recover-stuck-scans cron call
enqueuePluginScan(pluginId)which sends a message to theplugin_scanspgmq queue. - Kick — user-facing actions also fire
kickDrainAfterResponse()so the drain route is called vianext/serverafter()immediately after the response is flushed. Scans typically start within a few hundred ms. - Drain —
/api/queue/plugin-scans/drainreads one message (vt=900s,n=1), runsrunPluginScan(pluginId), archives the message on success, leaves it for VT-expiry on retryable error, or buries it afterMAX_ATTEMPTS=5deliveries. - Cron safety net — Vercel cron hits the same drain route every minute so any messages that missed their kick still get processed.
The drain route uses maxDuration = 800 (Vercel Pro+ Fluid Compute ceiling) and
relies on CURSOR_API_KEY + CRON_SECRET from the env file.
Admins can moderate Community Plugins from Cursor through the MCP server at
/api/mcp. It exposes the same operations as /admin/plugins:
| Tool | Effect |
|---|---|
list_moderation_queue |
Flagged, scan issues, verification requests, or hidden plugins |
search_plugins, get_plugin |
Find a plugin and see its status, scan verdict, owner, and components |
list_moderation_events |
Moderation audit log (web and MCP) |
approve_plugin, approve_flagged_plugin |
Publish a hidden plugin; flagged/blocked plugins need the explicit approve_flagged_plugin |
unpublish_plugin, block_plugin |
Hide a plugin, or hide and permanently block it (with a reason) |
rescan_plugin |
Queue a new security scan (refused while one is in progress) |
set_plugin_verified, dismiss_verification_request |
Manage the verified badge |
Deleting plugins stays in the web admin. Write tools are rate-limited per admin
(mcp-moderation Vercel Firewall rule), and every change from the web admin or
MCP is recorded in the append-only plugin_moderation_events table.
Admins are rows in the public.admins table, which also controls
/admin/plugins. Add or remove people in the Supabase dashboard (Table
Editor → admins) or with SQL. Changes apply on the next request; nothing
needs a redeploy. Fill in one of:
user_id: an existing account (auth.users.id).email: someone who hasn't signed in yet, or any of their accounts. It only matches a Google or GitHub sign-in whose provider verified that address; an email typed into an email/password signup never counts.
-- By email, works before they sign in with Google or GitHub
insert into public.admins (email, note) values ('jane@cursor.com', 'plugin moderation');
-- By account (check it's the right one first)
insert into public.admins (user_id, note)
select id, 'plugin moderation' from auth.users where email = 'jane@cursor.com';
-- Remove
delete from public.admins where email = 'jane@cursor.com';- Supabase Auth is the OAuth 2.1 authorization server. Cursor signs in through a pre-registered public OAuth client (PKCE, no secret); dynamic client registration stays off.
- The consent page (
/oauth/consent) only lets admins approve, and only for the allowlisted OAuth client. The production client's ID is built intosrc/lib/mcp/auth.ts; a comma-separatedMCP_OAUTH_CLIENT_IDSreplaces it, for example for a local Supabase stack. /api/mcpverifies every bearer token (signature and expiry viagetClaims, plus a session check with the Auth server), requires the allowlistedclient_id, and requires the user to be an admin. Deleting someone'sadminsrow or signing them out cuts off MCP access immediately. Website session tokens are not accepted by the MCP server, and MCP tokens are not accepted as website sessions.
-
In the Supabase dashboard, enable Authentication → OAuth Server, set the authorization path to
/oauth/consent, and leave dynamic client registration off. The Site URL must be the app's origin. Use asymmetric JWT signing keys if clients request theopenidscope. Locally,supabase/config.tomlalready enables this. -
Apply the migrations (adds
plugin_moderation_eventsandadmins) and add the admins. When moving off the oldADMIN_USER_IDSenv var, copy its IDs in before deploying, then deleteADMIN_USER_IDSandNEXT_PUBLIC_ADMIN_USER_IDS:insert into public.admins (user_id, note) select btrim(id)::uuid, 'migrated from ADMIN_USER_IDS' from unnest(string_to_array('<ADMIN_USER_IDS value>', ',')) as id;
-
Production's OAuth client is already registered and built in. For another Supabase project, register one and set
MCP_OAUTH_CLIENT_IDSto the printed client ID:cd apps/cursor && bun run mcp:create-oauth-client
It registers Cursor's fixed callbacks (
https://www.cursor.com/agents/mcp/oauth/callbackandhttp://localhost:8787/callback); pass--redirect-uri <url>for others. -
Add the
mcp-moderationrate limit rule (60 requests / 1h, keyed by user) in Vercel Firewall. Until it exists, write tools aren't rate limited and the server logs an error. -
Admins add the server to their Cursor
mcp.json:{ "mcpServers": { "cursor-directory": { "url": "https://cursor.directory/api/mcp", "auth": { "CLIENT_ID": "12bb4179-e29a-4f99-a718-335ef4d0edb2", "scopes": ["email"] } } } }Cursor opens the consent page on first use. Keep the write tools out of Cursor's auto-run allowlist so each change is approved by a person.
Prefer this per-admin
mcp.jsonentry over a Cursor Team MCP server: team servers are also available to Cloud Agents, which run tools without asking for approval, and Cursor's docs don't say whether team servers accept a pre-registered OAuth client ID.