Skip to main content
Glama

MCP Linear App

by zalab-inc
get-issue.ts10.1 kB
import { createSafeTool } from "../../libs/tool-utils.js"; import { z } from "zod"; import linearClient from '../../libs/client.js'; import { getPriorityLabel, formatDate, safeText } from '../../libs/utils.js'; /** * Schema definition for the get issue tool * Defines the required parameters to retrieve an issue */ const getIssueSchema = z.object({ issueId: z.string().describe("The ID of the issue to retrieve"), }); /** * Interface defining the structure of an issue comment * Used for type safety when processing comments */ interface IssueComment { id: string; body: string; createdAt: string; updatedAt: string; user: { id: string; name: string; email: string; } | null; } /** * Interface for issue data object */ interface IssueData { id: string; title: string; description?: string; priority: number; createdAt: string | Date; updatedAt: string | Date; dueDate?: string | Date; url: string; state?: unknown; assignee?: unknown; labels?: unknown; } /** * Interface for status object */ interface StatusData { id: string; name: string; color: string; type: string; } /** * Interface for sub-issue object */ interface SubIssueData { id: string; title: string; priority: number; status: StatusData | null; } /** * Format issue data to human readable text * Handles all edge cases with safe data processing * * @param issue The issue data * @param comments The issue comments * @param status The status object * @param subIssues The sub-issues array * @returns Formatted human readable text */ 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; } /** * Tool implementation for retrieving a Linear issue by its ID * * This tool fetches a specific issue from Linear along with its comments. * It provides detailed information about the issue including its metadata * and all associated comments with their authors. * * Error handling is managed automatically by the 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}`, }], }; } } });

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