diff --git a/fixtures/basic.txt b/fixtures/basic.txt index b0ea008..bc823bf 100644 --- a/fixtures/basic.txt +++ b/fixtures/basic.txt @@ -1 +1 @@ -use debug_shell_command tool to run 'git status' and explain the result \ No newline at end of file +summarize https://github.com/pullfrogai/scratch/issues/56, its status, and pertinent discussion on the issue \ No newline at end of file diff --git a/mcp/issueComments.ts b/mcp/issueComments.ts new file mode 100644 index 0000000..6c2acbd --- /dev/null +++ b/mcp/issueComments.ts @@ -0,0 +1,35 @@ +import { type } from "arktype"; +import { contextualize, tool } from "./shared.ts"; + +export const GetIssueComments = type({ + issue_number: type.number.describe("The issue number to get comments for"), +}); + +export const GetIssueCommentsTool = tool({ + name: "get_issue_comments", + description: "Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments.", + parameters: GetIssueComments, + execute: contextualize(async ({ issue_number }, ctx) => { + const comments = await ctx.octokit.paginate(ctx.octokit.rest.issues.listComments, { + owner: ctx.owner, + repo: ctx.name, + issue_number, + }); + + return { + issue_number, + comments: comments.map((comment) => ({ + id: comment.id, + body: comment.body, + user: comment.user?.login, + created_at: comment.created_at, + updated_at: comment.updated_at, + html_url: comment.html_url, + author_association: comment.author_association, + reactions: comment.reactions, + })), + count: comments.length, + }; + }), +}); + diff --git a/mcp/issueEvents.ts b/mcp/issueEvents.ts new file mode 100644 index 0000000..c4e5d32 --- /dev/null +++ b/mcp/issueEvents.ts @@ -0,0 +1,93 @@ +import { type } from "arktype"; +import { contextualize, tool } from "./shared.ts"; + +export const GetIssueEvents = type({ + issue_number: type.number.describe("The issue number to get events for"), +}); + +export const GetIssueEventsTool = tool({ + name: "get_issue_events", + description: + "Get timeline events for a GitHub issue that aren't reflected in the current state. Returns cross-references to other issues/PRs and commit references. Note: current labels, assignees, state, and milestone are already available via get_issue.", + parameters: GetIssueEvents, + execute: contextualize(async ({ issue_number }, ctx) => { + const events = await ctx.octokit.paginate(ctx.octokit.rest.issues.listEventsForTimeline, { + owner: ctx.owner, + repo: ctx.name, + issue_number, + }); + + // Only include events not reflected in current issue state (get_issue already has labels, assignees, state, etc.) + // Keep only relationship/reference events that show connections to other issues/PRs/commits + const relevantEventTypes = new Set(["cross_referenced", "referenced"]); + + const parsedEvents = events.flatMap((event) => { + // Filter to only events with an 'event' property and relevant types + if (!("event" in event) || !relevantEventTypes.has(event.event)) { + return []; + } + + const baseEvent: Record = { + event: event.event, + }; + + // Common fields + if ("id" in event) { + baseEvent.id = event.id; + } + if ("actor" in event && event.actor) { + baseEvent.actor = event.actor.login; + } else if ("user" in event && event.user) { + baseEvent.actor = event.user.login; + } + if ("created_at" in event) { + baseEvent.created_at = event.created_at; + } + + // Event-specific data + if (event.event === "cross_referenced") { + if ("source" in event && event.source) { + const source = event.source as { + type?: string; + issue?: { number: number; title: string; html_url: string }; + pull_request?: { number: number; title: string; html_url: string }; + }; + baseEvent.source = { + type: source.type, + issue: source.issue + ? { + number: source.issue.number, + title: source.issue.title, + html_url: source.issue.html_url, + } + : null, + pull_request: source.pull_request + ? { + number: source.pull_request.number, + title: source.pull_request.title, + html_url: source.pull_request.html_url, + } + : null, + }; + } + } + + if (event.event === "referenced") { + if ("commit_id" in event) { + baseEvent.commit_id = event.commit_id; + } + if ("commit_url" in event) { + baseEvent.commit_url = event.commit_url; + } + } + + return [baseEvent]; + }); + + return { + issue_number, + events: parsedEvents, + count: parsedEvents.length, + }; + }), +}); diff --git a/mcp/issueInfo.ts b/mcp/issueInfo.ts new file mode 100644 index 0000000..e8a6f5a --- /dev/null +++ b/mcp/issueInfo.ts @@ -0,0 +1,55 @@ +import { type } from "arktype"; +import { contextualize, tool } from "./shared.ts"; + +export const IssueInfo = type({ + issue_number: type.number.describe("The issue number to fetch"), +}); + +export const IssueInfoTool = tool({ + name: "get_issue", + description: "Retrieve GitHub issue information by issue number", + parameters: IssueInfo, + execute: contextualize(async ({ issue_number }, ctx) => { + const issue = await ctx.octokit.rest.issues.get({ + owner: ctx.owner, + repo: ctx.name, + issue_number, + }); + + const data = issue.data; + + const hints: string[] = []; + if (data.comments > 0) { + hints.push("use get_issue_comments to retrieve all comments for this issue"); + } + hints.push( + "use get_issue_events to retrieve cross-references and commit references (relationships not reflected in current state)" + ); + + return { + number: data.number, + url: data.html_url, + title: data.title, + body: data.body, + state: data.state, + locked: data.locked, + labels: data.labels?.map((label) => (typeof label === "string" ? label : label.name)), + assignees: data.assignees?.map((assignee) => assignee.login), + user: data.user?.login, + created_at: data.created_at, + updated_at: data.updated_at, + closed_at: data.closed_at, + comments: data.comments, + milestone: data.milestone?.title, + pull_request: data.pull_request + ? { + url: data.pull_request.url, + html_url: data.pull_request.html_url, + diff_url: data.pull_request.diff_url, + patch_url: data.pull_request.patch_url, + } + : null, + hints, + }; + }), +}); diff --git a/mcp/server.ts b/mcp/server.ts index e116ddc..7fad9c0 100644 --- a/mcp/server.ts +++ b/mcp/server.ts @@ -12,6 +12,9 @@ import { } from "./comment.ts"; import { DebugShellCommandTool } from "./debug.ts"; import { IssueTool } from "./issue.ts"; +import { GetIssueCommentsTool } from "./issueComments.ts"; +import { GetIssueEventsTool } from "./issueEvents.ts"; +import { IssueInfoTool } from "./issueInfo.ts"; import { PullRequestTool } from "./pr.ts"; import { PullRequestInfoTool } from "./prInfo.ts"; import { ReviewTool } from "./review.ts"; @@ -64,6 +67,9 @@ export async function startMcpHttpServer(): Promise<{ url: string; close: () => CreateWorkingCommentTool, UpdateWorkingCommentTool, IssueTool, + IssueInfoTool, + GetIssueCommentsTool, + GetIssueEventsTool, PullRequestTool, ReviewTool, PullRequestInfoTool,