scout_apm_mcp 0.1.3 → 0.1.4

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: 24de549e2d8e310ce563f8a9c380f642aabdd528259785ed17ae062c8b8e59aa
4
- data.tar.gz: 5a9c147401b3136be74f5197ec757b4f1d357c1ad5483f12c0813315f6d6d9e8
3
+ metadata.gz: ce6703433e4f11c4cf2769a636135ed7a429707a84a09e196e7151b43cb368b2
4
+ data.tar.gz: 2ddf028b1023a364f4eb5d6afc8f1693549ce4ace041f7d12487719f4513ef3d
5
5
  SHA512:
6
- metadata.gz: 509eeae8882791a33706ceda9a71a4095ef6e99abda4e7401082435d0d6e055882ebced248e176c74154bfa28db09eb965e49c5df88e8fa3c581f0f1e9dac782
7
- data.tar.gz: 33143a776cb940411007c14be1a34691a5569275fa8c3c507b2eca92846c8585bcfa438906b135dd31f986c6fbbd0ff7a98c7be74e27491242aa9f751b410202
6
+ metadata.gz: 1f1ff860d8e236a1338d4f1ceb62a0a6620ff667dc266cc528a7ffd8eceaaf5f4a874612fcc7e8fe45b64a83c446d42af6eca639d13b4ee5cf71b76ce7e7b6b6
7
+ data.tar.gz: 3cadb0d9852c1e661630c69148cfdb2d9c85076094e4672f5b0358448015ade8d13bc800c04a67c1b105ddf66b1ca842ac056c8e38b12b158c3c6290a289daa7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.1.4 (2025-12-08)
4
+
5
+ - Enhanced API client and documentation for improved endpoint management
6
+ - Refactored RSpec test suite with improved organization and clarity (split into focused spec files by component)
7
+ - Added RuboCop configuration (replacing Standard) with custom rules for RSpec and strict linting
8
+ - Updated trunk configuration to use RuboCop instead of Standard
9
+ - Added Code of Conduct, Governance, and Security Guidelines documents
10
+ - Updated MCP server configuration to use `gem exec` command with Ruby version specification
11
+ - Updated GitHub Actions workflow dependencies
12
+ - Updated bundler dependencies for improved compatibility
13
+
3
14
  ## 0.1.3 (2025-11-21)
4
15
 
5
16
  - Custom exception classes (`ScoutApmMcp::Error`, `ScoutApmMcp::AuthError`, `ScoutApmMcp::APIError`) for better error handling
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # scout_apm_mcp
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/scout_apm_mcp.svg?v=0.1.0)](https://badge.fury.io/rb/scout_apm_mcp) [![Test Status](https://github.com/amkisko/scout_apm_mcp.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/scout_apm_mcp.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/scout_apm_mcp.rb/graph/badge.svg?token=HVWDDNLEO5)](https://codecov.io/gh/amkisko/scout_apm_mcp.rb)
3
+ [![Gem Version](https://badge.fury.io/rb/scout_apm_mcp.svg?v=0.1.3)](https://badge.fury.io/rb/scout_apm_mcp) [![Test Status](https://github.com/amkisko/scout_apm_mcp.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/scout_apm_mcp.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/scout_apm_mcp.rb/graph/badge.svg?token=HVWDDNLEO5)](https://codecov.io/gh/amkisko/scout_apm_mcp.rb)
4
4
 
5
5
  Ruby gem providing ScoutAPM API client and MCP (Model Context Protocol) server tools for fetching traces, endpoints, metrics, errors, and insights. Integrates with MCP-compatible clients like Cursor IDE, Claude Desktop, and other MCP-enabled tools.
6
6
 
@@ -20,6 +20,12 @@ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
20
20
  2. In 1Password create an item with the name "Scout APM API" and store the API key in a new field named API_KEY
21
21
  3. Configure your favorite service to use local MCP server, ensure OP_ENV_ENTRY_PATH has correct vault and item names (both are visible in 1Password UI)
22
22
 
23
+ ### Installation
24
+
25
+ ```bash
26
+ gem install scout_apm_mcp
27
+ ```
28
+
23
29
  ### Cursor IDE Configuration
24
30
 
25
31
  For Cursor IDE, create or update `.cursor/mcp.json` in your project:
@@ -28,15 +34,19 @@ For Cursor IDE, create or update `.cursor/mcp.json` in your project:
28
34
  {
29
35
  "mcpServers": {
30
36
  "scout-apm": {
31
- "command": "scout_apm_mcp",
37
+ "command": "gem",
38
+ "args": ["exec", "scout_apm_mcp"],
32
39
  "env": {
33
- "OP_ENV_ENTRY_PATH": "op://Vault Name/Item Name"
40
+ "OP_ENV_ENTRY_PATH": "op://Vault Name/Item Name",
41
+ "RUBY_VERSION": "3.4.7"
34
42
  }
35
43
  }
36
44
  }
37
45
  }
38
46
  ```
39
47
 
48
+ **Note**: Using `gem exec` ensures the correct Ruby version is used. If you're using a Ruby version manager like [mise](https://mise.jdx.dev/) or [rbenv](https://github.com/rbenv/rbenv), set the `RUBY_VERSION` environment variable to match your desired Ruby version. The `gem exec` command will automatically use the correct Ruby version based on your version manager configuration.
49
+
40
50
  ### Claude Desktop Configuration
41
51
 
42
52
  For Claude Desktop, edit the MCP configuration file:
@@ -48,16 +58,18 @@ For Claude Desktop, edit the MCP configuration file:
48
58
  {
49
59
  "mcpServers": {
50
60
  "scout-apm": {
51
- "command": "scout_apm_mcp",
61
+ "command": "gem",
62
+ "args": ["exec", "scout_apm_mcp"],
52
63
  "env": {
53
- "OP_ENV_ENTRY_PATH": "op://Vault Name/Item Name"
64
+ "OP_ENV_ENTRY_PATH": "op://Vault Name/Item Name",
65
+ "RUBY_VERSION": "3.4.7"
54
66
  }
55
67
  }
56
68
  }
57
69
  }
58
70
  ```
59
71
 
60
- **Note**: After updating the configuration, restart Claude Desktop for changes to take effect.
72
+ **Note**: After updating the configuration, restart Claude Desktop for changes to take effect. Using `gem exec` ensures the correct Ruby version is used. If you're using a Ruby version manager like [mise](https://mise.jdx.dev/) or [rbenv](https://github.com/rbenv/rbenv), set the `RUBY_VERSION` environment variable to match your desired Ruby version. The `gem exec` command will automatically use the correct Ruby version based on your version manager configuration.
61
73
 
62
74
  ### Security Best Practice
63
75
 
@@ -107,6 +119,22 @@ scout_apm_mcp
107
119
 
108
120
  The server will start and communicate via STDIN/STDOUT using the MCP protocol. Make sure you have your ScoutAPM API key configured (see API Key Management section below).
109
121
 
122
+ ## Upgrading
123
+
124
+ To upgrade to the latest version of the gem:
125
+
126
+ ```bash
127
+ gem update scout_apm_mcp
128
+ ```
129
+
130
+ If you're using Bundler, update your `Gemfile.lock`:
131
+
132
+ ```bash
133
+ bundle update scout_apm_mcp
134
+ ```
135
+
136
+ **Note**: As of version 0.1.3, client methods return extracted data (arrays, hashes) instead of full API response structures. This is a breaking change from previous versions. See the [CHANGELOG.md](CHANGELOG.md) for details on breaking changes and new features.
137
+
110
138
  ## Features
111
139
 
112
140
  - **ScoutAPM API Client**: Full-featured client for ScoutAPM REST API
@@ -233,10 +261,11 @@ api_key = ScoutApmMcp::Helpers.get_api_key(
233
261
  ### Endpoints
234
262
 
235
263
  - `list_endpoints(app_id, from:, to:)` - List all endpoints
236
- - `get_endpoint(app_id, endpoint_id)` - Get endpoint details
237
264
  - `get_endpoint_metrics(app_id, endpoint_id, metric_type, from:, to:)` - Get endpoint metrics
238
265
  - `list_endpoint_traces(app_id, endpoint_id, from:, to:)` - List endpoint traces
239
266
 
267
+ Note: The API doesn't provide a direct endpoint detail endpoint. Use `list_endpoints` and filter by endpoint_id to get specific endpoint information.
268
+
240
269
  ### Traces
241
270
 
242
271
  - `fetch_trace(app_id, trace_id)` - Fetch detailed trace information
@@ -27,8 +27,12 @@ module ScoutApmMcp
27
27
 
28
28
  # @param api_key [String] ScoutAPM API key
29
29
  # @param api_base [String] API base URL (default: https://scoutapm.com/api/v0)
30
+ # @raise [ArgumentError] if api_key is nil or empty
30
31
  def initialize(api_key:, api_base: API_BASE)
31
- @api_key = api_key
32
+ if api_key.nil? || api_key.to_s.strip.empty?
33
+ raise ArgumentError, "API key is required and cannot be nil or empty"
34
+ end
35
+ @api_key = api_key.to_s
32
36
  @api_base = api_base
33
37
  @user_agent = "scout-apm-mcp-rb/#{VERSION}"
34
38
  end
@@ -42,7 +46,6 @@ module ScoutApmMcp
42
46
  response = make_request(uri)
43
47
  apps = response.dig("results", "apps") || []
44
48
 
45
- # Filter by active_since if provided
46
49
  if active_since
47
50
  active_time = Helpers.parse_time(active_since)
48
51
  apps = apps.select do |app|
@@ -84,8 +87,15 @@ module ScoutApmMcp
84
87
  # @param metric_type [String] Metric type (apdex, response_time, response_time_95th, errors, throughput, queue_time)
85
88
  # @param from [String, nil] Start time in ISO 8601 format
86
89
  # @param to [String, nil] End time in ISO 8601 format
90
+ # @param range [String, nil] Quick time range (e.g., "30min", "1day", "3days", "7days"). If provided, calculates from/to automatically.
87
91
  # @return [Hash] Hash containing metric series data
88
- def get_metric(app_id, metric_type, from: nil, to: nil)
92
+ def get_metric(app_id, metric_type, from: nil, to: nil, range: nil)
93
+ if range
94
+ calculated = Helpers.calculate_range(range: range, to: to)
95
+ from = calculated[:from]
96
+ to = calculated[:to]
97
+ end
98
+
89
99
  validate_metric_params(metric_type, from, to)
90
100
  uri = URI("#{@api_base}/apps/#{app_id}/metrics/#{metric_type}")
91
101
  uri.query = build_query_string(from: from, to: to)
@@ -98,8 +108,27 @@ module ScoutApmMcp
98
108
  # @param app_id [Integer] ScoutAPM application ID
99
109
  # @param from [String, nil] Start time in ISO 8601 format
100
110
  # @param to [String, nil] End time in ISO 8601 format
111
+ # @param range [String, nil] Quick time range (e.g., "30min", "1day", "3days", "7days"). If provided, calculates from/to automatically.
101
112
  # @return [Array<Hash>] Array of endpoint hashes
102
- def list_endpoints(app_id, from: nil, to: nil)
113
+ def list_endpoints(app_id, from: nil, to: nil, range: nil)
114
+ if from.nil? && to.nil? && range.nil?
115
+ range = "7days"
116
+ end
117
+
118
+ if range
119
+ calculated = Helpers.calculate_range(range: range, to: to)
120
+ from = calculated[:from]
121
+ to = calculated[:to]
122
+ end
123
+
124
+ now = Time.now.utc
125
+ if from.nil? && to
126
+ calculated = Helpers.calculate_range(range: "7days", to: to)
127
+ from = calculated[:from]
128
+ elsif from && to.nil?
129
+ to = Helpers.format_time(now)
130
+ end
131
+
103
132
  validate_time_range(from, to) if from && to
104
133
  uri = URI("#{@api_base}/apps/#{app_id}/endpoints")
105
134
  uri.query = build_query_string(from: from, to: to)
@@ -107,18 +136,6 @@ module ScoutApmMcp
107
136
  response["results"] || []
108
137
  end
109
138
 
110
- # Get endpoint details
111
- #
112
- # @param app_id [Integer] ScoutAPM application ID
113
- # @param endpoint_id [String] Endpoint ID (base64 URL-encoded)
114
- # @return [Hash] Endpoint details hash
115
- def get_endpoint(app_id, endpoint_id)
116
- encoded_endpoint_id = URI.encode_www_form_component(endpoint_id)
117
- uri = URI("#{@api_base}/apps/#{app_id}/endpoints/#{encoded_endpoint_id}")
118
- response = make_request(uri)
119
- response.dig("results", "endpoint") || response["results"] || {}
120
- end
121
-
122
139
  # Get metric data for a specific endpoint
123
140
  #
124
141
  # @param app_id [Integer] ScoutAPM application ID
@@ -126,11 +143,18 @@ module ScoutApmMcp
126
143
  # @param metric_type [String] Metric type (apdex, response_time, response_time_95th, errors, throughput, queue_time)
127
144
  # @param from [String, nil] Start time in ISO 8601 format
128
145
  # @param to [String, nil] End time in ISO 8601 format
146
+ # @param range [String, nil] Quick time range (e.g., "30min", "1day", "3days", "7days"). If provided, calculates from/to automatically.
129
147
  # @return [Array] Array of metric data points for the specified metric type
130
- def get_endpoint_metrics(app_id, endpoint_id, metric_type, from: nil, to: nil)
148
+ def get_endpoint_metrics(app_id, endpoint_id, metric_type, from: nil, to: nil, range: nil)
149
+ if range
150
+ calculated = Helpers.calculate_range(range: range, to: to)
151
+ from = calculated[:from]
152
+ to = calculated[:to]
153
+ end
154
+
131
155
  validate_metric_params(metric_type, from, to)
132
- encoded_endpoint_id = URI.encode_www_form_component(endpoint_id)
133
- uri = URI("#{@api_base}/apps/#{app_id}/endpoints/#{encoded_endpoint_id}/metrics/#{metric_type}")
156
+ uri = URI(@api_base)
157
+ uri.path = File.join(uri.path, "apps", app_id.to_s, "endpoints", endpoint_id, "metrics", metric_type)
134
158
  uri.query = build_query_string(from: from, to: to)
135
159
  response = make_request(uri)
136
160
  series = response.dig("results", "series") || {}
@@ -143,19 +167,25 @@ module ScoutApmMcp
143
167
  # @param endpoint_id [String] Endpoint ID (base64 URL-encoded)
144
168
  # @param from [String, nil] Start time in ISO 8601 format
145
169
  # @param to [String, nil] End time in ISO 8601 format
170
+ # @param range [String, nil] Quick time range (e.g., "30min", "1day", "3days", "7days"). If provided, calculates from/to automatically.
146
171
  # @return [Array<Hash>] Array of trace hashes
147
- def list_endpoint_traces(app_id, endpoint_id, from: nil, to: nil)
172
+ def list_endpoint_traces(app_id, endpoint_id, from: nil, to: nil, range: nil)
173
+ if range
174
+ calculated = Helpers.calculate_range(range: range, to: to)
175
+ from = calculated[:from]
176
+ to = calculated[:to]
177
+ end
178
+
148
179
  validate_time_range(from, to) if from && to
149
180
  if from && to
150
- # Validate that from_time is not older than 7 days
151
181
  from_time = Helpers.parse_time(from)
152
182
  seven_days_ago = Time.now.utc - (7 * 24 * 60 * 60)
153
183
  if from_time < seven_days_ago
154
184
  raise ArgumentError, "from_time cannot be older than 7 days"
155
185
  end
156
186
  end
157
- encoded_endpoint_id = URI.encode_www_form_component(endpoint_id)
158
- uri = URI("#{@api_base}/apps/#{app_id}/endpoints/#{encoded_endpoint_id}/traces")
187
+ uri = URI(@api_base)
188
+ uri.path = File.join(uri.path, "apps", app_id.to_s, "endpoints", endpoint_id, "traces")
159
189
  uri.query = build_query_string(from: from, to: to)
160
190
  response = make_request(uri)
161
191
  response.dig("results", "traces") || []
@@ -370,7 +400,6 @@ module ScoutApmMcp
370
400
  response = http.request(request)
371
401
  response_data = handle_response_errors(response)
372
402
 
373
- # Check for API-level errors in response body
374
403
  if response_data.is_a?(Hash)
375
404
  header = response_data["header"]
376
405
  if header && header["status"]
@@ -391,21 +420,17 @@ module ScoutApmMcp
391
420
  raise Error, "Request failed: #{e.class} - #{e.message}"
392
421
  end
393
422
 
394
- # Handle common response errors and parse JSON
395
- #
396
423
  # @param response [Net::HTTPResponse] HTTP response object
397
424
  # @return [Hash, Array] Parsed JSON response
398
425
  # @raise [AuthError] When authentication fails
399
426
  # @raise [APIError] When the API returns an error response
400
427
  def handle_response_errors(response)
401
- # Try to parse JSON response
402
428
  begin
403
429
  data = JSON.parse(response.body)
404
430
  rescue JSON::ParserError
405
431
  raise APIError.new("Invalid JSON response: #{response.body}", status_code: response.code.to_i)
406
432
  end
407
433
 
408
- # Check for HTTP-level errors
409
434
  case response
410
435
  when Net::HTTPSuccess
411
436
  data
@@ -14,14 +14,10 @@ module ScoutApmMcp
14
14
  # @return [String] API key
15
15
  # @raise [RuntimeError] if API key cannot be found
16
16
  def self.get_api_key(api_key: nil, op_vault: nil, op_item: nil, op_field: "API_KEY")
17
- # Use provided API key if available
18
17
  return api_key if api_key && !api_key.empty?
19
18
 
20
- # Check environment variable (may have been set by opdotenv loaded early in server startup)
21
19
  api_key = ENV["API_KEY"] || ENV["SCOUT_APM_API_KEY"]
22
20
  return api_key if api_key && !api_key.empty?
23
-
24
- # Try direct 1Password CLI as fallback (opdotenv was already tried in server startup)
25
21
  op_env_entry_path = ENV["OP_ENV_ENTRY_PATH"]
26
22
  if op_env_entry_path && !op_env_entry_path.empty?
27
23
  begin
@@ -55,7 +51,6 @@ module ScoutApmMcp
55
51
  api_key = `op read "op://#{op_vault}/#{op_item}/#{op_field}" 2>/dev/null`.strip
56
52
  return api_key if api_key && !api_key.empty?
57
53
  rescue
58
- # Silently fail
59
54
  end
60
55
  end
61
56
 
@@ -135,7 +130,6 @@ module ScoutApmMcp
135
130
  # @return [String] Decoded endpoint ID
136
131
  def self.decode_endpoint_id(endpoint_id)
137
132
  decoded = Base64.urlsafe_decode64(endpoint_id)
138
- # Check if decoded result is valid UTF-8
139
133
  if decoded.force_encoding(Encoding::UTF_8).valid_encoding?
140
134
  decoded.force_encoding(Encoding::UTF_8)
141
135
  else
@@ -200,5 +194,59 @@ module ScoutApmMcp
200
194
  end: parse_time(to_str)
201
195
  }
202
196
  end
197
+
198
+ # Parse a time range string into seconds
199
+ #
200
+ # Supports formats like: "30min", "60min", "3hrs", "6hrs", "12hrs", "1day", "3days", "7days"
201
+ # Case-insensitive, supports singular and plural forms
202
+ #
203
+ # @param range_str [String] Time range string (e.g., "30min", "1day", "7days")
204
+ # @return [Integer] Duration in seconds
205
+ # @raise [ArgumentError] If the range string format is invalid
206
+ def self.parse_range(range_str)
207
+ return nil if range_str.nil? || range_str.empty?
208
+
209
+ # Normalize: lowercase, remove spaces, handle singular/plural
210
+ normalized = range_str.downcase.strip.gsub(/\s+/, "")
211
+
212
+ # Match pattern: number followed by unit
213
+ match = normalized.match(/\A(\d+)(min|mins?|hr|hrs?|hour|hours|day|days)\z/)
214
+ unless match
215
+ valid_ranges = %w[30min 60min 3hrs 6hrs 12hrs 1day 3days 7days]
216
+ raise ArgumentError, "Invalid range format: #{range_str}. Valid formats: #{valid_ranges.join(", ")}"
217
+ end
218
+
219
+ value = match[1].to_i
220
+ unit = match[2]
221
+
222
+ case unit
223
+ when /^min/
224
+ value * 60
225
+ when /^hr/, /^hour/
226
+ value * 60 * 60
227
+ when /^day/
228
+ value * 24 * 60 * 60
229
+ else
230
+ raise ArgumentError, "Unknown time unit: #{unit}"
231
+ end
232
+ end
233
+
234
+ # Calculate from/to times based on a range string
235
+ #
236
+ # @param range [String, nil] Time range string (e.g., "30min", "1day", "3days")
237
+ # @param to [String, nil] End time in ISO 8601 format (defaults to now if not provided)
238
+ # @return [Hash] Hash with :from and :to as ISO 8601 strings
239
+ def self.calculate_range(range:, to: nil)
240
+ return {from: nil, to: to} if range.nil? || range.empty?
241
+
242
+ end_time = to ? parse_time(to) : Time.now.utc
243
+ duration_seconds = parse_range(range)
244
+ start_time = end_time - duration_seconds
245
+
246
+ {
247
+ from: format_time(start_time),
248
+ to: format_time(end_time)
249
+ }
250
+ end
203
251
  end
204
252
  end
@@ -133,7 +133,6 @@ module ScoutApmMcp
133
133
  server.register_tool(ListMetricsTool)
134
134
  server.register_tool(GetMetricTool)
135
135
  server.register_tool(ListEndpointsTool)
136
- server.register_tool(FetchEndpointTool)
137
136
  server.register_tool(GetEndpointMetricsTool)
138
137
  server.register_tool(ListEndpointTracesTool)
139
138
  server.register_tool(FetchTraceTool)
@@ -166,10 +165,23 @@ module ScoutApmMcp
166
165
 
167
166
  # Applications Tools
168
167
  class ListAppsTool < BaseTool
169
- description "List all applications accessible with the provided API key. Provide an optional active_since ISO 8601 to filter to only apps that have reported data since that time. Defaults to the metric retention period of thirty days."
168
+ description <<~DESC
169
+ List all applications accessible with the provided API key.
170
+
171
+ Returns an array of applications with details like name, ID, and last reported time.
172
+ Use the app_id from the results to make subsequent API calls.
173
+
174
+ Optional filtering:
175
+ - active_since: Only return apps that have reported data since this time (ISO 8601 format)
176
+ - Default behavior: Returns all apps (no filtering by default, but API may filter to last 30 days)
177
+
178
+ Example:
179
+ - List all apps: call without parameters
180
+ - List apps active in last 7 days: provide active_since="2025-01-08T00:00:00Z"
181
+ DESC
170
182
 
171
183
  arguments do
172
- optional(:active_since).maybe(:string).description("ISO 8601 datetime string to filter apps active since that time")
184
+ optional(:active_since).maybe(:string).description("ISO 8601 datetime string to filter apps active since that time (e.g., 2025-01-08T00:00:00Z)")
173
185
  end
174
186
 
175
187
  def call(active_since: nil)
@@ -203,80 +215,151 @@ module ScoutApmMcp
203
215
  end
204
216
 
205
217
  class GetMetricTool < BaseTool
206
- description "Get time-series data for a specific metric type"
218
+ description <<~DESC
219
+ Get time-series data for a specific metric type.
220
+
221
+ Available metric types:
222
+ - apdex: Application Performance Index (0-1, higher is better)
223
+ - response_time: Average response time in milliseconds
224
+ - response_time_95th: 95th percentile response time in milliseconds
225
+ - errors: Number of errors
226
+ - throughput: Requests per second
227
+ - queue_time: Time spent in queue in milliseconds
228
+
229
+ You can specify time ranges using:
230
+ 1. Quick range templates: range="30min", "1day", "3days", "7days", etc.
231
+ 2. Explicit times: from and to with ISO 8601 timestamps
232
+
233
+ Examples:
234
+ - Get response time for last hour: metric_type="response_time", range="1hr"
235
+ - Get error count for last day: metric_type="errors", range="1day"
236
+ - Get metrics for specific range: metric_type="apdex", from="2025-01-15T10:00:00Z", to="2025-01-15T12:00:00Z"
237
+ DESC
207
238
 
208
239
  arguments do
209
240
  required(:app_id).filled(:integer).description("ScoutAPM application ID")
210
241
  required(:metric_type).filled(:string).description("Metric type: apdex, response_time, response_time_95th, errors, throughput, queue_time")
211
- optional(:from).maybe(:string).description("Start time in ISO 8601 format (e.g., 2025-11-17T15:25:35Z)")
212
- optional(:to).maybe(:string).description("End time in ISO 8601 format (e.g., 2025-11-18T15:25:35Z)")
242
+ optional(:range).maybe(:string).description("Quick time range template: 30min, 60min, 3hrs, 6hrs, 12hrs, 1day, 3days, 7days. If provided, calculates from/to automatically.")
243
+ optional(:from).maybe(:string).description("Start time in ISO 8601 format (e.g., 2025-11-17T15:25:35Z). Ignored if range is provided.")
244
+ optional(:to).maybe(:string).description("End time in ISO 8601 format (e.g., 2025-11-18T15:25:35Z). Used as end point for range if range is provided.")
213
245
  end
214
246
 
215
- def call(app_id:, metric_type:, from: nil, to: nil)
216
- get_client.get_metric(app_id, metric_type, from: from, to: to)
247
+ def call(app_id:, metric_type:, range: nil, from: nil, to: nil)
248
+ get_client.get_metric(app_id, metric_type, from: from, to: to, range: range)
217
249
  end
218
250
  end
219
251
 
220
252
  # Endpoints Tools
221
253
  class ListEndpointsTool < BaseTool
222
- description "List all endpoints for an application"
254
+ description <<~DESC
255
+ List all endpoints for an application.
256
+
257
+ The API requires timeframe parameters. You can specify time ranges in two ways:
258
+ 1. Quick range templates: Use the 'range' parameter (e.g., "30min", "1day", "3days", "7days")
259
+ 2. Explicit times: Use 'from' and 'to' parameters with ISO 8601 timestamps
260
+
261
+ If neither from/to nor range are provided, defaults to the last 7 days.
262
+
263
+ Quick range templates (case-insensitive):
264
+ - "30min" or "30mins" - Last 30 minutes
265
+ - "60min" or "60mins" or "1hr" or "1hour" - Last 60 minutes
266
+ - "3hrs" or "3hours" - Last 3 hours
267
+ - "6hrs" or "6hours" - Last 6 hours
268
+ - "12hrs" or "12hours" - Last 12 hours
269
+ - "1day" or "1days" - Last 24 hours
270
+ - "3days" - Last 3 days
271
+ - "7days" - Last 7 days (default)
272
+
273
+ Examples:
274
+ - List endpoints for last 30 minutes: range="30min"
275
+ - List endpoints for last day: range="1day"
276
+ - List endpoints for a specific range: from="2025-01-15T10:00:00Z", to="2025-01-15T12:00:00Z"
277
+ - List endpoints from a specific time to now: from="2025-01-15T10:00:00Z"
278
+ - List endpoints for 1 day ending at specific time: range="1day", to="2025-01-15T12:00:00Z"
279
+ DESC
223
280
 
224
281
  arguments do
225
282
  required(:app_id).filled(:integer).description("ScoutAPM application ID")
226
- optional(:from).maybe(:string).description("Start time in ISO 8601 format (e.g., 2025-11-17T15:25:35Z)")
227
- optional(:to).maybe(:string).description("End time in ISO 8601 format (e.g., 2025-11-18T15:25:35Z)")
283
+ optional(:range).maybe(:string).description("Quick time range template: 30min, 60min, 3hrs, 6hrs, 12hrs, 1day, 3days, 7days. If provided, calculates from/to automatically.")
284
+ optional(:from).maybe(:string).description("Start time in ISO 8601 format (e.g., 2025-11-17T15:25:35Z). Ignored if range is provided.")
285
+ optional(:to).maybe(:string).description("End time in ISO 8601 format (e.g., 2025-11-18T15:25:35Z). Used as end point for range if range is provided, otherwise defaults to now.")
228
286
  end
229
287
 
230
- def call(app_id:, from: nil, to: nil)
231
- get_client.list_endpoints(app_id, from: from, to: to)
288
+ def call(app_id:, range: nil, from: nil, to: nil)
289
+ get_client.list_endpoints(app_id, from: from, to: to, range: range)
232
290
  end
233
291
  end
234
292
 
235
- class FetchEndpointTool < BaseTool
236
- description "Fetch endpoint details from ScoutAPM API"
293
+ class GetEndpointMetricsTool < BaseTool
294
+ description <<~DESC
295
+ Get metric data for a specific endpoint.
237
296
 
238
- arguments do
239
- required(:app_id).filled(:integer).description("ScoutAPM application ID")
240
- required(:endpoint_id).filled(:string).description("Endpoint ID (base64 URL-encoded)")
241
- end
297
+ Available metric types:
298
+ - apdex: Application Performance Index (0-1, higher is better)
299
+ - response_time: Average response time in milliseconds
300
+ - response_time_95th: 95th percentile response time in milliseconds
301
+ - errors: Number of errors
302
+ - throughput: Requests per second
303
+ - queue_time: Time spent in queue in milliseconds
242
304
 
243
- def call(app_id:, endpoint_id:)
244
- client = get_client
245
- {
246
- endpoint: client.get_endpoint(app_id, endpoint_id),
247
- decoded_endpoint: Helpers.decode_endpoint_id(endpoint_id)
248
- }
249
- end
250
- end
305
+ You can specify time ranges using:
306
+ 1. Quick range templates: range="30min", "1day", "3days", "7days", etc.
307
+ 2. Explicit times: from and to with ISO 8601 timestamps
251
308
 
252
- class GetEndpointMetricsTool < BaseTool
253
- description "Get metric data for a specific endpoint"
309
+ Returns time-series data points for the specified metric type.
310
+
311
+ Examples:
312
+ - Get response time for last hour: metric_type="response_time", range="1hr"
313
+ - Get error count for last day: metric_type="errors", range="1day"
314
+ - Get metrics for specific range: metric_type="apdex", from="2025-01-15T10:00:00Z", to="2025-01-15T12:00:00Z"
315
+ DESC
254
316
 
255
317
  arguments do
256
318
  required(:app_id).filled(:integer).description("ScoutAPM application ID")
257
- required(:endpoint_id).filled(:string).description("Endpoint ID (base64 URL-encoded)")
319
+ required(:endpoint_id).filled(:string).description("Endpoint ID (base64 URL-encoded). Extract from ScoutAPM URLs or use ParseScoutURLTool.")
258
320
  required(:metric_type).filled(:string).description("Metric type: apdex, response_time, response_time_95th, errors, throughput, queue_time")
259
- optional(:from).maybe(:string).description("Start time in ISO 8601 format (e.g., 2025-11-17T15:25:35Z)")
260
- optional(:to).maybe(:string).description("End time in ISO 8601 format (e.g., 2025-11-18T15:25:35Z)")
321
+ optional(:range).maybe(:string).description("Quick time range template: 30min, 60min, 3hrs, 6hrs, 12hrs, 1day, 3days, 7days. If provided, calculates from/to automatically.")
322
+ optional(:from).maybe(:string).description("Start time in ISO 8601 format (e.g., 2025-11-17T15:25:35Z). Ignored if range is provided.")
323
+ optional(:to).maybe(:string).description("End time in ISO 8601 format (e.g., 2025-11-18T15:25:35Z). Used as end point for range if range is provided.")
261
324
  end
262
325
 
263
- def call(app_id:, endpoint_id:, metric_type:, from: nil, to: nil)
264
- get_client.get_endpoint_metrics(app_id, endpoint_id, metric_type, from: from, to: to)
326
+ def call(app_id:, endpoint_id:, metric_type:, range: nil, from: nil, to: nil)
327
+ get_client.get_endpoint_metrics(app_id, endpoint_id, metric_type, from: from, to: to, range: range)
265
328
  end
266
329
  end
267
330
 
268
331
  class ListEndpointTracesTool < BaseTool
269
- description "List traces for a specific endpoint (max 100, within 7 days)"
332
+ description <<~DESC
333
+ List traces for a specific endpoint (max 100, within 7 days).
334
+
335
+ Traces are individual request executions that can be analyzed for performance issues.
336
+ Returns up to 100 traces for the specified endpoint within the last 7 days.
337
+
338
+ You can specify time ranges using:
339
+ 1. Quick range templates: range="30min", "1day", "3days", "7days", etc.
340
+ 2. Explicit times: from and to with ISO 8601 timestamps
341
+
342
+ Time range constraints:
343
+ - If from is provided, it must be within the last 7 days
344
+ - Maximum 100 traces returned per request
345
+ - Use the trace_id from results with FetchTraceTool for detailed analysis
346
+
347
+ Examples:
348
+ - List traces from last hour: range="1hr"
349
+ - List traces from last day: range="1day"
350
+ - List traces for specific range: from="2025-01-15T10:00:00Z", to="2025-01-15T12:00:00Z"
351
+ DESC
270
352
 
271
353
  arguments do
272
354
  required(:app_id).filled(:integer).description("ScoutAPM application ID")
273
- required(:endpoint_id).filled(:string).description("Endpoint ID (base64 URL-encoded)")
274
- optional(:from).maybe(:string).description("Start time in ISO 8601 format (e.g., 2025-11-17T15:25:35Z)")
275
- optional(:to).maybe(:string).description("End time in ISO 8601 format (e.g., 2025-11-18T15:25:35Z)")
355
+ required(:endpoint_id).filled(:string).description("Endpoint ID (base64 URL-encoded). Extract from ScoutAPM URLs or use ParseScoutURLTool.")
356
+ optional(:range).maybe(:string).description("Quick time range template: 30min, 60min, 3hrs, 6hrs, 12hrs, 1day, 3days, 7days. If provided, calculates from/to automatically.")
357
+ optional(:from).maybe(:string).description("Start time in ISO 8601 format (e.g., 2025-11-17T15:25:35Z). Must be within last 7 days. Ignored if range is provided.")
358
+ optional(:to).maybe(:string).description("End time in ISO 8601 format (e.g., 2025-11-18T15:25:35Z). Used as end point for range if range is provided.")
276
359
  end
277
360
 
278
- def call(app_id:, endpoint_id:, from: nil, to: nil)
279
- get_client.list_endpoint_traces(app_id, endpoint_id, from: from, to: to)
361
+ def call(app_id:, endpoint_id:, range: nil, from: nil, to: nil)
362
+ get_client.list_endpoint_traces(app_id, endpoint_id, from: from, to: to, range: range)
280
363
  end
281
364
  end
282
365
 
@@ -432,10 +515,28 @@ module ScoutApmMcp
432
515
 
433
516
  # Utility Tools
434
517
  class ParseScoutURLTool < BaseTool
435
- description "Parse a ScoutAPM URL and extract resource information (app_id, endpoint_id, trace_id, etc.)"
518
+ description <<~DESC
519
+ Parse a ScoutAPM URL and extract resource information (app_id, endpoint_id, trace_id, etc.).
520
+
521
+ This tool extracts structured information from ScoutAPM URLs without making API calls.
522
+ Useful for extracting IDs before making other API requests.
523
+
524
+ Returns a hash with:
525
+ - url_type: :endpoint, :trace, :error_group, :insight, :app, or :unknown
526
+ - app_id: Application ID (integer)
527
+ - endpoint_id: Base64 URL-encoded endpoint ID (if present)
528
+ - trace_id: Trace ID (if present)
529
+ - error_id: Error group ID (if present)
530
+ - insight_type: Insight type (if present)
531
+ - decoded_endpoint: Human-readable endpoint path (if endpoint_id present)
532
+
533
+ Example:
534
+ - Input: "https://scoutapm.com/apps/123/endpoints/ABC123.../trace/456"
535
+ - Output: {url_type: :trace, app_id: 123, endpoint_id: "ABC123...", trace_id: 456, decoded_endpoint: "Controller/Action"}
536
+ DESC
436
537
 
437
538
  arguments do
438
- required(:url).filled(:string).description("Full ScoutAPM URL (e.g., https://scoutapm.com/apps/123/endpoints/.../trace/456)")
539
+ required(:url).filled(:string).description("Full ScoutAPM URL")
439
540
  end
440
541
 
441
542
  def call(url:)
@@ -444,10 +545,27 @@ module ScoutApmMcp
444
545
  end
445
546
 
446
547
  class FetchScoutURLTool < BaseTool
447
- description "Fetch data from a ScoutAPM URL by automatically detecting the resource type and fetching the appropriate data"
548
+ description <<~DESC
549
+ Fetch data from a ScoutAPM URL by automatically detecting the resource type and fetching the appropriate data.
550
+
551
+ This tool automatically parses ScoutAPM URLs and fetches the corresponding data.
552
+ Supported URL types:
553
+ - Endpoint URLs: /apps/{app_id}/endpoints/{endpoint_id} (fetches from endpoint list)
554
+ - Trace URLs: /apps/{app_id}/endpoints/{endpoint_id}/trace/{trace_id}
555
+ - Error group URLs: /apps/{app_id}/error_groups/{error_id}
556
+ - Insight URLs: /apps/{app_id}/insights or /apps/{app_id}/insights/{insight_type}
557
+ - App URLs: /apps/{app_id}
558
+
559
+ Examples:
560
+ - https://scoutapm.com/apps/123/endpoints/ABC123... (endpoint)
561
+ - https://scoutapm.com/apps/123/endpoints/ABC123.../trace/456 (trace)
562
+ - https://scoutapm.com/apps/123/error_groups/789 (error group)
563
+
564
+ For trace URLs, set include_endpoint=true to also fetch endpoint context.
565
+ DESC
448
566
 
449
567
  arguments do
450
- required(:url).filled(:string).description("Full ScoutAPM URL (e.g., https://scoutapm.com/apps/123/endpoints/.../trace/456)")
568
+ required(:url).filled(:string).description("Full ScoutAPM URL")
451
569
  optional(:include_endpoint).filled(:bool).description("For trace URLs, also fetch endpoint details for context (default: false)")
452
570
  end
453
571
 
@@ -469,11 +587,16 @@ module ScoutApmMcp
469
587
 
470
588
  if include_endpoint && parsed[:endpoint_id]
471
589
  begin
472
- endpoint_data = client.get_endpoint(parsed[:app_id], parsed[:endpoint_id])
473
- result[:data][:endpoint] = endpoint_data
590
+ endpoints = client.list_endpoints(parsed[:app_id], range: "7days")
591
+ endpoint_data = endpoints.find { |ep| Helpers.get_endpoint_id(ep) == parsed[:endpoint_id] }
592
+
593
+ if endpoint_data
594
+ result[:data][:endpoint] = endpoint_data
595
+ else
596
+ result[:data][:endpoint_error] = "Endpoint not found in the last 7 days"
597
+ end
474
598
  result[:data][:decoded_endpoint] = parsed[:decoded_endpoint]
475
599
  rescue => e
476
- # Endpoint fetch failed, but we still have trace data
477
600
  result[:data][:endpoint_error] = "Failed to fetch endpoint: #{e.message}"
478
601
  result[:data][:decoded_endpoint] = parsed[:decoded_endpoint]
479
602
  end
@@ -483,11 +606,17 @@ module ScoutApmMcp
483
606
  end
484
607
  when :endpoint
485
608
  if parsed[:app_id] && parsed[:endpoint_id]
486
- endpoint_data = client.get_endpoint(parsed[:app_id], parsed[:endpoint_id])
487
- result[:data] = {
488
- endpoint: endpoint_data,
489
- decoded_endpoint: parsed[:decoded_endpoint]
490
- }
609
+ endpoints = client.list_endpoints(parsed[:app_id], range: "7days")
610
+ endpoint_data = endpoints.find { |ep| Helpers.get_endpoint_id(ep) == parsed[:endpoint_id] }
611
+
612
+ if endpoint_data
613
+ result[:data] = {
614
+ endpoint: endpoint_data,
615
+ decoded_endpoint: parsed[:decoded_endpoint]
616
+ }
617
+ else
618
+ raise "Endpoint not found in the last 7 days. Try using ListEndpointsTool with a longer time range."
619
+ end
491
620
  else
492
621
  raise "Invalid endpoint URL: missing app_id or endpoint_id"
493
622
  end
@@ -1,3 +1,3 @@
1
1
  module ScoutApmMcp
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.4"
3
3
  end
@@ -16,7 +16,6 @@ module ScoutApmMcp
16
16
  def list_metrics: (Integer app_id) -> Hash[String, untyped]
17
17
  def get_metric: (Integer app_id, String metric_type, ?from: String?, ?to: String?) -> Hash[String, untyped]
18
18
  def list_endpoints: (Integer app_id, ?from: String?, ?to: String?) -> Hash[String, untyped]
19
- def get_endpoint: (Integer app_id, String endpoint_id) -> Hash[String, untyped]
20
19
  def get_endpoint_metrics: (Integer app_id, String endpoint_id, String metric_type, ?from: String?, ?to: String?) -> Hash[String, untyped]
21
20
  def list_endpoint_traces: (Integer app_id, String endpoint_id, ?from: String?, ?to: String?) -> Hash[String, untyped]
22
21
  def fetch_trace: (Integer app_id, Integer trace_id) -> Hash[String, untyped]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout_apm_mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -47,22 +47,22 @@ dependencies:
47
47
  name: rack
48
48
  requirement: !ruby/object:Gem::Requirement
49
49
  requirements:
50
- - - "~>"
50
+ - - ">="
51
51
  - !ruby/object:Gem::Version
52
52
  version: '2.2'
53
- - - ">="
53
+ - - "<"
54
54
  - !ruby/object:Gem::Version
55
- version: 2.2.0
55
+ version: '4.0'
56
56
  type: :runtime
57
57
  prerelease: false
58
58
  version_requirements: !ruby/object:Gem::Requirement
59
59
  requirements:
60
- - - "~>"
60
+ - - ">="
61
61
  - !ruby/object:Gem::Version
62
62
  version: '2.2'
63
- - - ">="
63
+ - - "<"
64
64
  - !ruby/object:Gem::Version
65
- version: 2.2.0
65
+ version: '4.0'
66
66
  - !ruby/object:Gem::Dependency
67
67
  name: opdotenv
68
68
  requirement: !ruby/object:Gem::Requirement
@@ -175,6 +175,48 @@ dependencies:
175
175
  - - "~>"
176
176
  - !ruby/object:Gem::Version
177
177
  version: '1.52'
178
+ - !ruby/object:Gem::Dependency
179
+ name: standard-performance
180
+ requirement: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - "~>"
183
+ - !ruby/object:Gem::Version
184
+ version: '1.8'
185
+ type: :development
186
+ prerelease: false
187
+ version_requirements: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - "~>"
190
+ - !ruby/object:Gem::Version
191
+ version: '1.8'
192
+ - !ruby/object:Gem::Dependency
193
+ name: standard-rspec
194
+ requirement: !ruby/object:Gem::Requirement
195
+ requirements:
196
+ - - "~>"
197
+ - !ruby/object:Gem::Version
198
+ version: '0.3'
199
+ type: :development
200
+ prerelease: false
201
+ version_requirements: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - "~>"
204
+ - !ruby/object:Gem::Version
205
+ version: '0.3'
206
+ - !ruby/object:Gem::Dependency
207
+ name: rubocop-rspec
208
+ requirement: !ruby/object:Gem::Requirement
209
+ requirements:
210
+ - - "~>"
211
+ - !ruby/object:Gem::Version
212
+ version: '3.8'
213
+ type: :development
214
+ prerelease: false
215
+ version_requirements: !ruby/object:Gem::Requirement
216
+ requirements:
217
+ - - "~>"
218
+ - !ruby/object:Gem::Version
219
+ version: '3.8'
178
220
  - !ruby/object:Gem::Dependency
179
221
  name: appraisal
180
222
  requirement: !ruby/object:Gem::Requirement