Skip to content

Commit 485c5a2

Browse files
Add ping tool, runtime check CLI, and bump to v0.2.0
- Add lightweight ping tool for MCP parity (#18) - Add check-runtime subcommand and runtime_check module for verifying TiGL/TiXI/OpenCASCADE/Gmsh availability (#17) - Ship environment.yml for one-command conda setup - Add tigl-mcp-check script alias - Bump version from 0.1.2 to 0.2.0 Made-with: Cursor
1 parent f85b4e1 commit 485c5a2

6 files changed

Lines changed: 210 additions & 1 deletion

File tree

environment.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: tigl-mcp
2+
channels:
3+
- dlr-sc
4+
- conda-forge
5+
- defaults
6+
dependencies:
7+
- python=3.12
8+
- tigl3
9+
- tixi3
10+
- python-tigl3
11+
- python-tixi3
12+
- pythonocc-core
13+
- gmsh
14+
- python-gmsh
15+
- meshio
16+
- numpy
17+
- pip
18+
- pip:
19+
- -e .

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "tigl-mcp"
7-
version = "0.1.2"
7+
version = "0.2.0"
88
description = "An MCP around the TiGL library"
99
authors = [
1010
{ name = "The Design Research Collective", email = "ask-drc@cmu.edu" }
@@ -43,6 +43,7 @@ dev = [
4343

4444
[project.scripts]
4545
tigl-mcp = "tigl_mcp.main:main"
46+
tigl-mcp-check = "tigl_mcp.runtime_check:print_runtime_report"
4647

4748
[tool.ruff]
4849
line-length = 88

src/tigl_mcp/main.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ def build_parser() -> argparse.ArgumentParser:
4545

4646
def main(argv: Sequence[str] | None = None) -> int:
4747
"""Register tools and start the FastMCP server."""
48+
raw = list(argv) if argv is not None else __import__("sys").argv[1:]
49+
if raw and raw[0] == "check-runtime":
50+
from tigl_mcp.runtime_check import print_runtime_report
51+
52+
print_runtime_report()
53+
return 0
54+
4855
parser = build_parser()
4956
args = parser.parse_args(argv)
5057

src/tigl_mcp/runtime_check.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""Runtime validation for TiGL/TiXI native dependencies."""
2+
3+
from __future__ import annotations
4+
5+
import shutil
6+
import sys
7+
8+
9+
CONDA_INSTALL_CMD = (
10+
"conda install -c dlr-sc -c conda-forge tigl3 tixi3 python-tigl3 python-tixi3"
11+
)
12+
13+
DOCKER_BUILD_CMD = "docker build -t tigl-mcp:dev ."
14+
15+
INSTALL_GUIDE = f"""\
16+
TiGL/TiXI native bindings are not available in this Python environment.
17+
18+
To install, choose one of the following options:
19+
20+
1. Conda (recommended):
21+
{CONDA_INSTALL_CMD}
22+
23+
2. Conda environment file (ships with this repo):
24+
conda env create -f environment.yml
25+
conda activate tigl-mcp
26+
27+
3. Docker (includes TiGL + Gmsh + SU2):
28+
{DOCKER_BUILD_CMD}
29+
30+
4. Pre-built binaries from DLR:
31+
https://github.com/DLR-SC/tigl/releases
32+
33+
The server will still start in stub mode (synthetic geometry) without
34+
these bindings, but real STEP/STL export requires the native runtime.
35+
"""
36+
37+
38+
def check_tigl_runtime() -> dict[str, object]:
39+
"""Probe the environment for TiGL and TiXI availability.
40+
41+
Returns a dict suitable for printing or returning as a tool response.
42+
"""
43+
results: dict[str, object] = {
44+
"python": sys.version,
45+
"platform": sys.platform,
46+
}
47+
48+
# TiXI
49+
try:
50+
from tixi3 import tixi3wrapper # type: ignore[import-untyped]
51+
52+
tixi_h = tixi3wrapper.Tixi3()
53+
results["tixi3"] = {"available": True, "version": getattr(tixi_h, "version", "unknown")}
54+
except Exception as exc:
55+
results["tixi3"] = {"available": False, "error": str(exc)}
56+
57+
# TiGL
58+
try:
59+
from tigl3 import tigl3wrapper # type: ignore[import-untyped]
60+
61+
results["tigl3"] = {"available": True, "module": tigl3wrapper.__name__}
62+
except Exception as exc:
63+
results["tigl3"] = {"available": False, "error": str(exc)}
64+
65+
# OpenCASCADE (used for watertight STEP export)
66+
try:
67+
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeSolid # type: ignore[import-untyped] # noqa: F401
68+
69+
results["opencascade"] = {"available": True}
70+
except Exception as exc:
71+
results["opencascade"] = {"available": False, "error": str(exc)}
72+
73+
# Gmsh (used for meshing in the pipeline)
74+
gmsh_bin = shutil.which("gmsh")
75+
try:
76+
import gmsh as _gmsh # type: ignore[import-untyped] # noqa: F401
77+
78+
results["gmsh"] = {
79+
"available": True,
80+
"python_api": True,
81+
"cli": gmsh_bin or "not on PATH",
82+
}
83+
except Exception:
84+
results["gmsh"] = {
85+
"available": gmsh_bin is not None,
86+
"python_api": False,
87+
"cli": gmsh_bin or "not on PATH",
88+
}
89+
90+
all_ok = all(
91+
isinstance(v, dict) and v.get("available", False)
92+
for v in results.values()
93+
if isinstance(v, dict)
94+
)
95+
results["all_ok"] = all_ok
96+
97+
return results
98+
99+
100+
def print_runtime_report() -> None:
101+
"""Print a human-readable runtime diagnostics report."""
102+
report = check_tigl_runtime()
103+
104+
print("=" * 60)
105+
print(" TiGL MCP Runtime Check")
106+
print("=" * 60)
107+
print(f" Python: {report['python']}")
108+
print(f" Platform: {report['platform']}")
109+
print()
110+
111+
for name in ("tixi3", "tigl3", "opencascade", "gmsh"):
112+
info = report.get(name, {})
113+
if not isinstance(info, dict):
114+
continue
115+
ok = info.get("available", False)
116+
status = "OK" if ok else "MISSING"
117+
marker = "+" if ok else "x"
118+
line = f" [{marker}] {name:15s} {status}"
119+
if not ok and "error" in info:
120+
line += f" ({info['error'][:80]})"
121+
if ok and "version" in info:
122+
line += f" (v{info['version']})"
123+
if "cli" in info:
124+
line += f" [cli: {info['cli']}]"
125+
print(line)
126+
127+
print()
128+
if report.get("all_ok"):
129+
print(" All dependencies found. Full TiGL geometry export is available.")
130+
else:
131+
print(" Some dependencies are missing. The server will run in stub mode.")
132+
print()
133+
print(INSTALL_GUIDE)
134+
135+
print("=" * 60)

src/tigl_mcp/tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
get_high_level_parameters_tool,
2323
set_high_level_parameters_tool,
2424
)
25+
from tigl_mcp.tools.ping import ping_tool
2526
from tigl_mcp.tools.sampling import (
2627
intersect_components_tool,
2728
intersect_with_plane_tool,
@@ -32,6 +33,7 @@
3233
def build_tools(session_manager: SessionManager) -> list[ToolDefinition]:
3334
"""Instantiate all tool definitions with the provided session manager."""
3435
return [
36+
ping_tool(session_manager),
3537
open_cpacs_tool(session_manager),
3638
close_cpacs_tool(session_manager),
3739
get_configuration_summary_tool(session_manager),

src/tigl_mcp/tools/ping.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Lightweight health-check tool for the TiGL MCP server."""
2+
3+
from __future__ import annotations
4+
5+
from datetime import UTC, datetime
6+
from typing import Any
7+
8+
from tigl_mcp.tooling import ToolDefinition, ToolParameters
9+
10+
11+
class PingParams(ToolParameters):
12+
"""Request payload for the ping tool."""
13+
14+
message: str | None = None
15+
16+
17+
def ping_tool(_session_manager: Any = None) -> ToolDefinition:
18+
"""Build a ping tool definition (session_manager is accepted but unused)."""
19+
20+
def handler(params: dict[str, Any]) -> dict[str, Any]:
21+
now = datetime.now(UTC).isoformat()
22+
reply = params.get("message") or "pong"
23+
return {
24+
"ok": True,
25+
"message": reply,
26+
"server": "tigl-mcp",
27+
"timestamp": now,
28+
}
29+
30+
return ToolDefinition(
31+
name="ping",
32+
description="Lightweight health check that confirms the server is reachable without requiring a CPACS session or TiGL runtime.",
33+
parameters_model=PingParams,
34+
handler=handler,
35+
output_schema={
36+
"type": "object",
37+
"properties": {
38+
"ok": {"type": "boolean"},
39+
"message": {"type": "string"},
40+
"server": {"type": "string"},
41+
"timestamp": {"type": "string", "format": "date-time"},
42+
},
43+
"required": ["ok", "message", "server", "timestamp"],
44+
},
45+
)

0 commit comments

Comments
 (0)