Skip to main content
Glama
kurdin

GitHub Repos Manager MCP Server

issues.cjs11.4 kB
// src/handlers/issues.cjs const fs = require("node:fs").promises; const issueFormatters = require("../formatters/issues.cjs"); async function listIssues(args, apiService) { const { owner, repo, state = "open", per_page = 10 } = args; if (!owner || !repo) { throw new Error( "Owner and repository name are required for listIssues. Please provide them in arguments or ensure a default is set." ); } const params = new URLSearchParams({ state, per_page: per_page.toString(), sort: "updated", direction: "desc", }); const issues = await apiService.makeGitHubRequest( `/repos/${owner}/${repo}/issues?${params.toString()}` ); return issueFormatters.formatListIssuesOutput(issues, owner, repo, state); } async function createIssue(args, apiService) { let { title, body = "", labels = [], assignees = [], image_path } = args; const { owner, repo } = args; let finalBody = body; if (!owner || !repo) { throw new Error( "Owner and repository name are required for createIssue. Please provide them in arguments or ensure a default is set." ); } if (!title) { throw new Error("Title is required to create an issue."); } if (image_path) { try { const imageData = await fs.readFile(image_path); const base64Image = imageData.toString("base64"); // Ensure file extension is one GitHub allows for rendering (e.g., png, jpg, gif) const fileExtensionMatch = image_path.match(/\.(png|jpe?g|gif)$/i); if (!fileExtensionMatch) { throw new Error( "Unsupported image file extension. Please use png, jpg, or gif." ); } const fileExtension = fileExtensionMatch[1].toLowerCase(); const fileName = `issue-image-${Date.now()}.${fileExtension}`; // Assuming apiService has a method like createOrUpdateFile or a dedicated uploadFileToRepo // For simplicity, using a structure similar to createOrUpdateFile // The 'uploadFileToRepo' method was in the original class, assuming it's available on apiService // or can be adapted from existing apiService methods. // If apiService.uploadFileToRepo doesn't exist, this part needs adjustment // to use apiService.createOrUpdateFile or similar. // Let's assume apiService.createOrUpdateFile can be used for this. const uploadResult = await apiService.createOrUpdateFile( // Adjusted to use existing method owner, repo, `images/issues/${fileName}`, // Path in repo `Add image for issue: ${title}`, // Commit message base64Image, // base64 content null, // SHA (null for new file) null // Branch (null for default branch) ); if ( uploadResult && uploadResult.content && uploadResult.content.download_url ) { const imageUrl = uploadResult.content.download_url; finalBody += `\n\n![Image](${imageUrl})`; } else { console.error( "Image upload did not return a download_url:", uploadResult ); finalBody += `\n\n[Note: Image uploaded, but URL retrieval failed for ${image_path}]`; } } catch (error) { console.error(`Failed to upload image for new issue: ${error.message}`); finalBody += `\n\n[Note: Failed to upload image from ${image_path}. Error: ${error.message}]`; } } const issueData = { title, body: finalBody, }; if (labels && labels.length > 0) issueData.labels = labels; if (assignees && assignees.length > 0) issueData.assignees = assignees; const issue = await apiService.makeGitHubRequest( `/repos/${owner}/${repo}/issues`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(issueData), } ); return issueFormatters.formatCreateIssueOutput(issue, owner, repo); } async function editIssue(args, apiService) { const { owner, repo, issue_number } = args; if (!owner || !repo) { throw new Error( "Owner and repository name are required for editIssue. Please provide them in arguments or ensure a default is set." ); } if (!issue_number) { throw new Error("issue_number is required for editIssue."); } const payload = {}; if (args.title !== undefined) payload.title = args.title; if (args.state !== undefined) payload.state = args.state; if (args.labels !== undefined) payload.labels = args.labels; if (args.assignees !== undefined) payload.assignees = args.assignees; let currentBody = args.body; if (args.image_path) { try { const imageData = await fs.readFile(args.image_path); const base64Image = imageData.toString("base64"); const fileExtensionMatch = args.image_path.match(/\.(png|jpe?g|gif)$/i); if (!fileExtensionMatch) { throw new Error( "Unsupported image file extension. Please use png, jpg, or gif." ); } const fileExtension = fileExtensionMatch[1].toLowerCase(); const fileName = `issue-${issue_number}-image-${Date.now()}.${fileExtension}`; const uploadResult = await apiService.createOrUpdateFile( owner, repo, `images/issues/${fileName}`, `Update image for issue #${issue_number}`, base64Image, null, null ); if ( uploadResult && uploadResult.content && uploadResult.content.download_url ) { const imageUrl = uploadResult.content.download_url; // Append to body if body is being updated, otherwise, this might need more complex handling // to fetch existing body first if only image is added. // For now, assuming if image_path is provided, body is also part of the update intent. currentBody = (currentBody === undefined ? "" : currentBody) + `\n\n![Image](${imageUrl})`; } else { console.error( "Image upload for edit did not return a download_url:", uploadResult ); currentBody = (currentBody === undefined ? "" : currentBody) + `\n\n[Note: Image uploaded for edit, but URL retrieval failed for ${args.image_path}]`; } } catch (error) { console.error( `Failed to upload image during issue edit: ${error.message}` ); currentBody = (currentBody === undefined ? "" : currentBody) + `\n\n[Note: Failed to upload image from ${args.image_path} during edit. Error: ${error.message}]`; } } if (currentBody !== undefined) payload.body = currentBody; if (Object.keys(payload).length === 0) { throw new Error( "At least one field to update (title, body, state, labels, assignees) must be provided for editIssue." ); } const updatedIssue = await apiService.makeGitHubRequest( `/repos/${owner}/${repo}/issues/${issue_number}`, { method: "PATCH", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), } ); return issueFormatters.formatEditIssueOutput(updatedIssue, owner, repo); } async function getIssueDetails(args, apiService) { const { owner, repo, issue_number } = args; if (!owner || !repo) { throw new Error( "Owner and repository name are required for getIssueDetails. Please provide them in arguments or ensure a default is set." ); } if (!issue_number) { throw new Error("issue_number is required for getIssueDetails."); } const issue = await apiService.makeGitHubRequest( `/repos/${owner}/${repo}/issues/${issue_number}` ); return issueFormatters.formatGetIssueDetailsOutput(issue, owner, repo); } async function lockIssue(args, apiService) { const { owner, repo, issue_number, lock_reason } = args; if (!owner || !repo) { throw new Error( "Owner and repository name are required for lockIssue. Please provide them in arguments or ensure a default is set." ); } if (!issue_number) { throw new Error("issue_number is required for lockIssue."); } const payload = {}; if (lock_reason) payload.lock_reason = lock_reason; await apiService.makeGitHubRequest( `/repos/${owner}/${repo}/issues/${issue_number}/lock`, { method: "PUT", headers: { "Content-Type": "application/json" }, // Required even for empty body by some APIs body: JSON.stringify(payload), // GitHub API expects a body for lock, even if just {} } ); return issueFormatters.formatLockIssueOutput(issue_number, lock_reason); } async function unlockIssue(args, apiService) { const { owner, repo, issue_number } = args; if (!owner || !repo) { throw new Error( "Owner and repository name are required for unlockIssue. Please provide them in arguments or ensure a default is set." ); } if (!issue_number) { throw new Error("issue_number is required for unlockIssue."); } await apiService.makeGitHubRequest( `/repos/${owner}/${repo}/issues/${issue_number}/lock`, { method: "DELETE" } ); return issueFormatters.formatUnlockIssueOutput(issue_number); } async function addAssigneesToIssue(args, apiService) { const { owner, repo, issue_number, assignees } = args; if (!owner || !repo) { throw new Error( "Owner and repository name are required for addAssigneesToIssue. Please provide them in arguments or ensure a default is set." ); } if (!issue_number) { throw new Error("issue_number is required for addAssigneesToIssue."); } if (!assignees || !Array.isArray(assignees) || assignees.length === 0) { throw new Error( "A non-empty array of assignees is required for addAssigneesToIssue." ); } const updatedIssue = await apiService.makeGitHubRequest( `/repos/${owner}/${repo}/issues/${issue_number}/assignees`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ assignees }), } ); return issueFormatters.formatAddAssigneesToIssueOutput( updatedIssue, issue_number ); } async function removeAssigneesFromIssue(args, apiService) { const { owner, repo, issue_number, assignees } = args; if (!owner || !repo) { throw new Error( "Owner and repository name are required for removeAssigneesFromIssue. Please provide them in arguments or ensure a default is set." ); } if (!issue_number) { throw new Error("issue_number is required for removeAssigneesFromIssue."); } if (!assignees || !Array.isArray(assignees) || assignees.length === 0) { throw new Error( "A non-empty array of assignees to remove is required for removeAssigneesFromIssue." ); } // The GitHub API for removing assignees expects the list of assignees to remove in the body. const updatedIssue = await apiService.makeGitHubRequest( `/repos/${owner}/${repo}/issues/${issue_number}/assignees`, { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ assignees }), // Send assignees to be removed } ); // The response to DELETE assignees is the updated issue object. return issueFormatters.formatRemoveAssigneesFromIssueOutput( updatedIssue, issue_number ); } module.exports = { listIssues, createIssue, editIssue, getIssueDetails, lockIssue, unlockIssue, addAssigneesToIssue, removeAssigneesFromIssue, };

Latest Blog Posts

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/kurdin/github-repos-manager-mcp'

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