Skip to main content
Glama
create_project_view.ts11.7 kB
import { GitHubConfig, ToolResponse } from '../../shared/types.js'; interface CreateProjectViewArgs { project_number?: number; project_id?: string; name: string; layout: 'BOARD_LAYOUT' | 'TABLE_LAYOUT' | 'ROADMAP_LAYOUT'; description?: string; filter?: string; // GitHub search syntax filter sort_field?: string; // Field name to sort by sort_direction?: 'ASC' | 'DESC'; group_by_field?: string; // Field name to group by (for board layout) visibility?: 'PUBLIC' | 'PRIVATE'; } /** * Create project views for GitHub Projects v2 (board, table, timeline, roadmap) * Uses GraphQL mutation createProjectV2View */ export async function createProjectView(config: GitHubConfig, args: CreateProjectViewArgs): Promise<ToolResponse> { const { graphqlWithAuth, owner } = 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 instead of project_id, 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_number or project_id must be provided'); } // Validate layout-specific requirements if (args.layout === 'BOARD_LAYOUT' && !args.group_by_field) { // For board layout, we typically want to group by Status if no group_by_field is specified args.group_by_field = 'Status'; } // Get project fields to validate sort and group fields let validFields: string[] = ['Title', 'Status', 'Assignees', 'Labels', 'Repository', 'Milestone']; try { const fieldsQuery = ` query($projectId: ID!) { node(id: $projectId) { ... on ProjectV2 { fields(first: 100) { nodes { ... on ProjectV2Field { name } ... on ProjectV2SingleSelectField { name } ... on ProjectV2IterationField { name } } } } } } `; const fieldsResult = await graphqlWithAuth(fieldsQuery, { projectId }); const customFields = fieldsResult.node?.fields?.nodes?.map((field: any) => field.name) || []; validFields = [...validFields, ...customFields]; } catch (error) { // Continue with default fields if we can't fetch custom fields console.warn('Could not fetch custom fields, using default field list'); } // Validate sort_field if provided if (args.sort_field && !validFields.includes(args.sort_field)) { throw new Error(`Sort field "${args.sort_field}" not found. Available fields: ${validFields.join(', ')}`); } // Validate group_by_field if provided if (args.group_by_field && !validFields.includes(args.group_by_field)) { throw new Error(`Group by field "${args.group_by_field}" not found. Available fields: ${validFields.join(', ')}`); } // Create the view using GraphQL mutation const createViewMutation = ` mutation($input: CreateProjectV2ViewInput!) { createProjectV2View(input: $input) { projectV2View { id name layout createdAt updatedAt filter sortBy { field { ... on ProjectV2Field { name } ... on ProjectV2SingleSelectField { name } ... on ProjectV2IterationField { name } } direction } groupBy { field { ... on ProjectV2Field { name } ... on ProjectV2SingleSelectField { name } ... on ProjectV2IterationField { name } } } visibleFields { ... on ProjectV2Field { name } ... on ProjectV2SingleSelectField { name } ... on ProjectV2IterationField { name } } } } } `; const input: any = { projectId, name: args.name, layout: args.layout }; // Add optional configuration if (args.filter) { input.filter = args.filter; } if (args.visibility) { input.visibility = args.visibility; } const result = await graphqlWithAuth(createViewMutation, { input }); const view = result.createProjectV2View.projectV2View; // Configure sorting if specified if (args.sort_field) { const updateSortMutation = ` mutation($input: UpdateProjectV2ViewInput!) { updateProjectV2View(input: $input) { projectV2View { id sortBy { field { ... on ProjectV2Field { name } ... on ProjectV2SingleSelectField { name } } direction } } } } `; // Find the field ID for sorting const sortFieldQuery = ` query($projectId: ID!) { node(id: $projectId) { ... on ProjectV2 { fields(first: 100) { nodes { ... on ProjectV2Field { id name } ... on ProjectV2SingleSelectField { id name } ... on ProjectV2IterationField { id name } } } } } } `; const sortFieldResult = await graphqlWithAuth(sortFieldQuery, { projectId }); const fields = sortFieldResult.node?.fields?.nodes || []; const sortField = fields.find((field: any) => field.name === args.sort_field); if (sortField) { const sortInput = { viewId: view.id, sortBy: [{ fieldId: sortField.id, direction: args.sort_direction || 'ASC' }] }; await graphqlWithAuth(updateSortMutation, { input: sortInput }); } } // Configure grouping for board layout if (args.layout === 'BOARD_LAYOUT' && args.group_by_field) { const updateGroupMutation = ` mutation($input: UpdateProjectV2ViewInput!) { updateProjectV2View(input: $input) { projectV2View { id groupBy { field { ... on ProjectV2Field { name } ... on ProjectV2SingleSelectField { name } } } } } } `; const groupFieldQuery = ` query($projectId: ID!) { node(id: $projectId) { ... on ProjectV2 { fields(first: 100) { nodes { ... on ProjectV2Field { id name } ... on ProjectV2SingleSelectField { id name } ... on ProjectV2IterationField { id name } } } } } } `; const groupFieldResult = await graphqlWithAuth(groupFieldQuery, { projectId }); const fields = groupFieldResult.node?.fields?.nodes || []; const groupField = fields.find((field: any) => field.name === args.group_by_field); if (groupField) { const groupInput = { viewId: view.id, groupBy: [{ fieldId: groupField.id }] }; await graphqlWithAuth(updateGroupMutation, { input: groupInput }); } } let response = `🎨 **Project view created successfully!**\n\n`; response += `**Name:** ${view.name}\n`; response += `**ID:** ${view.id}\n`; response += `**Layout:** ${view.layout.replace('_LAYOUT', '').toLowerCase()}\n`; response += `**Created:** ${new Date(view.createdAt).toLocaleDateString()}\n`; if (view.filter) { response += `**Filter:** ${view.filter}\n`; } if (view.sortBy && view.sortBy.length > 0) { const sortInfo = view.sortBy[0]; response += `**Sorted by:** ${sortInfo.field.name} (${sortInfo.direction})\n`; } if (view.groupBy && view.groupBy.length > 0) { const groupInfo = view.groupBy[0]; response += `**Grouped by:** ${groupInfo.field.name}\n`; } // Add layout-specific information if (args.layout === 'BOARD_LAYOUT') { response += `\n**Board View Features:**\n`; response += `• Kanban-style columns based on ${args.group_by_field || 'Status'}\n`; response += `• Drag-and-drop card management\n`; response += `• Visual workflow tracking\n`; } else if (args.layout === 'TABLE_LAYOUT') { response += `\n**Table View Features:**\n`; response += `• Spreadsheet-style data view\n`; response += `• Sortable columns\n`; response += `• Bulk editing capabilities\n`; } else if (args.layout === 'ROADMAP_LAYOUT') { response += `\n**Roadmap View Features:**\n`; response += `• Timeline visualization\n`; response += `• Date-based planning\n`; response += `• Milestone tracking\n`; } response += `\n💡 **Next Steps:**\n`; response += `• Use 'list_project_views' to see all project views\n`; response += `• Use 'update_project_view' to modify view settings\n`; response += `• Configure visible fields in the project interface\n`; response += `• Set up filters and automation rules\n`; response += `• Share view with team members`; return { content: [{ type: "text", text: response }] }; } catch (error: any) { if (error.message?.includes('insufficient permission')) { throw new Error('Insufficient permissions to create project views. Ensure your GitHub token has "project" scope.'); } if (error.message?.includes('View limit exceeded')) { throw new Error('Project has reached the maximum number of views (50 per project).'); } if (error.message?.includes('already exists')) { throw new Error(`View with name "${args.name}" already exists in this project.`); } throw new Error(`Failed to create project view: ${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