Skip to main content
Glama
zalab-inc
by zalab-inc

get_issue

Retrieve specific issue details from Linear's project management system using the issue ID to access task information, status, and related data.

Instructions

A tool that gets an issue from Linear

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
issueIdYesThe ID of the issue to retrieve

Implementation Reference

  • Main handler for the get_issue tool. Fetches Linear issue by ID, retrieves comments, status, sub-issues, formats into human-readable text using createSafeTool wrapper.
    export const LinearGetIssueTool = createSafeTool({
      name: "get_issue",
      description: "A tool that gets an issue from Linear",
      schema: getIssueSchema.shape,
      handler: async (args: z.infer<typeof getIssueSchema>) => {
        try {
          // Validate input ID
          if (!args.issueId || args.issueId.trim() === "") {
            return {
              content: [{
                type: "text",
                text: "Error: Issue ID cannot be empty",
              }],
            };
          }
          
          // Fetch the issue using the Linear client
          const issue = await linearClient.issue(args.issueId);
          
          // Return an error message if the issue doesn't exist
          if (!issue) {
            return {
              content: [{
                type: "text",
                text: "Issue not found with that ID.",
              }],
            };
          }
          
          // Safely fetch comments with error handling
          let comments: IssueComment[] = [];
          try {
            const commentsQuery = await issue.comments();
            
            // Process and transform comments to a consistent format
            comments = commentsQuery.nodes.map((comment: unknown): IssueComment => {
              try {
                // Apply explicit type checking to ensure proper comment structure
                const typedComment = comment as {
                  id: string;
                  body: string;
                  createdAt: string;
                  updatedAt: string;
                  user: { id: string; name: string; email: string } | null;
                };
                
                // Extract and format relevant comment data
                return {
                  id: typedComment.id || "unknown-id",
                  body: typedComment.body || "",
                  createdAt: typedComment.createdAt || new Date().toISOString(),
                  updatedAt: typedComment.updatedAt || new Date().toISOString(),
                  user: typedComment.user ? {
                    id: typedComment.user.id || "unknown-user-id",
                    name: typedComment.user.name || "Unknown User",
                    email: typedComment.user.email || ""
                  } : null
                };
              } catch {
                // Return fallback comment on parsing error
                return {
                  id: "error-parsing",
                  body: "Error loading comment content",
                  createdAt: new Date().toISOString(),
                  updatedAt: new Date().toISOString(),
                  user: null
                };
              }
            });
          } catch {
            // Fail gracefully if comments can't be loaded
            comments = [{
              id: "comments-error",
              body: "Could not load comments: server error",
              createdAt: new Date().toISOString(),
              updatedAt: new Date().toISOString(),
              user: null
            }];
          }
    
          // Safely fetch issue state/status with error handling
          let status: StatusData | null = null;
          try {
            const stateData = await issue.state;
            if (stateData) {
              status = {
                id: stateData.id || "unknown-id",
                name: stateData.name || "Unknown status",
                color: stateData.color || "#cccccc",
                type: stateData.type || "unknown",
              };
            }
          } catch {
            // Fail gracefully if status can't be loaded
            status = {
              id: "status-error",
              name: "Error loading status",
              color: "#cccccc",
              type: "error",
            };
          }
          
          // Fetch sub-issues with error handling
          let subIssues: SubIssueData[] = [];
          try {
            // Get child issues using the Linear client
            const childIssuesQuery = await issue.children();
            
            if (childIssuesQuery && childIssuesQuery.nodes) {
              // Process each sub-issue
              for (const childIssue of childIssuesQuery.nodes) {
                try {
                  // Get status for each sub-issue
                  let subIssueStatus: StatusData | null = null;
                  try {
                    const childStateData = await childIssue.state;
                    if (childStateData) {
                      subIssueStatus = {
                        id: childStateData.id || "unknown-id",
                        name: childStateData.name || "Unknown status",
                        color: childStateData.color || "#cccccc",
                        type: childStateData.type || "unknown",
                      };
                    }
                  } catch {
                    subIssueStatus = null;
                  }
                  
                  // Add sub-issue to the array
                  subIssues.push({
                    id: childIssue.id || "unknown-id",
                    title: childIssue.title || "No title",
                    priority: typeof childIssue.priority === 'number' ? childIssue.priority : 0,
                    status: subIssueStatus
                  });
                } catch {
                  // Skip sub-issues that can't be processed
                  continue;
                }
              }
            }
          } catch {
            // Fail gracefully if sub-issues can't be loaded
            subIssues = [];
          }
          
          // Create normalized issue data object with safe defaults
          const issueData: IssueData = {
            id: issue.id || "unknown-id",
            title: issue.title || "No title",
            description: issue.description || undefined,
            priority: typeof issue.priority === 'number' ? issue.priority : 0,
            createdAt: issue.createdAt || new Date(),
            updatedAt: issue.updatedAt || new Date(),
            dueDate: issue.dueDate || undefined,
            url: issue.url || `https://linear.app/issue/${args.issueId}`,
            assignee: issue.assignee || undefined,
            labels: issue.labels || undefined
          };
          
          // Format issue data to human readable text
          const formattedText = formatIssueToHumanReadable(issueData, comments, status, subIssues);
          
          // Return the formatted text
          return {
            content: [{
              type: "text",
              text: formattedText,
            }],
          };
        } catch (error) {
          // Handle unexpected errors gracefully
          const errorMessage = error instanceof Error ? error.message : "Unknown error";
          return {
            content: [{
              type: "text",
              text: `An error occurred while retrieving issue data:\n${errorMessage}`,
            }],
          };
        }
      }
    }); 
  • Zod schema defining the single required input parameter 'issueId' for the get_issue tool.
    const getIssueSchema = z.object({
      issueId: z.string().describe("The ID of the issue to retrieve"),
    });
  • src/index.ts:31-41 (registration)
    Registration of LinearGetIssueTool (named 'get_issue') among other tools in the MCP server using registerTool.
    registerTool(server, [
      LinearSearchIssuesTool,
      LinearGetProfileTool,
      LinearCreateIssueTool,
      LinearCreateCommentTool,
      LinearUpdateCommentTool,
      LinearGetIssueTool,
      LinearGetTeamIdTool,
      LinearUpdateIssueTool,
      LinearGetCommentTool,
    ]);
  • Helper utility to format the retrieved issue data (including sub-issues and comments) into human-readable output used by the handler.
    function formatIssueToHumanReadable(
      issue: IssueData, 
      comments: IssueComment[], 
      status: StatusData | null,
      subIssues: SubIssueData[]
    ): string {
      if (!issue || !issue.id) {
        return "Invalid or incomplete issue data";
      }
      
      // Build formatted output with simpler structure
      let result = "";
      
      // Basic issue information
      result += `Id: ${issue.id}\n`;
      result += `Title: ${safeText(issue.title)}\n`;
      result += `Description: ${safeText(issue.description, "No description")}\n`;
      
      // Status and priority
      result += `Status: ${status && status.name ? status.name : "Unknown"}\n`;
      result += `Priority: ${getPriorityLabel(issue.priority)}\n`;
      
      // Dates
      result += `Created: ${formatDate(issue.createdAt)}\n`;
      result += `Updated: ${formatDate(issue.updatedAt)}\n`;
      
      // Due date if present
      if (issue.dueDate) {
        result += `Due date: ${formatDate(issue.dueDate)}\n`;
      }
      
      // URL
      result += `Url: ${safeText(issue.url)}\n\n`;
      
      // Sub-issues section
      result += `Sub-issues (${subIssues ? subIssues.length : 0}):\n`;
      
      if (subIssues && subIssues.length > 0) {
        subIssues.forEach((subIssue, index) => {
          result += `#${index + 1}: ${safeText(subIssue.title)}\n`;
          result += `Status: ${subIssue.status && subIssue.status.name ? subIssue.status.name : "Unknown"}\n`;
          result += `Priority: ${getPriorityLabel(subIssue.priority)}\n\n`;
        });
      } else {
        result += "No sub-issues for this issue\n\n";
      }
      
      // Comments section
      result += `Comments (${comments ? comments.length : 0}):\n`;
      
      if (comments && comments.length > 0) {
        comments.forEach((comment, index) => {
          result += `Comment #${index + 1}: ${safeText(comment.body)}\n`;
          result += `Created: ${formatDate(comment.createdAt)}\n\n`;
        });
      } else {
        result += "No comments for this issue\n\n";
      }
      
      return result;
    }
Behavior2/5

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

No annotations are provided, so the description carries full burden. It only states the action ('gets an issue') without disclosing behavioral traits like whether it's a read-only operation, authentication requirements, error handling, or rate limits. For a tool with zero annotation coverage, this is inadequate.

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

Conciseness5/5

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

The description is a single, efficient sentence with zero waste. It's appropriately sized and front-loaded, directly stating the tool's function without unnecessary elaboration.

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 (a read operation with one parameter) and lack of annotations/output schema, the description is incomplete. It doesn't explain what 'gets' entails (e.g., returns issue details), error cases, or how it differs from siblings. For a tool with no structured data support, more context is needed.

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

Parameters3/5

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

Schema description coverage is 100%, with the parameter 'issueId' documented as 'The ID of the issue to retrieve'. The description adds no additional meaning beyond what the schema provides. According to rules, with high schema coverage (>80%), the baseline is 3 even with no param info in the description.

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

Purpose3/5

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

The description states the tool 'gets an issue from Linear', which provides a basic verb+resource combination. However, it's vague about what 'gets' means (retrieves, fetches, reads) and doesn't distinguish it from sibling tools like 'search_issues' or 'get_comment'. The purpose is understandable but lacks specificity.

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

Usage Guidelines2/5

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

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention when to use 'get_issue' versus 'search_issues' for finding issues, or prerequisites like needing an issue ID. There's no explicit or implied context for usage decisions.

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

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/zalab-inc/mcp-linear-app'

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