pythonparts-mcp 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,47 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ .Python
8
+ build/
9
+ dist/
10
+ *.egg-info/
11
+ *.egg
12
+
13
+ # Virtual environments
14
+ .venv/
15
+ venv/
16
+ env/
17
+
18
+ # Environment variables
19
+ .env
20
+
21
+ # Pyright / type checking
22
+ .pyright/
23
+
24
+ # pytest
25
+ .pytest_cache/
26
+ htmlcov/
27
+ .coverage
28
+
29
+ # mypy
30
+ .mypy_cache/
31
+
32
+ # IDEs
33
+ .vscode/settings.json
34
+ .idea/
35
+
36
+ # OS
37
+ .DS_Store
38
+ Thumbs.db
39
+
40
+ # Logs
41
+ *.log
42
+
43
+ # Visual Studio Code
44
+ .vscode/
45
+
46
+ # Other
47
+ .temp/
@@ -0,0 +1,56 @@
1
+ # Changelog
2
+
3
+ All notable changes to `pythonparts-mcp` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.0] — 2026-09-16
11
+
12
+ ### Added
13
+
14
+ - **`run_snippet_in_sandbox`** — runs a Python snippet against the ALLPLAN libraries and
15
+ returns its `print()` output, so an agent can check geometry math or API behaviour instead
16
+ of guessing at it.
17
+
18
+ The snippet runs in a separate process under ALLPLAN's own interpreter, which means
19
+ ALLPLAN does not need to be running and the snippet cannot reach the user's drawing or
20
+ project data. The ALLPLAN interface modules are pre-imported as globals; `document` and
21
+ `coord_input` do not exist, and calls that open a user interface will hang until the
22
+ 60-second timeout. Each call gets a fresh set of globals, so snippets must be
23
+ self-contained.
24
+
25
+ The worker process is started on first use and reused, so only the first call pays for
26
+ loading the ALLPLAN DLLs.
27
+
28
+ ### Fixed
29
+
30
+ - `inspect_symbol` declared a 30-second timeout but never applied it, so a wedged worker
31
+ could hang the server indefinitely. The timeout is now enforced, and a worker that times
32
+ out, crashes or desyncs is replaced on the next call.
33
+
34
+ ## [0.1.2] — 2026-09-03
35
+
36
+ ### Fixed
37
+
38
+ - FastMCP imports updated for the current FastMCP release.
39
+
40
+ ## [0.1.1] — 2026-08-06
41
+
42
+ ### Changed
43
+
44
+ - `get_runtime_context` split into `get_allplan_info`, `get_python_env_info` and
45
+ `list_site_packages`.
46
+ - `inspect_symbol` now keeps one worker process alive for the lifetime of the server
47
+ instead of spawning one per call.
48
+
49
+ ### Fixed
50
+
51
+ - Corrected the declared project dependencies.
52
+
53
+ ## [0.1.0] — 2026-07-23
54
+
55
+ Initial packaged release, exposing `get_runtime_context`, `typecheck_file` and
56
+ `inspect_symbol`.
@@ -0,0 +1,172 @@
1
+ # PythonParts Local MCP Server — Developer Documentation
2
+
3
+ ## Repository Structure
4
+
5
+ ```
6
+ server/
7
+ __init__.py empty
8
+ __main__.py entry point — arg parsing, startup sequence, tool registration
9
+ config.py AllplanConfig dataclass + module-level singleton
10
+ paths.py interpreter_path(), framework_version() and build_sys_path() helpers
11
+ registry.py Windows registry reader → (prg, etc, program_name)
12
+ pyright_config.py writes temp pyrightconfig.json at startup, cleans up on exit
13
+ worker.py PersistentWorker — subprocess lifecycle shared by the worker-backed tools
14
+ tools/
15
+ __init__.py READ_ONLY_TOOL_ANNOTATIONS shared constant
16
+ runtime_context.py get_runtime_context tool
17
+ typecheck.py typecheck_file tool
18
+ inspect_symbol.py inspect_symbol tool
19
+ _introspect_worker.py worker script run under the ALLPLAN interpreter
20
+ run_snippet_in_allplan.py run_snippet_in_allplan tool — NOT REGISTERED, see below
21
+ run_snippet_in_sandbox.py run_snippet_in_sandbox tool
22
+ _sandbox_worker.py worker script run under the ALLPLAN interpreter
23
+ pyproject.toml
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Dependencies
29
+
30
+ Declared in `pyproject.toml`:
31
+
32
+ - `fastmcp` — FastMCP framework (provides `mcp.*` imports)
33
+
34
+ - `pyright` — static type checker (bundled Node binary, invoked via `python -m pyright`)
35
+ - `pywin32` — Windows API bindings (`win32api` used to read the ALLPLAN product version)
36
+
37
+ ---
38
+
39
+ ## Startup Sequence
40
+
41
+ 1. Parse CLI args
42
+ 2. Read `prg` and `etc` from Windows registry (`HKLM\SOFTWARE\NEMETSCHEK\{product_name}\{version}.0\InstallRoot`)
43
+ 3. If registry key missing → print error to stderr and exit 1
44
+ 4. Initialise `AllplanConfig` singleton
45
+ 5. Write `pyrightconfig.json` to a temp directory; register `atexit` cleanup
46
+ 6. Create `FastMCP` instance, register tools, call `mcp.run()`
47
+
48
+ ---
49
+
50
+ ## The Four ALLPLAN Paths
51
+
52
+ Everything derives from these four `Path` values stored in `AllplanConfig`:
53
+
54
+ | Field | Source | Example |
55
+ |---|---|---|
56
+ | `prg` | Registry `ProgramDrive` + `ProgramPath` | `C:\Program Files\Allplan\Allplan 2027\Prg` |
57
+ | `etc` | Registry `ProgramDataDrive` + `ProgramDataPath` + `/etc` | `C:\ProgramData\Nemetschek\Allplan\2027\etc` |
58
+ | `std` | CLI `--std-path` | `C:\Data\Allplan\Allplan 2027\Std` |
59
+ | `usr` | CLI `--usr-path` | `C:\Users\...\Nemetschek\Allplan\2027\Usr\Local` |
60
+
61
+ `paths.py` derives two shared values from these:
62
+ - `interpreter_path(prg)` → `{prg}/Python/python.exe`
63
+ - `build_sys_path(cfg)` → ordered list of 24 paths matching ALLPLAN's runtime `sys.path`
64
+
65
+ ---
66
+
67
+ ## Tool Implementation Notes
68
+ ### Runtime context tools
69
+
70
+ ### `get_runtime_context()`
71
+ - `get_allplan_info()` — ALLPLAN version + resolved (prg/etc/std/usr) paths
72
+
73
+ - `get_python_env_info()` — embedded interpreter path, version, and derived sys.path
74
+
75
+ - `list_site_packages()` — installed packages discovered by scanning *site-packages* dirs in sys.path
76
+
77
+ (first occurrence wins, matching Python's import resolution order)
78
+
79
+ ### `typecheck_file(file_path: str)`
80
+
81
+ 1. Validates the file exists
82
+ 2. Calls `pyright --outputjson --project <tmpdir> --pythonpath <interpreter> <file>`
83
+ via `python -m pyright` (server's own venv Python) with `stdin=DEVNULL`
84
+ (critical: prevents pyright from inheriting the MCP server's stdio pipe and hanging)
85
+ 3. Extracts JSON from stdout (skips any warning text printed before the `{`)
86
+ 4. Filters known false positives before returning
87
+
88
+ **Pyright config** (written to temp dir at startup):
89
+ ```json
90
+ {
91
+ "stubPath": "{etc}/PythonPartsFramework/InterfaceStubs",
92
+ "extraPaths": ["... full sys_path list ..."],
93
+ "typeCheckingMode": "basic",
94
+ "reportMissingModuleSource": "none"
95
+ }
96
+ ```
97
+ The interpreter path is passed as `--pythonpath` (CLI flag), not in the config file —
98
+ `pythonPath` in `pyrightconfig.json` is not recognised by pyright 1.1.x.
99
+
100
+ **Known false positives suppressed:**
101
+ - `reportAttributeAccessIssue` on `BuildingElement` — its attributes are populated
102
+ dynamically from `.pyp` files at runtime and cannot be statically verified
103
+
104
+ `error_count` and `warning_count` are recomputed from the filtered list, so they match the diagnostics array exactly.
105
+
106
+ ### Worker-backed tools (`inspect_symbol`, `run_snippet_in_sandbox`)
107
+
108
+ ALLPLAN's `NemAll_Python_*` modules are native extensions that only load in the interpreter
109
+ shipped in `Prg\Python`, so these tools cannot run in-process — they drive a subprocess.
110
+ Loading the DLLs costs a second or two, so `PersistentWorker` spawns the worker on first use
111
+ and keeps it alive for the lifetime of the server.
112
+
113
+ The protocol is one JSON line in, one JSON line out. The first line is an init handshake
114
+ carrying `sys_path`, `prg_plugins`, `allplan_paths_mock` and `etc_pythonparts`; the worker
115
+ sets up the runtime and replies with an ack before accepting requests.
116
+
117
+ The worker's stdout is drained by a pump thread into a queue, which is what allows a read to
118
+ be given a deadline — there is no `select()` on pipes on Windows. Timeouts, crashes and
119
+ protocol desync all kill the worker and return a structured error; the next request spawns a
120
+ fresh process. A snippet that crashes its worker therefore fails, and the call after it succeeds.
121
+
122
+ `_sandbox_worker.py` additionally imports the interface modules under the same aliases the
123
+ Vibe Coding Connector's `/execute` route exposes — minus `document` and `coord_input`, which
124
+ cannot exist without a running ALLPLAN. Each snippet is executed with a fresh copy of that
125
+ globals dict, so nothing leaks between invocations.
126
+
127
+ ### `run_snippet_in_allplan` (disabled)
128
+
129
+ Executes a snippet in the user's live ALLPLAN session by POSTing it to the Vibe Coding
130
+ Connector PythonPart's `/execute` endpoint. Unlike the sandbox it has `document` and
131
+ `coord_input`, and can therefore read and modify real project data.
132
+
133
+ Its registration in `__main__.py` is commented out, so it is not exposed by the released
134
+ package and is intentionally absent from `README.md`. Re-enabling it means uncommenting the
135
+ import and the `register_run_snippet_in_allplan_tools(mcp)` call — and documenting it again.
136
+
137
+ ---
138
+
139
+ ## Key Design Decisions
140
+
141
+ | Decision | Rationale |
142
+ |---|---|
143
+ | `stdin=subprocess.DEVNULL` on pyright subprocess | Without this, pyright inherits the MCP server's stdin (the stdio pipe to the MCP client) and hangs indefinitely waiting for input |
144
+ | `pythonPath` as CLI flag, not config key | `pythonPath` in `pyrightconfig.json` is unrecognised in pyright ≥1.1.x; use `--pythonpath` instead |
145
+ | `atexit` for temp dir cleanup | Simpler than FastMCP lifespan hooks; reliable for stdio servers |
146
+ | Filesystem scan instead of `pip list` | ALLPLAN may not have pip; packages are often dropped as folders without going through pip |
147
+ | `sys_path` derived statically | Avoids spawning a subprocess for every `get_runtime_context` call; order matches observed live behaviour |
148
+ | Sandbox runs out-of-process, not in a thread | The ALLPLAN DLLs must be loaded by ALLPLAN's own interpreter, not the server's; a separate process is also what keeps a crashing snippet from taking down the MCP server |
149
+ | Sandbox not gated by `requires_allplan_connector` | It deliberately works with ALLPLAN closed; requiring the connector would defeat the point |
150
+ | Sandbox keeps `MUTATING_TOOL_ANNOTATIONS` | The ALLPLAN document is safe, but the snippet is still arbitrary code with file-system access, so it stays confirmation-gated |
151
+ | Worker stdout drained by a pump thread | Windows cannot `select()` on pipes; queueing the lines is what makes a read deadline possible |
152
+
153
+ ---
154
+
155
+ ## Adding a New Tool
156
+
157
+ 1. Create `server/tools/<name>.py`
158
+ 2. Define `register_<name>_tools(mcp: FastMCP) -> None` with `@mcp.tool()` decorated functions inside
159
+ 3. Import and call the registration function in `server/__main__.py`
160
+ 4. Access shared state via `get_config()` (paths) and `get_pyright_config_dir()` / `get_python_interpreter()` (pyright)
161
+
162
+ ---
163
+
164
+ ## Planned / Not Yet Implemented
165
+
166
+ From the [original design brief](docs/MCP_Python_Environment_Aware_Tooling_Brief.md):
167
+
168
+ - `resolve_import` — confirm a module/symbol is importable; where it resolves
169
+ - `run_tests` — targeted test runner
170
+ - `search_repo_usages` — find in-repo usage patterns for a symbol
171
+ - VS Code extension wrapper for distribution (`.vsix` with bundled server)
172
+ - Response caching for `get_runtime_context` (currently re-computed on every call)
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.5
2
+ Name: pythonparts-mcp
3
+ Version: 0.2.0
4
+ Summary: MCP server exposing the ALLPLAN Python runtime to AI coding assistants
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: fastmcp>=3.0
7
+ Requires-Dist: pyright>=1.1
8
+ Requires-Dist: pywin32>=306; sys_platform == 'win32'
9
+ Description-Content-Type: text/markdown
10
+
11
+ # PythonParts Local MCP Server
12
+
13
+ An MCP server that exposes the local ALLPLAN Python runtime to AI coding assistants.
14
+ It is started once per ALLPLAN version and gives any MCP-compatible agent accurate,
15
+ live information about the interpreter, import paths, installed packages, and type
16
+ correctness — without guessing or hallucinating.
17
+
18
+ Transport: **stdio** (spawned as a subprocess by the MCP client).
19
+ Protocol: [Model Context Protocol](https://modelcontextprotocol.io/) via `FastMCP`.
20
+ Release history: [CHANGELOG.md](CHANGELOG.md).
21
+
22
+ ---
23
+
24
+ ## Prerequisites
25
+
26
+ - [**uv**](https://docs.astral.sh/uv/getting-started/installation/) — Python package manager used to run the server without a manual install step.
27
+
28
+ ```powershell
29
+ winget install astral-sh.uv
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Attaching to your AI agent
35
+
36
+ Add the following entry to your MCP configuration:
37
+
38
+ ```json
39
+ "pythonparts-local-mcp": {
40
+ "type": "stdio",
41
+ "command": "uvx",
42
+ "args": [
43
+ "--index", "https://test.pypi.org/simple",
44
+ "--index-strategy", "unsafe-best-match",
45
+ "pythonparts-mcp",
46
+ "--allplan-version", "2026",
47
+ "--std-path", "X:\\Path\\To\\Allplan\\Std",
48
+ "--usr-path", "X:\\Path\\To\\Allplan\\Usr\\Local"
49
+ ]
50
+ }
51
+ ```
52
+
53
+ **Arguments:**
54
+
55
+ | Argument | Description | Example |
56
+ |---|---|---|
57
+ | `--allplan-version` | Installed ALLPLAN version | `2026` |
58
+ | `--std-path` | Path to ALLPLAN's `Std` directory | `C:\Data\Allplan\Allplan 2026\Std` |
59
+ | `--usr-path` | Path to ALLPLAN's user directory. Located under `Usr\<username>` or `Usr\Local` depending on your ALLPLAN configuration. | `C:\Users\Jane\Nemetschek\Allplan\2026\Usr\Local` |
60
+
61
+ The `prg` and `etc` paths are resolved automatically from the Windows registry.
62
+
63
+ ---
64
+
65
+ ## Available Tools
66
+
67
+ ### `get_allplan_info()`
68
+
69
+ Returns the installed ALLPLAN version string and the four resolved file-system paths (`prg`, `etc`, `std`, `usr`). No input required.
70
+
71
+ ```json
72
+ {
73
+ "ok": true,
74
+ "data": {
75
+ "version": "2026.0.0",
76
+ "paths": {
77
+ "prg": "C:\\Program Files\\Allplan\\Allplan 2026\\Prg",
78
+ "etc": "C:\\ProgramData\\Nemetschek\\Allplan\\2026\\etc",
79
+ "std": "X:\\Path\\To\\Allplan\\Std",
80
+ "usr": "X:\\Path\\To\\Allplan\\Usr\\Local"
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ ### `get_python_env_info()`
87
+
88
+ Returns the path to the ALLPLAN-embedded Python interpreter, its version, and the ordered `sys.path` ALLPLAN uses when executing PythonParts scripts. No input required.
89
+
90
+ ```json
91
+ {
92
+ "ok": true,
93
+ "data": {
94
+ "interpreter_path": "C:\\...\\Prg\\Python\\python.exe",
95
+ "python_version": "3.11.9",
96
+ "sys_path": ["C:\\...\\Prg\\Python", "..."]
97
+ }
98
+ }
99
+ ```
100
+
101
+ ### `list_site_packages()`
102
+
103
+ Lists all Python packages installed in the ALLPLAN-embedded Python environment, deduplicated and sorted alphabetically. No input required.
104
+
105
+ ```json
106
+ {
107
+ "ok": true,
108
+ "data": [
109
+ {"name": "requests", "version": "2.31.0"},
110
+ {"name": "shapely", "version": null}
111
+ ]
112
+ }
113
+ ```
114
+
115
+ ### `typecheck_file(file_path: str)`
116
+
117
+ Type-checks a `.py` file against the ALLPLAN runtime using Pyright. Input: absolute path to the file.
118
+
119
+ ```json
120
+ {
121
+ "ok": true,
122
+ "data": {
123
+ "error_count": 2,
124
+ "warning_count": 0,
125
+ "diagnostics": [
126
+ {
127
+ "severity": "error",
128
+ "message": "...",
129
+ "file": "C:\\...\\main.py",
130
+ "line": 12,
131
+ "column": 5,
132
+ "rule": "reportMissingImports"
133
+ }
134
+ ]
135
+ }
136
+ }
137
+ ```
138
+
139
+ ### `inspect_symbol(symbol: str)`
140
+
141
+ Inspects a Python symbol in the live ALLPLAN runtime — returns its category, signature, docstring, and public members. Use this to verify a symbol exists and discover its API before writing code against it.
142
+
143
+ Input: dotted Python path to the symbol.
144
+
145
+ ```json
146
+ {
147
+ "ok": true,
148
+ "data": {
149
+ "symbol": "NemAll_Python_Geometry.Point3D",
150
+ "category": "class",
151
+ "module": "NemAll_Python_Geometry",
152
+ "docstring": "...",
153
+ "signature": null,
154
+ "members": ["X", "Y", "Z", "..."]
155
+
156
+
157
+
158
+ }
159
+ }
160
+ ```
161
+
162
+ ### `run_snippet_in_sandbox(code: str)`
163
+
164
+ Runs a Python snippet against the ALLPLAN libraries in a **separate process, outside ALLPLAN**, and returns its `print()` output. ALLPLAN does not need to be running, and the snippet cannot touch the open drawing or project data. This is the tool to reach for when verifying geometry math or API behaviour.
165
+
166
+ The ALLPLAN interface modules are pre-imported as globals (`AllplanGeometry`, `AllplanBasisElements`, …). There is no document and no coordinate input, so `document` and `coord_input` are **not** available — anything that needs the open drawing, the project or user settings will fail or return a meaningless value.
167
+
168
+ Globals do not persist between calls, so each snippet must be self-contained. The first call spends a second or two loading the ALLPLAN DLLs; the process is then reused. Snippets are abandoned after 60 seconds.
169
+
170
+ Snippets must not show a user interface — `AllplanUtility.ShowMessageBox`, file dialogs and the like. There is no ALLPLAN window to host them, so the call never returns and the snippet runs into the timeout.
171
+
172
+ ```json
173
+ {
174
+ "ok": true,
175
+ "data": {
176
+ "output": "(30000.0, NemAll_Python_Geometry.eServiceResult.POSITIVE_ORIENTATION)\n"
177
+ }
178
+ }
179
+ ```
@@ -0,0 +1,91 @@
1
+ # Publishing `pythonparts-mcp`
2
+
3
+ Release uploads authenticate with **PyPI Trusted Publishers (OIDC)**.
4
+ The workflow does not read or pass a PyPI API token.
5
+
6
+ Workflow: [`.github/workflows/publish-pythonparts-mcp.yml`](../.github/workflows/publish-pythonparts-mcp.yml)
7
+ Package: [`pyproject.toml`](pyproject.toml) (`name = "pythonparts-mcp"`)
8
+
9
+ A push to `dev` that changes `local-server/` or the publish workflows builds,
10
+ tests, and uploads the `pythonparts-mcp-dist` artifact for 7 days. That run
11
+ is ungated and publishes nowhere. See
12
+ [`.github/workflows/upload-pythonparts-mcp-dev.yml`](../.github/workflows/upload-pythonparts-mcp-dev.yml).
13
+
14
+ Production publishes only when a `pythonparts-mcp-v*` tag is pushed, and only
15
+ after a `prod` reviewer approves the deployment. The publish job depends on
16
+ the test job, so a failed test, version mismatch, or build does not upload.
17
+
18
+ ## Trusted Publisher (exact match)
19
+
20
+ PyPI.org must trust this identity. The environment name is part of the
21
+ OIDC claim, so it has to match the GitHub Environment on the publish job.
22
+ TestPyPI is not used for now.
23
+
24
+ | Field | PyPI.org |
25
+ |---|---|
26
+ | Owner | `Allplan-GmbH` |
27
+ | Repository | `InnovationAI-VibeCoding` |
28
+ | Workflow filename | `publish-pythonparts-mcp.yml` |
29
+ | Environment | `prod` |
30
+
31
+ The publish job uses the existing `prod` environment. Its required reviewers
32
+ are the people who can deploy to prod, and they decide who can publish.
33
+ Under **Deployment branches and tags** for `prod`, allow the tag
34
+ `pythonparts-mcp-v*`. A rule that only allows branch `main` will reject the
35
+ tag and the publish job will not start.
36
+
37
+ The publish job can read secrets stored on the `prod` environment, including
38
+ any AWS secrets used by ECS deploys. This workflow does not use those secrets.
39
+
40
+ ### PyPI.org (project does not exist yet)
41
+
42
+ Register a **pending** publisher while signed in as the account that should
43
+ own the project:
44
+
45
+ https://pypi.org/manage/account/publishing/
46
+
47
+ Use environment name `prod`. If a pending publisher was already registered
48
+ with environment `pypi`, delete it and register it again with `prod`.
49
+ Otherwise the upload fails with `invalid-publisher`.
50
+
51
+ The first successful tag publish creates `pythonparts-mcp` on PyPI.org.
52
+ Do not create the project with an API token first; that skips the pending
53
+ publisher.
54
+
55
+ Docs: https://docs.pypi.org/trusted-publishers/
56
+
57
+ ## Retire the API token
58
+
59
+ After the first PyPI.org upload from this workflow succeeds:
60
+
61
+ 1. Delete every `PYPI_*` and `TEST_PYPI_*` API token secret from this
62
+ repository and from the `Allplan-GmbH` organization Actions secrets.
63
+ 2. Revoke that token on PyPI and TestPyPI (Account settings → API tokens).
64
+ 3. Remove the service account that minted the token from Owners and
65
+ Maintainers on the `pythonparts-mcp` project. A human owner has to remain,
66
+ or the project is left with nobody who can manage it.
67
+
68
+ ## How to publish
69
+
70
+ ### Dev build (artifact)
71
+
72
+ A push to `dev` that changes this package starts **Upload pythonparts-mcp from dev**.
73
+ Download `pythonparts-mcp-dist` from that run's artifacts. The upload is
74
+ ungated, kept for 7 days, and does not require a version bump. It does not
75
+ publish to a package index.
76
+
77
+ ### Production
78
+
79
+ 1. Set `pyproject.toml` to the release version, for example `0.3.0`.
80
+ 2. Tag that exact version and push the tag:
81
+
82
+ ```bash
83
+ git tag pythonparts-mcp-v0.3.0
84
+ git push origin pythonparts-mcp-v0.3.0
85
+ ```
86
+
87
+ 3. After a `prod` reviewer approves the deployment, the workflow publishes to
88
+ https://pypi.org/project/pythonparts-mcp/ only when the tag version equals
89
+ `project.version`.
90
+
91
+ Manual runs never publish to any package index.