github-repos-manager-mcp
Version:
Model Context Protocol (MCP) server that enables your MCP client (e.g., Claude Desktop, Cline, Roo Code, Cursor, Windsurf, etc.) to interact with GitHub repositories using your GitHub personal access token.
849 lines (761 loc) • 28.5 kB
JavaScript
/**
* @fileoverview Handlers for advanced GitHub features.
* This file contains real implementations for advanced GitHub functionality.
*/
const { validateRequired, getOwnerRepo } = require("../utils/shared-utils.cjs");
/**
* Handler for code quality checks using GitHub's built-in code scanning.
* @param {object} args - The arguments for the tool.
* @param {object} defaultRepo - Default repository configuration.
* @param {object} apiService - GitHub API service instance.
* @returns {Promise<object>} Code scanning results or setup instructions.
*/
async function handleCodeQualityChecks(args, defaultRepo, apiService) {
const { owner, repo } = getOwnerRepo(args, defaultRepo);
const { tool_name = "codeql" } = args;
// Check for existing code scanning alerts
const alerts = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/code-scanning/alerts`
);
// Get code scanning analyses
let analyses = [];
try {
analyses = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/code-scanning/analyses`
);
} catch (error) {
// Code scanning might not be enabled
if (error.message.includes('404')) {
return {
success: true,
message: "Code scanning is not enabled for this repository.",
data: {
alerts: [],
analyses: [],
recommendations: [
"Enable GitHub Advanced Security to use code scanning",
"Set up CodeQL analysis workflow in .github/workflows/",
"Configure third-party security tools like Snyk or SonarCloud"
],
setup_url: `https://github.com/${owner}/${repo}/security/code-scanning`
}
};
}
throw error;
}
// Analyze the results
const highSeverityAlerts = alerts.filter(alert =>
alert.rule?.severity === 'error' || alert.rule?.security_severity_level === 'high'
);
const mediumSeverityAlerts = alerts.filter(alert =>
alert.rule?.severity === 'warning' || alert.rule?.security_severity_level === 'medium'
);
return {
success: true,
message: `Code quality analysis completed for ${owner}/${repo}`,
data: {
repository: `${owner}/${repo}`,
total_alerts: alerts.length,
high_severity: highSeverityAlerts.length,
medium_severity: mediumSeverityAlerts.length,
low_severity: alerts.length - highSeverityAlerts.length - mediumSeverityAlerts.length,
recent_analyses: analyses.slice(0, 5).map(analysis => ({
id: analysis.id,
tool: analysis.tool?.name || 'Unknown',
created_at: analysis.created_at,
commit_sha: analysis.commit_sha?.substring(0, 7),
results_count: analysis.results_count || 0
})),
alerts_url: `https://github.com/${owner}/${repo}/security/code-scanning`,
recommendations: alerts.length > 0 ?
["Review and fix high-severity alerts", "Set up automated security scanning"] :
["Code scanning is active with no current alerts", "Consider adding more security tools"]
}
};
}
/**
* Handler for custom dashboard data aggregation from GitHub API.
* @param {object} args - The arguments for the tool.
* @param {object} defaultRepo - Default repository configuration.
* @param {object} apiService - GitHub API service instance.
* @returns {Promise<object>} Custom dashboard data with repository metrics.
*/
async function handleCustomDashboards(args, defaultRepo, apiService) {
const { action = "view", dashboard_id = "default" } = args;
if (action === "view") {
// Determine scope - repo-specific or user-wide
let dashboardData = {};
if (args.owner && args.repo) {
// Repository-specific dashboard
const { owner, repo } = getOwnerRepo(args, defaultRepo);
// Fetch repository metrics
const [repoInfo, issues, prs, commits, contributors, languages] = await Promise.all([
apiService.makeGitHubRequest(`/repos/${owner}/${repo}`),
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/issues?state=all&per_page=100`),
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/pulls?state=all&per_page=100`),
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/commits?per_page=100`),
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/contributors?per_page=100`),
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/languages`)
]);
// Process metrics
const openIssues = issues.filter(issue => issue.state === 'open' && !issue.pull_request);
const closedIssues = issues.filter(issue => issue.state === 'closed' && !issue.pull_request);
const openPRs = prs.filter(pr => pr.state === 'open');
const mergedPRs = prs.filter(pr => pr.merged_at);
// Recent activity (last 30 days)
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const recentCommits = commits.filter(commit =>
new Date(commit.commit.author.date) > thirtyDaysAgo
);
dashboardData = {
repository: `${owner}/${repo}`,
overview: {
stars: repoInfo.stargazers_count,
forks: repoInfo.forks_count,
watchers: repoInfo.subscribers_count,
size_kb: repoInfo.size,
default_branch: repoInfo.default_branch,
visibility: repoInfo.private ? 'private' : 'public'
},
issues: {
total: issues.length,
open: openIssues.length,
closed: closedIssues.length,
open_ratio: issues.length > 0 ? (openIssues.length / issues.length * 100).toFixed(1) : 0
},
pull_requests: {
total: prs.length,
open: openPRs.length,
merged: mergedPRs.length,
merge_ratio: prs.length > 0 ? (mergedPRs.length / prs.length * 100).toFixed(1) : 0
},
activity: {
total_commits: commits.length,
commits_last_30_days: recentCommits.length,
contributors: contributors.length,
top_contributor: contributors[0]?.login || 'N/A',
last_update: repoInfo.updated_at
},
languages: Object.keys(languages).map(lang => ({
name: lang,
bytes: languages[lang],
percentage: ((languages[lang] / Object.values(languages).reduce((a, b) => a + b, 0)) * 100).toFixed(1)
})).slice(0, 5),
health_score: calculateRepoHealthScore(repoInfo, openIssues, mergedPRs, recentCommits)
};
} else {
// User-wide dashboard
const user = await apiService.makeGitHubRequest('/user');
const repos = await apiService.makeGitHubRequest('/user/repos?per_page=100&sort=updated');
const totalStars = repos.reduce((sum, repo) => sum + repo.stargazers_count, 0);
const totalForks = repos.reduce((sum, repo) => sum + repo.forks_count, 0);
const publicRepos = repos.filter(repo => !repo.private);
const privateRepos = repos.filter(repo => repo.private);
dashboardData = {
user: user.login,
profile: {
name: user.name,
public_repos: user.public_repos,
followers: user.followers,
following: user.following,
created_at: user.created_at
},
repositories: {
total: repos.length,
public: publicRepos.length,
private: privateRepos.length,
total_stars: totalStars,
total_forks: totalForks,
most_starred: repos.sort((a, b) => b.stargazers_count - a.stargazers_count)[0]?.name || 'N/A'
},
recent_activity: repos.slice(0, 10).map(repo => ({
name: repo.name,
updated: repo.updated_at,
stars: repo.stargazers_count,
language: repo.language
}))
};
}
return {
success: true,
message: `Dashboard ${dashboard_id} generated successfully`,
data: {
dashboard_id,
generated_at: new Date().toISOString(),
...dashboardData
}
};
}
return {
success: false,
message: `Unsupported dashboard action: ${action}`
};
}
/**
* Calculate repository health score based on various metrics
*/
function calculateRepoHealthScore(repo, openIssues, mergedPRs, recentCommits) {
let score = 50; // Base score
// Documentation
if (repo.description) score += 5;
if (repo.homepage) score += 5;
// Activity
const daysSinceUpdate = (Date.now() - new Date(repo.updated_at)) / (1000 * 60 * 60 * 24);
if (daysSinceUpdate < 30) score += 15;
else if (daysSinceUpdate < 90) score += 10;
else if (daysSinceUpdate < 365) score += 5;
// Issue management
if (openIssues.length < 10) score += 10;
else if (openIssues.length < 50) score += 5;
// Recent activity
if (recentCommits.length > 10) score += 10;
else if (recentCommits.length > 5) score += 5;
// Community
if (repo.stargazers_count > 100) score += 5;
if (repo.forks_count > 10) score += 5;
return Math.min(100, Math.max(0, score));
}
/**
* Handler for automated repository reporting.
* @param {object} args - The arguments for the tool.
* @param {object} defaultRepo - Default repository configuration.
* @param {object} apiService - GitHub API service instance.
* @returns {Promise<object>} Generated report data.
*/
async function handleAutomatedReporting(args, defaultRepo, apiService) {
const { owner, repo } = getOwnerRepo(args, defaultRepo);
const { report_type = "activity", output_format = "json" } = args;
let reportData = {};
const reportId = `${owner}-${repo}-${report_type}-${Date.now()}`;
switch (report_type) {
case "activity":
// Generate activity report
const [commits, issues, prs, contributors] = await Promise.all([
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/commits?per_page=100`),
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/issues?state=all&per_page=100`),
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/pulls?state=all&per_page=100`),
apiService.makeGitHubRequest(`/repos/${owner}/${repo}/contributors?per_page=100`)
]);
// Last 30 days activity
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const recentCommits = commits.filter(commit =>
new Date(commit.commit.author.date) > thirtyDaysAgo
);
const recentIssues = issues.filter(issue =>
new Date(issue.created_at) > thirtyDaysAgo
);
const recentPRs = prs.filter(pr =>
new Date(pr.created_at) > thirtyDaysAgo
);
reportData = {
repository: `${owner}/${repo}`,
period: "Last 30 days",
commits: {
total: commits.length,
recent: recentCommits.length,
top_authors: getTopCommitAuthors(recentCommits).slice(0, 5)
},
issues: {
total: issues.length,
recent: recentIssues.length,
open: issues.filter(i => i.state === 'open').length,
closed: issues.filter(i => i.state === 'closed').length
},
pull_requests: {
total: prs.length,
recent: recentPRs.length,
merged: prs.filter(pr => pr.merged_at).length,
open: prs.filter(pr => pr.state === 'open').length
},
contributors: {
total: contributors.length,
top_contributors: contributors.slice(0, 10).map(c => ({
login: c.login,
contributions: c.contributions
}))
}
};
break;
case "security":
// Generate security report
let vulnerabilityAlerts = [];
let secretScanningAlerts = [];
try {
vulnerabilityAlerts = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/vulnerability-alerts`
);
} catch (error) {
// Vulnerability alerts might not be available
}
try {
secretScanningAlerts = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/secret-scanning/alerts`
);
} catch (error) {
// Secret scanning might not be enabled
}
reportData = {
repository: `${owner}/${repo}`,
vulnerability_alerts: {
total: vulnerabilityAlerts.length,
alerts: vulnerabilityAlerts.slice(0, 10)
},
secret_scanning: {
total: secretScanningAlerts.length,
alerts: secretScanningAlerts.slice(0, 10)
},
security_recommendations: [
"Enable Dependabot alerts",
"Enable secret scanning",
"Use branch protection rules",
"Require status checks",
"Review and update dependencies regularly"
]
};
break;
case "issues":
// Generate issues report
const allIssues = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/issues?state=all&per_page=100`
);
const openIssues = allIssues.filter(issue => issue.state === 'open' && !issue.pull_request);
const closedIssues = allIssues.filter(issue => issue.state === 'closed' && !issue.pull_request);
// Issue aging
const oldIssues = openIssues.filter(issue =>
(Date.now() - new Date(issue.created_at)) > (90 * 24 * 60 * 60 * 1000)
);
reportData = {
repository: `${owner}/${repo}`,
summary: {
total_issues: openIssues.length + closedIssues.length,
open: openIssues.length,
closed: closedIssues.length,
stale_issues: oldIssues.length
},
labels: getIssueLabelStats(allIssues),
aging: {
new_7_days: openIssues.filter(i =>
(Date.now() - new Date(i.created_at)) < (7 * 24 * 60 * 60 * 1000)
).length,
older_30_days: openIssues.filter(i =>
(Date.now() - new Date(i.created_at)) > (30 * 24 * 60 * 60 * 1000)
).length,
older_90_days: oldIssues.length
}
};
break;
default:
throw new Error(`Unsupported report type: ${report_type}`);
}
return {
success: true,
message: `${report_type} report generated for ${owner}/${repo}`,
data: {
report_id: reportId,
report_type,
generated_at: new Date().toISOString(),
output_format,
...reportData
}
};
}
/**
* Helper function to get top commit authors
*/
function getTopCommitAuthors(commits) {
const authors = {};
commits.forEach(commit => {
const author = commit.commit.author.name;
authors[author] = (authors[author] || 0) + 1;
});
return Object.entries(authors)
.sort(([,a], [,b]) => b - a)
.map(([name, count]) => ({ name, commits: count }));
}
/**
* Helper function to get issue label statistics
*/
function getIssueLabelStats(issues) {
const labels = {};
issues.forEach(issue => {
issue.labels.forEach(label => {
labels[label.name] = (labels[label.name] || 0) + 1;
});
});
return Object.entries(labels)
.sort(([,a], [,b]) => b - a)
.slice(0, 10)
.map(([name, count]) => ({ name, count }));
}
/**
* Handler for GitHub notification management.
* @param {object} args - The arguments for the tool.
* @param {object} defaultRepo - Default repository configuration.
* @param {object} apiService - GitHub API service instance.
* @returns {Promise<object>} Notification management results.
*/
async function handleNotificationManagement(args, defaultRepo, apiService) {
const { action = "list", thread_id, all = false, participating = false } = args;
switch (action) {
case "list":
const params = new URLSearchParams();
if (all) params.append('all', 'true');
if (participating) params.append('participating', 'true');
const notifications = await apiService.makeGitHubRequest(
`/notifications?${params.toString()}`
);
return {
success: true,
message: `Found ${notifications.length} notifications`,
data: {
total: notifications.length,
unread: notifications.filter(n => n.unread).length,
notifications: notifications.slice(0, 20).map(notification => ({
id: notification.id,
title: notification.subject.title,
type: notification.subject.type,
repository: notification.repository.full_name,
unread: notification.unread,
updated_at: notification.updated_at,
url: notification.subject.url
}))
}
};
case "mark_read":
await apiService.makeGitHubRequest('/notifications', {
method: 'PUT'
});
return {
success: true,
message: "All notifications marked as read"
};
case "get_thread":
if (!thread_id) {
throw new Error("thread_id is required for get_thread action");
}
const thread = await apiService.makeGitHubRequest(`/notifications/threads/${thread_id}`);
return {
success: true,
message: "Thread retrieved successfully",
data: {
id: thread.id,
title: thread.subject.title,
type: thread.subject.type,
repository: thread.repository.full_name,
unread: thread.unread,
updated_at: thread.updated_at,
subscription: thread.subscription
}
};
case "mark_thread_read":
if (!thread_id) {
throw new Error("thread_id is required for mark_thread_read action");
}
await apiService.makeGitHubRequest(`/notifications/threads/${thread_id}`, {
method: 'PATCH'
});
return {
success: true,
message: `Thread ${thread_id} marked as read`
};
default:
throw new Error(`Unsupported notification action: ${action}`);
}
}
/**
* Handler for GitHub release management.
* @param {object} args - The arguments for the tool.
* @param {object} defaultRepo - Default repository configuration.
* @param {object} apiService - GitHub API service instance.
* @returns {Promise<object>} Release management results.
*/
async function handleReleaseManagement(args, defaultRepo, apiService) {
const { owner, repo } = getOwnerRepo(args, defaultRepo);
const { action = "list", release_id, tag_name, name, body, draft = false, prerelease = false } = args;
switch (action) {
case "list":
const releases = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/releases?per_page=20`
);
return {
success: true,
message: `Found ${releases.length} releases for ${owner}/${repo}`,
data: {
repository: `${owner}/${repo}`,
total: releases.length,
releases: releases.map(release => ({
id: release.id,
tag_name: release.tag_name,
name: release.name,
draft: release.draft,
prerelease: release.prerelease,
published_at: release.published_at,
assets_count: release.assets.length,
download_count: release.assets.reduce((sum, asset) => sum + asset.download_count, 0),
url: release.html_url
}))
}
};
case "get_latest":
const latestRelease = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/releases/latest`
);
return {
success: true,
message: `Latest release: ${latestRelease.tag_name}`,
data: {
id: latestRelease.id,
tag_name: latestRelease.tag_name,
name: latestRelease.name,
body: latestRelease.body,
draft: latestRelease.draft,
prerelease: latestRelease.prerelease,
published_at: latestRelease.published_at,
assets: latestRelease.assets.map(asset => ({
name: asset.name,
size: asset.size,
download_count: asset.download_count,
download_url: asset.browser_download_url
})),
url: latestRelease.html_url
}
};
case "get":
if (!release_id) {
throw new Error("release_id is required for get action");
}
const release = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/releases/${release_id}`
);
return {
success: true,
message: `Release ${release.tag_name} retrieved`,
data: {
id: release.id,
tag_name: release.tag_name,
name: release.name,
body: release.body,
draft: release.draft,
prerelease: release.prerelease,
published_at: release.published_at,
assets: release.assets.map(asset => ({
name: asset.name,
size: asset.size,
download_count: asset.download_count
}))
}
};
case "create":
if (!tag_name) {
throw new Error("tag_name is required for create action");
}
const createData = {
tag_name,
name: name || tag_name,
body: body || '',
draft,
prerelease
};
if (args.target_commitish) {
createData.target_commitish = args.target_commitish;
}
const newRelease = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/releases`,
{
method: 'POST',
body: JSON.stringify(createData),
headers: { 'Content-Type': 'application/json' }
}
);
return {
success: true,
message: `Release ${tag_name} created successfully`,
data: {
id: newRelease.id,
tag_name: newRelease.tag_name,
name: newRelease.name,
draft: newRelease.draft,
prerelease: newRelease.prerelease,
url: newRelease.html_url
}
};
case "delete":
if (!release_id) {
throw new Error("release_id is required for delete action");
}
await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/releases/${release_id}`,
{ method: 'DELETE' }
);
return {
success: true,
message: `Release ${release_id} deleted successfully`
};
default:
throw new Error(`Unsupported release action: ${action}`);
}
}
/**
* Handler for dependency analysis using GitHub's dependency APIs.
* @param {object} args - The arguments for the tool.
* @param {object} defaultRepo - Default repository configuration.
* @param {object} apiService - GitHub API service instance.
* @returns {Promise<object>} Dependency analysis results.
*/
async function handleDependencyAnalysis(args, defaultRepo, apiService) {
const { owner, repo } = getOwnerRepo(args, defaultRepo);
const { report_type = "vulnerabilities" } = args;
let analysisData = {};
switch (report_type) {
case "vulnerabilities":
// Get Dependabot alerts
let vulnerabilityAlerts = [];
try {
vulnerabilityAlerts = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/dependabot/alerts`
);
} catch (error) {
if (error.message.includes('404')) {
return {
success: true,
message: "Dependabot alerts are not enabled for this repository",
data: {
repository: `${owner}/${repo}`,
dependabot_enabled: false,
vulnerabilities: [],
recommendations: [
"Enable Dependabot alerts in repository settings",
"Enable GitHub Advanced Security",
"Review dependency files (package.json, requirements.txt, etc.)"
]
}
};
}
throw error;
}
// Categorize by severity
const critical = vulnerabilityAlerts.filter(alert => alert.security_vulnerability?.severity === 'critical');
const high = vulnerabilityAlerts.filter(alert => alert.security_vulnerability?.severity === 'high');
const medium = vulnerabilityAlerts.filter(alert => alert.security_vulnerability?.severity === 'medium');
const low = vulnerabilityAlerts.filter(alert => alert.security_vulnerability?.severity === 'low');
analysisData = {
repository: `${owner}/${repo}`,
dependabot_enabled: true,
total_alerts: vulnerabilityAlerts.length,
severity_breakdown: {
critical: critical.length,
high: high.length,
medium: medium.length,
low: low.length
},
vulnerabilities: vulnerabilityAlerts.slice(0, 20).map(alert => ({
id: alert.number,
state: alert.state,
package: alert.dependency?.package?.name,
manifest_path: alert.dependency?.manifest_path,
severity: alert.security_vulnerability?.severity,
summary: alert.security_vulnerability?.summary,
created_at: alert.created_at,
fixed_at: alert.fixed_at,
url: alert.html_url
})),
recommendations: vulnerabilityAlerts.length > 0 ? [
"Review and fix critical and high severity vulnerabilities",
"Update dependencies to latest secure versions",
"Enable automated dependency updates"
] : [
"No vulnerabilities detected",
"Keep dependencies updated",
"Monitor for new vulnerabilities"
]
};
break;
case "licenses":
// For license analysis, we'd typically need to analyze dependency files
// This is a simplified version that checks known dependency files
let dependencyInfo = [];
try {
// Check for common dependency files
const packageFiles = ['package.json', 'requirements.txt', 'Gemfile', 'pom.xml', 'go.mod'];
for (const file of packageFiles) {
try {
const content = await apiService.makeGitHubRequest(
`/repos/${owner}/${repo}/contents/${file}`
);
dependencyInfo.push({
file: file,
exists: true,
ecosystem: getEcosystemFromFile(file)
});
} catch (error) {
// File doesn't exist, continue
}
}
} catch (error) {
// Continue with what we have
}
analysisData = {
repository: `${owner}/${repo}`,
dependency_files: dependencyInfo,
license_analysis: {
status: "manual_review_required",
note: "Full license analysis requires specialized tools",
recommendations: [
"Use tools like FOSSA or Snyk for comprehensive license analysis",
"Review dependency licenses manually",
"Ensure license compatibility with your project"
]
}
};
break;
case "graph":
// Get dependency graph (requires GraphQL API)
analysisData = {
repository: `${owner}/${repo}`,
dependency_graph: {
status: "graphql_required",
note: "Dependency graph analysis requires GraphQL API",
alternatives: [
"Use GitHub's Insights > Dependency graph page",
"Enable Dependabot for security analysis",
"Use package manager specific tools (npm audit, pip-audit, etc.)"
]
}
};
break;
default:
throw new Error(`Unsupported dependency analysis type: ${report_type}`);
}
return {
success: true,
message: `Dependency analysis (${report_type}) completed for ${owner}/${repo}`,
data: {
analysis_type: report_type,
generated_at: new Date().toISOString(),
...analysisData
}
};
}
/**
* Helper function to determine ecosystem from dependency file
*/
function getEcosystemFromFile(filename) {
const ecosystems = {
'package.json': 'npm',
'requirements.txt': 'pip',
'Gemfile': 'ruby',
'pom.xml': 'maven',
'go.mod': 'go',
'Cargo.toml': 'cargo',
'composer.json': 'composer'
};
return ecosystems[filename] || 'unknown';
}
module.exports = {
handleCodeQualityChecks,
handleCustomDashboards,
handleAutomatedReporting,
handleNotificationManagement,
handleReleaseManagement,
handleDependencyAnalysis,
};