/**
* @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;
}