scout_apm_mcp 0.1.0 → 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: 94c095f06ab792c7e66a4a3d7893a0476308730d1d346f661ba34d52be5b665b
4
- data.tar.gz: c1823c4b464ab833663a3026ff3f1c1d90de4c6c3b832f57257ce1f495c161e3
3
+ metadata.gz: fe2bbad61c1acff6ae4c31b52a7e820acdc64449be4ae998a574c1937ac4775c
4
+ data.tar.gz: a2c68cf35aa37975a99cf98b68fa29a91673c449f983bbb96547b6403df8401f
5
5
  SHA512:
6
- metadata.gz: adba47ba7ab8d8838e42c236afe334243425f38701da5467e78badd91195ed029dfe4f5c3b308903c67f118ef10778a5e14dbd4dd4f8feb9801a3778da4d20d0
7
- data.tar.gz: 657457f4239d0531bf7cac76fcd15e5ac9416947fd24d9ce83e38a74c47d0583a41b8194ae526d915ab540c0a3fb56c50cb0c111a22e5f9a23197c725550031a
6
+ metadata.gz: e591d27a417285348f7257f150aeb11a9fa4390227893fc996fadf65d08dd856fd4121f3780e7d5403ecb3e985d5c3f2389dce5a99c3d4f57ad9f47bb4123876
7
+ data.tar.gz: 6112164513df8a51d4e0c039f9245621869ea4533a54ad493df3ec3e4aa67b738fd4952a0507c740a35a1c8568705ed72f8c2cf9d927603bd85fb9080b671f46
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
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
+
12
+ ## 0.1.1 (2025-11-21)
13
+
14
+ - Fixed `NullLogger` missing `set_client_initialized` method that caused MCP initialization errors
15
+ - Added `set_client_initialized` method with optional argument to match fast-mcp logger interface
16
+
3
17
  ## 0.1.0 (2025-11-20)
4
18
 
5
19
  - Initial release
@@ -9,5 +23,5 @@
9
23
  - Support for environment variables and 1Password integration (via optional `opdotenv` gem)
10
24
  - Complete RBS type signatures for all public APIs
11
25
  - Comprehensive test suite with RSpec
12
- - Requires Ruby 3.0 or higher
26
+ - Requires Ruby 3.1 or higher
13
27
  - All dependencies use latest compatible versions with pessimistic versioning for security
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # scout_apm_mcp
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/scout_apm_mcp.svg)](https://badge.fury.io/rb/scout_apm_mcp) [![Test Status](https://github.com/amkisko/scout_apm_mcp.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/scout_apm_mcp.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/scout_apm_mcp.rb/graph/badge.svg?token=HVWDDNLEO5)](https://codecov.io/gh/amkisko/scout_apm_mcp.rb)
3
+ [![Gem Version](https://badge.fury.io/rb/scout_apm_mcp.svg?v=0.1.0)](https://badge.fury.io/rb/scout_apm_mcp) [![Test Status](https://github.com/amkisko/scout_apm_mcp.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/scout_apm_mcp.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/scout_apm_mcp.rb/graph/badge.svg?token=HVWDDNLEO5)](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
 
@@ -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
  }
@@ -316,7 +289,13 @@ Review policy:
316
289
  - It might take up to 6 calendar months to review and merge pull request
317
290
  - It might take up to 1 calendar year to review an issue
318
291
 
292
+ For more information, see [CONTRIBUTING.md](CONTRIBUTING.md).
293
+
294
+ ## Security
295
+
296
+ If you discover a security vulnerability, please report it responsibly. See [SECURITY.md](SECURITY.md) for details.
297
+
319
298
  ## License
320
299
 
321
- The gem is available as open source under the terms of the [MIT License](LICENSE.md).
300
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
322
301
 
@@ -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
@@ -82,6 +86,10 @@ module ScoutApmMcp
82
86
  @client_initialized
83
87
  end
84
88
 
89
+ def set_client_initialized(value = true)
90
+ @client_initialized = value
91
+ end
92
+
85
93
  def stdio_transport?
86
94
  @transport == :stdio
87
95
  end
@@ -137,6 +145,7 @@ module ScoutApmMcp
137
145
  server.register_tool(GetInsightsHistoryTool)
138
146
  server.register_tool(GetInsightsHistoryByTypeTool)
139
147
  server.register_tool(ParseScoutURLTool)
148
+ server.register_tool(FetchScoutURLTool)
140
149
  server.register_tool(FetchOpenAPISchemaTool)
141
150
  end
142
151
 
@@ -423,10 +432,10 @@ module ScoutApmMcp
423
432
 
424
433
  # Utility Tools
425
434
  class ParseScoutURLTool < BaseTool
426
- 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.)"
427
436
 
428
437
  arguments do
429
- 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)")
430
439
  end
431
440
 
432
441
  def call(url:)
@@ -434,6 +443,84 @@ module ScoutApmMcp
434
443
  end
435
444
  end
436
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
+
437
524
  class FetchOpenAPISchemaTool < BaseTool
438
525
  description "Fetch the ScoutAPM OpenAPI schema from the API and optionally validate it"
439
526
 
@@ -1,3 +1,3 @@
1
1
  module ScoutApmMcp
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.2"
3
3
  end
metadata CHANGED
@@ -1,29 +1,34 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout_apm_mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-11-20 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: fast-mcp
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - "~>"
16
+ - - ">="
18
17
  - !ruby/object:Gem::Version
19
18
  version: '0.1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.0'
20
22
  type: :runtime
21
23
  prerelease: false
22
24
  version_requirements: !ruby/object:Gem::Requirement
23
25
  requirements:
24
- - - "~>"
26
+ - - ">="
25
27
  - !ruby/object:Gem::Version
26
28
  version: '0.1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2.0'
27
32
  - !ruby/object:Gem::Dependency
28
33
  name: base64
29
34
  requirement: !ruby/object:Gem::Requirement
@@ -242,7 +247,6 @@ metadata:
242
247
  homepage_uri: https://github.com/amkisko/scout_apm_mcp.rb
243
248
  documentation_uri: https://github.com/amkisko/scout_apm_mcp.rb#readme
244
249
  rubygems_mfa_required: 'true'
245
- post_install_message:
246
250
  rdoc_options: []
247
251
  require_paths:
248
252
  - lib
@@ -257,8 +261,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
257
261
  - !ruby/object:Gem::Version
258
262
  version: '0'
259
263
  requirements: []
260
- rubygems_version: 3.5.9
261
- signing_key:
264
+ rubygems_version: 3.6.9
262
265
  specification_version: 4
263
266
  summary: ScoutAPM MCP (Model Context Protocol) server for Cursor IDE integration
264
267
  test_files: []