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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,9 @@ The `depscore` tool allows AI assistants to query the Socket API for dependency
**Sample Response:**
```
pkg:npm/express@4.18.2: supply_chain: 1.0, quality: 0.9, maintenance: 1.0, vulnerability: 1.0, license: 1.0
Report: https://socket.dev/npm/package/express
pkg:pypi/fastapi@0.100.0: supply_chain: 1.0, quality: 0.95, maintenance: 0.98, vulnerability: 1.0, license: 1.0
Report: https://socket.dev/pypi/package/fastapi
```

### How to Use the Socket MCP Server
Expand Down
7 changes: 5 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'
import { randomUUID } from 'node:crypto'
import { buildPurl } from './lib/purl.ts'
import { deduplicateArtifacts } from './lib/artifacts.ts'
import { buildSocketReportUrl } from './lib/socket-url.ts'
import { z } from 'zod'
import pino from 'pino'
import readline from 'readline'
Expand Down Expand Up @@ -493,6 +494,7 @@ function createConfiguredServer (): McpServer {
const ns = jsonData.namespace ? `${jsonData.namespace}/` : ''
const purl: string = `pkg:${jsonData.type || 'unknown'}/${ns}${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`
if (jsonData.score && jsonData.score['overall'] !== undefined) {
const reportUrl = buildSocketReportUrl(jsonData)
const scoreEntries = Object.entries(jsonData.score)
.filter(([key]) => key !== 'overall' && key !== 'uuid')
.map(([key, value]) => {
Expand All @@ -502,7 +504,7 @@ function createConfiguredServer (): McpServer {
})
.join(', ')

results.push(`${purl}: ${scoreEntries}`)
results.push(`${purl}: ${scoreEntries}\n Report: ${reportUrl}`)
} else {
results.push(`${purl}: No score found`)
}
Expand All @@ -512,6 +514,7 @@ function createConfiguredServer (): McpServer {
const ns = jsonData.namespace ? `${jsonData.namespace}/` : ''
const purl: string = `pkg:${jsonData.type || 'unknown'}/${ns}${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`
if (jsonData.score && jsonData.score.overall !== undefined) {
const reportUrl = buildSocketReportUrl(jsonData)
const scoreEntries = Object.entries(jsonData.score)
.filter(([key]) => key !== 'overall' && key !== 'uuid')
.map(([key, value]) => {
Expand All @@ -521,7 +524,7 @@ function createConfiguredServer (): McpServer {
})
.join(', ')

results.push(`${purl}: ${scoreEntries}`)
results.push(`${purl}: ${scoreEntries}\n Report: ${reportUrl}`)
} else {
results.push(`${purl}: No score found`)
}
Expand Down
73 changes: 73 additions & 0 deletions lib/socket-url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env node
import { test } from 'node:test'
import assert from 'node:assert'
import { buildSocketReportUrl } from './socket-url.ts'

test('buildSocketReportUrl produces correct URLs across ecosystems', async (t) => {
await t.test('npm unscoped', () => {
assert.strictEqual(
buildSocketReportUrl({ type: 'npm', name: 'express' }),
'https://socket.dev/npm/package/express'
)
})

await t.test('npm scoped', () => {
assert.strictEqual(
buildSocketReportUrl({ type: 'npm', namespace: 'babel', name: 'core' }),
'https://socket.dev/npm/package/@babel/core'
)
})

await t.test('pypi', () => {
assert.strictEqual(
buildSocketReportUrl({ type: 'pypi', name: 'requests' }),
'https://socket.dev/pypi/package/requests'
)
})

await t.test('golang', () => {
assert.strictEqual(
buildSocketReportUrl({ type: 'golang', namespace: 'github.com/gin-gonic', name: 'gin' }),
'https://socket.dev/golang/package/github.com/gin-gonic/gin'
)
})

await t.test('maven', () => {
assert.strictEqual(
buildSocketReportUrl({ type: 'maven', namespace: 'org.apache.commons', name: 'commons-lang3' }),
'https://socket.dev/maven/package/org.apache.commons/commons-lang3'
)
})

await t.test('cargo', () => {
assert.strictEqual(
buildSocketReportUrl({ type: 'cargo', name: 'serde' }),
'https://socket.dev/cargo/package/serde'
)
})

await t.test('gem', () => {
assert.strictEqual(
buildSocketReportUrl({ type: 'gem', name: 'rails' }),
'https://socket.dev/gem/package/rails'
)
})

await t.test('nuget', () => {
assert.strictEqual(
buildSocketReportUrl({ type: 'nuget', name: 'Newtonsoft.Json' }),
'https://socket.dev/nuget/package/Newtonsoft.Json'
)
})

await t.test('handles unknown/missing data gracefully', () => {
assert.strictEqual(
buildSocketReportUrl({}),
'https://socket.dev/npm/package/unknown'
)
assert.strictEqual(
buildSocketReportUrl(null),
'https://socket.dev/npm/package/unknown'
)
})
})
37 changes: 37 additions & 0 deletions lib/socket-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const SOCKET_REPORT_BASE = 'https://socket.dev'

/**
* Build the Socket.dev report URL for a package so users can click through
* for deeper analysis when a score raises concerns.
*/
export function buildSocketReportUrl (data: unknown): string {
const obj = data && typeof data === 'object' ? data as Record<string, unknown> : Object.create(null)
const type = obj.type
const name = obj.name
const namespace = obj.namespace
const ecosystem = (typeof type === 'string' ? type : 'npm').toLowerCase()
const pkgName = typeof name === 'string' ? name : 'unknown'
const ns = typeof namespace === 'string' ? namespace : undefined

let packagePath: string
switch (ecosystem) {
case 'npm':
packagePath = ns ? `@${ns}/${pkgName}` : pkgName
break
case 'pypi':
case 'gem':
case 'nuget':
case 'cargo':
packagePath = pkgName
break
case 'golang':
case 'maven':
case 'composer':
packagePath = ns ? `${ns}/${pkgName}` : pkgName
break
default:
packagePath = ns ? `${ns}/${pkgName}` : pkgName
}

return `${SOCKET_REPORT_BASE}/${ecosystem}/package/${packagePath}`
}