Skip to content

Commit 15db2b3

Browse files
ashwin-antclaude
andauthored
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>
1 parent 188d526 commit 15db2b3

File tree

3 files changed

+201
-64
lines changed

3 files changed

+201
-64
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#!/usr/bin/env node
2+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4+
import { z } from "zod";
5+
import { createOctokit } from "../github/api/client";
6+
7+
// Get repository and PR information from environment variables
8+
const REPO_OWNER = process.env.REPO_OWNER;
9+
const REPO_NAME = process.env.REPO_NAME;
10+
const PR_NUMBER = process.env.PR_NUMBER;
11+
12+
if (!REPO_OWNER || !REPO_NAME || !PR_NUMBER) {
13+
console.error(
14+
"Error: REPO_OWNER, REPO_NAME, and PR_NUMBER environment variables are required",
15+
);
16+
process.exit(1);
17+
}
18+
19+
// GitHub Inline Comment MCP Server - Provides inline PR comment functionality
20+
// Provides an inline comment tool without exposing full PR review capabilities, so that
21+
// Claude can't accidentally approve a PR
22+
const server = new McpServer({
23+
name: "GitHub Inline Comment Server",
24+
version: "0.0.1",
25+
});
26+
27+
server.tool(
28+
"create_inline_comment",
29+
"Create an inline comment on a specific line or lines in a PR file",
30+
{
31+
path: z
32+
.string()
33+
.describe("The file path to comment on (e.g., 'src/index.js')"),
34+
body: z
35+
.string()
36+
.describe(
37+
"The comment text (supports markdown and GitHub code suggestion blocks). " +
38+
"For code suggestions, use: ```suggestion\\nreplacement code\\n```. " +
39+
"IMPORTANT: The suggestion block will REPLACE the ENTIRE line range (single line or startLine to line). " +
40+
"Ensure the replacement is syntactically complete and valid - it must work as a drop-in replacement for the selected lines.",
41+
),
42+
line: z
43+
.number()
44+
.optional()
45+
.describe(
46+
"Line number for single-line comments (required if startLine is not provided)",
47+
),
48+
startLine: z
49+
.number()
50+
.optional()
51+
.describe(
52+
"Start line for multi-line comments (use with line parameter for the end line)",
53+
),
54+
side: z
55+
.enum(["LEFT", "RIGHT"])
56+
.optional()
57+
.default("RIGHT")
58+
.describe(
59+
"Side of the diff to comment on: LEFT (old code) or RIGHT (new code)",
60+
),
61+
commit_id: z
62+
.string()
63+
.optional()
64+
.describe(
65+
"Specific commit SHA to comment on (defaults to latest commit)",
66+
),
67+
},
68+
async ({ path, body, line, startLine, side, commit_id }) => {
69+
try {
70+
const githubToken = process.env.GITHUB_TOKEN;
71+
72+
if (!githubToken) {
73+
throw new Error("GITHUB_TOKEN environment variable is required");
74+
}
75+
76+
const owner = REPO_OWNER;
77+
const repo = REPO_NAME;
78+
const pull_number = parseInt(PR_NUMBER, 10);
79+
80+
const octokit = createOctokit(githubToken).rest;
81+
82+
// Validate that either line or both startLine and line are provided
83+
if (!line && !startLine) {
84+
throw new Error(
85+
"Either 'line' for single-line comments or both 'startLine' and 'line' for multi-line comments must be provided",
86+
);
87+
}
88+
89+
// If only line is provided, it's a single-line comment
90+
// If both startLine and line are provided, it's a multi-line comment
91+
const isSingleLine = !startLine;
92+
93+
const pr = await octokit.pulls.get({
94+
owner,
95+
repo,
96+
pull_number,
97+
});
98+
99+
const params: Parameters<
100+
typeof octokit.rest.pulls.createReviewComment
101+
>[0] = {
102+
owner,
103+
repo,
104+
pull_number,
105+
body,
106+
path,
107+
side: side || "RIGHT",
108+
commit_id: commit_id || pr.data.head.sha,
109+
};
110+
111+
if (isSingleLine) {
112+
// Single-line comment
113+
params.line = line;
114+
} else {
115+
// Multi-line comment
116+
params.start_line = startLine;
117+
params.start_side = side || "RIGHT";
118+
params.line = line;
119+
}
120+
121+
const result = await octokit.rest.pulls.createReviewComment(params);
122+
123+
return {
124+
content: [
125+
{
126+
type: "text",
127+
text: JSON.stringify(
128+
{
129+
success: true,
130+
comment_id: result.data.id,
131+
html_url: result.data.html_url,
132+
path: result.data.path,
133+
line: result.data.line || result.data.original_line,
134+
message: `Inline comment created successfully on ${path}${isSingleLine ? ` at line ${line}` : ` from line ${startLine} to ${line}`}`,
135+
},
136+
null,
137+
2,
138+
),
139+
},
140+
],
141+
};
142+
} catch (error) {
143+
const errorMessage =
144+
error instanceof Error ? error.message : String(error);
145+
146+
// Provide more helpful error messages for common issues
147+
let helpMessage = "";
148+
if (errorMessage.includes("Validation Failed")) {
149+
helpMessage =
150+
"\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.";
151+
} else if (errorMessage.includes("Not Found")) {
152+
helpMessage =
153+
"\n\nThis usually means the PR number, repository, or file path is incorrect.";
154+
}
155+
156+
return {
157+
content: [
158+
{
159+
type: "text",
160+
text: `Error creating inline comment: ${errorMessage}${helpMessage}`,
161+
},
162+
],
163+
error: errorMessage,
164+
isError: true,
165+
};
166+
}
167+
},
168+
);
169+
170+
async function runServer() {
171+
const transport = new StdioServerTransport();
172+
await server.connect(transport);
173+
process.on("exit", () => {
174+
server.close();
175+
});
176+
}
177+
178+
runServer().catch(console.error);

src/mcp/install-mcp-server.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,24 @@ export async function prepareMcpConfig(
111111
};
112112
}
113113

114+
// Include inline comment server for experimental review mode
115+
if (context.inputs.mode === "experimental-review" && context.isPR) {
116+
baseMcpConfig.mcpServers.github_inline_comment = {
117+
command: "bun",
118+
args: [
119+
"run",
120+
`${process.env.GITHUB_ACTION_PATH}/src/mcp/github-inline-comment-server.ts`,
121+
],
122+
env: {
123+
GITHUB_TOKEN: githubToken,
124+
REPO_OWNER: owner,
125+
REPO_NAME: repo,
126+
PR_NUMBER: context.entityNumber?.toString() || "",
127+
GITHUB_API_URL: GITHUB_API_URL,
128+
},
129+
};
130+
}
131+
114132
// Only add CI server if we have actions:read permission and we're in a PR context
115133
const hasActionsReadPermission =
116134
context.inputs.additionalPermissions.get("actions") === "read";

src/modes/review/index.ts

Lines changed: 5 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -60,20 +60,8 @@ export const reviewMode: Mode = {
6060

6161
getAllowedTools() {
6262
return [
63-
// Context tools - to know who the current user is
64-
"mcp__github__get_me",
65-
// Core review tools
66-
"mcp__github__create_pending_pull_request_review",
67-
"mcp__github__add_comment_to_pending_review",
68-
"mcp__github__submit_pending_pull_request_review",
69-
"mcp__github__delete_pending_pull_request_review",
70-
"mcp__github__create_and_submit_pull_request_review",
71-
// Comment tools
72-
"mcp__github__add_issue_comment",
73-
// PR information tools
74-
"mcp__github__get_pull_request",
75-
"mcp__github__get_pull_request_reviews",
76-
"mcp__github__get_pull_request_status",
63+
"Bash(gh issue comment:*)",
64+
"mcp__github_inline_comment__create_inline_comment",
7765
];
7866
},
7967

@@ -163,17 +151,13 @@ REVIEW MODE WORKFLOW:
163151
164152
1. First, understand the PR context:
165153
- You are reviewing PR #${eventData.isPR && eventData.prNumber ? eventData.prNumber : "[PR number]"} in ${context.repository}
166-
- Use mcp__github__get_pull_request to get PR metadata
167154
- Use the Read, Grep, and Glob tools to examine the modified files directly from disk
168155
- This provides the full context and latest state of the code
169156
- Look at the changed_files section above to see which files were modified
170157
171-
2. Create a pending review:
172-
- Use mcp__github__create_pending_pull_request_review to start your review
173-
- This allows you to batch comments before submitting
174-
175-
3. Add inline comments:
176-
- Use mcp__github__add_comment_to_pending_review for each issue or suggestion
158+
2. Add comments:
159+
- use Bash(gh issue comment:*) to add top-level comments
160+
- Use mcp__github_inline_comment__create_inline_comment to add inline comments (prefer this where possible)
177161
- Parameters:
178162
* path: The file path (e.g., "src/index.js")
179163
* line: Line number for single-line comments
@@ -182,49 +166,6 @@ REVIEW MODE WORKFLOW:
182166
* subjectType: "line" for line-level comments
183167
* body: Your comment text
184168
185-
- When to use multi-line comments:
186-
* When replacing multiple consecutive lines
187-
* When the fix requires changes across several lines
188-
* Example: To replace lines 19-20, use startLine: 19, line: 20
189-
190-
- For code suggestions, use this EXACT format in the body:
191-
\`\`\`suggestion
192-
corrected code here
193-
\`\`\`
194-
195-
CRITICAL: GitHub suggestion blocks must ONLY contain the replacement for the specific line(s) being commented on:
196-
- For single-line comments: Replace ONLY that line
197-
- For multi-line comments: Replace ONLY the lines in the range
198-
- Do NOT include surrounding context or function signatures
199-
- Do NOT suggest changes that span beyond the commented lines
200-
201-
Example for line 19 \`var name = user.name;\`:
202-
WRONG:
203-
\\\`\\\`\\\`suggestion
204-
function processUser(user) {
205-
if (!user) throw new Error('Invalid user');
206-
const name = user.name;
207-
\\\`\\\`\\\`
208-
209-
CORRECT:
210-
\\\`\\\`\\\`suggestion
211-
const name = user.name;
212-
\\\`\\\`\\\`
213-
214-
For validation suggestions, comment on the function declaration line or create separate comments for each concern.
215-
216-
4. Submit your review:
217-
- Use mcp__github__submit_pending_pull_request_review
218-
- Parameters:
219-
* event: "COMMENT" (general feedback), "REQUEST_CHANGES" (issues found), or "APPROVE" (if appropriate)
220-
* body: Write a comprehensive review summary that includes:
221-
- Overview of what was reviewed (files, scope, focus areas)
222-
- Summary of all issues found (with counts by severity if applicable)
223-
- Key recommendations and action items
224-
- Highlights of good practices observed
225-
- Overall assessment and recommendation
226-
- The body should be detailed and informative since it's the main review content
227-
- Structure the body with clear sections using markdown headers
228169
229170
REVIEW GUIDELINES:
230171

0 commit comments

Comments
 (0)