diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73d0316..c87c701 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,11 +2,11 @@ name: CI/CD Pipeline on: push: - branches: [ master, release ] + branches: [master, release] pull_request: - branches: [ master ] + branches: [master] release: - types: [ published ] + types: [published] env: REGISTRY: ghcr.io @@ -14,36 +14,27 @@ env: jobs: lint: - name: Code Style & Linting + name: Lint runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 + - uses: actions/setup-python@v5 with: - python-version: '3.12' - - - name: Create virtual environment - run: python -m venv venv + python-version: "3.12" - name: Install dependencies run: | - source venv/bin/activate - pip install --upgrade pip - pip install black flake8 - pip install -e . + pip install \ + "git+https://github.com/vicentebolea/vtk-knowledge" \ + "git+https://github.com/vicentebolea/vtk-validate" + pip install -e ".[dev]" - - name: Run Black - run: | - source venv/bin/activate - black --check --diff src/ tests/ + - name: ruff lint + run: ruff check src/vtk_mcp/ - - name: Run Flake8 - run: | - source venv/bin/activate - flake8 src/ + - name: ruff format check + run: ruff format --check src/vtk_mcp/ actionlint: runs-on: ubuntu-latest @@ -52,7 +43,6 @@ jobs: - name: Install actionlint run: | - # Download and install actionlint bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) echo "${PWD}" >> "$GITHUB_PATH" @@ -60,45 +50,49 @@ jobs: run: actionlint test: - name: Run Tests + name: Test (Python ${{ matrix.python-version }}) runs-on: ubuntu-latest needs: [lint, actionlint] strategy: matrix: - python-version: ['3.10', '3.11', '3.12'] + python-version: ["3.10", "3.11", "3.12"] steps: - - name: Checkout code - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Create virtual environment - run: python -m venv venv - - name: Install dependencies run: | - source venv/bin/activate - pip install --upgrade pip - pip install -e ".[test]" + pip install \ + "git+https://github.com/vicentebolea/vtk-knowledge" \ + "git+https://github.com/vicentebolea/vtk-validate" + pip install -e ".[dev]" - name: Run unit tests - run: | - source venv/bin/activate - pytest -m unit -v + run: pytest -m unit -v - name: Run client tests - run: | - source venv/bin/activate - pytest tests/test_client_no_server.py -v + run: pytest tests/test_client_no_server.py -v - name: Run integration tests - run: | - source venv/bin/activate - pytest -m integration -v + run: pytest -m integration -v + + uv-smoke: + name: uv smoke test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/setup-uv@v5 + + - name: uv sync resolves all dependencies + run: uv sync --extra dev + + - name: Import check + run: uv run python -c "from vtk_mcp.config import Settings; Settings()" docker-deploy: name: Build and Push Deployment Image @@ -109,8 +103,7 @@ jobs: contents: read packages: write steps: - - name: Checkout code - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: submodules: recursive diff --git a/Dockerfile b/Dockerfile index 169cfc3..1e776a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -FROM python:3.11-slim +FROM python:3.12-slim LABEL org.opencontainers.image.title="VTK MCP Gateway" LABEL org.opencontainers.image.description="Production MCP gateway for VTK LLM tooling" -LABEL org.opencontainers.image.source="https://github.com/kitware/vtk-mcp" +LABEL org.opencontainers.image.source="https://github.com/Kitware/vtk-mcp" LABEL org.opencontainers.image.licenses="MIT" ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ @@ -10,22 +10,42 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 -WORKDIR /app - -# Install runtime dependencies — no VTK runtime, no LiteLLM -RUN pip install vtk-knowledge vtk-validate vtk-mcp - -# Bundle the knowledge artifact (built separately and passed at build time) +# VTK version to pre-cache at image build time ARG VTK_VERSION=9.3.0 -ARG KNOWLEDGE_ARTIFACT_URL="" -RUN if [ -n "$KNOWLEDGE_ARTIFACT_URL" ]; then \ - curl -fSL "$KNOWLEDGE_ARTIFACT_URL" -o /app/data/vtk-knowledge.jsonl; \ - fi +ENV VTK_MCP_VTK_VERSION=${VTK_VERSION} -COPY data/ /app/data/ +WORKDIR /app + +# Install uv for fast dependency installation +RUN pip install uv + +# Install vtk-* sibling packages from GitHub (not on PyPI) +RUN uv pip install --system \ + "git+https://github.com/vicentebolea/vtk-knowledge" \ + "git+https://github.com/vicentebolea/vtk-validate" + +# Install vtk-mcp with optional retrieval support +COPY . /app/ +RUN uv pip install --system -e ".[retrieval]" + +# Pre-download the vtk-knowledge JSONL artifact and vtk-index embedded +# Qdrant storage so the image is ready to serve without network access. +RUN python - <<'EOF' +import logging +logging.basicConfig(level=logging.INFO) +import os +vtk_version = os.environ["VTK_MCP_VTK_VERSION"] +from vtk_knowledge import VTKAPIIndex +VTKAPIIndex.from_artifact(vtk_version) +try: + from vtk_index import Retriever + Retriever.from_artifact(vtk_version) +except Exception as e: + logging.warning("vtk-index embedded storage skipped: %s", e) +EOF -ENV VTK_MCP_KNOWLEDGE_ARTIFACT_PATH=/app/data/vtk-knowledge.jsonl -ENV VTK_MCP_VTK_VERSION=${VTK_VERSION} ENV VTK_MCP_TRANSPORT=stdio +ENV VTK_MCP_ENABLE_VALIDATION=true +ENV VTK_MCP_ENABLE_RETRIEVAL=true ENTRYPOINT ["python", "-m", "vtk_mcp"] diff --git a/deploy.Dockerfile b/deploy.Dockerfile index b038ed4..1e776a7 100644 --- a/deploy.Dockerfile +++ b/deploy.Dockerfile @@ -1,46 +1,51 @@ -LABEL org.opencontainers.image.title="VTK MCP Server with Embeddings" -LABEL org.opencontainers.image.description="Model Context Protocol server for VTK with vector search embeddings" -LABEL org.opencontainers.image.source="https://github.com/kitware/vtk-mcp" -LABEL org.opencontainers.image.authors="Vicente Adolfo Bolea Sanchez " -LABEL org.opencontainers.image.licenses="MIT" -LABEL org.opencontainers.image.documentation="https://github.com/kitware/vtk-mcp/blob/main/README.md" - -FROM python:3.12-slim AS embeddings - -# Download embeddings database from GHCR -COPY --from=ghcr.io/kitware/vtk-mcp/embeddings-database:latest /vtk-examples-embeddings.tar.gz /tmp/ - -# Extract the database -RUN mkdir -p /app/db && \ - tar -xzf /tmp/vtk-examples-embeddings.tar.gz -C /app/db && \ - rm /tmp/vtk-examples-embeddings.tar.gz - FROM python:3.12-slim +LABEL org.opencontainers.image.title="VTK MCP Gateway" +LABEL org.opencontainers.image.description="Production MCP gateway for VTK LLM tooling" +LABEL org.opencontainers.image.source="https://github.com/Kitware/vtk-mcp" +LABEL org.opencontainers.image.licenses="MIT" + ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ PIP_NO_CACHE_DIR=1 \ PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 -# Install system dependencies for VTK -RUN apt update && \ - apt install --no-install-recommends --no-install-suggests -y \ - libgl1-mesa-dev \ - libxrender-dev/stable \ - git && \ - rm -rf /var/lib/apt/lists/* +# VTK version to pre-cache at image build time +ARG VTK_VERSION=9.3.0 +ENV VTK_MCP_VTK_VERSION=${VTK_VERSION} WORKDIR /app -# Copy application code -COPY . . - -# Copy embeddings database from first stage -COPY --from=embeddings /app/db /app/db - -# Install Python dependencies (including RAG dependencies) -RUN pip install --upgrade pip && \ - pip install --verbose . - -# Start server with database path configured -CMD ["vtk-mcp-server", "--transport", "http", "--host", "0.0.0.0", "--port", "8000", "--database-path", "/app/db/vtk-examples"] +# Install uv for fast dependency installation +RUN pip install uv + +# Install vtk-* sibling packages from GitHub (not on PyPI) +RUN uv pip install --system \ + "git+https://github.com/vicentebolea/vtk-knowledge" \ + "git+https://github.com/vicentebolea/vtk-validate" + +# Install vtk-mcp with optional retrieval support +COPY . /app/ +RUN uv pip install --system -e ".[retrieval]" + +# Pre-download the vtk-knowledge JSONL artifact and vtk-index embedded +# Qdrant storage so the image is ready to serve without network access. +RUN python - <<'EOF' +import logging +logging.basicConfig(level=logging.INFO) +import os +vtk_version = os.environ["VTK_MCP_VTK_VERSION"] +from vtk_knowledge import VTKAPIIndex +VTKAPIIndex.from_artifact(vtk_version) +try: + from vtk_index import Retriever + Retriever.from_artifact(vtk_version) +except Exception as e: + logging.warning("vtk-index embedded storage skipped: %s", e) +EOF + +ENV VTK_MCP_TRANSPORT=stdio +ENV VTK_MCP_ENABLE_VALIDATION=true +ENV VTK_MCP_ENABLE_RETRIEVAL=true + +ENTRYPOINT ["python", "-m", "vtk_mcp"] diff --git a/pyproject.toml b/pyproject.toml index 6aa4b3d..4f567bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [project] name = "vtk-mcp" version = "1.0.0" @@ -37,31 +41,34 @@ vtk-mcp = "vtk_mcp.__main__:main" retrieval = [ "vtk-index>=1.0.0", ] -test = [ +dev = [ "pytest>=7.0.0", "pytest-asyncio>=0.21.0", "pytest-mock>=3.10.0", "responses>=0.23.0", "httpx>=0.24.0", + "ruff>=0.8.0", ] [project.urls] -Homepage = "https://github.com/kitware/vtk-mcp" -Repository = "https://github.com/kitware/vtk-mcp" -Issues = "https://github.com/kitware/vtk-mcp/issues" +Homepage = "https://github.com/Kitware/vtk-mcp" +Repository = "https://github.com/Kitware/vtk-mcp" +Issues = "https://github.com/Kitware/vtk-mcp/issues" -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" +[tool.hatch.build.targets.wheel] +packages = ["src/vtk_mcp"] -[tool.setuptools.packages.find] -where = ["src"] +[tool.uv.sources] +vtk-knowledge = { git = "https://github.com/vicentebolea/vtk-knowledge" } +vtk-validate = { git = "https://github.com/vicentebolea/vtk-validate" } +vtk-index = { git = "https://github.com/vicentebolea/vtk-index" } [tool.pytest.ini_options] testpaths = ["tests"] -python_files = "test_*.py" -python_classes = "Test*" -python_functions = "test_*" +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +pythonpath = ["src"] addopts = [ "-v", "--tb=short", @@ -79,3 +86,14 @@ filterwarnings = [ "ignore::DeprecationWarning", "ignore::PendingDeprecationWarning", ] + +[tool.ruff] +line-length = 120 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "I", "W"] +ignore = ["E501"] + +[tool.ruff.lint.isort] +known-first-party = ["vtk_mcp"] diff --git a/src/vtk_mcp/__main__.py b/src/vtk_mcp/__main__.py index b5bdfd0..676c09d 100644 --- a/src/vtk_mcp/__main__.py +++ b/src/vtk_mcp/__main__.py @@ -17,16 +17,31 @@ "--knowledge-artifact", envvar="VTK_MCP_KNOWLEDGE_ARTIFACT_PATH", type=click.Path(exists=True), - help="Path to the vtk-knowledge JSONL artifact.", + help="Path to a local vtk-knowledge JSONL artifact (skips auto-download).", ) -def main(transport: str, host: str, port: int, knowledge_artifact: str | None) -> None: +@click.option( + "--vtk-version", + envvar="VTK_MCP_VTK_VERSION", + default="9.3.0", + show_default=True, + help="VTK version to fetch from ghcr.io when no local artifact is given.", +) +def main( + transport: str, + host: str, + port: int, + knowledge_artifact: str | None, + vtk_version: str, +) -> None: """Run the VTK MCP gateway server.""" import os - from .config import Settings + from .composition import init_context + from .config import Settings if knowledge_artifact: os.environ["VTK_MCP_KNOWLEDGE_ARTIFACT_PATH"] = knowledge_artifact + os.environ.setdefault("VTK_MCP_VTK_VERSION", vtk_version) settings = Settings() init_context(settings) @@ -34,9 +49,11 @@ def main(transport: str, host: str, port: int, knowledge_artifact: str | None) - if transport == "http": click.echo(f"Starting vtk-mcp on http://{host}:{port}") from .transport.http import run + run(host=host, port=port) else: from .transport.stdio import run + run() diff --git a/src/vtk_mcp/composition.py b/src/vtk_mcp/composition.py index 069648c..4a1a8f9 100644 --- a/src/vtk_mcp/composition.py +++ b/src/vtk_mcp/composition.py @@ -24,14 +24,19 @@ class VTKMCPContext: api_index: VTKAPIIndex retriever: object # vtk_index.Retriever | None - validate: object # callable(source: str) -> ValidationReport | None + validate: object # callable(source: str) -> ValidationReport | None settings: Settings def __init__(self, settings: Settings) -> None: self.settings = settings - # Layer 1: knowledge index - logger.info("Loading knowledge artifact from %s", settings.knowledge_artifact_path) - self.api_index = VTKAPIIndex.from_jsonl(settings.knowledge_artifact_path) + + # Layer 1: knowledge index — download artifact if no local path given + if settings.knowledge_artifact_path is not None: + logger.info("Loading knowledge artifact from %s", settings.knowledge_artifact_path) + self.api_index = VTKAPIIndex.from_jsonl(settings.knowledge_artifact_path) + else: + logger.info("Downloading knowledge artifact for VTK %s", settings.vtk_version) + self.api_index = VTKAPIIndex.from_artifact(settings.vtk_version) logger.info( "Loaded %d classes (vtk_version=%s)", len(self.api_index.classes), @@ -44,11 +49,21 @@ def __init__(self, settings: Settings) -> None: try: from vtk_index import Retriever - self.retriever = Retriever( - qdrant_url=settings.qdrant_url, - vtk_version=self.api_index.vtk_version, - ) - logger.info("Retriever connected to %s", settings.qdrant_url) + if settings.qdrant_url: + # Connect to a running Qdrant server + self.retriever = Retriever( + qdrant_url=settings.qdrant_url, + vtk_version=self.api_index.vtk_version, + ) + logger.info("Retriever connected to %s", settings.qdrant_url) + else: + # Download pre-built embedded storage (no server required) + logger.info( + "Downloading embedded Qdrant storage for VTK %s", + self.api_index.vtk_version, + ) + self.retriever = Retriever.from_artifact(self.api_index.vtk_version) + logger.info("Retriever ready (embedded storage)") except Exception as exc: logger.warning("Retrieval disabled: %s", exc) diff --git a/src/vtk_mcp/config.py b/src/vtk_mcp/config.py index a65c67b..9f7682b 100644 --- a/src/vtk_mcp/config.py +++ b/src/vtk_mcp/config.py @@ -5,17 +5,23 @@ """ from pathlib import Path +from typing import Optional + from pydantic_settings import BaseSettings class Settings(BaseSettings): # Layer 1 — knowledge artifact - knowledge_artifact_path: Path = Path("/app/data/vtk-knowledge.jsonl") + # When None, the artifact is downloaded automatically via VTKAPIIndex.from_artifact(). + knowledge_artifact_path: Optional[Path] = None vtk_version: str = "9.3.0" # Layer 2 — retrieval - enable_retrieval: bool = True - qdrant_url: str = "http://qdrant:6333" + # When enable_retrieval is True and vtk_version is set, uses Retriever.from_artifact() + # which downloads pre-built embedded Qdrant storage (no server required). + # Set qdrant_url to a real Qdrant instance to use a server instead. + enable_retrieval: bool = False + qdrant_url: Optional[str] = None # Layer 3 — validation enable_validation: bool = True diff --git a/src/vtk_mcp/server.py b/src/vtk_mcp/server.py index f9cc004..5817a04 100644 --- a/src/vtk_mcp/server.py +++ b/src/vtk_mcp/server.py @@ -19,15 +19,18 @@ def _ctx() -> VTKMCPContext: from .composition import get_context + return get_context() # ── Layer 1: Documentation lookup ───────────────────────────────────────── + @mcp.tool() def get_vtk_class_info_python(class_name: str) -> dict: """Get Python API info for a VTK class (module, methods, synopsis, role, etc.).""" from .tools.docs import get_vtk_class_info_python as _f + return _f(class_name, _ctx()) @@ -35,6 +38,7 @@ def get_vtk_class_info_python(class_name: str) -> dict: def vtk_search_classes(query: str, limit: int = 10) -> list: """Search for VTK classes by name or keyword.""" from .tools.docs import vtk_search_classes as _f + return _f(query, _ctx(), limit=limit) @@ -42,6 +46,7 @@ def vtk_search_classes(query: str, limit: int = 10) -> list: def vtk_get_class_doc(class_name: str) -> str: """Get the docstring for a VTK class.""" from .tools.docs import vtk_get_class_doc as _f + return _f(class_name, _ctx()) @@ -49,6 +54,7 @@ def vtk_get_class_doc(class_name: str) -> str: def vtk_get_class_synopsis(class_name: str) -> str: """Get a one-sentence synopsis for a VTK class.""" from .tools.docs import vtk_get_class_synopsis as _f + return _f(class_name, _ctx()) @@ -56,6 +62,7 @@ def vtk_get_class_synopsis(class_name: str) -> str: def vtk_get_class_role(class_name: str) -> str: """Get the pipeline role of a VTK class (source, filter, mapper, output, etc.).""" from .tools.docs import vtk_get_class_role as _f + return _f(class_name, _ctx()) @@ -63,6 +70,7 @@ def vtk_get_class_role(class_name: str) -> str: def vtk_get_class_input_datatype(class_name: str) -> str: """Get the input data type expected by a VTK class.""" from .tools.docs import vtk_get_class_input_datatype as _f + return _f(class_name, _ctx()) @@ -70,6 +78,7 @@ def vtk_get_class_input_datatype(class_name: str) -> str: def vtk_get_class_output_datatype(class_name: str) -> str: """Get the output data type produced by a VTK class.""" from .tools.docs import vtk_get_class_output_datatype as _f + return _f(class_name, _ctx()) @@ -77,6 +86,7 @@ def vtk_get_class_output_datatype(class_name: str) -> str: def vtk_get_class_methods(class_name: str) -> list: """List all methods (with signatures) for a VTK class.""" from .tools.docs import vtk_get_class_methods as _f + return _f(class_name, _ctx()) @@ -84,6 +94,7 @@ def vtk_get_class_methods(class_name: str) -> list: def vtk_get_class_semantic_methods(class_name: str) -> list: """List non-boilerplate callable methods for a VTK class.""" from .tools.docs import vtk_get_class_semantic_methods as _f + return _f(class_name, _ctx()) @@ -91,6 +102,7 @@ def vtk_get_class_semantic_methods(class_name: str) -> list: def vtk_get_method_info(class_name: str, method_name: str) -> dict: """Get documentation for a specific method of a VTK class.""" from .tools.docs import vtk_get_method_info as _f + return _f(class_name, method_name, _ctx()) @@ -98,6 +110,7 @@ def vtk_get_method_info(class_name: str, method_name: str) -> dict: def vtk_get_method_doc(class_name: str, method_name: str) -> str: """Get the docstring for a specific method of a VTK class.""" from .tools.docs import vtk_get_method_doc as _f + return _f(class_name, method_name, _ctx()) @@ -105,6 +118,7 @@ def vtk_get_method_doc(class_name: str, method_name: str) -> str: def vtk_get_method_signature(class_name: str, method_name: str) -> str: """Get the canonical signature for a specific method of a VTK class.""" from .tools.docs import vtk_get_method_signature as _f + return _f(class_name, method_name, _ctx()) @@ -112,6 +126,7 @@ def vtk_get_method_signature(class_name: str, method_name: str) -> str: def vtk_get_class_module(class_name: str) -> str: """Get the vtkmodules.* import path for a VTK class.""" from .tools.docs import vtk_get_class_module as _f + return _f(class_name, _ctx()) @@ -119,6 +134,7 @@ def vtk_get_class_module(class_name: str) -> str: def vtk_get_module_classes(module: str) -> list: """List all VTK classes in a specific module.""" from .tools.docs import vtk_get_module_classes as _f + return _f(module, _ctx()) @@ -126,6 +142,7 @@ def vtk_get_module_classes(module: str) -> list: def vtk_is_a_class(class_name: str) -> bool: """Check if a name is a valid VTK class.""" from .tools.docs import vtk_is_a_class as _f + return _f(class_name, _ctx()) @@ -133,6 +150,7 @@ def vtk_is_a_class(class_name: str) -> bool: def vtk_get_class_action_phrase(class_name: str) -> str: """Get the action phrase for a VTK class (e.g. 'mesh smoothing').""" from .tools.docs import vtk_get_class_action_phrase as _f + return _f(class_name, _ctx()) @@ -140,15 +158,18 @@ def vtk_get_class_action_phrase(class_name: str) -> str: def vtk_get_class_visibility(class_name: str) -> float | None: """Get the visibility score (0.0–1.0) for a VTK class.""" from .tools.docs import vtk_get_class_visibility as _f + return _f(class_name, _ctx()) # ── Layer 2: Retrieval ───────────────────────────────────────────────────── + @mcp.tool() def vector_search_docs(query: str, k: int = 10) -> list: """Hybrid semantic search over VTK documentation chunks.""" from .tools.search import vector_search_docs as _f + return _f(query, _ctx(), k=k) @@ -156,11 +177,13 @@ def vector_search_docs(query: str, k: int = 10) -> list: def vector_search_examples(query: str, k: int = 10) -> list: """Hybrid semantic search over VTK code example chunks.""" from .tools.search import vector_search_examples as _f + return _f(query, _ctx(), k=k) # ── Layer 3: Validation ──────────────────────────────────────────────────── + @mcp.tool() def validate_vtk_code(source: str) -> dict: """Validate a Python source string against the VTK API. @@ -168,6 +191,7 @@ def validate_vtk_code(source: str) -> dict: Returns a ValidationReport with status and diagnostics. """ from .tools.validation import validate_vtk_code as _f + return _f(source, _ctx()) @@ -175,15 +199,18 @@ def validate_vtk_code(source: str) -> dict: def vtk_validate_import(import_statement: str) -> dict: """Validate a VTK import statement and suggest corrections.""" from .tools.validation import vtk_validate_import as _f + return _f(import_statement, _ctx()) # ── C++ scraping (self-contained, no layer dependency) ───────────────────── + @mcp.tool() def get_vtk_class_info_cpp(class_name: str) -> str: """Get detailed information about a VTK class from the online C++ docs.""" from .tools.scraping import get_vtk_class_info_cpp as _f + return _f(class_name, _ctx()) @@ -191,11 +218,13 @@ def get_vtk_class_info_cpp(class_name: str) -> str: def search_vtk_classes_cpp(search_term: str) -> str: """Search for VTK classes in the C++ documentation.""" from .tools.scraping import search_vtk_classes_cpp as _f + return _f(search_term, _ctx()) # ── Meta ─────────────────────────────────────────────────────────────────── + @mcp.tool() def vtk_version_info() -> dict: """Return the VTK version loaded by this gateway instance.""" diff --git a/src/vtk_mcp/tools/docs.py b/src/vtk_mcp/tools/docs.py index e46b98a..3fae43a 100644 --- a/src/vtk_mcp/tools/docs.py +++ b/src/vtk_mcp/tools/docs.py @@ -11,84 +11,101 @@ def get_vtk_class_info_python(class_name: str, ctx: "VTKMCPContext") -> dict: """Get Python API info for a VTK class from the knowledge index.""" from vtk_validate.tools import vtk_get_class_info + return vtk_get_class_info(class_name, ctx.api_index) def vtk_search_classes(query: str, ctx: "VTKMCPContext", limit: int = 10) -> list: from vtk_validate.tools import vtk_search_classes as _search + return _search(query, ctx.api_index, limit=limit) def vtk_get_class_doc(class_name: str, ctx: "VTKMCPContext") -> str: from vtk_validate.tools import vtk_get_class_doc as _f + return _f(class_name, ctx.api_index) def vtk_get_class_synopsis(class_name: str, ctx: "VTKMCPContext") -> str: from vtk_validate.tools import vtk_get_class_synopsis as _f + return _f(class_name, ctx.api_index) def vtk_get_class_role(class_name: str, ctx: "VTKMCPContext") -> str: from vtk_validate.tools import vtk_get_class_role as _f + return _f(class_name, ctx.api_index) def vtk_get_class_input_datatype(class_name: str, ctx: "VTKMCPContext") -> str: from vtk_validate.tools import vtk_get_class_input_datatype as _f + return _f(class_name, ctx.api_index) def vtk_get_class_output_datatype(class_name: str, ctx: "VTKMCPContext") -> str: from vtk_validate.tools import vtk_get_class_output_datatype as _f + return _f(class_name, ctx.api_index) def vtk_get_class_methods(class_name: str, ctx: "VTKMCPContext") -> list: from vtk_validate.tools import vtk_get_class_methods as _f + return _f(class_name, ctx.api_index) def vtk_get_class_semantic_methods(class_name: str, ctx: "VTKMCPContext") -> list: from vtk_validate.tools import vtk_get_class_semantic_methods as _f + return _f(class_name, ctx.api_index) def vtk_get_method_info(class_name: str, method_name: str, ctx: "VTKMCPContext") -> dict: from vtk_validate.tools import vtk_get_method_info as _f + return _f(class_name, method_name, ctx.api_index) def vtk_get_method_doc(class_name: str, method_name: str, ctx: "VTKMCPContext") -> str: from vtk_validate.tools import vtk_get_method_doc as _f + return _f(class_name, method_name, ctx.api_index) def vtk_get_method_signature(class_name: str, method_name: str, ctx: "VTKMCPContext") -> str: from vtk_validate.tools import vtk_get_method_signature as _f + return _f(class_name, method_name, ctx.api_index) def vtk_get_class_module(class_name: str, ctx: "VTKMCPContext") -> str: from vtk_validate.tools import vtk_get_class_module as _f + return _f(class_name, ctx.api_index) def vtk_get_module_classes(module: str, ctx: "VTKMCPContext") -> list: from vtk_validate.tools import vtk_get_module_classes as _f + return _f(module, ctx.api_index) def vtk_is_a_class(class_name: str, ctx: "VTKMCPContext") -> bool: from vtk_validate.tools import vtk_is_a_class as _f + return _f(class_name, ctx.api_index) def vtk_get_class_action_phrase(class_name: str, ctx: "VTKMCPContext") -> str: from vtk_validate.tools import vtk_get_class_action_phrase as _f + return _f(class_name, ctx.api_index) def vtk_get_class_visibility(class_name: str, ctx: "VTKMCPContext"): from vtk_validate.tools import vtk_get_class_visibility as _f + return _f(class_name, ctx.api_index) diff --git a/src/vtk_mcp/tools/scraping.py b/src/vtk_mcp/tools/scraping.py index d5c75ed..73e7085 100644 --- a/src/vtk_mcp/tools/scraping.py +++ b/src/vtk_mcp/tools/scraping.py @@ -17,6 +17,7 @@ def get_vtk_class_info_cpp(class_name: str, ctx: "VTKMCPContext") -> str: if not ctx.settings.enable_cpp_scraping: return "C++ scraping disabled (VTK_MCP_ENABLE_CPP_SCRAPING=false)." from .vtk_scraper import VTKClassScraper + scraper = VTKClassScraper() info = scraper.get_class_info(class_name) if info is None: @@ -29,6 +30,7 @@ def search_vtk_classes_cpp(search_term: str, ctx: "VTKMCPContext") -> str: if not ctx.settings.enable_cpp_scraping: return "C++ scraping disabled (VTK_MCP_ENABLE_CPP_SCRAPING=false)." from .vtk_scraper import VTKClassScraper + scraper = VTKClassScraper() matches = scraper.search_classes(search_term) if not matches: diff --git a/src/vtk_mcp/tools/validation.py b/src/vtk_mcp/tools/validation.py index bd15285..07c56a5 100644 --- a/src/vtk_mcp/tools/validation.py +++ b/src/vtk_mcp/tools/validation.py @@ -21,4 +21,5 @@ def validate_vtk_code(source: str, ctx: "VTKMCPContext") -> dict[str, Any]: def vtk_validate_import(import_statement: str, ctx: "VTKMCPContext") -> dict[str, Any]: from vtk_validate.tools import vtk_validate_import as _f + return _f(import_statement, ctx.api_index) diff --git a/src/vtk_mcp/tools/vtk_scraper.py b/src/vtk_mcp/tools/vtk_scraper.py index db369c5..c27ea8e 100644 --- a/src/vtk_mcp/tools/vtk_scraper.py +++ b/src/vtk_mcp/tools/vtk_scraper.py @@ -1,4 +1,5 @@ import re + import requests from bs4 import BeautifulSoup @@ -57,9 +58,7 @@ def _parse_class_page(self, soup, class_name): descriptions.append(text) if descriptions: - info["detailed_description"] = " ".join( - descriptions[:2] - ) # Take first 2 meaningful blocks + info["detailed_description"] = " ".join(descriptions[:2]) # Take first 2 meaningful blocks # Get inheritance information from inheritance diagram or class hierarchy inheritance_links = soup.find_all("a", href=re.compile(r"class.*\.html")) @@ -92,9 +91,7 @@ def _parse_class_page(self, soup, class_name): # Approach 2: Look for method definition lists if not methods: - method_sections = soup.find_all( - ["h2", "h3"], string=re.compile(r"Member Function|Public.*Function") - ) + method_sections = soup.find_all(["h2", "h3"], string=re.compile(r"Member Function|Public.*Function")) for section in method_sections: next_elem = section.find_next_sibling() while next_elem and next_elem.name not in ["h1", "h2", "h3"]: @@ -104,8 +101,7 @@ def _parse_class_page(self, soup, class_name): for link in method_links: method_name = link.get_text(strip=True) if method_name and not any( - x in method_name.lower() - for x in ["class", "struct", "enum"] + x in method_name.lower() for x in ["class", "struct", "enum"] ): methods.append( { @@ -123,10 +119,7 @@ def _parse_class_page(self, soup, class_name): if ( method_name and len(method_name) > 2 - and not any( - x in method_name.lower() - for x in ["class", "struct", "enum", "typedef"] - ) + and not any(x in method_name.lower() for x in ["class", "struct", "enum", "typedef"]) ): # Try to get context for the method parent = link.find_parent(["td", "div", "span"]) @@ -134,9 +127,7 @@ def _parse_class_page(self, soup, class_name): methods.append( { "name": method_name, - "description": ( - context[:200] if context else f"Method: {method_name}" - ), + "description": (context[:200] if context else f"Method: {method_name}"), } ) diff --git a/src/vtk_mcp/transport/http.py b/src/vtk_mcp/transport/http.py index 9df8e8f..cd7eaf0 100644 --- a/src/vtk_mcp/transport/http.py +++ b/src/vtk_mcp/transport/http.py @@ -7,4 +7,5 @@ def run(host: str = "0.0.0.0", port: int = 8000) -> None: from ..server import mcp + asyncio.run(mcp.run_http_async(host=host, port=port)) diff --git a/src/vtk_mcp/transport/stdio.py b/src/vtk_mcp/transport/stdio.py index 9442ed1..01f2235 100644 --- a/src/vtk_mcp/transport/stdio.py +++ b/src/vtk_mcp/transport/stdio.py @@ -5,4 +5,5 @@ def run() -> None: from ..server import mcp + mcp.run() diff --git a/tests/test_server_functions.py b/tests/test_server_functions.py index 8675819..2739732 100644 --- a/tests/test_server_functions.py +++ b/tests/test_server_functions.py @@ -173,9 +173,12 @@ def test_get_vtk_class_info_cpp_empty_class_name(self): class TestServerFunctionsPython: """Test the Python API documentation function.""" + @pytest.mark.skipif( + __import__("importlib.util", fromlist=["find_spec"]).find_spec("vtk") is None, + reason="VTK Python package not installed", + ) def test_get_vtk_class_info_python_success(self): """Test successful Python class info retrieval.""" - # Since VTK is actually installed, let's test with real VTK result = get_vtk_class_info_python_func("vtkSphere") # Should return formatted Python API documentation