claude_swarm 0.1.5 → 0.1.7
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +25 -0
- data/README.md +49 -4
- data/claude-swarm.yml +9 -4
- data/lib/claude_swarm/claude_code_executor.rb +145 -22
- data/lib/claude_swarm/claude_mcp_server.rb +8 -152
- data/lib/claude_swarm/cli.rb +15 -0
- data/lib/claude_swarm/configuration.rb +2 -1
- data/lib/claude_swarm/mcp_generator.rb +22 -1
- data/lib/claude_swarm/orchestrator.rb +5 -1
- data/lib/claude_swarm/permission_mcp_server.rb +77 -0
- data/lib/claude_swarm/permission_tool.rb +63 -0
- data/lib/claude_swarm/reset_session_tool.rb +22 -0
- data/lib/claude_swarm/session_info_tool.rb +22 -0
- data/lib/claude_swarm/task_tool.rb +32 -0
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm.rb +2 -0
- data/sdk-docs.md +426 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0b5aa9e698fe0c055fc16aa5263df48a2c6e473179d5472e06f83c8845a3447c
|
4
|
+
data.tar.gz: ce36d61d30177b38e2f9dbb6f9b00fc1222f0d5ee4cb4539e75f912ad9777904
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ffbac2fb59e4f76ab39efe172a89d1a11b0893efd763770ba3f4c6e52785df44d19cc0db9a8dd87e2df52c05c7d8cceadc47deb2a8304e34ccb2c921b781842c
|
7
|
+
data.tar.gz: dd4eeb38f8d56c3cd41570fe0be0ccda029240893780d95c82c134ad02d91c840ba8fd3fb344dc205c7a3345e37fbce33b8f633abd076390783b31babaf6014d
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,28 @@
|
|
1
|
+
## [0.1.7]
|
2
|
+
|
3
|
+
### Added
|
4
|
+
- **Vibe mode support**: Per-instance `vibe: true` configuration to skip all permission checks for specific instances
|
5
|
+
- **Automatic permission management**: Built-in permission MCP server that handles tool authorization without manual approval
|
6
|
+
- **Permission logging**: All permission checks are logged to `.claude-swarm/sessions/{timestamp}/permissions.log`
|
7
|
+
- **Mixed permission modes**: Support for running some instances with full permissions while others remain restricted
|
8
|
+
- **New CLI command**: `claude-swarm tools-mcp` for starting a standalone permission management MCP server
|
9
|
+
- **Permission tool patterns**: Support for wildcard patterns in tool permissions (e.g., `mcp__frontend__*`)
|
10
|
+
|
11
|
+
### Changed
|
12
|
+
- Fixed `--system-prompt` to use `--append-system-prompt` for proper Claude Code integration
|
13
|
+
- Added `--permission-prompt-tool` flag pointing to `mcp__permissions__check_permission` when not in vibe mode
|
14
|
+
- Enhanced MCP generation to include a permission server for each instance (unless in vibe mode)
|
15
|
+
|
16
|
+
### Technical Details
|
17
|
+
- Permission checks use Fast MCP server with pattern matching for tool names
|
18
|
+
- Each instance can have its own permission configuration independent of global settings
|
19
|
+
- Permission decisions are made based on configured tool patterns with wildcard support
|
20
|
+
|
21
|
+
## [0.1.6]
|
22
|
+
- Refactor: move tools out of the ClaudeMcpServer class
|
23
|
+
- Move logging into code executor and save instance interaction streams to session.log
|
24
|
+
- Human readable logs with thoughts and tool calls
|
25
|
+
|
1
26
|
## [0.1.5]
|
2
27
|
|
3
28
|
### Changed
|
data/README.md
CHANGED
@@ -208,6 +208,7 @@ Each instance can have:
|
|
208
208
|
- **tools**: Array of tools this instance can use
|
209
209
|
- **mcps**: Array of additional MCP servers to connect
|
210
210
|
- **prompt**: Custom system prompt to append to the instance
|
211
|
+
- **vibe**: Enable vibe mode (--dangerously-skip-permissions) for this instance (default: false)
|
211
212
|
|
212
213
|
```yaml
|
213
214
|
instance_name:
|
@@ -216,6 +217,7 @@ instance_name:
|
|
216
217
|
model: opus
|
217
218
|
connections: [other_instance1, other_instance2]
|
218
219
|
prompt: "You are a specialized agent focused on..."
|
220
|
+
vibe: false # Set to true to skip all permission checks for this instance
|
219
221
|
tools:
|
220
222
|
- Read
|
221
223
|
- Edit
|
@@ -388,6 +390,37 @@ swarm:
|
|
388
390
|
- Read
|
389
391
|
```
|
390
392
|
|
393
|
+
#### Mixed Permission Modes
|
394
|
+
|
395
|
+
You can have different permission modes for different instances:
|
396
|
+
|
397
|
+
```yaml
|
398
|
+
version: 1
|
399
|
+
swarm:
|
400
|
+
name: "Mixed Mode Team"
|
401
|
+
main: lead
|
402
|
+
instances:
|
403
|
+
lead:
|
404
|
+
description: "Lead with full permissions"
|
405
|
+
directory: .
|
406
|
+
model: opus
|
407
|
+
vibe: true # This instance runs with --dangerously-skip-permissions
|
408
|
+
connections: [restricted_worker, trusted_worker]
|
409
|
+
|
410
|
+
restricted_worker:
|
411
|
+
description: "Worker with restricted permissions"
|
412
|
+
directory: ./sensitive
|
413
|
+
model: sonnet
|
414
|
+
tools: [Read, "Bash(ls:*)"] # Only allow read and ls commands
|
415
|
+
|
416
|
+
trusted_worker:
|
417
|
+
description: "Trusted worker with more permissions"
|
418
|
+
directory: ./workspace
|
419
|
+
model: sonnet
|
420
|
+
vibe: true # This instance also skips permissions
|
421
|
+
tools: [] # Tools list ignored when vibe: true
|
422
|
+
```
|
423
|
+
|
391
424
|
### Command Line Options
|
392
425
|
|
393
426
|
```bash
|
@@ -408,6 +441,9 @@ claude-swarm --prompt "Fix the bug in the payment module"
|
|
408
441
|
# Show version
|
409
442
|
claude-swarm version
|
410
443
|
|
444
|
+
# Start permission MCP server (for testing/debugging)
|
445
|
+
claude-swarm tools-mcp --allowed-tools 'mcp__frontend__*,mcp__backend__*'
|
446
|
+
|
411
447
|
# Internal command for MCP server (used by connected instances)
|
412
448
|
claude-swarm mcp-serve INSTANCE_NAME --config CONFIG_FILE --session-timestamp TIMESTAMP
|
413
449
|
```
|
@@ -418,18 +454,27 @@ claude-swarm mcp-serve INSTANCE_NAME --config CONFIG_FILE --session-timestamp TI
|
|
418
454
|
2. **MCP Generation**: For each instance, it generates an MCP configuration file that includes:
|
419
455
|
- Any explicitly defined MCP servers
|
420
456
|
- MCP servers for each connected instance (using `claude-swarm mcp-serve`)
|
421
|
-
|
457
|
+
- A permission MCP server (unless using `--vibe` mode)
|
458
|
+
3. **Tool Permissions**: Claude Swarm automatically manages tool permissions:
|
459
|
+
- Each instance's configured tools are allowed via the permission MCP
|
460
|
+
- Supports wildcard patterns (e.g., `mcp__frontend__*` allows all frontend MCP tools)
|
461
|
+
- Eliminates the need to manually accept each tool or use global `--vibe` mode
|
462
|
+
- Per-instance `vibe: true` skips all permission checks for that specific instance
|
463
|
+
- The permission MCP uses `--permission-prompt-tool` to check tool access
|
464
|
+
- Permission decisions are logged to `.claude-swarm/sessions/{timestamp}/permissions.log`
|
465
|
+
4. **Session Management**: Claude Swarm maintains session continuity:
|
422
466
|
- Generates a shared session timestamp for all instances
|
423
467
|
- Each instance can maintain its own Claude session ID
|
424
468
|
- Sessions can be reset via the MCP server interface
|
425
|
-
|
426
|
-
|
469
|
+
5. **Main Instance Launch**: The main instance is launched with its MCP configuration, giving it access to all connected instances
|
470
|
+
6. **Inter-Instance Communication**: Connected instances expose themselves as MCP servers with these tools:
|
427
471
|
- **task**: Execute tasks using Claude Code with configurable tools and return results. The tool description includes the instance name and description (e.g., "Execute a task using Agent frontend_dev. Frontend developer specializing in React and TypeScript")
|
428
472
|
- **session_info**: Get current Claude session information including ID and working directory
|
429
473
|
- **reset_session**: Reset the Claude session for a fresh start
|
430
|
-
|
474
|
+
7. **Session Management**: All session files are organized in `.claude-swarm/sessions/{timestamp}/`:
|
431
475
|
- MCP configuration files: `{instance_name}.mcp.json`
|
432
476
|
- Session log: `session.log` with detailed request/response tracking
|
477
|
+
- Permission log: `permissions.log` with all permission checks and decisions
|
433
478
|
|
434
479
|
## Troubleshooting
|
435
480
|
|
data/claude-swarm.yml
CHANGED
@@ -6,9 +6,9 @@ swarm:
|
|
6
6
|
lead_developer:
|
7
7
|
description: "Lead developer coordinating the team and making architectural decisions"
|
8
8
|
directory: .
|
9
|
-
model:
|
9
|
+
model: opus
|
10
10
|
prompt: "You are the lead developer coordinating the team"
|
11
|
-
|
11
|
+
vibe: true
|
12
12
|
connections: [frontend_dev]
|
13
13
|
|
14
14
|
# Example instances (uncomment and modify as needed):
|
@@ -16,6 +16,11 @@ swarm:
|
|
16
16
|
frontend_dev:
|
17
17
|
description: "Frontend developer specializing in React and modern web technologies"
|
18
18
|
directory: .
|
19
|
-
model:
|
19
|
+
model: opus
|
20
20
|
prompt: "You specialize in frontend development with React, TypeScript, and modern web technologies"
|
21
|
-
tools: [
|
21
|
+
tools: [mcp__headless_browser__*]
|
22
|
+
mcps:
|
23
|
+
- name: headless_browser
|
24
|
+
type: stdio
|
25
|
+
command: bundle
|
26
|
+
args: ["exec", "hbt", "stdio"]
|
@@ -2,43 +2,85 @@
|
|
2
2
|
|
3
3
|
require "json"
|
4
4
|
require "open3"
|
5
|
+
require "logger"
|
6
|
+
require "fileutils"
|
5
7
|
|
6
8
|
module ClaudeSwarm
|
7
9
|
class ClaudeCodeExecutor
|
8
|
-
|
10
|
+
SWARM_DIR = ".claude-swarm"
|
11
|
+
SESSIONS_DIR = "sessions"
|
9
12
|
|
10
|
-
|
13
|
+
attr_reader :session_id, :last_response, :working_directory, :logger, :session_timestamp
|
14
|
+
|
15
|
+
def initialize(working_directory: Dir.pwd, model: nil, mcp_config: nil, vibe: false, instance_name: nil, calling_instance: nil)
|
11
16
|
@working_directory = working_directory
|
12
17
|
@model = model
|
13
18
|
@mcp_config = mcp_config
|
14
19
|
@vibe = vibe
|
15
20
|
@session_id = nil
|
16
21
|
@last_response = nil
|
22
|
+
@instance_name = instance_name
|
23
|
+
@calling_instance = calling_instance
|
24
|
+
|
25
|
+
# Setup logging
|
26
|
+
setup_logging
|
17
27
|
end
|
18
28
|
|
19
29
|
def execute(prompt, options = {})
|
20
|
-
|
21
|
-
|
22
|
-
stdout, stderr, status = Open3.capture3(*cmd_array, chdir: @working_directory)
|
23
|
-
|
24
|
-
raise ExecutionError, "Claude Code execution failed: #{stderr}" unless status.success?
|
30
|
+
# Log the request
|
31
|
+
log_request(prompt)
|
25
32
|
|
26
|
-
|
27
|
-
response = JSON.parse(stdout)
|
28
|
-
@last_response = response
|
29
|
-
|
30
|
-
# Extract and store session ID from the response
|
31
|
-
@session_id = response["session_id"]
|
33
|
+
cmd_array = build_command_array(prompt, options)
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
-
|
35
|
+
# Variables to collect output
|
36
|
+
stderr_output = []
|
37
|
+
result_response = nil
|
38
|
+
|
39
|
+
# Execute command with streaming
|
40
|
+
Open3.popen3(*cmd_array, chdir: @working_directory) do |stdin, stdout, stderr, wait_thread|
|
41
|
+
stdin.close
|
42
|
+
|
43
|
+
# Read stderr in a separate thread
|
44
|
+
stderr_thread = Thread.new do
|
45
|
+
stderr.each_line { |line| stderr_output << line }
|
46
|
+
end
|
47
|
+
|
48
|
+
# Process stdout line by line
|
49
|
+
stdout.each_line do |line|
|
50
|
+
json_data = JSON.parse(line.strip)
|
51
|
+
|
52
|
+
# Log each JSON event
|
53
|
+
log_streaming_event(json_data)
|
54
|
+
|
55
|
+
# Capture session_id from system init
|
56
|
+
@session_id = json_data["session_id"] if json_data["type"] == "system" && json_data["subtype"] == "init"
|
57
|
+
|
58
|
+
# Capture the final result
|
59
|
+
result_response = json_data if json_data["type"] == "result"
|
60
|
+
rescue JSON::ParserError => e
|
61
|
+
@logger.warn("Failed to parse JSON line: #{line.strip} - #{e.message}")
|
62
|
+
end
|
63
|
+
|
64
|
+
# Wait for stderr thread to finish
|
65
|
+
stderr_thread.join
|
66
|
+
|
67
|
+
# Check exit status
|
68
|
+
exit_status = wait_thread.value
|
69
|
+
unless exit_status.success?
|
70
|
+
error_msg = stderr_output.join
|
71
|
+
@logger.error("Execution error for #{@instance_name}: #{error_msg}")
|
72
|
+
raise ExecutionError, "Claude Code execution failed: #{error_msg}"
|
73
|
+
end
|
36
74
|
end
|
37
|
-
end
|
38
75
|
|
39
|
-
|
40
|
-
|
41
|
-
|
76
|
+
# Ensure we got a result
|
77
|
+
raise ParseError, "No result found in stream output" unless result_response
|
78
|
+
|
79
|
+
result_response
|
80
|
+
rescue StandardError => e
|
81
|
+
@logger.error("Unexpected error for #{@instance_name}: #{e.class} - #{e.message}")
|
82
|
+
@logger.error("Backtrace: #{e.backtrace.join("\n")}")
|
83
|
+
raise
|
42
84
|
end
|
43
85
|
|
44
86
|
def reset_session
|
@@ -52,12 +94,90 @@ module ClaudeSwarm
|
|
52
94
|
|
53
95
|
private
|
54
96
|
|
97
|
+
def setup_logging
|
98
|
+
# Use environment variable for session timestamp if available (set by orchestrator)
|
99
|
+
# Otherwise create a new timestamp
|
100
|
+
@session_timestamp = ENV["CLAUDE_SWARM_SESSION_TIMESTAMP"] || Time.now.strftime("%Y%m%d_%H%M%S")
|
101
|
+
|
102
|
+
# Ensure the session directory exists
|
103
|
+
session_dir = File.join(Dir.pwd, SWARM_DIR, SESSIONS_DIR, @session_timestamp)
|
104
|
+
FileUtils.mkdir_p(session_dir)
|
105
|
+
|
106
|
+
# Create logger with session.log filename
|
107
|
+
log_filename = "session.log"
|
108
|
+
log_path = File.join(session_dir, log_filename)
|
109
|
+
@logger = Logger.new(log_path)
|
110
|
+
@logger.level = Logger::INFO
|
111
|
+
|
112
|
+
# Custom formatter for better readability
|
113
|
+
@logger.formatter = proc do |severity, datetime, _progname, msg|
|
114
|
+
"[#{datetime.strftime("%Y-%m-%d %H:%M:%S.%L")}] [#{severity}] #{msg}\n"
|
115
|
+
end
|
116
|
+
|
117
|
+
@logger.info("Started Claude Code executor for instance: #{@instance_name}") if @instance_name
|
118
|
+
end
|
119
|
+
|
120
|
+
def log_request(prompt)
|
121
|
+
@logger.info("#{@calling_instance} -> #{@instance_name}: \n---\n#{prompt}\n---")
|
122
|
+
end
|
123
|
+
|
124
|
+
def log_response(response)
|
125
|
+
@logger.info(
|
126
|
+
"($#{response["total_cost"]} - #{response["duration_ms"]}ms) #{@instance_name} -> #{@calling_instance}: \n---\n#{response["result"]}\n---"
|
127
|
+
)
|
128
|
+
end
|
129
|
+
|
130
|
+
def log_streaming_event(event)
|
131
|
+
return log_system_message(event) if event["type"] == "system"
|
132
|
+
|
133
|
+
# Add specific details based on event type
|
134
|
+
case event["type"]
|
135
|
+
when "assistant"
|
136
|
+
log_assistant_message(event["message"])
|
137
|
+
when "user"
|
138
|
+
log_user_message(event["message"]["content"])
|
139
|
+
when "result"
|
140
|
+
log_response(event)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def log_system_message(event)
|
145
|
+
@logger.debug("SYSTEM: #{JSON.pretty_generate(event)}")
|
146
|
+
end
|
147
|
+
|
148
|
+
def log_assistant_message(msg)
|
149
|
+
return if msg["stop_reason"] == "end_turn" # that means it is not a thought but the final answer
|
150
|
+
|
151
|
+
content = msg["content"]
|
152
|
+
@logger.debug("ASSISTANT: #{JSON.pretty_generate(content)}")
|
153
|
+
tool_calls = content.select { |c| c["type"] == "tool_use" }
|
154
|
+
tool_calls.each do |tool_call|
|
155
|
+
arguments = tool_call["input"].to_json
|
156
|
+
arguments = "#{arguments[0..300]} ...}" if arguments.length > 300
|
157
|
+
|
158
|
+
@logger.info(
|
159
|
+
"Tool call from #{@instance_name} -> Tool: #{tool_call["name"]}, ID: #{tool_call["id"]}, Arguments: #{arguments}"
|
160
|
+
)
|
161
|
+
end
|
162
|
+
|
163
|
+
text = content.select { |c| c["type"] == "text" }
|
164
|
+
text.each do |t|
|
165
|
+
@logger.info("#{@instance_name} is thinking:\n---\n#{t["text"]}\n---")
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def log_user_message(content)
|
170
|
+
@logger.debug("USER: #{JSON.pretty_generate(content)}")
|
171
|
+
end
|
172
|
+
|
55
173
|
def build_command_array(prompt, options)
|
56
174
|
cmd_array = ["claude"]
|
57
175
|
|
58
176
|
# Add model if specified
|
59
177
|
cmd_array += ["--model", @model]
|
60
178
|
|
179
|
+
cmd_array << "--verbose"
|
180
|
+
|
61
181
|
# Add MCP config if specified
|
62
182
|
cmd_array += ["--mcp-config", @mcp_config] if @mcp_config
|
63
183
|
|
@@ -65,13 +185,13 @@ module ClaudeSwarm
|
|
65
185
|
cmd_array += ["--resume", @session_id] if @session_id && !options[:new_session]
|
66
186
|
|
67
187
|
# Always use JSON output format for structured responses
|
68
|
-
cmd_array += ["--output-format", "json"]
|
188
|
+
cmd_array += ["--output-format", "stream-json"]
|
69
189
|
|
70
190
|
# Add non-interactive mode with prompt
|
71
191
|
cmd_array += ["--print", "-p", prompt]
|
72
192
|
|
73
193
|
# Add any custom system prompt
|
74
|
-
cmd_array += ["--system-prompt", options[:system_prompt]] if options[:system_prompt]
|
194
|
+
cmd_array += ["--append-system-prompt", options[:system_prompt]] if options[:system_prompt]
|
75
195
|
|
76
196
|
# Add any allowed tools or vibe flag
|
77
197
|
if @vibe
|
@@ -81,6 +201,9 @@ module ClaudeSwarm
|
|
81
201
|
cmd_array += ["--allowedTools", tools]
|
82
202
|
end
|
83
203
|
|
204
|
+
# Add permission prompt tool if not in vibe mode
|
205
|
+
cmd_array += ["--permission-prompt-tool", "mcp__permissions__check_permission"] unless @vibe
|
206
|
+
|
84
207
|
cmd_array
|
85
208
|
end
|
86
209
|
|
@@ -2,15 +2,13 @@
|
|
2
2
|
|
3
3
|
require "fast_mcp"
|
4
4
|
require "json"
|
5
|
-
require "fileutils"
|
6
|
-
require "logger"
|
7
5
|
require_relative "claude_code_executor"
|
6
|
+
require_relative "task_tool"
|
7
|
+
require_relative "session_info_tool"
|
8
|
+
require_relative "reset_session_tool"
|
8
9
|
|
9
10
|
module ClaudeSwarm
|
10
11
|
class ClaudeMcpServer
|
11
|
-
SWARM_DIR = ".claude-swarm"
|
12
|
-
SESSIONS_DIR = "sessions"
|
13
|
-
|
14
12
|
# Class variables to share state with tool classes
|
15
13
|
class << self
|
16
14
|
attr_accessor :executor, :instance_config, :logger, :session_timestamp, :calling_instance
|
@@ -23,46 +21,19 @@ module ClaudeSwarm
|
|
23
21
|
working_directory: instance_config[:directory],
|
24
22
|
model: instance_config[:model],
|
25
23
|
mcp_config: instance_config[:mcp_config_path],
|
26
|
-
vibe: instance_config[:vibe]
|
24
|
+
vibe: instance_config[:vibe],
|
25
|
+
instance_name: instance_config[:name],
|
26
|
+
calling_instance: calling_instance
|
27
27
|
)
|
28
28
|
|
29
|
-
# Setup logging
|
30
|
-
setup_logging
|
31
|
-
|
32
29
|
# Set class variables so tools can access them
|
33
30
|
self.class.executor = @executor
|
34
31
|
self.class.instance_config = @instance_config
|
35
|
-
self.class.logger = @logger
|
32
|
+
self.class.logger = @executor.logger
|
33
|
+
self.class.session_timestamp = @executor.session_timestamp
|
36
34
|
self.class.calling_instance = @calling_instance
|
37
35
|
end
|
38
36
|
|
39
|
-
private
|
40
|
-
|
41
|
-
def setup_logging
|
42
|
-
# Use environment variable for session timestamp if available (set by orchestrator)
|
43
|
-
# Otherwise create a new timestamp
|
44
|
-
self.class.session_timestamp ||= ENV["CLAUDE_SWARM_SESSION_TIMESTAMP"] || Time.now.strftime("%Y%m%d_%H%M%S")
|
45
|
-
|
46
|
-
# Ensure the session directory exists
|
47
|
-
session_dir = File.join(Dir.pwd, SWARM_DIR, SESSIONS_DIR, self.class.session_timestamp)
|
48
|
-
FileUtils.mkdir_p(session_dir)
|
49
|
-
|
50
|
-
# Create logger with session.log filename
|
51
|
-
log_filename = "session.log"
|
52
|
-
log_path = File.join(session_dir, log_filename)
|
53
|
-
@logger = Logger.new(log_path)
|
54
|
-
@logger.level = Logger::INFO
|
55
|
-
|
56
|
-
# Custom formatter for better readability
|
57
|
-
@logger.formatter = proc do |severity, datetime, _progname, msg|
|
58
|
-
"[#{datetime.strftime("%Y-%m-%d %H:%M:%S.%L")}] [#{severity}] #{msg}\n"
|
59
|
-
end
|
60
|
-
|
61
|
-
@logger.info("Started MCP server for instance: #{@instance_config[:name]}")
|
62
|
-
end
|
63
|
-
|
64
|
-
public
|
65
|
-
|
66
37
|
def start
|
67
38
|
server = FastMcp::Server.new(
|
68
39
|
name: @instance_config[:name],
|
@@ -84,120 +55,5 @@ module ClaudeSwarm
|
|
84
55
|
# Start the stdio server
|
85
56
|
server.start
|
86
57
|
end
|
87
|
-
|
88
|
-
class TaskTool < FastMcp::Tool
|
89
|
-
tool_name "task"
|
90
|
-
description "Execute a task using Claude Code"
|
91
|
-
|
92
|
-
arguments do
|
93
|
-
required(:prompt).filled(:string).description("The task or question for the agent")
|
94
|
-
optional(:new_session).filled(:bool).description("Start a new session (default: false)")
|
95
|
-
optional(:system_prompt).filled(:string).description("Override the system prompt for this request")
|
96
|
-
end
|
97
|
-
|
98
|
-
def call(prompt:, new_session: false, system_prompt: nil)
|
99
|
-
executor = ClaudeMcpServer.executor
|
100
|
-
instance_config = ClaudeMcpServer.instance_config
|
101
|
-
logger = ClaudeMcpServer.logger
|
102
|
-
|
103
|
-
options = {
|
104
|
-
new_session: new_session,
|
105
|
-
system_prompt: system_prompt || instance_config[:prompt]
|
106
|
-
}
|
107
|
-
|
108
|
-
# Add allowed tools from instance config
|
109
|
-
options[:allowed_tools] = instance_config[:tools] if instance_config[:tools]&.any?
|
110
|
-
|
111
|
-
begin
|
112
|
-
# Log the request
|
113
|
-
log_entry = {
|
114
|
-
timestamp: Time.now.utc.iso8601,
|
115
|
-
from_instance: ClaudeMcpServer.calling_instance, # The instance making the request
|
116
|
-
to_instance: instance_config[:name], # This instance is receiving the request
|
117
|
-
model: instance_config[:model],
|
118
|
-
working_directory: instance_config[:directory],
|
119
|
-
session_id: executor.session_id,
|
120
|
-
request: {
|
121
|
-
prompt: prompt,
|
122
|
-
new_session: new_session,
|
123
|
-
system_prompt: options[:system_prompt],
|
124
|
-
allowed_tools: options[:allowed_tools]
|
125
|
-
}
|
126
|
-
}
|
127
|
-
|
128
|
-
logger.info("REQUEST: #{JSON.pretty_generate(log_entry)}")
|
129
|
-
|
130
|
-
response = executor.execute(prompt, options)
|
131
|
-
|
132
|
-
# Log the response
|
133
|
-
response_entry = {
|
134
|
-
timestamp: Time.now.utc.iso8601,
|
135
|
-
from_instance: instance_config[:name], # This instance is sending the response
|
136
|
-
to_instance: ClaudeMcpServer.calling_instance, # The instance that made the request receives the response
|
137
|
-
session_id: executor.session_id, # Update with new session ID if changed
|
138
|
-
response: {
|
139
|
-
result: response["result"],
|
140
|
-
cost_usd: response["cost_usd"],
|
141
|
-
duration_ms: response["duration_ms"],
|
142
|
-
is_error: response["is_error"],
|
143
|
-
total_cost: response["total_cost"]
|
144
|
-
}
|
145
|
-
}
|
146
|
-
|
147
|
-
logger.info("RESPONSE: #{JSON.pretty_generate(response_entry)}")
|
148
|
-
|
149
|
-
# Return just the result text as expected by MCP
|
150
|
-
response["result"]
|
151
|
-
rescue ClaudeCodeExecutor::ExecutionError => e
|
152
|
-
logger.error("Execution error for #{instance_config[:name]}: #{e.message}")
|
153
|
-
raise StandardError, "Execution failed: #{e.message}"
|
154
|
-
rescue ClaudeCodeExecutor::ParseError => e
|
155
|
-
logger.error("Parse error for #{instance_config[:name]}: #{e.message}")
|
156
|
-
raise StandardError, "Parse error: #{e.message}"
|
157
|
-
rescue StandardError => e
|
158
|
-
logger.error("Unexpected error for #{instance_config[:name]}: #{e.class} - #{e.message}")
|
159
|
-
logger.error("Backtrace: #{e.backtrace.join("\n")}")
|
160
|
-
raise StandardError, "Unexpected error: #{e.message}"
|
161
|
-
end
|
162
|
-
end
|
163
|
-
end
|
164
|
-
|
165
|
-
class SessionInfoTool < FastMcp::Tool
|
166
|
-
tool_name "session_info"
|
167
|
-
description "Get information about the current Claude session for this agent"
|
168
|
-
|
169
|
-
arguments do
|
170
|
-
# No arguments needed
|
171
|
-
end
|
172
|
-
|
173
|
-
def call
|
174
|
-
executor = ClaudeMcpServer.executor
|
175
|
-
|
176
|
-
{
|
177
|
-
has_session: executor.has_session?,
|
178
|
-
session_id: executor.session_id,
|
179
|
-
working_directory: executor.working_directory
|
180
|
-
}
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
class ResetSessionTool < FastMcp::Tool
|
185
|
-
tool_name "reset_session"
|
186
|
-
description "Reset the Claude session for this agent, starting fresh on the next task"
|
187
|
-
|
188
|
-
arguments do
|
189
|
-
# No arguments needed
|
190
|
-
end
|
191
|
-
|
192
|
-
def call
|
193
|
-
executor = ClaudeMcpServer.executor
|
194
|
-
executor.reset_session
|
195
|
-
|
196
|
-
{
|
197
|
-
success: true,
|
198
|
-
message: "Session has been reset"
|
199
|
-
}
|
200
|
-
end
|
201
|
-
end
|
202
58
|
end
|
203
59
|
end
|
data/lib/claude_swarm/cli.rb
CHANGED
@@ -5,6 +5,7 @@ require_relative "configuration"
|
|
5
5
|
require_relative "mcp_generator"
|
6
6
|
require_relative "orchestrator"
|
7
7
|
require_relative "claude_mcp_server"
|
8
|
+
require_relative "permission_mcp_server"
|
8
9
|
|
9
10
|
module ClaudeSwarm
|
10
11
|
class CLI < Thor
|
@@ -156,6 +157,20 @@ module ClaudeSwarm
|
|
156
157
|
say "Claude Swarm #{VERSION}"
|
157
158
|
end
|
158
159
|
|
160
|
+
desc "tools-mcp", "Start a permission management MCP server for tool access control"
|
161
|
+
method_option :allowed_tools, aliases: "-t", type: :array,
|
162
|
+
desc: "Comma-separated list of allowed tool patterns (supports wildcards)"
|
163
|
+
method_option :debug, type: :boolean, default: false,
|
164
|
+
desc: "Enable debug output"
|
165
|
+
def tools_mcp
|
166
|
+
server = PermissionMcpServer.new(allowed_tools: options[:allowed_tools])
|
167
|
+
server.start
|
168
|
+
rescue StandardError => e
|
169
|
+
error "Error starting permission MCP server: #{e.message}"
|
170
|
+
error e.backtrace.join("\n") if options[:debug]
|
171
|
+
exit 1
|
172
|
+
end
|
173
|
+
|
159
174
|
default_task :start
|
160
175
|
|
161
176
|
private
|
@@ -58,6 +58,9 @@ module ClaudeSwarm
|
|
58
58
|
mcp_servers[connection_name] = build_instance_mcp_config(connection_name, connected_instance, calling_instance: name)
|
59
59
|
end
|
60
60
|
|
61
|
+
# Add permission MCP server if not in vibe mode (global or instance-specific)
|
62
|
+
mcp_servers["permissions"] = build_permission_mcp_config(instance[:tools]) unless @vibe || instance[:vibe]
|
63
|
+
|
61
64
|
config = {
|
62
65
|
"mcpServers" => mcp_servers
|
63
66
|
}
|
@@ -106,7 +109,7 @@ module ClaudeSwarm
|
|
106
109
|
|
107
110
|
args.push("--calling-instance", calling_instance) if calling_instance
|
108
111
|
|
109
|
-
args.push("--vibe") if @vibe
|
112
|
+
args.push("--vibe") if @vibe || instance[:vibe]
|
110
113
|
|
111
114
|
{
|
112
115
|
"type" => "stdio",
|
@@ -114,5 +117,23 @@ module ClaudeSwarm
|
|
114
117
|
"args" => args
|
115
118
|
}
|
116
119
|
end
|
120
|
+
|
121
|
+
def build_permission_mcp_config(allowed_tools)
|
122
|
+
exe_path = "claude-swarm"
|
123
|
+
|
124
|
+
args = ["tools-mcp"]
|
125
|
+
|
126
|
+
# Add allowed tools if specified
|
127
|
+
args.push("--allowed-tools", allowed_tools.join(",")) if allowed_tools && !allowed_tools.empty?
|
128
|
+
|
129
|
+
{
|
130
|
+
"type" => "stdio",
|
131
|
+
"command" => exe_path,
|
132
|
+
"args" => args,
|
133
|
+
"env" => {
|
134
|
+
"CLAUDE_SWARM_SESSION_TIMESTAMP" => @timestamp
|
135
|
+
}
|
136
|
+
}
|
137
|
+
end
|
117
138
|
end
|
118
139
|
end
|