Module: Html2rss::MCP::Contract

Defined in:
lib/html2rss/mcp/contract.rb

Overview

Published MCP contract: strategy enum, input/output schemas, listing annotations, and the single compact JSON envelope response.

Defined Under Namespace

Classes: UnpublishedRequestError

Constant Summary collapse

STRATEGIES =

Published MCP request strategies (excludes local_file).

%w[auto faraday botasaurus].freeze
URL_PROPERTY =

JSON Schema property for a source page URL.

{
  type: 'string',
  format: 'uri',
  description: 'Source page URL'
}.freeze
STRATEGY_PROPERTY =

JSON Schema property for scrape/capture strategy.

{
  type: 'string',
  enum: STRATEGIES,
  default: 'auto',
  description: 'Request strategy (auto runs faraday → botasaurus fallback chain)'
}.freeze
INSPECT_STRATEGY_PROPERTY =

JSON Schema property for inspect strategy (auto stays on Faraday).

STRATEGY_PROPERTY.merge(
  description: 'Request strategy (auto uses Faraday for cheap diagnostics; pin botasaurus when needed)'
).freeze
XOR_ONE_OF =

JSON Schema oneOf requiring exactly one of config or yaml.

[
  { required: %w[config], not: { required: %w[yaml] } }.freeze,
  { required: %w[yaml], not: { required: %w[config] } }.freeze
].freeze
CONFIG_XOR_PROPERTIES =

JSON Schema properties for the config/yaml XOR pair.

{
  config: {
    type: 'object',
    description: 'Feed configuration hash with channel and selectors (XOR yaml)'
  }.freeze,
  yaml: {
    type: 'string',
    pattern: '\\S',
    description: 'Feed configuration YAML string (XOR config)'
  }.freeze
}.freeze
CONFIG_XOR_SCHEMA =

Input schema for validate_config (config XOR yaml).

{
  type: 'object',
  properties: CONFIG_XOR_PROPERTIES,
  oneOf: XOR_ONE_OF
}.freeze
APPLY_INPUT_SCHEMA =

Input schema for apply_config (required URL plus config XOR yaml).

{
  type: 'object',
  properties: { url: URL_PROPERTY, **CONFIG_XOR_PROPERTIES }.freeze,
  required: %w[url],
  oneOf: XOR_ONE_OF
}.freeze
SCRAPE_INPUT_SCHEMA =

Input schema for scrape_url.

{
  type: 'object',
  properties: {
    url: URL_PROPERTY,
    strategy: STRATEGY_PROPERTY,
    limit: { type: 'integer', description: 'Max articles to keep (default 25)', default: 25 },
    items_selector: { type: 'string', description: 'Optional CSS selector hint for items' }
  }.freeze,
  required: %w[url]
}.freeze
INSPECT_INPUT_SCHEMA =

Input schema for inspect_url.

{
  type: 'object',
  properties: { url: URL_PROPERTY, strategy: INSPECT_STRATEGY_PROPERTY }.freeze,
  required: %w[url]
}.freeze
CAPTURE_INPUT_SCHEMA =

Input schema for capture_config.

{
  type: 'object',
  properties: {
    url: URL_PROPERTY,
    strategy: STRATEGY_PROPERTY,
    items_selector: { type: 'string', description: 'Optional CSS selector hint for items' }
  }.freeze,
  required: %w[url]
}.freeze
ANNOTATIONS_OPEN_WORLD =

Tool annotations for open-world read-only tools.

{
  read_only_hint: true,
  destructive_hint: false,
  idempotent_hint: true,
  open_world_hint: true
}.freeze
ANNOTATIONS_VALIDATE =

Tool annotations for validate_config (closed world).

ANNOTATIONS_OPEN_WORLD.merge(open_world_hint: false).freeze
TITLES =

Human titles for tools/list.

{
  scrape_url: 'Scrape URL',
  inspect_url: 'Inspect URL',
  capture_config: 'Capture feed config',
  validate_config: 'Validate feed config',
  apply_config: 'Apply feed config'
}.freeze

Class Method Summary collapse

Class Method Details

.assert_published_request!(config) ⇒ void

This method returns an undefined value.

Rejects unpublished MCP request adapters so apply/validate cannot File.read arbitrary paths. CLI and Config still allow local_file.

Parameters:

  • config (Hash)

Raises:



160
161
162
163
164
165
166
167
168
169
# File 'lib/html2rss/mcp/contract.rb', line 160

def assert_published_request!(config)
  strategy = config[:strategy]
  unless strategy.nil? || STRATEGIES.include?(strategy.to_s)
    raise UnpublishedRequestError,
          "MCP does not accept strategy #{strategy} (published: #{STRATEGIES.join(', ')})"
  end
  return unless config.dig(:request, :local_file_path)

  raise UnpublishedRequestError, 'MCP does not accept request.local_file_path'
end

.output_schemaHash

Envelope JSON Schema. Built lazily so Zeitwerk can load Contract before Outcome.

Returns:

  • (Hash)


124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/html2rss/mcp/contract.rb', line 124

def output_schema # rubocop:disable Metrics/MethodLength -- schema document is one hash
  {
    type: 'object',
    additionalProperties: false,
    required: %w[ok next_step guidance payload],
    properties: {
      ok: { type: 'boolean' },
      next_step: { type: 'string', enum: Outcome::NextStep::NAMES.map(&:to_s) },
      guidance: { type: 'string' },
      payload: { type: 'object' }
    }
  }
end

.response(outcome) ⇒ ::MCP::Tool::Response

One envelope Hash, one compact JSON body, no _meta.

Parameters:

Returns:

  • (::MCP::Tool::Response)


143
144
145
146
147
148
149
150
# File 'lib/html2rss/mcp/contract.rb', line 143

def response(outcome)
  wire = outcome.to_h
  ::MCP::Tool::Response.new(
    [{ type: 'text', text: JSON.generate(wire) }],
    error: !outcome.ok,
    structured_content: wire
  )
end