Skip to main content
Glama
project-runs.ts10.8 kB
/** * @fileoverview Project runs handler for the DeepSource MCP server * This module provides MCP tool handlers for DeepSource project analysis runs. */ import { DeepSourceClient, DeepSourceRun } from '../deepsource.js'; import { ApiResponse } from '../models/common.js'; import { createLogger, Logger } from '../utils/logging/logger.js'; import { RunFilterParams } from '../models/runs.js'; import { AnalyzerShortcode, asProjectKey } from '../types/branded.js'; import { BaseHandlerDeps } from './base/handler.interface.js'; import { createBaseHandlerFactory, wrapInApiResponse, createDefaultHandlerDeps, } from './base/handler.factory.js'; import { IAnalysisRunRepository } from '../domain/aggregates/analysis-run/analysis-run.repository.js'; import { AnalysisRun } from '../domain/aggregates/analysis-run/analysis-run.aggregate.js'; import { RepositoryFactory } from '../infrastructure/factories/repository.factory.js'; // Logger for the project runs handler const logger = createLogger('ProjectRunsHandler'); /** * Interface for parameters for fetching project runs * @public */ export interface DeepsourceProjectRunsParams extends RunFilterParams { /** DeepSource project key to fetch runs for */ projectKey: string; } /** * Extended dependencies interface for project runs handler */ interface ProjectRunsHandlerDeps { analysisRunRepository: IAnalysisRunRepository; logger: Logger; } /** * Creates a project runs handler with injected dependencies using domain aggregates * @param deps - The dependencies for the handler * @returns The configured handler function */ export function createProjectRunsHandlerWithRepo(deps: ProjectRunsHandlerDeps) { return async function handleProjectRuns(params: DeepsourceProjectRunsParams) { try { const { projectKey, analyzerIn, first } = params; // Note: after, last, before pagination parameters not yet implemented const projectKeyBranded = asProjectKey(projectKey); deps.logger.info('Fetching project runs from repository', { projectKey, hasAnalyzerFilter: Boolean(analyzerIn), }); // Get the analysis runs from repository // Note: Basic pagination using page/pageSize for now // Note: Advanced pagination and analyzer filtering can be implemented in future versions const pageSize = first || 20; // Default page size const page = 1; // For now, always fetch first page const result = await deps.analysisRunRepository.findByProject(projectKeyBranded, { page, pageSize, }); deps.logger.info('Successfully fetched project runs', { count: result.items.length, totalCount: result.totalCount, hasNextPage: result.hasNextPage, hasPreviousPage: result.hasPreviousPage, }); const runsData = { runs: result.items.map((run: AnalysisRun) => ({ id: run.runId, runUid: run.runId, // Domain aggregate uses runId as the unique identifier commitOid: run.commitInfo.oid, branchName: run.commitInfo.branch, baseOid: run.commitInfo.baseOid, status: run.status, createdAt: run.timestamps.createdAt, updatedAt: run.timestamps.startedAt || run.timestamps.createdAt, // Use startedAt or fallback to createdAt finishedAt: run.timestamps.finishedAt, summary: { occurrencesIntroduced: run.summary.totalIntroduced.count, occurrencesResolved: run.summary.totalResolved.count, occurrencesSuppressed: run.summary.totalSuppressed.count, occurrenceDistributionByAnalyzer: run.summary.byAnalyzer.map((dist) => ({ analyzerShortcode: dist.analyzerShortcode, introduced: dist.introduced.count, })), occurrenceDistributionByCategory: run.summary.byCategory.map((dist) => ({ category: dist.category, introduced: dist.introduced.count, })), }, repository: { name: 'Repository', // Domain aggregate doesn't store repository name directly id: run.repositoryId, }, })), pageInfo: { hasNextPage: result.hasNextPage || false, hasPreviousPage: result.hasPreviousPage || false, startCursor: null, // Cursor-based pagination not yet implemented in domain layer endCursor: null, // Cursor-based pagination not yet implemented in domain layer }, totalCount: result.totalCount, // Provide helpful guidance on filtering and pagination usage_examples: { filtering: { by_analyzer: 'Use the analyzerIn parameter to filter by specific analyzers', }, pagination: { next_page: 'For forward pagination, use first and after parameters', previous_page: 'For backward pagination, use last and before parameters', }, related_tools: { run_details: 'Use the run tool to get detailed information about a specific run', run_issues: 'Use the recent_run_issues tool to get issues from the most recent run', }, }, }; return { content: [ { type: 'text' as const, text: JSON.stringify(runsData), }, ], }; } catch (error) { deps.logger.error('Error in handleProjectRuns', { errorType: typeof error, errorName: error instanceof Error ? error.name : 'Unknown', errorMessage: error instanceof Error ? error.message : String(error), errorStack: error instanceof Error ? error.stack : 'No stack available', }); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; deps.logger.debug('Returning error response', { errorMessage }); return { isError: true, content: [ { type: 'text' as const, text: JSON.stringify({ error: errorMessage, details: 'Failed to retrieve project runs', }), }, ], }; } }; } /** * Creates a project runs handler with injected dependencies * @param deps - The dependencies for the handler * @returns The configured handler factory */ export const createProjectRunsHandler = createBaseHandlerFactory( 'runs', async ( deps: BaseHandlerDeps, { projectKey, analyzerIn, first, after, last, before }: DeepsourceProjectRunsParams ) => { const apiKey = deps.getApiKey(); deps.logger.debug('API key retrieved from config', { length: apiKey.length, prefix: `${apiKey.substring(0, 5)}...`, }); const client = new DeepSourceClient(apiKey); deps.logger.info('Fetching project runs', { projectKey, hasAnalyzerFilter: Boolean(analyzerIn), }); const params: { analyzerIn?: AnalyzerShortcode[]; first?: number; after?: string; last?: number; before?: string; } = {}; if (analyzerIn !== undefined) params.analyzerIn = analyzerIn as AnalyzerShortcode[]; if (first !== undefined) params.first = first; if (after !== undefined) params.after = after; if (last !== undefined) params.last = last; if (before !== undefined) params.before = before; const runs = await client.listRuns(projectKey, params); deps.logger.info('Successfully fetched project runs', { count: runs.items.length, totalCount: runs.totalCount, hasNextPage: runs.pageInfo?.hasNextPage, hasPreviousPage: runs.pageInfo?.hasPreviousPage, }); const runsData = { runs: runs.items.map((run: DeepSourceRun) => ({ id: run.id, runUid: run.runUid, commitOid: run.commitOid, branchName: run.branchName, baseOid: run.baseOid, status: run.status, createdAt: run.createdAt, updatedAt: run.updatedAt, finishedAt: run.finishedAt, summary: { occurrencesIntroduced: run.summary.occurrencesIntroduced, occurrencesResolved: run.summary.occurrencesResolved, occurrencesSuppressed: run.summary.occurrencesSuppressed, occurrenceDistributionByAnalyzer: run.summary.occurrenceDistributionByAnalyzer, occurrenceDistributionByCategory: run.summary.occurrenceDistributionByCategory, }, repository: { name: run.repository.name, id: run.repository.id, }, })), pageInfo: { hasNextPage: runs.pageInfo?.hasNextPage || false, hasPreviousPage: runs.pageInfo?.hasPreviousPage || false, startCursor: runs.pageInfo?.startCursor || null, endCursor: runs.pageInfo?.endCursor || null, }, totalCount: runs.totalCount, // Provide helpful guidance on filtering and pagination usage_examples: { filtering: { by_analyzer: 'Use the analyzerIn parameter to filter by specific analyzers', }, pagination: { next_page: 'For forward pagination, use first and after parameters', previous_page: 'For backward pagination, use last and before parameters', }, related_tools: { run_details: 'Use the run tool to get detailed information about a specific run', run_issues: 'Use the recent_run_issues tool to get issues from the most recent run', }, }, }; return wrapInApiResponse(runsData); } ); /** * Fetches and returns analysis runs from a specified DeepSource project using domain aggregates * @param params - Parameters for fetching runs, including project key and optional filters * @returns A response containing the runs data * @throws Error if the DEEPSOURCE_API_KEY environment variable is not set * @public */ export async function handleDeepsourceProjectRuns( params: DeepsourceProjectRunsParams ): Promise<ApiResponse> { const baseDeps = createDefaultHandlerDeps({ logger }); const apiKey = baseDeps.getApiKey(); const repositoryFactory = new RepositoryFactory({ apiKey }); const analysisRunRepository = repositoryFactory.createAnalysisRunRepository(); const deps: ProjectRunsHandlerDeps = { analysisRunRepository, logger, }; const handler = createProjectRunsHandlerWithRepo(deps); const result = await handler(params); // If the domain handler returned an error response, throw an error for backward compatibility if (result.isError) { const firstContent = result.content[0]; if (firstContent) { const errorData = JSON.parse(firstContent.text); throw new Error(errorData.error); } else { throw new Error('Unknown project runs error'); } } return result; }

Implementation Reference

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/sapientpants/deepsource-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server