Skip to main content
Glama
index.ts23.9 kB
if (process.env.SNAPBACK_MCP_SELFTEST === "1") { const rssMB = Math.round(process.memoryUsage().rss / 1024 / 1024); process.stderr.write(`SnapBack MCP Server started rssMB=${rssMB}\n`); setTimeout(() => process.exit(0), 50).unref(); } 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 { DependencyAnalyzer, MCPClientManager, validateToolArgs } from "@snapback/core"; import { SnapBackEventBusEventEmitter2 as SnapBackEventBus } from "@snapback/events"; import { evaluate } from "@snapback/policy-engine"; // Import the proper policy engine import { z } from "zod"; import { authenticate, hasToolAccess } from "./auth.js"; import { ExtensionIPCClient } from "./client/extension-ipc.js"; import { SnapBackAPIClient } from "./client/snapback-api.js"; import { AnalysisRouter } from "./services/AnalysisRouter.js"; import { Context7Service } from "./context7/index.js"; import { MCPHttpServer } from "./http-server.js"; import { CreateSnapshotSchema, createSnapshot } from "./tools/create-snapshot.js"; import { addSnapshot, listSnapshots } from "./tools/list-snapshots.js"; import { restoreSnapshot, storeSnapshotContent } from "./tools/restore-snapshot.js"; import { addResult, createSarifLog } from "./utils/sarif.js"; import { initializeSecurityTelemetry, setWorkspaceRoot } from "./utils/security.js"; /** * Performance tracker for monitoring operation times * * Note: Performance budgets are MCP-specific operational constraints. * For data-related thresholds (risk scores, session timeouts, etc.), * use centralized THRESHOLDS from @snapback/sdk. */ async function trackPerformance<T>(operation: string, fn: () => Promise<T>): Promise<T> { const start = Date.now(); try { return await fn(); } finally { const duration = Date.now() - start; console.error(`[PERF] ${operation}: ${duration}ms`); // MCP-specific performance budgets (operational, not data thresholds) // For data thresholds (risk, session, protection), import THRESHOLDS from @snapback/sdk const PERFORMANCE_BUDGETS: Record<string, number> = { analyze_risk: 200, // Budget for risk analysis operation create_snapshot: 500, // Budget for snapshot creation }; const budget = PERFORMANCE_BUDGETS[operation] || 1000; if (duration > budget) { console.warn(`[PERF] ⚠️ ${operation} exceeded budget: ${duration}ms > ${budget}ms`); } } } // Error handler for production-grade error sanitization function sanitizeError( error: unknown, context: string, ): { message: string; code: string; logId: string; } { const isDevelopment = process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test"; const logId = `ERR-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Log full details internally (stderr only) console.error(`[Error ${logId}] ${context}:`, error); if (isDevelopment) { return { message: error instanceof Error ? error.message : String(error), code: "INTERNAL_ERROR", logId, }; } return { message: "An internal error occurred. Contact support with log ID.", code: "INTERNAL_ERROR", logId, }; } // Export a function to create and start the server export async function startServer(): Promise<{ server: Server; transport: StdioServerTransport; }> { // ARCHITECTURAL DECISION: API-First Approach // See CLAUDE.md "Architectural Decisions" section for rationale // Benefits: Consistency, feature flags, circuit breaker, centralized updates // Tradeoffs: Network dependency vs local processing // Future: AnalysisRouter will provide tier-based routing (local for Free, API for Pro) // See: apps/mcp-server/src/services/AnalysisRouter.IMPLEMENTATION.ts // Remove local Guardian initialization since we'll use the backend API // const guardian = new Guardian(); // Add detection plugins // guardian.addPlugin(new SecretDetectionPlugin()); // guardian.addPlugin(new MockReplacementPlugin()); // guardian.addPlugin(new PhantomDependencyPlugin()); const dep = new DependencyAnalyzer(); // Use StorageBrokerAdapter instead of LocalStorage for single-writer discipline // Use the standard workspace database path const sdkModule = await import("@snapback/sdk"); const StorageBrokerAdapter = sdkModule.StorageBrokerAdapter; const storage = new StorageBrokerAdapter(`${process.cwd()}/.snapback/snapback.db`); await storage.initialize(); const mcpManager = new MCPClientManager(); // Initialize Context7 service for documentation and code search const context7Service = new Context7Service(storage); // Initialize event bus for pub/sub with EventEmitter2 const eventBus = new SnapBackEventBus(); await eventBus.initialize(); console.error("[SnapBack MCP] Using EventEmitter2 event bus"); // Initialize IPC client for request/response with Extension const extensionClient = new ExtensionIPCClient(); try { await extensionClient.connect(); console.error("[SnapBack MCP] Connected to Extension IPC"); } catch (err) { console.error("[SnapBack MCP] Failed to connect to Extension IPC:", err); } // Initialize AnalysisRouter with optional API client const apiClient = process.env.SNAPBACK_API_KEY ? new SnapBackAPIClient({ baseUrl: process.env.SNAPBACK_API_URL || "https://api.snapback.dev", apiKey: process.env.SNAPBACK_API_KEY, }) : undefined; const _analysisRouter = new AnalysisRouter(apiClient); // Set workspace root for path validation // In a real implementation, this would come from the extension or config // For now, we'll use the current working directory as the workspace root setWorkspaceRoot(process.cwd()); // Initialize security telemetry // In a real implementation, this would come from configuration initializeSecurityTelemetry("https://telemetry.snapback.dev"); const server = new Server( { name: "snapback-mcp", version: "0.1.1" }, { capabilities: { tools: {}, }, }, ); // Register tools listing server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "snapback.analyze_risk", description: `**Purpose:** Analyze code changes for potential risks before applying them. **When to Use:** - BEFORE accepting AI-generated code suggestions - When reviewing pull requests with complex changes - For critical files (auth, security, database schemas) - When user asks "is this safe to apply?" **When NOT to Use:** - For trivial changes (typo fixes, formatting) - For non-code files (images) - After changes are already applied **Returns:** - Risk level (safe, low, medium, high, critical) - Specific issues detected with severity levels - Actionable recommendations for mitigation **Performance:** < 200ms average`, inputSchema: { type: "object", properties: { changes: { type: "array", description: "Array of diff changes (added/removed lines with content)", items: { type: "object", properties: { added: { type: "boolean", description: "True if this line was added", }, removed: { type: "boolean", description: "True if this line was removed", }, value: { type: "string", description: "The actual line content", }, count: { type: "number", description: "Number of lines (optional)", }, }, required: ["value"], }, }, }, required: ["changes"], }, // Free-tier tool - does not require backend requiresBackend: false, }, { name: "snapback.check_dependencies", description: `**Purpose:** Check for dependency-related risks when package.json changes. **When to Use:** - After modifying package.json dependencies - Before installing new packages - When troubleshooting version conflicts - When user adds/removes npm packages **Returns:** - Added dependencies with known vulnerabilities - Removed dependencies that might break existing code - Version changes that could cause compatibility issues`, inputSchema: { type: "object", properties: { before: { type: "object", description: "Dependencies object before changes", additionalProperties: true, }, after: { type: "object", description: "Dependencies object after changes", additionalProperties: true, }, }, required: ["before", "after"], }, // Free-tier tool - does not require backend requiresBackend: false, }, { name: "snapback.create_snapshot", description: `**Purpose:** Create a code snapshot before making risky changes. **When to Use:** - Before major refactoring - Before implementing breaking changes - When about to modify critical files - As a safety net for experimental changes - When user explicitly asks to "create snapshot" or "save snapshot" **When NOT to Use:** - After every single file save (too frequent) - For trivial changes (typo fixes) **Returns:** - Snapshot ID for later restoration - Timestamp of snapshot - Files included in snapshot **Performance:** < 500ms average (larger codebases may take longer)`, inputSchema: CreateSnapshotSchema, // Pro-tier tool - requires backend requiresBackend: true, }, { name: "snapback.list_snapshots", description: `**Purpose:** List all available code snapshots for restoration. **When to Use:** - When user wants to see available restore points - Before choosing which snapshot to restore - To verify a snapshot was created successfully **Returns:** - Array of snapshots with IDs, timestamps, and descriptions - Recent snapshots listed first (up to 500 most recent)`, inputSchema: { type: "object", properties: {}, }, // Pro-tier tool - requires backend requiresBackend: true, }, { name: "snapback.restore_snapshot", description: `**Purpose:** Restore code from a previously created snapshot. **When to Use:** - When user wants to revert to a previous state - After realizing changes were problematic - To restore from a known good state **Returns:** - The content of the restored snapshot - Files and their contents`, inputSchema: { type: "object", properties: { snapshotId: { type: "string", description: "The ID of the snapshot to restore", }, }, required: ["snapshotId"], }, // Pro-tier tool - requires backend requiresBackend: true, }, { name: "catalog.list_tools", description: "List available tools from connected external MCP servers (Context7, GitHub, NPM Registry, etc.)", inputSchema: { type: "object", properties: {}, }, // Free-tier tool - does not require backend requiresBackend: false, }, { name: "ctx7.resolve-library-id", description: `**Purpose:** Resolve a library or package name into a Context7-compatible library ID. **When to Use:** - When you need documentation for a specific library - Before calling ctx7.get-library-docs - To find the correct library ID for a package name **Returns:** - Context7-compatible library ID - Library metadata (description, trust score, versions) - Multiple matches if applicable`, inputSchema: { type: "object", properties: { libraryName: { type: "string", description: "The name of the library to resolve", }, }, required: ["libraryName"], }, // Free-tier tool - does not require backend requiresBackend: false, }, { name: "ctx7.get-library-docs", description: `**Purpose:** Fetch up-to-date documentation for a specific library. **When to Use:** - When you need documentation for a library - To understand library APIs and usage patterns - For code examples and best practices **Returns:** - Formatted documentation with code examples - API references and integration patterns - Version-specific information`, inputSchema: { type: "object", properties: { context7CompatibleLibraryID: { type: "string", description: "The Context7-compatible library ID", }, topic: { type: "string", description: "Optional topic to filter documentation", }, tokens: { type: "number", description: "Optional limit on response size in tokens", }, }, required: ["context7CompatibleLibraryID"], }, // Free-tier tool - does not require backend requiresBackend: false, }, ], })); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { // Get API key from environment (for stdio transport) // In a real implementation with HTTP transport, this would come from headers const apiKey = process.env.SNAPBACK_API_KEY || ""; // Authenticate user const authResult = await authenticate(apiKey); // Check if user has access to this tool if (!hasToolAccess(authResult, name)) { return { content: [ { type: "text", text: `❌ Access denied. You don't have permission to use the ${name} tool.`, }, ], isError: true, }; } // Handle SnapBack's native tools if (name === "snapback.analyze_risk") { // ARCHITECTURAL DECISION: API-First Approach // See CLAUDE.md "Architectural Decisions" section for rationale // Current implementation proxies to backend API for consistent detection // Future: AnalysisRouter will provide tier-based routing // See: apps/mcp-server/src/services/AnalysisRouter.IMPLEMENTATION.ts const parsed = z .object({ changes: z.array( z.object({ added: z.boolean().optional().default(false), removed: z.boolean().optional().default(false), value: z.string(), count: z.number().optional(), }), ), }) .parse(args); // Proxy to backend API instead of using local Guardian try { // Create API client const apiClient = new SnapBackAPIClient({ baseUrl: process.env.SNAPBACK_API_URL || "https://api.snapback.dev", apiKey: apiKey, }); // Prepare the analysis request // Combine all changes into a single code string for analysis const code = parsed.changes.map((change) => change.value).join("\n"); // For now, we'll use a placeholder file path const filePath = "mcp-analysis.ts"; const analysisRequest = { code, filePath, context: { projectType: "mcp-analysis", language: "typescript", }, }; // Call the backend API const risk = await apiClient.analyzeFast(analysisRequest); // Create SARIF log const sarifLog = createSarifLog("snapback-analyze-risk", "1.0.0"); // Add results to SARIF log based on risk factors if (risk.factors && risk.factors.length > 0) { risk.factors.forEach((factor, index) => { addResult(sarifLog, `risk-factor-${index + 1}`, factor, undefined, undefined); }); } else { // Add a default result if no factors addResult(sarifLog, "analysis-complete", "Risk analysis completed", undefined, undefined); } // Evaluate SARIF against policy using the proper policy engine const policyDecision = evaluate(sarifLog); return { content: [ { type: "json", json: risk }, { type: "json", json: sarifLog }, { type: "text", text: `Policy Decision: ${policyDecision.action.toUpperCase()}\nReason: ${policyDecision.reason}\nConfidence: ${policyDecision.confidence.toFixed(2)}`, }, ], }; } catch (error) { // Fallback to basic analysis if API call fails console.error("Backend API call failed, using basic analysis:", error); // Create a basic risk analysis as fallback const basicRisk = { riskLevel: "low", score: 0, factors: [], analysisTimeMs: 10, issues: [], }; // Create SARIF log const sarifLog = createSarifLog("snapback-analyze-risk", "1.0.0"); addResult( sarifLog, "analysis-complete", "Risk analysis completed with basic fallback", undefined, undefined, ); // Evaluate SARIF against policy using the proper policy engine const policyDecision = evaluate(sarifLog); return { content: [ { type: "json", json: basicRisk }, { type: "json", json: sarifLog }, { type: "text", text: `Policy Decision: ${policyDecision.action.toUpperCase()} Reason: ${policyDecision.reason} Confidence: ${policyDecision.confidence.toFixed(2)} ⚠️ Using basic analysis due to backend connectivity issues.`, }, ], }; } } if (name === "snapback.check_dependencies") { const parsed = z .object({ before: z.record(z.string(), z.any()), after: z.record(z.string(), z.any()), }) .parse(args); const result = dep.quickAnalyze(parsed.before, parsed.after); return { content: [{ type: "json", json: result }] }; } if (name === "snapback.create_snapshot") { // Check if user has access to this Pro-tier tool if (authResult.tier !== "pro") { // Return SARIF note for Free users trying to access Pro tools const sarifLog = createSarifLog("snapback-create-snapshot", "1.0.0"); addResult( sarifLog, "pro-tool-restricted", "This tool requires a Pro subscription. Upgrade at https://snapback.dev/pricing", undefined, undefined, ); return { content: [ { type: "json", json: sarifLog }, { type: "text", text: "❌ This tool requires a Pro subscription. Upgrade at https://snapback.dev/pricing", }, ], }; } // Validate arguments using the security schema const validation = validateToolArgs(args, CreateSnapshotSchema); if (!validation.success) { return validation.error; } const input = validation.data; // Create snapshot const result = await createSnapshot(input); if (!result.success) { return { content: [ { type: "text", text: `❌ Failed to create snapshot: ${result.error}`, }, ], isError: true, }; } // Check if snapshot exists if (!result.snapshot) { return { content: [ { type: "text", text: "❌ Failed to create snapshot: No snapshot returned", }, ], isError: true, }; } // Store snapshot in the list addSnapshot(result.snapshot); // If files were provided, store their content if (input.files && input.files.length > 0) { storeSnapshotContent(result.snapshot.id, input.files); } return { content: [ { type: "text", text: `✅ Snapshot created successfully ID: ${result.snapshot.id} Timestamp: ${new Date(result.snapshot.timestamp).toLocaleString()} Reason: ${result.snapshot.reason} File Count: ${result.snapshot.fileCount} You can restore this snapshot using its ID.`, }, ], }; } if (name === "snapback.list_snapshots") { // Check if user has access to this Pro-tier tool if (authResult.tier !== "pro") { // Return SARIF note for Free users trying to access Pro tools const sarifLog = createSarifLog("snapback-list-snapshots", "1.0.0"); addResult( sarifLog, "pro-tool-restricted", "This tool requires a Pro subscription. Upgrade at https://snapback.dev/pricing", undefined, undefined, ); return { content: [ { type: "json", json: sarifLog }, { type: "text", text: "❌ This tool requires a Pro subscription. Upgrade at https://snapback.dev/pricing", }, ], }; } const result = await listSnapshots(); if (!result.success) { return { content: [ { type: "text", text: `❌ Failed to list snapshots: ${result.error}`, }, ], isError: true, }; } return { content: [{ type: "json", json: result.snapshots }] }; } if (name === "snapback.restore_snapshot") { // Check if user has access to this Pro-tier tool if (authResult.tier !== "pro") { // Return SARIF note for Free users trying to access Pro tools const sarifLog = createSarifLog("snapback-restore-snapshot", "1.0.0"); addResult( sarifLog, "pro-tool-restricted", "This tool requires a Pro subscription. Upgrade at https://snapback.dev/pricing", undefined, undefined, ); return { content: [ { type: "json", json: sarifLog }, { type: "text", text: "❌ This tool requires a Pro subscription. Upgrade at https://snapback.dev/pricing", }, ], }; } const parsed = z.object({ snapshotId: z.string() }).parse(args); const result = await restoreSnapshot(parsed.snapshotId); if (!result.success) { return { content: [ { type: "text", text: `❌ Failed to restore snapshot: ${result.error}`, }, ], isError: true, }; } return { content: [{ type: "json", json: result.snapshot }] }; } // Handle catalog tool if (name === "catalog.list_tools") { const catalog = mcpManager.getToolCatalog(); return { content: [{ type: "json", json: catalog }] }; } // Handle Context7 tools with internal implementation as fallback if (name === "ctx7.resolve-library-id") { const parsed = z.object({ libraryName: z.string().min(1) }).parse(args); const result = await trackPerformance("ctx7_resolve_library", () => context7Service.resolveLibraryId(parsed.libraryName), ); return result; } if (name === "ctx7.get-library-docs") { const parsed = z .object({ context7CompatibleLibraryID: z.string().min(1), topic: z.string().optional(), tokens: z.number().optional(), }) .parse(args); const result = await trackPerformance("ctx7_get_docs", () => context7Service.getLibraryDocs(parsed.context7CompatibleLibraryID, { topic: parsed.topic, tokens: parsed.tokens, }), ); return result; } // Handle proxied tools from external MCP servers if (name.startsWith("ctx7.") || name.startsWith("gh.") || name.startsWith("registry.")) { const result = await mcpManager.callToolByName(name, args); return result; } throw new Error(`Unknown tool: ${name}`); } catch (error: unknown) { const sanitized = sanitizeError(error, `tool_call_${name}`); console.error(`[SnapBack MCP] Error handling tool ${name}:`, error); return { content: [ { type: "text", text: `${sanitized.message} (Log ID: ${sanitized.logId})`, }, ], isError: true, error: { message: sanitized.message, code: sanitized.code, }, }; } }); const transport = new StdioServerTransport(); await server.connect(transport); // Log to stderr to avoid corrupting JSON-RPC on stdout console.error("SnapBack MCP Server started"); return { server, transport }; } // Export a function to create and start the HTTP server export async function startHttpServer(port?: number): Promise<MCPHttpServer> { // Create and start the MCP server const { server } = await startServer(); // Create HTTP server wrapper const httpServer = new MCPHttpServer(server); // Start listening await httpServer.listen(port || 3000); return httpServer; } // Only start the server if this file is run directly // ESM check: import.meta.url matches the entry point if (import.meta.url === new URL(process.argv[1], "file:").href) { startServer().catch(console.error); }

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/snapback-dev/mcp-server'

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