actionmcp 0.60.0 → 0.60.1

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: 93a62c8f6a0c3fc99c77869dc3c4234374cdcf10dc31b67a6cb69bc8d258a9b9
4
- data.tar.gz: bd216791bd8df048c94ee26d861a3aac6596fd3d62936a3e907a5b5080a00bae
3
+ metadata.gz: 2384a0cea84e17ca34e3c46e4029e2dc008561ebeeb8e52f15cb05c3fb5f5eb4
4
+ data.tar.gz: ccba3d9432ae0be54a4950e2f5d357c55fdd39cec2766253e2b3093dba9eedd8
5
5
  SHA512:
6
- metadata.gz: 78db835a29ef0d605677b74cc8ff7b1a3c7b63547c5fc4d24ad30149be3452aee37af06f8f29fda95b734509bffdf65179e5088b35f33c13fde77011de5e7c27
7
- data.tar.gz: a80dcfcad14f41076ccabe4ab9d9532305ea6e799bbd8be1cc89a29788858c3e28a08c674f8dc34b7a37bd8a9e78ea0d7fa3bc25da818c764cd16847e67c36e1
6
+ metadata.gz: e7ed0c8b71d4211dc48388a7544c17ecb9ff674faad9274cd1225cb3e3a65f76bcb546c2c2353882a1ade9016670659fed38aa1c90aa94e17479149012e1be01
7
+ data.tar.gz: add51e3d310ec89d96bef2767008bc4be9f9bddfeda60ca826d6e9168485771ebf61c03e1011c889d0a57fb4ff3d7bb0e0f5c5046cb35ca69a5285d647fe71d2
data/README.md CHANGED
@@ -48,7 +48,16 @@ To start using ActionMCP, add it to your project:
48
48
  $ bundle add actionmcp
49
49
  ```
50
50
 
51
- This will load the ActionMCP library so you can start defining MCP prompts, tools, and resources in your application.
51
+ After adding the gem, run the install generator to set up the basic ActionMCP structure:
52
+
53
+ ```bash
54
+ bundle install
55
+ bin/rails action_mcp:install:migrations
56
+ bin/rails db:migrate
57
+ bin/rails generate action_mcp:install
58
+ ```
59
+
60
+ This will create the base application classes, configuration file, and necessary database tables for ActionMCP to function properly.
52
61
 
53
62
  ## Core Components
54
63
 
@@ -309,7 +318,8 @@ Then install it:
309
318
 
310
319
  ```bash
311
320
  bundle install
312
- bin/rails solid_mcp:install
321
+ bin/rails solid_mcp:install:migrations
322
+ bin/rails db:migrate
313
323
  ```
314
324
 
315
325
  The installer will create the necessary database migration for message storage. Configure it in your `config/mcp.yml`.
@@ -623,6 +633,33 @@ run ActionMCP::Engine
623
633
  bin/rails s -c mcp.ru -p 62770 -P tmp/pids/mcps0.pid
624
634
  ```
625
635
 
636
+ ### Dealing with Middleware Conflicts
637
+
638
+ If your Rails application uses middleware that interferes with MCP server operation (like Devise, Warden, Ahoy, Rack::Cors, etc.), use `mcp_vanilla.ru` instead:
639
+
640
+ ```ruby
641
+ # mcp_vanilla.ru - A minimal Rack app with only essential middleware
642
+ # This avoids conflicts with authentication, tracking, and other web-specific middleware
643
+ # See the file for detailed documentation on when and why to use it
644
+
645
+ bundle exec rails s -c mcp_vanilla.ru -p 62770
646
+ # Or with Falcon:
647
+ bundle exec falcon serve --bind http://0.0.0.0:62770 --config mcp_vanilla.ru
648
+ ```
649
+
650
+ Common middleware that can cause issues:
651
+ - **Devise/Warden** - Expects cookies and sessions, throws `Devise::MissingWarden` errors
652
+ - **Ahoy** - Analytics tracking that intercepts requests
653
+ - **Rack::Attack** - Rate limiting designed for web traffic
654
+ - **Rack::Cors** - CORS headers meant for browsers
655
+ - Any middleware assuming HTML responses or cookie-based authentication
656
+
657
+ An example of a minimal `mcp_vanilla.ru` file is located in the dummy app : test/dummy/mcp_vanilla.ru.
658
+ This file is a minimal Rack application that only includes the essential middleware needed for MCP server operation, avoiding conflicts with web-specific middleware.
659
+ But remember to add any instrumentation or logging middleware you need, as the minimal setup will not include them by default.
660
+
661
+ ```ruby
662
+
626
663
  ## Production Deployment of MCPS0
627
664
 
628
665
  In production, **MCPS0** (the MCP server) is a standard Rack application. You can run it using any Rack-compatible server (such as Puma, Unicorn, or Passenger).
@@ -637,7 +674,7 @@ Run MCPS0 on its own TCP port (commonly `62770`):
637
674
 
638
675
  **With Falcon:**
639
676
  ```bash
640
- bundle exec falcon serve --bind http://0.0.0.0:62770 mcp.ru
677
+ bundle exec falcon serve --bind http://0.0.0.0:62770 --config mcp.ru
641
678
  ```
642
679
 
643
680
  **With Puma:**
@@ -5,7 +5,6 @@ module ActionMCP
5
5
  # Supports GET for server-initiated SSE streams, POST for client messages
6
6
  # (responding with JSON or SSE), and optionally DELETE for session termination.
7
7
  class ApplicationController < ActionController::API
8
- REQUIRED_PROTOCOL_VERSION = "2025-03-26"
9
8
  MCP_SESSION_ID_HEADER = "Mcp-Session-Id"
10
9
 
11
10
  include Engine.routes.url_helpers
@@ -143,11 +142,14 @@ module ActionMCP
143
142
  # Handles POST requests containing client JSON-RPC messages according to 2025-03-26 spec.
144
143
  # @route POST /mcp
145
144
  def create
146
- return render_not_acceptable(post_accept_headers_error_message) unless post_accept_headers_valid?
145
+ unless post_accept_headers_valid?
146
+ id = extract_jsonrpc_id_from_params
147
+ return render_not_acceptable(post_accept_headers_error_message, id)
148
+ end
147
149
 
148
150
  # Reject JSON-RPC batch requests as per MCP 2025-06-18 spec
149
151
  if jsonrpc_params_batch?
150
- return render_bad_request("JSON-RPC batch requests are not supported")
152
+ return render_bad_request("JSON-RPC batch requests are not supported", nil)
151
153
  end
152
154
 
153
155
  is_initialize_request = check_if_initialize_request(jsonrpc_params)
@@ -160,11 +162,14 @@ module ActionMCP
160
162
 
161
163
  unless is_initialize_request
162
164
  if session_initially_missing
163
- return render_bad_request("Mcp-Session-Id header is required for this request.")
165
+ id = jsonrpc_params.respond_to?(:id) ? jsonrpc_params.id : nil
166
+ return render_bad_request("Mcp-Session-Id header is required for this request.", id)
164
167
  elsif session.nil? || session.new_record?
165
- return render_not_found("Session not found.")
168
+ id = jsonrpc_params.respond_to?(:id) ? jsonrpc_params.id : nil
169
+ return render_not_found("Session not found.", id)
166
170
  elsif session.status == "closed"
167
- return render_not_found("Session has been terminated.")
171
+ id = jsonrpc_params.respond_to?(:id) ? jsonrpc_params.id : nil
172
+ return render_not_found("Session has been terminated.", id)
168
173
  end
169
174
  end
170
175
 
@@ -188,7 +193,8 @@ module ActionMCP
188
193
  end
189
194
  rescue StandardError => e
190
195
  Rails.logger.error "Unified POST Error: #{e.class} - #{e.message}\n#{e.backtrace.join("\n")}"
191
- render_internal_server_error("An unexpected error occurred.") unless performed?
196
+ id = jsonrpc_params.respond_to?(:id) ? jsonrpc_params.id : nil rescue nil
197
+ render_internal_server_error("An unexpected error occurred.", id) unless performed?
192
198
  end
193
199
 
194
200
  # Handles DELETE requests for session termination (2025-03-26 spec).
@@ -259,7 +265,7 @@ module ActionMCP
259
265
  # Session protocol version is set during initialization and should not be overridden
260
266
  session
261
267
  else
262
- Server.session_store.create_session(nil, protocol_version: self.class::REQUIRED_PROTOCOL_VERSION)
268
+ Server.session_store.create_session(nil, protocol_version: ActionMCP::DEFAULT_PROTOCOL_VERSION)
263
269
  end
264
270
  end
265
271
 
@@ -404,33 +410,58 @@ module ActionMCP
404
410
  # --- Error Rendering Methods ---
405
411
 
406
412
  # Renders a 400 Bad Request response with a JSON-RPC-like error structure.
407
- def render_bad_request(message = "Bad Request")
408
- render json: { jsonrpc: "2.0", error: { code: -32_600, message: message } }
413
+ def render_bad_request(message = "Bad Request", id = nil)
414
+ id ||= extract_jsonrpc_id_from_request
415
+ render json: { jsonrpc: "2.0", id: id, error: { code: -32_600, message: message } }
409
416
  end
410
417
 
411
418
  # Renders a 404 Not Found response with a JSON-RPC-like error structure.
412
- def render_not_found(message = "Not Found")
413
- render json: { jsonrpc: "2.0", error: { code: -32_001, message: message } }
419
+ def render_not_found(message = "Not Found", id = nil)
420
+ id ||= extract_jsonrpc_id_from_request
421
+ render json: { jsonrpc: "2.0", id: id, error: { code: -32_001, message: message } }
414
422
  end
415
423
 
416
424
  # Renders a 405 Method Not Allowed response.
417
- def render_method_not_allowed(message = "Method Not Allowed")
418
- render json: { jsonrpc: "2.0", error: { code: -32_601, message: message } }
425
+ def render_method_not_allowed(message = "Method Not Allowed", id = nil)
426
+ id ||= extract_jsonrpc_id_from_request
427
+ render json: { jsonrpc: "2.0", id: id, error: { code: -32_601, message: message } }
419
428
  end
420
429
 
421
430
  # Renders a 406 Not Acceptable response.
422
- def render_not_acceptable(message = "Not Acceptable")
423
- render json: { jsonrpc: "2.0", error: { code: -32_002, message: message } }
431
+ def render_not_acceptable(message = "Not Acceptable", id = nil)
432
+ id ||= extract_jsonrpc_id_from_request
433
+ render json: { jsonrpc: "2.0", id: id, error: { code: -32_002, message: message } }
424
434
  end
425
435
 
426
436
  # Renders a 501 Not Implemented response.
427
- def render_not_implemented(message = "Not Implemented")
428
- render json: { jsonrpc: "2.0", error: { code: -32_003, message: message } }
437
+ def render_not_implemented(message = "Not Implemented", id = nil)
438
+ id ||= extract_jsonrpc_id_from_request
439
+ render json: { jsonrpc: "2.0", id: id, error: { code: -32_003, message: message } }
429
440
  end
430
441
 
431
442
  # Renders a 500 Internal Server Error response.
432
443
  def render_internal_server_error(message = "Internal Server Error", id = nil)
433
444
  render json: { jsonrpc: "2.0", id: id, error: { code: -32_000, message: message } }
434
445
  end
446
+
447
+ # Extract JSON-RPC ID from request
448
+ def extract_jsonrpc_id_from_request
449
+ # Try to get from already parsed jsonrpc_params first
450
+ if defined?(jsonrpc_params) && jsonrpc_params
451
+ return jsonrpc_params.respond_to?(:id) ? jsonrpc_params.id : nil
452
+ end
453
+
454
+ # Otherwise try to parse from raw body, this need refactoring
455
+ return nil unless request.post? && request.content_type&.include?("application/json")
456
+
457
+ begin
458
+ body = request.body.read
459
+ request.body.rewind # Reset for subsequent reads
460
+ json = JSON.parse(body)
461
+ json["id"]
462
+ rescue JSON::ParserError, StandardError
463
+ nil
464
+ end
465
+ end
435
466
  end
436
467
  end
@@ -75,7 +75,7 @@ module ActionMCP
75
75
  before_create :set_server_info, if: -> { role == "server" }
76
76
  before_create :set_server_capabilities, if: -> { role == "server" }
77
77
 
78
- validates :protocol_version, inclusion: { in: SUPPORTED_VERSIONS }, allow_nil: true, unless: lambda {
78
+ validates :protocol_version, inclusion: { in: ActionMCP::SUPPORTED_VERSIONS }, allow_nil: true, unless: lambda {
79
79
  ActionMCP.configuration.vibed_ignore_version
80
80
  }
81
81
 
@@ -130,7 +130,7 @@ module ActionMCP
130
130
 
131
131
  def server_capabilities_payload
132
132
  {
133
- protocolVersion: PROTOCOL_VERSION,
133
+ protocolVersion: protocol_version || ActionMCP::DEFAULT_PROTOCOL_VERSION,
134
134
  serverInfo: server_info,
135
135
  capabilities: server_capabilities
136
136
  }
@@ -180,7 +180,7 @@ module ActionMCP
180
180
  end
181
181
 
182
182
  params = {
183
- protocolVersion: PROTOCOL_VERSION,
183
+ protocolVersion: ActionMCP::DEFAULT_PROTOCOL_VERSION,
184
184
  capabilities: client_capabilities,
185
185
  clientInfo: client_info
186
186
  }
@@ -180,7 +180,7 @@ module ActionMCP
180
180
  # Create a new session with the server-provided ID
181
181
  client.instance_variable_set(:@session, ActionMCP::Session.from_client.new(
182
182
  id: session_id,
183
- protocol_version: result["protocolVersion"] || PROTOCOL_VERSION,
183
+ protocol_version: result["protocolVersion"] || ActionMCP::DEFAULT_PROTOCOL_VERSION,
184
184
  client_info: client.client_info,
185
185
  client_capabilities: client.client_capabilities,
186
186
  server_info: result["serverInfo"],
@@ -67,7 +67,7 @@ module ActionMCP
67
67
 
68
68
  @sse_heartbeat_interval = 30
69
69
  @post_response_preference = :json
70
- @protocol_version = "2025-03-26"
70
+ @protocol_version = "2025-03-26" # Default to legacy for backwards compatibility
71
71
  @vibed_ignore_version = false
72
72
 
73
73
  # Resumability defaults
@@ -45,7 +45,7 @@ module ActionMCP
45
45
  # Return existing session info
46
46
  capabilities_payload = existing_session.server_capabilities_payload
47
47
  capabilities_payload[:protocolVersion] = if ActionMCP.configuration.vibed_ignore_version
48
- PROTOCOL_VERSION
48
+ ActionMCP::LATEST_VERSION
49
49
  else
50
50
  client_protocol_version
51
51
  end
@@ -66,7 +66,7 @@ module ActionMCP
66
66
 
67
67
  capabilities_payload = session.server_capabilities_payload
68
68
  capabilities_payload[:protocolVersion] = if ActionMCP.configuration.vibed_ignore_version
69
- PROTOCOL_VERSION
69
+ ActionMCP::LATEST_VERSION
70
70
  else
71
71
  client_protocol_version
72
72
  end
@@ -175,14 +175,14 @@ module ActionMCP
175
175
  # Capability methods
176
176
  def server_capabilities_payload
177
177
  {
178
- protocolVersion: ActionMCP::PROTOCOL_VERSION,
178
+ protocolVersion: ActionMCP::LATEST_VERSION,
179
179
  serverInfo: server_info,
180
180
  capabilities: server_capabilities
181
181
  }
182
182
  end
183
183
 
184
184
  def set_protocol_version(version)
185
- version = ActionMCP::PROTOCOL_VERSION if ActionMCP.configuration.vibed_ignore_version
185
+ version = ActionMCP::LATEST_VERSION if ActionMCP.configuration.vibed_ignore_version
186
186
  self.protocol_version = version
187
187
  save
188
188
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.60.0"
5
+ VERSION = "0.60.1"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
data/lib/action_mcp.rb CHANGED
@@ -37,9 +37,15 @@ module ActionMCP
37
37
  require_relative "action_mcp/version"
38
38
  require_relative "action_mcp/client"
39
39
  include Logging
40
- PROTOCOL_VERSION = "2025-03-26" # Default version
41
- CURRENT_VERSION = "2025-03-26" # Current version
42
- SUPPORTED_VERSIONS = %w[2025-06-18 2025-03-26].freeze
40
+
41
+ # Protocol version constants
42
+ SUPPORTED_VERSIONS = [
43
+ "2025-06-18", # Dr. Identity McBouncer - OAuth 2.1, elicitation, structured output, resource links
44
+ "2025-03-26" # The Persistent Negotiator - StreamableHTTP, resumability, audio support
45
+ ].freeze
46
+
47
+ LATEST_VERSION = SUPPORTED_VERSIONS.first.freeze
48
+ DEFAULT_PROTOCOL_VERSION = "2025-03-26".freeze # Default to initial stable version for backwards compatibility
43
49
  class << self
44
50
  # Returns a Rack-compatible application for serving MCP requests
45
51
  # This makes ActionMCP.server work similar to ActionCable.server
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionmcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.60.0
4
+ version: 0.60.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: 0.5.2
46
+ version: 0.5.3
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: 0.5.2
53
+ version: 0.5.3
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: multi_json
56
56
  requirement: !ruby/object:Gem::Requirement