|
| 1 | +# ADR-0050: Marketplace Is the Only Supported Install Path — uvx Removed |
| 2 | + |
| 3 | +**Status:** Accepted |
| 4 | +**Date:** 2026-04-25 |
| 5 | +**Supersedes:** the rationale in commit `9076fe5` ("adopt canonical uvx pattern") |
| 6 | + |
| 7 | +## Context |
| 8 | + |
| 9 | +Cortex has oscillated between two install paths over the past month: |
| 10 | + |
| 11 | +| Version | MCP launch command | Hook command | |
| 12 | +|---|---|---| |
| 13 | +| v3.13.0 → v3.14.2 | `uvx` (PyPI) | `uvx` cortex-hook | |
| 14 | +| v3.14.3 → v3.14.4 | `python3 scripts/launcher.py` | `python3 scripts/launcher.py` | |
| 15 | +| v3.14.5 → v3.14.6 | `uvx` (PyPI) | `uvx` cortex-hook | |
| 16 | +| v3.14.7+ | `bash $PY ${CLAUDE_PLUGIN_ROOT}/scripts/launcher.py` | same | |
| 17 | + |
| 18 | +Commit `9076fe5` (2026-04-25) reintroduced uvx with the rationale: |
| 19 | + |
| 20 | +> Researching Anthropic's reference servers shows the canonical pattern |
| 21 | +> explicitly: every Python stdio MCP server in |
| 22 | +> `modelcontextprotocol/servers` is wired up with `command: "uvx"`. |
| 23 | +
|
| 24 | +This rationale is **wrong for our distribution channel.** The |
| 25 | +`modelcontextprotocol/servers` reference repository targets standalone |
| 26 | +MCP servers added via `claude mcp add`, where the user separately |
| 27 | +installs the package and Claude Code spawns it. That is a different |
| 28 | +mechanism than the **plugin marketplace** (`/plugin install ...`), |
| 29 | +which clones the plugin's git repository into |
| 30 | +`~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/` and |
| 31 | +expects the plugin to launch from that directory. |
| 32 | + |
| 33 | +uvx in a marketplace plugin is a category error: it bypasses the |
| 34 | +material the marketplace already cloned and re-fetches code from PyPI |
| 35 | +with separate version drift, separate caches, and separate failure |
| 36 | +modes that don't exist in the marketplace path. |
| 37 | + |
| 38 | +## Concrete failures observed with the uvx path |
| 39 | + |
| 40 | +1. **Code-of-record divergence.** Edits to the marketplace clone (the |
| 41 | + `${CLAUDE_PLUGIN_ROOT}` Claude Code expands at launch) were never |
| 42 | + loaded. The running server fetched a stale wheel from PyPI. Every |
| 43 | + plugin update → reconnect cycle was a no-op for code changes that |
| 44 | + weren't yet on PyPI. Hours of debugging blamed Python module caching |
| 45 | + when the actual issue was that the marketplace clone wasn't on the |
| 46 | + import path at all. |
| 47 | + |
| 48 | +2. **PEP 668 friction.** uvx triggered EXTERNALLY-MANAGED errors on |
| 49 | + homebrew Python and recent Debian/Ubuntu distributions. The launcher |
| 50 | + detour in v3.14.3/v3.14.4 was an attempt to fix that, then reverted |
| 51 | + in v3.14.5 with a regression of its own. |
| 52 | + |
| 53 | +3. **Two-channel publish coupling.** Every release required PyPI |
| 54 | + publish to be coordinated with the marketplace clone. When PyPI |
| 55 | + credentials were missing or twine failed, the marketplace clone |
| 56 | + advertised a new version that uvx couldn't fetch — silent failure |
| 57 | + for end-users. |
| 58 | + |
| 59 | +4. **Adversarial misuse.** AI sessions assisting Cortex maintenance |
| 60 | + would search the web, find the modelcontextprotocol/servers README, |
| 61 | + and propose "follow the canonical pattern" — the **wrong** pattern |
| 62 | + for a marketplace plugin — every time the maintainer asked about |
| 63 | + distribution. Without a written-down rule, this loop is infinite. |
| 64 | + |
| 65 | +## Decision |
| 66 | + |
| 67 | +**The plugin marketplace is the only supported Cortex install path.** |
| 68 | +uvx is **not** removed because it is bad — it is removed because it is |
| 69 | +**not Anthropic's official mechanism for plugin distribution**. The |
| 70 | +marketplace was rolled out specifically so plugins could: |
| 71 | + |
| 72 | +- Ship code, hooks, skills, and slash commands as a single git tree. |
| 73 | +- Run from the cloned tree, not from a separate package channel. |
| 74 | +- Stay in lockstep without two-channel publish coordination. |
| 75 | +- Avoid PEP 668 entirely (no `pip install` step for users). |
| 76 | + |
| 77 | +Cortex's release path now follows that contract: |
| 78 | + |
| 79 | +``` |
| 80 | +.mcp.json::command = "bash" |
| 81 | +.mcp.json::args = "-c", "$PY ${CLAUDE_PLUGIN_ROOT}/scripts/launcher.py mcp_server" |
| 82 | +plugin.json::hooks[*].command = same shape, with the hook module name as the trailing arg |
| 83 | +``` |
| 84 | + |
| 85 | +`scripts/launcher.py` lives in the plugin tree. It prepends |
| 86 | +`${CLAUDE_PLUGIN_ROOT}` to `PYTHONPATH` and runs the module directly. |
| 87 | +There is no PyPI fetch, no uvx step, no version drift. |
| 88 | + |
| 89 | +## Consequences |
| 90 | + |
| 91 | +- `pyproject.toml::[project.scripts]` no longer declares the |
| 92 | + `neuro-cortex-memory` or `cortex-hook` console scripts. The package |
| 93 | + is **not** published to PyPI. |
| 94 | +- `mcp_server/hook_runner.py` is deleted (it existed solely to be the |
| 95 | + uvx target for `cortex-hook`). |
| 96 | +- `mcp_server/doctor.py::_uvx_available` check is deleted. Doctor no |
| 97 | + longer reports uvx state because uvx state is no longer relevant. |
| 98 | +- `mcp_server/infrastructure/mcp_client.py::_ALLOWED_COMMANDS` no |
| 99 | + longer accepts `"uvx"` or `"uv"` for sub-MCP-client spawns. CWE-78 |
| 100 | + attack surface narrowed. |
| 101 | +- `mcp_server/infrastructure/ap_bridge.py::_resolve_command` no longer |
| 102 | + has a uvx fallback. AP discovery returns `None` (graceful |
| 103 | + degradation) when no install can be found. |
| 104 | + |
| 105 | +## Note to future AI assistants and contributors |
| 106 | + |
| 107 | +If you propose reintroducing uvx because some Anthropic reference repo |
| 108 | +("modelcontextprotocol/servers", "claude-code-plugins-cookbook", a blog |
| 109 | +post, etc.) uses it — that reference is for a **different distribution |
| 110 | +channel** (`claude mcp add` standalone servers). Cortex is a |
| 111 | +**marketplace plugin** and the marketplace is the path. Read this ADR |
| 112 | +first. Do not ship a PR that adds `uvx` to `.mcp.json`, |
| 113 | +`.claude-plugin/plugin.json`, or any hook command. |
| 114 | + |
| 115 | +## References |
| 116 | + |
| 117 | +- `9076fe5` (commit that reintroduced uvx, now reverted) |
| 118 | +- `8f76f85` (commit that removed every uvx invocation) |
| 119 | +- Anthropic plugin marketplace documentation: |
| 120 | + https://docs.claude.com/en/docs/claude-code/plugins |
0 commit comments