actionmcp 0.80.1 → 0.82.0

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: 6251e9ce3e87878bf86bb3ca1bbfd3363fb4fedccd9567a0b9b6c42fbc87241a
4
- data.tar.gz: ee8f4bdd5ad42c611f7f652f836e40ce0477510e9c0e856a0a32eaa447b77718
3
+ metadata.gz: b0ca96b3275c9dcece973bd75cc7eb4585fea06f6a99d8dec6a7603817c28b69
4
+ data.tar.gz: a188765f7176684ee0875a9eecf25cf07ff4c2338c93be7fc5fe29954f26bbb0
5
5
  SHA512:
6
- metadata.gz: 80712d1d14d91fb087bf8bd35c4cedb61ea22015a958dc5c66df64dfa7872d0755ea08a4ecbe02c1b577bb14eff8bcd30aaaa78ffba61c4b6e16ea37c336c313
7
- data.tar.gz: '0920ffdcb46a86910789282365a7ff6391ec4a1bf424be3dee7b8174497953df4aa73098e89a58559bb59d8157791d7620045339e763b3b27c885037b5e420fd'
6
+ metadata.gz: 678b6231e5c2df59dc9b90c26d5135cfb2ceb2aaa45ed55e774dc9700cf15f634d8a796fa2c9d3e2a78896bf21f41a86d303f2ca97db4445d289f8a24a8892b5
7
+ data.tar.gz: 2d1458519c3b69045a8f885be8da811544de78e793cc191ca2145d08ebc41a019a209b3f51fc9bcc9138e72b75f6b1fcc11428e01a06b1b6326f7bcc902d7dee
data/README.md CHANGED
@@ -154,9 +154,9 @@ class CalculateSumTool < ApplicationMCPTool
154
154
  render(text: "Calculating #{a} + #{b}...")
155
155
  render(text: "The sum is #{sum}")
156
156
 
157
- # You can render errors if needed
157
+ # You can report errors if needed
158
158
  if sum > 1000
159
- render(error: ["Warning: Sum exceeds recommended limit"])
159
+ report_error("Warning: Sum exceeds recommended limit")
160
160
  end
161
161
 
162
162
  # Or even images
@@ -279,6 +279,65 @@ end
279
279
 
280
280
  Resource templates are automatically registered and used when LLMs request resources matching their patterns.
281
281
 
282
+ ## 📚 Documentation
283
+
284
+ ActionMCP provides comprehensive documentation across multiple specialized guides. Each guide focuses on specific aspects to keep information organized and prevent context overload:
285
+
286
+ ### Getting Started & Setup
287
+ - **[Installation & Configuration](README.md#installation)** - Initial setup, database migrations, and basic configuration
288
+ - **[Authentication with Gateway](README.md#authentication-with-gateway)** - User authentication and authorization patterns
289
+
290
+ ### Component Development
291
+ - **[📋 TOOLS.MD](TOOLS.MD)** - Complete guide to developing MCP tools
292
+ - Generator usage and best practices
293
+ - Property definitions, validation, and consent management
294
+ - Output schemas for structured responses
295
+ - Error handling, testing, and security considerations
296
+ - Advanced features like additional properties and authentication context
297
+
298
+ - **[📝 PROMPTS.MD](PROMPTS.MD)** - Prompt template development guide
299
+ - Creating reusable prompt templates
300
+ - Multi-step conversations and mixed content types
301
+ - Argument validation and prompt chaining
302
+
303
+ - **[🔗 RESOURCE_TEMPLATES.md](RESOURCE_TEMPLATES.md)** - Resource template implementation
304
+ - URI template patterns and parameter extraction
305
+ - Dynamic resource resolution and collections
306
+ - Callbacks and validation patterns
307
+
308
+ ### Client & Integration
309
+ - **[🔌 CLIENTUSAGE.MD](CLIENTUSAGE.MD)** - Complete client implementation guide
310
+ - Session management and resumability
311
+ - Transport configuration and connection handling
312
+ - Tool, prompt, and resource collections
313
+ - Production deployment patterns
314
+
315
+ ### Protocol & Technical Details
316
+ - **[🚀 The Hitchhiker's Guide to MCP](The_Hitchhikers_Guide_to_MCP.md)** - Protocol versions and migration
317
+ - Comprehensive comparison of MCP protocol versions (2024-11-05, 2025-03-26, 2025-06-18)
318
+ - Design decisions and architectural rationale
319
+ - Migration paths and compatibility considerations
320
+ - Feature evolution and technical specifications (*Don't Panic!*)
321
+
322
+ ### Advanced Configuration
323
+ - **[Session Storage](README.md#session-storage)** - Volatile vs ActiveRecord vs custom session stores
324
+ - **[Thread Pool Management](README.md#thread-pool-management)** - Performance tuning and graceful shutdown
325
+ - **[Profiles System](README.md#profiles)** - Multi-tenant capability filtering
326
+ - **[Production Deployment](README.md#production-deployment-of-mcps0)** - Falcon, Unix sockets, and reverse proxy setup
327
+
328
+ ### Development & Testing
329
+ - **[Generators](README.md#generators)** - Rails generators for scaffolding components
330
+ - **[Testing with TestHelper](README.md#testing-with-testhelper)** - Comprehensive testing strategies
331
+ - **[Development Commands](README.md#development-commands)** - Rake tasks for debugging and inspection
332
+ - **[MCP Inspector Integration](README.md#inspecting-your-mcp-server)** - Interactive testing and validation
333
+
334
+ ### Troubleshooting & Production
335
+ - **[Error Handling](README.md#error-handling-and-troubleshooting)** - JSON-RPC error codes and debugging
336
+ - **[Production Considerations](README.md#production-considerations)** - Security, performance, and monitoring
337
+ - **[Middleware Conflicts](README.md#dealing-with-middleware-conflicts)** - Using `mcp_vanilla.ru` for production
338
+
339
+ > **💡 Pro Tip**: Start with the component-specific guides (TOOLS.MD, PROMPTS.MD, RESOURCE_TEMPLATES.md) for hands-on development, then reference the Hitchhiker's Guide for protocol details and CLIENTUSAGE.MD for integration patterns.
340
+
282
341
  ## Configuration
283
342
 
284
343
  ActionMCP is configured via `config.action_mcp` in your Rails application.
@@ -871,7 +930,7 @@ class MyTool < ApplicationMCPTool
871
930
  def perform
872
931
  # Check for error conditions and return clear messages
873
932
  if some_error_condition?
874
- render(error: ["Clear error message for the LLM"])
933
+ report_error("Clear error message for the LLM")
875
934
  return
876
935
  end
877
936
 
@@ -8,7 +8,6 @@ module ActionMCP
8
8
  include ActiveModel::Attributes
9
9
  include Callbacks
10
10
  include Instrumentation::Instrumentation
11
- include Logging
12
11
  include Renderable
13
12
 
14
13
  class_attribute :_capability_name, instance_accessor: false
@@ -13,7 +13,6 @@ module ActionMCP
13
13
  include Resources
14
14
  include Roots
15
15
  include Elicitation
16
- include Logging
17
16
 
18
17
  attr_reader :logger, :transport,
19
18
  :connection_error, :server,
@@ -71,7 +71,6 @@ module ActionMCP
71
71
  # Base class for transport implementations
72
72
  class TransportBase
73
73
  include Transport
74
- include Logging
75
74
 
76
75
  attr_reader :url, :options, :session_store
77
76
 
@@ -51,9 +51,9 @@ module ActionMCP
51
51
  :connects_to
52
52
 
53
53
  def initialize
54
- @logging_enabled = true
54
+ @logging_enabled = false
55
55
  @list_changed = true
56
- @logging_level = :info
56
+ @logging_level = :warning
57
57
  @resources_subscribe = false
58
58
  @elicitation_enabled = false
59
59
  @verbose_logging = false
@@ -269,8 +269,8 @@ module ActionMCP
269
269
  resources: [ "all" ],
270
270
  options: {
271
271
  list_changed: false,
272
- logging_enabled: true,
273
- logging_level: :info,
272
+ logging_enabled: false,
273
+ logging_level: :warning,
274
274
  resources_subscribe: false
275
275
  }
276
276
  },
@@ -53,6 +53,11 @@ module ActionMCP
53
53
  ActionMCP.configuration.load_profiles
54
54
  end
55
55
 
56
+ # Initialize MCP logging system
57
+ initializer "action_mcp.initialize_logging" do
58
+ ActionMCP::Logging.initialize_from_config!
59
+ end
60
+
56
61
 
57
62
  # Configure autoloading for the mcp/tools directory and identifiers
58
63
  initializer "action_mcp.autoloading", before: :set_autoload_paths do |app|
@@ -25,6 +25,9 @@ module ActionMCP
25
25
  TOOLS_LIST = "tools/list"
26
26
  TOOLS_CALL = "tools/call"
27
27
 
28
+ # Logging methods
29
+ LOGGING_SET_LEVEL = "logging/setLevel"
30
+
28
31
  # Notification methods
29
32
  NOTIFICATIONS_INITIALIZED = "notifications/initialized"
30
33
  NOTIFICATIONS_CANCELLED = "notifications/cancelled"
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Logging
5
+ # RFC 5424 log levels for MCP logging
6
+ # @see https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1
7
+ class Level
8
+ # Log levels in order of severity (ascending)
9
+ LEVELS = {
10
+ debug: 0,
11
+ info: 1,
12
+ notice: 2,
13
+ warning: 3,
14
+ error: 4,
15
+ critical: 5,
16
+ alert: 6,
17
+ emergency: 7
18
+ }.freeze
19
+
20
+ # Reverse mapping for converting integers back to symbols
21
+ LEVEL_NAMES = LEVELS.invert.freeze
22
+
23
+ class << self
24
+ # Check if a level is valid
25
+ # @param level [String, Symbol, Integer] The level to check
26
+ # @return [Boolean] true if valid, false otherwise
27
+ def valid?(level)
28
+ case level
29
+ when String, Symbol
30
+ LEVELS.key?(level.to_sym)
31
+ when Integer
32
+ LEVEL_NAMES.key?(level)
33
+ else
34
+ false
35
+ end
36
+ end
37
+
38
+ # Coerce a level to its integer value
39
+ # @param level [String, Symbol, Integer] The level to coerce
40
+ # @return [Integer] The integer severity value
41
+ # @raise [ArgumentError] if level is invalid
42
+ def coerce(level)
43
+ case level
44
+ when String, Symbol
45
+ symbol_level = level.to_sym
46
+ LEVELS.fetch(symbol_level) do
47
+ raise ArgumentError, "Invalid log level: #{level}. Valid levels: #{LEVELS.keys.join(', ')}"
48
+ end
49
+ when Integer
50
+ unless LEVEL_NAMES.key?(level)
51
+ raise ArgumentError, "Invalid log level: #{level}. Valid levels: 0-7"
52
+ end
53
+ level
54
+ else
55
+ raise ArgumentError, "Invalid log level type: #{level.class}. Expected String, Symbol, or Integer"
56
+ end
57
+ end
58
+
59
+ # Convert integer level back to symbol
60
+ # @param level_int [Integer] The integer level
61
+ # @return [Symbol] The symbol name
62
+ def name_for(level_int)
63
+ LEVEL_NAMES.fetch(level_int) do
64
+ raise ArgumentError, "Invalid log level integer: #{level_int}"
65
+ end
66
+ end
67
+
68
+ # Get all valid level names as symbols
69
+ # @return [Array<Symbol>] Array of level symbols
70
+ def all_levels
71
+ LEVELS.keys
72
+ end
73
+
74
+ # Check if level_a is more severe than level_b
75
+ # @param level_a [String, Symbol, Integer] First level
76
+ # @param level_b [String, Symbol, Integer] Second level
77
+ # @return [Boolean] true if level_a >= level_b in severity
78
+ def more_severe_or_equal?(level_a, level_b)
79
+ coerce(level_a) >= coerce(level_b)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Logging
5
+ # MCP Logger that sends notifications/message to the MCP client
6
+ class Logger
7
+ attr_reader :name, :session, :state
8
+
9
+ # Initialize a new MCP logger
10
+ # @param name [String, nil] Optional logger name
11
+ # @param session [ActionMCP::Session] The MCP session for transport
12
+ # @param state [ActionMCP::Logging::State] The global logging state
13
+ def initialize(name: nil, session:, state:)
14
+ @name = name
15
+ @session = session
16
+ @state = state
17
+ end
18
+
19
+ # Log a debug message
20
+ # @param message [String, nil] The message (if no block given)
21
+ # @param data [Object] Additional structured data
22
+ # @yield Block that returns the message (evaluated only if logging)
23
+ # @return [void]
24
+ def debug(message = nil, data: nil, &block)
25
+ log(:debug, message, data: data, &block)
26
+ end
27
+
28
+ # Log an info message
29
+ # @param message [String, nil] The message (if no block given)
30
+ # @param data [Object] Additional structured data
31
+ # @yield Block that returns the message (evaluated only if logging)
32
+ # @return [void]
33
+ def info(message = nil, data: nil, &block)
34
+ log(:info, message, data: data, &block)
35
+ end
36
+
37
+ # Log a notice message
38
+ # @param message [String, nil] The message (if no block given)
39
+ # @param data [Object] Additional structured data
40
+ # @yield Block that returns the message (evaluated only if logging)
41
+ # @return [void]
42
+ def notice(message = nil, data: nil, &block)
43
+ log(:notice, message, data: data, &block)
44
+ end
45
+
46
+ # Log a warning message
47
+ # @param message [String, nil] The message (if no block given)
48
+ # @param data [Object] Additional structured data
49
+ # @yield Block that returns the message (evaluated only if logging)
50
+ # @return [void]
51
+ def warning(message = nil, data: nil, &block)
52
+ log(:warning, message, data: data, &block)
53
+ end
54
+ alias_method :warn, :warning
55
+
56
+ # Log an error message
57
+ # @param message [String, nil] The message (if no block given)
58
+ # @param data [Object] Additional structured data
59
+ # @yield Block that returns the message (evaluated only if logging)
60
+ # @return [void]
61
+ def error(message = nil, data: nil, &block)
62
+ log(:error, message, data: data, &block)
63
+ end
64
+
65
+ # Log a critical message
66
+ # @param message [String, nil] The message (if no block given)
67
+ # @param data [Object] Additional structured data
68
+ # @yield Block that returns the message (evaluated only if logging)
69
+ # @return [void]
70
+ def critical(message = nil, data: nil, &block)
71
+ log(:critical, message, data: data, &block)
72
+ end
73
+
74
+ # Log an alert message
75
+ # @param message [String, nil] The message (if no block given)
76
+ # @param data [Object] Additional structured data
77
+ # @yield Block that returns the message (evaluated only if logging)
78
+ # @return [void]
79
+ def alert(message = nil, data: nil, &block)
80
+ log(:alert, message, data: data, &block)
81
+ end
82
+
83
+ # Log an emergency message
84
+ # @param message [String, nil] The message (if no block given)
85
+ # @param data [Object] Additional structured data
86
+ # @yield Block that returns the message (evaluated only if logging)
87
+ # @return [void]
88
+ def emergency(message = nil, data: nil, &block)
89
+ log(:emergency, message, data: data, &block)
90
+ end
91
+
92
+ # Check if debug level is enabled
93
+ # @return [Boolean] true if debug messages will be logged
94
+ def debug?
95
+ state.should_log?(:debug)
96
+ end
97
+
98
+ # Check if info level is enabled
99
+ # @return [Boolean] true if info messages will be logged
100
+ def info?
101
+ state.should_log?(:info)
102
+ end
103
+
104
+ # Check if notice level is enabled
105
+ # @return [Boolean] true if notice messages will be logged
106
+ def notice?
107
+ state.should_log?(:notice)
108
+ end
109
+
110
+ # Check if warning level is enabled
111
+ # @return [Boolean] true if warning messages will be logged
112
+ def warning?
113
+ state.should_log?(:warning)
114
+ end
115
+ alias_method :warn?, :warning?
116
+
117
+ # Check if error level is enabled
118
+ # @return [Boolean] true if error messages will be logged
119
+ def error?
120
+ state.should_log?(:error)
121
+ end
122
+
123
+ # Check if critical level is enabled
124
+ # @return [Boolean] true if critical messages will be logged
125
+ def critical?
126
+ state.should_log?(:critical)
127
+ end
128
+
129
+ # Check if alert level is enabled
130
+ # @return [Boolean] true if alert messages will be logged
131
+ def alert?
132
+ state.should_log?(:alert)
133
+ end
134
+
135
+ # Check if emergency level is enabled
136
+ # @return [Boolean] true if emergency messages will be logged
137
+ def emergency?
138
+ state.should_log?(:emergency)
139
+ end
140
+
141
+ private
142
+
143
+ # Core logging method
144
+ # @param level [Symbol] The log level
145
+ # @param message [String, nil] The message
146
+ # @param data [Object] Additional data
147
+ # @yield Block that returns message
148
+ # @return [void]
149
+ def log(level, message = nil, data: nil, &block)
150
+ return unless state.should_log?(level)
151
+
152
+ # Evaluate message from block if provided
153
+ final_message = if block_given?
154
+ yield
155
+ else
156
+ message
157
+ end
158
+
159
+ # Send MCP notification
160
+ send_mcp_notification(level, final_message, data)
161
+ end
162
+
163
+ # Send notifications/message to MCP client
164
+ # @param level [Symbol] The log level
165
+ # @param message [String] The message
166
+ # @param data [Object] Additional data
167
+ # @return [void]
168
+ def send_mcp_notification(level, message, data)
169
+ params = {
170
+ level: level.to_s,
171
+ data: build_log_data(message, data)
172
+ }
173
+
174
+ # Add logger name if present
175
+ params[:logger] = name if name
176
+
177
+ # Send via session's messaging service
178
+ session.messaging_service.send_notification("notifications/message", params)
179
+ rescue StandardError => e
180
+ # Fallback to Rails logger if MCP transport fails
181
+ Rails.logger.error "Failed to send MCP log notification: #{e.message}"
182
+ end
183
+
184
+ # Build the data payload for the log message
185
+ # @param message [String] The primary message
186
+ # @param additional_data [Object] Additional structured data
187
+ # @return [Object] The data to send in the notification
188
+ def build_log_data(message, additional_data)
189
+ case additional_data
190
+ when nil
191
+ message
192
+ when Hash
193
+ if message
194
+ { message: message }.merge(additional_data)
195
+ else
196
+ additional_data
197
+ end
198
+ else
199
+ if message
200
+ { message: message, data: additional_data }
201
+ else
202
+ additional_data
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Logging
5
+ # Mixin to provide easy MCP logging access for tools, prompts, and resources
6
+ module Mixin
7
+ extend ActiveSupport::Concern
8
+
9
+ # Get the MCP logger for this instance
10
+ # @return [ActionMCP::Logging::Logger, ActionMCP::Logging::NullLogger] logger instance
11
+ def mcp_logger
12
+ @mcp_logger ||= ActionMCP::Logging.logger_for_context(
13
+ name: logger_name,
14
+ execution_context: execution_context
15
+ )
16
+ end
17
+
18
+ # Log a debug message
19
+ # @param message [String, nil] The message (if no block given)
20
+ # @param data [Object] Additional structured data
21
+ # @yield Block that returns the message (evaluated only if logging)
22
+ # @return [void]
23
+ def mcp_debug(message = nil, data: nil, &block)
24
+ mcp_logger.debug(message, data: data, &block)
25
+ end
26
+
27
+ # Log an info message
28
+ # @param message [String, nil] The message (if no block given)
29
+ # @param data [Object] Additional structured data
30
+ # @yield Block that returns the message (evaluated only if logging)
31
+ # @return [void]
32
+ def mcp_info(message = nil, data: nil, &block)
33
+ mcp_logger.info(message, data: data, &block)
34
+ end
35
+
36
+ # Log a notice message
37
+ # @param message [String, nil] The message (if no block given)
38
+ # @param data [Object] Additional structured data
39
+ # @yield Block that returns the message (evaluated only if logging)
40
+ # @return [void]
41
+ def mcp_notice(message = nil, data: nil, &block)
42
+ mcp_logger.notice(message, data: data, &block)
43
+ end
44
+
45
+ # Log a warning message
46
+ # @param message [String, nil] The message (if no block given)
47
+ # @param data [Object] Additional structured data
48
+ # @yield Block that returns the message (evaluated only if logging)
49
+ # @return [void]
50
+ def mcp_warning(message = nil, data: nil, &block)
51
+ mcp_logger.warning(message, data: data, &block)
52
+ end
53
+ alias_method :mcp_warn, :mcp_warning
54
+
55
+ # Log an error message
56
+ # @param message [String, nil] The message (if no block given)
57
+ # @param data [Object] Additional structured data
58
+ # @yield Block that returns the message (evaluated only if logging)
59
+ # @return [void]
60
+ def mcp_error(message = nil, data: nil, &block)
61
+ mcp_logger.error(message, data: data, &block)
62
+ end
63
+
64
+ # Log a critical message
65
+ # @param message [String, nil] The message (if no block given)
66
+ # @param data [Object] Additional structured data
67
+ # @yield Block that returns the message (evaluated only if logging)
68
+ # @return [void]
69
+ def mcp_critical(message = nil, data: nil, &block)
70
+ mcp_logger.critical(message, data: data, &block)
71
+ end
72
+
73
+ # Log an alert message
74
+ # @param message [String, nil] The message (if no block given)
75
+ # @param data [Object] Additional structured data
76
+ # @yield Block that returns the message (evaluated only if logging)
77
+ # @return [void]
78
+ def mcp_alert(message = nil, data: nil, &block)
79
+ mcp_logger.alert(message, data: data, &block)
80
+ end
81
+
82
+ # Log an emergency message
83
+ # @param message [String, nil] The message (if no block given)
84
+ # @param data [Object] Additional structured data
85
+ # @yield Block that returns the message (evaluated only if logging)
86
+ # @return [void]
87
+ def mcp_emergency(message = nil, data: nil, &block)
88
+ mcp_logger.emergency(message, data: data, &block)
89
+ end
90
+
91
+ # Check if debug level is enabled
92
+ # @return [Boolean] true if debug messages will be logged
93
+ def mcp_debug?
94
+ mcp_logger.debug?
95
+ end
96
+
97
+ # Check if info level is enabled
98
+ # @return [Boolean] true if info messages will be logged
99
+ def mcp_info?
100
+ mcp_logger.info?
101
+ end
102
+
103
+ # Check if notice level is enabled
104
+ # @return [Boolean] true if notice messages will be logged
105
+ def mcp_notice?
106
+ mcp_logger.notice?
107
+ end
108
+
109
+ # Check if warning level is enabled
110
+ # @return [Boolean] true if warning messages will be logged
111
+ def mcp_warning?
112
+ mcp_logger.warning?
113
+ end
114
+ alias_method :mcp_warn?, :mcp_warning?
115
+
116
+ # Check if error level is enabled
117
+ # @return [Boolean] true if error messages will be logged
118
+ def mcp_error?
119
+ mcp_logger.error?
120
+ end
121
+
122
+ # Check if critical level is enabled
123
+ # @return [Boolean] true if critical messages will be logged
124
+ def mcp_critical?
125
+ mcp_logger.critical?
126
+ end
127
+
128
+ # Check if alert level is enabled
129
+ # @return [Boolean] true if alert messages will be logged
130
+ def mcp_alert?
131
+ mcp_logger.alert?
132
+ end
133
+
134
+ # Check if emergency level is enabled
135
+ # @return [Boolean] true if emergency messages will be logged
136
+ def mcp_emergency?
137
+ mcp_logger.emergency?
138
+ end
139
+
140
+ private
141
+
142
+ # Generate logger name from class name
143
+ # @return [String] the logger name
144
+ def logger_name
145
+ self.class.name&.underscore || "unknown"
146
+ end
147
+ end
148
+ end
149
+ end