Skip to main content
Glama

mcp-github-project-manager

GitHubSprintRepository.ts10.9 kB
import { BaseGitHubRepository } from "./BaseRepository"; import { IssueId, Sprint, SprintId, SprintRepository, Issue } from "../../../domain/types"; import { ResourceStatus, ResourceType } from "../../../domain/resource-types"; import { GitHubIssueRepository } from "./GitHubIssueRepository"; import { GitHubConfig } from "../GitHubConfig"; // Import the class, not the interface import { CreateProjectV2FieldResponse, GraphQLResponse, } from "../util/graphql-helpers"; interface GetIterationFieldResponse { node: { iteration: { id: string; title: string; startDate: string; duration: number; items?: { nodes?: Array<{ content: { number: number; }; }>; }; }; }; } interface ListIterationFieldsResponse { repository: { projectsV2: { nodes: Array<{ fields: { nodes: Array<{ id?: string; name?: string; configuration?: { iterations: Array<{ id: string; title: string; startDate: string; duration: number; }>; }; }>; }; }>; }; }; } // Note: GitHub Projects V2 API doesn't support creating individual iterations // Iterations are managed through the project's iteration field configuration export class GitHubSprintRepository extends BaseGitHubRepository implements SprintRepository { private readonly factory: any; constructor(octokit: any, config: GitHubConfig) { super(octokit, config); // We need to add a factory field to the class in order to create other repositories this.factory = { createIssueRepository: () => { return new GitHubIssueRepository(octokit, config); } }; } async create(data: Omit<Sprint, "id" | "createdAt" | "updatedAt" | "type">): Promise<Sprint> { // GitHub Projects V2 doesn't support creating individual iterations via API // Iterations are managed through the project's iteration field configuration // For now, we'll create a mock sprint that represents the data structure const sprintId = `sprint_${Date.now()}`; const sprint: Sprint = { id: sprintId, title: data.title, description: data.description, startDate: data.startDate, endDate: data.endDate, status: this.determineSprintStatus(new Date(data.startDate), new Date(data.endDate)), issues: data.issues || [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; // Note: In a real implementation, you would need to: // 1. Ensure an iteration field exists in the project // 2. Configure the iteration field with the appropriate iterations // 3. Use updateProjectV2ItemFieldValue to assign issues to iterations return sprint; } async update(id: SprintId, data: Partial<Sprint>): Promise<Sprint> { const sprint = await this.findById(id); if (!sprint) { throw new Error("Sprint not found"); } // GitHub Projects V2 doesn't support updating individual iterations via API // For now, we'll return an updated mock sprint const updatedSprint: Sprint = { ...sprint, ...(data.title && { title: data.title }), ...(data.description && { description: data.description }), ...(data.startDate && { startDate: data.startDate }), ...(data.endDate && { endDate: data.endDate }), ...(data.status && { status: data.status }), ...(data.issues && { issues: data.issues }), updatedAt: new Date().toISOString(), }; return updatedSprint; } async delete(id: SprintId): Promise<void> { // GitHub Projects V2 doesn't support deleting individual iterations via API // Iterations are managed through the project's iteration field configuration // For now, this is a no-op console.log(`Sprint ${id} deletion requested - not supported by GitHub Projects V2 API`); } async findById(id: SprintId): Promise<Sprint | null> { // GitHub Projects V2 doesn't support querying individual iterations by ID // For now, we'll search through all sprints to find the matching one const allSprints = await this.findAll(); return allSprints.find(sprint => sprint.id === id) || null; } async findAll(options?: { status?: ResourceStatus }): Promise<Sprint[]> { const query = ` query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { projectsV2(first: 1) { nodes { fields(first: 100) { nodes { ... on ProjectV2IterationField { id name configuration { ... on ProjectV2IterationFieldConfiguration { iterations { id title startDate duration } } } } } } } } } } `; const response = await this.graphql<ListIterationFieldsResponse>(query, { owner: this.owner, repo: this.repo, }); if (!response.repository?.projectsV2?.nodes?.[0]?.fields?.nodes) { return []; } const sprints: Sprint[] = []; // Find iteration fields and extract their iterations for (const field of response.repository.projectsV2.nodes[0].fields.nodes) { if (field.configuration?.iterations) { for (const iteration of field.configuration.iterations) { const startDate = new Date(iteration.startDate); const endDate = new Date(startDate); endDate.setDate(endDate.getDate() + iteration.duration * 7); sprints.push({ id: iteration.id, title: iteration.title, description: "Sprint created from GitHub Projects iteration", // Default description startDate: startDate.toISOString(), endDate: endDate.toISOString(), status: this.determineSprintStatus(startDate, endDate), issues: [], // Issues would need separate query createdAt: startDate.toISOString(), updatedAt: new Date().toISOString(), }); } } } if (options?.status) { return sprints.filter(sprint => sprint.status === options.status); } return sprints; } async findCurrent(): Promise<Sprint | null> { const now = new Date(); const sprints = await this.findAll(); // Find a sprint that contains the current date return sprints.find(sprint => { const startDate = new Date(sprint.startDate); const endDate = new Date(sprint.endDate); return startDate <= now && now <= endDate; }) || null; } async addIssue(sprintId: SprintId, issueId: IssueId): Promise<Sprint> { const sprint = await this.findById(sprintId); if (!sprint) { throw new Error("Sprint not found"); } // Check if issue is already in the sprint if (sprint.issues.includes(issueId)) { return sprint; } // Add the issue to the sprint await this.addIssuesToSprint(sprintId, [issueId]); // Return the updated sprint return { ...sprint, issues: [...sprint.issues, issueId], updatedAt: new Date().toISOString() }; } async removeIssue(sprintId: SprintId, issueId: IssueId): Promise<Sprint> { const sprint = await this.findById(sprintId); if (!sprint) { throw new Error("Sprint not found"); } // Check if issue is not in the sprint if (!sprint.issues.includes(issueId)) { return sprint; } // Remove the issue from the sprint const removeQuery = ` mutation($input: UpdateProjectV2ItemFieldValueInput!) { updateProjectV2ItemFieldValue(input: $input) { projectV2Item { id } } } `; await this.graphql(removeQuery, { input: { projectId: this.config.projectId, itemId: `Issue_${issueId}`, fieldId: sprintId, value: null, }, }); // Return the updated sprint return { ...sprint, issues: sprint.issues.filter(id => id !== issueId), updatedAt: new Date().toISOString() }; } async getIssues(sprintId: SprintId): Promise<Issue[]> { const sprint = await this.findById(sprintId); if (!sprint) { throw new Error("Sprint not found"); } if (sprint.issues.length === 0) { return []; } // Use factory to create an issue repository const issueRepo = this.factory.createIssueRepository(); const issues = await Promise.all( sprint.issues.map(issueId => issueRepo.findById(issueId)) ); // Filter out any null results return issues.filter((issue): issue is Issue => issue !== null); } private determineSprintStatus(startDate: Date, endDate: Date): ResourceStatus { const now = new Date(); if (now < startDate) return ResourceStatus.PLANNED; if (now > endDate) return ResourceStatus.COMPLETED; return ResourceStatus.ACTIVE; } private async addIssuesToSprint(sprintId: string, issueIds: IssueId[]): Promise<void> { const addItemQuery = ` mutation($input: UpdateProjectV2ItemFieldValueInput!) { updateProjectV2ItemFieldValue(input: $input) { projectV2Item { id } } } `; for (const issueId of issueIds) { await this.graphql(addItemQuery, { input: { projectId: this.config.projectId, itemId: `Issue_${issueId}`, fieldId: sprintId, value: "ITERATION", }, }); } } private async updateSprintIssues(sprintId: string, issueIds: IssueId[]): Promise<void> { const sprint = await this.findById(sprintId); if (!sprint) { throw new Error("Sprint not found"); } // Remove existing issues const removeQuery = ` mutation($input: UpdateProjectV2ItemFieldValueInput!) { updateProjectV2ItemFieldValue(input: $input) { projectV2Item { id } } } `; for (const issueId of sprint.issues) { await this.graphql(removeQuery, { input: { projectId: this.config.projectId, itemId: `Issue_${issueId}`, fieldId: sprintId, value: null, }, }); } // Add new issues await this.addIssuesToSprint(sprintId, issueIds); } private toISODate(date: string | Date): string { const d = typeof date === 'string' ? new Date(date) : date; return d.toISOString().split('T')[0]; // YYYY-MM-DD format } }

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/kunwarVivek/mcp-github-project-manager'

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