AnyEvent-MCP

 view release on metacpan or  search on metacpan

t/03_simple.t  view on Meta::CPAN

($header, $body) = post ($server, "/", "Bearer wrong");
ok $header =~ m{HTTP/1.1 401}, "wrong bearer rejected";
($header, $body) = post ($server, "/wrong/");
ok $header =~ m{HTTP/1.1 404}, "wrong cookie path rejected";

$server = AnyEvent::MCP::Server::HTTP->new (tokens => ["first", "second"]);
($header, $body) = post ($server, "/second/mcp/");
ok $header =~ m{HTTP/1.1 200}, "second token works in URL";
($header, $body) = post ($server, "/mcp/", "Bearer first");
ok $header =~ m{HTTP/1.1 200}, "first token works as bearer";
($header, $body) = post ($server, "/mcp/", "Basic YTpi");
ok $header =~ m{HTTP/1.1 401} && $header !~ /WWW-Authenticate: Basic/, "Basic authentication is not supported";

$server = AnyEvent::MCP::Server::HTTP->new (
   path => "/", tokens => ["first", "second"],
   token_users => { first => "global:alice", second => "global:bob" },
   on_request => sub { $_[1]->reply ({ user => $_[1]{user} }) },
);
($header, $body) = post ($server, "/first/");
ok JSON::XS::decode_json ($body)->{result}{user} eq "global:alice", "path token identifies user";
($header, $body) = post ($server, "/", "Bearer second");
ok JSON::XS::decode_json ($body)->{result}{user} eq "global:bob", "Bearer identifies distinct user";
($header, $body) = post ($server, "/first/", "Bearer second");
ok $header =~ m{HTTP/1.1 401}, "conflicting path and Bearer identities rejected";

$server = AnyEvent::MCP::Server::HTTP->new (tokens => ["standalone"]);
($header, $body) = post ($server, "/mcp/");
ok $header =~ m{HTTP/1.1 401}, "embedded server with tokens rejects unauthenticated requests";
$server = AnyEvent::MCP::Server::HTTP->new (auth => "none", 
   host => "::1", port => 31740, handle => { tls => "accept" },
);
ok $server->url eq "https://[::1]:31740/mcp/", "IPv6 and TLS URL";
$server->{url_host} = "[::1]";
ok $server->url eq "https://[::1]:31740/mcp/", "prebracketed IPv6 URL host is not bracketed twice";
$server->{url_host} = "example.net";
$server->{url_scheme} = "http";
ok $server->url eq "http://example.net:31740/mcp/", "advertised URL overrides";
delete $server->{url_host};
$server->{host} = "0.0.0.0";
ok $server->url eq "http://" . (POSIX::uname)[1] . ":31740/mcp/", "wildcard nodename";
$server->{port} = 0;
eval { $server->url };
ok $@ =~ /port is not known/, "unknown port has no URL";

my $dir = File::Temp::tempdir (CLEANUP => 1);
local $ENV{XDG_CONFIG_HOME} = $dir;
local $ENV{PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL};
my $printed = "";
{
   local *STDOUT;
   open STDOUT, ">", \$printed or die $!;
   my $timer;
   mcp_start name => "example", server_version => 0.61, on_start => sub {
      $server = $_[0];
      $timer = AnyEvent->timer (after => 0.01, cb => sub { $server->close });
   };
}
ok $server->isa ("AnyEvent::MCP::Server::HTTP"), "normal startup uses HTTP";
ok $server->{port} && $printed eq "", "HTTP startup leaves stdout empty";
my $config = AnyEvent::MCP::Config->global;
ok $server->{tokens}[0] eq $config->user_credentials ("example")->[0][2],
   "startup token matches derived utility";
ok $server->{server_info}{name} eq "AnyEvent::MCP", "instance name is not the server implementation name";
ok JSON::XS::encode_json ($server->{server_info}) =~ /"version":"0\.61"/,
   "Simple stringifies numeric module versions";
$server->close;

my $err = Symbol::gensym;
my $pid = IPC::Open3::open3 (my $in, my $out, $err, $^X, "-Ilib", "-e", '
   use AnyEvent::MCP::Server::Simple;
   $ENV{XDG_CONFIG_HOME} = $ARGV[0];
   $ENV{PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL} = "stdio";
   mcp_start;
', "$dir/stdio");
print {$in} "{\"jsonrpc\":\"2.0\",\"id\":42,\"method\":\"ping\"}\n";
$in->flush;
close $in;
local $/;
my $output = <$out>;
my $errors = <$err>;
waitpid $pid, 0;
ok !$? && !$errors, "multibroker startup succeeds";
ok eval { JSON::XS::decode_json ($output)->{id} == 42 }, "stdio output contains only RPC";
ok !-e "$dir/stdio", "stdio does not access global configuration";

$err = Symbol::gensym;
$pid = do {
   local $ENV{PERL_ANYEVENT_LOG} = ""; # inspect the normal activity log.
   IPC::Open3::open3 ($in, $out, $err, $^X, "-Ilib", "-e", '
   use AnyEvent::MCP::Server::Simple;
   my $timer;
   mcp_tool delayed => "delayed", {}, sub {
      my ($req) = @_;
      $timer = AnyEvent->timer (after => 0.03, cb => sub {
         $req->reply ({ content => [{ type => "text", text => "x" x 200000 }] });
      });
   };
   mcp_stdio;
   print STDERR "stopped\\n";
   ');
};
print {$in} JSON::XS::encode_json ({
   jsonrpc => "2.0", id => 1, method => "initialize", params => {
      protocolVersion => "2025-11-25", clientInfo => {}, capabilities => {},
   },
}), "\n";
print {$in} JSON::XS::encode_json ({
   jsonrpc => "2.0", id => 2, method => "tools/call", params => { name => "delayed" },
}), "\n";
close $in;
$output = <$out>;
$errors = <$err>;
waitpid $pid, 0;
ok !$? && $errors =~ /\bdelayed\b.*\nstopped\n\z/s,
   "mcp_stdio logs tool activity and returns after input EOF";
my @messages = map JSON::XS::decode_json ($_), split /\n/, $output;
ok @messages == 2 && $messages[1]{id} == 2
   && length $messages[1]{result}{content}[0]{text} == 200000,
   "EOF waits for an asynchronous reply and drains large output";

my ($started, $closed, $timer);



( run in 0.689 second using v1.01-cache-2.11-cpan-3f962061493 )