claude-control 0.1.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.
- claude_control-0.1.0/PKG-INFO +178 -0
- claude_control-0.1.0/README.md +155 -0
- claude_control-0.1.0/claude_control/__init__.py +1 -0
- claude_control-0.1.0/claude_control/__main__.py +5 -0
- claude_control-0.1.0/claude_control/config.py +70 -0
- claude_control-0.1.0/claude_control/server.py +138 -0
- claude_control-0.1.0/claude_control/session_manager.py +160 -0
- claude_control-0.1.0/claude_control.egg-info/PKG-INFO +178 -0
- claude_control-0.1.0/claude_control.egg-info/SOURCES.txt +13 -0
- claude_control-0.1.0/claude_control.egg-info/dependency_links.txt +1 -0
- claude_control-0.1.0/claude_control.egg-info/entry_points.txt +2 -0
- claude_control-0.1.0/claude_control.egg-info/requires.txt +2 -0
- claude_control-0.1.0/claude_control.egg-info/top_level.txt +1 -0
- claude_control-0.1.0/pyproject.toml +41 -0
- claude_control-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: claude-control
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for coordinating Claude Code instances across multiple projects
|
|
5
|
+
Author-email: Red Duck Labs <support@redducklabs.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/redducklabs/claude-control
|
|
8
|
+
Project-URL: Repository, https://github.com/redducklabs/claude-control
|
|
9
|
+
Project-URL: Issues, https://github.com/redducklabs/claude-control/issues
|
|
10
|
+
Keywords: mcp,claude,claude-code,automation,orchestration
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: claude-code-sdk>=0.0.25
|
|
22
|
+
Requires-Dist: mcp>=1.12.0
|
|
23
|
+
|
|
24
|
+
# Claude Control
|
|
25
|
+
|
|
26
|
+
MCP server that lets a Claude Code session coordinate with Claude Code instances running in other project directories.
|
|
27
|
+
|
|
28
|
+
## How It Works
|
|
29
|
+
|
|
30
|
+
Claude Control is an MCP server (stdio transport) that exposes tools for dispatching prompts to Claude Code instances in configured project directories. Each remote instance:
|
|
31
|
+
|
|
32
|
+
- Runs as a persistent subprocess with conversation context preserved across calls
|
|
33
|
+
- Loads the target project's own `CLAUDE.md`, `.mcp.json`, hooks, and settings
|
|
34
|
+
- Runs with `bypassPermissions` for fully autonomous operation
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
### From PyPI
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install claude-control
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv tool install claude-control
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### From Source
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone https://github.com/redducklabs/claude-control.git
|
|
54
|
+
cd claude-control
|
|
55
|
+
pip install .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Setup
|
|
59
|
+
|
|
60
|
+
### 1. Configure Projects
|
|
61
|
+
|
|
62
|
+
Create a `projects.json` file (see `projects.json.example`):
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"projects": [
|
|
67
|
+
{
|
|
68
|
+
"name": "my-backend",
|
|
69
|
+
"path": "D:\\repos\\my-backend",
|
|
70
|
+
"description": "Backend API service"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "my-frontend",
|
|
74
|
+
"path": "D:\\repos\\my-frontend",
|
|
75
|
+
"description": "Frontend web application"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 2. Register as an MCP Server
|
|
82
|
+
|
|
83
|
+
Add to the `.mcp.json` of the project where you want coordination tools available.
|
|
84
|
+
|
|
85
|
+
**Using `uvx` (recommended — no global install needed):**
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"claude_control": {
|
|
91
|
+
"command": "uvx",
|
|
92
|
+
"args": ["claude-control"],
|
|
93
|
+
"env": {
|
|
94
|
+
"CLAUDE_CONTROL_PROJECTS": "/path/to/your/projects.json"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Using a pip install:**
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"mcpServers": {
|
|
106
|
+
"claude_control": {
|
|
107
|
+
"command": "claude-control",
|
|
108
|
+
"env": {
|
|
109
|
+
"CLAUDE_CONTROL_PROJECTS": "/path/to/your/projects.json"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Using `python -m`:**
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"mcpServers": {
|
|
121
|
+
"claude_control": {
|
|
122
|
+
"command": "python",
|
|
123
|
+
"args": ["-m", "claude_control"],
|
|
124
|
+
"env": {
|
|
125
|
+
"CLAUDE_CONTROL_PROJECTS": "/path/to/your/projects.json"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 3. Restart Claude Code
|
|
133
|
+
|
|
134
|
+
The tools will appear as `mcp__claude_control__send_command`, `mcp__claude_control__list_projects`, etc.
|
|
135
|
+
|
|
136
|
+
## Tools
|
|
137
|
+
|
|
138
|
+
### `send_command`
|
|
139
|
+
|
|
140
|
+
Send a prompt to a Claude Code instance in the specified project directory.
|
|
141
|
+
|
|
142
|
+
| Parameter | Type | Description |
|
|
143
|
+
|-----------|------|-------------|
|
|
144
|
+
| `project` | string | Project name (from projects.json) |
|
|
145
|
+
| `prompt` | string | The prompt to send |
|
|
146
|
+
|
|
147
|
+
Returns the full text response from the remote instance. Sessions persist across calls — follow-up prompts have access to prior context.
|
|
148
|
+
|
|
149
|
+
### `list_projects`
|
|
150
|
+
|
|
151
|
+
List all configured projects with their paths, descriptions, and session status.
|
|
152
|
+
|
|
153
|
+
### `reset_session`
|
|
154
|
+
|
|
155
|
+
Tear down a project's Claude Code session. The next `send_command` call creates a fresh session with no prior context.
|
|
156
|
+
|
|
157
|
+
| Parameter | Type | Description |
|
|
158
|
+
|-----------|------|-------------|
|
|
159
|
+
| `project` | string | Project name to reset |
|
|
160
|
+
|
|
161
|
+
### `get_session_status`
|
|
162
|
+
|
|
163
|
+
Check whether a project has an active session, its ID, and turn count.
|
|
164
|
+
|
|
165
|
+
| Parameter | Type | Description |
|
|
166
|
+
|-----------|------|-------------|
|
|
167
|
+
| `project` | string | Project name to check |
|
|
168
|
+
|
|
169
|
+
## Dependencies
|
|
170
|
+
|
|
171
|
+
- Python >= 3.11
|
|
172
|
+
- `claude-code-sdk >= 0.0.25`
|
|
173
|
+
- `mcp >= 1.12.0`
|
|
174
|
+
- Claude Code CLI installed and on PATH
|
|
175
|
+
|
|
176
|
+
## Configuration
|
|
177
|
+
|
|
178
|
+
The `CLAUDE_CONTROL_PROJECTS` environment variable points to your `projects.json`. If unset, defaults to `projects.json` in the package's parent directory.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Claude Control
|
|
2
|
+
|
|
3
|
+
MCP server that lets a Claude Code session coordinate with Claude Code instances running in other project directories.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
Claude Control is an MCP server (stdio transport) that exposes tools for dispatching prompts to Claude Code instances in configured project directories. Each remote instance:
|
|
8
|
+
|
|
9
|
+
- Runs as a persistent subprocess with conversation context preserved across calls
|
|
10
|
+
- Loads the target project's own `CLAUDE.md`, `.mcp.json`, hooks, and settings
|
|
11
|
+
- Runs with `bypassPermissions` for fully autonomous operation
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### From PyPI
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install claude-control
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv tool install claude-control
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### From Source
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/redducklabs/claude-control.git
|
|
31
|
+
cd claude-control
|
|
32
|
+
pip install .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Setup
|
|
36
|
+
|
|
37
|
+
### 1. Configure Projects
|
|
38
|
+
|
|
39
|
+
Create a `projects.json` file (see `projects.json.example`):
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"projects": [
|
|
44
|
+
{
|
|
45
|
+
"name": "my-backend",
|
|
46
|
+
"path": "D:\\repos\\my-backend",
|
|
47
|
+
"description": "Backend API service"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "my-frontend",
|
|
51
|
+
"path": "D:\\repos\\my-frontend",
|
|
52
|
+
"description": "Frontend web application"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 2. Register as an MCP Server
|
|
59
|
+
|
|
60
|
+
Add to the `.mcp.json` of the project where you want coordination tools available.
|
|
61
|
+
|
|
62
|
+
**Using `uvx` (recommended — no global install needed):**
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"mcpServers": {
|
|
67
|
+
"claude_control": {
|
|
68
|
+
"command": "uvx",
|
|
69
|
+
"args": ["claude-control"],
|
|
70
|
+
"env": {
|
|
71
|
+
"CLAUDE_CONTROL_PROJECTS": "/path/to/your/projects.json"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Using a pip install:**
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"mcpServers": {
|
|
83
|
+
"claude_control": {
|
|
84
|
+
"command": "claude-control",
|
|
85
|
+
"env": {
|
|
86
|
+
"CLAUDE_CONTROL_PROJECTS": "/path/to/your/projects.json"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Using `python -m`:**
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"mcpServers": {
|
|
98
|
+
"claude_control": {
|
|
99
|
+
"command": "python",
|
|
100
|
+
"args": ["-m", "claude_control"],
|
|
101
|
+
"env": {
|
|
102
|
+
"CLAUDE_CONTROL_PROJECTS": "/path/to/your/projects.json"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 3. Restart Claude Code
|
|
110
|
+
|
|
111
|
+
The tools will appear as `mcp__claude_control__send_command`, `mcp__claude_control__list_projects`, etc.
|
|
112
|
+
|
|
113
|
+
## Tools
|
|
114
|
+
|
|
115
|
+
### `send_command`
|
|
116
|
+
|
|
117
|
+
Send a prompt to a Claude Code instance in the specified project directory.
|
|
118
|
+
|
|
119
|
+
| Parameter | Type | Description |
|
|
120
|
+
|-----------|------|-------------|
|
|
121
|
+
| `project` | string | Project name (from projects.json) |
|
|
122
|
+
| `prompt` | string | The prompt to send |
|
|
123
|
+
|
|
124
|
+
Returns the full text response from the remote instance. Sessions persist across calls — follow-up prompts have access to prior context.
|
|
125
|
+
|
|
126
|
+
### `list_projects`
|
|
127
|
+
|
|
128
|
+
List all configured projects with their paths, descriptions, and session status.
|
|
129
|
+
|
|
130
|
+
### `reset_session`
|
|
131
|
+
|
|
132
|
+
Tear down a project's Claude Code session. The next `send_command` call creates a fresh session with no prior context.
|
|
133
|
+
|
|
134
|
+
| Parameter | Type | Description |
|
|
135
|
+
|-----------|------|-------------|
|
|
136
|
+
| `project` | string | Project name to reset |
|
|
137
|
+
|
|
138
|
+
### `get_session_status`
|
|
139
|
+
|
|
140
|
+
Check whether a project has an active session, its ID, and turn count.
|
|
141
|
+
|
|
142
|
+
| Parameter | Type | Description |
|
|
143
|
+
|-----------|------|-------------|
|
|
144
|
+
| `project` | string | Project name to check |
|
|
145
|
+
|
|
146
|
+
## Dependencies
|
|
147
|
+
|
|
148
|
+
- Python >= 3.11
|
|
149
|
+
- `claude-code-sdk >= 0.0.25`
|
|
150
|
+
- `mcp >= 1.12.0`
|
|
151
|
+
- Claude Code CLI installed and on PATH
|
|
152
|
+
|
|
153
|
+
## Configuration
|
|
154
|
+
|
|
155
|
+
The `CLAUDE_CONTROL_PROJECTS` environment variable points to your `projects.json`. If unset, defaults to `projects.json` in the package's parent directory.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Claude Control — MCP server for coordinating Claude Code instances across projects."""
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Load and validate project configuration."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ProjectConfig:
|
|
12
|
+
name: str
|
|
13
|
+
path: str
|
|
14
|
+
description: str = ""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_projects(config_path: str | None = None) -> dict[str, ProjectConfig]:
|
|
18
|
+
"""Load project configurations from JSON file.
|
|
19
|
+
|
|
20
|
+
Resolution order for config path:
|
|
21
|
+
1. Explicit config_path argument
|
|
22
|
+
2. CLAUDE_CONTROL_PROJECTS environment variable
|
|
23
|
+
3. projects.json next to this package's parent directory
|
|
24
|
+
"""
|
|
25
|
+
if config_path is None:
|
|
26
|
+
config_path = os.environ.get("CLAUDE_CONTROL_PROJECTS")
|
|
27
|
+
|
|
28
|
+
if config_path is None:
|
|
29
|
+
# Default: projects.json in the repo root (parent of claude_control/)
|
|
30
|
+
package_dir = Path(__file__).resolve().parent
|
|
31
|
+
config_path = str(package_dir.parent / "projects.json")
|
|
32
|
+
|
|
33
|
+
config_file = Path(config_path)
|
|
34
|
+
if not config_file.exists():
|
|
35
|
+
print(
|
|
36
|
+
f"Config file not found: {config_file}. "
|
|
37
|
+
f"Create it or set CLAUDE_CONTROL_PROJECTS env var.",
|
|
38
|
+
file=sys.stderr,
|
|
39
|
+
)
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
with open(config_file) as f:
|
|
43
|
+
data = json.load(f)
|
|
44
|
+
|
|
45
|
+
projects: dict[str, ProjectConfig] = {}
|
|
46
|
+
for entry in data.get("projects", []):
|
|
47
|
+
name = entry.get("name")
|
|
48
|
+
path = entry.get("path")
|
|
49
|
+
|
|
50
|
+
if not name or not path:
|
|
51
|
+
print(
|
|
52
|
+
f"Skipping invalid project entry (missing name or path): {entry}",
|
|
53
|
+
file=sys.stderr,
|
|
54
|
+
)
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
resolved_path = Path(path).resolve()
|
|
58
|
+
if not resolved_path.exists():
|
|
59
|
+
print(
|
|
60
|
+
f"Warning: path does not exist for project '{name}': {resolved_path}",
|
|
61
|
+
file=sys.stderr,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
projects[name] = ProjectConfig(
|
|
65
|
+
name=name,
|
|
66
|
+
path=str(resolved_path),
|
|
67
|
+
description=entry.get("description", ""),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return projects
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Claude Control MCP Server — coordinate Claude Code instances across projects."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
from mcp.server.fastmcp import FastMCP
|
|
9
|
+
|
|
10
|
+
from .config import load_projects
|
|
11
|
+
from .session_manager import SessionManager
|
|
12
|
+
|
|
13
|
+
logging.basicConfig(
|
|
14
|
+
level=logging.INFO,
|
|
15
|
+
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
|
16
|
+
stream=sys.stderr,
|
|
17
|
+
)
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Initialize MCP server
|
|
21
|
+
mcp = FastMCP("Claude Control")
|
|
22
|
+
|
|
23
|
+
# Module-level session manager (initialized lazily)
|
|
24
|
+
_session_manager: SessionManager | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_manager() -> SessionManager:
|
|
28
|
+
global _session_manager
|
|
29
|
+
if _session_manager is None:
|
|
30
|
+
projects = load_projects()
|
|
31
|
+
if not projects:
|
|
32
|
+
logger.warning("No projects configured. Tools will return errors.")
|
|
33
|
+
else:
|
|
34
|
+
logger.info("Loaded %d project(s): %s", len(projects), ", ".join(projects.keys()))
|
|
35
|
+
_session_manager = SessionManager(projects)
|
|
36
|
+
return _session_manager
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@mcp.tool()
|
|
40
|
+
async def send_command(project: str, prompt: str) -> Dict[str, Any]:
|
|
41
|
+
"""Send a prompt to a Claude Code instance running in the specified project directory.
|
|
42
|
+
|
|
43
|
+
The remote instance maintains conversation context across calls — follow-up
|
|
44
|
+
prompts will have access to prior context. Use reset_session to start fresh.
|
|
45
|
+
|
|
46
|
+
The remote instance runs with full permissions and loads the target project's
|
|
47
|
+
own CLAUDE.md, MCP servers, and hooks.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
project: Name of the project (as defined in projects.json)
|
|
51
|
+
prompt: The prompt/command to send to the remote Claude instance
|
|
52
|
+
"""
|
|
53
|
+
manager = _get_manager()
|
|
54
|
+
try:
|
|
55
|
+
result = await manager.send(project, prompt)
|
|
56
|
+
response: Dict[str, Any] = {"text": result.text}
|
|
57
|
+
if result.session_id:
|
|
58
|
+
response["session_id"] = result.session_id
|
|
59
|
+
if result.is_error:
|
|
60
|
+
response["is_error"] = True
|
|
61
|
+
if result.cost_usd is not None:
|
|
62
|
+
response["cost_usd"] = result.cost_usd
|
|
63
|
+
if result.num_turns:
|
|
64
|
+
response["num_turns"] = result.num_turns
|
|
65
|
+
return response
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.exception("send_command failed for project '%s'", project)
|
|
68
|
+
return {"text": f"Error: {e}", "is_error": True}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@mcp.tool()
|
|
72
|
+
async def list_projects() -> Dict[str, Any]:
|
|
73
|
+
"""List all configured projects and their session status.
|
|
74
|
+
|
|
75
|
+
Returns project names, directory paths, descriptions, and whether
|
|
76
|
+
each project has an active Claude Code session.
|
|
77
|
+
"""
|
|
78
|
+
manager = _get_manager()
|
|
79
|
+
projects = []
|
|
80
|
+
for name, config in manager.projects.items():
|
|
81
|
+
status = manager.get_status(name)
|
|
82
|
+
projects.append({
|
|
83
|
+
"name": name,
|
|
84
|
+
"path": config.path,
|
|
85
|
+
"description": config.description,
|
|
86
|
+
"active_session": status["active"],
|
|
87
|
+
"session_id": status["session_id"],
|
|
88
|
+
"turn_count": status["turn_count"],
|
|
89
|
+
})
|
|
90
|
+
return {"projects": projects}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@mcp.tool()
|
|
94
|
+
async def reset_session(project: str) -> Dict[str, Any]:
|
|
95
|
+
"""Reset a project's Claude Code session, starting fresh on the next command.
|
|
96
|
+
|
|
97
|
+
Disconnects the persistent Claude Code subprocess for the named project.
|
|
98
|
+
The next send_command call will create a new session with no prior context.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
project: Name of the project whose session to reset
|
|
102
|
+
"""
|
|
103
|
+
manager = _get_manager()
|
|
104
|
+
try:
|
|
105
|
+
existed = await manager.reset(project)
|
|
106
|
+
if existed:
|
|
107
|
+
return {"status": "reset", "project": project}
|
|
108
|
+
return {"status": "no_session", "project": project, "message": "No active session to reset"}
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.exception("reset_session failed for project '%s'", project)
|
|
111
|
+
return {"status": "error", "project": project, "message": str(e)}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@mcp.tool()
|
|
115
|
+
async def get_session_status(project: str) -> Dict[str, Any]:
|
|
116
|
+
"""Check whether a project has an active Claude Code session.
|
|
117
|
+
|
|
118
|
+
Returns session activity status, session ID, and turn count.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
project: Name of the project to check
|
|
122
|
+
"""
|
|
123
|
+
manager = _get_manager()
|
|
124
|
+
return manager.get_status(project)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main():
|
|
128
|
+
try:
|
|
129
|
+
mcp.run()
|
|
130
|
+
except KeyboardInterrupt:
|
|
131
|
+
pass
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.error("Server failed: %s", e)
|
|
134
|
+
sys.exit(1)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
main()
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Manage persistent ClaudeSDKClient sessions for each project."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from claude_code_sdk import (
|
|
8
|
+
AssistantMessage,
|
|
9
|
+
ClaudeCodeOptions,
|
|
10
|
+
ClaudeSDKClient,
|
|
11
|
+
ResultMessage,
|
|
12
|
+
TextBlock,
|
|
13
|
+
)
|
|
14
|
+
from claude_code_sdk._internal.message_parser import parse_message
|
|
15
|
+
|
|
16
|
+
from .config import ProjectConfig
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class SessionInfo:
|
|
23
|
+
client: ClaudeSDKClient
|
|
24
|
+
session_id: str | None = None
|
|
25
|
+
turn_count: int = 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class SendResult:
|
|
30
|
+
text: str
|
|
31
|
+
session_id: str | None = None
|
|
32
|
+
is_error: bool = False
|
|
33
|
+
num_turns: int = 0
|
|
34
|
+
cost_usd: float | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SessionManager:
|
|
38
|
+
"""Manages persistent Claude Code sessions, one per project."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, projects: dict[str, ProjectConfig]) -> None:
|
|
41
|
+
self.projects = projects
|
|
42
|
+
self._sessions: dict[str, SessionInfo] = {}
|
|
43
|
+
|
|
44
|
+
def _make_options(self, project: ProjectConfig) -> ClaudeCodeOptions:
|
|
45
|
+
return ClaudeCodeOptions(
|
|
46
|
+
cwd=project.path,
|
|
47
|
+
permission_mode="bypassPermissions",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def _ensure_session(self, project_name: str) -> SessionInfo:
|
|
51
|
+
"""Get existing session or create a new one."""
|
|
52
|
+
if project_name not in self.projects:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"Unknown project: '{project_name}'. "
|
|
55
|
+
f"Available: {', '.join(sorted(self.projects.keys()))}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if project_name in self._sessions:
|
|
59
|
+
return self._sessions[project_name]
|
|
60
|
+
|
|
61
|
+
project = self.projects[project_name]
|
|
62
|
+
options = self._make_options(project)
|
|
63
|
+
client = ClaudeSDKClient(options)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
await client.connect()
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error("Failed to connect to Claude for project '%s': %s", project_name, e)
|
|
69
|
+
raise
|
|
70
|
+
|
|
71
|
+
info = SessionInfo(client=client)
|
|
72
|
+
self._sessions[project_name] = info
|
|
73
|
+
logger.info("Created new session for project '%s' (cwd=%s)", project_name, project.path)
|
|
74
|
+
return info
|
|
75
|
+
|
|
76
|
+
async def send(self, project_name: str, prompt: str) -> SendResult:
|
|
77
|
+
"""Send a prompt to a project's Claude instance and collect the response."""
|
|
78
|
+
info = await self._ensure_session(project_name)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
await info.client.query(prompt)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
# Connection may have died — try once to reconnect
|
|
84
|
+
logger.warning("Query failed for '%s', attempting reconnect: %s", project_name, e)
|
|
85
|
+
await self.reset(project_name)
|
|
86
|
+
info = await self._ensure_session(project_name)
|
|
87
|
+
await info.client.query(prompt)
|
|
88
|
+
|
|
89
|
+
text_parts: list[str] = []
|
|
90
|
+
result_msg: ResultMessage | None = None
|
|
91
|
+
|
|
92
|
+
# Read raw messages from the internal query stream and parse them
|
|
93
|
+
# ourselves. This lets us skip unknown message types (e.g.
|
|
94
|
+
# rate_limit_event) that the SDK's parse_message() doesn't handle,
|
|
95
|
+
# without killing the iteration.
|
|
96
|
+
async for raw in info.client._query.receive_messages():
|
|
97
|
+
try:
|
|
98
|
+
message = parse_message(raw)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.debug("Skipping unparseable message (type=%s): %s", raw.get("type"), e)
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if isinstance(message, AssistantMessage):
|
|
104
|
+
for block in message.content:
|
|
105
|
+
if isinstance(block, TextBlock):
|
|
106
|
+
text_parts.append(block.text)
|
|
107
|
+
elif isinstance(message, ResultMessage):
|
|
108
|
+
result_msg = message
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
info.turn_count += 1
|
|
112
|
+
|
|
113
|
+
if result_msg:
|
|
114
|
+
info.session_id = result_msg.session_id
|
|
115
|
+
return SendResult(
|
|
116
|
+
text="\n".join(text_parts) if text_parts else "(no text response)",
|
|
117
|
+
session_id=result_msg.session_id,
|
|
118
|
+
is_error=result_msg.is_error,
|
|
119
|
+
num_turns=result_msg.num_turns,
|
|
120
|
+
cost_usd=result_msg.total_cost_usd,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return SendResult(
|
|
124
|
+
text="\n".join(text_parts) if text_parts else "(no response received)",
|
|
125
|
+
is_error=True,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
async def reset(self, project_name: str) -> bool:
|
|
129
|
+
"""Disconnect and remove a project's session. Returns True if a session existed."""
|
|
130
|
+
info = self._sessions.pop(project_name, None)
|
|
131
|
+
if info is None:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
await info.client.disconnect()
|
|
136
|
+
except Exception:
|
|
137
|
+
# The SDK's disconnect can throw cancel scope errors due to
|
|
138
|
+
# anyio/asyncio interaction. The subprocess still gets cleaned up.
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
logger.info("Reset session for project '%s'", project_name)
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
def get_status(self, project_name: str) -> dict:
|
|
145
|
+
"""Get session status for a project."""
|
|
146
|
+
if project_name not in self.projects:
|
|
147
|
+
return {"error": f"Unknown project: '{project_name}'"}
|
|
148
|
+
|
|
149
|
+
info = self._sessions.get(project_name)
|
|
150
|
+
return {
|
|
151
|
+
"project": project_name,
|
|
152
|
+
"active": info is not None,
|
|
153
|
+
"session_id": info.session_id if info else None,
|
|
154
|
+
"turn_count": info.turn_count if info else 0,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async def shutdown(self) -> None:
|
|
158
|
+
"""Disconnect all sessions."""
|
|
159
|
+
for name in list(self._sessions.keys()):
|
|
160
|
+
await self.reset(name)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: claude-control
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for coordinating Claude Code instances across multiple projects
|
|
5
|
+
Author-email: Red Duck Labs <support@redducklabs.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/redducklabs/claude-control
|
|
8
|
+
Project-URL: Repository, https://github.com/redducklabs/claude-control
|
|
9
|
+
Project-URL: Issues, https://github.com/redducklabs/claude-control/issues
|
|
10
|
+
Keywords: mcp,claude,claude-code,automation,orchestration
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: claude-code-sdk>=0.0.25
|
|
22
|
+
Requires-Dist: mcp>=1.12.0
|
|
23
|
+
|
|
24
|
+
# Claude Control
|
|
25
|
+
|
|
26
|
+
MCP server that lets a Claude Code session coordinate with Claude Code instances running in other project directories.
|
|
27
|
+
|
|
28
|
+
## How It Works
|
|
29
|
+
|
|
30
|
+
Claude Control is an MCP server (stdio transport) that exposes tools for dispatching prompts to Claude Code instances in configured project directories. Each remote instance:
|
|
31
|
+
|
|
32
|
+
- Runs as a persistent subprocess with conversation context preserved across calls
|
|
33
|
+
- Loads the target project's own `CLAUDE.md`, `.mcp.json`, hooks, and settings
|
|
34
|
+
- Runs with `bypassPermissions` for fully autonomous operation
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
### From PyPI
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install claude-control
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv tool install claude-control
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### From Source
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone https://github.com/redducklabs/claude-control.git
|
|
54
|
+
cd claude-control
|
|
55
|
+
pip install .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Setup
|
|
59
|
+
|
|
60
|
+
### 1. Configure Projects
|
|
61
|
+
|
|
62
|
+
Create a `projects.json` file (see `projects.json.example`):
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"projects": [
|
|
67
|
+
{
|
|
68
|
+
"name": "my-backend",
|
|
69
|
+
"path": "D:\\repos\\my-backend",
|
|
70
|
+
"description": "Backend API service"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "my-frontend",
|
|
74
|
+
"path": "D:\\repos\\my-frontend",
|
|
75
|
+
"description": "Frontend web application"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 2. Register as an MCP Server
|
|
82
|
+
|
|
83
|
+
Add to the `.mcp.json` of the project where you want coordination tools available.
|
|
84
|
+
|
|
85
|
+
**Using `uvx` (recommended — no global install needed):**
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"claude_control": {
|
|
91
|
+
"command": "uvx",
|
|
92
|
+
"args": ["claude-control"],
|
|
93
|
+
"env": {
|
|
94
|
+
"CLAUDE_CONTROL_PROJECTS": "/path/to/your/projects.json"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Using a pip install:**
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"mcpServers": {
|
|
106
|
+
"claude_control": {
|
|
107
|
+
"command": "claude-control",
|
|
108
|
+
"env": {
|
|
109
|
+
"CLAUDE_CONTROL_PROJECTS": "/path/to/your/projects.json"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Using `python -m`:**
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"mcpServers": {
|
|
121
|
+
"claude_control": {
|
|
122
|
+
"command": "python",
|
|
123
|
+
"args": ["-m", "claude_control"],
|
|
124
|
+
"env": {
|
|
125
|
+
"CLAUDE_CONTROL_PROJECTS": "/path/to/your/projects.json"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 3. Restart Claude Code
|
|
133
|
+
|
|
134
|
+
The tools will appear as `mcp__claude_control__send_command`, `mcp__claude_control__list_projects`, etc.
|
|
135
|
+
|
|
136
|
+
## Tools
|
|
137
|
+
|
|
138
|
+
### `send_command`
|
|
139
|
+
|
|
140
|
+
Send a prompt to a Claude Code instance in the specified project directory.
|
|
141
|
+
|
|
142
|
+
| Parameter | Type | Description |
|
|
143
|
+
|-----------|------|-------------|
|
|
144
|
+
| `project` | string | Project name (from projects.json) |
|
|
145
|
+
| `prompt` | string | The prompt to send |
|
|
146
|
+
|
|
147
|
+
Returns the full text response from the remote instance. Sessions persist across calls — follow-up prompts have access to prior context.
|
|
148
|
+
|
|
149
|
+
### `list_projects`
|
|
150
|
+
|
|
151
|
+
List all configured projects with their paths, descriptions, and session status.
|
|
152
|
+
|
|
153
|
+
### `reset_session`
|
|
154
|
+
|
|
155
|
+
Tear down a project's Claude Code session. The next `send_command` call creates a fresh session with no prior context.
|
|
156
|
+
|
|
157
|
+
| Parameter | Type | Description |
|
|
158
|
+
|-----------|------|-------------|
|
|
159
|
+
| `project` | string | Project name to reset |
|
|
160
|
+
|
|
161
|
+
### `get_session_status`
|
|
162
|
+
|
|
163
|
+
Check whether a project has an active session, its ID, and turn count.
|
|
164
|
+
|
|
165
|
+
| Parameter | Type | Description |
|
|
166
|
+
|-----------|------|-------------|
|
|
167
|
+
| `project` | string | Project name to check |
|
|
168
|
+
|
|
169
|
+
## Dependencies
|
|
170
|
+
|
|
171
|
+
- Python >= 3.11
|
|
172
|
+
- `claude-code-sdk >= 0.0.25`
|
|
173
|
+
- `mcp >= 1.12.0`
|
|
174
|
+
- Claude Code CLI installed and on PATH
|
|
175
|
+
|
|
176
|
+
## Configuration
|
|
177
|
+
|
|
178
|
+
The `CLAUDE_CONTROL_PROJECTS` environment variable points to your `projects.json`. If unset, defaults to `projects.json` in the package's parent directory.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
claude_control/__init__.py
|
|
4
|
+
claude_control/__main__.py
|
|
5
|
+
claude_control/config.py
|
|
6
|
+
claude_control/server.py
|
|
7
|
+
claude_control/session_manager.py
|
|
8
|
+
claude_control.egg-info/PKG-INFO
|
|
9
|
+
claude_control.egg-info/SOURCES.txt
|
|
10
|
+
claude_control.egg-info/dependency_links.txt
|
|
11
|
+
claude_control.egg-info/entry_points.txt
|
|
12
|
+
claude_control.egg-info/requires.txt
|
|
13
|
+
claude_control.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
claude_control
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "claude-control"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server for coordinating Claude Code instances across multiple projects"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Red Duck Labs", email = "support@redducklabs.com"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["mcp", "claude", "claude-code", "automation", "orchestration"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"claude-code-sdk>=0.0.25",
|
|
28
|
+
"mcp>=1.12.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
claude-control = "claude_control.server:main"
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/redducklabs/claude-control"
|
|
36
|
+
Repository = "https://github.com/redducklabs/claude-control"
|
|
37
|
+
Issues = "https://github.com/redducklabs/claude-control/issues"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["."]
|
|
41
|
+
include = ["claude_control*"]
|