Skip to content

Commit eb50c0c

Browse files
authored
Merge pull request #155 from SocketDev/feat/purl-encoding-fix
feat: use packageurl-js for correct PURL encoding across ecosystems
2 parents 1c88679 + 14de96b commit eb50c0c

6 files changed

Lines changed: 4420 additions & 8 deletions

File tree

index.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
66
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
77
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'
88
import { randomUUID } from 'node:crypto'
9+
import { buildPurl } from './lib/purl.ts'
910
import { z } from 'zod'
1011
import pino from 'pino'
1112
import readline from 'readline'
@@ -411,16 +412,14 @@ function createConfiguredServer (): McpServer {
411412
}
412413
}
413414

414-
// Build components array for the API request
415+
// Build components array for the API request. Use packageurl-js for correct PURL encoding
416+
// across ecosystems (e.g. @ in npm scoped packages, maven groupId:artifactId).
415417
const components = packages.map((pkg: { ecosystem?: string; depname: string; version?: string }) => {
416418
const cleanedVersion = (pkg.version ?? 'unknown').replace(/[\^~]/g, '') // Remove ^ and ~ from version
417419
const ecosystem = pkg.ecosystem ?? 'npm'
418-
let purl: string
419-
if (cleanedVersion === '1.0.0' || cleanedVersion === 'unknown' || !cleanedVersion) {
420-
purl = `pkg:${ecosystem}/${pkg.depname}`
421-
} else {
420+
const purl = buildPurl(ecosystem, pkg.depname, cleanedVersion)
421+
if (cleanedVersion !== '1.0.0' && cleanedVersion !== 'unknown' && cleanedVersion) {
422422
logger.info(`Using version ${cleanedVersion} for ${pkg.depname}`)
423-
purl = `pkg:${ecosystem}/${pkg.depname}@${cleanedVersion}`
424423
}
425424
return { purl }
426425
})
@@ -488,7 +487,8 @@ function createConfiguredServer (): McpServer {
488487

489488
// Process each result
490489
for (const jsonData of jsonLines) {
491-
const purl: string = `pkg:${jsonData.type || 'unknown'}/${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`
490+
const ns = jsonData.namespace ? `${jsonData.namespace}/` : ''
491+
const purl: string = `pkg:${jsonData.type || 'unknown'}/${ns}${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`
492492
if (jsonData.score && jsonData.score.overall !== undefined) {
493493
const scoreEntries = Object.entries(jsonData.score)
494494
.filter(([key]) => key !== 'overall' && key !== 'uuid')
@@ -506,7 +506,8 @@ function createConfiguredServer (): McpServer {
506506
}
507507
} else {
508508
const jsonData = JSON.parse(responseText)
509-
const purl: string = `pkg:${jsonData.type || 'unknown'}/${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`
509+
const ns = jsonData.namespace ? `${jsonData.namespace}/` : ''
510+
const purl: string = `pkg:${jsonData.type || 'unknown'}/${ns}${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`
510511
if (jsonData.score && jsonData.score.overall !== undefined) {
511512
const scoreEntries = Object.entries(jsonData.score)
512513
.filter(([key]) => key !== 'overall' && key !== 'uuid')

lib/purl.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { PackageURL } from 'packageurl-js'
2+
3+
/**
4+
* Build a PURL using packageurl-js for correct encoding across all ecosystems.
5+
* Handles namespace/name splitting per ecosystem (e.g. npm scoped @scope/name, maven groupId:artifactId).
6+
*/
7+
export function buildPurl (ecosystem: string, depname: string, version: string): string {
8+
const type = ecosystem.toLowerCase()
9+
let namespace: string | undefined
10+
let name: string
11+
12+
if (type === 'npm' && depname.startsWith('@') && depname.includes('/')) {
13+
const slash = depname.indexOf('/')
14+
namespace = depname.slice(0, slash)
15+
name = depname.slice(slash + 1)
16+
} else if (type === 'maven' && (depname.includes(':') || depname.includes('/'))) {
17+
const sep = depname.includes(':') ? ':' : '/'
18+
const idx = depname.indexOf(sep)
19+
namespace = depname.slice(0, idx)
20+
name = depname.slice(idx + 1)
21+
} else if (type === 'golang' && depname.includes('/')) {
22+
const lastSlash = depname.lastIndexOf('/')
23+
namespace = depname.slice(0, lastSlash)
24+
name = depname.slice(lastSlash + 1)
25+
} else {
26+
name = depname
27+
}
28+
29+
const purlVersion = (version === 'unknown' || version === '1.0.0' || !version) ? undefined : version
30+
const purl = new PackageURL(type, namespace ?? undefined, name, purlVersion ?? undefined)
31+
return purl.toString()
32+
}

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
"index.js",
4040
"index.d.ts",
4141
"index.d.ts.map",
42+
"lib/**/*.js",
43+
"lib/**/*.d.ts",
4244
"mock-client/**/*.js",
4345
"mock-client/**/*.d.ts*"
4446
],
@@ -51,6 +53,7 @@
5153
"dependencies": {
5254
"@anthropic-ai/mcpb": "^1.1.0",
5355
"@modelcontextprotocol/sdk": "1.26.0",
56+
"packageurl-js": "^2.0.1",
5457
"pino": "^10.0.0",
5558
"pino-pretty": "^13.0.0",
5659
"semver": "^7.7.2",

0 commit comments

Comments
 (0)