spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

claude-code / plugins / code-review / github-tools / inline-comment.ts
203 lines · 6.0 KBRaw
1#!/usr/bin/env node
2import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4import { z } from "zod";
5import { Octokit } from "@octokit/rest";
6
7const githubToken = process.env.GITHUB_TOKEN;
8
9if (!githubToken) {
10 throw new Error("GITHUB_TOKEN environment variable is required");
11}
12
13// Basic token sanitization to prevent accidental token leakage
14function sanitizeContent(content: string): string {
15 // Redact common GitHub token patterns
16 const patterns = [
17 /ghp_[a-zA-Z0-9]{36}/g, // Personal access tokens
18 /gho_[a-zA-Z0-9]{36}/g, // OAuth tokens
19 /ghs_[a-zA-Z0-9]{36}/g, // Installation tokens
20 /ghr_[a-zA-Z0-9]{36}/g, // Refresh tokens
21 /github_pat_[a-zA-Z0-9_]{82}/g, // Fine-grained personal access tokens
22 ];
23
24 let sanitized = content;
25 for (const pattern of patterns) {
26 sanitized = sanitized.replace(pattern, "[REDACTED_TOKEN]");
27 }
28 return sanitized;
29}
30
31// GitHub Inline Comment MCP Server - Provides inline PR comment functionality
32const server = new McpServer({
33 name: "GitHub Inline Comment Server",
34 version: "0.0.1",
35});
36
37server.tool(
38 "create_inline_comment",
39 "Create an inline comment on a specific line or lines in a PR file. Use this tool to provide code suggestions or feedback directly in the PR diff.",
40 {
41 owner: z.string().describe("Repository owner (e.g., 'anthropics')"),
42 repo: z.string().describe("Repository name (e.g., 'claude-code-action')"),
43 pull_number: z
44 .number()
45 .positive()
46 .describe("Pull request number (e.g., 42)"),
47 path: z
48 .string()
49 .describe("The file path to comment on (e.g., 'src/index.js')"),
50 body: z
51 .string()
52 .describe(
53 "The comment text (supports markdown and GitHub code suggestion blocks). " +
54 "For code suggestions, use: ```suggestion\\nreplacement code\\n```. " +
55 "IMPORTANT: The suggestion block will REPLACE the ENTIRE line range (single line or startLine to line). " +
56 "Ensure the replacement is syntactically complete and valid - it must work as a drop-in replacement for the selected lines."
57 ),
58 line: z
59 .number()
60 .nonnegative()
61 .optional()
62 .describe(
63 "Line number for single-line comments (required if startLine is not provided)"
64 ),
65 startLine: z
66 .number()
67 .nonnegative()
68 .optional()
69 .describe(
70 "Start line for multi-line comments (use with line parameter for the end line)"
71 ),
72 side: z
73 .enum(["LEFT", "RIGHT"])
74 .optional()
75 .default("RIGHT")
76 .describe(
77 "Side of the diff to comment on: LEFT (old code) or RIGHT (new code)"
78 ),
79 commit_id: z
80 .string()
81 .optional()
82 .describe(
83 "Specific commit SHA to comment on (defaults to latest commit)"
84 ),
85 },
86 async ({
87 owner,
88 repo,
89 pull_number,
90 path,
91 body,
92 line,
93 startLine,
94 side,
95 commit_id,
96 }) => {
97 try {
98 const octokit = new Octokit({ auth: githubToken });
99
100 // Sanitize the comment body to remove any potential GitHub tokens
101 const sanitizedBody = sanitizeContent(body);
102
103 // Validate that either line or both startLine and line are provided
104 if (!line && !startLine) {
105 throw new Error(
106 "Either 'line' for single-line comments or both 'startLine' and 'line' for multi-line comments must be provided"
107 );
108 }
109
110 // If only line is provided, it's a single-line comment
111 // If both startLine and line are provided, it's a multi-line comment
112 const isSingleLine = !startLine;
113
114 const pr = await octokit.rest.pulls.get({
115 owner,
116 repo,
117 pull_number,
118 });
119
120 const params: Parameters<
121 typeof octokit.rest.pulls.createReviewComment
122 >[0] = {
123 owner,
124 repo,
125 pull_number,
126 body: sanitizedBody,
127 path,
128 side: side || "RIGHT",
129 commit_id: commit_id || pr.data.head.sha,
130 };
131
132 if (isSingleLine) {
133 // Single-line comment
134 params.line = line;
135 } else {
136 // Multi-line comment
137 params.start_line = startLine;
138 params.start_side = side || "RIGHT";
139 params.line = line;
140 }
141
142 const result = await octokit.rest.pulls.createReviewComment(params);
143
144 return {
145 content: [
146 {
147 type: "text",
148 text: JSON.stringify(
149 {
150 success: true,
151 comment_id: result.data.id,
152 html_url: result.data.html_url,
153 path: result.data.path,
154 line: result.data.line || result.data.original_line,
155 message: `Inline comment created successfully on ${path}${
156 isSingleLine
157 ? ` at line ${line}`
158 : ` from line ${startLine} to ${line}`
159 }`,
160 },
161 null,
162 2
163 ),
164 },
165 ],
166 };
167 } catch (error) {
168 const errorMessage =
169 error instanceof Error ? error.message : String(error);
170
171 // Provide more helpful error messages for common issues
172 let helpMessage = "";
173 if (errorMessage.includes("Validation Failed")) {
174 helpMessage =
175 "\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.";
176 } else if (errorMessage.includes("Not Found")) {
177 helpMessage =
178 "\n\nThis usually means the PR number, repository, or file path is incorrect.";
179 }
180
181 return {
182 content: [
183 {
184 type: "text",
185 text: `Error creating inline comment: ${errorMessage}${helpMessage}`,
186 },
187 ],
188 error: errorMessage,
189 isError: true,
190 };
191 }
192 }
193);
194
195async function runServer() {
196 const transport = new StdioServerTransport();
197 await server.connect(transport);
198 process.on("exit", () => {
199 server.close();
200 });
201}
202
203runServer().catch(console.error);