Skip to main content
Glama
scoutos

Linear MCP Server

by scoutos

list_issues

Browse and filter Linear issues by assignee, status, or project to quickly find and manage tickets in your Linear workspace. Supports sorting and customizable limits for efficient issue tracking.

Instructions

List Linear issues (also called tickets) with filtering by assignee, status, and project. Use this to browse and find issues in your Linear workspace.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
assignedToMeNo
assigneeNo
debugNo
limitNo
projectNo
sortByNocreatedAt
sortDirectionNoDESC
statusNo

Implementation Reference

  • Primary ToolHandler for 'list_issues' tool. Processes inputs, creates Linear client, calls helper to fetch issues, formats output as numbered list with details, handles debug and errors.
    const handler = async (
      ctx,
      {
        assignedToMe,
        assignee,
        status,
        project,
        sortBy,
        sortDirection,
        limit,
        debug,
      }
    ) => {
      const logger = ctx.effects.logger;
    
      try {
        // Log details about config and parameters
        logger.debug('List issues called with parameters:', {
          assignedToMe,
          assignee,
          status,
          project,
          sortBy,
          sortDirection,
          limit,
          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
        );
    
        // List issues using the Linear SDK client with filters
        logger.debug('Executing Linear API list with filters');
        const results = await listIssues(
          linearClient,
          {
            assignedToMe,
            assignee,
            status,
            project,
          },
          {
            limit,
            sortBy,
            sortDirection,
          },
          logger
        );
    
        // Log the results count
        logger.info(`Found ${results.results.length} issues matching criteria`);
    
        // Format the output
        let responseText = '';
    
        logger.info(`Result: ${JSON.stringify(results, null, 2)}`);
    
        if (results.results.length === 0) {
          responseText = 'No issues found matching your criteria.';
        } else {
          responseText = 'Issues found:\n\n';
    
          results.results.forEach((issue, index) => {
            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();
            };
    
            responseText += `${index + 1}. ${issue.title || 'Untitled'}\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`;
    
            responseText += '\n';
          });
        }
    
        logger.debug('Returning formatted list results');
        return {
          content: [{ type: 'text', text: responseText }],
        };
      } catch (error) {
        logger.error(`Error listing issues: ${error.message}`);
        logger.error(error.stack);
    
        // Create a user-friendly error message with troubleshooting guidance
        let errorMessage = `Error listing issues: ${error.message}`;
    
        // Add detailed diagnostic information if in debug mode
        if (debug) {
          errorMessage += '\n\n=== DETAILED DEBUG INFORMATION ===';
    
          // Add filter parameters that were used
          errorMessage += `\nFilter parameters:
    - assignedToMe: ${assignedToMe}
    - assignee: ${assignee || '<not specified>'}
    - status: ${status || '<not specified>'}
    - project: ${project || '<not specified>'}
    - sortBy: ${sortBy}
    - sortDirection: ${sortDirection}
    - limit: ${limit}`;
    
          // 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 for filtering and sorting issues in the list_issues tool.
    const ListIssuesInputSchema = z.object({
      assignedToMe: z.boolean().default(false),
      assignee: z.string().optional(),
      status: z.string().optional(),
      project: z.string().optional(),
      sortBy: z.enum(['createdAt', 'updatedAt']).default('createdAt'),
      sortDirection: z.enum(['ASC', 'DESC']).default('DESC'),
      limit: z.number().min(1).max(100).default(25),
      debug: z.boolean().default(false), // Debug mode to show extra diagnostics
    });
  • Tool factory using create_tool to register 'list_issues' with name, description, schema, and handler.
    export const ListIssues = create_tool({
      name: 'list_issues',
      description:
        'List Linear issues (also called tickets) with filtering by assignee, status, and project. Use this to browse and find issues in your Linear workspace.',
      inputSchema: ListIssuesInputSchema,
      handler,
    });
  • Core helper function that uses Linear SDK to query issues with filters, processes promises for related data (state, assignee, project), and returns validated results.
    async function listIssues(
      client,
      filters = {},
      { limit = 25, sortBy, sortDirection = 'ASC' } = {},
      logger
    ) {
      try {
        logger?.debug('Building Linear SDK filter parameters', {
          filters,
          limit,
          sortBy: sortBy || 'none',
          sortDirection,
        });
    
        // Build filters properly using Linear SDK filter syntax
        const filter = {};
    
        // Add assignee filter
        if (filters.assignedToMe) {
          // For assignedToMe we'll use the viewer API directly,
          // but we'll still build the filter for normal query as fallback
          filter.assignee = { id: { eq: 'me' } };
        } else if (filters.assignee) {
          filter.assignee = { name: { eq: filters.assignee } };
        }
    
        // Add status filter
        if (filters.status) {
          // Properly filter by state name
          filter.state = { name: { eq: filters.status } };
          logger?.debug(`Filtering by state name: ${filters.status}`);
        }
    
        // Add project filter
        if (filters.project) {
          // Filter by project name
          filter.project = { name: { eq: filters.project } };
          logger?.debug(`Filtering by project name: ${filters.project}`);
        }
    
        logger?.debug('Built filter object:', JSON.stringify(filter, null, 2));
    
        // Use the sort direction as provided
        let direction = sortDirection;
        logger?.debug(`Using sort direction: ${direction} for field: ${sortBy}`);
    
        // Build search parameters
        const searchParams = {
          first: limit,
        };
    
        // Add sort parameters if specified
        if (sortBy) {
          searchParams.orderBy = sortBy;
          searchParams.orderDirection = direction;
        }
    
        // Build the full GraphQL query parameters with proper filter
        const queryParams = {
          filter, // Use the properly constructed filter object
          ...searchParams,
        };
    
        // Log the complete query parameters for debugging
        logger?.debug(
          'Full Linear API query parameters:',
          JSON.stringify(queryParams, null, 2)
        );
    
        let result;
    
        try {
          // First try using the viewer API which may be more reliable for getting assigned issues
          if (filters.assignedToMe) {
            logger?.debug(
              'Using viewer.assignedIssues API for retrieving assigned issues'
            );
            const me = await client.viewer;
            result = await me.assignedIssues(searchParams);
            logger?.debug('Successfully retrieved issues using viewer API');
          } else {
            // Otherwise use the direct GraphQL API from Linear SDK
            logger?.debug('Using issues API for general issue search');
            // @ts-ignore - The Linear SDK types are not accurate for the GraphQL API
            result = await client.issues(queryParams);
            logger?.debug('Successfully retrieved issues using issues API');
          }
        } catch (error) {
          logger?.error(`Linear API error:`, error);
          throw error;
        }
    
        // Log the raw response structure (without full content)
        logger?.debug(`Linear API response structure:`, {
          hasNodes: !!result.nodes,
          nodeCount: result.nodes?.length || 0,
          firstNodeKeys: result.nodes?.[0] ? Object.keys(result.nodes[0]) : [],
        });
    
        // Log search results at debug level
        logger?.debug(`Linear list returned ${result.nodes.length} results`);
    
        // Process issues from Linear SDK format to our domain model
        // The Linear SDK returns promises for many fields, so we need to await them
        const processedIssues = [];
    
        for (const issue of result.nodes) {
          try {
            logger?.debug(`Processing 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}`);
            }
    
            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,
              createdAt: issue.createdAt,
              updatedAt: issue.updatedAt,
            });
    
            processedIssues.push(processedIssue);
          } catch (processError) {
            logger?.error(
              `Error processing issue ${issue.id}: ${processError.message}`
            );
            // Continue processing other issues even if one fails
          }
        }
    
        logger?.debug(
          `Successfully processed ${processedIssues.length}/${result.nodes.length} issues`
        );
        const issues = processedIssues;
    
        // Return the processed results
        return SearchResultsSchema.parse({ results: issues });
      } catch (error) {
        // Enhanced error logging
        logger?.error(`Error listing Linear issues: ${error.message}`, {
          filters,
          limit,
          sortBy,
          sortDirection,
          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)
    Instantiation of ListIssues tool with context and inclusion in all_tools array for MCP server registration.
    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 full burden but offers minimal behavioral disclosure. It mentions filtering but doesn't cover pagination (implied by 'limit' parameter), rate limits, authentication needs, or what happens with large result sets. For a list tool with 8 parameters, this leaves significant gaps in understanding how the tool behaves operationally.

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 appropriately concise with two sentences that front-load the core functionality. Every sentence contributes value: the first states purpose and key filters, the second provides usage intent. No wasted words, though it could be slightly more structured for clarity.

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's complexity (8 parameters, no annotations, no output schema), the description is incomplete. It lacks details on return format, error handling, pagination behavior, and doesn't fully explain parameter usage. For a list tool with filtering and sorting capabilities, 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 description must compensate but only mentions three parameters (assignee, status, project) out of eight. It doesn't explain 'assignedToMe', 'debug', 'limit', 'sortBy', or 'sortDirection', leaving most parameters undocumented. The description adds some meaning for the three mentioned but fails to cover the majority.

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 ('List') and resource ('Linear issues/tickets'), and mentions filtering capabilities. It distinguishes the tool's purpose from siblings like 'get_issue' (single issue) and 'create_issue' (write operation). However, it doesn't explicitly differentiate from 'list_projects' or 'list_teams' in terms of resource type, which keeps it from a perfect score.

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 provides implied usage context ('to browse and find issues in your Linear workspace'), which suggests this is for exploration rather than specific retrieval. It doesn't explicitly state when to use this vs. alternatives like 'get_issue' for single issues or mention any exclusions, leaving some ambiguity for the agent.

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