From 9e25ffd59966aae245f5fd2642769a295a077c5e Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Fri, 10 Jan 2025 11:19:30 +0000 Subject: [PATCH 1/7] Add support for listing, reading, and reviewing PRs --- src/github/README.md | 37 +++++++++++++ src/github/index.ts | 118 ++++++++++++++++++++++++++++++++++++++++++ src/github/schemas.ts | 35 +++++++++++++ 3 files changed, 190 insertions(+) diff --git a/src/github/README.md b/src/github/README.md index d2277ab..ae29cda 100644 --- a/src/github/README.md +++ b/src/github/README.md @@ -188,6 +188,43 @@ MCP Server for the GitHub API, enabling file operations, repository management, - `issue_number` (number): Issue number to retrieve - Returns: Github Issue object & details +18. `get_pull_request` + - Get details of a specific pull request + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `pull_number` (number): Pull request number + - Returns: Pull request details including diff and review status + +19. `list_pull_requests` + - List and filter repository pull requests + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `state` (optional string): Filter by state ('open', 'closed', 'all') + - `head` (optional string): Filter by head user/org and branch + - `base` (optional string): Filter by base branch + - `sort` (optional string): Sort by ('created', 'updated', 'popularity', 'long-running') + - `direction` (optional string): Sort direction ('asc', 'desc') + - `per_page` (optional number): Results per page (max 100) + - `page` (optional number): Page number + - Returns: Array of pull request details + +20. `create_pull_request_review` + - Create a review on a pull request + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `pull_number` (number): Pull request number + - `body` (string): Review comment text + - `event` (string): Review action ('APPROVE', 'REQUEST_CHANGES', 'COMMENT') + - `commit_id` (optional string): SHA of commit to review + - `comments` (optional array): Line-specific comments, each with: + - `path` (string): File path + - `position` (number): Line position in diff + - `body` (string): Comment text + - Returns: Created review details + ## Search Query Syntax ### Code Search diff --git a/src/github/index.ts b/src/github/index.ts index 3759e8b..0e731ab 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -21,6 +21,7 @@ import { ForkRepositorySchema, GetFileContentsSchema, GetIssueSchema, + GetPullRequestSchema, GitHubCommitSchema, GitHubContentSchema, GitHubCreateUpdateFileResponseSchema, @@ -36,6 +37,8 @@ import { IssueCommentSchema, ListCommitsSchema, ListIssuesOptionsSchema, + ListPullRequestsSchema, + CreatePullRequestReviewSchema, PushFilesSchema, SearchCodeResponseSchema, SearchCodeSchema, @@ -715,6 +718,86 @@ async function getIssue( return GitHubIssueSchema.parse(await response.json()); } +async function getPullRequest( + owner: string, + repo: string, + pullNumber: number +): Promise { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}`, + { + headers: { + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + }, + } + ); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return GitHubPullRequestSchema.parse(await response.json()); +} + +async function listPullRequests( + owner: string, + repo: string, + options: Omit, 'owner' | 'repo'> +): Promise { + const url = new URL(`https://api.github.com/repos/${owner}/${repo}/pulls`); + + if (options.state) url.searchParams.append('state', options.state); + if (options.head) url.searchParams.append('head', options.head); + if (options.base) url.searchParams.append('base', options.base); + if (options.sort) url.searchParams.append('sort', options.sort); + if (options.direction) url.searchParams.append('direction', options.direction); + if (options.per_page) url.searchParams.append('per_page', options.per_page.toString()); + if (options.page) url.searchParams.append('page', options.page.toString()); + + const response = await fetch(url.toString(), { + headers: { + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + }, + }); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return z.array(GitHubPullRequestSchema).parse(await response.json()); +} + +async function createPullRequestReview( + owner: string, + repo: string, + pullNumber: number, + options: Omit, 'owner' | 'repo' | 'pull_number'> +): Promise { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/reviews`, + { + method: 'POST', + headers: { + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + "Content-Type": "application/json", + }, + body: JSON.stringify(options), + } + ); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return await response.json(); +} + server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ @@ -806,6 +889,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { name: "get_issue", description: "Get details of a specific issue in a GitHub repository.", inputSchema: zodToJsonSchema(GetIssueSchema) + }, + { + name: "get_pull_request", + description: "Get details of a specific pull request in a GitHub repository", + inputSchema: zodToJsonSchema(GetPullRequestSchema) + }, + { + name: "list_pull_requests", + description: "List pull requests in a GitHub repository with filtering options", + inputSchema: zodToJsonSchema(ListPullRequestsSchema) + }, + { + name: "create_pull_request_review", + description: "Create a review on a pull request", + inputSchema: zodToJsonSchema(CreatePullRequestReviewSchema) } ], }; @@ -1011,6 +1109,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return { toolResult: issue }; } + case "get_pull_request": { + const args = GetPullRequestSchema.parse(request.params.arguments); + const pullRequest = await getPullRequest(args.owner, args.repo, args.pull_number); + return { toolResult: pullRequest }; + } + + case "list_pull_requests": { + const args = ListPullRequestsSchema.parse(request.params.arguments); + const { owner, repo, ...options } = args; + const pullRequests = await listPullRequests(owner, repo, options); + return { toolResult: pullRequests }; + } + + case "create_pull_request_review": { + const args = CreatePullRequestReviewSchema.parse(request.params.arguments); + const { owner, repo, pull_number, ...options } = args; + const review = await createPullRequestReview(owner, repo, pull_number, options); + return { toolResult: review }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/src/github/schemas.ts b/src/github/schemas.ts index 0a32232..a84c101 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -683,6 +683,38 @@ export const GetIssueSchema = z.object({ issue_number: z.number().describe("Issue number") }); +export const GetPullRequestSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + pull_number: z.number().describe("Pull request number") +}); + +export const ListPullRequestsSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + state: z.enum(['open', 'closed', 'all']).optional().describe("State of the pull requests to return"), + head: z.string().optional().describe("Filter by head user or head organization and branch name"), + base: z.string().optional().describe("Filter by base branch name"), + sort: z.enum(['created', 'updated', 'popularity', 'long-running']).optional().describe("What to sort results by"), + direction: z.enum(['asc', 'desc']).optional().describe("The direction of the sort"), + per_page: z.number().optional().describe("Results per page (max 100)"), + page: z.number().optional().describe("Page number of the results") +}); + +export const CreatePullRequestReviewSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + pull_number: z.number().describe("Pull request number"), + commit_id: z.string().optional().describe("The SHA of the commit that needs a review"), + body: z.string().describe("The body text of the review"), + event: z.enum(['APPROVE', 'REQUEST_CHANGES', 'COMMENT']).describe("The review action to perform"), + comments: z.array(z.object({ + path: z.string().describe("The relative path to the file being commented on"), + position: z.number().describe("The position in the diff where you want to add a review comment"), + body: z.string().describe("Text of the review comment") + })).optional().describe("Comments to post as part of the review") +}); + // Export types export type GitHubAuthor = z.infer; export type GitHubFork = z.infer; @@ -717,3 +749,6 @@ export type SearchIssueItem = z.infer; export type SearchIssuesResponse = z.infer; export type SearchUserItem = z.infer; export type SearchUsersResponse = z.infer; +export type GetPullRequest = z.infer; +export type ListPullRequests = z.infer; +export type CreatePullRequestReview = z.infer; From 353fbb8d0ae37969d2c39996a986664c943241cd Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Fri, 10 Jan 2025 11:23:42 +0000 Subject: [PATCH 2/7] Add reading PR files and status, merging PRs --- src/github/README.md | 27 +++++++++ src/github/index.ts | 132 ++++++++++++++++++++++++++++++++++++++++++ src/github/schemas.ts | 61 +++++++++++++++++++ 3 files changed, 220 insertions(+) diff --git a/src/github/README.md b/src/github/README.md index ae29cda..50a52d4 100644 --- a/src/github/README.md +++ b/src/github/README.md @@ -225,6 +225,33 @@ MCP Server for the GitHub API, enabling file operations, repository management, - `body` (string): Comment text - Returns: Created review details +21. `merge_pull_request` + - Merge a pull request + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `pull_number` (number): Pull request number + - `commit_title` (optional string): Title for merge commit + - `commit_message` (optional string): Extra detail for merge commit + - `merge_method` (optional string): Merge method ('merge', 'squash', 'rebase') + - Returns: Merge result details + +22. `get_pull_request_files` + - Get the list of files changed in a pull request + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `pull_number` (number): Pull request number + - Returns: Array of changed files with patch and status details + +23. `get_pull_request_status` + - Get the combined status of all status checks for a pull request + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `pull_number` (number): Pull request number + - Returns: Combined status check results and individual check details + ## Search Query Syntax ### Code Search diff --git a/src/github/index.ts b/src/github/index.ts index 0e731ab..0147f49 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -22,6 +22,9 @@ import { GetFileContentsSchema, GetIssueSchema, GetPullRequestSchema, + GetPullRequestFilesSchema, + GetPullRequestStatusSchema, + MergePullRequestSchema, GitHubCommitSchema, GitHubContentSchema, GitHubCreateUpdateFileResponseSchema, @@ -40,6 +43,8 @@ import { ListPullRequestsSchema, CreatePullRequestReviewSchema, PushFilesSchema, + PullRequestFileSchema, + CombinedStatusSchema, SearchCodeResponseSchema, SearchCodeSchema, SearchIssuesResponseSchema, @@ -798,6 +803,99 @@ async function createPullRequestReview( return await response.json(); } +async function mergePullRequest( + owner: string, + repo: string, + pullNumber: number, + options: Omit, 'owner' | 'repo' | 'pull_number'> +): Promise { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/merge`, + { + method: 'PUT', + headers: { + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + "Content-Type": "application/json", + }, + body: JSON.stringify(options), + } + ); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return await response.json(); +} + +async function getPullRequestFiles( + owner: string, + repo: string, + pullNumber: number +): Promise[]> { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/files`, + { + headers: { + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + }, + } + ); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return z.array(PullRequestFileSchema).parse(await response.json()); +} + +async function getPullRequestStatus( + owner: string, + repo: string, + pullNumber: number +): Promise> { + // First get the PR to get the head SHA + const prResponse = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}`, + { + headers: { + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + }, + } + ); + + if (!prResponse.ok) { + throw new Error(`GitHub API error: ${prResponse.statusText}`); + } + + const pr = GitHubPullRequestSchema.parse(await prResponse.json()); + const sha = pr.head.sha; + + // Then get the combined status for that SHA + const statusResponse = await fetch( + `https://api.github.com/repos/${owner}/${repo}/commits/${sha}/status`, + { + headers: { + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + }, + } + ); + + if (!statusResponse.ok) { + throw new Error(`GitHub API error: ${statusResponse.statusText}`); + } + + return CombinedStatusSchema.parse(await statusResponse.json()); +} + server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ @@ -904,6 +1002,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { name: "create_pull_request_review", description: "Create a review on a pull request", inputSchema: zodToJsonSchema(CreatePullRequestReviewSchema) + }, + { + name: "merge_pull_request", + description: "Merge a pull request", + inputSchema: zodToJsonSchema(MergePullRequestSchema) + }, + { + name: "get_pull_request_files", + description: "Get the list of files changed in a pull request", + inputSchema: zodToJsonSchema(GetPullRequestFilesSchema) + }, + { + name: "get_pull_request_status", + description: "Get the combined status of all status checks for a pull request", + inputSchema: zodToJsonSchema(GetPullRequestStatusSchema) } ], }; @@ -1129,6 +1242,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return { toolResult: review }; } + case "merge_pull_request": { + const args = MergePullRequestSchema.parse(request.params.arguments); + const { owner, repo, pull_number, ...options } = args; + const result = await mergePullRequest(owner, repo, pull_number, options); + return { toolResult: result }; + } + + case "get_pull_request_files": { + const args = GetPullRequestFilesSchema.parse(request.params.arguments); + const files = await getPullRequestFiles(args.owner, args.repo, args.pull_number); + return { toolResult: files }; + } + + case "get_pull_request_status": { + const args = GetPullRequestStatusSchema.parse(request.params.arguments); + const status = await getPullRequestStatus(args.owner, args.repo, args.pull_number); + return { toolResult: status }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/src/github/schemas.ts b/src/github/schemas.ts index a84c101..98ceed8 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -752,3 +752,64 @@ export type SearchUsersResponse = z.infer; export type GetPullRequest = z.infer; export type ListPullRequests = z.infer; export type CreatePullRequestReview = z.infer; + +// Schema for merging a pull request +export const MergePullRequestSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + pull_number: z.number().describe("Pull request number"), + commit_title: z.string().optional().describe("Title for the automatic commit message"), + commit_message: z.string().optional().describe("Extra detail to append to automatic commit message"), + merge_method: z.enum(['merge', 'squash', 'rebase']).optional().describe("Merge method to use") +}); + +// Schema for getting PR files +export const GetPullRequestFilesSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + pull_number: z.number().describe("Pull request number") +}); + +export const PullRequestFileSchema = z.object({ + sha: z.string(), + filename: z.string(), + status: z.enum(['added', 'removed', 'modified', 'renamed', 'copied', 'changed', 'unchanged']), + additions: z.number(), + deletions: z.number(), + changes: z.number(), + blob_url: z.string(), + raw_url: z.string(), + contents_url: z.string(), + patch: z.string().optional() +}); + +// Schema for checking PR status +export const GetPullRequestStatusSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + pull_number: z.number().describe("Pull request number") +}); + +export const StatusCheckSchema = z.object({ + url: z.string(), + state: z.enum(['error', 'failure', 'pending', 'success']), + description: z.string().nullable(), + target_url: z.string().nullable(), + context: z.string(), + created_at: z.string(), + updated_at: z.string() +}); + +export const CombinedStatusSchema = z.object({ + state: z.enum(['error', 'failure', 'pending', 'success']), + statuses: z.array(StatusCheckSchema), + sha: z.string(), + total_count: z.number() +}); + +export type MergePullRequest = z.infer; +export type GetPullRequestFiles = z.infer; +export type PullRequestFile = z.infer; +export type GetPullRequestStatus = z.infer; +export type StatusCheck = z.infer; +export type CombinedStatus = z.infer; From f42cf77d5747939379244592681d3c9e9f2e7cf0 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Fri, 10 Jan 2025 11:33:57 +0000 Subject: [PATCH 3/7] Don't use old `toolResult` format --- src/github/index.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/github/index.ts b/src/github/index.ts index 0147f49..b50f06b 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -1189,21 +1189,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const args = ListIssuesOptionsSchema.parse(request.params.arguments); const { owner, repo, ...options } = args; const issues = await listIssues(owner, repo, options); - return { toolResult: issues }; + return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] }; } case "update_issue": { const args = UpdateIssueOptionsSchema.parse(request.params.arguments); const { owner, repo, issue_number, ...options } = args; const issue = await updateIssue(owner, repo, issue_number, options); - return { toolResult: issue }; + return { content: [{ type: "text", text: JSON.stringify(issue, null, 2) }] }; } case "add_issue_comment": { const args = IssueCommentSchema.parse(request.params.arguments); const { owner, repo, issue_number, body } = args; const comment = await addIssueComment(owner, repo, issue_number, body); - return { toolResult: comment }; + return { content: [{ type: "text", text: JSON.stringify(comment, null, 2) }] }; } case "list_commits": { @@ -1219,46 +1219,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { issue_number: z.number() }).parse(request.params.arguments); const issue = await getIssue(args.owner, args.repo, args.issue_number); - return { toolResult: issue }; + return { content: [{ type: "text", text: JSON.stringify(issue, null, 2) }] }; } case "get_pull_request": { const args = GetPullRequestSchema.parse(request.params.arguments); const pullRequest = await getPullRequest(args.owner, args.repo, args.pull_number); - return { toolResult: pullRequest }; + return { content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }] }; } case "list_pull_requests": { const args = ListPullRequestsSchema.parse(request.params.arguments); const { owner, repo, ...options } = args; const pullRequests = await listPullRequests(owner, repo, options); - return { toolResult: pullRequests }; + return { content: [{ type: "text", text: JSON.stringify(pullRequests, null, 2) }] }; } case "create_pull_request_review": { const args = CreatePullRequestReviewSchema.parse(request.params.arguments); const { owner, repo, pull_number, ...options } = args; const review = await createPullRequestReview(owner, repo, pull_number, options); - return { toolResult: review }; + return { content: [{ type: "text", text: JSON.stringify(review, null, 2) }] }; } case "merge_pull_request": { const args = MergePullRequestSchema.parse(request.params.arguments); const { owner, repo, pull_number, ...options } = args; const result = await mergePullRequest(owner, repo, pull_number, options); - return { toolResult: result }; + return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } case "get_pull_request_files": { const args = GetPullRequestFilesSchema.parse(request.params.arguments); const files = await getPullRequestFiles(args.owner, args.repo, args.pull_number); - return { toolResult: files }; + return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] }; } case "get_pull_request_status": { const args = GetPullRequestStatusSchema.parse(request.params.arguments); const status = await getPullRequestStatus(args.owner, args.repo, args.pull_number); - return { toolResult: status }; + return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] }; } default: From ac7592f71a41bb7abc1cc627ffe3bf634e41bd28 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Fri, 10 Jan 2025 11:53:23 +0000 Subject: [PATCH 4/7] Support updating PR branches --- src/github/README.md | 9 +++++++++ src/github/index.ts | 37 +++++++++++++++++++++++++++++++++++++ src/github/schemas.ts | 9 +++++++++ 3 files changed, 55 insertions(+) diff --git a/src/github/README.md b/src/github/README.md index 50a52d4..86b36e3 100644 --- a/src/github/README.md +++ b/src/github/README.md @@ -252,6 +252,15 @@ MCP Server for the GitHub API, enabling file operations, repository management, - `pull_number` (number): Pull request number - Returns: Combined status check results and individual check details +24. `update_pull_request_branch` + - Update a pull request branch with the latest changes from the base branch (equivalent to GitHub's "Update branch" button) + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `pull_number` (number): Pull request number + - `expected_head_sha` (optional string): The expected SHA of the pull request's HEAD ref + - Returns: Success message when branch is updated + ## Search Query Syntax ### Code Search diff --git a/src/github/index.ts b/src/github/index.ts index b50f06b..b53392a 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -48,6 +48,7 @@ import { SearchCodeResponseSchema, SearchCodeSchema, SearchIssuesResponseSchema, + UpdatePullRequestBranchSchema, SearchIssuesSchema, SearchRepositoriesSchema, SearchUsersResponseSchema, @@ -853,6 +854,31 @@ async function getPullRequestFiles( return z.array(PullRequestFileSchema).parse(await response.json()); } +async function updatePullRequestBranch( + owner: string, + repo: string, + pullNumber: number, + expectedHeadSha?: string +): Promise { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/update-branch`, + { + method: "PUT", + headers: { + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + "Content-Type": "application/json", + }, + body: expectedHeadSha ? JSON.stringify({ expected_head_sha: expectedHeadSha }) : undefined, + } + ); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } +} + async function getPullRequestStatus( owner: string, repo: string, @@ -1017,6 +1043,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { name: "get_pull_request_status", description: "Get the combined status of all status checks for a pull request", inputSchema: zodToJsonSchema(GetPullRequestStatusSchema) + }, + { + name: "update_pull_request_branch", + description: "Update a pull request branch with the latest changes from the base branch", + inputSchema: zodToJsonSchema(UpdatePullRequestBranchSchema) } ], }; @@ -1261,6 +1292,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] }; } + case "update_pull_request_branch": { + const args = UpdatePullRequestBranchSchema.parse(request.params.arguments); + await updatePullRequestBranch(args.owner, args.repo, args.pull_number, args.expected_head_sha); + return { content: [{ type: "text", text: "Pull request branch updated successfully" }] }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/src/github/schemas.ts b/src/github/schemas.ts index 98ceed8..8bca3a3 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -812,4 +812,13 @@ export type GetPullRequestFiles = z.infer; export type PullRequestFile = z.infer; export type GetPullRequestStatus = z.infer; export type StatusCheck = z.infer; +// Schema for updating a pull request branch +export const UpdatePullRequestBranchSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + pull_number: z.number().describe("Pull request number"), + expected_head_sha: z.string().optional().describe("The expected SHA of the pull request's HEAD ref") +}); + export type CombinedStatus = z.infer; +export type UpdatePullRequestBranch = z.infer; From a47abf5cceccaf62f60302d24e9bcc21f162e00a Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Fri, 10 Jan 2025 13:47:46 +0000 Subject: [PATCH 5/7] Add support for listing PR comments --- src/github/README.md | 8 ++++++++ src/github/index.ts | 36 ++++++++++++++++++++++++++++++++++++ src/github/schemas.ts | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/src/github/README.md b/src/github/README.md index 86b36e3..f2710f5 100644 --- a/src/github/README.md +++ b/src/github/README.md @@ -261,6 +261,14 @@ MCP Server for the GitHub API, enabling file operations, repository management, - `expected_head_sha` (optional string): The expected SHA of the pull request's HEAD ref - Returns: Success message when branch is updated +25. `get_pull_request_comments` + - Get the review comments on a pull request + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `pull_number` (number): Pull request number + - Returns: Array of pull request review comments with details like the comment text, author, and location in the diff + ## Search Query Syntax ### Code Search diff --git a/src/github/index.ts b/src/github/index.ts index b53392a..1d023da 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -54,6 +54,8 @@ import { SearchUsersResponseSchema, SearchUsersSchema, UpdateIssueOptionsSchema, + GetPullRequestCommentsSchema, + PullRequestCommentSchema, type FileOperation, type GitHubCommit, type GitHubContent, @@ -879,6 +881,29 @@ async function updatePullRequestBranch( } } +async function getPullRequestComments( + owner: string, + repo: string, + pullNumber: number +): Promise[]> { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/comments`, + { + headers: { + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + }, + } + ); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return z.array(PullRequestCommentSchema).parse(await response.json()); +} + async function getPullRequestStatus( owner: string, repo: string, @@ -1048,6 +1073,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { name: "update_pull_request_branch", description: "Update a pull request branch with the latest changes from the base branch", inputSchema: zodToJsonSchema(UpdatePullRequestBranchSchema) + }, + { + name: "get_pull_request_comments", + description: "Get the review comments on a pull request", + inputSchema: zodToJsonSchema(GetPullRequestCommentsSchema) } ], }; @@ -1298,6 +1328,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return { content: [{ type: "text", text: "Pull request branch updated successfully" }] }; } + case "get_pull_request_comments": { + const args = GetPullRequestCommentsSchema.parse(request.params.arguments); + const comments = await getPullRequestComments(args.owner, args.repo, args.pull_number); + return { content: [{ type: "text", text: JSON.stringify(comments, null, 2) }] }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/src/github/schemas.ts b/src/github/schemas.ts index 8bca3a3..680bfb1 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -820,5 +820,39 @@ export const UpdatePullRequestBranchSchema = z.object({ expected_head_sha: z.string().optional().describe("The expected SHA of the pull request's HEAD ref") }); +// Schema for PR comments +export const GetPullRequestCommentsSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + pull_number: z.number().describe("Pull request number") +}); + +export const PullRequestCommentSchema = z.object({ + url: z.string(), + id: z.number(), + node_id: z.string(), + pull_request_review_id: z.number().nullable(), + diff_hunk: z.string(), + path: z.string().nullable(), + position: z.number().nullable(), + original_position: z.number().nullable(), + commit_id: z.string(), + original_commit_id: z.string(), + user: GitHubIssueAssigneeSchema, + body: z.string(), + created_at: z.string(), + updated_at: z.string(), + html_url: z.string(), + pull_request_url: z.string(), + author_association: z.string(), + _links: z.object({ + self: z.object({ href: z.string() }), + html: z.object({ href: z.string() }), + pull_request: z.object({ href: z.string() }) + }) +}); + export type CombinedStatus = z.infer; export type UpdatePullRequestBranch = z.infer; +export type GetPullRequestComments = z.infer; +export type PullRequestComment = z.infer; From 6ec4dff99ac9dd7c02c7b3e5bb371525bd32da24 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Fri, 10 Jan 2025 13:53:13 +0000 Subject: [PATCH 6/7] Add tool to list existing PR reviews --- src/github/README.md | 8 ++++++++ src/github/index.ts | 36 ++++++++++++++++++++++++++++++++++++ src/github/schemas.ts | 23 +++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/src/github/README.md b/src/github/README.md index f2710f5..e06bf14 100644 --- a/src/github/README.md +++ b/src/github/README.md @@ -269,6 +269,14 @@ MCP Server for the GitHub API, enabling file operations, repository management, - `pull_number` (number): Pull request number - Returns: Array of pull request review comments with details like the comment text, author, and location in the diff +26. `get_pull_request_reviews` + - Get the reviews on a pull request + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `pull_number` (number): Pull request number + - Returns: Array of pull request reviews with details like the review state (APPROVED, CHANGES_REQUESTED, etc.), reviewer, and review body + ## Search Query Syntax ### Code Search diff --git a/src/github/index.ts b/src/github/index.ts index 1d023da..04b93a3 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -56,6 +56,8 @@ import { UpdateIssueOptionsSchema, GetPullRequestCommentsSchema, PullRequestCommentSchema, + GetPullRequestReviewsSchema, + PullRequestReviewSchema, type FileOperation, type GitHubCommit, type GitHubContent, @@ -904,6 +906,29 @@ async function getPullRequestComments( return z.array(PullRequestCommentSchema).parse(await response.json()); } +async function getPullRequestReviews( + owner: string, + repo: string, + pullNumber: number +): Promise[]> { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/reviews`, + { + headers: { + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + }, + } + ); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return z.array(PullRequestReviewSchema).parse(await response.json()); +} + async function getPullRequestStatus( owner: string, repo: string, @@ -1078,6 +1103,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { name: "get_pull_request_comments", description: "Get the review comments on a pull request", inputSchema: zodToJsonSchema(GetPullRequestCommentsSchema) + }, + { + name: "get_pull_request_reviews", + description: "Get the reviews on a pull request", + inputSchema: zodToJsonSchema(GetPullRequestReviewsSchema) } ], }; @@ -1334,6 +1364,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return { content: [{ type: "text", text: JSON.stringify(comments, null, 2) }] }; } + case "get_pull_request_reviews": { + const args = GetPullRequestReviewsSchema.parse(request.params.arguments); + const reviews = await getPullRequestReviews(args.owner, args.repo, args.pull_number); + return { content: [{ type: "text", text: JSON.stringify(reviews, null, 2) }] }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/src/github/schemas.ts b/src/github/schemas.ts index 680bfb1..b9c0fa6 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -856,3 +856,26 @@ export type CombinedStatus = z.infer; export type UpdatePullRequestBranch = z.infer; export type GetPullRequestComments = z.infer; export type PullRequestComment = z.infer; + +// Schema for listing PR reviews +export const GetPullRequestReviewsSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + pull_number: z.number().describe("Pull request number") +}); + +export const PullRequestReviewSchema = z.object({ + id: z.number(), + node_id: z.string(), + user: GitHubIssueAssigneeSchema, + body: z.string().nullable(), + state: z.enum(['APPROVED', 'CHANGES_REQUESTED', 'COMMENTED', 'DISMISSED', 'PENDING']), + html_url: z.string(), + pull_request_url: z.string(), + commit_id: z.string(), + submitted_at: z.string().nullable(), + author_association: z.string() +}); + +export type GetPullRequestReviews = z.infer; +export type PullRequestReview = z.infer; From de256a48b1155ce11d0b3f78c5aebc374ac6996d Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Tue, 14 Jan 2025 11:19:13 +0000 Subject: [PATCH 7/7] fix: Add missing CreatePullRequestSchema and createPullRequest function --- src/github/operations/pulls.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/github/operations/pulls.ts b/src/github/operations/pulls.ts index ef3e65c..9b1a5bd 100644 --- a/src/github/operations/pulls.ts +++ b/src/github/operations/pulls.ts @@ -76,6 +76,17 @@ export const PullRequestReviewSchema = z.object({ }); // Input schemas +export const CreatePullRequestSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + title: z.string().describe("Pull request title"), + body: z.string().optional().describe("Pull request body/description"), + head: z.string().describe("The name of the branch where your changes are implemented"), + base: z.string().describe("The name of the branch you want the changes pulled into"), + draft: z.boolean().optional().describe("Whether to create the pull request as a draft"), + maintainer_can_modify: z.boolean().optional().describe("Whether maintainers can modify the pull request") +}); + export const GetPullRequestSchema = z.object({ owner: z.string().describe("Repository owner (username or organization)"), repo: z.string().describe("Repository name"), @@ -149,6 +160,22 @@ export const GetPullRequestReviewsSchema = z.object({ }); // Function implementations +export async function createPullRequest( + params: z.infer +): Promise> { + const { owner, repo, ...options } = CreatePullRequestSchema.parse(params); + + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/pulls`, + { + method: "POST", + body: options, + } + ); + + return GitHubPullRequestSchema.parse(response); +} + export async function getPullRequest( owner: string, repo: string,