Class: ActionMCP::ProtocolValidator

Inherits:
Object
  • Object
show all
Defined in:
lib/action_mcp/protocol_validator.rb

Overview

Validates the stable MCP JSON-RPC envelope and client message parameters.

Defined Under Namespace

Classes: ValidationError

Constant Summary collapse

ID_SCHEMA =
{ "type" => [ "string", "integer" ] }.freeze
PROGRESS_TOKEN_SCHEMA =
{ "type" => [ "string", "integer" ] }.freeze
REQUEST_META_SCHEMA =
{
  "type" => "object",
  "properties" => {
    "progressToken" => PROGRESS_TOKEN_SCHEMA
  },
  "additionalProperties" => true
}.freeze
NOTIFICATION_META_SCHEMA =
{
  "type" => "object",
  "additionalProperties" => true
}.freeze
REQUEST_PARAMS_SCHEMA =
{
  "type" => "object",
  "properties" => {
    "_meta" => REQUEST_META_SCHEMA
  },
  "additionalProperties" => true
}.freeze
NOTIFICATION_PARAMS_SCHEMA =
{
  "type" => "object",
  "properties" => {
    "_meta" => NOTIFICATION_META_SCHEMA
  },
  "additionalProperties" => true
}.freeze
PAGINATED_PARAMS_SCHEMA =
{
  "type" => "object",
  "properties" => {
    "_meta" => REQUEST_META_SCHEMA,
    "cursor" => { "type" => "string" }
  },
  "additionalProperties" => true
}.freeze
ICON_SCHEMA =
{
  "type" => "object",
  "required" => [ "src" ],
  "properties" => {
    "src" => { "type" => "string", "format" => "uri" },
    "mimeType" => { "type" => "string" },
    "sizes" => {
      "type" => "array",
      "items" => { "type" => "string" }
    },
    "theme" => {
      "type" => "string",
      "enum" => %w[dark light]
    }
  },
  "additionalProperties" => true
}.freeze
IMPLEMENTATION_SCHEMA =
{
  "type" => "object",
  "required" => %w[name version],
  "properties" => {
    "name" => { "type" => "string" },
    "title" => { "type" => "string" },
    "version" => { "type" => "string" },
    "description" => { "type" => "string" },
    "websiteUrl" => { "type" => "string", "format" => "uri" },
    "icons" => {
      "type" => "array",
      "items" => ICON_SCHEMA
    }
  },
  "additionalProperties" => true
}.freeze
EMPTY_CAPABILITY_SCHEMA =
{
  "type" => "object",
  "additionalProperties" => true
}.freeze
CLIENT_CAPABILITIES_SCHEMA =
{
  "type" => "object",
  "properties" => {
    "elicitation" => {
      "type" => "object",
      "properties" => {
        "form" => EMPTY_CAPABILITY_SCHEMA,
        "url" => EMPTY_CAPABILITY_SCHEMA
      },
      "additionalProperties" => true
    },
    "experimental" => {
      "type" => "object",
      "additionalProperties" => EMPTY_CAPABILITY_SCHEMA
    },
    "roots" => {
      "type" => "object",
      "properties" => {
        "listChanged" => { "type" => "boolean" }
      },
      "additionalProperties" => true
    },
    "sampling" => {
      "type" => "object",
      "properties" => {
        "context" => EMPTY_CAPABILITY_SCHEMA,
        "tools" => EMPTY_CAPABILITY_SCHEMA
      },
      "additionalProperties" => true
    },
    "tasks" => {
      "type" => "object",
      "properties" => {
        "cancel" => EMPTY_CAPABILITY_SCHEMA,
        "list" => EMPTY_CAPABILITY_SCHEMA,
        "requests" => {
          "type" => "object",
          "properties" => {
            "elicitation" => {
              "type" => "object",
              "properties" => {
                "create" => EMPTY_CAPABILITY_SCHEMA
              },
              "additionalProperties" => true
            },
            "sampling" => {
              "type" => "object",
              "properties" => {
                "createMessage" => EMPTY_CAPABILITY_SCHEMA
              },
              "additionalProperties" => true
            }
          },
          "additionalProperties" => true
        }
      },
      "additionalProperties" => true
    }
  },
  "additionalProperties" => true
}.freeze
INITIALIZE_PARAMS_SCHEMA =
{
  "$schema" => "https://json-schema.org/draft/2020-12/schema",
  "type" => "object",
  "required" => %w[protocolVersion capabilities clientInfo],
  "properties" => {
    "_meta" => REQUEST_META_SCHEMA,
    "protocolVersion" => { "type" => "string" },
    "capabilities" => CLIENT_CAPABILITIES_SCHEMA,
    "clientInfo" => IMPLEMENTATION_SCHEMA
  },
  "additionalProperties" => true
}.freeze
RESOURCE_URI_PARAMS_SCHEMA =
{
  "type" => "object",
  "required" => [ "uri" ],
  "properties" => {
    "_meta" => REQUEST_META_SCHEMA,
    "uri" => { "type" => "string", "format" => "uri" }
  },
  "additionalProperties" => true
}.freeze
GET_PROMPT_PARAMS_SCHEMA =
{
  "type" => "object",
  "required" => [ "name" ],
  "properties" => {
    "_meta" => REQUEST_META_SCHEMA,
    "name" => { "type" => "string" },
    "arguments" => {
      "type" => "object",
      "additionalProperties" => { "type" => "string" }
    }
  },
  "additionalProperties" => true
}.freeze
TASK_METADATA_SCHEMA =
{
  "type" => "object",
  "properties" => {
    "ttl" => { "type" => "integer" }
  },
  "additionalProperties" => true
}.freeze
CALL_TOOL_PARAMS_SCHEMA =
{
  "type" => "object",
  "required" => [ "name" ],
  "properties" => {
    "_meta" => REQUEST_META_SCHEMA,
    "name" => { "type" => "string" },
    "arguments" => {
      "type" => "object",
      "additionalProperties" => true
    },
    "task" => TASK_METADATA_SCHEMA
  },
  "additionalProperties" => true
}.freeze
TASK_ID_PARAMS_SCHEMA =
{
  "type" => "object",
  "required" => [ "taskId" ],
  "properties" => {
    "taskId" => { "type" => "string" }
  },
  "additionalProperties" => true
}.freeze
SET_LEVEL_PARAMS_SCHEMA =
{
  "type" => "object",
  "required" => [ "level" ],
  "properties" => {
    "_meta" => REQUEST_META_SCHEMA,
    "level" => {
      "type" => "string",
      "enum" => %w[alert critical debug emergency error info notice warning]
    }
  },
  "additionalProperties" => true
}.freeze
COMPLETE_PARAMS_SCHEMA =
{
  "type" => "object",
  "required" => %w[argument ref],
  "properties" => {
    "_meta" => REQUEST_META_SCHEMA,
    "argument" => {
      "type" => "object",
      "required" => %w[name value],
      "properties" => {
        "name" => { "type" => "string" },
        "value" => { "type" => "string" }
      },
      "additionalProperties" => true
    },
    "context" => {
      "type" => "object",
      "properties" => {
        "arguments" => {
          "type" => "object",
          "additionalProperties" => { "type" => "string" }
        }
      },
      "additionalProperties" => true
    },
    "ref" => {
      "anyOf" => [
        {
          "type" => "object",
          "required" => %w[name type],
          "properties" => {
            "name" => { "type" => "string" },
            "title" => { "type" => "string" },
            "type" => { "const" => "ref/prompt" }
          },
          "additionalProperties" => true
        },
        {
          "type" => "object",
          "required" => %w[type uri],
          "properties" => {
            "type" => { "const" => "ref/resource" },
            "uri" => { "type" => "string", "format" => "uri-template" }
          },
          "additionalProperties" => true
        }
      ]
    }
  },
  "additionalProperties" => true
}.freeze
CANCELLED_NOTIFICATION_PARAMS_SCHEMA =
{
  "type" => "object",
  "properties" => {
    "_meta" => NOTIFICATION_META_SCHEMA,
    "reason" => { "type" => "string" },
    "requestId" => ID_SCHEMA
  },
  "additionalProperties" => true
}.freeze
PROGRESS_NOTIFICATION_PARAMS_SCHEMA =
{
  "type" => "object",
  "required" => %w[progress progressToken],
  "properties" => {
    "_meta" => NOTIFICATION_META_SCHEMA,
    "message" => { "type" => "string" },
    "progress" => { "type" => "number" },
    "progressToken" => PROGRESS_TOKEN_SCHEMA,
    "total" => { "type" => "number" }
  },
  "additionalProperties" => true
}.freeze
TASK_STATUS_NOTIFICATION_PARAMS_SCHEMA =
{
  "type" => "object",
  "required" => %w[createdAt lastUpdatedAt status taskId ttl],
  "properties" => {
    "_meta" => NOTIFICATION_META_SCHEMA,
    "createdAt" => { "type" => "string" },
    "lastUpdatedAt" => { "type" => "string" },
    "pollInterval" => { "type" => "integer" },
    "status" => {
      "type" => "string",
      "enum" => %w[cancelled completed failed input_required working]
    },
    "statusMessage" => { "type" => "string" },
    "taskId" => { "type" => "string" },
    "ttl" => { "type" => [ "integer", "null" ] }
  },
  "additionalProperties" => true
}.freeze
MESSAGE_SCHEMA =
{
  "$schema" => "https://json-schema.org/draft/2020-12/schema",
  "anyOf" => [
    {
      "type" => "object",
      "required" => %w[jsonrpc id method],
      "properties" => {
        "jsonrpc" => { "const" => "2.0" },
        "id" => ID_SCHEMA,
        "method" => { "type" => "string" },
        "params" => { "type" => "object", "additionalProperties" => true }
      },
      "additionalProperties" => true
    },
    {
      "type" => "object",
      "required" => %w[jsonrpc method],
      "not" => { "required" => [ "id" ] },
      "properties" => {
        "jsonrpc" => { "const" => "2.0" },
        "method" => { "type" => "string" },
        "params" => { "type" => "object", "additionalProperties" => true }
      },
      "additionalProperties" => true
    },
    {
      "type" => "object",
      "required" => %w[jsonrpc id result],
      "properties" => {
        "jsonrpc" => { "const" => "2.0" },
        "id" => ID_SCHEMA,
        "result" => { "type" => "object", "additionalProperties" => true }
      },
      "additionalProperties" => true
    },
    {
      "type" => "object",
      "required" => %w[jsonrpc error],
      "properties" => {
        "jsonrpc" => { "const" => "2.0" },
        "id" => ID_SCHEMA,
        "error" => {
          "type" => "object",
          "required" => %w[code message],
          "properties" => {
            "code" => { "type" => "integer" },
            "message" => { "type" => "string" },
            "data" => {}
          },
          "additionalProperties" => true
        }
      },
      "additionalProperties" => true
    }
  ]
}.freeze
REQUEST_PARAM_SCHEMAS =
{
  "initialize" => INITIALIZE_PARAMS_SCHEMA,
  "ping" => REQUEST_PARAMS_SCHEMA,
  "resources/list" => PAGINATED_PARAMS_SCHEMA,
  "resources/templates/list" => PAGINATED_PARAMS_SCHEMA,
  "resources/read" => RESOURCE_URI_PARAMS_SCHEMA,
  "resources/subscribe" => RESOURCE_URI_PARAMS_SCHEMA,
  "resources/unsubscribe" => RESOURCE_URI_PARAMS_SCHEMA,
  "prompts/list" => PAGINATED_PARAMS_SCHEMA,
  "prompts/get" => GET_PROMPT_PARAMS_SCHEMA,
  "tools/list" => PAGINATED_PARAMS_SCHEMA,
  "tools/call" => CALL_TOOL_PARAMS_SCHEMA,
  "tasks/get" => TASK_ID_PARAMS_SCHEMA,
  "tasks/result" => TASK_ID_PARAMS_SCHEMA,
  "tasks/cancel" => TASK_ID_PARAMS_SCHEMA,
  "tasks/list" => PAGINATED_PARAMS_SCHEMA,
  "logging/setLevel" => SET_LEVEL_PARAMS_SCHEMA,
  "completion/complete" => COMPLETE_PARAMS_SCHEMA
}.freeze
REQUIRED_REQUEST_PARAMS =
%w[
  initialize
  resources/read
  resources/subscribe
  resources/unsubscribe
  prompts/get
  tools/call
  tasks/get
  tasks/result
  tasks/cancel
  logging/setLevel
  completion/complete
].freeze
NOTIFICATION_PARAM_SCHEMAS =
{
  "notifications/cancelled" => CANCELLED_NOTIFICATION_PARAMS_SCHEMA,
  "notifications/initialized" => NOTIFICATION_PARAMS_SCHEMA,
  "notifications/progress" => PROGRESS_NOTIFICATION_PARAMS_SCHEMA,
  "notifications/tasks/status" => TASK_STATUS_NOTIFICATION_PARAMS_SCHEMA,
  "notifications/roots/list_changed" => NOTIFICATION_PARAMS_SCHEMA
}.freeze
REQUIRED_NOTIFICATION_PARAMS =
%w[
  notifications/cancelled
  notifications/progress
  notifications/tasks/status
].freeze
MESSAGE_SCHEMER =
JSONSchemer.schema(MESSAGE_SCHEMA)
REQUEST_PARAM_SCHEMERS =
REQUEST_PARAM_SCHEMAS.transform_values { |schema| JSONSchemer.schema(schema) }.freeze
NOTIFICATION_PARAM_SCHEMERS =
NOTIFICATION_PARAM_SCHEMAS.transform_values do |schema|
  JSONSchemer.schema(schema)
end.freeze

Class Method Summary collapse

Class Method Details

.client_message_validation_error(payload) ⇒ Object



452
453
454
455
456
457
458
459
# File 'lib/action_mcp/protocol_validator.rb', line 452

def client_message_validation_error(payload)
  case payload
  when JSON_RPC::Request
    validate_request(payload)
  when JSON_RPC::Notification
    validate_notification(payload)
  end
end

.request_params_validation_error(method, params) ⇒ Object



461
462
463
464
465
466
# File 'lib/action_mcp/protocol_validator.rb', line 461

def request_params_validation_error(method, params)
  schemer = REQUEST_PARAM_SCHEMERS[method]
  return unless schemer

  validate_params(method, params, schemer, REQUIRED_REQUEST_PARAMS)
end

.valid?(payload) ⇒ Boolean

Returns:

  • (Boolean)


444
445
446
# File 'lib/action_mcp/protocol_validator.rb', line 444

def valid?(payload)
  valid_message?(payload)
end

.valid_initialize_params?(params) ⇒ Boolean

Returns:

  • (Boolean)


448
449
450
# File 'lib/action_mcp/protocol_validator.rb', line 448

def valid_initialize_params?(params)
  REQUEST_PARAM_SCHEMERS.fetch("initialize").valid?(normalize(params))
end

.valid_message?(payload) ⇒ Boolean

Returns:

  • (Boolean)


440
441
442
# File 'lib/action_mcp/protocol_validator.rb', line 440

def valid_message?(payload)
  MESSAGE_SCHEMER.valid?(normalize(payload))
end