From 08015830a68f4dfa2cc56e40cdd82538ea85322c Mon Sep 17 00:00:00 2001 From: Jerad Bitner Date: Thu, 28 Nov 2024 13:59:37 -0800 Subject: [PATCH] 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. --- src/github/README.md | 37 ++++++++++++ src/github/index.ts | 133 +++++++++++++++++++++++++++++++++++++++++- src/github/schemas.ts | 33 +++++++++++ 3 files changed, 202 insertions(+), 1 deletion(-) diff --git a/src/github/README.md b/src/github/README.md index cfd268a8..a3ce0a13 100644 --- a/src/github/README.md +++ b/src/github/README.md @@ -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 diff --git a/src/github/index.ts b/src/github/index.ts index 0676a34c..ab691e38 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -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 +): Promise { + 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 +): Promise { + 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> { + 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}`); } diff --git a/src/github/schemas.ts b/src/github/schemas.ts index 213458eb..e57ea6d9 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -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; export type GitHubFork = z.infer;