From 2f56a7309336722a78104e03d5a92939d70dc59d Mon Sep 17 00:00:00 2001 From: Umutcan Edizsalan Date: Thu, 23 Jul 2026 15:15:07 +0300 Subject: [PATCH] feat(release): CI matrix, PyPI trusted-publishing pipeline, registry manifest, Docker fix - ci.yml: ruff lint/format, Linux 3.11/3.12 tests + coverage, Windows mocked-COM leg, package build + clean-venv wheel smoke, Docker smoke, server.json schema validation - release.yml: tag -> test gate -> tag/version check -> build -> PyPI trusted publishing (OIDC) -> GitHub Release - pyproject: SPDX license, authors, keywords, classifiers, project URLs, autocad-mcp-pro console alias; version 1.4.0 - Dockerfile: ship engineering/ + version.py (image previously missed both -> engineering tools failed in containers); healthcheck imports engineering - server.json: MCP registry manifest (io.github.u-c4n/autocad-mcp, pypi package) - docs/RELEASE-DISTRIBUTION.md: one-time owner actions (PyPI trusted publisher, mcp-publisher login) + directory submissions runbook Co-authored-by: Cursor --- .env.example | 8 +++ .github/workflows/ci.yml | 110 ++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 88 +++++++++++++++++++++++++++ .gitignore | 6 ++ Dockerfile | 7 ++- docs/RELEASE-DISTRIBUTION.md | 51 ++++++++++++++++ pyproject.toml | 38 +++++++++++- server.json | 44 ++++++++++++++ 8 files changed, 348 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 docs/RELEASE-DISTRIBUTION.md create mode 100644 server.json 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 + } + ] + } + ] +}