diff --git a/.env.example b/.env.example
index 3973822..387fe61 100644
--- a/.env.example
+++ b/.env.example
@@ -17,3 +17,11 @@ LOG_LEVEL=INFO
# Enable dangerous AutoCAD commands (ERASE ALL, QUIT, etc.)
# Set to true only if you trust all MCP clients
DANGEROUS_COMMANDS_ENABLED=false
+
+# Exposed tool profile: full (default) | core | lean
+# core hides raw escape hatches and long-tail tools; lean is a curated
+# ~46-tool drafting/inspection core for clients with tight tool caps.
+TOOL_PROFILE=full
+
+# Opt-in 3D solids (COM backend only; ezdxf reports them as unsupported)
+ENABLE_3D=false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..e6beec1
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,110 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
+
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ - run: pip install ruff
+ - run: ruff check .
+ - run: ruff format --check .
+
+ test-linux:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.11", "3.12"]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ pip install -e ".[pdf]"
+ pip install pytest pytest-asyncio pytest-cov
+ - name: Run tests
+ run: pytest --cov=. --cov-report=xml --cov-report=term-missing -v
+ - name: Upload coverage
+ if: matrix.python-version == '3.11'
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-report
+ path: coverage.xml
+
+ test-windows:
+ # Windows leg exercises the mocked-COM suite on the platform the COM
+ # backend actually targets (pywin32 installs; no live AutoCAD needed).
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ - name: Install dependencies
+ run: |
+ pip install -e ".[full]"
+ pip install pytest pytest-asyncio pytest-cov
+ - name: Run tests
+ run: pytest -v
+
+ package:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ - name: Build sdist and wheel
+ run: |
+ pip install build twine
+ python -m build
+ twine check dist/*
+ - name: Install wheel in a clean venv and smoke test
+ run: |
+ python -m venv /tmp/smoke
+ /tmp/smoke/bin/pip install dist/*.whl
+ /tmp/smoke/bin/autocad-mcp --help
+ /tmp/smoke/bin/python -c "import server, config, security, version, backends, engineering; print('wheel ok', version.__version__)"
+ - name: Upload distributions
+ uses: actions/upload-artifact@v4
+ with:
+ name: dist
+ path: dist/
+
+ docker:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Build image
+ run: docker build -t autocad-mcp:ci .
+ - name: Smoke test image
+ run: docker run --rm autocad-mcp:ci python -c "import server, engineering.gear, version; print('docker ok', version.__version__)"
+
+ registry-validate:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ - name: Validate server.json against MCP registry schema
+ if: ${{ hashFiles('server.json') != '' }}
+ run: |
+ pip install check-jsonschema
+ check-jsonschema --schemafile https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json server.json
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..851af91
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,88 @@
+name: Release
+
+# Tag-driven release: run the test gate, build, publish to PyPI via trusted
+# publishing (OIDC — no API token stored in the repo), then cut the GitHub
+# Release with the built artifacts attached.
+#
+# One-time setup on PyPI (manual): project "autocad-mcp-pro" -> Publishing ->
+# add GitHub publisher repo=U-C4N/Autocad-MCP workflow=release.yml
+# environment=pypi.
+
+on:
+ push:
+ tags: ["v*"]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ - name: Install dependencies
+ run: |
+ pip install -e ".[pdf]"
+ pip install pytest pytest-asyncio ruff
+ - run: ruff check .
+ - run: pytest -q
+
+ build:
+ needs: test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ - name: Verify tag matches package version
+ run: |
+ pip install tomli-w 2>/dev/null || true
+ PKG_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
+ if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
+ echo "Tag $GITHUB_REF_NAME does not match pyproject version $PKG_VERSION" >&2
+ exit 1
+ fi
+ - name: Build distributions
+ run: |
+ pip install build twine
+ python -m build
+ twine check dist/*
+ - uses: actions/upload-artifact@v4
+ with:
+ name: dist
+ path: dist/
+
+ publish-pypi:
+ needs: build
+ runs-on: ubuntu-latest
+ environment: pypi
+ permissions:
+ id-token: write
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ name: dist
+ path: dist/
+ - name: Publish to PyPI (trusted publishing)
+ uses: pypa/gh-action-pypi-publish@release/v1
+
+ github-release:
+ needs: publish-pypi
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/download-artifact@v4
+ with:
+ name: dist
+ path: dist/
+ - name: Create GitHub Release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ gh release create "$GITHUB_REF_NAME" dist/* \
+ --title "$GITHUB_REF_NAME" \
+ --generate-notes
diff --git a/.gitignore b/.gitignore
index f5fa673..a81c197 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,6 +43,12 @@ desktop.ini
out/
benchmarks/results/*
!benchmarks/results/.gitkeep
+!benchmarks/results/published
+!benchmarks/results/published/**
+benchmarks/.competitors/
*.dxf
*.dwg
*.bak
+
+# Local editor/agent state
+.cursor/
diff --git a/Dockerfile b/Dockerfile
index 294de7b..814afc7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,9 +4,10 @@ WORKDIR /app
RUN useradd --create-home --shell /bin/bash mcp
-COPY pyproject.toml ./
-COPY server.py config.py security.py ./
+COPY pyproject.toml README.md LICENSE ./
+COPY server.py config.py security.py version.py ./
COPY backends/ ./backends/
+COPY engineering/ ./engineering/
RUN pip install --no-cache-dir -e .
@@ -15,6 +16,6 @@ USER mcp
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
- CMD python -c "import server; print('ok')" || exit 1
+ CMD python -c "import server, engineering.gear; print('ok')" || exit 1
CMD ["python", "server.py"]
diff --git a/docs/RELEASE-DISTRIBUTION.md b/docs/RELEASE-DISTRIBUTION.md
new file mode 100644
index 0000000..37c0140
--- /dev/null
+++ b/docs/RELEASE-DISTRIBUTION.md
@@ -0,0 +1,51 @@
+# Release & Distribution Runbook (v1.4.0+)
+
+The code side of a release is fully automated; three account-bound steps need
+a human once, plus the directory submissions. Track them here.
+
+## One-time setup (owner action)
+
+- [ ] **PyPI trusted publisher** — on
+ add a *pending publisher* for project `autocad-mcp-pro`:
+ repository `U-C4N/Autocad-MCP`, workflow `release.yml`, environment `pypi`.
+ (No API token is stored anywhere; `release.yml` uses OIDC.)
+- [ ] **GitHub environment** — create the `pypi` environment in the repo
+ settings (Settings → Environments) so the publish job can bind to it.
+- [ ] **MCP registry login** — install `mcp-publisher`, then
+ `mcp-publisher login github` (authenticates the `io.github.u-c4n` namespace).
+
+## Every release
+
+1. Bump `pyproject.toml` + `server.json` versions, add the CHANGELOG section,
+ update the README snapshot line — `tests/test_release_consistency.py`
+ enforces all of it.
+2. Push `main`, wait for CI to go green (lint, linux, windows, package,
+ docker, registry-validate).
+3. Tag and push: `git tag v1.4.0 && git push origin v1.4.0`.
+ `release.yml` runs the test gate, verifies tag == package version, builds,
+ publishes to PyPI via trusted publishing and cuts the GitHub Release.
+4. Verify: `pip install autocad-mcp-pro==` in a clean venv;
+ `uvx autocad-mcp-pro --help`.
+5. Publish the registry manifest: `mcp-publisher publish` (validates
+ `server.json`, checks the `mcp-name` marker in the PyPI README).
+
+## Directory listings (submit once, then keep fresh)
+
+- [ ] Glama — (auto-indexes GitHub; claim the
+ server page and verify metadata).
+- [ ] mcpservers.org — submit via their GitHub repo PR flow.
+- [ ] awesome-mcp-servers — PR adding the server under CAD/engineering.
+- [ ] Smithery — optional; requires a `smithery.yaml` (evaluate demand first).
+
+## Benchmark refresh (per release)
+
+```bash
+python -m benchmarks.run_competitors --server autocad-mcp-pro --backend ezdxf
+python -m benchmarks.run_competitors --server puran-water-autocad-mcp --backend ezdxf
+python -m benchmarks.run_competitors --server beiming183-autocad-mcp --backend ezdxf
+# sanitize + copy reports into benchmarks/results/published/, then:
+python -m benchmarks.render_live_chart
+```
+
+Bump the competitor pins in `benchmarks/competitors.yaml` deliberately (new
+SHA = re-read their tool contracts before trusting the mappings).
diff --git a/pyproject.toml b/pyproject.toml
index 772130b..1337164 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,15 +1,48 @@
[project]
name = "autocad-mcp-pro"
-version = "1.3.0"
+version = "1.4.0"
description = "Production-grade AutoCAD MCP server — COM API + ezdxf dual engine"
readme = "README.md"
requires-python = ">=3.11"
+license = "MIT"
+license-files = ["LICENSE"]
+authors = [
+ { name = "Umutcan Edizsalan" },
+]
+keywords = [
+ "autocad",
+ "mcp",
+ "model-context-protocol",
+ "cad",
+ "dxf",
+ "ezdxf",
+ "drafting",
+ "engineering-drawing",
+]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Manufacturing",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Topic :: Scientific/Engineering",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+]
dependencies = [
"fastmcp>=3.0",
"ezdxf>=1.3",
"pydantic>=2.0",
]
+[project.urls]
+Homepage = "https://github.com/U-C4N/Autocad-MCP"
+Repository = "https://github.com/U-C4N/Autocad-MCP"
+Issues = "https://github.com/U-C4N/Autocad-MCP/issues"
+Changelog = "https://github.com/U-C4N/Autocad-MCP/blob/main/CHANGELOG.md"
+
[project.optional-dependencies]
com = [
"pywin32>=306",
@@ -26,6 +59,9 @@ full = [
[project.scripts]
autocad-mcp = "server:main"
+# Alias matching the PyPI distribution name so `uvx autocad-mcp-pro` works
+# without a --from indirection (MCP registry / uvx convention).
+autocad-mcp-pro = "server:main"
[tool.uv]
dev-dependencies = [
diff --git a/server.json b/server.json
new file mode 100644
index 0000000..5a82ce9
--- /dev/null
+++ b/server.json
@@ -0,0 +1,44 @@
+{
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
+ "name": "io.github.u-c4n/autocad-mcp",
+ "title": "AutoCAD MCP Pro",
+ "description": "AutoCAD automation for AI agents: live COM or headless ezdxf, ISO dims, GD&T, quality gate.",
+ "repository": {
+ "url": "https://github.com/U-C4N/Autocad-MCP",
+ "source": "github"
+ },
+ "version": "1.4.0",
+ "packages": [
+ {
+ "registryType": "pypi",
+ "identifier": "autocad-mcp-pro",
+ "version": "1.4.0",
+ "transport": {
+ "type": "stdio"
+ },
+ "environmentVariables": [
+ {
+ "name": "AUTOCAD_MCP_BACKEND",
+ "description": "Backend selection: auto (default), com (live AutoCAD, Windows) or ezdxf (headless DXF).",
+ "isRequired": false,
+ "format": "string",
+ "isSecret": false
+ },
+ {
+ "name": "ALLOWED_PATHS",
+ "description": "Comma-separated absolute paths the server may read and write. Empty blocks file access outside temp.",
+ "isRequired": false,
+ "format": "string",
+ "isSecret": false
+ },
+ {
+ "name": "TOOL_PROFILE",
+ "description": "Exposed tool profile: full (default), core or lean.",
+ "isRequired": false,
+ "format": "string",
+ "isSecret": false
+ }
+ ]
+ }
+ ]
+}