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
| 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 { Octokit } from "@octokit/rest"; |
| 6 | |
| 7 | const githubToken = process.env.GITHUB_TOKEN; |
| 8 | |
| 9 | if (!githubToken) { |
| 10 | throw new Error("GITHUB_TOKEN environment variable is required"); |
| 11 | } |
| 12 | |
| 13 | // Basic token sanitization to prevent accidental token leakage |
| 14 | function 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 |
| 32 | const server = new McpServer({ |
| 33 | name: "GitHub Inline Comment Server", |
| 34 | version: "0.0.1", |
| 35 | }); |
| 36 | |
| 37 | server.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 | |
| 195 | async function runServer() { |
| 196 | const transport = new StdioServerTransport(); |
| 197 | await server.connect(transport); |
| 198 | process.on("exit", () => { |
| 199 | server.close(); |
| 200 | }); |
| 201 | } |
| 202 | |
| 203 | runServer().catch(console.error); |