# Directory Structure ``` ├── .github │ └── workflows │ └── build.yml ├── .gitignore ├── package-lock.json ├── package.json ├── README.md ├── src │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules/ 2 | dist/ 3 | .vscode/ 4 | .venv/ 5 | venv/ 6 | __pycache__/ 7 | *.pyc 8 | *.tmp 9 | *.log 10 | *.swp 11 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Snyk MCP Server 2 | 3 | A standalone Model Context Protocol server for Snyk security scanning functionality. 4 | 5 | **WARNING: THIS MCP SERVER IS CURRENTLY IN ALPHA AND IS NOT YET FINISHED!** 6 | 7 | ## Configuration 8 | 9 | Update your Claude desktop config (`claude-config.json`): 10 | 11 | ```json 12 | { 13 | "mcpServers": { 14 | "snyk": { 15 | "command": "npx", 16 | "args": [ 17 | "-y", 18 | "github:sammcj/mcp-snyk" 19 | ], 20 | "env": { 21 | "SNYK_API_KEY": "your_snyk_token", 22 | "SNYK_ORG_ID": "your_default_org_id" // Optional: Configure a default organisation ID 23 | } 24 | } 25 | } 26 | } 27 | ``` 28 | 29 | Replace the token with your actual Snyk API token. The organisation ID can be configured in multiple ways: 30 | 31 | 1. In the MCP settings via `SNYK_ORG_ID` (as shown above) 32 | 2. Using the Snyk CLI: `snyk config set org=your-org-id` 33 | 3. Providing it directly in commands 34 | 35 | The server will try these methods in order until it finds a valid organisation ID. 36 | 37 | ### Verifying Configuration 38 | 39 | You can verify your Snyk token is configured correctly by asking Claude to run the verify_token command: 40 | 41 | ``` 42 | Verify my Snyk token configuration 43 | ``` 44 | 45 | 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. 46 | 47 | ## Features 48 | 49 | - Repository security scanning using GitHub/GitLab URLs 50 | - Snyk project scanning 51 | - Integration with Claude desktop 52 | - Token verification 53 | - Multiple organization ID configuration options 54 | - Snyk CLI integration for organization ID lookup 55 | 56 | ## Usage 57 | 58 | To scan a repository, you must provide its GitHub or GitLab URL: 59 | 60 | ``` 61 | Scan repository https://github.com/owner/repo for security vulnerabilities 62 | ``` 63 | 64 | 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. 65 | 66 | For Snyk projects: 67 | 68 | ``` 69 | Scan Snyk project project-id-here 70 | ``` 71 | 72 | ### Organization ID Configuration 73 | 74 | The server will look for the organization ID in this order: 75 | 76 | 1. Command argument (if provided) 77 | 2. MCP settings environment variable (`SNYK_ORG_ID`) 78 | 3. Snyk CLI configuration (`snyk config get org`) 79 | 80 | You only need to specify the organization ID in your command if you want to override the configured values: 81 | 82 | ``` 83 | Scan repository https://github.com/owner/repo in organisation org-id-here 84 | ``` 85 | 86 | ### Snyk CLI Integration 87 | 88 | If you have the Snyk CLI installed (`npm install -g snyk`), the server can use it to: 89 | 90 | - Get your default organisation ID 91 | - Fall back to CLI configuration when MCP settings are not provided 92 | - Show CLI configuration details in token verification output 93 | 94 | This integration makes it easier to use the same organisation ID across both CLI and MCP server usage. 95 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2020", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "outDir": "dist", 8 | "skipLibCheck": true 9 | }, 10 | "include": ["src"] 11 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@sammcj/mcp-server-snyk", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "main": "dist/index.js", 6 | "bin": "dist/index.js", 7 | "scripts": { 8 | "build": "tsc", 9 | "dev": "tsx watch src/index.ts" 10 | }, 11 | "dependencies": { 12 | "@modelcontextprotocol/sdk": "^1.5.0", 13 | "node-fetch": "^3.3.2", 14 | "zod": "^3.24.2", 15 | "zod-to-json-schema": "^3.24.1" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^22.13.1", 19 | "tsx": "^4.19.2", 20 | "typescript": "^5.7.3" 21 | } 22 | } 23 | ``` -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Use Node.js 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: '22.x' 23 | 24 | - name: Install dependencies 25 | run: npm install 26 | 27 | - name: Build 28 | run: npm run build 29 | 30 | - name: Commit dist 31 | run: | 32 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 33 | git config --local user.name "github-actions[bot]" 34 | git add dist/ 35 | git commit -m "Add built files" || echo "No changes to commit" 36 | git push 37 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; 5 | import { z } from 'zod'; 6 | import { zodToJsonSchema } from 'zod-to-json-schema'; 7 | import { execSync } from 'child_process'; 8 | 9 | const SNYK_API_KEY = process.env.SNYK_API_KEY; 10 | const SNYK_ORG_ID = process.env.SNYK_ORG_ID; // Optional default org ID from settings 11 | 12 | if (!SNYK_API_KEY) { 13 | console.error("SNYK_API_KEY environment variable is not set"); 14 | process.exit(1); 15 | } 16 | 17 | // Helper function to check if Snyk CLI is installed 18 | function isSnykCliInstalled(): boolean { 19 | try { 20 | execSync('snyk --version', { stdio: 'ignore' }); 21 | return true; 22 | } catch (error) { 23 | return false; 24 | } 25 | } 26 | 27 | // Helper function to get org ID from Snyk CLI 28 | function getOrgIdFromCli(): string | null { 29 | try { 30 | const output = execSync('snyk config get org', { encoding: 'utf8' }).trim(); 31 | if (output && output !== 'undefined' && output !== 'null') { 32 | console.error('Retrieved organisation ID from Snyk CLI configuration'); 33 | return output; 34 | } 35 | } catch (error) { 36 | console.error('Failed to get organisation ID from Snyk CLI:', error instanceof Error ? error.message : String(error)); 37 | } 38 | return null; 39 | } 40 | 41 | // Schema definitions 42 | const ScanRepoSchema = z.object({ 43 | url: z.string().url().describe('GitHub/GitLab repository URL (e.g., https://github.com/owner/repo)'), 44 | branch: z.string().optional().describe('Branch to scan (optional)'), 45 | org: z.string().optional().describe('Snyk organisation ID (optional if configured in settings or available via Snyk CLI)') 46 | }); 47 | 48 | const ScanProjectSchema = z.object({ 49 | projectId: z.string().describe('Snyk project ID to scan'), 50 | org: z.string().optional().describe('Snyk organisation ID (optional if configured in settings or available via Snyk CLI)') 51 | }); 52 | 53 | const ListProjectsSchema = z.object({ 54 | org: z.string().optional().describe('Snyk organisation ID (optional if configured in settings or available via Snyk CLI)') 55 | }); 56 | 57 | const VerifyTokenSchema = z.object({}); 58 | 59 | // Helper function to get org ID 60 | function getOrgId(providedOrgId?: string): string { 61 | // First try the provided org ID 62 | if (providedOrgId) { 63 | return providedOrgId; 64 | } 65 | 66 | // Then try the environment variable 67 | if (SNYK_ORG_ID) { 68 | return SNYK_ORG_ID; 69 | } 70 | 71 | // Finally, try to get it from the Snyk CLI if installed 72 | if (isSnykCliInstalled()) { 73 | const cliOrgId = getOrgIdFromCli(); 74 | if (cliOrgId) { 75 | return cliOrgId; 76 | } 77 | } 78 | 79 | throw new Error( 80 | 'Snyk organisation ID is required. You can provide it in one of these ways:\n' + 81 | '1. Include it in the command\n' + 82 | '2. Configure SNYK_ORG_ID in the MCP settings\n' + 83 | '3. Set it in your Snyk CLI configuration using "snyk config set org=<org-id>"' 84 | ); 85 | } 86 | 87 | // Helper function to execute Snyk CLI commands 88 | function executeSnykCommand(command: string, args: string[] = []): string { 89 | try { 90 | const fullCommand = command 91 | ? `snyk ${command} ${args.join(' ')}` 92 | : `snyk ${args.join(' ')}`; 93 | 94 | console.error('Executing command:', fullCommand); 95 | 96 | // Execute the command and capture both stdout and stderr 97 | return execSync(fullCommand, { encoding: 'utf8' }); 98 | } catch (error) { 99 | if (error instanceof Error && 'stdout' in error) { 100 | // If the command failed but returned output, return that output 101 | return (error as any).stdout || (error as any).stderr || error.message; 102 | } 103 | throw error; 104 | } 105 | } 106 | 107 | const server = new Server( 108 | { name: 'snyk-mcp-server', version: '1.0.0' }, 109 | { 110 | capabilities: { 111 | tools: {} 112 | } 113 | } 114 | ); 115 | 116 | server.setRequestHandler(ListToolsRequestSchema, async () => { 117 | return { 118 | tools: [ 119 | { 120 | name: "scan_repository", 121 | 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.", 122 | inputSchema: zodToJsonSchema(ScanRepoSchema) 123 | }, 124 | { 125 | name: "scan_project", 126 | description: "Scan an existing Snyk project", 127 | inputSchema: zodToJsonSchema(ScanProjectSchema) 128 | }, 129 | { 130 | name: "list_projects", 131 | description: "List all projects in a Snyk organisation", 132 | inputSchema: zodToJsonSchema(ListProjectsSchema) 133 | }, 134 | { 135 | name: "verify_token", 136 | description: "Verify that the configured Snyk token is valid", 137 | inputSchema: zodToJsonSchema(VerifyTokenSchema) 138 | } 139 | ] 140 | }; 141 | }); 142 | 143 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 144 | try { 145 | if (!request.params.arguments) { 146 | throw new Error("Arguments are required"); 147 | } 148 | 149 | switch (request.params.name) { 150 | case "verify_token": { 151 | try { 152 | // Use whoami to verify the token 153 | const output = executeSnykCommand('whoami'); 154 | return { 155 | content: [{ 156 | type: "text", 157 | text: `✅ Token verified successfully!\n${output}` 158 | }] 159 | }; 160 | } catch (error) { 161 | return { 162 | content: [{ 163 | type: "text", 164 | text: `❌ Token verification failed: ${error instanceof Error ? error.message : String(error)}` 165 | }], 166 | isError: true 167 | }; 168 | } 169 | } 170 | 171 | case "scan_repository": { 172 | const args = ScanRepoSchema.parse(request.params.arguments); 173 | const orgId = getOrgId(args.org); 174 | 175 | try { 176 | // Extract owner/repo from GitHub URL 177 | const repoPath = args.url.split('github.com/')[1]; 178 | if (!repoPath) { 179 | throw new Error('Invalid GitHub URL format'); 180 | } 181 | 182 | // Use snyk code test with GitHub repository path 183 | const cliArgs = [ 184 | 'code', 185 | 'test', 186 | '--org=' + orgId, 187 | '--json', 188 | 'github.com/' + repoPath 189 | ]; 190 | 191 | if (args.branch) { 192 | cliArgs.push('--branch=' + args.branch); 193 | } 194 | 195 | const output = executeSnykCommand('', cliArgs); 196 | return { 197 | content: [{ 198 | type: "text", 199 | text: output 200 | }] 201 | }; 202 | } catch (error) { 203 | return { 204 | content: [{ 205 | type: "text", 206 | text: `Failed to scan repository: ${error instanceof Error ? error.message : String(error)}` 207 | }], 208 | isError: true 209 | }; 210 | } 211 | } 212 | 213 | case "scan_project": { 214 | const args = ScanProjectSchema.parse(request.params.arguments); 215 | const orgId = getOrgId(args.org); 216 | 217 | try { 218 | // Use snyk test to scan the project 219 | const output = executeSnykCommand('test', [ 220 | '--org=' + orgId, 221 | '--project-id=' + args.projectId, 222 | '--json' 223 | ]); 224 | return { 225 | content: [{ 226 | type: "text", 227 | text: output 228 | }] 229 | }; 230 | } catch (error) { 231 | return { 232 | content: [{ 233 | type: "text", 234 | text: `Failed to scan project: ${error instanceof Error ? error.message : String(error)}` 235 | }], 236 | isError: true 237 | }; 238 | } 239 | } 240 | 241 | case "list_projects": { 242 | const args = ListProjectsSchema.parse(request.params.arguments); 243 | const orgId = getOrgId(args.org); 244 | 245 | try { 246 | // Use snyk projects to list all projects 247 | const output = executeSnykCommand('projects', [ 248 | 'list', 249 | '--org=' + orgId, 250 | '--json' 251 | ]); 252 | return { 253 | content: [{ 254 | type: "text", 255 | text: output 256 | }] 257 | }; 258 | } catch (error) { 259 | return { 260 | content: [{ 261 | type: "text", 262 | text: `Failed to list projects: ${error instanceof Error ? error.message : String(error)}` 263 | }], 264 | isError: true 265 | }; 266 | } 267 | } 268 | 269 | default: 270 | throw new Error(`Unknown tool: ${request.params.name}`); 271 | } 272 | } catch (error) { 273 | if (error instanceof z.ZodError) { 274 | throw new Error(`Invalid arguments: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`); 275 | } 276 | throw error; 277 | } 278 | }); 279 | 280 | const transport = new StdioServerTransport(); 281 | server.connect(transport).catch((error) => { 282 | console.error("Fatal error:", error); 283 | process.exit(1); 284 | }); 285 | 286 | console.error('Snyk MCP Server running on stdio'); 287 | ```