Class: McpServer
- Inherits:
-
Object
show all
- Defined in:
- lib/jirametrics/mcp_server.rb
Defined Under Namespace
Classes: AgingWorkTool, CompletedWorkTool, 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
-
.column_name_for(board, status_id) ⇒ Object
-
.flow_efficiency_percent(issue, end_time) ⇒ Object
-
.matches_blocked_stalled?(bsc, ever_blocked, ever_stalled, currently_blocked, currently_stalled) ⇒ Boolean
-
.matches_history?(issue, end_time, history_field, history_value, ever_blocked, ever_stalled, currently_blocked, currently_stalled) ⇒ Boolean
-
.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
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
.column_name_for(board, status_id) ⇒ Object
75
76
77
|
# File 'lib/jirametrics/mcp_server.rb', line 75
def self.column_name_for board, status_id
board.visible_columns.find { |c| c.status_ids.include?(status_id) }&.name
end
|
.flow_efficiency_percent(issue, end_time) ⇒ Object
142
143
144
145
|
# File 'lib/jirametrics/mcp_server.rb', line 142
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_stalled?(bsc, ever_blocked, ever_stalled, currently_blocked, currently_stalled) ⇒ Boolean
147
148
149
150
151
152
153
154
|
# File 'lib/jirametrics/mcp_server.rb', line 147
def self.matches_blocked_stalled?(bsc, ever_blocked, ever_stalled, currently_blocked, currently_stalled)
return false if ever_blocked && bsc.none?(&:blocked?)
return false if ever_stalled && bsc.none?(&:stalled?)
return false if currently_blocked && !bsc.last&.blocked?
return false if currently_stalled && !bsc.last&.stalled?
true
end
|
.matches_history?(issue, end_time, history_field, history_value, ever_blocked, ever_stalled, currently_blocked, currently_stalled) ⇒ Boolean
156
157
158
159
160
161
162
163
164
165
166
167
168
|
# File 'lib/jirametrics/mcp_server.rb', line 156
def self.matches_history?(issue, end_time, history_field, history_value,
ever_blocked, ever_stalled, currently_blocked, currently_stalled)
return false if history_field && history_value &&
issue.changes.none? { |c| c.field == history_field && c.value == history_value }
if ever_blocked || ever_stalled || currently_blocked || currently_stalled
bsc = issue.blocked_stalled_changes(end_time: end_time)
return false unless matches_blocked_stalled?(bsc, ever_blocked, ever_stalled,
currently_blocked, currently_stalled)
end
true
end
|
.resolve_projects(server_context, project_filter) ⇒ Object
68
69
70
71
72
73
|
# File 'lib/jirametrics/mcp_server.rb', line 68
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
|
# File 'lib/jirametrics/mcp_server.rb', line 79
def self.time_per_column issue, end_time
changes = issue.status_changes
_, stopped = issue.started_stopped_times
effective_end = stopped && stopped < end_time ? stopped : end_time
board = issue.board
result = Hash.new(0.0)
if changes.empty?
col = column_name_for(board, issue.status.id) || issue.status.name
duration = effective_end - issue.created
result[col] += duration if duration.positive?
return result
end
first_change = changes.first
initial_col = column_name_for(board, first_change.old_value_id) || first_change.old_value
initial_duration = first_change.time - issue.created
result[initial_col] += initial_duration if initial_duration.positive?
changes.each_cons(2) do |prev_change, next_change|
col = column_name_for(board, prev_change.value_id) || prev_change.value
duration = next_change.time - prev_change.time
result[col] += duration if duration.positive?
end
last_change = changes.last
final_col = column_name_for(board, last_change.value_id) || last_change.value
final_duration = effective_end - last_change.time
result[final_col] += final_duration if final_duration.positive?
result
end
|
.time_per_status(issue, end_time) ⇒ Object
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
|
# File 'lib/jirametrics/mcp_server.rb', line 113
def self.time_per_status 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?
duration = effective_end - issue.created
result[issue.status.name] += duration if duration.positive?
return result
end
first_change = changes.first
initial_duration = first_change.time - issue.created
result[first_change.old_value] += initial_duration if initial_duration.positive?
changes.each_cons(2) do |prev_change, next_change|
duration = next_change.time - prev_change.time
result[prev_change.value] += duration if duration.positive?
end
last_change = changes.last
final_duration = effective_end - last_change.time
result[last_change.value] += final_duration if final_duration.positive?
result
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
|