Skip to content

Commit 93d6455

Browse files
committed
feat: add Socket.dev report URL to depscore output
Include a link to the full Socket.dev report page for each package when a score is returned. Users can click through for deeper analysis when scores raise concerns. Report URL is omitted when package is not found. Made-with: Cursor
1 parent 32aae45 commit 93d6455

4 files changed

Lines changed: 117 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,9 @@ The `depscore` tool allows AI assistants to query the Socket API for dependency
260260
**Sample Response:**
261261
```
262262
pkg:npm/express@4.18.2: supply_chain: 1.0, quality: 0.9, maintenance: 1.0, vulnerability: 1.0, license: 1.0
263+
Report: https://socket.dev/npm/package/express
263264
pkg:pypi/fastapi@0.100.0: supply_chain: 1.0, quality: 0.95, maintenance: 0.98, vulnerability: 1.0, license: 1.0
265+
Report: https://socket.dev/pypi/package/fastapi
264266
```
265267

266268
### How to Use the Socket MCP Server

index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'
88
import { randomUUID } from 'node:crypto'
99
import { buildPurl } from './lib/purl.ts'
1010
import { deduplicateArtifacts } from './lib/artifacts.ts'
11+
import { buildSocketReportUrl } from './lib/socket-url.ts'
1112
import { z } from 'zod'
1213
import pino from 'pino'
1314
import readline from 'readline'
@@ -493,6 +494,7 @@ function createConfiguredServer (): McpServer {
493494
const ns = jsonData.namespace ? `${jsonData.namespace}/` : ''
494495
const purl: string = `pkg:${jsonData.type || 'unknown'}/${ns}${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`
495496
if (jsonData.score && jsonData.score['overall'] !== undefined) {
497+
const reportUrl = buildSocketReportUrl(jsonData)
496498
const scoreEntries = Object.entries(jsonData.score)
497499
.filter(([key]) => key !== 'overall' && key !== 'uuid')
498500
.map(([key, value]) => {
@@ -502,7 +504,7 @@ function createConfiguredServer (): McpServer {
502504
})
503505
.join(', ')
504506

505-
results.push(`${purl}: ${scoreEntries}`)
507+
results.push(`${purl}: ${scoreEntries}\n Report: ${reportUrl}`)
506508
} else {
507509
results.push(`${purl}: No score found`)
508510
}
@@ -512,6 +514,7 @@ function createConfiguredServer (): McpServer {
512514
const ns = jsonData.namespace ? `${jsonData.namespace}/` : ''
513515
const purl: string = `pkg:${jsonData.type || 'unknown'}/${ns}${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`
514516
if (jsonData.score && jsonData.score.overall !== undefined) {
517+
const reportUrl = buildSocketReportUrl(jsonData)
515518
const scoreEntries = Object.entries(jsonData.score)
516519
.filter(([key]) => key !== 'overall' && key !== 'uuid')
517520
.map(([key, value]) => {
@@ -521,7 +524,7 @@ function createConfiguredServer (): McpServer {
521524
})
522525
.join(', ')
523526
524-
results.push(`${purl}: ${scoreEntries}`)
527+
results.push(`${purl}: ${scoreEntries}\n Report: ${reportUrl}`)
525528
} else {
526529
results.push(`${purl}: No score found`)
527530
}

lib/socket-url.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env node
2+
import { test } from 'node:test'
3+
import assert from 'node:assert'
4+
import { buildSocketReportUrl } from './socket-url.ts'
5+
6+
test('buildSocketReportUrl produces correct URLs across ecosystems', async (t) => {
7+
await t.test('npm unscoped', () => {
8+
assert.strictEqual(
9+
buildSocketReportUrl({ type: 'npm', name: 'express' }),
10+
'https://socket.dev/npm/package/express'
11+
)
12+
})
13+
14+
await t.test('npm scoped', () => {
15+
assert.strictEqual(
16+
buildSocketReportUrl({ type: 'npm', namespace: 'babel', name: 'core' }),
17+
'https://socket.dev/npm/package/@babel/core'
18+
)
19+
})
20+
21+
await t.test('pypi', () => {
22+
assert.strictEqual(
23+
buildSocketReportUrl({ type: 'pypi', name: 'requests' }),
24+
'https://socket.dev/pypi/package/requests'
25+
)
26+
})
27+
28+
await t.test('golang', () => {
29+
assert.strictEqual(
30+
buildSocketReportUrl({ type: 'golang', namespace: 'github.com/gin-gonic', name: 'gin' }),
31+
'https://socket.dev/golang/package/github.com/gin-gonic/gin'
32+
)
33+
})
34+
35+
await t.test('maven', () => {
36+
assert.strictEqual(
37+
buildSocketReportUrl({ type: 'maven', namespace: 'org.apache.commons', name: 'commons-lang3' }),
38+
'https://socket.dev/maven/package/org.apache.commons/commons-lang3'
39+
)
40+
})
41+
42+
await t.test('cargo', () => {
43+
assert.strictEqual(
44+
buildSocketReportUrl({ type: 'cargo', name: 'serde' }),
45+
'https://socket.dev/cargo/package/serde'
46+
)
47+
})
48+
49+
await t.test('gem', () => {
50+
assert.strictEqual(
51+
buildSocketReportUrl({ type: 'gem', name: 'rails' }),
52+
'https://socket.dev/gem/package/rails'
53+
)
54+
})
55+
56+
await t.test('nuget', () => {
57+
assert.strictEqual(
58+
buildSocketReportUrl({ type: 'nuget', name: 'Newtonsoft.Json' }),
59+
'https://socket.dev/nuget/package/Newtonsoft.Json'
60+
)
61+
})
62+
63+
await t.test('handles unknown/missing data gracefully', () => {
64+
assert.strictEqual(
65+
buildSocketReportUrl({}),
66+
'https://socket.dev/npm/package/unknown'
67+
)
68+
assert.strictEqual(
69+
buildSocketReportUrl(null),
70+
'https://socket.dev/npm/package/unknown'
71+
)
72+
})
73+
})

lib/socket-url.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const SOCKET_REPORT_BASE = 'https://socket.dev'
2+
3+
/**
4+
* Build the Socket.dev report URL for a package so users can click through
5+
* for deeper analysis when a score raises concerns.
6+
*/
7+
export function buildSocketReportUrl (data: unknown): string {
8+
const obj = data && typeof data === 'object' ? data as Record<string, unknown> : Object.create(null)
9+
const type = obj.type
10+
const name = obj.name
11+
const namespace = obj.namespace
12+
const ecosystem = (typeof type === 'string' ? type : 'npm').toLowerCase()
13+
const pkgName = typeof name === 'string' ? name : 'unknown'
14+
const ns = typeof namespace === 'string' ? namespace : undefined
15+
16+
let packagePath: string
17+
switch (ecosystem) {
18+
case 'npm':
19+
packagePath = ns ? `@${ns}/${pkgName}` : pkgName
20+
break
21+
case 'pypi':
22+
case 'gem':
23+
case 'nuget':
24+
case 'cargo':
25+
packagePath = pkgName
26+
break
27+
case 'golang':
28+
case 'maven':
29+
case 'composer':
30+
packagePath = ns ? `${ns}/${pkgName}` : pkgName
31+
break
32+
default:
33+
packagePath = ns ? `${ns}/${pkgName}` : pkgName
34+
}
35+
36+
return `${SOCKET_REPORT_BASE}/${ecosystem}/package/${packagePath}`
37+
}

0 commit comments

Comments
 (0)