diff --git a/CHANGELOG.md b/CHANGELOG.md
index 711b642..275f5fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,53 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.5.1] — 2026-08-06
+
+A patch for three defects found *after* 1.5.0 went to PyPI, two of them by the
+adversarial pass over the rewritten README. 1.5.0 is yanked: its diameter and
+radius callouts put wrong numbers on drawings at default settings, and that is
+not something to leave installable.
+
+### Fixed
+
+- **The HTTP bind guard failed open on the launch path it was written for.**
+ `_guarded_run_async` read the bind address from the `host` kwarg and treated
+ an absent one as loopback. It is not: `fastmcp run` forwards that kwarg only
+ when `--host` was passed, and otherwise fastmcp resolves the host from
+ `fastmcp.settings.host`, which is environment-backed. So
+ `FASTMCP_HOST=0.0.0.0 fastmcp run server.py:mcp --transport http` bound every
+ interface — 149 tools including file open/save and AutoCAD command execution,
+ with no token — while the guard inspected the literal string `"127.0.0.1"`
+ and waved it through. v1.4.0's changelog claimed the guard "now fires on
+ every launch path"; it fired, and then looked at the wrong host. It now
+ resolves the effective host, and says so in a warning if it cannot.
+- **`[com]` and `[full]` could not be installed off Windows.** Both extras
+ listed `pywin32>=306` with no environment marker, and pywin32 publishes
+ Windows wheels and *no sdist* — so `pip install autocad-mcp-pro[full]` died
+ at resolution on Linux and macOS, and `[full]` is the extra this project's
+ own README recommended to contributors. CI never caught it because every
+ Linux lane installs `[pdf]` or syncs from `uv.lock`, whose resolution markers
+ hide the gap. Both entries now carry `; sys_platform == "win32"`, and a
+ release-consistency test fails if a Windows-only distribution loses its
+ marker again.
+- **`drawing_settings({"dimscale": ...})` reported success and changed nothing
+ headlessly.** It writes the `$DIMSCALE` header variable, which is what
+ AutoCAD consults when it draws a dimension — so the call worked on the live
+ backend. ezdxf renders from the dimstyle table entry and never reads the
+ header, so the same call produced two different drawings on the two engines
+ while both said `ok`. The header value is now folded into the per-dimension
+ override on all five dimension tools, including `dimension_aligned` and
+ `dimension_angular`, which do not share the tolerance-override path and were
+ missed by the obvious fix. DIMSCALE 1.0 writes no override.
+
+### Changed
+
+- README: the "What you get" table qualified every engine-conditional family
+ (boundary tracing, WIPEOUT/REVCLOUD/MTEXT masks, CHSPACE, typed hatch edge
+ paths, headless undo) instead of listing them as if both engines had them,
+ and the checkout install line no longer recommends an extra that cannot
+ resolve on the reader's platform.
+
## [1.5.0] — 2026-08-06
### Added
diff --git a/README.md b/README.md
index be0c1b4..5c407b6 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ Live through COM on Windows, or headless through ezdxf on any platform — one t
---
-> **v1.5 release snapshot:** 154 tools · 6 resources · 5 prompt templates · 1250 collected tests.
+> **v1.5 release snapshot:** 154 tools · 6 resources · 5 prompt templates · 1258 collected tests.
> 154 is the **registered** count. A default install **advertises 149** over `tools/list`, because
> `ENABLE_3D` is unset and the five `solid_*` tools stay hidden. `system_about` is the runtime authority.
@@ -74,7 +74,7 @@ AUTOCAD_MCP_BACKEND=com autocad-mcp # live AutoCAD (pip install "autocad-mc
> [!NOTE]
> The bare install **cannot draw pixels**. `ezdxf.addons.drawing.frontend` does an unconditional top-level `import PIL.Image`, so Pillow is required by *every* ezdxf render path — PNG screenshot and PDF export alike — not just by the COM window capture it is listed under. Add `[pdf]` (or `[full]`) if you want images on Linux or macOS.
-From a checkout: `pip install -e ".[full]"` then `python server.py`.
+From a checkout: `pip install -e ".[full]"` on Windows, or `pip install -e ".[pdf]"` elsewhere, then `python server.py`. (`[com]` and `[full]` carry a `sys_platform == "win32"` marker on `pywin32`, so they resolve everywhere — they simply install nothing Windows-specific off Windows.)
### Wire it to a client
@@ -100,18 +100,18 @@ Works with Claude Desktop, Cursor, or any stdio MCP host. For HTTP: `autocad-mcp
| Area | What it does |
-| Drawing lifecycle | create, open, save, export DXF/PDF, audit (now repairs), purge, undo/redo |
+| Drawing lifecycle | create, open, save, export DXF/PDF, audit (now repairs), purge; undo/redo live on COM and opt-in headless via EZDXF_UNDO_DEPTH |
| Geometry | lines, arcs, polylines, splines, hatches, trim/extend/fillet/chamfer, handle-preserving edits |
| Annotation | ISO 129 toleranced dimensions, ISO 286 fits (fit="H7"), TABLE, MLEADER, GD&T frames and datums (ISO 1101) |
| Engineering generators | involute gears (front + section A-A), DIN 6885 keyed bores, ISO A3 title block |
| Discovery ✨ | search_tools ranked over an AutoCAD command and synonym corpus — FILLET, BPOLY, QSELECT, WBLOCK, OVERKILL, CHSPACE each rank #1 of the 149-tool advertised catalog |
| Batching ✨ | cad_batch runs a whole step list in one round trip; fields= projects the response on 11 result-heavy tools (a closed, gated list) |
-| Paper space ✨ | tab lifecycle, viewports, entity_change_space (CHSPACE), drawing_export_pdf(layout=...) |
+| Paper space ✨ | tab lifecycle, viewports and drawing_export_pdf(layout=...) on both engines; entity_change_space (CHSPACE) headless only |
| Selection ✨ | window vs crossing stated back to the caller; a polygon tested against its own shape, not its bounding box |
-| Boundaries ✨ | boundary_trace (BOUNDARY/BPOLY) chains loose edges into one closed polyline, arcs kept as bulges |
+| Boundaries ✨ | boundary_trace (BOUNDARY/BPOLY) chains loose edges into one closed polyline, arcs kept as bulges — headless engine only |
| Measurement ✨ | analysis_measure_entity measures what is in the drawing, by handle |
-| Hatch depth ✨ | gradients, in-place edits, typed edge boundaries, island styles |
-| Annotation objects ✨ | WIPEOUT, REVCLOUD, MTEXT background masks, text find/replace |
+| Hatch depth ✨ | gradients, in-place edits and island styles on both; typed edge boundaries headless only |
+| Annotation objects ✨ | WIPEOUT, REVCLOUD, MTEXT background masks — headless only, ActiveX exposes no member — plus text find/replace on both |
| 3D solids | solid_box/cylinder/extrude/revolve/boolean on live AutoCAD (ENABLE_3D=true) |
| Quality loop | drawing_preflight → drawing_plan → drawing_critique → drawing_refine → drawing_finalize (0–100 score) |
| Delivery | drawing_deliver: DXF/PDF/PNG + SHA-256 manifest + reopen-parity checks |
diff --git a/backends/ezdxf_backend.py b/backends/ezdxf_backend.py
index ea1817a..dce2216 100644
--- a/backends/ezdxf_backend.py
+++ b/backends/ezdxf_backend.py
@@ -443,6 +443,30 @@ def _measure_hatch(ent, tolerance: float, result: dict) -> dict:
return _round_measure(result)
+def _with_header_dimscale(doc, override: dict | None) -> dict | None:
+ """Carry `$DIMSCALE` onto the dimension being created.
+
+ `drawing_settings({"dimscale": ...})` writes the header variable, which is
+ what AutoCAD consults when it draws a dimension — so the call worked on the
+ live backend. ezdxf renders from the dimstyle table entry and never reads
+ the header, so headlessly the same call reported success and changed
+ nothing. Folding it into the per-dimension override makes one call mean one
+ thing on both engines.
+
+ 1.0 is the default and is deliberately not written, so an untouched drawing
+ does not gain an override on every dimension.
+ """
+ try:
+ scale = float(doc.header.get("$DIMSCALE", 1.0) or 1.0)
+ except (AttributeError, TypeError, ValueError):
+ return override
+ if abs(scale - 1.0) < 1e-9:
+ return override
+ merged = dict(override or {})
+ merged.setdefault("dimscale", scale)
+ return merged
+
+
def _round_measure(result: dict) -> dict:
"""Six decimal places, matching analysis_measure_distance."""
result["area"] = round(float(result["area"]), 6)
@@ -2870,6 +2894,7 @@ class EzdxfBackend(AutoCADBackend):
from engineering.tolerances import build_dim_override
override, text = build_dim_override(tol_upper, tol_lower, tol_mode, text_override)
+ override = _with_header_dimscale(self._require_doc(), override)
msp = self._msp()
dim = msp.add_linear_dim(
base=(float(dim_x), float(dim_y)),
@@ -2906,6 +2931,7 @@ class EzdxfBackend(AutoCADBackend):
distance=math.sqrt(
(float(dim_x) - float(x1)) ** 2 + (float(dim_y) - float(y1)) ** 2
),
+ override=_with_header_dimscale(self._require_doc(), None),
)
dim.render()
ent = dim.dimension
@@ -2936,6 +2962,7 @@ class EzdxfBackend(AutoCADBackend):
line1=((vxf, vyf), (float(x1), float(y1))),
line2=((vxf, vyf), (float(x2), float(y2))),
location=(float(tx), float(ty)),
+ override=_with_header_dimscale(self._require_doc(), None),
)
dim.render()
ent = dim.dimension
@@ -2963,6 +2990,7 @@ class EzdxfBackend(AutoCADBackend):
from engineering.tolerances import build_dim_override
override, text = build_dim_override(tol_upper, tol_lower, tol_mode, text_override)
+ override = _with_header_dimscale(self._require_doc(), override)
msp = self._msp()
cxf, cyf = float(cx), float(cy)
radius = math.sqrt((chord_x - cxf) ** 2 + (chord_y - cyf) ** 2)
@@ -3006,6 +3034,7 @@ class EzdxfBackend(AutoCADBackend):
from engineering.tolerances import build_dim_override
override, text = build_dim_override(tol_upper, tol_lower, tol_mode, text_override)
+ override = _with_header_dimscale(self._require_doc(), override)
msp = self._msp()
x1f, y1f, x2f, y2f = float(x1), float(y1), float(x2), float(y2)
cx = (x1f + x2f) / 2
diff --git a/docs/tool-inventory.json b/docs/tool-inventory.json
index fb8517f..3c5c78e 100644
--- a/docs/tool-inventory.json
+++ b/docs/tool-inventory.json
@@ -1,7 +1,7 @@
{
"_comment": "GENERATED FILE - do not edit by hand. Derived from the live FastMCP component registry; regenerate with `python scripts/generate_tool_inventory.py`.",
"schema_version": 1,
- "package_version": "1.5.0",
+ "package_version": "1.5.1",
"totals": {
"tools": 154,
"resources": 6,
diff --git a/pyproject.toml b/pyproject.toml
index a5b4d4a..09cd8d9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "autocad-mcp-pro"
-version = "1.5.0"
+version = "1.5.1"
description = "Production-grade AutoCAD MCP server — COM API + ezdxf dual engine"
readme = "README.md"
requires-python = ">=3.11"
@@ -74,7 +74,13 @@ Changelog = "https://github.com/U-C4N/Autocad-MCP/blob/main/CHANGELOG.md"
# renders nothing. Install `[pdf]` (or `[full]`) for rendering on any platform:
# Pillow comes in transitively as a matplotlib dependency.
com = [
- "pywin32>=306",
+ # pywin32 publishes Windows wheels and no sdist, so without this marker
+ # the extra cannot be resolved at all on Linux or macOS -- pip dies at
+ # "no matching distribution" before installing anything. The marker
+ # makes `pip install autocad-mcp-pro[com]` a no-op there instead of a
+ # hard failure, which is the honest outcome: the COM backend needs
+ # Windows, and the server already falls back to ezdxf.
+ 'pywin32>=306; sys_platform == "win32"',
# Win32 window capture for the live-AutoCAD screenshot path.
"Pillow>=10.0",
]
@@ -83,7 +89,10 @@ pdf = [
"matplotlib>=3.8",
]
full = [
- "pywin32>=306",
+ # Same marker as `com` above, and for the same reason: `[full]` is the
+ # extra this project recommends to contributors, and it was
+ # uninstallable on every non-Windows machine.
+ 'pywin32>=306; sys_platform == "win32"',
"Pillow>=10.0",
"matplotlib>=3.8",
]
diff --git a/server.json b/server.json
index 30cd859..0fb614e 100644
--- a/server.json
+++ b/server.json
@@ -7,12 +7,12 @@
"url": "https://github.com/U-C4N/Autocad-MCP",
"source": "github"
},
- "version": "1.5.0",
+ "version": "1.5.1",
"packages": [
{
"registryType": "pypi",
"identifier": "autocad-mcp-pro",
- "version": "1.5.0",
+ "version": "1.5.1",
"transport": {
"type": "stdio"
},
diff --git a/server.py b/server.py
index 628cf73..b6bf774 100644
--- a/server.py
+++ b/server.py
@@ -6599,9 +6599,47 @@ _HTTP_TRANSPORTS = {"http", "sse", "streamable-http"}
_orig_run_async = mcp.run_async
+def _effective_http_host(kwargs: dict) -> str:
+ """The address that will actually be bound — not the one we hope for.
+
+ This used to read ``kwargs.get("host", "127.0.0.1")`` and treat an absent
+ kwarg as loopback. It is not: ``fastmcp run`` forwards ``host`` only when
+ ``--host`` was given, and otherwise fastmcp resolves it from
+ ``fastmcp.settings.host``, which is environment-backed. So
+ ``FASTMCP_HOST=0.0.0.0`` bound every interface while this guard inspected
+ the literal string ``"127.0.0.1"`` and let it through — the check failed
+ *open*, without a token, on the exact launch path it was written for.
+
+ If the setting cannot be resolved we fall back to loopback rather than
+ refusing to start, but we say so: a guard that cannot see the host is not
+ enforcing anything, and silence is how the original bug survived.
+ """
+ host = kwargs.get("host")
+ if host:
+ return str(host)
+ try:
+ import fastmcp
+
+ resolved = getattr(fastmcp.settings, "host", None)
+ if resolved:
+ return str(resolved)
+ except Exception as exc: # pragma: no cover - defensive
+ log.warning(
+ "Could not resolve the effective HTTP host from fastmcp settings (%s); "
+ "assuming loopback. Pass --host explicitly to be sure of the bind guard.",
+ exc,
+ )
+ return "127.0.0.1"
+ log.warning(
+ "fastmcp exposes no resolvable host setting; assuming loopback. Pass "
+ "--host explicitly to be sure of the bind guard."
+ )
+ return "127.0.0.1"
+
+
async def _guarded_run_async(transport=None, *args, **kwargs):
if (transport in _HTTP_TRANSPORTS) or (kwargs.get("transport") in _HTTP_TRANSPORTS):
- _validate_http_bind(kwargs.get("host", "127.0.0.1"))
+ _validate_http_bind(_effective_http_host(kwargs))
return await _orig_run_async(transport, *args, **kwargs)
diff --git a/tests/test_drawing_settings.py b/tests/test_drawing_settings.py
index ea8dae8..d0c5fec 100644
--- a/tests/test_drawing_settings.py
+++ b/tests/test_drawing_settings.py
@@ -78,3 +78,62 @@ async def test_server_tool_reads_and_writes(backend):
assert write["ok"] is True
read = await server.drawing_settings(ctx=ctx)
assert read["settings"]["units"]["name"] == "mm"
+
+
+# ── DIMSCALE has to reach the dimension, not just the header ────────────────
+
+
+def _dim_text_heights(backend, handle) -> list[float]:
+ """The text heights actually rendered into the dimension's geometry block.
+
+ The override lands in the dimension's XDATA, not on `dim.dxf`, so the only
+ honest check is the drawing it produces.
+ """
+ dim = backend._doc.entitydb.get(handle)
+ block = backend._doc.blocks.get(dim.dxf.geometry)
+ heights = []
+ for entity in block:
+ if entity.dxftype() == "MTEXT":
+ heights.append(round(float(entity.dxf.char_height), 3))
+ elif entity.dxftype() == "TEXT":
+ heights.append(round(float(entity.dxf.height), 3))
+ return sorted(set(heights))
+
+
+async def test_dimscale_changes_the_dimension_it_is_set_before(backend):
+ """`drawing_settings({"dimscale": ...})` reported success and did nothing.
+
+ It writes the `$DIMSCALE` header variable, which is what AutoCAD reads when
+ it creates a dimension — so on the live backend the call worked. ezdxf
+ renders dimensions from the dimstyle table entry and never consults the
+ header, so headlessly the text stayed the same size however large the
+ number was: one call, one reported success, two different drawings.
+ """
+ plain = await backend.dimension_linear(0, 0, 100, 0, 50, -30)
+ await backend.drawing_settings({"dimscale": 4.0})
+ scaled = await backend.dimension_linear(0, 0, 100, 0, 50, -60)
+
+ before = _dim_text_heights(backend, plain.handle)
+ after = _dim_text_heights(backend, scaled.handle)
+ assert before and after
+ assert after == pytest.approx([h * 4.0 for h in before]), (
+ f"DIMSCALE=4 must scale the dimension text: {before} -> {after}"
+ )
+
+
+async def test_the_default_dimscale_adds_no_override(backend):
+ """DIMSCALE 1.0 is the default; it must not litter every dimension."""
+ plain = await backend.dimension_linear(0, 0, 100, 0, 50, -30)
+
+ dim = backend._doc.entitydb.get(plain.handle)
+ assert not dim.dxf.hasattr("dimscale") or dim.dxf.dimscale == pytest.approx(1.0)
+
+
+async def test_dimscale_reaches_aligned_and_angular_too(backend):
+ """Those two do not go through `build_dim_override`, so they were missed."""
+ await backend.drawing_settings({"dimscale": 3.0})
+ aligned = await backend.dimension_aligned(0, 0, 60, 60, 40, 70)
+ angular = await backend.dimension_angular(0, 0, 50, 0, 0, 50, 30, 30)
+
+ for handle in (aligned.handle, angular.handle):
+ assert _dim_text_heights(backend, handle) == pytest.approx([3.0])
diff --git a/tests/test_http_auth.py b/tests/test_http_auth.py
index 32a34a7..56979e9 100644
--- a/tests/test_http_auth.py
+++ b/tests/test_http_auth.py
@@ -179,3 +179,70 @@ def test_validate_http_bind_remote_with_token_logs_warning(caplog):
finally:
cfg.settings.allow_remote_http = old_remote
cfg.settings.mcp_auth_token = old_token
+
+
+# ── the guard must read the host that will actually be bound ────────────────
+#
+# `_guarded_run_async` exists to cover the `fastmcp run server.py:mcp
+# --transport http` path, which the module docstring and CLAUDE.md both
+# advertise. It inferred the bind address from the `host` kwarg and treated an
+# absent one as loopback -- but fastmcp only forwards that kwarg when `--host`
+# was passed, and otherwise resolves the host from `fastmcp.settings.host`,
+# which is environment-backed (`FASTMCP_HOST`). So `FASTMCP_HOST=0.0.0.0`
+# bound every interface while the guard inspected the string "127.0.0.1" and
+# waved it through: the check failed OPEN, with no token required, on the one
+# path it was written for.
+
+
+def _guard_probe(monkeypatch, *, fastmcp_host, allow_remote=False, token=""):
+ """Drive `mcp.run_async` without starting a server."""
+ sys.modules.pop("server", None)
+ import fastmcp
+
+ import config as cfg
+ import server as srv
+
+ monkeypatch.setattr(fastmcp.settings, "host", fastmcp_host, raising=False)
+ monkeypatch.setattr(cfg.settings, "allow_remote_http", allow_remote, raising=False)
+ monkeypatch.setattr(cfg.settings, "mcp_auth_token", token, raising=False)
+
+ async def _never_runs(*args, **kwargs):
+ raise AssertionError("the server started despite the guard")
+
+ monkeypatch.setattr(srv, "_orig_run_async", _never_runs, raising=False)
+ return srv
+
+
+@pytest.mark.asyncio
+async def test_an_omitted_host_is_resolved_not_assumed_to_be_loopback(monkeypatch):
+ """FASTMCP_HOST=0.0.0.0 with no --host must still be refused."""
+ srv = _guard_probe(monkeypatch, fastmcp_host="0.0.0.0")
+
+ with pytest.raises(SystemExit, match="Refusing"):
+ await srv.mcp.run_async(transport="http")
+
+
+@pytest.mark.asyncio
+async def test_an_omitted_host_on_a_loopback_default_still_starts(monkeypatch):
+ """The fix must not refuse the ordinary case."""
+ srv = _guard_probe(monkeypatch, fastmcp_host="127.0.0.1")
+
+ with pytest.raises(AssertionError, match="the server started"):
+ await srv.mcp.run_async(transport="http")
+
+
+@pytest.mark.asyncio
+async def test_an_explicit_host_still_wins_over_the_setting(monkeypatch):
+ """An explicit loopback --host must not be overridden by the env default."""
+ srv = _guard_probe(monkeypatch, fastmcp_host="0.0.0.0")
+
+ with pytest.raises(AssertionError, match="the server started"):
+ await srv.mcp.run_async(transport="http", host="127.0.0.1")
+
+
+@pytest.mark.asyncio
+async def test_stdio_never_consults_the_http_guard(monkeypatch):
+ srv = _guard_probe(monkeypatch, fastmcp_host="0.0.0.0")
+
+ with pytest.raises(AssertionError, match="the server started"):
+ await srv.mcp.run_async(transport="stdio")
diff --git a/tests/test_release_consistency.py b/tests/test_release_consistency.py
index 10d1a1d..0ba07cf 100644
--- a/tests/test_release_consistency.py
+++ b/tests/test_release_consistency.py
@@ -245,3 +245,26 @@ def test_dockerfile_copies_every_wheel_include() -> None:
includes = _pyproject()["tool"]["hatch"]["build"]["targets"]["wheel"]["only-include"]
missing = [name for name in includes if name not in copy_lines]
assert not missing, f"Dockerfile COPY misses wheel-shipped paths: {missing}"
+
+
+def test_windows_only_dependencies_carry_a_platform_marker() -> None:
+ """`pip install autocad-mcp-pro[full]` must resolve on Linux and macOS.
+
+ pywin32 publishes Windows wheels and **no sdist**, so an unmarked
+ `pywin32>=306` in an extra makes that extra uninstallable everywhere else:
+ pip fails at resolution with "no matching distribution" before it reaches
+ anything else. `[full]` is the extra this project's own README recommends
+ to contributors, and CI never caught it because every Linux lane installs
+ `[pdf]` or uses uv.lock, whose resolution markers paper over the gap.
+ """
+ extras = _pyproject()["project"]["optional-dependencies"]
+ unmarked: list[str] = []
+ for extra, requirements in extras.items():
+ for requirement in requirements:
+ name = re.split(r"[<>=!~\[; ]", requirement, maxsplit=1)[0].strip().lower()
+ if name in {"pywin32"} and "sys_platform" not in requirement:
+ unmarked.append(f"{extra}: {requirement}")
+ assert not unmarked, (
+ 'Windows-only distributions need `; sys_platform == "win32"` or the '
+ f"extra cannot be installed off Windows: {unmarked}"
+ )
diff --git a/uv.lock b/uv.lock
index 1f12c53..dc2a74d 100644
--- a/uv.lock
+++ b/uv.lock
@@ -67,7 +67,7 @@ wheels = [
[[package]]
name = "autocad-mcp-pro"
-version = "1.5.0"
+version = "1.5.1"
source = { editable = "." }
dependencies = [
{ name = "ezdxf" },
@@ -78,12 +78,12 @@ dependencies = [
[package.optional-dependencies]
com = [
{ name = "pillow" },
- { name = "pywin32" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
]
full = [
{ name = "matplotlib" },
{ name = "pillow" },
- { name = "pywin32" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
]
pdf = [
{ name = "matplotlib" },
@@ -106,8 +106,8 @@ requires-dist = [
{ name = "pillow", marker = "extra == 'com'", specifier = ">=10.0" },
{ name = "pillow", marker = "extra == 'full'", specifier = ">=10.0" },
{ name = "pydantic", specifier = ">=2.0,<3" },
- { name = "pywin32", marker = "extra == 'com'", specifier = ">=306" },
- { name = "pywin32", marker = "extra == 'full'", specifier = ">=306" },
+ { name = "pywin32", marker = "sys_platform == 'win32' and extra == 'com'", specifier = ">=306" },
+ { name = "pywin32", marker = "sys_platform == 'win32' and extra == 'full'", specifier = ">=306" },
]
provides-extras = ["com", "pdf", "full"]