Skip to main content
Glama

get_project

Retrieve comprehensive details of a Linear project, including team, lead, issues, and members. Configure options to include comments, set issue/ member limits, and activate debug mode for diagnostics.

Instructions

Get detailed information about a Linear project including team, lead, issues, and members. Use this to see comprehensive details of a specific project.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
debugNoDebug mode to show extra diagnostics
includeCommentsNoWhether to include comments on issues in the project
includeIssuesNoWhether to include issues in the project details
includeMembersNoWhether to include member details in the project
limitNoMaximum number of issues/members to include in details
projectIdYesThe ID of the Linear project to retrieve

Implementation Reference

  • The main tool handler function for 'get_project'. It receives input parameters, creates a Linear client using effects, fetches project details via internal getProject function, formats a comprehensive Markdown response with project info, members, issues, etc., and handles errors with detailed debug info.
    const handler = async ( ctx, { projectId, includeIssues, includeMembers, includeComments, limit, debug } ) => { const logger = ctx.effects.logger; try { // Log details about parameters logger.debug('Get project called with parameters:', { projectId, includeIssues, includeMembers, includeComments, limit, debug, }); // Debug log for API key (masked) const apiKey = ctx.config.linearApiKey || ''; const maskedKey = apiKey ? apiKey.substring(0, 4) + '...' + apiKey.substring(apiKey.length - 4) : '<not set>'; logger.debug(`Using Linear API key: ${maskedKey}`); if (!ctx.config.linearApiKey) { throw new Error('LINEAR_API_KEY is not configured'); } // Create a Linear client using our effect logger.debug('Creating Linear client'); const linearClient = ctx.effects.linear.createClient( ctx.config.linearApiKey ); // Get the project using the Linear SDK client logger.debug('Executing Linear API to get project details'); const project = await getProject( linearClient, projectId, { includeIssues, includeMembers, includeComments, limit, }, logger ); logger.info( `Successfully retrieved project: ${project.name} (${project.id})` ); // Format the output const formatDisplayDate = timestamp => { if (!timestamp) return 'Not set'; try { const date = new Date(timestamp); return date.toLocaleString(); } catch (e) { return 'Invalid date'; } }; // Determine project status let status = 'Active'; if (project.archived) status = 'Archived'; else if (project.canceled) status = 'Canceled'; else if (project.completed) status = 'Completed'; else if (project.state) status = project.state; // Format completion percentage const progressPercent = Math.round(project.progress * 100); // Build the response let responseText = `# Project: ${project.name}\n\n`; responseText += `**ID:** ${project.id}\n`; if (project.description) { responseText += `\n**Description:**\n${project.description}\n`; } responseText += `\n**Status:** ${status} (${progressPercent}% complete)\n`; if (project.teamName) { responseText += `**Team:** ${project.teamName}`; if (project.teamKey) { responseText += ` (${project.teamKey})`; } responseText += '\n'; } if (project.leadName) { responseText += `**Lead:** ${project.leadName}\n`; } responseText += `**Issues:** ${project.completedIssueCount}/${project.issueCount} completed\n`; // Add dates responseText += `\n**Timeline:**\n`; if (project.startDate) { responseText += `- Start date: ${formatDisplayDate(project.startDate)}\n`; } if (project.targetDate) { responseText += `- Target date: ${formatDisplayDate( project.targetDate )}\n`; } responseText += `- Created: ${formatDisplayDate(project.createdAt)}\n`; if (project.updatedAt) { responseText += `- Last updated: ${formatDisplayDate( project.updatedAt )}\n`; } // Add URL if available if (project.url) { responseText += `\n**URL:** ${project.url}\n`; } // Add members if included if (project.members && project.members.length > 0) { responseText += `\n## Project Members (${project.members.length})\n\n`; project.members.forEach((member, idx) => { responseText += `${idx + 1}. **${member.name}**`; if (member.role) { responseText += ` - ${member.role}`; } if (member.email) { responseText += ` <${member.email}>`; } responseText += '\n'; }); } // Add issues if included if (project.issues && project.issues.length > 0) { responseText += `\n## Project Issues (${project.issues.length}/${project.issueCount})\n\n`; project.issues.forEach((issue, idx) => { responseText += `${idx + 1}. **${issue.title}** (${issue.id})\n`; if (issue.state) { responseText += ` - Status: ${issue.state}\n`; } if (issue.assigneeName) { responseText += ` - Assigned to: ${issue.assigneeName}\n`; } if (issue.priority !== undefined) { const priorityLabels = [ 'No priority', 'Urgent', 'High', 'Medium', 'Low', ]; responseText += ` - Priority: ${priorityLabels[issue.priority]}\n`; } if (issue.updatedAt) { responseText += ` - Last updated: ${formatDisplayDate( issue.updatedAt )}\n`; } if (issue.comments && issue.comments.length > 0) { responseText += ` - Comments (${issue.comments.length}):\n`; issue.comments.forEach((comment, commentIdx) => { responseText += ` ${commentIdx + 1}. `; if (comment.userName) { responseText += `**${comment.userName}**: `; } // Truncate long comments const commentText = comment.body.length > 100 ? comment.body.substring(0, 97) + '...' : comment.body; responseText += `${commentText}\n`; }); } responseText += '\n'; }); } logger.debug('Returning formatted project details'); return { content: [{ type: 'text', text: responseText }], }; } catch (error) { logger.error(`Error retrieving project: ${error.message}`); logger.error(error.stack); // Create a user-friendly error message with troubleshooting guidance let errorMessage = `Error retrieving project: ${error.message}`; // Add detailed diagnostic information if in debug mode if (debug) { errorMessage += '\n\n=== DETAILED DEBUG INFORMATION ==='; // Add parameters that were used errorMessage += `\nParameters: - projectId: ${projectId} - includeIssues: ${includeIssues} - includeMembers: ${includeMembers} - includeComments: ${includeComments} - limit: ${limit}`; // Check if API key is configured const apiKey = ctx.config.linearApiKey || ''; const keyStatus = apiKey ? `API key is configured (${apiKey.substring( 0, 4 )}...${apiKey.substring(apiKey.length - 4)})` : 'API key is NOT configured - set LINEAR_API_KEY'; errorMessage += `\n\nLinear API Status: ${keyStatus}`; // Add error details if (error.name) { errorMessage += `\nError type: ${error.name}`; } if (error.code) { errorMessage += `\nError code: ${error.code}`; } if (error.stack) { errorMessage += `\n\nStack trace: ${error.stack .split('\n') .slice(0, 3) .join('\n')}`; } // Add Linear API info for manual testing errorMessage += `\n\nLinear API: Using official Linear SDK (@linear/sdk) For manual testing, try using the SDK directly or the Linear API Explorer in the Linear UI.`; } // Add a note that debug mode can be enabled for more details if (!debug) { errorMessage += `\n\nFor more detailed diagnostics, retry with debug:true in the input.`; } return { content: [ { type: 'text', text: errorMessage, }, ], isError: true, }; } };
  • Zod input schema defining parameters for the get_project tool: projectId (required), optional flags for including issues/members/comments, limit, and debug mode.
    const GetProjectInputSchema = z.object({ projectId: z.string().describe('The ID of the Linear project to retrieve'), includeIssues: z .boolean() .default(true) .describe('Whether to include issues in the project details'), includeMembers: z .boolean() .default(true) .describe('Whether to include member details in the project'), includeComments: z .boolean() .default(false) .describe('Whether to include comments on issues in the project'), limit: z .number() .min(1) .max(50) .default(10) .describe('Maximum number of issues/members to include in details'), debug: z .boolean() .default(false) .describe('Debug mode to show extra diagnostics'), });
  • Tool factory using create_tool() that registers the 'get_project' tool with its name, description, input schema, and handler function.
    export const GetProject = create_tool({ name: 'get_project', description: 'Get detailed information about a Linear project including team, lead, issues, and members. Use this to see comprehensive details of a specific project.', inputSchema: GetProjectInputSchema, handler, });
  • Internal helper function that performs the actual Linear API calls to retrieve and enrich project data using the Linear SDK client.
    async function getProject( client, projectId, { includeIssues = true, includeMembers = true, includeComments = false, limit = 10, } = {}, logger ) { try { logger?.debug(`Fetching Linear project with ID: ${projectId}`); // Get the project // @ts-ignore - The Linear SDK types may not be fully accurate const project = await client.project(projectId); if (!project) { throw new Error(`Project with ID ${projectId} not found`); } logger?.debug(`Successfully retrieved project: ${project.name}`); // Get team information if available let teamData = undefined; try { // @ts-ignore - SDK structure may differ from types if (project.team) { // If it's a promise, await it // @ts-ignore - SDK structure may differ from types const team = // @ts-ignore - SDK structure may differ from types typeof project.team.then === 'function' ? // @ts-ignore - SDK structure may differ from types await project.team : // @ts-ignore - SDK structure may differ from types project.team; if (team) { teamData = { id: team.id, name: team.name, key: team.key, }; logger?.debug(`Found team for project: ${team.name}`); } } } catch (teamError) { logger?.warn(`Error fetching team data: ${teamError.message}`); } // Get lead information if available let leadData = undefined; try { if (project.lead) { // If it's a promise, await it const lead = typeof project.lead.then === 'function' ? await project.lead : project.lead; if (lead) { // @ts-ignore - LinearFetch<User> may not provide these properties on the type leadData = { // @ts-ignore - LinearFetch<User> may not provide id property id: lead.id, // @ts-ignore - LinearFetch<User> may not provide name property name: lead.name, }; // @ts-ignore - LinearFetch<User> may not provide name property logger?.debug(`Found lead for project: ${lead.name}`); } } } catch (leadError) { logger?.warn(`Error fetching lead data: ${leadError.message}`); } // Prepare the result object with basic project info const result = { id: project.id, name: project.name, description: project.description, // Add timestamps createdAt: formatDate(project.createdAt), updatedAt: formatDate(project.updatedAt), startDate: formatDate(project.startDate), targetDate: formatDate(project.targetDate), // Add status information state: project.state, progress: project.progress || 0, // Convert date properties to boolean status completed: !!project.completedAt, canceled: !!project.canceledAt, archived: !!project.archive, // Add team information teamId: teamData?.id, teamName: teamData?.name, teamKey: teamData?.key, // Add lead information leadId: leadData?.id, leadName: leadData?.name, // Metrics - these might be calculated or fetched separately // @ts-ignore - SDK may provide these properties issueCount: project.issueCount || 0, // @ts-ignore - SDK may provide these properties completedIssueCount: project.completedIssueCount || 0, // @ts-ignore - SDK may provide this property slackChannel: project.slackChannel, slugId: project.slugId, url: project.url, }; // Fetch members if requested if (includeMembers) { logger?.debug('Fetching project members'); try { // @ts-ignore - The Linear SDK types may not be fully accurate const projectMembers = await project.members(); if (projectMembers && projectMembers.nodes) { const memberNodes = projectMembers.nodes.slice(0, limit); result.members = await Promise.all( memberNodes.map(async member => { // Get full member data return { id: member.id, name: member.name, email: member.email, // @ts-ignore - SDK structure may differ from types role: member.role || 'Member', }; }) ); logger?.debug(`Retrieved ${result.members.length} project members`); } } catch (membersError) { logger?.warn(`Error fetching project members: ${membersError.message}`); } } // Fetch issues if requested if (includeIssues) { logger?.debug('Fetching project issues'); try { // Build query params for issues const issueParams = { first: limit, filter: { project: { id: { eq: projectId } }, }, }; // @ts-ignore - The Linear SDK types may not be fully accurate const projectIssues = await client.issues(issueParams); if (projectIssues && projectIssues.nodes) { result.issues = await Promise.all( projectIssues.nodes.map(async issue => { // Get assignee information let assigneeName = undefined; try { if (issue.assignee) { // @ts-ignore - LinearFetch<User> types need special handling const assignee = await issue.assignee; if (assignee) { // @ts-ignore - LinearFetch<User> may not provide expected properties assigneeName = assignee.name; } } } catch (assigneeError) { logger?.warn( `Error fetching assignee data: ${assigneeError.message}` ); } // Get state information let stateName = undefined; try { if (issue.state) { // @ts-ignore - LinearFetch<WorkflowState> types need special handling const state = await issue.state; if (state) { // @ts-ignore - LinearFetch<WorkflowState> may not provide expected properties stateName = state.name; } } } catch (stateError) { logger?.warn( `Error fetching state data: ${stateError.message}` ); } const issueResult = { id: issue.id, title: issue.title, description: issue.description, state: stateName, priority: issue.priority, assigneeName: assigneeName, createdAt: formatDate(issue.createdAt), updatedAt: formatDate(issue.updatedAt), }; // Fetch comments if requested if (includeComments) { logger?.debug(`Fetching comments for issue: ${issue.id}`); try { // @ts-ignore - The Linear SDK types may not be fully accurate const issueComments = await issue.comments(); if (issueComments && issueComments.nodes) { issueResult.comments = await Promise.all( issueComments.nodes.slice(0, limit).map(async comment => { let userName = undefined; try { if (comment.user) { // @ts-ignore - LinearFetch<User> types need special handling const user = await comment.user; if (user) { // @ts-ignore - LinearFetch<User> may not provide expected properties userName = user.name; } } } catch (userError) { logger?.warn( `Error fetching comment user data: ${userError.message}` ); } return { id: comment.id, body: comment.body, userName: userName, createdAt: formatDate(comment.createdAt), }; }) ); logger?.debug( `Retrieved ${issueResult.comments.length} comments for issue ${issue.id}` ); } } catch (commentsError) { logger?.warn( `Error fetching issue comments: ${commentsError.message}` ); } } return issueResult; }) ); logger?.debug(`Retrieved ${result.issues.length} project issues`); } } catch (issuesError) { logger?.warn(`Error fetching project issues: ${issuesError.message}`); } } // Parse the result with our schema return ExtendedProjectSchema.parse(result); } catch (error) { // Enhanced error logging logger?.error(`Error retrieving Linear project: ${error.message}`, { projectId, includeIssues, includeMembers, includeComments, limit, stack: error.stack, }); // Check if it's a Zod validation error (formatted differently) if (error.name === 'ZodError') { logger?.error( 'Zod validation error details:', JSON.stringify(error.errors, null, 2) ); } // Rethrow the error for the tool to handle throw error; } }
  • src/index.js:109-149 (registration)
    Server-level registration where GetProject tool instance is created and registered with the MCP server via server.tool() in the main entrypoint.
    const all_tools = [ new tools.ListIssues(toolContext), new tools.GetIssue(toolContext), new tools.ListMembers(toolContext), new tools.ListProjects(toolContext), new tools.GetProject(toolContext), new tools.ListTeams(toolContext), new tools.AddComment(toolContext), new tools.CreateIssue(toolContext), ]; // Register tools with the MCP server for (const tool of all_tools) { server.tool( tool.name, tool.description, tool.inputSchema.shape ?? {}, async args => { try { // Call our tool const result = await tool.call(args); // Return format expected by MCP SDK return { content: result.content, error: result.isError ? { message: result.content[0]?.text || 'An error occurred', } : undefined, }; } catch (error) { logger.error(`Error executing tool ${tool.name}: ${error.message}`); return { content: [{ type: 'text', text: `Error: ${error.message}` }], error: { message: error.message }, }; } } ); }

Other Tools

Related Tools

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/scoutos/mcp-linear'

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