Class: McpServer
- Inherits:
-
Object
- Object
- McpServer
- Defined in:
- lib/jirametrics/mcp_server.rb
Defined Under Namespace
Classes: AgingWorkTool, CompletedWorkTool, HistoryFilter, ListProjectsTool, NotYetStartedTool, StatusTimeAnalysisTool
Constant Summary collapse
- HISTORY_FILTER_SCHEMA =
{ history_field: { type: 'string', description: 'When combined with history_value, only return issues where this field ever had that value ' \ '(e.g. "priority", "status"). Both history_field and history_value must be provided together.' }, history_value: { type: 'string', description: 'The value to look for in the change history of history_field (e.g. "Highest", "Done").' }, ever_blocked: { type: 'boolean', description: 'When true, only return issues that were ever blocked. Blocked includes flagged items, ' \ 'issues in blocked statuses, and blocking issue links.' }, ever_stalled: { type: 'boolean', description: 'When true, only return issues that were ever stalled. Stalled means the issue sat ' \ 'inactive for longer than the stalled threshold, or entered a stalled status.' }, currently_blocked: { type: 'boolean', description: 'When true, only return issues that are currently blocked (as of the data end date).' }, currently_stalled: { type: 'boolean', description: 'When true, only return issues that are currently stalled (as of the data end date).' } }.freeze
- ALIASES =
Alternative tool names used by AI agents other than Claude. Each entry maps an alias name to the canonical tool class it delegates to. The alias inherits the canonical tool's schema and call behaviour automatically. To add a new alias, append one line: 'alias_name' => CanonicalToolClass
{ 'board_list' => ListProjectsTool }.freeze
Class Method Summary collapse
-
.accumulate_spans(issue, end_time) ⇒ Object
Walks an issue's status timeline as spans (created -> first change, between consecutive changes, last change -> effective end) and sums each span's duration under a bucket name.
- .add_span(result, name, duration) ⇒ Object
-
.collect_rows(server_context, project) ⇒ Object
Collects the non-nil rows a block returns for each allowed issue (the row-oriented tools).
- .column_name_for(board, status_id) ⇒ Object
-
.each_allowed_issue(server_context, project) ⇒ Object
The shared iteration spine of every query tool: resolve the project filter, skip disallowed projects, and yield each surviving issue with its project name and data.
- .flow_efficiency_percent(issue, end_time) ⇒ Object
- .matches_blocked?(bsc, filter) ⇒ Boolean
- .matches_blocked_stalled?(bsc, filter) ⇒ Boolean
-
.matches_current_state?(issue, current_status, current_column) ⇒ Boolean
Shared current-state filter for the aging and not-yet-started tools.
- .matches_history?(issue, end_time, filter) ⇒ Boolean
- .matches_stalled?(bsc, filter) ⇒ Boolean
-
.render_rows(rows, empty:, &block) ⇒ Object
Renders already-sorted rows as a text Response: the empty-message when there are none, otherwise each row formatted by the block and newline-joined.
- .resolve_projects(server_context, project_filter) ⇒ Object
- .time_per_column(issue, end_time) ⇒ Object
- .time_per_status(issue, end_time) ⇒ Object
Instance Method Summary collapse
-
#initialize(projects:, aggregates: {}, timezone_offset: '+00:00') ⇒ McpServer
constructor
A new instance of McpServer.
- #run ⇒ Object
Constructor Details
#initialize(projects:, aggregates: {}, timezone_offset: '+00:00') ⇒ McpServer
Returns a new instance of McpServer.
11 12 13 14 15 |
# File 'lib/jirametrics/mcp_server.rb', line 11 def initialize projects:, aggregates: {}, timezone_offset: '+00:00' @projects = projects @aggregates = aggregates @timezone_offset = timezone_offset end |
Class Method Details
.accumulate_spans(issue, end_time) ⇒ Object
Walks an issue's status timeline as spans (created -> first change, between consecutive changes, last change -> effective end) and sums each span's duration under a bucket name. The bucket for a span is chosen by the block, given the (status_name, status_id) that describe the status held during that span: for the opening span that's the first change's old value, and thereafter the change's value.
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/jirametrics/mcp_server.rb', line 142 def self.accumulate_spans issue, end_time changes = issue.status_changes _, stopped = issue.started_stopped_times effective_end = stopped && stopped < end_time ? stopped : end_time result = Hash.new(0.0) if changes.empty? add_span(result, yield(issue.status.name, issue.status.id), effective_end - issue.created) return result end first_change = changes.first add_span(result, yield(first_change.old_value, first_change.old_value_id), first_change.time - issue.created) changes.each_cons(2) do |prev_change, next_change| add_span(result, yield(prev_change.value, prev_change.value_id), next_change.time - prev_change.time) end last_change = changes.last add_span(result, yield(last_change.value, last_change.value_id), effective_end - last_change.time) result end |
.add_span(result, name, duration) ⇒ Object
165 166 167 |
# File 'lib/jirametrics/mcp_server.rb', line 165 def self.add_span result, name, duration result[name] += duration if duration.positive? end |
.collect_rows(server_context, project) ⇒ Object
Collects the non-nil rows a block returns for each allowed issue (the row-oriented tools).
114 115 116 117 118 119 120 121 |
# File 'lib/jirametrics/mcp_server.rb', line 114 def self.collect_rows server_context, project rows = [] each_allowed_issue(server_context, project) do |issue, project_name, project_data| row = yield issue, project_name, project_data rows << row if row end rows end |
.column_name_for(board, status_id) ⇒ Object
96 97 98 |
# File 'lib/jirametrics/mcp_server.rb', line 96 def self.column_name_for board, status_id board.visible_columns.find { |c| c.status_ids.include?(status_id) }&.name end |
.each_allowed_issue(server_context, project) ⇒ Object
The shared iteration spine of every query tool: resolve the project filter, skip disallowed projects, and yield each surviving issue with its project name and data.
102 103 104 105 106 107 108 109 110 111 |
# File 'lib/jirametrics/mcp_server.rb', line 102 def self.each_allowed_issue server_context, project allowed_projects = resolve_projects(server_context, project) server_context[:projects].each do |project_name, project_data| next if allowed_projects && !allowed_projects.include?(project_name) project_data[:issues].each do |issue| yield issue, project_name, project_data end end end |
.flow_efficiency_percent(issue, end_time) ⇒ Object
178 179 180 181 |
# File 'lib/jirametrics/mcp_server.rb', line 178 def self.flow_efficiency_percent issue, end_time active_time, total_time = issue.flow_efficiency_numbers(end_time: end_time) total_time.positive? ? (active_time / total_time * 100).round(1) : nil end |
.matches_blocked?(bsc, filter) ⇒ Boolean
183 184 185 186 187 188 |
# File 'lib/jirametrics/mcp_server.rb', line 183 def self.matches_blocked?(bsc, filter) return false if filter.ever_blocked && bsc.none?(&:blocked?) return false if filter.currently_blocked && !bsc.last&.blocked? true end |
.matches_blocked_stalled?(bsc, filter) ⇒ Boolean
197 198 199 |
# File 'lib/jirametrics/mcp_server.rb', line 197 def self.matches_blocked_stalled?(bsc, filter) matches_blocked?(bsc, filter) && matches_stalled?(bsc, filter) end |
.matches_current_state?(issue, current_status, current_column) ⇒ Boolean
Shared current-state filter for the aging and not-yet-started tools.
131 132 133 134 135 136 |
# File 'lib/jirametrics/mcp_server.rb', line 131 def self.matches_current_state? issue, current_status, current_column return false if current_status && issue.status.name != current_status return false if current_column && column_name_for(issue.board, issue.status.id) != current_column true end |
.matches_history?(issue, end_time, filter) ⇒ Boolean
201 202 203 204 205 206 207 |
# File 'lib/jirametrics/mcp_server.rb', line 201 def self.matches_history?(issue, end_time, filter) return false if filter.history? && !filter.matches_change?(issue.changes) return false if filter.blocked_stalled? && !matches_blocked_stalled?(issue.blocked_stalled_changes(end_time: end_time), filter) true end |
.matches_stalled?(bsc, filter) ⇒ Boolean
190 191 192 193 194 195 |
# File 'lib/jirametrics/mcp_server.rb', line 190 def self.matches_stalled?(bsc, filter) return false if filter.ever_stalled && bsc.none?(&:stalled?) return false if filter.currently_stalled && !bsc.last&.stalled? true end |
.render_rows(rows, empty:, &block) ⇒ Object
Renders already-sorted rows as a text Response: the empty-message when there are none, otherwise each row formatted by the block and newline-joined.
125 126 127 128 |
# File 'lib/jirametrics/mcp_server.rb', line 125 def self.render_rows rows, empty:, &block text = rows.empty? ? empty : rows.map(&block).join("\n") MCP::Tool::Response.new([{ type: 'text', text: text }]) end |
.resolve_projects(server_context, project_filter) ⇒ Object
89 90 91 92 93 94 |
# File 'lib/jirametrics/mcp_server.rb', line 89 def self.resolve_projects server_context, project_filter return nil if project_filter.nil? aggregates = server_context[:aggregates] || {} aggregates[project_filter] || [project_filter] end |
.time_per_column(issue, end_time) ⇒ Object
169 170 171 172 |
# File 'lib/jirametrics/mcp_server.rb', line 169 def self.time_per_column issue, end_time board = issue.board accumulate_spans(issue, end_time) { |name, id| column_name_for(board, id) || name } end |
.time_per_status(issue, end_time) ⇒ Object
174 175 176 |
# File 'lib/jirametrics/mcp_server.rb', line 174 def self.time_per_status issue, end_time accumulate_spans(issue, end_time) { |name, _id| name } end |
Instance Method Details
#run ⇒ Object
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/jirametrics/mcp_server.rb', line 17 def run canonical_tools = [ListProjectsTool, AgingWorkTool, CompletedWorkTool, NotYetStartedTool, StatusTimeAnalysisTool] alias_tools = ALIASES.map do |alias_name, canonical| schema = canonical.input_schema Class.new(canonical) do tool_name alias_name input_schema schema end end server = MCP::Server.new( name: 'jirametrics', version: Gem.loaded_specs['jirametrics']&.version&.to_s || '0.0.0', tools: canonical_tools + alias_tools, server_context: { projects: @projects, aggregates: @aggregates, timezone_offset: @timezone_offset } ) transport = MCP::Server::Transports::StdioTransport.new(server) transport.open end |