Skip to main content
Glama
add_project_item.ts11.1 kB
import { GitHubConfig, ToolResponse } from '../../shared/types.js'; interface AddProjectItemArgs { project_id?: string; project_number?: number; content_id?: string; content_type?: 'issue' | 'pull_request' | 'draft'; issue_number?: number; pr_number?: number; draft_title?: string; draft_body?: string; bulk_items?: Array<{ content_id?: string; issue_number?: number; pr_number?: number; type?: 'issue' | 'pull_request'; }>; } /** * Add issues, pull requests, or draft items to GitHub Projects v2 * Uses GraphQL mutations addProjectV2ItemById and addProjectV2DraftIssue */ export async function addProjectItem(config: GitHubConfig, args: AddProjectItemArgs): Promise<ToolResponse> { const { graphqlWithAuth, owner, repo, octokit } = config; if (!owner) { throw new Error('GITHUB_OWNER environment variable is required for project operations'); } try { let projectId = args.project_id; // If project_number is provided, get the project ID if (!projectId && args.project_number) { const projectQuery = ` query($owner: String!, $number: Int!) { user(login: $owner) { projectV2(number: $number) { id title } } organization(login: $owner) { projectV2(number: $number) { id title } } } `; const projectResult = await graphqlWithAuth(projectQuery, { owner, number: args.project_number }); const project = projectResult.user?.projectV2 || projectResult.organization?.projectV2; if (!project) { throw new Error(`Project #${args.project_number} not found`); } projectId = project.id; } if (!projectId) { throw new Error('Either project_id or project_number must be provided'); } const results: any[] = []; let totalAdded = 0; // Handle bulk items if (args.bulk_items && args.bulk_items.length > 0) { for (const item of args.bulk_items) { try { let contentId = item.content_id; // Get content ID from issue/PR number if needed if (!contentId && item.issue_number) { const issueData = await octokit.rest.issues.get({ owner, repo, issue_number: item.issue_number }); contentId = issueData.data.node_id; } else if (!contentId && item.pr_number) { const prData = await octokit.rest.pulls.get({ owner, repo, pull_number: item.pr_number }); contentId = prData.data.node_id; } if (contentId) { const addItemMutation = ` mutation($projectId: ID!, $contentId: ID!) { addProjectV2ItemById(input: { projectId: $projectId contentId: $contentId }) { item { id type content { ... on Issue { number title url } ... on PullRequest { number title url } } } } } `; const result = await graphqlWithAuth(addItemMutation, { projectId, contentId }); if (result.addProjectV2ItemById?.item) { results.push({ success: true, item: result.addProjectV2ItemById.item, original: item }); totalAdded++; } } } catch (error: any) { results.push({ success: false, error: error.message, original: item }); } } } else { // Handle single item let contentId = args.content_id; // Create draft item if (args.content_type === 'draft' && args.draft_title) { const draftMutation = ` mutation($projectId: ID!, $title: String!, $body: String) { addProjectV2DraftIssue(input: { projectId: $projectId title: $title body: $body }) { projectItem { id type content { ... on DraftIssue { id title body } } } } } `; const result = await graphqlWithAuth(draftMutation, { projectId, title: args.draft_title, body: args.draft_body || "" }); if (result.addProjectV2DraftIssue?.projectItem) { results.push({ success: true, item: result.addProjectV2DraftIssue.projectItem, type: 'draft' }); totalAdded++; } } else { // Get content ID from issue/PR number if needed if (!contentId && args.issue_number && repo) { const issueData = await octokit.rest.issues.get({ owner, repo, issue_number: args.issue_number }); contentId = issueData.data.node_id; } else if (!contentId && args.pr_number && repo) { const prData = await octokit.rest.pulls.get({ owner, repo, pull_number: args.pr_number }); contentId = prData.data.node_id; } if (!contentId) { throw new Error('content_id, issue_number, or pr_number must be provided for non-draft items'); } const addItemMutation = ` mutation($projectId: ID!, $contentId: ID!) { addProjectV2ItemById(input: { projectId: $projectId contentId: $contentId }) { item { id type content { ... on Issue { number title url state labels(first: 10) { nodes { name color } } assignees(first: 5) { nodes { login } } } ... on PullRequest { number title url state isDraft labels(first: 10) { nodes { name color } } assignees(first: 5) { nodes { login } } } ... on DraftIssue { id title body } } } } } `; const result = await graphqlWithAuth(addItemMutation, { projectId, contentId }); if (result.addProjectV2ItemById?.item) { results.push({ success: true, item: result.addProjectV2ItemById.item }); totalAdded++; } } } // Format response let response = `📦 **Project Item(s) Added Successfully!**\n\n`; response += `**Total Added:** ${totalAdded} item(s)\n`; response += `**Project ID:** ${projectId}\n\n`; if (results.length > 0) { response += `## Added Items\n\n`; results.forEach((result, index) => { if (result.success && result.item) { const item = result.item; const content = item.content; response += `### ${index + 1}. `; if (content) { if (content.number) { // Issue or PR const type = item.type === 'ISSUE' ? 'Issue' : item.type === 'PULL_REQUEST' ? 'Pull Request' : 'Item'; response += `**${type} #${content.number}: ${content.title}**\n`; response += ` 🔗 ${content.url}\n`; if (content.state) { const stateEmoji = content.state === 'OPEN' ? '🟢' : content.state === 'CLOSED' ? '🔴' : '🟡'; response += ` ${stateEmoji} Status: ${content.state}\n`; } if (content.labels?.nodes && content.labels.nodes.length > 0) { const labels = content.labels.nodes.map((label: any) => label.name).join(', '); response += ` 🏷️ Labels: ${labels}\n`; } if (content.assignees?.nodes && content.assignees.nodes.length > 0) { const assignees = content.assignees.nodes.map((assignee: any) => assignee.login).join(', '); response += ` 👤 Assignees: ${assignees}\n`; } } else if (content.title) { // Draft item response += `**Draft: ${content.title}**\n`; if (content.body) { const shortBody = content.body.length > 100 ? content.body.substring(0, 100) + '...' : content.body; response += ` 📝 ${shortBody}\n`; } } } response += ` 🆔 Item ID: ${item.id}\n\n`; } else if (!result.success) { response += `### ${index + 1}. ❌ **Failed to add item**\n`; response += ` Error: ${result.error}\n`; if (result.original) { response += ` Original: ${JSON.stringify(result.original)}\n`; } response += `\n`; } }); } if (totalAdded > 0) { response += `🎯 **Next Steps:**\n`; response += `• Use 'set_field_value' to set custom field values\n`; response += `• Use 'list_project_items' to view all project items\n`; response += `• Use 'get_project' to see the updated project`; } return { content: [{ type: "text", text: response }] }; } catch (error: any) { if (error.message?.includes('insufficient permission')) { throw new Error('Insufficient permissions to add items to project. Ensure your GitHub token has "project" scope and write access to the project.'); } if (error.message?.includes('not found')) { throw new Error(`Project or content not found: ${error.message}`); } throw new Error(`Failed to add project item: ${error.message}`); } }

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

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