actionmcp 0.110.2 → 0.111.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 +4 -4
- data/lib/action_mcp/apps.rb +23 -6
- data/lib/action_mcp/capability.rb +4 -2
- data/lib/action_mcp/configuration.rb +24 -0
- data/lib/action_mcp/resource_template.rb +66 -12
- data/lib/action_mcp/tool.rb +76 -8
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +1 -1
- data/lib/generators/action_mcp/install/templates/mcp.yml +4 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c0047ad57f7f03ccd09f3ebfc05f865412bd4731c5e918b0e7886a37c9a708e1
|
|
4
|
+
data.tar.gz: c85e5ffc964b2bc2587a12b669cd5e754feff1e8266fc0abf50fa0e3a40e2b15
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a8d4f3f7f47d9ac6031ebb925aa7ef59bb4e42ce7b2273ca768e431b13860d32887db55a1599ca6fe5991d39f5339347827fef12ef78e4a96ea0dfa1671ec864
|
|
7
|
+
data.tar.gz: 45ce27835f49e3f1068ec69e76c1c9f0d54fe5c13be1cabce94993490647d282b098fed63410953e10835227d980b6ae4450b12fd65915dcf176f422c5b97c33
|
data/lib/action_mcp/apps.rb
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ActionMCP
|
|
4
|
-
# Constants for the MCP Apps extension (ext-apps, SEP-1865,
|
|
5
|
-
#
|
|
4
|
+
# Constants and helpers for the MCP Apps extension (ext-apps, SEP-1865,
|
|
5
|
+
# stable 2026-01-26).
|
|
6
|
+
# See: https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx
|
|
6
7
|
module Apps
|
|
7
8
|
EXTENSION_KEY = "io.modelcontextprotocol/ui"
|
|
8
9
|
VISIBILITY_VALUES = %w[model app].freeze
|
|
9
10
|
URI_SCHEME = %r{\Aui://\S+\z}
|
|
10
11
|
MIME_TYPE = MimeTypes::APP_HTML
|
|
12
|
+
EXTENSION_SETTINGS = { mimeTypes: [ MIME_TYPE ] }.freeze
|
|
11
13
|
|
|
12
14
|
# `_meta.ui.csp` directive keys per ext-apps spec.
|
|
13
15
|
CSP_KEYS = %i[connectDomains resourceDomains frameDomains baseUriDomains].freeze
|
|
16
|
+
PERMISSION_KEYS = %i[camera microphone geolocation clipboardWrite].freeze
|
|
14
17
|
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
# The stable MCP Apps spec allows WebSocket endpoints for connect-src only.
|
|
19
|
+
CONNECT_ORIGIN_PATTERN = %r{\A(?:https?|wss?)://[^\s"'<>]+\z}
|
|
20
|
+
RESOURCE_ORIGIN_PATTERN = %r{\Ahttps?://[^\s"'<>]+\z}
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def extension_settings
|
|
25
|
+
EXTENSION_SETTINGS.deep_dup
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def client_supports?(client_capabilities)
|
|
29
|
+
settings = client_capabilities&.dig("extensions", EXTENSION_KEY) ||
|
|
30
|
+
client_capabilities&.dig(:extensions, EXTENSION_KEY)
|
|
31
|
+
return false unless settings.is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
mime_types = settings["mimeTypes"] || settings[:mimeTypes]
|
|
34
|
+
Array(mime_types).include?(MIME_TYPE)
|
|
35
|
+
end
|
|
19
36
|
end
|
|
20
37
|
end
|
|
@@ -32,10 +32,12 @@ module ActionMCP
|
|
|
32
32
|
|
|
33
33
|
delegate :session_data, to: :session, allow_nil: true
|
|
34
34
|
|
|
35
|
-
# Returns true when the connected client advertises the MCP Apps UI extension
|
|
35
|
+
# Returns true when the connected client advertises the MCP Apps UI extension
|
|
36
|
+
# and supports ActionMCP's HTML app resource MIME type.
|
|
36
37
|
def client_supports_ui?
|
|
37
|
-
|
|
38
|
+
Apps.client_supports?(session&.client_capabilities)
|
|
38
39
|
end
|
|
40
|
+
alias client_supports_mcp_app? client_supports_ui?
|
|
39
41
|
|
|
40
42
|
# use _capability_name or default_capability_name
|
|
41
43
|
def self.capability_name
|
|
@@ -48,6 +48,8 @@ module ActionMCP
|
|
|
48
48
|
:tasks_result_strategy,
|
|
49
49
|
:tasks_result_timeout,
|
|
50
50
|
:tasks_result_poll_interval,
|
|
51
|
+
# --- MCP Apps Extension Options ---
|
|
52
|
+
:mcp_apps_enabled,
|
|
51
53
|
# --- Schema Validation Options ---
|
|
52
54
|
:validate_structured_content,
|
|
53
55
|
# --- Allowed identity keys for gateway ---
|
|
@@ -79,6 +81,9 @@ module ActionMCP
|
|
|
79
81
|
@tasks_result_timeout = 30.seconds
|
|
80
82
|
@tasks_result_poll_interval = 0.25
|
|
81
83
|
|
|
84
|
+
# MCP Apps extension defaults to explicit opt-in.
|
|
85
|
+
@mcp_apps_enabled = false
|
|
86
|
+
|
|
82
87
|
# Pagination - nil means off. Set a number to enable with that page size.
|
|
83
88
|
# Most MCP clients (including Claude Code) don't follow nextCursor yet.
|
|
84
89
|
@pagination_page_size = nil
|
|
@@ -319,6 +324,11 @@ module ActionMCP
|
|
|
319
324
|
capabilities[:tasks] = tasks_cap
|
|
320
325
|
end
|
|
321
326
|
|
|
327
|
+
if @mcp_apps_enabled
|
|
328
|
+
capabilities[:extensions] ||= {}
|
|
329
|
+
capabilities[:extensions][Apps::EXTENSION_KEY] = Apps.extension_settings
|
|
330
|
+
end
|
|
331
|
+
|
|
322
332
|
capabilities
|
|
323
333
|
end
|
|
324
334
|
|
|
@@ -341,6 +351,7 @@ module ActionMCP
|
|
|
341
351
|
@logging_enabled = options[:logging_enabled] unless options[:logging_enabled].nil?
|
|
342
352
|
@logging_level = options[:logging_level] unless options[:logging_level].nil?
|
|
343
353
|
@resources_subscribe = options[:resources_subscribe] unless options[:resources_subscribe].nil?
|
|
354
|
+
@mcp_apps_enabled = boolean_config_value(options[:mcp_apps_enabled]) unless options[:mcp_apps_enabled].nil?
|
|
344
355
|
end
|
|
345
356
|
|
|
346
357
|
def eager_load_if_needed
|
|
@@ -437,6 +448,8 @@ module ActionMCP
|
|
|
437
448
|
self.tasks_result_poll_interval = config["tasks_result_poll_interval"]
|
|
438
449
|
end
|
|
439
450
|
|
|
451
|
+
@mcp_apps_enabled = boolean_config_value(config["mcp_apps_enabled"]) if config.key?("mcp_apps_enabled")
|
|
452
|
+
|
|
440
453
|
# Extract allowed origins for DNS rebinding protection
|
|
441
454
|
if config["allowed_origins"]
|
|
442
455
|
@allowed_origins = Array(config["allowed_origins"])
|
|
@@ -470,6 +483,17 @@ module ActionMCP
|
|
|
470
483
|
duration
|
|
471
484
|
end
|
|
472
485
|
|
|
486
|
+
def boolean_config_value(value)
|
|
487
|
+
case value
|
|
488
|
+
when true, false
|
|
489
|
+
value
|
|
490
|
+
when String
|
|
491
|
+
!%w[false 0 no off].include?(value.strip.downcase)
|
|
492
|
+
else
|
|
493
|
+
!!value
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
473
497
|
def ensure_mcp_components_loaded
|
|
474
498
|
# Only load if we haven't loaded yet - but in development, always reload
|
|
475
499
|
return if @mcp_components_loaded && !Rails.env.development?
|
|
@@ -121,26 +121,79 @@ module ActionMCP
|
|
|
121
121
|
def ui(**data)
|
|
122
122
|
raise ArgumentError, "ui metadata must not be empty" if data.empty?
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
validate_ui_metadata!(data)
|
|
125
125
|
@ui_meta ||= {}
|
|
126
126
|
@ui_meta = @ui_meta.deep_merge(data)
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
def meta_with_ui(meta = nil)
|
|
130
|
+
supplied_meta = coerce_meta(meta)
|
|
131
|
+
ui_meta = @ui_meta&.any? ? { ui: @ui_meta } : {}
|
|
132
|
+
|
|
133
|
+
return nil if ui_meta.empty? && supplied_meta.empty?
|
|
134
|
+
return supplied_meta if ui_meta.empty?
|
|
135
|
+
return ui_meta if supplied_meta.empty?
|
|
136
|
+
|
|
137
|
+
ui_meta.deep_merge(supplied_meta)
|
|
138
|
+
end
|
|
139
|
+
|
|
129
140
|
private
|
|
130
141
|
|
|
142
|
+
def validate_ui_metadata!(data)
|
|
143
|
+
validate_ui_csp_origins!(data[:csp] || data["csp"])
|
|
144
|
+
validate_ui_permissions!(data[:permissions] || data["permissions"])
|
|
145
|
+
end
|
|
146
|
+
|
|
131
147
|
def validate_ui_csp_origins!(csp)
|
|
132
148
|
return unless csp.is_a?(Hash)
|
|
133
149
|
|
|
134
150
|
Apps::CSP_KEYS.each do |key|
|
|
135
|
-
|
|
136
|
-
|
|
151
|
+
pattern = key == :connectDomains ? Apps::CONNECT_ORIGIN_PATTERN : Apps::RESOURCE_ORIGIN_PATTERN
|
|
152
|
+
scheme_message = key == :connectDomains ? "http(s):// or ws(s)://" : "http(s)://"
|
|
153
|
+
Array(csp[key] || csp[key.to_s]).each do |origin|
|
|
154
|
+
next if origin.is_a?(String) && pattern.match?(origin)
|
|
137
155
|
|
|
138
156
|
raise ArgumentError,
|
|
139
|
-
"ui csp #{key} entries must be
|
|
157
|
+
"ui csp #{key} entries must be #{scheme_message} origins, got: #{origin.inspect}"
|
|
140
158
|
end
|
|
141
159
|
end
|
|
142
160
|
end
|
|
143
161
|
|
|
162
|
+
def validate_ui_permissions!(permissions)
|
|
163
|
+
return unless permissions
|
|
164
|
+
raise ArgumentError, "ui permissions must be a hash" unless permissions.is_a?(Hash)
|
|
165
|
+
|
|
166
|
+
normalized_keys = permissions.keys.map(&:to_sym)
|
|
167
|
+
invalid = normalized_keys - Apps::PERMISSION_KEYS
|
|
168
|
+
if invalid.any?
|
|
169
|
+
raise ArgumentError,
|
|
170
|
+
"ui permissions keys must be #{Apps::PERMISSION_KEYS.join('/')}, got: #{invalid.inspect}"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
permissions.each do |key, value|
|
|
174
|
+
next if value.respond_to?(:to_hash)
|
|
175
|
+
|
|
176
|
+
raise ArgumentError, "ui permissions #{key} value must be a hash, got: #{value.inspect}"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def coerce_meta(meta)
|
|
181
|
+
coerced =
|
|
182
|
+
if meta.nil?
|
|
183
|
+
{}
|
|
184
|
+
elsif meta.respond_to?(:to_hash)
|
|
185
|
+
meta.to_hash
|
|
186
|
+
elsif meta.respond_to?(:to_h)
|
|
187
|
+
meta.to_h
|
|
188
|
+
else
|
|
189
|
+
raise ArgumentError, "meta must respond to :to_hash or :to_h, got: #{meta.class}"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
coerced = coerced.deep_dup
|
|
193
|
+
coerced[:ui] = coerced.delete("ui") if coerced.key?("ui") && !coerced.key?(:ui)
|
|
194
|
+
coerced
|
|
195
|
+
end
|
|
196
|
+
|
|
144
197
|
public
|
|
145
198
|
|
|
146
199
|
def to_h
|
|
@@ -153,8 +206,8 @@ module ActionMCP
|
|
|
153
206
|
mimeType: @mime_type
|
|
154
207
|
}.compact
|
|
155
208
|
|
|
156
|
-
|
|
157
|
-
result[:_meta] =
|
|
209
|
+
meta = meta_with_ui(@_meta)
|
|
210
|
+
result[:_meta] = meta if meta&.any?
|
|
158
211
|
|
|
159
212
|
result
|
|
160
213
|
end
|
|
@@ -201,7 +254,7 @@ module ActionMCP
|
|
|
201
254
|
mime_type: mime_type || @mime_type,
|
|
202
255
|
size: size,
|
|
203
256
|
annotations: annotations,
|
|
204
|
-
meta: meta
|
|
257
|
+
meta: meta_with_ui(meta)
|
|
205
258
|
)
|
|
206
259
|
end
|
|
207
260
|
|
|
@@ -338,11 +391,13 @@ module ActionMCP
|
|
|
338
391
|
#
|
|
339
392
|
# @example
|
|
340
393
|
# render_ui(template: "mcp/ui/weather_dashboard")
|
|
341
|
-
def render_ui(text: nil, template: nil, layout: false, locals: {})
|
|
394
|
+
def render_ui(text: nil, template: nil, layout: false, locals: {}, meta: nil)
|
|
395
|
+
raise ArgumentError, "render_ui accepts either :text or :template, not both" if !text.nil? && !template.nil?
|
|
396
|
+
|
|
342
397
|
resolved =
|
|
343
|
-
if text
|
|
398
|
+
if !text.nil?
|
|
344
399
|
text
|
|
345
|
-
elsif template
|
|
400
|
+
elsif !template.nil?
|
|
346
401
|
rendered = ActionMCP::MCPAppRenderer.render(template: template, layout: layout, locals: locals)
|
|
347
402
|
if rendered.to_s.strip.empty?
|
|
348
403
|
ActionMCP.logger.warn(
|
|
@@ -356,12 +411,11 @@ module ActionMCP
|
|
|
356
411
|
raise ArgumentError, "render_ui requires :text or :template"
|
|
357
412
|
end
|
|
358
413
|
|
|
359
|
-
ui = self.class.ui_meta
|
|
360
414
|
ActionMCP::Content::Resource.new(
|
|
361
415
|
self.class.uri_template,
|
|
362
416
|
self.class.mime_type || ActionMCP::Apps::MIME_TYPE,
|
|
363
417
|
text: resolved,
|
|
364
|
-
meta: (
|
|
418
|
+
meta: self.class.meta_with_ui(meta)
|
|
365
419
|
)
|
|
366
420
|
end
|
|
367
421
|
|
data/lib/action_mcp/tool.rb
CHANGED
|
@@ -30,6 +30,7 @@ module ActionMCP
|
|
|
30
30
|
class_attribute :_output_schema_builder, instance_accessor: false, default: nil
|
|
31
31
|
class_attribute :_additional_properties, instance_accessor: false, default: nil
|
|
32
32
|
class_attribute :_cached_schema_property_keys, instance_accessor: false, default: nil
|
|
33
|
+
class_attribute :_property_aliases, instance_accessor: false, default: {}
|
|
33
34
|
class_attribute :_task_support, instance_accessor: false, default: :forbidden
|
|
34
35
|
class_attribute :_resumable_steps_block, instance_accessor: false, default: nil
|
|
35
36
|
|
|
@@ -296,16 +297,46 @@ module ActionMCP
|
|
|
296
297
|
def schema_property_keys
|
|
297
298
|
return _cached_schema_property_keys if _cached_schema_property_keys
|
|
298
299
|
|
|
299
|
-
self._cached_schema_property_keys = _schema_properties.keys.map(&:to_s)
|
|
300
|
+
self._cached_schema_property_keys = (_schema_properties.keys + _property_aliases.keys).map(&:to_s)
|
|
300
301
|
_cached_schema_property_keys
|
|
301
302
|
end
|
|
302
303
|
|
|
303
|
-
#
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
304
|
+
# Creates an alternate input name for an existing property.
|
|
305
|
+
#
|
|
306
|
+
# @example
|
|
307
|
+
# property :thread_id, type: "string", required: true
|
|
308
|
+
# alias_property :root_id, :thread_id
|
|
309
|
+
#
|
|
310
|
+
# Both `root_id` and `thread_id` are accepted when initializing the tool,
|
|
311
|
+
# and both readers resolve to the same ActiveModel attribute.
|
|
312
|
+
def alias_property(alias_name, property_name)
|
|
313
|
+
alias_key = alias_name.to_s
|
|
314
|
+
property_key = canonical_property_name(property_name)
|
|
315
|
+
|
|
316
|
+
unless _schema_properties.key?(property_key)
|
|
317
|
+
raise ArgumentError, "Cannot alias unknown property '#{property_name}'"
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
if alias_key == property_key
|
|
321
|
+
raise ArgumentError, "Cannot alias property '#{property_key}' to itself"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
if _schema_properties.key?(alias_key)
|
|
325
|
+
raise ArgumentError, "Cannot alias '#{alias_key}' because it is already defined as a property"
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
existing_alias = _property_aliases[alias_key]
|
|
329
|
+
if existing_alias && existing_alias != property_key
|
|
330
|
+
raise ArgumentError, "Alias '#{alias_key}' already points to property '#{existing_alias}'"
|
|
308
331
|
end
|
|
332
|
+
|
|
333
|
+
self._property_aliases = _property_aliases.merge(alias_key => property_key)
|
|
334
|
+
alias_attribute alias_key, property_key
|
|
335
|
+
invalidate_schema_cache
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def canonical_property_name(name)
|
|
339
|
+
_property_aliases[name.to_s] || name.to_s
|
|
309
340
|
end
|
|
310
341
|
|
|
311
342
|
private
|
|
@@ -332,6 +363,12 @@ module ActionMCP
|
|
|
332
363
|
# @param opts [Hash] Additional options for the JSON Schema.
|
|
333
364
|
# @return [void]
|
|
334
365
|
def self.property(prop_name, type: "string", description: nil, required: false, default: nil, **opts)
|
|
366
|
+
if _property_aliases.key?(prop_name.to_s)
|
|
367
|
+
raise ArgumentError, "Cannot define property '#{prop_name}' because it is already defined as an alias"
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
invalidate_schema_cache
|
|
371
|
+
|
|
335
372
|
# Build the JSON Schema definition.
|
|
336
373
|
prop_definition = { type: type }
|
|
337
374
|
prop_definition[:description] = description if description && !description.empty?
|
|
@@ -365,6 +402,12 @@ module ActionMCP
|
|
|
365
402
|
def self.collection(prop_name, type:, description: nil, required: false, default: [])
|
|
366
403
|
raise ArgumentError, "Type is required for a collection" if type.nil?
|
|
367
404
|
|
|
405
|
+
if _property_aliases.key?(prop_name.to_s)
|
|
406
|
+
raise ArgumentError, "Cannot define collection '#{prop_name}' because it is already defined as an alias"
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
invalidate_schema_cache
|
|
410
|
+
|
|
368
411
|
collection_definition = { type: "array", items: { type: type } }
|
|
369
412
|
collection_definition[:description] = description if description && !description.empty?
|
|
370
413
|
|
|
@@ -443,6 +486,8 @@ module ActionMCP
|
|
|
443
486
|
|
|
444
487
|
# Override initialize to validate parameters before ActiveModel conversion
|
|
445
488
|
def initialize(attributes = {})
|
|
489
|
+
validate_property_alias_conflicts(attributes)
|
|
490
|
+
|
|
446
491
|
# Separate additional properties from defined attributes if enabled
|
|
447
492
|
if self.class.accepts_additional_properties?
|
|
448
493
|
defined_keys = self.class.schema_property_keys
|
|
@@ -647,20 +692,43 @@ module ActionMCP
|
|
|
647
692
|
|
|
648
693
|
private
|
|
649
694
|
|
|
695
|
+
def validate_property_alias_conflicts(attributes)
|
|
696
|
+
return unless attributes.is_a?(Hash)
|
|
697
|
+
|
|
698
|
+
seen_values = {}
|
|
699
|
+
seen_keys = {}
|
|
700
|
+
|
|
701
|
+
attributes.each do |key, value|
|
|
702
|
+
key_str = key.to_s
|
|
703
|
+
property_key = self.class.canonical_property_name(key_str)
|
|
704
|
+
next unless self.class._schema_properties.key?(property_key)
|
|
705
|
+
|
|
706
|
+
if seen_values.key?(property_key) && seen_keys[property_key] != key_str && seen_values[property_key] != value
|
|
707
|
+
raise ArgumentError,
|
|
708
|
+
"Conflicting values provided for aliased property '#{property_key}' " \
|
|
709
|
+
"via '#{seen_keys[property_key]}' and '#{key_str}'"
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
seen_values[property_key] = value
|
|
713
|
+
seen_keys[property_key] = key_str
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
|
|
650
717
|
# Validates parameter types before ActiveModel conversion
|
|
651
718
|
def validate_parameter_types(attributes)
|
|
652
719
|
return unless attributes.is_a?(Hash)
|
|
653
720
|
|
|
654
721
|
attributes.each do |key, value|
|
|
655
722
|
key_str = key.to_s
|
|
656
|
-
|
|
723
|
+
property_key = self.class.canonical_property_name(key_str)
|
|
724
|
+
property_schema = self.class._schema_properties[property_key]
|
|
657
725
|
|
|
658
726
|
next unless property_schema
|
|
659
727
|
|
|
660
728
|
expected_type = property_schema[:type]
|
|
661
729
|
|
|
662
730
|
# Skip validation if value is nil and property is not required
|
|
663
|
-
next if value.nil? && !self.class._required_properties.include?(
|
|
731
|
+
next if value.nil? && !self.class._required_properties.include?(property_key)
|
|
664
732
|
|
|
665
733
|
# Validate based on expected JSON Schema type
|
|
666
734
|
case expected_type
|
data/lib/action_mcp/version.rb
CHANGED
data/lib/action_mcp.rb
CHANGED
|
@@ -42,7 +42,7 @@ module ActionMCP
|
|
|
42
42
|
LATEST_VERSION = SUPPORTED_VERSIONS.first.freeze
|
|
43
43
|
DEFAULT_PROTOCOL_VERSION = "2025-06-18" # Default to previous stable version for backwards compatibility
|
|
44
44
|
|
|
45
|
-
MIME_TYPE_APP_HTML = Apps::MIME_TYPE # MCP Apps UI resources (ext-apps,
|
|
45
|
+
MIME_TYPE_APP_HTML = Apps::MIME_TYPE # MCP Apps UI resources (ext-apps, stable 2026-01-26)
|
|
46
46
|
class << self
|
|
47
47
|
# Returns a Rack-compatible application for serving MCP requests
|
|
48
48
|
# @return [#call] A Rack application that can be used with `run ActionMCP.server`
|
|
@@ -20,6 +20,10 @@ shared:
|
|
|
20
20
|
# - "Use this server to manage project tickets and workflows"
|
|
21
21
|
# - "Helpful for tracking bugs, features, and sprint planning"
|
|
22
22
|
|
|
23
|
+
# MCP Apps are an optional extension. Enable this to advertise
|
|
24
|
+
# io.modelcontextprotocol/ui with text/html;profile=mcp-app support.
|
|
25
|
+
# mcp_apps_enabled: true
|
|
26
|
+
|
|
23
27
|
# MCP capability profiles
|
|
24
28
|
profiles:
|
|
25
29
|
primary:
|