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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6fa670f8d967ad8976c05b96fe3d2103125ec17e31e3c343bb38a3002dc5182
4
- data.tar.gz: e4cb03e288ea20ac810414bd71e5a84c6d1bb9b4c77ee041a29deebb8270d7ee
3
+ metadata.gz: 0b5aa9e698fe0c055fc16aa5263df48a2c6e473179d5472e06f83c8845a3447c
4
+ data.tar.gz: ce36d61d30177b38e2f9dbb6f9b00fc1222f0d5ee4cb4539e75f912ad9777904
5
5
  SHA512:
6
- metadata.gz: 06d9f249a5f0c52d13a6dc9d34b1b49d572d9432b0617a616cd9c3d35058b17320b23d63b3db641c55d5a812b810c6ea8fab4ae742555da483f28064360f3326
7
- data.tar.gz: '082a16da97c25bffa67fcdf831a1b5c612b02f1ba3884a0a952eb77bf78f9cf22d045b728dad351b0cce8b201943dd972df4fd4301396ace0322fb1de0dd711a'
6
+ metadata.gz: ffbac2fb59e4f76ab39efe172a89d1a11b0893efd763770ba3f4c6e52785df44d19cc0db9a8dd87e2df52c05c7d8cceadc47deb2a8304e34ccb2c921b781842c
7
+ data.tar.gz: dd4eeb38f8d56c3cd41570fe0be0ccda029240893780d95c82c134ad02d91c840ba8fd3fb344dc205c7a3345e37fbce33b8f633abd076390783b31babaf6014d
data/.rubocop.yml CHANGED
@@ -59,4 +59,7 @@ Metrics/ModuleLength:
59
59
  Enabled: false
60
60
 
61
61
  Minitest/MultipleAssertions:
62
+ Enabled: false
63
+
64
+ Metrics/ParameterLists:
62
65
  Enabled: false
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
- 3. **Session Management**: Claude Swarm maintains session continuity:
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
- 4. **Main Instance Launch**: The main instance is launched with its MCP configuration, giving it access to all connected instances
426
- 5. **Inter-Instance Communication**: Connected instances expose themselves as MCP servers with these tools:
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
- 6. **Session Management**: All session files are organized in `.claude-swarm/sessions/{timestamp}/`:
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: sonnet
9
+ model: opus
10
10
  prompt: "You are the lead developer coordinating the team"
11
- tools: [Read, Edit, Bash, Write]
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: sonnet
19
+ model: opus
20
20
  prompt: "You specialize in frontend development with React, TypeScript, and modern web technologies"
21
- tools: [Read, Edit, Write, "Bash(npm:*)", "Bash(yarn:*)", "Bash(pnpm:*)"]
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
- attr_reader :session_id, :last_response, :working_directory
10
+ SWARM_DIR = ".claude-swarm"
11
+ SESSIONS_DIR = "sessions"
9
12
 
10
- def initialize(working_directory: Dir.pwd, model: nil, mcp_config: nil, vibe: false)
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
- cmd_array = build_command_array(prompt, options)
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
- begin
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
- response
34
- rescue JSON::ParserError => e
35
- raise ParseError, "Failed to parse JSON response: #{e.message}\nOutput: #{stdout}"
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
- def execute_text(prompt, options = {})
40
- response = execute(prompt, options)
41
- response["result"] || ""
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
@@ -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
@@ -84,7 +84,8 @@ module ClaudeSwarm
84
84
  tools: Array(config["tools"]),
85
85
  mcps: parse_mcps(config["mcps"] || []),
86
86
  prompt: config["prompt"],
87
- description: config["description"]
87
+ description: config["description"],
88
+ vibe: config["vibe"] || false
88
89
  }
89
90
  end
90
91
 
@@ -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