UNPKG

@mcp-consultant-tools/github-enterprise

Version:

MCP server for GitHub Enterprise - repositories, commits, pull requests, and code search

955 lines 45.1 kB
#!/usr/bin/env node import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { pathToFileURL } from "node:url"; import { realpathSync } from "node:fs"; import { createMcpServer, createEnvLoader } from "@mcp-consultant-tools/core"; import { GitHubEnterpriseService } from "./GitHubEnterpriseService.js"; import { z } from 'zod'; import * as gheFormatters from './utils/ghe-formatters.js'; export function registerGitHubEnterpriseTools(server, githubenterpriseService) { let service = githubenterpriseService || null; function getGitHubEnterpriseService() { if (!service) { const missingConfig = []; let repos = []; if (process.env.GHE_REPOS) { try { repos = JSON.parse(process.env.GHE_REPOS); } catch (error) { throw new Error("Failed to parse GHE_REPOS JSON"); } } else { missingConfig.push("GHE_REPOS"); } if (!process.env.GHE_TOKEN) missingConfig.push("GHE_TOKEN"); if (missingConfig.length > 0) { throw new Error(`Missing GitHub Enterprise configuration: ${missingConfig.join(", ")}`); } const config = { repos, baseUrl: process.env.GHE_BASE_URL || 'https://github.com', apiVersion: process.env.GHE_API_VERSION || '2022-11-28', authMethod: 'pat', pat: process.env.GHE_TOKEN, enableWrite: process.env.GHE_ENABLE_WRITE === 'true', enableCreate: process.env.GHE_ENABLE_CREATE === 'true', enableCache: process.env.GHE_ENABLE_CACHE !== 'false', cacheTtl: parseInt(process.env.GHE_CACHE_TTL || '300'), maxFileSize: parseInt(process.env.GHE_MAX_FILE_SIZE || '1048576'), maxSearchResults: parseInt(process.env.GHE_MAX_SEARCH_RESULTS || '100'), }; service = new GitHubEnterpriseService(config); console.error("GitHub Enterprise service initialized"); } return service; } // ======================================== // PROMPTS // ======================================== server.prompt("ghe-repo-overview", "Get a comprehensive repository overview with branch analysis and recent commits", { repoId: z.string().describe("Repository ID from configuration"), }, async ({ repoId }) => { const service = getGitHubEnterpriseService(); const repo = service.getRepoById(repoId); const [branches, defaultBranchInfo] = await Promise.all([ service.listBranches(repoId), service.getDefaultBranch(repoId), ]); const recentCommits = await service.getCommits(repoId, defaultBranchInfo.branch, undefined, undefined, undefined, undefined, 10); const output = gheFormatters.formatRepositoryOverviewAsMarkdown({ owner: repo.owner, repo: repo.repo, url: `${service['config'].baseUrl}/${repo.owner}/${repo.repo}`, defaultBranch: defaultBranchInfo.branch, description: repo.description, active: repo.active, }, branches, recentCommits); return { messages: [ { role: "user", content: { type: "text", text: output, }, }, ], }; }); server.prompt("ghe-code-search-report", "Search code across repositories and get formatted results with analysis", { query: z.string().describe("Search query"), repoId: z.string().optional().describe("Limit to specific repository ID"), extension: z.string().optional().describe("Filter by file extension (e.g., 'cs', 'js')"), }, async ({ query, repoId, extension }) => { const service = getGitHubEnterpriseService(); const results = await service.searchCode(query, repoId, undefined, extension); const output = gheFormatters.formatCodeSearchResultsAsMarkdown(results); return { messages: [ { role: "user", content: { type: "text", text: output, }, }, ], }; }); server.prompt("ghe-branch-comparison-report", "Compare branches and generate deployment-ready summary with checklist", { repoId: z.string().describe("Repository ID from configuration"), base: z.string().describe("Base branch (e.g., 'main')"), head: z.string().describe("Head branch to compare (e.g., 'release/9.0')"), }, async ({ repoId, base, head }) => { const service = getGitHubEnterpriseService(); const repo = service.getRepoById(repoId); const comparison = await service.compareBranches(repoId, base, head); const insights = gheFormatters.analyzeBranchComparison(comparison); const checklist = gheFormatters.generateDeploymentChecklist(comparison); let output = `# Branch Comparison: ${base}${head}\n\n`; output += `**Repository:** ${repo.owner}/${repo.repo}\n`; output += `**Comparing:** \`${base}\` (base) ← \`${head}\` (head)\n\n`; output += `## Summary\n\n`; output += insights.join('\n') + '\n\n'; if (comparison.commits && comparison.commits.length > 0) { output += `## Commits to Deploy\n\n`; output += gheFormatters.formatCommitHistoryAsMarkdown(comparison.commits) + '\n\n'; } if (comparison.files && comparison.files.length > 0) { output += `## Files Changed (${comparison.files.length})\n\n`; const header = '| File | Status | +/- | Changes |'; const separator = '|------|--------|-----|---------|'; const rows = comparison.files.slice(0, 20).map((f) => { const status = f.status === 'added' ? '🆕 Added' : f.status === 'modified' ? '📝 Modified' : f.status === 'removed' ? '🗑️ Removed' : f.status === 'renamed' ? '📋 Renamed' : f.status; return `| \`${f.filename}\` | ${status} | +${f.additions}/-${f.deletions} | ${f.changes} |`; }); output += [header, separator, ...rows].join('\n'); if (comparison.files.length > 20) { output += `\n\n*Showing 20 of ${comparison.files.length} files*`; } output += '\n\n'; } output += `## Deployment Checklist\n\n`; output += checklist.join('\n'); return { messages: [ { role: "user", content: { type: "text", text: output, }, }, ], }; }); server.prompt("ghe-troubleshooting-guide", "Generate comprehensive bug troubleshooting report with source code analysis", { repoId: z.string().describe("Repository ID to investigate"), searchQuery: z.string().describe("Search query (e.g., plugin name, entity name, or code pattern)"), branch: z.string().optional().describe("Branch to search (default: auto-detected)"), }, async ({ repoId, searchQuery, branch }) => { const service = getGitHubEnterpriseService(); const repo = service.getRepoById(repoId); // Search for code const codeResults = await service.searchCode(searchQuery, repoId); // Search commits for references const commitResults = await service.searchCommits(repoId, searchQuery); let output = `# Bug Troubleshooting Report\n\n`; output += `**Repository:** ${repo.owner}/${repo.repo}\n`; output += `**Search Query:** \`${searchQuery}\`\n\n`; output += `## Source Code Analysis\n\n`; if (codeResults.total_count > 0) { output += `Found **${codeResults.total_count} code matches** across ${codeResults.items.length} files:\n\n`; output += gheFormatters.formatCodeSearchResultsAsMarkdown(codeResults) + '\n\n'; } else { output += `*No code matches found for query: "${searchQuery}"*\n\n`; } output += `## Related Commits\n\n`; if (commitResults.length > 0) { output += `Found **${commitResults.length} commits** referencing "${searchQuery}":\n\n`; output += gheFormatters.formatCommitHistoryAsMarkdown(commitResults.slice(0, 10)) + '\n\n'; if (commitResults.length > 10) { output += `*Showing 10 of ${commitResults.length} commits*\n\n`; } } else { output += `*No commits found referencing "${searchQuery}"*\n\n`; } output += `## Recommendations\n\n`; output += `1. **Review Code Matches**: Check the code search results above for relevant implementations\n`; output += `2. **Analyze Recent Changes**: Review commit history for recent modifications\n`; output += `3. **Check Branch**: Current search is on branch \`${branch || 'auto-detected'}\`\n`; output += `4. **Cross-Reference**: Use ADO work items or PowerPlatform plugin names to correlate issues\n`; return { messages: [ { role: "user", content: { type: "text", text: output, }, }, ], }; }); server.prompt("ghe-deployment-report", "Generate deployment-ready report with code changes, testing checklist, and rollback plan", { repoId: z.string().describe("Repository ID"), fromBranch: z.string().optional().describe("Source branch (default: main)"), toBranch: z.string().optional().describe("Target branch (default: auto-detected)"), }, async ({ repoId, fromBranch = "main", toBranch }) => { const service = getGitHubEnterpriseService(); const repo = service.getRepoById(repoId); // Auto-detect target branch if not specified const targetBranch = toBranch || (await service.getDefaultBranch(repoId)).branch; // Get branch comparison const comparison = await service.compareBranches(repoId, fromBranch, targetBranch); const insights = gheFormatters.analyzeBranchComparison(comparison); const checklist = gheFormatters.generateDeploymentChecklist(comparison); let output = `# Deployment Report: ${targetBranch}${fromBranch}\n\n`; output += `**Repository:** ${repo.owner}/${repo.repo}\n`; output += `**Source:** \`${targetBranch}\`\n`; output += `**Target:** \`${fromBranch}\` (Production)\n`; output += `**Date:** ${new Date().toISOString().split('T')[0]}\n\n`; output += `## Executive Summary\n\n`; output += insights.join('\n') + '\n\n'; output += `## Changes by Component\n\n`; if (comparison.files && comparison.files.length > 0) { // Group files by directory/component const filesByDir = {}; comparison.files.forEach((f) => { const dir = f.filename.split('/')[0] || 'root'; if (!filesByDir[dir]) filesByDir[dir] = []; filesByDir[dir].push(f); }); Object.entries(filesByDir).forEach(([dir, files]) => { output += `### ${dir}/ (${files.length} files)\n\n`; const rows = files.slice(0, 10).map((f) => `- \`${f.filename}\` (+${f.additions}, -${f.deletions})`); output += rows.join('\n') + '\n\n'; if (files.length > 10) { output += `*...and ${files.length - 10} more files*\n\n`; } }); } output += `## Deployment Steps\n\n`; output += `### 1. Pre-Deployment Verification\n`; output += `\`\`\`bash\n# Review changes\ngit diff ${fromBranch}...${targetBranch}\n\n# Run tests\nnpm test # or: dotnet test\n\`\`\`\n\n`; output += `### 2. Merge to Production\n`; output += `\`\`\`bash\ngit checkout ${fromBranch}\ngit merge ${targetBranch} --no-ff\ngit push origin ${fromBranch}\n\`\`\`\n\n`; output += `### 3. Post-Deployment Verification\n`; output += `- [ ] Smoke tests passing\n`; output += `- [ ] No errors in logs (first 15 minutes)\n`; output += `- [ ] Verify key functionality works\n\n`; output += `## Rollback Plan\n\n`; output += `If issues occur after deployment:\n\n`; output += `\`\`\`bash\n# Option 1: Revert merge commit\ngit revert -m 1 HEAD\ngit push origin ${fromBranch}\n\n`; output += `# Option 2: Reset to previous commit (if not pushed)\ngit reset --hard HEAD~1\n\`\`\`\n\n`; output += `## Testing Checklist\n\n`; output += checklist.join('\n'); return { messages: [ { role: "user", content: { type: "text", text: output, }, }, ], }; }); // ======================================== // TOOLS // ======================================== server.tool("ghe-list-repos", "List all configured GitHub Enterprise repositories (active and inactive)", {}, async () => { try { const service = getGitHubEnterpriseService(); const repos = service.getAllRepos(); const reposWithUrls = repos.map((r) => ({ ...r, url: `${service['config'].baseUrl}/${r.owner}/${r.repo}` })); return { content: [{ type: "text", text: `# Configured GitHub Enterprise Repositories\n\n` + `**Total:** ${repos.length} repositories\n` + `**Active:** ${repos.filter(r => r.active).length}\n\n` + JSON.stringify(reposWithUrls, null, 2) }] }; } catch (error) { console.error("Error listing GitHub Enterprise repositories:", error); return { content: [{ type: "text", text: `Failed to list repositories: ${error.message}\n\n` + `Troubleshooting:\n` + `1. Verify GHE_URL is set correctly\n` + `2. Verify GHE_PAT or GitHub App credentials are set\n` + `3. Verify GHE_REPOS is configured as JSON array\n` + `4. Check repository access permissions` }] }; } }); server.tool("ghe-list-branches", "List all branches for a GitHub Enterprise repository", { repoId: z.string().describe("Repository ID from configuration (e.g., 'plugin-core')"), protectedOnly: z.boolean().optional().describe("Filter by protection status (true for protected branches only)"), }, async ({ repoId, protectedOnly }) => { try { const service = getGitHubEnterpriseService(); const branches = await service.listBranches(repoId, protectedOnly); return { content: [{ type: "text", text: `# Branches for Repository: ${repoId}\n\n` + `**Total:** ${branches.length} branches\n\n` + gheFormatters.formatBranchListAsMarkdown(branches) }] }; } catch (error) { console.error("Error listing branches:", error); return { content: [{ type: "text", text: `Failed to list branches: ${error.message}` }] }; } }); server.tool("ghe-get-default-branch", "Auto-detect the default branch for a repository (handles typos, provides alternatives)", { repoId: z.string().describe("Repository ID from configuration"), userSpecified: z.string().optional().describe("User-specified branch name (overrides auto-detection)"), }, async ({ repoId, userSpecified }) => { try { const service = getGitHubEnterpriseService(); const result = await service.getDefaultBranch(repoId, userSpecified); let output = `# Default Branch for Repository: ${repoId}\n\n`; output += `**Selected Branch:** \`${result.branch}\` \n`; output += `**Reason:** ${result.reason} \n`; output += `**Confidence:** ${result.confidence} \n\n`; if (result.alternatives && result.alternatives.length > 0) { output += `**Alternative Branches:**\n`; result.alternatives.slice(0, 5).forEach(alt => { output += `- \`${alt}\`\n`; }); if (result.alternatives.length > 5) { output += `- ... and ${result.alternatives.length - 5} more\n`; } } if (result.message) { output += `\n**Note:** ${result.message}\n`; } return { content: [{ type: "text", text: output }] }; } catch (error) { console.error("Error getting default branch:", error); return { content: [{ type: "text", text: `Failed to get default branch: ${error.message}` }] }; } }); server.tool("ghe-get-file", "Get file content from a GitHub Enterprise repository", { repoId: z.string().describe("Repository ID from configuration"), path: z.string().describe("File path (e.g., 'src/Plugins/ContactPlugin.cs')"), branch: z.string().optional().describe("Branch name (default: auto-detected)"), }, async ({ repoId, path, branch }) => { try { const service = getGitHubEnterpriseService(); const file = await service.getFile(repoId, path, branch); return { content: [{ type: "text", text: `# File: ${path}\n\n` + `**Repository:** ${repoId} \n` + `**Branch:** \`${file.branch}\` \n` + `**Size:** ${file.size} bytes \n` + `**SHA:** \`${file.sha}\` \n\n` + `## Content\n\n\`\`\`\n${file.decodedContent}\n\`\`\`` }] }; } catch (error) { console.error("Error getting file:", error); return { content: [{ type: "text", text: `Failed to get file: ${error.message}\n\n` + `Troubleshooting:\n` + `1. Verify file path is correct\n` + `2. Verify branch exists (or let auto-detection find it)\n` + `3. Check if file size exceeds GHE_MAX_FILE_SIZE (default: 1MB)` }] }; } }); server.tool("ghe-search-code", "Search code across GitHub Enterprise repositories", { query: z.string().describe("Search query (e.g., 'class ContactPlugin')"), repoId: z.string().optional().describe("Limit to specific repository"), path: z.string().optional().describe("Filter by file path pattern"), extension: z.string().optional().describe("Filter by file extension (e.g., 'cs', 'js')"), }, async ({ query, repoId, path, extension }) => { try { const service = getGitHubEnterpriseService(); const results = await service.searchCode(query, repoId, path, extension); return { content: [{ type: "text", text: gheFormatters.formatCodeSearchResultsAsMarkdown(results) }] }; } catch (error) { console.error("Error searching code:", error); return { content: [{ type: "text", text: `Failed to search code: ${error.message}\n\n` + `Troubleshooting:\n` + `1. Simplify search query if too complex\n` + `2. Check rate limits if search fails\n` + `3. Verify repository access permissions` }] }; } }); server.tool("ghe-list-files", "List files in a directory of a GitHub Enterprise repository", { repoId: z.string().describe("Repository ID from configuration"), path: z.string().optional().describe("Directory path (default: root)"), branch: z.string().optional().describe("Branch name (default: auto-detected)"), }, async ({ repoId, path, branch }) => { try { const service = getGitHubEnterpriseService(); const result = await service.listFiles(repoId, path, branch); return { content: [{ type: "text", text: `# Directory: ${path || '/'}\n\n` + `**Repository:** ${repoId} \n` + `**Branch:** \`${result.branch}\` \n\n` + gheFormatters.formatDirectoryContentsAsMarkdown(result.contents) }] }; } catch (error) { console.error("Error listing files:", error); return { content: [{ type: "text", text: `Failed to list files: ${error.message}` }] }; } }); server.tool("ghe-clear-cache", "Clear cached GitHub Enterprise API responses (useful after pushing code updates)", { pattern: z.string().optional().describe("Clear only cache entries matching this pattern (e.g., 'ContactPlugin.cs')"), repoId: z.string().optional().describe("Clear cache for specific repository only"), }, async ({ pattern, repoId }) => { try { const service = getGitHubEnterpriseService(); const cleared = service.clearCache(pattern, repoId); return { content: [{ type: "text", text: `✅ Cleared ${cleared} cache entries` + (pattern ? ` matching pattern '${pattern}'` : '') + (repoId ? ` for repository '${repoId}'` : '') }] }; } catch (error) { console.error("Error clearing cache:", error); return { content: [{ type: "text", text: `Failed to clear cache: ${error.message}` }] }; } }); server.tool("ghe-get-commits", "Get commit history for a branch in a GitHub Enterprise repository", { repoId: z.string().describe("Repository ID from configuration"), branch: z.string().optional().describe("Branch name (default: auto-detected)"), since: z.string().optional().describe("ISO 8601 date (e.g., '2025-01-01T00:00:00Z')"), until: z.string().optional().describe("ISO 8601 date"), author: z.string().optional().describe("Filter by author"), path: z.string().optional().describe("Filter by file path"), limit: z.number().optional().describe("Max commits (default: 50)"), }, async ({ repoId, branch, since, until, author, path, limit }) => { try { const service = getGitHubEnterpriseService(); const commits = await service.getCommits(repoId, branch, since, until, author, path, limit || 50); return { content: [{ type: "text", text: `# Commit History\n\n` + `**Repository:** ${repoId} \n` + `**Count:** ${commits.length}\n\n` + gheFormatters.formatCommitHistoryAsMarkdown(commits) }] }; } catch (error) { console.error("Error getting commits:", error); return { content: [{ type: "text", text: `Failed to get commits: ${error.message}` }] }; } }); server.tool("ghe-get-commit-details", "Get detailed information about a specific commit in a GitHub Enterprise repository", { repoId: z.string().describe("Repository ID from configuration"), sha: z.string().describe("Commit SHA"), }, async ({ repoId, sha }) => { try { const service = getGitHubEnterpriseService(); const commit = await service.getCommitDetails(repoId, sha); return { content: [{ type: "text", text: gheFormatters.formatCommitDetailsAsMarkdown(commit) }] }; } catch (error) { console.error("Error getting commit details:", error); return { content: [{ type: "text", text: `Failed to get commit details: ${error.message}` }] }; } }); server.tool("ghe-search-commits", "Search commits by message or hash (supports work item references like '#1234')", { query: z.string().describe("Search query (e.g., '#1234', 'fix bug')"), repoId: z.string().optional().describe("Limit to specific repository"), author: z.string().optional().describe("Filter by author"), since: z.string().optional().describe("ISO 8601 date"), until: z.string().optional().describe("ISO 8601 date"), }, async ({ query, repoId, author, since, until }) => { try { const service = getGitHubEnterpriseService(); const results = await service.searchCommits(query, repoId, author, since, until); return { content: [{ type: "text", text: `# Commit Search Results\n\n` + `**Query:** ${query} \n` + `**Total Results:** ${results.total_count} \n` + `**Showing:** ${results.items.length}\n\n` + gheFormatters.formatCommitHistoryAsMarkdown(results.items) }] }; } catch (error) { console.error("Error searching commits:", error); return { content: [{ type: "text", text: `Failed to search commits: ${error.message}` }] }; } }); server.tool("ghe-get-commit-diff", "Get detailed diff for a commit in unified format", { repoId: z.string().describe("Repository ID from configuration"), sha: z.string().describe("Commit SHA"), format: z.enum(['diff', 'patch']).optional().describe("Format: 'diff' or 'patch' (default: 'diff')"), }, async ({ repoId, sha, format }) => { try { const service = getGitHubEnterpriseService(); const diff = await service.getCommitDiff(repoId, sha, format || 'diff'); return { content: [{ type: "text", text: `# Commit Diff: ${sha}\n\n` + `**Repository:** ${repoId} \n` + `**Format:** ${format || 'diff'} \n\n` + `\`\`\`diff\n${diff}\n\`\`\`` }] }; } catch (error) { console.error("Error getting commit diff:", error); return { content: [{ type: "text", text: `Failed to get commit diff: ${error.message}` }] }; } }); server.tool("ghe-compare-branches", "Compare two branches and show differences", { repoId: z.string().describe("Repository ID from configuration"), base: z.string().describe("Base branch name"), head: z.string().describe("Head branch name"), }, async ({ repoId, base, head }) => { try { const service = getGitHubEnterpriseService(); const comparison = await service.compareBranches(repoId, base, head); const insights = gheFormatters.analyzeBranchComparison(comparison); return { content: [{ type: "text", text: `# Branch Comparison: ${base}${head}\n\n` + `**Repository:** ${repoId} \n\n` + `## Summary\n\n` + insights.join('\n') + '\n\n' + `## Commits (${comparison.commits.length})\n\n` + gheFormatters.formatCommitHistoryAsMarkdown(comparison.commits.slice(0, 10)) }] }; } catch (error) { console.error("Error comparing branches:", error); return { content: [{ type: "text", text: `Failed to compare branches: ${error.message}` }] }; } }); server.tool("ghe-get-branch-details", "Get detailed information about a specific branch", { repoId: z.string().describe("Repository ID from configuration"), branch: z.string().describe("Branch name"), }, async ({ repoId, branch }) => { try { const service = getGitHubEnterpriseService(); const branchInfo = await service.getBranchDetails(repoId, branch); return { content: [{ type: "text", text: `# Branch Details: ${branch}\n\n` + `**Repository:** ${repoId} \n` + `**Protected:** ${branchInfo.protected ? '🔒 Yes' : 'No'} \n` + `**Last Commit:** \`${branchInfo.commit.sha.substring(0, 7)}\` \n` + `**Commit Message:** ${branchInfo.commit.commit.message.split('\n')[0]} \n` + `**Author:** ${branchInfo.commit.commit.author.name} \n` + `**Date:** ${new Date(branchInfo.commit.commit.author.date).toLocaleString()} \n\n` + JSON.stringify(branchInfo, null, 2) }] }; } catch (error) { console.error("Error getting branch details:", error); return { content: [{ type: "text", text: `Failed to get branch details: ${error.message}` }] }; } }); server.tool("ghe-list-pull-requests", "List pull requests for a GitHub Enterprise repository", { repoId: z.string().describe("Repository ID from configuration"), state: z.enum(['open', 'closed', 'all']).optional().describe("PR state (default: 'open')"), base: z.string().optional().describe("Filter by base branch"), head: z.string().optional().describe("Filter by head branch"), sort: z.enum(['created', 'updated', 'popularity']).optional().describe("Sort order (default: 'created')"), limit: z.number().optional().describe("Max results (default: 30)"), }, async ({ repoId, state, base, head, sort, limit }) => { try { const service = getGitHubEnterpriseService(); const prs = await service.listPullRequests(repoId, state || 'open', base, head, sort || 'created', limit || 30); return { content: [{ type: "text", text: `# Pull Requests\n\n` + `**Repository:** ${repoId} \n` + `**State:** ${state || 'open'} \n` + `**Count:** ${prs.length}\n\n` + gheFormatters.formatPullRequestsAsMarkdown(prs) }] }; } catch (error) { console.error("Error listing pull requests:", error); return { content: [{ type: "text", text: `Failed to list pull requests: ${error.message}` }] }; } }); server.tool("ghe-get-pull-request", "Get detailed information about a specific pull request", { repoId: z.string().describe("Repository ID from configuration"), prNumber: z.number().describe("Pull request number"), }, async ({ repoId, prNumber }) => { try { const service = getGitHubEnterpriseService(); const pr = await service.getPullRequest(repoId, prNumber); return { content: [{ type: "text", text: gheFormatters.formatPullRequestDetailsAsMarkdown(pr) }] }; } catch (error) { console.error("Error getting pull request:", error); return { content: [{ type: "text", text: `Failed to get pull request: ${error.message}` }] }; } }); server.tool("ghe-get-pr-files", "Get files changed in a pull request", { repoId: z.string().describe("Repository ID from configuration"), prNumber: z.number().describe("Pull request number"), }, async ({ repoId, prNumber }) => { try { const service = getGitHubEnterpriseService(); const files = await service.getPullRequestFiles(repoId, prNumber); const header = '| File | Status | +/- | Changes |'; const separator = '|------|--------|-----|---------|'; const rows = files.map(f => { const status = f.status === 'added' ? '🆕 Added' : f.status === 'modified' ? '📝 Modified' : f.status === 'removed' ? '🗑️ Removed' : f.status === 'renamed' ? '📋 Renamed' : f.status; return `| \`${f.filename}\` | ${status} | +${f.additions}/-${f.deletions} | ${f.changes} |`; }); return { content: [{ type: "text", text: `# Pull Request #${prNumber} - Files Changed\n\n` + `**Repository:** ${repoId} \n` + `**Total Files:** ${files.length}\n\n` + [header, separator, ...rows].join('\n') }] }; } catch (error) { console.error("Error getting PR files:", error); return { content: [{ type: "text", text: `Failed to get PR files: ${error.message}` }] }; } }); server.tool("ghe-get-directory-structure", "Get recursive directory tree structure", { repoId: z.string().describe("Repository ID from configuration"), path: z.string().optional().describe("Directory path (default: root)"), branch: z.string().optional().describe("Branch name (default: auto-detected)"), depth: z.number().optional().describe("Recursion depth limit (default: 3)"), }, async ({ repoId, path, branch, depth }) => { try { const service = getGitHubEnterpriseService(); const result = await service.getDirectoryStructure(repoId, path, branch, depth || 3); return { content: [{ type: "text", text: `# Directory Structure: ${path || '/'}\n\n` + `**Repository:** ${repoId} \n` + `**Branch:** \`${result.branch}\` \n` + `**Max Depth:** ${depth || 3}\n\n` + '```\n' + gheFormatters.formatFileTreeAsMarkdown(result.tree) + '\n```' }] }; } catch (error) { console.error("Error getting directory structure:", error); return { content: [{ type: "text", text: `Failed to get directory structure: ${error.message}` }] }; } }); server.tool("ghe-get-file-history", "Get commit history for a specific file", { repoId: z.string().describe("Repository ID from configuration"), path: z.string().describe("File path"), branch: z.string().optional().describe("Branch name (default: auto-detected)"), limit: z.number().optional().describe("Max commits (default: 50)"), }, async ({ repoId, path, branch, limit }) => { try { const service = getGitHubEnterpriseService(); const commits = await service.getFileHistory(repoId, path, branch, limit || 50); return { content: [{ type: "text", text: `# File History: ${path}\n\n` + `**Repository:** ${repoId} \n` + `**Commits:** ${commits.length}\n\n` + gheFormatters.formatCommitHistoryAsMarkdown(commits) }] }; } catch (error) { console.error("Error getting file history:", error); return { content: [{ type: "text", text: `Failed to get file history: ${error.message}` }] }; } }); server.tool("ghe-create-branch", "Create a new branch (requires GHE_ENABLE_CREATE=true)", { repoId: z.string().describe("Repository ID from configuration"), branchName: z.string().describe("New branch name"), fromBranch: z.string().optional().describe("Source branch (default: auto-detected)"), }, async ({ repoId, branchName, fromBranch }) => { try { const service = getGitHubEnterpriseService(); const result = await service.createBranch(repoId, branchName, fromBranch); return { content: [{ type: "text", text: `✅ Branch '${branchName}' created successfully\n\n` + JSON.stringify(result, null, 2) }] }; } catch (error) { console.error("Error creating branch:", error); return { content: [{ type: "text", text: `Failed to create branch: ${error.message}\n\n` + `Note: Branch creation requires GHE_ENABLE_CREATE=true` }] }; } }); server.tool("ghe-update-file", "Update file content (requires GHE_ENABLE_WRITE=true)", { repoId: z.string().describe("Repository ID from configuration"), path: z.string().describe("File path"), content: z.string().describe("New file content"), message: z.string().describe("Commit message"), branch: z.string().describe("Branch name"), sha: z.string().describe("Current file SHA (for conflict detection)"), }, async ({ repoId, path, content, message, branch, sha }) => { try { const service = getGitHubEnterpriseService(); const result = await service.updateFile(repoId, path, content, message, branch, sha); return { content: [{ type: "text", text: `✅ File '${path}' updated successfully\n\n` + `**Commit SHA:** \`${result.commit.sha}\` \n` + `**Branch:** \`${branch}\` \n` + `**Message:** ${message}` }] }; } catch (error) { console.error("Error updating file:", error); return { content: [{ type: "text", text: `Failed to update file: ${error.message}\n\n` + `Note: File updates require GHE_ENABLE_WRITE=true` }] }; } }); server.tool("ghe-create-file", "Create a new file (requires GHE_ENABLE_CREATE=true)", { repoId: z.string().describe("Repository ID from configuration"), path: z.string().describe("File path"), content: z.string().describe("File content"), message: z.string().describe("Commit message"), branch: z.string().describe("Branch name"), }, async ({ repoId, path, content, message, branch }) => { try { const service = getGitHubEnterpriseService(); const result = await service.createFile(repoId, path, content, message, branch); return { content: [{ type: "text", text: `✅ File '${path}' created successfully\n\n` + `**Commit SHA:** \`${result.commit.sha}\` \n` + `**Branch:** \`${branch}\` \n` + `**Message:** ${message}` }] }; } catch (error) { console.error("Error creating file:", error); return { content: [{ type: "text", text: `Failed to create file: ${error.message}\n\n` + `Note: File creation requires GHE_ENABLE_CREATE=true` }] }; } }); server.tool("ghe-search-repos", "Search repositories by name or description across GitHub Enterprise", { query: z.string().describe("Search query"), owner: z.string().optional().describe("Filter by organization/owner"), }, async ({ query, owner }) => { try { const service = getGitHubEnterpriseService(); const results = await service.searchRepositories(query, owner); return { content: [{ type: "text", text: `# Repository Search Results\n\n` + `**Query:** ${query} \n` + `**Total Results:** ${results.total_count} \n` + `**Showing:** ${results.items.length}\n\n` + JSON.stringify(results.items, null, 2) }] }; } catch (error) { console.error("Error searching repositories:", error); return { content: [{ type: "text", text: `Failed to search repositories: ${error.message}` }] }; } }); console.error("github-enterprise tools registered: 22 tools, 5 prompts"); console.error("GitHub Enterprise tools registered: 22 tools, 5 prompts"); } // CLI entry point (standalone execution) // Uses realpathSync to resolve symlinks created by npx if (import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) { const loadEnv = createEnvLoader(); loadEnv(); const server = createMcpServer({ name: "mcp-github-enterprise", version: "1.0.0", capabilities: { tools: {}, prompts: {} } }); registerGitHubEnterpriseTools(server); const transport = new StdioServerTransport(); server.connect(transport).catch((error) => { console.error("Failed to start GitHub Enterprise MCP server:", error); process.exit(1); }); console.error("GitHub Enterprise MCP server running"); } //# sourceMappingURL=index.js.map