/**
* @fileoverview Run handler for the DeepSource MCP server
* This module provides MCP tool handlers for fetching a specific DeepSource analysis run.
*/
import {
DeepSourceClient,
OccurrenceDistributionByAnalyzer,
OccurrenceDistributionByCategory,
} from '../deepsource.js';
import { ApiResponse } from '../models/common.js';
import { createLogger, Logger } from '../utils/logging/logger.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';
import { asProjectKey, asRunId, asCommitOid } from '../types/branded.js';
// Logger for the run handler
const logger = createLogger('RunHandler');
/**
* Interface for parameters for fetching a specific run
* @public
*/
export interface DeepsourceRunParams {
/** DeepSource project key to fetch the run from */
projectKey: string;
/** The run identifier (runUid or commitOid) */
runIdentifier: string;
/** Flag to indicate whether the runIdentifier is a commitOid (default: false) */
isCommitOid?: boolean;
}
/**
* Extended dependencies interface for run handler
*/
interface RunHandlerDeps {
analysisRunRepository: IAnalysisRunRepository;
logger: Logger;
}
/**
* Creates a run handler with injected dependencies using domain aggregates
* @param deps - The dependencies for the handler
* @returns The configured handler function
*/
export function createRunHandlerWithRepo(deps: RunHandlerDeps) {
return async function handleRun(params: DeepsourceRunParams) {
try {
const { projectKey, runIdentifier, isCommitOid = false } = params;
const projectKeyBranded = asProjectKey(projectKey);
deps.logger.info('Fetching run from repository', {
projectKey,
runIdentifier,
identifierType: isCommitOid ? 'commitOid' : 'runUid',
});
let domainRun: AnalysisRun | null;
if (isCommitOid) {
// Search by commit OID
const commitOidBranded = asCommitOid(runIdentifier);
domainRun = await deps.analysisRunRepository.findByCommit(
projectKeyBranded,
commitOidBranded
);
} else {
// Search by run ID (assuming runIdentifier is the runId)
const runIdBranded = asRunId(runIdentifier);
domainRun = await deps.analysisRunRepository.findByRunId(runIdBranded);
}
if (!domainRun) {
deps.logger.error('Run not found', {
projectKey,
runIdentifier,
identifierType: isCommitOid ? 'commitOid' : 'runUid',
});
throw new Error(
`Run with ${isCommitOid ? 'commitOid' : 'runUid'} "${runIdentifier}" not found`
);
}
deps.logger.info('Successfully fetched run', {
runId: domainRun.runId,
commitOid: domainRun.commitInfo.oid,
branchName: domainRun.commitInfo.branch,
status: domainRun.status,
});
const runData = {
run: {
id: domainRun.runId,
runUid: domainRun.runId, // Domain aggregate uses runId as the unique identifier
commitOid: domainRun.commitInfo.oid,
branchName: domainRun.commitInfo.branch,
baseOid: domainRun.commitInfo.baseOid,
status: domainRun.status,
createdAt: domainRun.timestamps.createdAt,
updatedAt: domainRun.timestamps.startedAt || domainRun.timestamps.createdAt,
finishedAt: domainRun.timestamps.finishedAt,
summary: {
occurrencesIntroduced: domainRun.summary.totalIntroduced.count,
occurrencesResolved: domainRun.summary.totalResolved.count,
occurrencesSuppressed: domainRun.summary.totalSuppressed.count,
occurrenceDistributionByAnalyzer: domainRun.summary.byAnalyzer.map((dist) => ({
analyzerShortcode: dist.analyzerShortcode,
introduced: dist.introduced.count,
})),
occurrenceDistributionByCategory: domainRun.summary.byCategory.map((dist) => ({
category: dist.category,
introduced: dist.introduced.count,
})),
},
repository: {
name: 'Repository', // Domain aggregate doesn't store repository name directly
id: domainRun.repositoryId,
},
},
// Provide helpful guidance and related information
analysis: {
status_info: getStatusInfo(domainRun.status),
issue_summary: `This run introduced ${domainRun.summary.totalIntroduced.count} issues, resolved ${domainRun.summary.totalResolved.count} issues, and suppressed ${domainRun.summary.totalSuppressed.count} issues.`,
analyzers_used: domainRun.summary.byAnalyzer?.map((a) => a.analyzerShortcode) || [],
issue_categories: domainRun.summary.byCategory?.map((c) => c.category) || [],
},
related_tools: {
issues: 'Use the project_issues tool to get all issues in the project',
runs: 'Use the runs tool to list all runs for the project',
recent_issues:
'Use the recent_run_issues tool to get issues from the most recent run on a branch',
},
};
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(runData),
},
],
};
} catch (error) {
deps.logger.error('Error in handleRun', {
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 run',
}),
},
],
};
}
};
}
/**
* Creates a run handler with injected dependencies
* @param deps - The dependencies for the handler
* @returns The configured handler factory
*/
export const createRunHandler = createBaseHandlerFactory(
'run',
async (
deps: BaseHandlerDeps,
{ projectKey, runIdentifier, isCommitOid = false }: DeepsourceRunParams
) => {
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 run', {
projectKey,
runIdentifier,
identifierType: isCommitOid ? 'commitOid' : 'runUid',
});
const run = await client.getRun(runIdentifier);
if (!run) {
deps.logger.error('Run not found', {
projectKey,
runIdentifier,
identifierType: isCommitOid ? 'commitOid' : 'runUid',
});
throw new Error(
`Run with ${isCommitOid ? 'commitOid' : 'runUid'} "${runIdentifier}" not found in project "${projectKey}"`
);
}
deps.logger.info('Successfully fetched run', {
runUid: run.runUid,
commitOid: run.commitOid,
branchName: run.branchName,
status: run.status,
});
const runData = {
run: {
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,
},
},
// Provide helpful guidance and related information
analysis: {
status_info: getStatusInfo(run.status),
issue_summary: `This run introduced ${run.summary.occurrencesIntroduced} issues, resolved ${run.summary.occurrencesResolved} issues, and suppressed ${run.summary.occurrencesSuppressed} issues.`,
analyzers_used:
run.summary.occurrenceDistributionByAnalyzer?.map(
(a: OccurrenceDistributionByAnalyzer) => a.analyzerShortcode
) || [],
issue_categories:
run.summary.occurrenceDistributionByCategory?.map(
(c: OccurrenceDistributionByCategory) => c.category
) || [],
},
related_tools: {
issues: 'Use the project_issues tool to get all issues in the project',
runs: 'Use the runs tool to list all runs for the project',
recent_issues:
'Use the recent_run_issues tool to get issues from the most recent run on a branch',
},
};
return wrapInApiResponse(runData);
}
);
/**
* Fetches and returns information about a specific analysis run using domain aggregates
* @param params - Parameters for fetching the run, including project key and run identifier
* @returns A response containing the run data
* @throws Error if the DEEPSOURCE_API_KEY environment variable is not set
* @public
*/
export async function handleDeepsourceRun(params: DeepsourceRunParams): Promise<ApiResponse> {
const baseDeps = createDefaultHandlerDeps({ logger });
const apiKey = baseDeps.getApiKey();
const repositoryFactory = new RepositoryFactory({ apiKey });
const analysisRunRepository = repositoryFactory.createAnalysisRunRepository();
const deps: RunHandlerDeps = {
analysisRunRepository,
logger,
};
const handler = createRunHandlerWithRepo(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 run error');
}
}
return result;
}
/**
* Helper function to get human-readable status information
* @param status The run status
* @returns Information about the status
* @private
*/
function getStatusInfo(status: string): string {
switch (status) {
case 'PENDING':
return 'The run is currently queued and waiting to be processed.';
case 'SUCCESS':
return 'The run completed successfully with all analyzers.';
case 'FAILURE':
return 'The run failed due to an error during analysis.';
case 'TIMEOUT':
return 'The run exceeded the maximum allowed time and was terminated.';
case 'CANCEL':
return 'The run was manually cancelled.';
case 'READY':
return 'The run is ready to be processed but not yet started.';
case 'SKIPPED':
return 'The run was skipped, possibly due to no code changes detected.';
default:
return `Unknown status: ${status}`;
}
}