Skip to main content
Glama
scoutos

Linear MCP Server

by scoutos

get_issue

Retrieve detailed information about a Linear issue, including comments, using the issue ID. Designed to integrate with the Linear MCP Server for efficient issue tracking and management.

Instructions

Get detailed information about a specific Linear issue (also called a ticket), including comments if requested.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
debugNo
includeCommentsNo
issueIdYes

Implementation Reference

  • Main tool handler: creates Linear client, fetches issue data using getIssue helper, formats Markdown response with details and comments, handles errors with debug info.
    const handler = async (ctx, { issueId, includeComments, debug }) => {
      const logger = ctx.effects.logger;
    
      try {
        // Log details about config and parameters
        logger.debug('Get issue called with parameters:', {
          issueId,
          includeComments,
          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 issue using the Linear SDK client
        logger.debug('Executing Linear API get with issue ID:', issueId);
        const issue = await getIssue(
          linearClient,
          issueId,
          {
            includeComments,
          },
          logger
        );
    
        // Log that we found the issue
        logger.info(`Found issue with ID: ${issue.id}`);
    
        // Format the output
        let responseText = '';
    
        // Format priority to be readable
        const priorityMap = {
          0: 'No priority',
          1: 'Urgent',
          2: 'High',
          3: 'Medium',
          4: 'Low',
        };
    
        const priority = issue.priority ?? 0;
    
        // Format timestamps to be more readable
        const formatDate = timestamp => {
          if (!timestamp) return 'Unknown';
          const date = new Date(timestamp);
          return date.toLocaleString();
        };
    
        // Format the issue details
        responseText += `# ${issue.title || 'Untitled'}\n\n`;
        responseText += `**ID:** ${issue.id}\n`;
        if (issue.identifier) {
          responseText += `**Identifier:** ${issue.identifier}\n`;
        }
        if (issue.url) {
          responseText += `**URL:** ${issue.url}\n`;
        }
        responseText += `**Status:** ${issue.status || 'Unknown'}\n`;
        responseText += `**Priority:** ${priorityMap[priority] || 'Unknown'}\n`;
    
        if (issue.project) {
          responseText += `**Project:** ${issue.project.name}\n`;
        }
    
        if (issue.assignee) {
          responseText += `**Assignee:** ${issue.assignee.name}\n`;
        }
    
        responseText += `**Created:** ${formatDate(issue.createdAt)}\n`;
        responseText += `**Updated:** ${formatDate(issue.updatedAt)}\n\n`;
    
        // Add description if available
        if (issue.description) {
          responseText += `## Description\n\n${issue.description}\n\n`;
        }
    
        // Add comments if available
        if (issue.comments && issue.comments.length > 0) {
          responseText += `## Comments (${issue.comments.length})\n\n`;
    
          issue.comments.forEach((comment, index) => {
            const userName = comment.user ? comment.user.name : 'Unknown User';
            const commentDate = formatDate(comment.createdAt);
    
            responseText += `### ${index + 1}. ${userName} (${commentDate})\n\n`;
            responseText += `${comment.body}\n\n`;
    
            // Add edit info if comment was updated
            if (comment.updatedAt && comment.createdAt !== comment.updatedAt) {
              responseText += `*Edited: ${formatDate(comment.updatedAt)}*\n\n`;
            }
          });
        } else if (includeComments) {
          responseText += `## Comments\n\nNo comments found for this issue.\n\n`;
        }
    
        logger.debug('Returning formatted issue results');
        return {
          content: [{ type: 'text', text: responseText }],
        };
      } catch (error) {
        logger.error(`Error getting issue: ${error.message}`);
        logger.error(error.stack);
    
        // Create a user-friendly error message with troubleshooting guidance
        let errorMessage = `Error getting issue: ${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:
    - issueId: ${issueId}
    - includeComments: ${includeComments}`;
    
          // 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: issueId (required), includeComments and debug (optional).
    const GetIssueInputSchema = z.object({
      issueId: z.string(),
      includeComments: z.boolean().default(true),
      debug: z.boolean().default(false), // Debug mode to show extra diagnostics
    });
  • Tool factory using create_tool: registers name 'get_issue', description, inputSchema, and handler function.
    export const GetIssue = create_tool({
      name: 'get_issue',
      description:
        'Get detailed information about a specific Linear issue (also called a ticket), including comments if requested.',
      inputSchema: GetIssueInputSchema,
      handler,
    });
  • Core helper function: interacts with Linear SDK to fetch issue details, assignee, project, state, comments; processes and validates data.
    async function getIssue(
      client,
      issueId,
      { includeComments = true } = {},
      logger
    ) {
      try {
        logger?.debug(`Fetching Linear issue with ID: ${issueId}`);
    
        // Fetch the issue from Linear
        const issue = await client.issue(issueId);
    
        if (!issue) {
          logger?.error(`Issue with ID ${issueId} not found`);
          throw new Error(`Issue with ID ${issueId} not found`);
        }
    
        logger?.debug(`Successfully retrieved issue: ${issue.id}`);
    
        // Get state/status information (it's a promise in the Linear SDK)
        let statusName = 'Unknown';
        try {
          if (issue.state) {
            const state = await issue.state;
            if (state && state.name) {
              statusName = state.name;
              logger?.debug(`Found state: ${statusName}`);
            }
          }
        } catch (stateError) {
          logger?.warn(`Error fetching state data: ${stateError.message}`);
        }
    
        // Get assignee if present (it's a promise in the Linear SDK)
        let assigneeData = undefined;
        try {
          if (issue.assignee) {
            const assignee = await issue.assignee;
            if (assignee) {
              assigneeData = {
                id: assignee.id,
                name: assignee.name,
                email: assignee.email,
              };
              logger?.debug(`Found assignee: ${assignee.name}`);
            }
          }
        } catch (assigneeError) {
          logger?.warn(`Error fetching assignee data: ${assigneeError.message}`);
        }
    
        // Get project if present (it's a promise in the Linear SDK)
        let projectData = undefined;
        try {
          if (issue.project) {
            const project = await issue.project;
            if (project) {
              projectData = {
                id: project.id,
                name: project.name,
              };
              logger?.debug(`Found project: ${project.name}`);
            }
          }
        } catch (projectError) {
          logger?.warn(`Error fetching project data: ${projectError.message}`);
        }
    
        // Fetch comments if requested
        let commentsData = [];
        if (includeComments) {
          try {
            logger?.debug(`Fetching comments for issue: ${issue.id}`);
            const comments = await issue.comments();
    
            // Process comments (also resolving promises as needed)
            for (const comment of comments.nodes) {
              let userData = undefined;
    
              // Get user information (it's a promise in the Linear SDK)
              try {
                if (comment.user) {
                  const user = await comment.user;
                  if (user) {
                    userData = {
                      id: user.id,
                      name: user.name,
                      email: user.email,
                    };
                  }
                }
              } catch (userError) {
                logger?.warn(
                  `Error fetching comment user data: ${userError.message}`
                );
              }
    
              commentsData.push(
                CommentSchema.parse({
                  id: comment.id,
                  body: comment.body,
                  user: userData,
                  createdAt: comment.createdAt,
                  updatedAt: comment.updatedAt,
                })
              );
            }
    
            logger?.debug(`Successfully processed ${commentsData.length} comments`);
          } catch (commentsError) {
            logger?.warn(`Error fetching comments: ${commentsError.message}`);
            // Continue with the issue data even if comments fail
          }
        }
    
        // Process and validate the issue data
        const processedIssue = IssueSchema.parse({
          id: issue.id,
          identifier: issue.identifier || undefined, // Add the issue identifier (e.g. TEAM-123)
          url: issue.url || undefined, // Add the issue URL for linking
          title: issue.title,
          description: issue.description || undefined,
          priority: issue.priority,
          assignee: assigneeData,
          project: projectData,
          status: statusName,
          comments: commentsData,
          createdAt: issue.createdAt,
          updatedAt: issue.updatedAt,
        });
    
        return processedIssue;
      } catch (error) {
        // Enhanced error logging
        logger?.error(`Error retrieving Linear issue: ${error.message}`, {
          issueId,
          includeComments,
          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-118 (registration)
    Top-level instantiation new tools.GetIssue(toolContext) and registration to MCP server via generic loop for all tools.
    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),
    ];
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