Module: Html2rss::MCP::Server::Inspect

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

Overview

Diagnostic inspect path (not Capture ownership). Fetches once for SST/scraper stats.

Class Method Summary collapse

Class Method Details

.call(url:, strategy: :auto) ⇒ Hash

Parameters:

  • url (String)
  • strategy (String, Symbol) (defaults to: :auto)

Returns:

  • (Hash)


406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/html2rss/mcp/server.rb', line 406

def call(url:, strategy: :auto) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
  resolved = concrete_strategy(strategy)
  response = fetch_response(url, resolved)
  parsed = response.parsed_body

  result = {
    url:,
    strategy: resolved,
    content_type: response.content_type,
    html_response: response.html_response?,
    scraper_eligibility: scraper_info(parsed),
    sst_stats: sst_stats_from(response)
  }

  if response.html_response?
    sst = sst_document(response)
    if sst
      result[:sst] = {
        node_count: sst.node_count,
        degraded: sst.degraded,
        segment_stats: segment_stats(sst, url)
      }
    end
  end

  blocked = Html2rss::RequestService::BlockedSurface.interstitial_signature_for(response.body)
  result[:blocked_surface] = blocked[:key].to_s if blocked
  result[:xhr_capture] = xhr_capture_info(response) if resolved == :botasaurus

  result
end

.concrete_strategy(strategy) ⇒ Symbol

Resolves feed-level strategy plans to concrete strategies for diagnostic fetch. :auto collapses to :faraday (inspect is a single-request diagnostic, not a fallback run).

Parameters:

  • strategy (String, Symbol)

Returns:

  • (Symbol)


397
398
399
400
# File 'lib/html2rss/mcp/server.rb', line 397

def concrete_strategy(strategy)
  plan = FeedPipeline::StrategyPlan.resolve(Server.resolve_mcp_strategy(strategy))
  plan.is_a?(FeedPipeline::StrategyPlan::Auto) ? :faraday : plan.strategy
end

.discover_segments(sst, url) ⇒ Array

Parameters:

Returns:

  • (Array)


562
563
564
565
566
567
568
569
570
571
572
# File 'lib/html2rss/mcp/server.rb', line 562

def discover_segments(sst, url)
  link_resolver = Scoring::LinkResolver.new(url)
  AutoSource::Segmenter.call(
    sst,
    base_url: url,
    strategy: :list,
    link_resolver:
  )
rescue StandardError
  []
end

.fetch_response(url, strategy) ⇒ Html2rss::RequestService::Response

Parameters:

  • url (String)
  • strategy (Symbol)

Returns:



485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# File 'lib/html2rss/mcp/server.rb', line 485

def fetch_response(url, strategy) # rubocop:disable Metrics/MethodLength -- session construction
  raw_config = Config.auto_source_config(
    url:,
    request_controls: Config::RequestControls.from_shortcut(strategy:)
  )
  raw_config[:strategy] = strategy
  config = Config.from_hash(raw_config)
  resources = FeedPipeline::RuntimePolicy.resources_for(config)
  session = RequestSession.build(
    config:,
    strategy: config.strategy,
    budget: resources.budget,
    policy: resources.policy
  )
  session.fetch_initial_response
end

.redacted_endpoint(entry) ⇒ String?

Returns scheme+host+path only.

Parameters:

  • entry (Hash)

    captured response hash

Returns:

  • (String, nil)

    scheme+host+path only



454
455
456
457
458
459
460
461
462
463
464
# File 'lib/html2rss/mcp/server.rb', line 454

def redacted_endpoint(entry)
  raw = entry['url'] || entry[:url]
  return unless raw

  uri = URI.parse(raw.to_s)
  return unless uri.scheme && uri.host

  "#{uri.scheme}://#{uri.host}#{uri.path}"
rescue URI::InvalidURIError
  nil
end

.scraper_info(parsed) ⇒ Array<String>, Hash

Parameters:

  • parsed (Object)

    parsed response body

Returns:

  • (Array<String>, Hash)


506
507
508
509
510
511
512
513
514
# File 'lib/html2rss/mcp/server.rb', line 506

def scraper_info(parsed)
  return { error: 'Response is not HTML' } unless parsed.is_a?(Nokogiri::HTML::Document)

  begin
    Html2rss::AutoSource::Scraper.from(parsed).map(&:name)
  rescue Html2rss::AutoSource::Scraper::NoScraperFound => error
    { none_found: error.category.to_s }
  end
end

.segment_stats(sst, url) ⇒ Hash

Parameters:

Returns:

  • (Hash)


546
547
548
549
550
551
552
553
554
555
# File 'lib/html2rss/mcp/server.rb', line 546

def segment_stats(sst, url)
  segments = discover_segments(sst, url)
  return { found: 0 } if segments.empty?

  {
    found: segments.size,
    strategies: segments.map(&:strategy).uniq,
    sample_paths: segments.first(5).map { |s| s.root_node.tag_path }
  }
end

.sst_document(response) ⇒ Html2rss::SST::Document?

Parameters:

Returns:



535
536
537
538
539
# File 'lib/html2rss/mcp/server.rb', line 535

def sst_document(response)
  Html2rss::SST::Normalizer.call(response.body)
rescue ArgumentError
  nil
end

.sst_stats_from(response) ⇒ Hash?

Parameters:

Returns:

  • (Hash, nil)


520
521
522
523
524
525
526
527
528
529
# File 'lib/html2rss/mcp/server.rb', line 520

def sst_stats_from(response)
  return nil unless response.html_response?

  doc = sst_document(response)
  return nil unless doc

  { node_count: doc.node_count, degraded: doc.degraded }
rescue StandardError
  nil
end

.xhr_candidate_articles?(entry) ⇒ Boolean

Parameters:

  • entry (Hash)

    captured response hash

Returns:

  • (Boolean)


470
471
472
473
474
475
476
477
478
# File 'lib/html2rss/mcp/server.rb', line 470

def xhr_candidate_articles?(entry)
  body = entry['body'] || entry[:body]
  return false unless body.is_a?(String)

  document = JSON.parse(body, symbolize_names: true)
  AutoSource::Scraper::JsonState::CandidateDetector.candidate_array?(document)
rescue JSON::ParserError
  false
end

.xhr_capture_info(response) ⇒ Hash

Returns redacted XHR capture diagnostics (no query strings).

Parameters:

Returns:

  • (Hash)

    redacted XHR capture diagnostics (no query strings)



441
442
443
444
445
446
447
448
# File 'lib/html2rss/mcp/server.rb', line 441

def xhr_capture_info(response)
  captured = response.captured_responses
  {
    count: captured.size,
    sample_endpoints: captured.first(5).filter_map { |entry| redacted_endpoint(entry) },
    candidate_articles: captured.any? { |entry| xhr_candidate_articles?(entry) }
  }
end