Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,637 changes: 8 additions & 1,629 deletions src/index.ts

Large diffs are not rendered by default.

124 changes: 124 additions & 0 deletions src/tools/detect-circular-dependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
import { promises as fs } from 'fs';
import type { ToolContext, ToolResponse } from './types.js';
import { InternalFileGraph } from '../utils/usage-tracker.js';

export const definition: Tool = {
name: 'detect_circular_dependencies',
description:
'Analyze the import graph to detect circular dependencies between files. ' +
'Circular dependencies can cause initialization issues, tight coupling, and maintenance problems. ' +
'Returns all detected cycles sorted by length (shorter cycles are often more problematic).',
inputSchema: {
type: 'object',
properties: {
scope: {
type: 'string',
description:
"Optional path prefix to limit analysis (e.g., 'src/features', 'libs/shared')"
}
}
}
};

export async function handle(
args: Record<string, unknown>,
ctx: ToolContext
): Promise<ToolResponse> {
const { scope } = args as { scope?: string };

try {
const intelligencePath = ctx.paths.intelligence;
const content = await fs.readFile(intelligencePath, 'utf-8');
const intelligence = JSON.parse(content);

if (!intelligence.internalFileGraph) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
status: 'error',
message:
'Internal file graph not found. Please run refresh_index to rebuild the index with cycle detection support.'
},
null,
2
)
}
]
};
}

// Reconstruct the graph from stored data
const graph = InternalFileGraph.fromJSON(intelligence.internalFileGraph, ctx.rootPath);
const cycles = graph.findCycles(scope);
const graphStats = intelligence.internalFileGraph.stats || graph.getStats();

if (cycles.length === 0) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
status: 'success',
message: scope
? `No circular dependencies detected in scope: ${scope}`
: 'No circular dependencies detected in the codebase.',
scope,
graphStats
},
null,
2
)
}
]
};
}

return {
content: [
{
type: 'text',
text: JSON.stringify(
{
status: 'warning',
message: `Found ${cycles.length} circular dependency cycle(s).`,
scope,
cycles: cycles.map((c) => ({
files: c.files,
length: c.length,
severity: c.length === 2 ? 'high' : c.length <= 3 ? 'medium' : 'low'
})),
count: cycles.length,
graphStats,
advice:
'Shorter cycles (length 2-3) are typically more problematic. Consider breaking the cycle by extracting shared dependencies.'
},
null,
2
)
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
status: 'error',
message: 'Failed to detect circular dependencies. Run indexing first.',
error: error instanceof Error ? error.message : String(error)
},
null,
2
)
}
]
};
}
}
66 changes: 66 additions & 0 deletions src/tools/get-codebase-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
import { promises as fs } from 'fs';
import type { ToolContext, ToolResponse } from './types.js';
import { CodebaseIndexer } from '../core/indexer.js';

export const definition: Tool = {
name: 'get_codebase_metadata',
description:
'Get codebase metadata including framework information, dependencies, architecture patterns, ' +
'and project statistics.',
inputSchema: {
type: 'object',
properties: {}
}
};

export async function handle(
_args: Record<string, unknown>,
ctx: ToolContext
): Promise<ToolResponse> {
const indexer = new CodebaseIndexer({ rootPath: ctx.rootPath });
const metadata = await indexer.detectMetadata();

// Load team patterns from intelligence file
let teamPatterns = {};
try {
const intelligencePath = ctx.paths.intelligence;
const intelligenceContent = await fs.readFile(intelligencePath, 'utf-8');
const intelligence = JSON.parse(intelligenceContent);

if (intelligence.patterns) {
teamPatterns = {
dependencyInjection: intelligence.patterns.dependencyInjection,
stateManagement: intelligence.patterns.stateManagement,
componentInputs: intelligence.patterns.componentInputs
};
}
} catch (_error) {
// No intelligence file or parsing error
}

return {
content: [
{
type: 'text',
text: JSON.stringify(
{
status: 'success',
metadata: {
name: metadata.name,
framework: metadata.framework,
languages: metadata.languages,
dependencies: metadata.dependencies.slice(0, 20),
architecture: metadata.architecture,
projectStructure: metadata.projectStructure,
statistics: metadata.statistics,
teamPatterns
}
},
null,
2
)
}
]
};
}
108 changes: 108 additions & 0 deletions src/tools/get-component-usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
import { promises as fs } from 'fs';
import type { ToolContext, ToolResponse } from './types.js';

export const definition: Tool = {
name: 'get_component_usage',
description:
'Find WHERE a library or component is used in the codebase. ' +
"This is 'Find Usages' - returns all files that import a given package/module. " +
"Example: get_component_usage('@mycompany/utils') -> shows all files using it.",
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description:
"Import source to find usages for (e.g., 'primeng/table', '@mycompany/ui/button', 'lodash')"
}
},
required: ['name']
}
};

export async function handle(
args: Record<string, unknown>,
ctx: ToolContext
): Promise<ToolResponse> {
const { name: componentName } = args as { name: string };

try {
const intelligencePath = ctx.paths.intelligence;
const content = await fs.readFile(intelligencePath, 'utf-8');
const intelligence = JSON.parse(content);

const importGraph = intelligence.importGraph || {};
const usages = importGraph.usages || {};

// Find matching usages (exact match or partial match)
let matchedUsage = usages[componentName];

// Try partial match if exact match not found
if (!matchedUsage) {
const matchingKeys = Object.keys(usages).filter(
(key) => key.includes(componentName) || componentName.includes(key)
);
if (matchingKeys.length > 0) {
matchedUsage = usages[matchingKeys[0]];
}
}

if (matchedUsage) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
status: 'success',
component: componentName,
usageCount: matchedUsage.usageCount,
usedIn: matchedUsage.usedIn
},
null,
2
)
}
]
};
} else {
// Show top used as alternatives
const topUsed = importGraph.topUsed || [];
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
status: 'not_found',
component: componentName,
message: `No usages found for '${componentName}'.`,
suggestions: topUsed.slice(0, 10)
},
null,
2
)
}
]
};
}
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
status: 'error',
message: 'Failed to get component usage. Run indexing first.',
error: error instanceof Error ? error.message : String(error)
},
null,
2
)
}
]
};
}
}
56 changes: 56 additions & 0 deletions src/tools/get-indexing-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
import type { ToolContext, ToolResponse } from './types.js';

export const definition: Tool = {
name: 'get_indexing_status',
description:
'Get current indexing status: state, statistics, and progress. ' +
'Use refresh_index to manually trigger re-indexing when needed.',
inputSchema: {
type: 'object',
properties: {}
}
};

export async function handle(
_args: Record<string, unknown>,
ctx: ToolContext
): Promise<ToolResponse> {
const progress = ctx.indexState.indexer?.getProgress();

return {
content: [
{
type: 'text',
text: JSON.stringify(
{
status: ctx.indexState.status,
rootPath: ctx.rootPath,
lastIndexed: ctx.indexState.lastIndexed?.toISOString(),
stats: ctx.indexState.stats
? {
totalFiles: ctx.indexState.stats.totalFiles,
indexedFiles: ctx.indexState.stats.indexedFiles,
totalChunks: ctx.indexState.stats.totalChunks,
duration: `${(ctx.indexState.stats.duration / 1000).toFixed(2)}s`,
incremental: ctx.indexState.stats.incremental
}
: undefined,
progress: progress
? {
phase: progress.phase,
percentage: progress.percentage,
filesProcessed: progress.filesProcessed,
totalFiles: progress.totalFiles
}
: undefined,
error: ctx.indexState.error,
hint: 'Use refresh_index to manually trigger re-indexing when needed.'
},
null,
2
)
}
]
};
}
Loading
Loading