Skip to content

Commit 6d5026b

Browse files
committed
refactor: extract 11 MCP tool handlers into src/tools/
Splits the monolithic src/index.ts (2,227 lines) into focused modules. Each of the 11 tools now lives in its own file under src/tools/, and src/index.ts is reduced to pure MCP protocol wiring (~607 lines). Changes: - Add src/tools/types.ts — ToolContext, ToolResponse, ToolPaths, IndexState - Add src/tools/index.ts — TOOLS array aggregator and dispatchTool() router - Add src/tools/{search-codebase, get-codebase-metadata, get-indexing-status, refresh-index, get-style-guide, get-team-patterns, get-symbol-references, get-component-usage, detect-circular-dependencies, remember, get-memory}.ts - Remove inline TOOLS array and CallToolRequestSchema switch from src/index.ts - Replace with a single dispatchTool() call via injected ToolContext - Add tests/tools/dispatch.test.ts — 6 integration tests for tool routing
1 parent 9629447 commit 6d5026b

15 files changed

+1892
-1629
lines changed

src/index.ts

Lines changed: 8 additions & 1629 deletions
Large diffs are not rendered by default.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
2+
import { promises as fs } from 'fs';
3+
import type { ToolContext, ToolResponse } from './types.js';
4+
import { InternalFileGraph } from '../utils/usage-tracker.js';
5+
6+
export const definition: Tool = {
7+
name: 'detect_circular_dependencies',
8+
description:
9+
'Analyze the import graph to detect circular dependencies between files. ' +
10+
'Circular dependencies can cause initialization issues, tight coupling, and maintenance problems. ' +
11+
'Returns all detected cycles sorted by length (shorter cycles are often more problematic).',
12+
inputSchema: {
13+
type: 'object',
14+
properties: {
15+
scope: {
16+
type: 'string',
17+
description:
18+
"Optional path prefix to limit analysis (e.g., 'src/features', 'libs/shared')"
19+
}
20+
}
21+
}
22+
};
23+
24+
export async function handle(
25+
args: Record<string, unknown>,
26+
ctx: ToolContext
27+
): Promise<ToolResponse> {
28+
const { scope } = args as { scope?: string };
29+
30+
try {
31+
const intelligencePath = ctx.paths.intelligence;
32+
const content = await fs.readFile(intelligencePath, 'utf-8');
33+
const intelligence = JSON.parse(content);
34+
35+
if (!intelligence.internalFileGraph) {
36+
return {
37+
content: [
38+
{
39+
type: 'text',
40+
text: JSON.stringify(
41+
{
42+
status: 'error',
43+
message:
44+
'Internal file graph not found. Please run refresh_index to rebuild the index with cycle detection support.'
45+
},
46+
null,
47+
2
48+
)
49+
}
50+
]
51+
};
52+
}
53+
54+
// Reconstruct the graph from stored data
55+
const graph = InternalFileGraph.fromJSON(intelligence.internalFileGraph, ctx.rootPath);
56+
const cycles = graph.findCycles(scope);
57+
const graphStats = intelligence.internalFileGraph.stats || graph.getStats();
58+
59+
if (cycles.length === 0) {
60+
return {
61+
content: [
62+
{
63+
type: 'text',
64+
text: JSON.stringify(
65+
{
66+
status: 'success',
67+
message: scope
68+
? `No circular dependencies detected in scope: ${scope}`
69+
: 'No circular dependencies detected in the codebase.',
70+
scope,
71+
graphStats
72+
},
73+
null,
74+
2
75+
)
76+
}
77+
]
78+
};
79+
}
80+
81+
return {
82+
content: [
83+
{
84+
type: 'text',
85+
text: JSON.stringify(
86+
{
87+
status: 'warning',
88+
message: `Found ${cycles.length} circular dependency cycle(s).`,
89+
scope,
90+
cycles: cycles.map((c) => ({
91+
files: c.files,
92+
length: c.length,
93+
severity: c.length === 2 ? 'high' : c.length <= 3 ? 'medium' : 'low'
94+
})),
95+
count: cycles.length,
96+
graphStats,
97+
advice:
98+
'Shorter cycles (length 2-3) are typically more problematic. Consider breaking the cycle by extracting shared dependencies.'
99+
},
100+
null,
101+
2
102+
)
103+
}
104+
]
105+
};
106+
} catch (error) {
107+
return {
108+
content: [
109+
{
110+
type: 'text',
111+
text: JSON.stringify(
112+
{
113+
status: 'error',
114+
message: 'Failed to detect circular dependencies. Run indexing first.',
115+
error: error instanceof Error ? error.message : String(error)
116+
},
117+
null,
118+
2
119+
)
120+
}
121+
]
122+
};
123+
}
124+
}

src/tools/get-codebase-metadata.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
2+
import { promises as fs } from 'fs';
3+
import type { ToolContext, ToolResponse } from './types.js';
4+
import { CodebaseIndexer } from '../core/indexer.js';
5+
6+
export const definition: Tool = {
7+
name: 'get_codebase_metadata',
8+
description:
9+
'Get codebase metadata including framework information, dependencies, architecture patterns, ' +
10+
'and project statistics.',
11+
inputSchema: {
12+
type: 'object',
13+
properties: {}
14+
}
15+
};
16+
17+
export async function handle(
18+
_args: Record<string, unknown>,
19+
ctx: ToolContext
20+
): Promise<ToolResponse> {
21+
const indexer = new CodebaseIndexer({ rootPath: ctx.rootPath });
22+
const metadata = await indexer.detectMetadata();
23+
24+
// Load team patterns from intelligence file
25+
let teamPatterns = {};
26+
try {
27+
const intelligencePath = ctx.paths.intelligence;
28+
const intelligenceContent = await fs.readFile(intelligencePath, 'utf-8');
29+
const intelligence = JSON.parse(intelligenceContent);
30+
31+
if (intelligence.patterns) {
32+
teamPatterns = {
33+
dependencyInjection: intelligence.patterns.dependencyInjection,
34+
stateManagement: intelligence.patterns.stateManagement,
35+
componentInputs: intelligence.patterns.componentInputs
36+
};
37+
}
38+
} catch (_error) {
39+
// No intelligence file or parsing error
40+
}
41+
42+
return {
43+
content: [
44+
{
45+
type: 'text',
46+
text: JSON.stringify(
47+
{
48+
status: 'success',
49+
metadata: {
50+
name: metadata.name,
51+
framework: metadata.framework,
52+
languages: metadata.languages,
53+
dependencies: metadata.dependencies.slice(0, 20),
54+
architecture: metadata.architecture,
55+
projectStructure: metadata.projectStructure,
56+
statistics: metadata.statistics,
57+
teamPatterns
58+
}
59+
},
60+
null,
61+
2
62+
)
63+
}
64+
]
65+
};
66+
}

src/tools/get-component-usage.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
2+
import { promises as fs } from 'fs';
3+
import type { ToolContext, ToolResponse } from './types.js';
4+
5+
export const definition: Tool = {
6+
name: 'get_component_usage',
7+
description:
8+
'Find WHERE a library or component is used in the codebase. ' +
9+
"This is 'Find Usages' - returns all files that import a given package/module. " +
10+
"Example: get_component_usage('@mycompany/utils') -> shows all files using it.",
11+
inputSchema: {
12+
type: 'object',
13+
properties: {
14+
name: {
15+
type: 'string',
16+
description:
17+
"Import source to find usages for (e.g., 'primeng/table', '@mycompany/ui/button', 'lodash')"
18+
}
19+
},
20+
required: ['name']
21+
}
22+
};
23+
24+
export async function handle(
25+
args: Record<string, unknown>,
26+
ctx: ToolContext
27+
): Promise<ToolResponse> {
28+
const { name: componentName } = args as { name: string };
29+
30+
try {
31+
const intelligencePath = ctx.paths.intelligence;
32+
const content = await fs.readFile(intelligencePath, 'utf-8');
33+
const intelligence = JSON.parse(content);
34+
35+
const importGraph = intelligence.importGraph || {};
36+
const usages = importGraph.usages || {};
37+
38+
// Find matching usages (exact match or partial match)
39+
let matchedUsage = usages[componentName];
40+
41+
// Try partial match if exact match not found
42+
if (!matchedUsage) {
43+
const matchingKeys = Object.keys(usages).filter(
44+
(key) => key.includes(componentName) || componentName.includes(key)
45+
);
46+
if (matchingKeys.length > 0) {
47+
matchedUsage = usages[matchingKeys[0]];
48+
}
49+
}
50+
51+
if (matchedUsage) {
52+
return {
53+
content: [
54+
{
55+
type: 'text',
56+
text: JSON.stringify(
57+
{
58+
status: 'success',
59+
component: componentName,
60+
usageCount: matchedUsage.usageCount,
61+
usedIn: matchedUsage.usedIn
62+
},
63+
null,
64+
2
65+
)
66+
}
67+
]
68+
};
69+
} else {
70+
// Show top used as alternatives
71+
const topUsed = importGraph.topUsed || [];
72+
return {
73+
content: [
74+
{
75+
type: 'text',
76+
text: JSON.stringify(
77+
{
78+
status: 'not_found',
79+
component: componentName,
80+
message: `No usages found for '${componentName}'.`,
81+
suggestions: topUsed.slice(0, 10)
82+
},
83+
null,
84+
2
85+
)
86+
}
87+
]
88+
};
89+
}
90+
} catch (error) {
91+
return {
92+
content: [
93+
{
94+
type: 'text',
95+
text: JSON.stringify(
96+
{
97+
status: 'error',
98+
message: 'Failed to get component usage. Run indexing first.',
99+
error: error instanceof Error ? error.message : String(error)
100+
},
101+
null,
102+
2
103+
)
104+
}
105+
]
106+
};
107+
}
108+
}

src/tools/get-indexing-status.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
2+
import type { ToolContext, ToolResponse } from './types.js';
3+
4+
export const definition: Tool = {
5+
name: 'get_indexing_status',
6+
description:
7+
'Get current indexing status: state, statistics, and progress. ' +
8+
'Use refresh_index to manually trigger re-indexing when needed.',
9+
inputSchema: {
10+
type: 'object',
11+
properties: {}
12+
}
13+
};
14+
15+
export async function handle(
16+
_args: Record<string, unknown>,
17+
ctx: ToolContext
18+
): Promise<ToolResponse> {
19+
const progress = ctx.indexState.indexer?.getProgress();
20+
21+
return {
22+
content: [
23+
{
24+
type: 'text',
25+
text: JSON.stringify(
26+
{
27+
status: ctx.indexState.status,
28+
rootPath: ctx.rootPath,
29+
lastIndexed: ctx.indexState.lastIndexed?.toISOString(),
30+
stats: ctx.indexState.stats
31+
? {
32+
totalFiles: ctx.indexState.stats.totalFiles,
33+
indexedFiles: ctx.indexState.stats.indexedFiles,
34+
totalChunks: ctx.indexState.stats.totalChunks,
35+
duration: `${(ctx.indexState.stats.duration / 1000).toFixed(2)}s`,
36+
incremental: ctx.indexState.stats.incremental
37+
}
38+
: undefined,
39+
progress: progress
40+
? {
41+
phase: progress.phase,
42+
percentage: progress.percentage,
43+
filesProcessed: progress.filesProcessed,
44+
totalFiles: progress.totalFiles
45+
}
46+
: undefined,
47+
error: ctx.indexState.error,
48+
hint: 'Use refresh_index to manually trigger re-indexing when needed.'
49+
},
50+
null,
51+
2
52+
)
53+
}
54+
]
55+
};
56+
}

0 commit comments

Comments
 (0)