|
1 | 1 | #!/usr/bin/env node
|
2 |
| - |
3 | 2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
4 | 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
5 | 4 | import {
|
6 | 5 | CallToolRequestSchema,
|
7 | 6 | ListToolsRequestSchema,
|
8 | 7 | } from "@modelcontextprotocol/sdk/types.js";
|
9 | 8 | import fetch from "node-fetch";
|
| 9 | +import { z } from 'zod'; |
| 10 | +import { zodToJsonSchema } from 'zod-to-json-schema'; |
10 | 11 | import {
|
| 12 | + CreateBranchOptionsSchema, |
| 13 | + CreateBranchSchema, |
| 14 | + CreateIssueOptionsSchema, |
| 15 | + CreateIssueSchema, |
| 16 | + CreateOrUpdateFileSchema, |
| 17 | + CreatePullRequestOptionsSchema, |
| 18 | + CreatePullRequestSchema, |
| 19 | + CreateRepositoryOptionsSchema, |
| 20 | + CreateRepositorySchema, |
| 21 | + ForkRepositorySchema, |
| 22 | + GetFileContentsSchema, |
| 23 | + GitHubCommitSchema, |
| 24 | + GitHubContentSchema, |
| 25 | + GitHubCreateUpdateFileResponseSchema, |
11 | 26 | GitHubForkSchema,
|
12 |
| - GitHubReferenceSchema, |
13 |
| - GitHubRepositorySchema, |
14 | 27 | GitHubIssueSchema,
|
15 | 28 | GitHubPullRequestSchema,
|
16 |
| - GitHubContentSchema, |
17 |
| - GitHubCreateUpdateFileResponseSchema, |
| 29 | + GitHubReferenceSchema, |
| 30 | + GitHubRepositorySchema, |
18 | 31 | GitHubSearchResponseSchema,
|
19 | 32 | GitHubTreeSchema,
|
20 |
| - GitHubCommitSchema, |
21 |
| - CreateRepositoryOptionsSchema, |
22 |
| - CreateIssueOptionsSchema, |
23 |
| - CreatePullRequestOptionsSchema, |
24 |
| - CreateBranchOptionsSchema, |
| 33 | + IssueCommentSchema, |
| 34 | + ListIssuesOptionsSchema, |
| 35 | + PushFilesSchema, |
| 36 | + SearchCodeResponseSchema, |
| 37 | + SearchCodeSchema, |
| 38 | + SearchIssuesResponseSchema, |
| 39 | + SearchIssuesSchema, |
| 40 | + SearchRepositoriesSchema, |
| 41 | + SearchUsersResponseSchema, |
| 42 | + SearchUsersSchema, |
| 43 | + UpdateIssueOptionsSchema, |
| 44 | + type FileOperation, |
| 45 | + type GitHubCommit, |
| 46 | + type GitHubContent, |
| 47 | + type GitHubCreateUpdateFileResponse, |
25 | 48 | type GitHubFork,
|
26 |
| - type GitHubReference, |
27 |
| - type GitHubRepository, |
28 | 49 | type GitHubIssue,
|
29 | 50 | type GitHubPullRequest,
|
30 |
| - type GitHubContent, |
31 |
| - type GitHubCreateUpdateFileResponse, |
| 51 | + type GitHubReference, |
| 52 | + type GitHubRepository, |
32 | 53 | type GitHubSearchResponse,
|
33 | 54 | type GitHubTree,
|
34 |
| - type GitHubCommit, |
35 |
| - type FileOperation, |
36 |
| - CreateOrUpdateFileSchema, |
37 |
| - SearchRepositoriesSchema, |
38 |
| - CreateRepositorySchema, |
39 |
| - GetFileContentsSchema, |
40 |
| - PushFilesSchema, |
41 |
| - CreateIssueSchema, |
42 |
| - CreatePullRequestSchema, |
43 |
| - ForkRepositorySchema, |
44 |
| - CreateBranchSchema, |
45 |
| - SearchCodeSchema, |
46 |
| - SearchIssuesSchema, |
47 |
| - SearchUsersSchema, |
48 |
| - SearchCodeResponseSchema, |
49 |
| - SearchIssuesResponseSchema, |
50 |
| - SearchUsersResponseSchema, |
51 | 55 | type SearchCodeResponse,
|
52 | 56 | type SearchIssuesResponse,
|
53 | 57 | type SearchUsersResponse,
|
54 |
| -} from "./schemas.js"; |
55 |
| -import { zodToJsonSchema } from "zod-to-json-schema"; |
56 |
| -import { z } from "zod"; |
57 |
| -import type { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; |
| 58 | +} from './schemas.js'; |
58 | 59 |
|
59 | 60 | const server = new Server(
|
60 | 61 | {
|
@@ -486,6 +487,98 @@ async function createRepository(
|
486 | 487 | return GitHubRepositorySchema.parse(await response.json());
|
487 | 488 | }
|
488 | 489 |
|
| 490 | +async function listIssues( |
| 491 | + owner: string, |
| 492 | + repo: string, |
| 493 | + options: Omit<z.infer<typeof ListIssuesOptionsSchema>, 'owner' | 'repo'> |
| 494 | +): Promise<GitHubIssue[]> { |
| 495 | + const url = new URL(`https://api.github.com/repos/${owner}/${repo}/issues`); |
| 496 | + |
| 497 | + // Add query parameters |
| 498 | + if (options.state) url.searchParams.append('state', options.state); |
| 499 | + if (options.labels) url.searchParams.append('labels', options.labels.join(',')); |
| 500 | + if (options.sort) url.searchParams.append('sort', options.sort); |
| 501 | + if (options.direction) url.searchParams.append('direction', options.direction); |
| 502 | + if (options.since) url.searchParams.append('since', options.since); |
| 503 | + if (options.page) url.searchParams.append('page', options.page.toString()); |
| 504 | + if (options.per_page) url.searchParams.append('per_page', options.per_page.toString()); |
| 505 | + |
| 506 | + const response = await fetch(url.toString(), { |
| 507 | + headers: { |
| 508 | + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, |
| 509 | + "Accept": "application/vnd.github.v3+json", |
| 510 | + "User-Agent": "github-mcp-server" |
| 511 | + } |
| 512 | + }); |
| 513 | + |
| 514 | + if (!response.ok) { |
| 515 | + throw new Error(`GitHub API error: ${response.statusText}`); |
| 516 | + } |
| 517 | + |
| 518 | + return z.array(GitHubIssueSchema).parse(await response.json()); |
| 519 | +} |
| 520 | + |
| 521 | +async function updateIssue( |
| 522 | + owner: string, |
| 523 | + repo: string, |
| 524 | + issueNumber: number, |
| 525 | + options: Omit<z.infer<typeof UpdateIssueOptionsSchema>, 'owner' | 'repo' | 'issue_number'> |
| 526 | +): Promise<GitHubIssue> { |
| 527 | + const response = await fetch( |
| 528 | + `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`, |
| 529 | + { |
| 530 | + method: "PATCH", |
| 531 | + headers: { |
| 532 | + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, |
| 533 | + "Accept": "application/vnd.github.v3+json", |
| 534 | + "User-Agent": "github-mcp-server", |
| 535 | + "Content-Type": "application/json" |
| 536 | + }, |
| 537 | + body: JSON.stringify({ |
| 538 | + title: options.title, |
| 539 | + body: options.body, |
| 540 | + state: options.state, |
| 541 | + labels: options.labels, |
| 542 | + assignees: options.assignees, |
| 543 | + milestone: options.milestone |
| 544 | + }) |
| 545 | + } |
| 546 | + ); |
| 547 | + |
| 548 | + if (!response.ok) { |
| 549 | + throw new Error(`GitHub API error: ${response.statusText}`); |
| 550 | + } |
| 551 | + |
| 552 | + return GitHubIssueSchema.parse(await response.json()); |
| 553 | +} |
| 554 | + |
| 555 | +async function addIssueComment( |
| 556 | + owner: string, |
| 557 | + repo: string, |
| 558 | + issueNumber: number, |
| 559 | + body: string |
| 560 | +): Promise<z.infer<typeof IssueCommentSchema>> { |
| 561 | + const response = await fetch( |
| 562 | + `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`, |
| 563 | + { |
| 564 | + method: "POST", |
| 565 | + headers: { |
| 566 | + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, |
| 567 | + "Accept": "application/vnd.github.v3+json", |
| 568 | + "User-Agent": "github-mcp-server", |
| 569 | + "Content-Type": "application/json" |
| 570 | + }, |
| 571 | + body: JSON.stringify({ body }) |
| 572 | + } |
| 573 | + ); |
| 574 | + |
| 575 | + if (!response.ok) { |
| 576 | + throw new Error(`GitHub API error: ${response.statusText}`); |
| 577 | + } |
| 578 | + |
| 579 | + return IssueCommentSchema.parse(await response.json()); |
| 580 | +} |
| 581 | + |
489 | 582 | async function searchCode(
|
490 | 583 | params: z.infer<typeof SearchCodeSchema>
|
491 | 584 | ): Promise<SearchCodeResponse> {
|
@@ -612,6 +705,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
612 | 705 | description: "Create a new branch in a GitHub repository",
|
613 | 706 | inputSchema: zodToJsonSchema(CreateBranchSchema),
|
614 | 707 | },
|
| 708 | + { |
| 709 | + name: "list_issues", |
| 710 | + description: "List issues in a GitHub repository with filtering options", |
| 711 | + inputSchema: zodToJsonSchema(ListIssuesOptionsSchema) |
| 712 | + }, |
| 713 | + { |
| 714 | + name: "update_issue", |
| 715 | + description: "Update an existing issue in a GitHub repository", |
| 716 | + inputSchema: zodToJsonSchema(UpdateIssueOptionsSchema) |
| 717 | + }, |
| 718 | + { |
| 719 | + name: "add_issue_comment", |
| 720 | + description: "Add a comment to an existing issue", |
| 721 | + inputSchema: zodToJsonSchema(IssueCommentSchema) |
| 722 | + }, |
615 | 723 | {
|
616 | 724 | name: "search_code",
|
617 | 725 | description: "Search for code across GitHub repositories",
|
@@ -795,6 +903,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
795 | 903 | };
|
796 | 904 | }
|
797 | 905 |
|
| 906 | + case "list_issues": { |
| 907 | + const args = ListIssuesOptionsSchema.parse(request.params.arguments); |
| 908 | + const { owner, repo, ...options } = args; |
| 909 | + const issues = await listIssues(owner, repo, options); |
| 910 | + return { toolResult: issues }; |
| 911 | + } |
| 912 | + |
| 913 | + case "update_issue": { |
| 914 | + const args = UpdateIssueOptionsSchema.parse(request.params.arguments); |
| 915 | + const { owner, repo, issue_number, ...options } = args; |
| 916 | + const issue = await updateIssue(owner, repo, issue_number, options); |
| 917 | + return { toolResult: issue }; |
| 918 | + } |
| 919 | + |
| 920 | + case "add_issue_comment": { |
| 921 | + const args = IssueCommentSchema.parse(request.params.arguments); |
| 922 | + const { owner, repo, issue_number, body } = args; |
| 923 | + const comment = await addIssueComment(owner, repo, issue_number, body); |
| 924 | + return { toolResult: comment }; |
| 925 | + } |
| 926 | + |
798 | 927 | default:
|
799 | 928 | throw new Error(`Unknown tool: ${request.params.name}`);
|
800 | 929 | }
|
|
0 commit comments