actionmcp 0.50.4 → 0.50.5

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: b2ba13241313978eedd0619b256d320728391aa9afaa899b6299e65e54867358
4
- data.tar.gz: 6bfc9173a29184139f7b9f0ceb543a56da3a557d3c633abae09e8bdc4c8465ee
3
+ metadata.gz: 334dbf522501eda848a1ab5d66378792b71acfd4177366de980e868be405d4dc
4
+ data.tar.gz: 1d29dd025817770f2389e99e9e11978e3ca813af5326d1c89ffef867267a82be
5
5
  SHA512:
6
- metadata.gz: 77740b75ca3ec92fdb1eff6ecbfe1020113093899652b5cee0a56fbfbbe0c40c93e26f82803df732f33464e5cf532018901118cc6092d3e204001c490966b36f
7
- data.tar.gz: c4ddcadeac684c84116e7f6fe04dff67b6f9749425bd4d5901471affacc2bfb1bb644e6a3d955718bd0ae6a788af0fd7981df621a2f15f4e372510b809190710
6
+ metadata.gz: 5c7ef8680677a244d909f482440aeed23033cfb9fbe4d109a14b474eefc4eec061436c22c39bd9b161f84c30cda9e707b9b79acf96bf54582e4fadef219ce5bd
7
+ data.tar.gz: b8f01db29cac834c91d75353dbeaa9d96c2cc5646c04eb8d17f625b63432e1304d0ae85a7a41a9f892020ab1d64a6c2fafe2cc2d28e017bc3184886da677d9f6
data/README.md CHANGED
@@ -222,12 +222,31 @@ module Tron
222
222
  config.action_mcp.version = "1.2.3" # defaults to "0.0.1"
223
223
  config.action_mcp.logging_enabled = true # defaults to true
224
224
  config.action_mcp.logging_level = :info # defaults to :info, can be :debug, :info, :warn, :error, :fatal
225
+ config.action_mcp.vibed_ignore_version = false # defaults to false, set to true to ignore client protocol version mismatches
225
226
  end
226
227
  end
227
228
  ```
228
229
 
229
230
  For dynamic versioning, consider adding the `rails_app_version` gem.
230
231
 
232
+ ### Protocol Version Compatibility
233
+
234
+ By default, ActionMCP requires clients to use the exact protocol version supported by the server (currently "2025-03-26"). If the client specifies a different version during initialization, the request will be rejected with an error.
235
+
236
+ To support clients with incompatible protocol versions, you can enable the `vibed_ignore_version` option:
237
+
238
+ ```ruby
239
+ # In config/application.rb or an initializer
240
+ Rails.application.config.action_mcp.vibed_ignore_version = true
241
+ ```
242
+
243
+ When enabled, the server will ignore protocol version mismatches from clients and always use the latest supported version. This is useful for:
244
+ - Development environments with older client libraries
245
+ - Supporting clients that cannot be easily updated
246
+ - Situations where protocol differences are minor and known to be compatible
247
+
248
+ > **Note:** Using `vibed_ignore_version = true` in production is not recommended as it may lead to unexpected behavior if clients rely on specific protocol features that differ between versions.
249
+
231
250
  ### PubSub Configuration
232
251
 
233
252
  ActionMCP uses a pub/sub system for real-time communication. You can choose between several adapters:
@@ -346,7 +365,7 @@ This ensures all thread pools are properly terminated and tasks are completed.
346
365
 
347
366
  ## Engine and Mounting
348
367
 
349
- **ActionMCP** runs as a standalone Rack application. It is **not** mounted in `routes.rb`.
368
+ **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 always available at the root path (`/`).
350
369
 
351
370
  ### Installing the Configuration Generator
352
371
 
@@ -367,17 +386,69 @@ This will create `config/mcp.yml` with example configurations for all environmen
367
386
  # Load the full Rails environment to access models, DB, Redis, etc.
368
387
  require_relative "config/environment"
369
388
 
370
- ActionMCP.configure do |config|
371
- config.mcp_endpoint_path = "/mcp"
372
- end
373
-
389
+ # No need to set a custom endpoint path. The MCP endpoint is always served at root ("/")
390
+ # when using ActionMCP::Engine directly.
374
391
  run ActionMCP::Engine
375
392
  ```
376
393
  ### 2. Start the server
377
394
  ```bash
378
- bin/rails s -c mcp.ru -p 6277 -P tmp/pids/mcp.pid
395
+ bin/rails s -c mcp.ru -p 62770 -P tmp/pids/mcps0.pid
379
396
  ```
380
397
 
398
+ ## Production Deployment of MCPS0
399
+
400
+ 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).
401
+
402
+ > **For best performance and concurrency, it is highly recommended to use a modern, synchronous server like [Falcon](https://github.com/socketry/falcon)**. Falcon is optimized for streaming and concurrent workloads, making it ideal for MCP servers. You can still use Puma, Unicorn, or Passenger, but Falcon will generally provide superior throughput and responsiveness for real-time and streaming use cases.
403
+
404
+ You have two main options for exposing the server:
405
+
406
+ ### 1. Dedicated Port
407
+
408
+ Run MCPS0 on its own TCP port (commonly `62770`):
409
+
410
+ **With Falcon:**
411
+ ```bash
412
+ bundle exec falcon serve --bind http://0.0.0.0:62770 mcp.ru
413
+ ```
414
+
415
+ **With Puma:**
416
+ ```bash
417
+ bundle exec rails s -c mcp.ru -p 62770
418
+ ```
419
+
420
+ Then, use your web server (Nginx, Apache, etc.) to reverse proxy requests to this port.
421
+
422
+ ### 2. Unix Socket
423
+
424
+ Alternatively, you can run MCPS0 on a Unix socket for improved performance and security (especially when the web server and app server are on the same machine):
425
+
426
+ **With Falcon:**
427
+ ```bash
428
+ bundle exec falcon serve --bind unix:/tmp/mcps0.sock mcp.ru
429
+ ```
430
+
431
+ **With Puma:**
432
+ ```bash
433
+ bundle exec puma -C config/puma.rb -b unix:///tmp/mcps0.sock -c mcp.ru
434
+ ```
435
+
436
+ And configure your web server to proxy to the socket:
437
+
438
+ ```nginx
439
+ location /mcp/ {
440
+ proxy_pass http://unix:/tmp/mcps0.sock:;
441
+ proxy_set_header Host $host;
442
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
443
+ }
444
+ ```
445
+
446
+ **Key Points:**
447
+ - MCPS0 is a standalone Rack app—run it separately from your main Rails server.
448
+ - You can expose it via a TCP port (e.g., 62770) or a Unix socket.
449
+ - Use a reverse proxy (Nginx, Apache, etc.) to route requests to MCPS0 as needed.
450
+ - This separation ensures reliability and scalability for both your main app and MCP services.
451
+
381
452
  ## Generators
382
453
 
383
454
  ActionMCP includes Rails generators to help you quickly set up your MCP server components.
@@ -207,8 +207,12 @@ module ActionMCP
207
207
  session_id = extract_session_id
208
208
  if session_id
209
209
  session = Session.find_by(id: session_id)
210
- if session && session.protocol_version != self.class::REQUIRED_PROTOCOL_VERSION
211
- session.update!(protocol_version: self.class::REQUIRED_PROTOCOL_VERSION)
210
+ if session
211
+ if ActionMCP.configuration.vibed_ignore_version
212
+ session.update!(protocol_version: self.class::REQUIRED_PROTOCOL_VERSION) if session.protocol_version != self.class::REQUIRED_PROTOCOL_VERSION
213
+ elsif session.protocol_version != self.class::REQUIRED_PROTOCOL_VERSION
214
+ session.update!(protocol_version: self.class::REQUIRED_PROTOCOL_VERSION)
215
+ end
212
216
  end
213
217
  session
214
218
  else
@@ -64,7 +64,7 @@ module ActionMCP
64
64
  before_create :set_server_info, if: -> { role == "server" }
65
65
  before_create :set_server_capabilities, if: -> { role == "server" }
66
66
 
67
- validates :protocol_version, inclusion: { in: SUPPORTED_VERSIONS }, allow_nil: true
67
+ validates :protocol_version, inclusion: { in: SUPPORTED_VERSIONS }, allow_nil: true, unless: -> { ActionMCP.configuration.vibed_ignore_version }
68
68
 
69
69
  def close!
70
70
  dummy_callback = ->(*) { } # this callback seem broken
@@ -102,6 +102,8 @@ module ActionMCP
102
102
  end
103
103
 
104
104
  def set_protocol_version(version)
105
+ # If vibed_ignore_version is true, always use the latest supported version
106
+ version = PROTOCOL_VERSION if ActionMCP.configuration.vibed_ignore_version
105
107
  update(protocol_version: version)
106
108
  end
107
109
 
@@ -89,18 +89,19 @@ module ActionMCP
89
89
 
90
90
  # Internal Blueprint class to represent individual URI templates
91
91
  class ResourceTemplate
92
- attr_reader :pattern, :name, :description, :mime_type
92
+ attr_reader :pattern, :name, :description, :mime_type, :annotations
93
93
 
94
94
  # Initialize a new ResourceTemplate instance
95
95
  #
96
96
  # @param data [Hash] ResourceTemplate definition hash containing uriTemplate, name, description,
97
- # and optionally mimeType
97
+ # and optionally mimeType, and annotations
98
98
  def initialize(data)
99
99
  @pattern = data["uriTemplate"]
100
100
  @name = data["name"]
101
101
  @description = data["description"]
102
102
  @mime_type = data["mimeType"]
103
103
  @variable_pattern = /{([^}]+)}/
104
+ @annotations = data["annotations"] || {}
104
105
  end
105
106
 
106
107
  # Extract variable names from the template pattern
@@ -152,7 +153,8 @@ module ActionMCP
152
153
  "uriTemplate" => @pattern,
153
154
  "name" => @name,
154
155
  "description" => @description,
155
- "mimeType" => @mime_type
156
+ "mimeType" => @mime_type,
157
+ "annotations" => @annotations
156
158
  }
157
159
  end
158
160
  end
@@ -103,7 +103,7 @@ module ActionMCP
103
103
 
104
104
  # Internal Resource class to represent individual resources
105
105
  class Resource
106
- attr_reader :uri, :name, :description, :mime_type
106
+ attr_reader :uri, :name, :description, :mime_type, :annotations
107
107
 
108
108
  # Initialize a new Resource instance
109
109
  #
@@ -113,6 +113,7 @@ module ActionMCP
113
113
  @name = data["name"]
114
114
  @description = data["description"]
115
115
  @mime_type = data["mimeType"]
116
+ @annotations = data["annotations"] || {}
116
117
  end
117
118
 
118
119
  # Get the file extension from the resource name
@@ -153,7 +154,8 @@ module ActionMCP
153
154
  "uri" => @uri,
154
155
  "name" => @name,
155
156
  "description" => @description,
156
- "mimeType" => @mime_type
157
+ "mimeType" => @mime_type,
158
+ "annotations" => @annotations
157
159
  }
158
160
  end
159
161
  end
@@ -6,18 +6,28 @@ module ActionMCP
6
6
  class Collection
7
7
  include RequestTimeouts
8
8
 
9
- attr_reader :client, :loaded
9
+ attr_reader :client, :loaded, :next_cursor, :total
10
10
 
11
11
  def initialize(items, client, silence_sql: true)
12
12
  @collection_data = items || []
13
13
  @client = client
14
14
  @loaded = !@collection_data.empty?
15
15
  @silence_sql = silence_sql
16
+ @next_cursor = nil
17
+ @total = items&.size || 0
16
18
  end
17
19
 
18
- def all
19
- silence_logs { load_items unless @loaded }
20
- @collection_data
20
+ def all(limit: nil)
21
+ if limit
22
+ # If a limit is provided, use pagination
23
+ result = []
24
+ each_page(limit: limit) { |page| result.concat(page) }
25
+ result
26
+ else
27
+ # Otherwise, maintain the old behavior
28
+ silence_logs { load_items unless @loaded }
29
+ @collection_data
30
+ end
21
31
  end
22
32
 
23
33
  def all!(timeout: DEFAULT_TIMEOUT)
@@ -25,6 +35,49 @@ module ActionMCP
25
35
  @collection_data
26
36
  end
27
37
 
38
+ # Fetch a single page of results
39
+ #
40
+ # @param cursor [String, nil] Optional cursor for pagination
41
+ # @param limit [Integer, nil] Optional limit for page size
42
+ # @return [Array<Object>] The page of items
43
+ def page(cursor: nil, limit: nil)
44
+ silence_logs { load_page(cursor: cursor, limit: limit) }
45
+ @collection_data
46
+ end
47
+
48
+ # Check if there are more pages available
49
+ #
50
+ # @return [Boolean] true if there are more pages to fetch
51
+ def has_more_pages?
52
+ !@next_cursor.nil?
53
+ end
54
+
55
+ # Fetch the next page of results
56
+ #
57
+ # @param limit [Integer, nil] Optional limit for page size
58
+ # @return [Array<Object>] The next page of items, or empty array if no more pages
59
+ def next_page(limit: nil)
60
+ return [] unless has_more_pages?
61
+ page(cursor: @next_cursor, limit: limit)
62
+ end
63
+
64
+ # Iterate through all pages of results
65
+ #
66
+ # @param limit [Integer, nil] Optional limit for page size
67
+ # @yield [page] Block to process each page
68
+ # @yieldparam page [Array<Object>] A page of items
69
+ def each_page(limit: nil, &block)
70
+ return unless block_given?
71
+
72
+ current_page = page(limit: limit)
73
+ yield current_page
74
+
75
+ while has_more_pages?
76
+ current_page = next_page(limit: limit)
77
+ yield current_page unless current_page.empty?
78
+ end
79
+ end
80
+
28
81
  # Filter items based on a given block
29
82
  #
30
83
  # @yield [item] Block that determines whether to include an item
@@ -63,6 +116,29 @@ module ActionMCP
63
116
  load_with_timeout(@load_method, force: force, timeout: timeout)
64
117
  end
65
118
 
119
+ def load_page(cursor: nil, limit: nil, timeout: DEFAULT_TIMEOUT)
120
+ # Make sure @load_method is defined in the subclass
121
+ raise NotImplementedError, "Subclass must define @load_method" unless defined?(@load_method)
122
+
123
+ # Use the RequestTimeouts module to handle the request with pagination params
124
+ params = {}
125
+ params[:cursor] = cursor if cursor
126
+ params[:limit] = limit if limit
127
+
128
+ request_id = client.send(@load_method, params)
129
+
130
+ start_time = Time.now
131
+ while !@loaded && (Time.now - start_time) < timeout
132
+ sleep(0.1)
133
+ end
134
+
135
+ # Update @loaded status even if we timed out
136
+ @loaded = true
137
+
138
+ # Return the loaded data
139
+ @collection_data
140
+ end
141
+
66
142
  private
67
143
 
68
144
  def silence_logs
@@ -85,15 +85,23 @@ module ActionMCP
85
85
  case request.rpc_method
86
86
  when "tools/list"
87
87
  client.toolbox.tools = result["tools"]
88
+ client.toolbox.instance_variable_set(:@next_cursor, result["nextCursor"])
89
+ client.toolbox.instance_variable_set(:@total, result["tools"]&.size || 0)
88
90
  return true
89
91
  when "prompts/list"
90
92
  client.prompt_book.prompts = result["prompts"]
93
+ client.prompt_book.instance_variable_set(:@next_cursor, result["nextCursor"])
94
+ client.prompt_book.instance_variable_set(:@total, result["prompts"]&.size || 0)
91
95
  return true
92
96
  when "resources/list"
93
97
  client.catalog.resources = result["resources"]
98
+ client.catalog.instance_variable_set(:@next_cursor, result["nextCursor"])
99
+ client.catalog.instance_variable_set(:@total, result["resources"]&.size || 0)
94
100
  return true
95
101
  when "resources/templates/list"
96
102
  client.blueprint.templates = result["resourceTemplates"]
103
+ client.blueprint.instance_variable_set(:@next_cursor, result["nextCursor"])
104
+ client.blueprint.instance_variable_set(:@total, result["resourceTemplates"]&.size || 0)
97
105
  return true
98
106
  end
99
107
 
@@ -4,12 +4,21 @@ module ActionMCP
4
4
  module Client
5
5
  module Prompts
6
6
  # List all available prompts from the server
7
+ # @param params [Hash] Optional parameters for pagination
8
+ # @option params [String] :cursor Pagination cursor for fetching next page
9
+ # @option params [Integer] :limit Maximum number of items to return
7
10
  # @return [String] Request ID for tracking the request
8
- def list_prompts
11
+ def list_prompts(params = {})
9
12
  request_id = SecureRandom.uuid_v7
10
13
 
11
- # Send request
12
- send_jsonrpc_request("prompts/list", id: request_id)
14
+ # Send request with pagination parameters if provided
15
+ request_params = {}
16
+ request_params[:cursor] = params[:cursor] if params[:cursor]
17
+ request_params[:limit] = params[:limit] if params[:limit]
18
+
19
+ send_jsonrpc_request("prompts/list",
20
+ params: request_params.empty? ? nil : request_params,
21
+ id: request_id)
13
22
 
14
23
  # Return request ID for tracking the request
15
24
  request_id
@@ -4,24 +4,42 @@ module ActionMCP
4
4
  module Client
5
5
  module Resources
6
6
  # List all available resources from the server
7
+ # @param params [Hash] Optional parameters for pagination
8
+ # @option params [String] :cursor Pagination cursor for fetching next page
9
+ # @option params [Integer] :limit Maximum number of items to return
7
10
  # @return [String] Request ID for tracking the request
8
- def list_resources
11
+ def list_resources(params = {})
9
12
  request_id = SecureRandom.uuid_v7
10
13
 
11
- # Send request
12
- send_jsonrpc_request("resources/list", id: request_id)
14
+ # Send request with pagination parameters if provided
15
+ request_params = {}
16
+ request_params[:cursor] = params[:cursor] if params[:cursor]
17
+ request_params[:limit] = params[:limit] if params[:limit]
18
+
19
+ send_jsonrpc_request("resources/list",
20
+ params: request_params.empty? ? nil : request_params,
21
+ id: request_id)
13
22
 
14
23
  # Return request ID for tracking the request
15
24
  request_id
16
25
  end
17
26
 
18
27
  # List resource templates from the server
28
+ # @param params [Hash] Optional parameters for pagination
29
+ # @option params [String] :cursor Pagination cursor for fetching next page
30
+ # @option params [Integer] :limit Maximum number of items to return
19
31
  # @return [String] Request ID for tracking the request
20
- def list_resource_templates
32
+ def list_resource_templates(params = {})
21
33
  request_id = SecureRandom.uuid_v7
22
34
 
23
- # Send request
24
- send_jsonrpc_request("resources/templates/list", id: request_id)
35
+ # Send request with pagination parameters if provided
36
+ request_params = {}
37
+ request_params[:cursor] = params[:cursor] if params[:cursor]
38
+ request_params[:limit] = params[:limit] if params[:limit]
39
+
40
+ send_jsonrpc_request("resources/templates/list",
41
+ params: request_params.empty? ? nil : request_params,
42
+ id: request_id)
25
43
 
26
44
  # Return request ID for tracking the request
27
45
  request_id
@@ -104,15 +104,17 @@ module ActionMCP
104
104
 
105
105
  # Internal Tool class to represent individual tools
106
106
  class Tool
107
- attr_reader :name, :description, :input_schema
107
+ attr_reader :name, :description, :input_schema, :annotations
108
108
 
109
109
  # Initialize a new Tool instance
110
110
  #
111
111
  # @param data [Hash] Tool definition hash containing name, description, and inputSchema
112
+ # and optionally annotations
112
113
  def initialize(data)
113
114
  @name = data["name"]
114
115
  @description = data["description"]
115
116
  @input_schema = data["inputSchema"] || {}
117
+ @annotations = data["annotations"] || {}
116
118
  end
117
119
 
118
120
  # Get all required properties for this tool
@@ -160,7 +162,8 @@ module ActionMCP
160
162
  {
161
163
  "name" => @name,
162
164
  "description" => @description,
163
- "inputSchema" => @input_schema
165
+ "inputSchema" => @input_schema,
166
+ "annotations" => @annotations
164
167
  }
165
168
  end
166
169
 
@@ -171,7 +174,8 @@ module ActionMCP
171
174
  {
172
175
  "name" => @name,
173
176
  "description" => @description,
174
- "input_schema" => @input_schema.transform_keys { |k| k == "inputSchema" ? "input_schema" : k }
177
+ "input_schema" => @input_schema.transform_keys { |k| k == "inputSchema" ? "input_schema" : k },
178
+ "annotations" => @annotations
175
179
  }
176
180
  end
177
181
 
@@ -184,7 +188,8 @@ module ActionMCP
184
188
  "function" => {
185
189
  "name" => @name,
186
190
  "description" => @description,
187
- "parameters" => @input_schema
191
+ "parameters" => @input_schema,
192
+ "annotations" => @annotations
188
193
  }
189
194
  }
190
195
  end
@@ -4,12 +4,21 @@ module ActionMCP
4
4
  module Client
5
5
  module Tools
6
6
  # List all available tools from the server
7
+ # @param params [Hash] Optional parameters for pagination
8
+ # @option params [String] :cursor Pagination cursor for fetching next page
9
+ # @option params [Integer] :limit Maximum number of items to return
7
10
  # @return [String] Request ID for tracking the request
8
- def list_tools
11
+ def list_tools(params = {})
9
12
  request_id = SecureRandom.uuid_v7
10
13
 
11
- # Send request
12
- send_jsonrpc_request("tools/list", id: request_id)
14
+ # Send request with pagination parameters if provided
15
+ request_params = {}
16
+ request_params[:cursor] = params[:cursor] if params[:cursor]
17
+ request_params[:limit] = params[:limit] if params[:limit]
18
+
19
+ send_jsonrpc_request("tools/list",
20
+ params: request_params.empty? ? nil : request_params,
21
+ id: request_id)
13
22
 
14
23
  # Return request ID for timeout tracking
15
24
  request_id
@@ -26,6 +26,8 @@ module ActionMCP
26
26
  :sse_heartbeat_interval,
27
27
  :post_response_preference, # :json or :sse
28
28
  :protocol_version,
29
+ # --- VibedIgnoreVersion Option ---
30
+ :vibed_ignore_version,
29
31
  # --- SSE Resumability Options ---
30
32
  :enable_sse_resumability,
31
33
  :sse_event_retention_period,
@@ -38,10 +40,10 @@ module ActionMCP
38
40
  @resources_subscribe = false
39
41
  @active_profile = :primary
40
42
  @profiles = default_profiles
41
-
42
43
  @sse_heartbeat_interval = 30
43
44
  @post_response_preference = :json
44
45
  @protocol_version = "2025-03-26"
46
+ @vibed_ignore_version = false
45
47
 
46
48
  # Resumability defaults
47
49
  @enable_sse_resumability = true
@@ -12,8 +12,9 @@ module ActionMCP
12
12
  #
13
13
  # @param data [String] The base64-encoded audio data.
14
14
  # @param mime_type [String] The MIME type of the audio data.
15
- def initialize(data, mime_type)
16
- super("audio")
15
+ # @param annotations [Hash, nil] Optional annotations for the audio content.
16
+ def initialize(data, mime_type, annotations: nil)
17
+ super("audio", annotations: annotations)
17
18
  @data = data
18
19
  @mime_type = mime_type
19
20
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Content
5
+ # Base class for all content types, supporting annotations.
6
+ class Base
7
+ attr_reader :type, :annotations
8
+
9
+ # Initializes a new content base.
10
+ #
11
+ # @param type [String] The type of the content (e.g., "text", "image", etc.)
12
+ # @param annotations [Hash, nil] Optional annotations for the content.
13
+ def initialize(type, annotations: nil)
14
+ @type = type
15
+ @annotations = annotations
16
+ end
17
+
18
+ # Returns a hash representation of the base content.
19
+ #
20
+ # @return [Hash] The hash representation of the base content.
21
+ def to_h
22
+ h = { type: @type }
23
+ h[:annotations] = @annotations if @annotations
24
+ h
25
+ end
26
+ end
27
+ end
28
+ end
@@ -12,8 +12,9 @@ module ActionMCP
12
12
  #
13
13
  # @param data [String] The base64-encoded image data.
14
14
  # @param mime_type [String] The MIME type of the image data.
15
- def initialize(data, mime_type)
16
- super("image")
15
+ # @param annotations [Hash, nil] Optional annotations for the image content.
16
+ def initialize(data, mime_type, annotations: nil)
17
+ super("image", annotations: annotations)
17
18
  @data = data
18
19
  @mime_type = mime_type
19
20
  end
@@ -22,7 +23,8 @@ module ActionMCP
22
23
  #
23
24
  # @return [Hash] The hash representation of the image content.
24
25
  def to_h
25
- super.merge(data: @data, mimeType: @mime_type)
26
+ h = super.merge(data: @data, mimeType: @mime_type)
27
+ h
26
28
  end
27
29
  end
28
30
  end
@@ -9,7 +9,7 @@ module ActionMCP
9
9
  # @return [String] The MIME type of the resource.
10
10
  # @return [String, nil] The text content of the resource (optional).
11
11
  # @return [String, nil] The base64-encoded blob of the resource (optional).
12
- attr_reader :uri, :mime_type, :text, :blob
12
+ attr_reader :uri, :mime_type, :text, :blob, :annotations
13
13
 
14
14
  # Initializes a new Resource content.
15
15
  #
@@ -17,22 +17,23 @@ module ActionMCP
17
17
  # @param mime_type [String] The MIME type of the resource.
18
18
  # @param text [String, nil] The text content of the resource (optional).
19
19
  # @param blob [String, nil] The base64-encoded blob of the resource (optional).
20
- def initialize(uri, mime_type = "text/plain", text: nil, blob: nil)
21
- super("resource")
20
+ # @param annotations [Hash, nil] Optional annotations for the resource.
21
+ def initialize(uri, mime_type = "text/plain", text: nil, blob: nil, annotations: nil)
22
+ super("resource", annotations: annotations)
22
23
  @uri = uri
23
24
  @mime_type = mime_type
24
25
  @text = text
25
26
  @blob = blob
27
+ @annotations = annotations
26
28
  end
27
29
 
28
30
  # Returns a hash representation of the resource content.
29
31
  #
30
32
  # @return [Hash] The hash representation of the resource content.
31
33
  def to_h
32
- resource_data = { uri: @uri, mimeType: @mime_type }
34
+ resource_data = super.merge(uri: @uri, mimeType: @mime_type)
33
35
  resource_data[:text] = @text if @text
34
36
  resource_data[:blob] = @blob if @blob
35
-
36
37
  resource_data
37
38
  end
38
39
  end
@@ -10,8 +10,9 @@ module ActionMCP
10
10
  # Initializes a new Text content.
11
11
  #
12
12
  # @param text [String] The text content.
13
- def initialize(text)
14
- super("text")
13
+ # @param annotations [Hash, nil] Optional annotations for the content.
14
+ def initialize(text, annotations: nil)
15
+ super("text", annotations: annotations)
15
16
  @text = text.to_s
16
17
  end
17
18
 
@@ -6,20 +6,25 @@ module ActionMCP
6
6
  # Base class for MCP content items.
7
7
  class Base
8
8
  # @return [Symbol] The type of content.
9
- attr_reader :type
9
+ # @return [Hash, nil] Optional annotations for the content.
10
+ attr_reader :type, :annotations
10
11
 
11
12
  # Initializes a new content item.
12
13
  #
13
14
  # @param type [Symbol] The type of content.
14
- def initialize(type)
15
+ # @param annotations [Hash, nil] Optional annotations for the content.
16
+ def initialize(type, annotations: nil)
15
17
  @type = type
18
+ @annotations = annotations
16
19
  end
17
20
 
18
21
  # Returns a hash representation of the content.
19
22
  #
20
23
  # @return [Hash] The hash representation.
21
24
  def to_h
22
- { type: @type }
25
+ h = { type: @type }
26
+ h[:annotations] = @annotations if @annotations
27
+ h
23
28
  end
24
29
 
25
30
  # Returns a JSON representation of the content.
@@ -22,13 +22,13 @@ module ActionMCP
22
22
  #
23
23
  def render(text: nil, audio: nil, image: nil, resource: nil, mime_type: nil, blob: nil)
24
24
  if text
25
- Content::Text.new(text)
25
+ Content::Text.new(text, annotations: nil)
26
26
  elsif audio && mime_type
27
- Content::Audio.new(audio, mime_type)
27
+ Content::Audio.new(audio, mime_type, annotations: nil)
28
28
  elsif image && mime_type
29
- Content::Image.new(image, mime_type)
29
+ Content::Image.new(image, mime_type, annotations: nil)
30
30
  elsif resource && mime_type
31
- Content::Resource.new(resource, mime_type, text: text, blob: blob)
31
+ Content::Resource.new(resource, mime_type, text: text, blob: blob, annotations: nil)
32
32
  else
33
33
  raise ArgumentError, "No content to render"
34
34
  end
@@ -20,7 +20,7 @@ module ActionMCP
20
20
  payload: { jsonrpc: "2.0", id: request_id, error: { code: -32_602, message: "Missing or invalid 'protocolVersion'" } } }
21
21
  end
22
22
  # Check if the protocol version is supported
23
- unless ActionMCP::SUPPORTED_VERSIONS.include?(client_protocol_version)
23
+ unless ActionMCP.configuration.vibed_ignore_version || ActionMCP::SUPPORTED_VERSIONS.include?(client_protocol_version)
24
24
  error_data = {
25
25
  supported: ActionMCP::SUPPORTED_VERSIONS,
26
26
  requested: client_protocol_version
@@ -53,9 +53,15 @@ module ActionMCP
53
53
  payload: { jsonrpc: "2.0", id: request_id, error: { code: -32_603, message: "Failed to initialize session" } } }
54
54
  end
55
55
 
56
- # Send the successful response with the protocol version the client requested
56
+ # Send the successful response with the correct protocol version
57
57
  capabilities_payload = session.server_capabilities_payload
58
- capabilities_payload[:protocolVersion] = client_protocol_version # Use the client's requested version
58
+ # If vibed_ignore_version is true, always use the latest supported version in the response
59
+ # Otherwise, use the client's requested version
60
+ if ActionMCP.configuration.vibed_ignore_version
61
+ capabilities_payload[:protocolVersion] = PROTOCOL_VERSION
62
+ else
63
+ capabilities_payload[:protocolVersion] = client_protocol_version
64
+ end
59
65
 
60
66
  send_jsonrpc_response(request_id, result: capabilities_payload)
61
67
  { type: :responses, id: request_id, payload: { jsonrpc: "2.0", id: request_id, result: capabilities_payload } }
@@ -92,7 +92,7 @@ module ActionMCP
92
92
  if content.is_a?(Content::Base) || (content.respond_to?(:to_h) && !content.is_a?(Hash))
93
93
  @messages << { role: role, content: content.to_h }
94
94
  else
95
- content = Content::Text.new(content).to_h if content.is_a?(String)
95
+ content = Content::Text.new(content, annotations: nil).to_h if content.is_a?(String)
96
96
  @messages << { role: role, content: content }
97
97
  end
98
98
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.50.4"
5
+ VERSION = "0.50.5"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionmcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.50.4
4
+ version: 0.50.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2025-05-14 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: activerecord
@@ -143,6 +144,7 @@ files:
143
144
  - lib/action_mcp/console_detector.rb
144
145
  - lib/action_mcp/content.rb
145
146
  - lib/action_mcp/content/audio.rb
147
+ - lib/action_mcp/content/base.rb
146
148
  - lib/action_mcp/content/image.rb
147
149
  - lib/action_mcp/content/resource.rb
148
150
  - lib/action_mcp/content/text.rb
@@ -213,6 +215,7 @@ metadata:
213
215
  source_code_uri: https://github.com/seuros/action_mcp
214
216
  changelog_uri: https://github.com/seuros/action_mcp/blob/master/CHANGELOG.md
215
217
  rubygems_mfa_required: 'true'
218
+ post_install_message:
216
219
  rdoc_options: []
217
220
  require_paths:
218
221
  - lib
@@ -227,7 +230,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
227
230
  - !ruby/object:Gem::Version
228
231
  version: '0'
229
232
  requirements: []
230
- rubygems_version: 3.6.7
233
+ rubygems_version: 3.5.22
234
+ signing_key:
231
235
  specification_version: 4
232
236
  summary: Provides essential tooling for building Model Context Protocol (MCP) capable
233
237
  servers