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

search_issues

Search and filter Linear project issues by status, priority, or keywords to quickly find specific tasks and track progress.

Instructions

A tool that searches for issues in Linear

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
keywordNoFilter issues by keyword in title or description
limitNoMaximum number of issues to return (default: 50)
priorityNoFilter issues by priority
skipNoNumber of issues to skip (default: 0)
statusNoFilter issues by status

Implementation Reference

  • The core handler function for the 'search_issues' tool. Fetches issues from Linear API, applies optional filters (status, priority, keyword), handles client-side pagination (limit/skip), formats results with helper function, and returns human-readable text plus JSON metadata.
    handler: async (args: z.infer<typeof searchIssuesSchema>) => {
      try {
        // Set default values for pagination
        const limit = typeof args.limit === 'number' ? args.limit : 50;
        const skip = typeof args.skip === 'number' ? args.skip : 0;
        
        // Fetch issues with pagination - we need to request more to handle skiping
        // Linear API has a first parameter but no skip/offset parameter
        const maxFetch = skip + limit;
        const getAllIssues = await linearClient.issues({
          first: maxFetch,
        });
    
        if (!getAllIssues || !getAllIssues.nodes || getAllIssues.nodes.length === 0) {
          return {
            content: [
              {
                type: "text",
                text: "No issues found."
              },
              {
                type: "text",
                text: JSON.stringify({
                  total: 0,
                  limit,
                  skip,
                  start: 0,
                  end: 0,
                  hasMore: false,
                  nextSkip: null,
                  message: "No issues available."
                }, null, 2)
              }
            ],
          };
        }
    
        // Filter issues by status if provided
        let filteredNodes = getAllIssues.nodes;
        
        if (args.status) {
          filteredNodes = filteredNodes.filter(issue => {
            // Handle case where state might be null or different structure
            if (!issue.state) return false;
            
            // Normalize the state name for comparison
            const stateName = typeof issue.state === 'object' 
              ? (issue.state as { name?: string })?.name?.toLowerCase?.()?.replace?.(/\s+/g, '_') || ''
              : '';
            
            const targetState = args.status?.toLowerCase().replace(/\s+/g, '_') || '';
            
            return stateName.includes(targetState) || targetState.includes(stateName);
          });
        }
        
        // Filter issues by priority if provided
        if (args.priority) {
          const priorityValue = PriorityStringToNumber[args.priority];
          
          if (priorityValue !== undefined) {
            filteredNodes = filteredNodes.filter(issue => 
              issue.priority === priorityValue
            );
          }
        }
        
        // Filter issues by keyword if provided
        if (args.keyword && args.keyword.trim() !== '') {
          const keyword = args.keyword.toLowerCase().trim();
          filteredNodes = filteredNodes.filter(issue => {
            const title = (issue.title || '').toLowerCase();
            const description = (issue.description || '').toLowerCase();
            
            return title.includes(keyword) || description.includes(keyword);
          });
        }
        
        // Calculate total after filtering
        const totalFilteredIssues = filteredNodes.length;
        
        // If skip is provided, slice the nodes array to simulate pagination
        const startIndex = Math.min(skip, totalFilteredIssues);
        const endIndex = Math.min(startIndex + limit, totalFilteredIssues);
        const paginatedNodes = filteredNodes.slice(startIndex, endIndex);
        
        // Check if we have any issues to display after pagination and filtering
        if (paginatedNodes.length === 0) {
          return {
            content: [
              {
                type: "text",
                text: "No issues found matching the search criteria."
              },
              {
                type: "text",
                text: JSON.stringify({
                  total: 0,
                  filteredTotal: totalFilteredIssues,
                  limit,
                  skip,
                  start: 0,
                  end: 0,
                  hasMore: false,
                  nextSkip: null,
                  filters: {
                    status: args.status || null,
                    priority: args.priority || null,
                    keyword: args.keyword || null
                  },
                  message: "No issues match the search criteria."
                }, null, 2)
              }
            ],
          };
        }
        
        // Format each issue to human readable text
        let issuesText = "";
        
        // Add search information if filters were applied
        if (args.status || args.priority || args.keyword) {
          issuesText += "Search filters:\n";
          if (args.status) issuesText += `- Status: ${args.status}\n`;
          if (args.priority) issuesText += `- Priority: ${args.priority}\n`;
          if (args.keyword) issuesText += `- Keyword: "${args.keyword}"\n`;
          issuesText += `\n`;
        }
        
        // Add note about status display limitation
        issuesText += "Note: Status display may not accurately reflect updated status due to API limitations.\n";
        issuesText += "Use 'get_issue' with specific issue ID to see accurate status information.\n\n";
        
        // Add each formatted issue
        for (const issue of paginatedNodes) {
          // Cast to any first to handle unknown structure of Linear API response
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          const linearIssue = issue as any;
          
          // Improved state extraction
          let state = null;
          if (linearIssue.state) {
            if (typeof linearIssue.state === 'object') {
              // Normal case: state is an object
              state = { 
                name: linearIssue.state.name || "No status yet" 
              };
            } else {
              // Handle case where state is just an ID reference
              state = { name: "Status set" };
            }
          }
          
          // Create issue data object with required fields
          const issueData: IssueData = {
            id: linearIssue.id,
            title: linearIssue.title,
            description: linearIssue.description,
            state: state,
            priority: linearIssue.priority,
            assignee: linearIssue.assignee,
            createdAt: linearIssue.createdAt,
            updatedAt: linearIssue.updatedAt,
            url: linearIssue.url,
            labels: linearIssue.labels,
            dueDate: linearIssue.dueDate,
            commentsCount: linearIssue.commentCount || 0  // Use commentCount property directly
          };
          
          issuesText += formatIssueToHumanReadable(issueData);
        }
        
        // Add pagination information
        const hasMoreIssues = endIndex < totalFilteredIssues;
        const nextSkip = skip + limit;
        
        // Create metadata object
        const metadataObj = {
          total: getAllIssues.nodes.length,
          filteredTotal: totalFilteredIssues,
          limit,
          skip,
          start: startIndex + 1,
          end: endIndex,
          hasMore: hasMoreIssues,
          nextSkip: hasMoreIssues ? nextSkip : null,
          filters: {
            status: args.status || null,
            priority: args.priority || null,
            keyword: args.keyword || null
          },
          message: hasMoreIssues ? `For next page, use skip: ${nextSkip}` : "No more search results available."
        };
        
        return {
          content: [
            {
              type: "text",
              text: issuesText
            },
            {
              type: "text",
              text: JSON.stringify(metadataObj, null, 2)
            }
          ],
        };
      } catch (error) {
        // Handle unexpected errors gracefully
        const errorMessage = error instanceof Error ? error.message : "Unknown error";
        return {
          content: [
            {
              type: "text",
              text: `An error occurred while searching for issues:\n${errorMessage}`
            },
            {
              type: "text",
              text: JSON.stringify({
                error: true,
                errorMessage,
                total: 0,
                limit: typeof args.limit === 'number' ? args.limit : 50,
                skip: typeof args.skip === 'number' ? args.skip : 0,
                filters: {
                  status: args.status || null,
                  priority: args.priority || null,
                  keyword: args.keyword || null
                },
                start: 0,
                end: 0,
                hasMore: false,
                nextSkip: null
              }, null, 2)
            }
          ],
        };
      }
    }
  • Zod schema defining the input parameters for the search_issues tool: pagination (limit, skip), filters (status, priority, keyword).
    const searchIssuesSchema = z.object({
      limit: z.number().optional().describe("Maximum number of issues to return (default: 50)"),
      skip: z.number().optional().describe("Number of issues to skip (default: 0)"),
      status: z.enum([
        "triage", "backlog", "todo", "in_progress", "done", "canceled"
      ]).optional().describe("Filter issues by status"),
      priority: z.enum([
        "no_priority", "urgent", "high", "medium", "low"
      ]).optional().describe("Filter issues by priority"),
      keyword: z.string().optional().describe("Filter issues by keyword in title or description"),
    });
  • src/index.ts:31-41 (registration)
    Registration of the LinearSearchIssuesTool (named 'search_issues') along with other Linear tools to the MCP server using registerTool.
    registerTool(server, [
      LinearSearchIssuesTool,
      LinearGetProfileTool,
      LinearCreateIssueTool,
      LinearCreateCommentTool,
      LinearUpdateCommentTool,
      LinearGetIssueTool,
      LinearGetTeamIdTool,
      LinearUpdateIssueTool,
      LinearGetCommentTool,
    ]);
  • Helper function used by the handler to format each Linear issue into human-readable multi-line text including ID, title, status, priority, etc.
    function formatIssueToHumanReadable(issue: IssueData): string {
      if (!issue || !issue.id) {
        return "Invalid or incomplete issue data";
      }
      
      // Build formatted output
      let result = "";
      
      // Basic issue information
      result += `Id: ${issue.id}\n`;
      result += `Title: ${safeText(issue.title)}\n`;
      
      // Improved status handling
      let statusText = "No status yet";
      if (issue.state) {
        if (typeof issue.state === 'object' && issue.state?.name) {
          statusText = issue.state.name;
        } else if (typeof issue.state === 'string') {
          // Handle case where state might be a string ID
          statusText = "Status available (ID only)";
        }
      }
      result += `Status: ${statusText}\n`;
      
      // Priority
      result += `Priority: ${getPriorityLabel(issue.priority)}\n`;
      
      // Comments count if available
      if (typeof issue.commentsCount === 'number') {
        result += `Comments: ${issue.commentsCount}\n`;
      }
      
      // Due date if present
      if (issue.dueDate) {
        result += `Due date: ${formatDate(issue.dueDate)}\n`;
      }
      
      // URL
      result += `Url: ${safeText(issue.url)}\n\n`;
      
      return result;
    }
  • Mapping helper from string priority enums to numeric values used for filtering in the handler.
    export const PriorityStringToNumber: Record<string, number> = {
      'no_priority': 0,
      'urgent': 1,
      'high': 2,
      'medium': 3,
      'low': 4
    };
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 but only states the basic action without mentioning critical details like whether this is a read-only operation, if it requires authentication, potential rate limits, or what the return format looks like. For a search tool with multiple parameters, this leaves significant gaps in understanding its behavior.

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 extremely concise with a single sentence that directly states the tool's function without any unnecessary words. It's front-loaded with the core purpose and wastes no space on redundant information, making it efficient for quick understanding.

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 tool has 5 parameters, no annotations, and no output schema, the description is insufficiently complete. It doesn't explain what the search returns (e.g., issue objects with fields), how results are ordered, or any behavioral constraints. For a search operation in a system like Linear, more context about the operation's scope and results 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?

The schema description coverage is 100%, with all parameters well-documented in the schema itself (e.g., 'keyword' filters by title/description, 'priority' and 'status' have enums, 'limit' and 'skip' have defaults). The description adds no additional parameter information beyond what's already in the schema, meeting the baseline expectation but not providing extra value.

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 tool's purpose as 'searches for issues in Linear' with a specific verb ('searches') and resource ('issues'), making it immediately understandable. However, it doesn't differentiate from sibling tools like 'get_issue' which might retrieve a single issue, leaving some ambiguity about when to use this versus other issue-related tools.

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 like 'get_issue' or 'create_issue'. It lacks any context about appropriate scenarios, prerequisites, or exclusions, leaving the agent to infer usage based solely on the tool name and parameters.

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