@codebakers/cli 2.6.1 → 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.
- package/dist/mcp/server.js +577 -0
- package/package.json +1 -1
- package/src/mcp/server.ts +671 -1
package/dist/mcp/server.js
CHANGED
|
@@ -53,6 +53,8 @@ class CodeBakersServer {
|
|
|
53
53
|
server;
|
|
54
54
|
apiKey;
|
|
55
55
|
apiUrl;
|
|
56
|
+
autoUpdateChecked = false;
|
|
57
|
+
autoUpdateInProgress = false;
|
|
56
58
|
constructor() {
|
|
57
59
|
this.apiKey = (0, config_js_1.getApiKey)();
|
|
58
60
|
this.apiUrl = (0, config_js_1.getApiUrl)();
|
|
@@ -65,6 +67,109 @@ class CodeBakersServer {
|
|
|
65
67
|
},
|
|
66
68
|
});
|
|
67
69
|
this.setupHandlers();
|
|
70
|
+
// Trigger auto-update check on startup (non-blocking)
|
|
71
|
+
this.checkAndAutoUpdate().catch(() => {
|
|
72
|
+
// Silently ignore errors - don't interrupt user
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Automatically check for and apply pattern updates
|
|
77
|
+
* Runs silently in background - no user intervention needed
|
|
78
|
+
*/
|
|
79
|
+
async checkAndAutoUpdate() {
|
|
80
|
+
if (this.autoUpdateChecked || this.autoUpdateInProgress || !this.apiKey) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.autoUpdateInProgress = true;
|
|
84
|
+
try {
|
|
85
|
+
const cwd = process.cwd();
|
|
86
|
+
const versionPath = path.join(cwd, '.claude', '.version.json');
|
|
87
|
+
// Check if we should auto-update (once per 24 hours)
|
|
88
|
+
let lastCheck = null;
|
|
89
|
+
let installed = null;
|
|
90
|
+
if (fs.existsSync(versionPath)) {
|
|
91
|
+
try {
|
|
92
|
+
installed = JSON.parse(fs.readFileSync(versionPath, 'utf-8'));
|
|
93
|
+
const checkTime = installed?.updatedAt || installed?.installedAt;
|
|
94
|
+
if (checkTime) {
|
|
95
|
+
lastCheck = new Date(checkTime);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Ignore parse errors
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Skip if checked within last 24 hours
|
|
103
|
+
if (lastCheck) {
|
|
104
|
+
const hoursSinceCheck = (Date.now() - lastCheck.getTime()) / (1000 * 60 * 60);
|
|
105
|
+
if (hoursSinceCheck < 24) {
|
|
106
|
+
this.autoUpdateChecked = true;
|
|
107
|
+
this.autoUpdateInProgress = false;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Fetch latest version
|
|
112
|
+
const response = await fetch(`${this.apiUrl}/api/content/version`, {
|
|
113
|
+
headers: { 'Authorization': `Bearer ${this.apiKey}` },
|
|
114
|
+
});
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
this.autoUpdateInProgress = false;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const latest = await response.json();
|
|
120
|
+
// Check if update needed
|
|
121
|
+
if (installed && installed.version === latest.version) {
|
|
122
|
+
// Already up to date - update timestamp to avoid checking for 24h
|
|
123
|
+
installed.updatedAt = new Date().toISOString();
|
|
124
|
+
fs.writeFileSync(versionPath, JSON.stringify(installed, null, 2));
|
|
125
|
+
this.autoUpdateChecked = true;
|
|
126
|
+
this.autoUpdateInProgress = false;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Fetch full content and update
|
|
130
|
+
const contentResponse = await fetch(`${this.apiUrl}/api/content`, {
|
|
131
|
+
headers: { 'Authorization': `Bearer ${this.apiKey}` },
|
|
132
|
+
});
|
|
133
|
+
if (!contentResponse.ok) {
|
|
134
|
+
this.autoUpdateInProgress = false;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const content = await contentResponse.json();
|
|
138
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
139
|
+
// Ensure .claude directory exists
|
|
140
|
+
if (!fs.existsSync(claudeDir)) {
|
|
141
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
// Write updated modules
|
|
144
|
+
let moduleCount = 0;
|
|
145
|
+
if (content.modules) {
|
|
146
|
+
for (const [name, data] of Object.entries(content.modules)) {
|
|
147
|
+
fs.writeFileSync(path.join(claudeDir, name), data);
|
|
148
|
+
moduleCount++;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Update CLAUDE.md if router content exists
|
|
152
|
+
if (content.router || content.claudeMd) {
|
|
153
|
+
const routerContent = content.claudeMd || content.router;
|
|
154
|
+
fs.writeFileSync(path.join(cwd, 'CLAUDE.md'), routerContent);
|
|
155
|
+
}
|
|
156
|
+
// Write version file
|
|
157
|
+
const versionInfo = {
|
|
158
|
+
version: content.version,
|
|
159
|
+
moduleCount,
|
|
160
|
+
updatedAt: new Date().toISOString(),
|
|
161
|
+
cliVersion: (0, api_js_1.getCliVersion)(),
|
|
162
|
+
};
|
|
163
|
+
fs.writeFileSync(versionPath, JSON.stringify(versionInfo, null, 2));
|
|
164
|
+
this.autoUpdateChecked = true;
|
|
165
|
+
this.autoUpdateInProgress = false;
|
|
166
|
+
// Log success (visible in MCP logs)
|
|
167
|
+
console.error(`[CodeBakers] Auto-updated patterns to v${content.version} (${moduleCount} modules)`);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// Silently fail - don't interrupt user's workflow
|
|
171
|
+
this.autoUpdateInProgress = false;
|
|
172
|
+
}
|
|
68
173
|
}
|
|
69
174
|
gatherProjectContext() {
|
|
70
175
|
const cwd = process.cwd();
|
|
@@ -613,6 +718,81 @@ class CodeBakersServer {
|
|
|
613
718
|
required: ['eventType'],
|
|
614
719
|
},
|
|
615
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
|
+
},
|
|
616
796
|
],
|
|
617
797
|
}));
|
|
618
798
|
// Handle tool calls
|
|
@@ -660,6 +840,14 @@ class CodeBakersServer {
|
|
|
660
840
|
return this.handleReportPatternGap(args);
|
|
661
841
|
case 'track_analytics':
|
|
662
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);
|
|
663
851
|
default:
|
|
664
852
|
throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
665
853
|
}
|
|
@@ -2287,6 +2475,395 @@ Just describe what you want to build! I'll automatically:
|
|
|
2287
2475
|
};
|
|
2288
2476
|
}
|
|
2289
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
|
+
}
|
|
2290
2867
|
async run() {
|
|
2291
2868
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
2292
2869
|
await this.server.connect(transport);
|
package/package.json
CHANGED
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';
|
|
@@ -61,6 +61,8 @@ class CodeBakersServer {
|
|
|
61
61
|
private server: Server;
|
|
62
62
|
private apiKey: string | null;
|
|
63
63
|
private apiUrl: string;
|
|
64
|
+
private autoUpdateChecked = false;
|
|
65
|
+
private autoUpdateInProgress = false;
|
|
64
66
|
|
|
65
67
|
constructor() {
|
|
66
68
|
this.apiKey = getApiKey();
|
|
@@ -79,6 +81,128 @@ class CodeBakersServer {
|
|
|
79
81
|
);
|
|
80
82
|
|
|
81
83
|
this.setupHandlers();
|
|
84
|
+
|
|
85
|
+
// Trigger auto-update check on startup (non-blocking)
|
|
86
|
+
this.checkAndAutoUpdate().catch(() => {
|
|
87
|
+
// Silently ignore errors - don't interrupt user
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Automatically check for and apply pattern updates
|
|
93
|
+
* Runs silently in background - no user intervention needed
|
|
94
|
+
*/
|
|
95
|
+
private async checkAndAutoUpdate(): Promise<void> {
|
|
96
|
+
if (this.autoUpdateChecked || this.autoUpdateInProgress || !this.apiKey) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.autoUpdateInProgress = true;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const cwd = process.cwd();
|
|
104
|
+
const versionPath = path.join(cwd, '.claude', '.version.json');
|
|
105
|
+
|
|
106
|
+
// Check if we should auto-update (once per 24 hours)
|
|
107
|
+
let lastCheck: Date | null = null;
|
|
108
|
+
let installed: VersionInfo | null = null;
|
|
109
|
+
|
|
110
|
+
if (fs.existsSync(versionPath)) {
|
|
111
|
+
try {
|
|
112
|
+
installed = JSON.parse(fs.readFileSync(versionPath, 'utf-8'));
|
|
113
|
+
const checkTime = installed?.updatedAt || installed?.installedAt;
|
|
114
|
+
if (checkTime) {
|
|
115
|
+
lastCheck = new Date(checkTime);
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// Ignore parse errors
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Skip if checked within last 24 hours
|
|
123
|
+
if (lastCheck) {
|
|
124
|
+
const hoursSinceCheck = (Date.now() - lastCheck.getTime()) / (1000 * 60 * 60);
|
|
125
|
+
if (hoursSinceCheck < 24) {
|
|
126
|
+
this.autoUpdateChecked = true;
|
|
127
|
+
this.autoUpdateInProgress = false;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Fetch latest version
|
|
133
|
+
const response = await fetch(`${this.apiUrl}/api/content/version`, {
|
|
134
|
+
headers: { 'Authorization': `Bearer ${this.apiKey}` },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
this.autoUpdateInProgress = false;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const latest = await response.json();
|
|
143
|
+
|
|
144
|
+
// Check if update needed
|
|
145
|
+
if (installed && installed.version === latest.version) {
|
|
146
|
+
// Already up to date - update timestamp to avoid checking for 24h
|
|
147
|
+
installed.updatedAt = new Date().toISOString();
|
|
148
|
+
fs.writeFileSync(versionPath, JSON.stringify(installed, null, 2));
|
|
149
|
+
this.autoUpdateChecked = true;
|
|
150
|
+
this.autoUpdateInProgress = false;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Fetch full content and update
|
|
155
|
+
const contentResponse = await fetch(`${this.apiUrl}/api/content`, {
|
|
156
|
+
headers: { 'Authorization': `Bearer ${this.apiKey}` },
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!contentResponse.ok) {
|
|
160
|
+
this.autoUpdateInProgress = false;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const content = await contentResponse.json();
|
|
165
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
166
|
+
|
|
167
|
+
// Ensure .claude directory exists
|
|
168
|
+
if (!fs.existsSync(claudeDir)) {
|
|
169
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Write updated modules
|
|
173
|
+
let moduleCount = 0;
|
|
174
|
+
if (content.modules) {
|
|
175
|
+
for (const [name, data] of Object.entries(content.modules)) {
|
|
176
|
+
fs.writeFileSync(path.join(claudeDir, name), data as string);
|
|
177
|
+
moduleCount++;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Update CLAUDE.md if router content exists
|
|
182
|
+
if (content.router || content.claudeMd) {
|
|
183
|
+
const routerContent = content.claudeMd || content.router;
|
|
184
|
+
fs.writeFileSync(path.join(cwd, 'CLAUDE.md'), routerContent);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Write version file
|
|
188
|
+
const versionInfo: VersionInfo = {
|
|
189
|
+
version: content.version,
|
|
190
|
+
moduleCount,
|
|
191
|
+
updatedAt: new Date().toISOString(),
|
|
192
|
+
cliVersion: getCliVersion(),
|
|
193
|
+
};
|
|
194
|
+
fs.writeFileSync(versionPath, JSON.stringify(versionInfo, null, 2));
|
|
195
|
+
|
|
196
|
+
this.autoUpdateChecked = true;
|
|
197
|
+
this.autoUpdateInProgress = false;
|
|
198
|
+
|
|
199
|
+
// Log success (visible in MCP logs)
|
|
200
|
+
console.error(`[CodeBakers] Auto-updated patterns to v${content.version} (${moduleCount} modules)`);
|
|
201
|
+
|
|
202
|
+
} catch {
|
|
203
|
+
// Silently fail - don't interrupt user's workflow
|
|
204
|
+
this.autoUpdateInProgress = false;
|
|
205
|
+
}
|
|
82
206
|
}
|
|
83
207
|
|
|
84
208
|
private gatherProjectContext(): ProjectContext {
|
|
@@ -663,6 +787,85 @@ class CodeBakersServer {
|
|
|
663
787
|
required: ['eventType'],
|
|
664
788
|
},
|
|
665
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
|
+
},
|
|
666
869
|
],
|
|
667
870
|
}));
|
|
668
871
|
|
|
@@ -735,6 +938,18 @@ class CodeBakersServer {
|
|
|
735
938
|
case 'track_analytics':
|
|
736
939
|
return this.handleTrackAnalytics(args as { eventType: string; eventData?: Record<string, unknown>; projectHash?: string });
|
|
737
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
|
+
|
|
738
953
|
default:
|
|
739
954
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
740
955
|
}
|
|
@@ -2564,6 +2779,461 @@ Just describe what you want to build! I'll automatically:
|
|
|
2564
2779
|
}
|
|
2565
2780
|
}
|
|
2566
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
|
+
|
|
2567
3237
|
async run(): Promise<void> {
|
|
2568
3238
|
const transport = new StdioServerTransport();
|
|
2569
3239
|
await this.server.connect(transport);
|