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
data/lib/action_mcp/logging.rb
CHANGED
|
@@ -7,6 +7,8 @@ require "logger"
|
|
|
7
7
|
module ActionMCP
|
|
8
8
|
# Global MCP logging interface
|
|
9
9
|
module Logging
|
|
10
|
+
SESSION_LEVEL_KEY = "action_mcp_logging_level"
|
|
11
|
+
|
|
10
12
|
class << self
|
|
11
13
|
# Get the global logging state
|
|
12
14
|
# @return [ActionMCP::Logging::State] The global state
|
|
@@ -54,12 +56,43 @@ module ActionMCP
|
|
|
54
56
|
end
|
|
55
57
|
alias_method :set_level, :level=
|
|
56
58
|
|
|
59
|
+
# Return the minimum log level negotiated for a session, falling back to
|
|
60
|
+
# the configured server default until the client sends logging/setLevel.
|
|
61
|
+
def level_for(session)
|
|
62
|
+
session_data = session.respond_to?(:session_data) && session.session_data
|
|
63
|
+
stored_level = session_data[SESSION_LEVEL_KEY] if session_data.is_a?(Hash)
|
|
64
|
+
Level.name_for(Level.coerce(stored_level || level))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def set_level_for(session, new_level)
|
|
68
|
+
normalized_level = Level.name_for(Level.coerce(new_level))
|
|
69
|
+
session_data = session.session_data.is_a?(Hash) ? session.session_data.deep_dup : {}
|
|
70
|
+
session_data[SESSION_LEVEL_KEY] = normalized_level.to_s
|
|
71
|
+
|
|
72
|
+
if session.respond_to?(:update!)
|
|
73
|
+
session.update!(session_data: session_data)
|
|
74
|
+
else
|
|
75
|
+
session.session_data = session_data
|
|
76
|
+
session.save! if session.respond_to?(:save!)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
normalized_level
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def supported_by?(session)
|
|
83
|
+
return false unless session&.respond_to?(:server_capabilities)
|
|
84
|
+
|
|
85
|
+
capabilities = session.server_capabilities
|
|
86
|
+
capabilities.is_a?(Hash) &&
|
|
87
|
+
(capabilities.key?("logging") || capabilities.key?(:logging))
|
|
88
|
+
end
|
|
89
|
+
|
|
57
90
|
# Create a logger for the given session
|
|
58
91
|
# @param name [String, nil] Optional logger name
|
|
59
92
|
# @param session [ActionMCP::Session] The MCP session
|
|
60
93
|
# @return [ActionMCP::Logging::Logger, ActionMCP::Logging::NullLogger] logger instance
|
|
61
94
|
def logger(name: nil, session:)
|
|
62
|
-
if enabled?
|
|
95
|
+
if enabled? && supported_by?(session)
|
|
63
96
|
Logger.new(name: name, session: session, state: state)
|
|
64
97
|
else
|
|
65
98
|
NullLogger.new
|
|
@@ -7,8 +7,8 @@ module ActionMCP
|
|
|
7
7
|
# Rack middleware that validates the Origin header on MCP requests to
|
|
8
8
|
# prevent DNS rebinding attacks per the MCP Streamable HTTP security
|
|
9
9
|
# section. Non-browser clients (Claude Desktop, curl) never send Origin
|
|
10
|
-
# and are always allowed. Present Origins must match
|
|
11
|
-
# `ActionMCP.configuration.allowed_origins
|
|
10
|
+
# and are always allowed. Present Origins must match the explicitly trusted
|
|
11
|
+
# hosts in `ActionMCP.configuration.allowed_origins`.
|
|
12
12
|
#
|
|
13
13
|
# Runs as middleware — same layer as `ActionDispatch::HostAuthorization` —
|
|
14
14
|
# so invalid requests are rejected before they reach routing.
|
|
@@ -28,7 +28,7 @@ module ActionMCP
|
|
|
28
28
|
|
|
29
29
|
request = ActionDispatch::Request.new(env)
|
|
30
30
|
origin = request.origin
|
|
31
|
-
return @app.call(env) if origin.nil?
|
|
31
|
+
return @app.call(env) if origin.nil?
|
|
32
32
|
return @app.call(env) if origin_allowed?(origin, request)
|
|
33
33
|
|
|
34
34
|
forbidden_response
|
|
@@ -49,21 +49,17 @@ module ActionMCP
|
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
def origin_allowed?(origin,
|
|
52
|
+
def origin_allowed?(origin, _request)
|
|
53
53
|
return false if origin == "null"
|
|
54
54
|
|
|
55
55
|
uri = URI.parse(origin)
|
|
56
|
-
return false
|
|
56
|
+
return false unless %w[http https].include?(uri.scheme)
|
|
57
|
+
return false if uri.host.nil? || uri.host.empty? || uri.userinfo || uri.query || uri.fragment
|
|
58
|
+
return false unless uri.path.empty?
|
|
57
59
|
|
|
58
60
|
origin_host = strip_brackets(uri.host)
|
|
59
|
-
allowed = ActionMCP.configuration.allowed_origins
|
|
60
|
-
|
|
61
|
-
if allowed && !allowed.empty?
|
|
62
|
-
allowed.any? { |pattern| match?(pattern, origin_host) }
|
|
63
|
-
else
|
|
64
|
-
# ActionDispatch::Request#host strips the port and handles IPv6 brackets
|
|
65
|
-
origin_host.casecmp?(strip_brackets(request.host))
|
|
66
|
-
end
|
|
61
|
+
allowed = Array(ActionMCP.configuration.allowed_origins)
|
|
62
|
+
allowed.any? { |pattern| match?(pattern, origin_host) }
|
|
67
63
|
rescue URI::InvalidURIError
|
|
68
64
|
false
|
|
69
65
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "schema_helpers"
|
|
4
|
+
require_relative "schema_validator"
|
|
4
5
|
|
|
5
6
|
module ActionMCP
|
|
6
7
|
# DSL builder for creating output JSON Schema from Ruby-like syntax
|
|
@@ -173,6 +174,7 @@ module ActionMCP
|
|
|
173
174
|
# Generate the final JSON Schema
|
|
174
175
|
def to_json_schema
|
|
175
176
|
schema = {
|
|
177
|
+
"$schema" => SchemaValidator::DEFAULT_DIALECT,
|
|
176
178
|
"type" => "object",
|
|
177
179
|
"properties" => @properties
|
|
178
180
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module ActionMCP
|
|
4
4
|
class PromptResponse < BaseResponse
|
|
5
|
+
ROLES = %w[user assistant].freeze
|
|
6
|
+
|
|
5
7
|
attr_reader :messages
|
|
6
8
|
|
|
7
9
|
# Delegate methods to the underlying messages array
|
|
@@ -14,7 +16,13 @@ module ActionMCP
|
|
|
14
16
|
|
|
15
17
|
# Add a message to the response
|
|
16
18
|
def add_message(role:, content:)
|
|
17
|
-
|
|
19
|
+
normalized_role = role.to_s
|
|
20
|
+
unless ROLES.include?(normalized_role)
|
|
21
|
+
raise ArgumentError, "role must be one of: #{ROLES.join(', ')}"
|
|
22
|
+
end
|
|
23
|
+
raise ArgumentError, "content must be an MCP content object" unless content.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
@messages << { role: normalized_role, content: Content::Validation.copy_content_block!(content) }
|
|
18
26
|
self
|
|
19
27
|
end
|
|
20
28
|
|
|
@@ -26,9 +34,11 @@ module ActionMCP
|
|
|
26
34
|
|
|
27
35
|
# Implementation of build_success_hash for PromptResponse
|
|
28
36
|
def build_success_hash
|
|
29
|
-
{
|
|
37
|
+
result = {
|
|
30
38
|
messages: @messages
|
|
31
39
|
}
|
|
40
|
+
Content::Validation.validate_prompt_result!(result)
|
|
41
|
+
result
|
|
32
42
|
end
|
|
33
43
|
|
|
34
44
|
# Implementation of compare_with_same_class for PromptResponse
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json_schemer"
|
|
4
|
+
|
|
5
|
+
module ActionMCP
|
|
6
|
+
# Validates the stable MCP JSON-RPC envelope and client message parameters.
|
|
7
|
+
class ProtocolValidator
|
|
8
|
+
ValidationError = Data.define(:code, :message)
|
|
9
|
+
|
|
10
|
+
ID_SCHEMA = { "type" => [ "string", "integer" ] }.freeze
|
|
11
|
+
PROGRESS_TOKEN_SCHEMA = { "type" => [ "string", "integer" ] }.freeze
|
|
12
|
+
|
|
13
|
+
REQUEST_META_SCHEMA = {
|
|
14
|
+
"type" => "object",
|
|
15
|
+
"properties" => {
|
|
16
|
+
"progressToken" => PROGRESS_TOKEN_SCHEMA
|
|
17
|
+
},
|
|
18
|
+
"additionalProperties" => true
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
NOTIFICATION_META_SCHEMA = {
|
|
22
|
+
"type" => "object",
|
|
23
|
+
"additionalProperties" => true
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
REQUEST_PARAMS_SCHEMA = {
|
|
27
|
+
"type" => "object",
|
|
28
|
+
"properties" => {
|
|
29
|
+
"_meta" => REQUEST_META_SCHEMA
|
|
30
|
+
},
|
|
31
|
+
"additionalProperties" => true
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
NOTIFICATION_PARAMS_SCHEMA = {
|
|
35
|
+
"type" => "object",
|
|
36
|
+
"properties" => {
|
|
37
|
+
"_meta" => NOTIFICATION_META_SCHEMA
|
|
38
|
+
},
|
|
39
|
+
"additionalProperties" => true
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
PAGINATED_PARAMS_SCHEMA = {
|
|
43
|
+
"type" => "object",
|
|
44
|
+
"properties" => {
|
|
45
|
+
"_meta" => REQUEST_META_SCHEMA,
|
|
46
|
+
"cursor" => { "type" => "string" }
|
|
47
|
+
},
|
|
48
|
+
"additionalProperties" => true
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
ICON_SCHEMA = {
|
|
52
|
+
"type" => "object",
|
|
53
|
+
"required" => [ "src" ],
|
|
54
|
+
"properties" => {
|
|
55
|
+
"src" => { "type" => "string", "format" => "uri" },
|
|
56
|
+
"mimeType" => { "type" => "string" },
|
|
57
|
+
"sizes" => {
|
|
58
|
+
"type" => "array",
|
|
59
|
+
"items" => { "type" => "string" }
|
|
60
|
+
},
|
|
61
|
+
"theme" => {
|
|
62
|
+
"type" => "string",
|
|
63
|
+
"enum" => %w[dark light]
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"additionalProperties" => true
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
IMPLEMENTATION_SCHEMA = {
|
|
70
|
+
"type" => "object",
|
|
71
|
+
"required" => %w[name version],
|
|
72
|
+
"properties" => {
|
|
73
|
+
"name" => { "type" => "string" },
|
|
74
|
+
"title" => { "type" => "string" },
|
|
75
|
+
"version" => { "type" => "string" },
|
|
76
|
+
"description" => { "type" => "string" },
|
|
77
|
+
"websiteUrl" => { "type" => "string", "format" => "uri" },
|
|
78
|
+
"icons" => {
|
|
79
|
+
"type" => "array",
|
|
80
|
+
"items" => ICON_SCHEMA
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"additionalProperties" => true
|
|
84
|
+
}.freeze
|
|
85
|
+
|
|
86
|
+
EMPTY_CAPABILITY_SCHEMA = {
|
|
87
|
+
"type" => "object",
|
|
88
|
+
"additionalProperties" => true
|
|
89
|
+
}.freeze
|
|
90
|
+
|
|
91
|
+
CLIENT_CAPABILITIES_SCHEMA = {
|
|
92
|
+
"type" => "object",
|
|
93
|
+
"properties" => {
|
|
94
|
+
"elicitation" => {
|
|
95
|
+
"type" => "object",
|
|
96
|
+
"properties" => {
|
|
97
|
+
"form" => EMPTY_CAPABILITY_SCHEMA,
|
|
98
|
+
"url" => EMPTY_CAPABILITY_SCHEMA
|
|
99
|
+
},
|
|
100
|
+
"additionalProperties" => true
|
|
101
|
+
},
|
|
102
|
+
"experimental" => {
|
|
103
|
+
"type" => "object",
|
|
104
|
+
"additionalProperties" => EMPTY_CAPABILITY_SCHEMA
|
|
105
|
+
},
|
|
106
|
+
"roots" => {
|
|
107
|
+
"type" => "object",
|
|
108
|
+
"properties" => {
|
|
109
|
+
"listChanged" => { "type" => "boolean" }
|
|
110
|
+
},
|
|
111
|
+
"additionalProperties" => true
|
|
112
|
+
},
|
|
113
|
+
"sampling" => {
|
|
114
|
+
"type" => "object",
|
|
115
|
+
"properties" => {
|
|
116
|
+
"context" => EMPTY_CAPABILITY_SCHEMA,
|
|
117
|
+
"tools" => EMPTY_CAPABILITY_SCHEMA
|
|
118
|
+
},
|
|
119
|
+
"additionalProperties" => true
|
|
120
|
+
},
|
|
121
|
+
"tasks" => {
|
|
122
|
+
"type" => "object",
|
|
123
|
+
"properties" => {
|
|
124
|
+
"cancel" => EMPTY_CAPABILITY_SCHEMA,
|
|
125
|
+
"list" => EMPTY_CAPABILITY_SCHEMA,
|
|
126
|
+
"requests" => {
|
|
127
|
+
"type" => "object",
|
|
128
|
+
"properties" => {
|
|
129
|
+
"elicitation" => {
|
|
130
|
+
"type" => "object",
|
|
131
|
+
"properties" => {
|
|
132
|
+
"create" => EMPTY_CAPABILITY_SCHEMA
|
|
133
|
+
},
|
|
134
|
+
"additionalProperties" => true
|
|
135
|
+
},
|
|
136
|
+
"sampling" => {
|
|
137
|
+
"type" => "object",
|
|
138
|
+
"properties" => {
|
|
139
|
+
"createMessage" => EMPTY_CAPABILITY_SCHEMA
|
|
140
|
+
},
|
|
141
|
+
"additionalProperties" => true
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"additionalProperties" => true
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"additionalProperties" => true
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"additionalProperties" => true
|
|
151
|
+
}.freeze
|
|
152
|
+
|
|
153
|
+
INITIALIZE_PARAMS_SCHEMA = {
|
|
154
|
+
"$schema" => "https://json-schema.org/draft/2020-12/schema",
|
|
155
|
+
"type" => "object",
|
|
156
|
+
"required" => %w[protocolVersion capabilities clientInfo],
|
|
157
|
+
"properties" => {
|
|
158
|
+
"_meta" => REQUEST_META_SCHEMA,
|
|
159
|
+
"protocolVersion" => { "type" => "string" },
|
|
160
|
+
"capabilities" => CLIENT_CAPABILITIES_SCHEMA,
|
|
161
|
+
"clientInfo" => IMPLEMENTATION_SCHEMA
|
|
162
|
+
},
|
|
163
|
+
"additionalProperties" => true
|
|
164
|
+
}.freeze
|
|
165
|
+
|
|
166
|
+
RESOURCE_URI_PARAMS_SCHEMA = {
|
|
167
|
+
"type" => "object",
|
|
168
|
+
"required" => [ "uri" ],
|
|
169
|
+
"properties" => {
|
|
170
|
+
"_meta" => REQUEST_META_SCHEMA,
|
|
171
|
+
"uri" => { "type" => "string", "format" => "uri" }
|
|
172
|
+
},
|
|
173
|
+
"additionalProperties" => true
|
|
174
|
+
}.freeze
|
|
175
|
+
|
|
176
|
+
GET_PROMPT_PARAMS_SCHEMA = {
|
|
177
|
+
"type" => "object",
|
|
178
|
+
"required" => [ "name" ],
|
|
179
|
+
"properties" => {
|
|
180
|
+
"_meta" => REQUEST_META_SCHEMA,
|
|
181
|
+
"name" => { "type" => "string" },
|
|
182
|
+
"arguments" => {
|
|
183
|
+
"type" => "object",
|
|
184
|
+
"additionalProperties" => { "type" => "string" }
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
"additionalProperties" => true
|
|
188
|
+
}.freeze
|
|
189
|
+
|
|
190
|
+
TASK_METADATA_SCHEMA = {
|
|
191
|
+
"type" => "object",
|
|
192
|
+
"properties" => {
|
|
193
|
+
"ttl" => { "type" => "integer" }
|
|
194
|
+
},
|
|
195
|
+
"additionalProperties" => true
|
|
196
|
+
}.freeze
|
|
197
|
+
|
|
198
|
+
CALL_TOOL_PARAMS_SCHEMA = {
|
|
199
|
+
"type" => "object",
|
|
200
|
+
"required" => [ "name" ],
|
|
201
|
+
"properties" => {
|
|
202
|
+
"_meta" => REQUEST_META_SCHEMA,
|
|
203
|
+
"name" => { "type" => "string" },
|
|
204
|
+
"arguments" => {
|
|
205
|
+
"type" => "object",
|
|
206
|
+
"additionalProperties" => true
|
|
207
|
+
},
|
|
208
|
+
"task" => TASK_METADATA_SCHEMA
|
|
209
|
+
},
|
|
210
|
+
"additionalProperties" => true
|
|
211
|
+
}.freeze
|
|
212
|
+
|
|
213
|
+
TASK_ID_PARAMS_SCHEMA = {
|
|
214
|
+
"type" => "object",
|
|
215
|
+
"required" => [ "taskId" ],
|
|
216
|
+
"properties" => {
|
|
217
|
+
"taskId" => { "type" => "string" }
|
|
218
|
+
},
|
|
219
|
+
"additionalProperties" => true
|
|
220
|
+
}.freeze
|
|
221
|
+
|
|
222
|
+
SET_LEVEL_PARAMS_SCHEMA = {
|
|
223
|
+
"type" => "object",
|
|
224
|
+
"required" => [ "level" ],
|
|
225
|
+
"properties" => {
|
|
226
|
+
"_meta" => REQUEST_META_SCHEMA,
|
|
227
|
+
"level" => {
|
|
228
|
+
"type" => "string",
|
|
229
|
+
"enum" => %w[alert critical debug emergency error info notice warning]
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
"additionalProperties" => true
|
|
233
|
+
}.freeze
|
|
234
|
+
|
|
235
|
+
COMPLETE_PARAMS_SCHEMA = {
|
|
236
|
+
"type" => "object",
|
|
237
|
+
"required" => %w[argument ref],
|
|
238
|
+
"properties" => {
|
|
239
|
+
"_meta" => REQUEST_META_SCHEMA,
|
|
240
|
+
"argument" => {
|
|
241
|
+
"type" => "object",
|
|
242
|
+
"required" => %w[name value],
|
|
243
|
+
"properties" => {
|
|
244
|
+
"name" => { "type" => "string" },
|
|
245
|
+
"value" => { "type" => "string" }
|
|
246
|
+
},
|
|
247
|
+
"additionalProperties" => true
|
|
248
|
+
},
|
|
249
|
+
"context" => {
|
|
250
|
+
"type" => "object",
|
|
251
|
+
"properties" => {
|
|
252
|
+
"arguments" => {
|
|
253
|
+
"type" => "object",
|
|
254
|
+
"additionalProperties" => { "type" => "string" }
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
"additionalProperties" => true
|
|
258
|
+
},
|
|
259
|
+
"ref" => {
|
|
260
|
+
"anyOf" => [
|
|
261
|
+
{
|
|
262
|
+
"type" => "object",
|
|
263
|
+
"required" => %w[name type],
|
|
264
|
+
"properties" => {
|
|
265
|
+
"name" => { "type" => "string" },
|
|
266
|
+
"title" => { "type" => "string" },
|
|
267
|
+
"type" => { "const" => "ref/prompt" }
|
|
268
|
+
},
|
|
269
|
+
"additionalProperties" => true
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"type" => "object",
|
|
273
|
+
"required" => %w[type uri],
|
|
274
|
+
"properties" => {
|
|
275
|
+
"type" => { "const" => "ref/resource" },
|
|
276
|
+
"uri" => { "type" => "string", "format" => "uri-template" }
|
|
277
|
+
},
|
|
278
|
+
"additionalProperties" => true
|
|
279
|
+
}
|
|
280
|
+
]
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
"additionalProperties" => true
|
|
284
|
+
}.freeze
|
|
285
|
+
|
|
286
|
+
CANCELLED_NOTIFICATION_PARAMS_SCHEMA = {
|
|
287
|
+
"type" => "object",
|
|
288
|
+
"properties" => {
|
|
289
|
+
"_meta" => NOTIFICATION_META_SCHEMA,
|
|
290
|
+
"reason" => { "type" => "string" },
|
|
291
|
+
"requestId" => ID_SCHEMA
|
|
292
|
+
},
|
|
293
|
+
"additionalProperties" => true
|
|
294
|
+
}.freeze
|
|
295
|
+
|
|
296
|
+
PROGRESS_NOTIFICATION_PARAMS_SCHEMA = {
|
|
297
|
+
"type" => "object",
|
|
298
|
+
"required" => %w[progress progressToken],
|
|
299
|
+
"properties" => {
|
|
300
|
+
"_meta" => NOTIFICATION_META_SCHEMA,
|
|
301
|
+
"message" => { "type" => "string" },
|
|
302
|
+
"progress" => { "type" => "number" },
|
|
303
|
+
"progressToken" => PROGRESS_TOKEN_SCHEMA,
|
|
304
|
+
"total" => { "type" => "number" }
|
|
305
|
+
},
|
|
306
|
+
"additionalProperties" => true
|
|
307
|
+
}.freeze
|
|
308
|
+
|
|
309
|
+
TASK_STATUS_NOTIFICATION_PARAMS_SCHEMA = {
|
|
310
|
+
"type" => "object",
|
|
311
|
+
"required" => %w[createdAt lastUpdatedAt status taskId ttl],
|
|
312
|
+
"properties" => {
|
|
313
|
+
"_meta" => NOTIFICATION_META_SCHEMA,
|
|
314
|
+
"createdAt" => { "type" => "string" },
|
|
315
|
+
"lastUpdatedAt" => { "type" => "string" },
|
|
316
|
+
"pollInterval" => { "type" => "integer" },
|
|
317
|
+
"status" => {
|
|
318
|
+
"type" => "string",
|
|
319
|
+
"enum" => %w[cancelled completed failed input_required working]
|
|
320
|
+
},
|
|
321
|
+
"statusMessage" => { "type" => "string" },
|
|
322
|
+
"taskId" => { "type" => "string" },
|
|
323
|
+
"ttl" => { "type" => [ "integer", "null" ] }
|
|
324
|
+
},
|
|
325
|
+
"additionalProperties" => true
|
|
326
|
+
}.freeze
|
|
327
|
+
|
|
328
|
+
MESSAGE_SCHEMA = {
|
|
329
|
+
"$schema" => "https://json-schema.org/draft/2020-12/schema",
|
|
330
|
+
"anyOf" => [
|
|
331
|
+
{
|
|
332
|
+
"type" => "object",
|
|
333
|
+
"required" => %w[jsonrpc id method],
|
|
334
|
+
"properties" => {
|
|
335
|
+
"jsonrpc" => { "const" => "2.0" },
|
|
336
|
+
"id" => ID_SCHEMA,
|
|
337
|
+
"method" => { "type" => "string" },
|
|
338
|
+
"params" => { "type" => "object", "additionalProperties" => true }
|
|
339
|
+
},
|
|
340
|
+
"additionalProperties" => true
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"type" => "object",
|
|
344
|
+
"required" => %w[jsonrpc method],
|
|
345
|
+
"not" => { "required" => [ "id" ] },
|
|
346
|
+
"properties" => {
|
|
347
|
+
"jsonrpc" => { "const" => "2.0" },
|
|
348
|
+
"method" => { "type" => "string" },
|
|
349
|
+
"params" => { "type" => "object", "additionalProperties" => true }
|
|
350
|
+
},
|
|
351
|
+
"additionalProperties" => true
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"type" => "object",
|
|
355
|
+
"required" => %w[jsonrpc id result],
|
|
356
|
+
"properties" => {
|
|
357
|
+
"jsonrpc" => { "const" => "2.0" },
|
|
358
|
+
"id" => ID_SCHEMA,
|
|
359
|
+
"result" => { "type" => "object", "additionalProperties" => true }
|
|
360
|
+
},
|
|
361
|
+
"additionalProperties" => true
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
"type" => "object",
|
|
365
|
+
"required" => %w[jsonrpc error],
|
|
366
|
+
"properties" => {
|
|
367
|
+
"jsonrpc" => { "const" => "2.0" },
|
|
368
|
+
"id" => ID_SCHEMA,
|
|
369
|
+
"error" => {
|
|
370
|
+
"type" => "object",
|
|
371
|
+
"required" => %w[code message],
|
|
372
|
+
"properties" => {
|
|
373
|
+
"code" => { "type" => "integer" },
|
|
374
|
+
"message" => { "type" => "string" },
|
|
375
|
+
"data" => {}
|
|
376
|
+
},
|
|
377
|
+
"additionalProperties" => true
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
"additionalProperties" => true
|
|
381
|
+
}
|
|
382
|
+
]
|
|
383
|
+
}.freeze
|
|
384
|
+
|
|
385
|
+
REQUEST_PARAM_SCHEMAS = {
|
|
386
|
+
"initialize" => INITIALIZE_PARAMS_SCHEMA,
|
|
387
|
+
"ping" => REQUEST_PARAMS_SCHEMA,
|
|
388
|
+
"resources/list" => PAGINATED_PARAMS_SCHEMA,
|
|
389
|
+
"resources/templates/list" => PAGINATED_PARAMS_SCHEMA,
|
|
390
|
+
"resources/read" => RESOURCE_URI_PARAMS_SCHEMA,
|
|
391
|
+
"resources/subscribe" => RESOURCE_URI_PARAMS_SCHEMA,
|
|
392
|
+
"resources/unsubscribe" => RESOURCE_URI_PARAMS_SCHEMA,
|
|
393
|
+
"prompts/list" => PAGINATED_PARAMS_SCHEMA,
|
|
394
|
+
"prompts/get" => GET_PROMPT_PARAMS_SCHEMA,
|
|
395
|
+
"tools/list" => PAGINATED_PARAMS_SCHEMA,
|
|
396
|
+
"tools/call" => CALL_TOOL_PARAMS_SCHEMA,
|
|
397
|
+
"tasks/get" => TASK_ID_PARAMS_SCHEMA,
|
|
398
|
+
"tasks/result" => TASK_ID_PARAMS_SCHEMA,
|
|
399
|
+
"tasks/cancel" => TASK_ID_PARAMS_SCHEMA,
|
|
400
|
+
"tasks/list" => PAGINATED_PARAMS_SCHEMA,
|
|
401
|
+
"logging/setLevel" => SET_LEVEL_PARAMS_SCHEMA,
|
|
402
|
+
"completion/complete" => COMPLETE_PARAMS_SCHEMA
|
|
403
|
+
}.freeze
|
|
404
|
+
|
|
405
|
+
REQUIRED_REQUEST_PARAMS = %w[
|
|
406
|
+
initialize
|
|
407
|
+
resources/read
|
|
408
|
+
resources/subscribe
|
|
409
|
+
resources/unsubscribe
|
|
410
|
+
prompts/get
|
|
411
|
+
tools/call
|
|
412
|
+
tasks/get
|
|
413
|
+
tasks/result
|
|
414
|
+
tasks/cancel
|
|
415
|
+
logging/setLevel
|
|
416
|
+
completion/complete
|
|
417
|
+
].freeze
|
|
418
|
+
|
|
419
|
+
NOTIFICATION_PARAM_SCHEMAS = {
|
|
420
|
+
"notifications/cancelled" => CANCELLED_NOTIFICATION_PARAMS_SCHEMA,
|
|
421
|
+
"notifications/initialized" => NOTIFICATION_PARAMS_SCHEMA,
|
|
422
|
+
"notifications/progress" => PROGRESS_NOTIFICATION_PARAMS_SCHEMA,
|
|
423
|
+
"notifications/tasks/status" => TASK_STATUS_NOTIFICATION_PARAMS_SCHEMA,
|
|
424
|
+
"notifications/roots/list_changed" => NOTIFICATION_PARAMS_SCHEMA
|
|
425
|
+
}.freeze
|
|
426
|
+
|
|
427
|
+
REQUIRED_NOTIFICATION_PARAMS = %w[
|
|
428
|
+
notifications/cancelled
|
|
429
|
+
notifications/progress
|
|
430
|
+
notifications/tasks/status
|
|
431
|
+
].freeze
|
|
432
|
+
|
|
433
|
+
MESSAGE_SCHEMER = JSONSchemer.schema(MESSAGE_SCHEMA)
|
|
434
|
+
REQUEST_PARAM_SCHEMERS = REQUEST_PARAM_SCHEMAS.transform_values { |schema| JSONSchemer.schema(schema) }.freeze
|
|
435
|
+
NOTIFICATION_PARAM_SCHEMERS = NOTIFICATION_PARAM_SCHEMAS.transform_values do |schema|
|
|
436
|
+
JSONSchemer.schema(schema)
|
|
437
|
+
end.freeze
|
|
438
|
+
|
|
439
|
+
class << self
|
|
440
|
+
def valid_message?(payload)
|
|
441
|
+
MESSAGE_SCHEMER.valid?(normalize(payload))
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def valid?(payload)
|
|
445
|
+
valid_message?(payload)
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def valid_initialize_params?(params)
|
|
449
|
+
REQUEST_PARAM_SCHEMERS.fetch("initialize").valid?(normalize(params))
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def client_message_validation_error(payload)
|
|
453
|
+
case payload
|
|
454
|
+
when JSON_RPC::Request
|
|
455
|
+
validate_request(payload)
|
|
456
|
+
when JSON_RPC::Notification
|
|
457
|
+
validate_notification(payload)
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def request_params_validation_error(method, params)
|
|
462
|
+
schemer = REQUEST_PARAM_SCHEMERS[method]
|
|
463
|
+
return unless schemer
|
|
464
|
+
|
|
465
|
+
validate_params(method, params, schemer, REQUIRED_REQUEST_PARAMS)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
private
|
|
469
|
+
|
|
470
|
+
def validate_request(payload)
|
|
471
|
+
request_params_validation_error(payload.method, payload.params)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def validate_notification(payload)
|
|
475
|
+
schemer = NOTIFICATION_PARAM_SCHEMERS[payload.method]
|
|
476
|
+
unless schemer
|
|
477
|
+
return ValidationError.new(
|
|
478
|
+
code: :method_not_found,
|
|
479
|
+
message: "Unsupported MCP notification method: #{payload.method}"
|
|
480
|
+
)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
validate_params(payload.method, payload.params, schemer, REQUIRED_NOTIFICATION_PARAMS)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def validate_params(method, params, schemer, required_methods)
|
|
487
|
+
if params.nil?
|
|
488
|
+
return unless required_methods.include?(method)
|
|
489
|
+
|
|
490
|
+
return ValidationError.new(code: :invalid_params, message: "Invalid params for #{method}: params are required")
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
errors = schemer.validate(normalize(params)).to_a
|
|
494
|
+
return if errors.empty?
|
|
495
|
+
|
|
496
|
+
ValidationError.new(
|
|
497
|
+
code: :invalid_params,
|
|
498
|
+
message: "Invalid params for #{method}: #{validation_messages(errors).join(', ')}"
|
|
499
|
+
)
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def validation_messages(errors)
|
|
503
|
+
errors.map do |error|
|
|
504
|
+
error["error"].presence || "#{error['data_pointer'].presence || '/'} (#{error['type']})"
|
|
505
|
+
end.uniq
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def normalize(value)
|
|
509
|
+
value.is_a?(Hash) ? value.deep_stringify_keys : value
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
end
|