Skip to main content
Glama
scoutos

Linear MCP Server

by scoutos

list_projects

Retrieve and filter Linear projects by team, name, or archive status to display details like status, lead, progress, and project dates for efficient tracking and management.

Instructions

List Linear projects with optional filtering by team, name, and archive status. Shows project details including status, lead, progress, and dates.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
debugNo
fuzzyMatchNo
includeArchivedNo
includeThroughIssuesNo
limitNo
nameFilterNo
projectIdNo
stateNo
teamIdNo

Implementation Reference

  • The MCP ToolHandler function that executes the tool logic: processes validated input, creates Linear client using effects, calls the core listProjects function, formats detailed project list as text or error response.
    const handler = async (
      ctx,
      {
        teamId,
        nameFilter,
        projectId,
        state,
        includeArchived,
        includeThroughIssues,
        fuzzyMatch,
        limit,
        debug,
      }
    ) => {
      const logger = ctx.effects.logger;
    
      try {
        // Log details about config and parameters
        logger.debug('List projects called with parameters:', {
          teamId,
          nameFilter,
          projectId,
          state,
          includeArchived,
          includeThroughIssues,
          fuzzyMatch,
          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 projects using the Linear SDK client with filters
        logger.debug('Executing Linear API list with filters');
        const results = await listProjects(
          linearClient,
          {
            teamId,
            nameFilter,
            projectId,
            state,
            includeArchived,
            includeThroughIssues,
            fuzzyMatch,
          },
          {
            limit,
          },
          logger
        );
    
        // Log the results count
        logger.info(`Found ${results.results.length} projects matching criteria`);
    
        // Format the output
        let responseText = '';
    
        if (results.results.length === 0) {
          responseText = 'No projects found matching your criteria.';
        } else {
          responseText = 'Projects found:\n\n';
    
          results.results.forEach((project, index) => {
            // Format dates for display
            const formatDisplayDate = timestamp => {
              if (!timestamp) return 'Not set';
              try {
                const date = new Date(timestamp);
                return date.toLocaleString();
              } catch (e) {
                return 'Invalid date';
              }
            };
    
            // Determine project status
            let status = 'Active';
            if (project.archived) status = 'Archived';
            else if (project.canceled) status = 'Canceled';
            else if (project.completed) status = 'Completed';
            else if (project.state) status = project.state;
    
            // Format completion percentage
            const progressPercent = Math.round(project.progress * 100);
    
            responseText += `${index + 1}. ${project.name} [ID: ${project.id}]\n`;
    
            if (project.description) {
              // Truncate description to keep output manageable
              const truncatedDescription =
                project.description.length > 100
                  ? project.description.substring(0, 97) + '...'
                  : project.description;
              responseText += `   Description: ${truncatedDescription}\n`;
            }
    
            responseText += `   Status: ${status} (${progressPercent}% complete)\n`;
    
            if (project.teamName) {
              responseText += `   Team: ${project.teamName}\n`;
            }
    
            if (project.leadName) {
              responseText += `   Lead: ${project.leadName}\n`;
            }
    
            responseText += `   Issues: ${project.completedIssueCount}/${project.issueCount} completed\n`;
    
            // Add dates
            if (project.startDate) {
              responseText += `   Start date: ${formatDisplayDate(
                project.startDate
              )}\n`;
            }
    
            if (project.targetDate) {
              responseText += `   Target date: ${formatDisplayDate(
                project.targetDate
              )}\n`;
            }
    
            responseText += `   Created: ${formatDisplayDate(project.createdAt)}\n`;
    
            if (project.updatedAt) {
              responseText += `   Updated: ${formatDisplayDate(
                project.updatedAt
              )}\n`;
            }
    
            if (project.url) {
              responseText += `   URL: ${project.url}\n`;
            }
    
            responseText += '\n';
          });
        }
    
        logger.debug('Returning formatted list results');
        return {
          content: [{ type: 'text', text: responseText }],
        };
      } catch (error) {
        logger.error(`Error listing projects: ${error.message}`);
        logger.error(error.stack);
    
        // Create a user-friendly error message with troubleshooting guidance
        let errorMessage = `Error listing projects: ${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:
    - teamId: ${teamId || '<not specified>'}
    - nameFilter: ${nameFilter || '<not specified>'}
    - projectId: ${projectId || '<not specified>'}
    - state: ${state || '<not specified>'}
    - includeArchived: ${includeArchived}
    - includeThroughIssues: ${includeThroughIssues}
    - fuzzyMatch: ${fuzzyMatch}
    - 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 for the list_projects tool defining optional filters and parameters.
    const ListProjectsInputSchema = z.object({
      teamId: z.string().optional(),
      nameFilter: z.string().optional(),
      projectId: z.string().optional(), // Add direct project ID lookup
      state: z.enum(['active', 'completed', 'canceled', 'all']).optional(), // Add state filter
      includeArchived: z.boolean().default(false),
      includeThroughIssues: z.boolean().default(true), // Add option to include projects referenced by issues
      fuzzyMatch: z.boolean().default(true), // Enable fuzzy name matching by default
      limit: z.number().min(1).max(100).default(25),
      debug: z.boolean().default(false), // Debug mode to show extra diagnostics
    });
  • Tool registration via create_tool factory exporting ListProjects with name, description, schema, and handler.
    export const ListProjects = create_tool({
      name: 'list_projects',
      description:
        'List Linear projects with optional filtering by team, name, and archive status. Shows project details including status, lead, progress, and dates.',
      inputSchema: ListProjectsInputSchema,
      handler,
    });
  • Core helper function implementing Linear API queries, filtering (including fuzzy name matching), data enrichment from related entities, and result processing.
    async function listProjects(client, filters = {}, { limit = 25 } = {}, logger) {
      try {
        logger?.debug('Building Linear SDK filter parameters', {
          filters,
          limit,
        });
    
        // Store all projects we find
        const allProjects = new Map();
    
        // Direct lookup by project ID if specified
        if (filters.projectId) {
          logger?.debug(`Looking up project by ID: ${filters.projectId}`);
          try {
            // @ts-ignore - The Linear SDK types may not be fully accurate
            const project = await client.project(filters.projectId);
            if (project) {
              allProjects.set(project.id, project);
              logger?.debug(`Found project by ID: ${project.name} (${project.id})`);
            }
          } catch (projectError) {
            logger?.warn(`Error fetching project by ID: ${projectError.message}`);
          }
        }
    
        // Build query parameters for projects() method with all possible filters
        const queryParams = {
          first: Math.min(100, limit * 2), // Request enough projects but not too many
          includeArchived: filters.includeArchived,
          // Note: Removed orderBy parameter to fix TypeScript error
          // Would need to use the proper enum instead of a string
        };
    
        // Initialize filter object
        queryParams.filter = {};
        let hasFilter = false;
    
        // Apply state filter if provided
        if (filters.state && filters.state !== 'all') {
          queryParams.filter.state = { eq: filters.state };
          hasFilter = true;
          logger?.debug(`Added state filter: ${filters.state}`);
        }
    
        // Apply team filter if provided
        if (filters.teamId) {
          queryParams.filter.team = { id: { eq: filters.teamId } };
          hasFilter = true;
          logger?.debug(`Added team filter in projects query: ${filters.teamId}`);
        }
    
        // Apply name filter directly in the query if possible
        if (filters.nameFilter && !filters.fuzzyMatch) {
          // Only apply exact name filter here - fuzzy filtering will be done after fetching
          queryParams.filter.name = { contains: filters.nameFilter };
          hasFilter = true;
          logger?.debug(`Added name filter: ${filters.nameFilter}`);
        }
    
        // If no filters applied, remove empty filter object
        if (!hasFilter) {
          delete queryParams.filter;
        }
    
        // No need to separately fetch team projects since we're already filtering by team in the main query
        // This simplifies the code and reduces API calls
    
        // Skip fetching all projects if we're just looking up by ID
        if (!filters.projectId) {
          // Get all projects regardless of team
          logger?.debug(
            'Querying all projects with params:',
            JSON.stringify(queryParams, null, 2)
          );
    
          try {
            // @ts-ignore - The Linear SDK types may not be fully accurate
            const projectsResponse = await client.projects(queryParams);
            logger?.debug(
              `Found ${projectsResponse.nodes.length} projects from direct query`
            );
    
            // Add all projects to our collection
            for (const project of projectsResponse.nodes) {
              allProjects.set(project.id, project);
            }
          } catch (projectsError) {
            logger?.warn(`Error fetching projects: ${projectsError.message}`);
          }
        }
    
        // Add projects referenced by issues if enabled and we have fewer than expected projects
        // Only do this as a last resort if direct project queries don't yield enough results
        if (
          filters.includeThroughIssues !== false &&
          !filters.projectId &&
          allProjects.size < limit
        ) {
          logger?.debug('Looking for additional projects referenced by issues');
    
          try {
            // Build issue query parameters - only fetch what we need
            const issueQueryParams = {
              first: Math.min(30, limit), // Further reduce API load - we just need a few more projects
              // Removed orderBy parameter to fix TypeScript error - PaginationOrderBy enum is required
            };
    
            // If we're filtering by team, also filter issues by team
            if (filters.teamId) {
              issueQueryParams.filter = { team: { id: { eq: filters.teamId } } };
            }
    
            // Get issues and extract their projects in parallel
            const issuesResponse = await client.issues(issueQueryParams);
    
            if (issuesResponse.nodes.length > 0) {
              logger?.debug(
                `Found ${issuesResponse.nodes.length} issues to scan for projects`
              );
    
              // Extract projects from issues all at once
              const projectPromises = issuesResponse.nodes
                .filter(issue => issue.project) // Only process issues with project references
                .map(async issue => {
                  try {
                    return await issue.project;
                  } catch (error) {
                    return null;
                  }
                });
    
              // Wait for all project promises to resolve in parallel
              const projects = await Promise.all(projectPromises);
    
              // Add valid projects to our collection
              projects
                .filter(project => project && !allProjects.has(project.id))
                .forEach(project => {
                  allProjects.set(project.id, project);
                  logger?.debug(`Found additional project: ${project.name}`);
                });
            }
          } catch (error) {
            logger?.warn(`Error fetching issues: ${error.message}`);
          }
        }
    
        logger?.debug(
          `Total projects found (before filtering): ${allProjects.size}`
        );
    
        // Convert projects map to array
        let projectsArray = Array.from(allProjects.values());
    
        // Apply name filtering
        if (filters.nameFilter) {
          projectsArray = filterProjectsByName(
            projectsArray,
            filters.nameFilter,
            filters.fuzzyMatch !== false, // Enable fuzzy matching unless explicitly disabled
            logger
          );
    
          logger?.debug(`Projects after name filtering: ${projectsArray.length}`);
        }
    
        // Sort projects by updatedAt (most recent first) or name
        projectsArray.sort((a, b) => {
          if (a.updatedAt && b.updatedAt) {
            return (
              new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
            );
          }
          return a.name.localeCompare(b.name);
        });
    
        // Apply limit
        projectsArray = projectsArray.slice(0, limit);
        logger?.debug(
          `Projects after limiting to ${limit}: ${projectsArray.length}`
        );
    
        // Process projects to extract all relevant information
        const processedProjects = await processProjects(projectsArray, logger);
        logger?.debug(
          `Successfully processed ${processedProjects.length} projects`
        );
    
        return ProjectSearchResultsSchema.parse({ results: processedProjects });
      } catch (error) {
        // Enhanced error logging
        logger?.error(`Error listing Linear projects: ${error.message}`, {
          filters,
          limit,
          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-149 (registration)
    Final MCP server registration: instantiates ListProjects and registers it with the server.tool() method alongside other 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),
    ];
    
    // Register tools with the MCP server
    for (const tool of all_tools) {
      server.tool(
        tool.name,
        tool.description,
        tool.inputSchema.shape ?? {},
        async args => {
          try {
            // Call our tool
            const result = await tool.call(args);
    
            // Return format expected by MCP SDK
            return {
              content: result.content,
              error: result.isError
                ? {
                    message: result.content[0]?.text || 'An error occurred',
                  }
                : undefined,
            };
          } catch (error) {
            logger.error(`Error executing tool ${tool.name}: ${error.message}`);
            return {
              content: [{ type: 'text', text: `Error: ${error.message}` }],
              error: { message: error.message },
            };
          }
        }
      );
    }
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