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),
    ];
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It mentions retrieving 'detailed information' and comments, but fails to address critical aspects like whether this is a read-only operation (implied but not stated), error handling for invalid issue IDs, rate limits, authentication needs, or what 'detailed information' includes beyond comments. This leaves significant gaps for an agent to understand the tool's behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads the core purpose. It avoids unnecessary words, though it could be slightly more structured by explicitly separating purpose from parameter guidance.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity (3 parameters, no output schema, no annotations), the description is incomplete. It doesn't explain what 'detailed information' entails, how to handle the issueId format, the purpose of the debug parameter, or the response structure. For a tool with undocumented parameters and no annotations, this leaves the agent with insufficient context to use it effectively.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the schema provides no parameter descriptions. The description only mentions 'comments if requested,' which loosely relates to the includeComments parameter but doesn't explain its purpose, default behavior, or the other parameters (issueId and debug). It adds minimal value beyond the bare schema, failing to compensate for the lack of schema descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the verb ('Get') and resource ('detailed information about a specific Linear issue'), distinguishing it from siblings like list_issues (which lists multiple issues) and create_issue (which creates new issues). However, it doesn't explicitly differentiate from get_project or other get_* tools beyond the resource name.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage context by specifying 'a specific Linear issue' and mentioning comments, suggesting this tool is for retrieving details of a known issue rather than listing or creating. However, it lacks explicit guidance on when to use this versus alternatives like list_issues or when not to use it (e.g., for bulk operations).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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