Skip to content
Open
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
113 changes: 113 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,119 @@ pkg:pypi/fastapi@0.100.0: supply_chain: 1.0, quality: 0.95, maintenance: 0.98, v
Report: https://socket.dev/pypi/package/fastapi
```

#### organizations

List the Socket organizations the authenticated user belongs to. Takes no parameters. Use it to discover the `org_slug` value that the org-scoped tools (`alerts`, `threat_feed`) require.

This tool needs a Socket API token. See [Authentication for organization-scoped tools](#authentication-for-organization-scoped-tools) below.

#### alerts

List the latest security alerts for one Socket organization: supply-chain, vulnerability, quality, license, and maintenance issues across the org's monitored packages. Backed by `GET /v0/orgs/{org_slug}/alerts`. Results are paginated; pass the previous response's `endCursor` as `cursor` to fetch the next page.

| Parameter | Type | Required | Default | Description |
| --------------- | ------- | -------- | ------- | ------------------------------------------------------------------------------------- |
| `org_slug` | String | ✅ Yes | - | Organization slug (get it from the `organizations` tool) |
| `severity` | String | No | - | Comma-separated subset of `low,medium,high,critical` |
| `status` | String | No | - | `open` or `cleared` |
| `category` | String | No | - | Comma-separated subset of `supplyChainRisk,maintenance,quality,license,vulnerability` |
| `artifact_type` | String | No | - | Comma-separated ecosystems: `npm,pypi,gem,maven,golang,nuget,cargo,chrome,openvsx` |
| `artifact_name` | String | No | - | Restrict to a single package name |
| `alert_type` | String | No | - | Comma-separated Socket alert types (e.g. `usesEval,unmaintained`) |
| `repo_slug` | String | No | - | Comma-separated repository slugs |
| `per_page` | Integer | No | `100` | Results per page (1–5000) |
| `cursor` | String | No | - | Pagination cursor — the `endCursor` from a previous response |

#### threat_feed

Look up items in a Socket organization's threat feed: packages recently flagged as malware, typosquats, obfuscated code, and similar. Backed by `GET /v0/orgs/{org_slug}/threat-feed`. The response carries a `nextPageCursor`; pass it as `cursor` to page forward.

| Parameter | Type | Required | Default | Description |
| ------------------- | ------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------- |
| `org_slug` | String | ✅ Yes | - | Organization slug (get it from the `organizations` tool) |
| `filter` | String | No | `mal` | Threat category: `mal` (malware), `vuln`, `typ` (typosquat), `obf` (obfuscated), `mjo`, `kes`, `spy`, etc. |
| `ecosystem` | String | No | - | Ecosystem: `npm`, `pypi`, `gem`, `maven`, `golang`, `nuget`, `cargo`, `chrome`, `openvsx`, `huggingface` |
| `name` | String | No | - | Filter by package name |
| `version` | String | No | - | Filter by package version |
| `is_human_reviewed` | Boolean | No | `false` | Only return human-reviewed items |
| `sort` | String | No | `updated_at` | Sort field: `id`, `created_at`, `updated_at` |
| `direction` | String | No | `desc` | Sort direction: `asc`, `desc` |
| `updated_after` | String | No | - | ISO timestamp; only items updated after this |
| `created_after` | String | No | - | ISO timestamp; only items created after this |
| `per_page` | Integer | No | `30` | Results per page (1–100) |
| `cursor` | String | No | - | Pagination cursor — the `nextPageCursor` from a previous response |

#### package_files

List the files published in a package: a tree of paths and sizes for any package on a supported ecosystem. Use it to inspect what a dependency ships before installing it. Each entry prints a blob `hash` that `package_file_contents` and `package_file_grep` consume.

| Parameter | Type | Required | Default | Description |
| ------------ | ------ | -------- | ------- | --------------------------------------------------------------------------------------- |
| `ecosystem` | String | No | `npm` | `npm`, `pypi`, `gem`, `cargo`, `maven`, `golang`, `nuget`, `chrome`, `openvsx` |
| `depname` | String | ✅ Yes | - | Package name (e.g. `lodash`, `@babel/core`, `org.springframework:spring-core`) |
| `version` | String | ✅ Yes | - | Package version |
| `artifactId` | String | No | - | Per-version disambiguator (PyPI filename, Maven artifact id, NuGet asset) |
| `platform` | String | No | - | Platform qualifier for per-OS/arch artifacts (e.g. openvsx `linux-x64`, `darwin-arm64`) |

#### package_file_contents

Read a single file from a package. Pass the `hash` printed next to an entry in `package_files` output. Returns up to 1 MB of UTF-8 text; binary files return metadata only.

| Parameter | Type | Required | Default | Description |
| --------- | ------ | -------- | ------- | ------------------------------------------------------- |
| `hash` | String | ✅ Yes | - | Blob hash from `package_files` |
| `path` | String | No | - | File path, for display only; does not affect the lookup |

#### package_file_grep

Search a single file from a package for lines matching a JavaScript regular expression, returning matches with line numbers (grep -n style). The file is fetched once per session and cached, so repeated greps on the same hash skip the network.

| Parameter | Type | Required | Default | Description |
| ----------------- | ------- | -------- | ------- | ------------------------------------------------------- |
| `hash` | String | ✅ Yes | - | Blob hash from `package_files` |
| `pattern` | String | ✅ Yes | - | JavaScript regular expression (plain literals work too) |
| `caseInsensitive` | Boolean | No | `false` | Match case-insensitively |
| `contextLines` | Integer | No | `0` | Lines of context before and after each match (0–5) |
| `maxMatches` | Integer | No | `100` | Cap on matching lines returned (1–500) |
| `path` | String | No | - | File path, for display only; does not affect the lookup |

### Authentication for organization-scoped tools

`depscore` works without credentials on the public server. The `organizations`, `alerts`, `threat_feed`, and `package_files` tools call Socket's authenticated REST API, so they need a Socket API token.

How the server resolves a token depends on the transport:

- **stdio mode** reads one token at startup from the environment and uses it for every request. Set `SOCKET_API_TOKEN`. The server also accepts these aliases, in priority order: `SOCKET_API_TOKEN` → `SOCKET_API_KEY` → `SOCKET_CLI_API_TOKEN` → `SOCKET_CLI_API_KEY` → `SOCKET_SECURITY_API_TOKEN` → `SOCKET_SECURITY_API_KEY`. `SOCKET_API_TOKEN` is canonical; `SOCKET_API_KEY` is the alias most local setups already export. Because the process belongs to one user, this token is yours and scopes every tool to your account.
- **HTTP mode** scopes the organization tools to the caller, never to the server's own token. Send your Socket API token as an `Authorization: Bearer <token>` header on each request, or use an OAuth access token when the server runs OAuth. The server uses that per-request token for the Socket API calls it makes on your behalf. A shared deployment never answers `organizations`, `alerts`, `threat_feed`, or `package_files` with the operator's data: when a request carries no token, those tools return the auth-required error. `depscore` alone may fall back to the server's startup token, since package scores are the same for every caller.

Generate a token from the [Socket dashboard](https://socket.dev/) under API tokens, then export it before launching the server:

```sh
export SOCKET_API_TOKEN="your-socket-api-token"
```

When no token is available, these tools return an authentication-required error explaining how to supply one for each transport.

### Worked example: organization details and alerts

With `SOCKET_API_KEY` (or `SOCKET_API_TOKEN`) set, ask your assistant something like "show me the open critical alerts for my Socket org". Under the hood the assistant chains two tools:

1. **Discover the org slug.** Call `organizations` (no arguments). The server reads your token, calls `GET /v0/organizations`, and returns the organizations your token can see. Pick the `slug` you want, e.g. `my-org`.

2. **Fetch alerts for that org.** Call `alerts` with the slug and any filters:

```json
{
"org_slug": "my-org",
"severity": "high,critical",
"status": "open"
}
```

The server calls `GET /v0/orgs/my-org/alerts` with the same token and returns the matching alerts plus pagination metadata. To page forward, pass the response's `endCursor` back as `cursor`.

The same token scopes every org-scoped tool, so `threat_feed` and `package_files` work the moment `organizations` confirms which slug the token belongs to.

### Adjusting tool usage via client rules

You can customize how the MCP server interacts with your AI assistant by editing your client's rules file:
Expand Down
8 changes: 8 additions & 0 deletions docs/agents.md/repo/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ socket-mcp is the **Socket Model Context Protocol server** — exposes Socket de
- `lib/` — tool implementations and Socket API wrappers.
- `artifacts.test.ts` — co-located unit tests; additional fixtures under `docs/`.

## Tools

Registered in `lib/server.ts` (`buildToolSpecs`); user-facing reference lives in `README.md` under "Tools exposed".

- `depscore` — dependency scores; works without a token on the public server.
- `organizations` / `alerts` / `threat_feed` — org-scoped Socket REST API; require a token resolved by `resolveAuthToken` (per-request OAuth in HTTP mode, else the boot-time static key from `setStaticApiKey`).
- `package_files` / `package_file_contents` / `package_file_grep` — package file listing, read, and grep; `package_files` requires a token, the read/grep tools resolve a cached blob by hash.

## Commands

- Install: `pnpm install`.
Expand Down
20 changes: 10 additions & 10 deletions hooks/socket-gate/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -36,34 +36,34 @@ const MCP_URL = 'https://mcp.socket.dev/'
const SUPPLY_CHAIN_THRESHOLD = 20
const REQUEST_TIMEOUT_MS = 10_000

type Ecosystem = 'npm' | 'pypi' | 'cargo' | 'gem' | 'golang' | 'nuget'
export type Ecosystem = 'npm' | 'pypi' | 'cargo' | 'gem' | 'golang' | 'nuget'

interface HookInput {
export interface HookInput {
session_id: string
tool_name: string
tool_input: Record<string, unknown> | string
}

const INSTALL_PATTERNS: Array<{ ecosystem: Ecosystem; pattern: RegExp }> = [
{ ecosystem: 'npm', pattern: /\bnpm\s+(?:add|i|install)\s+([^\s-][^\s]*)/i },
{ ecosystem: 'npm', pattern: /\bnpm\s+(?:add|i|install)\s+([^\s-][^\s]*)/i }, // socket-lint: allow uncommented-regex
{ ecosystem: 'npm', pattern: /\byarn\s+add\s+([^\s-][^\s]*)/i },
{ ecosystem: 'npm', pattern: /\bpnpm\s+add\s+([^\s-][^\s]*)/i },
{ ecosystem: 'npm', pattern: /\bbun\s+add\s+([^\s-][^\s]*)/i },
{
ecosystem: 'pypi',
pattern: /(?:\bpython3?\s+-m\s+)?\bpip3?\s+install\s+([^\s-][^\s]*)/i,
pattern: /(?:\bpython3?\s+-m\s+)?\bpip3?\s+install\s+([^\s-][^\s]*)/i, // socket-lint: allow uncommented-regex
},
{ ecosystem: 'pypi', pattern: /\buv\s+add\s+([^\s-][^\s]*)/i },
{ ecosystem: 'pypi', pattern: /\buv\s+pip\s+install\s+([^\s-][^\s]*)/i },
{ ecosystem: 'pypi', pattern: /\bpoetry\s+add\s+([^\s-][^\s]*)/i },
{ ecosystem: 'pypi', pattern: /\bpipenv\s+install\s+([^\s-][^\s]*)/i },
{
ecosystem: 'cargo',
pattern: /\bcargo\s+(?:add|install)\s+([^\s-][^\s]*)/i,
pattern: /\bcargo\s+(?:add|install)\s+([^\s-][^\s]*)/i, // socket-lint: allow uncommented-regex
},
{ ecosystem: 'gem', pattern: /\bgem\s+install\s+([^\s-][^\s]*)/i },
{ ecosystem: 'gem', pattern: /\bbundle\s+add\s+([^\s-][^\s]*)/i },
{ ecosystem: 'golang', pattern: /\bgo\s+(?:get|install)\s+([^\s-][^\s]*)/i },
{ ecosystem: 'golang', pattern: /\bgo\s+(?:get|install)\s+([^\s-][^\s]*)/i }, // socket-lint: allow uncommented-regex
{ ecosystem: 'nuget', pattern: /\bdotnet\s+add\s+package\s+([^\s-][^\s]*)/i },
{ ecosystem: 'nuget', pattern: /\bnuget\s+install\s+([^\s-][^\s]*)/i },
]
Expand Down Expand Up @@ -184,7 +184,7 @@ export function outputAllow(): void {
permissionDecision: 'allow',
},
})
process.stdout.write(payload) // socket-hook: allow console
process.stdout.write(payload) // socket-lint: allow process-stdio
}

export function outputDeny(reason: string): void {
Expand All @@ -195,11 +195,11 @@ export function outputDeny(reason: string): void {
permissionDecisionReason: reason,
},
})
process.stdout.write(payload) // socket-hook: allow console
process.stdout.write(payload) // socket-lint: allow process-stdio
}

export function parseSupplyChainScore(text: string): number | undefined {
const match = text.match(/supplyChain:\s*(\d+(?:\.\d+)?)/i)
const match = text.match(/supplyChain:\s*(\d+(?:\.\d+)?)/i) // socket-lint: allow uncommented-regex
return match ? Number(match[1]) : undefined
}

Expand Down Expand Up @@ -274,7 +274,7 @@ async function main(): Promise<void> {
// not a hard gate — see the file header). Surface the error on stderr so
// the failure is observable; stdout stays the allow/deny IPC channel.
const errLine = `socket-gate: check failed for ${target.ecosystem}/${target.name}, failing open: ${errorMessage(e)}\n`
process.stderr.write(errLine) // socket-hook: allow console
process.stderr.write(errLine) // socket-lint: allow process-stdio
outputAllow()
}
}
Expand Down
6 changes: 5 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ async function main(): Promise<void> {
}
}

setStaticApiKey(apiKey)
// `shared` marks the static key as a deploy-operator key in HTTP mode, so
// per-tenant tools refuse to fall back to it and use the caller's
// per-request token instead. In stdio mode the static key is the local
// user's own token, so it stays usable everywhere.
setStaticApiKey(apiKey, { shared: useHttp })

if (oauthEnabled && oauthEnabledResult) {
try {
Expand Down
4 changes: 2 additions & 2 deletions lib/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ export interface ArtifactData {
[key: string]: unknown
}

type PlatformPattern = RegExp
export type PlatformPattern = RegExp

const PLATFORM_PATTERNS: Record<string, PlatformPattern[]> = {
'darwin-arm64': [/macosx.*arm64/i],
'darwin-x64': [/macosx.*x86_64/i],
'linux-arm64': [/(linux|manylinux).*(aarch64|arm64)/i],
'linux-arm64': [/(linux|manylinux).*(aarch64|arm64)/i], // socket-lint: allow uncommented-regex
'linux-x64': [/(linux|manylinux).*x86_64/i],
'win32-ia32': [/win.*win32/i],
'win32-x64': [/win.*(amd64|x86_64)/i],
Expand Down
6 changes: 3 additions & 3 deletions lib/blob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ export interface FetchBlobOptions {

const DEFAULT_MAX_BYTES = 1024 * 1024 // 1 MB

interface ChunkedManifest {
export interface ChunkedManifest {
_version?: string | undefined
size?: number | undefined
chunks?: unknown | undefined
offset?: unknown | undefined
}

interface RawFetchResult {
export interface RawFetchResult {
bytes: Uint8Array
contentType: string | undefined
}

interface ChunkedFetchResult {
export interface ChunkedFetchResult {
// Concatenated chunk bytes, possibly less than `totalSize` when stopped
// early at maxBytes.
bytes: Uint8Array
Expand Down
6 changes: 3 additions & 3 deletions lib/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,18 @@ export interface FetchFileListOptions {
onRequest?: ((url: string) => void) | undefined
}

interface RawFileEntry {
export interface RawFileEntry {
path?: unknown | undefined
type?: unknown | undefined
size?: unknown | undefined
hash?: unknown | undefined
}

interface RawFileListResponse {
export interface RawFileListResponse {
files?: RawFileEntry[] | undefined
}

interface TreeNode {
export interface TreeNode {
name: string
isFile: boolean
size?: number | undefined
Expand Down
2 changes: 1 addition & 1 deletion lib/http-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function assertSafeHttpUrl(
throw new Error(`${label} must be http(s): ${rawUrl}`)
}
const host = url.hostname.toLowerCase()
const isLocal = host === '127.0.0.1' || host === '::1' || host === 'localhost'
const isLocal = host === '::1' || host === '127.0.0.1' || host === 'localhost'
if (isLocal && allowLocalhost) {
return url
}
Expand Down
Loading
Loading