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 },
            };
          }
        }
      );
    }
Behavior2/5

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

No annotations are provided, so the description carries the full burden. It mentions 'optional filtering' and output details, but lacks critical behavioral traits like pagination (implied by 'limit' param but not described), rate limits, authentication requirements, or whether it's read-only. The description doesn't contradict annotations, but is insufficient for a mutation-free tool with 9 parameters.

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, well-structured sentence that front-loads the core action and key features. Every word earns its place, with no redundancy or fluff, making it highly efficient.

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 9 parameters with 0% schema coverage, no annotations, and no output schema, the description is incomplete. It covers basic purpose and some filtering but misses behavioral context, parameter details, and output format. For a list tool with many options, this leaves significant gaps for an AI agent.

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. It mentions filtering by 'team, name, and archive status', which partially covers 3 of 9 parameters (teamId, nameFilter, includeArchived), but omits details on others like 'state', 'limit', 'fuzzyMatch', 'debug', 'projectId', and 'includeThroughIssues'. The description adds some meaning but leaves most parameters undocumented.

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 projects'), and specifies optional filtering criteria (team, name, archive status) and output details (status, lead, progress, dates). It distinguishes from siblings like 'get_project' by indicating it lists multiple projects with filtering, though it doesn't explicitly contrast with 'list_issues' or 'list_teams'.

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 implies usage for retrieving multiple projects with filtering, but doesn't explicitly state when to use this vs. alternatives like 'get_project' (for a single project) or other list tools. No guidance on prerequisites, exclusions, or specific scenarios is provided.

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