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 default httpx botasaurus].freeze
- MIGRATION_STRATEGIES =
Accepted migration strategies retained for backwards compatibility.
%w[faraday].freeze
- ALL_ACCEPTED_STRATEGIES =
Complete set of strategies accepted by assert_published_request!.
(STRATEGIES + MIGRATION_STRATEGIES).freeze
- MCP_CONTRACT_VERSION =
Bump when tool names, required inputs, or envelope semantics change (independent of gem
VERSION). 2- 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 default → botasaurus fallback chain)' }.freeze
- INSPECT_STRATEGY_PROPERTY =
JSON Schema property for inspect
strategy(auto stays on default). STRATEGY_PROPERTY.merge( description: 'Request strategy (auto uses default for cheap diagnostics; pin botasaurus when needed)' ).freeze
- XOR_ONE_OF =
JSON Schema
oneOfrequiring exactly one ofconfigoryaml. [ { 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 XOR yaml). { type: 'object', properties: CONFIG_XOR_PROPERTIES, oneOf: XOR_ONE_OF }.freeze
- APPLY_INPUT_SCHEMA =
Input schema for
apply(required URL plus config XOR yaml). { type: 'object', properties: { url: URL_PROPERTY, **CONFIG_XOR_PROPERTIES }.freeze, required: %w[url], oneOf: XOR_ONE_OF }.freeze
- TEST_INPUT_SCHEMA =
Input schema for
test. { type: 'object', properties: { **CONFIG_XOR_PROPERTIES, min_items: { type: 'integer', description: 'Minimum required items (default: 1)', default: 1 }, strict_quality: { type: 'boolean', description: 'Fail when ship-quality audit thresholds are exceeded (default: false)', default: false }, compare_enhance: { type: 'boolean', description: 'Diagnostic: compare extraction with enhance on vs off on cached HTML (default: false)', default: false }, strategy: STRATEGY_PROPERTY }.freeze, oneOf: XOR_ONE_OF }.freeze
- SCRAPE_INPUT_SCHEMA =
Input schema for
scrape. { 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. { type: 'object', properties: { url: URL_PROPERTY, strategy: INSPECT_STRATEGY_PROPERTY }.freeze, required: %w[url] }.freeze
- RECON_INPUT_SCHEMA =
Input schema for
recon. INSPECT_INPUT_SCHEMA- CAPTURE_INPUT_SCHEMA =
Input schema for
capture. { type: 'object', properties: { url: URL_PROPERTY, strategy: STRATEGY_PROPERTY, items_selector: { type: 'string', description: 'Optional CSS selector hint for items' }, force: { type: 'boolean', description: 'Bypass native feed check', default: false }, topics: { type: 'array', items: { type: 'string' }, description: 'Directory topics override' }.freeze, title: { type: 'string', description: 'Channel title override' }, summary: { type: 'string', description: 'Directory summary override' }, enhance: { type: 'boolean', description: 'Force enhance on or off' }, limit: { type: 'integer', description: 'Max articles to keep' }, max_redirects: { type: 'integer', description: 'Optional redirect limit override' }, max_requests: { type: 'integer', description: 'Optional request budget override' } }.freeze, required: %w[url] }.freeze
- BATCH_SCRAPE_INPUT_SCHEMA =
Input schema for
batch_scrape. batch_urls_input_schema( urls_description: 'List of page URLs to scrape (1..25)', strategy_property: STRATEGY_PROPERTY, extra_properties: { limit: { type: 'integer', description: 'Max articles per URL to keep (default 10)', default: 10 } } ).freeze
- BATCH_INSPECT_INPUT_SCHEMA =
Input schema for
batch_inspect. batch_urls_input_schema( urls_description: 'List of page URLs to inspect (1..25)', strategy_property: INSPECT_STRATEGY_PROPERTY ).freeze
- BATCH_RECON_INPUT_SCHEMA =
Input schema for
batch_recon. BATCH_INSPECT_INPUT_SCHEMA- 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(closed world). ANNOTATIONS_OPEN_WORLD.merge(open_world_hint: false).freeze
- TITLES =
Human titles for
tools/list. { scrape: 'Scrape', inspect: 'Inspect', recon: 'Recon', batch_scrape: 'Batch scrape', batch_inspect: 'Batch inspect', batch_recon: 'Batch recon', capture: 'Capture', validate: 'Validate', apply: 'Apply', test: 'Test' }.freeze
- CATALOG_ENTRY_LINE =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
lambda do |entry| schema = entry.fetch(:input_schema) required = Array(schema[:required]).sort.join(',') one_of = Array(schema[:oneOf]).map { |branch| Array(branch[:required]).sort.join('+') }.sort.join('|') [entry.fetch(:name), required, one_of].reject(&:empty?).join(':') end.freeze
Class Method Summary collapse
-
.assert_published_request!(config) ⇒ void
Rejects unpublished MCP request adapters so apply/validate cannot
File.readarbitrary paths. -
.batch_urls_input_schema(urls_description:, strategy_property:, extra_properties: {}) ⇒ Hash
Shared JSON Schema for batch URL tools (+batch_scrape+,
batch_inspect,batch_recon). -
.catalog_fingerprint ⇒ String
Stable fingerprint of published tools, required keys, and
oneOfbranches. -
.catalog_tools ⇒ Array<String>
Canonical MCP tool names in alphabetical order (same set as
tools/list). -
.output_schema ⇒ Hash
Envelope JSON Schema.
-
.response(outcome) ⇒ ::MCP::Tool::Response
One envelope Hash, one compact JSON body, no
_meta.
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.
286 287 288 289 290 291 292 293 294 295 |
# File 'lib/html2rss/mcp/contract.rb', line 286 def assert_published_request!(config) strategy = config[:strategy] unless strategy.nil? || ALL_ACCEPTED_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 |
.batch_urls_input_schema(urls_description:, strategy_property:, extra_properties: {}) ⇒ Hash
Shared JSON Schema for batch URL tools (+batch_scrape+, batch_inspect, batch_recon).
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/html2rss/mcp/contract.rb', line 152 def self.batch_urls_input_schema(urls_description:, strategy_property:, extra_properties: {}) # rubocop:disable Metrics/MethodLength { type: 'object', properties: { urls: { type: 'array', items: URL_PROPERTY, minItems: 1, maxItems: 25, description: urls_description }.freeze, strategy: strategy_property, concurrency: { type: 'integer', description: 'Max parallel worker threads (1..10, default: 5)', default: 5 }, **extra_properties }.freeze, required: %w[urls] } end |
.catalog_fingerprint ⇒ String
Stable fingerprint of published tools, required keys, and oneOf branches.
Clients compare against a cached tools/list to detect stale catalogs.
Bump MCP_CONTRACT_VERSION for envelope or breaking wire semantics only.
241 242 243 244 |
# File 'lib/html2rss/mcp/contract.rb', line 241 def catalog_fingerprint lines = Server::Tools::TOOLS.sort_by { |entry| entry.fetch(:name) }.map(&CATALOG_ENTRY_LINE) Digest::SHA256.hexdigest(lines.join("\n")).slice(0, 16) end |
.catalog_tools ⇒ Array<String>
Canonical MCP tool names in alphabetical order (same set as tools/list).
231 232 233 |
# File 'lib/html2rss/mcp/contract.rb', line 231 def catalog_tools Server::Tools::TOOLS.map { |entry| entry.fetch(:name) }.sort end |
.output_schema ⇒ Hash
Envelope JSON Schema. Built lazily so Zeitwerk can load Contract before Outcome.
250 251 252 253 254 255 256 257 258 259 260 261 262 |
# File 'lib/html2rss/mcp/contract.rb', line 250 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.
269 270 271 272 273 274 275 276 |
# File 'lib/html2rss/mcp/contract.rb', line 269 def response(outcome) wire = outcome.to_h ::MCP::Tool::Response.new( [{ type: 'text', text: JSON.generate(wire) }], error: !outcome.ok, structured_content: wire ) end |