[build-system] # setuptools>=77 is the floor for PEP 639 SPDX `license` / `license-files` # (see [project] below); older backends reject both keys. requires = ["setuptools>=77", "wheel"] build-backend = "setuptools.build_meta" [project] name = "ankusdrive" # Version comes from ankusdrive/__init__.py — single source of truth. Bump # __version__ there and tag matching vX.Y.Z to release. dynamic = ["version"] description = "CLI + MCP server that drives FreeCAD's Python API for LLM-driven mechanical design and FEM." readme = "README.md" # No upper bound, DELIBERATELY (issue #279). The reporter there was on a brand-new # CPython 3.14 and the install died somewhere in pip's resolver with nothing naming # the interpreter as the cause, so the question "what is the ceiling?" had to be # answered rather than left to the resolver. It was answered by running it: on # Windows 11 + CPython 3.14.7, `pip install -e .` resolves mcp 1.29.0 (pure-Python # wheel), numpy 2.5.2 and Pillow 12.3.0 (both ship cp314 win_amd64 wheels), the MCP # server completes a stdio handshake, and `ankusdrive ping` returns freecad=1.1.1. # 3.14 WORKS - hence a 3.14 classifier below and no `<3.15` here, since a hard # ceiling would break the next release for no reason. The host interpreter is also # independent of FreeCAD's bundled Python (freecadcmd runs as a subprocess), so # nothing about a new CPython touches the CAD half. The classifier list is the # honest statement of what has been *verified*; scripts/install-core.ps1 warns # (does not refuse) above it, and tests/test_windows_support.py keeps the two in # sync. requires-python = ">=3.10" # PEP 639: an SPDX expression, not the legacy table. The table form stuffed the # entire Apache-2.0 text into the `License:` core-metadata field; this emits # `License-Expression: Apache-2.0` instead and ships the files verbatim. # NOTICE is listed because Apache-2.0 s.4(d) requires downstream redistributors # to carry it — a wheel that omits it makes compliance impossible for them. license = "Apache-2.0" license-files = ["LICENSE", "NOTICE", "TRADEMARKS.md"] authors = [ { name = "George Chen" }, ] keywords = [ "freecad", "mcp", "model-context-protocol", "cad", "fem", "calculix", "llm", "mechanical-design", ] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Operating System :: MacOS", "Operating System :: POSIX :: Linux", # Windows is a first-class target (docs/WINDOWS.md, the hosted CI lane in # .github/workflows/test-windows.yml) but was missing from this list, so PyPI # advertised AnkusDrive as Unix-only to exactly the users #279 came from. "Operating System :: Microsoft :: Windows", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", # 3.14 verified end-to-end on Windows 11 (#279) - see the requires-python note. "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Physics", ] # Host-side runtime deps. Floors are the OLDEST versions verified to work: # mcp>=1.10,<2 — ankusdrive/mcp_server.py imports `mcp.server.fastmcp.FastMCP` # (first shipped in 1.2.0), and every tool carries a `title` and # ToolAnnotations (#368), which FastMCP's Tool only has from 1.10.0 # (1.8.1 and 1.9.4 have annotations but no title — empirically # checked). Below 1.10 the server would boot with its tool titles # silently missing, so the floor is 1.10, not 1.2. The <2 CEILING is just as load- # bearing: mcp 2.0.0 restructured the server package and dropped # `mcp.server.fastmcp`, so an unconstrained resolve installs # fine and then `ankusdrive mcp` dies with ImportError while # ping/doctor still pass (field-reported, issue #277). Raising # the ceiling requires porting mcp_server.py to the 2.x server # API and re-verifying the anyio thread-pool concurrency # contract documented there (issue #167). # Pillow>=10 — render path uses only Image.{fromarray,new,open,convert} # (ankusdrive/render.py), stable since Pillow 9; 10.0 is a safe # conservative floor with wheels for py3.10–3.13. # numpy>=1.24 — basic ndarray/dtype usage only, no post-1.24 APIs; a lower # bound (pip still selects a py3.13-compatible newer numpy where # needed). 1.24 is the conservative floor. dependencies = [ "mcp>=1.10,<2", "Pillow>=10.0", "numpy>=1.24", ] # NOTE: headless drawing PDF/SVG export (export_drawing) renders inside FreeCAD's # *bundled* Python, so reportlab + svglib must be installed there, not in this # host env — see README "Drawing export (PDF/SVG)". They are deliberately NOT # host dependencies: pin svglib<1.6 anywhere you install it, or pip drags in # rlPyCairo -> pycairo (a native build that has nothing to do with our line art). # DXF export and every other tool need nothing extra. [project.optional-dependencies] dev = [ "anthropic>=0.40", "python-dotenv>=1.0", ] # P2 external-solver extras (docs/archive/SIMULATION_P2_KICKOFF.md). Each family discovers # its solver via ankusdrive.solvers; these extras pull the pip-installable ones. # `pip install ankusdrive[mbd]` is a clean wheel install (no system package). The # apt/conda solvers (OpenFOAM, Elmer) are documented in scripts/install-solvers.sh, # not vendored — so `cfd` is intentionally empty (the solver is a system package). mbd = [ # The registry accepts PyBullet OR MuJoCo for the mbd family. PyBullet ships no # macOS wheels and its sdist fails to compile under clang (verified on arm64 / # Python 3.14, issue #194), so Darwin gets mujoco instead. 'pybullet>=3.2; platform_system != "Darwin"', 'mujoco; platform_system == "Darwin"', ] topology = [ "solidspy", # 'topopt' is the registry's alternative ] optics = [ "rayoptics", # sequential flat-interface trace (gated by analysis/optics) "optiland", # sequential lens design + optimization (MIT, in-process) ] # Fluid / thermophysical property corpus (analysis/fluids.py, issue #100). CoolProp # is BSD-3-Clause and a pure wheel → imported IN-PROCESS (like optiland), no copyleft # subprocess boundary. It is the DEFAULT f(T,P) source behind the convection/CFD # screens; when absent those paths fall back to constants so nothing hard-breaks. fluids = [ "CoolProp", # ρ, μ, cp, k, Pr f(T,P) for air/water/refrigerants (BSD-3) ] # Non-sequential STL solid tracing runs on KrakenOS, which is GPL-3.0. It is a # SEPARATE, opt-in extra because AnkusDrive invokes it only out-of-process (via # ankusdrive/optics_gpl_runner.py) to keep the copyleft boundary clean — installing it # does not link GPL code into AnkusDrive. setuptools<81 restores pkg_resources, which # KrakenOS imports. optics_gpl = [ "KrakenOS", "setuptools<81", ] # Exterior-acoustics BEM runs on bempp-cl, which is MIT (NOT copyleft). It is still # a SEPARATE, opt-in extra installed into a DEDICATED venv (.venv-bempp) and invoked # only out-of-process (via ankusdrive/bempp_runner.py) for a DEPENDENCY reason, not a # license one: bempp needs meshio>=4 (cells_dict), but the shared venv pins # meshio==3 for solidspy (the `topology` extra / analysis/topology.py). Installing # this extra into the shared venv would break topology optimisation — so install it # in its own venv (scripts/install-solvers.sh acoustics_bem) and point # ANKUSDRIVE_BEMPP_PYTHON at that venv's python. acoustics_bem = [ "bempp-cl", "gmsh", # bempp's shape generators shell out to the gmsh CLI binary "meshio>=5", # bempp needs cells_dict (>=4); KEEP OUT of the shared venv ] # Full-wave FDTD EM runs on openEMS/CSXCAD, which is GPL-3.0 AND is not a pip wheel # (not on PyPI/conda — it is source-built from the openEMS-Project meta-repo). Like # optics_gpl it is a SEPARATE, opt-in extra invoked only out-of-process (via # ankusdrive/em_fullwave_gpl_runner.py) so its copyleft does not link into AnkusDrive. # The python bindings install into a DEDICATED venv; this extra carries only the # pure-Python deps the runner needs there. See scripts/install-solvers.sh em_gpl # for the source build, then set ANKUSDRIVE_OPENEMS_PYTHON to that venv's python. em_gpl = [ "numpy", "h5py", # openEMS + CSXCAD are NOT here — they are source-built (GPL-3.0); see # scripts/install-solvers.sh em_gpl and update_openEMS.sh --python. ] cfd = [] # OpenFOAM / SU2 ship via apt/conda — see scripts/install-solvers.sh # Partitioned fluid-structure interaction (FSI) runs on preCICE (LGPL-3.0) coupling # OpenFOAM <-> CalculiX. preCICE itself is NOT a pip wheel for this path (the version- # sensitive serial libprecice + the two C++ adapters — calculix-adapter producing # ccx_preCICE, openfoam-adapter producing the function-object .so — are source-built), # and the two heavy solvers run ONLY out-of-process (ankusdrive/analysis/fsi_case.py) so # the LGPL coupling lib never links into AnkusDrive. This extra carries only the pure- # Python deps the artifact/render path needs; the C++ stack is built by # scripts/install-solvers.sh fsi (then auto-discovered or pointed at via # ANKUSDRIVE_CCX_PRECICE / ANKUSDRIVE_PRECICE_LIB / ANKUSDRIVE_OPENFOAM_ADAPTER_LIB). fsi = [ "numpy", # libprecice + ccx_preCICE + the OpenFOAM adapter are NOT here — they are # source-built (LGPL core, version-sensitive); see scripts/install-solvers.sh fsi. ] # Granular / powder discrete-element mechanics runs on YADE, which is GPL-3.0 and # ships no PyPI/conda-noble wheel — it is SOURCE-BUILT (scripts/install-solvers.sh # dem_gpl, into ~/opt/yade) and driven ONLY out-of-process via the `yade` executable # running ankusdrive/dem_gpl_runner.py, so installing it does not link GPL code into # AnkusDrive. There is no pip package to pull, so this opt-in extra is intentionally # empty (the marker exists so `ankusdrive[dem_gpl]` resolves and the registry can name # the extra); the build itself is the install step, documented in install-solvers.sh. dem_gpl = [] # YADE (GPL-3.0) is source-built — see scripts/install-solvers.sh [project.scripts] ankusdrive = "ankusdrive.cli:main" # Deprecated pre-0.5 name (#295). Kept for one minor release so existing MCP # host configs, aliases and scripts don't break with `command not found`; the # shim prints a rename notice, then delegates. Remove in 0.6 together with # ankusdrive/_legacy_cli.py. driftpin = "ankusdrive._legacy_cli:main" [project.urls] Homepage = "https://github.com/gchen19/AnkusDrive" Repository = "https://github.com/gchen19/AnkusDrive" Issues = "https://github.com/gchen19/AnkusDrive/issues" [tool.ruff] # Fast "real-bug" lint for the CI fast lane (no FreeCAD needed). Throwaway # probe scripts and the example tree aren't part of the shipped surface. target-version = "py310" extend-exclude = ["scratch", "examples"] [tool.ruff.lint] # E9 = syntax errors; F = pyflakes (undefined names, redefinitions, bad # f-strings, unused imports). Deliberately NOT the full style ruleset — this # gate catches bugs, not bikeshedding. select = ["E9", "F"] [tool.ruff.lint.per-file-ignores] # Test scaffolding legitimately keeps a named result for readability and # imports fixtures it doesn't always reference — don't gate on that noise. "tests/*" = ["F401", "F841"] [tool.setuptools.dynamic] version = { attr = "ankusdrive.__version__" } [tool.setuptools.packages.find] include = ["ankusdrive*"] exclude = ["tests*", "scratch*", "examples*", "docs*"] [tool.setuptools.package-data] # Everything the package reads back off disk at runtime. This list is NOT # cosmetic: a corpus missing here is absent from the built wheel, so every call # that loads it raises FileNotFoundError for pip-installed users while working # perfectly from a git checkout — which is how the ISO standards corpus stayed # unshipped for the project's whole life (#234) and the FSI case template after # it (#249). tests/test_package_data.py builds a distribution and re-derives this # list from the loaders themselves, so the next omission fails CI, not a user. # worker.py — spawned BY PATH inside freecadcmd's bundled Python # (it opens with `import FreeCAD`), never imported here. # analysis/materials — seed.json, optical.json, vendored fcmat.json. # analysis/standards — threads / bearings / stock / catalog ISO corpus. # analysis/fsi_template — the preCICE + OpenFOAM + CalculiX case skeleton # copied by analysis/fsi_case.write_fsi_case(). Mostly # extensionless OpenFOAM dictionaries in nested dirs # with no __init__.py, so it needs a recursive `**/*` # rather than an extension glob. ankusdrive = [ "worker.py", "analysis/materials/*.json", "analysis/standards/*.json", "analysis/fsi_template/**/*", ]