# Directory Structure ``` ├── .github │ └── workflows │ └── build.yml ├── .gitignore ├── package-lock.json ├── package.json ├── README.md ├── src │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` node_modules/ dist/ .vscode/ .venv/ venv/ __pycache__/ *.pyc *.tmp *.log *.swp ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Snyk MCP Server A standalone Model Context Protocol server for Snyk security scanning functionality. **WARNING: THIS MCP SERVER IS CURRENTLY IN ALPHA AND IS NOT YET FINISHED!** ## Configuration Update your Claude desktop config (`claude-config.json`): ```json { "mcpServers": { "snyk": { "command": "npx", "args": [ "-y", "github:sammcj/mcp-snyk" ], "env": { "SNYK_API_KEY": "your_snyk_token", "SNYK_ORG_ID": "your_default_org_id" // Optional: Configure a default organisation ID } } } } ``` Replace the token with your actual Snyk API token. The organisation ID can be configured in multiple ways: 1. In the MCP settings via `SNYK_ORG_ID` (as shown above) 2. Using the Snyk CLI: `snyk config set org=your-org-id` 3. Providing it directly in commands The server will try these methods in order until it finds a valid organisation ID. ### Verifying Configuration You can verify your Snyk token is configured correctly by asking Claude to run the verify_token command: ``` Verify my Snyk token configuration ``` This will check if your token is valid and show your Snyk user information. If you have the Snyk CLI installed and configured, it will also show your CLI-configured organization ID. ## Features - Repository security scanning using GitHub/GitLab URLs - Snyk project scanning - Integration with Claude desktop - Token verification - Multiple organization ID configuration options - Snyk CLI integration for organization ID lookup ## Usage To scan a repository, you must provide its GitHub or GitLab URL: ``` Scan repository https://github.com/owner/repo for security vulnerabilities ``` IMPORTANT: The scan_repository command requires the actual repository URL (e.g., https://github.com/owner/repo). Do not use local file paths - always use the repository's URL on GitHub or GitLab. For Snyk projects: ``` Scan Snyk project project-id-here ``` ### Organization ID Configuration The server will look for the organization ID in this order: 1. Command argument (if provided) 2. MCP settings environment variable (`SNYK_ORG_ID`) 3. Snyk CLI configuration (`snyk config get org`) You only need to specify the organization ID in your command if you want to override the configured values: ``` Scan repository https://github.com/owner/repo in organisation org-id-here ``` ### Snyk CLI Integration If you have the Snyk CLI installed (`npm install -g snyk`), the server can use it to: - Get your default organisation ID - Fall back to CLI configuration when MCP settings are not provided - Show CLI configuration details in token verification output This integration makes it easier to use the same organisation ID across both CLI and MCP server usage. ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2020", "module": "ES2020", "moduleResolution": "Bundler", "strict": true, "outDir": "dist", "skipLibCheck": true }, "include": ["src"] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "@sammcj/mcp-server-snyk", "version": "1.0.0", "type": "module", "main": "dist/index.js", "bin": "dist/index.js", "scripts": { "build": "tsc", "dev": "tsx watch src/index.ts" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.5.0", "node-fetch": "^3.3.2", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.1" }, "devDependencies": { "@types/node": "^22.13.1", "tsx": "^4.19.2", "typescript": "^5.7.3" } } ``` -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- ```yaml name: Build on: push: branches: [ main ] pull_request: branches: [ main ] permissions: contents: write jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Use Node.js uses: actions/setup-node@v4 with: node-version: '22.x' - name: Install dependencies run: npm install - name: Build run: npm run build - name: Commit dist run: | git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" git add dist/ git commit -m "Add built files" || echo "No changes to commit" git push ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { execSync } from 'child_process'; const SNYK_API_KEY = process.env.SNYK_API_KEY; const SNYK_ORG_ID = process.env.SNYK_ORG_ID; // Optional default org ID from settings if (!SNYK_API_KEY) { console.error("SNYK_API_KEY environment variable is not set"); process.exit(1); } // Helper function to check if Snyk CLI is installed function isSnykCliInstalled(): boolean { try { execSync('snyk --version', { stdio: 'ignore' }); return true; } catch (error) { return false; } } // Helper function to get org ID from Snyk CLI function getOrgIdFromCli(): string | null { try { const output = execSync('snyk config get org', { encoding: 'utf8' }).trim(); if (output && output !== 'undefined' && output !== 'null') { console.error('Retrieved organisation ID from Snyk CLI configuration'); return output; } } catch (error) { console.error('Failed to get organisation ID from Snyk CLI:', error instanceof Error ? error.message : String(error)); } return null; } // Schema definitions const ScanRepoSchema = z.object({ url: z.string().url().describe('GitHub/GitLab repository URL (e.g., https://github.com/owner/repo)'), branch: z.string().optional().describe('Branch to scan (optional)'), org: z.string().optional().describe('Snyk organisation ID (optional if configured in settings or available via Snyk CLI)') }); const ScanProjectSchema = z.object({ projectId: z.string().describe('Snyk project ID to scan'), org: z.string().optional().describe('Snyk organisation ID (optional if configured in settings or available via Snyk CLI)') }); const ListProjectsSchema = z.object({ org: z.string().optional().describe('Snyk organisation ID (optional if configured in settings or available via Snyk CLI)') }); const VerifyTokenSchema = z.object({}); // Helper function to get org ID function getOrgId(providedOrgId?: string): string { // First try the provided org ID if (providedOrgId) { return providedOrgId; } // Then try the environment variable if (SNYK_ORG_ID) { return SNYK_ORG_ID; } // Finally, try to get it from the Snyk CLI if installed if (isSnykCliInstalled()) { const cliOrgId = getOrgIdFromCli(); if (cliOrgId) { return cliOrgId; } } throw new Error( 'Snyk organisation ID is required. You can provide it in one of these ways:\n' + '1. Include it in the command\n' + '2. Configure SNYK_ORG_ID in the MCP settings\n' + '3. Set it in your Snyk CLI configuration using "snyk config set org=<org-id>"' ); } // Helper function to execute Snyk CLI commands function executeSnykCommand(command: string, args: string[] = []): string { try { const fullCommand = command ? `snyk ${command} ${args.join(' ')}` : `snyk ${args.join(' ')}`; console.error('Executing command:', fullCommand); // Execute the command and capture both stdout and stderr return execSync(fullCommand, { encoding: 'utf8' }); } catch (error) { if (error instanceof Error && 'stdout' in error) { // If the command failed but returned output, return that output return (error as any).stdout || (error as any).stderr || error.message; } throw error; } } const server = new Server( { name: 'snyk-mcp-server', version: '1.0.0' }, { capabilities: { tools: {} } } ); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "scan_repository", description: "Scan a GitHub/GitLab repository for security vulnerabilities using Snyk. Requires the repository's URL (e.g., https://github.com/owner/repo). Do not use local file paths.", inputSchema: zodToJsonSchema(ScanRepoSchema) }, { name: "scan_project", description: "Scan an existing Snyk project", inputSchema: zodToJsonSchema(ScanProjectSchema) }, { name: "list_projects", description: "List all projects in a Snyk organisation", inputSchema: zodToJsonSchema(ListProjectsSchema) }, { name: "verify_token", description: "Verify that the configured Snyk token is valid", inputSchema: zodToJsonSchema(VerifyTokenSchema) } ] }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { if (!request.params.arguments) { throw new Error("Arguments are required"); } switch (request.params.name) { case "verify_token": { try { // Use whoami to verify the token const output = executeSnykCommand('whoami'); return { content: [{ type: "text", text: `✅ Token verified successfully!\n${output}` }] }; } catch (error) { return { content: [{ type: "text", text: `❌ Token verification failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } case "scan_repository": { const args = ScanRepoSchema.parse(request.params.arguments); const orgId = getOrgId(args.org); try { // Extract owner/repo from GitHub URL const repoPath = args.url.split('github.com/')[1]; if (!repoPath) { throw new Error('Invalid GitHub URL format'); } // Use snyk code test with GitHub repository path const cliArgs = [ 'code', 'test', '--org=' + orgId, '--json', 'github.com/' + repoPath ]; if (args.branch) { cliArgs.push('--branch=' + args.branch); } const output = executeSnykCommand('', cliArgs); return { content: [{ type: "text", text: output }] }; } catch (error) { return { content: [{ type: "text", text: `Failed to scan repository: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } case "scan_project": { const args = ScanProjectSchema.parse(request.params.arguments); const orgId = getOrgId(args.org); try { // Use snyk test to scan the project const output = executeSnykCommand('test', [ '--org=' + orgId, '--project-id=' + args.projectId, '--json' ]); return { content: [{ type: "text", text: output }] }; } catch (error) { return { content: [{ type: "text", text: `Failed to scan project: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } case "list_projects": { const args = ListProjectsSchema.parse(request.params.arguments); const orgId = getOrgId(args.org); try { // Use snyk projects to list all projects const output = executeSnykCommand('projects', [ 'list', '--org=' + orgId, '--json' ]); return { content: [{ type: "text", text: output }] }; } catch (error) { return { content: [{ type: "text", text: `Failed to list projects: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } default: throw new Error(`Unknown tool: ${request.params.name}`); } } catch (error) { if (error instanceof z.ZodError) { throw new Error(`Invalid arguments: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`); } throw error; } }); const transport = new StdioServerTransport(); server.connect(transport).catch((error) => { console.error("Fatal error:", error); process.exit(1); }); console.error('Snyk MCP Server running on stdio'); ```