List Skippr Issues
skippr_list_issuesList Skippr issues with filters for project, review, severity, category, and resolution status to find and address product problems.
Instructions
Lists all available Skippr issues with optional filtering by project, review, severity, category, and resolution status
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| projectId | Yes | Project identifier | |
| reviewId | No | Filter by review ID | |
| severity | No | Filter by severity level | |
| category | No | Filter by category | |
| resolved | No | Filter by resolved status |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| issues | Yes | ||
| totalCount | Yes |
Implementation Reference
- src/tools/list-issues.ts:42-90 (handler)Main handler function for skippr_list_issues. Takes validated input, finds all issue files via findAllIssues, reads each issue file, applies optional filters (reviewId, severity, category, resolved), and returns a list of IssueSummary objects with totalCount.
export async function listIssues(input: ListIssuesInput): Promise<ListIssuesOutput> { // Validate input const validated = ListIssuesInputSchema.parse(input); const { projectId, reviewId, severity, category, resolved } = validated; // Find all issue file paths const issueFiles = await findAllIssues(projectId); // Read and parse each issue file const issues: IssueSummary[] = []; for (const filePath of issueFiles) { // Extract reviewId and issueId from path // Path format: .skippr/projects/{projectId}/reviews/{reviewId}/issues/{issueId}.md const pathParts = filePath.split('/'); const fileReviewId = pathParts[4]; // reviews/{reviewId} const fileName = pathParts[6]; // {issueId}.md const fileIssueId = fileName.replace('.md', ''); try { const { frontmatter } = await readIssueFile(projectId, fileReviewId, fileIssueId); // Apply filters if (reviewId && frontmatter.reviewId !== reviewId) continue; if (severity && frontmatter.severity !== severity) continue; if (category && frontmatter.category !== category) continue; if (resolved !== undefined && frontmatter.resolved !== resolved) continue; // Add to results issues.push({ id: frontmatter.id, reviewId: frontmatter.reviewId, title: frontmatter.title, severity: frontmatter.severity as IssueSummary['severity'], resolved: frontmatter.resolved, category: frontmatter.category as IssueSummary['category'], }); } catch (error) { // Skip files that can't be parsed console.error(`Failed to parse issue file: ${filePath}`, error); continue; } } return { issues, totalCount: issues.length, }; } - src/tools/list-issues.ts:12-33 (schema)Input schema (ListIssuesInputSchema) defines required projectId and optional filters: reviewId (uuid), severity, category, resolved. Output schema (ListIssuesOutputSchema) defines an array of issues with id, reviewId, title, severity, resolved, optional category, plus totalCount.
export const ListIssuesInputSchema = z.object({ projectId: z.string().describe('Project identifier'), reviewId: z.string().uuid().optional().describe('Filter by review ID'), severity: IssueSeveritySchema.optional().describe('Filter by severity level'), category: CategorySchema.optional().describe('Filter by category'), resolved: z.boolean().optional().describe('Filter by resolved status'), }); // Output schema for the tool export const ListIssuesOutputSchema = z.object({ issues: z.array( z.object({ id: z.string().uuid(), reviewId: z.string().uuid(), title: z.string(), severity: IssueSeveritySchema, resolved: z.boolean(), category: CategorySchema.optional(), }) ), totalCount: z.number(), }); - src/servers/mcp.ts:39-51 (registration)Registration of 'skippr_list_issues' MCP tool on the MCP server via mcpServer.registerTool() with title, description, inputSchema, outputSchema, and an async handler that calls listIssues().
mcpServer.registerTool( 'skippr_list_issues', { title: 'List Skippr Issues', description: 'Lists all available Skippr issues with optional filtering by project, review, severity, category, and resolution status', inputSchema: ListIssuesInputSchema.shape, outputSchema: ListIssuesOutputSchema.shape }, async (args) => { const result = await listIssues(args as ListIssuesInput); return createStructuredResponse(result); } ); - src/utils/issues-reader.ts:16-51 (helper)Helper function findAllIssues() that scans .skippr/projects/{projectId}/reviews/*/issues/ for .md files and returns all issue file paths.
export async function findAllIssues(projectId: string): Promise<string[]> { const skipprDir = join(getWorkingDirectory(), '.skippr', 'projects', projectId, 'reviews'); const issueFiles: string[] = []; try { // Check if .skippr/reviews exists await stat(skipprDir); } catch { // .skippr directory doesn't exist return []; } // Read all review directories const reviewDirs = await readdir(skipprDir, { withFileTypes: true }); for (const reviewDir of reviewDirs) { if (!reviewDir.isDirectory()) continue; const issuesDir = join(skipprDir, reviewDir.name, 'issues'); try { const issueFilenames = await readdir(issuesDir); for (const filename of issueFilenames) { if (filename.endsWith('.md')) { issueFiles.push(join('.skippr', 'projects', projectId, 'reviews', reviewDir.name, 'issues', filename)); } } } catch { // issues directory doesn't exist for this review, skip continue; } } return issueFiles; } - src/utils/issues-reader.ts:60-74 (helper)Helper function readIssueFile() that reads and parses a single issue .md file, returning structured frontmatter and raw markdown content.
export async function readIssueFile( projectId: string, reviewId: string, issueId: string ): Promise<{ frontmatter: ParsedIssueFrontmatter; markdown: string }> { const filePath = join(getWorkingDirectory(), '.skippr', 'projects', projectId, 'reviews', reviewId, 'issues', `${issueId}.md`); const content = await readFile(filePath, 'utf-8'); const parsed = parseIssueFrontmatter(content); return { frontmatter: parsed.frontmatter, markdown: parsed.content, // Raw markdown for Claude to read }; }