Files
freecad-robust-mcp-fc111/pyproject.toml
T
8c338f6da7 feat: MCP Bridge Workbench, just command cleanup, testing, etc. (#24)
* fix: lots of fixes and name refactoring

* feat: Add workbench preferences

* fix: MCP bridge status widget and just command fixes

* fix(tests): Use the correct mesa-glx package

* fix(ci): Add fontconfig to GUI test dependencies

FreeCAD GUI was failing to start with:
"Fontconfig error: Cannot load default config file: No such file"

Added fontconfig and fonts-dejavu-core packages to the GUI test job
dependencies to resolve the font configuration issue.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(addon): Extract path utilities into shared module

Create path_utils.py module that consolidates duplicated path-finding
logic from commands.py and InitGui.py:
- get_addon_path(): Find addon directory with caching and fallbacks
- get_icon_path(): Get full path to an icon file
- get_icons_dir(): Get path to icons directory
- get_workbench_icon(): Get path to workbench main icon

This removes ~100 lines of duplicated code while preserving the same
behavior including _addon_path_cache and all fallback methods.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(addon): Prevent stale plugin state on startup failure

The StartMCPBridgeCommand.Activated method could leave _mcp_plugin in
a partially initialized state if FreecadMCPPlugin.start() failed after
the plugin was instantiated.

Changes:
- Create plugin in a local variable first
- Only assign to _mcp_plugin after start() succeeds
- Explicitly clear _mcp_plugin and _running_config in exception
  handlers to ensure clean state for subsequent retry attempts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Lot of broad improvements

* fix(ci): Use blocking headless_server.py for GUI tests

The GUI test was using startup_bridge.py which is non-blocking
(designed for interactive use). For CI, even in GUI mode, we need
the blocking headless_server.py that calls run_forever() to keep
FreeCAD running. GUI features are still available since we use
the 'freecad' executable instead of 'freecadcmd'.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(addon): Rename headless_server.py to blocking_bridge.py

The old name was misleading because:
- It works with both GUI (freecad) and headless (freecadcmd) modes
- The key characteristic is that it BLOCKS with run_forever()

New naming convention clarifies the difference:
- blocking_bridge.py: Starts bridge and blocks (for CI, servers)
- startup_bridge.py: Starts bridge and returns (for interactive GUI)

Updated all references across:
- GitHub workflow (macro-test.yaml)
- Just commands (freecad.just)
- Unit tests (test_addon_structure.py)
- Documentation (5 files)
- CLAUDE.md

Also improved the script to detect GUI mode dynamically using
FreeCAD.GuiUp and display the appropriate status message.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(just): Remove erroneous rm of startup_bridge.py on error

The startup script is now a permanent source file in the repository,
not a generated temporary file. The rm -f would have deleted source
code if FreeCAD wasn't found.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: General improvements

* fix: Lots of general fixes and only stable to PyPi

* fix: small cleanup

* fix: Small fixes and hopefully fixes the GUI tests

* fix: Add proper library paths for FreeCAD GUI in CI

- Create wrapper scripts instead of symlinks for AppImage binaries
- Set LD_LIBRARY_PATH, QT_PLUGIN_PATH for GUI mode
- Add diagnostic output to identify startup failures

* fix: Use apprun for GUI tests in CI

* fix: Improving Xvfb tests

* fix: GUI tests worlk

* chore: remove invalid --no-splash comments

* fix: ARM64 architecture support and other fixes

* fix: cleanup

* test: just commands test suite

* test: improve just command tests

* fix: more general improvements

* fix: more cleanup

* fix: more updates

* fix: small tweaks

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:26:33 -08:00

341 lines
10 KiB
TOML

[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
name = "freecad-robust-mcp"
dynamic = ["version"]
description = "Robust MCP Server for FreeCAD integration with Claude Code and other AI assistants"
readme = "README.md"
license = "MIT"
requires-python = ">=3.11"
authors = [
{ name = "Sean P. Kane", email = "spkane@gmail.com" }
]
maintainers = [
{ name = "Sean P. Kane", email = "spkane@gmail.com" }
]
keywords = [
"freecad",
"mcp",
"model-context-protocol",
"claude",
"claude-code",
"ai",
"llm",
"cad",
"3d-modeling",
"3d-printing",
"parametric-design",
"automation",
]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Framework :: AsyncIO",
"Intended Audience :: Developers",
"Intended Audience :: Manufacturing",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Operating System :: MacOS",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Visualization",
"Topic :: Software Development :: Libraries :: Python Modules",
"Typing :: Typed",
]
dependencies = [
"mcp>=1.25.0",
"pydantic>=2.10.0",
"pydantic-settings>=2.7.0",
]
[project.optional-dependencies]
dev = [
# Testing
"pytest>=8.3.0",
"pytest-asyncio>=0.25.0",
"pytest-cov>=6.0.0",
"pytest-timeout>=2.3.0",
"pytest-watch>=4.2.0",
# Linting and formatting
"mypy>=1.14.0",
"ruff>=0.8.0",
# Security scanning
"bandit[toml]>=1.8.0",
"safety>=3.2.0",
"detect-secrets>=1.5.0",
# Pre-commit and commit conventions
"pre-commit>=4.0.0",
"commitizen>=4.1.0",
# Documentation
"mkdocs>=1.6.0",
"mkdocs-material>=9.7.1",
"mkdocstrings[python]>=0.27.0",
"mkdocs-git-revision-date-localized-plugin>=1.2.0",
"mkdocs-minify-plugin>=0.8.0",
"mkdocs-redirects>=1.2.0",
"mkdocs-glightbox>=0.4.0",
"mkdocs-macros-plugin>=1.0.0",
"md-toc>=9.0.0",
"codespell>=2.3.0",
# Publishing
"twine>=6.0.0",
"build>=1.2.0",
]
[project.scripts]
freecad-mcp = "freecad_mcp.server:main"
[project.urls]
Homepage = "https://github.com/spkane/freecad-robust-mcp-and-more"
Documentation = "https://github.com/spkane/freecad-robust-mcp-and-more#readme"
Repository = "https://github.com/spkane/freecad-robust-mcp-and-more.git"
Issues = "https://github.com/spkane/freecad-robust-mcp-and-more/issues"
Changelog = "https://github.com/spkane/freecad-robust-mcp-and-more/blob/main/CHANGELOG.md"
"Docker Hub" = "https://hub.docker.com/r/spkane/freecad-robust-mcp"
[tool.hatch.version]
source = "vcs"
# Use git tags for versioning: v0.1.0 -> 0.1.0
# For untagged commits: 0.1.0.dev5+g1a2b3c4
[tool.hatch.version.raw-options]
# Fall back version when not in a git repo (e.g., source tarball)
version_scheme = "guess-next-dev"
local_scheme = "node-and-date"
[tool.hatch.build.hooks.vcs]
version-file = "src/freecad_mcp/_version.py"
[tool.hatch.build.targets.wheel]
packages = ["src/freecad_mcp"]
[tool.ruff]
target-version = "py311"
line-length = 88
src = ["src", "tests", "macros"]
extend-include = ["*.FCMacro"]
# Exclude auto-generated files (hatch-vcs version file)
exclude = ["src/freecad_mcp/_version.py"]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG", # flake8-unused-arguments
"SIM", # flake8-simplify
"TCH", # flake8-type-checking
"PTH", # flake8-use-pathlib
"ERA", # eradicate (commented-out code)
"PL", # Pylint
"RUF", # Ruff-specific rules
"S", # flake8-bandit (security)
"ANN", # flake8-annotations
"ASYNC", # flake8-async
"D", # pydocstyle
]
ignore = [
"D104", # Missing docstring in public package - __init__.py often just imports
"D107", # Missing docstring in __init__ - class docstring covers this
"ANN401", # Allow Any in type annotations - FreeCAD types are dynamic
"ANN001", # Allow missing type annotations - FastMCP doesn't export types
"PLR0913",# Too many arguments - CAD APIs have many params
"PLR0915",# Too many statements - registration functions are long
"PLC0415",# Allow imports in functions - FreeCAD must be imported lazily
"PLR2004",# Allow magic numbers in comparisons - CAD dimensions
"PLW0603",# Allow global statement - bridge lifecycle management
"E501", # Line too long - embedded Python code strings
"RUF022", # Don't sort __all__ - prefer logical grouping
"S110", # Allow try-except-pass for optional operations
"ARG002", # Allow unused arguments - needed for interface compliance
]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = [
"S101", # Allow asserts in tests
"S108", # Allow /tmp in tests
"ANN", # Don't require annotations in tests
"D", # Don't require docstrings in tests
"PLR2004",# Allow magic numbers in tests
"PTH", # Allow os.path in tests - matches embedded FreeCAD code style
"SIM105", # Allow try-except-pass in tests
"SIM117", # Allow nested with statements
"B017", # Allow blind exception catch in tests
]
# FreeCAD macros have relaxed rules - they are standalone GUI scripts
# But still require essential docstrings (module, class, public function)
"**/*.FCMacro" = [
"ANN", # Don't require type annotations - FreeCAD types are complex
"D104", # Missing docstring in public package - N/A for macros
"D105", # Missing docstring in magic method - __init__ etc don't need them
"D107", # Missing docstring in __init__ - class docstring covers this
"D203", # Blank line before class docstring (conflicts with D211)
"D212", # Multi-line docstring start (conflicts with D213)
"PLR0911",# Allow many return statements - GUI code paths vary
"PLR0912",# Allow many branches - GUI code is complex
"PLR0913",# Allow many arguments - CAD APIs have many params
"PLR0915",# Allow many statements - macros are self-contained
"PTH", # Allow os.path - better FreeCAD compatibility than pathlib
"SIM102", # Allow nested if statements - improves readability
"SIM103", # Allow return after if-else for clarity
"SIM108", # Allow if-else over ternary for readability
"S112", # Allow try-except-continue - used for geometry iteration
"F841", # Allow unused variables - Qt signal connections
"UP008", # Allow super(Class, self) - PySide compatibility
]
[tool.mypy]
python_version = "3.11"
# Not using strict mode due to FastMCP lacking type stubs
warn_return_any = false
warn_unused_configs = true
disallow_untyped_defs = false
disallow_incomplete_defs = false
check_untyped_defs = true
disallow_untyped_decorators = false
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = false
warn_no_return = true
follow_imports = "silent"
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
disallow_incomplete_defs = false
[[tool.mypy.overrides]]
module = [
"freecad_mcp.tools",
"freecad_mcp.tools.*",
]
disallow_untyped_decorators = false
disallow_untyped_defs = false
warn_return_any = false
[[tool.mypy.overrides]]
module = [
"freecad_mcp.resources",
"freecad_mcp.resources.*",
]
disallow_untyped_decorators = false
disallow_untyped_defs = false
warn_return_any = false
[[tool.mypy.overrides]]
module = [
"freecad_mcp.prompts",
"freecad_mcp.prompts.*",
]
disallow_untyped_decorators = false
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = [
"freecad_mcp.bridge",
"freecad_mcp.bridge.*",
]
warn_return_any = false
[[tool.mypy.overrides]]
module = [
"freecad_mcp.freecad_plugin",
"freecad_mcp.freecad_plugin.*",
]
warn_return_any = false
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = "freecad_mcp.server"
disallow_untyped_decorators = false
disallow_untyped_defs = false
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
addopts = [
"-ra",
"-q",
"--strict-markers",
"--strict-config",
]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks integration tests requiring FreeCAD",
"gui: marks tests requiring FreeCAD GUI mode (not headless)",
]
[tool.coverage.run]
source = ["src/freecad_mcp"]
branch = true
parallel = true
# Exclude bridge modules from unit test coverage - they require a running
# FreeCAD instance. These modules are exercised by integration tests which
# run separately (just testing::integration). To verify bridge coverage:
# uv run pytest tests/integration --cov=freecad_mcp.bridge --cov-report=term-missing
omit = [
"src/freecad_mcp/bridge/embedded.py",
"src/freecad_mcp/bridge/socket.py",
"src/freecad_mcp/bridge/xmlrpc.py",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if TYPE_CHECKING:",
"if __name__ == .__main__.:",
"@abstractmethod",
]
show_missing = true
fail_under = 80
[tool.bandit]
exclude_dirs = ["tests", "docs"]
skips = [
"B101", # Allow asserts
"B102", # exec is required for FreeCAD Python execution
"B104", # Binding to 0.0.0.0 is intentional for container/network access
"B110", # try-except-pass used for optional cleanup
"B112", # try-except-continue used for iterating over faces safely
"B411", # XML-RPC is required for FreeCAD compatibility
]
[tool.codespell]
skip = ".git,*.lock,*.toml"
ignore-words-list = "crate"
[tool.commitizen]
name = "cz_conventional_commits"
version_scheme = "semver2"
major_version_zero = true
# Note: This project uses component-specific tags for releases:
# - robust-mcp-server-vX.Y.Z (MCP server: PyPI, Docker, GitHub release)
# - robust-mcp-workbench-vX.Y.Z (FreeCAD workbench)
# - macro-cut-object-for-magnets-vX.Y.Z
# - macro-multi-export-vX.Y.Z
# Use `just release::tag-<component> <version>` to create release tags.
# Commitizen is used for commit message validation and changelog generation.