Skip to main content
Glama
scoutos

Linear MCP Server

by scoutos

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 },
            };
          }
        }
      );
    }
Install Server

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