From 15db2b3c79c0681556c056e9bc3f61fd3fc0347d Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Wed, 6 Aug 2025 08:21:29 -0700 Subject: [PATCH] feat: add inline comment MCP server for experimental review mode (#414) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add inline comment MCP server for experimental review mode - Create standalone inline PR comments without review workflow - Support single-line and multi-line comments - Auto-install server when in experimental review mode - Uses octokit.rest.pulls.createReviewComment() directly * docs: clarify GitHub code suggestion syntax in inline comment server Add clear documentation that suggestion blocks replace the entire selected line range and must be syntactically complete drop-in replacements. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- src/mcp/github-inline-comment-server.ts | 178 ++++++++++++++++++++++++ src/mcp/install-mcp-server.ts | 18 +++ src/modes/review/index.ts | 69 +-------- 3 files changed, 201 insertions(+), 64 deletions(-) create mode 100644 src/mcp/github-inline-comment-server.ts diff --git a/src/mcp/github-inline-comment-server.ts b/src/mcp/github-inline-comment-server.ts new file mode 100644 index 0000000..28a8658 --- /dev/null +++ b/src/mcp/github-inline-comment-server.ts @@ -0,0 +1,178 @@ +#!/usr/bin/env node +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import { createOctokit } from "../github/api/client"; + +// Get repository and PR information from environment variables +const REPO_OWNER = process.env.REPO_OWNER; +const REPO_NAME = process.env.REPO_NAME; +const PR_NUMBER = process.env.PR_NUMBER; + +if (!REPO_OWNER || !REPO_NAME || !PR_NUMBER) { + console.error( + "Error: REPO_OWNER, REPO_NAME, and PR_NUMBER environment variables are required", + ); + process.exit(1); +} + +// GitHub Inline Comment MCP Server - Provides inline PR comment functionality +// Provides an inline comment tool without exposing full PR review capabilities, so that +// Claude can't accidentally approve a PR +const server = new McpServer({ + name: "GitHub Inline Comment Server", + version: "0.0.1", +}); + +server.tool( + "create_inline_comment", + "Create an inline comment on a specific line or lines in a PR file", + { + path: z + .string() + .describe("The file path to comment on (e.g., 'src/index.js')"), + body: z + .string() + .describe( + "The comment text (supports markdown and GitHub code suggestion blocks). " + + "For code suggestions, use: ```suggestion\\nreplacement code\\n```. " + + "IMPORTANT: The suggestion block will REPLACE the ENTIRE line range (single line or startLine to line). " + + "Ensure the replacement is syntactically complete and valid - it must work as a drop-in replacement for the selected lines.", + ), + line: z + .number() + .optional() + .describe( + "Line number for single-line comments (required if startLine is not provided)", + ), + startLine: z + .number() + .optional() + .describe( + "Start line for multi-line comments (use with line parameter for the end line)", + ), + side: z + .enum(["LEFT", "RIGHT"]) + .optional() + .default("RIGHT") + .describe( + "Side of the diff to comment on: LEFT (old code) or RIGHT (new code)", + ), + commit_id: z + .string() + .optional() + .describe( + "Specific commit SHA to comment on (defaults to latest commit)", + ), + }, + async ({ path, body, line, startLine, side, commit_id }) => { + try { + const githubToken = process.env.GITHUB_TOKEN; + + if (!githubToken) { + throw new Error("GITHUB_TOKEN environment variable is required"); + } + + const owner = REPO_OWNER; + const repo = REPO_NAME; + const pull_number = parseInt(PR_NUMBER, 10); + + const octokit = createOctokit(githubToken).rest; + + // Validate that either line or both startLine and line are provided + if (!line && !startLine) { + throw new Error( + "Either 'line' for single-line comments or both 'startLine' and 'line' for multi-line comments must be provided", + ); + } + + // If only line is provided, it's a single-line comment + // If both startLine and line are provided, it's a multi-line comment + const isSingleLine = !startLine; + + const pr = await octokit.pulls.get({ + owner, + repo, + pull_number, + }); + + const params: Parameters< + typeof octokit.rest.pulls.createReviewComment + >[0] = { + owner, + repo, + pull_number, + body, + path, + side: side || "RIGHT", + commit_id: commit_id || pr.data.head.sha, + }; + + if (isSingleLine) { + // Single-line comment + params.line = line; + } else { + // Multi-line comment + params.start_line = startLine; + params.start_side = side || "RIGHT"; + params.line = line; + } + + const result = await octokit.rest.pulls.createReviewComment(params); + + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + success: true, + comment_id: result.data.id, + html_url: result.data.html_url, + path: result.data.path, + line: result.data.line || result.data.original_line, + message: `Inline comment created successfully on ${path}${isSingleLine ? ` at line ${line}` : ` from line ${startLine} to ${line}`}`, + }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + + // Provide more helpful error messages for common issues + let helpMessage = ""; + if (errorMessage.includes("Validation Failed")) { + helpMessage = + "\n\nThis usually means the line number doesn't exist in the diff or the file path is incorrect. Make sure you're commenting on lines that are part of the PR's changes."; + } else if (errorMessage.includes("Not Found")) { + helpMessage = + "\n\nThis usually means the PR number, repository, or file path is incorrect."; + } + + return { + content: [ + { + type: "text", + text: `Error creating inline comment: ${errorMessage}${helpMessage}`, + }, + ], + error: errorMessage, + isError: true, + }; + } + }, +); + +async function runServer() { + const transport = new StdioServerTransport(); + await server.connect(transport); + process.on("exit", () => { + server.close(); + }); +} + +runServer().catch(console.error); diff --git a/src/mcp/install-mcp-server.ts b/src/mcp/install-mcp-server.ts index 61b11d6..9a87f12 100644 --- a/src/mcp/install-mcp-server.ts +++ b/src/mcp/install-mcp-server.ts @@ -111,6 +111,24 @@ export async function prepareMcpConfig( }; } + // Include inline comment server for experimental review mode + if (context.inputs.mode === "experimental-review" && context.isPR) { + baseMcpConfig.mcpServers.github_inline_comment = { + command: "bun", + args: [ + "run", + `${process.env.GITHUB_ACTION_PATH}/src/mcp/github-inline-comment-server.ts`, + ], + env: { + GITHUB_TOKEN: githubToken, + REPO_OWNER: owner, + REPO_NAME: repo, + PR_NUMBER: context.entityNumber?.toString() || "", + GITHUB_API_URL: GITHUB_API_URL, + }, + }; + } + // Only add CI server if we have actions:read permission and we're in a PR context const hasActionsReadPermission = context.inputs.additionalPermissions.get("actions") === "read"; diff --git a/src/modes/review/index.ts b/src/modes/review/index.ts index 4213c1c..e53f8f8 100644 --- a/src/modes/review/index.ts +++ b/src/modes/review/index.ts @@ -60,20 +60,8 @@ export const reviewMode: Mode = { getAllowedTools() { return [ - // Context tools - to know who the current user is - "mcp__github__get_me", - // Core review tools - "mcp__github__create_pending_pull_request_review", - "mcp__github__add_comment_to_pending_review", - "mcp__github__submit_pending_pull_request_review", - "mcp__github__delete_pending_pull_request_review", - "mcp__github__create_and_submit_pull_request_review", - // Comment tools - "mcp__github__add_issue_comment", - // PR information tools - "mcp__github__get_pull_request", - "mcp__github__get_pull_request_reviews", - "mcp__github__get_pull_request_status", + "Bash(gh issue comment:*)", + "mcp__github_inline_comment__create_inline_comment", ]; }, @@ -163,17 +151,13 @@ REVIEW MODE WORKFLOW: 1. First, understand the PR context: - You are reviewing PR #${eventData.isPR && eventData.prNumber ? eventData.prNumber : "[PR number]"} in ${context.repository} - - Use mcp__github__get_pull_request to get PR metadata - Use the Read, Grep, and Glob tools to examine the modified files directly from disk - This provides the full context and latest state of the code - Look at the changed_files section above to see which files were modified -2. Create a pending review: - - Use mcp__github__create_pending_pull_request_review to start your review - - This allows you to batch comments before submitting - -3. Add inline comments: - - Use mcp__github__add_comment_to_pending_review for each issue or suggestion +2. Add comments: + - use Bash(gh issue comment:*) to add top-level comments + - Use mcp__github_inline_comment__create_inline_comment to add inline comments (prefer this where possible) - Parameters: * path: The file path (e.g., "src/index.js") * line: Line number for single-line comments @@ -182,49 +166,6 @@ REVIEW MODE WORKFLOW: * subjectType: "line" for line-level comments * body: Your comment text - - When to use multi-line comments: - * When replacing multiple consecutive lines - * When the fix requires changes across several lines - * Example: To replace lines 19-20, use startLine: 19, line: 20 - - - For code suggestions, use this EXACT format in the body: - \`\`\`suggestion - corrected code here - \`\`\` - - CRITICAL: GitHub suggestion blocks must ONLY contain the replacement for the specific line(s) being commented on: - - For single-line comments: Replace ONLY that line - - For multi-line comments: Replace ONLY the lines in the range - - Do NOT include surrounding context or function signatures - - Do NOT suggest changes that span beyond the commented lines - - Example for line 19 \`var name = user.name;\`: - WRONG: - \\\`\\\`\\\`suggestion - function processUser(user) { - if (!user) throw new Error('Invalid user'); - const name = user.name; - \\\`\\\`\\\` - - CORRECT: - \\\`\\\`\\\`suggestion - const name = user.name; - \\\`\\\`\\\` - - For validation suggestions, comment on the function declaration line or create separate comments for each concern. - -4. Submit your review: - - Use mcp__github__submit_pending_pull_request_review - - Parameters: - * event: "COMMENT" (general feedback), "REQUEST_CHANGES" (issues found), or "APPROVE" (if appropriate) - * body: Write a comprehensive review summary that includes: - - Overview of what was reviewed (files, scope, focus areas) - - Summary of all issues found (with counts by severity if applicable) - - Key recommendations and action items - - Highlights of good practices observed - - Overall assessment and recommendation - - The body should be detailed and informative since it's the main review content - - Structure the body with clear sections using markdown headers REVIEW GUIDELINES: