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
| Name | Required | Description | Default |
|---|---|---|---|
| debug | No | ||
| fuzzyMatch | No | ||
| includeArchived | No | ||
| includeThroughIssues | No | ||
| limit | No | ||
| nameFilter | No | ||
| projectId | No | ||
| state | No | ||
| teamId | No |
Implementation Reference
- src/tools/list-projects.js:506-730 (handler)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, }; } };
- src/tools/list-projects.js:27-37 (schema)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 });
- src/tools/list-projects.js:735-741 (registration)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, });
- src/tools/list-projects.js:94-302 (helper)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 }, }; } } ); }