Skip to content

Commit 7a207b0

Browse files
committed
refactor mcp tools entrypoints into handlers
1 parent 0c11f34 commit 7a207b0

File tree

5 files changed

+500
-115
lines changed

5 files changed

+500
-115
lines changed

docs/design.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Design documents should be kept up-to-date with implementation changes.
2323
**Coding Rules**
2424

2525
- No comments (self-explanatory code)
26+
- Prefer strict types, no any
2627
- No duplication (eliminate redundant functions)
2728
- Simple logic (straightforward over complex)
2829
- Clear naming (functions explain purpose)

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "directory-indexer",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"description": "AI-powered directory indexing with semantic search for MCP servers",
55
"main": "dist/cli.js",
66
"bin": {
@@ -11,7 +11,7 @@
1111
"build": "vite build",
1212
"dev": "vite build --watch",
1313
"test": "vitest run",
14-
"test:unit": "vitest run tests/unit.test.ts tests/cli.unit.test.ts tests/edge-cases.unit.test.ts tests/error-handling.unit.test.ts tests/providers.unit.test.ts",
14+
"test:unit": "vitest run tests/unit.test.ts tests/cli.unit.test.ts tests/edge-cases.unit.test.ts tests/error-handling.unit.test.ts tests/providers.unit.test.ts tests/mcp-handlers.test.ts",
1515
"test:integration": "vitest run tests/integration.test.ts",
1616
"test:watch": "vitest",
1717
"test:coverage": "vitest run --coverage",

src/mcp-handlers.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { Config } from './config.js';
2+
import { indexDirectories } from './indexing.js';
3+
import { searchContent, findSimilarFiles, getFileContent, getChunkContent } from './search.js';
4+
import { getIndexStatus } from './storage.js';
5+
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
6+
7+
// Type-safe interfaces for MCP tool arguments
8+
interface IndexToolArgs {
9+
directory_path: string;
10+
}
11+
12+
interface SearchToolArgs {
13+
query: string;
14+
limit?: number;
15+
workspace?: string;
16+
}
17+
18+
interface SimilarFilesToolArgs {
19+
file_path: string;
20+
limit?: number;
21+
workspace?: string;
22+
}
23+
24+
interface GetContentToolArgs {
25+
file_path: string;
26+
chunks?: string;
27+
}
28+
29+
interface GetChunkToolArgs {
30+
file_path: string;
31+
chunk_id: string;
32+
}
33+
34+
// Type guard functions
35+
function isIndexToolArgs(args: unknown): args is IndexToolArgs {
36+
return typeof args === 'object' && args !== null &&
37+
typeof (args as IndexToolArgs).directory_path === 'string';
38+
}
39+
40+
function isSearchToolArgs(args: unknown): args is SearchToolArgs {
41+
return typeof args === 'object' && args !== null &&
42+
typeof (args as SearchToolArgs).query === 'string';
43+
}
44+
45+
function isSimilarFilesToolArgs(args: unknown): args is SimilarFilesToolArgs {
46+
return typeof args === 'object' && args !== null &&
47+
typeof (args as SimilarFilesToolArgs).file_path === 'string';
48+
}
49+
50+
function isGetContentToolArgs(args: unknown): args is GetContentToolArgs {
51+
return typeof args === 'object' && args !== null &&
52+
typeof (args as GetContentToolArgs).file_path === 'string';
53+
}
54+
55+
function isGetChunkToolArgs(args: unknown): args is GetChunkToolArgs {
56+
return typeof args === 'object' && args !== null &&
57+
typeof (args as GetChunkToolArgs).file_path === 'string' &&
58+
typeof (args as GetChunkToolArgs).chunk_id === 'string';
59+
}
60+
61+
export async function handleIndexTool(args: unknown, config: Config): Promise<CallToolResult> {
62+
if (!isIndexToolArgs(args)) {
63+
throw new Error('directory_path is required');
64+
}
65+
66+
const paths = args.directory_path.split(',').map((p: string) => p.trim());
67+
const result = await indexDirectories(paths, config);
68+
69+
return {
70+
content: [
71+
{
72+
type: 'text',
73+
text: `Indexed ${result.indexed} files, skipped ${result.skipped} files, ${result.errors.length} errors`
74+
}
75+
]
76+
};
77+
}
78+
79+
export async function handleSearchTool(args: unknown): Promise<CallToolResult> {
80+
if (!isSearchToolArgs(args)) {
81+
throw new Error('query is required');
82+
}
83+
84+
const options = {
85+
limit: args.limit || 10,
86+
workspace: args.workspace
87+
};
88+
89+
const results = await searchContent(args.query, options);
90+
91+
return {
92+
content: [
93+
{
94+
type: 'text',
95+
text: JSON.stringify(results, null, 2)
96+
}
97+
]
98+
};
99+
}
100+
101+
export async function handleSimilarFilesTool(args: unknown): Promise<CallToolResult> {
102+
if (!isSimilarFilesToolArgs(args)) {
103+
throw new Error('file_path is required');
104+
}
105+
106+
const results = await findSimilarFiles(
107+
args.file_path,
108+
args.limit || 10,
109+
args.workspace
110+
);
111+
112+
return {
113+
content: [
114+
{
115+
type: 'text',
116+
text: JSON.stringify(results, null, 2)
117+
}
118+
]
119+
};
120+
}
121+
122+
export async function handleGetContentTool(args: unknown): Promise<CallToolResult> {
123+
if (!isGetContentToolArgs(args)) {
124+
throw new Error('file_path is required');
125+
}
126+
127+
const content = await getFileContent(args.file_path, args.chunks);
128+
129+
return {
130+
content: [
131+
{
132+
type: 'text',
133+
text: content
134+
}
135+
]
136+
};
137+
}
138+
139+
export async function handleGetChunkTool(args: unknown): Promise<CallToolResult> {
140+
if (!isGetChunkToolArgs(args)) {
141+
throw new Error('file_path and chunk_id are required');
142+
}
143+
144+
const content = await getChunkContent(args.file_path, args.chunk_id);
145+
146+
return {
147+
content: [
148+
{
149+
type: 'text',
150+
text: content
151+
}
152+
]
153+
};
154+
}
155+
156+
export async function handleServerInfoTool(version: string): Promise<CallToolResult> {
157+
const status = await getIndexStatus();
158+
159+
return {
160+
content: [
161+
{
162+
type: 'text',
163+
text: JSON.stringify({
164+
name: 'directory-indexer',
165+
version: version,
166+
status: status
167+
}, null, 2)
168+
}
169+
]
170+
};
171+
}
172+
173+
export function formatErrorResponse(error: unknown): CallToolResult {
174+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
175+
return {
176+
content: [
177+
{
178+
type: 'text',
179+
text: `Error: ${errorMessage}`
180+
}
181+
],
182+
isError: true
183+
};
184+
}

src/mcp.ts

Lines changed: 28 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@ import { readFileSync } from 'fs';
99
import { join, dirname } from 'path';
1010
import { fileURLToPath } from 'url';
1111
import { Config } from './config.js';
12-
import { indexDirectories } from './indexing.js';
13-
import { searchContent, findSimilarFiles, getFileContent, getChunkContent } from './search.js';
14-
import { getIndexStatus } from './storage.js';
12+
import {
13+
handleIndexTool,
14+
handleSearchTool,
15+
handleSimilarFilesTool,
16+
handleGetContentTool,
17+
handleGetChunkTool,
18+
handleServerInfoTool,
19+
formatErrorResponse
20+
} from './mcp-handlers.js';
1521

1622
// Read version from package.json
1723
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -278,120 +284,29 @@ export async function startMcpServer(config: Config): Promise<void> {
278284

279285
try {
280286
switch (name) {
281-
case 'index': {
282-
if (!args || typeof args.directory_path !== 'string') {
283-
throw new Error('directory_path is required');
284-
}
285-
const paths = args.directory_path.split(',').map((p: string) => p.trim());
286-
const result = await indexDirectories(paths, config);
287-
return {
288-
content: [
289-
{
290-
type: 'text',
291-
text: `Indexed ${result.indexed} files, skipped ${result.skipped} files, ${result.errors.length} errors`
292-
}
293-
]
294-
};
295-
}
296-
297-
case 'search': {
298-
if (!args || typeof args.query !== 'string') {
299-
throw new Error('query is required');
300-
}
301-
const options = {
302-
limit: (args.limit as number) || 10,
303-
workspace: args.workspace as string | undefined
304-
};
305-
const results = await searchContent(args.query, options);
306-
return {
307-
content: [
308-
{
309-
type: 'text',
310-
text: JSON.stringify(results, null, 2)
311-
}
312-
]
313-
};
314-
}
315-
316-
case 'similar_files': {
317-
if (!args || typeof args.file_path !== 'string') {
318-
throw new Error('file_path is required');
319-
}
320-
const results = await findSimilarFiles(
321-
args.file_path,
322-
(args.limit as number) || 10,
323-
args.workspace as string | undefined
324-
);
325-
return {
326-
content: [
327-
{
328-
type: 'text',
329-
text: JSON.stringify(results, null, 2)
330-
}
331-
]
332-
};
333-
}
334-
335-
case 'get_content': {
336-
if (!args || typeof args.file_path !== 'string') {
337-
throw new Error('file_path is required');
338-
}
339-
const content = await getFileContent(args.file_path, args.chunks as string);
340-
return {
341-
content: [
342-
{
343-
type: 'text',
344-
text: content
345-
}
346-
]
347-
};
348-
}
349-
350-
case 'get_chunk': {
351-
if (!args || typeof args.file_path !== 'string' || typeof args.chunk_id !== 'string') {
352-
throw new Error('file_path and chunk_id are required');
353-
}
354-
const content = await getChunkContent(args.file_path, args.chunk_id);
355-
return {
356-
content: [
357-
{
358-
type: 'text',
359-
text: content
360-
}
361-
]
362-
};
363-
}
364-
365-
case 'server_info': {
366-
const status = await getIndexStatus();
367-
return {
368-
content: [
369-
{
370-
type: 'text',
371-
text: JSON.stringify({
372-
name: 'directory-indexer',
373-
version: VERSION,
374-
status: status
375-
}, null, 2)
376-
}
377-
]
378-
};
379-
}
380-
287+
case 'index':
288+
return await handleIndexTool(args, config);
289+
290+
case 'search':
291+
return await handleSearchTool(args);
292+
293+
case 'similar_files':
294+
return await handleSimilarFilesTool(args);
295+
296+
case 'get_content':
297+
return await handleGetContentTool(args);
298+
299+
case 'get_chunk':
300+
return await handleGetChunkTool(args);
301+
302+
case 'server_info':
303+
return await handleServerInfoTool(VERSION);
304+
381305
default:
382306
throw new Error(`Unknown tool: ${name}`);
383307
}
384308
} catch (error) {
385-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
386-
return {
387-
content: [
388-
{
389-
type: 'text',
390-
text: `Error: ${errorMessage}`
391-
}
392-
],
393-
isError: true
394-
};
309+
return formatErrorResponse(error);
395310
}
396311
});
397312

0 commit comments

Comments
 (0)