Skip to content

Commit 9e25ffd

Browse files
committed
Add support for listing, reading, and reviewing PRs
1 parent d373bb8 commit 9e25ffd

File tree

3 files changed

+190
-0
lines changed

3 files changed

+190
-0
lines changed

src/github/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,43 @@ MCP Server for the GitHub API, enabling file operations, repository management,
188188
- `issue_number` (number): Issue number to retrieve
189189
- Returns: Github Issue object & details
190190

191+
18. `get_pull_request`
192+
- Get details of a specific pull request
193+
- Inputs:
194+
- `owner` (string): Repository owner
195+
- `repo` (string): Repository name
196+
- `pull_number` (number): Pull request number
197+
- Returns: Pull request details including diff and review status
198+
199+
19. `list_pull_requests`
200+
- List and filter repository pull requests
201+
- Inputs:
202+
- `owner` (string): Repository owner
203+
- `repo` (string): Repository name
204+
- `state` (optional string): Filter by state ('open', 'closed', 'all')
205+
- `head` (optional string): Filter by head user/org and branch
206+
- `base` (optional string): Filter by base branch
207+
- `sort` (optional string): Sort by ('created', 'updated', 'popularity', 'long-running')
208+
- `direction` (optional string): Sort direction ('asc', 'desc')
209+
- `per_page` (optional number): Results per page (max 100)
210+
- `page` (optional number): Page number
211+
- Returns: Array of pull request details
212+
213+
20. `create_pull_request_review`
214+
- Create a review on a pull request
215+
- Inputs:
216+
- `owner` (string): Repository owner
217+
- `repo` (string): Repository name
218+
- `pull_number` (number): Pull request number
219+
- `body` (string): Review comment text
220+
- `event` (string): Review action ('APPROVE', 'REQUEST_CHANGES', 'COMMENT')
221+
- `commit_id` (optional string): SHA of commit to review
222+
- `comments` (optional array): Line-specific comments, each with:
223+
- `path` (string): File path
224+
- `position` (number): Line position in diff
225+
- `body` (string): Comment text
226+
- Returns: Created review details
227+
191228
## Search Query Syntax
192229

193230
### Code Search

src/github/index.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
ForkRepositorySchema,
2222
GetFileContentsSchema,
2323
GetIssueSchema,
24+
GetPullRequestSchema,
2425
GitHubCommitSchema,
2526
GitHubContentSchema,
2627
GitHubCreateUpdateFileResponseSchema,
@@ -36,6 +37,8 @@ import {
3637
IssueCommentSchema,
3738
ListCommitsSchema,
3839
ListIssuesOptionsSchema,
40+
ListPullRequestsSchema,
41+
CreatePullRequestReviewSchema,
3942
PushFilesSchema,
4043
SearchCodeResponseSchema,
4144
SearchCodeSchema,
@@ -715,6 +718,86 @@ async function getIssue(
715718
return GitHubIssueSchema.parse(await response.json());
716719
}
717720

721+
async function getPullRequest(
722+
owner: string,
723+
repo: string,
724+
pullNumber: number
725+
): Promise<GitHubPullRequest> {
726+
const response = await fetch(
727+
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}`,
728+
{
729+
headers: {
730+
Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
731+
Accept: "application/vnd.github.v3+json",
732+
"User-Agent": "github-mcp-server",
733+
},
734+
}
735+
);
736+
737+
if (!response.ok) {
738+
throw new Error(`GitHub API error: ${response.statusText}`);
739+
}
740+
741+
return GitHubPullRequestSchema.parse(await response.json());
742+
}
743+
744+
async function listPullRequests(
745+
owner: string,
746+
repo: string,
747+
options: Omit<z.infer<typeof ListPullRequestsSchema>, 'owner' | 'repo'>
748+
): Promise<GitHubPullRequest[]> {
749+
const url = new URL(`https://api.github.com/repos/${owner}/${repo}/pulls`);
750+
751+
if (options.state) url.searchParams.append('state', options.state);
752+
if (options.head) url.searchParams.append('head', options.head);
753+
if (options.base) url.searchParams.append('base', options.base);
754+
if (options.sort) url.searchParams.append('sort', options.sort);
755+
if (options.direction) url.searchParams.append('direction', options.direction);
756+
if (options.per_page) url.searchParams.append('per_page', options.per_page.toString());
757+
if (options.page) url.searchParams.append('page', options.page.toString());
758+
759+
const response = await fetch(url.toString(), {
760+
headers: {
761+
Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
762+
Accept: "application/vnd.github.v3+json",
763+
"User-Agent": "github-mcp-server",
764+
},
765+
});
766+
767+
if (!response.ok) {
768+
throw new Error(`GitHub API error: ${response.statusText}`);
769+
}
770+
771+
return z.array(GitHubPullRequestSchema).parse(await response.json());
772+
}
773+
774+
async function createPullRequestReview(
775+
owner: string,
776+
repo: string,
777+
pullNumber: number,
778+
options: Omit<z.infer<typeof CreatePullRequestReviewSchema>, 'owner' | 'repo' | 'pull_number'>
779+
): Promise<any> {
780+
const response = await fetch(
781+
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/reviews`,
782+
{
783+
method: 'POST',
784+
headers: {
785+
Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
786+
Accept: "application/vnd.github.v3+json",
787+
"User-Agent": "github-mcp-server",
788+
"Content-Type": "application/json",
789+
},
790+
body: JSON.stringify(options),
791+
}
792+
);
793+
794+
if (!response.ok) {
795+
throw new Error(`GitHub API error: ${response.statusText}`);
796+
}
797+
798+
return await response.json();
799+
}
800+
718801
server.setRequestHandler(ListToolsRequestSchema, async () => {
719802
return {
720803
tools: [
@@ -806,6 +889,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
806889
name: "get_issue",
807890
description: "Get details of a specific issue in a GitHub repository.",
808891
inputSchema: zodToJsonSchema(GetIssueSchema)
892+
},
893+
{
894+
name: "get_pull_request",
895+
description: "Get details of a specific pull request in a GitHub repository",
896+
inputSchema: zodToJsonSchema(GetPullRequestSchema)
897+
},
898+
{
899+
name: "list_pull_requests",
900+
description: "List pull requests in a GitHub repository with filtering options",
901+
inputSchema: zodToJsonSchema(ListPullRequestsSchema)
902+
},
903+
{
904+
name: "create_pull_request_review",
905+
description: "Create a review on a pull request",
906+
inputSchema: zodToJsonSchema(CreatePullRequestReviewSchema)
809907
}
810908
],
811909
};
@@ -1011,6 +1109,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
10111109
return { toolResult: issue };
10121110
}
10131111

1112+
case "get_pull_request": {
1113+
const args = GetPullRequestSchema.parse(request.params.arguments);
1114+
const pullRequest = await getPullRequest(args.owner, args.repo, args.pull_number);
1115+
return { toolResult: pullRequest };
1116+
}
1117+
1118+
case "list_pull_requests": {
1119+
const args = ListPullRequestsSchema.parse(request.params.arguments);
1120+
const { owner, repo, ...options } = args;
1121+
const pullRequests = await listPullRequests(owner, repo, options);
1122+
return { toolResult: pullRequests };
1123+
}
1124+
1125+
case "create_pull_request_review": {
1126+
const args = CreatePullRequestReviewSchema.parse(request.params.arguments);
1127+
const { owner, repo, pull_number, ...options } = args;
1128+
const review = await createPullRequestReview(owner, repo, pull_number, options);
1129+
return { toolResult: review };
1130+
}
1131+
10141132
default:
10151133
throw new Error(`Unknown tool: ${request.params.name}`);
10161134
}

src/github/schemas.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,38 @@ export const GetIssueSchema = z.object({
683683
issue_number: z.number().describe("Issue number")
684684
});
685685

686+
export const GetPullRequestSchema = z.object({
687+
owner: z.string().describe("Repository owner (username or organization)"),
688+
repo: z.string().describe("Repository name"),
689+
pull_number: z.number().describe("Pull request number")
690+
});
691+
692+
export const ListPullRequestsSchema = z.object({
693+
owner: z.string().describe("Repository owner (username or organization)"),
694+
repo: z.string().describe("Repository name"),
695+
state: z.enum(['open', 'closed', 'all']).optional().describe("State of the pull requests to return"),
696+
head: z.string().optional().describe("Filter by head user or head organization and branch name"),
697+
base: z.string().optional().describe("Filter by base branch name"),
698+
sort: z.enum(['created', 'updated', 'popularity', 'long-running']).optional().describe("What to sort results by"),
699+
direction: z.enum(['asc', 'desc']).optional().describe("The direction of the sort"),
700+
per_page: z.number().optional().describe("Results per page (max 100)"),
701+
page: z.number().optional().describe("Page number of the results")
702+
});
703+
704+
export const CreatePullRequestReviewSchema = z.object({
705+
owner: z.string().describe("Repository owner (username or organization)"),
706+
repo: z.string().describe("Repository name"),
707+
pull_number: z.number().describe("Pull request number"),
708+
commit_id: z.string().optional().describe("The SHA of the commit that needs a review"),
709+
body: z.string().describe("The body text of the review"),
710+
event: z.enum(['APPROVE', 'REQUEST_CHANGES', 'COMMENT']).describe("The review action to perform"),
711+
comments: z.array(z.object({
712+
path: z.string().describe("The relative path to the file being commented on"),
713+
position: z.number().describe("The position in the diff where you want to add a review comment"),
714+
body: z.string().describe("Text of the review comment")
715+
})).optional().describe("Comments to post as part of the review")
716+
});
717+
686718
// Export types
687719
export type GitHubAuthor = z.infer<typeof GitHubAuthorSchema>;
688720
export type GitHubFork = z.infer<typeof GitHubForkSchema>;
@@ -717,3 +749,6 @@ export type SearchIssueItem = z.infer<typeof SearchIssueItemSchema>;
717749
export type SearchIssuesResponse = z.infer<typeof SearchIssuesResponseSchema>;
718750
export type SearchUserItem = z.infer<typeof SearchUserItemSchema>;
719751
export type SearchUsersResponse = z.infer<typeof SearchUsersResponseSchema>;
752+
export type GetPullRequest = z.infer<typeof GetPullRequestSchema>;
753+
export type ListPullRequests = z.infer<typeof ListPullRequestsSchema>;
754+
export type CreatePullRequestReview = z.infer<typeof CreatePullRequestReviewSchema>;

0 commit comments

Comments
 (0)