cmux-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.
- cmux_mcp-0.2.0/.gitignore +105 -0
- cmux_mcp-0.2.0/.gitignore.bak.20260921T082924Z +75 -0
- cmux_mcp-0.2.0/CHANGELOG.md +35 -0
- cmux_mcp-0.2.0/LICENSE +28 -0
- cmux_mcp-0.2.0/PKG-INFO +23 -0
- cmux_mcp-0.2.0/README.md +103 -0
- cmux_mcp-0.2.0/cmux_mcp/__init__.py +11 -0
- cmux_mcp-0.2.0/cmux_mcp/__main__.py +24 -0
- cmux_mcp-0.2.0/cmux_mcp/_tools.py +49 -0
- cmux_mcp-0.2.0/cmux_mcp/cli.py +3 -0
- cmux_mcp-0.2.0/cmux_mcp/cli_discovery.py +98 -0
- cmux_mcp-0.2.0/cmux_mcp/client.py +538 -0
- cmux_mcp-0.2.0/cmux_mcp/config.py +102 -0
- cmux_mcp-0.2.0/cmux_mcp/errors.py +204 -0
- cmux_mcp-0.2.0/cmux_mcp/health.py +120 -0
- cmux_mcp-0.2.0/cmux_mcp/logging_setup.py +52 -0
- cmux_mcp-0.2.0/cmux_mcp/models.py +349 -0
- cmux_mcp-0.2.0/cmux_mcp/server.py +223 -0
- cmux_mcp-0.2.0/cmux_mcp/tools/__init__.py +3 -0
- cmux_mcp-0.2.0/cmux_mcp/tools/browser_tools.py +419 -0
- cmux_mcp-0.2.0/cmux_mcp/tools/socket_tools.py +287 -0
- cmux_mcp-0.2.0/docs/superpowers/plans/2026-09-16-cmux-mcp-impl.md +5081 -0
- cmux_mcp-0.2.0/docs/superpowers/plans/PLAN_INDEX.md +47 -0
- cmux_mcp-0.2.0/docs/superpowers/specs/2026-09-16-cmux-mcp-design.md +1358 -0
- cmux_mcp-0.2.0/pyproject.toml +53 -0
- cmux_mcp-0.2.0/scripts/README.md +1 -0
- cmux_mcp-0.2.0/settings/cmux-mcp.yaml +18 -0
- cmux_mcp-0.2.0/tests/__init__.py +0 -0
- cmux_mcp-0.2.0/tests/e2e/__init__.py +0 -0
- cmux_mcp-0.2.0/tests/fixtures/cmux_responses.yaml +21 -0
- cmux_mcp-0.2.0/tests/integration/__init__.py +0 -0
- cmux_mcp-0.2.0/tests/unit/__init__.py +0 -0
- cmux_mcp-0.2.0/tests/unit/test_browser_tools.py +262 -0
- cmux_mcp-0.2.0/tests/unit/test_cli_discovery.py +93 -0
- cmux_mcp-0.2.0/tests/unit/test_config.py +115 -0
- cmux_mcp-0.2.0/tests/unit/test_errors.py +60 -0
- cmux_mcp-0.2.0/tests/unit/test_health.py +78 -0
- cmux_mcp-0.2.0/tests/unit/test_lifecycle.py +251 -0
- cmux_mcp-0.2.0/tests/unit/test_logging_pii.py +75 -0
- cmux_mcp-0.2.0/tests/unit/test_models.py +286 -0
- cmux_mcp-0.2.0/tests/unit/test_socket_tools.py +239 -0
- cmux_mcp-0.2.0/tests/unit/test_tools_registration.py +53 -0
- cmux_mcp-0.2.0/tests/unit/test_transport_cli.py +291 -0
- cmux_mcp-0.2.0/tests/unit/test_transport_mock.py +53 -0
- cmux_mcp-0.2.0/tests/unit/test_transport_socket.py +140 -0
- cmux_mcp-0.2.0/uv.lock +4676 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
MANIFEST
|
|
23
|
+
|
|
24
|
+
# Virtual environments
|
|
25
|
+
.venv/
|
|
26
|
+
venv/
|
|
27
|
+
ENV/
|
|
28
|
+
env/
|
|
29
|
+
|
|
30
|
+
# uv
|
|
31
|
+
uv.lock.bak
|
|
32
|
+
|
|
33
|
+
# Testing
|
|
34
|
+
.pytest_cache/
|
|
35
|
+
.coverage
|
|
36
|
+
.coverage.*
|
|
37
|
+
htmlcov/
|
|
38
|
+
coverage.json
|
|
39
|
+
.tox/
|
|
40
|
+
.nox/
|
|
41
|
+
|
|
42
|
+
# Type checkers
|
|
43
|
+
.mypy_cache/
|
|
44
|
+
.pyright_cache/
|
|
45
|
+
|
|
46
|
+
# Linters / formatters
|
|
47
|
+
.ruff_cache/
|
|
48
|
+
.black/
|
|
49
|
+
|
|
50
|
+
# Crackerjack
|
|
51
|
+
.crackerjack/
|
|
52
|
+
# Crackerjack tool output caches
|
|
53
|
+
.cache/
|
|
54
|
+
# lychee link checker persistent cache (single file at repo root)
|
|
55
|
+
.lycheecache
|
|
56
|
+
|
|
57
|
+
# IDE
|
|
58
|
+
.idea/
|
|
59
|
+
.vscode/
|
|
60
|
+
*.swp
|
|
61
|
+
*.swo
|
|
62
|
+
.DS_Store
|
|
63
|
+
|
|
64
|
+
# Logs
|
|
65
|
+
*.log
|
|
66
|
+
logs/
|
|
67
|
+
|
|
68
|
+
# Worktrees (no-op; designed for repos that use clones)
|
|
69
|
+
.claude/worktrees/
|
|
70
|
+
|
|
71
|
+
# Oneiric runtime cache (mcp-common BaseOneiricServerMixin writes snapshots here)
|
|
72
|
+
.oneiric_cache/
|
|
73
|
+
|
|
74
|
+
# direnv auto-generated files
|
|
75
|
+
.envrc
|
|
76
|
+
|
|
77
|
+
# >>> bodai-shared-gitignore >>>
|
|
78
|
+
# Source of truth: crackerjack/templates/GITIGNORE_BODAI.md
|
|
79
|
+
# Enforced by: crackerjack check `gitignore-conformance`
|
|
80
|
+
# Fleet list: /Users/les/Projects/mahavishnu/BODAI_REPO_REGISTRY.md
|
|
81
|
+
# Do NOT remove the marker line above — `crackerjack gitignore sync` uses it
|
|
82
|
+
# to detect already-applied state.
|
|
83
|
+
|
|
84
|
+
# Editor / pre-edit backups (foo.py.backup, foo.py.backup.json)
|
|
85
|
+
*.backup
|
|
86
|
+
*.backup.*
|
|
87
|
+
*.bak
|
|
88
|
+
*.tmpl
|
|
89
|
+
# XDG mistake dirs (env var expansion failed; literal names)
|
|
90
|
+
# Note: both `{` and `}` are escaped (`{` -> `[{]`, `}` -> `[}]`)
|
|
91
|
+
# because gitignore parsers interpret `${...}` as a brace-expansion
|
|
92
|
+
# alternate group; escaping only the open brace leaves a stray `}`.
|
|
93
|
+
~
|
|
94
|
+
[{]HOME[}]
|
|
95
|
+
[{]XDG_DATA_HOME:-$HOME[}]
|
|
96
|
+
# pytest-benchmark autosave cache (sibling of the tracked `benchmarks/` test suite)
|
|
97
|
+
.benchmarks/
|
|
98
|
+
# Vitest config timestamp cache
|
|
99
|
+
vitest.config.js.timestamp-*.mjs
|
|
100
|
+
# Playwright output cruft
|
|
101
|
+
playwright-junit.xml
|
|
102
|
+
playwright-report/
|
|
103
|
+
playwright-results.json
|
|
104
|
+
test-results/
|
|
105
|
+
# <<< bodai-shared-gitignore <<<
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
MANIFEST
|
|
23
|
+
|
|
24
|
+
# Virtual environments
|
|
25
|
+
.venv/
|
|
26
|
+
venv/
|
|
27
|
+
ENV/
|
|
28
|
+
env/
|
|
29
|
+
|
|
30
|
+
# uv
|
|
31
|
+
uv.lock.bak
|
|
32
|
+
|
|
33
|
+
# Testing
|
|
34
|
+
.pytest_cache/
|
|
35
|
+
.coverage
|
|
36
|
+
.coverage.*
|
|
37
|
+
htmlcov/
|
|
38
|
+
coverage.json
|
|
39
|
+
.tox/
|
|
40
|
+
.nox/
|
|
41
|
+
|
|
42
|
+
# Type checkers
|
|
43
|
+
.mypy_cache/
|
|
44
|
+
.pyright_cache/
|
|
45
|
+
|
|
46
|
+
# Linters / formatters
|
|
47
|
+
.ruff_cache/
|
|
48
|
+
.black/
|
|
49
|
+
|
|
50
|
+
# Crackerjack
|
|
51
|
+
.crackerjack/
|
|
52
|
+
# Crackerjack tool output caches
|
|
53
|
+
.cache/
|
|
54
|
+
# lychee link checker persistent cache (single file at repo root)
|
|
55
|
+
.lycheecache
|
|
56
|
+
|
|
57
|
+
# IDE
|
|
58
|
+
.idea/
|
|
59
|
+
.vscode/
|
|
60
|
+
*.swp
|
|
61
|
+
*.swo
|
|
62
|
+
.DS_Store
|
|
63
|
+
|
|
64
|
+
# Logs
|
|
65
|
+
*.log
|
|
66
|
+
logs/
|
|
67
|
+
|
|
68
|
+
# Worktrees (no-op; designed for repos that use clones)
|
|
69
|
+
.claude/worktrees/
|
|
70
|
+
|
|
71
|
+
# Oneiric runtime cache (mcp-common BaseOneiricServerMixin writes snapshots here)
|
|
72
|
+
.oneiric_cache/
|
|
73
|
+
|
|
74
|
+
# direnv auto-generated files
|
|
75
|
+
.envrc
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.0] - 2026-09-21
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- browser: Handle CmuxMockTransport missing _config in truncate path
|
|
13
|
+
- client+server: H7/H9/H10 — counter, JSONDecodeError, PID PermissionError
|
|
14
|
+
- client: H8 — surface-lock refcount + eviction (no more lock leak)
|
|
15
|
+
- client: Subprocess lifecycle on cancellation + real aclose() (C2+C3)
|
|
16
|
+
- errors+README: H5 — missing exception classes + honest README (H6)
|
|
17
|
+
- gitignore: Exclude crackerjack tool-output caches (.cache/ + .lycheecache)
|
|
18
|
+
- logging: Use Oneiric's structlog-based logger (C7 — CLAUDE.md violation)
|
|
19
|
+
- quality: All fast hooks now pass — lint + tc-refs scope fix
|
|
20
|
+
- server: Custom /health route returns 503 on degraded (C4)
|
|
21
|
+
- tools: Cmux_browser_type returns BrowserTypeOutput (H3)
|
|
22
|
+
- tools: Cmux_list_workspaces iterates correct field + adds surface.list (H4)
|
|
23
|
+
- tools: H1/H2 — wire max_response_bytes + notify rate limit
|
|
24
|
+
- tools: Record_success() reachable in cmux_browser_navigate (C5)
|
|
25
|
+
- tools: Tool errors raise ToolError (C6 — was returning plain dict)
|
|
26
|
+
- Wire register_tools into startup (C1 — server exposed zero tools)
|
|
27
|
+
|
|
28
|
+
### Documentation
|
|
29
|
+
|
|
30
|
+
- plans: Mark cmux-mcp plan + spec shipped (v0.1.0)
|
|
31
|
+
|
|
32
|
+
### Internal
|
|
33
|
+
|
|
34
|
+
- deps: Add crackerjack to PEP 735 dependency-groups
|
|
35
|
+
- Migrate cmux-mcp from src-layout to flat layout
|
cmux_mcp-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Les Leslie, Wedgwood Web Works LLC
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
cmux_mcp-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cmux-mcp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: MCP server for cmux terminal automation (macOS only)
|
|
5
|
+
Author-email: Les Leslie <les@wedgwoodwebworks.com>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.14
|
|
9
|
+
Requires-Dist: fastmcp<4,>=3.4
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Requires-Dist: mcp-common<0.27,>=0.26
|
|
12
|
+
Requires-Dist: oneiric>=0.21.0
|
|
13
|
+
Requires-Dist: psutil>=7.2.2
|
|
14
|
+
Requires-Dist: pydantic-settings>=2
|
|
15
|
+
Requires-Dist: pydantic>=2.13.4
|
|
16
|
+
Requires-Dist: pyyaml>=6
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: crackerjack>=0.20; extra == 'dev'
|
|
19
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
cmux_mcp-0.2.0/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# cmux-mcp
|
|
2
|
+
|
|
3
|
+
[](https://github.com/lesleslie/crackerjack)
|
|
4
|
+
[](https://github.com/lesleslie/oneiric)
|
|
5
|
+
[](https://github.com/jlowin/fastmcp)
|
|
6
|
+
[](https://github.com/astral-sh/uv)
|
|
7
|
+
[](https://www.python.org/downloads/)
|
|
8
|
+
|
|
9
|
+
Catalog and operating map for cmux-mcp.
|
|
10
|
+
|
|
11
|
+
**Status:** v0.1.0 (initial release — 12 tools, BSD-3-Clause)
|
|
12
|
+
|
|
13
|
+
## Quick Links
|
|
14
|
+
|
|
15
|
+
- [Overview](#overview)
|
|
16
|
+
- [Tool Reference](#tool-reference)
|
|
17
|
+
- [Quick Start](#quick-start)
|
|
18
|
+
- [MCP Client Configuration](#mcp-client-configuration)
|
|
19
|
+
- [Configuration](#configuration)
|
|
20
|
+
- [Security Notes](#security-notes)
|
|
21
|
+
|
|
22
|
+
## Overview
|
|
23
|
+
|
|
24
|
+
cmux-mcp exposes programmatic control of a running [cmux](https://github.com/manaflow-ai/cmux) instance (macOS-only, Ghostty-based terminal for AI coding agents) as 12 MCP tools over Streamable HTTP. The server speaks cmux's Unix-socket JSON-RPC protocol directly for orchestration and notifications, and shells out to the cmux CLI for browser automation.
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Install
|
|
30
|
+
cd /Users/les/Projects/cmux-mcp && uv sync --extra dev
|
|
31
|
+
|
|
32
|
+
# Run (mock mode, for testing)
|
|
33
|
+
CMUX_MCP_MOCK=1 uv run cmux-mcp start
|
|
34
|
+
|
|
35
|
+
# Run (real, on macOS with cmux running)
|
|
36
|
+
uv run cmux-mcp start
|
|
37
|
+
|
|
38
|
+
# Lifecycle
|
|
39
|
+
uv run cmux-mcp start --bg
|
|
40
|
+
uv run cmux-mcp status
|
|
41
|
+
uv run cmux-mcp health
|
|
42
|
+
uv run cmux-mcp stop
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## MCP Client Configuration
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"cmux": {
|
|
51
|
+
"command": "uv",
|
|
52
|
+
"args": ["run", "--project", "/Users/les/Projects/cmux-mcp", "cmux-mcp", "start"]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Tool Reference
|
|
59
|
+
|
|
60
|
+
| Tool | Mode | Description |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `cmux_list_workspaces` | Read-only | List all workspaces with panes and surfaces |
|
|
63
|
+
| `cmux_list_notifications` | Read-only | List pending cmux notifications |
|
|
64
|
+
| `cmux_identify` | Read-only | Return focused window/workspace/pane/surface |
|
|
65
|
+
| `cmux_send_keys` | Destructive | Send text or special key to terminal surface (fire-and-forget) |
|
|
66
|
+
| `cmux_notify` | Mutation | Dispatch OS notification that rings pane and lights sidebar |
|
|
67
|
+
| `cmux_browser_navigate` | Destructive | Navigate browser surface to URL |
|
|
68
|
+
| `cmux_browser_snapshot` | Read-only | A11y tree with Playwright-style refs |
|
|
69
|
+
| `cmux_browser_evaluate` | Destructive | Execute JS in browser (arbitrary; trust model documented) |
|
|
70
|
+
| `cmux_browser_click` | Destructive | Click element by CSS selector |
|
|
71
|
+
| `cmux_browser_type` | Destructive | Type into input (uses fill semantics) |
|
|
72
|
+
| `cmux_browser_tabs` | Read-only | List open tabs |
|
|
73
|
+
| `cmux_browser_console` | Read-only | Read console + JS errors (aggregates 2 sub-calls) |
|
|
74
|
+
|
|
75
|
+
## Configuration
|
|
76
|
+
|
|
77
|
+
All settings via `CMUX_MCP_*` env vars. See `settings/cmux-mcp.yaml` for committed defaults.
|
|
78
|
+
|
|
79
|
+
| Setting | Default | Description |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `CMUX_MCP_HOST` | `127.0.0.1` | HTTP bind address |
|
|
82
|
+
| `CMUX_MCP_PORT` | `3061` | HTTP port |
|
|
83
|
+
| `CMUX_MCP_SOCKET_PATH` | `/tmp/cmux.sock` | cmux Unix socket |
|
|
84
|
+
| `CMUX_MCP_CLI_PATH` | auto-discover | cmux CLI binary path |
|
|
85
|
+
| `CMUX_MCP_MOCK` | unset | `1`/`true` enables mock mode |
|
|
86
|
+
| `CMUX_MCP_AUTH_ENABLED` | `false` | Enable JWT auth for non-loopback deployments |
|
|
87
|
+
|
|
88
|
+
## Security Notes
|
|
89
|
+
|
|
90
|
+
- macOS-only; non-macOS hosts must use `CMUX_MCP_MOCK=1` (auto-flipped with WARN banner)
|
|
91
|
+
- cmux's `cmuxOnly` access mode restricts socket connections to processes spawned inside cmux terminals
|
|
92
|
+
- PII redaction helpers (`redact_url_query_string`, `redact_expression`) exist in `logging_setup.py` for future log-site use; v0.1.x ships with no PII-logging call sites by default (deferred)
|
|
93
|
+
- Subprocess env filtered to `SUBPROCESS_ENV_ALLOWLIST` to prevent leaking API keys
|
|
94
|
+
- Default loopback bind; non-loopback requires `CMUX_MCP_AUTH_ENABLED=true`
|
|
95
|
+
|
|
96
|
+
## Development Commands
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
uv run pytest # Run all tests
|
|
100
|
+
uv run pytest -m unit # Unit tests only
|
|
101
|
+
uv run pytest -m integration # Integration tests (require live cmux)
|
|
102
|
+
uv run crackerjack run # Full quality gate
|
|
103
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""cmux-mcp: MCP server for cmux terminal automation (macOS only)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
|
|
7
|
+
__version__ = importlib.metadata.version("cmux-mcp")
|
|
8
|
+
|
|
9
|
+
DEFAULT_PORT: int = 3061 # per spec decision log row 2 + 8
|
|
10
|
+
|
|
11
|
+
__all__ = ["DEFAULT_PORT", "__version__"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""cmux-mcp entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from mcp_common.cli import MCPServerCLIFactory
|
|
6
|
+
|
|
7
|
+
from cmux_mcp.config import CmuxMCPConfig
|
|
8
|
+
from cmux_mcp.server import CmuxMCPServer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> None:
|
|
12
|
+
"""Entry point for `cmux-mcp` console script."""
|
|
13
|
+
factory = MCPServerCLIFactory.create_server_cli(
|
|
14
|
+
server_class=CmuxMCPServer,
|
|
15
|
+
config_class=CmuxMCPConfig,
|
|
16
|
+
name="cmux-mcp",
|
|
17
|
+
_description="MCP server for cmux terminal automation (macOS only).",
|
|
18
|
+
)
|
|
19
|
+
app = factory.create_app()
|
|
20
|
+
app()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
main()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""cmux_mcp._tools — programmatic tool registration.
|
|
2
|
+
|
|
3
|
+
Per spec §"_tools.py — registration pattern":
|
|
4
|
+
Per-instance FastMCP (in CmuxMCPServer.__init__) means @mcp.tool() decorators
|
|
5
|
+
can't bind at module-load time. Instead, register_tools() registers all 12
|
|
6
|
+
tools programmatically in CmuxMCPServer.startup().
|
|
7
|
+
|
|
8
|
+
Each tool body follows the try/except/else/return pattern per spec §"/health
|
|
9
|
+
envelope wiring → Tool feed placement".
|
|
10
|
+
|
|
11
|
+
FastMCP 3.4+ API used: `FunctionTool.from_function(fn, name=..., description=...,
|
|
12
|
+
annotations=ToolAnnotations(...), output_schema=...)` → `mcp.add_tool(tool)`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
from cmux_mcp.health import TOOL_NAMES
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from fastmcp import FastMCP
|
|
23
|
+
|
|
24
|
+
from cmux_mcp.client import CmuxCliTransportProtocol, CmuxSocketTransportProtocol
|
|
25
|
+
from cmux_mcp.health import ToolFeedComponent
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_tool_feed_components() -> list[ToolFeedComponent]:
|
|
29
|
+
"""Delegate to health.build_tool_feed_components (single source of truth)."""
|
|
30
|
+
from cmux_mcp.health import build_tool_feed_components as _impl
|
|
31
|
+
|
|
32
|
+
return _impl()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def register_tools(
|
|
36
|
+
mcp: FastMCP,
|
|
37
|
+
socket_transport: CmuxSocketTransportProtocol,
|
|
38
|
+
cli_transport: CmuxCliTransportProtocol,
|
|
39
|
+
tool_feeds: dict[str, ToolFeedComponent],
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Register all 12 tools on the FastMCP instance. Idempotent."""
|
|
42
|
+
from cmux_mcp.tools.browser_tools import register_browser_tools
|
|
43
|
+
from cmux_mcp.tools.socket_tools import register_socket_tools
|
|
44
|
+
|
|
45
|
+
register_socket_tools(mcp, socket_transport, tool_feeds)
|
|
46
|
+
register_browser_tools(mcp, cli_transport, tool_feeds)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ["TOOL_NAMES", "build_tool_feed_components", "register_tools"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""cmux_mcp.cli_discovery — locate the cmux CLI binary.
|
|
2
|
+
|
|
3
|
+
Per spec §"cmux CLI binary discovery":
|
|
4
|
+
1. CMUX_MCP_CLI_PATH env var (operator override)
|
|
5
|
+
2. which cmux from $PATH
|
|
6
|
+
3. /Applications/cmux.app/Contents/Resources/bin/cmux
|
|
7
|
+
4. /opt/homebrew/Caskroom/cmux/*/cmux.app/.../bin/cmux (latest version)
|
|
8
|
+
5. /usr/local/Caskroom/cmux/*/cmux.app/.../bin/cmux (Intel macs)
|
|
9
|
+
6. ~/Library/Developer/Xcode/DerivedData/cmux-*/Build/Products/{Debug,Release}/cmux.app/.../bin/cmux
|
|
10
|
+
7. fail with CmuxBinaryNotFoundError listing probed paths
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import shutil
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from cmux_mcp.errors import CmuxBinaryNotFoundError
|
|
20
|
+
|
|
21
|
+
# Module-level so tests can monkeypatch (see test_cli_discovery.py).
|
|
22
|
+
_DEFAULT_APP_BUNDLE = Path("/Applications/cmux.app")
|
|
23
|
+
_CASKROOM_PATHS: list[Path] = [
|
|
24
|
+
Path("/opt/homebrew/Caskroom/cmux"),
|
|
25
|
+
Path("/usr/local/Caskroom/cmux"),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _candidate_paths() -> list[Path]:
|
|
30
|
+
candidates: list[Path] = []
|
|
31
|
+
if "PATH" in os.environ:
|
|
32
|
+
which_result = shutil.which("cmux")
|
|
33
|
+
if which_result:
|
|
34
|
+
candidates.append(Path(which_result))
|
|
35
|
+
candidates.append(_DEFAULT_APP_BUNDLE / "Contents" / "Resources" / "bin" / "cmux")
|
|
36
|
+
for caskroom in _CASKROOM_PATHS:
|
|
37
|
+
if caskroom.exists() and caskroom.is_dir():
|
|
38
|
+
# Numeric tuple sort so "10.0.0" sorts after "2.0.0".
|
|
39
|
+
versions = sorted(
|
|
40
|
+
(p for p in caskroom.iterdir() if p.is_dir()),
|
|
41
|
+
key=lambda p: tuple(
|
|
42
|
+
int(x) if x.isdigit() else 0 for x in p.name.split(".")
|
|
43
|
+
),
|
|
44
|
+
reverse=True,
|
|
45
|
+
)
|
|
46
|
+
for v in versions:
|
|
47
|
+
candidates.append(
|
|
48
|
+
v / "cmux.app" / "Contents" / "Resources" / "bin" / "cmux"
|
|
49
|
+
)
|
|
50
|
+
derived = Path.home() / "Library" / "Developer" / "Xcode" / "DerivedData"
|
|
51
|
+
if derived.exists():
|
|
52
|
+
# PERF402: candidates.extend(glob(...)) beats `for d in glob: append`
|
|
53
|
+
# — single C-level call instead of N Python appends.
|
|
54
|
+
candidates.extend(
|
|
55
|
+
derived.glob(
|
|
56
|
+
"cmux-*/Build/Products/Debug/cmux.app/Contents/Resources/bin/cmux"
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
candidates.extend(
|
|
60
|
+
derived.glob(
|
|
61
|
+
"cmux-*/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux"
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
return candidates
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def discover_cmux_cli(explicit_path: Path | None = None) -> Path:
|
|
68
|
+
"""Resolve the cmux CLI binary path.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
explicit_path: if set (e.g., via CMUX_MCP_CLI_PATH env), use directly.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Path to a verified cmux binary.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
CmuxBinaryNotFoundError: when no candidate is found.
|
|
78
|
+
"""
|
|
79
|
+
env_override = os.environ.get("CMUX_MCP_CLI_PATH")
|
|
80
|
+
candidates: list[Path] = []
|
|
81
|
+
if explicit_path is not None:
|
|
82
|
+
candidates.append(explicit_path)
|
|
83
|
+
elif env_override:
|
|
84
|
+
candidates.append(Path(env_override))
|
|
85
|
+
candidates.extend(_candidate_paths())
|
|
86
|
+
|
|
87
|
+
for path in candidates:
|
|
88
|
+
if path.exists() and path.is_file() and os.access(path, os.X_OK):
|
|
89
|
+
return path
|
|
90
|
+
|
|
91
|
+
probed = "\n ".join(str(p) for p in candidates)
|
|
92
|
+
raise CmuxBinaryNotFoundError(
|
|
93
|
+
f"cmux CLI binary not found. Probed paths:\n {probed}\n"
|
|
94
|
+
"Set CMUX_MCP_CLI_PATH or install cmux from https://cmux.com/"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
__all__ = ["discover_cmux_cli"]
|