sxn 0.2.5 → 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 +19 -0
- data/Gemfile.lock +6 -1
- data/bin/sxn-mcp +58 -0
- data/docs/MCP_IMPLEMENTATION.md +425 -0
- data/lib/sxn/CLI.rb +15 -0
- data/lib/sxn/commands/mcp.rb +219 -0
- data/lib/sxn/commands/sessions.rb +111 -4
- data/lib/sxn/commands/templates.rb +226 -0
- data/lib/sxn/commands.rb +2 -0
- data/lib/sxn/config/templates_config.rb +153 -0
- data/lib/sxn/config.rb +1 -0
- data/lib/sxn/core/session_config.rb +6 -1
- data/lib/sxn/core/session_manager.rb +10 -4
- data/lib/sxn/core/template_manager.rb +187 -0
- data/lib/sxn/core.rb +1 -0
- data/lib/sxn/errors.rb +24 -1
- 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/ui/table.rb +18 -1
- data/lib/sxn/version.rb +1 -1
- data/lib/sxn.rb +1 -0
- data/sxn.gemspec +1 -0
- data/test.txt +1 -0
- metadata +33 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sxn
|
|
4
|
+
module MCP
|
|
5
|
+
module Resources
|
|
6
|
+
# Factory methods for creating MCP Resource objects
|
|
7
|
+
module SessionResources
|
|
8
|
+
class << self
|
|
9
|
+
# Build all resources for the server
|
|
10
|
+
def build_all(server_context)
|
|
11
|
+
[
|
|
12
|
+
build_current_session(server_context),
|
|
13
|
+
build_all_sessions(server_context),
|
|
14
|
+
build_all_projects(server_context)
|
|
15
|
+
].compact
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def build_current_session(server_context)
|
|
21
|
+
return nil unless server_context[:config_manager]
|
|
22
|
+
|
|
23
|
+
::MCP::Resource.new(
|
|
24
|
+
uri: "sxn://session/current",
|
|
25
|
+
name: "Current Session",
|
|
26
|
+
description: "Information about the currently active sxn session",
|
|
27
|
+
mime_type: "application/json"
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build_all_sessions(server_context)
|
|
32
|
+
return nil unless server_context[:config_manager]
|
|
33
|
+
|
|
34
|
+
::MCP::Resource.new(
|
|
35
|
+
uri: "sxn://sessions",
|
|
36
|
+
name: "All Sessions",
|
|
37
|
+
description: "Summary of all sxn sessions",
|
|
38
|
+
mime_type: "application/json"
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def build_all_projects(server_context)
|
|
43
|
+
return nil unless server_context[:config_manager]
|
|
44
|
+
|
|
45
|
+
::MCP::Resource.new(
|
|
46
|
+
uri: "sxn://projects",
|
|
47
|
+
name: "Registered Projects",
|
|
48
|
+
description: "All registered projects in the sxn workspace",
|
|
49
|
+
mime_type: "application/json"
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Resource content readers
|
|
56
|
+
module ResourceContentReader
|
|
57
|
+
class << self
|
|
58
|
+
def read_content(uri, server_context)
|
|
59
|
+
case uri
|
|
60
|
+
when "sxn://session/current"
|
|
61
|
+
read_current_session(server_context)
|
|
62
|
+
when "sxn://sessions"
|
|
63
|
+
read_all_sessions(server_context)
|
|
64
|
+
when "sxn://projects"
|
|
65
|
+
read_all_projects(server_context)
|
|
66
|
+
else
|
|
67
|
+
JSON.generate({ error: "Unknown resource: #{uri}" })
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def read_current_session(server_context)
|
|
74
|
+
config_manager = server_context[:config_manager]
|
|
75
|
+
return JSON.generate({ error: "sxn not initialized" }) unless config_manager
|
|
76
|
+
|
|
77
|
+
session_manager = server_context[:session_manager]
|
|
78
|
+
session = session_manager.current_session
|
|
79
|
+
|
|
80
|
+
unless session
|
|
81
|
+
return JSON.generate({
|
|
82
|
+
current_session: nil,
|
|
83
|
+
message: "No active session"
|
|
84
|
+
})
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
JSON.generate({
|
|
88
|
+
name: session[:name],
|
|
89
|
+
status: session[:status],
|
|
90
|
+
path: session[:path],
|
|
91
|
+
created_at: session[:created_at],
|
|
92
|
+
default_branch: session[:default_branch],
|
|
93
|
+
worktrees: session[:worktrees],
|
|
94
|
+
projects: session[:projects]
|
|
95
|
+
})
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
JSON.generate({ error: e.message })
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def read_all_sessions(server_context)
|
|
101
|
+
config_manager = server_context[:config_manager]
|
|
102
|
+
return JSON.generate({ error: "sxn not initialized" }) unless config_manager
|
|
103
|
+
|
|
104
|
+
session_manager = server_context[:session_manager]
|
|
105
|
+
sessions = session_manager.list_sessions
|
|
106
|
+
|
|
107
|
+
JSON.generate({
|
|
108
|
+
total: sessions.length,
|
|
109
|
+
sessions: sessions.map do |s|
|
|
110
|
+
{
|
|
111
|
+
name: s[:name],
|
|
112
|
+
status: s[:status],
|
|
113
|
+
worktree_count: s[:worktrees].keys.length
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
})
|
|
117
|
+
rescue StandardError => e
|
|
118
|
+
JSON.generate({ error: e.message })
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def read_all_projects(server_context)
|
|
122
|
+
config_manager = server_context[:config_manager]
|
|
123
|
+
return JSON.generate({ error: "sxn not initialized" }) unless config_manager
|
|
124
|
+
|
|
125
|
+
project_manager = server_context[:project_manager]
|
|
126
|
+
projects = project_manager.list_projects
|
|
127
|
+
|
|
128
|
+
JSON.generate({
|
|
129
|
+
total: projects.length,
|
|
130
|
+
projects: projects.map do |p|
|
|
131
|
+
{
|
|
132
|
+
name: p[:name],
|
|
133
|
+
type: p[:type],
|
|
134
|
+
path: p[:path]
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
})
|
|
138
|
+
rescue StandardError => e
|
|
139
|
+
JSON.generate({ error: e.message })
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sxn
|
|
4
|
+
module MCP
|
|
5
|
+
# Main MCP server class that orchestrates tools, resources, and prompts
|
|
6
|
+
class Server
|
|
7
|
+
attr_reader :workspace_path, :config_manager
|
|
8
|
+
|
|
9
|
+
def initialize(workspace_path: nil)
|
|
10
|
+
@workspace_path = workspace_path || ENV["SXN_WORKSPACE"] || discover_workspace
|
|
11
|
+
@config_manager = initialize_config_manager
|
|
12
|
+
@server = build_mcp_server
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Run the server with STDIO transport (for Claude Code integration)
|
|
16
|
+
def run_stdio
|
|
17
|
+
transport = ::MCP::Server::Transports::StdioTransport.new(@server)
|
|
18
|
+
transport.open
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Run the server with HTTP transport (for web-based integrations)
|
|
22
|
+
def run_http(port: 3000)
|
|
23
|
+
transport = ::MCP::Server::Transports::StreamableHTTPTransport.new(@server)
|
|
24
|
+
transport.run(port: port)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def discover_workspace
|
|
30
|
+
# Try to find .sxn directory in current or parent directories
|
|
31
|
+
current = Dir.pwd
|
|
32
|
+
loop do
|
|
33
|
+
sxn_path = File.join(current, ".sxn")
|
|
34
|
+
return current if File.directory?(sxn_path)
|
|
35
|
+
|
|
36
|
+
parent = File.dirname(current)
|
|
37
|
+
break if parent == current
|
|
38
|
+
|
|
39
|
+
current = parent
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Fall back to current directory
|
|
43
|
+
Dir.pwd
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initialize_config_manager
|
|
47
|
+
Sxn::Core::ConfigManager.new(@workspace_path)
|
|
48
|
+
rescue Sxn::ConfigurationError
|
|
49
|
+
# Allow server to start even if sxn isn't initialized
|
|
50
|
+
# Tools will return appropriate errors
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def build_mcp_server
|
|
55
|
+
context = build_context
|
|
56
|
+
|
|
57
|
+
server = ::MCP::Server.new(
|
|
58
|
+
name: "sxn",
|
|
59
|
+
version: Sxn::VERSION,
|
|
60
|
+
tools: registered_tools,
|
|
61
|
+
resources: registered_resources(context),
|
|
62
|
+
prompts: registered_prompts,
|
|
63
|
+
server_context: context
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Set up resource read handler
|
|
67
|
+
server.resources_read_handler do |params|
|
|
68
|
+
uri = params[:uri]
|
|
69
|
+
content = Resources::ResourceContentReader.read_content(uri, context)
|
|
70
|
+
[{ uri: uri, text: content, mimeType: "application/json" }]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
server
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def build_context
|
|
77
|
+
{
|
|
78
|
+
config_manager: @config_manager,
|
|
79
|
+
session_manager: @config_manager && Sxn::Core::SessionManager.new(@config_manager),
|
|
80
|
+
project_manager: @config_manager && Sxn::Core::ProjectManager.new(@config_manager),
|
|
81
|
+
worktree_manager: @config_manager && Sxn::Core::WorktreeManager.new(@config_manager),
|
|
82
|
+
template_manager: @config_manager && Sxn::Core::TemplateManager.new(@config_manager),
|
|
83
|
+
rules_manager: @config_manager && Sxn::Core::RulesManager.new(@config_manager),
|
|
84
|
+
workspace_path: @workspace_path
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def registered_tools
|
|
89
|
+
[
|
|
90
|
+
# Session tools
|
|
91
|
+
Tools::Sessions::ListSessions,
|
|
92
|
+
Tools::Sessions::CreateSession,
|
|
93
|
+
Tools::Sessions::GetSession,
|
|
94
|
+
Tools::Sessions::DeleteSession,
|
|
95
|
+
Tools::Sessions::ArchiveSession,
|
|
96
|
+
Tools::Sessions::ActivateSession,
|
|
97
|
+
Tools::Sessions::SwapSession,
|
|
98
|
+
# Worktree tools
|
|
99
|
+
Tools::Worktrees::ListWorktrees,
|
|
100
|
+
Tools::Worktrees::AddWorktree,
|
|
101
|
+
Tools::Worktrees::RemoveWorktree,
|
|
102
|
+
# Project tools
|
|
103
|
+
Tools::Projects::ListProjects,
|
|
104
|
+
Tools::Projects::AddProject,
|
|
105
|
+
Tools::Projects::GetProject,
|
|
106
|
+
# Template tools
|
|
107
|
+
Tools::Templates::ListTemplates,
|
|
108
|
+
Tools::Templates::ApplyTemplate,
|
|
109
|
+
# Rules tools
|
|
110
|
+
Tools::Rules::ListRules,
|
|
111
|
+
Tools::Rules::ApplyRules
|
|
112
|
+
]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def registered_resources(context)
|
|
116
|
+
Resources::SessionResources.build_all(context)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def registered_prompts
|
|
120
|
+
[
|
|
121
|
+
Prompts::NewSession,
|
|
122
|
+
Prompts::MultiRepoSetup
|
|
123
|
+
]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sxn
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
# Base module for MCP tools with shared error handling
|
|
7
|
+
module BaseTool
|
|
8
|
+
# Maps sxn errors to user-friendly messages
|
|
9
|
+
module ErrorMapping
|
|
10
|
+
SXN_ERROR_MESSAGES = {
|
|
11
|
+
# Session errors
|
|
12
|
+
Sxn::SessionNotFoundError => "Session not found",
|
|
13
|
+
Sxn::SessionAlreadyExistsError => "Session already exists",
|
|
14
|
+
Sxn::SessionExistsError => "Session already exists",
|
|
15
|
+
Sxn::SessionHasChangesError => "Session has uncommitted changes",
|
|
16
|
+
Sxn::InvalidSessionNameError => "Invalid session name",
|
|
17
|
+
Sxn::NoActiveSessionError => "No active session",
|
|
18
|
+
# Project errors
|
|
19
|
+
Sxn::ProjectNotFoundError => "Project not found",
|
|
20
|
+
Sxn::ProjectAlreadyExistsError => "Project already exists",
|
|
21
|
+
Sxn::ProjectExistsError => "Project already exists",
|
|
22
|
+
Sxn::ProjectInUseError => "Project is in use",
|
|
23
|
+
Sxn::InvalidProjectNameError => "Invalid project name",
|
|
24
|
+
Sxn::InvalidProjectPathError => "Invalid project path",
|
|
25
|
+
# Worktree errors
|
|
26
|
+
Sxn::WorktreeError => "Worktree error",
|
|
27
|
+
Sxn::WorktreeExistsError => "Worktree already exists",
|
|
28
|
+
Sxn::WorktreeNotFoundError => "Worktree not found",
|
|
29
|
+
Sxn::WorktreeCreationError => "Failed to create worktree",
|
|
30
|
+
Sxn::WorktreeRemovalError => "Failed to remove worktree",
|
|
31
|
+
# Template errors
|
|
32
|
+
Sxn::SessionTemplateNotFoundError => "Template not found",
|
|
33
|
+
Sxn::SessionTemplateValidationError => "Template validation failed",
|
|
34
|
+
# Rule errors
|
|
35
|
+
Sxn::RuleError => "Rule error",
|
|
36
|
+
Sxn::RuleNotFoundError => "Rule not found",
|
|
37
|
+
Sxn::InvalidRuleTypeError => "Invalid rule type",
|
|
38
|
+
Sxn::InvalidRuleConfigError => "Invalid rule configuration",
|
|
39
|
+
# Configuration errors
|
|
40
|
+
Sxn::ConfigurationError => "Configuration error",
|
|
41
|
+
# MCP errors
|
|
42
|
+
Sxn::MCPError => "MCP error",
|
|
43
|
+
Sxn::MCPServerError => "MCP server error",
|
|
44
|
+
Sxn::MCPValidationError => "MCP validation error"
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
# Wrap a block with error handling that converts sxn errors to error responses
|
|
48
|
+
def self.wrap
|
|
49
|
+
yield
|
|
50
|
+
rescue Sxn::ConfigurationError => e
|
|
51
|
+
error_response("sxn not initialized: #{e.message}. Run 'sxn init' first.")
|
|
52
|
+
rescue Sxn::Error => e
|
|
53
|
+
error_type = SXN_ERROR_MESSAGES[e.class] || "Error"
|
|
54
|
+
error_response("#{error_type}: #{e.message}")
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
error_response("Unexpected error: #{e.message}")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.error_response(message)
|
|
60
|
+
::MCP::Tool::Response.new([{ type: "text", text: message }], error: true)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Helper to check if sxn is initialized
|
|
65
|
+
def self.ensure_initialized!(server_context)
|
|
66
|
+
return true if server_context[:config_manager]
|
|
67
|
+
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Helper to return error response for uninitialized sxn
|
|
72
|
+
def self.not_initialized_response
|
|
73
|
+
ErrorMapping.error_response("sxn not initialized in this workspace. Run 'sxn init' first.")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Helper to build a successful text response
|
|
77
|
+
def self.text_response(text)
|
|
78
|
+
::MCP::Tool::Response.new([{ type: "text", text: text }])
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Helper to build a JSON response
|
|
82
|
+
def self.json_response(data, summary: nil)
|
|
83
|
+
content = []
|
|
84
|
+
content << { type: "text", text: summary } if summary
|
|
85
|
+
content << { type: "text", text: JSON.pretty_generate(data) }
|
|
86
|
+
::MCP::Tool::Response.new(content)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Helper to build an error response
|
|
90
|
+
def self.error_response(message)
|
|
91
|
+
ErrorMapping.error_response(message)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sxn
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
module Projects
|
|
7
|
+
# List registered projects
|
|
8
|
+
class ListProjects < ::MCP::Tool
|
|
9
|
+
description "List all registered projects in the sxn workspace"
|
|
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
|
+
project_manager = server_context[:project_manager]
|
|
23
|
+
projects = project_manager.list_projects
|
|
24
|
+
|
|
25
|
+
if projects.empty?
|
|
26
|
+
BaseTool.text_response("No projects registered. Use sxn_projects_add to register a project.")
|
|
27
|
+
else
|
|
28
|
+
formatted = projects.map do |p|
|
|
29
|
+
"- #{p[:name]} (#{p[:type]})\n #{p[:path]}\n Default branch: #{p[:default_branch]}"
|
|
30
|
+
end.join("\n")
|
|
31
|
+
|
|
32
|
+
BaseTool.text_response("Registered projects (#{projects.length}):\n#{formatted}")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Register a new project
|
|
40
|
+
class AddProject < ::MCP::Tool
|
|
41
|
+
description "Register a new git repository as a project. " \
|
|
42
|
+
"Automatically detects project type and default branch."
|
|
43
|
+
|
|
44
|
+
input_schema(
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
name: {
|
|
48
|
+
type: "string",
|
|
49
|
+
pattern: "^[a-zA-Z0-9_-]+$",
|
|
50
|
+
description: "Project name (alphanumeric, hyphens, underscores only)"
|
|
51
|
+
},
|
|
52
|
+
path: {
|
|
53
|
+
type: "string",
|
|
54
|
+
description: "Path to the git repository (absolute or relative)"
|
|
55
|
+
},
|
|
56
|
+
type: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "Project type (auto-detected if not specified). " \
|
|
59
|
+
"Options: rails, ruby, javascript, typescript, react, nextjs, etc."
|
|
60
|
+
},
|
|
61
|
+
default_branch: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Default branch for worktrees (auto-detected if not specified)"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
required: %w[name path]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
class << self
|
|
70
|
+
def call(name:, path:, server_context:, type: nil, default_branch: nil)
|
|
71
|
+
BaseTool.ensure_initialized!(server_context)
|
|
72
|
+
|
|
73
|
+
BaseTool::ErrorMapping.wrap do
|
|
74
|
+
project_manager = server_context[:project_manager]
|
|
75
|
+
|
|
76
|
+
result = project_manager.add_project(
|
|
77
|
+
name,
|
|
78
|
+
path,
|
|
79
|
+
type: type,
|
|
80
|
+
default_branch: default_branch
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
BaseTool.text_response(
|
|
84
|
+
"Project '#{result[:name]}' registered successfully.\n" \
|
|
85
|
+
"Type: #{result[:type]}\n" \
|
|
86
|
+
"Path: #{result[:path]}\n" \
|
|
87
|
+
"Default branch: #{result[:default_branch]}"
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Get project details
|
|
95
|
+
class GetProject < ::MCP::Tool
|
|
96
|
+
description "Get detailed information about a registered project"
|
|
97
|
+
|
|
98
|
+
input_schema(
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
name: {
|
|
102
|
+
type: "string",
|
|
103
|
+
description: "Project name to retrieve"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
required: ["name"]
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
class << self
|
|
110
|
+
def call(name:, server_context:)
|
|
111
|
+
BaseTool.ensure_initialized!(server_context)
|
|
112
|
+
|
|
113
|
+
BaseTool::ErrorMapping.wrap do
|
|
114
|
+
project_manager = server_context[:project_manager]
|
|
115
|
+
project = project_manager.get_project(name)
|
|
116
|
+
|
|
117
|
+
# Validate the project
|
|
118
|
+
validation = project_manager.validate_project(name)
|
|
119
|
+
|
|
120
|
+
# Get project rules
|
|
121
|
+
rules = project_manager.get_project_rules(name)
|
|
122
|
+
rules_summary = rules.map { |type, configs| "#{type}: #{Array(configs).length}" }.join(", ")
|
|
123
|
+
|
|
124
|
+
output = <<~INFO
|
|
125
|
+
Project: #{project[:name]}
|
|
126
|
+
Type: #{project[:type]}
|
|
127
|
+
Path: #{project[:path]}
|
|
128
|
+
Default branch: #{project[:default_branch]}
|
|
129
|
+
|
|
130
|
+
Validation: #{validation[:valid] ? "Valid" : "Invalid"}
|
|
131
|
+
#{validation[:issues].map { |i| " - #{i}" }.join("\n") unless validation[:valid]}
|
|
132
|
+
|
|
133
|
+
Rules: #{rules_summary.empty? ? "(none)" : rules_summary}
|
|
134
|
+
INFO
|
|
135
|
+
|
|
136
|
+
BaseTool.text_response(output.strip)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sxn
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
module Rules
|
|
7
|
+
# List rules for a project
|
|
8
|
+
class ListRules < ::MCP::Tool
|
|
9
|
+
description "List project rules (copy_files, setup_commands, templates)"
|
|
10
|
+
|
|
11
|
+
input_schema(
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
project_name: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Project name (lists all rules if not specified)"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
required: []
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def call(server_context:, project_name: nil)
|
|
24
|
+
BaseTool.ensure_initialized!(server_context)
|
|
25
|
+
|
|
26
|
+
BaseTool::ErrorMapping.wrap do
|
|
27
|
+
rules_manager = server_context[:rules_manager]
|
|
28
|
+
rules = rules_manager.list_rules(project_name)
|
|
29
|
+
|
|
30
|
+
if rules.empty?
|
|
31
|
+
BaseTool.text_response("No rules defined.")
|
|
32
|
+
else
|
|
33
|
+
# Group by project
|
|
34
|
+
by_project = rules.group_by { |r| r[:project] }
|
|
35
|
+
|
|
36
|
+
output = by_project.map do |proj, proj_rules|
|
|
37
|
+
proj_output = "#{proj}:\n"
|
|
38
|
+
proj_rules.each do |rule|
|
|
39
|
+
config_preview = case rule[:type]
|
|
40
|
+
when "copy_files"
|
|
41
|
+
rule[:config]["source"]
|
|
42
|
+
when "setup_commands"
|
|
43
|
+
rule[:config]["command"]&.join(" ")
|
|
44
|
+
when "template"
|
|
45
|
+
"#{rule[:config]["source"]} -> #{rule[:config]["destination"]}"
|
|
46
|
+
else
|
|
47
|
+
rule[:config].to_s
|
|
48
|
+
end
|
|
49
|
+
proj_output += " - [#{rule[:type]}] #{config_preview}\n"
|
|
50
|
+
end
|
|
51
|
+
proj_output
|
|
52
|
+
end.join("\n")
|
|
53
|
+
|
|
54
|
+
BaseTool.text_response("Project rules:\n\n#{output}")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Apply rules to a worktree
|
|
62
|
+
class ApplyRules < ::MCP::Tool
|
|
63
|
+
description "Apply project-specific rules to a worktree (copy files, run setup commands)"
|
|
64
|
+
|
|
65
|
+
input_schema(
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
project_name: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "Project name"
|
|
71
|
+
},
|
|
72
|
+
session_name: {
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "Session name (defaults to current session)"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
required: ["project_name"]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
class << self
|
|
81
|
+
def call(project_name:, server_context:, session_name: nil)
|
|
82
|
+
BaseTool.ensure_initialized!(server_context)
|
|
83
|
+
|
|
84
|
+
BaseTool::ErrorMapping.wrap do
|
|
85
|
+
rules_manager = server_context[:rules_manager]
|
|
86
|
+
|
|
87
|
+
result = rules_manager.apply_rules(project_name, session_name)
|
|
88
|
+
|
|
89
|
+
if result[:success]
|
|
90
|
+
BaseTool.text_response(
|
|
91
|
+
"Rules applied successfully to '#{project_name}'.\n" \
|
|
92
|
+
"Applied: #{result[:applied_count]} rule(s)"
|
|
93
|
+
)
|
|
94
|
+
else
|
|
95
|
+
BaseTool.text_response(
|
|
96
|
+
"Some rules failed for '#{project_name}'.\n" \
|
|
97
|
+
"Applied: #{result[:applied_count]} rule(s)\n" \
|
|
98
|
+
"Errors:\n#{result[:errors].map { |e| " - #{e}" }.join("\n")}"
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|