Skip to main content
Glama
index.ts15.4 kB
#!/usr/bin/env -S node --experimental-strip-types import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' import { z } from 'zod' import pino from 'pino' import readline from 'readline' import { join } from 'path' import { readFileSync } from 'fs' import { tmpdir } from 'os' import { createServer } from 'http' const __dirname = import.meta.dirname // Extract version from package.json const packageJson = JSON.parse(readFileSync(join(__dirname, './package.json'), 'utf8')) const VERSION = packageJson.version || '0.0.1' // Configure pino logger with cross-platform temp directory const logger = pino({ level: 'info', transport: { targets: [ { target: 'pino/file', options: { destination: join(tmpdir(), 'socket-mcp-error.log') }, level: 'error' }, { target: 'pino/file', options: { destination: join(tmpdir(), 'socket-mcp.log') }, level: 'info' } ] } }) // Socket API URL - use localhost when debugging is enabled, otherwise use production const SOCKET_API_URL = process.env['SOCKET_DEBUG'] === 'true' ? 'http://localhost:8866/v0/purl?alerts=false&compact=false&fixable=false&licenseattrib=false&licensedetails=false' : 'https://api.socket.dev/v0/purl?alerts=false&compact=false&fixable=false&licenseattrib=false&licensedetails=false' // Function to get API key interactively (only for HTTP mode) async function getApiKeyInteractively (): Promise<string> { const rl = readline.createInterface({ input: process.stdin, output: process.stderr }) const apiKey = await new Promise<string>((resolve) => { rl.question('Please enter your Socket API key: ', (answer: string | PromiseLike<string>) => { rl.close() resolve(answer) }) }) if (!apiKey) { logger.error('No API key provided') process.exit(1) } return apiKey } // Initialize API key let SOCKET_API_KEY = process.env['SOCKET_API_KEY'] || '' // Build headers dynamically to reflect current API key function buildSocketHeaders (): Record<string, string> { return { 'user-agent': `socket-mcp/${VERSION}`, accept: 'application/x-ndjson', 'content-type': 'application/json', authorization: `Bearer ${SOCKET_API_KEY}` } } // No session management: each HTTP request is handled statelessly // Create server instance 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`) // Build components array for the API request const components = packages.map(pkg => { 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 { logger.info(`Using version ${cleanedVersion} for ${pkg.depname}`) purl = `pkg:${ecosystem}/${pkg.depname}@${cleanedVersion}` } return { purl } }) try { // Make a POST request to the Socket API with all packages 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 { // Handle NDJSON (multiple JSON objects, one per line) const results: string[] = [] 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 } } // Process each result for (const jsonData of jsonLines) { const purl: string = `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`) } } } else { const jsonData = JSON.parse(responseText) const purl: string = `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`) } } 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 as Error 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 } } } catch (e) { const error = e as Error const errorMsg = `Error processing packages: ${error.message}` logger.error(errorMsg) return { content: [{ type: 'text', text: 'Error connecting to Socket API' }], isError: true } } } ) // Determine transport mode from environment or arguments const useHttp = process.env['MCP_HTTP_MODE'] === 'true' || process.argv.includes('--http') const port = parseInt(process.env['MCP_PORT'] || '3000', 10) // Validate API key - in stdio mode, we can't prompt interactively if (!SOCKET_API_KEY) { if (useHttp) { // In HTTP mode, we can prompt for the API key logger.error('SOCKET_API_KEY environment variable is not set') SOCKET_API_KEY = await getApiKeyInteractively() } else { // In stdio mode, we must have the API key as an environment variable logger.error('SOCKET_API_KEY environment variable is required in stdio mode') logger.error('Please set the SOCKET_API_KEY environment variable and try again') process.exit(1) } } if (useHttp) { // HTTP mode with Server-Sent Events logger.info(`Starting HTTP server on port ${port}`) // Singleton transport to preserve initialization state without explicit sessions let httpTransport: StreamableHTTPServerTransport | null = null const httpServer = createServer(async (req, res) => { // Parse URL first to check for health endpoint let url: URL try { url = new URL(req.url!, `http://localhost:${port}`) } catch (error) { logger.warn(`Invalid URL in request: ${req.url} - ${error}`) res.writeHead(400, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: Invalid URL' }, id: null })) return } // Health check endpoint for K8s/Docker - bypass origin validation if (url.pathname === '/health') { res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ status: 'healthy', service: 'socket-mcp', version: VERSION, timestamp: new Date().toISOString() })) return } // Validate Origin header as required by MCP spec (for non-health endpoints) const origin = req.headers.origin // Check if origin is from localhost (any port) - safe for local development const isLocalhostOrigin = (originUrl: string): boolean => { try { const url = new URL(originUrl) return url.hostname === 'localhost' || url.hostname === '127.0.0.1' } catch { return false } } const allowedOrigins = [ 'https://mcp.socket.dev', 'https://mcp.socket-staging.dev' ] // Check if request is from localhost (for same-origin requests that don't send Origin header) // Use strict matching to prevent spoofing via subdomains like "malicious-localhost.evil.com" const host = req.headers.host || '' // Extract hostnames from allowedOrigins for Host header validation const allowedHosts = allowedOrigins.map(o => new URL(o).hostname) const isAllowedHost = host === `localhost:${port}` || host === `127.0.0.1:${port}` || host === 'localhost' || host === '127.0.0.1' || allowedHosts.includes(host) // Allow requests: // 1. With Origin header from localhost (any port) or production domains // 2. Without Origin header if they're from localhost or allowed domains (same-origin requests) const isValidOrigin = origin ? (isLocalhostOrigin(origin) || allowedOrigins.includes(origin)) : isAllowedHost if (!isValidOrigin) { logger.warn(`Rejected request from invalid origin: ${origin || 'missing'} (host: ${host})`) res.writeHead(403, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Forbidden: Invalid origin' }, id: null })) return } // Set CORS headers for valid origins (only needed for cross-origin requests) // Same-origin requests don't send Origin header and don't need CORS headers if (origin) { res.setHeader('Access-Control-Allow-Origin', origin) res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS') res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept') } if (req.method === 'OPTIONS') { res.writeHead(200) res.end() return } if (url.pathname === '/') { if (req.method === 'POST') { // Handle JSON-RPC messages statelessly let body = '' req.on('data', chunk => (body += chunk)) req.on('end', async () => { try { const jsonData = JSON.parse(body) // If this is an initialize, reset the singleton transport so clients can (re)initialize cleanly if (jsonData && jsonData.method === 'initialize') { 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({ // Stateless mode: no session management required sessionIdGenerator: undefined, // Return JSON responses to avoid SSE streaming enableJsonResponse: true }) await server.connect(httpTransport) await httpTransport.handleRequest(req, res, jsonData) return } // For non-initialize requests, ensure transport exists (client should have initialized already) if (!httpTransport) { httpTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true }) await server.connect(httpTransport) } await httpTransport.handleRequest(req, res, jsonData) } catch (error) { logger.error(`Error processing POST 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 { res.writeHead(405) res.end('Method not allowed') } } else { res.writeHead(404) res.end('Not found') } }) httpServer.listen(port, () => { logger.info(`Socket MCP HTTP server version ${VERSION} started successfully on port ${port}`) logger.info(`Connect to: http://localhost:${port}/`) }) } else { // Stdio mode (default) logger.info('Starting in stdio mode') const transport = new StdioServerTransport() server.connect(transport) .then(() => { logger.info(`Socket MCP server version ${VERSION} started successfully`) }) .catch((error: Error) => { logger.error(`Failed to start Socket MCP server: ${error.message}`) process.exit(1) }) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/SocketDev/socket-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server