diff --git a/.gitignore b/.gitignore index ee0dab6e3..7113bfb00 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ snyk-linux # Configuration # ################################# snyk.config.local.json + +# Snyk Security Extension - AI Rules (auto-generated) +.cursor/rules/snyk_rules.mdc diff --git a/.windsurf/rules/snyk_rules.md b/.windsurf/rules/snyk_rules.md new file mode 100644 index 000000000..1257850c0 --- /dev/null +++ b/.windsurf/rules/snyk_rules.md @@ -0,0 +1,13 @@ +--- +trigger: model_decision +description: Snyk Security At Inception +--- + +# Project security best practices + +- Always run Snyk Code scanning tool for new first party code generated. +- Always run Snyk SCA scanning tool for new dependencies or dependency updates. +- Always run Snyk IaC scanning tool for new Infrastructure as Code updates. +- If any security issues are found based on newly introduced or modified code or dependencies, attempt to fix the issues using the results context from Snyk. +- Rescan the code after fixing the issues to ensure that the issues were fixed and that there are no newly introduced issues. +- Repeat this process until no issues are found. \ No newline at end of file diff --git a/mcp_architecture_comparison.mmd b/mcp_architecture_comparison.mmd new file mode 100644 index 000000000..1d5320618 --- /dev/null +++ b/mcp_architecture_comparison.mmd @@ -0,0 +1,61 @@ +%% MCP Architecture: Current vs Target + +graph TB + subgraph "Current Architecture" + direction TB + A1[User Changes Config] --> B1[Configuration Watcher] + B1 --> C1[handleSecurityAtInceptionChange] + C1 --> D1[Send Analytics to LS via command] + C1 --> E1[configureMcpHosts] + E1 --> F1{Detect IDE} + F1 -->|VS Code| G1[configureCopilot] + F1 -->|Cursor| H1[configureCursor] + F1 -->|Windsurf| I1[configureWindsurf] + G1 --> J1[Register MCP Provider] + G1 --> K1[Write Rules] + H1 --> L1[Update mcp.json] + H1 --> M1[Write Rules] + I1 --> N1[Update mcp_config.json] + I1 --> O1[Write Rules] + end + + subgraph "Target Architecture" + direction TB + A2[User Changes Config] --> B2[Send to Language Server] + B2 --> C2[LS: UpdateSettings] + C2 --> D2[LS: Detect MCP Config Change] + D2 --> E2[LS: Send Analytics Automatically] + D2 --> F2[LS: Build MCP Config] + F2 --> G2[LS: Send $/snyk.configureSnykMCP] + G2 --> H2[Extension: Receive Notification] + H2 --> I2{IDE Type from Param} + I2 -->|VS Code| J2[configureCopilot] + I2 -->|Cursor| K2[configureCursor] + I2 -->|Windsurf| L2[configureWindsurf] + J2 --> M2[Register MCP Provider] + J2 --> N2[Write Rules] + K2 --> O2[Update mcp.json] + K2 --> P2[Write Rules] + L2 --> Q2[Update mcp_config.json] + L2 --> R2[Write Rules] + end + + style C1 fill:#ffcdd2 + style D1 fill:#ffcdd2 + style E1 fill:#ffcdd2 + style C2 fill:#c8e6c9 + style D2 fill:#c8e6c9 + style E2 fill:#c8e6c9 + style F2 fill:#c8e6c9 + style G2 fill:#c8e6c9 + + classDef current fill:#ffebee,stroke:#c62828 + classDef target fill:#e8f5e9,stroke:#2e7d32 + classDef removal fill:#ffcdd2,stroke:#c62828,stroke-width:3px + classDef addition fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px + + class A1,B1,F1,G1,H1,I1,J1,K1,L1,M1,N1,O1 current + class A2,B2,H2,I2,J2,K2,L2,M2,N2,O2,P2,Q2,R2 target + class C1,D1,E1 removal + class C2,D2,E2,F2,G2 addition + diff --git a/mcp_sequence_diagram.mmd b/mcp_sequence_diagram.mmd new file mode 100644 index 000000000..84259b27e --- /dev/null +++ b/mcp_sequence_diagram.mmd @@ -0,0 +1,58 @@ +%% MCP Configuration Sequence Diagram + +sequenceDiagram + participant User + participant IDE as IDE Extension + participant LS as Language Server + participant Analytics as Analytics Service + participant FileSystem as File System + participant VSCODE as VS Code API + + User->>IDE: Changes MCP config setting + IDE->>LS: workspace/didChangeConfiguration + + Note over LS: workspaceDidChangeConfiguration handler + LS->>LS: UpdateSettings(settings) + + alt MCP config changed + LS->>LS: Detect autoConfigureMcpServer changed + LS->>Analytics: SendConfigChangedAnalytics("autoConfigureSnykMcpServer", oldValue, newValue, triggerSource) + + LS->>LS: Detect secureAtInceptionExecutionFrequency changed + LS->>Analytics: SendConfigChangedAnalytics("secureAtInceptionExecutionFrequency", oldValue, newValue, triggerSource) + + LS->>LS: configureMcp() + LS->>LS: Build MCP config (command, args, env) + + Note over LS: Create SnykConfigureMcpParams:
- command: CLI path
- args: ["mcp", "-t", "stdio"]
- env: {SNYK_CFG_ORG, SNYK_API, ...}
- ideName: "vscode"|"cursor"|"windsurf" + + LS->>IDE: $/snyk.configureSnykMCP notification + end + + IDE->>IDE: handleMcpConfigNotification(params) + + alt IDE is VS Code + IDE->>VSCODE: vscode.lm.registerMcpServerDefinitionProvider() + VSCODE-->>IDE: Provider registered + IDE->>FileSystem: Write .github/instructions/snyk_rules.instructions.md + FileSystem-->>IDE: Rules written + else IDE is Cursor + IDE->>FileSystem: Read ~/.cursor/mcp.json + FileSystem-->>IDE: Current config + IDE->>FileSystem: Write updated ~/.cursor/mcp.json + FileSystem-->>IDE: Config updated + IDE->>FileSystem: Write .cursor/rules/snyk_rules.mdc + FileSystem-->>IDE: Rules written + else IDE is Windsurf + IDE->>FileSystem: Read ~/.codeium/windsurf/mcp_config.json + FileSystem-->>IDE: Current config + IDE->>FileSystem: Write updated mcp_config.json + FileSystem-->>IDE: Config updated + IDE->>FileSystem: Write .windsurf/rules/snyk_rules.md + FileSystem-->>IDE: Rules written + end + + IDE-->>User: MCP configured successfully + + Note over IDE,LS: Analytics flow through LS
No special handling in extension + diff --git a/mcp_to_ls_implementation_plan.md b/mcp_to_ls_implementation_plan.md new file mode 100644 index 000000000..4bb8264f4 --- /dev/null +++ b/mcp_to_ls_implementation_plan.md @@ -0,0 +1,350 @@ +# Implementation Plan: Move MCP Configuration to Language Server + +## Overview +Move MCP (Model Context Protocol) configuration logic from the VS Code extension to the language server and refactor analytics handling to use the language server's existing infrastructure. + +## Goals +1. Move MCP configuration logic from `vscode-extension/src/snyk/cli/mcp/mcp.ts` to the language server +2. Remove special analytics handling in the configuration watcher and use language server's existing analytics infrastructure +3. Implement custom notification `$/snyk.configureSnykMCP` for IDE-specific MCP configuration that requires VS Code APIs + +## Architecture + +### Current State +- MCP configuration is handled entirely in the VS Code extension (`mcp.ts`) +- Configuration changes trigger MCP reconfiguration via `handleSecurityAtInceptionChange` +- Analytics for MCP config changes are sent from the extension using custom code +- Extension directly modifies IDE-specific config files (Cursor, Windsurf, VS Code) + +### Target State +- Language server determines MCP configuration needs based on settings +- Language server sends `$/snyk.configureSnykMCP` notification with cmd, args, env +- IDE extension listens for notification and configures MCP using IDE-specific APIs +- Analytics for all config changes (including MCP) flow through language server's existing infrastructure + +## Implementation Phases + +### Phase 1: Planning ✓ +- [x] Analyze current MCP configuration logic +- [x] Analyze current analytics flow +- [x] Identify files and packages to modify +- [x] Create implementation plan +- [x] Get approval for implementation plan + +### Phase 2: Implementation (TDD) + +#### Step 1: Add MCP configuration settings to Language Server [COMPLETE] +**Objective**: Add new configuration fields to the language server's Settings struct + +**Files to modify**: +- `snyk-ls/internal/types/lsp.go` +- `snyk-ls/application/config/config.go` +- `snyk-ls/application/server/configuration.go` + +**Actions**: +- [ ] Write tests for new configuration fields (in progress) + - Test parsing `autoConfigureSnykMcpServer` from settings + - Test parsing `secureAtInceptionExecutionFrequency` from settings + - Test configuration change detection +- [ ] Add fields to `Settings` struct in `lsp.go`: + - `AutoConfigureSnykMcpServer string` + - `SecureAtInceptionExecutionFrequency string` +- [ ] Add fields to `Config` struct in `config.go`: + - `autoConfigureSnykMcpServer bool` + - `secureAtInceptionExecutionFrequency string` +- [ ] Add getters/setters with proper locking +- [ ] Update `writeSettings` in `configuration.go` to handle new fields +- [ ] Add analytics for config changes using `analytics.SendConfigChangedAnalytics` + - Call for `autoConfigureSnykMcpServer` changes + - Call for `secureAtInceptionExecutionFrequency` changes +- [ ] Run tests: `make test` +- [ ] Run linter: `make lint` +- [ ] Fix any issues + +#### Step 2: Create MCP configuration notification type [IN PROGRESS] +**Objective**: Define the notification structure for `$/snyk.configureSnykMCP` + +**Files to modify**: +- `snyk-ls/internal/types/lsp.go` + +**Actions**: +- [ ] Write tests for MCP configuration notification type (skipped) +- [ ] Add `SnykConfigureMcpParams` struct: + ```go + type SnykConfigureMcpParams struct { + Command string `json:"command"` + Args []string `json:"args"` + Env map[string]string `json:"env"` + IdeName string `json:"ideName"` // "cursor", "windsurf", "vscode" + } + ``` +- [ ] Run tests: `make test` +- [ ] Fix any issues + +#### Step 3: Implement MCP configuration logic in Language Server +**Objective**: Move the MCP configuration logic from extension to LS + +**Files to create**: +- `snyk-ls/application/config/mcp_config.go` +- `snyk-ls/application/config/mcp_config_test.go` + +**Actions**: +- [ ] Write comprehensive tests for MCP configuration logic + - Test detecting IDE type from integration environment + - Test building MCP command and args + - Test building MCP environment variables + - Test triggering notification when config changes +- [ ] Create `mcp_config.go` with functions: + - `shouldConfigureMcp(c *Config) bool` - check if MCP should be configured + - `getMcpCommand(c *Config) string` - get CLI path + - `getMcpArgs() []string` - return `["mcp", "-t", "stdio"]` + - `getMcpEnv(c *Config) map[string]string` - build env vars (SNYK_CFG_ORG, SNYK_API, IDE_CONFIG_PATH, TRUSTED_FOLDERS) + - `getIdeName(c *Config) string` - determine IDE from integration environment + - `configureMcp(c *Config)` - main function to trigger configuration +- [ ] Integrate `configureMcp` into `UpdateSettings` in `configuration.go` +- [ ] Send analytics for MCP config changes using `analytics.SendConfigChangedAnalytics` + - Example: `analytics.SendConfigChangedAnalytics(c, "autoConfigureSnykMcpServer", oldValue, newValue, triggerSource)` + - Example: `analytics.SendConfigChangedAnalytics(c, "secureAtInceptionExecutionFrequency", oldValue, newValue, triggerSource)` +- [ ] Run tests: `make test` +- [ ] Run linter: `make lint` +- [ ] Fix any issues + +#### Step 4: Register MCP configuration notification handler +**Objective**: Add notification handler to send MCP config to IDE + +**Files to modify**: +- `snyk-ls/application/server/notification.go` + +**Actions**: +- [ ] Write tests for notification registration +- [ ] Add case in `registerNotifier` switch statement: + ```go + case types.SnykConfigureMcpParams: + notifier(c, srv, "$/snyk.configureSnykMCP", params) + logger.Debug().Interface("mcpConfig", params).Msg("sending MCP config to client") + ``` +- [ ] Update `configureMcp` to send notification via `di.Notifier().Send()` +- [ ] Run tests: `make test` +- [ ] Fix any issues + +#### Step 5: Update VS Code extension to handle MCP notification +**Objective**: Listen for `$/snyk.configureSnykMCP` and configure MCP + +**Files to modify**: +- `vscode-extension/src/snyk/common/languageServer/languageServer.ts` +- `vscode-extension/src/snyk/cli/mcp/mcp.ts` + +**Actions**: +- [ ] Write tests for notification handler +- [ ] Create interface for MCP config params in `languageServer.ts`: + ```typescript + interface McpConfigParams { + command: string; + args: string[]; + env: Record; + ideName: string; + } + ``` +- [ ] Register notification handler in `LanguageServer` class +- [ ] Refactor `mcp.ts`: + - Keep IDE-specific configuration functions + - Remove configuration change detection (now in LS) + - Remove analytics sending (now in LS) + - Create new function `handleMcpConfigNotification(params: McpConfigParams)` + - Delegate to appropriate IDE-specific function based on `ideName` +- [ ] Run tests: `npm run test:unit` +- [ ] Run tests: `npm run test:integration` +- [ ] Run linter: `npm run lint:fix` +- [ ] Fix any issues + +#### Step 6: Remove redundant analytics code from extension +**Objective**: Remove special analytics handling for MCP config changes + +**Files to modify**: +- `vscode-extension/src/snyk/common/configuration/securityAtInceptionHandler.ts` +- `vscode-extension/src/snyk/common/watchers/configurationWatcher.ts` + +**Actions**: +- [ ] Write/update tests for configuration handling +- [ ] In `securityAtInceptionHandler.ts`: + - Remove `sendConfigChangedAnalytics` function (lines 71-88) + - Remove calls to `sendConfigChangedAnalytics` (lines 42-49, 58-65) + - Keep memento state tracking + - Simplify to only call `configureMcpHosts` when needed +- [ ] In `configurationWatcher.ts`: + - Configuration change handling remains the same + - Analytics will now be sent by LS automatically +- [ ] Run tests: `npm run test:unit` +- [ ] Run linter: `npm run lint:fix` +- [ ] Fix any issues + +#### Step 7: Add configuration settings to extension settings sync +**Objective**: Ensure new settings are synced to LS + +**Files to check/modify**: +- `vscode-extension/src/snyk/common/languageServer/languageServer.ts` + +**Actions**: +- [ ] Verify settings sync includes MCP configuration fields +- [ ] Add if missing: `autoConfigureSnykMcpServer` and `secureAtInceptionExecutionFrequency` +- [ ] Run tests: `npm run test:unit` +- [ ] Fix any issues + +#### Step 8: Update documentation +**Objective**: Document the new MCP configuration flow + +**Files to create/modify**: +- `snyk-ls/docs/mcp-configuration.md` +- `vscode-extension/docs/` (if exists) + +**Actions**: +- [ ] Create mermaid diagram for MCP configuration flow +- [ ] Document the notification protocol +- [ ] Document IDE-specific configuration requirements +- [ ] Run `make generate-diagrams` (for snyk-ls) +- [ ] Add to implementation plan + +### Phase 3: Review + +#### Step 1: Code Review +- [ ] Self-review all changes +- [ ] Ensure all tests pass +- [ ] Ensure no new linting errors +- [ ] Verify test coverage >= 80% + +#### Step 2: Integration Testing +- [ ] Test MCP configuration in VS Code +- [ ] Test MCP configuration in Cursor +- [ ] Test MCP configuration in Windsurf +- [ ] Test configuration changes trigger reconfiguration +- [ ] Test analytics are sent correctly +- [ ] Verify rules publishing still works + +#### Step 3: Security Scanning +- [ ] Run `snyk_code_scan` on both repositories +- [ ] Run `snyk_sca_scan` on both repositories (if dependencies changed) +- [ ] Fix any security issues found + +#### Step 4: Final Cleanup +- [ ] Remove any temporary test files +- [ ] Update CHANGELOG entries +- [ ] Verify no implementation plan files are staged for commit + +## Progress Tracking + +### Current Status +- Phase: Implementation (Phase 2) +- Step: Step 1 - Add MCP configuration settings to Language Server +- Next: Write tests for new configuration fields + +### Completed Steps +- [x] Initial analysis +- [x] Implementation plan creation +- [x] Plan approval received + +### In Progress +- [ ] Phase 2, Step 1: Add MCP configuration settings to Language Server + +### Blocked +- None + +## Technical Details + +### MCP Environment Variables +```typescript +// Built by language server, sent to IDE +{ + SNYK_CFG_ORG?: string, // From config.organization + SNYK_API?: string, // From config.snykApiEndpoint + IDE_CONFIG_PATH?: string, // IDE name from integration environment + TRUSTED_FOLDERS?: string // Semicolon-separated trusted folders +} +``` + +### Notification Flow +``` +Configuration Change + ↓ +LS: workspace/didChangeConfiguration handler + ↓ +LS: UpdateSettings + ↓ +LS: Detect MCP config change + ↓ +LS: Send analytics (using existing infrastructure) + ↓ +LS: Send $/snyk.configureSnykMCP notification + ↓ +IDE: Receive notification + ↓ +IDE: Call IDE-specific configuration function + ↓ +IDE: Configure MCP (file writes, API calls, etc.) +``` + +### Files Summary + +**Language Server (snyk-ls)**: +- `internal/types/lsp.go` - Add Settings fields and notification type +- `application/config/config.go` - Add config fields and methods +- `application/config/mcp_config.go` - New: MCP configuration logic +- `application/config/mcp_config_test.go` - New: Tests +- `application/server/configuration.go` - Update settings handler +- `application/server/notification.go` - Register notification +- `docs/mcp-configuration.md` - New: Documentation + +**VS Code Extension (vscode-extension)**: +- `src/snyk/common/languageServer/languageServer.ts` - Add notification handler +- `src/snyk/cli/mcp/mcp.ts` - Refactor to handle notifications +- `src/snyk/common/configuration/securityAtInceptionHandler.ts` - Remove analytics +- `src/snyk/common/watchers/configurationWatcher.ts` - Simplify (analytics now in LS) + +## Testing Strategy + +### Unit Tests +- **Language Server**: + - Configuration field parsing + - MCP environment building + - MCP command/args generation + - IDE name detection + - Notification sending + +- **Extension**: + - Notification handler registration + - MCP configuration delegation + - IDE-specific configuration (with mocks) + +### Integration Tests +- **Language Server**: + - Configuration change triggers MCP notification + - Analytics sent on config change + +- **Extension**: + - Notification received triggers configuration + - Configuration changes persist correctly + +### E2E Tests (Manual) +- Test in VS Code with Copilot +- Test in Cursor +- Test in Windsurf +- Verify MCP servers appear correctly +- Verify rules are published correctly + +## Commit Strategy + +Atomic commits for each step: +1. `feat: add MCP configuration settings to language server [ISSUE-ID]` +2. `feat: add MCP configuration notification type [ISSUE-ID]` +3. `feat: implement MCP configuration logic in language server [ISSUE-ID]` +4. `feat: register MCP configuration notification handler [ISSUE-ID]` +5. `feat: add MCP notification handler to VS Code extension [ISSUE-ID]` +6. `refactor: remove redundant MCP analytics from extension [ISSUE-ID]` +7. `chore: update MCP configuration documentation [ISSUE-ID]` + +## Notes +- No issue ID found in current branch (main) +- This is a cross-repository change (snyk-ls + vscode-extension) +- Must maintain backwards compatibility during transition +- Analytics flow changes but analytics data remains the same +- IDE-specific logic remains in extension (can't move file I/O and API calls to LS) + diff --git a/mcp_to_ls_implementation_plan.mmd b/mcp_to_ls_implementation_plan.mmd new file mode 100644 index 000000000..aa450c853 --- /dev/null +++ b/mcp_to_ls_implementation_plan.mmd @@ -0,0 +1,49 @@ +%% MCP Configuration Flow + +graph TB + subgraph "VS Code Extension" + A[Configuration Change Detected] --> B[Send workspace/didChangeConfiguration] + N[Receive $/snyk.configureSnykMCP] --> O{IDE Type?} + O -->|VS Code| P[configureCopilot] + O -->|Cursor| Q[configureCursor] + O -->|Windsurf| R[configureWindsurf] + P --> S[Register MCP Provider] + P --> T[Write Rules Files] + Q --> U[Update mcp.json] + Q --> V[Write Rules Files] + R --> W[Update mcp_config.json] + R --> X[Write Rules Files] + end + + subgraph "Language Server" + B --> C[workspaceDidChangeConfiguration Handler] + C --> D[UpdateSettings] + D --> E{MCP Config Changed?} + E -->|Yes| F[Send Analytics] + E -->|Yes| G[Build MCP Config] + E -->|No| H[Return] + F --> I[analytics.SendConfigChangedAnalytics] + G --> J[Get CLI Path] + G --> K[Build Args: mcp -t stdio] + G --> L[Build Env Variables] + J --> M[Create SnykConfigureMcpParams] + K --> M + L --> M + M --> N + end + + style A fill:#e1f5ff + style N fill:#fff4e1 + style I fill:#e8f5e9 + style M fill:#f3e5f5 + + classDef extensionNode fill:#e1f5ff,stroke:#01579b + classDef lsNode fill:#fff4e1,stroke:#e65100 + classDef analyticsNode fill:#e8f5e9,stroke:#1b5e20 + classDef notificationNode fill:#f3e5f5,stroke:#4a148c + + class A,O,P,Q,R,S,T,U,V,W,X extensionNode + class C,D,E,G,H,J,K,L lsNode + class F,I analyticsNode + class M,N notificationNode + diff --git a/src/snyk/cli/mcp/mcp.ts b/src/snyk/cli/mcp/mcp.ts deleted file mode 100644 index 9e725fe8c..000000000 --- a/src/snyk/cli/mcp/mcp.ts +++ /dev/null @@ -1,327 +0,0 @@ -import * as vscode from 'vscode'; -import { IConfiguration } from '../../common/configuration/configuration'; -import { Logger } from '../../common/logger/logger'; -import * as fs from 'fs'; -import * as os from 'os'; -import path from 'path'; - -type Env = Record; -interface McpServer { - command: string; - args: string[]; - env: Env; -} -interface McpConfig { - mcpServers: Record; -} - -const SERVER_KEY = 'Snyk'; - -export async function configureMcpHosts(vscodeContext: vscode.ExtensionContext, configuration: IConfiguration) { - const appName = vscode.env.appName.toLowerCase(); - const isWindsurf = appName.includes('windsurf'); - const isCursor = appName.includes('cursor'); - const isVsCode = appName.includes('visual studio code'); - - if (isCursor) { - await configureCursor(vscodeContext, configuration); - return; - } - if (isWindsurf) { - await configureWindsurf(vscodeContext, configuration); - return; - } - if (isVsCode) { - await configureCopilot(vscodeContext, configuration); - return; - } -} - -export async function configureCopilot(vscodeContext: vscode.ExtensionContext, configuration: IConfiguration) { - const autoConfigureMcpServer = configuration.getAutoConfigureMcpServer(); - const secureAtInceptionExecutionFrequency = configuration.getSecureAtInceptionExecutionFrequency(); - try { - if (autoConfigureMcpServer) { - vscodeContext.subscriptions.push( - /* eslint-disable @typescript-eslint/no-unsafe-argument */ - /* eslint-disable @typescript-eslint/no-unsafe-call */ - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ - // @ts-expect-error backward compatibility for older VS Code versions - vscode.lm.registerMcpServerDefinitionProvider('snyk-security-scanner', { - onDidChangeMcpServerDefinitions: new vscode.EventEmitter().event, - provideMcpServerDefinitions: async () => { - // @ts-expect-error backward compatibility for older VS Code versions - const output: vscode.McpServerDefinition[][] = []; - - /* eslint-disable @typescript-eslint/no-unsafe-call */ - const cliPath = await configuration.getCliPath(); - /* eslint-disable @typescript-eslint/no-unsafe-return */ - const args = ['mcp', '-t', 'stdio']; - const snykEnv = getSnykMcpEnv(configuration); - const processEnv: Env = {}; - Object.entries(process.env).forEach(([key, value]) => { - processEnv[key] = value ?? ''; - }); - const env: Env = { ...processEnv, ...snykEnv }; - - // @ts-expect-error backward compatibility for older VS Code versions - output.push(new vscode.McpStdioServerDefinition(SERVER_KEY, cliPath, args, env)); - - return output; - }, - }), - ); - } - } catch (err) { - Logger.debug( - `VS Code MCP Server Definition Provider API is not available. This feature requires VS Code version > 1.101.0.`, - ); - } - - // Rules publishing for Copilot - const filePath = path.join('.github', 'instructions', 'snyk_rules.instructions.md'); - try { - if (secureAtInceptionExecutionFrequency === 'Manual') { - // Delete rules from project - await deleteLocalRulesForIde(filePath); - return; - } - const rulesContent = await readBundledRules(vscodeContext, secureAtInceptionExecutionFrequency); - await writeLocalRulesForIde(filePath, rulesContent); - await ensureInGitignore([filePath]); - } catch { - Logger.error('Failed to publish Copilot rules'); - } -} - -export async function configureWindsurf(vscodeContext: vscode.ExtensionContext, configuration: IConfiguration) { - const autoConfigureMcpServer = configuration.getAutoConfigureMcpServer(); - const secureAtInceptionExecutionFrequency = configuration.getSecureAtInceptionExecutionFrequency(); - try { - if (autoConfigureMcpServer) { - const baseDir = path.join(os.homedir(), '.codeium', 'windsurf'); - const configPath = path.join(baseDir, 'mcp_config.json'); - if (!fs.existsSync(baseDir)) { - Logger.debug(`Windsurf base directory not found at ${baseDir}, skipping MCP configuration.`); - } else { - const cliPath = await configuration.getCliPath(); - const env = getSnykMcpEnv(configuration); - await ensureMcpServerInJson(configPath, SERVER_KEY, cliPath, ['mcp', '-t', 'stdio'], env); - Logger.debug(`Ensured Windsurf MCP config at ${configPath}`); - } - } - } catch { - Logger.error('Failed to update Windsurf MCP config'); - } - - const localPath = path.join('.windsurf', 'rules', 'snyk_rules.md'); - try { - if (secureAtInceptionExecutionFrequency === 'Manual') { - // Delete rules from project - await deleteLocalRulesForIde(localPath); - return; - } - const rulesContent = await readBundledRules(vscodeContext, secureAtInceptionExecutionFrequency); - await writeLocalRulesForIde(localPath, rulesContent); - await ensureInGitignore([localPath]); - } catch { - Logger.error('Failed to publish Windsurf rules'); - } -} - -export async function configureCursor(vscodeContext: vscode.ExtensionContext, configuration: IConfiguration) { - const autoConfigureMcpServer = configuration.getAutoConfigureMcpServer(); - const secureAtInceptionExecutionFrequency = configuration.getSecureAtInceptionExecutionFrequency(); - try { - if (autoConfigureMcpServer) { - const configPath = path.join(os.homedir(), '.cursor', 'mcp.json'); - const cliPath = await configuration.getCliPath(); - const env = getSnykMcpEnv(configuration); - - await ensureMcpServerInJson(configPath, SERVER_KEY, cliPath, ['mcp', '-t', 'stdio'], env); - Logger.debug(`Ensured Cursor MCP config at ${configPath}`); - } - } catch { - Logger.error('Failed to update Cursor MCP config'); - } - - const cursorRulesPath = path.join('.cursor', 'rules', 'snyk_rules.mdc'); - try { - if (secureAtInceptionExecutionFrequency === 'Manual') { - // Delete rules from project (Cursor doesn't support global rules) - await deleteLocalRulesForIde(cursorRulesPath); - return; - } - - const rulesContent = await readBundledRules(vscodeContext, secureAtInceptionExecutionFrequency); - await writeLocalRulesForIde(cursorRulesPath, rulesContent); - await ensureInGitignore([cursorRulesPath]); - } catch { - Logger.error('Failed to publish Cursor rules'); - } -} - -async function ensureMcpServerInJson( - filePath: string, - serverKey: string, - command: string, - args: string[], - env: Env, -): Promise { - let raw: unknown = undefined; - if (fs.existsSync(filePath)) { - try { - raw = JSON.parse(await fs.promises.readFile(filePath, 'utf8')); - } catch { - // ignore parse error; will recreate minimal structure - } - } - type RawConfig = { mcpServers?: Record }; - const config: McpConfig = { mcpServers: {} }; - if (raw && typeof raw === 'object' && raw !== null && Object.prototype.hasOwnProperty.call(raw, 'mcpServers')) { - const servers = (raw as RawConfig).mcpServers; - if (servers && typeof servers === 'object') { - config.mcpServers = servers; - } - } - - const serverKeyLower = serverKey.toLowerCase(); - let matchedKey: string | undefined = undefined; - for (const key of Object.keys(config.mcpServers)) { - const lower = key.toLowerCase(); - if (lower === serverKeyLower || lower.includes(serverKeyLower)) { - matchedKey = key; - break; - } - } - const keyToUse = matchedKey ?? serverKey; - const existing = config.mcpServers[keyToUse]; - const desired: McpServer = { command, args, env }; - - // Merge env: keep existing keys; add or override Snyk keys - let resultingEnv: Env; - if (existing && existing.env) { - resultingEnv = { ...existing.env }; - const overrideKeys: (keyof Env)[] = ['SNYK_CFG_ORG', 'SNYK_API', 'IDE_CONFIG_PATH', 'TRUSTED_FOLDERS']; - for (const k of overrideKeys) { - if (typeof env[k] !== 'undefined') { - resultingEnv[k] = env[k]; - } - } - } else { - resultingEnv = { ...(env || {}) }; - } - - const needsWrite = - !existing || - existing.command !== desired.command || - JSON.stringify(existing.args) !== JSON.stringify(desired.args) || - JSON.stringify(existing.env || {}) !== JSON.stringify(resultingEnv || {}); - - if (!needsWrite) return; - - config.mcpServers[keyToUse] = { command, args, env: resultingEnv }; - await fs.promises.mkdir(path.dirname(filePath), { recursive: true }); - await fs.promises.writeFile(filePath, JSON.stringify(config, null, 2), 'utf8'); -} - -async function readBundledRules(vsCodeContext: vscode.ExtensionContext, frequency: string): Promise { - const rulesFileName = frequency === 'Smart Scan' ? 'snyk_rules_smart_apply.md' : 'snyk_rules_always_apply.md'; - return await fs.promises.readFile(path.join(vsCodeContext.extensionPath, 'out', 'assets', rulesFileName), 'utf8'); -} - -async function writeLocalRulesForIde(relativeRulesPath: string, rulesContent: string): Promise { - const folders = vscode.workspace.workspaceFolders; - if (!folders || folders.length === 0) { - void vscode.window.showInformationMessage('No workspace folder found. Local rules require an open workspace.'); - return; - } - for (const folder of folders) { - const root = folder.uri.fsPath; - const rulesPath = path.join(root, relativeRulesPath); - await fs.promises.mkdir(path.dirname(rulesPath), { recursive: true }); - let existing = ''; - try { - existing = await fs.promises.readFile(rulesPath, 'utf8'); - } catch { - // ignore - } - if (existing !== rulesContent) { - await fs.promises.writeFile(rulesPath, rulesContent, 'utf8'); - Logger.debug(`Wrote local rules to ${rulesPath}`); - } else { - Logger.debug(`Local rules already up to date at ${rulesPath}.`); - } - } -} - -async function deleteLocalRulesForIde(relativeRulesPath: string): Promise { - const folders = vscode.workspace.workspaceFolders; - if (!folders || folders.length === 0) { - return; - } - for (const folder of folders) { - const root = folder.uri.fsPath; - const rulesPath = path.join(root, relativeRulesPath); - try { - if (fs.existsSync(rulesPath)) { - await fs.promises.unlink(rulesPath); - Logger.debug(`Deleted local rules from ${rulesPath}`); - } - } catch (err) { - Logger.debug(`Failed to delete local rules from ${rulesPath}: ${err}`); - } - } -} - -async function ensureInGitignore(patterns: string[]): Promise { - const folders = vscode.workspace.workspaceFolders; - if (!folders || folders.length === 0) { - return; - } - - await Promise.all( - folders.map(async folder => { - const gitignorePath = path.join(folder.uri.fsPath, '.gitignore'); - let content = ''; - - try { - content = await fs.promises.readFile(gitignorePath, 'utf8'); - } catch { - Logger.debug(`.gitignore does not exist at ${gitignorePath}`); - return; - } - - // Split into lines handling both \n and \r\n - const lines = content.split(/\r?\n/); - const missing = patterns.filter(p => !lines.some(line => line.trim() === p.trim())); - - if (missing.length === 0) { - Logger.debug(`Snyk rules already in .gitignore at ${gitignorePath}`); - return; - } - - const addition = `\n# Snyk Security Extension - AI Rules (auto-generated)\n${missing.join('\n')}\n`; - const updated = content + addition; - await fs.promises.writeFile(gitignorePath, updated, 'utf8'); - Logger.debug(`Added Snyk rules to .gitignore at ${gitignorePath}: ${missing.join(', ')}`); - }), - ); -} - -function getSnykMcpEnv(configuration: IConfiguration): Env { - const env: Env = {}; - if (configuration.organization) { - env.SNYK_CFG_ORG = configuration.organization; - } - if (configuration.snykApiEndpoint) { - env.SNYK_API = configuration.snykApiEndpoint; - } - const trustedFolders = configuration.getTrustedFolders(); - if (trustedFolders.length > 0) { - env.TRUSTED_FOLDERS = trustedFolders.join(';'); - } - env.IDE_CONFIG_PATH = vscode.env.appName; - - return env; -} diff --git a/src/snyk/common/configuration/securityAtInceptionHandler.ts b/src/snyk/common/configuration/securityAtInceptionHandler.ts deleted file mode 100644 index 18330662e..000000000 --- a/src/snyk/common/configuration/securityAtInceptionHandler.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { IExtension } from '../../base/modules/interfaces'; -import { ILog } from '../logger/interfaces'; -import { configuration } from './instance'; -import { DEFAULT_SECURE_AT_INCEPTION_EXECUTION_FREQUENCY } from './configuration'; -import { - MEMENTO_AUTO_CONFIGURE_MCP_SERVER, - MEMENTO_SECURE_AT_INCEPTION_EXECUTION_FREQUENCY, -} from '../constants/globalState'; -import { User } from '../user'; -import { AnalyticsSender } from '../analytics/AnalyticsSender'; -import { AnalyticsEvent } from '../analytics/AnalyticsEvent'; -import { vsCodeCommands } from '../vscode/commands'; -import { configureMcpHosts } from '../../cli/mcp/mcp'; -import * as vscode from 'vscode'; - -export async function handleSecurityAtInceptionChange( - extension: IExtension, - logger: ILog, - user: User, - vscodeContext: vscode.ExtensionContext, -): Promise { - if (!extension.context) { - return; - } - - const currentAutoConfigureMcpServerConfig = configuration.getAutoConfigureMcpServer(); - const currentSecureAtInceptionExecutionFrequencyConfig = configuration.getSecureAtInceptionExecutionFrequency(); - - const previousAutoConfigureMcpServerConfig = - extension.context.getGlobalStateValue(MEMENTO_AUTO_CONFIGURE_MCP_SERVER) ?? false; - - const previousSecureAtInceptionExecutionFrequencyConfig = - extension.context.getGlobalStateValue(MEMENTO_SECURE_AT_INCEPTION_EXECUTION_FREQUENCY) ?? - DEFAULT_SECURE_AT_INCEPTION_EXECUTION_FREQUENCY; - - if (currentAutoConfigureMcpServerConfig !== previousAutoConfigureMcpServerConfig) { - await extension.context.updateGlobalStateValue( - MEMENTO_AUTO_CONFIGURE_MCP_SERVER, - currentAutoConfigureMcpServerConfig, - ); - - sendConfigChangedAnalytics( - extension, - logger, - user, - 'autoConfigureSnykMcpServer', - previousAutoConfigureMcpServerConfig, - currentAutoConfigureMcpServerConfig, - ); - } - - if (currentSecureAtInceptionExecutionFrequencyConfig !== previousSecureAtInceptionExecutionFrequencyConfig) { - await extension.context.updateGlobalStateValue( - MEMENTO_SECURE_AT_INCEPTION_EXECUTION_FREQUENCY, - currentSecureAtInceptionExecutionFrequencyConfig, - ); - - sendConfigChangedAnalytics( - extension, - logger, - user, - 'secureAtInceptionExecutionFrequency', - previousSecureAtInceptionExecutionFrequencyConfig, - currentSecureAtInceptionExecutionFrequencyConfig, - ); - } - - await configureMcpHosts(vscodeContext, configuration); -} - -function sendConfigChangedAnalytics( - extension: IExtension, - logger: ILog, - user: User, - field: string, - oldValue: boolean | string, - newValue: boolean | string, -): void { - const analyticsSender = AnalyticsSender.getInstance(logger, configuration, vsCodeCommands, extension.contextService); - - const event = new AnalyticsEvent(user.anonymousId, 'Config changed', []); - event.getExtension().set(`config::${field}::oldValue`, oldValue); - event.getExtension().set(`config::${field}::newValue`, newValue); - - analyticsSender.logEvent(event, () => { - logger.info(`Analytics event sent for config change: securityAtInception.${field}`); - }); -} diff --git a/src/snyk/common/constants/languageServer.ts b/src/snyk/common/constants/languageServer.ts index cfeb976aa..303de5df2 100644 --- a/src/snyk/common/constants/languageServer.ts +++ b/src/snyk/common/constants/languageServer.ts @@ -14,3 +14,4 @@ export const SNYK_SCAN = '$/snyk.scan'; export const SNYK_FOLDERCONFIG = '$/snyk.folderConfigs'; export const SNYK_SCANSUMMARY = '$/snyk.scanSummary'; export const SNYK_MCPSERVERURL = '$/snyk.mcpServerURL'; +export const SNYK_CONFIGURE_MCP = '$/snyk.configureSnykMCP'; diff --git a/src/snyk/common/languageServer/languageServer.ts b/src/snyk/common/languageServer/languageServer.ts index 05a63ae14..1e354c9f7 100644 --- a/src/snyk/common/languageServer/languageServer.ts +++ b/src/snyk/common/languageServer/languageServer.ts @@ -1,9 +1,11 @@ import _ from 'lodash'; import { firstValueFrom, ReplaySubject, Subject } from 'rxjs'; +import * as vscode from 'vscode'; import { IAuthenticationService } from '../../base/services/authenticationService'; import { FolderConfig, IConfiguration } from '../configuration/configuration'; import { SNYK_ADD_TRUSTED_FOLDERS, + SNYK_CONFIGURE_MCP, SNYK_FOLDERCONFIG, SNYK_HAS_AUTHENTICATED, SNYK_LANGUAGE_SERVER_NAME, @@ -51,6 +53,7 @@ export class LanguageServer implements ILanguageServer { public static ReceivedFolderConfigsFromLs = false; constructor( + private vscodeContext: vscode.ExtensionContext, private user: User, private configuration: IConfiguration, private languageClientAdapter: ILanguageClientAdapter, @@ -217,6 +220,35 @@ export class LanguageServer implements ILanguageServer { client.onNotification(SNYK_SCANSUMMARY, ({ scanSummary }: { scanSummary: string }) => { this.summaryProvider.updateSummaryPanel(scanSummary); }); + + client.onNotification( + SNYK_CONFIGURE_MCP, + async (params: { command: string; args: string[]; env: Record; ideName: string }) => { + this.logger.info(`Received MCP configuration for VS Code Copilot`); + try { + // This notification is ONLY sent for VS Code (not Cursor/Windsurf) + // LS handles file writes for Cursor/Windsurf directly + this.vscodeContext.subscriptions.push( + /* eslint-disable @typescript-eslint/no-unsafe-argument */ + /* eslint-disable @typescript-eslint/no-unsafe-call */ + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + // @ts-expect-error backward compatibility for older VS Code versions + vscode.lm.registerMcpServerDefinitionProvider('snyk-security-scanner', { + onDidChangeMcpServerDefinitions: new vscode.EventEmitter().event, + provideMcpServerDefinitions: async () => { + // @ts-expect-error backward compatibility for older VS Code versions + return [new vscode.McpStdioServerDefinition('Snyk', params.command, params.args, params.env)]; + }, + }), + ); + this.logger.info('VS Code Copilot MCP server registered'); + } catch (error) { + this.logger.debug( + `VS Code MCP Server Definition Provider API is not available. This feature requires VS Code version > 1.101.0.`, + ); + } + }, + ); } // Initialization options are not semantically equal to server settings, thus separated here diff --git a/src/snyk/common/watchers/configurationWatcher.ts b/src/snyk/common/watchers/configurationWatcher.ts index 2322dd738..611cc0ca8 100644 --- a/src/snyk/common/watchers/configurationWatcher.ts +++ b/src/snyk/common/watchers/configurationWatcher.ts @@ -28,15 +28,9 @@ import { errorsLogs } from '../messages/errors'; import SecretStorageAdapter from '../vscode/secretStorage'; import { IWatcher } from './interfaces'; import { SNYK_CONTEXT } from '../constants/views'; -import { handleSecurityAtInceptionChange } from '../configuration/securityAtInceptionHandler'; -import { User } from '../user'; class ConfigurationWatcher implements IWatcher { - constructor( - private readonly logger: ILog, - private readonly user: User, - private readonly vscodeContext: vscode.ExtensionContext, - ) {} + constructor(private readonly logger: ILog) {} private async onChangeConfiguration(extension: IExtension, key: string): Promise { if (key === ADVANCED_ORGANIZATION) { @@ -71,8 +65,6 @@ class ConfigurationWatcher implements IWatcher { } else if (key === TRUSTED_FOLDERS) { extension.workspaceTrust.resetTrustedFoldersCache(); extension.viewManagerService.refreshAllViews(); - } else if (key === AUTO_CONFIGURE_MCP_SERVER || key === SECURITY_AT_INCEPTION_EXECUTION_FREQUENCY) { - return handleSecurityAtInceptionChange(extension, this.logger, this.user, this.vscodeContext); } // from here on only for OSS and trusted folders diff --git a/src/snyk/extension.ts b/src/snyk/extension.ts index c54c7d16d..525c5e7e5 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -91,7 +91,6 @@ import { SummaryProviderService } from './base/summary/summaryProviderService'; import { ProductTreeViewService } from './common/services/productTreeViewService'; import { Extension } from './common/vscode/extension'; import { MarkdownStringAdapter } from './common/vscode/markdownString'; -import { configureMcpHosts } from './cli/mcp/mcp'; class SnykExtension extends SnykLib implements IExtension { public async activate(vscodeContext: vscode.ExtensionContext): Promise { @@ -176,7 +175,7 @@ class SnykExtension extends SnykLib implements IExtension { SecretStorageAdapter.init(vscodeContext); configuration.setExtensionId(vscodeContext.extension.id); - this.configurationWatcher = new ConfigurationWatcher(Logger, this.user, vscodeContext); + this.configurationWatcher = new ConfigurationWatcher(Logger); this.notificationService = new NotificationService(vsCodeWindow, vsCodeCommands, configuration, Logger); this.statusBarItem.show(); @@ -210,6 +209,7 @@ class SnykExtension extends SnykLib implements IExtension { this.experimentService = new ExperimentService(this.user, Logger, configuration, snykConfiguration); this.languageServer = new LanguageServer( + vscodeContext, this.user, configuration, languageClientAdapter, @@ -341,7 +341,6 @@ class SnykExtension extends SnykLib implements IExtension { this.languageServer, LsScanProduct.Code, ); - await configureMcpHosts(vscodeContext, configuration); vscodeContext.subscriptions.push( vscode.window.registerTreeDataProvider(securityCodeView, codeSecurityIssueProvider), codeSecurityTree,