directory-indexer 0.2.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli-handlers.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/config.js +4 -4
- package/dist/config.js.map +1 -1
- package/dist/indexing.js +10 -2
- package/dist/indexing.js.map +1 -1
- package/dist/logger.js +43 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp-handlers.js +215 -42
- package/dist/mcp-handlers.js.map +1 -1
- package/dist/mcp.js +200 -106
- package/dist/mcp.js.map +1 -1
- package/dist/path-validation.js +47 -0
- package/dist/path-validation.js.map +1 -0
- package/dist/storage.js +48 -10
- package/dist/storage.js.map +1 -1
- package/dist/utils.js +2 -0
- package/dist/utils.js.map +1 -1
- package/package.json +18 -20
package/dist/mcp-handlers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-handlers.js","sources":["../src/mcp-handlers.ts"],"sourcesContent":["import { Config } from './config.js';\nimport { indexDirectories } from './indexing.js';\nimport { searchContent, findSimilarFiles, getFileContent, getChunkContent } from './search.js';\nimport { getIndexStatus } from './storage.js';\nimport { validateIndexPrerequisites, validateSearchPrerequisites } from './prerequisites.js';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\n// Type-safe interfaces for MCP tool arguments\ninterface IndexToolArgs {\n directory_path: string;\n}\n\ninterface SearchToolArgs {\n query: string;\n limit?: number;\n workspace?: string;\n}\n\ninterface SimilarFilesToolArgs {\n file_path: string;\n limit?: number;\n workspace?: string;\n}\n\ninterface GetContentToolArgs {\n file_path: string;\n chunks?: string;\n}\n\ninterface GetChunkToolArgs {\n file_path: string;\n chunk_id: string;\n}\n\n// Type guard functions\nfunction isIndexToolArgs(args: unknown): args is IndexToolArgs {\n return typeof args === 'object' && args !== null && \n typeof (args as IndexToolArgs).directory_path === 'string';\n}\n\nfunction isSearchToolArgs(args: unknown): args is SearchToolArgs {\n return typeof args === 'object' && args !== null && \n typeof (args as SearchToolArgs).query === 'string';\n}\n\nfunction isSimilarFilesToolArgs(args: unknown): args is SimilarFilesToolArgs {\n return typeof args === 'object' && args !== null && \n typeof (args as SimilarFilesToolArgs).file_path === 'string';\n}\n\nfunction isGetContentToolArgs(args: unknown): args is GetContentToolArgs {\n return typeof args === 'object' && args !== null && \n typeof (args as GetContentToolArgs).file_path === 'string';\n}\n\nfunction isGetChunkToolArgs(args: unknown): args is GetChunkToolArgs {\n return typeof args === 'object' && args !== null && \n typeof (args as GetChunkToolArgs).file_path === 'string' &&\n typeof (args as GetChunkToolArgs).chunk_id === 'string';\n}\n\nexport async function handleIndexTool(args: unknown, config: Config): Promise<CallToolResult> {\n if (!isIndexToolArgs(args)) {\n throw new Error('directory_path is required');\n }\n \n // Validate prerequisites before proceeding\n await validateIndexPrerequisites(config);\n \n const paths = args.directory_path.split(',').map((p: string) => p.trim());\n const result = await indexDirectories(paths, config);\n \n let responseText = `Indexed ${result.indexed} files, skipped ${result.skipped} files, cleaned up ${result.deleted} deleted files, ${result.failed} failed`;\n \n if (result.errors.length > 0) {\n responseText += `\\nErrors: [\\n`;\n result.errors.forEach(error => {\n responseText += ` '${error}'\\n`;\n });\n responseText += `]`;\n }\n \n return {\n content: [\n {\n type: 'text',\n text: responseText\n }\n ]\n };\n}\n\nasync function validateWorkspace(workspace?: string): Promise<{ workspace?: string; message?: string }> {\n if (!workspace) return { workspace };\n \n const config = (await import('./config.js')).loadConfig();\n const { getAvailableWorkspaces } = await import('./config.js');\n const availableWorkspaces = getAvailableWorkspaces(config);\n \n if (availableWorkspaces.includes(workspace)) {\n return { workspace };\n }\n \n // Invalid workspace - search all content with informative message\n const message = availableWorkspaces.length > 0\n ? `Note: Workspace '${workspace}' not found. Searching all content instead. Available workspaces: ${availableWorkspaces.join(', ')}. Use server_info tool to see workspace details.`\n : `Note: Workspace '${workspace}' not found and no workspaces are configured. Searching all indexed content.`;\n \n return { workspace: undefined, message };\n}\n\nexport async function handleSearchTool(args: unknown): Promise<CallToolResult> {\n if (!isSearchToolArgs(args)) {\n throw new Error('query is required');\n }\n \n // Validate prerequisites before proceeding\n const config = (await import('./config.js')).loadConfig();\n await validateSearchPrerequisites(config);\n \n const { workspace, message } = await validateWorkspace(args.workspace);\n const results = await searchContent(args.query, { limit: args.limit || 10, workspace });\n \n const response = message \n ? `${message}\\n\\n${JSON.stringify(results, null, 2)}`\n : JSON.stringify(results, null, 2);\n \n return {\n content: [{ type: 'text', text: response }]\n };\n}\n\nexport async function handleSimilarFilesTool(args: unknown): Promise<CallToolResult> {\n if (!isSimilarFilesToolArgs(args)) {\n throw new Error('file_path is required');\n }\n \n // Validate prerequisites before proceeding\n const config = (await import('./config.js')).loadConfig();\n await validateSearchPrerequisites(config);\n \n const { workspace, message } = await validateWorkspace(args.workspace);\n const results = await findSimilarFiles(args.file_path, args.limit || 10, workspace);\n \n const response = message \n ? `${message}\\n\\n${JSON.stringify(results, null, 2)}`\n : JSON.stringify(results, null, 2);\n \n return {\n content: [{ type: 'text', text: response }]\n };\n}\n\nexport async function handleGetContentTool(args: unknown): Promise<CallToolResult> {\n if (!isGetContentToolArgs(args)) {\n throw new Error('file_path is required');\n }\n \n const content = await getFileContent(args.file_path, args.chunks);\n \n return {\n content: [\n {\n type: 'text',\n text: content\n }\n ]\n };\n}\n\nexport async function handleGetChunkTool(args: unknown): Promise<CallToolResult> {\n if (!isGetChunkToolArgs(args)) {\n throw new Error('file_path and chunk_id are required');\n }\n \n const content = await getChunkContent(args.file_path, args.chunk_id);\n \n return {\n content: [\n {\n type: 'text',\n text: content\n }\n ]\n };\n}\n\nexport async function handleServerInfoTool(version: string): Promise<CallToolResult> {\n const status = await getIndexStatus();\n \n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify({\n name: 'directory-indexer',\n version: version,\n status: status\n }, null, 2)\n }\n ]\n };\n}\n\nexport function formatErrorResponse(error: unknown): CallToolResult {\n const errorMessage = error instanceof Error ? error.message : 'Unknown error';\n return {\n content: [\n {\n type: 'text',\n text: `Error: ${errorMessage}`\n }\n ],\n isError: true\n };\n}"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mcp-handlers.js","sources":["../src/mcp-handlers.ts"],"sourcesContent":["import { Config } from './config.js';\nimport { indexDirectories } from './indexing.js';\nimport { searchContent, findSimilarFiles, getFileContent, getChunkContent } from './search.js';\nimport { getIndexStatus, SQLiteStorage, initializeStorage } from './storage.js';\nimport { validateIndexPrerequisites, validateSearchPrerequisites } from './prerequisites.js';\nimport { validatePathWithinIndexedDirs, resolveIndexedDirectories } from './path-validation.js';\nimport { log } from './logger.js';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport type { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { resolve, sep } from 'path';\nimport { realpathSync } from 'fs';\n\n// MCP server reference for sending client-visible log notifications\nlet mcpServer: Server | null = null;\n\n/**\n * Set the MCP server reference for logging notifications.\n * Called from startMcpServer() after server creation.\n */\nexport function setMcpServer(server: Server): void {\n mcpServer = server;\n}\n\n/**\n * Normalize a directory path for use as a mutex key.\n * Resolves symlinks, makes absolute, and strips trailing separators\n * so that '/repo', '/repo/', and symlinks all map to the same key.\n */\nfunction normalizeMutexKey(dirPath: string): string {\n let normalized: string;\n try {\n normalized = realpathSync(resolve(dirPath));\n } catch {\n // Directory may not exist yet; fall back to resolve only\n normalized = resolve(dirPath);\n }\n // Strip trailing separator (but keep root '/' or 'C:\\')\n while (normalized.length > 1 && normalized.endsWith(sep)) {\n normalized = normalized.slice(0, -sep.length);\n }\n return normalized;\n}\n\n// Workspace-level indexing mutex: keyed by normalized directory path\nconst indexingMutex = new Map<string, Promise<void>>();\n\n// Cached set of resolved indexed directory paths for path validation\nlet indexedDirsCache: Set<string> = new Set();\nlet indexedDirsCacheInitialized = false;\n\n/**\n * Refresh the indexed directories cache from storage.\n * Exported for testability.\n */\nexport function refreshIndexedDirsCache(storage: SQLiteStorage): void {\n indexedDirsCache = resolveIndexedDirectories(storage);\n indexedDirsCacheInitialized = true;\n}\n\n/**\n * Ensure the cache is populated, lazily initializing from SQLite only (no Qdrant).\n * Uses a boolean flag so an empty result doesn't re-trigger initialization.\n */\nasync function ensureIndexedDirsCache(config: Config): Promise<void> {\n if (!indexedDirsCacheInitialized) {\n const sqlite = new SQLiteStorage(config);\n try {\n refreshIndexedDirsCache(sqlite);\n } finally {\n sqlite.close();\n }\n }\n}\n\n// Type-safe interfaces for MCP tool arguments\ninterface IndexToolArgs {\n directory_paths: string[];\n}\n\ninterface SearchToolArgs {\n query: string;\n limit?: number;\n workspace?: string;\n}\n\ninterface SimilarFilesToolArgs {\n file_path: string;\n limit?: number;\n workspace?: string;\n}\n\ninterface GetContentToolArgs {\n file_path: string;\n chunks?: string;\n}\n\ninterface GetChunkToolArgs {\n file_path: string;\n chunk_id: string;\n}\n\ninterface DeleteIndexToolArgs {\n directory_path: string;\n}\n\n// Type guard functions\nfunction isIndexToolArgs(args: unknown): args is IndexToolArgs {\n return typeof args === 'object' && args !== null &&\n Array.isArray((args as IndexToolArgs).directory_paths);\n}\n\nfunction isSearchToolArgs(args: unknown): args is SearchToolArgs {\n return typeof args === 'object' && args !== null && \n typeof (args as SearchToolArgs).query === 'string';\n}\n\nfunction isSimilarFilesToolArgs(args: unknown): args is SimilarFilesToolArgs {\n return typeof args === 'object' && args !== null && \n typeof (args as SimilarFilesToolArgs).file_path === 'string';\n}\n\nfunction isGetContentToolArgs(args: unknown): args is GetContentToolArgs {\n return typeof args === 'object' && args !== null && \n typeof (args as GetContentToolArgs).file_path === 'string';\n}\n\nfunction isGetChunkToolArgs(args: unknown): args is GetChunkToolArgs {\n return typeof args === 'object' && args !== null &&\n typeof (args as GetChunkToolArgs).file_path === 'string' &&\n typeof (args as GetChunkToolArgs).chunk_id === 'string';\n}\n\nfunction isDeleteIndexToolArgs(args: unknown): args is DeleteIndexToolArgs {\n return typeof args === 'object' && args !== null &&\n typeof (args as DeleteIndexToolArgs).directory_path === 'string';\n}\n\nexport async function handleIndexTool(args: unknown, config: Config): Promise<CallToolResult> {\n if (!isIndexToolArgs(args)) {\n throw new Error('directory_paths is required and must be an array');\n }\n\n // Validate prerequisites before proceeding\n await validateIndexPrerequisites(config);\n\n const paths = args.directory_paths.map((p: string) => p.trim());\n const mutexKeys = paths.map(normalizeMutexKey);\n\n log('info', 'Index start', { directories: paths });\n mcpServer?.sendLoggingMessage({ level: 'info', data: { event: 'index_start', directories: paths } });\n\n // Per-directory mutex: serialize concurrent calls targeting the same directory\n for (const key of mutexKeys) {\n const existing = indexingMutex.get(key);\n if (existing) {\n log('info', 'Waiting for ongoing indexing', { directory: key });\n await existing;\n }\n }\n\n // Create a deferred promise for this indexing operation\n let resolveIndexing: () => void;\n const indexingPromise = new Promise<void>((resolve) => { resolveIndexing = resolve; });\n for (const key of mutexKeys) {\n indexingMutex.set(key, indexingPromise);\n }\n\n try {\n const result = await indexDirectories(paths, config);\n\n // Refresh the indexed directories cache after successful indexing\n const { sqlite } = await initializeStorage(config);\n try {\n refreshIndexedDirsCache(sqlite);\n } finally {\n sqlite.close();\n }\n\n log('info', 'Index complete', { result });\n mcpServer?.sendLoggingMessage({ level: 'info', data: { event: 'index_complete', result } });\n\n let responseText = `Indexed ${result.indexed} files, skipped ${result.skipped} files, cleaned up ${result.deleted} deleted files, ${result.failed} failed`;\n\n if (result.errors.length > 0) {\n responseText += `\\nErrors: [\\n`;\n result.errors.forEach(error => {\n responseText += ` '${error}'\\n`;\n });\n responseText += `]`;\n }\n\n return {\n content: [\n {\n type: 'text',\n text: responseText\n }\n ]\n };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n log('error', 'Index error', { error: errorMessage, directories: paths });\n mcpServer?.sendLoggingMessage({ level: 'error', data: { event: 'index_error', error: errorMessage } });\n throw new Error(\n `Indexing failed for ${paths.join(', ')}. Verify the directory exists and is readable. Use 'server_info' to check current status.`\n );\n } finally {\n resolveIndexing!();\n for (const key of mutexKeys) {\n indexingMutex.delete(key);\n }\n }\n}\n\nasync function validateWorkspace(workspace?: string): Promise<{ workspace?: string; message?: string }> {\n if (!workspace) return { workspace };\n \n const config = (await import('./config.js')).loadConfig();\n const { getAvailableWorkspaces } = await import('./config.js');\n const availableWorkspaces = getAvailableWorkspaces(config);\n \n if (availableWorkspaces.includes(workspace)) {\n return { workspace };\n }\n \n // Invalid workspace - search all content with informative message\n const message = availableWorkspaces.length > 0\n ? `Note: Workspace '${workspace}' not found. Searching all content instead. Available workspaces: ${availableWorkspaces.join(', ')}. Use server_info tool to see workspace details.`\n : `Note: Workspace '${workspace}' not found and no workspaces are configured. Searching all indexed content.`;\n \n return { workspace: undefined, message };\n}\n\nexport async function handleSearchTool(args: unknown): Promise<CallToolResult> {\n if (!isSearchToolArgs(args)) {\n throw new Error('query is required');\n }\n \n // Validate prerequisites before proceeding\n const config = (await import('./config.js')).loadConfig();\n await validateSearchPrerequisites(config);\n \n const { workspace, message } = await validateWorkspace(args.workspace);\n const results = await searchContent(args.query, { limit: args.limit || 10, workspace });\n \n const response = message \n ? `${message}\\n\\n${JSON.stringify(results, null, 2)}`\n : JSON.stringify(results, null, 2);\n \n return {\n content: [{ type: 'text', text: response }]\n };\n}\n\nexport async function handleSimilarFilesTool(args: unknown): Promise<CallToolResult> {\n if (!isSimilarFilesToolArgs(args)) {\n throw new Error('file_path is required');\n }\n \n // Validate prerequisites before proceeding\n const config = (await import('./config.js')).loadConfig();\n await validateSearchPrerequisites(config);\n \n const { workspace, message } = await validateWorkspace(args.workspace);\n const results = await findSimilarFiles(args.file_path, args.limit || 10, workspace);\n \n const response = message \n ? `${message}\\n\\n${JSON.stringify(results, null, 2)}`\n : JSON.stringify(results, null, 2);\n \n return {\n content: [{ type: 'text', text: response }]\n };\n}\n\nexport async function handleGetContentTool(args: unknown, config?: Config): Promise<CallToolResult> {\n if (!isGetContentToolArgs(args)) {\n throw new Error('file_path is required');\n }\n\n // Lazily populate cache and validate path\n const resolvedConfig = config || (await import('./config.js')).loadConfig();\n await ensureIndexedDirsCache(resolvedConfig);\n validatePathWithinIndexedDirs(args.file_path, indexedDirsCache);\n\n try {\n const content = await getFileContent(args.file_path, args.chunks);\n\n return {\n content: [\n {\n type: 'text',\n text: content\n }\n ]\n };\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n if (msg.includes('ENOENT') || msg.toLowerCase().includes('not found') || msg.toLowerCase().includes('no such file')) {\n throw new Error(\n `File not found: ${args.file_path}. The file may have been moved or deleted. Use 'search' to find similar content.`\n );\n }\n throw error;\n }\n}\n\nexport async function handleGetChunkTool(args: unknown, config?: Config): Promise<CallToolResult> {\n if (!isGetChunkToolArgs(args)) {\n throw new Error('file_path and chunk_id are required');\n }\n\n // Lazily populate cache and validate path\n const resolvedConfig = config || (await import('./config.js')).loadConfig();\n await ensureIndexedDirsCache(resolvedConfig);\n validatePathWithinIndexedDirs(args.file_path, indexedDirsCache);\n\n try {\n const content = await getChunkContent(args.file_path, args.chunk_id);\n\n return {\n content: [\n {\n type: 'text',\n text: content\n }\n ]\n };\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n if (msg.includes('ENOENT') || msg.toLowerCase().includes('not found') || msg.toLowerCase().includes('no such file')) {\n throw new Error(\n `File not found: ${args.file_path}. The file may have been moved or deleted. Use 'search' to find similar content.`\n );\n }\n throw error;\n }\n}\n\nexport async function handleServerInfoTool(version: string): Promise<CallToolResult> {\n const status = await getIndexStatus();\n \n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify({\n name: 'directory-indexer',\n version: version,\n status: status\n }, null, 2)\n }\n ]\n };\n}\n\nexport async function handleDeleteIndexTool(args: unknown, config: Config): Promise<CallToolResult> {\n if (!isDeleteIndexToolArgs(args)) {\n throw new Error('directory_path is required');\n }\n\n const dirPath = args.directory_path.trim();\n const mutexKey = normalizeMutexKey(dirPath);\n\n // Acquire per-directory mutex to avoid racing with in-flight indexing\n const existing = indexingMutex.get(mutexKey);\n if (existing) {\n log('info', 'Waiting for ongoing indexing before delete', { directory: dirPath });\n await existing;\n }\n\n let resolveDelete: () => void;\n const deletePromise = new Promise<void>((r) => { resolveDelete = r; });\n indexingMutex.set(mutexKey, deletePromise);\n\n const { sqlite, qdrant } = await initializeStorage(config);\n\n try {\n // Check if the directory is actually indexed\n const directory = await sqlite.getDirectory(dirPath);\n if (!directory) {\n throw new Error(\n `Directory '${dirPath}' is not indexed. Use 'server_info' to see indexed directories.`\n );\n }\n\n // Get files for this directory to clean up Qdrant points\n const files = await sqlite.getFilesByDirectory(dirPath);\n const vectorErrors: string[] = [];\n for (const file of files) {\n try {\n await qdrant.deletePointsByFilePath(file.path);\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n log('warning', 'Failed to delete Qdrant points for file', {\n path: file.path,\n error: msg\n });\n vectorErrors.push(`${file.path}: ${msg}`);\n }\n }\n\n // If any vector deletions failed, abort to prevent orphaned vectors\n if (vectorErrors.length > 0) {\n throw new Error(\n `Aborting delete: failed to remove vector embeddings for ${vectorErrors.length} file(s). ` +\n `SQLite rows were NOT deleted to avoid orphaned vectors.\\n` +\n vectorErrors.join('\\n')\n );\n }\n\n // Delete file records and directory record from SQLite\n const deletedFiles = sqlite.deleteFilesByDirectory(dirPath);\n sqlite.deleteDirectory(dirPath);\n\n // Refresh the indexed directories cache\n refreshIndexedDirsCache(sqlite);\n\n const chunksCount = files.reduce((sum, f) => sum + (f.chunks?.length || 0), 0);\n\n log('info', 'Index deleted', { directory: dirPath, files: deletedFiles, chunks: chunksCount });\n mcpServer?.sendLoggingMessage({ level: 'info', data: { event: 'index_deleted', directory: dirPath } });\n\n return {\n content: [\n {\n type: 'text',\n text: `Deleted index for ${dirPath}: removed ${deletedFiles} files and ${chunksCount} chunks`\n }\n ]\n };\n } finally {\n resolveDelete!();\n indexingMutex.delete(mutexKey);\n sqlite.close();\n }\n}\n\nexport function formatErrorResponse(error: unknown): CallToolResult {\n const errorMessage = error instanceof Error ? error.message : 'Unknown error';\n log('error', 'Tool error', { error: errorMessage });\n return {\n content: [\n {\n type: 'text',\n text: `Error: ${errorMessage}`\n }\n ],\n isError: true\n };\n}"],"names":["resolve"],"mappings":"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"}
|
package/dist/mcp.js
CHANGED
|
@@ -4,15 +4,55 @@ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprot
|
|
|
4
4
|
import { readFileSync } from "fs";
|
|
5
5
|
import { dirname, join } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import { closeAllStorage } from "./storage.js";
|
|
8
|
+
import { initLogLevel, log } from "./logger.js";
|
|
9
|
+
import { setMcpServer, handleDeleteIndexTool, handleServerInfoTool, handleGetChunkTool, handleGetContentTool, handleSimilarFilesTool, handleSearchTool, handleIndexTool, formatErrorResponse } from "./mcp-handlers.js";
|
|
10
|
+
const __dirname$1 = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const packageJsonPath = join(__dirname$1, "../package.json");
|
|
10
12
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
11
13
|
const VERSION = packageJson.version;
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const DELETE_INDEX_TOOL = {
|
|
15
|
+
name: "delete_index",
|
|
16
|
+
description: `Remove the index for a directory. Deletes all file records, vector embeddings, and the directory entry from the database.
|
|
17
|
+
|
|
18
|
+
When to use this tool:
|
|
19
|
+
- User wants to remove a directory from the search index
|
|
20
|
+
- Cleaning up old or irrelevant indexed content
|
|
21
|
+
- Freeing database space by removing unused indexes
|
|
22
|
+
|
|
23
|
+
How it works:
|
|
24
|
+
- Removes all file records for the specified directory from SQLite
|
|
25
|
+
- Deletes corresponding vector embeddings from Qdrant
|
|
26
|
+
- Removes the directory entry from the directories table
|
|
27
|
+
- Does NOT delete the actual files on disk
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
- Remove old project: directory_path="/home/user/old-project"
|
|
31
|
+
- Clean up test data: directory_path="/home/user/test-files"
|
|
32
|
+
|
|
33
|
+
Use server_info to see what directories are currently indexed before removing.`,
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
directory_path: {
|
|
38
|
+
type: "string",
|
|
39
|
+
description: "Absolute path of the directory whose index should be removed"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
required: ["directory_path"]
|
|
43
|
+
},
|
|
44
|
+
annotations: {
|
|
45
|
+
readOnlyHint: false,
|
|
46
|
+
destructiveHint: true,
|
|
47
|
+
idempotentHint: true,
|
|
48
|
+
openWorldHint: false
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
function getMcpTools() {
|
|
52
|
+
const tools = [
|
|
53
|
+
{
|
|
54
|
+
name: "index",
|
|
55
|
+
description: `Index directories to make their files searchable. Processes files to create vector embeddings for semantic search.
|
|
16
56
|
|
|
17
57
|
When to use this tool:
|
|
18
58
|
- User specifically requests indexing a directory as a knowledge base
|
|
@@ -26,25 +66,32 @@ How it works:
|
|
|
26
66
|
- Stores in database for fast retrieval
|
|
27
67
|
|
|
28
68
|
Examples:
|
|
29
|
-
- Index documentation: "/home/user/docs/project-wiki"
|
|
30
|
-
- Index codebase: "/home/user/projects/api-server"
|
|
31
|
-
- Index multiple directories: "/home/user/docs
|
|
69
|
+
- Index documentation: ["/home/user/docs/project-wiki"]
|
|
70
|
+
- Index codebase: ["/home/user/projects/api-server"]
|
|
71
|
+
- Index multiple directories: ["/home/user/docs", "/home/user/configs"]
|
|
32
72
|
|
|
33
73
|
Indexing can take several minutes for large directories. Most users will already have directories indexed and can directly use search tool. Use server_info to check current indexing status first.`,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
directory_paths: {
|
|
78
|
+
type: "array",
|
|
79
|
+
items: { type: "string" },
|
|
80
|
+
description: 'Array of absolute directory paths to index. Must be absolute paths since MCP server runs independently. Examples: ["/home/user/projects"] (Unix) or ["C:\\\\Users\\\\user\\\\projects"] (Windows)'
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
required: ["directory_paths"]
|
|
41
84
|
},
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
85
|
+
annotations: {
|
|
86
|
+
readOnlyHint: false,
|
|
87
|
+
destructiveHint: false,
|
|
88
|
+
idempotentHint: true,
|
|
89
|
+
openWorldHint: false
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "search",
|
|
94
|
+
description: `Search indexed files using natural language queries. Finds files containing content semantically similar to the query.
|
|
48
95
|
|
|
49
96
|
When to use this tool:
|
|
50
97
|
- Find documentation, guides, or explanations about specific topics
|
|
@@ -78,29 +125,34 @@ Example queries:
|
|
|
78
125
|
- "error handling patterns and exception management strategies" (finds try/catch, error classes, logging)
|
|
79
126
|
- "database migration scripts and schema versioning approaches" (finds SQL, schema changes, migration files)
|
|
80
127
|
- "authentication middleware and JWT token validation logic" (finds auth logic, JWT handling, middleware functions)`,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
128
|
+
inputSchema: {
|
|
129
|
+
type: "object",
|
|
130
|
+
properties: {
|
|
131
|
+
query: {
|
|
132
|
+
type: "string",
|
|
133
|
+
description: "Natural language search query describing what you are looking for. Can be concepts, functionality, or specific technical terms."
|
|
134
|
+
},
|
|
135
|
+
limit: {
|
|
136
|
+
type: "number",
|
|
137
|
+
description: "Maximum number of files to return (default: 10). Each file may contain multiple matching chunks.",
|
|
138
|
+
default: 10
|
|
139
|
+
},
|
|
140
|
+
workspace: {
|
|
141
|
+
type: "string",
|
|
142
|
+
description: "Optional workspace name to filter search results. Only files within the workspace directories will be searched. IMPORTANT: Use server_info tool first to discover available workspace names - using invalid workspace names will result in empty results."
|
|
143
|
+
}
|
|
92
144
|
},
|
|
93
|
-
|
|
94
|
-
type: "string",
|
|
95
|
-
description: "Optional workspace name to filter search results. Only files within the workspace directories will be searched. IMPORTANT: Use server_info tool first to discover available workspace names - using invalid workspace names will result in empty results."
|
|
96
|
-
}
|
|
145
|
+
required: ["query"]
|
|
97
146
|
},
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
147
|
+
annotations: {
|
|
148
|
+
readOnlyHint: true,
|
|
149
|
+
destructiveHint: false,
|
|
150
|
+
openWorldHint: false
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "similar_files",
|
|
155
|
+
description: `Find files with content similar to a reference file. Uses semantic similarity to find related documents, code files, or any text content.
|
|
104
156
|
|
|
105
157
|
When to use this tool:
|
|
106
158
|
- Find documentation similar to a specific guide or README
|
|
@@ -120,29 +172,34 @@ Examples:
|
|
|
120
172
|
- Given "auth.py" - finds other authentication modules, security code, middleware
|
|
121
173
|
|
|
122
174
|
Returns file paths with similarity scores. Use get_content to read full files or get_chunk for specific sections.`,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
175
|
+
inputSchema: {
|
|
176
|
+
type: "object",
|
|
177
|
+
properties: {
|
|
178
|
+
file_path: {
|
|
179
|
+
type: "string",
|
|
180
|
+
description: "Absolute or relative path to the reference file. This file must have been previously indexed."
|
|
181
|
+
},
|
|
182
|
+
limit: {
|
|
183
|
+
type: "number",
|
|
184
|
+
description: "Maximum number of similar files to return (default: 10). Results are sorted by similarity score.",
|
|
185
|
+
default: 10
|
|
186
|
+
},
|
|
187
|
+
workspace: {
|
|
188
|
+
type: "string",
|
|
189
|
+
description: "Optional workspace name to filter results. Only files within the workspace directories will be considered. IMPORTANT: Use server_info tool first to discover available workspace names - using invalid workspace names will result in empty results."
|
|
190
|
+
}
|
|
129
191
|
},
|
|
130
|
-
|
|
131
|
-
type: "number",
|
|
132
|
-
description: "Maximum number of similar files to return (default: 10). Results are sorted by similarity score.",
|
|
133
|
-
default: 10
|
|
134
|
-
},
|
|
135
|
-
workspace: {
|
|
136
|
-
type: "string",
|
|
137
|
-
description: "Optional workspace name to filter results. Only files within the workspace directories will be considered. IMPORTANT: Use server_info tool first to discover available workspace names - using invalid workspace names will result in empty results."
|
|
138
|
-
}
|
|
192
|
+
required: ["file_path"]
|
|
139
193
|
},
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
194
|
+
annotations: {
|
|
195
|
+
readOnlyHint: true,
|
|
196
|
+
destructiveHint: false,
|
|
197
|
+
openWorldHint: false
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "get_content",
|
|
202
|
+
description: `Retrieve the full content of a file or specific chunks. Reads files directly from the filesystem.
|
|
146
203
|
|
|
147
204
|
When to use this tool:
|
|
148
205
|
- Get complete file content after finding files through search
|
|
@@ -162,24 +219,29 @@ Examples:
|
|
|
162
219
|
- Get single chunk: file_path="/home/user/config.json", chunks="1"
|
|
163
220
|
|
|
164
221
|
Returns file content as text. Use this after search or similar_files to read actual content.`,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
222
|
+
inputSchema: {
|
|
223
|
+
type: "object",
|
|
224
|
+
properties: {
|
|
225
|
+
file_path: {
|
|
226
|
+
type: "string",
|
|
227
|
+
description: "Absolute or relative path to the file to retrieve. File must be readable and text-based."
|
|
228
|
+
},
|
|
229
|
+
chunks: {
|
|
230
|
+
type: "string",
|
|
231
|
+
description: 'Optional chunk range specification. Examples: "3" (single chunk), "2-5" (chunks 2 through 5), "1-3" (first three chunks). Only works for indexed files.'
|
|
232
|
+
}
|
|
171
233
|
},
|
|
172
|
-
|
|
173
|
-
type: "string",
|
|
174
|
-
description: 'Optional chunk range specification. Examples: "3" (single chunk), "2-5" (chunks 2 through 5), "1-3" (first three chunks). Only works for indexed files.'
|
|
175
|
-
}
|
|
234
|
+
required: ["file_path"]
|
|
176
235
|
},
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
236
|
+
annotations: {
|
|
237
|
+
readOnlyHint: true,
|
|
238
|
+
destructiveHint: false,
|
|
239
|
+
openWorldHint: false
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: "get_chunk",
|
|
244
|
+
description: `Retrieve content of a specific chunk from an indexed file. Gets exact text segments identified during search.
|
|
183
245
|
|
|
184
246
|
When to use this tool:
|
|
185
247
|
- Get specific relevant sections after performing a search
|
|
@@ -199,24 +261,29 @@ Examples:
|
|
|
199
261
|
- Returns the specific text segment that matched your query
|
|
200
262
|
|
|
201
263
|
Returns chunk content as text. Use this with chunk IDs from search results to get precise content sections.`,
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
264
|
+
inputSchema: {
|
|
265
|
+
type: "object",
|
|
266
|
+
properties: {
|
|
267
|
+
file_path: {
|
|
268
|
+
type: "string",
|
|
269
|
+
description: "Absolute or relative path to the indexed file containing the desired chunk."
|
|
270
|
+
},
|
|
271
|
+
chunk_id: {
|
|
272
|
+
type: "string",
|
|
273
|
+
description: 'ID of the specific chunk to retrieve. This is typically obtained from search results and is a sequential string like "0", "1", "2", etc.'
|
|
274
|
+
}
|
|
208
275
|
},
|
|
209
|
-
|
|
210
|
-
type: "string",
|
|
211
|
-
description: 'ID of the specific chunk to retrieve. This is typically obtained from search results and is a sequential string like "0", "1", "2", etc.'
|
|
212
|
-
}
|
|
276
|
+
required: ["file_path", "chunk_id"]
|
|
213
277
|
},
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
278
|
+
annotations: {
|
|
279
|
+
readOnlyHint: true,
|
|
280
|
+
destructiveHint: false,
|
|
281
|
+
openWorldHint: false
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: "server_info",
|
|
286
|
+
description: `Get information about server status and indexed content. Shows what directories and files are available for search.
|
|
220
287
|
|
|
221
288
|
When to use this tool:
|
|
222
289
|
- REQUIRED: Check available workspace names before using workspace parameter in search or similar_files tools
|
|
@@ -239,14 +306,25 @@ Examples:
|
|
|
239
306
|
- Monitor system: "How many files are searchable?"
|
|
240
307
|
|
|
241
308
|
Returns server version, indexing statistics, directory list, workspace information, and any errors. IMPORTANT: Always use this tool first to discover available workspace names when you need to search within specific workspaces.`,
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
309
|
+
inputSchema: {
|
|
310
|
+
type: "object",
|
|
311
|
+
properties: {},
|
|
312
|
+
additionalProperties: false
|
|
313
|
+
},
|
|
314
|
+
annotations: {
|
|
315
|
+
readOnlyHint: true,
|
|
316
|
+
destructiveHint: false,
|
|
317
|
+
openWorldHint: false
|
|
318
|
+
}
|
|
246
319
|
}
|
|
320
|
+
];
|
|
321
|
+
if (process.env.DISABLE_DESTRUCTIVE !== "true") {
|
|
322
|
+
tools.push(DELETE_INDEX_TOOL);
|
|
247
323
|
}
|
|
248
|
-
|
|
324
|
+
return tools;
|
|
325
|
+
}
|
|
249
326
|
async function startMcpServer(config) {
|
|
327
|
+
initLogLevel();
|
|
250
328
|
const server = new Server(
|
|
251
329
|
{
|
|
252
330
|
name: "directory-indexer",
|
|
@@ -254,13 +332,15 @@ async function startMcpServer(config) {
|
|
|
254
332
|
},
|
|
255
333
|
{
|
|
256
334
|
capabilities: {
|
|
257
|
-
tools: {}
|
|
335
|
+
tools: {},
|
|
336
|
+
logging: {}
|
|
258
337
|
}
|
|
259
338
|
}
|
|
260
339
|
);
|
|
340
|
+
setMcpServer(server);
|
|
261
341
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
262
342
|
return {
|
|
263
|
-
tools:
|
|
343
|
+
tools: getMcpTools()
|
|
264
344
|
};
|
|
265
345
|
});
|
|
266
346
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -274,11 +354,16 @@ async function startMcpServer(config) {
|
|
|
274
354
|
case "similar_files":
|
|
275
355
|
return await handleSimilarFilesTool(args);
|
|
276
356
|
case "get_content":
|
|
277
|
-
return await handleGetContentTool(args);
|
|
357
|
+
return await handleGetContentTool(args, config);
|
|
278
358
|
case "get_chunk":
|
|
279
|
-
return await handleGetChunkTool(args);
|
|
359
|
+
return await handleGetChunkTool(args, config);
|
|
280
360
|
case "server_info":
|
|
281
361
|
return await handleServerInfoTool(VERSION);
|
|
362
|
+
case "delete_index":
|
|
363
|
+
if (process.env.DISABLE_DESTRUCTIVE === "true") {
|
|
364
|
+
throw new Error("delete_index is disabled via DISABLE_DESTRUCTIVE environment variable");
|
|
365
|
+
}
|
|
366
|
+
return await handleDeleteIndexTool(args, config);
|
|
282
367
|
default:
|
|
283
368
|
throw new Error(`Unknown tool: ${name}`);
|
|
284
369
|
}
|
|
@@ -288,11 +373,20 @@ async function startMcpServer(config) {
|
|
|
288
373
|
});
|
|
289
374
|
const transport = new StdioServerTransport();
|
|
290
375
|
await server.connect(transport);
|
|
376
|
+
const cleanup = () => {
|
|
377
|
+
log("info", "Shutting down MCP server");
|
|
378
|
+
closeAllStorage();
|
|
379
|
+
process.exit(0);
|
|
380
|
+
};
|
|
381
|
+
process.on("SIGTERM", cleanup);
|
|
382
|
+
process.on("SIGINT", cleanup);
|
|
291
383
|
if (config.verbose) {
|
|
292
384
|
console.error("MCP server started successfully");
|
|
293
385
|
}
|
|
386
|
+
log("info", "MCP server started", { version: VERSION });
|
|
294
387
|
}
|
|
295
388
|
export {
|
|
389
|
+
getMcpTools,
|
|
296
390
|
startMcpServer
|
|
297
391
|
};
|
|
298
392
|
//# sourceMappingURL=mcp.js.map
|
package/dist/mcp.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp.js","sources":["../src/mcp.ts"],"sourcesContent":["import { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { \n CallToolRequestSchema, \n ListToolsRequestSchema,\n Tool\n} from '@modelcontextprotocol/sdk/types.js';\nimport { readFileSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { fileURLToPath } from 'url';\nimport { Config } from './config.js';\nimport { \n handleIndexTool, \n handleSearchTool, \n handleSimilarFilesTool, \n handleGetContentTool, \n handleGetChunkTool, \n handleServerInfoTool,\n formatErrorResponse\n} from './mcp-handlers.js';\n\n// Read version from package.json\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst packageJsonPath = join(__dirname, '../package.json');\nconst packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));\nconst VERSION = packageJson.version;\n\nconst MCP_TOOLS: Tool[] = [\n {\n name: 'index',\n description: `Index directories to make their files searchable. Processes files to create vector embeddings for semantic search.\n\nWhen to use this tool:\n- User specifically requests indexing a directory as a knowledge base\n- Adding new documentation, code repositories, or file collections to search\n- Updating index when many files have changed\n\nHow it works:\n- Recursively scans directories for supported file types\n- Extracts text content and splits into chunks\n- Generates vector embeddings for semantic similarity\n- Stores in database for fast retrieval\n\nExamples:\n- Index documentation: \"/home/user/docs/project-wiki\"\n- Index codebase: \"/home/user/projects/api-server\"\n- Index multiple directories: \"/home/user/docs,/home/user/configs\"\n\nIndexing can take several minutes for large directories. Most users will already have directories indexed and can directly use search tool. Use server_info to check current indexing status first.`,\n inputSchema: {\n type: 'object',\n properties: {\n directory_path: {\n type: 'string',\n description: 'Comma-separated list of absolute directory paths to index. Must be absolute paths since MCP server runs independently. Examples: \"/home/user/projects\" (Unix) or \"C:\\\\Users\\\\user\\\\projects\" (Windows)'\n }\n },\n required: ['directory_path']\n }\n },\n {\n name: 'search',\n description: `Search indexed files using natural language queries. Finds files containing content semantically similar to the query.\n\nWhen to use this tool:\n- Find documentation, guides, or explanations about specific topics\n- Locate code files implementing certain functionality or patterns\n- Discover configuration files, scripts, or settings related to a topic\n- Search for files covering specific concepts or technologies\n\nHow it works:\n- Converts query to vector embedding using semantic similarity\n- Searches all indexed file chunks for relevant content\n- Groups results by file and calculates average relevance scores\n- Returns files ranked by relevance score\n\nExamples:\n- \"database configuration and connection pooling setup\" - finds config files, documentation about DB setup\n- \"comprehensive error handling patterns and exception management\" - finds code files with exception handling\n- \"JWT authentication implementation and session management\" - finds auth-related code and docs\n- \"REST API documentation and endpoint specifications\" - finds API guides, endpoint definitions\n- \"Docker deployment scripts and CI/CD pipeline configuration\" - finds deployment automation\n\nReturns files with similarity scores and chunk information. Use get_content to retrieve full file content or get_chunk to retrieve specific chunk content by chunk ID.\n- Groups results by file to avoid duplicates from multiple matching sections\n\nResponse format:\n- Returns lightweight metadata including file paths, relevance scores, and chunk IDs\n- Use 'get_chunk' or 'get_content' tools to fetch actual content from search results\n- Chunks are sorted by relevance score within each file\n- Average similarity score calculated across all matching chunks per file\n\nExample queries:\n- \"error handling patterns and exception management strategies\" (finds try/catch, error classes, logging)\n- \"database migration scripts and schema versioning approaches\" (finds SQL, schema changes, migration files)\n- \"authentication middleware and JWT token validation logic\" (finds auth logic, JWT handling, middleware functions)`,\n inputSchema: {\n type: 'object',\n properties: {\n query: {\n type: 'string',\n description: 'Natural language search query describing what you are looking for. Can be concepts, functionality, or specific technical terms.'\n },\n limit: {\n type: 'number',\n description: 'Maximum number of files to return (default: 10). Each file may contain multiple matching chunks.',\n default: 10\n },\n workspace: {\n type: 'string',\n description: 'Optional workspace name to filter search results. Only files within the workspace directories will be searched. IMPORTANT: Use server_info tool first to discover available workspace names - using invalid workspace names will result in empty results.'\n }\n },\n required: ['query']\n }\n },\n {\n name: 'similar_files',\n description: `Find files with content similar to a reference file. Uses semantic similarity to find related documents, code files, or any text content.\n\nWhen to use this tool:\n- Find documentation similar to a specific guide or README\n- Locate related code files, configuration files, or scripts\n- Discover alternative implementations or approaches\n- Find files covering similar topics or concepts\n\nHow it works:\n- Analyzes the semantic content of the reference file\n- Compares against all indexed files using vector similarity\n- Returns files ranked by content similarity score\n\nExamples:\n- Given \"deployment-guide.md\" - finds other deployment docs, CI/CD guides, infrastructure setup\n- Given \"troubleshooting.md\" - finds other troubleshooting guides, FAQ files, error documentation\n- Given \"config.yaml\" - finds other configuration files, settings, environment setups\n- Given \"auth.py\" - finds other authentication modules, security code, middleware\n\nReturns file paths with similarity scores. Use get_content to read full files or get_chunk for specific sections.`,\n inputSchema: {\n type: 'object',\n properties: {\n file_path: {\n type: 'string',\n description: 'Absolute or relative path to the reference file. This file must have been previously indexed.'\n },\n limit: {\n type: 'number',\n description: 'Maximum number of similar files to return (default: 10). Results are sorted by similarity score.',\n default: 10\n },\n workspace: {\n type: 'string',\n description: 'Optional workspace name to filter results. Only files within the workspace directories will be considered. IMPORTANT: Use server_info tool first to discover available workspace names - using invalid workspace names will result in empty results.'\n }\n },\n required: ['file_path']\n }\n },\n {\n name: 'get_content',\n description: `Retrieve the full content of a file or specific chunks. Reads files directly from the filesystem.\n\nWhen to use this tool:\n- Get complete file content after finding files through search\n- Read documentation, code files, or configuration files for analysis\n- Extract specific sections of large files using chunk ranges\n- Access any text-based file content\n\nHow it works:\n- Reads files directly from filesystem (not from search index)\n- Returns entire file by default\n- Can return specific chunk ranges for indexed files\n- Preserves original formatting and content\n\nExamples:\n- Get full file: file_path=\"/home/user/docs/api.md\"\n- Get specific chunks: file_path=\"/home/user/code/main.py\", chunks=\"2-5\"\n- Get single chunk: file_path=\"/home/user/config.json\", chunks=\"1\"\n\nReturns file content as text. Use this after search or similar_files to read actual content.`,\n inputSchema: {\n type: 'object',\n properties: {\n file_path: {\n type: 'string',\n description: 'Absolute or relative path to the file to retrieve. File must be readable and text-based.'\n },\n chunks: {\n type: 'string',\n description: 'Optional chunk range specification. Examples: \"3\" (single chunk), \"2-5\" (chunks 2 through 5), \"1-3\" (first three chunks). Only works for indexed files.'\n }\n },\n required: ['file_path']\n }\n },\n {\n name: 'get_chunk',\n description: `Retrieve content of a specific chunk from an indexed file. Gets exact text segments identified during search.\n\nWhen to use this tool:\n- Get specific relevant sections after performing a search\n- Access only the most pertinent parts of large files\n- Retrieve content from high-scoring chunks identified in search results\n- Avoid reading entire files when only specific sections are needed\n\nHow it works:\n- Files are split into overlapping text chunks during indexing\n- Each chunk has a sequential ID (\"0\", \"1\", \"2\", etc.)\n- Search results include chunk IDs for relevant sections\n- Returns the exact content that was semantically matched\n\nExamples:\n- After search returns chunk \"3\" from \"api-docs.md\" with high score\n- Get chunk content: file_path=\"/docs/api-docs.md\", chunk_id=\"3\"\n- Returns the specific text segment that matched your query\n\nReturns chunk content as text. Use this with chunk IDs from search results to get precise content sections.`,\n inputSchema: {\n type: 'object',\n properties: {\n file_path: {\n type: 'string',\n description: 'Absolute or relative path to the indexed file containing the desired chunk.'\n },\n chunk_id: {\n type: 'string',\n description: 'ID of the specific chunk to retrieve. This is typically obtained from search results and is a sequential string like \"0\", \"1\", \"2\", etc.'\n }\n },\n required: ['file_path', 'chunk_id']\n }\n },\n {\n name: 'server_info',\n description: `Get information about server status and indexed content. Shows what directories and files are available for search.\n\nWhen to use this tool:\n- REQUIRED: Check available workspace names before using workspace parameter in search or similar_files tools\n- Check what content is already indexed before performing searches\n- Verify system is working properly\n- See indexing statistics and status\n- Understand scope of available searchable content\n\nHow it works:\n- Reports total indexed directories, files, and chunks\n- Shows database size and last indexing time\n- Lists all indexed directories with file counts\n- Lists all configured workspaces with their paths and file counts\n- Reports any errors or issues\n\nExamples:\n- Check workspaces before searching: \"What workspaces are available?\"\n- Check before searching: \"What content is indexed?\"\n- Verify after indexing: \"Did the indexing complete successfully?\"\n- Monitor system: \"How many files are searchable?\"\n\nReturns server version, indexing statistics, directory list, workspace information, and any errors. IMPORTANT: Always use this tool first to discover available workspace names when you need to search within specific workspaces.`,\n inputSchema: {\n type: 'object',\n properties: {},\n additionalProperties: false\n }\n }\n];\n\nexport async function startMcpServer(config: Config): Promise<void> {\n const server = new Server(\n {\n name: 'directory-indexer',\n version: VERSION\n },\n {\n capabilities: {\n tools: {}\n }\n }\n );\n\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n return {\n tools: MCP_TOOLS\n };\n });\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params;\n\n try {\n switch (name) {\n case 'index':\n return await handleIndexTool(args, config);\n \n case 'search':\n return await handleSearchTool(args);\n \n case 'similar_files':\n return await handleSimilarFilesTool(args);\n \n case 'get_content':\n return await handleGetContentTool(args);\n \n case 'get_chunk':\n return await handleGetChunkTool(args);\n \n case 'server_info':\n return await handleServerInfoTool(VERSION);\n \n default:\n throw new Error(`Unknown tool: ${name}`);\n }\n } catch (error) {\n return formatErrorResponse(error);\n }\n });\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n \n if (config.verbose) {\n console.error('MCP server started successfully');\n }\n}"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mcp.js","sources":["../src/mcp.ts"],"sourcesContent":["import { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { \n CallToolRequestSchema, \n ListToolsRequestSchema,\n Tool\n} from '@modelcontextprotocol/sdk/types.js';\nimport { readFileSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { fileURLToPath } from 'url';\nimport { Config } from './config.js';\nimport { closeAllStorage } from './storage.js';\nimport { initLogLevel, log } from './logger.js';\nimport {\n handleIndexTool,\n handleSearchTool,\n handleSimilarFilesTool,\n handleGetContentTool,\n handleGetChunkTool,\n handleServerInfoTool,\n handleDeleteIndexTool,\n formatErrorResponse,\n setMcpServer\n} from './mcp-handlers.js';\n\n// Read version from package.json\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst packageJsonPath = join(__dirname, '../package.json');\nconst packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));\nconst VERSION = packageJson.version;\n\nconst DELETE_INDEX_TOOL: Tool = {\n name: 'delete_index',\n description: `Remove the index for a directory. Deletes all file records, vector embeddings, and the directory entry from the database.\n\nWhen to use this tool:\n- User wants to remove a directory from the search index\n- Cleaning up old or irrelevant indexed content\n- Freeing database space by removing unused indexes\n\nHow it works:\n- Removes all file records for the specified directory from SQLite\n- Deletes corresponding vector embeddings from Qdrant\n- Removes the directory entry from the directories table\n- Does NOT delete the actual files on disk\n\nExamples:\n- Remove old project: directory_path=\"/home/user/old-project\"\n- Clean up test data: directory_path=\"/home/user/test-files\"\n\nUse server_info to see what directories are currently indexed before removing.`,\n inputSchema: {\n type: 'object',\n properties: {\n directory_path: {\n type: 'string',\n description: 'Absolute path of the directory whose index should be removed'\n }\n },\n required: ['directory_path']\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: true,\n idempotentHint: true,\n openWorldHint: false\n }\n};\n\nexport function getMcpTools(): Tool[] {\n const tools: Tool[] = [\n {\n name: 'index',\n description: `Index directories to make their files searchable. Processes files to create vector embeddings for semantic search.\n\nWhen to use this tool:\n- User specifically requests indexing a directory as a knowledge base\n- Adding new documentation, code repositories, or file collections to search\n- Updating index when many files have changed\n\nHow it works:\n- Recursively scans directories for supported file types\n- Extracts text content and splits into chunks\n- Generates vector embeddings for semantic similarity\n- Stores in database for fast retrieval\n\nExamples:\n- Index documentation: [\"/home/user/docs/project-wiki\"]\n- Index codebase: [\"/home/user/projects/api-server\"]\n- Index multiple directories: [\"/home/user/docs\", \"/home/user/configs\"]\n\nIndexing can take several minutes for large directories. Most users will already have directories indexed and can directly use search tool. Use server_info to check current indexing status first.`,\n inputSchema: {\n type: 'object',\n properties: {\n directory_paths: {\n type: 'array',\n items: { type: 'string' },\n description: 'Array of absolute directory paths to index. Must be absolute paths since MCP server runs independently. Examples: [\"/home/user/projects\"] (Unix) or [\"C:\\\\\\\\Users\\\\\\\\user\\\\\\\\projects\"] (Windows)'\n }\n },\n required: ['directory_paths']\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false\n }\n },\n {\n name: 'search',\n description: `Search indexed files using natural language queries. Finds files containing content semantically similar to the query.\n\nWhen to use this tool:\n- Find documentation, guides, or explanations about specific topics\n- Locate code files implementing certain functionality or patterns\n- Discover configuration files, scripts, or settings related to a topic\n- Search for files covering specific concepts or technologies\n\nHow it works:\n- Converts query to vector embedding using semantic similarity\n- Searches all indexed file chunks for relevant content\n- Groups results by file and calculates average relevance scores\n- Returns files ranked by relevance score\n\nExamples:\n- \"database configuration and connection pooling setup\" - finds config files, documentation about DB setup\n- \"comprehensive error handling patterns and exception management\" - finds code files with exception handling\n- \"JWT authentication implementation and session management\" - finds auth-related code and docs\n- \"REST API documentation and endpoint specifications\" - finds API guides, endpoint definitions\n- \"Docker deployment scripts and CI/CD pipeline configuration\" - finds deployment automation\n\nReturns files with similarity scores and chunk information. Use get_content to retrieve full file content or get_chunk to retrieve specific chunk content by chunk ID.\n- Groups results by file to avoid duplicates from multiple matching sections\n\nResponse format:\n- Returns lightweight metadata including file paths, relevance scores, and chunk IDs\n- Use 'get_chunk' or 'get_content' tools to fetch actual content from search results\n- Chunks are sorted by relevance score within each file\n- Average similarity score calculated across all matching chunks per file\n\nExample queries:\n- \"error handling patterns and exception management strategies\" (finds try/catch, error classes, logging)\n- \"database migration scripts and schema versioning approaches\" (finds SQL, schema changes, migration files)\n- \"authentication middleware and JWT token validation logic\" (finds auth logic, JWT handling, middleware functions)`,\n inputSchema: {\n type: 'object',\n properties: {\n query: {\n type: 'string',\n description: 'Natural language search query describing what you are looking for. Can be concepts, functionality, or specific technical terms.'\n },\n limit: {\n type: 'number',\n description: 'Maximum number of files to return (default: 10). Each file may contain multiple matching chunks.',\n default: 10\n },\n workspace: {\n type: 'string',\n description: 'Optional workspace name to filter search results. Only files within the workspace directories will be searched. IMPORTANT: Use server_info tool first to discover available workspace names - using invalid workspace names will result in empty results.'\n }\n },\n required: ['query']\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: false\n }\n },\n {\n name: 'similar_files',\n description: `Find files with content similar to a reference file. Uses semantic similarity to find related documents, code files, or any text content.\n\nWhen to use this tool:\n- Find documentation similar to a specific guide or README\n- Locate related code files, configuration files, or scripts\n- Discover alternative implementations or approaches\n- Find files covering similar topics or concepts\n\nHow it works:\n- Analyzes the semantic content of the reference file\n- Compares against all indexed files using vector similarity\n- Returns files ranked by content similarity score\n\nExamples:\n- Given \"deployment-guide.md\" - finds other deployment docs, CI/CD guides, infrastructure setup\n- Given \"troubleshooting.md\" - finds other troubleshooting guides, FAQ files, error documentation\n- Given \"config.yaml\" - finds other configuration files, settings, environment setups\n- Given \"auth.py\" - finds other authentication modules, security code, middleware\n\nReturns file paths with similarity scores. Use get_content to read full files or get_chunk for specific sections.`,\n inputSchema: {\n type: 'object',\n properties: {\n file_path: {\n type: 'string',\n description: 'Absolute or relative path to the reference file. This file must have been previously indexed.'\n },\n limit: {\n type: 'number',\n description: 'Maximum number of similar files to return (default: 10). Results are sorted by similarity score.',\n default: 10\n },\n workspace: {\n type: 'string',\n description: 'Optional workspace name to filter results. Only files within the workspace directories will be considered. IMPORTANT: Use server_info tool first to discover available workspace names - using invalid workspace names will result in empty results.'\n }\n },\n required: ['file_path']\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: false\n }\n },\n {\n name: 'get_content',\n description: `Retrieve the full content of a file or specific chunks. Reads files directly from the filesystem.\n\nWhen to use this tool:\n- Get complete file content after finding files through search\n- Read documentation, code files, or configuration files for analysis\n- Extract specific sections of large files using chunk ranges\n- Access any text-based file content\n\nHow it works:\n- Reads files directly from filesystem (not from search index)\n- Returns entire file by default\n- Can return specific chunk ranges for indexed files\n- Preserves original formatting and content\n\nExamples:\n- Get full file: file_path=\"/home/user/docs/api.md\"\n- Get specific chunks: file_path=\"/home/user/code/main.py\", chunks=\"2-5\"\n- Get single chunk: file_path=\"/home/user/config.json\", chunks=\"1\"\n\nReturns file content as text. Use this after search or similar_files to read actual content.`,\n inputSchema: {\n type: 'object',\n properties: {\n file_path: {\n type: 'string',\n description: 'Absolute or relative path to the file to retrieve. File must be readable and text-based.'\n },\n chunks: {\n type: 'string',\n description: 'Optional chunk range specification. Examples: \"3\" (single chunk), \"2-5\" (chunks 2 through 5), \"1-3\" (first three chunks). Only works for indexed files.'\n }\n },\n required: ['file_path']\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: false\n }\n },\n {\n name: 'get_chunk',\n description: `Retrieve content of a specific chunk from an indexed file. Gets exact text segments identified during search.\n\nWhen to use this tool:\n- Get specific relevant sections after performing a search\n- Access only the most pertinent parts of large files\n- Retrieve content from high-scoring chunks identified in search results\n- Avoid reading entire files when only specific sections are needed\n\nHow it works:\n- Files are split into overlapping text chunks during indexing\n- Each chunk has a sequential ID (\"0\", \"1\", \"2\", etc.)\n- Search results include chunk IDs for relevant sections\n- Returns the exact content that was semantically matched\n\nExamples:\n- After search returns chunk \"3\" from \"api-docs.md\" with high score\n- Get chunk content: file_path=\"/docs/api-docs.md\", chunk_id=\"3\"\n- Returns the specific text segment that matched your query\n\nReturns chunk content as text. Use this with chunk IDs from search results to get precise content sections.`,\n inputSchema: {\n type: 'object',\n properties: {\n file_path: {\n type: 'string',\n description: 'Absolute or relative path to the indexed file containing the desired chunk.'\n },\n chunk_id: {\n type: 'string',\n description: 'ID of the specific chunk to retrieve. This is typically obtained from search results and is a sequential string like \"0\", \"1\", \"2\", etc.'\n }\n },\n required: ['file_path', 'chunk_id']\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: false\n }\n },\n {\n name: 'server_info',\n description: `Get information about server status and indexed content. Shows what directories and files are available for search.\n\nWhen to use this tool:\n- REQUIRED: Check available workspace names before using workspace parameter in search or similar_files tools\n- Check what content is already indexed before performing searches\n- Verify system is working properly\n- See indexing statistics and status\n- Understand scope of available searchable content\n\nHow it works:\n- Reports total indexed directories, files, and chunks\n- Shows database size and last indexing time\n- Lists all indexed directories with file counts\n- Lists all configured workspaces with their paths and file counts\n- Reports any errors or issues\n\nExamples:\n- Check workspaces before searching: \"What workspaces are available?\"\n- Check before searching: \"What content is indexed?\"\n- Verify after indexing: \"Did the indexing complete successfully?\"\n- Monitor system: \"How many files are searchable?\"\n\nReturns server version, indexing statistics, directory list, workspace information, and any errors. IMPORTANT: Always use this tool first to discover available workspace names when you need to search within specific workspaces.`,\n inputSchema: {\n type: 'object',\n properties: {},\n additionalProperties: false\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: false\n }\n }\n ];\n\n // Include delete_index tool unless DISABLE_DESTRUCTIVE is set\n if (process.env.DISABLE_DESTRUCTIVE !== 'true') {\n tools.push(DELETE_INDEX_TOOL);\n }\n\n return tools;\n}\n\nexport async function startMcpServer(config: Config): Promise<void> {\n initLogLevel();\n\n const server = new Server(\n {\n name: 'directory-indexer',\n version: VERSION\n },\n {\n capabilities: {\n tools: {},\n logging: {}\n }\n }\n );\n\n setMcpServer(server);\n\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n return {\n tools: getMcpTools()\n };\n });\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params;\n\n try {\n switch (name) {\n case 'index':\n return await handleIndexTool(args, config);\n \n case 'search':\n return await handleSearchTool(args);\n \n case 'similar_files':\n return await handleSimilarFilesTool(args);\n \n case 'get_content':\n return await handleGetContentTool(args, config);\n\n case 'get_chunk':\n return await handleGetChunkTool(args, config);\n \n case 'server_info':\n return await handleServerInfoTool(VERSION);\n\n case 'delete_index':\n if (process.env.DISABLE_DESTRUCTIVE === 'true') {\n throw new Error('delete_index is disabled via DISABLE_DESTRUCTIVE environment variable');\n }\n return await handleDeleteIndexTool(args, config);\n\n default:\n throw new Error(`Unknown tool: ${name}`);\n }\n } catch (error) {\n return formatErrorResponse(error);\n }\n });\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n // Graceful shutdown: close all SQLite connections before exiting\n const cleanup = () => {\n log('info', 'Shutting down MCP server');\n closeAllStorage();\n process.exit(0);\n };\n process.on('SIGTERM', cleanup);\n process.on('SIGINT', cleanup);\n\n if (config.verbose) {\n console.error('MCP server started successfully');\n }\n\n log('info', 'MCP server started', { version: VERSION });\n}"],"names":["__dirname"],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { realpathSync } from "fs";
|
|
2
|
+
import { resolve, sep } from "path";
|
|
3
|
+
function validatePathWithinIndexedDirs(filePath, indexedDirs) {
|
|
4
|
+
if (filePath.includes("\0")) {
|
|
5
|
+
throw new Error("Access denied: path contains null bytes");
|
|
6
|
+
}
|
|
7
|
+
if (filePath.startsWith("\\\\")) {
|
|
8
|
+
const uncParts = filePath.split("\\").filter(Boolean);
|
|
9
|
+
if (uncParts.length < 2) {
|
|
10
|
+
throw new Error("Invalid UNC path format: expected \\\\server\\share\\... pattern");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
let resolved;
|
|
14
|
+
try {
|
|
15
|
+
resolved = realpathSync(resolve(filePath));
|
|
16
|
+
} catch {
|
|
17
|
+
resolved = resolve(filePath);
|
|
18
|
+
}
|
|
19
|
+
for (const dir of indexedDirs) {
|
|
20
|
+
if (resolved === dir) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (resolved.startsWith(dir + sep)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Access denied: ${filePath} is outside indexed directories. Only files within indexed directories can be accessed.`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
function resolveIndexedDirectories(storage) {
|
|
32
|
+
const dirs = storage.getDirectories();
|
|
33
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
34
|
+
for (const dir of dirs) {
|
|
35
|
+
try {
|
|
36
|
+
resolved.add(realpathSync(resolve(dir)));
|
|
37
|
+
} catch {
|
|
38
|
+
resolved.add(resolve(dir));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return resolved;
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
resolveIndexedDirectories,
|
|
45
|
+
validatePathWithinIndexedDirs
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=path-validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-validation.js","sources":["../src/path-validation.ts"],"sourcesContent":["import { realpathSync } from 'fs';\nimport { resolve, sep } from 'path';\nimport { SQLiteStorage } from './storage.js';\n\n/**\n * Validates that a file path falls within one of the indexed directories.\n * Prevents path traversal attacks by resolving symlinks and checking prefixes.\n */\nexport function validatePathWithinIndexedDirs(filePath: string, indexedDirs: Set<string>): void {\n // Reject null bytes\n if (filePath.includes('\\x00')) {\n throw new Error('Access denied: path contains null bytes');\n }\n\n // Validate Windows UNC path format if it starts with \\\\\n if (filePath.startsWith('\\\\\\\\')) {\n const uncParts = filePath.split('\\\\').filter(Boolean);\n if (uncParts.length < 2) {\n throw new Error('Invalid UNC path format: expected \\\\\\\\server\\\\share\\\\... pattern');\n }\n }\n\n // Resolve the path, following symlinks if the file exists\n let resolved: string;\n try {\n resolved = realpathSync(resolve(filePath));\n } catch {\n // File may not exist yet (ENOENT), fall back to resolve only\n resolved = resolve(filePath);\n }\n\n for (const dir of indexedDirs) {\n // Exact match (the directory itself)\n if (resolved === dir) {\n return;\n }\n // Prefix match with separator to prevent /docs-evil matching /docs\n if (resolved.startsWith(dir + sep)) {\n return;\n }\n }\n\n throw new Error(\n `Access denied: ${filePath} is outside indexed directories. Only files within indexed directories can be accessed.`\n );\n}\n\n/**\n * Resolves all indexed directory paths from storage, following symlinks where possible.\n * Returns a Set of resolved absolute paths.\n */\nexport function resolveIndexedDirectories(storage: SQLiteStorage): Set<string> {\n const dirs = storage.getDirectories();\n const resolved = new Set<string>();\n\n for (const dir of dirs) {\n try {\n resolved.add(realpathSync(resolve(dir)));\n } catch {\n // Directory may have been removed; keep the raw resolved path\n resolved.add(resolve(dir));\n }\n }\n\n return resolved;\n}\n"],"names":[],"mappings":";;AAQO,SAAS,8BAA8B,UAAkB,aAAgC;AAE9F,MAAI,SAAS,SAAS,IAAM,GAAG;AAC7B,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAGA,MAAI,SAAS,WAAW,MAAM,GAAG;AAC/B,UAAM,WAAW,SAAS,MAAM,IAAI,EAAE,OAAO,OAAO;AACpD,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,IAAI,MAAM,kEAAkE;AAAA,IACpF;AAAA,EACF;AAGA,MAAI;AACJ,MAAI;AACF,eAAW,aAAa,QAAQ,QAAQ,CAAC;AAAA,EAC3C,QAAQ;AAEN,eAAW,QAAQ,QAAQ;AAAA,EAC7B;AAEA,aAAW,OAAO,aAAa;AAE7B,QAAI,aAAa,KAAK;AACpB;AAAA,IACF;AAEA,QAAI,SAAS,WAAW,MAAM,GAAG,GAAG;AAClC;AAAA,IACF;AAAA,EACF;AAEA,QAAM,IAAI;AAAA,IACR,kBAAkB,QAAQ;AAAA,EAAA;AAE9B;AAMO,SAAS,0BAA0B,SAAqC;AAC7E,QAAM,OAAO,QAAQ,eAAA;AACrB,QAAM,+BAAe,IAAA;AAErB,aAAW,OAAO,MAAM;AACtB,QAAI;AACF,eAAS,IAAI,aAAa,QAAQ,GAAG,CAAC,CAAC;AAAA,IACzC,QAAQ;AAEN,eAAS,IAAI,QAAQ,GAAG,CAAC;AAAA,IAC3B;AAAA,EACF;AAEA,SAAO;AACT;"}
|