scout_apm_mcp 0.1.2 → 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 +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +94 -15
- data/lib/scout_apm_mcp/client.rb +218 -59
- data/lib/scout_apm_mcp/errors.rb +18 -0
- data/lib/scout_apm_mcp/helpers.rb +104 -7
- data/lib/scout_apm_mcp/server.rb +189 -54
- data/lib/scout_apm_mcp/version.rb +1 -1
- data/lib/scout_apm_mcp.rb +1 -0
- data/sig/scout_apm_mcp.rbs +0 -1
- metadata +50 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ce6703433e4f11c4cf2769a636135ed7a429707a84a09e196e7151b43cb368b2
|
|
4
|
+
data.tar.gz: 2ddf028b1023a364f4eb5d6afc8f1693549ce4ace041f7d12487719f4513ef3d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f1ff860d8e236a1338d4f1ceb62a0a6620ff667dc266cc528a7ffd8eceaaf5f4a874612fcc7e8fe45b64a83c446d42af6eca639d13b4ee5cf71b76ce7e7b6b6
|
|
7
|
+
data.tar.gz: 3cadb0d9852c1e661630c69148cfdb2d9c85076094e4672f5b0358448015ade8d13bc800c04a67c1b105ddf66b1ca842ac056c8e38b12b158c3c6290a289daa7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
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
|
+
|
|
14
|
+
## 0.1.3 (2025-11-21)
|
|
15
|
+
|
|
16
|
+
- Custom exception classes (`ScoutApmMcp::Error`, `ScoutApmMcp::AuthError`, `ScoutApmMcp::APIError`) for better error handling
|
|
17
|
+
- Input validation for metric types (`VALID_METRICS` constant) and insight types (`VALID_INSIGHTS` constant)
|
|
18
|
+
- Time range validation (ensures from_time < to_time and range doesn't exceed 2 weeks)
|
|
19
|
+
- Trace age validation (ensures trace queries aren't older than 7 days)
|
|
20
|
+
- Client methods now return extracted data instead of full API response structure
|
|
21
|
+
- Time/Duration helpers (`Helpers.format_time`, `Helpers.parse_time`, `Helpers.make_duration`)
|
|
22
|
+
- Endpoint ID extraction helper (`Helpers.get_endpoint_id`)
|
|
23
|
+
- User-Agent header (`scout-apm-mcp-rb/VERSION`) on all API requests
|
|
24
|
+
- `active_since` parameter to `list_apps` method for filtering apps by last reported time
|
|
25
|
+
- API-level error parsing - checks for `header.status.code` in response body
|
|
26
|
+
- Error handling now uses custom exception classes instead of generic `RuntimeError`
|
|
27
|
+
- MCP `ListAppsTool` now supports optional `active_since` parameter
|
|
28
|
+
- Error responses now properly parse API-level error codes from response body
|
|
29
|
+
- Invalid metric types, insight types, and time ranges are now validated before API calls
|
|
30
|
+
|
|
3
31
|
## 0.1.2 (2025-11-21)
|
|
4
32
|
|
|
5
33
|
- Enhanced SSL certificate handling with support for `SSL_CERT_FILE` environment variable and automatic fallback to system certificates
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# scout_apm_mcp
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/rb/scout_apm_mcp) [](https://github.com/amkisko/scout_apm_mcp.rb/actions/workflows/test.yml) [](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": "
|
|
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": "
|
|
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,18 +119,40 @@ 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
|
|
113
141
|
- **MCP Server Integration**: Ready-to-use MCP server compatible with Cursor IDE, Claude Desktop, and other MCP-enabled tools
|
|
114
142
|
- **API Key Management**: Supports environment variables and 1Password integration (via optional `opdotenv` gem)
|
|
115
143
|
- **URL Parsing**: Helper methods to parse ScoutAPM URLs and extract IDs
|
|
144
|
+
- **Time Utilities**: Helper methods for formatting, parsing, and working with ISO 8601 time strings
|
|
145
|
+
- **Input Validation**: Validates metric types, insight types, and time ranges before API calls
|
|
146
|
+
- **Custom Error Handling**: Dedicated exception classes for better error handling and debugging
|
|
116
147
|
- **Comprehensive API Coverage**: Supports all ScoutAPM API endpoints (apps, metrics, endpoints, traces, errors, insights)
|
|
148
|
+
- **Data Extraction**: Client methods return extracted data instead of full API response structure for easier usage
|
|
117
149
|
|
|
118
150
|
## Basic Usage
|
|
119
151
|
|
|
120
152
|
### API Client
|
|
121
153
|
|
|
154
|
+
**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.
|
|
155
|
+
|
|
122
156
|
```ruby
|
|
123
157
|
require "scout_apm_mcp"
|
|
124
158
|
|
|
@@ -128,10 +162,13 @@ api_key = ScoutApmMcp::Helpers.get_api_key
|
|
|
128
162
|
# Create client
|
|
129
163
|
client = ScoutApmMcp::Client.new(api_key: api_key)
|
|
130
164
|
|
|
131
|
-
# List applications
|
|
165
|
+
# List applications (returns Array<Hash>)
|
|
132
166
|
apps = client.list_apps
|
|
133
167
|
|
|
134
|
-
#
|
|
168
|
+
# List applications filtered by active_since
|
|
169
|
+
apps = client.list_apps(active_since: "2025-11-01T00:00:00Z")
|
|
170
|
+
|
|
171
|
+
# Get application details (returns Hash)
|
|
135
172
|
app = client.get_app(123)
|
|
136
173
|
|
|
137
174
|
# List endpoints
|
|
@@ -140,7 +177,7 @@ endpoints = client.list_endpoints(123)
|
|
|
140
177
|
# Fetch trace
|
|
141
178
|
trace = client.fetch_trace(123, 456)
|
|
142
179
|
|
|
143
|
-
# Get metrics
|
|
180
|
+
# Get metrics (returns Hash with series data)
|
|
144
181
|
metrics = client.get_metric(123, "response_time", from: "2025-01-01T00:00:00Z", to: "2025-01-02T00:00:00Z")
|
|
145
182
|
|
|
146
183
|
# List error groups
|
|
@@ -159,6 +196,26 @@ parsed = ScoutApmMcp::Helpers.parse_scout_url(url)
|
|
|
159
196
|
# => { app_id: 123, endpoint_id: "...", trace_id: 456, decoded_endpoint: "...", query_params: {...} }
|
|
160
197
|
```
|
|
161
198
|
|
|
199
|
+
### Time and Duration Helpers
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
# Format Time object to ISO 8601 string
|
|
203
|
+
time_str = ScoutApmMcp::Helpers.format_time(Time.now)
|
|
204
|
+
# => "2025-11-21T12:00:00Z"
|
|
205
|
+
|
|
206
|
+
# Parse ISO 8601 string to Time object
|
|
207
|
+
time = ScoutApmMcp::Helpers.parse_time("2025-11-21T12:00:00Z")
|
|
208
|
+
# => #<Time: 2025-11-21 12:00:00 UTC>
|
|
209
|
+
|
|
210
|
+
# Create duration hash from two ISO 8601 strings
|
|
211
|
+
duration = ScoutApmMcp::Helpers.make_duration("2025-11-21T00:00:00Z", "2025-11-21T12:00:00Z")
|
|
212
|
+
# => { start: #<Time: ...>, end: #<Time: ...> }
|
|
213
|
+
|
|
214
|
+
# Extract endpoint ID from endpoint hash
|
|
215
|
+
endpoint_id = ScoutApmMcp::Helpers.get_endpoint_id(endpoint_hash)
|
|
216
|
+
# => "base64-encoded-endpoint-id"
|
|
217
|
+
```
|
|
218
|
+
|
|
162
219
|
### API Key Management
|
|
163
220
|
|
|
164
221
|
The gem supports multiple methods for API key retrieval (checked in order):
|
|
@@ -193,7 +250,7 @@ api_key = ScoutApmMcp::Helpers.get_api_key(
|
|
|
193
250
|
|
|
194
251
|
### Applications
|
|
195
252
|
|
|
196
|
-
- `list_apps` - List all applications
|
|
253
|
+
- `list_apps(active_since:)` - List all applications (optionally filtered by last reported time)
|
|
197
254
|
- `get_app(app_id)` - Get application details
|
|
198
255
|
|
|
199
256
|
### Metrics
|
|
@@ -204,10 +261,11 @@ api_key = ScoutApmMcp::Helpers.get_api_key(
|
|
|
204
261
|
### Endpoints
|
|
205
262
|
|
|
206
263
|
- `list_endpoints(app_id, from:, to:)` - List all endpoints
|
|
207
|
-
- `get_endpoint(app_id, endpoint_id)` - Get endpoint details
|
|
208
264
|
- `get_endpoint_metrics(app_id, endpoint_id, metric_type, from:, to:)` - Get endpoint metrics
|
|
209
265
|
- `list_endpoint_traces(app_id, endpoint_id, from:, to:)` - List endpoint traces
|
|
210
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
|
+
|
|
211
269
|
### Traces
|
|
212
270
|
|
|
213
271
|
- `fetch_trace(app_id, trace_id)` - Fetch detailed trace information
|
|
@@ -249,11 +307,32 @@ The server will communicate via STDIN/STDOUT using the MCP protocol. Configure i
|
|
|
249
307
|
|
|
250
308
|
## Error Handling
|
|
251
309
|
|
|
252
|
-
The client
|
|
310
|
+
The client uses custom exception classes for better error handling:
|
|
311
|
+
|
|
312
|
+
- `ScoutApmMcp::Error` - Base exception class for all ScoutAPM SDK errors
|
|
313
|
+
- `ScoutApmMcp::AuthError` - Raised when authentication fails (401 Unauthorized)
|
|
314
|
+
- `ScoutApmMcp::APIError` - Raised for API errors (includes `status_code` and `response_data` attributes)
|
|
253
315
|
|
|
254
|
-
|
|
255
|
-
-
|
|
256
|
-
-
|
|
316
|
+
The client also validates input parameters and raises `ArgumentError` for:
|
|
317
|
+
- Invalid metric types (must be one of: `apdex`, `response_time`, `response_time_95th`, `errors`, `throughput`, `queue_time`)
|
|
318
|
+
- Invalid insight types (must be one of: `n_plus_one`, `memory_bloat`, `slow_query`)
|
|
319
|
+
- Invalid time ranges (from_time must be before to_time, and range cannot exceed 2 weeks)
|
|
320
|
+
- Trace queries older than 7 days (for `list_endpoint_traces`)
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
begin
|
|
324
|
+
client.get_metric(123, "invalid_metric", from: "2025-01-01T00:00:00Z", to: "2025-01-02T00:00:00Z")
|
|
325
|
+
rescue ScoutApmMcp::AuthError => e
|
|
326
|
+
puts "Authentication failed: #{e.message}"
|
|
327
|
+
rescue ScoutApmMcp::APIError => e
|
|
328
|
+
puts "API error (#{e.status_code}): #{e.message}"
|
|
329
|
+
puts "Response data: #{e.response_data}"
|
|
330
|
+
rescue ArgumentError => e
|
|
331
|
+
puts "Invalid parameter: #{e.message}"
|
|
332
|
+
rescue ScoutApmMcp::Error => e
|
|
333
|
+
puts "Error: #{e.message}"
|
|
334
|
+
end
|
|
335
|
+
```
|
|
257
336
|
|
|
258
337
|
## Development
|
|
259
338
|
|
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,66 @@ 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)
|
|
30
|
+
# @raise [ArgumentError] if api_key is nil or empty
|
|
20
31
|
def initialize(api_key:, api_base: API_BASE)
|
|
21
|
-
|
|
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
|
|
22
36
|
@api_base = api_base
|
|
37
|
+
@user_agent = "scout-apm-mcp-rb/#{VERSION}"
|
|
23
38
|
end
|
|
24
39
|
|
|
25
40
|
# List all applications accessible with the provided API key
|
|
26
41
|
#
|
|
27
|
-
# @
|
|
28
|
-
|
|
42
|
+
# @param active_since [String, nil] ISO 8601 datetime string to filter apps active since that time (default: 30 days ago)
|
|
43
|
+
# @return [Array<Hash>] Array of application hashes
|
|
44
|
+
def list_apps(active_since: nil)
|
|
29
45
|
uri = URI("#{@api_base}/apps")
|
|
30
|
-
make_request(uri)
|
|
46
|
+
response = make_request(uri)
|
|
47
|
+
apps = response.dig("results", "apps") || []
|
|
48
|
+
|
|
49
|
+
if active_since
|
|
50
|
+
active_time = Helpers.parse_time(active_since)
|
|
51
|
+
apps = apps.select do |app|
|
|
52
|
+
reported_at = app["last_reported_at"]
|
|
53
|
+
if reported_at && !reported_at.empty?
|
|
54
|
+
Helpers.parse_time(reported_at) >= active_time
|
|
55
|
+
else
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
apps
|
|
31
62
|
end
|
|
32
63
|
|
|
33
64
|
# Get application details for a specific application
|
|
34
65
|
#
|
|
35
66
|
# @param app_id [Integer] ScoutAPM application ID
|
|
36
|
-
# @return [Hash]
|
|
67
|
+
# @return [Hash] Application details hash
|
|
37
68
|
def get_app(app_id)
|
|
38
69
|
uri = URI("#{@api_base}/apps/#{app_id}")
|
|
39
|
-
make_request(uri)
|
|
70
|
+
response = make_request(uri)
|
|
71
|
+
response.dig("results", "app") || {}
|
|
40
72
|
end
|
|
41
73
|
|
|
42
74
|
# List available metric types for an application
|
|
43
75
|
#
|
|
44
76
|
# @param app_id [Integer] ScoutAPM application ID
|
|
45
|
-
# @return [
|
|
77
|
+
# @return [Array<String>] Array of available metric type names
|
|
46
78
|
def list_metrics(app_id)
|
|
47
79
|
uri = URI("#{@api_base}/apps/#{app_id}/metrics")
|
|
48
|
-
make_request(uri)
|
|
80
|
+
response = make_request(uri)
|
|
81
|
+
response.dig("results", "availableMetrics") || []
|
|
49
82
|
end
|
|
50
83
|
|
|
51
84
|
# Get time-series data for a specific metric type
|
|
@@ -54,11 +87,20 @@ module ScoutApmMcp
|
|
|
54
87
|
# @param metric_type [String] Metric type (apdex, response_time, response_time_95th, errors, throughput, queue_time)
|
|
55
88
|
# @param from [String, nil] Start time in ISO 8601 format
|
|
56
89
|
# @param to [String, nil] End time in ISO 8601 format
|
|
57
|
-
# @
|
|
58
|
-
|
|
90
|
+
# @param range [String, nil] Quick time range (e.g., "30min", "1day", "3days", "7days"). If provided, calculates from/to automatically.
|
|
91
|
+
# @return [Hash] Hash containing metric series data
|
|
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
|
+
|
|
99
|
+
validate_metric_params(metric_type, from, to)
|
|
59
100
|
uri = URI("#{@api_base}/apps/#{app_id}/metrics/#{metric_type}")
|
|
60
101
|
uri.query = build_query_string(from: from, to: to)
|
|
61
|
-
make_request(uri)
|
|
102
|
+
response = make_request(uri)
|
|
103
|
+
response.dig("results", "series") || {}
|
|
62
104
|
end
|
|
63
105
|
|
|
64
106
|
# List all endpoints for an application
|
|
@@ -66,22 +108,32 @@ module ScoutApmMcp
|
|
|
66
108
|
# @param app_id [Integer] ScoutAPM application ID
|
|
67
109
|
# @param from [String, nil] Start time in ISO 8601 format
|
|
68
110
|
# @param to [String, nil] End time in ISO 8601 format
|
|
69
|
-
# @
|
|
70
|
-
|
|
111
|
+
# @param range [String, nil] Quick time range (e.g., "30min", "1day", "3days", "7days"). If provided, calculates from/to automatically.
|
|
112
|
+
# @return [Array<Hash>] Array of endpoint hashes
|
|
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
|
+
|
|
132
|
+
validate_time_range(from, to) if from && to
|
|
71
133
|
uri = URI("#{@api_base}/apps/#{app_id}/endpoints")
|
|
72
134
|
uri.query = build_query_string(from: from, to: to)
|
|
73
|
-
make_request(uri)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
# Get endpoint details
|
|
77
|
-
#
|
|
78
|
-
# @param app_id [Integer] ScoutAPM application ID
|
|
79
|
-
# @param endpoint_id [String] Endpoint ID (base64 URL-encoded)
|
|
80
|
-
# @return [Hash] API response containing endpoint details
|
|
81
|
-
def get_endpoint(app_id, endpoint_id)
|
|
82
|
-
encoded_endpoint_id = URI.encode_www_form_component(endpoint_id)
|
|
83
|
-
uri = URI("#{@api_base}/apps/#{app_id}/endpoints/#{encoded_endpoint_id}")
|
|
84
|
-
make_request(uri)
|
|
135
|
+
response = make_request(uri)
|
|
136
|
+
response["results"] || []
|
|
85
137
|
end
|
|
86
138
|
|
|
87
139
|
# Get metric data for a specific endpoint
|
|
@@ -91,12 +143,22 @@ module ScoutApmMcp
|
|
|
91
143
|
# @param metric_type [String] Metric type (apdex, response_time, response_time_95th, errors, throughput, queue_time)
|
|
92
144
|
# @param from [String, nil] Start time in ISO 8601 format
|
|
93
145
|
# @param to [String, nil] End time in ISO 8601 format
|
|
94
|
-
# @
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
146
|
+
# @param range [String, nil] Quick time range (e.g., "30min", "1day", "3days", "7days"). If provided, calculates from/to automatically.
|
|
147
|
+
# @return [Array] Array of metric data points for the specified metric type
|
|
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
|
+
|
|
155
|
+
validate_metric_params(metric_type, from, to)
|
|
156
|
+
uri = URI(@api_base)
|
|
157
|
+
uri.path = File.join(uri.path, "apps", app_id.to_s, "endpoints", endpoint_id, "metrics", metric_type)
|
|
98
158
|
uri.query = build_query_string(from: from, to: to)
|
|
99
|
-
make_request(uri)
|
|
159
|
+
response = make_request(uri)
|
|
160
|
+
series = response.dig("results", "series") || {}
|
|
161
|
+
series[metric_type] || []
|
|
100
162
|
end
|
|
101
163
|
|
|
102
164
|
# List traces for a specific endpoint (max 100, within 7 days)
|
|
@@ -105,22 +167,39 @@ module ScoutApmMcp
|
|
|
105
167
|
# @param endpoint_id [String] Endpoint ID (base64 URL-encoded)
|
|
106
168
|
# @param from [String, nil] Start time in ISO 8601 format
|
|
107
169
|
# @param to [String, nil] End time in ISO 8601 format
|
|
108
|
-
# @
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
170
|
+
# @param range [String, nil] Quick time range (e.g., "30min", "1day", "3days", "7days"). If provided, calculates from/to automatically.
|
|
171
|
+
# @return [Array<Hash>] Array of trace hashes
|
|
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
|
+
|
|
179
|
+
validate_time_range(from, to) if from && to
|
|
180
|
+
if from && to
|
|
181
|
+
from_time = Helpers.parse_time(from)
|
|
182
|
+
seven_days_ago = Time.now.utc - (7 * 24 * 60 * 60)
|
|
183
|
+
if from_time < seven_days_ago
|
|
184
|
+
raise ArgumentError, "from_time cannot be older than 7 days"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
uri = URI(@api_base)
|
|
188
|
+
uri.path = File.join(uri.path, "apps", app_id.to_s, "endpoints", endpoint_id, "traces")
|
|
112
189
|
uri.query = build_query_string(from: from, to: to)
|
|
113
|
-
make_request(uri)
|
|
190
|
+
response = make_request(uri)
|
|
191
|
+
response.dig("results", "traces") || []
|
|
114
192
|
end
|
|
115
193
|
|
|
116
194
|
# Fetch detailed trace information
|
|
117
195
|
#
|
|
118
196
|
# @param app_id [Integer] ScoutAPM application ID
|
|
119
197
|
# @param trace_id [Integer] Trace identifier
|
|
120
|
-
# @return [Hash]
|
|
198
|
+
# @return [Hash] Trace details hash
|
|
121
199
|
def fetch_trace(app_id, trace_id)
|
|
122
200
|
uri = URI("#{@api_base}/apps/#{app_id}/traces/#{trace_id}")
|
|
123
|
-
make_request(uri)
|
|
201
|
+
response = make_request(uri)
|
|
202
|
+
response.dig("results", "trace") || {}
|
|
124
203
|
end
|
|
125
204
|
|
|
126
205
|
# List error groups for an application (max 100, within 30 days)
|
|
@@ -129,46 +208,51 @@ module ScoutApmMcp
|
|
|
129
208
|
# @param from [String, nil] Start time in ISO 8601 format
|
|
130
209
|
# @param to [String, nil] End time in ISO 8601 format
|
|
131
210
|
# @param endpoint [String, nil] Base64 URL-encoded endpoint filter (optional)
|
|
132
|
-
# @return [Hash]
|
|
211
|
+
# @return [Array<Hash>] Array of error group hashes
|
|
133
212
|
def list_error_groups(app_id, from: nil, to: nil, endpoint: nil)
|
|
213
|
+
validate_time_range(from, to) if from && to
|
|
134
214
|
uri = URI("#{@api_base}/apps/#{app_id}/error_groups")
|
|
135
215
|
params = {}
|
|
136
216
|
params["from"] = from if from
|
|
137
217
|
params["to"] = to if to
|
|
138
218
|
params["endpoint"] = endpoint if endpoint
|
|
139
219
|
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
140
|
-
make_request(uri)
|
|
220
|
+
response = make_request(uri)
|
|
221
|
+
response.dig("results", "error_groups") || []
|
|
141
222
|
end
|
|
142
223
|
|
|
143
224
|
# Get details for a specific error group
|
|
144
225
|
#
|
|
145
226
|
# @param app_id [Integer] ScoutAPM application ID
|
|
146
227
|
# @param error_id [Integer] Error group identifier
|
|
147
|
-
# @return [Hash]
|
|
228
|
+
# @return [Hash] Error group details hash
|
|
148
229
|
def get_error_group(app_id, error_id)
|
|
149
230
|
uri = URI("#{@api_base}/apps/#{app_id}/error_groups/#{error_id}")
|
|
150
|
-
make_request(uri)
|
|
231
|
+
response = make_request(uri)
|
|
232
|
+
response.dig("results", "error_group") || {}
|
|
151
233
|
end
|
|
152
234
|
|
|
153
235
|
# Get individual errors within an error group (max 100)
|
|
154
236
|
#
|
|
155
237
|
# @param app_id [Integer] ScoutAPM application ID
|
|
156
238
|
# @param error_id [Integer] Error group identifier
|
|
157
|
-
# @return [Hash]
|
|
239
|
+
# @return [Array<Hash>] Array of error hashes
|
|
158
240
|
def get_error_group_errors(app_id, error_id)
|
|
159
241
|
uri = URI("#{@api_base}/apps/#{app_id}/error_groups/#{error_id}/errors")
|
|
160
|
-
make_request(uri)
|
|
242
|
+
response = make_request(uri)
|
|
243
|
+
response.dig("results", "errors") || []
|
|
161
244
|
end
|
|
162
245
|
|
|
163
246
|
# Get all insight types for an application (cached for 5 minutes)
|
|
164
247
|
#
|
|
165
248
|
# @param app_id [Integer] ScoutAPM application ID
|
|
166
249
|
# @param limit [Integer, nil] Maximum number of items per insight type (default: 20)
|
|
167
|
-
# @return [Hash]
|
|
250
|
+
# @return [Hash] Hash containing all insight types
|
|
168
251
|
def get_all_insights(app_id, limit: nil)
|
|
169
252
|
uri = URI("#{@api_base}/apps/#{app_id}/insights")
|
|
170
253
|
uri.query = "limit=#{limit}" if limit
|
|
171
|
-
make_request(uri)
|
|
254
|
+
response = make_request(uri)
|
|
255
|
+
response["results"] || {}
|
|
172
256
|
end
|
|
173
257
|
|
|
174
258
|
# Get data for a specific insight type
|
|
@@ -176,11 +260,15 @@ module ScoutApmMcp
|
|
|
176
260
|
# @param app_id [Integer] ScoutAPM application ID
|
|
177
261
|
# @param insight_type [String] Insight type (n_plus_one, memory_bloat, slow_query)
|
|
178
262
|
# @param limit [Integer, nil] Maximum number of items (default: 20)
|
|
179
|
-
# @return [Hash]
|
|
263
|
+
# @return [Hash] Hash containing insight data
|
|
180
264
|
def get_insight_by_type(app_id, insight_type, limit: nil)
|
|
265
|
+
unless VALID_INSIGHTS.include?(insight_type)
|
|
266
|
+
raise ArgumentError, "Invalid insight_type. Must be one of: #{VALID_INSIGHTS.join(", ")}"
|
|
267
|
+
end
|
|
181
268
|
uri = URI("#{@api_base}/apps/#{app_id}/insights/#{insight_type}")
|
|
182
269
|
uri.query = "limit=#{limit}" if limit
|
|
183
|
-
make_request(uri)
|
|
270
|
+
response = make_request(uri)
|
|
271
|
+
response["results"] || {}
|
|
184
272
|
end
|
|
185
273
|
|
|
186
274
|
# Get historical insights data with cursor-based pagination
|
|
@@ -239,6 +327,7 @@ module ScoutApmMcp
|
|
|
239
327
|
|
|
240
328
|
request = Net::HTTP::Get.new(uri)
|
|
241
329
|
request["X-SCOUT-API"] = @api_key
|
|
330
|
+
request["User-Agent"] = @user_agent
|
|
242
331
|
request["Accept"] = "application/x-yaml, application/yaml, text/yaml, */*"
|
|
243
332
|
|
|
244
333
|
response = http.request(request)
|
|
@@ -251,14 +340,16 @@ module ScoutApmMcp
|
|
|
251
340
|
status: response.code.to_i
|
|
252
341
|
}
|
|
253
342
|
when Net::HTTPUnauthorized
|
|
254
|
-
raise "Authentication failed. Check your API key."
|
|
343
|
+
raise AuthError, "Authentication failed. Check your API key."
|
|
255
344
|
else
|
|
256
|
-
raise "API request failed: #{response.code} #{response.message}"
|
|
345
|
+
raise APIError.new("API request failed: #{response.code} #{response.message}", status_code: response.code.to_i)
|
|
257
346
|
end
|
|
258
347
|
rescue OpenSSL::SSL::SSLError => e
|
|
259
|
-
raise "SSL verification failed: #{e.message}. This may be due to system certificate configuration issues."
|
|
348
|
+
raise Error, "SSL verification failed: #{e.message}. This may be due to system certificate configuration issues."
|
|
349
|
+
rescue Error
|
|
350
|
+
raise
|
|
260
351
|
rescue => e
|
|
261
|
-
raise "Request failed: #{e.class} - #{e.message}"
|
|
352
|
+
raise Error, "Request failed: #{e.class} - #{e.message}"
|
|
262
353
|
end
|
|
263
354
|
|
|
264
355
|
private
|
|
@@ -303,24 +394,92 @@ module ScoutApmMcp
|
|
|
303
394
|
|
|
304
395
|
request = Net::HTTP::Get.new(uri)
|
|
305
396
|
request["X-SCOUT-API"] = @api_key
|
|
397
|
+
request["User-Agent"] = @user_agent
|
|
306
398
|
request["Accept"] = "application/json"
|
|
307
399
|
|
|
308
400
|
response = http.request(request)
|
|
401
|
+
response_data = handle_response_errors(response)
|
|
402
|
+
|
|
403
|
+
if response_data.is_a?(Hash)
|
|
404
|
+
header = response_data["header"]
|
|
405
|
+
if header && header["status"]
|
|
406
|
+
status_code = header["status"]["code"]
|
|
407
|
+
if status_code && status_code >= 400
|
|
408
|
+
error_msg = header["status"]["message"] || "Unknown API error"
|
|
409
|
+
raise APIError.new(error_msg, status_code: status_code, response_data: response_data)
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
response_data
|
|
415
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
416
|
+
raise Error, "SSL verification failed: #{e.message}. This may be due to system certificate configuration issues."
|
|
417
|
+
rescue Error
|
|
418
|
+
raise
|
|
419
|
+
rescue => e
|
|
420
|
+
raise Error, "Request failed: #{e.class} - #{e.message}"
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# @param response [Net::HTTPResponse] HTTP response object
|
|
424
|
+
# @return [Hash, Array] Parsed JSON response
|
|
425
|
+
# @raise [AuthError] When authentication fails
|
|
426
|
+
# @raise [APIError] When the API returns an error response
|
|
427
|
+
def handle_response_errors(response)
|
|
428
|
+
begin
|
|
429
|
+
data = JSON.parse(response.body)
|
|
430
|
+
rescue JSON::ParserError
|
|
431
|
+
raise APIError.new("Invalid JSON response: #{response.body}", status_code: response.code.to_i)
|
|
432
|
+
end
|
|
309
433
|
|
|
310
434
|
case response
|
|
311
435
|
when Net::HTTPSuccess
|
|
312
|
-
|
|
436
|
+
data
|
|
313
437
|
when Net::HTTPUnauthorized
|
|
314
|
-
raise "Authentication failed
|
|
438
|
+
raise AuthError, "Authentication failed - check your API key"
|
|
315
439
|
when Net::HTTPNotFound
|
|
316
|
-
raise "Resource not found
|
|
440
|
+
raise APIError.new("Resource not found", status_code: 404, response_data: data)
|
|
317
441
|
else
|
|
318
|
-
|
|
442
|
+
error_msg = "API request failed"
|
|
443
|
+
if data.is_a?(Hash) && data.dig("header", "status", "message")
|
|
444
|
+
error_msg = data.dig("header", "status", "message")
|
|
445
|
+
end
|
|
446
|
+
raise APIError.new(error_msg, status_code: response.code.to_i, response_data: data)
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Validate metric parameters
|
|
451
|
+
#
|
|
452
|
+
# @param metric_type [String] Metric type to validate
|
|
453
|
+
# @param from [String, nil] Start time in ISO 8601 format
|
|
454
|
+
# @param to [String, nil] End time in ISO 8601 format
|
|
455
|
+
# @raise [ArgumentError] If validation fails
|
|
456
|
+
def validate_metric_params(metric_type, from, to)
|
|
457
|
+
unless VALID_METRICS.include?(metric_type)
|
|
458
|
+
raise ArgumentError, "Invalid metric_type. Must be one of: #{VALID_METRICS.join(", ")}"
|
|
459
|
+
end
|
|
460
|
+
validate_time_range(from, to) if from && to
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Validate time ranges
|
|
464
|
+
#
|
|
465
|
+
# @param from [String, nil] Start time in ISO 8601 format
|
|
466
|
+
# @param to [String, nil] End time in ISO 8601 format
|
|
467
|
+
# @raise [ArgumentError] If validation fails
|
|
468
|
+
def validate_time_range(from, to)
|
|
469
|
+
return unless from && to
|
|
470
|
+
|
|
471
|
+
from_time = Helpers.parse_time(from)
|
|
472
|
+
to_time = Helpers.parse_time(to)
|
|
473
|
+
|
|
474
|
+
if from_time >= to_time
|
|
475
|
+
raise ArgumentError, "from_time must be before to_time"
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Validate time range (2 week maximum)
|
|
479
|
+
max_duration = 14 * 24 * 60 * 60 # 14 days in seconds
|
|
480
|
+
if (to_time - from_time) > max_duration
|
|
481
|
+
raise ArgumentError, "Time range cannot exceed 2 weeks"
|
|
319
482
|
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
483
|
end
|
|
325
484
|
end
|
|
326
485
|
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
|
#
|
|
@@ -13,14 +14,10 @@ module ScoutApmMcp
|
|
|
13
14
|
# @return [String] API key
|
|
14
15
|
# @raise [RuntimeError] if API key cannot be found
|
|
15
16
|
def self.get_api_key(api_key: nil, op_vault: nil, op_item: nil, op_field: "API_KEY")
|
|
16
|
-
# Use provided API key if available
|
|
17
17
|
return api_key if api_key && !api_key.empty?
|
|
18
18
|
|
|
19
|
-
# Check environment variable (may have been set by opdotenv loaded early in server startup)
|
|
20
19
|
api_key = ENV["API_KEY"] || ENV["SCOUT_APM_API_KEY"]
|
|
21
20
|
return api_key if api_key && !api_key.empty?
|
|
22
|
-
|
|
23
|
-
# Try direct 1Password CLI as fallback (opdotenv was already tried in server startup)
|
|
24
21
|
op_env_entry_path = ENV["OP_ENV_ENTRY_PATH"]
|
|
25
22
|
if op_env_entry_path && !op_env_entry_path.empty?
|
|
26
23
|
begin
|
|
@@ -54,7 +51,6 @@ module ScoutApmMcp
|
|
|
54
51
|
api_key = `op read "op://#{op_vault}/#{op_item}/#{op_field}" 2>/dev/null`.strip
|
|
55
52
|
return api_key if api_key && !api_key.empty?
|
|
56
53
|
rescue
|
|
57
|
-
# Silently fail
|
|
58
54
|
end
|
|
59
55
|
end
|
|
60
56
|
|
|
@@ -134,7 +130,6 @@ module ScoutApmMcp
|
|
|
134
130
|
# @return [String] Decoded endpoint ID
|
|
135
131
|
def self.decode_endpoint_id(endpoint_id)
|
|
136
132
|
decoded = Base64.urlsafe_decode64(endpoint_id)
|
|
137
|
-
# Check if decoded result is valid UTF-8
|
|
138
133
|
if decoded.force_encoding(Encoding::UTF_8).valid_encoding?
|
|
139
134
|
decoded.force_encoding(Encoding::UTF_8)
|
|
140
135
|
else
|
|
@@ -151,5 +146,107 @@ module ScoutApmMcp
|
|
|
151
146
|
# If decoding raises an exception, return original string
|
|
152
147
|
endpoint_id.dup.force_encoding(Encoding::UTF_8)
|
|
153
148
|
end
|
|
149
|
+
|
|
150
|
+
# Get a unique identifier for an endpoint from an endpoint dictionary
|
|
151
|
+
#
|
|
152
|
+
# This is provided by the API implicitly in the 'link' field.
|
|
153
|
+
#
|
|
154
|
+
# @param endpoint [Hash] Endpoint dictionary from the API
|
|
155
|
+
# @return [String] Endpoint ID extracted from the link field, or empty string if not found
|
|
156
|
+
def self.get_endpoint_id(endpoint)
|
|
157
|
+
link = endpoint["link"] || endpoint[:link] || ""
|
|
158
|
+
return "" if link.empty?
|
|
159
|
+
|
|
160
|
+
# Extract the endpoint ID from the link (last path segment)
|
|
161
|
+
link.split("/").last || ""
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Format datetime to ISO 8601 string for API
|
|
165
|
+
#
|
|
166
|
+
# Relies on UTC timezone. Converts the time to UTC if it's not already.
|
|
167
|
+
#
|
|
168
|
+
# @param time [Time] Time object to format
|
|
169
|
+
# @return [String] ISO 8601 formatted time string (e.g., "2025-01-01T00:00:00Z")
|
|
170
|
+
def self.format_time(time)
|
|
171
|
+
time.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Parse ISO 8601 time string to Time object
|
|
175
|
+
#
|
|
176
|
+
# Handles both 'Z' suffix and timezone offsets.
|
|
177
|
+
#
|
|
178
|
+
# @param time_str [String] ISO 8601 time string (e.g., "2025-01-01T00:00:00Z")
|
|
179
|
+
# @return [Time] Time object in UTC
|
|
180
|
+
def self.parse_time(time_str)
|
|
181
|
+
# Replace Z with +00:00 for Ruby's Time parser
|
|
182
|
+
normalized = time_str.sub(/Z\z/i, "+00:00")
|
|
183
|
+
Time.parse(normalized).utc
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Create a Duration object from ISO 8601 strings
|
|
187
|
+
#
|
|
188
|
+
# @param from_str [String] Start time in ISO 8601 format
|
|
189
|
+
# @param to_str [String] End time in ISO 8601 format
|
|
190
|
+
# @return [Hash] Hash with :start and :end Time objects
|
|
191
|
+
def self.make_duration(from_str, to_str)
|
|
192
|
+
{
|
|
193
|
+
start: parse_time(from_str),
|
|
194
|
+
end: parse_time(to_str)
|
|
195
|
+
}
|
|
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
|
|
154
251
|
end
|
|
155
252
|
end
|
data/lib/scout_apm_mcp/server.rb
CHANGED
|
@@ -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,14 +165,27 @@ module ScoutApmMcp
|
|
|
166
165
|
|
|
167
166
|
# Applications Tools
|
|
168
167
|
class ListAppsTool < BaseTool
|
|
169
|
-
description
|
|
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
|
-
|
|
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
|
-
def call
|
|
176
|
-
get_client.list_apps
|
|
187
|
+
def call(active_since: nil)
|
|
188
|
+
get_client.list_apps(active_since: active_since)
|
|
177
189
|
end
|
|
178
190
|
end
|
|
179
191
|
|
|
@@ -203,80 +215,151 @@ module ScoutApmMcp
|
|
|
203
215
|
end
|
|
204
216
|
|
|
205
217
|
class GetMetricTool < BaseTool
|
|
206
|
-
description
|
|
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(:
|
|
212
|
-
optional(:
|
|
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
|
|
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(:
|
|
227
|
-
optional(:
|
|
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
|
|
236
|
-
description
|
|
293
|
+
class GetEndpointMetricsTool < BaseTool
|
|
294
|
+
description <<~DESC
|
|
295
|
+
Get metric data for a specific endpoint.
|
|
237
296
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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(:
|
|
260
|
-
optional(:
|
|
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
|
|
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(:
|
|
275
|
-
optional(:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -468,20 +586,37 @@ module ScoutApmMcp
|
|
|
468
586
|
result[:data] = {trace: trace_data}
|
|
469
587
|
|
|
470
588
|
if include_endpoint && parsed[:endpoint_id]
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
589
|
+
begin
|
|
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
|
|
598
|
+
result[:data][:decoded_endpoint] = parsed[:decoded_endpoint]
|
|
599
|
+
rescue => e
|
|
600
|
+
result[:data][:endpoint_error] = "Failed to fetch endpoint: #{e.message}"
|
|
601
|
+
result[:data][:decoded_endpoint] = parsed[:decoded_endpoint]
|
|
602
|
+
end
|
|
474
603
|
end
|
|
475
604
|
else
|
|
476
605
|
raise "Invalid trace URL: missing app_id or trace_id"
|
|
477
606
|
end
|
|
478
607
|
when :endpoint
|
|
479
608
|
if parsed[:app_id] && parsed[:endpoint_id]
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
|
485
620
|
else
|
|
486
621
|
raise "Invalid endpoint URL: missing app_id or endpoint_id"
|
|
487
622
|
end
|
data/lib/scout_apm_mcp.rb
CHANGED
data/sig/scout_apm_mcp.rbs
CHANGED
|
@@ -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.
|
|
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:
|
|
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:
|
|
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
|
|
@@ -233,6 +275,7 @@ files:
|
|
|
233
275
|
- bin/scout_apm_mcp
|
|
234
276
|
- lib/scout_apm_mcp.rb
|
|
235
277
|
- lib/scout_apm_mcp/client.rb
|
|
278
|
+
- lib/scout_apm_mcp/errors.rb
|
|
236
279
|
- lib/scout_apm_mcp/helpers.rb
|
|
237
280
|
- lib/scout_apm_mcp/server.rb
|
|
238
281
|
- lib/scout_apm_mcp/version.rb
|