scout_apm_mcp 0.1.1 → 0.1.2
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 +9 -0
- data/README.md +4 -31
- data/lib/scout_apm_mcp/client.rb +25 -1
- data/lib/scout_apm_mcp/helpers.rb +40 -15
- data/lib/scout_apm_mcp/server.rb +96 -13
- data/lib/scout_apm_mcp/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fe2bbad61c1acff6ae4c31b52a7e820acdc64449be4ae998a574c1937ac4775c
|
|
4
|
+
data.tar.gz: a2c68cf35aa37975a99cf98b68fa29a91673c449f983bbb96547b6403df8401f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e591d27a417285348f7257f150aeb11a9fa4390227893fc996fadf65d08dd856fd4121f3780e7d5403ecb3e985d5c3f2389dce5a99c3d4f57ad9f47bb4123876
|
|
7
|
+
data.tar.gz: 6112164513df8a51d4e0c039f9245621869ea4533a54ad493df3ec3e4aa67b738fd4952a0507c740a35a1c8568705ed72f8c2cf9d927603bd85fb9080b671f46
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.1.2 (2025-11-21)
|
|
4
|
+
|
|
5
|
+
- Enhanced SSL certificate handling with support for `SSL_CERT_FILE` environment variable and automatic fallback to system certificates
|
|
6
|
+
- Improved error handling for SSL verification failures with clearer error messages
|
|
7
|
+
- Extended `Helpers.parse_scout_url` to support parsing multiple URL types (endpoints, error_groups, insights, apps) beyond just traces
|
|
8
|
+
- Added `FetchScoutURLTool` MCP tool for automatically detecting and fetching data from any ScoutAPM URL
|
|
9
|
+
- Fixed MCP error handling to ensure error responses always have valid IDs for strict MCP client validation
|
|
10
|
+
- Improved URL parsing to return `url_type` field for better resource type detection
|
|
11
|
+
|
|
3
12
|
## 0.1.1 (2025-11-21)
|
|
4
13
|
|
|
5
14
|
- 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
|
}
|
data/lib/scout_apm_mcp/client.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "uri"
|
|
2
2
|
require "net/http"
|
|
3
|
+
require "openssl"
|
|
3
4
|
require "json"
|
|
4
5
|
require "base64"
|
|
5
6
|
|
|
@@ -254,6 +255,10 @@ module ScoutApmMcp
|
|
|
254
255
|
else
|
|
255
256
|
raise "API request failed: #{response.code} #{response.message}"
|
|
256
257
|
end
|
|
258
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
259
|
+
raise "SSL verification failed: #{e.message}. This may be due to system certificate configuration issues."
|
|
260
|
+
rescue => e
|
|
261
|
+
raise "Request failed: #{e.class} - #{e.message}"
|
|
257
262
|
end
|
|
258
263
|
|
|
259
264
|
private
|
|
@@ -264,9 +269,24 @@ module ScoutApmMcp
|
|
|
264
269
|
# @return [Net::HTTP] Configured HTTP client
|
|
265
270
|
def build_http_client(uri)
|
|
266
271
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
267
|
-
http.use_ssl = uri.scheme == "https"
|
|
268
272
|
http.read_timeout = 10
|
|
269
273
|
http.open_timeout = 10
|
|
274
|
+
|
|
275
|
+
if uri.scheme == "https"
|
|
276
|
+
http.use_ssl = true
|
|
277
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
278
|
+
|
|
279
|
+
# Set ca_file directly - this is the simplest and most reliable approach
|
|
280
|
+
# Try SSL_CERT_FILE first, then default cert file
|
|
281
|
+
ca_file = if ENV["SSL_CERT_FILE"] && File.file?(ENV["SSL_CERT_FILE"])
|
|
282
|
+
ENV["SSL_CERT_FILE"]
|
|
283
|
+
elsif File.exist?(OpenSSL::X509::DEFAULT_CERT_FILE)
|
|
284
|
+
OpenSSL::X509::DEFAULT_CERT_FILE
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
http.ca_file = ca_file if ca_file
|
|
288
|
+
end
|
|
289
|
+
|
|
270
290
|
http
|
|
271
291
|
end
|
|
272
292
|
|
|
@@ -297,6 +317,10 @@ module ScoutApmMcp
|
|
|
297
317
|
else
|
|
298
318
|
raise "API request failed: #{response.code} #{response.message}\n#{response.body}"
|
|
299
319
|
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}"
|
|
300
324
|
end
|
|
301
325
|
end
|
|
302
326
|
end
|
|
@@ -63,30 +63,55 @@ module ScoutApmMcp
|
|
|
63
63
|
"or provide OP_ENV_ENTRY_PATH, or op_vault and op_item parameters for 1Password integration"
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
# Parse a ScoutAPM
|
|
66
|
+
# Parse a ScoutAPM URL and extract resource information
|
|
67
67
|
#
|
|
68
|
-
# @param url [String] Full ScoutAPM
|
|
69
|
-
# @return [Hash] Hash containing
|
|
68
|
+
# @param url [String] Full ScoutAPM URL
|
|
69
|
+
# @return [Hash] Hash containing resource type and extracted IDs
|
|
70
|
+
# Possible keys: :url_type, :app_id, :endpoint_id, :trace_id, :error_id, :insight_type,
|
|
71
|
+
# :query_params, :decoded_endpoint
|
|
70
72
|
def self.parse_scout_url(url)
|
|
71
73
|
uri = URI.parse(url)
|
|
72
74
|
path_parts = uri.path.split("/").reject(&:empty?)
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
result = {}
|
|
75
77
|
app_index = path_parts.index("apps")
|
|
76
|
-
endpoints_index = path_parts.index("endpoints")
|
|
77
|
-
trace_index = path_parts.index("trace")
|
|
78
78
|
|
|
79
|
-
result
|
|
79
|
+
return result unless app_index
|
|
80
|
+
|
|
81
|
+
result[:app_id] = path_parts[app_index + 1].to_i
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
result[:
|
|
83
|
+
# Detect URL type and extract IDs
|
|
84
|
+
# Pattern: /apps/{app_id}/endpoints/{endpoint_id}/trace/{trace_id}
|
|
85
|
+
if path_parts.include?("trace")
|
|
86
|
+
result[:url_type] = :trace
|
|
87
|
+
endpoints_index = path_parts.index("endpoints")
|
|
88
|
+
trace_index = path_parts.index("trace")
|
|
89
|
+
if endpoints_index && trace_index
|
|
90
|
+
result[:endpoint_id] = path_parts[endpoints_index + 1]
|
|
91
|
+
result[:trace_id] = path_parts[trace_index + 1].to_i
|
|
92
|
+
end
|
|
93
|
+
# Pattern: /apps/{app_id}/endpoints/{endpoint_id}
|
|
94
|
+
elsif path_parts.include?("endpoints")
|
|
95
|
+
result[:url_type] = :endpoint
|
|
96
|
+
endpoints_index = path_parts.index("endpoints")
|
|
97
|
+
result[:endpoint_id] = path_parts[endpoints_index + 1] if endpoints_index
|
|
98
|
+
# Pattern: /apps/{app_id}/error_groups/{error_id}
|
|
99
|
+
elsif path_parts.include?("error_groups")
|
|
100
|
+
result[:url_type] = :error_group
|
|
101
|
+
error_groups_index = path_parts.index("error_groups")
|
|
102
|
+
result[:error_id] = path_parts[error_groups_index + 1].to_i if error_groups_index
|
|
103
|
+
# Pattern: /apps/{app_id}/insights or /apps/{app_id}/insights/{insight_type}
|
|
104
|
+
elsif path_parts.include?("insights")
|
|
105
|
+
result[:url_type] = :insight
|
|
106
|
+
insights_index = path_parts.index("insights")
|
|
107
|
+
if insights_index && path_parts.length > insights_index + 1
|
|
108
|
+
result[:insight_type] = path_parts[insights_index + 1]
|
|
109
|
+
end
|
|
110
|
+
# Pattern: /apps/{app_id}
|
|
111
|
+
elsif path_parts.length == 2 && path_parts[0] == "apps"
|
|
112
|
+
result[:url_type] = :app
|
|
85
113
|
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"
|
|
114
|
+
result[:url_type] = :unknown
|
|
90
115
|
end
|
|
91
116
|
|
|
92
117
|
# Parse query parameters
|
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
|
|
|
@@ -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,84 @@ 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
|
+
endpoint_data = client.get_endpoint(parsed[:app_id], parsed[:endpoint_id])
|
|
472
|
+
result[:data][:endpoint] = endpoint_data
|
|
473
|
+
result[:data][:decoded_endpoint] = parsed[:decoded_endpoint]
|
|
474
|
+
end
|
|
475
|
+
else
|
|
476
|
+
raise "Invalid trace URL: missing app_id or trace_id"
|
|
477
|
+
end
|
|
478
|
+
when :endpoint
|
|
479
|
+
if parsed[:app_id] && parsed[:endpoint_id]
|
|
480
|
+
endpoint_data = client.get_endpoint(parsed[:app_id], parsed[:endpoint_id])
|
|
481
|
+
result[:data] = {
|
|
482
|
+
endpoint: endpoint_data,
|
|
483
|
+
decoded_endpoint: parsed[:decoded_endpoint]
|
|
484
|
+
}
|
|
485
|
+
else
|
|
486
|
+
raise "Invalid endpoint URL: missing app_id or endpoint_id"
|
|
487
|
+
end
|
|
488
|
+
when :error_group
|
|
489
|
+
if parsed[:app_id] && parsed[:error_id]
|
|
490
|
+
error_data = client.get_error_group(parsed[:app_id], parsed[:error_id])
|
|
491
|
+
result[:data] = {error_group: error_data}
|
|
492
|
+
else
|
|
493
|
+
raise "Invalid error group URL: missing app_id or error_id"
|
|
494
|
+
end
|
|
495
|
+
when :insight
|
|
496
|
+
if parsed[:app_id]
|
|
497
|
+
if parsed[:insight_type]
|
|
498
|
+
insight_data = client.get_insight_by_type(parsed[:app_id], parsed[:insight_type])
|
|
499
|
+
result[:data] = {insight: insight_data, insight_type: parsed[:insight_type]}
|
|
500
|
+
else
|
|
501
|
+
insights_data = client.get_all_insights(parsed[:app_id])
|
|
502
|
+
result[:data] = {insights: insights_data}
|
|
503
|
+
end
|
|
504
|
+
else
|
|
505
|
+
raise "Invalid insight URL: missing app_id"
|
|
506
|
+
end
|
|
507
|
+
when :app
|
|
508
|
+
if parsed[:app_id]
|
|
509
|
+
app_data = client.get_app(parsed[:app_id])
|
|
510
|
+
result[:data] = {app: app_data}
|
|
511
|
+
else
|
|
512
|
+
raise "Invalid app URL: missing app_id"
|
|
513
|
+
end
|
|
514
|
+
when :unknown
|
|
515
|
+
raise "Unknown or unsupported ScoutAPM URL format: #{url}"
|
|
516
|
+
else
|
|
517
|
+
raise "Unable to determine URL type from: #{url}"
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
result
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
441
524
|
class FetchOpenAPISchemaTool < BaseTool
|
|
442
525
|
description "Fetch the ScoutAPM OpenAPI schema from the API and optionally validate it"
|
|
443
526
|
|