Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add issue management functionalities for github #119

Merged
37 changes: 37 additions & 0 deletions src/github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,43 @@ MCP Server for the GitHub API, enabling file operations, repository management,
- `from_branch` (optional string): Source branch (defaults to repo default)
- Returns: Created branch reference

10. `list_issues`
- List and filter repository issues
- Inputs:
- `owner` (string): Repository owner
- `repo` (string): Repository name
- `state` (optional string): Filter by state ('open', 'closed', 'all')
- `labels` (optional string[]): Filter by labels
- `sort` (optional string): Sort by ('created', 'updated', 'comments')
- `direction` (optional string): Sort direction ('asc', 'desc')
- `since` (optional string): Filter by date (ISO 8601 timestamp)
- `page` (optional number): Page number
- `per_page` (optional number): Results per page
- Returns: Array of issue details

11. `update_issue`
- Update an existing issue
- Inputs:
- `owner` (string): Repository owner
- `repo` (string): Repository name
- `issue_number` (number): Issue number to update
- `title` (optional string): New title
- `body` (optional string): New description
- `state` (optional string): New state ('open' or 'closed')
- `labels` (optional string[]): New labels
- `assignees` (optional string[]): New assignees
- `milestone` (optional number): New milestone number
- Returns: Updated issue details

12. `add_issue_comment`
- Add a comment to an issue
- Inputs:
- `owner` (string): Repository owner
- `repo` (string): Repository name
- `issue_number` (number): Issue number to comment on
- `body` (string): Comment text
- Returns: Created comment details

## Setup

### Personal Access Token
Expand Down
133 changes: 132 additions & 1 deletion src/github/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ import {
CreateIssueSchema,
CreatePullRequestSchema,
ForkRepositorySchema,
CreateBranchSchema
CreateBranchSchema,
ListIssuesOptionsSchema,
UpdateIssueOptionsSchema,
IssueCommentSchema
} from './schemas.js';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
Expand Down Expand Up @@ -467,6 +470,98 @@ async function createRepository(
return GitHubRepositorySchema.parse(await response.json());
}

async function listIssues(
owner: string,
repo: string,
options: z.infer<typeof ListIssuesOptionsSchema>
): Promise<GitHubIssue[]> {
const url = new URL(`https://api.github.com/repos/${owner}/${repo}/issues`);

// Add query parameters
if (options.state) url.searchParams.append('state', options.state);
if (options.labels) url.searchParams.append('labels', options.labels.join(','));
if (options.sort) url.searchParams.append('sort', options.sort);
if (options.direction) url.searchParams.append('direction', options.direction);
if (options.since) url.searchParams.append('since', options.since);
if (options.page) url.searchParams.append('page', options.page.toString());
if (options.per_page) url.searchParams.append('per_page', options.per_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(GitHubIssueSchema).parse(await response.json());
}

async function updateIssue(
owner: string,
repo: string,
issueNumber: number,
options: z.infer<typeof UpdateIssueOptionsSchema>
): Promise<GitHubIssue> {
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`,
{
method: "PATCH",
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({
title: options.title,
body: options.body,
state: options.state,
labels: options.labels,
assignees: options.assignees,
milestone: options.milestone
})
}
);

if (!response.ok) {
throw new Error(`GitHub API error: ${response.statusText}`);
}

return GitHubIssueSchema.parse(await response.json());
}

async function addIssueComment(
owner: string,
repo: string,
issueNumber: number,
body: string
): Promise<z.infer<typeof IssueCommentSchema>> {
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
{
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({ body })
}
);

if (!response.ok) {
throw new Error(`GitHub API error: ${response.statusText}`);
}

return IssueCommentSchema.parse(await response.json());
}

server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
Expand Down Expand Up @@ -514,6 +609,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
name: "create_branch",
description: "Create a new branch in a GitHub repository",
inputSchema: zodToJsonSchema(CreateBranchSchema)
},
{
name: "list_issues",
description: "List issues in a GitHub repository with filtering options",
inputSchema: zodToJsonSchema(ListIssuesOptionsSchema)
},
{
name: "update_issue",
description: "Update an existing issue in a GitHub repository",
inputSchema: zodToJsonSchema(UpdateIssueOptionsSchema)
},
{
name: "add_issue_comment",
description: "Add a comment to an existing issue",
inputSchema: zodToJsonSchema(IssueCommentSchema)
}
]
};
Expand Down Expand Up @@ -623,6 +733,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
return { content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }] };
}

case "list_issues": {
const args = ListIssuesOptionsSchema.parse(request.params.arguments);
const { owner, repo, ...options } = args;
const issues = await listIssues(owner, repo, options);
return { toolResult: issues };
}

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 };
}

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 };
}

default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
Expand Down
33 changes: 33 additions & 0 deletions src/github/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,39 @@ export const CreateBranchSchema = RepoParamsSchema.extend({
.describe("Optional: source branch to create from (defaults to the repository's default branch)")
});

// Add these schema definitions for issue management

export const ListIssuesOptionsSchema = z.object({
owner: z.string(),
repo: z.string(),
state: z.enum(['open', 'closed', 'all']).optional(),
labels: z.array(z.string()).optional(),
sort: z.enum(['created', 'updated', 'comments']).optional(),
direction: z.enum(['asc', 'desc']).optional(),
since: z.string().optional(), // ISO 8601 timestamp
page: z.number().optional(),
per_page: z.number().optional()
});

export const UpdateIssueOptionsSchema = z.object({
owner: z.string(),
repo: z.string(),
issue_number: z.number(),
title: z.string().optional(),
body: z.string().optional(),
state: z.enum(['open', 'closed']).optional(),
labels: z.array(z.string()).optional(),
assignees: z.array(z.string()).optional(),
milestone: z.number().optional()
});

export const IssueCommentSchema = z.object({
owner: z.string(),
repo: z.string(),
issue_number: z.number(),
body: z.string()
});

// Export types
export type GitHubAuthor = z.infer<typeof GitHubAuthorSchema>;
export type GitHubFork = z.infer<typeof GitHubForkSchema>;
Expand Down
Loading