diff --git a/docs/inline-comments-batching.md b/docs/inline-comments-batching.md new file mode 100644 index 000000000..772e503a9 --- /dev/null +++ b/docs/inline-comments-batching.md @@ -0,0 +1,255 @@ +# Batching Inline Comments + +When reviewing pull requests, you often need to post multiple inline comments on different lines or files. Instead of making separate API calls for each comment, use the batch tool `create_inline_comments_batch` to post all comments in a single API call. This is more efficient, faster, and reduces GitHub API rate limit usage. + +## When to Use + +- **Posting 2+ inline comments** - Use batch tool for efficiency +- **Posting 1 inline comment** - Use single comment tool +- **Including a summary comment** - Batch tool supports both inline comments and review summary in one call +- **Automated PR reviews** - Perfect for workflows that review multiple files + +--- + +## Tool Names + +- **Single comment**: `mcp__github_inline_comment__create_inline_comment` +- **Batch comments**: `mcp__github_inline_comment__create_inline_comments_batch` + +--- + +## Features + +- ✅ Post multiple inline comments in a single API call +- ✅ Optionally include a review summary comment in the same call +- ✅ More efficient than multiple separate API calls +- ✅ Reduces GitHub API rate limit usage +- ✅ Faster execution time + +--- + +## Basic Usage + +### Simple Batch Example + +```yaml +name: PR Review with Batch Comments + +on: + pull_request: + types: [opened, synchronize] + +jobs: + review: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v5 + + - uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + Review this PR and post all inline comments together. + Use the batch tool to post all comments in a single API call. + + claude_args: | + --allowedTools "mcp__github_inline_comment__create_inline_comments_batch,Read,Grep" +``` + +**Key Configuration:** + +- Include `mcp__github_inline_comment__create_inline_comments_batch` in `allowedTools` +- Prompt should mention "post all comments together" or "use batch tool" +- Works with any PR review workflow + +**Expected Output:** All inline comments appear in a single review, posted efficiently in one API call. + +--- + +## Advanced Examples + +### Example 1: Explicit Batch Instruction + +```yaml +prompt: | + Review this PR and provide inline comments for all issues you find. + + IMPORTANT: When posting multiple inline comments, use the + `mcp__github_inline_comment__create_inline_comments_batch` tool to post + them all in a single API call. Only use the single comment tool if you're + posting just one comment. + +claude_args: | + --allowedTools "mcp__github_inline_comment__create_inline_comments_batch,mcp__github_inline_comment__create_inline_comment" +``` + +### Example 2: With Review Summary Comment + +You can include both inline comments AND a summary comment in a single API call: + +```yaml +prompt: | + Review this PR and provide inline comments for all issues you find. + + IMPORTANT: Use create_inline_comments_batch to post all comments together. + Also include a summary comment highlighting the most critical issues. + + The batch tool supports both inline comments and a review summary in one call. + +claude_args: | + --allowedTools "mcp__github_inline_comment__create_inline_comments_batch" +``` + +**Benefits:** + +- All inline comments (on specific lines) +- A summary comment (top-level review comment) +- All in a single API call! + +### Example 3: Comprehensive Code Review + +```yaml +prompt: | + Perform a thorough code review of this PR. For each issue you find: + + 1. Identify the file path and line number + 2. Write a clear, actionable comment + 3. Collect all comments and post them together using + `create_inline_comments_batch` in a single call + + Review checklist: + - [ ] Code quality and best practices + - [ ] Security vulnerabilities + - [ ] Performance issues + - [ ] Error handling + - [ ] Documentation + + After reviewing, batch all inline comments together for efficiency. + +claude_args: | + --allowedTools "mcp__github_inline_comment__create_inline_comments_batch,mcp__github_inline_comment__create_inline_comment,Bash(gh pr view:*),Bash(gh pr diff:*)" +``` + +--- + +## Comment Object Structure + +Each comment in the batch array should have: + +```typescript +{ + path: string, // File path (e.g., "src/index.js") + body: string, // Comment text (supports markdown) + line?: number, // Line number for single-line comments + startLine?: number, // Start line for multi-line comments + side?: "LEFT" | "RIGHT" // Default: "RIGHT" +} +``` + +### Single-Line Comment Example + +```json +{ + "path": "src/utils.ts", + "line": 42, + "body": "Consider extracting this into a helper function for reusability" +} +``` + +### Multi-Line Comment Example + +```json +{ + "path": "src/api.ts", + "startLine": 10, + "line": 15, + "body": "This entire block could be simplified using async/await" +} +``` + +### Code Suggestion Example + +```json +{ + "path": "src/validator.ts", + "line": 28, + "body": "```suggestion\nif (!value || value.trim() === '') {\n throw new Error('Value cannot be empty');\n}\n```" +} +``` + +--- + +## Complete Workflow Example + +```yaml +name: PR Review with Batch Comments + +on: + pull_request: + types: [opened, synchronize] + +jobs: + review: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v5 + + - name: Review PR with Batch Comments + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + Review this PR thoroughly. When you find multiple issues: + + 1. Collect all comments with their file paths and line numbers + 2. Use `create_inline_comments_batch` to post them all at once + 3. Include a summary comment highlighting critical issues + 4. This is more efficient than multiple API calls + + Focus on: + - Code quality and maintainability + - Security best practices + - Performance optimizations + - Error handling completeness + + claude_args: | + --allowedTools "mcp__github_inline_comment__create_inline_comments_batch,mcp__github_inline_comment__create_inline_comment,Bash(gh pr view:*),Bash(gh pr diff:*),Read,Grep" +``` + +--- + +## Best Practices + +1. **Always batch when posting multiple comments** - Reduces API calls and improves performance +2. **Use single tool for one comment** - Simpler and clearer for single comments +3. **Include summary comment** - Use the `review_body` parameter for top-level feedback +4. **Group by file** - Organize comments by file path for better readability +5. **Be specific** - Include exact line numbers and clear, actionable feedback +6. **Use code suggestions** - For fixable issues, use GitHub's suggestion syntax + +--- + +## Tool Selection Strategy + +The LLM will automatically choose the right tool based on: + +- **Number of comments**: Batch for 2+, single for 1 +- **Tool availability**: If both are allowed, batch is preferred +- **Prompt instructions**: Explicit instructions guide the choice + +--- + +## Troubleshooting + +If the LLM isn't using the batch tool: + +1. **Be explicit**: Mention "use batch tool" or "post all comments together" +2. **Show format**: Provide an example of the array structure +3. **Emphasize efficiency**: Explain why batching is better +4. **Check tools**: Ensure `create_inline_comments_batch` is in `allowedTools` diff --git a/src/mcp/github-inline-comment-server.ts b/src/mcp/github-inline-comment-server.ts index 703cda2e0..aff374b6f 100644 --- a/src/mcp/github-inline-comment-server.ts +++ b/src/mcp/github-inline-comment-server.ts @@ -173,6 +173,204 @@ server.tool( }, ); +// Batch tool for creating multiple inline comments in a single API call +server.tool( + "create_inline_comments_batch", + "Create multiple inline comments in a single API call. Optionally include a review summary comment.", + { + comments: z + .array( + z.object({ + 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() + .nonnegative() + .optional() + .describe( + "Line number for single-line comments (required if startLine is not provided)", + ), + startLine: z + .number() + .nonnegative() + .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)", + ), + }), + ) + .min(1) + .describe( + "Array of comment objects. Each object requires 'path', 'body', and either 'line' (single-line) or both 'startLine' and 'line' (multi-line).", + ), + review_body: z + .string() + .optional() + .describe( + "Optional summary comment for the review. This appears as a top-level review comment alongside the inline comments.", + ), + commit_id: z + .string() + .optional() + .describe( + "Commit SHA to comment on (defaults to latest commit). All comments use the same commit.", + ), + }, + async ({ comments, review_body, 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; + + // Get PR to get the head SHA if commit_id is not provided + const pr = await octokit.pulls.get({ + owner, + repo, + pull_number, + }); + + const finalCommitId = commit_id || pr.data.head.sha; + + // Prepare comments array for the review API + const reviewComments: Array<{ + path: string; + body: string; + line?: number; + start_line?: number; + start_side?: "LEFT" | "RIGHT"; + side?: "LEFT" | "RIGHT"; + }> = []; + + for (const comment of comments) { + // Validate that either line or both startLine and line are provided + if (!comment.line && !comment.startLine) { + throw new Error( + `Comment on ${comment.path} is missing required 'line' or 'startLine' parameter`, + ); + } + + // Sanitize the comment body + const sanitizedBody = sanitizeContent(comment.body); + + const isSingleLine = !comment.startLine; + const side = comment.side || "RIGHT"; + + if (isSingleLine) { + // Single-line comment + reviewComments.push({ + path: comment.path, + body: sanitizedBody, + line: comment.line, + side: side, + }); + } else { + // Multi-line comment + reviewComments.push({ + path: comment.path, + body: sanitizedBody, + start_line: comment.startLine, + start_side: side, + line: comment.line, + side: side, + }); + } + } + + // Prepare review body if provided + const sanitizedReviewBody = review_body + ? sanitizeContent(review_body) + : undefined; + + // Create review with all comments in a single API call + const result = await octokit.rest.pulls.createReview({ + owner, + repo, + pull_number, + commit_id: finalCommitId, + event: "COMMENT", // Just comment, don't approve or request changes + body: sanitizedReviewBody, // Optional review summary + comments: reviewComments, + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + success: true, + review_id: result.data.id, + review_html_url: result.data.html_url, + comments_submitted: reviewComments.length, + review_body_included: !!sanitizedReviewBody, + message: `Successfully created review with ${reviewComments.length} inline comment(s)${sanitizedReviewBody ? " and a summary comment" : ""}.`, + comment_details: reviewComments.map((c, idx) => ({ + index: idx + 1, + path: c.path, + line: c.line || c.start_line, + body_preview: + c.body.substring(0, 100) + + (c.body.length > 100 ? "..." : ""), + })), + }, + 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 one or more line numbers don't exist in the diff or file paths are 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 one of the file paths is incorrect."; + } + + return { + content: [ + { + type: "text", + text: `Error creating batch inline comments: ${errorMessage}${helpMessage}`, + }, + ], + error: errorMessage, + isError: true, + }; + } + }, +); + async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/test/install-mcp-server.test.ts b/test/install-mcp-server.test.ts index 9d628504d..f3ff33418 100644 --- a/test/install-mcp-server.test.ts +++ b/test/install-mcp-server.test.ts @@ -164,6 +164,29 @@ describe("prepareMcpConfig", () => { expect(parsed.mcpServers.github_inline_comment.env.PR_NUMBER).toBe("456"); }); + test("should include inline comment server when batch tool is allowed", async () => { + const result = await prepareMcpConfig({ + githubToken: "test-token", + owner: "test-owner", + repo: "test-repo", + branch: "test-branch", + baseBranch: "main", + allowedTools: [ + "mcp__github_inline_comment__create_inline_comments_batch", + ], + mode: "tag", + context: mockPRContext, + }); + + const parsed = JSON.parse(result); + expect(parsed.mcpServers).toBeDefined(); + expect(parsed.mcpServers.github_inline_comment).toBeDefined(); + expect(parsed.mcpServers.github_inline_comment.env.GITHUB_TOKEN).toBe( + "test-token", + ); + expect(parsed.mcpServers.github_inline_comment.env.PR_NUMBER).toBe("456"); + }); + test("should include comment server when no GitHub tools are allowed and signing disabled", async () => { const result = await prepareMcpConfig({ githubToken: "test-token",