UNPKG

coderabbitai-mcp

Version:

MCP server for interacting with CodeRabbit AI reviews on GitHub pull requests. Enables LLMs to analyze, implement, and resolve CodeRabbit suggestions programmatically.

211 lines 8.23 kB
/** * Direct GitHub API client for CodeRabbit MCP server * Uses GitHub REST API v4 with Personal Access Token authentication */ export class GitHubClient { token; baseUrl; constructor(token, baseUrl = 'https://api.github.com') { this.token = token || process.env.GITHUB_PAT || ''; this.baseUrl = baseUrl; if (!this.token) { throw new Error('GITHUB_PAT environment variable is required'); } } /** * Make an authenticated request to the GitHub API */ async makeRequest(endpoint, method = 'GET', body) { const url = `${this.baseUrl}${endpoint}`; const headers = { 'Authorization': `Bearer ${this.token}`, 'Accept': 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', 'User-Agent': 'CodeRabbit-MCP-Server/1.0.0' }; if (body) { headers['Content-Type'] = 'application/json'; } try { const response = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : undefined }); if (!response.ok) { const errorBody = await response.text(); const error = new Error(`GitHub API error: ${response.status} ${response.statusText}`); error.status = response.status; error.response = errorBody; throw error; } // Handle empty responses (e.g., 204 No Content) if (response.status === 204 || response.headers.get('content-length') === '0') { return {}; } return await response.json(); } catch (error) { if (error instanceof Error) { throw error; } throw new Error(`Network error: ${String(error)}`); } } /** * Get all reviews for a pull request */ async getPullRequestReviews(owner, repo, pullNumber) { const endpoint = `/repos/${owner}/${repo}/pulls/${pullNumber}/reviews`; return this.makeRequest(endpoint); } /** * Get all comments for a pull request (review comments) */ async getPullRequestComments(owner, repo, pullNumber) { const endpoint = `/repos/${owner}/${repo}/pulls/${pullNumber}/comments`; return this.makeRequest(endpoint); } /** * Get a specific pull request */ async getPullRequest(owner, repo, pullNumber) { const endpoint = `/repos/${owner}/${repo}/pulls/${pullNumber}`; return this.makeRequest(endpoint); } /** * List pull requests for a repository */ async listPullRequests(owner, repo, options = {}) { const { state = 'open', sort = 'updated', direction = 'desc', perPage = 30, page = 1 } = options; const params = new URLSearchParams({ state, sort, direction, per_page: perPage.toString(), page: page.toString() }); const endpoint = `/repos/${owner}/${repo}/pulls?${params}`; return this.makeRequest(endpoint); } /** * Add a comment to an issue (including pull requests) */ async addIssueComment(owner, repo, issueNumber, body) { const endpoint = `/repos/${owner}/${repo}/issues/${issueNumber}/comments`; return this.makeRequest(endpoint, 'POST', { body }); } /** * Get a specific comment by ID */ async getComment(owner, repo, commentId) { const endpoint = `/repos/${owner}/${repo}/pulls/comments/${commentId}`; return this.makeRequest(endpoint); } /** * React to a comment (add reaction) */ async addReactionToComment(owner, repo, commentId, reaction) { const endpoint = `/repos/${owner}/${repo}/pulls/comments/${commentId}/reactions`; return this.makeRequest(endpoint, 'POST', { content: reaction }); } /** * Resolve a pull request review conversation * This marks the conversation thread as resolved * * Note: GitHub's REST API doesn't support direct conversation resolution. * This method uses a fallback approach with reactions to indicate resolution. */ async resolveReviewConversation(owner, repo, commentId) { const endpoint = `/repos/${owner}/${repo}/pulls/comments/${commentId}`; // First get the comment to find the conversation ID const comment = await this.makeRequest(endpoint); if (!comment.pull_request_review_id) { throw new Error('Comment is not associated with a review conversation'); } // GitHub API doesn't support direct conversation resolution via REST API // Fallback: Add a reaction to indicate resolution console.warn('GitHub conversation resolution API not available; using reaction fallback'); return await this.addReactionToComment(owner, repo, commentId, '+1'); } /** * Unresolve a pull request review conversation * * Note: GitHub's REST API doesn't support direct conversation resolution. * This method uses a fallback approach with reactions to indicate unresolving. */ async unresolveReviewConversation(owner, repo, commentId) { // GitHub API doesn't support direct conversation resolution via REST API // For unresolving, we can only indicate this through external means console.warn('GitHub conversation resolution API not available for unresolving'); // Fallback: Add a reaction to indicate the conversation needs attention try { return await this.addReactionToComment(owner, repo, commentId, 'eyes'); } catch (error) { throw new Error('Cannot unresolve conversation: GitHub API does not support this operation via REST API'); } } /** * Search for a comment across multiple pull requests * This is a helper method since GitHub doesn't provide direct comment search */ async findCommentInRecentPRs(owner, repo, commentId, maxPRs = 20) { try { // Get recent PRs const prs = await this.listPullRequests(owner, repo, { state: 'all', sort: 'updated', direction: 'desc', perPage: maxPRs }); // Search through each PR for the comment for (const pr of prs) { try { const comments = await this.getPullRequestComments(owner, repo, pr.number); const targetComment = comments.find(comment => comment.id === commentId); if (targetComment) { return { comment: targetComment, pr }; } } catch (error) { // Continue searching other PRs if one fails console.warn(`Failed to get comments for PR #${pr.number}:`, error); continue; } } return null; } catch (error) { throw new Error(`Failed to search for comment ${commentId}: ${error instanceof Error ? error.message : String(error)}`); } } /** * Check if the token has required permissions */ async validateToken() { try { const response = await fetch(`${this.baseUrl}/user`, { headers: { 'Authorization': `Bearer ${this.token}`, 'Accept': 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28' } }); if (!response.ok) { return { valid: false, scopes: [], user: '' }; } const user = await response.json(); const scopes = response.headers.get('X-OAuth-Scopes')?.split(', ') || []; return { valid: true, scopes, user: user.login }; } catch (error) { return { valid: false, scopes: [], user: '' }; } } } //# sourceMappingURL=github-client.js.map