"""MCP server for Navi 3D modeling tools (OpenSCAD)."""
from __future__ import annotations
import json
import os
from typing import Annotated, Any
from mcp.server.fastmcp import FastMCP
from pydantic import Field
from .config import Settings
from .model_compile import compile_scad
from .render_preview import render_stl
from .scad_analyze import lint_scad
INSTRUCTIONS = """
Navi 3D MCP server provides OpenSCAD-based modeling tools.
Use it when the task involves:
- generating 3D models from OpenSCAD scripts (.scad → .stl);
- rendering preview images from STL files;
- linting OpenSCAD source before compilation.
Workflow:
1. Write or read the .scad script via Navi's filesystem tool inside the
current session directory.
2. Call compile_scad to produce the STL.
3. Call lint_scad before compilation to catch common mistakes.
4. Call render_stl to generate PNG previews.
5. Use content_publish or share_file in Navi to show results to the user.
All paths are session-scoped. Pass the exact Navi session_id so files are
resolved inside session_files/<session_id>/.
""".strip()
mcp = FastMCP("navi-3d", instructions=INSTRUCTIONS)
def _json(data: Any) -> str:
return json.dumps(data, ensure_ascii=False, indent=2)
def _settings() -> Settings:
return Settings()
@mcp.tool()
def compile_scad_tool(
session_id: Annotated[str, Field(description="Navi session ID — files are resolved inside session_files/<session_id>/.")],
source_path: Annotated[str, Field(description="Relative or absolute path to the .scad file.")],
output_path: Annotated[str, Field(description="Relative or absolute path for the output STL.")],
) -> str:
"""Compile an OpenSCAD script into a binary STL."""
result = compile_scad(_settings(), session_id, source_path, output_path)
return _json(result)
@mcp.tool()
def render_stl_tool(
session_id: Annotated[str, Field(description="Navi session ID — files are resolved inside session_files/<session_id>/.")],
source_path: Annotated[str, Field(description="Path to the STL file to render.")],
views: Annotated[list[str] | None, Field(description="Camera views: front, back, top, bottom, left, right, iso. Max 3.")] = None,
) -> str:
"""Render preview PNG images from an STL file."""
result = render_stl(_settings(), session_id, source_path, views or ["iso"])
return _json(result)
@mcp.tool()
def lint_scad_tool(
session_id: Annotated[str, Field(description="Navi session ID — files are resolved inside session_files/<session_id>/.")],
source_path: Annotated[str, Field(description="Path to the .scad file.")],
strict: Annotated[bool, Field(description="Treat warnings as errors.")] = False,
) -> str:
"""Lint an OpenSCAD source file before compiling."""
result = lint_scad(_settings(), session_id, source_path, strict)
return _json(result)
def main() -> None:
transport = os.environ.get("NAVI_3D_MCP_TRANSPORT", "sse")
if transport not in {"stdio", "sse", "streamable-http"}:
raise SystemExit("NAVI_3D_MCP_TRANSPORT must be stdio, sse, or streamable-http")
mcp.run(transport=transport) # type: ignore[arg-type]
if __name__ == "__main__":
main()