scout_apm_mcp 0.1.2 → 0.1.3
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 +17 -0
- data/README.md +58 -8
- data/lib/scout_apm_mcp/client.rb +175 -41
- data/lib/scout_apm_mcp/errors.rb +18 -0
- data/lib/scout_apm_mcp/helpers.rb +50 -1
- data/lib/scout_apm_mcp/server.rb +13 -7
- data/lib/scout_apm_mcp/version.rb +1 -1
- data/lib/scout_apm_mcp.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 24de549e2d8e310ce563f8a9c380f642aabdd528259785ed17ae062c8b8e59aa
|
|
4
|
+
data.tar.gz: 5a9c147401b3136be74f5197ec757b4f1d357c1ad5483f12c0813315f6d6d9e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 509eeae8882791a33706ceda9a71a4095ef6e99abda4e7401082435d0d6e055882ebced248e176c74154bfa28db09eb965e49c5df88e8fa3c581f0f1e9dac782
|
|
7
|
+
data.tar.gz: 33143a776cb940411007c14be1a34691a5569275fa8c3c507b2eca92846c8585bcfa438906b135dd31f986c6fbbd0ff7a98c7be74e27491242aa9f751b410202
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.1.3 (2025-11-21)
|
|
4
|
+
|
|
5
|
+
- Custom exception classes (`ScoutApmMcp::Error`, `ScoutApmMcp::AuthError`, `ScoutApmMcp::APIError`) for better error handling
|
|
6
|
+
- Input validation for metric types (`VALID_METRICS` constant) and insight types (`VALID_INSIGHTS` constant)
|
|
7
|
+
- Time range validation (ensures from_time < to_time and range doesn't exceed 2 weeks)
|
|
8
|
+
- Trace age validation (ensures trace queries aren't older than 7 days)
|
|
9
|
+
- Client methods now return extracted data instead of full API response structure
|
|
10
|
+
- Time/Duration helpers (`Helpers.format_time`, `Helpers.parse_time`, `Helpers.make_duration`)
|
|
11
|
+
- Endpoint ID extraction helper (`Helpers.get_endpoint_id`)
|
|
12
|
+
- User-Agent header (`scout-apm-mcp-rb/VERSION`) on all API requests
|
|
13
|
+
- `active_since` parameter to `list_apps` method for filtering apps by last reported time
|
|
14
|
+
- API-level error parsing - checks for `header.status.code` in response body
|
|
15
|
+
- Error handling now uses custom exception classes instead of generic `RuntimeError`
|
|
16
|
+
- MCP `ListAppsTool` now supports optional `active_since` parameter
|
|
17
|
+
- Error responses now properly parse API-level error codes from response body
|
|
18
|
+
- Invalid metric types, insight types, and time ranges are now validated before API calls
|
|
19
|
+
|
|
3
20
|
## 0.1.2 (2025-11-21)
|
|
4
21
|
|
|
5
22
|
- Enhanced SSL certificate handling with support for `SSL_CERT_FILE` environment variable and automatic fallback to system certificates
|
data/README.md
CHANGED
|
@@ -113,12 +113,18 @@ The server will start and communicate via STDIN/STDOUT using the MCP protocol. M
|
|
|
113
113
|
- **MCP Server Integration**: Ready-to-use MCP server compatible with Cursor IDE, Claude Desktop, and other MCP-enabled tools
|
|
114
114
|
- **API Key Management**: Supports environment variables and 1Password integration (via optional `opdotenv` gem)
|
|
115
115
|
- **URL Parsing**: Helper methods to parse ScoutAPM URLs and extract IDs
|
|
116
|
+
- **Time Utilities**: Helper methods for formatting, parsing, and working with ISO 8601 time strings
|
|
117
|
+
- **Input Validation**: Validates metric types, insight types, and time ranges before API calls
|
|
118
|
+
- **Custom Error Handling**: Dedicated exception classes for better error handling and debugging
|
|
116
119
|
- **Comprehensive API Coverage**: Supports all ScoutAPM API endpoints (apps, metrics, endpoints, traces, errors, insights)
|
|
120
|
+
- **Data Extraction**: Client methods return extracted data instead of full API response structure for easier usage
|
|
117
121
|
|
|
118
122
|
## Basic Usage
|
|
119
123
|
|
|
120
124
|
### API Client
|
|
121
125
|
|
|
126
|
+
**Note**: As of version 0.1.3, all client methods return extracted data (arrays, hashes) instead of the full API response structure. This is a breaking change from previous versions.
|
|
127
|
+
|
|
122
128
|
```ruby
|
|
123
129
|
require "scout_apm_mcp"
|
|
124
130
|
|
|
@@ -128,10 +134,13 @@ api_key = ScoutApmMcp::Helpers.get_api_key
|
|
|
128
134
|
# Create client
|
|
129
135
|
client = ScoutApmMcp::Client.new(api_key: api_key)
|
|
130
136
|
|
|
131
|
-
# List applications
|
|
137
|
+
# List applications (returns Array<Hash>)
|
|
132
138
|
apps = client.list_apps
|
|
133
139
|
|
|
134
|
-
#
|
|
140
|
+
# List applications filtered by active_since
|
|
141
|
+
apps = client.list_apps(active_since: "2025-11-01T00:00:00Z")
|
|
142
|
+
|
|
143
|
+
# Get application details (returns Hash)
|
|
135
144
|
app = client.get_app(123)
|
|
136
145
|
|
|
137
146
|
# List endpoints
|
|
@@ -140,7 +149,7 @@ endpoints = client.list_endpoints(123)
|
|
|
140
149
|
# Fetch trace
|
|
141
150
|
trace = client.fetch_trace(123, 456)
|
|
142
151
|
|
|
143
|
-
# Get metrics
|
|
152
|
+
# Get metrics (returns Hash with series data)
|
|
144
153
|
metrics = client.get_metric(123, "response_time", from: "2025-01-01T00:00:00Z", to: "2025-01-02T00:00:00Z")
|
|
145
154
|
|
|
146
155
|
# List error groups
|
|
@@ -159,6 +168,26 @@ parsed = ScoutApmMcp::Helpers.parse_scout_url(url)
|
|
|
159
168
|
# => { app_id: 123, endpoint_id: "...", trace_id: 456, decoded_endpoint: "...", query_params: {...} }
|
|
160
169
|
```
|
|
161
170
|
|
|
171
|
+
### Time and Duration Helpers
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
# Format Time object to ISO 8601 string
|
|
175
|
+
time_str = ScoutApmMcp::Helpers.format_time(Time.now)
|
|
176
|
+
# => "2025-11-21T12:00:00Z"
|
|
177
|
+
|
|
178
|
+
# Parse ISO 8601 string to Time object
|
|
179
|
+
time = ScoutApmMcp::Helpers.parse_time("2025-11-21T12:00:00Z")
|
|
180
|
+
# => #<Time: 2025-11-21 12:00:00 UTC>
|
|
181
|
+
|
|
182
|
+
# Create duration hash from two ISO 8601 strings
|
|
183
|
+
duration = ScoutApmMcp::Helpers.make_duration("2025-11-21T00:00:00Z", "2025-11-21T12:00:00Z")
|
|
184
|
+
# => { start: #<Time: ...>, end: #<Time: ...> }
|
|
185
|
+
|
|
186
|
+
# Extract endpoint ID from endpoint hash
|
|
187
|
+
endpoint_id = ScoutApmMcp::Helpers.get_endpoint_id(endpoint_hash)
|
|
188
|
+
# => "base64-encoded-endpoint-id"
|
|
189
|
+
```
|
|
190
|
+
|
|
162
191
|
### API Key Management
|
|
163
192
|
|
|
164
193
|
The gem supports multiple methods for API key retrieval (checked in order):
|
|
@@ -193,7 +222,7 @@ api_key = ScoutApmMcp::Helpers.get_api_key(
|
|
|
193
222
|
|
|
194
223
|
### Applications
|
|
195
224
|
|
|
196
|
-
- `list_apps` - List all applications
|
|
225
|
+
- `list_apps(active_since:)` - List all applications (optionally filtered by last reported time)
|
|
197
226
|
- `get_app(app_id)` - Get application details
|
|
198
227
|
|
|
199
228
|
### Metrics
|
|
@@ -249,11 +278,32 @@ The server will communicate via STDIN/STDOUT using the MCP protocol. Configure i
|
|
|
249
278
|
|
|
250
279
|
## Error Handling
|
|
251
280
|
|
|
252
|
-
The client
|
|
281
|
+
The client uses custom exception classes for better error handling:
|
|
282
|
+
|
|
283
|
+
- `ScoutApmMcp::Error` - Base exception class for all ScoutAPM SDK errors
|
|
284
|
+
- `ScoutApmMcp::AuthError` - Raised when authentication fails (401 Unauthorized)
|
|
285
|
+
- `ScoutApmMcp::APIError` - Raised for API errors (includes `status_code` and `response_data` attributes)
|
|
253
286
|
|
|
254
|
-
|
|
255
|
-
-
|
|
256
|
-
-
|
|
287
|
+
The client also validates input parameters and raises `ArgumentError` for:
|
|
288
|
+
- Invalid metric types (must be one of: `apdex`, `response_time`, `response_time_95th`, `errors`, `throughput`, `queue_time`)
|
|
289
|
+
- Invalid insight types (must be one of: `n_plus_one`, `memory_bloat`, `slow_query`)
|
|
290
|
+
- Invalid time ranges (from_time must be before to_time, and range cannot exceed 2 weeks)
|
|
291
|
+
- Trace queries older than 7 days (for `list_endpoint_traces`)
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
begin
|
|
295
|
+
client.get_metric(123, "invalid_metric", from: "2025-01-01T00:00:00Z", to: "2025-01-02T00:00:00Z")
|
|
296
|
+
rescue ScoutApmMcp::AuthError => e
|
|
297
|
+
puts "Authentication failed: #{e.message}"
|
|
298
|
+
rescue ScoutApmMcp::APIError => e
|
|
299
|
+
puts "API error (#{e.status_code}): #{e.message}"
|
|
300
|
+
puts "Response data: #{e.response_data}"
|
|
301
|
+
rescue ArgumentError => e
|
|
302
|
+
puts "Invalid parameter: #{e.message}"
|
|
303
|
+
rescue ScoutApmMcp::Error => e
|
|
304
|
+
puts "Error: #{e.message}"
|
|
305
|
+
end
|
|
306
|
+
```
|
|
257
307
|
|
|
258
308
|
## Development
|
|
259
309
|
|
data/lib/scout_apm_mcp/client.rb
CHANGED
|
@@ -3,6 +3,10 @@ require "net/http"
|
|
|
3
3
|
require "openssl"
|
|
4
4
|
require "json"
|
|
5
5
|
require "base64"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
require_relative "errors"
|
|
9
|
+
require_relative "version"
|
|
6
10
|
|
|
7
11
|
module ScoutApmMcp
|
|
8
12
|
# ScoutAPM API client for making authenticated requests to the ScoutAPM API
|
|
@@ -15,37 +19,63 @@ module ScoutApmMcp
|
|
|
15
19
|
class Client
|
|
16
20
|
API_BASE = "https://scoutapm.com/api/v0"
|
|
17
21
|
|
|
22
|
+
# Valid metric types
|
|
23
|
+
VALID_METRICS = %w[apdex response_time response_time_95th errors throughput queue_time].freeze
|
|
24
|
+
|
|
25
|
+
# Valid insight types
|
|
26
|
+
VALID_INSIGHTS = %w[n_plus_one memory_bloat slow_query].freeze
|
|
27
|
+
|
|
18
28
|
# @param api_key [String] ScoutAPM API key
|
|
19
29
|
# @param api_base [String] API base URL (default: https://scoutapm.com/api/v0)
|
|
20
30
|
def initialize(api_key:, api_base: API_BASE)
|
|
21
31
|
@api_key = api_key
|
|
22
32
|
@api_base = api_base
|
|
33
|
+
@user_agent = "scout-apm-mcp-rb/#{VERSION}"
|
|
23
34
|
end
|
|
24
35
|
|
|
25
36
|
# List all applications accessible with the provided API key
|
|
26
37
|
#
|
|
27
|
-
# @
|
|
28
|
-
|
|
38
|
+
# @param active_since [String, nil] ISO 8601 datetime string to filter apps active since that time (default: 30 days ago)
|
|
39
|
+
# @return [Array<Hash>] Array of application hashes
|
|
40
|
+
def list_apps(active_since: nil)
|
|
29
41
|
uri = URI("#{@api_base}/apps")
|
|
30
|
-
make_request(uri)
|
|
42
|
+
response = make_request(uri)
|
|
43
|
+
apps = response.dig("results", "apps") || []
|
|
44
|
+
|
|
45
|
+
# Filter by active_since if provided
|
|
46
|
+
if active_since
|
|
47
|
+
active_time = Helpers.parse_time(active_since)
|
|
48
|
+
apps = apps.select do |app|
|
|
49
|
+
reported_at = app["last_reported_at"]
|
|
50
|
+
if reported_at && !reported_at.empty?
|
|
51
|
+
Helpers.parse_time(reported_at) >= active_time
|
|
52
|
+
else
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
apps
|
|
31
59
|
end
|
|
32
60
|
|
|
33
61
|
# Get application details for a specific application
|
|
34
62
|
#
|
|
35
63
|
# @param app_id [Integer] ScoutAPM application ID
|
|
36
|
-
# @return [Hash]
|
|
64
|
+
# @return [Hash] Application details hash
|
|
37
65
|
def get_app(app_id)
|
|
38
66
|
uri = URI("#{@api_base}/apps/#{app_id}")
|
|
39
|
-
make_request(uri)
|
|
67
|
+
response = make_request(uri)
|
|
68
|
+
response.dig("results", "app") || {}
|
|
40
69
|
end
|
|
41
70
|
|
|
42
71
|
# List available metric types for an application
|
|
43
72
|
#
|
|
44
73
|
# @param app_id [Integer] ScoutAPM application ID
|
|
45
|
-
# @return [
|
|
74
|
+
# @return [Array<String>] Array of available metric type names
|
|
46
75
|
def list_metrics(app_id)
|
|
47
76
|
uri = URI("#{@api_base}/apps/#{app_id}/metrics")
|
|
48
|
-
make_request(uri)
|
|
77
|
+
response = make_request(uri)
|
|
78
|
+
response.dig("results", "availableMetrics") || []
|
|
49
79
|
end
|
|
50
80
|
|
|
51
81
|
# Get time-series data for a specific metric type
|
|
@@ -54,11 +84,13 @@ module ScoutApmMcp
|
|
|
54
84
|
# @param metric_type [String] Metric type (apdex, response_time, response_time_95th, errors, throughput, queue_time)
|
|
55
85
|
# @param from [String, nil] Start time in ISO 8601 format
|
|
56
86
|
# @param to [String, nil] End time in ISO 8601 format
|
|
57
|
-
# @return [Hash]
|
|
87
|
+
# @return [Hash] Hash containing metric series data
|
|
58
88
|
def get_metric(app_id, metric_type, from: nil, to: nil)
|
|
89
|
+
validate_metric_params(metric_type, from, to)
|
|
59
90
|
uri = URI("#{@api_base}/apps/#{app_id}/metrics/#{metric_type}")
|
|
60
91
|
uri.query = build_query_string(from: from, to: to)
|
|
61
|
-
make_request(uri)
|
|
92
|
+
response = make_request(uri)
|
|
93
|
+
response.dig("results", "series") || {}
|
|
62
94
|
end
|
|
63
95
|
|
|
64
96
|
# List all endpoints for an application
|
|
@@ -66,22 +98,25 @@ module ScoutApmMcp
|
|
|
66
98
|
# @param app_id [Integer] ScoutAPM application ID
|
|
67
99
|
# @param from [String, nil] Start time in ISO 8601 format
|
|
68
100
|
# @param to [String, nil] End time in ISO 8601 format
|
|
69
|
-
# @return [Hash]
|
|
101
|
+
# @return [Array<Hash>] Array of endpoint hashes
|
|
70
102
|
def list_endpoints(app_id, from: nil, to: nil)
|
|
103
|
+
validate_time_range(from, to) if from && to
|
|
71
104
|
uri = URI("#{@api_base}/apps/#{app_id}/endpoints")
|
|
72
105
|
uri.query = build_query_string(from: from, to: to)
|
|
73
|
-
make_request(uri)
|
|
106
|
+
response = make_request(uri)
|
|
107
|
+
response["results"] || []
|
|
74
108
|
end
|
|
75
109
|
|
|
76
110
|
# Get endpoint details
|
|
77
111
|
#
|
|
78
112
|
# @param app_id [Integer] ScoutAPM application ID
|
|
79
113
|
# @param endpoint_id [String] Endpoint ID (base64 URL-encoded)
|
|
80
|
-
# @return [Hash]
|
|
114
|
+
# @return [Hash] Endpoint details hash
|
|
81
115
|
def get_endpoint(app_id, endpoint_id)
|
|
82
116
|
encoded_endpoint_id = URI.encode_www_form_component(endpoint_id)
|
|
83
117
|
uri = URI("#{@api_base}/apps/#{app_id}/endpoints/#{encoded_endpoint_id}")
|
|
84
|
-
make_request(uri)
|
|
118
|
+
response = make_request(uri)
|
|
119
|
+
response.dig("results", "endpoint") || response["results"] || {}
|
|
85
120
|
end
|
|
86
121
|
|
|
87
122
|
# Get metric data for a specific endpoint
|
|
@@ -91,12 +126,15 @@ module ScoutApmMcp
|
|
|
91
126
|
# @param metric_type [String] Metric type (apdex, response_time, response_time_95th, errors, throughput, queue_time)
|
|
92
127
|
# @param from [String, nil] Start time in ISO 8601 format
|
|
93
128
|
# @param to [String, nil] End time in ISO 8601 format
|
|
94
|
-
# @return [
|
|
129
|
+
# @return [Array] Array of metric data points for the specified metric type
|
|
95
130
|
def get_endpoint_metrics(app_id, endpoint_id, metric_type, from: nil, to: nil)
|
|
131
|
+
validate_metric_params(metric_type, from, to)
|
|
96
132
|
encoded_endpoint_id = URI.encode_www_form_component(endpoint_id)
|
|
97
133
|
uri = URI("#{@api_base}/apps/#{app_id}/endpoints/#{encoded_endpoint_id}/metrics/#{metric_type}")
|
|
98
134
|
uri.query = build_query_string(from: from, to: to)
|
|
99
|
-
make_request(uri)
|
|
135
|
+
response = make_request(uri)
|
|
136
|
+
series = response.dig("results", "series") || {}
|
|
137
|
+
series[metric_type] || []
|
|
100
138
|
end
|
|
101
139
|
|
|
102
140
|
# List traces for a specific endpoint (max 100, within 7 days)
|
|
@@ -105,22 +143,33 @@ module ScoutApmMcp
|
|
|
105
143
|
# @param endpoint_id [String] Endpoint ID (base64 URL-encoded)
|
|
106
144
|
# @param from [String, nil] Start time in ISO 8601 format
|
|
107
145
|
# @param to [String, nil] End time in ISO 8601 format
|
|
108
|
-
# @return [Hash]
|
|
146
|
+
# @return [Array<Hash>] Array of trace hashes
|
|
109
147
|
def list_endpoint_traces(app_id, endpoint_id, from: nil, to: nil)
|
|
148
|
+
validate_time_range(from, to) if from && to
|
|
149
|
+
if from && to
|
|
150
|
+
# Validate that from_time is not older than 7 days
|
|
151
|
+
from_time = Helpers.parse_time(from)
|
|
152
|
+
seven_days_ago = Time.now.utc - (7 * 24 * 60 * 60)
|
|
153
|
+
if from_time < seven_days_ago
|
|
154
|
+
raise ArgumentError, "from_time cannot be older than 7 days"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
110
157
|
encoded_endpoint_id = URI.encode_www_form_component(endpoint_id)
|
|
111
158
|
uri = URI("#{@api_base}/apps/#{app_id}/endpoints/#{encoded_endpoint_id}/traces")
|
|
112
159
|
uri.query = build_query_string(from: from, to: to)
|
|
113
|
-
make_request(uri)
|
|
160
|
+
response = make_request(uri)
|
|
161
|
+
response.dig("results", "traces") || []
|
|
114
162
|
end
|
|
115
163
|
|
|
116
164
|
# Fetch detailed trace information
|
|
117
165
|
#
|
|
118
166
|
# @param app_id [Integer] ScoutAPM application ID
|
|
119
167
|
# @param trace_id [Integer] Trace identifier
|
|
120
|
-
# @return [Hash]
|
|
168
|
+
# @return [Hash] Trace details hash
|
|
121
169
|
def fetch_trace(app_id, trace_id)
|
|
122
170
|
uri = URI("#{@api_base}/apps/#{app_id}/traces/#{trace_id}")
|
|
123
|
-
make_request(uri)
|
|
171
|
+
response = make_request(uri)
|
|
172
|
+
response.dig("results", "trace") || {}
|
|
124
173
|
end
|
|
125
174
|
|
|
126
175
|
# List error groups for an application (max 100, within 30 days)
|
|
@@ -129,46 +178,51 @@ module ScoutApmMcp
|
|
|
129
178
|
# @param from [String, nil] Start time in ISO 8601 format
|
|
130
179
|
# @param to [String, nil] End time in ISO 8601 format
|
|
131
180
|
# @param endpoint [String, nil] Base64 URL-encoded endpoint filter (optional)
|
|
132
|
-
# @return [Hash]
|
|
181
|
+
# @return [Array<Hash>] Array of error group hashes
|
|
133
182
|
def list_error_groups(app_id, from: nil, to: nil, endpoint: nil)
|
|
183
|
+
validate_time_range(from, to) if from && to
|
|
134
184
|
uri = URI("#{@api_base}/apps/#{app_id}/error_groups")
|
|
135
185
|
params = {}
|
|
136
186
|
params["from"] = from if from
|
|
137
187
|
params["to"] = to if to
|
|
138
188
|
params["endpoint"] = endpoint if endpoint
|
|
139
189
|
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
140
|
-
make_request(uri)
|
|
190
|
+
response = make_request(uri)
|
|
191
|
+
response.dig("results", "error_groups") || []
|
|
141
192
|
end
|
|
142
193
|
|
|
143
194
|
# Get details for a specific error group
|
|
144
195
|
#
|
|
145
196
|
# @param app_id [Integer] ScoutAPM application ID
|
|
146
197
|
# @param error_id [Integer] Error group identifier
|
|
147
|
-
# @return [Hash]
|
|
198
|
+
# @return [Hash] Error group details hash
|
|
148
199
|
def get_error_group(app_id, error_id)
|
|
149
200
|
uri = URI("#{@api_base}/apps/#{app_id}/error_groups/#{error_id}")
|
|
150
|
-
make_request(uri)
|
|
201
|
+
response = make_request(uri)
|
|
202
|
+
response.dig("results", "error_group") || {}
|
|
151
203
|
end
|
|
152
204
|
|
|
153
205
|
# Get individual errors within an error group (max 100)
|
|
154
206
|
#
|
|
155
207
|
# @param app_id [Integer] ScoutAPM application ID
|
|
156
208
|
# @param error_id [Integer] Error group identifier
|
|
157
|
-
# @return [Hash]
|
|
209
|
+
# @return [Array<Hash>] Array of error hashes
|
|
158
210
|
def get_error_group_errors(app_id, error_id)
|
|
159
211
|
uri = URI("#{@api_base}/apps/#{app_id}/error_groups/#{error_id}/errors")
|
|
160
|
-
make_request(uri)
|
|
212
|
+
response = make_request(uri)
|
|
213
|
+
response.dig("results", "errors") || []
|
|
161
214
|
end
|
|
162
215
|
|
|
163
216
|
# Get all insight types for an application (cached for 5 minutes)
|
|
164
217
|
#
|
|
165
218
|
# @param app_id [Integer] ScoutAPM application ID
|
|
166
219
|
# @param limit [Integer, nil] Maximum number of items per insight type (default: 20)
|
|
167
|
-
# @return [Hash]
|
|
220
|
+
# @return [Hash] Hash containing all insight types
|
|
168
221
|
def get_all_insights(app_id, limit: nil)
|
|
169
222
|
uri = URI("#{@api_base}/apps/#{app_id}/insights")
|
|
170
223
|
uri.query = "limit=#{limit}" if limit
|
|
171
|
-
make_request(uri)
|
|
224
|
+
response = make_request(uri)
|
|
225
|
+
response["results"] || {}
|
|
172
226
|
end
|
|
173
227
|
|
|
174
228
|
# Get data for a specific insight type
|
|
@@ -176,11 +230,15 @@ module ScoutApmMcp
|
|
|
176
230
|
# @param app_id [Integer] ScoutAPM application ID
|
|
177
231
|
# @param insight_type [String] Insight type (n_plus_one, memory_bloat, slow_query)
|
|
178
232
|
# @param limit [Integer, nil] Maximum number of items (default: 20)
|
|
179
|
-
# @return [Hash]
|
|
233
|
+
# @return [Hash] Hash containing insight data
|
|
180
234
|
def get_insight_by_type(app_id, insight_type, limit: nil)
|
|
235
|
+
unless VALID_INSIGHTS.include?(insight_type)
|
|
236
|
+
raise ArgumentError, "Invalid insight_type. Must be one of: #{VALID_INSIGHTS.join(", ")}"
|
|
237
|
+
end
|
|
181
238
|
uri = URI("#{@api_base}/apps/#{app_id}/insights/#{insight_type}")
|
|
182
239
|
uri.query = "limit=#{limit}" if limit
|
|
183
|
-
make_request(uri)
|
|
240
|
+
response = make_request(uri)
|
|
241
|
+
response["results"] || {}
|
|
184
242
|
end
|
|
185
243
|
|
|
186
244
|
# Get historical insights data with cursor-based pagination
|
|
@@ -239,6 +297,7 @@ module ScoutApmMcp
|
|
|
239
297
|
|
|
240
298
|
request = Net::HTTP::Get.new(uri)
|
|
241
299
|
request["X-SCOUT-API"] = @api_key
|
|
300
|
+
request["User-Agent"] = @user_agent
|
|
242
301
|
request["Accept"] = "application/x-yaml, application/yaml, text/yaml, */*"
|
|
243
302
|
|
|
244
303
|
response = http.request(request)
|
|
@@ -251,14 +310,16 @@ module ScoutApmMcp
|
|
|
251
310
|
status: response.code.to_i
|
|
252
311
|
}
|
|
253
312
|
when Net::HTTPUnauthorized
|
|
254
|
-
raise "Authentication failed. Check your API key."
|
|
313
|
+
raise AuthError, "Authentication failed. Check your API key."
|
|
255
314
|
else
|
|
256
|
-
raise "API request failed: #{response.code} #{response.message}"
|
|
315
|
+
raise APIError.new("API request failed: #{response.code} #{response.message}", status_code: response.code.to_i)
|
|
257
316
|
end
|
|
258
317
|
rescue OpenSSL::SSL::SSLError => e
|
|
259
|
-
raise "SSL verification failed: #{e.message}. This may be due to system certificate configuration issues."
|
|
318
|
+
raise Error, "SSL verification failed: #{e.message}. This may be due to system certificate configuration issues."
|
|
319
|
+
rescue Error
|
|
320
|
+
raise
|
|
260
321
|
rescue => e
|
|
261
|
-
raise "Request failed: #{e.class} - #{e.message}"
|
|
322
|
+
raise Error, "Request failed: #{e.class} - #{e.message}"
|
|
262
323
|
end
|
|
263
324
|
|
|
264
325
|
private
|
|
@@ -303,24 +364,97 @@ module ScoutApmMcp
|
|
|
303
364
|
|
|
304
365
|
request = Net::HTTP::Get.new(uri)
|
|
305
366
|
request["X-SCOUT-API"] = @api_key
|
|
367
|
+
request["User-Agent"] = @user_agent
|
|
306
368
|
request["Accept"] = "application/json"
|
|
307
369
|
|
|
308
370
|
response = http.request(request)
|
|
371
|
+
response_data = handle_response_errors(response)
|
|
372
|
+
|
|
373
|
+
# Check for API-level errors in response body
|
|
374
|
+
if response_data.is_a?(Hash)
|
|
375
|
+
header = response_data["header"]
|
|
376
|
+
if header && header["status"]
|
|
377
|
+
status_code = header["status"]["code"]
|
|
378
|
+
if status_code && status_code >= 400
|
|
379
|
+
error_msg = header["status"]["message"] || "Unknown API error"
|
|
380
|
+
raise APIError.new(error_msg, status_code: status_code, response_data: response_data)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
309
384
|
|
|
385
|
+
response_data
|
|
386
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
387
|
+
raise Error, "SSL verification failed: #{e.message}. This may be due to system certificate configuration issues."
|
|
388
|
+
rescue Error
|
|
389
|
+
raise
|
|
390
|
+
rescue => e
|
|
391
|
+
raise Error, "Request failed: #{e.class} - #{e.message}"
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Handle common response errors and parse JSON
|
|
395
|
+
#
|
|
396
|
+
# @param response [Net::HTTPResponse] HTTP response object
|
|
397
|
+
# @return [Hash, Array] Parsed JSON response
|
|
398
|
+
# @raise [AuthError] When authentication fails
|
|
399
|
+
# @raise [APIError] When the API returns an error response
|
|
400
|
+
def handle_response_errors(response)
|
|
401
|
+
# Try to parse JSON response
|
|
402
|
+
begin
|
|
403
|
+
data = JSON.parse(response.body)
|
|
404
|
+
rescue JSON::ParserError
|
|
405
|
+
raise APIError.new("Invalid JSON response: #{response.body}", status_code: response.code.to_i)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Check for HTTP-level errors
|
|
310
409
|
case response
|
|
311
410
|
when Net::HTTPSuccess
|
|
312
|
-
|
|
411
|
+
data
|
|
313
412
|
when Net::HTTPUnauthorized
|
|
314
|
-
raise "Authentication failed
|
|
413
|
+
raise AuthError, "Authentication failed - check your API key"
|
|
315
414
|
when Net::HTTPNotFound
|
|
316
|
-
raise "Resource not found
|
|
415
|
+
raise APIError.new("Resource not found", status_code: 404, response_data: data)
|
|
317
416
|
else
|
|
318
|
-
|
|
417
|
+
error_msg = "API request failed"
|
|
418
|
+
if data.is_a?(Hash) && data.dig("header", "status", "message")
|
|
419
|
+
error_msg = data.dig("header", "status", "message")
|
|
420
|
+
end
|
|
421
|
+
raise APIError.new(error_msg, status_code: response.code.to_i, response_data: data)
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Validate metric parameters
|
|
426
|
+
#
|
|
427
|
+
# @param metric_type [String] Metric type to validate
|
|
428
|
+
# @param from [String, nil] Start time in ISO 8601 format
|
|
429
|
+
# @param to [String, nil] End time in ISO 8601 format
|
|
430
|
+
# @raise [ArgumentError] If validation fails
|
|
431
|
+
def validate_metric_params(metric_type, from, to)
|
|
432
|
+
unless VALID_METRICS.include?(metric_type)
|
|
433
|
+
raise ArgumentError, "Invalid metric_type. Must be one of: #{VALID_METRICS.join(", ")}"
|
|
434
|
+
end
|
|
435
|
+
validate_time_range(from, to) if from && to
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Validate time ranges
|
|
439
|
+
#
|
|
440
|
+
# @param from [String, nil] Start time in ISO 8601 format
|
|
441
|
+
# @param to [String, nil] End time in ISO 8601 format
|
|
442
|
+
# @raise [ArgumentError] If validation fails
|
|
443
|
+
def validate_time_range(from, to)
|
|
444
|
+
return unless from && to
|
|
445
|
+
|
|
446
|
+
from_time = Helpers.parse_time(from)
|
|
447
|
+
to_time = Helpers.parse_time(to)
|
|
448
|
+
|
|
449
|
+
if from_time >= to_time
|
|
450
|
+
raise ArgumentError, "from_time must be before to_time"
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Validate time range (2 week maximum)
|
|
454
|
+
max_duration = 14 * 24 * 60 * 60 # 14 days in seconds
|
|
455
|
+
if (to_time - from_time) > max_duration
|
|
456
|
+
raise ArgumentError, "Time range cannot exceed 2 weeks"
|
|
319
457
|
end
|
|
320
|
-
rescue OpenSSL::SSL::SSLError => e
|
|
321
|
-
raise "SSL verification failed: #{e.message}. This may be due to system certificate configuration issues."
|
|
322
|
-
rescue => e
|
|
323
|
-
raise "Request failed: #{e.class} - #{e.message}"
|
|
324
458
|
end
|
|
325
459
|
end
|
|
326
460
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module ScoutApmMcp
|
|
2
|
+
# Base exception for Scout APM SDK errors
|
|
3
|
+
class Error < StandardError; end
|
|
4
|
+
|
|
5
|
+
# Raised when authentication fails
|
|
6
|
+
class AuthError < Error; end
|
|
7
|
+
|
|
8
|
+
# Raised when the API returns an error response
|
|
9
|
+
class APIError < Error
|
|
10
|
+
attr_reader :status_code, :response_data
|
|
11
|
+
|
|
12
|
+
def initialize(message, status_code: nil, response_data: nil)
|
|
13
|
+
super(message)
|
|
14
|
+
@status_code = status_code
|
|
15
|
+
@response_data = response_data
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
require "uri"
|
|
2
2
|
require "base64"
|
|
3
|
+
require "time"
|
|
3
4
|
|
|
4
5
|
module ScoutApmMcp
|
|
5
|
-
# Helper module for API key management and
|
|
6
|
+
# Helper module for API key management, URL parsing, and time utilities
|
|
6
7
|
module Helpers
|
|
7
8
|
# Get API key from environment or 1Password
|
|
8
9
|
#
|
|
@@ -151,5 +152,53 @@ module ScoutApmMcp
|
|
|
151
152
|
# If decoding raises an exception, return original string
|
|
152
153
|
endpoint_id.dup.force_encoding(Encoding::UTF_8)
|
|
153
154
|
end
|
|
155
|
+
|
|
156
|
+
# Get a unique identifier for an endpoint from an endpoint dictionary
|
|
157
|
+
#
|
|
158
|
+
# This is provided by the API implicitly in the 'link' field.
|
|
159
|
+
#
|
|
160
|
+
# @param endpoint [Hash] Endpoint dictionary from the API
|
|
161
|
+
# @return [String] Endpoint ID extracted from the link field, or empty string if not found
|
|
162
|
+
def self.get_endpoint_id(endpoint)
|
|
163
|
+
link = endpoint["link"] || endpoint[:link] || ""
|
|
164
|
+
return "" if link.empty?
|
|
165
|
+
|
|
166
|
+
# Extract the endpoint ID from the link (last path segment)
|
|
167
|
+
link.split("/").last || ""
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Format datetime to ISO 8601 string for API
|
|
171
|
+
#
|
|
172
|
+
# Relies on UTC timezone. Converts the time to UTC if it's not already.
|
|
173
|
+
#
|
|
174
|
+
# @param time [Time] Time object to format
|
|
175
|
+
# @return [String] ISO 8601 formatted time string (e.g., "2025-01-01T00:00:00Z")
|
|
176
|
+
def self.format_time(time)
|
|
177
|
+
time.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Parse ISO 8601 time string to Time object
|
|
181
|
+
#
|
|
182
|
+
# Handles both 'Z' suffix and timezone offsets.
|
|
183
|
+
#
|
|
184
|
+
# @param time_str [String] ISO 8601 time string (e.g., "2025-01-01T00:00:00Z")
|
|
185
|
+
# @return [Time] Time object in UTC
|
|
186
|
+
def self.parse_time(time_str)
|
|
187
|
+
# Replace Z with +00:00 for Ruby's Time parser
|
|
188
|
+
normalized = time_str.sub(/Z\z/i, "+00:00")
|
|
189
|
+
Time.parse(normalized).utc
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Create a Duration object from ISO 8601 strings
|
|
193
|
+
#
|
|
194
|
+
# @param from_str [String] Start time in ISO 8601 format
|
|
195
|
+
# @param to_str [String] End time in ISO 8601 format
|
|
196
|
+
# @return [Hash] Hash with :start and :end Time objects
|
|
197
|
+
def self.make_duration(from_str, to_str)
|
|
198
|
+
{
|
|
199
|
+
start: parse_time(from_str),
|
|
200
|
+
end: parse_time(to_str)
|
|
201
|
+
}
|
|
202
|
+
end
|
|
154
203
|
end
|
|
155
204
|
end
|
data/lib/scout_apm_mcp/server.rb
CHANGED
|
@@ -166,14 +166,14 @@ module ScoutApmMcp
|
|
|
166
166
|
|
|
167
167
|
# Applications Tools
|
|
168
168
|
class ListAppsTool < BaseTool
|
|
169
|
-
description "List all applications accessible with the provided API key"
|
|
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."
|
|
170
170
|
|
|
171
171
|
arguments do
|
|
172
|
-
|
|
172
|
+
optional(:active_since).maybe(:string).description("ISO 8601 datetime string to filter apps active since that time")
|
|
173
173
|
end
|
|
174
174
|
|
|
175
|
-
def call
|
|
176
|
-
get_client.list_apps
|
|
175
|
+
def call(active_since: nil)
|
|
176
|
+
get_client.list_apps(active_since: active_since)
|
|
177
177
|
end
|
|
178
178
|
end
|
|
179
179
|
|
|
@@ -468,9 +468,15 @@ module ScoutApmMcp
|
|
|
468
468
|
result[:data] = {trace: trace_data}
|
|
469
469
|
|
|
470
470
|
if include_endpoint && parsed[:endpoint_id]
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
471
|
+
begin
|
|
472
|
+
endpoint_data = client.get_endpoint(parsed[:app_id], parsed[:endpoint_id])
|
|
473
|
+
result[:data][:endpoint] = endpoint_data
|
|
474
|
+
result[:data][:decoded_endpoint] = parsed[:decoded_endpoint]
|
|
475
|
+
rescue => e
|
|
476
|
+
# Endpoint fetch failed, but we still have trace data
|
|
477
|
+
result[:data][:endpoint_error] = "Failed to fetch endpoint: #{e.message}"
|
|
478
|
+
result[:data][:decoded_endpoint] = parsed[:decoded_endpoint]
|
|
479
|
+
end
|
|
474
480
|
end
|
|
475
481
|
else
|
|
476
482
|
raise "Invalid trace URL: missing app_id or trace_id"
|
data/lib/scout_apm_mcp.rb
CHANGED
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.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -233,6 +233,7 @@ files:
|
|
|
233
233
|
- bin/scout_apm_mcp
|
|
234
234
|
- lib/scout_apm_mcp.rb
|
|
235
235
|
- lib/scout_apm_mcp/client.rb
|
|
236
|
+
- lib/scout_apm_mcp/errors.rb
|
|
236
237
|
- lib/scout_apm_mcp/helpers.rb
|
|
237
238
|
- lib/scout_apm_mcp/server.rb
|
|
238
239
|
- lib/scout_apm_mcp/version.rb
|