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
1 change: 1 addition & 0 deletions .cursor-plugin/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
36 changes: 36 additions & 0 deletions .cursor-plugin/rules/socket-mcp.mdc
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion scripts/repo/check/shipped-content-is-consumer-clean.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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]!
Expand Down
6 changes: 6 additions & 0 deletions scripts/repo/constants/shipped-surfaces.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 30 additions & 2 deletions scripts/repo/generate-cursor-plugin.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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/.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<string, unknown> = {
name,
displayName: CURSOR_DISPLAY_NAME,
skills: 'skills',
rules: CURSOR_RULES_RELATIVE,
mcpServers: CURSOR_MCP_RELATIVE,
logo: CURSOR_LOGO_RELATIVE,
}

const optionalKeys = [
Expand All @@ -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]!
Expand All @@ -103,7 +131,7 @@ export const SOCKET_MCP_URL = 'https://mcp.socket.dev/'
export function buildMcpConfig(): Record<string, unknown> {
return {
mcpServers: {
socket: { url: SOCKET_MCP_URL },
[SOCKET_MCP_SERVER_NAME]: { url: SOCKET_MCP_URL },
},
}
}
Expand Down
46 changes: 46 additions & 0 deletions test/repo/unit/generate-cursor-plugin.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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 = {
Expand Down Expand Up @@ -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), '<svg/>\n', 'utf-8')
return root
}

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand All @@ -25,6 +28,7 @@ const REPO_ROOT = path.resolve(
const SCANNED_EXTENSIONS = new Set([
'.json',
'.md',
'.mdc',
'.mdx',
'.mjs',
'.mts',
Expand Down Expand Up @@ -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 })) {
Expand Down
Loading