From b52b72fb20b36795192224c862ea250fb92a1470 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 4 Nov 2026 11:17:53 -0600 Subject: [PATCH] feat(cursor): ship socket's hosted oauth mcp server in the cursor plugin The Cursor plugin declares https://mcp.socket.dev/ in .cursor-plugin/mcp.json under the server name `socket`, the same name the socket-connect marketplace entry and the Gemini extension use, so users sign in to Socket in the browser instead of pasting one org's API key. The manifest also gets a display name, a logo, and a rule that tells the agent when to use the Socket MCP tools and how to fall back to the CLI if sign-in does not complete. The shipped-content gates now scan .cursor-plugin/rules, including .mdc files, because agents read and obey them. --- .cursor-plugin/logo.svg | 1 + .cursor-plugin/plugin.json | 3 ++ .cursor-plugin/rules/socket-mcp.mdc | 36 +++++++++++++++ README.md | 13 ++++-- .../shipped-content-is-consumer-clean.mts | 5 +- scripts/repo/constants/shipped-surfaces.mts | 6 +++ scripts/repo/generate-cursor-plugin.mts | 32 ++++++++++++- .../repo/unit/generate-cursor-plugin.test.mts | 46 +++++++++++++++++++ .../shipped-surfaces-injection-clean.test.mts | 10 +++- 9 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 .cursor-plugin/logo.svg create mode 100644 .cursor-plugin/rules/socket-mcp.mdc 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 })) {