Skip to content

Commit 0bc1fb9

Browse files
committed
Refactor as composition root (Layer 4)
- Add src/vtk_mcp/ package: config, composition, server, tools/, transport/ - VTKMCPContext wires vtk-knowledge + vtk-validate + vtk-index at startup - Pydantic Settings with VTK_MCP_ env prefix - 25 FastMCP tool registrations delegating to tools/ modules - Dockerfile and docker-compose.yml for production deployment - Remove chromadb, sys.path hack, live help() introspection, per-request model loading - vtk-mcp-server package kept for C++ scraper backwards compatibility
1 parent 7c7b78a commit 0bc1fb9

19 files changed

Lines changed: 940 additions & 14 deletions

Dockerfile

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
FROM python:3.11-slim
2+
3+
LABEL org.opencontainers.image.title="VTK MCP Gateway"
4+
LABEL org.opencontainers.image.description="Production MCP gateway for VTK LLM tooling"
5+
LABEL org.opencontainers.image.source="https://github.com/kitware/vtk-mcp"
6+
LABEL org.opencontainers.image.licenses="MIT"
7+
8+
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
9+
PIP_NO_CACHE_DIR=1 \
10+
PYTHONDONTWRITEBYTECODE=1 \
11+
PYTHONUNBUFFERED=1
12+
13+
WORKDIR /app
14+
15+
# Install runtime dependencies — no VTK runtime, no LiteLLM
16+
RUN pip install vtk-knowledge vtk-validate vtk-mcp
17+
18+
# Bundle the knowledge artifact (built separately and passed at build time)
19+
ARG VTK_VERSION=9.3.0
20+
ARG KNOWLEDGE_ARTIFACT_URL=""
21+
RUN if [ -n "$KNOWLEDGE_ARTIFACT_URL" ]; then \
22+
curl -fSL "$KNOWLEDGE_ARTIFACT_URL" -o /app/data/vtk-knowledge.jsonl; \
23+
fi
24+
25+
COPY data/ /app/data/
26+
27+
ENV VTK_MCP_KNOWLEDGE_ARTIFACT_PATH=/app/data/vtk-knowledge.jsonl
28+
ENV VTK_MCP_VTK_VERSION=${VTK_VERSION}
29+
ENV VTK_MCP_TRANSPORT=stdio
30+
31+
ENTRYPOINT ["python", "-m", "vtk_mcp"]

TOOLS.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# vtk-mcp — MCP Tool Reference
2+
3+
25 tools in 5 groups. Each group lists the delegating library.
4+
5+
---
6+
7+
## Class discovery — `vtk-knowledge`
8+
9+
Find classes by name, keyword, or module membership.
10+
11+
| Tool | What it does |
12+
|---|---|
13+
| `vtk_is_a_class(class_name)` | Returns true if the name is a known VTK class |
14+
| `vtk_search_classes(query, limit)` | Substring search across all class names |
15+
| `vtk_get_class_module(class_name)` | Returns the `vtkmodules.*` import path |
16+
| `vtk_get_module_classes(module)` | Lists all classes in a given module |
17+
18+
---
19+
20+
## Class documentation — `vtk-knowledge`
21+
22+
Retrieve metadata about a specific class.
23+
24+
| Tool | What it does |
25+
|---|---|
26+
| `get_vtk_class_info_python(class_name)` | Full record: module, methods, role, datatypes, synopsis |
27+
| `vtk_get_class_doc(class_name)` | Raw class docstring |
28+
| `vtk_get_class_synopsis(class_name)` | One-sentence LLM-generated summary |
29+
| `vtk_get_class_action_phrase(class_name)` | Short noun-phrase for the class's primary action |
30+
| `vtk_get_class_role(class_name)` | Pipeline role: source, filter, mapper, output, utility, etc. |
31+
| `vtk_get_class_input_datatype(class_name)` | Expected input data type (e.g. `vtkPolyData`) |
32+
| `vtk_get_class_output_datatype(class_name)` | Produced output data type |
33+
| `vtk_get_class_visibility(class_name)` | Score 0.0–1.0 for how often this class is used directly |
34+
| `vtk_get_class_methods(class_name)` | All methods with signatures |
35+
| `vtk_get_class_semantic_methods(class_name)` | Non-boilerplate methods only |
36+
37+
---
38+
39+
## Method documentation — `vtk-knowledge`
40+
41+
Drill into a specific method on a specific class.
42+
43+
| Tool | What it does |
44+
|---|---|
45+
| `vtk_get_method_info(class_name, method_name)` | Full method record: signatures + docstring |
46+
| `vtk_get_method_doc(class_name, method_name)` | Docstring only |
47+
| `vtk_get_method_signature(class_name, method_name)` | Canonical signature string only |
48+
49+
---
50+
51+
## Validation — `vtk-validate`
52+
53+
Check VTK Python code or imports for API mistakes.
54+
55+
| Tool | What it does |
56+
|---|---|
57+
| `validate_vtk_code(source)` | Full AST check: imports, constructors, methods, ordering, security — returns a `ValidationReport` |
58+
| `vtk_validate_import(import_statement)` | Validates a single import line and suggests the correct module |
59+
60+
---
61+
62+
## Semantic search — `vtk-index` (Qdrant)
63+
64+
Retrieve relevant documentation or code by meaning, not by exact name.
65+
66+
| Tool | What it does |
67+
|---|---|
68+
| `vector_search_docs(query, k)` | Hybrid dense+BM25 search over documentation chunks |
69+
| `vector_search_examples(query, k)` | Hybrid dense+BM25 search over VTK code example chunks |
70+
71+
---
72+
73+
## C++ documentation — vtk.org scraper (self-contained)
74+
75+
Live HTML scraping of the VTK C++ API docs. No offline artifact required.
76+
77+
| Tool | What it does |
78+
|---|---|
79+
| `get_vtk_class_info_cpp(class_name)` | Fetches class info from vtk.org C++ docs |
80+
| `search_vtk_classes_cpp(search_term)` | Searches class names in the C++ docs |
81+
82+
---
83+
84+
## Meta — `vtk-mcp` itself
85+
86+
| Tool | What it does |
87+
|---|---|
88+
| `vtk_version_info()` | Returns the loaded VTK version, class count, and which capabilities are enabled |

data/.gitkeep

Whitespace-only changes.

docker-compose.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
services:
2+
qdrant:
3+
image: qdrant/qdrant:latest
4+
ports:
5+
- "6333:6333"
6+
volumes:
7+
- qdrant_data:/qdrant/storage
8+
9+
vtk-mcp:
10+
build: .
11+
environment:
12+
VTK_MCP_TRANSPORT: http
13+
VTK_MCP_HTTP_PORT: "8000"
14+
VTK_MCP_ENABLE_RETRIEVAL: "true"
15+
VTK_MCP_QDRANT_URL: http://qdrant:6333
16+
VTK_MCP_KNOWLEDGE_ARTIFACT_PATH: /app/data/vtk-knowledge.jsonl
17+
ports:
18+
- "8000:8000"
19+
depends_on:
20+
- qdrant
21+
volumes:
22+
- ./data:/app/data:ro
23+
24+
volumes:
25+
qdrant_data:

pyproject.toml

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
[project]
2-
name = "vtk-mcp-server"
3-
dynamic = ["version"]
4-
description = "MCP server for VTK class documentation"
5-
authors = [{name = "Vicente Adolfo Bolea Sanchez", email = "vicente.bolea@kitware.com"}]
2+
name = "vtk-mcp"
3+
version = "1.0.0"
4+
description = "Production MCP gateway composing vtk-knowledge, vtk-index, and vtk-validate"
5+
authors = [{name = "Patrick O'Leary"}, {name = "Vicente Adolfo Bolea Sanchez", email = "vicente.bolea@kitware.com"}]
66
license = {text = "MIT"}
77
readme = "README.md"
88
requires-python = ">=3.10"
@@ -20,21 +20,23 @@ classifiers = [
2020
]
2121
dependencies = [
2222
"fastmcp>=2.0.0",
23+
"pydantic>=2.0.0",
24+
"pydantic-settings>=2.0.0",
2325
"beautifulsoup4>=4.12.0",
24-
"importlib_resources>=5.0.0",
2526
"requests>=2.31.0",
2627
"lxml>=4.9.0",
2728
"click>=8.0.0",
28-
"chromadb==0.6.3",
29-
"sentence-transformers==3.4.1",
30-
"vtk",
29+
"vtk-knowledge>=1.0.0",
30+
"vtk-validate>=1.0.0",
3131
]
3232

3333
[project.scripts]
34-
vtk-mcp-server = "vtk_mcp_server.server:main"
35-
vtk-mcp-client = "vtk_mcp_server.simple_client:main"
34+
vtk-mcp = "vtk_mcp.__main__:main"
3635

3736
[project.optional-dependencies]
37+
retrieval = [
38+
"vtk-index>=1.0.0",
39+
]
3840
test = [
3941
"pytest>=7.0.0",
4042
"pytest-asyncio>=0.21.0",
@@ -45,16 +47,15 @@ test = [
4547

4648
[project.urls]
4749
Homepage = "https://github.com/kitware/vtk-mcp"
48-
Documentation = "https://github.com/kitware/vtk-mcp/blob/master/README.md"
4950
Repository = "https://github.com/kitware/vtk-mcp"
5051
Issues = "https://github.com/kitware/vtk-mcp/issues"
5152

5253
[build-system]
53-
requires = ["setuptools>=61.0", "setuptools-scm>=8.0"]
54+
requires = ["setuptools>=61.0"]
5455
build-backend = "setuptools.build_meta"
5556

56-
[tool.setuptools_scm]
57-
fallback_version = "0.1.0"
57+
[tool.setuptools.packages.find]
58+
where = ["src"]
5859

5960
[tool.pytest.ini_options]
6061
testpaths = ["tests"]

src/vtk_mcp/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""vtk-mcp — production MCP gateway composing vtk-knowledge, vtk-index, and vtk-validate."""
2+
3+
from importlib.metadata import PackageNotFoundError, version
4+
5+
try:
6+
__version__ = version("vtk-mcp")
7+
except PackageNotFoundError:
8+
__version__ = "unknown"

src/vtk_mcp/__main__.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""vtk-mcp entry point: ``python -m vtk_mcp``."""
2+
3+
import click
4+
5+
6+
@click.command()
7+
@click.option(
8+
"--transport",
9+
type=click.Choice(["stdio", "http"]),
10+
default="stdio",
11+
show_default=True,
12+
help="Transport protocol.",
13+
)
14+
@click.option("--host", default="0.0.0.0", show_default=True, help="HTTP host.")
15+
@click.option("--port", default=8000, show_default=True, help="HTTP port.")
16+
@click.option(
17+
"--knowledge-artifact",
18+
envvar="VTK_MCP_KNOWLEDGE_ARTIFACT_PATH",
19+
type=click.Path(exists=True),
20+
help="Path to the vtk-knowledge JSONL artifact.",
21+
)
22+
def main(transport: str, host: str, port: int, knowledge_artifact: str | None) -> None:
23+
"""Run the VTK MCP gateway server."""
24+
import os
25+
from .config import Settings
26+
from .composition import init_context
27+
28+
if knowledge_artifact:
29+
os.environ["VTK_MCP_KNOWLEDGE_ARTIFACT_PATH"] = knowledge_artifact
30+
31+
settings = Settings()
32+
init_context(settings)
33+
34+
if transport == "http":
35+
click.echo(f"Starting vtk-mcp on http://{host}:{port}")
36+
from .transport.http import run
37+
run(host=host, port=port)
38+
else:
39+
from .transport.stdio import run
40+
run()
41+
42+
43+
if __name__ == "__main__":
44+
main()

src/vtk_mcp/composition.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Composition root — wires layers 1–3 at startup.
2+
3+
All layer dependencies are constructed exactly once here and held as
4+
singletons on VTKMCPContext. Tool handlers receive a context instance
5+
rather than constructing their own dependencies.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import logging
11+
12+
from vtk_knowledge import VTKAPIIndex
13+
14+
from .config import Settings
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
class VTKMCPContext:
20+
"""Holds all layer-1/2/3 dependencies as singletons.
21+
22+
Constructed once at gateway startup; injected into every tool handler.
23+
"""
24+
25+
api_index: VTKAPIIndex
26+
retriever: object # vtk_index.Retriever | None
27+
validate: object # callable(source: str) -> ValidationReport | None
28+
settings: Settings
29+
30+
def __init__(self, settings: Settings) -> None:
31+
self.settings = settings
32+
# Layer 1: knowledge index
33+
logger.info("Loading knowledge artifact from %s", settings.knowledge_artifact_path)
34+
self.api_index = VTKAPIIndex.from_jsonl(settings.knowledge_artifact_path)
35+
logger.info(
36+
"Loaded %d classes (vtk_version=%s)",
37+
len(self.api_index.classes),
38+
self.api_index.vtk_version,
39+
)
40+
41+
# Layer 2: retrieval (optional)
42+
self.retriever = None
43+
if settings.enable_retrieval:
44+
try:
45+
from vtk_index import Retriever
46+
47+
self.retriever = Retriever(
48+
qdrant_url=settings.qdrant_url,
49+
vtk_version=self.api_index.vtk_version,
50+
)
51+
logger.info("Retriever connected to %s", settings.qdrant_url)
52+
except Exception as exc:
53+
logger.warning("Retrieval disabled: %s", exc)
54+
55+
# Layer 3: validation (optional — library call, no subprocess)
56+
self.validate = None
57+
if settings.enable_validation:
58+
try:
59+
from vtk_validate import validate as _validate
60+
61+
_index = self.api_index
62+
63+
def _validate_bound(source: str):
64+
return _validate(source, _index)
65+
66+
self.validate = _validate_bound
67+
logger.info("Validation enabled")
68+
except Exception as exc:
69+
logger.warning("Validation disabled: %s", exc)
70+
71+
72+
_context: VTKMCPContext | None = None
73+
74+
75+
def get_context() -> VTKMCPContext:
76+
if _context is None:
77+
raise RuntimeError("VTKMCPContext not initialised. Call init_context() first.")
78+
return _context
79+
80+
81+
def init_context(settings: Settings | None = None) -> VTKMCPContext:
82+
global _context
83+
if settings is None:
84+
settings = Settings()
85+
_context = VTKMCPContext(settings)
86+
return _context

src/vtk_mcp/config.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Pydantic Settings for the vtk-mcp gateway.
2+
3+
All parameters are configured via environment variables prefixed with
4+
``VTK_MCP_``. For example: ``VTK_MCP_TRANSPORT=http``.
5+
"""
6+
7+
from pathlib import Path
8+
from pydantic_settings import BaseSettings
9+
10+
11+
class Settings(BaseSettings):
12+
# Layer 1 — knowledge artifact
13+
knowledge_artifact_path: Path = Path("/app/data/vtk-knowledge.jsonl")
14+
vtk_version: str = "9.3.0"
15+
16+
# Layer 2 — retrieval
17+
enable_retrieval: bool = True
18+
qdrant_url: str = "http://qdrant:6333"
19+
20+
# Layer 3 — validation
21+
enable_validation: bool = True
22+
23+
# Transport
24+
transport: str = "stdio"
25+
http_host: str = "0.0.0.0"
26+
http_port: int = 8000
27+
28+
# C++ docs scraping
29+
enable_cpp_scraping: bool = True
30+
vtk_docs_base_url: str = "https://vtk.org/doc/nightly/html/"
31+
32+
class Config:
33+
env_prefix = "VTK_MCP_"

0 commit comments

Comments
 (0)