scout_apm_mcp 0.1.1 → 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 +26 -0
- data/README.md +62 -39
- data/lib/scout_apm_mcp/client.rb +194 -36
- data/lib/scout_apm_mcp/errors.rb +18 -0
- data/lib/scout_apm_mcp/helpers.rb +90 -16
- data/lib/scout_apm_mcp/server.rb +106 -17
- 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,31 @@
|
|
|
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
|
+
|
|
20
|
+
## 0.1.2 (2025-11-21)
|
|
21
|
+
|
|
22
|
+
- Enhanced SSL certificate handling with support for `SSL_CERT_FILE` environment variable and automatic fallback to system certificates
|
|
23
|
+
- Improved error handling for SSL verification failures with clearer error messages
|
|
24
|
+
- Extended `Helpers.parse_scout_url` to support parsing multiple URL types (endpoints, error_groups, insights, apps) beyond just traces
|
|
25
|
+
- Added `FetchScoutURLTool` MCP tool for automatically detecting and fetching data from any ScoutAPM URL
|
|
26
|
+
- Fixed MCP error handling to ensure error responses always have valid IDs for strict MCP client validation
|
|
27
|
+
- Improved URL parsing to return `url_type` field for better resource type detection
|
|
28
|
+
|
|
3
29
|
## 0.1.1 (2025-11-21)
|
|
4
30
|
|
|
5
31
|
- Fixed `NullLogger` missing `set_client_initialized` method that caused MCP initialization errors
|
data/README.md
CHANGED
|
@@ -24,22 +24,6 @@ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
|
|
|
24
24
|
|
|
25
25
|
For Cursor IDE, create or update `.cursor/mcp.json` in your project:
|
|
26
26
|
|
|
27
|
-
```json
|
|
28
|
-
{
|
|
29
|
-
"mcpServers": {
|
|
30
|
-
"scout-apm": {
|
|
31
|
-
"command": "bundle",
|
|
32
|
-
"args": ["exec", "scout_apm_mcp"],
|
|
33
|
-
"env": {
|
|
34
|
-
"OP_ENV_ENTRY_PATH": "op://Vault Name/Item Name"
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Or if installed globally:
|
|
42
|
-
|
|
43
27
|
```json
|
|
44
28
|
{
|
|
45
29
|
"mcpServers": {
|
|
@@ -64,21 +48,10 @@ For Claude Desktop, edit the MCP configuration file:
|
|
|
64
48
|
{
|
|
65
49
|
"mcpServers": {
|
|
66
50
|
"scout-apm": {
|
|
67
|
-
"command": "
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Or if installed globally:
|
|
76
|
-
|
|
77
|
-
```json
|
|
78
|
-
{
|
|
79
|
-
"mcpServers": {
|
|
80
|
-
"scout-apm": {
|
|
81
|
-
"command": "scout_apm_mcp"
|
|
51
|
+
"command": "scout_apm_mcp",
|
|
52
|
+
"env": {
|
|
53
|
+
"OP_ENV_ENTRY_PATH": "op://Vault Name/Item Name"
|
|
54
|
+
}
|
|
82
55
|
}
|
|
83
56
|
}
|
|
84
57
|
}
|
|
@@ -140,12 +113,18 @@ The server will start and communicate via STDIN/STDOUT using the MCP protocol. M
|
|
|
140
113
|
- **MCP Server Integration**: Ready-to-use MCP server compatible with Cursor IDE, Claude Desktop, and other MCP-enabled tools
|
|
141
114
|
- **API Key Management**: Supports environment variables and 1Password integration (via optional `opdotenv` gem)
|
|
142
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
|
|
143
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
|
|
144
121
|
|
|
145
122
|
## Basic Usage
|
|
146
123
|
|
|
147
124
|
### API Client
|
|
148
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
|
+
|
|
149
128
|
```ruby
|
|
150
129
|
require "scout_apm_mcp"
|
|
151
130
|
|
|
@@ -155,10 +134,13 @@ api_key = ScoutApmMcp::Helpers.get_api_key
|
|
|
155
134
|
# Create client
|
|
156
135
|
client = ScoutApmMcp::Client.new(api_key: api_key)
|
|
157
136
|
|
|
158
|
-
# List applications
|
|
137
|
+
# List applications (returns Array<Hash>)
|
|
159
138
|
apps = client.list_apps
|
|
160
139
|
|
|
161
|
-
#
|
|
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)
|
|
162
144
|
app = client.get_app(123)
|
|
163
145
|
|
|
164
146
|
# List endpoints
|
|
@@ -167,7 +149,7 @@ endpoints = client.list_endpoints(123)
|
|
|
167
149
|
# Fetch trace
|
|
168
150
|
trace = client.fetch_trace(123, 456)
|
|
169
151
|
|
|
170
|
-
# Get metrics
|
|
152
|
+
# Get metrics (returns Hash with series data)
|
|
171
153
|
metrics = client.get_metric(123, "response_time", from: "2025-01-01T00:00:00Z", to: "2025-01-02T00:00:00Z")
|
|
172
154
|
|
|
173
155
|
# List error groups
|
|
@@ -186,6 +168,26 @@ parsed = ScoutApmMcp::Helpers.parse_scout_url(url)
|
|
|
186
168
|
# => { app_id: 123, endpoint_id: "...", trace_id: 456, decoded_endpoint: "...", query_params: {...} }
|
|
187
169
|
```
|
|
188
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
|
+
|
|
189
191
|
### API Key Management
|
|
190
192
|
|
|
191
193
|
The gem supports multiple methods for API key retrieval (checked in order):
|
|
@@ -220,7 +222,7 @@ api_key = ScoutApmMcp::Helpers.get_api_key(
|
|
|
220
222
|
|
|
221
223
|
### Applications
|
|
222
224
|
|
|
223
|
-
- `list_apps` - List all applications
|
|
225
|
+
- `list_apps(active_since:)` - List all applications (optionally filtered by last reported time)
|
|
224
226
|
- `get_app(app_id)` - Get application details
|
|
225
227
|
|
|
226
228
|
### Metrics
|
|
@@ -276,11 +278,32 @@ The server will communicate via STDIN/STDOUT using the MCP protocol. Configure i
|
|
|
276
278
|
|
|
277
279
|
## Error Handling
|
|
278
280
|
|
|
279
|
-
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)
|
|
286
|
+
|
|
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`)
|
|
280
292
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
```
|
|
284
307
|
|
|
285
308
|
## Development
|
|
286
309
|
|
data/lib/scout_apm_mcp/client.rb
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
require "uri"
|
|
2
2
|
require "net/http"
|
|
3
|
+
require "openssl"
|
|
3
4
|
require "json"
|
|
4
5
|
require "base64"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
require_relative "errors"
|
|
9
|
+
require_relative "version"
|
|
5
10
|
|
|
6
11
|
module ScoutApmMcp
|
|
7
12
|
# ScoutAPM API client for making authenticated requests to the ScoutAPM API
|
|
@@ -14,37 +19,63 @@ module ScoutApmMcp
|
|
|
14
19
|
class Client
|
|
15
20
|
API_BASE = "https://scoutapm.com/api/v0"
|
|
16
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
|
+
|
|
17
28
|
# @param api_key [String] ScoutAPM API key
|
|
18
29
|
# @param api_base [String] API base URL (default: https://scoutapm.com/api/v0)
|
|
19
30
|
def initialize(api_key:, api_base: API_BASE)
|
|
20
31
|
@api_key = api_key
|
|
21
32
|
@api_base = api_base
|
|
33
|
+
@user_agent = "scout-apm-mcp-rb/#{VERSION}"
|
|
22
34
|
end
|
|
23
35
|
|
|
24
36
|
# List all applications accessible with the provided API key
|
|
25
37
|
#
|
|
26
|
-
# @
|
|
27
|
-
|
|
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)
|
|
28
41
|
uri = URI("#{@api_base}/apps")
|
|
29
|
-
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
|
|
30
59
|
end
|
|
31
60
|
|
|
32
61
|
# Get application details for a specific application
|
|
33
62
|
#
|
|
34
63
|
# @param app_id [Integer] ScoutAPM application ID
|
|
35
|
-
# @return [Hash]
|
|
64
|
+
# @return [Hash] Application details hash
|
|
36
65
|
def get_app(app_id)
|
|
37
66
|
uri = URI("#{@api_base}/apps/#{app_id}")
|
|
38
|
-
make_request(uri)
|
|
67
|
+
response = make_request(uri)
|
|
68
|
+
response.dig("results", "app") || {}
|
|
39
69
|
end
|
|
40
70
|
|
|
41
71
|
# List available metric types for an application
|
|
42
72
|
#
|
|
43
73
|
# @param app_id [Integer] ScoutAPM application ID
|
|
44
|
-
# @return [
|
|
74
|
+
# @return [Array<String>] Array of available metric type names
|
|
45
75
|
def list_metrics(app_id)
|
|
46
76
|
uri = URI("#{@api_base}/apps/#{app_id}/metrics")
|
|
47
|
-
make_request(uri)
|
|
77
|
+
response = make_request(uri)
|
|
78
|
+
response.dig("results", "availableMetrics") || []
|
|
48
79
|
end
|
|
49
80
|
|
|
50
81
|
# Get time-series data for a specific metric type
|
|
@@ -53,11 +84,13 @@ module ScoutApmMcp
|
|
|
53
84
|
# @param metric_type [String] Metric type (apdex, response_time, response_time_95th, errors, throughput, queue_time)
|
|
54
85
|
# @param from [String, nil] Start time in ISO 8601 format
|
|
55
86
|
# @param to [String, nil] End time in ISO 8601 format
|
|
56
|
-
# @return [Hash]
|
|
87
|
+
# @return [Hash] Hash containing metric series data
|
|
57
88
|
def get_metric(app_id, metric_type, from: nil, to: nil)
|
|
89
|
+
validate_metric_params(metric_type, from, to)
|
|
58
90
|
uri = URI("#{@api_base}/apps/#{app_id}/metrics/#{metric_type}")
|
|
59
91
|
uri.query = build_query_string(from: from, to: to)
|
|
60
|
-
make_request(uri)
|
|
92
|
+
response = make_request(uri)
|
|
93
|
+
response.dig("results", "series") || {}
|
|
61
94
|
end
|
|
62
95
|
|
|
63
96
|
# List all endpoints for an application
|
|
@@ -65,22 +98,25 @@ module ScoutApmMcp
|
|
|
65
98
|
# @param app_id [Integer] ScoutAPM application ID
|
|
66
99
|
# @param from [String, nil] Start time in ISO 8601 format
|
|
67
100
|
# @param to [String, nil] End time in ISO 8601 format
|
|
68
|
-
# @return [Hash]
|
|
101
|
+
# @return [Array<Hash>] Array of endpoint hashes
|
|
69
102
|
def list_endpoints(app_id, from: nil, to: nil)
|
|
103
|
+
validate_time_range(from, to) if from && to
|
|
70
104
|
uri = URI("#{@api_base}/apps/#{app_id}/endpoints")
|
|
71
105
|
uri.query = build_query_string(from: from, to: to)
|
|
72
|
-
make_request(uri)
|
|
106
|
+
response = make_request(uri)
|
|
107
|
+
response["results"] || []
|
|
73
108
|
end
|
|
74
109
|
|
|
75
110
|
# Get endpoint details
|
|
76
111
|
#
|
|
77
112
|
# @param app_id [Integer] ScoutAPM application ID
|
|
78
113
|
# @param endpoint_id [String] Endpoint ID (base64 URL-encoded)
|
|
79
|
-
# @return [Hash]
|
|
114
|
+
# @return [Hash] Endpoint details hash
|
|
80
115
|
def get_endpoint(app_id, endpoint_id)
|
|
81
116
|
encoded_endpoint_id = URI.encode_www_form_component(endpoint_id)
|
|
82
117
|
uri = URI("#{@api_base}/apps/#{app_id}/endpoints/#{encoded_endpoint_id}")
|
|
83
|
-
make_request(uri)
|
|
118
|
+
response = make_request(uri)
|
|
119
|
+
response.dig("results", "endpoint") || response["results"] || {}
|
|
84
120
|
end
|
|
85
121
|
|
|
86
122
|
# Get metric data for a specific endpoint
|
|
@@ -90,12 +126,15 @@ module ScoutApmMcp
|
|
|
90
126
|
# @param metric_type [String] Metric type (apdex, response_time, response_time_95th, errors, throughput, queue_time)
|
|
91
127
|
# @param from [String, nil] Start time in ISO 8601 format
|
|
92
128
|
# @param to [String, nil] End time in ISO 8601 format
|
|
93
|
-
# @return [
|
|
129
|
+
# @return [Array] Array of metric data points for the specified metric type
|
|
94
130
|
def get_endpoint_metrics(app_id, endpoint_id, metric_type, from: nil, to: nil)
|
|
131
|
+
validate_metric_params(metric_type, from, to)
|
|
95
132
|
encoded_endpoint_id = URI.encode_www_form_component(endpoint_id)
|
|
96
133
|
uri = URI("#{@api_base}/apps/#{app_id}/endpoints/#{encoded_endpoint_id}/metrics/#{metric_type}")
|
|
97
134
|
uri.query = build_query_string(from: from, to: to)
|
|
98
|
-
make_request(uri)
|
|
135
|
+
response = make_request(uri)
|
|
136
|
+
series = response.dig("results", "series") || {}
|
|
137
|
+
series[metric_type] || []
|
|
99
138
|
end
|
|
100
139
|
|
|
101
140
|
# List traces for a specific endpoint (max 100, within 7 days)
|
|
@@ -104,22 +143,33 @@ module ScoutApmMcp
|
|
|
104
143
|
# @param endpoint_id [String] Endpoint ID (base64 URL-encoded)
|
|
105
144
|
# @param from [String, nil] Start time in ISO 8601 format
|
|
106
145
|
# @param to [String, nil] End time in ISO 8601 format
|
|
107
|
-
# @return [Hash]
|
|
146
|
+
# @return [Array<Hash>] Array of trace hashes
|
|
108
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
|
|
109
157
|
encoded_endpoint_id = URI.encode_www_form_component(endpoint_id)
|
|
110
158
|
uri = URI("#{@api_base}/apps/#{app_id}/endpoints/#{encoded_endpoint_id}/traces")
|
|
111
159
|
uri.query = build_query_string(from: from, to: to)
|
|
112
|
-
make_request(uri)
|
|
160
|
+
response = make_request(uri)
|
|
161
|
+
response.dig("results", "traces") || []
|
|
113
162
|
end
|
|
114
163
|
|
|
115
164
|
# Fetch detailed trace information
|
|
116
165
|
#
|
|
117
166
|
# @param app_id [Integer] ScoutAPM application ID
|
|
118
167
|
# @param trace_id [Integer] Trace identifier
|
|
119
|
-
# @return [Hash]
|
|
168
|
+
# @return [Hash] Trace details hash
|
|
120
169
|
def fetch_trace(app_id, trace_id)
|
|
121
170
|
uri = URI("#{@api_base}/apps/#{app_id}/traces/#{trace_id}")
|
|
122
|
-
make_request(uri)
|
|
171
|
+
response = make_request(uri)
|
|
172
|
+
response.dig("results", "trace") || {}
|
|
123
173
|
end
|
|
124
174
|
|
|
125
175
|
# List error groups for an application (max 100, within 30 days)
|
|
@@ -128,46 +178,51 @@ module ScoutApmMcp
|
|
|
128
178
|
# @param from [String, nil] Start time in ISO 8601 format
|
|
129
179
|
# @param to [String, nil] End time in ISO 8601 format
|
|
130
180
|
# @param endpoint [String, nil] Base64 URL-encoded endpoint filter (optional)
|
|
131
|
-
# @return [Hash]
|
|
181
|
+
# @return [Array<Hash>] Array of error group hashes
|
|
132
182
|
def list_error_groups(app_id, from: nil, to: nil, endpoint: nil)
|
|
183
|
+
validate_time_range(from, to) if from && to
|
|
133
184
|
uri = URI("#{@api_base}/apps/#{app_id}/error_groups")
|
|
134
185
|
params = {}
|
|
135
186
|
params["from"] = from if from
|
|
136
187
|
params["to"] = to if to
|
|
137
188
|
params["endpoint"] = endpoint if endpoint
|
|
138
189
|
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
139
|
-
make_request(uri)
|
|
190
|
+
response = make_request(uri)
|
|
191
|
+
response.dig("results", "error_groups") || []
|
|
140
192
|
end
|
|
141
193
|
|
|
142
194
|
# Get details for a specific error group
|
|
143
195
|
#
|
|
144
196
|
# @param app_id [Integer] ScoutAPM application ID
|
|
145
197
|
# @param error_id [Integer] Error group identifier
|
|
146
|
-
# @return [Hash]
|
|
198
|
+
# @return [Hash] Error group details hash
|
|
147
199
|
def get_error_group(app_id, error_id)
|
|
148
200
|
uri = URI("#{@api_base}/apps/#{app_id}/error_groups/#{error_id}")
|
|
149
|
-
make_request(uri)
|
|
201
|
+
response = make_request(uri)
|
|
202
|
+
response.dig("results", "error_group") || {}
|
|
150
203
|
end
|
|
151
204
|
|
|
152
205
|
# Get individual errors within an error group (max 100)
|
|
153
206
|
#
|
|
154
207
|
# @param app_id [Integer] ScoutAPM application ID
|
|
155
208
|
# @param error_id [Integer] Error group identifier
|
|
156
|
-
# @return [Hash]
|
|
209
|
+
# @return [Array<Hash>] Array of error hashes
|
|
157
210
|
def get_error_group_errors(app_id, error_id)
|
|
158
211
|
uri = URI("#{@api_base}/apps/#{app_id}/error_groups/#{error_id}/errors")
|
|
159
|
-
make_request(uri)
|
|
212
|
+
response = make_request(uri)
|
|
213
|
+
response.dig("results", "errors") || []
|
|
160
214
|
end
|
|
161
215
|
|
|
162
216
|
# Get all insight types for an application (cached for 5 minutes)
|
|
163
217
|
#
|
|
164
218
|
# @param app_id [Integer] ScoutAPM application ID
|
|
165
219
|
# @param limit [Integer, nil] Maximum number of items per insight type (default: 20)
|
|
166
|
-
# @return [Hash]
|
|
220
|
+
# @return [Hash] Hash containing all insight types
|
|
167
221
|
def get_all_insights(app_id, limit: nil)
|
|
168
222
|
uri = URI("#{@api_base}/apps/#{app_id}/insights")
|
|
169
223
|
uri.query = "limit=#{limit}" if limit
|
|
170
|
-
make_request(uri)
|
|
224
|
+
response = make_request(uri)
|
|
225
|
+
response["results"] || {}
|
|
171
226
|
end
|
|
172
227
|
|
|
173
228
|
# Get data for a specific insight type
|
|
@@ -175,11 +230,15 @@ module ScoutApmMcp
|
|
|
175
230
|
# @param app_id [Integer] ScoutAPM application ID
|
|
176
231
|
# @param insight_type [String] Insight type (n_plus_one, memory_bloat, slow_query)
|
|
177
232
|
# @param limit [Integer, nil] Maximum number of items (default: 20)
|
|
178
|
-
# @return [Hash]
|
|
233
|
+
# @return [Hash] Hash containing insight data
|
|
179
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
|
|
180
238
|
uri = URI("#{@api_base}/apps/#{app_id}/insights/#{insight_type}")
|
|
181
239
|
uri.query = "limit=#{limit}" if limit
|
|
182
|
-
make_request(uri)
|
|
240
|
+
response = make_request(uri)
|
|
241
|
+
response["results"] || {}
|
|
183
242
|
end
|
|
184
243
|
|
|
185
244
|
# Get historical insights data with cursor-based pagination
|
|
@@ -238,6 +297,7 @@ module ScoutApmMcp
|
|
|
238
297
|
|
|
239
298
|
request = Net::HTTP::Get.new(uri)
|
|
240
299
|
request["X-SCOUT-API"] = @api_key
|
|
300
|
+
request["User-Agent"] = @user_agent
|
|
241
301
|
request["Accept"] = "application/x-yaml, application/yaml, text/yaml, */*"
|
|
242
302
|
|
|
243
303
|
response = http.request(request)
|
|
@@ -250,10 +310,16 @@ module ScoutApmMcp
|
|
|
250
310
|
status: response.code.to_i
|
|
251
311
|
}
|
|
252
312
|
when Net::HTTPUnauthorized
|
|
253
|
-
raise "Authentication failed. Check your API key."
|
|
313
|
+
raise AuthError, "Authentication failed. Check your API key."
|
|
254
314
|
else
|
|
255
|
-
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)
|
|
256
316
|
end
|
|
317
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
318
|
+
raise Error, "SSL verification failed: #{e.message}. This may be due to system certificate configuration issues."
|
|
319
|
+
rescue Error
|
|
320
|
+
raise
|
|
321
|
+
rescue => e
|
|
322
|
+
raise Error, "Request failed: #{e.class} - #{e.message}"
|
|
257
323
|
end
|
|
258
324
|
|
|
259
325
|
private
|
|
@@ -264,9 +330,24 @@ module ScoutApmMcp
|
|
|
264
330
|
# @return [Net::HTTP] Configured HTTP client
|
|
265
331
|
def build_http_client(uri)
|
|
266
332
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
267
|
-
http.use_ssl = uri.scheme == "https"
|
|
268
333
|
http.read_timeout = 10
|
|
269
334
|
http.open_timeout = 10
|
|
335
|
+
|
|
336
|
+
if uri.scheme == "https"
|
|
337
|
+
http.use_ssl = true
|
|
338
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
339
|
+
|
|
340
|
+
# Set ca_file directly - this is the simplest and most reliable approach
|
|
341
|
+
# Try SSL_CERT_FILE first, then default cert file
|
|
342
|
+
ca_file = if ENV["SSL_CERT_FILE"] && File.file?(ENV["SSL_CERT_FILE"])
|
|
343
|
+
ENV["SSL_CERT_FILE"]
|
|
344
|
+
elsif File.exist?(OpenSSL::X509::DEFAULT_CERT_FILE)
|
|
345
|
+
OpenSSL::X509::DEFAULT_CERT_FILE
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
http.ca_file = ca_file if ca_file
|
|
349
|
+
end
|
|
350
|
+
|
|
270
351
|
http
|
|
271
352
|
end
|
|
272
353
|
|
|
@@ -283,19 +364,96 @@ module ScoutApmMcp
|
|
|
283
364
|
|
|
284
365
|
request = Net::HTTP::Get.new(uri)
|
|
285
366
|
request["X-SCOUT-API"] = @api_key
|
|
367
|
+
request["User-Agent"] = @user_agent
|
|
286
368
|
request["Accept"] = "application/json"
|
|
287
369
|
|
|
288
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
|
|
289
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
|
|
290
409
|
case response
|
|
291
410
|
when Net::HTTPSuccess
|
|
292
|
-
|
|
411
|
+
data
|
|
293
412
|
when Net::HTTPUnauthorized
|
|
294
|
-
raise "Authentication failed
|
|
413
|
+
raise AuthError, "Authentication failed - check your API key"
|
|
295
414
|
when Net::HTTPNotFound
|
|
296
|
-
raise "Resource not found
|
|
415
|
+
raise APIError.new("Resource not found", status_code: 404, response_data: data)
|
|
297
416
|
else
|
|
298
|
-
|
|
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"
|
|
299
457
|
end
|
|
300
458
|
end
|
|
301
459
|
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
|
#
|
|
@@ -63,30 +64,55 @@ module ScoutApmMcp
|
|
|
63
64
|
"or provide OP_ENV_ENTRY_PATH, or op_vault and op_item parameters for 1Password integration"
|
|
64
65
|
end
|
|
65
66
|
|
|
66
|
-
# Parse a ScoutAPM
|
|
67
|
+
# Parse a ScoutAPM URL and extract resource information
|
|
67
68
|
#
|
|
68
|
-
# @param url [String] Full ScoutAPM
|
|
69
|
-
# @return [Hash] Hash containing
|
|
69
|
+
# @param url [String] Full ScoutAPM URL
|
|
70
|
+
# @return [Hash] Hash containing resource type and extracted IDs
|
|
71
|
+
# Possible keys: :url_type, :app_id, :endpoint_id, :trace_id, :error_id, :insight_type,
|
|
72
|
+
# :query_params, :decoded_endpoint
|
|
70
73
|
def self.parse_scout_url(url)
|
|
71
74
|
uri = URI.parse(url)
|
|
72
75
|
path_parts = uri.path.split("/").reject(&:empty?)
|
|
73
76
|
|
|
74
|
-
|
|
77
|
+
result = {}
|
|
75
78
|
app_index = path_parts.index("apps")
|
|
76
|
-
endpoints_index = path_parts.index("endpoints")
|
|
77
|
-
trace_index = path_parts.index("trace")
|
|
78
79
|
|
|
79
|
-
result
|
|
80
|
+
return result unless app_index
|
|
81
|
+
|
|
82
|
+
result[:app_id] = path_parts[app_index + 1].to_i
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
result[:
|
|
84
|
+
# Detect URL type and extract IDs
|
|
85
|
+
# Pattern: /apps/{app_id}/endpoints/{endpoint_id}/trace/{trace_id}
|
|
86
|
+
if path_parts.include?("trace")
|
|
87
|
+
result[:url_type] = :trace
|
|
88
|
+
endpoints_index = path_parts.index("endpoints")
|
|
89
|
+
trace_index = path_parts.index("trace")
|
|
90
|
+
if endpoints_index && trace_index
|
|
91
|
+
result[:endpoint_id] = path_parts[endpoints_index + 1]
|
|
92
|
+
result[:trace_id] = path_parts[trace_index + 1].to_i
|
|
93
|
+
end
|
|
94
|
+
# Pattern: /apps/{app_id}/endpoints/{endpoint_id}
|
|
95
|
+
elsif path_parts.include?("endpoints")
|
|
96
|
+
result[:url_type] = :endpoint
|
|
97
|
+
endpoints_index = path_parts.index("endpoints")
|
|
98
|
+
result[:endpoint_id] = path_parts[endpoints_index + 1] if endpoints_index
|
|
99
|
+
# Pattern: /apps/{app_id}/error_groups/{error_id}
|
|
100
|
+
elsif path_parts.include?("error_groups")
|
|
101
|
+
result[:url_type] = :error_group
|
|
102
|
+
error_groups_index = path_parts.index("error_groups")
|
|
103
|
+
result[:error_id] = path_parts[error_groups_index + 1].to_i if error_groups_index
|
|
104
|
+
# Pattern: /apps/{app_id}/insights or /apps/{app_id}/insights/{insight_type}
|
|
105
|
+
elsif path_parts.include?("insights")
|
|
106
|
+
result[:url_type] = :insight
|
|
107
|
+
insights_index = path_parts.index("insights")
|
|
108
|
+
if insights_index && path_parts.length > insights_index + 1
|
|
109
|
+
result[:insight_type] = path_parts[insights_index + 1]
|
|
110
|
+
end
|
|
111
|
+
# Pattern: /apps/{app_id}
|
|
112
|
+
elsif path_parts.length == 2 && path_parts[0] == "apps"
|
|
113
|
+
result[:url_type] = :app
|
|
85
114
|
else
|
|
86
|
-
|
|
87
|
-
result[:app_id] = path_parts[1].to_i if path_parts[0] == "apps"
|
|
88
|
-
result[:endpoint_id] = path_parts[3] if path_parts[2] == "endpoints"
|
|
89
|
-
result[:trace_id] = path_parts[5].to_i if path_parts[4] == "trace"
|
|
115
|
+
result[:url_type] = :unknown
|
|
90
116
|
end
|
|
91
117
|
|
|
92
118
|
# Parse query parameters
|
|
@@ -126,5 +152,53 @@ module ScoutApmMcp
|
|
|
126
152
|
# If decoding raises an exception, return original string
|
|
127
153
|
endpoint_id.dup.force_encoding(Encoding::UTF_8)
|
|
128
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
|
|
129
203
|
end
|
|
130
204
|
end
|
data/lib/scout_apm_mcp/server.rb
CHANGED
|
@@ -16,6 +16,21 @@ FastMcp = MCP unless defined?(FastMcp)
|
|
|
16
16
|
module MCP
|
|
17
17
|
module Transports
|
|
18
18
|
class StdioTransport
|
|
19
|
+
if method_defined?(:send_error)
|
|
20
|
+
alias_method :original_send_error, :send_error
|
|
21
|
+
|
|
22
|
+
def send_error(code, message, id = nil)
|
|
23
|
+
# Use placeholder id if nil to satisfy strict MCP client validation
|
|
24
|
+
# JSON-RPC 2.0 allows null for notifications, but MCP clients require valid id
|
|
25
|
+
id = "error_#{SecureRandom.hex(8)}" if id.nil?
|
|
26
|
+
original_send_error(code, message, id)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Server
|
|
33
|
+
if method_defined?(:send_error)
|
|
19
34
|
alias_method :original_send_error, :send_error
|
|
20
35
|
|
|
21
36
|
def send_error(code, message, id = nil)
|
|
@@ -26,17 +41,6 @@ module MCP
|
|
|
26
41
|
end
|
|
27
42
|
end
|
|
28
43
|
end
|
|
29
|
-
|
|
30
|
-
class Server
|
|
31
|
-
alias_method :original_send_error, :send_error
|
|
32
|
-
|
|
33
|
-
def send_error(code, message, id = nil)
|
|
34
|
-
# Use placeholder id if nil to satisfy strict MCP client validation
|
|
35
|
-
# JSON-RPC 2.0 allows null for notifications, but MCP clients require valid id
|
|
36
|
-
id = "error_#{SecureRandom.hex(8)}" if id.nil?
|
|
37
|
-
original_send_error(code, message, id)
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
module ScoutApmMcp
|
|
@@ -141,6 +145,7 @@ module ScoutApmMcp
|
|
|
141
145
|
server.register_tool(GetInsightsHistoryTool)
|
|
142
146
|
server.register_tool(GetInsightsHistoryByTypeTool)
|
|
143
147
|
server.register_tool(ParseScoutURLTool)
|
|
148
|
+
server.register_tool(FetchScoutURLTool)
|
|
144
149
|
server.register_tool(FetchOpenAPISchemaTool)
|
|
145
150
|
end
|
|
146
151
|
|
|
@@ -161,14 +166,14 @@ module ScoutApmMcp
|
|
|
161
166
|
|
|
162
167
|
# Applications Tools
|
|
163
168
|
class ListAppsTool < BaseTool
|
|
164
|
-
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."
|
|
165
170
|
|
|
166
171
|
arguments do
|
|
167
|
-
|
|
172
|
+
optional(:active_since).maybe(:string).description("ISO 8601 datetime string to filter apps active since that time")
|
|
168
173
|
end
|
|
169
174
|
|
|
170
|
-
def call
|
|
171
|
-
get_client.list_apps
|
|
175
|
+
def call(active_since: nil)
|
|
176
|
+
get_client.list_apps(active_since: active_since)
|
|
172
177
|
end
|
|
173
178
|
end
|
|
174
179
|
|
|
@@ -427,10 +432,10 @@ module ScoutApmMcp
|
|
|
427
432
|
|
|
428
433
|
# Utility Tools
|
|
429
434
|
class ParseScoutURLTool < BaseTool
|
|
430
|
-
description "Parse a ScoutAPM
|
|
435
|
+
description "Parse a ScoutAPM URL and extract resource information (app_id, endpoint_id, trace_id, etc.)"
|
|
431
436
|
|
|
432
437
|
arguments do
|
|
433
|
-
required(:url).filled(:string).description("Full ScoutAPM
|
|
438
|
+
required(:url).filled(:string).description("Full ScoutAPM URL (e.g., https://scoutapm.com/apps/123/endpoints/.../trace/456)")
|
|
434
439
|
end
|
|
435
440
|
|
|
436
441
|
def call(url:)
|
|
@@ -438,6 +443,90 @@ module ScoutApmMcp
|
|
|
438
443
|
end
|
|
439
444
|
end
|
|
440
445
|
|
|
446
|
+
class FetchScoutURLTool < BaseTool
|
|
447
|
+
description "Fetch data from a ScoutAPM URL by automatically detecting the resource type and fetching the appropriate data"
|
|
448
|
+
|
|
449
|
+
arguments do
|
|
450
|
+
required(:url).filled(:string).description("Full ScoutAPM URL (e.g., https://scoutapm.com/apps/123/endpoints/.../trace/456)")
|
|
451
|
+
optional(:include_endpoint).filled(:bool).description("For trace URLs, also fetch endpoint details for context (default: false)")
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def call(url:, include_endpoint: false)
|
|
455
|
+
parsed = Helpers.parse_scout_url(url)
|
|
456
|
+
client = get_client
|
|
457
|
+
|
|
458
|
+
result = {
|
|
459
|
+
url: url,
|
|
460
|
+
parsed: parsed,
|
|
461
|
+
data: nil
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
case parsed[:url_type]
|
|
465
|
+
when :trace
|
|
466
|
+
if parsed[:app_id] && parsed[:trace_id]
|
|
467
|
+
trace_data = client.fetch_trace(parsed[:app_id], parsed[:trace_id])
|
|
468
|
+
result[:data] = {trace: trace_data}
|
|
469
|
+
|
|
470
|
+
if include_endpoint && parsed[:endpoint_id]
|
|
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
|
|
480
|
+
end
|
|
481
|
+
else
|
|
482
|
+
raise "Invalid trace URL: missing app_id or trace_id"
|
|
483
|
+
end
|
|
484
|
+
when :endpoint
|
|
485
|
+
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
|
+
}
|
|
491
|
+
else
|
|
492
|
+
raise "Invalid endpoint URL: missing app_id or endpoint_id"
|
|
493
|
+
end
|
|
494
|
+
when :error_group
|
|
495
|
+
if parsed[:app_id] && parsed[:error_id]
|
|
496
|
+
error_data = client.get_error_group(parsed[:app_id], parsed[:error_id])
|
|
497
|
+
result[:data] = {error_group: error_data}
|
|
498
|
+
else
|
|
499
|
+
raise "Invalid error group URL: missing app_id or error_id"
|
|
500
|
+
end
|
|
501
|
+
when :insight
|
|
502
|
+
if parsed[:app_id]
|
|
503
|
+
if parsed[:insight_type]
|
|
504
|
+
insight_data = client.get_insight_by_type(parsed[:app_id], parsed[:insight_type])
|
|
505
|
+
result[:data] = {insight: insight_data, insight_type: parsed[:insight_type]}
|
|
506
|
+
else
|
|
507
|
+
insights_data = client.get_all_insights(parsed[:app_id])
|
|
508
|
+
result[:data] = {insights: insights_data}
|
|
509
|
+
end
|
|
510
|
+
else
|
|
511
|
+
raise "Invalid insight URL: missing app_id"
|
|
512
|
+
end
|
|
513
|
+
when :app
|
|
514
|
+
if parsed[:app_id]
|
|
515
|
+
app_data = client.get_app(parsed[:app_id])
|
|
516
|
+
result[:data] = {app: app_data}
|
|
517
|
+
else
|
|
518
|
+
raise "Invalid app URL: missing app_id"
|
|
519
|
+
end
|
|
520
|
+
when :unknown
|
|
521
|
+
raise "Unknown or unsupported ScoutAPM URL format: #{url}"
|
|
522
|
+
else
|
|
523
|
+
raise "Unable to determine URL type from: #{url}"
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
result
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
441
530
|
class FetchOpenAPISchemaTool < BaseTool
|
|
442
531
|
description "Fetch the ScoutAPM OpenAPI schema from the API and optionally validate it"
|
|
443
532
|
|
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
|