actionmcp 0.106.1 → 0.107.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: e6c17941ba128e9db3431290d21c3eea28e284eef07e48077b5b72073c8bd108
4
- data.tar.gz: 17d1ab49293f6b948b35862c92f00c52b1b44aad48c2a9dbc761009d3103d330
3
+ metadata.gz: c333cdfa82e92c5954fd45b97c45d3f569f7fb9a5711d85195d3e2f960a284a3
4
+ data.tar.gz: 6d9fa7a7aa0b9ee2bb7b1549a5a6ca288db448fe7e729c3e833147cc8e71742c
5
5
  SHA512:
6
- metadata.gz: f6ec2bdf02b5758885178a43911f320931cb67613ca5d3526296e332e77b0627b968dd2137d3d6778431e281af3bfc83c99ca5dd3e6ee718ed0f8fa79a98168a
7
- data.tar.gz: dafc2ff9f7cdc3d4ab4e1f8a26837f300e92fa74aefeee95ef2756ffc347aa29669fbdf6e1053bc14cc983bb225fe2e947d736a248b58afedbcc485d20fd8b58
6
+ metadata.gz: f4f60433d3d29a41ef1b884a54954954c08f08efa086b6b4cd239eb64527106297f8b4d33bf2dc97c328fe2292701ba2d235a1c2052bbba3127cc732f5b2b39c
7
+ data.tar.gz: ea0ed7c13e3b0cb92b09774a8aef1657550fc3d0e4244de24c502fe289164ffbb2b33e78ff55771f9a13df20194ae2c02cf1a8ebfba6c11baa4e93a4fabc8793
data/README.md CHANGED
@@ -491,51 +491,6 @@ end
491
491
 
492
492
  For dynamic versioning, consider adding the `rails_app_version` gem.
493
493
 
494
-
495
- ### PubSub Configuration
496
-
497
- ActionMCP uses a pub/sub system for real-time communication. You can choose between several adapters:
498
-
499
- 1. **SolidMCP** - Database-backed pub/sub (no Redis required)
500
- 2. **Simple** - In-memory pub/sub for development and testing
501
- 3. **Redis** - Redis-backed pub/sub (if you prefer Redis)
502
-
503
- #### Migrating from ActionCable
504
-
505
- If you were previously using ActionCable with ActionMCP, you will need to migrate to the new PubSub system. Here's how:
506
-
507
- 1. Remove the ActionCable dependency from your Gemfile (if you don't need it for other purposes)
508
- 2. Install one of the PubSub adapters (SolidMCP recommended)
509
- 3. Create a configuration file at `config/mcp.yml` (you can use the generator: `bin/rails g action_mcp:config`)
510
- 4. Run your tests to ensure everything works correctly
511
-
512
- The new PubSub system maintains the same API as the previous ActionCable-based implementation, so your existing code should continue to work without changes.
513
-
514
- Configure your adapter in `config/mcp.yml`:
515
-
516
- ```yaml
517
- development:
518
- adapter: solid_mcp
519
- polling_interval: 0.1.seconds
520
- # Thread pool configuration (optional)
521
- # min_threads: 5 # Minimum number of threads in the pool
522
- # max_threads: 10 # Maximum number of threads in the pool
523
- # max_queue: 100 # Maximum number of tasks that can be queued
524
-
525
- test:
526
- adapter: test # Uses the simple in-memory adapter
527
-
528
- production:
529
- adapter: solid_mcp
530
- polling_interval: 0.5.seconds
531
- # Optional: connects_to: cable # If using a separate database
532
-
533
- # Thread pool configuration for high-traffic environments
534
- min_threads: 10 # Minimum number of threads in the pool
535
- max_threads: 20 # Maximum number of threads in the pool
536
- max_queue: 500 # Maximum number of tasks that can be queued
537
- ```
538
-
539
494
  ### Server Instructions
540
495
 
541
496
  Server instructions help LLMs understand **what your server is for** and **when to use it**. They describe the server's purpose and goal, not technical details like rate limits or authentication (tools are self-documented via their own descriptions).
@@ -565,46 +520,6 @@ production:
565
520
 
566
521
  Instructions are sent as a single string (joined by newlines) at the top level of the initialization response, helping LLMs understand your server's purpose.
567
522
 
568
- #### SolidMCP (Database-backed, Recommended)
569
-
570
- For SolidMCP, add it to your Gemfile:
571
-
572
- ```ruby
573
- gem "solid_mcp" # Database-backed adapter optimized for MCP
574
- ```
575
-
576
- Then install it:
577
-
578
- ```bash
579
- bundle install
580
- bin/rails solid_mcp:install:migrations
581
- bin/rails db:migrate
582
- ```
583
-
584
- The installer will create the necessary database migration for message storage. Configure it in your `config/mcp.yml`.
585
-
586
- #### Redis Adapter
587
-
588
- If you prefer Redis, add it to your Gemfile:
589
-
590
- ```ruby
591
- gem "redis", "~> 5.0"
592
- ```
593
-
594
- Then configure the Redis adapter in your `config/mcp.yml`:
595
-
596
- ```yaml
597
- production:
598
- adapter: redis
599
- url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
600
- channel_prefix: your_app_production
601
-
602
- # Thread pool configuration for high-traffic environments
603
- min_threads: 10 # Minimum number of threads in the pool
604
- max_threads: 20 # Maximum number of threads in the pool
605
- max_queue: 500 # Maximum number of tasks that can be queued
606
- ```
607
-
608
523
  ## Session Storage
609
524
 
610
525
  ActionMCP provides a pluggable session storage system that allows you to choose how sessions are persisted based on your environment and requirements.
@@ -747,7 +662,6 @@ You can configure the thread pool in your `config/mcp.yml`:
747
662
 
748
663
  ```yaml
749
664
  production:
750
- adapter: solid_mcp
751
665
  # Thread pool configuration
752
666
  min_threads: 10 # Minimum number of threads to keep in the pool
753
667
  max_threads: 20 # Maximum number of threads the pool can grow to
@@ -772,7 +686,9 @@ This ensures all thread pools are properly terminated and tasks are completed.
772
686
 
773
687
  ## Engine and Mounting
774
688
 
775
- **ActionMCP** runs as a standalone Rack application. **Do not attempt to mount it in your application's `routes.rb`**—it is not designed to be mounted as an engine at a custom path. When you use `run ActionMCP::Engine` in your `mcp.ru`, the MCP endpoint is available at the root path (`/`) by default and can be configured via `config.action_mcp.base_path`.
689
+ > **WARNING: Do NOT mount ActionMCP::Engine in your `routes.rb`.** ActionMCP is a standalone Rack application that runs on its own port via `mcp/config.ru`. Mounting it as a Rails engine route will not work correctly.
690
+
691
+ When you use `run ActionMCP.server` in your `mcp/config.ru`, the MCP endpoint is available at the root path (`/`) by default and can be configured via `config.action_mcp.base_path`. Always use `ActionMCP.server` (not `ActionMCP::Engine` directly) — it initializes required subsystems.
776
692
 
777
693
  ### Installing ActionMCP
778
694
 
@@ -789,12 +705,14 @@ This will create:
789
705
  - `app/mcp/resource_templates/application_mcp_res_template.rb` - Base resource template class
790
706
  - `app/mcp/application_gateway.rb` - Gateway for authentication
791
707
  - `config/mcp.yml` - Configuration file with example settings for all environments
708
+ - `mcp/config.ru` - Standalone Rack server configuration
709
+ - `bin/mcp` - Server binstub (prefers Falcon, falls back to Puma)
792
710
 
793
711
  > **Note:** Authentication and authorization are not included. You are responsible for securing the endpoint.
794
712
 
795
713
  ## Authentication with Gateway
796
714
 
797
- ActionMCP provides a Gateway system similar to ActionCable's Connection for handling authentication. The Gateway allows you to authenticate users and make them available throughout your MCP components.
715
+ ActionMCP provides a Gateway system for handling authentication. The Gateway allows you to authenticate users and make them available throughout your MCP components.
798
716
 
799
717
  ActionMCP uses a Gateway pattern with pluggable identifiers for authentication. You can implement custom authentication strategies using session-based auth, API keys, bearer tokens, or integrate with existing authentication systems like Warden, Devise, or external OAuth providers.
800
718
 
@@ -877,19 +795,43 @@ ActionMCP uses Rails' CurrentAttributes to store the authenticated context. The
877
795
  - `ActionMCP::Current.gateway` - The gateway instance
878
796
  - Any other attributes you define with `identified_by`
879
797
 
880
- ### 1. Create `mcp.ru`
798
+ ### Persisting Context on Sessions
799
+
800
+ Override `configure_session` in your gateway to persist auth context (user_id, tenant_id, roles) on the session for use in audit trails, background jobs, and cross-request scenarios:
801
+
802
+ ```ruby
803
+ class ApplicationGateway < ActionMCP::Gateway
804
+ identified_by UserIdentifier
805
+
806
+ def configure_session(session)
807
+ session.session_data = { "user_id" => user.id, "tenant_id" => user.tenant_id }
808
+ end
809
+ end
810
+ ```
811
+
812
+ Tools access it via `session_data["user_id"]`. See [GATEWAY.md](GATEWAY.md) for details.
813
+
814
+ ### 1. Create `mcp/config.ru`
815
+
816
+ The install generator (`rails generate action_mcp:install`) creates this automatically. If you need to create it manually:
881
817
 
882
818
  ```ruby
883
819
  # Load the full Rails environment to access models, DB, Redis, etc.
884
- require_relative "config/environment"
820
+ require_relative "../config/environment"
821
+
822
+ $stdout.sync = true
823
+
824
+ # Eager load so all tools, prompts, and resources are registered.
825
+ Rails.application.eager_load!
885
826
 
886
- # No need to set a custom endpoint path. The MCP endpoint is always served at root ("/")
887
- # when using ActionMCP::Engine directly.
888
- run ActionMCP::Engine
827
+ # IMPORTANT: Use ActionMCP.server it initializes required subsystems.
828
+ # Do NOT use ActionMCP::Engine directly.
829
+ run ActionMCP.server
889
830
  ```
890
831
  ### 2. Start the server
891
832
  ```bash
892
- bin/rails s -c mcp.ru -p 62770 -P tmp/pids/mcps0.pid
833
+ bin/mcp # Uses Falcon (recommended)
834
+ bundle exec rails s -c mcp/config.ru -p 62770 # Uses Puma (fallback)
893
835
  ```
894
836
 
895
837
  ### Dealing with Middleware Conflicts
@@ -933,17 +875,17 @@ Run MCPS0 on its own TCP port (commonly `62770`):
933
875
 
934
876
  **With Falcon:**
935
877
  ```bash
936
- bundle exec falcon serve --bind http://0.0.0.0:62770 --config mcp.ru
878
+ bundle exec falcon serve --bind http://0.0.0.0:62770 --config mcp/config.ru
937
879
  ```
938
880
 
939
881
  **With Puma:**
940
882
  ```bash
941
- bundle exec rails s -c mcp.ru -p 62770
883
+ bundle exec rails s -c mcp/config.ru -p 62770
942
884
  ```
943
885
 
944
886
  **With Passenger:**
945
887
  ```bash
946
- passenger start --rackup mcp.ru --port 62770
888
+ passenger start --rackup mcp/config.ru --port 62770
947
889
  ```
948
890
 
949
891
  Then, use your web server (Nginx, Apache, etc.) to reverse proxy requests to this port.
@@ -954,17 +896,17 @@ Alternatively, you can run MCPS0 on a Unix socket for improved performance and s
954
896
 
955
897
  **With Falcon:**
956
898
  ```bash
957
- bundle exec falcon serve --bind unix:/tmp/mcps0.sock mcp.ru
899
+ bundle exec falcon serve --bind unix:/tmp/mcps0.sock mcp/config.ru
958
900
  ```
959
901
 
960
902
  **With Puma:**
961
903
  ```bash
962
- bundle exec puma -C config/puma.rb -b unix:///tmp/mcps0.sock -c mcp.ru
904
+ bundle exec puma -C config/puma.rb -b unix:///tmp/mcps0.sock -c mcp/config.ru
963
905
  ```
964
906
 
965
907
  **With Passenger:**
966
908
  ```bash
967
- passenger start --rackup mcp.ru --socket /tmp/mcps0.sock
909
+ passenger start --rackup mcp/config.ru --socket /tmp/mcps0.sock
968
910
  ```
969
911
 
970
912
  And configure your web server to proxy to the socket:
@@ -994,7 +936,7 @@ location ~* ^/mcp {
994
936
  root /path/to/current/public;
995
937
  passenger_app_root /path/to/current;
996
938
  passenger_enabled on;
997
- passenger_startup_file mcp.ru;
939
+ passenger_startup_file mcp/config.ru;
998
940
  passenger_app_group_name mcp;
999
941
  }
1000
942
  ```
@@ -1018,7 +960,7 @@ bin/rails action_mcp:install:migrations # to copy the migrations
1018
960
  bin/rails generate action_mcp:install
1019
961
  ```
1020
962
 
1021
- This will create the base application classes, configuration file, and authentication gateway in your app directory.
963
+ This will create the base application classes, configuration file, authentication gateway, `mcp/config.ru` rackup file, and `bin/mcp` binstub in your app directory.
1022
964
 
1023
965
  ### Generate a New Prompt
1024
966
 
@@ -1071,7 +1013,7 @@ You can use the MCP Inspector to test your server implementation:
1071
1013
 
1072
1014
  ```bash
1073
1015
  # Start your MCP server
1074
- bundle exec rails s -c mcp.ru -p 62770
1016
+ bundle exec rails s -c mcp/config.ru -p 62770
1075
1017
 
1076
1018
  # In another terminal, run the inspector
1077
1019
  npx @modelcontextprotocol/inspector --url http://localhost:62770
@@ -320,6 +320,7 @@ module ActionMCP
320
320
  begin
321
321
  gateway = gateway_class.new(request)
322
322
  gateway.call
323
+ gateway.configure_session(mcp_session)
323
324
  rescue ActionMCP::UnauthorizedError => e
324
325
  render_unauthorized(e.message)
325
326
  rescue StandardError => e
@@ -5,17 +5,17 @@
5
5
  # database_dialect = "SQLite"
6
6
  #
7
7
  # columns = [
8
- # { name = "id", type = "integer", primary_key = true, nullable = false },
9
- # { name = "session_id", type = "string", nullable = false },
10
- # { name = "direction", type = "string", nullable = false, default = "client" },
11
- # { name = "message_type", type = "string", nullable = false },
12
- # { name = "jsonrpc_id", type = "string", nullable = true },
13
- # { name = "message_json", type = "json", nullable = true },
14
- # { name = "is_ping", type = "boolean", nullable = false, default = "0" },
15
- # { name = "request_acknowledged", type = "boolean", nullable = false, default = "0" },
16
- # { name = "request_cancelled", type = "boolean", nullable = false, default = "0" },
17
- # { name = "created_at", type = "datetime", nullable = false },
18
- # { name = "updated_at", type = "datetime", nullable = false }
8
+ # { name = "id", type = "integer", pk = true, null = false },
9
+ # { name = "created_at", type = "datetime", null = false },
10
+ # { name = "direction", type = "string", null = false, default = "client" },
11
+ # { name = "is_ping", type = "boolean", null = false },
12
+ # { name = "jsonrpc_id", type = "string" },
13
+ # { name = "message_json", type = "json" },
14
+ # { name = "message_type", type = "string", null = false },
15
+ # { name = "request_acknowledged", type = "boolean", null = false },
16
+ # { name = "request_cancelled", type = "boolean", null = false },
17
+ # { name = "session_id", type = "string", null = false },
18
+ # { name = "updated_at", type = "datetime", null = false }
19
19
  # ]
20
20
  #
21
21
  # indexes = [
@@ -26,14 +26,10 @@
26
26
  # { column = "session_id", references_table = "action_mcp_sessions", references_column = "id", on_delete = "cascade", on_update = "cascade" }
27
27
  # ]
28
28
  #
29
- # == Notes
30
- # - Column 'message_json' should probably have NOT NULL constraint
31
- # - String column 'session_id' has no length limit - consider adding one
32
- # - String column 'direction' has no length limit - consider adding one
33
- # - String column 'message_type' has no length limit - consider adding one
34
- # - String column 'jsonrpc_id' has no length limit - consider adding one
35
- # - Column 'message_type' is commonly used in queries - consider adding an index
36
- # - Column 'is_ping' uses non-conventional prefix - consider removing 'is_' or 'has_'
29
+ # [callbacks]
30
+ # after_create = [{ method = "acknowledge_request", if = ["proc"] }]
31
+ #
32
+ # notes = ["message_json:NOT_NULL", "direction:LIMIT", "jsonrpc_id:LIMIT", "message_type:LIMIT", "session_id:LIMIT", "message_type:INDEX"]
37
33
  # <rails-lens:schema:end>
38
34
  module ActionMCP
39
35
  class Session
@@ -49,15 +45,12 @@ module ActionMCP
49
45
  inverse_of: :messages,
50
46
  counter_cache: true
51
47
 
52
- delegate :adapter,
53
- :role,
54
- :session_key,
48
+ delegate :role,
55
49
  to: :session
56
50
 
57
51
  # Virtual attribute for data
58
52
  attr_reader :data
59
53
 
60
- after_create_commit :broadcast_message, if: :outgoing_message?
61
54
  # Set is_ping on responses if the original request was a ping
62
55
  after_create :acknowledge_request, if: -> { %w[response error].include?(message_type) }
63
56
 
@@ -117,16 +110,6 @@ module ActionMCP
117
110
 
118
111
  private
119
112
 
120
- def outgoing_message?
121
- direction != role
122
- end
123
-
124
- def broadcast_message
125
- return unless adapter.present?
126
-
127
- adapter.broadcast(session_key, data.to_json)
128
- end
129
-
130
113
  def process_json_content(content)
131
114
  if content.is_a?(JSON_RPC::Notification) || content.is_a?(JSON_RPC::Request) || content.is_a?(JSON_RPC::Response)
132
115
  content = content.to_h.with_indifferent_access
@@ -5,12 +5,12 @@
5
5
  # database_dialect = "SQLite"
6
6
  #
7
7
  # columns = [
8
- # { name = "id", type = "integer", primary_key = true, nullable = false },
9
- # { name = "session_id", type = "string", nullable = false },
10
- # { name = "uri", type = "string", nullable = false },
11
- # { name = "last_notification_at", type = "datetime", nullable = true },
12
- # { name = "created_at", type = "datetime", nullable = false },
13
- # { name = "updated_at", type = "datetime", nullable = false }
8
+ # { name = "id", type = "integer", pk = true, null = false },
9
+ # { name = "created_at", type = "datetime", null = false },
10
+ # { name = "last_notification_at", type = "datetime" },
11
+ # { name = "session_id", type = "string", null = false },
12
+ # { name = "updated_at", type = "datetime", null = false },
13
+ # { name = "uri", type = "string", null = false }
14
14
  # ]
15
15
  #
16
16
  # indexes = [
@@ -21,10 +21,7 @@
21
21
  # { column = "session_id", references_table = "action_mcp_sessions", references_column = "id", on_delete = "cascade" }
22
22
  # ]
23
23
  #
24
- # == Notes
25
- # - Consider adding counter cache for 'session'
26
- # - String column 'session_id' has no length limit - consider adding one
27
- # - String column 'uri' has no length limit - consider adding one
24
+ # notes = ["session:COUNTER_CACHE", "session_id:LIMIT", "uri:LIMIT"]
28
25
  # <rails-lens:schema:end>
29
26
  module ActionMCP
30
27
  class Session
@@ -1,5 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # <rails-lens:schema:begin>
4
+ # table = "action_mcp_session_tasks"
5
+ # database_dialect = "SQLite"
6
+ #
7
+ # columns = [
8
+ # { name = "id", type = "string", pk = true, null = false },
9
+ # { name = "created_at", type = "datetime", null = false },
10
+ # { name = "last_updated_at", type = "datetime", null = false },
11
+ # { name = "poll_interval", type = "integer" },
12
+ # { name = "progress_message", type = "string" },
13
+ # { name = "progress_percent", type = "integer" },
14
+ # { name = "request_method", type = "string" },
15
+ # { name = "request_name", type = "string" },
16
+ # { name = "request_params", type = "json" },
17
+ # { name = "result_payload", type = "json" },
18
+ # { name = "session_id", type = "string", null = false },
19
+ # { name = "status", type = "string", null = false, default = "working" },
20
+ # { name = "status_message", type = "string" },
21
+ # { name = "ttl", type = "integer" },
22
+ # { name = "updated_at", type = "datetime", null = false }
23
+ # ]
24
+ #
25
+ # indexes = [
26
+ # { name = "index_action_mcp_session_tasks_on_status", columns = ["status"] },
27
+ # { name = "index_action_mcp_session_tasks_on_session_id", columns = ["session_id"] },
28
+ # { name = "index_action_mcp_session_tasks_on_session_id_and_status", columns = ["session_id", "status"] },
29
+ # { name = "index_action_mcp_session_tasks_on_created_at", columns = ["created_at"] }
30
+ # ]
31
+ #
32
+ # foreign_keys = [
33
+ # { column = "session_id", references_table = "action_mcp_sessions", references_column = "id", on_delete = "cascade", on_update = "cascade" }
34
+ # ]
35
+ #
36
+ # [callbacks]
37
+ # before_validation = [{ method = "set_last_updated_at" }]
38
+ # around_validation = [{ method = "machine" }]
39
+ #
40
+ # notes = ["index_action_mcp_session_tasks_on_session_id:REDUND_IDX", "session:COUNTER_CACHE", "poll_interval:NOT_NULL", "progress_message:NOT_NULL", "progress_percent:NOT_NULL", "request_method:NOT_NULL", "request_name:NOT_NULL", "request_params:NOT_NULL", "result_payload:NOT_NULL", "status_message:NOT_NULL", "ttl:NOT_NULL", "status_message:DEFAULT", "id:LIMIT", "progress_message:LIMIT", "request_method:LIMIT", "request_name:LIMIT", "session_id:LIMIT", "status:LIMIT", "status_message:LIMIT", "status_message:INDEX"]
41
+ # <rails-lens:schema:end>
3
42
  require "state_machines-activerecord"
4
43
 
5
44
  module ActionMCP
@@ -5,44 +5,31 @@
5
5
  # database_dialect = "SQLite"
6
6
  #
7
7
  # columns = [
8
- # { name = "id", type = "string", primary_key = true, nullable = false },
9
- # { name = "role", type = "string", nullable = false, default = "server" },
10
- # { name = "status", type = "string", nullable = false, default = "pre_initialize" },
11
- # { name = "ended_at", type = "datetime", nullable = true },
12
- # { name = "protocol_version", type = "string", nullable = true },
13
- # { name = "server_capabilities", type = "json", nullable = true },
14
- # { name = "client_capabilities", type = "json", nullable = true },
15
- # { name = "server_info", type = "json", nullable = true },
16
- # { name = "client_info", type = "json", nullable = true },
17
- # { name = "initialized", type = "boolean", nullable = false, default = "0" },
18
- # { name = "messages_count", type = "integer", nullable = false, default = "0" },
19
- # { name = "sse_event_counter", type = "integer", nullable = false, default = "0" },
20
- # { name = "tool_registry", type = "json", nullable = true, default = "[]" },
21
- # { name = "prompt_registry", type = "json", nullable = true, default = "[]" },
22
- # { name = "resource_registry", type = "json", nullable = true, default = "[]" },
23
- # { name = "created_at", type = "datetime", nullable = false },
24
- # { name = "updated_at", type = "datetime", nullable = false },
25
- # { name = "consents", type = "json", nullable = false, default = "{}" }
8
+ # { name = "id", type = "string", pk = true, null = false },
9
+ # { name = "client_capabilities", type = "json" },
10
+ # { name = "client_info", type = "json" },
11
+ # { name = "consents", type = "json", null = false, default = "{}" },
12
+ # { name = "session_data", type = "json", null = false, default = "{}" },
13
+ # { name = "created_at", type = "datetime", null = false },
14
+ # { name = "ended_at", type = "datetime" },
15
+ # { name = "initialized", type = "boolean", null = false },
16
+ # { name = "messages_count", type = "integer", null = false, default = 0 },
17
+ # { name = "prompt_registry", type = "json", default = "[]" },
18
+ # { name = "protocol_version", type = "string" },
19
+ # { name = "resource_registry", type = "json", default = "[]" },
20
+ # { name = "role", type = "string", null = false, default = "server" },
21
+ # { name = "server_capabilities", type = "json" },
22
+ # { name = "server_info", type = "json" },
23
+ # { name = "status", type = "string", null = false, default = "pre_initialize" },
24
+ # { name = "tool_registry", type = "json", default = "[]" },
25
+ # { name = "updated_at", type = "datetime", null = false }
26
26
  # ]
27
27
  #
28
- # == Notes
29
- # - Association 'messages' has N+1 query risk. Consider using includes/preload
30
- # - Association 'subscriptions' has N+1 query risk. Consider using includes/preload
31
- # - Association 'resources' has N+1 query risk. Consider using includes/preload
32
- # - Association 'sse_events' has N+1 query risk. Consider using includes/preload
33
- # - Column 'protocol_version' should probably have NOT NULL constraint
34
- # - Column 'server_capabilities' should probably have NOT NULL constraint
35
- # - Column 'client_capabilities' should probably have NOT NULL constraint
36
- # - Column 'server_info' should probably have NOT NULL constraint
37
- # - Column 'client_info' should probably have NOT NULL constraint
38
- # - Column 'tool_registry' should probably have NOT NULL constraint
39
- # - Column 'prompt_registry' should probably have NOT NULL constraint
40
- # - Column 'resource_registry' should probably have NOT NULL constraint
41
- # - String column 'id' has no length limit - consider adding one
42
- # - String column 'role' has no length limit - consider adding one
43
- # - String column 'status' has no length limit - consider adding one
44
- # - String column 'protocol_version' has no length limit - consider adding one
45
- # - Column 'status' is commonly used in queries - consider adding an index
28
+ # [callbacks]
29
+ # before_create = [{ method = "initialize_registries" }, { method = "set_server_info", if = ["proc"] }, { method = "set_server_capabilities", if = ["proc"] }]
30
+ # after_initialize = [{ method = "proc" }]
31
+ #
32
+ # notes = ["messages:N_PLUS_ONE", "subscriptions:N_PLUS_ONE", "tasks:N_PLUS_ONE", "client_capabilities:NOT_NULL", "client_info:NOT_NULL", "prompt_registry:NOT_NULL", "protocol_version:NOT_NULL", "resource_registry:NOT_NULL", "server_capabilities:NOT_NULL", "server_info:NOT_NULL", "tool_registry:NOT_NULL", "id:LIMIT", "protocol_version:LIMIT", "role:LIMIT", "status:LIMIT", "status:INDEX"]
46
33
  # <rails-lens:schema:end>
47
34
  module ActionMCP
48
35
  ##
@@ -67,12 +54,6 @@ module ActionMCP
67
54
  foreign_key: "session_id",
68
55
  dependent: :delete_all,
69
56
  inverse_of: :session
70
- has_many :resources,
71
- class_name: "ActionMCP::Session::Resource",
72
- foreign_key: "session_id",
73
- dependent: :delete_all,
74
- inverse_of: :session
75
-
76
57
  has_many :tasks,
77
58
  class_name: "ActionMCP::Session::Task",
78
59
  foreign_key: "session_id",
@@ -93,8 +74,6 @@ module ActionMCP
93
74
  validates :protocol_version, inclusion: { in: ActionMCP::SUPPORTED_VERSIONS }, allow_nil: true
94
75
 
95
76
  def close!
96
- dummy_callback = ->(*) { } # this callback seem broken
97
- adapter.unsubscribe(session_key, dummy_callback)
98
77
  if messages_count.zero?
99
78
  # if there are no messages, we can delete the session immediately
100
79
  destroy
@@ -119,14 +98,6 @@ module ActionMCP
119
98
  messages.create!(data: data, direction: role)
120
99
  end
121
100
 
122
- def session_key
123
- "action_mcp:session:#{id}"
124
- end
125
-
126
- def adapter
127
- @adapter ||= ActionMCP::Server.server.pubsub
128
- end
129
-
130
101
  def set_protocol_version(version)
131
102
  update(protocol_version: version)
132
103
  end
@@ -321,7 +292,6 @@ module ActionMCP
321
292
  resource_registry == [ "*" ]
322
293
  end
323
294
 
324
-
325
295
  # Consent management methods as per MCP specification
326
296
  # These methods manage user consents for tools and resources
327
297
 
@@ -17,7 +17,6 @@ class ConsolidatedMigration < ActiveRecord::Migration[8.0]
17
17
  t.json :client_info, comment: 'The information about the client'
18
18
  t.boolean :initialized, null: false, default: false
19
19
  t.integer :messages_count, null: false, default: 0
20
- t.integer :sse_event_counter, default: 0, null: false
21
20
  t.json :tool_registry, default: []
22
21
  t.json :prompt_registry, default: []
23
22
  t.json :resource_registry, default: []
@@ -57,46 +56,10 @@ class ConsolidatedMigration < ActiveRecord::Migration[8.0]
57
56
  end
58
57
  end
59
58
 
60
- # Create session resources table
61
- unless table_exists?(:action_mcp_session_resources)
62
- create_table :action_mcp_session_resources do |t|
63
- t.references :session,
64
- null: false,
65
- foreign_key: { to_table: :action_mcp_sessions, on_delete: :cascade },
66
- type: :string
67
- t.string :uri, null: false
68
- t.string :name
69
- t.text :description
70
- t.string :mime_type, null: false
71
- t.boolean :created_by_tool, default: false
72
- t.datetime :last_accessed_at
73
- t.json :metadata
74
- t.timestamps
75
- end
76
- end
77
-
78
- # Create SSE events table
79
- unless table_exists?(:action_mcp_sse_events)
80
- create_table :action_mcp_sse_events do |t|
81
- t.references :session, null: false, foreign_key: { to_table: :action_mcp_sessions }, index: true, type: :string
82
- t.integer :event_id, null: false
83
- t.text :data, null: false
84
- t.timestamps
85
-
86
- # Index for efficiently retrieving events after a given ID for a specific session
87
- t.index %i[session_id event_id], unique: true
88
- t.index :created_at # For cleanup of old events
89
- end
90
- end
91
-
92
59
  # Add missing columns to existing tables if they exist
93
60
 
94
61
  # For action_mcp_sessions
95
62
  if table_exists?(:action_mcp_sessions)
96
- unless column_exists?(:action_mcp_sessions, :sse_event_counter)
97
- add_column :action_mcp_sessions, :sse_event_counter, :integer, default: 0, null: false
98
- end
99
-
100
63
  unless column_exists?(:action_mcp_sessions, :tool_registry)
101
64
  add_column :action_mcp_sessions, :tool_registry, :json, default: []
102
65
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddSessionDataToActionMCPSessions < ActiveRecord::Migration[8.1]
4
+ def change
5
+ return if column_exists?(:action_mcp_sessions, :session_data)
6
+
7
+ add_column :action_mcp_sessions, :session_data, :json, default: {}, null: false
8
+ end
9
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RemoveSessionResources < ActiveRecord::Migration[8.1]
4
+ def up
5
+ drop_table :action_mcp_session_resources, if_exists: true
6
+ end
7
+
8
+ def down
9
+ return if table_exists?(:action_mcp_session_resources)
10
+
11
+ create_table :action_mcp_session_resources do |t|
12
+ t.references :session,
13
+ null: false,
14
+ foreign_key: { to_table: :action_mcp_sessions, on_delete: :cascade },
15
+ type: :string
16
+ t.string :uri, null: false
17
+ t.string :name
18
+ t.text :description
19
+ t.string :mime_type, null: false
20
+ t.boolean :created_by_tool, default: false
21
+ t.datetime :last_accessed_at
22
+ t.json :metadata
23
+ t.timestamps
24
+ end
25
+ end
26
+ end
@@ -30,6 +30,8 @@ module ActionMCP
30
30
  execution_context[:session]
31
31
  end
32
32
 
33
+ delegate :session_data, to: :session, allow_nil: true
34
+
33
35
  # use _capability_name or default_capability_name
34
36
  def self.capability_name
35
37
  _capability_name || default_capability_name