scout_apm_mcp 0.1.5 → 0.1.6
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 +7 -0
- data/bin/scout_apm_mcp +0 -2
- data/lib/scout_apm_mcp/client.rb +110 -0
- data/lib/scout_apm_mcp/helpers.rb +30 -2
- data/lib/scout_apm_mcp/mcp_error_id_patch.rb +29 -0
- data/lib/scout_apm_mcp/server.rb +129 -39
- data/lib/scout_apm_mcp/version.rb +1 -1
- data/sig/scout_apm_mcp.rbs +5 -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: 3e4f9717b9ae8caf19fa92e364acc8fd7183a27ca686b8c587748ec017bb1764
|
|
4
|
+
data.tar.gz: 7c8261b3a2f17d320d69051c67e152d851e0fe701c93fca62cd68c92f069475b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba2f1f24479d17fb119c08c1ca14b22a2059e9ee0f3ca5047fc47c2a57b8eece7e2fb6541036434a8e623fb8781c2eeb95a9b91ce3dfe576ae615a8a1ba6bf5e
|
|
7
|
+
data.tar.gz: 94291c406ff5d7d5755349de80e03e6ca7001201c8d3642d613404ec2e1a0dcb0d96641439fca0ef5442a2738f5f20168fbee38219de6578ffe7551edf398c6e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.1.6 (2026-03-29)
|
|
4
|
+
|
|
5
|
+
- Background jobs API support: `list_jobs`, `list_job_metrics`, `get_job_metrics`, `list_job_traces` on the client, matching MCP tools, and URL parsing / `FetchScoutURLTool` handling for job and job-trace links.
|
|
6
|
+
- Refreshed bundled OpenAPI fixture from Scout’s live schema (includes jobs paths).
|
|
7
|
+
- Extracted MCP JSON-RPC error-id monkey patch into `lib/scout_apm_mcp/mcp_error_id_patch.rb` (excluded from coverage metrics).
|
|
8
|
+
- Expanded test suite: `Server.start`, `NullLogger`, batch tool dispatch, `FetchScoutURLTool` / `FetchOpenAPISchemaTool` branches, and list endpoints/jobs timeframe edge cases
|
|
9
|
+
|
|
3
10
|
## 0.1.5 (2026-02-10)
|
|
4
11
|
|
|
5
12
|
- Fix range handling in calculate_range method
|
data/bin/scout_apm_mcp
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
2
|
# Suppress all output to prevent breaking JSON parsing in MCP clients
|
|
5
3
|
# STDOUT is used for MCP protocol communication (JSON-RPC), so we must not output anything
|
|
6
4
|
# STDERR is also suppressed to prevent any error messages from breaking JSON parsing
|
data/lib/scout_apm_mcp/client.rb
CHANGED
|
@@ -25,6 +25,9 @@ module ScoutApmMcp
|
|
|
25
25
|
# Valid insight types
|
|
26
26
|
VALID_INSIGHTS = %w[n_plus_one memory_bloat slow_query].freeze
|
|
27
27
|
|
|
28
|
+
# Valid job metric types (distinct from application endpoint metrics)
|
|
29
|
+
VALID_JOB_METRICS = %w[throughput execution_time latency errors allocations].freeze
|
|
30
|
+
|
|
28
31
|
# @param api_key [String] ScoutAPM API key
|
|
29
32
|
# @param api_base [String] API base URL (default: https://scoutapm.com/api/v0)
|
|
30
33
|
# @raise [ArgumentError] if api_key is nil or empty
|
|
@@ -191,6 +194,106 @@ module ScoutApmMcp
|
|
|
191
194
|
response.dig("results", "traces") || []
|
|
192
195
|
end
|
|
193
196
|
|
|
197
|
+
# List background jobs for an application
|
|
198
|
+
#
|
|
199
|
+
# @param app_id [Integer] ScoutAPM application ID
|
|
200
|
+
# @param from [String, nil] Start time in ISO 8601 format
|
|
201
|
+
# @param to [String, nil] End time in ISO 8601 format
|
|
202
|
+
# @param range [String, nil] Quick time range (e.g., "30min", "1day", "3days", "7days"). If provided, calculates from/to automatically.
|
|
203
|
+
# @return [Array<Hash>] Array of job hashes
|
|
204
|
+
def list_jobs(app_id, from: nil, to: nil, range: nil)
|
|
205
|
+
if from.nil? && to.nil? && range.nil?
|
|
206
|
+
range = "7days"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
if range
|
|
210
|
+
calculated = Helpers.calculate_range(range: range, to: to)
|
|
211
|
+
from = calculated[:from]
|
|
212
|
+
to = calculated[:to]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
now = Time.now.utc
|
|
216
|
+
if from.nil? && to
|
|
217
|
+
calculated = Helpers.calculate_range(range: "7days", to: to)
|
|
218
|
+
from = calculated[:from]
|
|
219
|
+
elsif from && to.nil?
|
|
220
|
+
to = Helpers.format_time(now)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
validate_time_range(from, to) if from && to
|
|
224
|
+
uri = URI("#{@api_base}/apps/#{app_id}/jobs")
|
|
225
|
+
uri.query = build_query_string(from: from, to: to)
|
|
226
|
+
response = make_request(uri)
|
|
227
|
+
response["results"] || []
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# List available metric types for a background job
|
|
231
|
+
#
|
|
232
|
+
# @param app_id [Integer] ScoutAPM application ID
|
|
233
|
+
# @param job_id [String] Job ID (base64 URL-encoded, as returned by list_jobs)
|
|
234
|
+
# @return [Array<String>] Metric type names
|
|
235
|
+
def list_job_metrics(app_id, job_id)
|
|
236
|
+
uri = URI(@api_base)
|
|
237
|
+
uri.path = File.join(uri.path, "apps", app_id.to_s, "jobs", job_id, "metrics")
|
|
238
|
+
response = make_request(uri)
|
|
239
|
+
response.dig("results", "availableMetrics") || []
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Get time-series data for a job metric
|
|
243
|
+
#
|
|
244
|
+
# @param app_id [Integer] ScoutAPM application ID
|
|
245
|
+
# @param job_id [String] Job ID (base64 URL-encoded)
|
|
246
|
+
# @param metric_type [String] One of throughput, execution_time, latency, errors, allocations
|
|
247
|
+
# @param from [String, nil] Start time in ISO 8601 format
|
|
248
|
+
# @param to [String, nil] End time in ISO 8601 format
|
|
249
|
+
# @param range [String, nil] Quick time range; if provided, calculates from/to automatically.
|
|
250
|
+
# @return [Array] Data points for the metric
|
|
251
|
+
def get_job_metrics(app_id, job_id, metric_type, from: nil, to: nil, range: nil)
|
|
252
|
+
if range
|
|
253
|
+
calculated = Helpers.calculate_range(range: range, to: to)
|
|
254
|
+
from = calculated[:from]
|
|
255
|
+
to = calculated[:to]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
validate_job_metric_params(metric_type, from, to)
|
|
259
|
+
uri = URI(@api_base)
|
|
260
|
+
uri.path = File.join(uri.path, "apps", app_id.to_s, "jobs", job_id, "metrics", metric_type)
|
|
261
|
+
uri.query = build_query_string(from: from, to: to)
|
|
262
|
+
response = make_request(uri)
|
|
263
|
+
series = response.dig("results", "series") || {}
|
|
264
|
+
series[metric_type] || []
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# List traces for a background job (max 100, within 7 days)
|
|
268
|
+
#
|
|
269
|
+
# @param app_id [Integer] ScoutAPM application ID
|
|
270
|
+
# @param job_id [String] Job ID (base64 URL-encoded)
|
|
271
|
+
# @param from [String, nil] Start time in ISO 8601 format
|
|
272
|
+
# @param to [String, nil] End time in ISO 8601 format
|
|
273
|
+
# @param range [String, nil] Quick time range; if provided, calculates from/to automatically.
|
|
274
|
+
# @return [Array<Hash>] Array of trace hashes
|
|
275
|
+
def list_job_traces(app_id, job_id, from: nil, to: nil, range: nil)
|
|
276
|
+
if range
|
|
277
|
+
calculated = Helpers.calculate_range(range: range, to: to)
|
|
278
|
+
from = calculated[:from]
|
|
279
|
+
to = calculated[:to]
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
validate_time_range(from, to) if from && to
|
|
283
|
+
if from && to
|
|
284
|
+
from_time = Helpers.parse_time(from)
|
|
285
|
+
seven_days_ago = Time.now.utc - (7 * 24 * 60 * 60)
|
|
286
|
+
if from_time < seven_days_ago
|
|
287
|
+
raise ArgumentError, "from_time cannot be older than 7 days"
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
uri = URI(@api_base)
|
|
291
|
+
uri.path = File.join(uri.path, "apps", app_id.to_s, "jobs", job_id, "traces")
|
|
292
|
+
uri.query = build_query_string(from: from, to: to)
|
|
293
|
+
response = make_request(uri)
|
|
294
|
+
response.dig("results", "traces") || []
|
|
295
|
+
end
|
|
296
|
+
|
|
194
297
|
# Fetch detailed trace information
|
|
195
298
|
#
|
|
196
299
|
# @param app_id [Integer] ScoutAPM application ID
|
|
@@ -460,6 +563,13 @@ module ScoutApmMcp
|
|
|
460
563
|
validate_time_range(from, to) if from && to
|
|
461
564
|
end
|
|
462
565
|
|
|
566
|
+
def validate_job_metric_params(metric_type, from, to)
|
|
567
|
+
unless VALID_JOB_METRICS.include?(metric_type)
|
|
568
|
+
raise ArgumentError, "Invalid metric_type. Must be one of: #{VALID_JOB_METRICS.join(", ")}"
|
|
569
|
+
end
|
|
570
|
+
validate_time_range(from, to) if from && to
|
|
571
|
+
end
|
|
572
|
+
|
|
463
573
|
# Validate time ranges
|
|
464
574
|
#
|
|
465
575
|
# @param from [String, nil] Start time in ISO 8601 format
|
|
@@ -64,7 +64,7 @@ module ScoutApmMcp
|
|
|
64
64
|
# @param url [String] Full ScoutAPM URL
|
|
65
65
|
# @return [Hash] Hash containing resource type and extracted IDs
|
|
66
66
|
# Possible keys: :url_type, :app_id, :endpoint_id, :trace_id, :error_id, :insight_type,
|
|
67
|
-
# :query_params, :decoded_endpoint
|
|
67
|
+
# :job_id, :query_params, :decoded_endpoint, :decoded_job
|
|
68
68
|
def self.parse_scout_url(url)
|
|
69
69
|
uri = URI.parse(url)
|
|
70
70
|
path_parts = uri.path.split("/").reject(&:empty?)
|
|
@@ -78,7 +78,7 @@ module ScoutApmMcp
|
|
|
78
78
|
|
|
79
79
|
# Detect URL type and extract IDs
|
|
80
80
|
# Pattern: /apps/{app_id}/endpoints/{endpoint_id}/trace/{trace_id}
|
|
81
|
-
if path_parts.include?("trace")
|
|
81
|
+
if path_parts.include?("trace") && path_parts.include?("endpoints")
|
|
82
82
|
result[:url_type] = :trace
|
|
83
83
|
endpoints_index = path_parts.index("endpoints")
|
|
84
84
|
trace_index = path_parts.index("trace")
|
|
@@ -86,6 +86,24 @@ module ScoutApmMcp
|
|
|
86
86
|
result[:endpoint_id] = path_parts[endpoints_index + 1]
|
|
87
87
|
result[:trace_id] = path_parts[trace_index + 1].to_i
|
|
88
88
|
end
|
|
89
|
+
# Pattern: /apps/{app_id}/jobs/{job_id}/trace/{trace_id} or /apps/{app_id}/jobs/{job_id}
|
|
90
|
+
elsif path_parts.include?("jobs")
|
|
91
|
+
jobs_index = path_parts.index("jobs")
|
|
92
|
+
if jobs_index && path_parts[jobs_index + 1]
|
|
93
|
+
result[:job_id] = path_parts[jobs_index + 1]
|
|
94
|
+
if path_parts.include?("trace")
|
|
95
|
+
trace_index = path_parts.index("trace")
|
|
96
|
+
if trace_index && path_parts[trace_index + 1]
|
|
97
|
+
result[:url_type] = :job_trace
|
|
98
|
+
result[:trace_id] = path_parts[trace_index + 1].to_i
|
|
99
|
+
else
|
|
100
|
+
result[:url_type] = :job
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
result[:url_type] = :job
|
|
104
|
+
end
|
|
105
|
+
result[:decoded_job] = decode_endpoint_id(result[:job_id])
|
|
106
|
+
end
|
|
89
107
|
# Pattern: /apps/{app_id}/endpoints/{endpoint_id}
|
|
90
108
|
elsif path_parts.include?("endpoints")
|
|
91
109
|
result[:url_type] = :endpoint
|
|
@@ -161,6 +179,14 @@ module ScoutApmMcp
|
|
|
161
179
|
link.split("/").last || ""
|
|
162
180
|
end
|
|
163
181
|
|
|
182
|
+
# Job ID from a job hash returned by the jobs API
|
|
183
|
+
#
|
|
184
|
+
# @param job [Hash] Job dictionary from list_jobs
|
|
185
|
+
# @return [String] job_id for path parameters, or empty string
|
|
186
|
+
def self.get_job_id(job)
|
|
187
|
+
job["job_id"] || job[:job_id] || ""
|
|
188
|
+
end
|
|
189
|
+
|
|
164
190
|
# Format datetime to ISO 8601 string for API
|
|
165
191
|
#
|
|
166
192
|
# Relies on UTC timezone. Converts the time to UTC if it's not already.
|
|
@@ -226,8 +252,10 @@ module ScoutApmMcp
|
|
|
226
252
|
value * 60 * 60
|
|
227
253
|
when /^day/
|
|
228
254
|
value * 24 * 60 * 60
|
|
255
|
+
# :nocov:
|
|
229
256
|
else
|
|
230
257
|
raise ArgumentError, "Unknown time unit: #{unit}"
|
|
258
|
+
# :nocov:
|
|
231
259
|
end
|
|
232
260
|
end
|
|
233
261
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
# Monkey-patch fast-mcp so JSON-RPC error responses always include a non-nil id (strict MCP clients).
|
|
4
|
+
# Loaded from server only; excluded.from SimpleCov — see spec_helper.
|
|
5
|
+
module MCP
|
|
6
|
+
module Transports
|
|
7
|
+
class StdioTransport
|
|
8
|
+
if method_defined?(:send_error)
|
|
9
|
+
alias_method :original_send_error, :send_error
|
|
10
|
+
|
|
11
|
+
def send_error(code, message, id = nil)
|
|
12
|
+
id = "error_#{SecureRandom.hex(8)}" if id.nil?
|
|
13
|
+
original_send_error(code, message, id)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class Server
|
|
20
|
+
if method_defined?(:send_error)
|
|
21
|
+
alias_method :original_send_error, :send_error
|
|
22
|
+
|
|
23
|
+
def send_error(code, message, id = nil)
|
|
24
|
+
id = "error_#{SecureRandom.hex(8)}" if id.nil?
|
|
25
|
+
original_send_error(code, message, id)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/scout_apm_mcp/server.rb
CHANGED
|
@@ -1,48 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
2
|
require "fast_mcp"
|
|
5
3
|
require "scout_apm_mcp"
|
|
6
4
|
require "logger"
|
|
7
5
|
require "stringio"
|
|
8
|
-
|
|
6
|
+
require_relative "mcp_error_id_patch"
|
|
9
7
|
|
|
10
8
|
# Alias MCP to FastMcp for compatibility
|
|
11
9
|
FastMcp = MCP unless defined?(FastMcp)
|
|
12
10
|
|
|
13
|
-
# Monkey-patch fast-mcp to ensure error responses always have a valid id
|
|
14
|
-
# JSON-RPC 2.0 allows id: null for notifications, but MCP clients (Cursor/Inspector)
|
|
15
|
-
# use strict Zod validation that requires id to be a string or number
|
|
16
|
-
module MCP
|
|
17
|
-
module Transports
|
|
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)
|
|
34
|
-
alias_method :original_send_error, :send_error
|
|
35
|
-
|
|
36
|
-
def send_error(code, message, id = nil)
|
|
37
|
-
# Use placeholder id if nil to satisfy strict MCP client validation
|
|
38
|
-
# JSON-RPC 2.0 allows null for notifications, but MCP clients require valid id
|
|
39
|
-
id = "error_#{SecureRandom.hex(8)}" if id.nil?
|
|
40
|
-
original_send_error(code, message, id)
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
11
|
module ScoutApmMcp
|
|
47
12
|
# MCP Server for ScoutAPM integration
|
|
48
13
|
#
|
|
@@ -135,6 +100,10 @@ module ScoutApmMcp
|
|
|
135
100
|
server.register_tool(ListEndpointsTool)
|
|
136
101
|
server.register_tool(GetEndpointMetricsTool)
|
|
137
102
|
server.register_tool(ListEndpointTracesTool)
|
|
103
|
+
server.register_tool(ListJobsTool)
|
|
104
|
+
server.register_tool(ListJobMetricsTool)
|
|
105
|
+
server.register_tool(GetJobMetricsTool)
|
|
106
|
+
server.register_tool(ListJobTracesTool)
|
|
138
107
|
server.register_tool(FetchTraceTool)
|
|
139
108
|
server.register_tool(ListErrorGroupsTool)
|
|
140
109
|
server.register_tool(GetErrorGroupTool)
|
|
@@ -363,6 +332,83 @@ module ScoutApmMcp
|
|
|
363
332
|
end
|
|
364
333
|
end
|
|
365
334
|
|
|
335
|
+
class ListJobsTool < BaseTool
|
|
336
|
+
description <<~DESC
|
|
337
|
+
List background jobs for an application with performance metrics (throughput, execution time, etc.).
|
|
338
|
+
|
|
339
|
+
Time range: same as ListEndpointsTool — use range and/or from/to. If none are given, defaults to the last 7 days.
|
|
340
|
+
|
|
341
|
+
Each job includes a `job_id` (base64) for ListJobMetricsTool, GetJobMetricsTool, and ListJobTracesTool.
|
|
342
|
+
DESC
|
|
343
|
+
|
|
344
|
+
arguments do
|
|
345
|
+
required(:app_id).filled(:integer).description("ScoutAPM application ID")
|
|
346
|
+
optional(:range).maybe(:string).description("Quick time range: 30min, 60min, 3hrs, 6hrs, 12hrs, 1day, 3days, 7days")
|
|
347
|
+
optional(:from).maybe(:string).description("Start time ISO 8601. Ignored if range is provided.")
|
|
348
|
+
optional(:to).maybe(:string).description("End time ISO 8601; anchor for range if range is provided.")
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def call(app_id:, range: nil, from: nil, to: nil)
|
|
352
|
+
get_client.list_jobs(app_id, from: from, to: to, range: range)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
class ListJobMetricsTool < BaseTool
|
|
357
|
+
description "List available metric types for a specific background job (throughput, execution_time, latency, errors, allocations)"
|
|
358
|
+
|
|
359
|
+
arguments do
|
|
360
|
+
required(:app_id).filled(:integer).description("ScoutAPM application ID")
|
|
361
|
+
required(:job_id).filled(:string).description("Job ID (base64 URL-encoded) from list_jobs results")
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def call(app_id:, job_id:)
|
|
365
|
+
get_client.list_job_metrics(app_id, job_id)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
class GetJobMetricsTool < BaseTool
|
|
370
|
+
description <<~DESC
|
|
371
|
+
Get time-series data for a background job metric.
|
|
372
|
+
|
|
373
|
+
Metric types: throughput, execution_time, latency, errors, allocations (not the same set as HTTP endpoint metrics).
|
|
374
|
+
|
|
375
|
+
Use range or explicit from/to, same as GetEndpointMetricsTool.
|
|
376
|
+
DESC
|
|
377
|
+
|
|
378
|
+
arguments do
|
|
379
|
+
required(:app_id).filled(:integer).description("ScoutAPM application ID")
|
|
380
|
+
required(:job_id).filled(:string).description("Job ID (base64 URL-encoded)")
|
|
381
|
+
required(:metric_type).filled(:string).description("Job metric: throughput, execution_time, latency, errors, allocations")
|
|
382
|
+
optional(:range).maybe(:string).description("Quick time range template")
|
|
383
|
+
optional(:from).maybe(:string).description("Start time ISO 8601. Ignored if range is provided.")
|
|
384
|
+
optional(:to).maybe(:string).description("End time ISO 8601.")
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def call(app_id:, job_id:, metric_type:, range: nil, from: nil, to: nil)
|
|
388
|
+
get_client.get_job_metrics(app_id, job_id, metric_type, from: from, to: to, range: range)
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
class ListJobTracesTool < BaseTool
|
|
393
|
+
description <<~DESC
|
|
394
|
+
List traces for a background job (max 100, within 7 days). Same time constraints as ListEndpointTracesTool.
|
|
395
|
+
|
|
396
|
+
Use FetchTraceTool with a trace id from results for full span detail.
|
|
397
|
+
DESC
|
|
398
|
+
|
|
399
|
+
arguments do
|
|
400
|
+
required(:app_id).filled(:integer).description("ScoutAPM application ID")
|
|
401
|
+
required(:job_id).filled(:string).description("Job ID (base64 URL-encoded)")
|
|
402
|
+
optional(:range).maybe(:string).description("Quick time range template")
|
|
403
|
+
optional(:from).maybe(:string).description("Start time ISO 8601; must be within last 7 days if used with to.")
|
|
404
|
+
optional(:to).maybe(:string).description("End time ISO 8601.")
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def call(app_id:, job_id:, range: nil, from: nil, to: nil)
|
|
408
|
+
get_client.list_job_traces(app_id, job_id, from: from, to: to, range: range)
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
366
412
|
class FetchTraceTool < BaseTool
|
|
367
413
|
description "Fetch detailed trace information from ScoutAPM API"
|
|
368
414
|
|
|
@@ -522,10 +568,12 @@ module ScoutApmMcp
|
|
|
522
568
|
Useful for extracting IDs before making other API requests.
|
|
523
569
|
|
|
524
570
|
Returns a hash with:
|
|
525
|
-
- url_type: :endpoint, :trace, :error_group, :insight, :app, or :unknown
|
|
571
|
+
- url_type: :endpoint, :trace, :job, :job_trace, :error_group, :insight, :app, or :unknown
|
|
526
572
|
- app_id: Application ID (integer)
|
|
527
573
|
- endpoint_id: Base64 URL-encoded endpoint ID (if present)
|
|
574
|
+
- job_id: Base64 URL-encoded job ID (if present)
|
|
528
575
|
- trace_id: Trace ID (if present)
|
|
576
|
+
- decoded_job: Human-readable queue/job name (if job_id present)
|
|
529
577
|
- error_id: Error group ID (if present)
|
|
530
578
|
- insight_type: Insight type (if present)
|
|
531
579
|
- decoded_endpoint: Human-readable endpoint path (if endpoint_id present)
|
|
@@ -551,7 +599,9 @@ module ScoutApmMcp
|
|
|
551
599
|
This tool automatically parses ScoutAPM URLs and fetches the corresponding data.
|
|
552
600
|
Supported URL types:
|
|
553
601
|
- Endpoint URLs: /apps/{app_id}/endpoints/{endpoint_id} (fetches from endpoint list)
|
|
602
|
+
- Job URLs: /apps/{app_id}/jobs/{job_id} (fetches from job list)
|
|
554
603
|
- Trace URLs: /apps/{app_id}/endpoints/{endpoint_id}/trace/{trace_id}
|
|
604
|
+
- Job trace URLs: /apps/{app_id}/jobs/{job_id}/trace/{trace_id}
|
|
555
605
|
- Error group URLs: /apps/{app_id}/error_groups/{error_id}
|
|
556
606
|
- Insight URLs: /apps/{app_id}/insights or /apps/{app_id}/insights/{insight_type}
|
|
557
607
|
- App URLs: /apps/{app_id}
|
|
@@ -561,12 +611,12 @@ module ScoutApmMcp
|
|
|
561
611
|
- https://scoutapm.com/apps/123/endpoints/ABC123.../trace/456 (trace)
|
|
562
612
|
- https://scoutapm.com/apps/123/error_groups/789 (error group)
|
|
563
613
|
|
|
564
|
-
For trace URLs, set include_endpoint=true to also fetch endpoint
|
|
614
|
+
For endpoint or job trace URLs, set include_endpoint=true to also fetch endpoint or job summary from the last 7 days.
|
|
565
615
|
DESC
|
|
566
616
|
|
|
567
617
|
arguments do
|
|
568
618
|
required(:url).filled(:string).description("Full ScoutAPM URL")
|
|
569
|
-
optional(:include_endpoint).filled(:bool).description("For trace URLs, also fetch endpoint
|
|
619
|
+
optional(:include_endpoint).filled(:bool).description("For trace URLs, also fetch endpoint or job context from recent list (default: false)")
|
|
570
620
|
end
|
|
571
621
|
|
|
572
622
|
def call(url:, include_endpoint: false)
|
|
@@ -604,6 +654,46 @@ module ScoutApmMcp
|
|
|
604
654
|
else
|
|
605
655
|
raise "Invalid trace URL: missing app_id or trace_id"
|
|
606
656
|
end
|
|
657
|
+
when :job_trace
|
|
658
|
+
if parsed[:app_id] && parsed[:trace_id]
|
|
659
|
+
trace_data = client.fetch_trace(parsed[:app_id], parsed[:trace_id])
|
|
660
|
+
result[:data] = {trace: trace_data}
|
|
661
|
+
|
|
662
|
+
if include_endpoint && parsed[:job_id]
|
|
663
|
+
begin
|
|
664
|
+
jobs = client.list_jobs(parsed[:app_id], range: "7days")
|
|
665
|
+
job_data = jobs.find { |j| Helpers.get_job_id(j) == parsed[:job_id] }
|
|
666
|
+
|
|
667
|
+
if job_data
|
|
668
|
+
result[:data][:job] = job_data
|
|
669
|
+
else
|
|
670
|
+
result[:data][:job_error] = "Job not found in the last 7 days"
|
|
671
|
+
end
|
|
672
|
+
result[:data][:decoded_job] = parsed[:decoded_job]
|
|
673
|
+
rescue => e
|
|
674
|
+
result[:data][:job_error] = "Failed to fetch job: #{e.message}"
|
|
675
|
+
result[:data][:decoded_job] = parsed[:decoded_job]
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
else
|
|
679
|
+
raise "Invalid job trace URL: missing app_id or trace_id"
|
|
680
|
+
end
|
|
681
|
+
when :job
|
|
682
|
+
if parsed[:app_id] && parsed[:job_id]
|
|
683
|
+
jobs = client.list_jobs(parsed[:app_id], range: "7days")
|
|
684
|
+
job_data = jobs.find { |j| Helpers.get_job_id(j) == parsed[:job_id] }
|
|
685
|
+
|
|
686
|
+
if job_data
|
|
687
|
+
result[:data] = {
|
|
688
|
+
job: job_data,
|
|
689
|
+
decoded_job: parsed[:decoded_job]
|
|
690
|
+
}
|
|
691
|
+
else
|
|
692
|
+
raise "Job not found in the last 7 days. Try using ListJobsTool with a longer time range."
|
|
693
|
+
end
|
|
694
|
+
else
|
|
695
|
+
raise "Invalid job URL: missing app_id or job_id"
|
|
696
|
+
end
|
|
607
697
|
when :endpoint
|
|
608
698
|
if parsed[:app_id] && parsed[:endpoint_id]
|
|
609
699
|
endpoints = client.list_endpoints(parsed[:app_id], range: "7days")
|
data/sig/scout_apm_mcp.rbs
CHANGED
|
@@ -5,6 +5,7 @@ module ScoutApmMcp
|
|
|
5
5
|
def self.get_api_key: (?api_key: String?, ?op_vault: String, ?op_item: String, ?op_field: String) -> String
|
|
6
6
|
def self.parse_scout_url: (String url) -> Hash[Symbol, untyped]
|
|
7
7
|
def self.decode_endpoint_id: (String endpoint_id) -> String
|
|
8
|
+
def self.get_job_id: (Hash[String, untyped] | Hash[Symbol, untyped] job) -> String
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
class Client
|
|
@@ -18,6 +19,10 @@ module ScoutApmMcp
|
|
|
18
19
|
def list_endpoints: (Integer app_id, ?from: String?, ?to: String?) -> Hash[String, untyped]
|
|
19
20
|
def get_endpoint_metrics: (Integer app_id, String endpoint_id, String metric_type, ?from: String?, ?to: String?) -> Hash[String, untyped]
|
|
20
21
|
def list_endpoint_traces: (Integer app_id, String endpoint_id, ?from: String?, ?to: String?) -> Hash[String, untyped]
|
|
22
|
+
def list_jobs: (Integer app_id, ?from: String?, ?to: String?, ?range: String?) -> untyped
|
|
23
|
+
def list_job_metrics: (Integer app_id, String job_id) -> untyped
|
|
24
|
+
def get_job_metrics: (Integer app_id, String job_id, String metric_type, ?from: String?, ?to: String?, ?range: String?) -> untyped
|
|
25
|
+
def list_job_traces: (Integer app_id, String job_id, ?from: String?, ?to: String?, ?range: String?) -> untyped
|
|
21
26
|
def fetch_trace: (Integer app_id, Integer trace_id) -> Hash[String, untyped]
|
|
22
27
|
def list_error_groups: (Integer app_id, ?from: String?, ?to: String?, ?endpoint: String?) -> Hash[String, untyped]
|
|
23
28
|
def get_error_group: (Integer app_id, Integer error_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.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -305,6 +305,7 @@ files:
|
|
|
305
305
|
- lib/scout_apm_mcp/client.rb
|
|
306
306
|
- lib/scout_apm_mcp/errors.rb
|
|
307
307
|
- lib/scout_apm_mcp/helpers.rb
|
|
308
|
+
- lib/scout_apm_mcp/mcp_error_id_patch.rb
|
|
308
309
|
- lib/scout_apm_mcp/server.rb
|
|
309
310
|
- lib/scout_apm_mcp/version.rb
|
|
310
311
|
- sig/scout_apm_mcp.rbs
|