diff --git a/.cursor-plugin/logo.svg b/.cursor-plugin/logo.svg
new file mode 100644
index 00000000..2c6eefb8
--- /dev/null
+++ b/.cursor-plugin/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json
index 910723d3..4f62e9c1 100644
--- a/.cursor-plugin/plugin.json
+++ b/.cursor-plugin/plugin.json
@@ -1,7 +1,10 @@
{
"name": "socket-skills",
+ "displayName": "Socket",
"skills": "skills",
+ "rules": ".cursor-plugin/rules",
"mcpServers": ".cursor-plugin/mcp.json",
+ "logo": ".cursor-plugin/logo.svg",
"description": "Agent Skills for dependency security including vulnerability scanning, package review, patching, setup and configuration, and secure dependency updates via Socket.dev",
"version": "1.0.0",
"author": {
diff --git a/.cursor-plugin/rules/socket-mcp.mdc b/.cursor-plugin/rules/socket-mcp.mdc
new file mode 100644
index 00000000..3cbc612b
--- /dev/null
+++ b/.cursor-plugin/rules/socket-mcp.mdc
@@ -0,0 +1,36 @@
+---
+description: Use the Socket MCP server to check a package before you add, upgrade, or review a dependency, and tell the user how to sign in to Socket without an API key.
+alwaysApply: false
+---
+
+# Socket MCP
+
+This plugin connects Cursor to Socket's hosted MCP server at `https://mcp.socket.dev/`. Cursor shows it as `plugin-socket-skills-socket`.
+
+## Sign in
+
+The server uses OAuth. The user signs in to Socket in the browser. Nobody copies or pastes an API key.
+
+When a Socket tool returns `Unauthorized`, or the server shows that it needs a login:
+
+1. Tell the user to open **Customize** in the Cursor sidebar, find the Socket MCP server, and choose **Authenticate**.
+2. When the user says the sign-in is done, try the tool one more time.
+3. If the tool still returns `Unauthorized`, do not ask the user to sign in again. Some Cursor versions do not complete the sign-in for MCP servers that a plugin adds. Continue with the Socket CLI steps in the skills (`pnpm dlx socket login`), and tell the user that MCP checks are not available in this session.
+
+Do not ask the user for an API key for this server. Do not put a token in an `Authorization` header for this server.
+
+## Tools
+
+- `depscore`: get Socket scores (0-100) for one or more packages. Give each package as `depname`, `ecosystem` (default `npm`), and `version` (use the exact version when you know it). Lower scores mean higher risk. A missing score is not a safety verdict.
+- `organizations`: list the Socket organizations that the sign-in can access. Use it to find the `org_slug` that `alerts` and `threat_feed` need. If there is more than one organization, ask the user which one to use.
+- `alerts`: list the security alerts of an organization. It needs `org_slug`. Filter with `status`, `severity`, and other fields, and use `cursor` to get more pages.
+- `threat_feed`: list packages that Socket flagged recently, for example malware and typosquats. It needs `org_slug`.
+- `package_files`, `package_file_contents`, `package_file_grep`: read the files in a published package before you install it.
+
+## When to check
+
+- Before you add a new dependency, run `depscore` on it. Report low scores to the user before you continue.
+- Before you upgrade a dependency, run `depscore` on the target version.
+- When the user asks if a package is safe, use `depscore`, then the `/socket-inspect` skill for a full review.
+
+The MCP server does not create scans. For a full project scan, use the `/socket-scan` skill.
diff --git a/README.md b/README.md
index 929f0832..7bf923f4 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ Socket Security Skills are definitions for dependency security tasks like vulner
- **Codex** (OpenAI) - Agent Skills standard + AGENTS.md fallback
- **Gemini CLI** (Google DeepMind) - extensions support
- **OpenCode** - skill directory support
-- **Cursor** - plugin manifest support
+- **Cursor** - plugin with skills, rules, and the Socket MCP server
- **VS Code Copilot / GitHub Copilot** - via AGENTS.md or Skills CLI
- **Windsurf** - via Skills CLI
- **Roo Code** - via Skills CLI
@@ -94,12 +94,19 @@ gemini extensions install https://github.com/SocketDev/skills.git --consent
### Cursor
-This repository includes Cursor plugin manifests:
+This repository is a Cursor plugin. It gives Cursor:
-- `.cursor-plugin/plugin.json`
+- the skills in `skills/`
+- the Socket MCP server at `https://mcp.socket.dev/` (`.cursor-plugin/mcp.json`)
+- a rule that tells the agent when to use the Socket MCP tools (`.cursor-plugin/rules/`)
Install from repository URL (or local checkout) via the Cursor plugin flow.
+To connect to Socket, open **Customize** in the Cursor sidebar, find the Socket MCP server, and choose **Authenticate**. You sign in to Socket in the browser. You do not paste an API key. The skills that run the Socket CLI use their own sign-in (`pnpm dlx socket login`).
+
+> [!WARNING]
+> Some Cursor versions do not complete the OAuth sign-in for MCP servers that a plugin adds. The Socket tools then keep returning `Unauthorized`. See the [Cursor forum report](https://forum.cursor.com/t/plugin-mcp-oauth-never-reaches-needsauth-authenticate-button-missing-browser-never-opens/170058). If this occurs, use the skills, which sign in through the Socket CLI.
+
For contributors, regenerate manifests with:
```bash
diff --git a/scripts/repo/check/shipped-content-is-consumer-clean.mts b/scripts/repo/check/shipped-content-is-consumer-clean.mts
index eb415fe0..842296d2 100644
--- a/scripts/repo/check/shipped-content-is-consumer-clean.mts
+++ b/scripts/repo/check/shipped-content-is-consumer-clean.mts
@@ -23,6 +23,7 @@ import {
FLEET_INTERNAL_MARKERS,
SCAFFOLDING_ENTRIES,
SHIPPED_DIRS,
+ SHIPPED_INSTRUCTION_DIRS,
SHIPPED_MARKER_ALLOWLIST,
SHIPPED_ROOT_FILES,
} from '../constants/shipped-surfaces.mts'
@@ -69,7 +70,9 @@ export function isAllowlistedMarker(rel: string, marker: string): boolean {
export function findFleetLeaks(tracked: string[]): string[] {
const leaks: string[] = []
const shipped = tracked.filter(f =>
- SHIPPED_DIRS.some(dir => f.startsWith(`${dir}/`)),
+ [...SHIPPED_DIRS, ...SHIPPED_INSTRUCTION_DIRS].some(dir =>
+ f.startsWith(`${dir}/`),
+ ),
)
for (let i = 0, { length } = shipped; i < length; i += 1) {
const rel = shipped[i]!
diff --git a/scripts/repo/constants/shipped-surfaces.mts b/scripts/repo/constants/shipped-surfaces.mts
index 767da229..4f4621d9 100644
--- a/scripts/repo/constants/shipped-surfaces.mts
+++ b/scripts/repo/constants/shipped-surfaces.mts
@@ -18,6 +18,12 @@ export const SHIPPED_DIRS = [
'skills',
] as const
+/**
+ * Agent-instruction trees inside a shipped root entry. Agents read and obey
+ * these files, so the shipped-content gates scan them like SHIPPED_DIRS.
+ */
+export const SHIPPED_INSTRUCTION_DIRS = ['.cursor-plugin/rules'] as const
+
/**
* Consumer-facing manifests and adapters at the root: generated for the
* harnesses consumers use, referenced by shipped docs, but not trees a
diff --git a/scripts/repo/generate-cursor-plugin.mts b/scripts/repo/generate-cursor-plugin.mts
index 1b40b26d..a2086185 100644
--- a/scripts/repo/generate-cursor-plugin.mts
+++ b/scripts/repo/generate-cursor-plugin.mts
@@ -3,6 +3,9 @@
* Generate Cursor plugin artifacts — .cursor-plugin/plugin.json and
* .cursor-plugin/mcp.json — from .claude-plugin/plugin.json.
*
+ * The mcp config declares Socket's hosted OAuth MCP server. No API key ships in
+ * the plugin.
+ *
* The plugin's mcp artifact lives under .cursor-plugin/, never at the repo
* root, where it would overwrite a hand-maintained .mcp.json. This generator
* must never write outside .cursor-plugin/.
@@ -34,6 +37,16 @@ const ROOT = path.resolve(
// Manifest-relative reference to the plugin's own mcp config artifact.
const CURSOR_MCP_RELATIVE = '.cursor-plugin/mcp.json'
+// Hand-maintained Cursor-only sources the manifest references.
+export const CURSOR_LOGO_RELATIVE = '.cursor-plugin/logo.svg'
+export const CURSOR_RULES_RELATIVE = '.cursor-plugin/rules'
+
+export const CURSOR_DISPLAY_NAME = 'Socket'
+
+// Same server name as the socket-connect marketplace entry, the Gemini
+// extension, and the Codex plugin, so every agent shows one "socket" server.
+export const SOCKET_MCP_SERVER_NAME = 'socket'
+
const PLUGIN_NAME_RE = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/
export interface GenerateResult {
@@ -66,10 +79,26 @@ export function buildCursorPluginManifest(
throw new Error('No skills discovered under skills/*/SKILL.md')
}
+ const sources = [CURSOR_LOGO_RELATIVE, CURSOR_RULES_RELATIVE]
+ for (let i = 0, { length } = sources; i < length; i += 1) {
+ const relative = sources[i]!
+ if (!existsSync(path.join(root, relative))) {
+ throw new Error(
+ `Cursor plugin manifest references a missing file.\n` +
+ ` Where: ${relative}\n` +
+ ` Saw: no file or directory at that path\n` +
+ ` Fix: restore ${relative}; the Cursor manifest points at it.`,
+ )
+ }
+ }
+
const manifest: Record = {
name,
+ displayName: CURSOR_DISPLAY_NAME,
skills: 'skills',
+ rules: CURSOR_RULES_RELATIVE,
mcpServers: CURSOR_MCP_RELATIVE,
+ logo: CURSOR_LOGO_RELATIVE,
}
const optionalKeys = [
@@ -80,7 +109,6 @@ export function buildCursorPluginManifest(
'repository',
'license',
'keywords',
- 'logo',
]
for (let i = 0, { length } = optionalKeys; i < length; i += 1) {
const key = optionalKeys[i]!
@@ -103,7 +131,7 @@ export const SOCKET_MCP_URL = 'https://mcp.socket.dev/'
export function buildMcpConfig(): Record {
return {
mcpServers: {
- socket: { url: SOCKET_MCP_URL },
+ [SOCKET_MCP_SERVER_NAME]: { url: SOCKET_MCP_URL },
},
}
}
diff --git a/test/repo/unit/generate-cursor-plugin.test.mts b/test/repo/unit/generate-cursor-plugin.test.mts
index 2d2a5e3f..f376a534 100644
--- a/test/repo/unit/generate-cursor-plugin.test.mts
+++ b/test/repo/unit/generate-cursor-plugin.test.mts
@@ -5,6 +5,9 @@
// silently emptying the MCP server inventory. The plugin's mcp artifact now
// lives at .cursor-plugin/mcp.json; a populated root .mcp.json must survive a
// generator run byte-for-byte.
+//
+// The plugin mcp config declares Socket's hosted OAuth MCP server and nothing
+// else: no headers, no token placeholders.
import { expect, it } from 'vitest'
@@ -16,8 +19,12 @@ import { safeDeleteSync } from '@socketsecurity/lib-stable/fs/safe'
import {
buildCursorPluginManifest,
+ buildMcpConfig,
+ CURSOR_LOGO_RELATIVE,
+ CURSOR_RULES_RELATIVE,
generateCursorPlugin,
renderJson,
+ SOCKET_MCP_URL,
} from '../../../scripts/repo/generate-cursor-plugin.mts'
const POPULATED_MCP_SERVERS = {
@@ -50,6 +57,13 @@ function makeFixtureRoot(): string {
'utf-8',
)
writeFileSync(path.join(root, '.mcp.json'), POPULATED_MCP, 'utf-8')
+ mkdirSync(path.join(root, CURSOR_RULES_RELATIVE), { recursive: true })
+ writeFileSync(
+ path.join(root, CURSOR_RULES_RELATIVE, 'demo.mdc'),
+ '---\ndescription: A demo rule.\nalwaysApply: false\n---\n\n# demo\n',
+ 'utf-8',
+ )
+ writeFileSync(path.join(root, CURSOR_LOGO_RELATIVE), '\n', 'utf-8')
return root
}
@@ -80,6 +94,38 @@ it('generate writes the hosted Socket MCP server config under .cursor-plugin/',
}
})
+it('mcp config carries only the OAuth server URL, never a credential', () => {
+ const config = buildMcpConfig()
+ expect(config).toStrictEqual({
+ mcpServers: { socket: { url: SOCKET_MCP_URL } },
+ })
+ expect(JSON.stringify(config)).not.toMatch(/\$\{|authorization|token/i)
+})
+
+it('manifest references the Cursor rules, logo and display name', () => {
+ const root = makeFixtureRoot()
+ try {
+ const manifest = buildCursorPluginManifest({ root })
+ expect(manifest['displayName']).toBe('Socket')
+ expect(manifest['rules']).toBe(CURSOR_RULES_RELATIVE)
+ expect(manifest['logo']).toBe(CURSOR_LOGO_RELATIVE)
+ } finally {
+ safeDeleteSync(root)
+ }
+})
+
+it('manifest generation fails when a Cursor source is missing', () => {
+ const root = makeFixtureRoot()
+ try {
+ safeDeleteSync(path.join(root, CURSOR_LOGO_RELATIVE))
+ expect(() => buildCursorPluginManifest({ root })).toThrow(
+ CURSOR_LOGO_RELATIVE,
+ )
+ } finally {
+ safeDeleteSync(root)
+ }
+})
+
it('manifest references the .cursor-plugin mcp artifact, not root .mcp.json', () => {
const root = makeFixtureRoot()
try {
diff --git a/test/repo/unit/structural/shipped-surfaces-injection-clean.test.mts b/test/repo/unit/structural/shipped-surfaces-injection-clean.test.mts
index 2f8dcb3a..3a9e6ac9 100644
--- a/test/repo/unit/structural/shipped-surfaces-injection-clean.test.mts
+++ b/test/repo/unit/structural/shipped-surfaces-injection-clean.test.mts
@@ -12,7 +12,10 @@ import { describe, expect, it } from 'vitest'
* install one-liners outside an auditable trusted-prefix allowlist.
*/
-import { SHIPPED_DIRS } from '../../../../scripts/repo/constants/shipped-surfaces.mts'
+import {
+ SHIPPED_DIRS,
+ SHIPPED_INSTRUCTION_DIRS,
+} from '../../../../scripts/repo/constants/shipped-surfaces.mts'
const REPO_ROOT = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
@@ -25,6 +28,7 @@ const REPO_ROOT = path.resolve(
const SCANNED_EXTENSIONS = new Set([
'.json',
'.md',
+ '.mdc',
'.mdx',
'.mjs',
'.mts',
@@ -68,7 +72,9 @@ const PIPE_TO_SHELL =
export function listShippedFiles(): string[] {
const files: string[] = []
- const stack = SHIPPED_DIRS.map(d => path.join(REPO_ROOT, d))
+ const stack = [...SHIPPED_DIRS, ...SHIPPED_INSTRUCTION_DIRS].map(d =>
+ path.join(REPO_ROOT, d),
+ )
while (stack.length) {
const dir = stack.pop()!
for (const entry of readdirSync(dir, { withFileTypes: true })) {