mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 22:44:13 +08:00
feat: add inline comment MCP server for experimental review mode (#414)
* 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 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
178
src/mcp/github-inline-comment-server.ts
Normal file
178
src/mcp/github-inline-comment-server.ts
Normal file
@@ -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);
|
||||||
@@ -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
|
// Only add CI server if we have actions:read permission and we're in a PR context
|
||||||
const hasActionsReadPermission =
|
const hasActionsReadPermission =
|
||||||
context.inputs.additionalPermissions.get("actions") === "read";
|
context.inputs.additionalPermissions.get("actions") === "read";
|
||||||
|
|||||||
@@ -60,20 +60,8 @@ export const reviewMode: Mode = {
|
|||||||
|
|
||||||
getAllowedTools() {
|
getAllowedTools() {
|
||||||
return [
|
return [
|
||||||
// Context tools - to know who the current user is
|
"Bash(gh issue comment:*)",
|
||||||
"mcp__github__get_me",
|
"mcp__github_inline_comment__create_inline_comment",
|
||||||
// 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",
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -163,17 +151,13 @@ REVIEW MODE WORKFLOW:
|
|||||||
|
|
||||||
1. First, understand the PR context:
|
1. First, understand the PR context:
|
||||||
- You are reviewing PR #${eventData.isPR && eventData.prNumber ? eventData.prNumber : "[PR number]"} in ${context.repository}
|
- 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
|
- 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
|
- This provides the full context and latest state of the code
|
||||||
- Look at the changed_files section above to see which files were modified
|
- Look at the changed_files section above to see which files were modified
|
||||||
|
|
||||||
2. Create a pending review:
|
2. Add comments:
|
||||||
- Use mcp__github__create_pending_pull_request_review to start your review
|
- use Bash(gh issue comment:*) to add top-level comments
|
||||||
- This allows you to batch comments before submitting
|
- Use mcp__github_inline_comment__create_inline_comment to add inline comments (prefer this where possible)
|
||||||
|
|
||||||
3. Add inline comments:
|
|
||||||
- Use mcp__github__add_comment_to_pending_review for each issue or suggestion
|
|
||||||
- Parameters:
|
- Parameters:
|
||||||
* path: The file path (e.g., "src/index.js")
|
* path: The file path (e.g., "src/index.js")
|
||||||
* line: Line number for single-line comments
|
* line: Line number for single-line comments
|
||||||
@@ -182,49 +166,6 @@ REVIEW MODE WORKFLOW:
|
|||||||
* subjectType: "line" for line-level comments
|
* subjectType: "line" for line-level comments
|
||||||
* body: Your comment text
|
* 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:
|
REVIEW GUIDELINES:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user