App-Raider

 view release on metacpan or  search on metacpan

lib/App/Raider/WebTools.pm  view on Meta::CPAN

    description  => 'Search the web across multiple providers (DuckDuckGo always; Brave/Serper/Google added when their API keys are in the environment). Returns ranked merged results.',
    input_schema => {
      type       => 'object',
      properties => {
        query => { type => 'string',  description => 'Search query' },
        limit => { type => 'integer', description => 'Max results (default 8)' },
      },
      required => ['query'],
    },
    code => sub {
      my ($tool, $in) = @_;
      my $query = $in->{query};
      my $limit = $in->{limit} // 8;
      my $f = $ws->search(query => $query, limit => $limit);
      my $out = eval { $f->get };
      return $tool->text_result("Error: $@", 1) if $@;
      my @lines;
      my $i = 0;
      for my $r (@{$out->{results} // []}) {
        $i++;
        my $title = $r->title // '(no title)';
        my $url   = $r->url   // '';
        my $sn    = $r->snippet // '';
        $sn =~ s/\s+/ /g;
        push @lines, "[$i] $title";
        push @lines, "    $url";
        push @lines, "    $sn" if length $sn;
      }
      if (my $errs = $out->{errors}) {
        for my $e (@$errs) {
          push @lines, "(error from $e->{provider}: $e->{error})";
        }
      }
      return $tool->text_result(@lines ? join("\n", @lines) : "No results.");
    },
  );

  $server->tool(
    name         => 'web_fetch',
    description  => 'Fetch a URL and return its body. HTML is flattened to readable text. Binary/large bodies are truncated.',
    input_schema => {
      type       => 'object',
      properties => {
        url    => { type => 'string',  description => 'URL to fetch' },
        as_html => { type => 'boolean', description => 'Keep raw HTML instead of flattening to text (default false)' },
      },
      required => ['url'],
    },
    code => sub {
      my ($tool, $in) = @_;
      my $url = $in->{url};
      my $req = GET($url);
      $req->header('User-Agent' => 'raider/0.001');
      my $f = $http->do_request(request => $req);
      my $resp = eval { $f->get };
      return $tool->text_result("Error: $@", 1) if $@;
      unless ($resp->is_success) {
        return $tool->text_result(
          sprintf("HTTP %s for %s", $resp->status_line, $url), 1);
      }
      my $body = $resp->decoded_content // '';
      if (length($body) > $max_fetch_bytes) {
        $body = substr($body, 0, $max_fetch_bytes) . "\n[truncated]\n";
      }
      my $ctype = $resp->header('Content-Type') // '';
      if (!$in->{as_html} && $ctype =~ m{text/html}i) {
        $body = _flatten_html($body);
      }
      my $header = sprintf("URL: %s\nStatus: %s\nContent-Type: %s\n\n",
        $url, $resp->status_line, $ctype);
      return $tool->text_result($header . $body);
    },
  );

  return $server;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

App::Raider::WebTools - MCP::Server factory with web search and fetch tools (Net::Async::WebSearch + Net::Async::HTTP)

=head1 VERSION

version 0.003

=head2 build_web_tools_server

    my $server = App::Raider::WebTools::build_web_tools_server(
        loop           => $loop,         # required
        max_fetch_bytes => 2_000_000,
    );

Returns an L<MCP::Server> instance with two tools:

=over

=item * C<web_search(query, [limit])>  -- multi-provider search via
L<Net::Async::WebSearch>. DuckDuckGo is always enabled (keyless). Brave,
Serper and Google are auto-added when C<BRAVE_API_KEY>, C<SERPER_API_KEY>,
or both C<GOOGLE_API_KEY> + C<GOOGLE_CSE_ID> are set in the environment.

=item * C<web_fetch(url)>  -- fetch a URL via L<Net::Async::HTTP>. Returns
the response body (HTML is flattened to readable text, up to
C<max_fetch_bytes>).

=back

=head1 SEE ALSO

=over

=item * L<MCP::Server>



( run in 1.175 second using v1.01-cache-2.11-cpan-9581c071862 )