sxn 0.3.0 → 0.4.0
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/CHANGELOG.md +16 -13
- data/Gemfile.lock +6 -1
- data/bin/sxn-mcp +58 -0
- data/docs/MCP_IMPLEMENTATION.md +425 -0
- data/lib/sxn/CLI.rb +7 -0
- data/lib/sxn/commands/mcp.rb +219 -0
- data/lib/sxn/commands/sessions.rb +1 -4
- data/lib/sxn/commands/templates.rb +1 -5
- data/lib/sxn/commands.rb +1 -0
- data/lib/sxn/mcp/prompts/workflow_prompts.rb +107 -0
- data/lib/sxn/mcp/resources/session_resources.rb +145 -0
- data/lib/sxn/mcp/server.rb +127 -0
- data/lib/sxn/mcp/tools/base_tool.rb +96 -0
- data/lib/sxn/mcp/tools/projects.rb +144 -0
- data/lib/sxn/mcp/tools/rules.rb +108 -0
- data/lib/sxn/mcp/tools/sessions.rb +375 -0
- data/lib/sxn/mcp/tools/templates.rb +119 -0
- data/lib/sxn/mcp/tools/worktrees.rb +168 -0
- data/lib/sxn/mcp.rb +20 -0
- data/lib/sxn/version.rb +1 -1
- data/lib/sxn.rb +1 -0
- data/sxn.gemspec +86 -0
- data/test.txt +1 -0
- metadata +31 -1
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sxn
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
module Sessions
|
|
7
|
+
# List all sessions with optional filtering
|
|
8
|
+
class ListSessions < ::MCP::Tool
|
|
9
|
+
description "List all sxn development sessions with optional status filtering"
|
|
10
|
+
|
|
11
|
+
input_schema(
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
status: {
|
|
15
|
+
type: "string",
|
|
16
|
+
enum: %w[active inactive archived],
|
|
17
|
+
description: "Filter sessions by status"
|
|
18
|
+
},
|
|
19
|
+
limit: {
|
|
20
|
+
type: "integer",
|
|
21
|
+
default: 100,
|
|
22
|
+
description: "Maximum number of sessions to return"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
required: []
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
def call(server_context:, status: nil, limit: 100)
|
|
30
|
+
BaseTool.ensure_initialized!(server_context)
|
|
31
|
+
|
|
32
|
+
BaseTool::ErrorMapping.wrap do
|
|
33
|
+
session_manager = server_context[:session_manager]
|
|
34
|
+
sessions = session_manager.list_sessions(status: status, limit: limit)
|
|
35
|
+
|
|
36
|
+
if sessions.empty?
|
|
37
|
+
BaseTool.text_response("No sessions found.")
|
|
38
|
+
else
|
|
39
|
+
summary = "Found #{sessions.length} session(s):"
|
|
40
|
+
formatted = sessions.map do |s|
|
|
41
|
+
"- #{s[:name]} (#{s[:status]}) - #{s[:worktrees].keys.length} worktrees"
|
|
42
|
+
end.join("\n")
|
|
43
|
+
|
|
44
|
+
BaseTool.text_response("#{summary}\n#{formatted}")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Create a new session
|
|
52
|
+
class CreateSession < ::MCP::Tool
|
|
53
|
+
description "Create a new sxn development session with optional template"
|
|
54
|
+
|
|
55
|
+
input_schema(
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
name: {
|
|
59
|
+
type: "string",
|
|
60
|
+
pattern: "^[a-zA-Z0-9_-]+$",
|
|
61
|
+
description: "Session name (alphanumeric, hyphens, underscores only)"
|
|
62
|
+
},
|
|
63
|
+
description: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "Optional session description"
|
|
66
|
+
},
|
|
67
|
+
default_branch: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Default branch for worktrees (defaults to session name)"
|
|
70
|
+
},
|
|
71
|
+
template_id: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "Template to apply (creates predefined worktrees)"
|
|
74
|
+
},
|
|
75
|
+
linear_task: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Associated Linear task ID (e.g., ATL-1234)"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
required: ["name"]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
class << self
|
|
84
|
+
def call(name:, server_context:, description: nil, default_branch: nil, template_id: nil, linear_task: nil)
|
|
85
|
+
BaseTool.ensure_initialized!(server_context)
|
|
86
|
+
|
|
87
|
+
BaseTool::ErrorMapping.wrap do
|
|
88
|
+
session_manager = server_context[:session_manager]
|
|
89
|
+
|
|
90
|
+
session = session_manager.create_session(
|
|
91
|
+
name,
|
|
92
|
+
description: description,
|
|
93
|
+
default_branch: default_branch,
|
|
94
|
+
template_id: template_id,
|
|
95
|
+
linear_task: linear_task
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# If template specified, apply it
|
|
99
|
+
apply_template(server_context, name, template_id, default_branch || name) if template_id && server_context[:template_manager]
|
|
100
|
+
|
|
101
|
+
BaseTool.text_response(
|
|
102
|
+
"Session '#{name}' created successfully.\n" \
|
|
103
|
+
"Path: #{session[:path]}\n" \
|
|
104
|
+
"Branch: #{session[:default_branch]}"
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def apply_template(server_context, session_name, template_id, default_branch)
|
|
112
|
+
template_manager = server_context[:template_manager]
|
|
113
|
+
worktree_manager = server_context[:worktree_manager]
|
|
114
|
+
|
|
115
|
+
# Get template projects
|
|
116
|
+
projects = template_manager.get_template_projects(template_id, default_branch: default_branch)
|
|
117
|
+
|
|
118
|
+
# Create worktrees for each project
|
|
119
|
+
projects.each do |project|
|
|
120
|
+
worktree_manager.add_worktree(
|
|
121
|
+
project[:name],
|
|
122
|
+
project[:branch],
|
|
123
|
+
session_name: session_name
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
# Log template application error but don't fail session creation
|
|
128
|
+
Sxn.logger&.warn("Failed to apply template: #{e.message}")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Get detailed session info
|
|
134
|
+
class GetSession < ::MCP::Tool
|
|
135
|
+
description "Get detailed information about a specific session"
|
|
136
|
+
|
|
137
|
+
input_schema(
|
|
138
|
+
type: "object",
|
|
139
|
+
properties: {
|
|
140
|
+
name: {
|
|
141
|
+
type: "string",
|
|
142
|
+
description: "Session name to retrieve"
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
required: ["name"]
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
class << self
|
|
149
|
+
def call(name:, server_context:)
|
|
150
|
+
BaseTool.ensure_initialized!(server_context)
|
|
151
|
+
|
|
152
|
+
BaseTool::ErrorMapping.wrap do
|
|
153
|
+
session_manager = server_context[:session_manager]
|
|
154
|
+
session = session_manager.get_session(name)
|
|
155
|
+
|
|
156
|
+
raise Sxn::SessionNotFoundError, "Session '#{name}' not found" unless session
|
|
157
|
+
|
|
158
|
+
# Format session info
|
|
159
|
+
worktrees_info = session[:worktrees].map do |project, info|
|
|
160
|
+
" - #{project}: #{info[:branch] || info["branch"]} (#{info[:path] || info["path"]})"
|
|
161
|
+
end.join("\n")
|
|
162
|
+
|
|
163
|
+
output = <<~INFO
|
|
164
|
+
Session: #{session[:name]}
|
|
165
|
+
Status: #{session[:status]}
|
|
166
|
+
Path: #{session[:path]}
|
|
167
|
+
Created: #{session[:created_at]}
|
|
168
|
+
Default Branch: #{session[:default_branch]}
|
|
169
|
+
#{"Description: #{session[:description]}" if session[:description]}
|
|
170
|
+
#{"Linear Task: #{session[:linear_task]}" if session[:linear_task]}
|
|
171
|
+
#{"Template: #{session[:template_id]}" if session[:template_id]}
|
|
172
|
+
|
|
173
|
+
Worktrees (#{session[:worktrees].keys.length}):
|
|
174
|
+
#{worktrees_info.empty? ? " (none)" : worktrees_info}
|
|
175
|
+
INFO
|
|
176
|
+
|
|
177
|
+
BaseTool.text_response(output.strip)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Delete a session
|
|
184
|
+
class DeleteSession < ::MCP::Tool
|
|
185
|
+
description "Delete a session and its worktrees"
|
|
186
|
+
|
|
187
|
+
input_schema(
|
|
188
|
+
type: "object",
|
|
189
|
+
properties: {
|
|
190
|
+
name: {
|
|
191
|
+
type: "string",
|
|
192
|
+
description: "Session name to delete"
|
|
193
|
+
},
|
|
194
|
+
force: {
|
|
195
|
+
type: "boolean",
|
|
196
|
+
default: false,
|
|
197
|
+
description: "Force deletion even with uncommitted changes"
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
required: ["name"]
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
class << self
|
|
204
|
+
def call(name:, server_context:, force: false)
|
|
205
|
+
BaseTool.ensure_initialized!(server_context)
|
|
206
|
+
|
|
207
|
+
BaseTool::ErrorMapping.wrap do
|
|
208
|
+
session_manager = server_context[:session_manager]
|
|
209
|
+
session_manager.remove_session(name, force: force)
|
|
210
|
+
|
|
211
|
+
BaseTool.text_response("Session '#{name}' deleted successfully.")
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Archive a session
|
|
218
|
+
class ArchiveSession < ::MCP::Tool
|
|
219
|
+
description "Archive a session (preserves data but marks as archived)"
|
|
220
|
+
|
|
221
|
+
input_schema(
|
|
222
|
+
type: "object",
|
|
223
|
+
properties: {
|
|
224
|
+
name: {
|
|
225
|
+
type: "string",
|
|
226
|
+
description: "Session name to archive"
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
required: ["name"]
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
class << self
|
|
233
|
+
def call(name:, server_context:)
|
|
234
|
+
BaseTool.ensure_initialized!(server_context)
|
|
235
|
+
|
|
236
|
+
BaseTool::ErrorMapping.wrap do
|
|
237
|
+
session_manager = server_context[:session_manager]
|
|
238
|
+
session_manager.archive_session(name)
|
|
239
|
+
|
|
240
|
+
BaseTool.text_response("Session '#{name}' archived successfully.")
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Activate an archived session
|
|
247
|
+
class ActivateSession < ::MCP::Tool
|
|
248
|
+
description "Activate an archived session"
|
|
249
|
+
|
|
250
|
+
input_schema(
|
|
251
|
+
type: "object",
|
|
252
|
+
properties: {
|
|
253
|
+
name: {
|
|
254
|
+
type: "string",
|
|
255
|
+
description: "Session name to activate"
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
required: ["name"]
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
class << self
|
|
262
|
+
def call(name:, server_context:)
|
|
263
|
+
BaseTool.ensure_initialized!(server_context)
|
|
264
|
+
|
|
265
|
+
BaseTool::ErrorMapping.wrap do
|
|
266
|
+
session_manager = server_context[:session_manager]
|
|
267
|
+
session_manager.activate_session(name)
|
|
268
|
+
|
|
269
|
+
BaseTool.text_response("Session '#{name}' activated successfully.")
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Swap to a different session with navigation info
|
|
276
|
+
class SwapSession < ::MCP::Tool
|
|
277
|
+
description "Switch to a different session and get navigation instructions. " \
|
|
278
|
+
"Returns the session path and shell commands for changing directory."
|
|
279
|
+
|
|
280
|
+
input_schema(
|
|
281
|
+
type: "object",
|
|
282
|
+
properties: {
|
|
283
|
+
name: {
|
|
284
|
+
type: "string",
|
|
285
|
+
description: "Session name to switch to"
|
|
286
|
+
},
|
|
287
|
+
project: {
|
|
288
|
+
type: "string",
|
|
289
|
+
description: "Specific project worktree to navigate to (optional)"
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
required: ["name"]
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
class << self
|
|
296
|
+
def call(name:, server_context:, project: nil)
|
|
297
|
+
BaseTool.ensure_initialized!(server_context)
|
|
298
|
+
|
|
299
|
+
BaseTool::ErrorMapping.wrap do
|
|
300
|
+
session_manager = server_context[:session_manager]
|
|
301
|
+
workspace_path = server_context[:workspace_path]
|
|
302
|
+
|
|
303
|
+
# Activate the session
|
|
304
|
+
session = session_manager.use_session(name)
|
|
305
|
+
|
|
306
|
+
# Determine target path
|
|
307
|
+
target_path = if project
|
|
308
|
+
worktrees = session[:worktrees]
|
|
309
|
+
worktree_info = worktrees[project]
|
|
310
|
+
if worktree_info
|
|
311
|
+
worktree_info[:path] || worktree_info["path"]
|
|
312
|
+
else
|
|
313
|
+
session[:path]
|
|
314
|
+
end
|
|
315
|
+
else
|
|
316
|
+
session[:path]
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Determine navigation strategy
|
|
320
|
+
navigation = determine_navigation_strategy(workspace_path, target_path)
|
|
321
|
+
|
|
322
|
+
# Build response
|
|
323
|
+
worktrees_list = session[:worktrees].map do |proj, info|
|
|
324
|
+
"- #{proj}: #{info[:path] || info["path"]}"
|
|
325
|
+
end.join("\n")
|
|
326
|
+
|
|
327
|
+
output = <<~SWAP
|
|
328
|
+
Switched to session '#{name}'.
|
|
329
|
+
|
|
330
|
+
Session path: #{session[:path]}
|
|
331
|
+
Target path: #{target_path}
|
|
332
|
+
|
|
333
|
+
Navigation (#{navigation[:strategy]}):
|
|
334
|
+
#{navigation[:instruction]}
|
|
335
|
+
|
|
336
|
+
Worktrees:
|
|
337
|
+
#{worktrees_list.empty? ? "(none)" : worktrees_list}
|
|
338
|
+
SWAP
|
|
339
|
+
|
|
340
|
+
# Add structured data for programmatic use
|
|
341
|
+
BaseTool.text_response(output.strip)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
private
|
|
346
|
+
|
|
347
|
+
def determine_navigation_strategy(workspace_path, target_path)
|
|
348
|
+
# Check if target is within or contains workspace
|
|
349
|
+
target_expanded = File.expand_path(target_path)
|
|
350
|
+
workspace_expanded = File.expand_path(workspace_path)
|
|
351
|
+
|
|
352
|
+
if target_expanded.start_with?(workspace_expanded) ||
|
|
353
|
+
workspace_expanded.start_with?(target_expanded)
|
|
354
|
+
{
|
|
355
|
+
strategy: "bash_cd",
|
|
356
|
+
instruction: "Run: cd #{target_path}",
|
|
357
|
+
bash_command: "cd #{target_path}",
|
|
358
|
+
reason: "Session is within the current workspace"
|
|
359
|
+
}
|
|
360
|
+
else
|
|
361
|
+
{
|
|
362
|
+
strategy: "new_instance",
|
|
363
|
+
instruction: "Session is outside current workspace. " \
|
|
364
|
+
"Start a new Claude Code instance with:\n claude --cwd #{target_path}",
|
|
365
|
+
shell_command: "claude --cwd #{target_path}",
|
|
366
|
+
reason: "Session is outside the current workspace"
|
|
367
|
+
}
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sxn
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
module Templates
|
|
7
|
+
# List available templates
|
|
8
|
+
class ListTemplates < ::MCP::Tool
|
|
9
|
+
description "List available session templates"
|
|
10
|
+
|
|
11
|
+
input_schema(
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {},
|
|
14
|
+
required: []
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def call(server_context:)
|
|
19
|
+
BaseTool.ensure_initialized!(server_context)
|
|
20
|
+
|
|
21
|
+
BaseTool::ErrorMapping.wrap do
|
|
22
|
+
template_manager = server_context[:template_manager]
|
|
23
|
+
templates = template_manager.list_templates
|
|
24
|
+
|
|
25
|
+
if templates.empty?
|
|
26
|
+
BaseTool.text_response(
|
|
27
|
+
"No templates defined. Templates can be created in .sxn/templates.yml"
|
|
28
|
+
)
|
|
29
|
+
else
|
|
30
|
+
formatted = templates.map do |t|
|
|
31
|
+
desc = t[:description] ? " - #{t[:description]}" : ""
|
|
32
|
+
"- #{t[:name]} (#{t[:project_count]} projects)#{desc}"
|
|
33
|
+
end.join("\n")
|
|
34
|
+
|
|
35
|
+
BaseTool.text_response("Available templates:\n#{formatted}")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Apply a template to a session
|
|
43
|
+
class ApplyTemplate < ::MCP::Tool
|
|
44
|
+
description "Apply a template to create multiple worktrees in a session"
|
|
45
|
+
|
|
46
|
+
input_schema(
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
template_name: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Template name to apply"
|
|
52
|
+
},
|
|
53
|
+
session_name: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "Target session (defaults to current session)"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
required: ["template_name"]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
class << self
|
|
62
|
+
def call(template_name:, server_context:, session_name: nil)
|
|
63
|
+
BaseTool.ensure_initialized!(server_context)
|
|
64
|
+
|
|
65
|
+
BaseTool::ErrorMapping.wrap do
|
|
66
|
+
template_manager = server_context[:template_manager]
|
|
67
|
+
worktree_manager = server_context[:worktree_manager]
|
|
68
|
+
session_manager = server_context[:session_manager]
|
|
69
|
+
config_manager = server_context[:config_manager]
|
|
70
|
+
|
|
71
|
+
# Get session (use current if not specified)
|
|
72
|
+
session_name ||= config_manager.current_session
|
|
73
|
+
raise Sxn::NoActiveSessionError, "No active session" unless session_name
|
|
74
|
+
|
|
75
|
+
session = session_manager.get_session(session_name)
|
|
76
|
+
raise Sxn::SessionNotFoundError, "Session '#{session_name}' not found" unless session
|
|
77
|
+
|
|
78
|
+
# Validate template
|
|
79
|
+
template_manager.validate_template(template_name)
|
|
80
|
+
|
|
81
|
+
# Get template projects with default branch
|
|
82
|
+
default_branch = session[:default_branch] || session_name
|
|
83
|
+
projects = template_manager.get_template_projects(template_name, default_branch: default_branch)
|
|
84
|
+
|
|
85
|
+
# Create worktrees for each project
|
|
86
|
+
results = []
|
|
87
|
+
projects.each do |project|
|
|
88
|
+
result = worktree_manager.add_worktree(
|
|
89
|
+
project[:name],
|
|
90
|
+
project[:branch],
|
|
91
|
+
session_name: session_name
|
|
92
|
+
)
|
|
93
|
+
results << { project: project[:name], status: "success", path: result[:path] }
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
results << { project: project[:name], status: "error", error: e.message }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Format output
|
|
99
|
+
successful = results.select { |r| r[:status] == "success" }
|
|
100
|
+
failed = results.select { |r| r[:status] == "error" }
|
|
101
|
+
|
|
102
|
+
output = "Template '#{template_name}' applied to session '#{session_name}'.\n\n"
|
|
103
|
+
output += "Created #{successful.length} worktree(s):\n"
|
|
104
|
+
successful.each { |r| output += " - #{r[:project]}: #{r[:path]}\n" }
|
|
105
|
+
|
|
106
|
+
unless failed.empty?
|
|
107
|
+
output += "\nFailed (#{failed.length}):\n"
|
|
108
|
+
failed.each { |r| output += " - #{r[:project]}: #{r[:error]}\n" }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
BaseTool.text_response(output.strip)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sxn
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
module Worktrees
|
|
7
|
+
# List worktrees in a session
|
|
8
|
+
class ListWorktrees < ::MCP::Tool
|
|
9
|
+
description "List all worktrees in a session"
|
|
10
|
+
|
|
11
|
+
input_schema(
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
session_name: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Session name (defaults to current session)"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
required: []
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def call(server_context:, session_name: nil)
|
|
24
|
+
BaseTool.ensure_initialized!(server_context)
|
|
25
|
+
|
|
26
|
+
BaseTool::ErrorMapping.wrap do
|
|
27
|
+
worktree_manager = server_context[:worktree_manager]
|
|
28
|
+
worktrees = worktree_manager.list_worktrees(session_name: session_name)
|
|
29
|
+
|
|
30
|
+
if worktrees.empty?
|
|
31
|
+
BaseTool.text_response("No worktrees found in the session.")
|
|
32
|
+
else
|
|
33
|
+
formatted = worktrees.map do |w|
|
|
34
|
+
status_icon = case w[:status]
|
|
35
|
+
when "clean" then "[clean]"
|
|
36
|
+
when "modified" then "[modified]"
|
|
37
|
+
when "staged" then "[staged]"
|
|
38
|
+
when "untracked" then "[untracked]"
|
|
39
|
+
when "missing" then "[missing]"
|
|
40
|
+
else "[#{w[:status]}]"
|
|
41
|
+
end
|
|
42
|
+
"- #{w[:project]} (#{w[:branch]}) #{status_icon}\n #{w[:path]}"
|
|
43
|
+
end.join("\n")
|
|
44
|
+
|
|
45
|
+
BaseTool.text_response("Worktrees (#{worktrees.length}):\n#{formatted}")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Add a worktree to a session
|
|
53
|
+
class AddWorktree < ::MCP::Tool
|
|
54
|
+
description "Add a project worktree to the current or specified session. " \
|
|
55
|
+
"Automatically applies project rules after creation."
|
|
56
|
+
|
|
57
|
+
input_schema(
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
project_name: {
|
|
61
|
+
type: "string",
|
|
62
|
+
description: "Registered project name"
|
|
63
|
+
},
|
|
64
|
+
branch: {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "Branch name (defaults to session's default branch)"
|
|
67
|
+
},
|
|
68
|
+
session_name: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "Target session (defaults to current session)"
|
|
71
|
+
},
|
|
72
|
+
apply_rules: {
|
|
73
|
+
type: "boolean",
|
|
74
|
+
default: true,
|
|
75
|
+
description: "Apply project rules after creation (copy files, etc.)"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
required: ["project_name"]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
class << self
|
|
82
|
+
def call(project_name:, server_context:, branch: nil, session_name: nil, apply_rules: true)
|
|
83
|
+
BaseTool.ensure_initialized!(server_context)
|
|
84
|
+
|
|
85
|
+
BaseTool::ErrorMapping.wrap do
|
|
86
|
+
worktree_manager = server_context[:worktree_manager]
|
|
87
|
+
|
|
88
|
+
result = worktree_manager.add_worktree(
|
|
89
|
+
project_name,
|
|
90
|
+
branch,
|
|
91
|
+
session_name: session_name
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Apply rules if requested
|
|
95
|
+
rules_result = nil
|
|
96
|
+
if apply_rules && server_context[:rules_manager]
|
|
97
|
+
rules_result = apply_project_rules(
|
|
98
|
+
server_context,
|
|
99
|
+
project_name,
|
|
100
|
+
result[:session]
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
output = <<~RESULT
|
|
105
|
+
Worktree created successfully:
|
|
106
|
+
- Project: #{result[:project]}
|
|
107
|
+
- Branch: #{result[:branch]}
|
|
108
|
+
- Path: #{result[:path]}
|
|
109
|
+
- Session: #{result[:session]}
|
|
110
|
+
RESULT
|
|
111
|
+
|
|
112
|
+
if rules_result
|
|
113
|
+
output += "\nRules applied: #{rules_result[:applied_count]} rule(s)"
|
|
114
|
+
output += "\nRule errors: #{rules_result[:errors].join(", ")}" unless rules_result[:errors].empty?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
BaseTool.text_response(output.strip)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def apply_project_rules(server_context, project_name, session_name)
|
|
124
|
+
rules_manager = server_context[:rules_manager]
|
|
125
|
+
rules_manager.apply_rules(project_name, session_name)
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
# Don't fail worktree creation if rules fail
|
|
128
|
+
{ applied_count: 0, errors: [e.message] }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Remove a worktree from a session
|
|
134
|
+
class RemoveWorktree < ::MCP::Tool
|
|
135
|
+
description "Remove a worktree from a session"
|
|
136
|
+
|
|
137
|
+
input_schema(
|
|
138
|
+
type: "object",
|
|
139
|
+
properties: {
|
|
140
|
+
project_name: {
|
|
141
|
+
type: "string",
|
|
142
|
+
description: "Project name of the worktree to remove"
|
|
143
|
+
},
|
|
144
|
+
session_name: {
|
|
145
|
+
type: "string",
|
|
146
|
+
description: "Session name (defaults to current session)"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
required: ["project_name"]
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
class << self
|
|
153
|
+
def call(project_name:, server_context:, session_name: nil)
|
|
154
|
+
BaseTool.ensure_initialized!(server_context)
|
|
155
|
+
|
|
156
|
+
BaseTool::ErrorMapping.wrap do
|
|
157
|
+
worktree_manager = server_context[:worktree_manager]
|
|
158
|
+
worktree_manager.remove_worktree(project_name, session_name: session_name)
|
|
159
|
+
|
|
160
|
+
BaseTool.text_response("Worktree for '#{project_name}' removed successfully.")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
data/lib/sxn/mcp.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
|
|
5
|
+
require_relative "mcp/server"
|
|
6
|
+
require_relative "mcp/tools/base_tool"
|
|
7
|
+
require_relative "mcp/tools/sessions"
|
|
8
|
+
require_relative "mcp/tools/worktrees"
|
|
9
|
+
require_relative "mcp/tools/projects"
|
|
10
|
+
require_relative "mcp/tools/templates"
|
|
11
|
+
require_relative "mcp/tools/rules"
|
|
12
|
+
require_relative "mcp/resources/session_resources"
|
|
13
|
+
require_relative "mcp/prompts/workflow_prompts"
|
|
14
|
+
|
|
15
|
+
module Sxn
|
|
16
|
+
# MCP (Model Context Protocol) server for sxn
|
|
17
|
+
# Enables AI assistants like Claude Code to manage development sessions
|
|
18
|
+
module MCP
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/sxn/version.rb
CHANGED