github-project-manager.ts•53.6 kB
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import {
GitHubIssueService,
GitHubProjectService,
GitHubPullRequestService,
GitHubMilestoneService,
} from '../services/index.js';
import { AuthenticationError, ResourceNotFoundError, ValidationError } from '../errors/index.js';
// Environment variable for GitHub token
/**
* Initialize and start the MCP GitHub Project Manager server
*/
export async function startGitHubProjectManagerServer(token: string) {
// Use provided token or environment variable
const githubToken = token;
// Create services
const issueService = new GitHubIssueService(githubToken);
const projectService = new GitHubProjectService(githubToken);
const pullRequestService = new GitHubPullRequestService(githubToken);
const milestoneService = new GitHubMilestoneService(githubToken);
// Create a new MCP server
const server = new McpServer({
name: 'MCP GitHub Project Manager',
version: '1.0.0',
});
// 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);
// Register GitHub Milestone Management tools
registerMilestoneTools(server, milestoneService);
// Set up server transport using Standard I/O
const transport = new StdioServerTransport();
await server.connect(transport);
// 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) => {
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) => {
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) => {
try {
const issues = await issueService.listIssues(args);
// Format the response to include only necessary information
const formattedIssues = issues.map((issue) => {
// Handle labels which can be strings or objects
const formattedLabels = Array.isArray(issue.labels)
? issue.labels.map((label) => {
if (typeof label === 'string') {
return { name: label, color: '' };
}
return {
name: label.name || '',
color: label.color || '',
};
})
: [];
// Handle assignees
const formattedAssignees = Array.isArray(issue.assignees)
? issue.assignees.map((assignee) => assignee.login)
: [];
return {
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: formattedLabels,
assignees: formattedAssignees,
};
});
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) => {
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: Array.isArray(issue.labels)
? issue.labels.map((label) => {
if (typeof label === 'string') {
return { name: label, color: '' };
}
return {
name: label.name || '',
color: label.color || '',
};
})
: [],
assignees:
issue.assignees?.map((assignee) =>
typeof assignee === 'string' ? assignee : 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) => {
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) => {
try {
const project = await projectService.createProject(args);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify({
success: true,
project: {
id: project.id,
title: project.title,
url: project.url,
number: project.number,
createdAt: project.createdAt,
},
}),
},
],
};
} catch (error) {
return handleToolError(error);
}
},
);
// Get Project Fields Tool
server.tool(
'get_project_fields',
'Get all fields available in a GitHub project',
{
projectId: z.string().describe('Project ID (GraphQL node ID)'),
},
async (args) => {
try {
const fields = await projectService.getProjectFields(args);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify({
success: true,
fields: fields.map((field) => ({
id: field.id,
name: field.name,
type: field.type,
options: field.options,
})),
}),
},
],
};
} catch (error) {
return handleToolError(error);
}
},
);
// Get Project Columns Tool
server.tool(
'get_project_columns',
'Get columns (status options) available in a GitHub project',
{
projectId: z.string().describe('Project ID (GraphQL node ID)'),
},
async (args) => {
try {
const { statusFieldId, columns } = await projectService.getProjectColumns(args);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify({
success: true,
statusFieldId,
columns,
}),
},
],
};
} 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 (GraphQL node ID)'),
contentId: z.string().describe('Issue or PR ID (GraphQL node ID)'),
},
async (args) => {
try {
const item = await projectService.addProjectItem(args);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify({
success: true,
item: {
id: item.id,
},
}),
},
],
};
} catch (error) {
return handleToolError(error);
}
},
);
// Add Project Item with Column Tool
server.tool(
'add_project_item_with_column',
'Add an issue or pull request to a GitHub project and place it in a specific column',
{
projectId: z.string().describe('Project ID (GraphQL node ID)'),
contentId: z.string().describe('Issue or PR ID (GraphQL node ID)'),
fieldId: z.string().describe('Status field ID from get_project_columns'),
columnId: z.string().describe('Column (status option) ID from get_project_columns'),
},
async (args) => {
try {
const item = await projectService.addProjectItemWithColumn(args);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify({
success: true,
item: {
id: item.id,
message: 'Item added and placed in the specified column',
},
}),
},
],
};
} catch (error) {
return handleToolError(error);
}
},
);
// Update Project Item Tool
server.tool(
'update_project_item',
'Move an item between columns in a GitHub project',
{
projectId: z.string().describe('Project ID (GraphQL node ID)'),
itemId: z.string().describe('Item ID to move (GraphQL node ID)'),
fieldId: z.string().describe('Status field ID from get_project_columns'),
columnId: z.string().describe('Column ID to move the item to (from get_project_columns)'),
},
async (args) => {
try {
await projectService.updateProjectItem(args);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify({
success: true,
message: `Item ${args.itemId} moved to column ${args.columnId}`,
}),
},
],
};
} 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 (GraphQL node ID)'),
first: z.number().optional().describe('Number of items to return (default: 20)'),
},
async (args) => {
try {
const items = await projectService.listProjectItems(args);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify({
success: true,
count: items.length,
items,
}),
},
],
};
} catch (error) {
return handleToolError(error);
}
},
);
// List Projects Tool
server.tool(
'list_projects',
'List GitHub projects for an organization or user',
{
owner: z.string().describe('Organization name or username'),
first: z.number().optional().describe('Number of projects to return (default: 20)'),
},
async (args) => {
try {
const projects = await projectService.listProjects(args);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify({
success: true,
count: projects.length,
projects: projects.map((project) => ({
id: project.id,
title: project.title,
url: project.url,
number: project.number,
shortDescription: project.shortDescription,
createdAt: project.createdAt,
closed: project.closed,
})),
}),
},
],
};
} 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) => {
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) => {
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) => {
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) => {
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) => {
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) => {
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) => {
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) => {
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) => {
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) => {
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) => {
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) => {
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) => {
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);
}
},
);
}
/**
* Register GitHub Milestone Management tools
*/
function registerMilestoneTools(server: McpServer, milestoneService: GitHubMilestoneService) {
// Create Milestone Tool
server.tool(
'create_milestone',
'Create a new milestone in a GitHub repository',
{
owner: z.string().describe('Repository owner (username or organization)'),
repo: z.string().describe('Repository name'),
title: z.string().describe('Milestone title'),
description: z.string().optional().describe('Milestone description'),
due_on: z.string().optional().describe('Due date for the milestone (ISO 8601 format)'),
state: z.enum(['open', 'closed']).optional().describe('Milestone state'),
},
async (args) => {
try {
const milestone = await milestoneService.createMilestone(args);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify({
success: true,
milestone: {
number: milestone.number,
title: milestone.title,
description: milestone.description,
state: milestone.state,
due_on: milestone.due_on,
created_at: milestone.created_at,
html_url: milestone.html_url,
},
}),
},
],
};
} catch (error) {
return handleToolError(error);
}
},
);
// Update Milestone Tool
server.tool(
'update_milestone',
'Update an existing milestone in a GitHub repository',
{
owner: z.string().describe('Repository owner (username or organization)'),
repo: z.string().describe('Repository name'),
milestone_number: z.number().describe('Milestone number'),
title: z.string().optional().describe('New milestone title'),
description: z.string().optional().describe('New milestone description'),
due_on: z.string().optional().describe('New due date for the milestone (ISO 8601 format)'),
state: z.enum(['open', 'closed']).optional().describe('New milestone state'),
},
async (args) => {
try {
const milestone = await milestoneService.updateMilestone(args);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify({
success: true,
milestone: {
number: milestone.number,
title: milestone.title,
description: milestone.description,
state: milestone.state,
due_on: milestone.due_on,
updated_at: milestone.updated_at,
html_url: milestone.html_url,
},
}),
},
],
};
} catch (error) {
return handleToolError(error);
}
},
);
// Get Milestone Tool
server.tool(
'get_milestone',
'Get details of a specific milestone in a GitHub repository',
{
owner: z.string().describe('Repository owner (username or organization)'),
repo: z.string().describe('Repository name'),
milestone_number: z.number().describe('Milestone number'),
},
async (args) => {
try {
const milestone = await milestoneService.getMilestone(args);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify({
success: true,
milestone: {
number: milestone.number,
title: milestone.title,
description: milestone.description,
state: milestone.state,
open_issues: milestone.open_issues,
closed_issues: milestone.closed_issues,
due_on: milestone.due_on,
created_at: milestone.created_at,
updated_at: milestone.updated_at,
closed_at: milestone.closed_at,
html_url: milestone.html_url,
},
}),
},
],
};
} catch (error) {
return handleToolError(error);
}
},
);
// List Milestones Tool
server.tool(
'list_milestones',
'List milestones 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('Milestone state'),
sort: z.enum(['due_on', 'completeness']).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) => {
try {
const milestones = await milestoneService.listMilestones(args);
// Format the response to include only necessary information
const formattedMilestones = milestones.map((milestone) => ({
number: milestone.number,
title: milestone.title,
description: milestone.description,
state: milestone.state,
open_issues: milestone.open_issues,
closed_issues: milestone.closed_issues,
due_on: milestone.due_on,
created_at: milestone.created_at,
updated_at: milestone.updated_at,
closed_at: milestone.closed_at,
html_url: milestone.html_url,
}));
return {
content: [
{
type: 'text' as const,
text: JSON.stringify({
success: true,
count: formattedMilestones.length,
milestones: formattedMilestones,
}),
},
],
};
} catch (error) {
return handleToolError(error);
}
},
);
// Delete Milestone Tool
server.tool(
'delete_milestone',
'Delete a milestone from a GitHub repository',
{
owner: z.string().describe('Repository owner (username or organization)'),
repo: z.string().describe('Repository name'),
milestone_number: z.number().describe('Milestone number'),
},
async (args) => {
try {
const { owner, repo, milestone_number } = args;
await milestoneService.deleteMilestone(owner, repo, milestone_number);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify({
success: true,
message: `Milestone ${milestone_number} was successfully deleted`,
}),
},
],
};
} 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,
},
}),
},
],
};
}