mcp-server-debug-thinking
Version:
Graph-based MCP server for systematic debugging using Problem-Solution Trees and Hypothesis-Experiment-Learning cycles
316 lines • 13.4 kB
JavaScript
import { ERROR_TYPE_EXACT_MATCH_SCORE, ERROR_TYPE_PARTIAL_MATCH_SCORE, ERROR_TYPE_SIMILARITY_THRESHOLD, KEYWORD_MATCH_SCORE, KEYWORD_AND_PARTIAL_SCORE, METADATA_ONLY_BASE_SCORE, DEFAULT_BASE_SCORE, RECENT_SESSION_BOOST, RECENT_SESSION_DAYS, SOMEWHAT_RECENT_SESSION_BOOST, SOMEWHAT_RECENT_SESSION_DAYS, HIGH_SUCCESS_RATE_THRESHOLD, HIGH_SUCCESS_RATE_BOOST, HIGH_SIMILARITY_THRESHOLD, DEFAULT_SEARCH_MODE, DEFAULT_KEYWORD_LOGIC } from '../constants.js';
import { logger } from '../utils/logger.js';
export class SearchIndex {
index = new Map();
addSession(sessionId, session) {
const searchableText = this.extractSearchableText(session);
const errorType = this.extractErrorType(session.problem?.errorMessage);
this.index.set(sessionId, {
sessionId,
session,
searchableText,
errorType
});
logger.dim(` 📚 Added to index: ${sessionId} (errorType: ${errorType || 'none'})`);
}
removeSession(sessionId) {
this.index.delete(sessionId);
}
hasSession(sessionId) {
return this.index.has(sessionId);
}
search(query, limit = 10) {
const matches = [];
const debugInfo = {
totalSessionsSearched: this.index.size,
keywordMatches: 0,
filteredByMetadata: 0,
filteredByConfidence: 0,
searchMode: query.searchMode || DEFAULT_SEARCH_MODE,
metadataOnlySearch: !query.errorType && (!query.keywords || query.keywords.length === 0)
};
for (const [sessionId, indexEntry] of this.index) {
const similarity = this.calculateSimilarity(query, indexEntry);
if (similarity > 0) {
if (query.keywords && query.keywords.length > 0 && similarity > 0) {
debugInfo.keywordMatches++;
}
const filterResult = this.matchesFilters(query, indexEntry, debugInfo);
if (filterResult) {
matches.push({ sessionId, similarity, indexEntry });
}
}
}
matches.sort((a, b) => b.similarity - a.similarity);
return matches.slice(0, limit).map(match => this.toPatternMatch(match, query, debugInfo));
}
extractSearchableText(session) {
const texts = [];
if (session.problem) {
texts.push(session.problem.description, session.problem.errorMessage || '', session.problem.expectedBehavior, session.problem.actualBehavior);
}
for (const step of session.steps) {
// Include thinking steps in searchable text
if (step.thinkingSteps) {
for (const thinking of step.thinkingSteps) {
texts.push(thinking.thought);
}
}
if (step.hypothesis) {
texts.push(step.hypothesis.cause);
texts.push(...step.hypothesis.affectedCode);
if (step.hypothesis.thoughtConclusion) {
texts.push(step.hypothesis.thoughtConclusion);
}
}
if (step.experiment) {
for (const change of step.experiment.changes) {
texts.push(change.reasoning);
}
}
if (step.result) {
texts.push(step.result.learning);
}
}
return texts.join(' ').toLowerCase();
}
extractErrorType(errorMessage) {
if (!errorMessage)
return undefined;
const patterns = [
/Error:\s*(\w+Error)/i,
/Error:\s*(\w+Exception)/i,
/^(\w+Error):/,
/^(\w+Exception):/,
/\b(\w+Error)\b/,
/\b(\w+Exception)\b/,
/\b(ECONNREFUSED|ENOENT|EACCES|ETIMEDOUT|EADDRINUSE)\b/,
/UnhandledPromiseRejectionWarning:\s*(\w+)/,
/at\s+\w+\s+\(.*?(\w+Error)/
];
for (const pattern of patterns) {
const match = errorMessage.match(pattern);
if (match) {
return match[1];
}
}
return undefined;
}
calculateSimilarity(query, indexEntry) {
let score = 0;
const searchMode = query.searchMode || DEFAULT_SEARCH_MODE;
const keywordLogic = query.keywordLogic || DEFAULT_KEYWORD_LOGIC;
// Error type matching
if (query.errorType && indexEntry.errorType) {
const queryError = query.errorType.toLowerCase();
const indexError = indexEntry.errorType.toLowerCase();
if (searchMode === "exact") {
if (queryError === indexError) {
score += ERROR_TYPE_EXACT_MATCH_SCORE;
}
}
else {
if (queryError === indexError) {
score += ERROR_TYPE_EXACT_MATCH_SCORE;
}
else if (indexError.includes(queryError) || queryError.includes(indexError)) {
score += ERROR_TYPE_PARTIAL_MATCH_SCORE;
}
else {
const similarity = this.calculateStringSimilarity(queryError, indexError);
if (similarity > ERROR_TYPE_SIMILARITY_THRESHOLD) {
score += 0.2 * similarity;
}
}
}
}
// Keyword matching
if (query.keywords && query.keywords.length > 0) {
const searchableText = indexEntry.searchableText;
let keywordMatches = 0;
const totalKeywords = query.keywords.length;
for (const keyword of query.keywords) {
const lowerKeyword = keyword.toLowerCase();
if (searchMode === "exact") {
const wordBoundaryRegex = new RegExp(`\\b${lowerKeyword}\\b`);
if (wordBoundaryRegex.test(searchableText)) {
keywordMatches++;
}
}
else {
if (searchableText.includes(lowerKeyword)) {
keywordMatches++;
}
}
}
if (keywordLogic === "AND") {
if (keywordMatches === totalKeywords) {
score += KEYWORD_MATCH_SCORE;
}
else {
score += (keywordMatches / totalKeywords) * KEYWORD_AND_PARTIAL_SCORE;
}
}
else {
if (keywordMatches > 0) {
score += (keywordMatches / totalKeywords) * KEYWORD_MATCH_SCORE;
}
}
}
// Boost score for recent sessions
const session = indexEntry.session;
const daysSinceCreated = (Date.now() - new Date(session.startTime).getTime()) / (1000 * 60 * 60 * 24);
if (daysSinceCreated < RECENT_SESSION_DAYS) {
score += RECENT_SESSION_BOOST;
}
else if (daysSinceCreated < SOMEWHAT_RECENT_SESSION_DAYS) {
score += SOMEWHAT_RECENT_SESSION_BOOST;
}
// Boost score for high success rate
const successRate = session.steps.filter((s) => s.result?.success).length / session.steps.length;
if (successRate > HIGH_SUCCESS_RATE_THRESHOLD) {
score += HIGH_SUCCESS_RATE_BOOST;
}
// If only metadata criteria are provided
if (!query.errorType && (!query.keywords || query.keywords.length === 0)) {
if (query.language || query.framework) {
score = METADATA_ONLY_BASE_SCORE;
}
else {
score = DEFAULT_BASE_SCORE;
}
}
return Math.min(score, 1.0);
}
calculateStringSimilarity(str1, str2) {
const maxLen = Math.max(str1.length, str2.length);
if (maxLen === 0)
return 1.0;
const distance = this.levenshteinDistance(str1, str2);
return 1 - (distance / maxLen);
}
levenshteinDistance(str1, str2) {
const matrix = [];
for (let i = 0; i <= str2.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= str1.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= str2.length; i++) {
for (let j = 1; j <= str1.length; j++) {
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
}
else {
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
}
}
}
return matrix[str2.length][str1.length];
}
matchesFilters(query, indexEntry, debugInfo) {
const session = indexEntry.session;
// Language filter
if (query.language) {
if (session.metadata?.language) {
if (query.language.toLowerCase() !== session.metadata.language.toLowerCase()) {
if (debugInfo)
debugInfo.filteredByMetadata++;
return false;
}
}
}
// Framework filter
if (query.framework) {
if (session.metadata?.framework) {
if (query.framework.toLowerCase() !== session.metadata.framework.toLowerCase()) {
if (debugInfo)
debugInfo.filteredByMetadata++;
return false;
}
}
}
// Tags filter
if (query.tags && query.tags.length > 0) {
if (session.metadata?.tags && session.metadata.tags.length > 0) {
const sessionTags = session.metadata.tags.map((t) => t.toLowerCase());
const queryTags = query.tags.map(t => t.toLowerCase());
const hasMatchingTag = queryTags.some(tag => sessionTags.includes(tag));
if (!hasMatchingTag) {
if (debugInfo)
debugInfo.filteredByMetadata++;
return false;
}
}
}
// Confidence threshold filter
if (query.confidence_threshold !== undefined) {
const avgConfidence = this.getAverageConfidence(session);
if (avgConfidence < query.confidence_threshold) {
if (debugInfo)
debugInfo.filteredByConfidence++;
return false;
}
}
return true;
}
getAverageConfidence(session) {
const confidences = session.steps
.filter(step => step.hypothesis?.confidence !== undefined)
.map(step => step.hypothesis.confidence);
if (confidences.length === 0)
return 0;
return confidences.reduce((sum, conf) => sum + conf, 0) / confidences.length;
}
toPatternMatch(match, query, debugInfo) {
const session = match.indexEntry.session;
const successfulStep = session.steps.find((step) => step.result?.success);
const lastStep = session.steps[session.steps.length - 1];
let suggestedApproach;
if (match.similarity > HIGH_SIMILARITY_THRESHOLD && successfulStep) {
const approaches = [
`Based on similar issue: ${successfulStep.hypothesis?.cause}`,
`Try: ${successfulStep.experiment?.changes[0]?.reasoning}`,
successfulStep.result?.learning ? `Key insight: ${successfulStep.result.learning}` : ''
].filter(a => a);
suggestedApproach = approaches.join('\n');
}
const result = {
sessionId: match.sessionId,
similarity: match.similarity,
problem: {
description: session.problem?.description || '',
errorMessage: session.problem?.errorMessage || '',
actualBehavior: session.problem?.actualBehavior || '',
expectedBehavior: session.problem?.expectedBehavior || ''
},
solution: {
hypothesis: successfulStep?.hypothesis?.cause || lastStep?.hypothesis?.cause || '',
changes: (successfulStep?.experiment?.changes || lastStep?.experiment?.changes || []).map((change) => ({
file: change.file,
reasoning: change.reasoning
})),
learning: successfulStep?.result?.learning || lastStep?.result?.learning || ''
},
metadata: {
confidence: this.getAverageConfidence(session),
language: session.metadata?.language,
framework: session.metadata?.framework,
tags: session.metadata?.tags,
createdAt: session.startTime.toISOString(),
timesApplied: 1,
successRate: session.steps.filter((s) => s.result?.success).length / session.steps.length * 100
},
suggestedApproach
};
if (query.includeDebugInfo && debugInfo) {
result.debugInfo = {
...debugInfo,
errorTypeExtracted: match.indexEntry.errorType
};
}
return result;
}
}
//# sourceMappingURL=SearchIndex.js.map