actionmcp 0.111.1 → 0.200.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/README.md +26 -18
- data/Rakefile +3 -0
- data/app/controllers/action_mcp/application_controller.rb +166 -109
- data/app/controllers/action_mcp/mcp_app_renderer.rb +1 -0
- data/app/helpers/action_mcp/apps_helper.rb +48 -0
- data/app/jobs/action_mcp/tool_execution_job.rb +67 -35
- data/app/models/action_mcp/session/message.rb +8 -3
- data/app/models/action_mcp/session/task.rb +15 -5
- data/app/models/action_mcp/session.rb +93 -19
- data/config/routes.rb +1 -1
- data/lib/action_mcp/apps/javascript/ext_apps.LICENSE +216 -0
- data/lib/action_mcp/apps/javascript/ext_apps.js +78 -0
- data/lib/action_mcp/apps/view_manifest.rb +170 -0
- data/lib/action_mcp/apps.rb +17 -2
- data/lib/action_mcp/configuration.rb +14 -30
- data/lib/action_mcp/content/audio.rb +4 -1
- data/lib/action_mcp/content/base.rb +3 -1
- data/lib/action_mcp/content/image.rb +4 -1
- data/lib/action_mcp/content/resource.rb +15 -14
- data/lib/action_mcp/content/resource_link.rb +15 -5
- data/lib/action_mcp/content/text.rb +2 -1
- data/lib/action_mcp/content/validation.rb +253 -0
- data/lib/action_mcp/content.rb +3 -1
- data/lib/action_mcp/dev/runner.rb +203 -0
- data/lib/action_mcp/engine.rb +24 -2
- data/lib/action_mcp/json_rpc_handler_base.rb +7 -12
- data/lib/action_mcp/logging/logger.rb +16 -11
- data/lib/action_mcp/logging.rb +34 -1
- data/lib/action_mcp/middleware/origin_validation.rb +9 -13
- data/lib/action_mcp/output_schema_builder.rb +2 -0
- data/lib/action_mcp/prompt_response.rb +12 -2
- data/lib/action_mcp/protocol_validator.rb +513 -0
- data/lib/action_mcp/renderable.rb +4 -4
- data/lib/action_mcp/resource.rb +15 -11
- data/lib/action_mcp/resource_template.rb +105 -90
- data/lib/action_mcp/resource_templates_registry.rb +31 -97
- data/lib/action_mcp/schema_validator.rb +43 -0
- data/lib/action_mcp/server/base_session.rb +134 -28
- data/lib/action_mcp/server/base_session_store.rb +1 -1
- data/lib/action_mcp/server/capabilities.rb +10 -30
- data/lib/action_mcp/server/client_notifications.rb +63 -0
- data/lib/action_mcp/server/completions.rb +68 -0
- data/lib/action_mcp/server/elicitation.rb +47 -25
- data/lib/action_mcp/server/elicitation_request.rb +136 -51
- data/lib/action_mcp/server/handlers/logging_handler.rb +7 -5
- data/lib/action_mcp/server/handlers/prompt_handler.rb +12 -1
- data/lib/action_mcp/server/handlers/resource_handler.rb +41 -7
- data/lib/action_mcp/server/handlers/task_handler.rb +26 -0
- data/lib/action_mcp/server/handlers/tool_handler.rb +8 -0
- data/lib/action_mcp/server/json_rpc_handler.rb +5 -21
- data/lib/action_mcp/server/messaging_service.rb +3 -1
- data/lib/action_mcp/server/prompts.rb +5 -3
- data/lib/action_mcp/server/resources.rb +35 -2
- data/lib/action_mcp/server/response_collector.rb +5 -1
- data/lib/action_mcp/server/roots.rb +15 -2
- data/lib/action_mcp/server/sampling.rb +36 -1
- data/lib/action_mcp/server/sampling_request.rb +317 -55
- data/lib/action_mcp/server/tasks.rb +27 -45
- data/lib/action_mcp/server/tool_result.rb +27 -0
- data/lib/action_mcp/server/tools.rb +12 -10
- data/lib/action_mcp/server/transport_handler.rb +3 -0
- data/lib/action_mcp/server/url_elicitation_request.rb +13 -0
- data/lib/action_mcp/tool.rb +133 -199
- data/lib/action_mcp/tool_response.rb +35 -4
- data/lib/action_mcp/tools_registry.rb +1 -1
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +2 -5
- data/lib/generators/action_mcp/identifier/identifier_generator.rb +1 -1
- data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +1 -1
- data/lib/generators/action_mcp/install/templates/bin/mcp +16 -4
- data/lib/generators/action_mcp/install/templates/mcp/config.ru.tt +2 -2
- data/lib/generators/action_mcp/install/templates/mcp.yml +4 -0
- data/lib/generators/action_mcp/resource_template/resource_template_generator.rb +18 -1
- data/lib/generators/action_mcp/resource_template/templates/resource_template_ui.rb.erb +27 -0
- data/lib/generators/action_mcp/tool/templates/tool.rb.erb +6 -0
- data/lib/generators/action_mcp/tool/tool_generator.rb +2 -0
- data/lib/generators/action_mcp/view/templates/resource_template.rb.erb +16 -0
- data/lib/generators/action_mcp/view/templates/view.html.erb +27 -0
- data/lib/generators/action_mcp/view/view_generator.rb +57 -0
- data/lib/tasks/action_mcp_apps.rake +17 -0
- data/lib/tasks/action_mcp_tasks.rake +135 -74
- metadata +47 -17
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionMCP
|
|
4
|
+
module Apps
|
|
5
|
+
# Namespace for ResourceTemplate classes generated from a compiled-views
|
|
6
|
+
# manifest.
|
|
7
|
+
module Views; end
|
|
8
|
+
|
|
9
|
+
# Loads `.action_mcp/views/manifest.json` (emitted by
|
|
10
|
+
# @action-mcp/vite-plugin) and registers one ResourceTemplate per compiled
|
|
11
|
+
# view, serving the bundle inlined into an HTML shell as a `ui://` resource.
|
|
12
|
+
class ViewManifest
|
|
13
|
+
SCHEMA_VERSION = 1
|
|
14
|
+
MANIFEST_FILENAME = "manifest.json"
|
|
15
|
+
|
|
16
|
+
class Error < StandardError; end
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Registers templates for every view in the manifest. Idempotent:
|
|
20
|
+
# previously generated view classes are evicted first, so rebuilt
|
|
21
|
+
# manifests (new hashes) load cleanly on app reload.
|
|
22
|
+
#
|
|
23
|
+
# @return [Array<Class>] the generated ResourceTemplate subclasses
|
|
24
|
+
def load!(dir = nil)
|
|
25
|
+
dir = resolve_dir(dir)
|
|
26
|
+
manifest_path = File.join(dir, MANIFEST_FILENAME)
|
|
27
|
+
unless File.exist?(manifest_path)
|
|
28
|
+
evict_generated_templates!
|
|
29
|
+
return []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
data = parse_manifest(manifest_path)
|
|
33
|
+
evict_generated_templates!
|
|
34
|
+
|
|
35
|
+
aliases = {}
|
|
36
|
+
classes = data.fetch("views", {}).map do |name, entry|
|
|
37
|
+
klass, logical_uri, resource_uri = register_view(dir, name, entry)
|
|
38
|
+
if aliases.key?(logical_uri)
|
|
39
|
+
raise Error, "Duplicate logicalUri #{logical_uri.inspect} in views manifest"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
aliases[logical_uri] = resource_uri
|
|
43
|
+
klass
|
|
44
|
+
end
|
|
45
|
+
@resource_uri_aliases = aliases.freeze
|
|
46
|
+
classes
|
|
47
|
+
rescue StandardError
|
|
48
|
+
evict_generated_templates!
|
|
49
|
+
raise
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Resolves the stable URI declared by a tool to the content-addressed
|
|
53
|
+
# resource URI currently registered from the compiled views manifest.
|
|
54
|
+
def resolve_resource_uri(uri)
|
|
55
|
+
(@resource_uri_aliases || {}).fetch(uri, uri)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def resolve_dir(dir)
|
|
61
|
+
path = (dir || ActionMCP.configuration.mcp_apps_views_path).to_s
|
|
62
|
+
return path if File.absolute_path?(path)
|
|
63
|
+
|
|
64
|
+
defined?(Rails) && Rails.root ? Rails.root.join(path).to_s : File.expand_path(path)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def parse_manifest(path)
|
|
68
|
+
data = JSON.parse(File.read(path))
|
|
69
|
+
version = data["schemaVersion"]
|
|
70
|
+
unless version == SCHEMA_VERSION
|
|
71
|
+
raise Error, "Unsupported views manifest schemaVersion #{version.inspect} at #{path} " \
|
|
72
|
+
"(expected #{SCHEMA_VERSION}; rebuild with a matching @action-mcp/vite-plugin)"
|
|
73
|
+
end
|
|
74
|
+
data
|
|
75
|
+
rescue JSON::ParserError => e
|
|
76
|
+
raise Error, "Invalid views manifest at #{path}: #{e.message}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def evict_generated_templates!
|
|
80
|
+
generated = Views.constants(false).filter_map do |const|
|
|
81
|
+
Views.const_get(const, false)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
generated.concat(ResourceTemplate.registered_templates.select { |klass| generated_template?(klass) })
|
|
85
|
+
generated.concat(ResourceTemplatesRegistry.items.values.select { |klass| generated_template?(klass) })
|
|
86
|
+
generated.uniq!
|
|
87
|
+
|
|
88
|
+
ResourceTemplate.registered_templates.delete_if do |klass|
|
|
89
|
+
generated.include?(klass) || generated_template?(klass)
|
|
90
|
+
end
|
|
91
|
+
ResourceTemplatesRegistry.items.delete_if do |_name, klass|
|
|
92
|
+
generated.include?(klass) || generated_template?(klass)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
Views.constants(false).each do |const|
|
|
96
|
+
Views.send(:remove_const, const)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
@resource_uri_aliases = {}.freeze
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def generated_template?(klass)
|
|
103
|
+
klass.is_a?(Class) && klass.name&.start_with?("#{Views.name}::")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def register_view(dir, name, entry)
|
|
107
|
+
unless entry.is_a?(Hash)
|
|
108
|
+
raise Error, "Views manifest entry for #{name.inspect} must be an object"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
bundle_path = File.join(dir, entry.fetch("file"))
|
|
112
|
+
unless File.exist?(bundle_path)
|
|
113
|
+
raise Error, "Views manifest references missing bundle #{entry['file'].inspect} for view #{name.inspect}. " \
|
|
114
|
+
"Rebuild with action-mcp-build-views."
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
uri = entry.fetch("uri")
|
|
118
|
+
logical_uri = entry.fetch("logicalUri", "ui://views/#{name}")
|
|
119
|
+
validate_view_uri!(uri, field: "uri", view_name: name)
|
|
120
|
+
validate_view_uri!(logical_uri, field: "logicalUri", view_name: name)
|
|
121
|
+
ui_meta = entry["ui"]
|
|
122
|
+
|
|
123
|
+
klass = Class.new(ResourceTemplate)
|
|
124
|
+
const_name = "#{name.underscore.camelize}View"
|
|
125
|
+
Views.const_set(const_name, klass)
|
|
126
|
+
# The inheritance hook sees the class before it has a constant name
|
|
127
|
+
# and may cache/register the empty capability name. Reset that state
|
|
128
|
+
# now that the generated class has its final identity.
|
|
129
|
+
klass.remove_instance_variable(:@capability_name) if klass.instance_variable_defined?(:@capability_name)
|
|
130
|
+
|
|
131
|
+
klass.class_eval do
|
|
132
|
+
description "Compiled MCP Apps view: #{name}"
|
|
133
|
+
uri_template uri
|
|
134
|
+
mime_type :mcp_app
|
|
135
|
+
ui(**ui_meta.deep_symbolize_keys) if ui_meta.is_a?(Hash) && ui_meta.any?
|
|
136
|
+
|
|
137
|
+
define_method(:resolve) do
|
|
138
|
+
render_ui(text: ViewManifest.send(:html_shell, name, bundle_path))
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
ResourceTemplatesRegistry.items.delete_if { |_key, registered| registered.equal?(klass) }
|
|
143
|
+
ResourceTemplatesRegistry.register(klass)
|
|
144
|
+
|
|
145
|
+
[ klass, logical_uri, uri ]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def validate_view_uri!(uri, field:, view_name:)
|
|
149
|
+
return if uri.is_a?(String) && Apps::URI_SCHEME.match?(uri)
|
|
150
|
+
|
|
151
|
+
raise Error, "Views manifest #{field} for #{view_name.inspect} must be a ui:// URI, got: #{uri.inspect}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def html_shell(name, bundle_path)
|
|
155
|
+
js = File.read(bundle_path).gsub("</script", "<\\/script")
|
|
156
|
+
<<~HTML
|
|
157
|
+
<!doctype html>
|
|
158
|
+
<html>
|
|
159
|
+
<head><meta charset="utf-8"><title>#{ERB::Util.html_escape(name)}</title></head>
|
|
160
|
+
<body>
|
|
161
|
+
<div id="root"></div>
|
|
162
|
+
<script type="module">#{js}</script>
|
|
163
|
+
</body>
|
|
164
|
+
</html>
|
|
165
|
+
HTML
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
data/lib/action_mcp/apps.rb
CHANGED
|
@@ -11,7 +11,8 @@ module ActionMCP
|
|
|
11
11
|
MIME_TYPE = MimeTypes::APP_HTML
|
|
12
12
|
EXTENSION_SETTINGS = { mimeTypes: [ MIME_TYPE ] }.freeze
|
|
13
13
|
|
|
14
|
-
# `_meta.ui
|
|
14
|
+
# `_meta.ui` fields and nested CSP directive keys per ext-apps spec.
|
|
15
|
+
UI_META_KEYS = %i[csp permissions domain prefersBorder].freeze
|
|
15
16
|
CSP_KEYS = %i[connectDomains resourceDomains frameDomains baseUriDomains].freeze
|
|
16
17
|
PERMISSION_KEYS = %i[camera microphone geolocation clipboardWrite].freeze
|
|
17
18
|
|
|
@@ -19,8 +20,22 @@ module ActionMCP
|
|
|
19
20
|
CONNECT_ORIGIN_PATTERN = %r{\A(?:https?|wss?)://[^\s"'<>]+\z}
|
|
20
21
|
RESOURCE_ORIGIN_PATTERN = %r{\Ahttps?://[^\s"'<>]+\z}
|
|
21
22
|
|
|
23
|
+
# Vendored browser bridge: the official self-contained ESM bundle from
|
|
24
|
+
# @modelcontextprotocol/ext-apps (app-with-deps export). Refresh with
|
|
25
|
+
# bin/update-apps-bridge.
|
|
26
|
+
BRIDGE_PACKAGE = "@modelcontextprotocol/ext-apps"
|
|
27
|
+
BRIDGE_VERSION = "1.7.4"
|
|
28
|
+
BRIDGE_PATH = File.expand_path("apps/javascript/ext_apps.js", __dir__)
|
|
29
|
+
|
|
22
30
|
module_function
|
|
23
31
|
|
|
32
|
+
# Raw ESM source of the vendored ext-apps browser bundle, with `</script>`
|
|
33
|
+
# sequences neutralized so it can be inlined into a <script> element.
|
|
34
|
+
# (`<\/` and `/` are identical inside JS strings and regexes.)
|
|
35
|
+
def bridge_source
|
|
36
|
+
@bridge_source ||= File.read(BRIDGE_PATH).gsub("</script", "<\\/script").freeze
|
|
37
|
+
end
|
|
38
|
+
|
|
24
39
|
def extension_settings
|
|
25
40
|
EXTENSION_SETTINGS.deep_dup
|
|
26
41
|
end
|
|
@@ -31,7 +46,7 @@ module ActionMCP
|
|
|
31
46
|
return false unless settings.is_a?(Hash)
|
|
32
47
|
|
|
33
48
|
mime_types = settings["mimeTypes"] || settings[:mimeTypes]
|
|
34
|
-
Array
|
|
49
|
+
mime_types.is_a?(Array) && mime_types.include?(MIME_TYPE)
|
|
35
50
|
end
|
|
36
51
|
end
|
|
37
52
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "gateway"
|
|
4
|
-
require "active_support/core_ext/integer/time"
|
|
5
4
|
|
|
6
5
|
module ActionMCP
|
|
7
6
|
# Configuration class to hold settings for the ActionMCP server.
|
|
@@ -45,13 +44,10 @@ module ActionMCP
|
|
|
45
44
|
:tasks_enabled,
|
|
46
45
|
:tasks_list_enabled,
|
|
47
46
|
:tasks_cancel_enabled,
|
|
48
|
-
:tasks_result_strategy,
|
|
49
|
-
:tasks_result_timeout,
|
|
50
47
|
:tasks_result_poll_interval,
|
|
51
48
|
# --- MCP Apps Extension Options ---
|
|
52
49
|
:mcp_apps_enabled,
|
|
53
|
-
|
|
54
|
-
:validate_structured_content,
|
|
50
|
+
:mcp_apps_views_path,
|
|
55
51
|
# --- Allowed identity keys for gateway ---
|
|
56
52
|
:allowed_identity_keys,
|
|
57
53
|
# --- JSON-RPC Path ---
|
|
@@ -71,26 +67,24 @@ module ActionMCP
|
|
|
71
67
|
# Authentication defaults - empty means all configured identifiers will be tried
|
|
72
68
|
@authentication_methods = []
|
|
73
69
|
|
|
74
|
-
@protocol_version = "2025-
|
|
70
|
+
@protocol_version = "2025-11-25"
|
|
75
71
|
|
|
76
72
|
# Tasks defaults (MCP 2025-11-25)
|
|
77
73
|
@tasks_enabled = false
|
|
78
74
|
@tasks_list_enabled = true
|
|
79
75
|
@tasks_cancel_enabled = true
|
|
80
|
-
@tasks_result_strategy = :blocking_http
|
|
81
|
-
@tasks_result_timeout = 30.seconds
|
|
82
76
|
@tasks_result_poll_interval = 0.25
|
|
83
77
|
|
|
84
78
|
# MCP Apps extension defaults to explicit opt-in.
|
|
85
79
|
@mcp_apps_enabled = false
|
|
80
|
+
# Directory holding compiled view bundles + manifest.json, relative to
|
|
81
|
+
# Rails.root unless absolute.
|
|
82
|
+
@mcp_apps_views_path = ".action_mcp/views"
|
|
86
83
|
|
|
87
84
|
# Pagination - nil means off. Set a number to enable with that page size.
|
|
88
85
|
# Most MCP clients (including Claude Code) don't follow nextCursor yet.
|
|
89
86
|
@pagination_page_size = nil
|
|
90
87
|
|
|
91
|
-
# Schema validation - disabled by default for backward compatibility
|
|
92
|
-
@validate_structured_content = false
|
|
93
|
-
|
|
94
88
|
# Server instructions - empty by default
|
|
95
89
|
@server_instructions = []
|
|
96
90
|
|
|
@@ -110,8 +104,9 @@ module ActionMCP
|
|
|
110
104
|
# Path for JSON-RPC endpoint
|
|
111
105
|
@base_path = "/"
|
|
112
106
|
|
|
113
|
-
#
|
|
114
|
-
|
|
107
|
+
# Browser origins allowed to reach a local server. Remote deployments must
|
|
108
|
+
# explicitly add their trusted origin hosts.
|
|
109
|
+
@allowed_origins = %w[localhost 127.0.0.1 ::1].freeze
|
|
115
110
|
end
|
|
116
111
|
|
|
117
112
|
def name
|
|
@@ -163,19 +158,6 @@ module ActionMCP
|
|
|
163
158
|
end
|
|
164
159
|
end
|
|
165
160
|
|
|
166
|
-
def tasks_result_strategy=(value)
|
|
167
|
-
strategy = value.to_sym
|
|
168
|
-
unless %i[blocking_http polling_only].include?(strategy)
|
|
169
|
-
raise ArgumentError, "tasks_result_strategy must be :blocking_http or :polling_only, got: #{value.inspect}"
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
@tasks_result_strategy = strategy
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def tasks_result_timeout=(value)
|
|
176
|
-
@tasks_result_timeout = normalize_positive_duration(value, "tasks_result_timeout")
|
|
177
|
-
end
|
|
178
|
-
|
|
179
161
|
def tasks_result_poll_interval=(value)
|
|
180
162
|
@tasks_result_poll_interval = normalize_positive_duration(value, "tasks_result_poll_interval")
|
|
181
163
|
end
|
|
@@ -184,12 +166,14 @@ module ActionMCP
|
|
|
184
166
|
# Resolve gateway class lazily to account for Zeitwerk autoloading
|
|
185
167
|
# This allows ApplicationGateway to be loaded from app/mcp even if the
|
|
186
168
|
# configuration is initialized before Zeitwerk runs
|
|
187
|
-
if
|
|
169
|
+
if instance_variable_defined?(:@gateway_class)
|
|
170
|
+
@gateway_class
|
|
171
|
+
elsif @gateway_class_name
|
|
188
172
|
@gateway_class_name.constantize
|
|
189
173
|
elsif defined?(::ApplicationGateway)
|
|
190
174
|
::ApplicationGateway
|
|
191
175
|
else
|
|
192
|
-
|
|
176
|
+
nil
|
|
193
177
|
end
|
|
194
178
|
end
|
|
195
179
|
|
|
@@ -305,6 +289,7 @@ module ActionMCP
|
|
|
305
289
|
capabilities[:prompts] = { listChanged: @list_changed } if profile && profile[:prompts]&.any?
|
|
306
290
|
|
|
307
291
|
capabilities[:logging] = {} if @logging_enabled
|
|
292
|
+
capabilities[:completions] = {}
|
|
308
293
|
|
|
309
294
|
# If profile includes resources, advertise resources capability
|
|
310
295
|
if profile && profile[:resources]&.any?
|
|
@@ -442,13 +427,12 @@ module ActionMCP
|
|
|
442
427
|
self.pagination_page_size = config["pagination_page_size"]
|
|
443
428
|
end
|
|
444
429
|
|
|
445
|
-
self.tasks_result_strategy = config["tasks_result_strategy"] if config.key?("tasks_result_strategy")
|
|
446
|
-
self.tasks_result_timeout = config["tasks_result_timeout"] if config.key?("tasks_result_timeout")
|
|
447
430
|
if config.key?("tasks_result_poll_interval")
|
|
448
431
|
self.tasks_result_poll_interval = config["tasks_result_poll_interval"]
|
|
449
432
|
end
|
|
450
433
|
|
|
451
434
|
@mcp_apps_enabled = boolean_config_value(config["mcp_apps_enabled"]) if config.key?("mcp_apps_enabled")
|
|
435
|
+
@mcp_apps_views_path = config["mcp_apps_views_path"].to_s if config.key?("mcp_apps_views_path")
|
|
452
436
|
|
|
453
437
|
# Extract allowed origins for DNS rebinding protection
|
|
454
438
|
if config["allowed_origins"]
|
|
@@ -17,13 +17,16 @@ module ActionMCP
|
|
|
17
17
|
super("audio", annotations: annotations)
|
|
18
18
|
@data = data
|
|
19
19
|
@mime_type = mime_type
|
|
20
|
+
to_h
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
# Returns a hash representation of the audio content.
|
|
23
24
|
#
|
|
24
25
|
# @return [Hash] The hash representation of the audio content.
|
|
25
26
|
def to_h
|
|
26
|
-
super.merge(data: @data, mimeType: @mime_type)
|
|
27
|
+
super.merge(data: @data, mimeType: @mime_type).tap do |result|
|
|
28
|
+
Validation.validate_content_block!(result)
|
|
29
|
+
end
|
|
27
30
|
end
|
|
28
31
|
end
|
|
29
32
|
end
|
|
@@ -12,7 +12,8 @@ module ActionMCP
|
|
|
12
12
|
# @param annotations [Hash, nil] Optional annotations for the content.
|
|
13
13
|
def initialize(type, annotations: nil)
|
|
14
14
|
@type = type
|
|
15
|
-
@annotations = annotations
|
|
15
|
+
@annotations = annotations.nil? ? nil : Validation.copy_object!(annotations, "annotations")
|
|
16
|
+
Validation.validate_annotations!(@annotations)
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
# Returns a hash representation of the base content.
|
|
@@ -21,6 +22,7 @@ module ActionMCP
|
|
|
21
22
|
def to_h
|
|
22
23
|
h = { type: @type }
|
|
23
24
|
h[:annotations] = @annotations if @annotations
|
|
25
|
+
Validation.validate_annotations!(@annotations)
|
|
24
26
|
h
|
|
25
27
|
end
|
|
26
28
|
end
|
|
@@ -17,13 +17,16 @@ module ActionMCP
|
|
|
17
17
|
super("image", annotations: annotations)
|
|
18
18
|
@data = data
|
|
19
19
|
@mime_type = mime_type
|
|
20
|
+
to_h
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
# Returns a hash representation of the image content.
|
|
23
24
|
#
|
|
24
25
|
# @return [Hash] The hash representation of the image content.
|
|
25
26
|
def to_h
|
|
26
|
-
super.merge(data: @data, mimeType: @mime_type)
|
|
27
|
+
super.merge(data: @data, mimeType: @mime_type).tap do |result|
|
|
28
|
+
Validation.validate_content_block!(result)
|
|
29
|
+
end
|
|
27
30
|
end
|
|
28
31
|
end
|
|
29
32
|
end
|
|
@@ -21,22 +21,23 @@ module ActionMCP
|
|
|
21
21
|
# @param annotations [Hash, nil] Optional annotations for the resource.
|
|
22
22
|
# @param meta [Hash, #to_hash, #to_h, nil] Optional extension metadata. Emitted on the wire as `_meta`.
|
|
23
23
|
def initialize(uri, mime_type = "text/plain", text: nil, blob: nil, annotations: nil, meta: nil)
|
|
24
|
+
raise ArgumentError, "uri must be a non-empty string" unless uri.is_a?(String) && uri.present?
|
|
25
|
+
unless mime_type.nil? || (mime_type.is_a?(String) && mime_type.present?)
|
|
26
|
+
raise ArgumentError, "mime_type must be a non-empty string or nil"
|
|
27
|
+
end
|
|
28
|
+
unless [ !text.nil?, !blob.nil? ].one?
|
|
29
|
+
raise ArgumentError, "embedded resources require exactly one of text or blob"
|
|
30
|
+
end
|
|
31
|
+
raise ArgumentError, "text must be a string" if !text.nil? && !text.is_a?(String)
|
|
32
|
+
raise ArgumentError, "blob must be a base64 string" if !blob.nil? && !blob.is_a?(String)
|
|
33
|
+
|
|
24
34
|
super("resource", annotations: annotations)
|
|
25
35
|
@uri = uri
|
|
26
36
|
@mime_type = mime_type
|
|
27
37
|
@text = text
|
|
28
38
|
@blob = blob
|
|
29
|
-
@
|
|
30
|
-
|
|
31
|
-
if meta.nil?
|
|
32
|
-
nil
|
|
33
|
-
elsif meta.respond_to?(:to_hash)
|
|
34
|
-
meta.to_hash
|
|
35
|
-
elsif meta.respond_to?(:to_h)
|
|
36
|
-
meta.to_h
|
|
37
|
-
else
|
|
38
|
-
raise ArgumentError, "meta must respond to :to_hash or :to_h, got: #{meta.class}"
|
|
39
|
-
end
|
|
39
|
+
@meta = meta.nil? ? nil : Validation.copy_object!(meta, "meta")
|
|
40
|
+
to_h
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
# Returns a hash representation of the resource content.
|
|
@@ -46,13 +47,13 @@ module ActionMCP
|
|
|
46
47
|
#
|
|
47
48
|
# @return [Hash] The hash representation of the resource content.
|
|
48
49
|
def to_h
|
|
49
|
-
inner = { uri: @uri
|
|
50
|
+
inner = { uri: @uri }
|
|
51
|
+
inner[:mimeType] = @mime_type if @mime_type
|
|
50
52
|
inner[:text] = @text if @text
|
|
51
53
|
inner[:blob] = @blob if @blob
|
|
52
|
-
inner[:annotations] = @annotations if @annotations
|
|
53
54
|
inner[:_meta] = @meta if @meta && !@meta.empty?
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
super.merge(resource: inner).tap { |result| Validation.validate_content_block!(result) }
|
|
56
57
|
end
|
|
57
58
|
end
|
|
58
59
|
end
|
|
@@ -7,7 +7,7 @@ module ActionMCP
|
|
|
7
7
|
# Note: resource links returned by tools are not guaranteed to appear in resources/list requests.
|
|
8
8
|
class ResourceLink < Base
|
|
9
9
|
# @return [String] The URI of the resource.
|
|
10
|
-
# @return [String
|
|
10
|
+
# @return [String] The name of the resource.
|
|
11
11
|
# @return [String, nil] The description of the resource (optional).
|
|
12
12
|
# @return [String, nil] The MIME type of the resource (optional).
|
|
13
13
|
attr_reader :uri, :name, :description, :mime_type
|
|
@@ -15,26 +15,36 @@ module ActionMCP
|
|
|
15
15
|
# Initializes a new ResourceLink content.
|
|
16
16
|
#
|
|
17
17
|
# @param uri [String] The URI of the resource.
|
|
18
|
-
# @param name [String
|
|
18
|
+
# @param name [String] The name of the resource.
|
|
19
19
|
# @param description [String, nil] The description of the resource (optional).
|
|
20
20
|
# @param mime_type [String, nil] The MIME type of the resource (optional).
|
|
21
21
|
# @param annotations [Hash, nil] Optional annotations for the resource link.
|
|
22
|
-
def initialize(uri, name
|
|
22
|
+
def initialize(uri, name:, description: nil, mime_type: nil, annotations: nil)
|
|
23
|
+
raise ArgumentError, "uri must be a non-empty string" unless uri.is_a?(String) && uri.present?
|
|
24
|
+
raise ArgumentError, "name must be a non-empty string" unless name.is_a?(String) && name.present?
|
|
25
|
+
unless description.nil? || description.is_a?(String)
|
|
26
|
+
raise ArgumentError, "description must be a string or nil"
|
|
27
|
+
end
|
|
28
|
+
unless mime_type.nil? || mime_type.is_a?(String)
|
|
29
|
+
raise ArgumentError, "mime_type must be a string or nil"
|
|
30
|
+
end
|
|
31
|
+
|
|
23
32
|
super("resource_link", annotations: annotations)
|
|
24
33
|
@uri = uri
|
|
25
34
|
@name = name
|
|
26
35
|
@description = description
|
|
27
36
|
@mime_type = mime_type
|
|
37
|
+
to_h
|
|
28
38
|
end
|
|
29
39
|
|
|
30
40
|
# Returns a hash representation of the resource link content.
|
|
31
41
|
#
|
|
32
42
|
# @return [Hash] The hash representation of the resource link content.
|
|
33
43
|
def to_h
|
|
34
|
-
result = super.merge(uri: @uri)
|
|
35
|
-
result[:name] = @name if @name
|
|
44
|
+
result = super.merge(uri: @uri, name: @name)
|
|
36
45
|
result[:description] = @description if @description
|
|
37
46
|
result[:mimeType] = @mime_type if @mime_type
|
|
47
|
+
Validation.validate_content_block!(result)
|
|
38
48
|
result
|
|
39
49
|
end
|
|
40
50
|
end
|
|
@@ -14,13 +14,14 @@ module ActionMCP
|
|
|
14
14
|
def initialize(text, annotations: nil)
|
|
15
15
|
super("text", annotations: annotations)
|
|
16
16
|
@text = text.to_s
|
|
17
|
+
to_h
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
# Returns a hash representation of the text content.
|
|
20
21
|
#
|
|
21
22
|
# @return [Hash] The hash representation of the text content.
|
|
22
23
|
def to_h
|
|
23
|
-
super.merge(text: @text)
|
|
24
|
+
super.merge(text: @text).tap { |result| Validation.validate_content_block!(result) }
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
end
|