Skip to main content
Glama

Github Project Manager

github-project-manager-sse.ts40.1 kB
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { z } from 'zod'; import { GitHubIssueService, GitHubProjectService, GitHubPullRequestService } from '../services/index.js'; import { AuthenticationError, ResourceNotFoundError, ValidationError } from '../errors/index.js'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import express from 'express'; // Environment variable for GitHub token /** * Initialize and start the MCP GitHub Project Manager server */ export async function startGitHubProjectManagerServerSSE(token: string, port: number) { // Use provided token or environment variable const githubToken = token; // Validate GitHub token if (!githubToken) { console.error('GITHUB_TOKEN environment variable is required'); process.exit(1); } // Create services const issueService = new GitHubIssueService(githubToken); const projectService = new GitHubProjectService(githubToken); const pullRequestService = new GitHubPullRequestService(githubToken); // Create a new MCP server const server = new McpServer({ name: 'MCP GitHub Project Manager', version: '1.0.0', }); const app = express(); // Register GitHub Issue Management tools registerIssueTools(server, issueService); // Register GitHub Project Management tools registerProjectTools(server, projectService); // Register GitHub Pull Request Management tools registerPullRequestTools(server, pullRequestService); let transport: SSEServerTransport | null = null; app.get('/sse', (_req: express.Request, res: express.Response) => { transport = new SSEServerTransport('/messages', res); server.connect(transport); }); app.post('/messages', (req: express.Request, res: express.Response) => { if (transport) { transport.handlePostMessage(req, res); } }); app.listen(port); // Keep the server running return server; } /** * Register GitHub Issue Management tools */ function registerIssueTools(server: McpServer, issueService: GitHubIssueService) { // Create Issue Tool server.tool( 'create_issue', 'Create a new issue in a GitHub repository', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), title: z.string().describe('Issue title'), body: z.string().optional().describe('Issue body/description'), labels: z.array(z.string()).optional().describe('Labels to add to this issue'), assignees: z.array(z.string()).optional().describe('GitHub usernames to assign to this issue'), milestone: z.number().optional().describe('Milestone number to associate with this issue'), }, async (args, _extra) => { try { const issue = await issueService.createIssue(args); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, issue: { number: issue.number, title: issue.title, html_url: issue.html_url, state: issue.state, created_at: issue.created_at, }, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // Update Issue Tool server.tool( 'update_issue', 'Update an existing issue in a GitHub repository', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), issue_number: z.number().describe('Issue number'), title: z.string().optional().describe('New issue title'), body: z.string().optional().describe('New issue body'), state: z.enum(['open', 'closed']).optional().describe('New issue state'), labels: z.array(z.string()).optional().describe('Labels to set'), assignees: z.array(z.string()).optional().describe('GitHub usernames to assign'), milestone: z.number().nullable().optional().describe('Milestone to set'), }, async (args, _extra) => { try { const issue = await issueService.updateIssue(args); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, issue: { number: issue.number, title: issue.title, html_url: issue.html_url, state: issue.state, updated_at: issue.updated_at, }, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // List Issues Tool server.tool( 'list_issues', 'List issues in a GitHub repository with filtering options', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), state: z.enum(['open', 'closed', 'all']).optional().describe('Issue state'), sort: z.enum(['created', 'updated', 'comments']).optional().describe('Sort field'), direction: z.enum(['asc', 'desc']).optional().describe('Sort direction'), since: z.string().optional().describe('Filter by updated date (ISO 8601 format)'), per_page: z.number().optional().describe('Results per page'), page: z.number().optional().describe('Page number'), labels: z.array(z.string()).optional().describe('Filter by labels'), assignee: z.string().optional().describe('Filter by assignee'), creator: z.string().optional().describe('Filter by creator'), mentioned: z.string().optional().describe('Filter by mentioned user'), milestone: z.string().optional().describe('Filter by milestone number or title'), }, async (args, _extra) => { try { const issues = await issueService.listIssues(args); // Format the response to include only necessary information const formattedIssues = issues.map((issue: any) => ({ number: issue.number, title: issue.title, state: issue.state, html_url: issue.html_url, created_at: issue.created_at, updated_at: issue.updated_at, labels: issue.labels.map((label: any) => ({ name: label.name, color: label.color, })), assignees: issue.assignees.map((assignee: any) => assignee.login), })); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, count: formattedIssues.length, issues: formattedIssues, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // Get Issue Tool server.tool( 'get_issue', 'Get details of a specific issue in a GitHub repository.', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), issue_number: z.number().describe('Issue number'), }, async (args, _extra) => { try { const issue = await issueService.getIssue(args); // Format the response const formattedIssue = { number: issue.number, title: issue.title, body: issue.body, state: issue.state, html_url: issue.html_url, created_at: issue.created_at, updated_at: issue.updated_at, closed_at: issue.closed_at, labels: issue.labels.map((label: any) => ({ name: label.name, color: label.color, })), assignees: issue.assignees?.map((assignee: any) => assignee.login) || [], milestone: issue.milestone ? { number: issue.milestone.number, title: issue.milestone.title, } : null, comments: issue.comments, }; return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, issue: formattedIssue, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // Add Issue Comment Tool server.tool( 'add_issue_comment', 'Add a comment to an existing issue', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), issue_number: z.number().describe('Issue number'), body: z.string().describe('Comment text'), }, async (args, _extra) => { try { const comment = await issueService.addIssueComment(args); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, comment: { id: comment.id, body: comment.body, html_url: comment.html_url, created_at: comment.created_at, }, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); } /** * Register GitHub Project Management tools */ function registerProjectTools(server: McpServer, projectService: GitHubProjectService) { // Create Project Tool server.tool( 'create_project', 'Create a new GitHub project board', { owner: z.string().describe('Organization name or username'), name: z.string().describe('Project name'), body: z.string().optional().describe('Project description'), }, async (args, _extra) => { try { const project = await projectService.createProject(args); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, project: { id: project.id, name: project.name, html_url: project.html_url, created_at: project.created_at, }, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // Add Project Item Tool server.tool( 'add_project_item', 'Add an issue or pull request to a GitHub project', { projectId: z.string().describe('Project ID'), contentId: z.string().describe('Issue or PR ID'), content_type: z.enum(['Issue', 'PullRequest']).describe('Type of content to add'), }, async (args, _extra) => { try { const item = await projectService.addProjectItem(args); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, card: { id: item.id, url: item.url, created_at: item.created_at, }, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // Update Project Item Tool server.tool( 'update_project_item', 'Move an item between columns in a GitHub project', { project_id: z.number().describe('Project ID'), item_id: z.number().describe('Card ID to move'), column_id: z.number().describe('Column ID to move the card to'), position: z .union([z.enum(['top', 'bottom']), z.number()]) .optional() .describe('Position in the column (top, bottom, or specific position)'), }, async (args, _extra) => { try { await projectService.updateProjectItem({ projectId: String(args.project_id), itemId: String(args.item_id), columnId: String(args.column_id), position: args.position, }); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, message: `Card ${args.item_id} moved to column ${args.column_id}`, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // List Project Items Tool server.tool( 'list_project_items', 'List items in a GitHub project', { projectId: z.string().describe('Project ID'), columnId: z.string().optional().describe('Column ID to filter by'), }, async (args, _extra) => { try { const cards = await projectService.listProjectItems(args); // Format the response const formattedCards = cards.map((card: any) => ({ id: card.id, url: card.url, created_at: card.created_at, updated_at: card.updated_at, column: card.column, content_url: card.content_url, note: card.note, })); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, count: formattedCards.length, cards: formattedCards, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); } /** * Register GitHub Pull Request Management tools */ function registerPullRequestTools(server: McpServer, pullRequestService: GitHubPullRequestService) { // Create Pull Request Tool server.tool( 'create_pull_request', 'Create a new pull request in a GitHub repository', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), title: z.string().describe('Pull request title'), body: z.string().optional().describe('Pull request body/description'), head: z.string().describe('The name of the branch where your changes are implemented'), base: z.string().describe('The name of the branch you want the changes pulled into'), draft: z.boolean().optional().describe('Whether to create the pull request as a draft'), maintainer_can_modify: z.boolean().optional().describe('Whether maintainers can modify the pull request'), }, async (args, _extra) => { try { const pullRequest = await pullRequestService.createPullRequest(args); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, pull_request: { number: pullRequest.number, title: pullRequest.title, html_url: pullRequest.html_url, state: pullRequest.state, created_at: pullRequest.created_at, }, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // Update Pull Request Tool server.tool( 'update_pull_request', 'Update an existing pull request in a GitHub repository', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), pull_number: z.number().describe('Pull request number'), title: z.string().optional().describe('New pull request title'), body: z.string().optional().describe('New pull request body'), state: z.enum(['open', 'closed']).optional().describe('New pull request state'), base: z.string().optional().describe('The name of the branch you want the changes pulled into'), maintainer_can_modify: z.boolean().optional().describe('Whether maintainers can modify the pull request'), }, async (args, _extra) => { try { const pullRequest = await pullRequestService.updatePullRequest(args); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, pull_request: { number: pullRequest.number, title: pullRequest.title, html_url: pullRequest.html_url, state: pullRequest.state, updated_at: pullRequest.updated_at, }, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // List Pull Requests Tool server.tool( 'list_pull_requests', 'List pull requests in a GitHub repository with filtering options', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), state: z.enum(['open', 'closed', 'all']).optional().describe('Pull request state'), head: z.string().optional().describe('Filter by head branch'), base: z.string().optional().describe('Filter by base branch'), sort: z.enum(['created', 'updated', 'popularity', 'long-running']).optional().describe('Sort field'), direction: z.enum(['asc', 'desc']).optional().describe('Sort direction'), per_page: z.number().optional().describe('Results per page'), page: z.number().optional().describe('Page number'), }, async (args, _extra) => { try { const pullRequests = await pullRequestService.listPullRequests(args); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, pull_requests: pullRequests.map((pr) => ({ number: pr.number, title: pr.title, html_url: pr.html_url, state: pr.state, created_at: pr.created_at, updated_at: pr.updated_at, })), }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // Get Pull Request Tool server.tool( 'get_pull_request', 'Get details of a specific pull request in a GitHub repository', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), pull_number: z.number().describe('Pull request number'), }, async (args, _extra) => { try { const pullRequest = await pullRequestService.getPullRequest(args); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, pull_request: { number: pullRequest.number, title: pullRequest.title, body: pullRequest.body, html_url: pullRequest.html_url, state: pullRequest.state, created_at: pullRequest.created_at, updated_at: pullRequest.updated_at, merged_at: pullRequest.merged_at, head: pullRequest.head, base: pullRequest.base, user: pullRequest.user, assignees: pullRequest.assignees, requested_reviewers: pullRequest.requested_reviewers, labels: pullRequest.labels, draft: pullRequest.draft, }, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // Merge Pull Request Tool server.tool( 'merge_pull_request', 'Merge a pull request', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), pull_number: z.number().describe('Pull request number'), commit_title: z.string().optional().describe('Title for the automatic commit message'), commit_message: z.string().optional().describe('Extra detail to append to automatic commit message'), merge_method: z.enum(['merge', 'squash', 'rebase']).optional().describe('Merge method to use'), }, async (args, _extra) => { try { const result = await pullRequestService.mergePullRequest(args); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, merged: result.merged, message: result.message, sha: result.sha, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // Check If Pull Request Is Merged Tool server.tool( 'is_pull_request_merged', 'Check if a pull request has been merged', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), pull_number: z.number().describe('Pull request number'), }, async (args, _extra) => { try { const { owner, repo, pull_number } = args; const isMerged = await pullRequestService.isPullRequestMerged(owner, repo, pull_number); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, merged: isMerged, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // Create Pull Request Review Tool server.tool( 'create_pull_request_review', 'Create a review for a pull request', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), pull_number: z.number().describe('Pull request number'), body: z.string().optional().describe('The body text of the review'), event: z .enum(['APPROVE', 'REQUEST_CHANGES', 'COMMENT']) .optional() .describe('The review action to perform'), comments: z .array( z.object({ path: z.string().describe('The relative path to the file being commented on'), position: z.number().describe('The position in the diff where the comment should be placed'), body: z.string().describe('The text of the comment'), }), ) .optional() .describe('Comments to post as part of the review'), }, async (args, _extra) => { try { const review = await pullRequestService.createPullRequestReview(args); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, review: { id: review.id, body: review.body, state: review.state, html_url: review.html_url, user: review.user, submitted_at: review.submitted_at, }, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // List Pull Request Reviews Tool server.tool( 'list_pull_request_reviews', 'List reviews for a pull request', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), pull_number: z.number().describe('Pull request number'), per_page: z.number().optional().describe('Results per page'), page: z.number().optional().describe('Page number'), }, async (args, _extra) => { try { const reviews = await pullRequestService.listPullRequestReviews(args); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, reviews: reviews.map((review) => ({ id: review.id, body: review.body, state: review.state, html_url: review.html_url, user: review.user, submitted_at: review.submitted_at, })), }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // Create Pull Request Review Comment Tool server.tool( 'create_pull_request_review_comment', 'Create a review comment for a pull request', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), pull_number: z.number().describe('Pull request number'), body: z.string().describe('The text of the review comment'), commit_id: z.string().optional().describe('The SHA of the commit to comment on'), path: z.string().optional().describe('The relative path to the file being commented on'), position: z.number().optional().describe('The position in the diff where the comment should be placed'), in_reply_to: z.number().optional().describe('The comment ID to reply to'), }, async (args, _extra) => { try { const comment = await pullRequestService.createPullRequestReviewComment(args); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, comment: { id: comment.id, body: comment.body, html_url: comment.html_url, user: comment.user, created_at: comment.created_at, }, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // List Pull Request Review Comments Tool server.tool( 'list_pull_request_review_comments', 'List review comments for a pull request', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), pull_number: z.number().describe('Pull request number'), sort: z.enum(['created', 'updated']).optional().describe('Sort field'), direction: z.enum(['asc', 'desc']).optional().describe('Sort direction'), since: z.string().optional().describe('Only comments updated at or after this time are returned'), per_page: z.number().optional().describe('Results per page'), page: z.number().optional().describe('Page number'), }, async (args, _extra) => { try { const comments = await pullRequestService.listPullRequestReviewComments(args); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, comments: comments.map((comment) => ({ id: comment.id, body: comment.body, html_url: comment.html_url, user: comment.user, created_at: comment.created_at, updated_at: comment.updated_at, })), }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // Request Reviewers Tool server.tool( 'request_reviewers', 'Request reviewers for a pull request', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), pull_number: z.number().describe('Pull request number'), reviewers: z.array(z.string()).optional().describe('Usernames of people to request a review from'), team_reviewers: z.array(z.string()).optional().describe('Names of teams to request a review from'), }, async (args, _extra) => { try { const pullRequest = await pullRequestService.requestReviewers(args); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, pull_request: { number: pullRequest.number, requested_reviewers: pullRequest.requested_reviewers, requested_teams: pullRequest.requested_teams, }, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // Remove Requested Reviewers Tool server.tool( 'remove_requested_reviewers', 'Remove requested reviewers from a pull request', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), pull_number: z.number().describe('Pull request number'), reviewers: z.array(z.string()).describe('Usernames of people to remove from the review request'), team_reviewers: z.array(z.string()).optional().describe('Names of teams to remove from the review request'), }, async (args, _extra) => { try { const { owner, repo, pull_number, reviewers, team_reviewers } = args; const pullRequest = await pullRequestService.removeRequestedReviewers( owner, repo, pull_number, reviewers, team_reviewers, ); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, pull_request: { number: pullRequest.number, requested_reviewers: pullRequest.requested_reviewers, requested_teams: pullRequest.requested_teams, }, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); // Update Pull Request Branch Tool server.tool( 'update_pull_request_branch', 'Update a pull request branch with the latest upstream changes', { owner: z.string().describe('Repository owner (username or organization)'), repo: z.string().describe('Repository name'), pull_number: z.number().describe('Pull request number'), expected_head_sha: z.string().optional().describe('The expected SHA of the pull request head'), }, async (args, _extra) => { try { const result = await pullRequestService.updatePullRequestBranch(args); return { content: [ { type: 'text' as const, text: JSON.stringify({ success: true, message: result.message, url: result.url, }), }, ], }; } catch (error) { return handleToolError(error); } }, ); } /** * Handle errors from tool execution */ function handleToolError(error: unknown) { let errorMessage = 'An unknown error occurred'; let errorType = 'UnknownError'; if (error instanceof AuthenticationError) { errorMessage = error.message; errorType = 'AuthenticationError'; } else if (error instanceof ResourceNotFoundError) { errorMessage = error.message; errorType = 'ResourceNotFoundError'; } else if (error instanceof ValidationError) { errorMessage = error.message; errorType = 'ValidationError'; } else if (error instanceof Error) { errorMessage = error.message; errorType = error.name; } return { content: [ { type: 'text' as const, text: JSON.stringify({ success: false, error: { type: errorType, message: errorMessage, }, }), }, ], }; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Monsoft-Solutions/model-context-protocols'

If you have feedback or need assistance with the MCP directory API, please join our Discord server