🐕

code-index-mcp解析

に公開

code-index-mcp解析

1.プロジェクトのファイル構造

ツリー形式でプロジェクト構造(主要部分)を示します。

/
├── .github/
│   └── workflows/
│       └── publish-to-pypi.yml
├── src/
│   ├── code_index_mcp/
│   │   ├── __init__.py
│   │   ├── __main__.py
│   │   ├── project_settings.py
│   │   └── server.py
├── .dockerignore
├── .gitignore
├── .python-version
├── Dockerfile
├── LICENSE
├── README.md
├── README_zh.md
├── pyproject.toml
├── requirements.txt
├── run.py
└── uv.lock
  • src/code_index_mcp/ 配下が主な実装ディレクトリです。
  • それぞれのファイルは以下の役割が想定されます:
    • __init__.py:パッケージ初期化
    • __main__.py:モジュールとして実行した場合のエントリポイント
    • project_settings.py:プロジェクト設定に関する処理
    • server.py:サーバ機能の実装

2.code-index-mcpの使用可能ツール一覧

以下はcode-index-mcpで定義されたツールの一覧とそのユースケースを表形式で示したものです。

# ツール名 説明 ユースケース
1 set_project_path インデックス用のプロジェクトのベースパスを設定します。 プロジェクトのインデックスを開始する前に、分析対象のコードが格納されているディレクトリのパスを指定する際に使用します。例えば、プロジェクトのパスをC:\git-opensource\MCP\code-index-mcpに設定してください。のように指定します。
2 search_code インデックスされたファイル内でコードの一致を検索します。 特定のコードパターンや関数定義をプロジェクト全体で検索する際に使用します。例えば、Pythonファイル内で "set_project_path" が出現する箇所を検索してください。のように指定します。
3 find_files プロジェクト内で指定されたパターンに一致するファイルを検索します。 特定の拡張子を持つファイルや、特定の名前を持つファイルを検索する際に使用します。例えば、ファイル名に"server"を含むPythonファイルを検索してください。のように指定します。
4 get_file_summary 特定のファイルの要約を取得します。要約には、行数、関数、インポートなどが含まれます。 特定のファイルの構造や内容の概要を把握する際に使用します。例えば、プロジェクト内のrun.pyファイルの概要を教えてください。のように指定します。
5 refresh_index プロジェクトのインデックスを更新します。 プロジェクト内のファイルが追加、削除、変更された場合に、インデックスを最新の状態に更新する際に使用します。例えば、プロジェクトのインデックスを更新してください。のように指定します。
6 get_settings_info プロジェクトの設定に関する情報を取得します。 プロジェクトの設定やキャッシュの状態を確認する際に使用します。例えば、プロジェクト設定の情報を取得してください。のように指定します。
7 create_temp_directory インデックスデータを格納するための一時ディレクトリを作成します。 インデックスデータを保存するためのディレクトリが存在しない場合に、自動的に作成する際に使用します。例えば、インデックスデータを保存するための一時ディレクトリを作成してください。のように指定します。
8 check_temp_directory インデックスデータを格納するための一時ディレクトリを確認します。 一時ディレクトリが存在するか、書き込み可能かなどを確認する際に使用します。例えば、インデックスデータの保存に使用されている一時ディレクトリを確認してください。のように指定します。
9 clear_settings すべての設定とキャッシュデータを削除します。 設定やキャッシュデータを初期化する際に使用します。例えば、すべての設定とキャッシュデータをクリアしてください。のように指定します。

2.1. プロジェクトパス設定「set_project_path」(※)

プロンプト例

プロジェクトのパスをC:\git-opensource\MCP\code-index-mcpに設定してください。

実行結果

2.2. コード検索「search_code」(※)

プロンプト例

Pythonファイル内で "set_project_path" が出現する箇所を検索してください。

実行結果

{
  "src/code_index_mcp/server.py": [
    [
      111,
      "\"message\": \"Project path not set. Please use set_project_path to set a project directory first.\","
    ],
    [
      142,
      "return \"Error: Project path not set. Please use set_project_path to set a project directory first.\""
    ],
    [
      189,
      "\"message\": \"Project path not set. Please use set_project_path to set a project directory first.\""
    ],
    [
      218,
      "def set_project_path(path: str, ctx: Context) -> str:"
    ],
    [
      320,
      "return {\"error\": \"Project path not set. Please use set_project_path to set a project directory first.\"}"
    ],
    [
      378,
      "return [\"Error: Project path not set. Please use set_project_path to set a project directory first.\"]"
    ],
    [
      406,
      "return {\"error\": \"Project path not set. Please use set_project_path to set a project directory first.\"}"
    ],
    [
      528,
      "return \"Error: Project path not set. Please use set_project_path to set a project directory first.\""
    ],
    [
      561,
      "\"message\": \"Project path not set. Please use set_project_path to set a project directory first.\","
    ],
    [
      658,
      "return \"Error: Project path not set. Please use set_project_path to set a project directory first.\""
    ],
    [
      716,
      "Once you provide the path, I'll use the `set_project_path` tool to configure the code analyzer to work with your project."
    ]
  ]
}

2.3. ファイル検索「find_files」(※)

プロンプト例

ファイル名に"server"を含むPythonファイルを検索してください。

実行結果

2.4. ファイル概要取得「get_file_summary」

プロンプト例

プロジェクト内のrun.pyファイルの概要を教えてください。

実行結果

{
  "file_path": "run.py",
  "line_count": 28,
  "size_bytes": 895,
  "extension": ".py",
  "imports": [
    "import sys",
    "import os",
    "import traceback",
    "from code_index_mcp.server import main"
  ],
  "classes": [],
  "functions": [],
  "import_count": 4,
  "class_count": 0,
  "function_count": 0
}

2.5. インデックス更新「refresh_index」

プロンプト例

プロジェクトのインデックスを更新してください。

実行結果

2.6. 設定情報取得「get_settings_info」

プロンプト例

プロジェクト設定の情報を取得してください。

実行結果

{
  "settings_directory": "C:\\Users\\username\\AppData\\Local\\Temp\\code_indexer\\8b7ed37a70a23d29931807303a9390f5",
  "temp_directory": "C:\\Users\\username\\AppData\\Local\\Temp\\code_indexer",
  "temp_directory_exists": true,
  "config": {
    "base_path": "C:\\git-opensource\\MCP\\code-index-mcp",
    "supported_extensions": [
      ".py",
      ".js",
      ".ts",
      ".jsx",
      ".tsx",
      ".java",
      ".c",
      ".cpp",
      ".h",
      ".hpp",
      ".cs",
      ".go",
      ".rb",
      ".php",
      ".swift",
      ".kt",
      ".rs",
      ".scala",
      ".sh",
      ".bash",
      ".html",
      ".css",
      ".scss",
      ".md",
      ".json",
      ".xml",
      ".yml",
      ".yaml",
      ".vue",
      ".svelte",
      ".mjs",
      ".cjs",
      ".less",
      ".sass",
      ".stylus",
      ".styl",
      ".hbs",
      ".handlebars",
      ".ejs",
      ".pug",
      ".astro",
      ".mdx",
      ".sql",
      ".ddl",
      ".dml",
      ".mysql",
      ".postgresql",
      ".psql",
      ".sqlite",
      ".mssql",
      ".oracle",
      ".ora",
      ".db2",
      ".proc",
      ".procedure",
      ".func",
      ".function",
      ".view",
      ".trigger",
      ".index",
      ".migration",
      ".seed",
      ".fixture",
      ".schema",
      ".cql",
      ".cypher",
      ".sparql",
      ".gql",
      ".liquibase",
      ".flyway"
    ],
    "last_indexed": "2025-06-11T13:06:28.263481",
    "last_updated": "2025-06-11T13:06:28.263481"
  },
  "stats": {
    "settings_path": "C:\\Users\\username\\AppData\\Local\\Temp\\code_indexer\\8b7ed37a70a23d29931807303a9390f5",
    "exists": true,
    "is_directory": true,
    "writable": true,
    "files": {
      "config.json": {
        "path": "C:\\Users\\username\\AppData\\Local\\Temp\\code_indexer\\8b7ed37a70a23d29931807303a9390f5\\config.json",
        "size_bytes": 1185,
        "last_modified": "2025-06-11T13:06:28.264486",
        "readable": true,
        "writable": true
      },
      "file_index.pickle": {
        "path": "C:\\Users\\username\\AppData\\Local\\Temp\\code_indexer\\8b7ed37a70a23d29931807303a9390f5\\file_index.pickle",
        "size_bytes": 462,
        "last_modified": "2025-06-11T13:06:28.243476",
        "readable": true,
        "writable": true
      },
      "content_cache.pickle": {
        "path": "C:\\Users\\username\\AppData\\Local\\Temp\\code_indexer\\8b7ed37a70a23d29931807303a9390f5\\content_cache.pickle",
        "size_bytes": 93068,
        "last_modified": "2025-06-11T12:41:18.258873",
        "readable": true,
        "writable": true
      }
    },
    "temp_dir": "C:\\Users\\username\\AppData\\Local\\Temp",
    "current_dir": "c:\\git-opensource\\MCP",
    "all_files": [
      "config.json",
      "content_cache.pickle",
      "file_index.pickle",
      "README.md"
    ],
    "fallback_path": "c:\\git-opensource\\MCP\\.code_indexer",
    "fallback_exists": false,
    "fallback_is_directory": false
  },
  "exists": true
}

プロジェクト設定の情報は以下の通りです。

  • 設定ディレクトリ: C:\Users\username\AppData\Local\Temp\code_indexer\8b7ed37a70a23d29931807303a9390f5
  • 一時ディレクトリ: C:\Users\username\AppData\Local\Temp\code_indexer
  • 一時ディレクトリ存在有無: True
  • 設定:
    • ベースパス: C:\git-opensource\MCP\code-index-mcp
    • サポートされている拡張子: (多数のため省略)
    • 最終インデックス日時: 2025-06-11T13:06:28.263481
    • 最終更新日時: 2025-06-11T13:06:28.263481
  • 統計情報:
    • 設定パス: C:\Users\username\AppData\Local\Temp\code_indexer\8b7ed37a70a23d29931807303a9390f5
    • 存在有無: True
    • ディレクトリかどうか: True
    • 書き込み可能か: True
    • ファイル:
      • config.json:
        • パス: C:\Users\username\AppData\Local\Temp\code_indexer\8b7ed37a70a23d29931807303a9390f5\config.json
        • サイズ: 1185 バイト
        • 最終更新日時: 2025-06-11T13:06:28.264486
        • 読み取り可能か: True
        • 書き込み可能か: True
      • file_index.pickle:
        • パス: C:\Users\username\AppData\Local\Temp\code_indexer\8b7ed37a70a23d29931807303a9390f5\file_index.pickle
        • サイズ: 462 バイト
        • 最終更新日時: 2025-06-11T13:06:28.243476
        • 読み取り可能か: True
        • 書き込み可能か: True
      • content_cache.pickle:
        • パス: C:\Users\username\AppData\Local\Temp\code_indexer\8b7ed37a70a23d29931807303a9390f5\content_cache.pickle
        • サイズ: 93068 バイト
        • 最終更新日時: 2025-06-11T12:41:18.258873
        • 読み取り可能か: True
        • 書き込み可能か: True
    • 一時ディレクトリ: C:\Users\username\AppData\Local\Temp
    • カレントディレクトリ: c:\git-opensource\MCP
    • 全ファイル: config.json, content_cache.pickle, file_index.pickle, README.md
    • フォールバックパス: c:\git-opensource\MCP\.code_indexer
    • フォールバックパス存在有無: False
    • フォールバックパスがディレクトリかどうか: False
  • 存在有無: True

2.7. 一時ディレクトリ作成「create_temp_directory」

プロンプト例

インデックスデータを保存するための一時ディレクトリを作成してください。

実行結果

{
  "temp_directory": "C:\\Users\\username\\AppData\\Local\\Temp\\code_indexer",
  "existed_before": true,
  "created": false,
  "exists_now": true,
  "is_directory": true
}

2.8. 一時ディレクトリチェック「check_temp_directory」

プロンプト例

インデックスデータの保存に使用されている一時ディレクトリを確認してください。

実行結果

{
  "temp_directory": "C:\\Users\\username\\AppData\\Local\\Temp\\code_indexer",
  "exists": true,
  "is_directory": true,
  "temp_root": "C:\\Users\\username\\AppData\\Local\\Temp",
  "contents": [
    "7166b97b678bd0fbf6203a936f9d5145",
    "8b7ed37a70a23d29931807303a9390f5"
  ],
  "subdirectories": [
    {
      "name": "7166b97b678bd0fbf6203a936f9d5145",
      "path": "C:\\Users\\username\\AppData\\Local\\Temp\\code_indexer\\7166b97b678bd0fbf6203a936f9d5145",
      "contents": [
        "content_cache.pickle"
      ]
    },
    {
      "name": "8b7ed37a70a23d29931807303a9390f5",
      "path": "C:\\Users\\username\\AppData\\Local\\Temp\\code_indexer\\8b7ed37a70a23d29931807303a9390f5",
      "contents": [
        "content_cache.pickle",
        "file_index.pickle"
      ]
    }
  ]
}

2.9. 設定クリア「clear_settings」

プロンプト例

すべての設定とキャッシュデータをクリアしてください。

実行結果

All settings and cache cleared from C:\Users\username\AppData\Local\Temp\code_indexer\8b7ed37a70a23d29931807303a9390f5

すべての設定とキャッシュデータが C:\Users\username\AppData\Local\Temp\code_indexer\8b7ed37a70a23d29931807303a9390f5 からクリアされました。

3.ソース解析

3.1. set_project_path(path: str, ctx: Context) -> str:


@mcp.tool()
def set_project_path(path: str, ctx: Context) -> str:
    """Set the base project path for indexing."""
    # Validate and normalize path
    try:
        norm_path = os.path.normpath(path)
        abs_path = os.path.abspath(norm_path)

        if not os.path.exists(abs_path):
            return f"Error: Path does not exist: {abs_path}"

        if not os.path.isdir(abs_path):
            return f"Error: Path is not a directory: {abs_path}"

        # Clear existing in-memory index and cache
        global file_index, code_content_cache
        file_index.clear()
        code_content_cache.clear()

        # Update the base path in context
        ctx.request_context.lifespan_context.base_path = abs_path

        # Create a new settings manager for the new path (don't skip loading files)
        ctx.request_context.lifespan_context.settings = ProjectSettings(abs_path, skip_load=False)

        # Ensure .code_indexer is added to project's .gitignore
        gitignore_path = os.path.join(abs_path, ".gitignore")
        try:
            # Check if .gitignore exists
            if os.path.exists(gitignore_path):
                # Read existing content
                with open(gitignore_path, 'r', encoding='utf-8') as f:
                    content = f.read()

                # Check if .code_indexer is already in .gitignore
                if ".code_indexer/" not in content and ".code_indexer" not in content:
                    # Append to .gitignore
                    with open(gitignore_path, 'a', encoding='utf-8') as f:
                        f.write("\n# Code Index MCP cache directory\n.code_indexer/\n")
                    ctx.info(f"Added .code_indexer/ to project's .gitignore file.")
            else:
                # Create new .gitignore
                with open(gitignore_path, 'w', encoding='utf-8') as f:
                    f.write("# Code Index MCP cache directory\n.code_indexer/\n")
                ctx.info(f"Created .gitignore file with .code_indexer/ entry.")
        except Exception as gitignore_error:
            ctx.info(f"Note: Could not update .gitignore file: {gitignore_error}")

        # Print the settings path for debugging
        settings_path = ctx.request_context.lifespan_context.settings.settings_path
        print(f"Project settings path: {settings_path}")

        # Try to load existing index and cache
        print(f"Project path set to: {abs_path}")
        print(f"Attempting to load existing index and cache...")

        # Try to load index
        loaded_index = ctx.request_context.lifespan_context.settings.load_index()
        if loaded_index:
            print(f"Existing index found and loaded successfully")
            file_index = loaded_index
            file_count = _count_files(file_index)
            ctx.request_context.lifespan_context.file_count = file_count

            # Try to load cache
            loaded_cache = ctx.request_context.lifespan_context.settings.load_cache()
            if loaded_cache:
                print(f"Existing cache found and loaded successfully")
                code_content_cache.update(loaded_cache)

            return f"Project path set to: {abs_path}. Loaded existing index with {file_count} files."
        else:
            print(f"No existing index found, creating new index...")

        # If no existing index, create a new one
        file_count = _index_project(abs_path)
        ctx.request_context.lifespan_context.file_count = file_count

        # Save the new index
        ctx.request_context.lifespan_context.settings.save_index(file_index)

        # Save project config
        config = {
            "base_path": abs_path,
            "supported_extensions": supported_extensions,
            "last_indexed": ctx.request_context.lifespan_context.settings.load_config().get('last_indexed', None)
        }
        ctx.request_context.lifespan_context.settings.save_config(config)

        return f"Project path set to: {abs_path}. Indexed {file_count} files."
    except Exception as e:
        return f"Error setting project path: {e}"

3.2. search_code(query: str, ctx: Context, extensions: Optional[List[str]] = None, case_sensitive: bool = False) -> Dict[str, List[Tuple[int, str]]]:

@mcp.tool()
def search_code(query: str, ctx: Context, extensions: Optional[List[str]] = None, case_sensitive: bool = False) -> Dict[str, List[Tuple[int, str]]]:
    """
    Search for code matches within the indexed files.
    Returns a dictionary mapping filenames to lists of (line_number, line_content) tuples.
    """
    base_path = ctx.request_context.lifespan_context.base_path

    # Check if base_path is set
    if not base_path:
        return {"error": "Project path not set. Please use set_project_path to set a project directory first."}

    # Check if we need to index the project
    if not file_index:
        _index_project(base_path)
        ctx.request_context.lifespan_context.file_count = _count_files(file_index)
        ctx.request_context.lifespan_context.settings.save_index(file_index)

    results = {}

    # Filter by extensions if provided
    if extensions:
        valid_extensions = [ext if ext.startswith('.') else f'.{ext}' for ext in extensions]
    else:
        valid_extensions = supported_extensions

    # Process the search
    for file_path, _info in _get_all_files(file_index):
        # Check if the file has a supported extension
        if not any(file_path.endswith(ext) for ext in valid_extensions):
            continue

        try:
            # Get file content (from cache if available)
            if file_path in code_content_cache:
                content = code_content_cache[file_path]
            else:
                full_path = os.path.join(base_path, file_path)
                with open(full_path, 'r', encoding='utf-8') as f:
                    content = f.read()
                code_content_cache[file_path] = content

            # Search for matches
            matches = []
            for i, line in enumerate(content.splitlines(), 1):
                if (case_sensitive and query in line) or (not case_sensitive and query.lower() in line.lower()):
                    matches.append((i, line.strip()))

            if matches:
                results[file_path] = matches
        except Exception as e:
            ctx.info(f"Error searching file {file_path}: {e}")

    # Save the updated cache
    ctx.request_context.lifespan_context.settings.save_cache(code_content_cache)

    return results

3.3. find_files(pattern: str, ctx: Context) -> List[str]:

@mcp.tool()
def find_files(pattern: str, ctx: Context) -> List[str]:
    """
    Find files in the project that match the given pattern.
    Supports glob patterns like *.py or **/*.js.
    """
    base_path = ctx.request_context.lifespan_context.base_path

    # Check if base_path is set
    if not base_path:
        return ["Error: Project path not set. Please use set_project_path to set a project directory first."]

    # Check if we need to index the project
    if not file_index:
        _index_project(base_path)
        ctx.request_context.lifespan_context.file_count = _count_files(file_index)
        ctx.request_context.lifespan_context.settings.save_index(file_index)

    matching_files = []
    for file_path, _info in _get_all_files(file_index):
        if fnmatch.fnmatch(file_path, pattern):
            matching_files.append(file_path)

    return matching_files

3.4. get_file_summary(file_path: str, ctx: Context) -> Dict[str, Any]:

@mcp.tool()
def get_file_summary(file_path: str, ctx: Context) -> Dict[str, Any]:
    """
    Get a summary of a specific file, including:
    - Line count
    - Function/class definitions (for supported languages)
    - Import statements
    - Basic complexity metrics
    """
    base_path = ctx.request_context.lifespan_context.base_path

    # Check if base_path is set
    if not base_path:
        return {"error": "Project path not set. Please use set_project_path to set a project directory first."}

    # Normalize the file path
    norm_path = os.path.normpath(file_path)
    if norm_path.startswith('..'):
        return {"error": f"Invalid file path: {file_path}"}

    full_path = os.path.join(base_path, norm_path)

    try:
        # Get file content
        if norm_path in code_content_cache:
            content = code_content_cache[norm_path]
        else:
            with open(full_path, 'r', encoding='utf-8') as f:
                content = f.read()
            code_content_cache[norm_path] = content
            # Save the updated cache
            ctx.request_context.lifespan_context.settings.save_cache(code_content_cache)

        # Basic file info
        lines = content.splitlines()
        line_count = len(lines)

        # File extension for language-specific analysis
        _, ext = os.path.splitext(norm_path)

        summary = {
            "file_path": norm_path,
            "line_count": line_count,
            "size_bytes": os.path.getsize(full_path),
            "extension": ext,
        }

        # Language-specific analysis
        if ext == '.py':
            # Python analysis
            imports = []
            classes = []
            functions = []

            for i, line in enumerate(lines):
                line = line.strip()

                # Check for imports
                if line.startswith('import ') or line.startswith('from '):
                    imports.append(line)

                # Check for class definitions
                if line.startswith('class '):
                    classes.append({
                        "line": i + 1,
                        "name": line.replace('class ', '').split('(')[0].split(':')[0].strip()
                    })

                # Check for function definitions
                if line.startswith('def '):
                    functions.append({
                        "line": i + 1,
                        "name": line.replace('def ', '').split('(')[0].strip()
                    })

            summary.update({
                "imports": imports,
                "classes": classes,
                "functions": functions,
                "import_count": len(imports),
                "class_count": len(classes),
                "function_count": len(functions),
            })

        elif ext in ['.js', '.jsx', '.ts', '.tsx']:
            # JavaScript/TypeScript analysis
            imports = []
            classes = []
            functions = []

            for i, line in enumerate(lines):
                line = line.strip()

                # Check for imports
                if line.startswith('import ') or line.startswith('require('):
                    imports.append(line)

                # Check for class definitions
                if line.startswith('class ') or 'class ' in line:
                    class_name = ""
                    if 'class ' in line:
                        parts = line.split('class ')[1]
                        class_name = parts.split(' ')[0].split('{')[0].split('extends')[0].strip()
                    classes.append({
                        "line": i + 1,
                        "name": class_name
                    })

                # Check for function definitions
                if 'function ' in line or '=>' in line:
                    functions.append({
                        "line": i + 1,
                        "content": line
                    })

            summary.update({
                "imports": imports,
                "classes": classes,
                "functions": functions,
                "import_count": len(imports),
                "class_count": len(classes),
                "function_count": len(functions),
            })

        return summary
    except Exception as e:
        return {"error": f"Error analyzing file: {e}"}

■サンプルResult:

{
  "file_path": "src\\code_index_mcp\\server.py",
  "line_count": 821,
  "size_bytes": 30845,
  "extension": ".py",
  "imports": [
    "from contextlib import asynccontextmanager",
    "from dataclasses import dataclass",
    "from typing import AsyncIterator, Dict, List, Optional, Tuple, Any",
    "import os",
    "import pathlib",
    "import json",
    "import fnmatch",
    "import sys",
    "import tempfile",
    "from mcp.server.fastmcp import FastMCP, Context, Image",
    "from mcp import types",
    "from .project_settings import ProjectSettings"
  ],
  "classes": [
    {
      "line": 54,
      "name": "CodeIndexerContext"
    }
  ],
  "functions": [
    {
      "line": 100,
      "name": "get_config"
    },
    {
      "line": 133,
      "name": "get_file_content"
    },
    {
      "line": 178,
      "name": "get_project_structure"
    },
    {
      "line": 203,
      "name": "get_settings_stats"
    },
    {
      "line": 218,
      "name": "set_project_path"
    },
    {
      "line": 311,
      "name": "search_code"
    },
    {
      "line": 369,
      "name": "find_files"
    },
    {
      "line": 394,
      "name": "get_file_summary"
    },
    {
      "line": 522,
      "name": "refresh_index"
    },
    {
      "line": 551,
      "name": "get_settings_info"
    },
    {
      "line": 587,
      "name": "create_temp_directory"
    },
    {
      "line": 618,
      "name": "check_temp_directory"
    },
    {
      "line": 652,
      "name": "clear_settings"
    },
    {
      "line": 675,
      "name": "analyze_code"
    },
    {
      "line": 690,
      "name": "code_search"
    },
    {
      "line": 701,
      "name": "set_project"
    },
    {
      "line": 723,
      "name": "_index_project"
    },
    {
      "line": 770,
      "name": "_count_files"
    },
    {
      "line": 783,
      "name": "_get_all_files"
    },
    {
      "line": 800,
      "name": "main"
    }
  ],
  "import_count": 12,
  "class_count": 1,
  "function_count": 20
}

3.5. refresh_index(ctx: Context) -> str:

@mcp.tool()
def refresh_index(ctx: Context) -> str:
    """Refresh the project index."""
    base_path = ctx.request_context.lifespan_context.base_path

    # Check if base_path is set
    if not base_path:
        return "Error: Project path not set. Please use set_project_path to set a project directory first."

    # Clear existing index
    global file_index
    file_index.clear()

    # Re-index the project
    file_count = _index_project(base_path)
    ctx.request_context.lifespan_context.file_count = file_count

    # Save the updated index
    ctx.request_context.lifespan_context.settings.save_index(file_index)

    # Update the last indexed timestamp in config
    config = ctx.request_context.lifespan_context.settings.load_config()
    ctx.request_context.lifespan_context.settings.save_config({
        **config,
        'last_indexed': ctx.request_context.lifespan_context.settings._get_timestamp()
    })

    return f"Project re-indexed. Found {file_count} files."

3.6. get_settings_info(ctx: Context) -> Dict[str, Any]:

@mcp.tool()
def get_settings_info(ctx: Context) -> Dict[str, Any]:
    """Get information about the project settings."""
    base_path = ctx.request_context.lifespan_context.base_path

    # Check if base_path is set
    if not base_path:
        # Even if base_path is not set, we can still show the temp directory
        temp_dir = os.path.join(tempfile.gettempdir(), "code_indexer")
        return {
            "status": "not_configured",
            "message": "Project path not set. Please use set_project_path to set a project directory first.",
            "temp_directory": temp_dir,
            "temp_directory_exists": os.path.exists(temp_dir)
        }

    settings = ctx.request_context.lifespan_context.settings

    # Get config
    config = settings.load_config()

    # Get stats
    stats = settings.get_stats()

    # Get temp directory
    temp_dir = os.path.join(tempfile.gettempdir(), "code_indexer")

    return {
        "settings_directory": settings.settings_path,
        "temp_directory": temp_dir,
        "temp_directory_exists": os.path.exists(temp_dir),
        "config": config,
        "stats": stats,
        "exists": os.path.exists(settings.settings_path)
    }

3.7. create_temp_directory() -> Dict[str, Any]:

@mcp.tool()
def create_temp_directory() -> Dict[str, Any]:
    """Create the temporary directory used for storing index data."""
    temp_dir = os.path.join(tempfile.gettempdir(), "code_indexer")

    result = {
        "temp_directory": temp_dir,
        "existed_before": os.path.exists(temp_dir),
    }

    try:
        # Create the directory if it doesn't exist
        if not os.path.exists(temp_dir):
            os.makedirs(temp_dir, exist_ok=True)
            result["created"] = True

            # Create a README file
            readme_path = os.path.join(temp_dir, "README.md")
            with open(readme_path, 'w', encoding='utf-8') as f:
                f.write("# Code Indexer Cache Directory\n\nThis directory contains cached data for the Code Index MCP tool.\nEach subdirectory corresponds to a different project.\n")
            result["readme_created"] = True
        else:
            result["created"] = False

        result["exists_now"] = os.path.exists(temp_dir)
        result["is_directory"] = os.path.isdir(temp_dir)
    except Exception as e:
        result["error"] = str(e)

    return result

3.8. check_temp_directory() -> Dict[str, Any]:

@mcp.tool()
def check_temp_directory() -> Dict[str, Any]:
    """Check the temporary directory used for storing index data."""
    temp_dir = os.path.join(tempfile.gettempdir(), "code_indexer")

    result = {
        "temp_directory": temp_dir,
        "exists": os.path.exists(temp_dir),
        "is_directory": os.path.isdir(temp_dir) if os.path.exists(temp_dir) else False,
        "temp_root": tempfile.gettempdir(),
    }

    # If the directory exists, list its contents
    if result["exists"] and result["is_directory"]:
        try:
            contents = os.listdir(temp_dir)
            result["contents"] = contents
            result["subdirectories"] = []

            # Check each subdirectory
            for item in contents:
                item_path = os.path.join(temp_dir, item)
                if os.path.isdir(item_path):
                    subdir_info = {
                        "name": item,
                        "path": item_path,
                        "contents": os.listdir(item_path) if os.path.exists(item_path) else []
                    }
                    result["subdirectories"].append(subdir_info)
        except Exception as e:
            result["error"] = str(e)

    return result

3.9. clear_settings(ctx: Context) -> str:

@mcp.tool()
def clear_settings(ctx: Context) -> str:
    """Clear all settings and cached data."""
    base_path = ctx.request_context.lifespan_context.base_path

    # Check if base_path is set
    if not base_path:
        return "Error: Project path not set. Please use set_project_path to set a project directory first."

    settings = ctx.request_context.lifespan_context.settings

    # Clear all settings files
    settings.clear()

    # Clear in-memory cache and index
    global file_index, code_content_cache
    file_index.clear()
    code_content_cache.clear()

    return f"All settings and cache cleared from {settings.settings_path}"

@mcp.resource("config://code-indexer")

get_config() -> str:

@mcp.resource("config://code-indexer")
def get_config() -> str:
    """Get the current configuration of the Code Indexer."""
    ctx = mcp.get_context()

    # Get the base path from context
    base_path = ctx.request_context.lifespan_context.base_path

    # Check if base_path is set
    if not base_path:
        return json.dumps({
            "status": "not_configured",
            "message": "Project path not set. Please use set_project_path to set a project directory first.",
            "supported_extensions": supported_extensions
        }, indent=2)

    # Get file count
    file_count = ctx.request_context.lifespan_context.file_count

    # Get settings stats
    settings = ctx.request_context.lifespan_context.settings
    settings_stats = settings.get_stats()

    config = {
        "base_path": base_path,
        "supported_extensions": supported_extensions,
        "file_count": file_count,
        "settings_directory": settings.settings_path,
        "settings_stats": settings_stats
    }

    return json.dumps(config, indent=2)

@mcp.resource("files://{file_path}")

get_file_content(file_path: str) -> str:


@mcp.resource("files://{file_path}")
def get_file_content(file_path: str) -> str:
    """Get the content of a specific file."""
    ctx = mcp.get_context()

    # Get the base path from context
    base_path = ctx.request_context.lifespan_context.base_path

    # Check if base_path is set
    if not base_path:
        return "Error: Project path not set. Please use set_project_path to set a project directory first."

    # Handle absolute paths (especially Windows paths starting with drive letters)
    if os.path.isabs(file_path) or (len(file_path) > 1 and file_path[1] == ':'):
        # Absolute paths are not allowed via this endpoint
        return f"Error: Absolute file paths like '{file_path}' are not allowed. Please use paths relative to the project root."

    # Normalize the file path
    norm_path = os.path.normpath(file_path)

    # Check for path traversal attempts
    if "..\\" in norm_path or "../" in norm_path or norm_path.startswith(".."):
        return f"Error: Invalid file path: {file_path} (directory traversal not allowed)"

    # Construct the full path and verify it's within the project bounds
    full_path = os.path.join(base_path, norm_path)
    real_full_path = os.path.realpath(full_path)
    real_base_path = os.path.realpath(base_path)

    if not real_full_path.startswith(real_base_path):
        return f"Error: Access denied. File path must be within project directory."

    try:
        with open(full_path, 'r', encoding='utf-8') as f:
            content = f.read()

        # Cache the content for faster retrieval later
        code_content_cache[norm_path] = content

        return content
    except UnicodeDecodeError:
        return f"Error: File {file_path} appears to be a binary file or uses unsupported encoding."
    except Exception as e:
        return f"Error reading file: {e}"

@mcp.resource("structure://project")

get_project_structure() -> str:

@mcp.resource("structure://project")
def get_project_structure() -> str:
    """Get the structure of the project as a JSON tree."""
    ctx = mcp.get_context()

    # Get the base path from context
    base_path = ctx.request_context.lifespan_context.base_path

    # Check if base_path is set
    if not base_path:
        return json.dumps({
            "status": "not_configured",
            "message": "Project path not set. Please use set_project_path to set a project directory first."
        }, indent=2)

    # Check if we need to refresh the index
    if not file_index:
        _index_project(base_path)
        # Update file count in context
        ctx.request_context.lifespan_context.file_count = _count_files(file_index)
        # Save updated index
        ctx.request_context.lifespan_context.settings.save_index(file_index)

    return json.dumps(file_index, indent=2)

@mcp.resource("settings://stats")

get_settings_stats() -> str:


@mcp.resource("settings://stats")
def get_settings_stats() -> str:
    """Get statistics about the settings directory and files."""
    ctx = mcp.get_context()

    # Get settings manager from context
    settings = ctx.request_context.lifespan_context.settings

    # Get settings stats
    stats = settings.get_stats()

    return json.dumps(stats, indent=2)

_index_project(base_path: str) -> int:

def _index_project(base_path: str) -> int:
    """
    Create an index of the project files.
    Returns the number of files indexed.
    """
    file_count = 0
    file_index.clear()

    for root, dirs, files in os.walk(base_path):
        # Skip hidden directories and common build/dependency directories
        dirs[:] = [d for d in dirs if not d.startswith('.') and
                 d not in ['node_modules', 'venv', '__pycache__', 'build', 'dist']]

        # Create relative path from base_path
        rel_path = os.path.relpath(root, base_path)
        current_dir = file_index

        # Skip the '.' directory (base_path itself)
        if rel_path != '.':
            # Split the path and navigate/create the tree
            path_parts = rel_path.replace('\\', '/').split('/')
            for part in path_parts:
                if part not in current_dir:
                    current_dir[part] = {}
                current_dir = current_dir[part]

        # Add files to current directory
        for file in files:
            # Skip hidden files and files with unsupported extensions
            _, ext = os.path.splitext(file)
            if file.startswith('.') or ext not in supported_extensions:
                continue

            # Store file information
            file_path = os.path.join(rel_path, file).replace('\\', '/')
            if rel_path == '.':
                file_path = file

            current_dir[file] = {
                "type": "file",
                "path": file_path,
                "ext": ext
            }
            file_count += 1

    return file_count

■サンプル結果:

{
  "README.md": {
    "type": "file",
    "path": "README.md",
    "ext": ".md"
  },
  "README_zh.md": {
    "type": "file",
    "path": "README_zh.md",
    "ext": ".md"
  },
  "run.py": {
    "type": "file",
    "path": "run.py",
    "ext": ".py"
  },
  "src": {
    "code_index_mcp": {
      "project_settings.py": {
        "type": "file",
        "path": "src/code_index_mcp/project_settings.py",
        "ext": ".py"
      },
      "server.py": {
        "type": "file",
        "path": "src/code_index_mcp/server.py",
        "ext": ".py"
      },
      "__init__.py": {
        "type": "file",
        "path": "src/code_index_mcp/__init__.py",
        "ext": ".py"
      },
      "__main__.py": {
        "type": "file",
        "path": "src/code_index_mcp/__main__.py",
        "ext": ".py"
      }
    }
  }
}

_count_files(directory: Dict) -> int:

def _count_files(directory: Dict) -> int:
    """
    Count the number of files in the index.
    """
    count = 0
    for name, value in directory.items():
        if isinstance(value, dict):
            if "type" in value and value["type"] == "file":
                count += 1
            else:
                count += _count_files(value)
    return count
def _get_all_files(directory: Dict, prefix: str = "") -> List[Tuple[str, Dict]]:
    """
    Recursively get all files from the directory structure.
    Returns a list of (file_path, file_info) tuples.
    """
    result = []

    for name, value in directory.items():
        if isinstance(value, dict):
            if "type" in value and value["type"] == "file":
                result.append((value["path"], value))
            else:
                new_prefix = f"{prefix}/{name}" if prefix else name
                result.extend(_get_all_files(value, new_prefix))

    return result

@mcp.prompt()

analyze_code(file_path: str = "", query: str = "") -> list[types.PromptMessage]:

@mcp.prompt()
def analyze_code(file_path: str = "", query: str = "") -> list[types.PromptMessage]:
    """Prompt for analyzing code in the project."""
    messages = [
        types.PromptMessage(role="user", content=types.TextContent(type="text", text=f"""I need you to analyze some code from my project.

{f'Please analyze the file: {file_path}' if file_path else ''}
{f'I want to understand: {query}' if query else ''}

First, let me give you some context about the project structure. Then, I'll provide the code to analyze.
""")),
        types.PromptMessage(role="assistant", content=types.TextContent(type="text", text="I'll help you analyze the code. Let me first examine the project structure to get a better understanding of the codebase."))
    ]
    return messages

@mcp.prompt()

code_search(query: str = "") -> types.TextContent:

@mcp.prompt()
def code_search(query: str = "") -> types.TextContent:
    """Prompt for searching code in the project."""
    search_text = f"\"query\"" if not query else f"\"{query}\""
    return types.TextContent(type="text", text=f"""I need to search through my codebase for {search_text}.

Please help me find all occurrences of this query and explain what each match means in its context.
Focus on the most relevant files and provide a brief explanation of how each match is used in the code.

If there are too many results, prioritize the most important ones and summarize the patterns you see.""")

@mcp.prompt()

set_project() -> list[types.PromptMessage]:

@mcp.prompt()
def set_project() -> list[types.PromptMessage]:
    """Prompt for setting the project path."""
    messages = [
        types.PromptMessage(role="user", content=types.TextContent(type="text", text="""
        I need to analyze code from a project, but I haven't set the project path yet. Please help me set up the project path and index the code.

        First, I need to specify which project directory to analyze.
        """)),
        types.PromptMessage(role="assistant", content=types.TextContent(type="text", text="""
        Before I can help you analyze any code, we need to set up the project path. This is a required first step.

        Please provide the full path to your project folder. For example:
        - Windows: "C:/Users/username/projects/my-project"
        - macOS/Linux: "/home/username/projects/my-project"

        Once you provide the path, I'll use the `set_project_path` tool to configure the code analyzer to work with your project.
        """))
    ]
    return messages

Discussion