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
JavaScript
/**
* 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