Skip to content

Commit 370198d

Browse files
Merge pull request #119 from sirkitree/feat__github-issue-tools
feat: add issue management functionalities for github
2 parents 0724ae4 + d300fec commit 370198d

File tree

3 files changed

+237
-38
lines changed

3 files changed

+237
-38
lines changed

src/github/README.md

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,44 @@ MCP Server for the GitHub API, enabling file operations, repository management,
103103
- `from_branch` (optional string): Source branch (defaults to repo default)
104104
- Returns: Created branch reference
105105

106-
10. `search_code`
106+
10. `list_issues`
107+
- List and filter repository issues
108+
- Inputs:
109+
- `owner` (string): Repository owner
110+
- `repo` (string): Repository name
111+
- `state` (optional string): Filter by state ('open', 'closed', 'all')
112+
- `labels` (optional string[]): Filter by labels
113+
- `sort` (optional string): Sort by ('created', 'updated', 'comments')
114+
- `direction` (optional string): Sort direction ('asc', 'desc')
115+
- `since` (optional string): Filter by date (ISO 8601 timestamp)
116+
- `page` (optional number): Page number
117+
- `per_page` (optional number): Results per page
118+
- Returns: Array of issue details
119+
120+
11. `update_issue`
121+
- Update an existing issue
122+
- Inputs:
123+
- `owner` (string): Repository owner
124+
- `repo` (string): Repository name
125+
- `issue_number` (number): Issue number to update
126+
- `title` (optional string): New title
127+
- `body` (optional string): New description
128+
- `state` (optional string): New state ('open' or 'closed')
129+
- `labels` (optional string[]): New labels
130+
- `assignees` (optional string[]): New assignees
131+
- `milestone` (optional number): New milestone number
132+
- Returns: Updated issue details
133+
134+
12. `add_issue_comment`
135+
- Add a comment to an issue
136+
- Inputs:
137+
- `owner` (string): Repository owner
138+
- `repo` (string): Repository name
139+
- `issue_number` (number): Issue number to comment on
140+
- `body` (string): Comment text
141+
- Returns: Created comment details
142+
143+
13. `search_code`
107144
- Search for code across GitHub repositories
108145
- Inputs:
109146
- `q` (string): Search query using GitHub code search syntax
@@ -113,7 +150,7 @@ MCP Server for the GitHub API, enabling file operations, repository management,
113150
- `page` (optional number): Page number
114151
- Returns: Code search results with repository context
115152

116-
11. `search_issues`
153+
14. `search_issues`
117154
- Search for issues and pull requests
118155
- Inputs:
119156
- `q` (string): Search query using GitHub issues search syntax
@@ -123,7 +160,7 @@ MCP Server for the GitHub API, enabling file operations, repository management,
123160
- `page` (optional number): Page number
124161
- Returns: Issue and pull request search results
125162

126-
12. `search_users`
163+
15. `search_users`
127164
- Search for GitHub users
128165
- Inputs:
129166
- `q` (string): Search query using GitHub users search syntax

src/github/index.ts

Lines changed: 164 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,61 @@
11
#!/usr/bin/env node
2-
32
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
43
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
54
import {
65
CallToolRequestSchema,
76
ListToolsRequestSchema,
87
} from "@modelcontextprotocol/sdk/types.js";
98
import fetch from "node-fetch";
9+
import { z } from 'zod';
10+
import { zodToJsonSchema } from 'zod-to-json-schema';
1011
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,
1126
GitHubForkSchema,
12-
GitHubReferenceSchema,
13-
GitHubRepositorySchema,
1427
GitHubIssueSchema,
1528
GitHubPullRequestSchema,
16-
GitHubContentSchema,
17-
GitHubCreateUpdateFileResponseSchema,
29+
GitHubReferenceSchema,
30+
GitHubRepositorySchema,
1831
GitHubSearchResponseSchema,
1932
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,
2548
type GitHubFork,
26-
type GitHubReference,
27-
type GitHubRepository,
2849
type GitHubIssue,
2950
type GitHubPullRequest,
30-
type GitHubContent,
31-
type GitHubCreateUpdateFileResponse,
51+
type GitHubReference,
52+
type GitHubRepository,
3253
type GitHubSearchResponse,
3354
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,
5155
type SearchCodeResponse,
5256
type SearchIssuesResponse,
5357
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';
5859

5960
const server = new Server(
6061
{
@@ -486,6 +487,98 @@ async function createRepository(
486487
return GitHubRepositorySchema.parse(await response.json());
487488
}
488489

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+
489582
async function searchCode(
490583
params: z.infer<typeof SearchCodeSchema>
491584
): Promise<SearchCodeResponse> {
@@ -612,6 +705,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
612705
description: "Create a new branch in a GitHub repository",
613706
inputSchema: zodToJsonSchema(CreateBranchSchema),
614707
},
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+
},
615723
{
616724
name: "search_code",
617725
description: "Search for code across GitHub repositories",
@@ -795,6 +903,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
795903
};
796904
}
797905

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+
798927
default:
799928
throw new Error(`Unknown tool: ${request.params.name}`);
800929
}

src/github/schemas.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,39 @@ export const SearchUsersSchema = z.object({
616616
page: z.number().min(1).optional().describe("Page number"),
617617
});
618618

619+
// Add these schema definitions for issue management
620+
621+
export const ListIssuesOptionsSchema = z.object({
622+
owner: z.string(),
623+
repo: z.string(),
624+
state: z.enum(['open', 'closed', 'all']).optional(),
625+
labels: z.array(z.string()).optional(),
626+
sort: z.enum(['created', 'updated', 'comments']).optional(),
627+
direction: z.enum(['asc', 'desc']).optional(),
628+
since: z.string().optional(), // ISO 8601 timestamp
629+
page: z.number().optional(),
630+
per_page: z.number().optional()
631+
});
632+
633+
export const UpdateIssueOptionsSchema = z.object({
634+
owner: z.string(),
635+
repo: z.string(),
636+
issue_number: z.number(),
637+
title: z.string().optional(),
638+
body: z.string().optional(),
639+
state: z.enum(['open', 'closed']).optional(),
640+
labels: z.array(z.string()).optional(),
641+
assignees: z.array(z.string()).optional(),
642+
milestone: z.number().optional()
643+
});
644+
645+
export const IssueCommentSchema = z.object({
646+
owner: z.string(),
647+
repo: z.string(),
648+
issue_number: z.number(),
649+
body: z.string()
650+
});
651+
619652
// Export types
620653
export type GitHubAuthor = z.infer<typeof GitHubAuthorSchema>;
621654
export type GitHubFork = z.infer<typeof GitHubForkSchema>;

0 commit comments

Comments
 (0)