@codebakers/cli 2.6.2 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -718,6 +718,81 @@ class CodeBakersServer {
718
718
  required: ['eventType'],
719
719
  },
720
720
  },
721
+ {
722
+ name: 'vercel_logs',
723
+ description: 'Fetch runtime logs from Vercel for the current project. Use when user asks about errors, API failures, production issues, or wants to debug their deployed app. Requires VERCEL_TOKEN env var or vercel login. Examples: "show me errors from yesterday", "what API calls are failing", "why is my app crashing".',
724
+ inputSchema: {
725
+ type: 'object',
726
+ properties: {
727
+ hours: {
728
+ type: 'number',
729
+ description: 'How many hours of logs to fetch (default: 24, max: 168)',
730
+ },
731
+ level: {
732
+ type: 'string',
733
+ enum: ['error', 'warn', 'info', 'all'],
734
+ description: 'Filter by log level (default: error)',
735
+ },
736
+ route: {
737
+ type: 'string',
738
+ description: 'Filter logs by API route path (e.g., "/api/auth", "/api/payments")',
739
+ },
740
+ limit: {
741
+ type: 'number',
742
+ description: 'Maximum number of log entries to return (default: 50, max: 500)',
743
+ },
744
+ },
745
+ },
746
+ },
747
+ {
748
+ name: 'vercel_analyze_errors',
749
+ description: 'Analyze Vercel logs and suggest fixes using CodeBakers patterns. Fetches recent errors, classifies them, and provides actionable fixes. Use when user says "fix my production errors", "why is X failing", or "help me debug".',
750
+ inputSchema: {
751
+ type: 'object',
752
+ properties: {
753
+ hours: {
754
+ type: 'number',
755
+ description: 'How many hours of logs to analyze (default: 24)',
756
+ },
757
+ autoFix: {
758
+ type: 'boolean',
759
+ description: 'Automatically apply safe fixes with high confidence (default: false)',
760
+ },
761
+ },
762
+ },
763
+ },
764
+ {
765
+ name: 'vercel_deployments',
766
+ description: 'List recent Vercel deployments and their status. Use when user asks "what was deployed", "show deployment history", or "why did the last deploy fail".',
767
+ inputSchema: {
768
+ type: 'object',
769
+ properties: {
770
+ limit: {
771
+ type: 'number',
772
+ description: 'Number of deployments to show (default: 10)',
773
+ },
774
+ state: {
775
+ type: 'string',
776
+ enum: ['READY', 'ERROR', 'BUILDING', 'QUEUED', 'CANCELED', 'all'],
777
+ description: 'Filter by deployment state',
778
+ },
779
+ },
780
+ },
781
+ },
782
+ {
783
+ name: 'vercel_connect',
784
+ description: 'Connect to Vercel using an API token. Required before using other Vercel tools. Token is stored securely in config. Use when user says "connect to vercel", "setup vercel", or before any vercel_* tool if not connected.',
785
+ inputSchema: {
786
+ type: 'object',
787
+ properties: {
788
+ token: {
789
+ type: 'string',
790
+ description: 'Vercel API token from https://vercel.com/account/tokens',
791
+ },
792
+ },
793
+ required: ['token'],
794
+ },
795
+ },
721
796
  ],
722
797
  }));
723
798
  // Handle tool calls
@@ -765,6 +840,14 @@ class CodeBakersServer {
765
840
  return this.handleReportPatternGap(args);
766
841
  case 'track_analytics':
767
842
  return this.handleTrackAnalytics(args);
843
+ case 'vercel_logs':
844
+ return this.handleVercelLogs(args);
845
+ case 'vercel_analyze_errors':
846
+ return this.handleVercelAnalyzeErrors(args);
847
+ case 'vercel_deployments':
848
+ return this.handleVercelDeployments(args);
849
+ case 'vercel_connect':
850
+ return this.handleVercelConnect(args);
768
851
  default:
769
852
  throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
770
853
  }
@@ -2392,6 +2475,395 @@ Just describe what you want to build! I'll automatically:
2392
2475
  };
2393
2476
  }
2394
2477
  }
2478
+ // ========== VERCEL INTEGRATION ==========
2479
+ getVercelToken() {
2480
+ // Check config first, then env var
2481
+ return (0, config_js_1.getServiceKey)('vercel') || process.env.VERCEL_TOKEN || null;
2482
+ }
2483
+ async handleVercelConnect(args) {
2484
+ const { token } = args;
2485
+ // Validate the token by making a test API call
2486
+ try {
2487
+ const response = await fetch('https://api.vercel.com/v2/user', {
2488
+ headers: { Authorization: `Bearer ${token}` },
2489
+ });
2490
+ if (!response.ok) {
2491
+ return {
2492
+ content: [{
2493
+ type: 'text',
2494
+ text: `❌ Invalid Vercel token. Please check your token and try again.\n\nGet a new token at: https://vercel.com/account/tokens`,
2495
+ }],
2496
+ isError: true,
2497
+ };
2498
+ }
2499
+ const user = await response.json();
2500
+ // Store the token securely
2501
+ (0, config_js_1.setServiceKey)('vercel', token);
2502
+ return {
2503
+ content: [{
2504
+ type: 'text',
2505
+ text: `✅ Connected to Vercel as ${user.user?.username || user.user?.email || 'unknown user'}\n\nYou can now use:\n- vercel_logs: Fetch runtime logs\n- vercel_analyze_errors: Analyze and fix errors\n- vercel_deployments: View deployment history`,
2506
+ }],
2507
+ };
2508
+ }
2509
+ catch (error) {
2510
+ return {
2511
+ content: [{
2512
+ type: 'text',
2513
+ text: `❌ Failed to connect to Vercel: ${error instanceof Error ? error.message : 'Unknown error'}`,
2514
+ }],
2515
+ isError: true,
2516
+ };
2517
+ }
2518
+ }
2519
+ async handleVercelLogs(args) {
2520
+ const token = this.getVercelToken();
2521
+ if (!token) {
2522
+ return {
2523
+ content: [{
2524
+ type: 'text',
2525
+ text: `❌ Not connected to Vercel.\n\nTo connect, either:\n1. Use the vercel_connect tool with your API token\n2. Set VERCEL_TOKEN environment variable\n\nGet a token at: https://vercel.com/account/tokens`,
2526
+ }],
2527
+ isError: true,
2528
+ };
2529
+ }
2530
+ const hours = Math.min(args.hours || 24, 168); // Max 7 days
2531
+ const level = args.level || 'error';
2532
+ const limit = Math.min(args.limit || 50, 500);
2533
+ const since = Date.now() - (hours * 60 * 60 * 1000);
2534
+ try {
2535
+ // First, get the team/user's projects
2536
+ const projectsRes = await fetch('https://api.vercel.com/v9/projects?limit=10', {
2537
+ headers: { Authorization: `Bearer ${token}` },
2538
+ });
2539
+ if (!projectsRes.ok) {
2540
+ throw new Error(`Failed to fetch projects: ${projectsRes.statusText}`);
2541
+ }
2542
+ const projectsData = await projectsRes.json();
2543
+ const projects = projectsData.projects || [];
2544
+ if (projects.length === 0) {
2545
+ return {
2546
+ content: [{
2547
+ type: 'text',
2548
+ text: `❌ No Vercel projects found. Make sure your token has access to your projects.`,
2549
+ }],
2550
+ isError: true,
2551
+ };
2552
+ }
2553
+ // Try to match current project by name
2554
+ const cwd = process.cwd();
2555
+ const packageJsonPath = `${cwd}/package.json`;
2556
+ let currentProjectName = '';
2557
+ try {
2558
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
2559
+ currentProjectName = pkg.name?.replace(/^@[^/]+\//, '') || '';
2560
+ }
2561
+ catch {
2562
+ // Ignore
2563
+ }
2564
+ // Find matching project or use first one
2565
+ const project = projects.find((p) => p.name.toLowerCase() === currentProjectName.toLowerCase()) || projects[0];
2566
+ // Fetch logs for the project
2567
+ // Note: Vercel's logs API varies by plan. Using runtime logs endpoint
2568
+ const logsUrl = new URL(`https://api.vercel.com/v1/projects/${project.id}/logs`);
2569
+ logsUrl.searchParams.set('since', since.toString());
2570
+ logsUrl.searchParams.set('limit', limit.toString());
2571
+ if (level !== 'all') {
2572
+ logsUrl.searchParams.set('level', level);
2573
+ }
2574
+ if (args.route) {
2575
+ logsUrl.searchParams.set('path', args.route);
2576
+ }
2577
+ const logsRes = await fetch(logsUrl.toString(), {
2578
+ headers: { Authorization: `Bearer ${token}` },
2579
+ });
2580
+ if (!logsRes.ok) {
2581
+ // Try alternative endpoint for edge/serverless logs
2582
+ const altLogsRes = await fetch(`https://api.vercel.com/v2/deployments/${project.latestDeployments?.[0]?.id}/events?limit=${limit}`, { headers: { Authorization: `Bearer ${token}` } });
2583
+ if (!altLogsRes.ok) {
2584
+ throw new Error(`Failed to fetch logs: ${logsRes.statusText}. Your Vercel plan may not support log access via API.`);
2585
+ }
2586
+ const altLogs = await altLogsRes.json();
2587
+ return this.formatVercelLogs(altLogs, project.name, hours, level);
2588
+ }
2589
+ const logs = await logsRes.json();
2590
+ return this.formatVercelLogs(logs, project.name, hours, level);
2591
+ }
2592
+ catch (error) {
2593
+ return {
2594
+ content: [{
2595
+ type: 'text',
2596
+ text: `❌ Failed to fetch Vercel logs: ${error instanceof Error ? error.message : 'Unknown error'}`,
2597
+ }],
2598
+ isError: true,
2599
+ };
2600
+ }
2601
+ }
2602
+ formatVercelLogs(logs, projectName, hours, level) {
2603
+ const logEntries = Array.isArray(logs) ? logs : logs?.logs || [];
2604
+ if (logEntries.length === 0) {
2605
+ return {
2606
+ content: [{
2607
+ type: 'text',
2608
+ text: `📋 No ${level === 'all' ? '' : level + ' '}logs found for **${projectName}** in the last ${hours} hours.\n\nThis could mean:\n- No matching log entries exist\n- Your Vercel plan may have limited log retention\n- Logs may still be processing`,
2609
+ }],
2610
+ };
2611
+ }
2612
+ const formattedLogs = logEntries.slice(0, 50).map((log) => {
2613
+ const timestamp = log.timestamp || log.created || log.createdAt;
2614
+ const date = timestamp ? new Date(timestamp).toISOString() : 'Unknown';
2615
+ const logLevel = (log.level || log.type || 'info');
2616
+ const message = log.message || log.text || log.payload || JSON.stringify(log);
2617
+ const path = log.path || log.route || '';
2618
+ return `[${date}] ${logLevel.toUpperCase()}${path ? ` ${path}` : ''}\n${message}`;
2619
+ }).join('\n\n---\n\n');
2620
+ const summary = this.summarizeErrors(logEntries);
2621
+ return {
2622
+ content: [{
2623
+ type: 'text',
2624
+ text: `# Vercel Logs: ${projectName}\n\n**Period:** Last ${hours} hours\n**Filter:** ${level}\n**Found:** ${logEntries.length} entries\n\n${summary}\n\n## Log Entries\n\n${formattedLogs}`,
2625
+ }],
2626
+ };
2627
+ }
2628
+ summarizeErrors(logs) {
2629
+ const errorCounts = {};
2630
+ const routeCounts = {};
2631
+ logs.forEach((log) => {
2632
+ const message = String(log.message || log.text || '');
2633
+ const path = String(log.path || log.route || 'unknown');
2634
+ // Extract error type from message
2635
+ const errorMatch = message.match(/^(\w+Error):|Error:\s*(\w+)/);
2636
+ if (errorMatch) {
2637
+ const errorType = errorMatch[1] || errorMatch[2];
2638
+ errorCounts[errorType] = (errorCounts[errorType] || 0) + 1;
2639
+ }
2640
+ // Count by route
2641
+ if (path !== 'unknown') {
2642
+ routeCounts[path] = (routeCounts[path] || 0) + 1;
2643
+ }
2644
+ });
2645
+ const topErrors = Object.entries(errorCounts)
2646
+ .sort(([, a], [, b]) => b - a)
2647
+ .slice(0, 5);
2648
+ const topRoutes = Object.entries(routeCounts)
2649
+ .sort(([, a], [, b]) => b - a)
2650
+ .slice(0, 5);
2651
+ let summary = '## Summary\n\n';
2652
+ if (topErrors.length > 0) {
2653
+ summary += '**Top Error Types:**\n';
2654
+ topErrors.forEach(([error, count]) => {
2655
+ summary += `- ${error}: ${count} occurrences\n`;
2656
+ });
2657
+ summary += '\n';
2658
+ }
2659
+ if (topRoutes.length > 0) {
2660
+ summary += '**Most Affected Routes:**\n';
2661
+ topRoutes.forEach(([route, count]) => {
2662
+ summary += `- ${route}: ${count} errors\n`;
2663
+ });
2664
+ }
2665
+ return summary || 'No patterns detected in logs.';
2666
+ }
2667
+ async handleVercelAnalyzeErrors(args) {
2668
+ const token = this.getVercelToken();
2669
+ if (!token) {
2670
+ return {
2671
+ content: [{
2672
+ type: 'text',
2673
+ text: `❌ Not connected to Vercel. Use vercel_connect first.`,
2674
+ }],
2675
+ isError: true,
2676
+ };
2677
+ }
2678
+ const hours = args.hours || 24;
2679
+ try {
2680
+ // Fetch error logs
2681
+ const logsResult = await this.handleVercelLogs({ hours, level: 'error', limit: 100 });
2682
+ const logsText = logsResult.content?.[0]?.text || '';
2683
+ if (logsText.includes('No') && logsText.includes('logs found')) {
2684
+ return {
2685
+ content: [{
2686
+ type: 'text',
2687
+ text: `✅ No errors found in the last ${hours} hours! Your app is running smoothly.`,
2688
+ }],
2689
+ };
2690
+ }
2691
+ // Classify errors and suggest fixes
2692
+ const analysis = this.classifyAndSuggestFixes(logsText);
2693
+ return {
2694
+ content: [{
2695
+ type: 'text',
2696
+ text: `# Error Analysis\n\n${analysis}`,
2697
+ }],
2698
+ };
2699
+ }
2700
+ catch (error) {
2701
+ return {
2702
+ content: [{
2703
+ type: 'text',
2704
+ text: `❌ Failed to analyze errors: ${error instanceof Error ? error.message : 'Unknown error'}`,
2705
+ }],
2706
+ isError: true,
2707
+ };
2708
+ }
2709
+ }
2710
+ classifyAndSuggestFixes(logsText) {
2711
+ const issues = [];
2712
+ // Common error patterns and their fixes
2713
+ const errorPatterns = [
2714
+ {
2715
+ pattern: /TypeError.*undefined|Cannot read propert/gi,
2716
+ type: 'Null/Undefined Access',
2717
+ severity: 'HIGH',
2718
+ fix: 'Add null checks or optional chaining (?.) before accessing properties.',
2719
+ codePattern: '02-auth',
2720
+ },
2721
+ {
2722
+ pattern: /ECONNREFUSED|ETIMEDOUT|fetch failed/gi,
2723
+ type: 'Network/Connection Error',
2724
+ severity: 'HIGH',
2725
+ fix: 'Add retry logic with exponential backoff. Check if external services are available.',
2726
+ codePattern: '03-api',
2727
+ },
2728
+ {
2729
+ pattern: /401|Unauthorized|invalid.*token/gi,
2730
+ type: 'Authentication Error',
2731
+ severity: 'CRITICAL',
2732
+ fix: 'Check token expiration and refresh logic. Verify auth middleware is properly configured.',
2733
+ codePattern: '02-auth',
2734
+ },
2735
+ {
2736
+ pattern: /500|Internal Server Error/gi,
2737
+ type: 'Server Error',
2738
+ severity: 'CRITICAL',
2739
+ fix: 'Add proper error boundaries and try-catch blocks. Check server logs for stack traces.',
2740
+ codePattern: '00-core',
2741
+ },
2742
+ {
2743
+ pattern: /429|Too Many Requests|rate limit/gi,
2744
+ type: 'Rate Limiting',
2745
+ severity: 'MEDIUM',
2746
+ fix: 'Implement request throttling and caching. Add rate limit headers handling.',
2747
+ codePattern: '03-api',
2748
+ },
2749
+ {
2750
+ pattern: /CORS|cross-origin|Access-Control/gi,
2751
+ type: 'CORS Error',
2752
+ severity: 'MEDIUM',
2753
+ fix: 'Configure CORS headers in next.config.js or API routes. Check allowed origins.',
2754
+ codePattern: '03-api',
2755
+ },
2756
+ {
2757
+ pattern: /prisma|drizzle|database|sql/gi,
2758
+ type: 'Database Error',
2759
+ severity: 'HIGH',
2760
+ fix: 'Check database connection string. Verify migrations are applied. Add connection pooling.',
2761
+ codePattern: '01-database',
2762
+ },
2763
+ {
2764
+ pattern: /stripe|payment|charge failed/gi,
2765
+ type: 'Payment Error',
2766
+ severity: 'CRITICAL',
2767
+ fix: 'Check Stripe webhook configuration. Verify API keys. Add idempotency keys.',
2768
+ codePattern: '05-payments',
2769
+ },
2770
+ {
2771
+ pattern: /hydration|Minified React|client.*server/gi,
2772
+ type: 'React Hydration Error',
2773
+ severity: 'MEDIUM',
2774
+ fix: 'Ensure server and client render the same content. Use useEffect for client-only code.',
2775
+ codePattern: '04-frontend',
2776
+ },
2777
+ ];
2778
+ errorPatterns.forEach(({ pattern, type, severity, fix, codePattern }) => {
2779
+ const matches = logsText.match(pattern);
2780
+ if (matches) {
2781
+ issues.push({ type, severity, count: matches.length, fix, pattern: codePattern });
2782
+ }
2783
+ });
2784
+ if (issues.length === 0) {
2785
+ return `No common error patterns detected. Review the raw logs for custom application errors.`;
2786
+ }
2787
+ // Sort by severity
2788
+ const severityOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
2789
+ issues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
2790
+ let output = `Found **${issues.length}** error patterns:\n\n`;
2791
+ issues.forEach((issue, i) => {
2792
+ const emoji = issue.severity === 'CRITICAL' ? '🔴' : issue.severity === 'HIGH' ? '🟠' : '🟡';
2793
+ output += `### ${i + 1}. ${emoji} ${issue.type}\n`;
2794
+ output += `**Severity:** ${issue.severity} | **Occurrences:** ${issue.count}\n\n`;
2795
+ output += `**Fix:** ${issue.fix}\n\n`;
2796
+ if (issue.pattern) {
2797
+ output += `**Pattern:** Load \`${issue.pattern}.md\` for detailed implementation guidance.\n\n`;
2798
+ }
2799
+ });
2800
+ output += `\n---\n\n**Next Steps:**\n`;
2801
+ output += `1. Address CRITICAL issues first\n`;
2802
+ output += `2. Use \`get_pattern\` to load relevant CodeBakers patterns\n`;
2803
+ output += `3. Run \`heal\` to auto-fix safe issues\n`;
2804
+ return output;
2805
+ }
2806
+ async handleVercelDeployments(args) {
2807
+ const token = this.getVercelToken();
2808
+ if (!token) {
2809
+ return {
2810
+ content: [{
2811
+ type: 'text',
2812
+ text: `❌ Not connected to Vercel. Use vercel_connect first.`,
2813
+ }],
2814
+ isError: true,
2815
+ };
2816
+ }
2817
+ const limit = args.limit || 10;
2818
+ const stateFilter = args.state;
2819
+ try {
2820
+ // Get deployments
2821
+ const url = new URL('https://api.vercel.com/v6/deployments');
2822
+ url.searchParams.set('limit', limit.toString());
2823
+ if (stateFilter && stateFilter !== 'all') {
2824
+ url.searchParams.set('state', stateFilter);
2825
+ }
2826
+ const response = await fetch(url.toString(), {
2827
+ headers: { Authorization: `Bearer ${token}` },
2828
+ });
2829
+ if (!response.ok) {
2830
+ throw new Error(`Failed to fetch deployments: ${response.statusText}`);
2831
+ }
2832
+ const data = await response.json();
2833
+ const deployments = data.deployments || [];
2834
+ if (deployments.length === 0) {
2835
+ return {
2836
+ content: [{
2837
+ type: 'text',
2838
+ text: `No deployments found${stateFilter ? ` with state: ${stateFilter}` : ''}.`,
2839
+ }],
2840
+ };
2841
+ }
2842
+ const formatted = deployments.map((d) => {
2843
+ const created = new Date(d.created).toLocaleString();
2844
+ const state = d.state || d.readyState || 'UNKNOWN';
2845
+ const emoji = state === 'READY' ? '✅' : state === 'ERROR' ? '❌' : state === 'BUILDING' ? '🔨' : '⏳';
2846
+ const url = d.url ? `https://${d.url}` : 'N/A';
2847
+ const commit = d.meta?.githubCommitMessage || d.meta?.gitlabCommitMessage || 'No commit message';
2848
+ return `${emoji} **${state}** - ${created}\n URL: ${url}\n Commit: ${commit}`;
2849
+ }).join('\n\n');
2850
+ return {
2851
+ content: [{
2852
+ type: 'text',
2853
+ text: `# Recent Deployments\n\n${formatted}`,
2854
+ }],
2855
+ };
2856
+ }
2857
+ catch (error) {
2858
+ return {
2859
+ content: [{
2860
+ type: 'text',
2861
+ text: `❌ Failed to fetch deployments: ${error instanceof Error ? error.message : 'Unknown error'}`,
2862
+ }],
2863
+ isError: true,
2864
+ };
2865
+ }
2866
+ }
2395
2867
  async run() {
2396
2868
  const transport = new stdio_js_1.StdioServerTransport();
2397
2869
  await this.server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebakers/cli",
3
- "version": "2.6.2",
3
+ "version": "2.7.0",
4
4
  "description": "CodeBakers CLI - Production patterns for AI-assisted development",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/mcp/server.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  ErrorCode,
9
9
  McpError,
10
10
  } from '@modelcontextprotocol/sdk/types.js';
11
- import { getApiKey, getApiUrl, getExperienceLevel, setExperienceLevel, type ExperienceLevel } from '../config.js';
11
+ import { getApiKey, getApiUrl, getExperienceLevel, setExperienceLevel, getServiceKey, setServiceKey, type ExperienceLevel } from '../config.js';
12
12
  import { audit as runAudit } from '../commands/audit.js';
13
13
  import { heal as runHeal } from '../commands/heal.js';
14
14
  import { getCliVersion } from '../lib/api.js';
@@ -787,6 +787,85 @@ class CodeBakersServer {
787
787
  required: ['eventType'],
788
788
  },
789
789
  },
790
+ {
791
+ name: 'vercel_logs',
792
+ description:
793
+ 'Fetch runtime logs from Vercel for the current project. Use when user asks about errors, API failures, production issues, or wants to debug their deployed app. Requires VERCEL_TOKEN env var or vercel login. Examples: "show me errors from yesterday", "what API calls are failing", "why is my app crashing".',
794
+ inputSchema: {
795
+ type: 'object' as const,
796
+ properties: {
797
+ hours: {
798
+ type: 'number',
799
+ description: 'How many hours of logs to fetch (default: 24, max: 168)',
800
+ },
801
+ level: {
802
+ type: 'string',
803
+ enum: ['error', 'warn', 'info', 'all'],
804
+ description: 'Filter by log level (default: error)',
805
+ },
806
+ route: {
807
+ type: 'string',
808
+ description: 'Filter logs by API route path (e.g., "/api/auth", "/api/payments")',
809
+ },
810
+ limit: {
811
+ type: 'number',
812
+ description: 'Maximum number of log entries to return (default: 50, max: 500)',
813
+ },
814
+ },
815
+ },
816
+ },
817
+ {
818
+ name: 'vercel_analyze_errors',
819
+ description:
820
+ 'Analyze Vercel logs and suggest fixes using CodeBakers patterns. Fetches recent errors, classifies them, and provides actionable fixes. Use when user says "fix my production errors", "why is X failing", or "help me debug".',
821
+ inputSchema: {
822
+ type: 'object' as const,
823
+ properties: {
824
+ hours: {
825
+ type: 'number',
826
+ description: 'How many hours of logs to analyze (default: 24)',
827
+ },
828
+ autoFix: {
829
+ type: 'boolean',
830
+ description: 'Automatically apply safe fixes with high confidence (default: false)',
831
+ },
832
+ },
833
+ },
834
+ },
835
+ {
836
+ name: 'vercel_deployments',
837
+ description:
838
+ 'List recent Vercel deployments and their status. Use when user asks "what was deployed", "show deployment history", or "why did the last deploy fail".',
839
+ inputSchema: {
840
+ type: 'object' as const,
841
+ properties: {
842
+ limit: {
843
+ type: 'number',
844
+ description: 'Number of deployments to show (default: 10)',
845
+ },
846
+ state: {
847
+ type: 'string',
848
+ enum: ['READY', 'ERROR', 'BUILDING', 'QUEUED', 'CANCELED', 'all'],
849
+ description: 'Filter by deployment state',
850
+ },
851
+ },
852
+ },
853
+ },
854
+ {
855
+ name: 'vercel_connect',
856
+ description:
857
+ 'Connect to Vercel using an API token. Required before using other Vercel tools. Token is stored securely in config. Use when user says "connect to vercel", "setup vercel", or before any vercel_* tool if not connected.',
858
+ inputSchema: {
859
+ type: 'object' as const,
860
+ properties: {
861
+ token: {
862
+ type: 'string',
863
+ description: 'Vercel API token from https://vercel.com/account/tokens',
864
+ },
865
+ },
866
+ required: ['token'],
867
+ },
868
+ },
790
869
  ],
791
870
  }));
792
871
 
@@ -859,6 +938,18 @@ class CodeBakersServer {
859
938
  case 'track_analytics':
860
939
  return this.handleTrackAnalytics(args as { eventType: string; eventData?: Record<string, unknown>; projectHash?: string });
861
940
 
941
+ case 'vercel_logs':
942
+ return this.handleVercelLogs(args as { hours?: number; level?: string; route?: string; limit?: number });
943
+
944
+ case 'vercel_analyze_errors':
945
+ return this.handleVercelAnalyzeErrors(args as { hours?: number; autoFix?: boolean });
946
+
947
+ case 'vercel_deployments':
948
+ return this.handleVercelDeployments(args as { limit?: number; state?: string });
949
+
950
+ case 'vercel_connect':
951
+ return this.handleVercelConnect(args as { token: string });
952
+
862
953
  default:
863
954
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
864
955
  }
@@ -2688,6 +2779,461 @@ Just describe what you want to build! I'll automatically:
2688
2779
  }
2689
2780
  }
2690
2781
 
2782
+ // ========== VERCEL INTEGRATION ==========
2783
+
2784
+ private getVercelToken(): string | null {
2785
+ // Check config first, then env var
2786
+ return getServiceKey('vercel') || process.env.VERCEL_TOKEN || null;
2787
+ }
2788
+
2789
+ private async handleVercelConnect(args: { token: string }) {
2790
+ const { token } = args;
2791
+
2792
+ // Validate the token by making a test API call
2793
+ try {
2794
+ const response = await fetch('https://api.vercel.com/v2/user', {
2795
+ headers: { Authorization: `Bearer ${token}` },
2796
+ });
2797
+
2798
+ if (!response.ok) {
2799
+ return {
2800
+ content: [{
2801
+ type: 'text' as const,
2802
+ text: `❌ Invalid Vercel token. Please check your token and try again.\n\nGet a new token at: https://vercel.com/account/tokens`,
2803
+ }],
2804
+ isError: true,
2805
+ };
2806
+ }
2807
+
2808
+ const user = await response.json();
2809
+
2810
+ // Store the token securely
2811
+ setServiceKey('vercel', token);
2812
+
2813
+ return {
2814
+ content: [{
2815
+ type: 'text' as const,
2816
+ text: `✅ Connected to Vercel as ${user.user?.username || user.user?.email || 'unknown user'}\n\nYou can now use:\n- vercel_logs: Fetch runtime logs\n- vercel_analyze_errors: Analyze and fix errors\n- vercel_deployments: View deployment history`,
2817
+ }],
2818
+ };
2819
+ } catch (error) {
2820
+ return {
2821
+ content: [{
2822
+ type: 'text' as const,
2823
+ text: `❌ Failed to connect to Vercel: ${error instanceof Error ? error.message : 'Unknown error'}`,
2824
+ }],
2825
+ isError: true,
2826
+ };
2827
+ }
2828
+ }
2829
+
2830
+ private async handleVercelLogs(args: { hours?: number; level?: string; route?: string; limit?: number }) {
2831
+ const token = this.getVercelToken();
2832
+ if (!token) {
2833
+ return {
2834
+ content: [{
2835
+ type: 'text' as const,
2836
+ text: `❌ Not connected to Vercel.\n\nTo connect, either:\n1. Use the vercel_connect tool with your API token\n2. Set VERCEL_TOKEN environment variable\n\nGet a token at: https://vercel.com/account/tokens`,
2837
+ }],
2838
+ isError: true,
2839
+ };
2840
+ }
2841
+
2842
+ const hours = Math.min(args.hours || 24, 168); // Max 7 days
2843
+ const level = args.level || 'error';
2844
+ const limit = Math.min(args.limit || 50, 500);
2845
+ const since = Date.now() - (hours * 60 * 60 * 1000);
2846
+
2847
+ try {
2848
+ // First, get the team/user's projects
2849
+ const projectsRes = await fetch('https://api.vercel.com/v9/projects?limit=10', {
2850
+ headers: { Authorization: `Bearer ${token}` },
2851
+ });
2852
+
2853
+ if (!projectsRes.ok) {
2854
+ throw new Error(`Failed to fetch projects: ${projectsRes.statusText}`);
2855
+ }
2856
+
2857
+ const projectsData = await projectsRes.json();
2858
+ const projects = projectsData.projects || [];
2859
+
2860
+ if (projects.length === 0) {
2861
+ return {
2862
+ content: [{
2863
+ type: 'text' as const,
2864
+ text: `❌ No Vercel projects found. Make sure your token has access to your projects.`,
2865
+ }],
2866
+ isError: true,
2867
+ };
2868
+ }
2869
+
2870
+ // Try to match current project by name
2871
+ const cwd = process.cwd();
2872
+ const packageJsonPath = `${cwd}/package.json`;
2873
+ let currentProjectName = '';
2874
+ try {
2875
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
2876
+ currentProjectName = pkg.name?.replace(/^@[^/]+\//, '') || '';
2877
+ } catch {
2878
+ // Ignore
2879
+ }
2880
+
2881
+ // Find matching project or use first one
2882
+ const project = projects.find((p: { name: string }) =>
2883
+ p.name.toLowerCase() === currentProjectName.toLowerCase()
2884
+ ) || projects[0];
2885
+
2886
+ // Fetch logs for the project
2887
+ // Note: Vercel's logs API varies by plan. Using runtime logs endpoint
2888
+ const logsUrl = new URL(`https://api.vercel.com/v1/projects/${project.id}/logs`);
2889
+ logsUrl.searchParams.set('since', since.toString());
2890
+ logsUrl.searchParams.set('limit', limit.toString());
2891
+ if (level !== 'all') {
2892
+ logsUrl.searchParams.set('level', level);
2893
+ }
2894
+ if (args.route) {
2895
+ logsUrl.searchParams.set('path', args.route);
2896
+ }
2897
+
2898
+ const logsRes = await fetch(logsUrl.toString(), {
2899
+ headers: { Authorization: `Bearer ${token}` },
2900
+ });
2901
+
2902
+ if (!logsRes.ok) {
2903
+ // Try alternative endpoint for edge/serverless logs
2904
+ const altLogsRes = await fetch(
2905
+ `https://api.vercel.com/v2/deployments/${project.latestDeployments?.[0]?.id}/events?limit=${limit}`,
2906
+ { headers: { Authorization: `Bearer ${token}` } }
2907
+ );
2908
+
2909
+ if (!altLogsRes.ok) {
2910
+ throw new Error(`Failed to fetch logs: ${logsRes.statusText}. Your Vercel plan may not support log access via API.`);
2911
+ }
2912
+
2913
+ const altLogs = await altLogsRes.json();
2914
+ return this.formatVercelLogs(altLogs, project.name, hours, level);
2915
+ }
2916
+
2917
+ const logs = await logsRes.json();
2918
+ return this.formatVercelLogs(logs, project.name, hours, level);
2919
+
2920
+ } catch (error) {
2921
+ return {
2922
+ content: [{
2923
+ type: 'text' as const,
2924
+ text: `❌ Failed to fetch Vercel logs: ${error instanceof Error ? error.message : 'Unknown error'}`,
2925
+ }],
2926
+ isError: true,
2927
+ };
2928
+ }
2929
+ }
2930
+
2931
+ private formatVercelLogs(logs: unknown, projectName: string, hours: number, level: string) {
2932
+ const logEntries = Array.isArray(logs) ? logs : (logs as { logs?: unknown[] })?.logs || [];
2933
+
2934
+ if (logEntries.length === 0) {
2935
+ return {
2936
+ content: [{
2937
+ type: 'text' as const,
2938
+ text: `📋 No ${level === 'all' ? '' : level + ' '}logs found for **${projectName}** in the last ${hours} hours.\n\nThis could mean:\n- No matching log entries exist\n- Your Vercel plan may have limited log retention\n- Logs may still be processing`,
2939
+ }],
2940
+ };
2941
+ }
2942
+
2943
+ const formattedLogs = logEntries.slice(0, 50).map((log: Record<string, unknown>) => {
2944
+ const timestamp = log.timestamp || log.created || log.createdAt;
2945
+ const date = timestamp ? new Date(timestamp as number).toISOString() : 'Unknown';
2946
+ const logLevel = (log.level || log.type || 'info') as string;
2947
+ const message = log.message || log.text || log.payload || JSON.stringify(log);
2948
+ const path = log.path || log.route || '';
2949
+
2950
+ return `[${date}] ${logLevel.toUpperCase()}${path ? ` ${path}` : ''}\n${message}`;
2951
+ }).join('\n\n---\n\n');
2952
+
2953
+ const summary = this.summarizeErrors(logEntries);
2954
+
2955
+ return {
2956
+ content: [{
2957
+ type: 'text' as const,
2958
+ text: `# Vercel Logs: ${projectName}\n\n**Period:** Last ${hours} hours\n**Filter:** ${level}\n**Found:** ${logEntries.length} entries\n\n${summary}\n\n## Log Entries\n\n${formattedLogs}`,
2959
+ }],
2960
+ };
2961
+ }
2962
+
2963
+ private summarizeErrors(logs: Record<string, unknown>[]) {
2964
+ const errorCounts: Record<string, number> = {};
2965
+ const routeCounts: Record<string, number> = {};
2966
+
2967
+ logs.forEach((log) => {
2968
+ const message = String(log.message || log.text || '');
2969
+ const path = String(log.path || log.route || 'unknown');
2970
+
2971
+ // Extract error type from message
2972
+ const errorMatch = message.match(/^(\w+Error):|Error:\s*(\w+)/);
2973
+ if (errorMatch) {
2974
+ const errorType = errorMatch[1] || errorMatch[2];
2975
+ errorCounts[errorType] = (errorCounts[errorType] || 0) + 1;
2976
+ }
2977
+
2978
+ // Count by route
2979
+ if (path !== 'unknown') {
2980
+ routeCounts[path] = (routeCounts[path] || 0) + 1;
2981
+ }
2982
+ });
2983
+
2984
+ const topErrors = Object.entries(errorCounts)
2985
+ .sort(([, a], [, b]) => b - a)
2986
+ .slice(0, 5);
2987
+
2988
+ const topRoutes = Object.entries(routeCounts)
2989
+ .sort(([, a], [, b]) => b - a)
2990
+ .slice(0, 5);
2991
+
2992
+ let summary = '## Summary\n\n';
2993
+
2994
+ if (topErrors.length > 0) {
2995
+ summary += '**Top Error Types:**\n';
2996
+ topErrors.forEach(([error, count]) => {
2997
+ summary += `- ${error}: ${count} occurrences\n`;
2998
+ });
2999
+ summary += '\n';
3000
+ }
3001
+
3002
+ if (topRoutes.length > 0) {
3003
+ summary += '**Most Affected Routes:**\n';
3004
+ topRoutes.forEach(([route, count]) => {
3005
+ summary += `- ${route}: ${count} errors\n`;
3006
+ });
3007
+ }
3008
+
3009
+ return summary || 'No patterns detected in logs.';
3010
+ }
3011
+
3012
+ private async handleVercelAnalyzeErrors(args: { hours?: number; autoFix?: boolean }) {
3013
+ const token = this.getVercelToken();
3014
+ if (!token) {
3015
+ return {
3016
+ content: [{
3017
+ type: 'text' as const,
3018
+ text: `❌ Not connected to Vercel. Use vercel_connect first.`,
3019
+ }],
3020
+ isError: true,
3021
+ };
3022
+ }
3023
+
3024
+ const hours = args.hours || 24;
3025
+
3026
+ try {
3027
+ // Fetch error logs
3028
+ const logsResult = await this.handleVercelLogs({ hours, level: 'error', limit: 100 });
3029
+ const logsText = logsResult.content?.[0]?.text || '';
3030
+
3031
+ if (logsText.includes('No') && logsText.includes('logs found')) {
3032
+ return {
3033
+ content: [{
3034
+ type: 'text' as const,
3035
+ text: `✅ No errors found in the last ${hours} hours! Your app is running smoothly.`,
3036
+ }],
3037
+ };
3038
+ }
3039
+
3040
+ // Classify errors and suggest fixes
3041
+ const analysis = this.classifyAndSuggestFixes(logsText);
3042
+
3043
+ return {
3044
+ content: [{
3045
+ type: 'text' as const,
3046
+ text: `# Error Analysis\n\n${analysis}`,
3047
+ }],
3048
+ };
3049
+
3050
+ } catch (error) {
3051
+ return {
3052
+ content: [{
3053
+ type: 'text' as const,
3054
+ text: `❌ Failed to analyze errors: ${error instanceof Error ? error.message : 'Unknown error'}`,
3055
+ }],
3056
+ isError: true,
3057
+ };
3058
+ }
3059
+ }
3060
+
3061
+ private classifyAndSuggestFixes(logsText: string): string {
3062
+ const issues: Array<{ type: string; severity: string; count: number; fix: string; pattern?: string }> = [];
3063
+
3064
+ // Common error patterns and their fixes
3065
+ const errorPatterns = [
3066
+ {
3067
+ pattern: /TypeError.*undefined|Cannot read propert/gi,
3068
+ type: 'Null/Undefined Access',
3069
+ severity: 'HIGH',
3070
+ fix: 'Add null checks or optional chaining (?.) before accessing properties.',
3071
+ codePattern: '02-auth',
3072
+ },
3073
+ {
3074
+ pattern: /ECONNREFUSED|ETIMEDOUT|fetch failed/gi,
3075
+ type: 'Network/Connection Error',
3076
+ severity: 'HIGH',
3077
+ fix: 'Add retry logic with exponential backoff. Check if external services are available.',
3078
+ codePattern: '03-api',
3079
+ },
3080
+ {
3081
+ pattern: /401|Unauthorized|invalid.*token/gi,
3082
+ type: 'Authentication Error',
3083
+ severity: 'CRITICAL',
3084
+ fix: 'Check token expiration and refresh logic. Verify auth middleware is properly configured.',
3085
+ codePattern: '02-auth',
3086
+ },
3087
+ {
3088
+ pattern: /500|Internal Server Error/gi,
3089
+ type: 'Server Error',
3090
+ severity: 'CRITICAL',
3091
+ fix: 'Add proper error boundaries and try-catch blocks. Check server logs for stack traces.',
3092
+ codePattern: '00-core',
3093
+ },
3094
+ {
3095
+ pattern: /429|Too Many Requests|rate limit/gi,
3096
+ type: 'Rate Limiting',
3097
+ severity: 'MEDIUM',
3098
+ fix: 'Implement request throttling and caching. Add rate limit headers handling.',
3099
+ codePattern: '03-api',
3100
+ },
3101
+ {
3102
+ pattern: /CORS|cross-origin|Access-Control/gi,
3103
+ type: 'CORS Error',
3104
+ severity: 'MEDIUM',
3105
+ fix: 'Configure CORS headers in next.config.js or API routes. Check allowed origins.',
3106
+ codePattern: '03-api',
3107
+ },
3108
+ {
3109
+ pattern: /prisma|drizzle|database|sql/gi,
3110
+ type: 'Database Error',
3111
+ severity: 'HIGH',
3112
+ fix: 'Check database connection string. Verify migrations are applied. Add connection pooling.',
3113
+ codePattern: '01-database',
3114
+ },
3115
+ {
3116
+ pattern: /stripe|payment|charge failed/gi,
3117
+ type: 'Payment Error',
3118
+ severity: 'CRITICAL',
3119
+ fix: 'Check Stripe webhook configuration. Verify API keys. Add idempotency keys.',
3120
+ codePattern: '05-payments',
3121
+ },
3122
+ {
3123
+ pattern: /hydration|Minified React|client.*server/gi,
3124
+ type: 'React Hydration Error',
3125
+ severity: 'MEDIUM',
3126
+ fix: 'Ensure server and client render the same content. Use useEffect for client-only code.',
3127
+ codePattern: '04-frontend',
3128
+ },
3129
+ ];
3130
+
3131
+ errorPatterns.forEach(({ pattern, type, severity, fix, codePattern }) => {
3132
+ const matches = logsText.match(pattern);
3133
+ if (matches) {
3134
+ issues.push({ type, severity, count: matches.length, fix, pattern: codePattern });
3135
+ }
3136
+ });
3137
+
3138
+ if (issues.length === 0) {
3139
+ return `No common error patterns detected. Review the raw logs for custom application errors.`;
3140
+ }
3141
+
3142
+ // Sort by severity
3143
+ const severityOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
3144
+ issues.sort((a, b) => severityOrder[a.severity as keyof typeof severityOrder] - severityOrder[b.severity as keyof typeof severityOrder]);
3145
+
3146
+ let output = `Found **${issues.length}** error patterns:\n\n`;
3147
+
3148
+ issues.forEach((issue, i) => {
3149
+ const emoji = issue.severity === 'CRITICAL' ? '🔴' : issue.severity === 'HIGH' ? '🟠' : '🟡';
3150
+ output += `### ${i + 1}. ${emoji} ${issue.type}\n`;
3151
+ output += `**Severity:** ${issue.severity} | **Occurrences:** ${issue.count}\n\n`;
3152
+ output += `**Fix:** ${issue.fix}\n\n`;
3153
+ if (issue.pattern) {
3154
+ output += `**Pattern:** Load \`${issue.pattern}.md\` for detailed implementation guidance.\n\n`;
3155
+ }
3156
+ });
3157
+
3158
+ output += `\n---\n\n**Next Steps:**\n`;
3159
+ output += `1. Address CRITICAL issues first\n`;
3160
+ output += `2. Use \`get_pattern\` to load relevant CodeBakers patterns\n`;
3161
+ output += `3. Run \`heal\` to auto-fix safe issues\n`;
3162
+
3163
+ return output;
3164
+ }
3165
+
3166
+ private async handleVercelDeployments(args: { limit?: number; state?: string }) {
3167
+ const token = this.getVercelToken();
3168
+ if (!token) {
3169
+ return {
3170
+ content: [{
3171
+ type: 'text' as const,
3172
+ text: `❌ Not connected to Vercel. Use vercel_connect first.`,
3173
+ }],
3174
+ isError: true,
3175
+ };
3176
+ }
3177
+
3178
+ const limit = args.limit || 10;
3179
+ const stateFilter = args.state;
3180
+
3181
+ try {
3182
+ // Get deployments
3183
+ const url = new URL('https://api.vercel.com/v6/deployments');
3184
+ url.searchParams.set('limit', limit.toString());
3185
+ if (stateFilter && stateFilter !== 'all') {
3186
+ url.searchParams.set('state', stateFilter);
3187
+ }
3188
+
3189
+ const response = await fetch(url.toString(), {
3190
+ headers: { Authorization: `Bearer ${token}` },
3191
+ });
3192
+
3193
+ if (!response.ok) {
3194
+ throw new Error(`Failed to fetch deployments: ${response.statusText}`);
3195
+ }
3196
+
3197
+ const data = await response.json();
3198
+ const deployments = data.deployments || [];
3199
+
3200
+ if (deployments.length === 0) {
3201
+ return {
3202
+ content: [{
3203
+ type: 'text' as const,
3204
+ text: `No deployments found${stateFilter ? ` with state: ${stateFilter}` : ''}.`,
3205
+ }],
3206
+ };
3207
+ }
3208
+
3209
+ const formatted = deployments.map((d: Record<string, unknown>) => {
3210
+ const created = new Date(d.created as number).toLocaleString();
3211
+ const state = d.state || d.readyState || 'UNKNOWN';
3212
+ const emoji = state === 'READY' ? '✅' : state === 'ERROR' ? '❌' : state === 'BUILDING' ? '🔨' : '⏳';
3213
+ const url = d.url ? `https://${d.url}` : 'N/A';
3214
+ const commit = (d.meta as Record<string, unknown>)?.githubCommitMessage || (d.meta as Record<string, unknown>)?.gitlabCommitMessage || 'No commit message';
3215
+
3216
+ return `${emoji} **${state}** - ${created}\n URL: ${url}\n Commit: ${commit}`;
3217
+ }).join('\n\n');
3218
+
3219
+ return {
3220
+ content: [{
3221
+ type: 'text' as const,
3222
+ text: `# Recent Deployments\n\n${formatted}`,
3223
+ }],
3224
+ };
3225
+
3226
+ } catch (error) {
3227
+ return {
3228
+ content: [{
3229
+ type: 'text' as const,
3230
+ text: `❌ Failed to fetch deployments: ${error instanceof Error ? error.message : 'Unknown error'}`,
3231
+ }],
3232
+ isError: true,
3233
+ };
3234
+ }
3235
+ }
3236
+
2691
3237
  async run(): Promise<void> {
2692
3238
  const transport = new StdioServerTransport();
2693
3239
  await this.server.connect(transport);