This file is a merged representation of the entire codebase, combining all repository files into a single document.
Generated by Repomix on: 2025-03-16T20:32:02.038Z

================================================================
File Summary
================================================================

Purpose:
--------
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.

File Format:
------------
The content is organized as follows:
1. This summary section
2. Repository information
3. Directory structure
4. Multiple file entries, each consisting of:
  a. A separator line (================)
  b. The file path (File: path/to/file)
  c. Another separator line
  d. The full contents of the file
  e. A blank line

Usage Guidelines:
-----------------
- This file should be treated as read-only. Any changes should be made to the
  original repository files, not this packed version.
- When processing this file, use the file path to distinguish
  between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
  the same level of security as you would the original repository.

Notes:
------
- Some files may have been excluded based on .gitignore rules and Repomix's
  configuration.
- Binary files are not included in this packed representation. Please refer to
  the Repository Structure section for a complete list of file paths, including
  binary files.

Additional Info:
----------------

================================================================
Directory Structure
================================================================
src/
  windtools_mcp/
    __init__.py
    __main__.py
    server.py
tests/
  test_client.py
.gitignore
.python-version
pyproject.toml

================================================================
Files
================================================================

================
File: src/windtools_mcp/__init__.py
================
from .server import mcp


def main():
    mcp.run()


if __name__ == "__main__":
    main()

================
File: src/windtools_mcp/__main__.py
================
from windtools_mcp import main

main()

================
File: src/windtools_mcp/server.py
================
import json
import logging
import os
import os.path
from contextlib import asynccontextmanager
from dataclasses import dataclass
from logging import INFO, basicConfig
from typing import Any, AsyncIterator, Dict, List

from mcp.server.fastmcp import FastMCP

basicConfig(
    level=INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(filename)s - %(message)s",
)
# PARAMS
DATA_ROOT = os.environ.get("DATA_ROOT", os.path.join(os.path.dirname(__file__), "data"))
CHROMA_DB_FOLDER_NAME = os.environ.get("CHROMA_DB_FOLDER_NAME", "default")
SENTENCE_TRANSFORMER_PATH = os.environ.get(
    "SENTENCE_TRANSFORMER_PATH", "jinaai/jina-embeddings-v2-base-code"
)

# Ensure data directories exist
os.makedirs(DATA_ROOT, exist_ok=True)
CHROMA_DB_PATH = os.path.join(DATA_ROOT, CHROMA_DB_FOLDER_NAME)
SENTENCE_TRANSFORMER_CACHE_FOLDER = os.path.join(DATA_ROOT, "embedding_cache")


# Server lifespan context for ChromaDB initialization and project directory
@dataclass
class ServerContext:
    chroma_client: Any
    code_collection: Any
    embedding_model: str


@asynccontextmanager
async def server_lifespan(server: FastMCP) -> AsyncIterator[ServerContext]:
    """Initialize and clean up resources during server lifecycle"""
    logging.info(f"Initializing ChromaDB at {CHROMA_DB_PATH} and embedding model...")

    # Ensure all data directories exist
    os.makedirs(CHROMA_DB_PATH, exist_ok=True)
    os.makedirs(SENTENCE_TRANSFORMER_CACHE_FOLDER, exist_ok=True)

    # Import ChromaDB here to allow for dependency installation
    import chromadb
    from chromadb.utils import embedding_functions

    chroma_client = chromadb.PersistentClient(path=CHROMA_DB_PATH)

    embedding_function = embedding_functions.SentenceTransformerEmbeddingFunction(
        model_name=SENTENCE_TRANSFORMER_PATH,
        cache_folder=SENTENCE_TRANSFORMER_CACHE_FOLDER,
    )

    # Create or get the code collection
    try:
        code_collection = chroma_client.get_collection(
            name="code_collection", embedding_function=embedding_function
        )
        logging.info(
            f"Using existing code collection with {code_collection.count()} documents"
        )
    except Exception as e:
        logging.info(f"Collection not found, creating new one. Error: {str(e)}")
        code_collection = chroma_client.create_collection(
            name="code_collection", embedding_function=embedding_function
        )
        logging.info("Created new code collection")

    ctx = ServerContext(
        chroma_client=chroma_client,
        code_collection=code_collection,
        embedding_model=SENTENCE_TRANSFORMER_PATH,
    )

    try:
        yield ctx
    finally:
        logging.info("Cleaning up ChromaDB resources...")
        # ChromaDB client will be closed automatically when the process ends


mcp = FastMCP(
    "WindCodeAssistant",
    dependencies=["glob", "re", "json", "subprocess"],
    lifespan=server_lifespan,
)


def _get_directory_info(directory_path: str) -> List[Dict[str, Any]]:
    """
    Helper function to get information about directory contents

    Args:
        directory_path: Absolute path to the directory

    Returns:
        List of dictionaries containing information about each child in the directory
    """
    result = []

    # Check if directory exists and is a directory
    if not os.path.exists(directory_path):
        raise ValueError(f"Directory does not exist: {directory_path}")
    if not os.path.isdir(directory_path):
        raise ValueError(f"Path is not a directory: {directory_path}")

    # Get all entries in the directory
    for entry in os.listdir(directory_path):
        entry_path = os.path.join(directory_path, entry)
        relative_path = os.path.relpath(entry_path, directory_path)

        if os.path.isdir(entry_path):
            # For directories, count children recursively
            child_count = sum(len(files) for _, _, files in os.walk(entry_path))
            result.append(
                {"path": relative_path, "type": "directory", "child_count": child_count}
            )
        else:
            # For files, get size in bytes
            size = os.path.getsize(entry_path)
            result.append({"path": relative_path, "type": "file", "size": size})

    return result


@mcp.tool()
def list_dir(directory_path: str) -> str:
    """
    List the contents of a directory.

    Directory path must be an absolute path to a directory that exists.
    For each child in the directory, output will have:
    - relative path to the directory
    - whether it is a directory or file
    - size in bytes if file
    - number of children (recursive) if directory

    Args:
        directory_path: Path to list contents of, should be absolute path to a directory

    Returns:
        JSON string containing directory information
    """
    try:
        logging.info(f"Listing directory: {directory_path}")
        directory_info = _get_directory_info(directory_path)
        return json.dumps(directory_info, indent=2)
    except Exception as e:
        logging.error(f"Error listing directory {directory_path}: {str(e)}")
        return json.dumps({"error": str(e)})

================
File: tests/test_client.py
================
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

# Create server parameters for stdio connection
server_params = StdioServerParameters(
    command="python",  # Executable
    args=["../server/main.py"],  # Optional command line arguments
    env=None,  # Optional environment variables
)


async def run():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # Initialize the connection
            await session.initialize()

            # List available prompts
            prompts = await session.list_prompts()
            print(prompts)

            # List available resources
            resources = await session.list_resources()
            print(resources)

            # List available tools
            tools = await session.list_tools()
            print(tools)


if __name__ == "__main__":
    import asyncio

    asyncio.run(run())

================
File: .gitignore
================
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

# Virtual environments
.venv
.env

================
File: .python-version
================
3.10.13

================
File: pyproject.toml
================
[project]
name = "windtools-mcp"
version = "0.0.1"
description = "A Codebase MCP Tools"
classifiers = [
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
    "Programming Language :: Python :: 3.14",
]
readme = "README.md"
requires-python = ">=3.10.13"
dependencies = [
    "mcp>=1.4.1",
    "chromadb>=0.6.3",
    "sentence-transformers>=3.4.1",
]

[[project.authors]]
name = "ZahidGalea"
email = "zahidale.zg@gmail.com"

[project.scripts]
mcp-wintools = "windtools_mcp:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.uv]
dev-dependencies = [
    "freezegun>=1.5.1",
    "pyright>=1.1.389",
    "pytest>=8.3.3",
    "ruff>=0.8.1",
]

[tool.ruff]
line-length = 120

[tool.ruff.format]
docstring-code-format = true

[tool.ruff.lint]
select = ["E", "F", "I"]
