feat: add issue management functionalities for github

- Implemented `listIssues`, `updateIssue`, and `addIssueComment` functions to manage GitHub issues.
- Introduced corresponding schemas: `ListIssuesOptionsSchema`, `UpdateIssueOptionsSchema`, and `IssueCommentSchema`.
- Updated server request handlers to support new functionalities.
- Enhanced README with documentation for new features.
This commit is contained in:
Jerad Bitner
2024-11-28 13:59:37 -08:00
parent 6d83c78752
commit 08015830a6
3 changed files with 202 additions and 1 deletions

View File

@@ -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

View File

@@ -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';
@@ -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: [
@@ -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)
}
]
};
@@ -623,6 +733,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
return { toolResult: pullRequest };
}
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}`);
}

View File

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