actionmcp 0.60.2 → 0.71.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +46 -59
  3. data/app/controllers/action_mcp/application_controller.rb +95 -28
  4. data/app/controllers/action_mcp/oauth/metadata_controller.rb +13 -13
  5. data/app/controllers/action_mcp/oauth/registration_controller.rb +206 -0
  6. data/app/models/action_mcp/oauth_client.rb +157 -0
  7. data/app/models/action_mcp/oauth_token.rb +141 -0
  8. data/app/models/action_mcp/session/message.rb +12 -12
  9. data/app/models/action_mcp/session/resource.rb +2 -2
  10. data/app/models/action_mcp/session/sse_event.rb +2 -2
  11. data/app/models/action_mcp/session/subscription.rb +2 -2
  12. data/app/models/action_mcp/session.rb +68 -43
  13. data/config/routes.rb +1 -0
  14. data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +42 -0
  15. data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +37 -0
  16. data/lib/action_mcp/capability.rb +2 -0
  17. data/lib/action_mcp/client/json_rpc_handler.rb +9 -9
  18. data/lib/action_mcp/client/jwt_client_provider.rb +134 -0
  19. data/lib/action_mcp/configuration.rb +90 -11
  20. data/lib/action_mcp/engine.rb +26 -1
  21. data/lib/action_mcp/filtered_logger.rb +32 -0
  22. data/lib/action_mcp/oauth/active_record_storage.rb +183 -0
  23. data/lib/action_mcp/oauth/memory_storage.rb +23 -1
  24. data/lib/action_mcp/oauth/middleware.rb +33 -0
  25. data/lib/action_mcp/oauth/provider.rb +49 -13
  26. data/lib/action_mcp/oauth.rb +12 -0
  27. data/lib/action_mcp/prompt.rb +14 -0
  28. data/lib/action_mcp/registry_base.rb +25 -4
  29. data/lib/action_mcp/resource_response.rb +110 -0
  30. data/lib/action_mcp/resource_template.rb +30 -2
  31. data/lib/action_mcp/server/capabilities.rb +3 -14
  32. data/lib/action_mcp/server/memory_session.rb +0 -1
  33. data/lib/action_mcp/server/prompts.rb +8 -1
  34. data/lib/action_mcp/server/resources.rb +9 -6
  35. data/lib/action_mcp/server/tools.rb +41 -20
  36. data/lib/action_mcp/server.rb +6 -3
  37. data/lib/action_mcp/sse_listener.rb +0 -7
  38. data/lib/action_mcp/test_helper.rb +5 -0
  39. data/lib/action_mcp/tool.rb +108 -4
  40. data/lib/action_mcp/tools_registry.rb +3 -0
  41. data/lib/action_mcp/version.rb +1 -1
  42. data/lib/generators/action_mcp/install/templates/mcp.yml +16 -16
  43. data/lib/tasks/action_mcp_tasks.rake +238 -0
  44. metadata +11 -1
@@ -26,6 +26,8 @@ module ActionMCP
26
26
 
27
27
  def abstract!
28
28
  @abstract = true
29
+ # Unregister from the appropriate registry if already registered
30
+ ActionMCP::ResourceTemplatesRegistry.unregister(self) if ActionMCP::ResourceTemplatesRegistry.items.values.include?(self)
29
31
  end
30
32
 
31
33
  def inherited(subclass)
@@ -33,6 +35,11 @@ module ActionMCP
33
35
  subclass.instance_variable_set(:@abstract, false)
34
36
  # Create a copy of validation requirements for subclasses
35
37
  subclass.instance_variable_set(:@required_parameters, [])
38
+
39
+ # Run the ActiveSupport load hook when a resource template is defined
40
+ subclass.class_eval do
41
+ ActiveSupport.run_load_hooks(:action_mcp_resource_template, subclass)
42
+ end
36
43
  end
37
44
 
38
45
  # Track required parameters for validation
@@ -109,6 +116,7 @@ module ActionMCP
109
116
  end
110
117
 
111
118
  def capability_name
119
+ return "" if name.nil?
112
120
  @capability_name ||= name.demodulize.underscore.sub(/_template$/, "")
113
121
  end
114
122
 
@@ -245,9 +253,29 @@ module ActionMCP
245
253
  end
246
254
 
247
255
  def call
248
- run_callbacks :resolve do
249
- resolve
256
+ @response = ResourceResponse.new
257
+
258
+ # Validate parameters first
259
+ unless valid?
260
+ missing_params = errors.full_messages
261
+ @response.mark_as_parameter_validation_failed!(missing_params, "template://#{self.class.name}")
262
+ return @response
263
+ end
264
+
265
+ begin
266
+ run_callbacks :resolve do
267
+ result = resolve
268
+ if result.nil?
269
+ @response.mark_as_not_found!("template://#{self.class.name}")
270
+ else
271
+ @response.add_content(result)
272
+ end
273
+ end
274
+ rescue StandardError => e
275
+ @response.mark_as_resolution_failed!("template://#{self.class.name}", e.message)
250
276
  end
277
+
278
+ @response
251
279
  end
252
280
  end
253
281
  end
@@ -18,7 +18,7 @@ module ActionMCP
18
18
  unless client_protocol_version.is_a?(String) && client_protocol_version.present?
19
19
  return send_jsonrpc_error(request_id, :invalid_params, "Missing or invalid 'protocolVersion'")
20
20
  end
21
- unless ActionMCP.configuration.vibed_ignore_version || ActionMCP::SUPPORTED_VERSIONS.include?(client_protocol_version)
21
+ unless ActionMCP::SUPPORTED_VERSIONS.include?(client_protocol_version)
22
22
  error_message = "Unsupported protocol version. Client requested '#{client_protocol_version}' but server supports #{ActionMCP::SUPPORTED_VERSIONS.join(', ')}"
23
23
  error_data = {
24
24
  supported: ActionMCP::SUPPORTED_VERSIONS,
@@ -40,18 +40,11 @@ module ActionMCP
40
40
  if existing_session && existing_session.initialized?
41
41
  # Resume existing session - update transport reference
42
42
  transport.instance_variable_set(:@session, existing_session)
43
- Rails.logger.info("Resuming existing session: #{session_id}")
44
43
 
45
44
  # Return existing session info
46
45
  capabilities_payload = existing_session.server_capabilities_payload
47
- capabilities_payload[:protocolVersion] = if ActionMCP.configuration.vibed_ignore_version
48
- ActionMCP::LATEST_VERSION
49
- else
50
- client_protocol_version
51
- end
46
+ capabilities_payload[:protocolVersion] = client_protocol_version
52
47
  return send_jsonrpc_response(request_id, result: capabilities_payload)
53
- else
54
- Rails.logger.warn("Session #{session_id} not found or not initialized, creating new session")
55
48
  end
56
49
  end
57
50
 
@@ -65,11 +58,7 @@ module ActionMCP
65
58
  end
66
59
 
67
60
  capabilities_payload = session.server_capabilities_payload
68
- capabilities_payload[:protocolVersion] = if ActionMCP.configuration.vibed_ignore_version
69
- ActionMCP::LATEST_VERSION
70
- else
71
- client_protocol_version
72
- end
61
+ capabilities_payload[:protocolVersion] = client_protocol_version
73
62
 
74
63
  send_jsonrpc_response(request_id, result: capabilities_payload)
75
64
  end
@@ -182,7 +182,6 @@ module ActionMCP
182
182
  end
183
183
 
184
184
  def set_protocol_version(version)
185
- version = ActionMCP::LATEST_VERSION if ActionMCP.configuration.vibed_ignore_version
186
185
  self.protocol_version = version
187
186
  save
188
187
  end
@@ -18,7 +18,14 @@ module ActionMCP
18
18
  prompt = prompt_class.new(params)
19
19
  prompt.with_context({ session: session })
20
20
 
21
- result = prompt.call
21
+ # Wrap prompt execution with Rails reloader for development
22
+ result = if Rails.env.development? && defined?(Rails.application.reloader)
23
+ Rails.application.reloader.wrap do
24
+ prompt.call
25
+ end
26
+ else
27
+ prompt.call
28
+ end
22
29
 
23
30
  if result.is_error
24
31
  send_jsonrpc_response(request_id, error: result)
@@ -50,12 +50,15 @@ module ActionMCP
50
50
  record = template.process(params[:uri])
51
51
  record.with_context({ session: session })
52
52
 
53
- if (resource = record.call)
54
- resource = [ resource ] unless resource.respond_to?(:each)
55
- content = resource.map(&:to_h)
56
- send_jsonrpc_response(id, result: { contents: content })
53
+ response = record.call
54
+
55
+ if response.error?
56
+ # Convert ResourceResponse errors to JSON-RPC errors
57
+ error_info = response.to_h
58
+ send_jsonrpc_error(id, error_info[:code], error_info[:message], error_info[:data])
57
59
  else
58
- send_jsonrpc_error(id, :invalid_params, "Resource not found")
60
+ # Handle successful response - ResourceResponse.contents is already an array
61
+ send_jsonrpc_response(id, result: { contents: response.contents.map(&:to_h) })
59
62
  end
60
63
  else
61
64
  send_jsonrpc_error(id, :invalid_params, "Invalid resource URI")
@@ -72,7 +75,7 @@ module ActionMCP
72
75
  # @example Output:
73
76
  # # Logs: "Registered Resource Templates: ["db://{table}", "file://{path}"]"
74
77
  def log_resource_templates
75
- Rails.logger.info("Registered Resource Templates: #{ActionMCP::ResourceTemplatesRegistry.resource_templates.keys}")
78
+ # Resource templates: #{ActionMCP::ResourceTemplatesRegistry.resource_templates.keys}
76
79
  end
77
80
  end
78
81
  end
@@ -18,7 +18,9 @@ module ActionMCP
18
18
  end
19
19
 
20
20
  # Use session's registered tools instead of global registry
21
- tools = session.registered_tools.map do |tool_class|
21
+ registered_tools = session.registered_tools
22
+
23
+ tools = registered_tools.map do |tool_class|
22
24
  tool_class.to_h(protocol_version: protocol_version)
23
25
  end
24
26
 
@@ -39,28 +41,47 @@ module ActionMCP
39
41
  tool_class = session.registered_tools.find { |t| t.tool_name == tool_name }
40
42
 
41
43
  if tool_class
42
- # Create tool and set execution context with request info
43
- tool = tool_class.new(arguments)
44
- tool.with_context({
45
- session: session,
46
- request: {
47
- params: {
48
- name: tool_name,
49
- arguments: arguments,
50
- _meta: _meta
44
+ begin
45
+ # Create tool and set execution context with request info
46
+ tool = tool_class.new(arguments)
47
+ tool.with_context({
48
+ session: session,
49
+ request: {
50
+ params: {
51
+ name: tool_name,
52
+ arguments: arguments,
53
+ _meta: _meta
54
+ }
51
55
  }
52
- }
53
- })
56
+ })
57
+
58
+ # Wrap tool execution with Rails reloader for development
59
+ result = if Rails.env.development?
60
+ # Preserve Current attributes across reloader boundary
61
+ current_user = ActionMCP::Current.user
62
+ current_gateway = ActionMCP::Current.gateway
54
63
 
55
- result = tool.call
64
+ Rails.application.reloader.wrap do
65
+ # Restore Current attributes inside reloader
66
+ ActionMCP::Current.user = current_user
67
+ ActionMCP::Current.gateway = current_gateway
68
+ tool.call
69
+ end
70
+ else
71
+ tool.call
72
+ end
56
73
 
57
- if result.is_error
58
- # Convert ToolResponse error to proper JSON-RPC error format
59
- # Pass the error hash directly - the Response class will handle it
60
- error_hash = result.to_h
61
- send_jsonrpc_response(request_id, error: error_hash)
62
- else
63
- send_jsonrpc_response(request_id, result: result)
74
+ if result.is_error
75
+ # Convert ToolResponse error to proper JSON-RPC error format
76
+ # Pass the error hash directly - the Response class will handle it
77
+ error_hash = result.to_h
78
+ send_jsonrpc_response(request_id, error: error_hash)
79
+ else
80
+ send_jsonrpc_response(request_id, result: result)
81
+ end
82
+ rescue ArgumentError => e
83
+ # Handle parameter validation errors
84
+ send_jsonrpc_error(request_id, :invalid_params, e.message)
64
85
  end
65
86
  else
66
87
  send_jsonrpc_error(request_id, :method_not_found, "Tool '#{tool_name}' not available in this session")
@@ -30,9 +30,12 @@ module ActionMCP
30
30
 
31
31
  # Access the session store
32
32
  def session_store
33
- @session_store ||= SessionStoreFactory.create(
34
- ActionMCP.configuration.server_session_store_type
35
- )
33
+ current_type = ActionMCP.configuration.server_session_store_type
34
+ if @session_store.nil? || @session_store_type != current_type
35
+ @session_store_type = current_type
36
+ @session_store = SessionStoreFactory.create(current_type)
37
+ end
38
+ @session_store
36
39
  end
37
40
 
38
41
  # Available pubsub adapter types
@@ -19,10 +19,7 @@ module ActionMCP
19
19
  # @yield [Hash] Yields parsed message received from the pub/sub channel
20
20
  # @return [Boolean] True if subscription was successful within timeout, false otherwise.
21
21
  def start(&callback)
22
- Rails.logger.debug "SSEListener: Starting for channel: #{session_key}"
23
-
24
22
  success_callback = lambda {
25
- Rails.logger.info "SSEListener: Successfully subscribed to channel: #{session_key}"
26
23
  @subscription_active.make_true
27
24
  }
28
25
 
@@ -41,7 +38,6 @@ module ActionMCP
41
38
  return if @stopped.true?
42
39
 
43
40
  @stopped.make_true
44
- Rails.logger.debug "SSEListener: Stopping listener for channel: #{session_key}"
45
41
  end
46
42
 
47
43
  private
@@ -50,8 +46,6 @@ module ActionMCP
50
46
  return if @stopped.true?
51
47
 
52
48
  begin
53
- Rails.logger.debug "SSEListener: Received raw message of type: #{raw_message.class}"
54
-
55
49
  # Check if the message is a valid JSON string or has a message attribute
56
50
  if raw_message.is_a?(String) && valid_json_format?(raw_message)
57
51
  message = MultiJson.load(raw_message)
@@ -80,7 +74,6 @@ module ActionMCP
80
74
  begin
81
75
  subscription_future.value(5) || @subscription_active.true?
82
76
  rescue Concurrent::TimeoutError
83
- Rails.logger.warn "SSEListener: Timed out waiting for subscription for #{session_key}"
84
77
  false
85
78
  end
86
79
  end
@@ -49,6 +49,11 @@ module ActionMCP
49
49
  end
50
50
  alias execute_tool execute_mcp_tool
51
51
 
52
+ def execute_mcp_tool_with_error(name, args = {})
53
+ ActionMCP::ToolsRegistry.tool_call(name, args)
54
+ end
55
+ alias execute_tool_with_error execute_mcp_tool_with_error
56
+
52
57
  def execute_mcp_prompt(name, args = {})
53
58
  resp = ActionMCP::PromptsRegistry.prompt_call(name, args)
54
59
  assert !resp.is_error, "Prompt #{name.inspect} returned error: #{resp.to_h[:message]}"
@@ -43,6 +43,7 @@ module ActionMCP
43
43
  #
44
44
  # @return [String] The default tool name.
45
45
  def self.default_tool_name
46
+ return "" if name.nil?
46
47
  name.demodulize.underscore.sub(/_tool$/, "")
47
48
  end
48
49
 
@@ -53,6 +54,19 @@ module ActionMCP
53
54
  :tool
54
55
  end
55
56
 
57
+ def unregister_from_registry
58
+ ActionMCP::ToolsRegistry.unregister(self) if ActionMCP::ToolsRegistry.items.values.include?(self)
59
+ end
60
+
61
+ # Hook called when a class inherits from Tool
62
+ def inherited(subclass)
63
+ super
64
+ # Run the ActiveSupport load hook when a tool is defined
65
+ subclass.class_eval do
66
+ ActiveSupport.run_load_hooks(:action_mcp_tool, subclass)
67
+ end
68
+ end
69
+
56
70
  def annotate(key, value)
57
71
  self._annotations = _annotations.merge(key.to_s => value)
58
72
  end
@@ -162,7 +176,7 @@ module ActionMCP
162
176
 
163
177
  return unless %w[number integer].include?(type)
164
178
 
165
- validates prop_name, numericality: true, allow_nil: true
179
+ validates prop_name, numericality: true, allow_nil: !required
166
180
  end
167
181
 
168
182
  # --------------------------------------------------------------------------
@@ -243,6 +257,13 @@ module ActionMCP
243
257
  # Instance Methods
244
258
  # --------------------------------------------------------------------------
245
259
 
260
+ # Override initialize to validate parameters before ActiveModel conversion
261
+ def initialize(attributes = {})
262
+ # Validate parameters before ActiveModel processes them
263
+ validate_parameter_types(attributes)
264
+ super
265
+ end
266
+
246
267
  # Public entry point for executing the tool
247
268
  # Returns an array of Content objects collected from render calls
248
269
  def call
@@ -259,14 +280,16 @@ module ActionMCP
259
280
  @response.mark_as_error!(:internal_error, message: e.message)
260
281
  end
261
282
  else
262
- @response.mark_as_error!(:invalid_request,
283
+ @response.mark_as_error!(:invalid_params,
263
284
  message: "Invalid input",
264
285
  data: errors.full_messages)
265
286
  end
266
287
 
267
288
  # If callbacks halted execution (`performed` still false) and
268
- # nothing else marked an error, surface it as invalid_request.
269
- @response.mark_as_error!(:invalid_request, message: "Aborted by callback") if !performed && !@response.error?
289
+ # nothing else marked an error, surface it as invalid_params.
290
+ if !performed && !@response.error?
291
+ @response.mark_as_error!(:invalid_params, message: "Tool execution was aborted")
292
+ end
270
293
 
271
294
  @response
272
295
  end
@@ -348,5 +371,86 @@ module ActionMCP
348
371
  end
349
372
 
350
373
  private_class_method :map_json_type_to_active_model_type
374
+
375
+ private
376
+
377
+ # Validates parameter types before ActiveModel conversion
378
+ def validate_parameter_types(attributes)
379
+ return unless attributes.is_a?(Hash)
380
+
381
+ attributes.each do |key, value|
382
+ key_str = key.to_s
383
+ property_schema = self.class._schema_properties[key_str]
384
+
385
+ next unless property_schema
386
+
387
+ expected_type = property_schema[:type]
388
+
389
+ # Skip validation if value is nil and property is not required
390
+ next if value.nil? && !self.class._required_properties.include?(key_str)
391
+
392
+ # Validate based on expected JSON Schema type
393
+ case expected_type
394
+ when "number"
395
+ validate_number_parameter(key_str, value)
396
+ when "integer"
397
+ validate_integer_parameter(key_str, value)
398
+ when "string"
399
+ validate_string_parameter(key_str, value)
400
+ when "boolean"
401
+ validate_boolean_parameter(key_str, value)
402
+ when "array"
403
+ validate_array_parameter(key_str, value, property_schema)
404
+ end
405
+ end
406
+ end
407
+
408
+ def validate_number_parameter(key, value)
409
+ return if value.is_a?(Numeric)
410
+
411
+ if value.is_a?(String)
412
+ # Check if string can be converted to a valid number
413
+ begin
414
+ Float(value)
415
+ rescue ArgumentError, TypeError
416
+ raise ArgumentError, "Parameter '#{key}' must be a valid number, got: #{value.inspect}"
417
+ end
418
+ else
419
+ raise ArgumentError, "Parameter '#{key}' must be a number, got: #{value.class}"
420
+ end
421
+ end
422
+
423
+ def validate_integer_parameter(key, value)
424
+ return if value.is_a?(Integer)
425
+
426
+ if value.is_a?(String)
427
+ # Check if string can be converted to a valid integer
428
+ begin
429
+ Integer(value)
430
+ rescue ArgumentError, TypeError
431
+ raise ArgumentError, "Parameter '#{key}' must be a valid integer, got: #{value.inspect}"
432
+ end
433
+ else
434
+ raise ArgumentError, "Parameter '#{key}' must be an integer, got: #{value.class}"
435
+ end
436
+ end
437
+
438
+ def validate_string_parameter(key, value)
439
+ return if value.is_a?(String)
440
+
441
+ raise ArgumentError, "Parameter '#{key}' must be a string, got: #{value.class}"
442
+ end
443
+
444
+ def validate_boolean_parameter(key, value)
445
+ return if value.is_a?(TrueClass) || value.is_a?(FalseClass)
446
+
447
+ raise ArgumentError, "Parameter '#{key}' must be a boolean, got: #{value.class}"
448
+ end
449
+
450
+ def validate_array_parameter(key, value, property_schema)
451
+ return if value.is_a?(Array)
452
+
453
+ raise ArgumentError, "Parameter '#{key}' must be an array, got: #{value.class}"
454
+ end
351
455
  end
352
456
  end
@@ -22,6 +22,9 @@ module ActionMCP
22
22
  tool.call
23
23
  rescue RegistryBase::NotFound
24
24
  error_response(:invalid_params, message: "Tool not found: #{tool_name}")
25
+ rescue ArgumentError => e
26
+ # Handle parameter validation errors
27
+ error_response(:invalid_params, message: e.message)
25
28
  rescue StandardError => e
26
29
  # FIXME, we should maybe not return the error message to the user
27
30
  error_response(:invalid_params, message: "Tool execution failed: #{e.message}")
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.60.2"
5
+ VERSION = "0.71.0"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
@@ -5,17 +5,17 @@
5
5
  shared:
6
6
  # Authentication configuration - array of methods to try in order
7
7
  authentication: ["none"] # No authentication required by default
8
-
8
+
9
9
  # Session store configuration
10
10
  # Global session store type used by both client and server (default: volatile in dev/test, active_record in production)
11
11
  # session_store_type: volatile
12
-
12
+
13
13
  # Client-specific session store type (falls back to session_store_type if not specified)
14
14
  # client_session_store_type: volatile
15
-
15
+
16
16
  # Server-specific session store type (falls back to session_store_type if not specified)
17
17
  # server_session_store_type: active_record
18
-
18
+
19
19
  # OAuth configuration (if using OAuth authentication)
20
20
  # oauth:
21
21
  # provider: "demo_oauth_provider"
@@ -24,7 +24,7 @@ shared:
24
24
  # enable_token_revocation: true
25
25
  # pkce_required: true
26
26
  # issuer_url: https://yourapp.com
27
-
27
+
28
28
  # MCP capability profiles
29
29
  profiles:
30
30
  primary:
@@ -38,7 +38,7 @@ shared:
38
38
  list_changed: false
39
39
  logging_enabled: true
40
40
  resources_subscribe: false
41
-
41
+
42
42
  minimal:
43
43
  tools: []
44
44
  prompts: []
@@ -53,13 +53,13 @@ shared:
53
53
  development:
54
54
  # Use simple pub/sub adapter for development
55
55
  adapter: simple
56
-
56
+
57
57
  # Session store examples for development
58
58
  # Use volatile client sessions for faster development
59
59
  # client_session_store_type: volatile
60
60
  # Use persistent server sessions to maintain state across restarts
61
61
  # server_session_store_type: active_record
62
-
62
+
63
63
  # Thread pool configuration (optional)
64
64
  # min_threads: 5 # Minimum number of threads in the pool
65
65
  # max_threads: 10 # Maximum number of threads in the pool
@@ -69,10 +69,10 @@ development:
69
69
  test:
70
70
  # JWT authentication for testing environment
71
71
  authentication: ["jwt"]
72
-
72
+
73
73
  # Test adapter for testing
74
74
  adapter: test
75
-
75
+
76
76
  # Use volatile sessions for testing (fast cleanup)
77
77
  # session_store_type: volatile
78
78
 
@@ -80,7 +80,7 @@ test:
80
80
  production:
81
81
  # Multiple authentication methods - try OAuth first, fallback to JWT
82
82
  authentication: ["oauth", "jwt"]
83
-
83
+
84
84
  # OAuth configuration for production
85
85
  oauth:
86
86
  provider: "application_oauth_provider" # Your custom provider class
@@ -89,29 +89,29 @@ production:
89
89
  enable_token_revocation: true
90
90
  pkce_required: true
91
91
  issuer_url: https://yourapp.com
92
-
92
+
93
93
  # Additional production profiles for external clients
94
94
  profiles:
95
95
  external_clients:
96
96
  tools: ["WeatherForecastTool"] # Limited tool access for external clients
97
97
  prompts: []
98
98
  resources: []
99
-
99
+
100
100
  # Production session store configuration
101
101
  # Use persistent storage for production reliability
102
102
  # session_store_type: active_record
103
103
  # Or configure separately:
104
104
  # client_session_store_type: active_record # Client connections persist across restarts
105
105
  # server_session_store_type: active_record # Server state persists across deployments
106
-
106
+
107
107
  # Choose one of the following adapters:
108
-
108
+
109
109
  # 1. Database-backed adapter (recommended)
110
110
  adapter: solid_mcp
111
111
  polling_interval: 0.5.seconds
112
112
  batch_size: 200 # Number of messages to write in a single batch
113
113
  flush_interval: 0.05 # Seconds between batch flushes
114
-
114
+
115
115
  # Thread pool configuration (optional)
116
116
  min_threads: 10 # Minimum number of threads in the pool
117
117
  max_threads: 20 # Maximum number of threads in the pool