diff --git a/src/github/README.md b/src/github/README.md index b5b0bfa6..9d98b1b6 100644 --- a/src/github/README.md +++ b/src/github/README.md @@ -103,7 +103,44 @@ 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. `search_code` +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 + +13. `search_code` - Search for code across GitHub repositories - Inputs: - `q` (string): Search query using GitHub code search syntax @@ -113,7 +150,7 @@ MCP Server for the GitHub API, enabling file operations, repository management, - `page` (optional number): Page number - Returns: Code search results with repository context -11. `search_issues` +14. `search_issues` - Search for issues and pull requests - Inputs: - `q` (string): Search query using GitHub issues search syntax @@ -123,7 +160,7 @@ MCP Server for the GitHub API, enabling file operations, repository management, - `page` (optional number): Page number - Returns: Issue and pull request search results -12. `search_users` +15. `search_users` - Search for GitHub users - Inputs: - `q` (string): Search query using GitHub users search syntax diff --git a/src/github/index.ts b/src/github/index.ts index 2d73dee4..a8341a9d 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -1,5 +1,4 @@ #!/usr/bin/env node - import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { @@ -7,54 +6,56 @@ import { ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import fetch from "node-fetch"; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; import { - GitHubForkSchema, - GitHubReferenceSchema, - GitHubRepositorySchema, - GitHubIssueSchema, - GitHubPullRequestSchema, + CreateBranchOptionsSchema, + CreateBranchSchema, + CreateIssueOptionsSchema, + CreateIssueSchema, + CreateOrUpdateFileSchema, + CreatePullRequestOptionsSchema, + CreatePullRequestSchema, + CreateRepositoryOptionsSchema, + CreateRepositorySchema, + ForkRepositorySchema, + GetFileContentsSchema, + GitHubCommitSchema, GitHubContentSchema, GitHubCreateUpdateFileResponseSchema, + GitHubForkSchema, + GitHubIssueSchema, + GitHubPullRequestSchema, + GitHubReferenceSchema, + GitHubRepositorySchema, GitHubSearchResponseSchema, GitHubTreeSchema, - GitHubCommitSchema, - CreateRepositoryOptionsSchema, - CreateIssueOptionsSchema, - CreatePullRequestOptionsSchema, - CreateBranchOptionsSchema, - type GitHubFork, - type GitHubReference, - type GitHubRepository, - type GitHubIssue, - type GitHubPullRequest, + IssueCommentSchema, + ListIssuesOptionsSchema, + PushFilesSchema, + SearchCodeResponseSchema, + SearchCodeSchema, + SearchIssuesResponseSchema, + SearchIssuesSchema, + SearchRepositoriesSchema, + SearchUsersResponseSchema, + SearchUsersSchema, + UpdateIssueOptionsSchema, + type FileOperation, + type GitHubCommit, type GitHubContent, type GitHubCreateUpdateFileResponse, + type GitHubFork, + type GitHubIssue, + type GitHubPullRequest, + type GitHubReference, + type GitHubRepository, type GitHubSearchResponse, type GitHubTree, - type GitHubCommit, - type FileOperation, - CreateOrUpdateFileSchema, - SearchRepositoriesSchema, - CreateRepositorySchema, - GetFileContentsSchema, - PushFilesSchema, - CreateIssueSchema, - CreatePullRequestSchema, - ForkRepositorySchema, - CreateBranchSchema, - SearchCodeSchema, - SearchIssuesSchema, - SearchUsersSchema, - SearchCodeResponseSchema, - SearchIssuesResponseSchema, - SearchUsersResponseSchema, type SearchCodeResponse, type SearchIssuesResponse, type SearchUsersResponse, -} from "./schemas.js"; -import { zodToJsonSchema } from "zod-to-json-schema"; -import { z } from "zod"; -import type { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +} from './schemas.js'; const server = new Server( { @@ -486,6 +487,98 @@ async function createRepository( return GitHubRepositorySchema.parse(await response.json()); } +async function listIssues( + owner: string, + repo: string, + options: Omit, 'owner' | 'repo'> +): 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: Omit, 'owner' | 'repo' | 'issue_number'> +): 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()); +} + async function searchCode( params: z.infer ): Promise { @@ -612,6 +705,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { 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) + }, { name: "search_code", description: "Search for code across GitHub repositories", @@ -795,6 +903,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } + 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 b9c81a15..f40da150 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -616,6 +616,39 @@ export const SearchUsersSchema = z.object({ page: z.number().min(1).optional().describe("Page number"), }); +// 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;