@socketsecurity/mcp
Advanced tools
+236
-129
@@ -5,2 +5,4 @@ #!/usr/bin/env -S node --experimental-strip-types | ||
| import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; | ||
| import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; | ||
| import { randomUUID } from 'node:crypto'; | ||
| import { z } from 'zod'; | ||
@@ -62,71 +64,88 @@ import pino from 'pino'; | ||
| } | ||
| const server = new McpServer({ | ||
| name: 'socket', | ||
| version: VERSION, | ||
| }); | ||
| server.registerTool('depscore', { | ||
| title: 'Dependency Score Tool', | ||
| description: "Get the dependency score of packages with the `depscore` tool from Socket. Use 'unknown' for version if not known. Use this tool to scan dependencies for their quality and security on existing code or when code is generated. Stop generating code and ask the user how to proceed when any of the scores are low. When checking dependencies, make sure to also check the imports in the code, not just the manifest files (pyproject.toml, package.json, etc).", | ||
| inputSchema: z.object({ | ||
| packages: z.array(z.object({ | ||
| ecosystem: z.string().describe('The package ecosystem (e.g., npm, pypi)').default('npm'), | ||
| depname: z.string().describe('The name of the dependency'), | ||
| version: z.string().describe("The version of the dependency, use 'unknown' if not known").default('unknown'), | ||
| })).describe('Array of packages to check'), | ||
| }), | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| }, | ||
| }, async ({ packages }) => { | ||
| logger.info(`Received request for ${packages.length} packages`); | ||
| const components = packages.map(pkg => { | ||
| const cleanedVersion = (pkg.version ?? 'unknown').replace(/[\^~]/g, ''); | ||
| const ecosystem = pkg.ecosystem ?? 'npm'; | ||
| let purl; | ||
| if (cleanedVersion === '1.0.0' || cleanedVersion === 'unknown' || !cleanedVersion) { | ||
| purl = `pkg:${ecosystem}/${pkg.depname}`; | ||
| } | ||
| else { | ||
| logger.info(`Using version ${cleanedVersion} for ${pkg.depname}`); | ||
| purl = `pkg:${ecosystem}/${pkg.depname}@${cleanedVersion}`; | ||
| } | ||
| return { purl }; | ||
| }); | ||
| try { | ||
| const response = await fetch(SOCKET_API_URL, { | ||
| method: 'POST', | ||
| headers: buildSocketHeaders(), | ||
| body: JSON.stringify({ components }) | ||
| function createConfiguredServer() { | ||
| const srv = new McpServer({ name: 'socket', version: VERSION }); | ||
| srv.registerTool('depscore', { | ||
| title: 'Dependency Score Tool', | ||
| description: "Get the dependency score of packages with the `depscore` tool from Socket. Use 'unknown' for version if not known. Use this tool to scan dependencies for their quality and security on existing code or when code is generated. Stop generating code and ask the user how to proceed when any of the scores are low. When checking dependencies, make sure to also check the imports in the code, not just the manifest files (pyproject.toml, package.json, etc).", | ||
| inputSchema: { | ||
| packages: z.array(z.object({ | ||
| ecosystem: z.string().describe('The package ecosystem (e.g., npm, pypi)').default('npm'), | ||
| depname: z.string().describe('The name of the dependency'), | ||
| version: z.string().describe("The version of the dependency, use 'unknown' if not known").default('unknown'), | ||
| })).describe('Array of packages to check'), | ||
| }, | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| }, | ||
| }, async ({ packages }) => { | ||
| logger.info(`Received request for ${packages.length} packages`); | ||
| const components = packages.map((pkg) => { | ||
| const cleanedVersion = (pkg.version ?? 'unknown').replace(/[\^~]/g, ''); | ||
| const ecosystem = pkg.ecosystem ?? 'npm'; | ||
| let purl; | ||
| if (cleanedVersion === '1.0.0' || cleanedVersion === 'unknown' || !cleanedVersion) { | ||
| purl = `pkg:${ecosystem}/${pkg.depname}`; | ||
| } | ||
| else { | ||
| logger.info(`Using version ${cleanedVersion} for ${pkg.depname}`); | ||
| purl = `pkg:${ecosystem}/${pkg.depname}@${cleanedVersion}`; | ||
| } | ||
| return { purl }; | ||
| }); | ||
| const responseText = await response.text(); | ||
| if (response.status !== 200) { | ||
| const errorMsg = `Error processing packages: [${response.status}] ${responseText}`; | ||
| logger.error(errorMsg); | ||
| return { | ||
| content: [{ type: 'text', text: errorMsg }], | ||
| isError: true | ||
| }; | ||
| } | ||
| else if (!responseText.trim()) { | ||
| const errorMsg = 'No packages were found.'; | ||
| logger.error(errorMsg); | ||
| return { | ||
| content: [{ type: 'text', text: errorMsg }], | ||
| isError: true | ||
| }; | ||
| } | ||
| try { | ||
| const results = []; | ||
| if ((response.headers.get('content-type') || '').includes('x-ndjson')) { | ||
| const jsonLines = responseText.split('\n') | ||
| .filter(line => line.trim()) | ||
| .map(line => JSON.parse(line)); | ||
| if (!jsonLines.length) { | ||
| const errorMsg = 'No valid JSON objects found in NDJSON response'; | ||
| return { | ||
| content: [{ type: 'text', text: errorMsg }], | ||
| isError: true | ||
| }; | ||
| const response = await fetch(SOCKET_API_URL, { | ||
| method: 'POST', | ||
| headers: buildSocketHeaders(), | ||
| body: JSON.stringify({ components }) | ||
| }); | ||
| const responseText = await response.text(); | ||
| if (response.status !== 200) { | ||
| const errorMsg = `Error processing packages: [${response.status}] ${responseText}`; | ||
| logger.error(errorMsg); | ||
| return { | ||
| content: [{ type: 'text', text: errorMsg }], | ||
| isError: true | ||
| }; | ||
| } | ||
| else if (!responseText.trim()) { | ||
| const errorMsg = 'No packages were found.'; | ||
| logger.error(errorMsg); | ||
| return { | ||
| content: [{ type: 'text', text: errorMsg }], | ||
| isError: true | ||
| }; | ||
| } | ||
| try { | ||
| const results = []; | ||
| if ((response.headers.get('content-type') || '').includes('x-ndjson')) { | ||
| const jsonLines = responseText.split('\n') | ||
| .filter(line => line.trim()) | ||
| .map(line => JSON.parse(line)); | ||
| if (!jsonLines.length) { | ||
| const errorMsg = 'No valid JSON objects found in NDJSON response'; | ||
| return { | ||
| content: [{ type: 'text', text: errorMsg }], | ||
| isError: true | ||
| }; | ||
| } | ||
| for (const jsonData of jsonLines) { | ||
| const purl = `pkg:${jsonData.type || 'unknown'}/${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') | ||
| .map(([key, value]) => { | ||
| const numValue = Number(value); | ||
| const displayValue = numValue <= 1 ? Math.round(numValue * 100) : numValue; | ||
| return `${key}: ${displayValue}`; | ||
| }) | ||
| .join(', '); | ||
| results.push(`${purl}: ${scoreEntries}`); | ||
| } | ||
| else { | ||
| results.push(`${purl}: No score found`); | ||
| } | ||
| } | ||
| } | ||
| for (const jsonData of jsonLines) { | ||
| else { | ||
| const jsonData = JSON.parse(responseText); | ||
| const purl = `pkg:${jsonData.type || 'unknown'}/${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`; | ||
@@ -148,52 +167,35 @@ if (jsonData.score && jsonData.score.overall !== undefined) { | ||
| } | ||
| return { | ||
| content: [ | ||
| { | ||
| type: 'text', | ||
| text: results.length > 0 | ||
| ? `Dependency scores:\n${results.join('\n')}` | ||
| : 'No scores found for the provided packages' | ||
| } | ||
| ] | ||
| }; | ||
| } | ||
| else { | ||
| const jsonData = JSON.parse(responseText); | ||
| const purl = `pkg:${jsonData.type || 'unknown'}/${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') | ||
| .map(([key, value]) => { | ||
| const numValue = Number(value); | ||
| const displayValue = numValue <= 1 ? Math.round(numValue * 100) : numValue; | ||
| return `${key}: ${displayValue}`; | ||
| }) | ||
| .join(', '); | ||
| results.push(`${purl}: ${scoreEntries}`); | ||
| } | ||
| else { | ||
| results.push(`${purl}: No score found`); | ||
| } | ||
| catch (e) { | ||
| const error = e; | ||
| const errorMsg = `JSON parsing error: ${error.message} -- Response: ${responseText}`; | ||
| logger.error(errorMsg); | ||
| return { | ||
| content: [{ type: 'text', text: 'Error parsing response from Socket API' }], | ||
| isError: true | ||
| }; | ||
| } | ||
| return { | ||
| content: [ | ||
| { | ||
| type: 'text', | ||
| text: results.length > 0 | ||
| ? `Dependency scores:\n${results.join('\n')}` | ||
| : 'No scores found for the provided packages' | ||
| } | ||
| ] | ||
| }; | ||
| } | ||
| catch (e) { | ||
| const error = e; | ||
| const errorMsg = `JSON parsing error: ${error.message} -- Response: ${responseText}`; | ||
| const errorMsg = `Error processing packages: ${error.message}`; | ||
| logger.error(errorMsg); | ||
| return { | ||
| content: [{ type: 'text', text: 'Error parsing response from Socket API' }], | ||
| content: [{ type: 'text', text: 'Error connecting to Socket API' }], | ||
| isError: true | ||
| }; | ||
| } | ||
| } | ||
| catch (e) { | ||
| const error = e; | ||
| const errorMsg = `Error processing packages: ${error.message}`; | ||
| logger.error(errorMsg); | ||
| return { | ||
| content: [{ type: 'text', text: 'Error connecting to Socket API' }], | ||
| isError: true | ||
| }; | ||
| } | ||
| }); | ||
| }); | ||
| return srv; | ||
| } | ||
| const useHttp = process.env['MCP_HTTP_MODE'] === 'true' || process.argv.includes('--http'); | ||
@@ -214,3 +216,26 @@ const port = parseInt(process.env['MCP_PORT'] || '3000', 10); | ||
| logger.info(`Starting HTTP server on port ${port}`); | ||
| let httpTransport = null; | ||
| const sessions = new Map(); | ||
| function destroySession(id) { | ||
| const s = sessions.get(id); | ||
| if (!s) | ||
| return; | ||
| sessions.delete(id); | ||
| try { | ||
| s.transport.close(); | ||
| } | ||
| catch { } | ||
| s.server.close().catch(() => { }); | ||
| logger.info(`Session ${id} destroyed`); | ||
| } | ||
| const SESSION_TTL_MS = 30 * 60 * 1000; | ||
| const reapInterval = setInterval(() => { | ||
| const now = Date.now(); | ||
| for (const [id, session] of sessions.entries()) { | ||
| if (now - session.lastActivity > SESSION_TTL_MS) { | ||
| logger.info(`Reaping idle session ${id}`); | ||
| destroySession(id); | ||
| } | ||
| } | ||
| }, 60_000); | ||
| reapInterval.unref(); | ||
| const httpServer = createServer(async (req, res) => { | ||
@@ -277,4 +302,5 @@ let url; | ||
| res.setHeader('Access-Control-Allow-Origin', origin); | ||
| res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); | ||
| res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept'); | ||
| res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); | ||
| res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id'); | ||
| res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id'); | ||
| } | ||
@@ -287,33 +313,58 @@ if (req.method === 'OPTIONS') { | ||
| if (url.pathname === '/') { | ||
| const accept = req.headers.accept || ''; | ||
| if (!accept.includes('application/json') || !accept.includes('text/event-stream')) { | ||
| const requiredAccept = 'application/json, text/event-stream'; | ||
| req.headers.accept = requiredAccept; | ||
| const idx = req.rawHeaders.findIndex(h => h.toLowerCase() === 'accept'); | ||
| if (idx !== -1) { | ||
| req.rawHeaders[idx + 1] = requiredAccept; | ||
| } | ||
| else { | ||
| req.rawHeaders.push('Accept', requiredAccept); | ||
| } | ||
| } | ||
| if (req.method === 'POST') { | ||
| let body = ''; | ||
| req.on('data', chunk => (body += chunk)); | ||
| req.on('data', (chunk) => { body += chunk; }); | ||
| req.on('end', async () => { | ||
| try { | ||
| const jsonData = JSON.parse(body); | ||
| if (jsonData && jsonData.method === 'initialize') { | ||
| const sessionId = req.headers['mcp-session-id'] || undefined; | ||
| const session = sessionId ? sessions.get(sessionId) : undefined; | ||
| let transport = session?.transport; | ||
| if (!transport && isInitializeRequest(jsonData)) { | ||
| const clientInfo = jsonData.params?.clientInfo; | ||
| logger.info(`Client connected: ${clientInfo?.name || 'unknown'} v${clientInfo?.version || 'unknown'} from ${origin || host}`); | ||
| if (httpTransport) { | ||
| try { | ||
| httpTransport.close(); | ||
| } | ||
| catch { } | ||
| } | ||
| httpTransport = new StreamableHTTPServerTransport({ | ||
| sessionIdGenerator: undefined, | ||
| enableJsonResponse: true | ||
| const server = createConfiguredServer(); | ||
| const newTransport = new StreamableHTTPServerTransport({ | ||
| enableJsonResponse: true, | ||
| sessionIdGenerator: () => randomUUID(), | ||
| onsessioninitialized: (id) => { | ||
| sessions.set(id, { transport: newTransport, server, lastActivity: Date.now() }); | ||
| }, | ||
| onsessionclosed: (id) => { destroySession(id); } | ||
| }); | ||
| await server.connect(httpTransport); | ||
| await httpTransport.handleRequest(req, res, jsonData); | ||
| newTransport.onclose = () => { | ||
| const id = newTransport.sessionId; | ||
| if (id) | ||
| destroySession(id); | ||
| }; | ||
| transport = newTransport; | ||
| await server.connect(transport); | ||
| } | ||
| if (!transport) { | ||
| res.writeHead(400, { 'Content-Type': 'application/json' }); | ||
| res.end(JSON.stringify({ | ||
| jsonrpc: '2.0', | ||
| error: { code: -32000, message: 'Bad Request: No valid session. Send initialize first.' }, | ||
| id: null | ||
| })); | ||
| return; | ||
| } | ||
| if (!httpTransport) { | ||
| httpTransport = new StreamableHTTPServerTransport({ | ||
| sessionIdGenerator: undefined, | ||
| enableJsonResponse: true | ||
| }); | ||
| await server.connect(httpTransport); | ||
| if (sessionId) { | ||
| const activeSession = sessions.get(sessionId); | ||
| if (activeSession) | ||
| activeSession.lastActivity = Date.now(); | ||
| } | ||
| await httpTransport.handleRequest(req, res, jsonData); | ||
| await transport.handleRequest(req, res, jsonData); | ||
| } | ||
@@ -333,2 +384,57 @@ catch (error) { | ||
| } | ||
| else if (req.method === 'GET') { | ||
| const sessionId = req.headers['mcp-session-id'] || undefined; | ||
| const session = sessionId ? sessions.get(sessionId) : undefined; | ||
| if (!session) { | ||
| res.writeHead(404, { 'Content-Type': 'application/json' }); | ||
| res.end(JSON.stringify({ | ||
| jsonrpc: '2.0', | ||
| error: { code: -32000, message: 'Not Found: Invalid or expired session. Re-initialize.' }, | ||
| id: null | ||
| })); | ||
| return; | ||
| } | ||
| try { | ||
| session.lastActivity = Date.now(); | ||
| await session.transport.handleRequest(req, res); | ||
| } | ||
| catch (error) { | ||
| logger.error(`Error processing GET request: ${error}`); | ||
| if (!res.headersSent) { | ||
| res.writeHead(500); | ||
| res.end(JSON.stringify({ | ||
| jsonrpc: '2.0', | ||
| error: { code: -32603, message: 'Internal server error' }, | ||
| id: null | ||
| })); | ||
| } | ||
| } | ||
| } | ||
| else if (req.method === 'DELETE') { | ||
| const sessionId = req.headers['mcp-session-id'] || undefined; | ||
| const transport = sessionId ? sessions.get(sessionId)?.transport : undefined; | ||
| if (!transport) { | ||
| res.writeHead(404, { 'Content-Type': 'application/json' }); | ||
| res.end(JSON.stringify({ | ||
| jsonrpc: '2.0', | ||
| error: { code: -32000, message: 'Not Found: Invalid or expired session.' }, | ||
| id: null | ||
| })); | ||
| return; | ||
| } | ||
| try { | ||
| await transport.handleRequest(req, res); | ||
| } | ||
| catch (error) { | ||
| logger.error(`Error processing DELETE request: ${error}`); | ||
| if (!res.headersSent) { | ||
| res.writeHead(500); | ||
| res.end(JSON.stringify({ | ||
| jsonrpc: '2.0', | ||
| error: { code: -32603, message: 'Internal server error' }, | ||
| id: null | ||
| })); | ||
| } | ||
| } | ||
| } | ||
| else { | ||
@@ -351,2 +457,3 @@ res.writeHead(405); | ||
| logger.info('Starting in stdio mode'); | ||
| const server = createConfiguredServer(); | ||
| const transport = new StdioServerTransport(); | ||
@@ -353,0 +460,0 @@ server.connect(transport) |
+13
-3
| { | ||
| "name": "@socketsecurity/mcp", | ||
| "version": "0.0.16", | ||
| "version": "0.0.17", | ||
| "type": "module", | ||
@@ -53,8 +53,18 @@ "main": "./index.js", | ||
| "@anthropic-ai/mcpb": "^1.1.0", | ||
| "@modelcontextprotocol/sdk": "^1.18.0", | ||
| "@modelcontextprotocol/sdk": "1.26.0", | ||
| "pino": "^10.0.0", | ||
| "pino-pretty": "^13.0.0", | ||
| "semver": "^7.7.2", | ||
| "zod": "^3.24.4" | ||
| "zod": "3.25.76" | ||
| }, | ||
| "overrides": { | ||
| "zod": "3.25.76", | ||
| "zod-to-json-schema": "3.25.1" | ||
| }, | ||
| "pnpm": { | ||
| "overrides": { | ||
| "zod": "3.25.76", | ||
| "zod-to-json-schema": "3.25.1" | ||
| } | ||
| }, | ||
| "devDependencies": { | ||
@@ -61,0 +71,0 @@ "@types/node": "^24.0.7", |
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 6 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 6 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
48903
13.35%774
16.04%+ Added
- Removed
Updated