diff --git a/agents/instructions.ts b/agents/instructions.ts index 5f71c4d..ec9ec5e 100644 --- a/agents/instructions.ts +++ b/agents/instructions.ts @@ -44,9 +44,6 @@ Do not try to handle github auth- treat ${ghPullfrogMcpName} as a black box that When using ${ghPullfrogMcpName}, use the tools to comment and interact in a way that a real member of the team would. Ensure after your edits are done, your final comments do not contain intermediate reasoning or context, e.g. "I'll respond to the question." -For CI failures: -- Use ${ghPullfrogMcpName}/get_check_suite_logs with check_suite.id to get all failed workflow logs - ## Mode Selection Before starting any work, you must first determine which mode to use by examining the request and calling ${ghPullfrogMcpName}/select_mode. diff --git a/mcp/README.md b/mcp/README.md index 1d1fe79..bc18b96 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -27,6 +27,53 @@ await mcp.call("gh-pullfrog/get_check_suite_logs", { }); ``` +### review tools + +#### `get_review_comments` +get all line-by-line comments for a specific pull request review. + +**parameters:** +- `pull_number` (number): the pull request number +- `review_id` (number): the id from review.id in the webhook payload + +**replaces:** `gh api repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments` + +**returns:** +array of review comments including: +- file path, line number, comment body +- side (LEFT/RIGHT) and position in diff +- user, timestamps, html_url +- in_reply_to_id for threaded comments + +**example:** +```typescript +// when handling a pull_request_review_submitted webhook +await mcp.call("gh-pullfrog/get_review_comments", { + pull_number: 47, + review_id: review.id +}); +``` + +#### `list_pull_request_reviews` +list all reviews for a pull request. + +**parameters:** +- `pull_number` (number): the pull request number + +**replaces:** `gh api repos/{owner}/{repo}/pulls/{pull_number}/reviews` + +**returns:** +array of reviews with: +- review id, body, state (approved/changes_requested/commented) +- user, commit_id, submitted_at, html_url + +**example:** +```typescript +await mcp.call("gh-pullfrog/list_pull_request_reviews", { + pull_number: 47 +}); +``` + ### other tools see individual files for documentation on other tools: diff --git a/mcp/checkSuite.ts b/mcp/checkSuite.ts index f07ee08..d910e02 100644 --- a/mcp/checkSuite.ts +++ b/mcp/checkSuite.ts @@ -12,17 +12,18 @@ export const GetCheckSuiteLogsTool = tool({ parameters: GetCheckSuiteLogs, execute: contextualize(async ({ check_suite_id }, ctx) => { // get workflow runs for this specific check suite - const runsResponse = await ctx.octokit.rest.actions.listWorkflowRunsForRepo({ - owner: ctx.owner, - repo: ctx.name, - check_suite_id, - per_page: 100, - }); - - const failedRuns = runsResponse.data.workflow_runs.filter( - (run) => run.conclusion === "failure" + const workflowRuns = await ctx.octokit.paginate( + ctx.octokit.rest.actions.listWorkflowRunsForRepo, + { + owner: ctx.owner, + repo: ctx.name, + check_suite_id, + per_page: 100, + } ); + const failedRuns = workflowRuns.filter((run) => run.conclusion === "failure"); + if (failedRuns.length === 0) { return { check_suite_id, @@ -34,14 +35,14 @@ export const GetCheckSuiteLogsTool = tool({ // get logs for each failed run const logsForRuns = await Promise.all( failedRuns.map(async (run) => { - const jobsResponse = await ctx.octokit.rest.actions.listJobsForWorkflowRun({ + const jobs = await ctx.octokit.paginate(ctx.octokit.rest.actions.listJobsForWorkflowRun, { owner: ctx.owner, repo: ctx.name, run_id: run.id, }); const jobLogs = await Promise.all( - jobsResponse.data.jobs.map(async (job) => { + jobs.map(async (job) => { try { const logsResponse = await ctx.octokit.rest.actions.downloadJobLogsForWorkflowRun({ owner: ctx.owner, diff --git a/mcp/files.ts b/mcp/files.ts new file mode 100644 index 0000000..8d566e7 --- /dev/null +++ b/mcp/files.ts @@ -0,0 +1,43 @@ +import { execSync } from "node:child_process"; +import { type } from "arktype"; +import { contextualize, tool } from "./shared.ts"; + +export const ListFiles = type({ + path: type.string + .describe("The path to list files from (defaults to current directory)") + .default("."), +}); + +export const ListFilesTool = tool({ + name: "list_files", + description: + "List files in the repository using git ls-files. Useful for discovering the file structure and locating files.", + parameters: ListFiles, + execute: contextualize(async ({ path }) => { + try { + // Use git ls-files to list tracked files + // This respects .gitignore and gives a clean list of source files + const command = path === "." ? "git ls-files" : `git ls-files ${path}`; + const output = execSync(command, { encoding: "utf-8" }); + const files = output.split("\n").filter((f) => f.trim() !== ""); + + if (files.length === 0) { + // Fallback for non-git environments or untracked files + const findCmd = `find ${path} -maxdepth 3 -not -path '*/.*' -type f`; + const findOutput = execSync(findCmd, { encoding: "utf-8" }); + return { + files: findOutput.split("\n").filter((f) => f.trim() !== ""), + method: "find", + }; + } + + return { files, method: "git" }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + error: `Failed to list files: ${errorMessage}`, + hint: "Try using a specific path if the repository root is not the current directory.", + }; + } + }), +}); diff --git a/mcp/prInfo.ts b/mcp/prInfo.ts index 22e13b6..857bb30 100644 --- a/mcp/prInfo.ts +++ b/mcp/prInfo.ts @@ -36,7 +36,8 @@ export const PullRequestInfoTool = tool({ execSync(`git fetch origin ${headBranch}`, { stdio: "inherit" }); log.info(`Checking out PR branch: origin/${headBranch}`); - execSync(`git checkout origin/${headBranch}`, { stdio: "inherit" }); + // check out a local branch tracking the remote branch so we can push changes + execSync(`git checkout -B ${headBranch} origin/${headBranch}`, { stdio: "inherit" }); return { number: data.number, diff --git a/mcp/reviewComments.ts b/mcp/reviewComments.ts new file mode 100644 index 0000000..1906d41 --- /dev/null +++ b/mcp/reviewComments.ts @@ -0,0 +1,76 @@ +import { type } from "arktype"; +import { contextualize, tool } from "./shared.ts"; + +export const GetReviewComments = type({ + pull_number: type.number.describe("The pull request number"), + review_id: type.number.describe("The review ID to get comments for"), +}); + +export const GetReviewCommentsTool = tool({ + name: "get_review_comments", + description: + "Get all review comments for a specific pull request review. Returns line-by-line comments that were left on specific code locations.", + parameters: GetReviewComments, + execute: contextualize(async ({ pull_number, review_id }, ctx) => { + const comments = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listCommentsForReview, { + owner: ctx.owner, + repo: ctx.name, + pull_number, + review_id, + }); + + return { + review_id, + pull_number, + comments: comments.map((comment) => ({ + id: comment.id, + body: comment.body, + path: comment.path, + line: comment.line, + side: comment.side, + start_line: comment.start_line, + start_side: comment.start_side, + user: typeof comment.user === "string" ? comment.user : comment.user?.login, + created_at: comment.created_at, + updated_at: comment.updated_at, + html_url: comment.html_url, + in_reply_to_id: comment.in_reply_to_id, + diff_hunk: comment.diff_hunk, + reactions: comment.reactions, + })), + count: comments.length, + }; + }), +}); + +export const ListPullRequestReviews = type({ + pull_number: type.number.describe("The pull request number to list reviews for"), +}); + +export const ListPullRequestReviewsTool = tool({ + name: "list_pull_request_reviews", + description: + "List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments.", + parameters: ListPullRequestReviews, + execute: contextualize(async ({ pull_number }, ctx) => { + const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, { + owner: ctx.owner, + repo: ctx.name, + pull_number, + }); + + return { + pull_number, + reviews: reviews.map((review) => ({ + id: review.id, + body: review.body, + state: review.state, + user: review.user?.login, + commit_id: review.commit_id, + submitted_at: review.submitted_at, + html_url: review.html_url, + })), + count: reviews.length, + }; + }), +}); diff --git a/mcp/server.ts b/mcp/server.ts index ef6de1c..96e68e0 100644 --- a/mcp/server.ts +++ b/mcp/server.ts @@ -6,10 +6,12 @@ import { EditCommentTool, UpdateWorkingCommentTool, } from "./comment.ts"; +// import { ListFilesTool } from "./files.ts"; import { IssueTool } from "./issue.ts"; import { PullRequestTool } from "./pr.ts"; import { PullRequestInfoTool } from "./prInfo.ts"; import { ReviewTool } from "./review.ts"; +import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComments.ts"; import { SelectModeTool } from "./selectMode.ts"; import { addTools } from "./shared.ts"; @@ -25,9 +27,12 @@ addTools(server, [ CreateWorkingCommentTool, UpdateWorkingCommentTool, IssueTool, + // ListFilesTool, PullRequestTool, ReviewTool, PullRequestInfoTool, + GetReviewCommentsTool, + ListPullRequestReviewsTool, GetCheckSuiteLogsTool, ]);