feat: add unified update_claude_comment tool

- Add new update_claude_comment tool that automatically handles both issue and PR comments
- Remove individual update_issue_comment and update_pull_request_comment tools
- Pass CLAUDE_COMMENT_ID, GITHUB_EVENT_NAME, and IS_PR to MCP server environment
- Use Octokit instead of raw fetch for better type safety and error handling
- Simplify Claude's comment update workflow by removing need for owner/repo/commentId params
- Update prompts and tests to use the new unified tool
This commit is contained in:
Ashwin Bhat
2025-05-30 07:56:32 -07:00
parent e98246e0d1
commit b61c881f0e
3 changed files with 40 additions and 84 deletions

View File

@@ -35,10 +35,7 @@ const BASE_ALLOWED_TOOLS = [
]; ];
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"]; const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
export function buildAllowedToolsString( export function buildAllowedToolsString(customAllowedTools?: string): string {
eventData: EventData,
customAllowedTools?: string,
): string {
let baseTools = [...BASE_ALLOWED_TOOLS]; let baseTools = [...BASE_ALLOWED_TOOLS];
let allAllowedTools = baseTools.join(","); let allAllowedTools = baseTools.join(",");
@@ -639,7 +636,6 @@ export async function createPrompt(
// Set allowed tools // Set allowed tools
const allAllowedTools = buildAllowedToolsString( const allAllowedTools = buildAllowedToolsString(
preparedContext.eventData,
preparedContext.allowedTools, preparedContext.allowedTools,
); );
const allDisallowedTools = buildDisallowedToolsString( const allDisallowedTools = buildDisallowedToolsString(

View File

@@ -7,6 +7,7 @@ import { readFile } from "fs/promises";
import { join } from "path"; import { join } from "path";
import fetch from "node-fetch"; import fetch from "node-fetch";
import { GITHUB_API_URL } from "../github/api/config"; import { GITHUB_API_URL } from "../github/api/config";
import { Octokit } from "@octokit/rest";
type GitHubRef = { type GitHubRef = {
object: { object: {
@@ -451,7 +452,6 @@ server.tool(
const githubToken = process.env.GITHUB_TOKEN; const githubToken = process.env.GITHUB_TOKEN;
const claudeCommentId = process.env.CLAUDE_COMMENT_ID; const claudeCommentId = process.env.CLAUDE_COMMENT_ID;
const eventName = process.env.GITHUB_EVENT_NAME; const eventName = process.env.GITHUB_EVENT_NAME;
const isPR = process.env.IS_PR === "true";
if (!githubToken) { if (!githubToken) {
throw new Error("GITHUB_TOKEN environment variable is required"); throw new Error("GITHUB_TOKEN environment variable is required");
@@ -464,73 +464,58 @@ server.tool(
const repo = REPO_NAME; const repo = REPO_NAME;
const commentId = parseInt(claudeCommentId, 10); const commentId = parseInt(claudeCommentId, 10);
// Create Octokit instance
const octokit = new Octokit({
auth: githubToken,
});
// Determine if this is a PR review comment based on event type // Determine if this is a PR review comment based on event type
const isPullRequestReviewComment = const isPullRequestReviewComment =
eventName === "pull_request_review_comment"; eventName === "pull_request_review_comment";
let response; let response;
let updateUrl: string;
try {
if (isPullRequestReviewComment) { if (isPullRequestReviewComment) {
// Use the PR review comment API // Try PR review comment API first
updateUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/pulls/comments/${commentId}`; response = await octokit.rest.pulls.updateReviewComment({
} else { owner,
// Use the issue comment API (works for both issues and PR general comments) repo,
updateUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/issues/comments/${commentId}`; comment_id: commentId,
} body,
response = await fetch(updateUrl, {
method: "PATCH",
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${githubToken}`,
"X-GitHub-Api-Version": "2022-11-28",
"Content-Type": "application/json",
},
body: JSON.stringify({ body }),
}); });
if (!response.ok) {
const errorText = await response.text();
// If PR review comment update fails, fall back to issue comment API
if (isPullRequestReviewComment && response.status === 404) {
const fallbackUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/issues/comments/${commentId}`;
response = await fetch(fallbackUrl, {
method: "PATCH",
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${githubToken}`,
"X-GitHub-Api-Version": "2022-11-28",
"Content-Type": "application/json",
},
body: JSON.stringify({ body }),
});
if (!response.ok) {
const fallbackErrorText = await response.text();
throw new Error(
`Failed to update comment: ${response.status} - ${fallbackErrorText}`,
);
}
} else { } else {
throw new Error( // Use issue comment API (works for both issues and PR general comments)
`Failed to update comment: ${response.status} - ${errorText}`, response = await octokit.rest.issues.updateComment({
); owner,
repo,
comment_id: commentId,
body,
});
}
} catch (error: any) {
// If PR review comment update fails with 404, fall back to issue comment API
if (isPullRequestReviewComment && error.status === 404) {
response = await octokit.rest.issues.updateComment({
owner,
repo,
comment_id: commentId,
body,
});
} else {
throw error;
} }
} }
const updatedComment = await response.json();
return { return {
content: [ content: [
{ {
type: "text", type: "text",
text: JSON.stringify( text: JSON.stringify(
{ {
id: updatedComment.id, id: response.data.id,
html_url: updatedComment.html_url, html_url: response.data.html_url,
updated_at: updatedComment.updated_at, updated_at: response.data.updated_at,
}, },
null, null,
2, 2,

View File

@@ -8,7 +8,6 @@ import {
buildDisallowedToolsString, buildDisallowedToolsString,
} from "../src/create-prompt"; } from "../src/create-prompt";
import type { PreparedContext } from "../src/create-prompt"; import type { PreparedContext } from "../src/create-prompt";
import type { EventData } from "../src/create-prompt/types";
describe("generatePrompt", () => { describe("generatePrompt", () => {
const mockGitHubData = { const mockGitHubData = {
@@ -619,15 +618,7 @@ describe("getEventTypeAndContext", () => {
describe("buildAllowedToolsString", () => { describe("buildAllowedToolsString", () => {
test("should return issue comment tool for regular events", () => { test("should return issue comment tool for regular events", () => {
const mockEventData: EventData = { const result = buildAllowedToolsString();
eventName: "issue_comment",
commentId: "123",
isPR: true,
prNumber: "456",
commentBody: "Test comment",
};
const result = buildAllowedToolsString(mockEventData);
// The base tools should be in the result // The base tools should be in the result
expect(result).toContain("Edit"); expect(result).toContain("Edit");
@@ -644,15 +635,7 @@ describe("buildAllowedToolsString", () => {
}); });
test("should return PR comment tool for inline review comments", () => { test("should return PR comment tool for inline review comments", () => {
const mockEventData: EventData = { const result = buildAllowedToolsString();
eventName: "pull_request_review_comment",
isPR: true,
prNumber: "456",
commentBody: "Test review comment",
commentId: "789",
};
const result = buildAllowedToolsString(mockEventData);
// The base tools should be in the result // The base tools should be in the result
expect(result).toContain("Edit"); expect(result).toContain("Edit");
@@ -669,16 +652,8 @@ describe("buildAllowedToolsString", () => {
}); });
test("should append custom tools when provided", () => { test("should append custom tools when provided", () => {
const mockEventData: EventData = {
eventName: "issue_comment",
commentId: "123",
isPR: true,
prNumber: "456",
commentBody: "Test comment",
};
const customTools = "Tool1,Tool2,Tool3"; const customTools = "Tool1,Tool2,Tool3";
const result = buildAllowedToolsString(mockEventData, customTools); const result = buildAllowedToolsString(customTools);
// Base tools should be present // Base tools should be present
expect(result).toContain("Edit"); expect(result).toContain("Edit");