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
17 changes: 9 additions & 8 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'
import { randomUUID } from 'node:crypto'
import { buildPurl } from './lib/purl.ts'
import { z } from 'zod'
import pino from 'pino'
import readline from 'readline'
Expand Down Expand Up @@ -411,16 +412,14 @@ function createConfiguredServer (): McpServer {
}
}

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

// Process each result
for (const jsonData of jsonLines) {
const purl: string = `pkg:${jsonData.type || 'unknown'}/${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`
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 scoreEntries = Object.entries(jsonData.score)
.filter(([key]) => key !== 'overall' && key !== 'uuid')
Expand All @@ -506,7 +506,8 @@ function createConfiguredServer (): McpServer {
}
} else {
const jsonData = JSON.parse(responseText)
const purl: string = `pkg:${jsonData.type || 'unknown'}/${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`
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 scoreEntries = Object.entries(jsonData.score)
.filter(([key]) => key !== 'overall' && key !== 'uuid')
Expand Down
32 changes: 32 additions & 0 deletions lib/purl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { PackageURL } from 'packageurl-js'

/**
* Build a PURL using packageurl-js for correct encoding across all ecosystems.
* Handles namespace/name splitting per ecosystem (e.g. npm scoped @scope/name, maven groupId:artifactId).
*/
export function buildPurl (ecosystem: string, depname: string, version: string): string {
const type = ecosystem.toLowerCase()
let namespace: string | undefined
let name: string

if (type === 'npm' && depname.startsWith('@') && depname.includes('/')) {
const slash = depname.indexOf('/')
namespace = depname.slice(0, slash)
name = depname.slice(slash + 1)
} else if (type === 'maven' && (depname.includes(':') || depname.includes('/'))) {
const sep = depname.includes(':') ? ':' : '/'
const idx = depname.indexOf(sep)
namespace = depname.slice(0, idx)
name = depname.slice(idx + 1)
} else if (type === 'golang' && depname.includes('/')) {
const lastSlash = depname.lastIndexOf('/')
namespace = depname.slice(0, lastSlash)
name = depname.slice(lastSlash + 1)
} else {
name = depname
}

const purlVersion = (version === 'unknown' || version === '1.0.0' || !version) ? undefined : version
const purl = new PackageURL(type, namespace ?? undefined, name, purlVersion ?? undefined)
return purl.toString()
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
"index.js",
"index.d.ts",
"index.d.ts.map",
"lib/**/*.js",
"lib/**/*.d.ts",
"mock-client/**/*.js",
"mock-client/**/*.d.ts*"
],
Expand All @@ -51,6 +53,7 @@
"dependencies": {
"@anthropic-ai/mcpb": "^1.1.0",
"@modelcontextprotocol/sdk": "1.26.0",
"packageurl-js": "^2.0.1",
"pino": "^10.0.0",
"pino-pretty": "^13.0.0",
"semver": "^7.7.2",
Expand Down
Loading