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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee786e2a50c8985e1423731c5acf85264c5dce178d366238a317685762170c57
4
- data.tar.gz: 8fda9d886c840af02bc5caeb0375b4d963bfef95c4c9c12492f415256a21d361
3
+ metadata.gz: fe2bbad61c1acff6ae4c31b52a7e820acdc64449be4ae998a574c1937ac4775c
4
+ data.tar.gz: a2c68cf35aa37975a99cf98b68fa29a91673c449f983bbb96547b6403df8401f
5
5
  SHA512:
6
- metadata.gz: 3434d01dfa3783ba3a1364caadd17aae11deb0170557fba1277d3b9b72e3706cbb5eed30a984ca9b0222f85933caff79242a05b19d400693e27b1b0a8adc9a08
7
- data.tar.gz: 87540aa4f25083d54b2aee4c9080c3b3544f3c87fe39462198949814ce12a82aae9dae266f73bbbf940ad0f643c78cf5435c40520a221d642b3859d8ab1b6683
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": "bundle",
68
- "args": ["exec", "scout_apm_mcp"],
69
- "cwd": "/path/to/your/project"
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
  }
@@ -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 trace URL and extract app_id, endpoint_id, and trace_id
66
+ # Parse a ScoutAPM URL and extract resource information
67
67
  #
68
- # @param url [String] Full ScoutAPM trace URL
69
- # @return [Hash] Hash containing :app_id, :endpoint_id, :trace_id, :query_params, and :decoded_endpoint
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
- # Extract from URL: /apps/{app_id}/endpoints/{endpoint_id}/trace/{trace_id}
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
- if app_index && endpoints_index && trace_index
82
- result[:app_id] = path_parts[app_index + 1].to_i
83
- result[:endpoint_id] = path_parts[endpoints_index + 1]
84
- result[:trace_id] = path_parts[trace_index + 1].to_i
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
- # Fallback: try to extract by position
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
@@ -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 trace URL and extract app_id, endpoint_id, and trace_id"
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 trace URL (e.g., https://scoutapm.com/apps/123/endpoints/.../trace/456)")
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
 
@@ -1,3 +1,3 @@
1
1
  module ScoutApmMcp
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
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.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov