"""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(name="compile_scad")
async 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="Just the filename, e.g. 'falcon9_rocket.scad'. The server resolves it inside session_files/<session_id>/ automatically.")],
output_path: Annotated[str, Field(description="Just the filename, e.g. 'falcon9_rocket.stl'. The server resolves it inside session_files/<session_id>/ automatically.")],
) -> str:
"""Compile an OpenSCAD script into a binary STL."""
result = await compile_scad(_settings(), session_id, source_path, output_path)
return _json(result)
@mcp.tool(name="render_stl")
async 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="Just the filename, e.g. 'falcon9_rocket.stl'. The server resolves it inside session_files/<session_id>/ automatically.")],
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 = await render_stl(_settings(), session_id, source_path, views or ["iso"])
return _json(result)
@mcp.tool(name="lint_scad")
async 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="Just the filename, e.g. 'falcon9_rocket.scad'. The server resolves it inside session_files/<session_id>/ automatically.")],
strict: Annotated[bool, Field(description="Treat warnings as errors.")] = False,
) -> str:
"""Lint an OpenSCAD source file before compiling."""
result = await 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()