Skip to content

Commit

Permalink
Merge pull request #119 from sirkitree/feat__github-issue-tools
Browse files Browse the repository at this point in the history
feat: add issue management functionalities for github
  • Loading branch information
jspahrsummers authored Dec 5, 2024
2 parents 0724ae4 + d300fec commit 370198d
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 38 deletions.
43 changes: 40 additions & 3 deletions src/github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
199 changes: 164 additions & 35 deletions src/github/index.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,61 @@
#!/usr/bin/env node

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import fetch from "node-fetch";
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import {
CreateBranchOptionsSchema,
CreateBranchSchema,
CreateIssueOptionsSchema,
CreateIssueSchema,
CreateOrUpdateFileSchema,
CreatePullRequestOptionsSchema,
CreatePullRequestSchema,
CreateRepositoryOptionsSchema,
CreateRepositorySchema,
ForkRepositorySchema,
GetFileContentsSchema,
GitHubCommitSchema,
GitHubContentSchema,
GitHubCreateUpdateFileResponseSchema,
GitHubForkSchema,
GitHubReferenceSchema,
GitHubRepositorySchema,
GitHubIssueSchema,
GitHubPullRequestSchema,
GitHubContentSchema,
GitHubCreateUpdateFileResponseSchema,
GitHubReferenceSchema,
GitHubRepositorySchema,
GitHubSearchResponseSchema,
GitHubTreeSchema,
GitHubCommitSchema,
CreateRepositoryOptionsSchema,
CreateIssueOptionsSchema,
CreatePullRequestOptionsSchema,
CreateBranchOptionsSchema,
IssueCommentSchema,
ListIssuesOptionsSchema,
PushFilesSchema,
SearchCodeResponseSchema,
SearchCodeSchema,
SearchIssuesResponseSchema,
SearchIssuesSchema,
SearchRepositoriesSchema,
SearchUsersResponseSchema,
SearchUsersSchema,
UpdateIssueOptionsSchema,
type FileOperation,
type GitHubCommit,
type GitHubContent,
type GitHubCreateUpdateFileResponse,
type GitHubFork,
type GitHubReference,
type GitHubRepository,
type GitHubIssue,
type GitHubPullRequest,
type GitHubContent,
type GitHubCreateUpdateFileResponse,
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(
{
Expand Down Expand Up @@ -486,6 +487,98 @@ async function createRepository(
return GitHubRepositorySchema.parse(await response.json());
}

async function listIssues(
owner: string,
repo: string,
options: Omit<z.infer<typeof ListIssuesOptionsSchema>, 'owner' | 'repo'>
): 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: Omit<z.infer<typeof UpdateIssueOptionsSchema>, 'owner' | 'repo' | 'issue_number'>
): 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());
}

async function searchCode(
params: z.infer<typeof SearchCodeSchema>
): Promise<SearchCodeResponse> {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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}`);
}
Expand Down
33 changes: 33 additions & 0 deletions src/github/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof GitHubAuthorSchema>;
export type GitHubFork = z.infer<typeof GitHubForkSchema>;
Expand Down

0 comments on commit 370198d

Please sign in to comment.