AnyEvent-MCP
view release on metacpan or search on metacpan
lib/AnyEvent/MCP/Client/HTTP.pm view on Meta::CPAN
}
sub _send {
my ($self, $message, $done) = @_;
my $key = exists $message->{id} && !exists $message->{result} && !exists $message->{error}
? $message->{id} : "send:" . ++$self->{send_id};
my $generation = $self->{generation};
my %headers = (
%{$self->{headers} || {}},
"content-type" => "application/json",
accept => "application/json, text/event-stream",
);
$headers{authorization} = "Bearer $self->{bearer}" if defined $self->{bearer};
$headers{"mcp-protocol-version"} = $self->{protocol_version} if defined $self->{protocol_version};
$headers{"mcp-session-id"} = $self->{session}
if $self->{protocol_version} lt "2026" && defined $self->{session};
if ($self->{protocol_version} ge "2026" && exists $message->{method} && exists $message->{id}) {
$headers{"mcp-method"} = $message->{method};
lib/AnyEvent/MCP/Client/HTTP.pm view on Meta::CPAN
(map { exists $self->{$_} ? ($_ => $self->{$_}) : () }
qw(timeout proxy tls_ctx handle_params sessionid on_prepare on_connect tcp_connect)),
headers => \%headers,
body => JSON::XS::encode_json ($message),
recurse => 0,
persistent => 0,
on_header => sub {
my ($hdr) = @_;
return 0 unless $self && $self->{generation} == $generation;
$sse = $hdr->{"content-type"} =~ m{\Atext/event-stream(?:\s*;|\z)}i;
if ($message->{method} eq "initialize" && $hdr->{Status} >= 200 && $hdr->{Status} < 300
&& defined $hdr->{"mcp-session-id"}) {
$self->{session} = $hdr->{"mcp-session-id"};
}
1
},
on_body => sub {
my ($chunk, $hdr) = @_;
return 0 unless $self && $self->{generation} == $generation;
lib/AnyEvent/MCP/Server/HTTP.pm view on Meta::CPAN
if defined $authenticated && $authenticated ne $token;
$authenticated = $token;
}
}
return $self->_respond ($handle, 401, "Unauthorized", "")
unless defined $authenticated;
}
return $self->_respond ($handle, 415, "Unsupported Media Type", "")
unless $hdr->{"content-type"} =~ m{\Aapplication/json(?:\s*;|\z)}i;
my $message = eval { JSON::XS::decode_json $body };
if ($@) {
return $self->_rpc_error (sub {
$self->_respond ($handle, 400, "Bad Request", JSON::XS::encode_json $_[0]);
}, undef, -32700, "Parse error");
}
my $params = ref $message eq "HASH" && ref $message->{params} eq "HASH" ? $message->{params} : {};
lib/AnyEvent/MCP/Server/HTTP.pm view on Meta::CPAN
my $extra = "";
if ($headers) {
ref $headers eq "HASH" or die "HTTP response headers must be a hash\n";
while (my ($name, $value) = each %$headers) {
$name =~ /\A[!#\$%&'*+.^_`|~0-9A-Za-z-]+\z/
&& defined $value && !ref $value && $value !~ /[\r\n\0]/
or die "Invalid HTTP response header\n";
$name !~ /\A(?:content-length|connection|transfer-encoding)\z/i
or die "HTTP response framing headers are managed by the server\n";
if (lc $name eq "content-type") {
$content_type = $value;
} else {
$extra .= "$name: $value\r\n";
}
}
}
if ($status >= 400) {
my $level = $status == 404 ? "trace" : "warn";
AE::log $level => "MCP HTTP"
lib/AnyEvent/MCP/Server/HTTP.pm view on Meta::CPAN
my $close = $handle->{mcp_close} || $status != 200 && $status != 202;
$handle->push_write (
"HTTP/1.1 $status $reason\r\n"
. "Content-Type: $content_type\r\n"
. "Content-Length: " . (length $body) . "\r\n"
. $extra
. (defined $handle->{mcp_origin} ? "Access-Control-Allow-Origin: $handle->{mcp_origin}\r\n"
. ($handle->{mcp_origin} ne "*" ? "Vary: Origin\r\n" : "") : "")
. ($status == 204 ? "Access-Control-Allow-Methods: POST, OPTIONS\r\n"
. "Access-Control-Allow-Headers: content-type,authorization,mcp-protocol-version,mcp-method,mcp-name\r\n"
. "Access-Control-Max-Age: 86400\r\n" : "")
. ($status == 405 ? "Allow: POST" . (@{$self->{origins}} ? ", OPTIONS" : "") . "\r\n" : "")
. ($status == 401 && $self->{tokens} ? "WWW-Authenticate: Bearer\r\n" : "")
. ($close ? "Connection: close\r\n" : "")
. "\r\n$body"
);
if ($close) {
weaken $self;
$handle->on_drain (sub { $self->_close ($_[0]) if $self });
t/c01_server.t view on Meta::CPAN
$http->accept ($a);
$client = AnyEvent::Handle->new (fh => $b, on_error => sub { die $_[2] });
$cv = AnyEvent->condvar;
$timer = AnyEvent->timer (after => 30, cb => sub { die "CORS timeout" });
my $request = $method eq "POST"
? post (message (50, "ping"), defined $origin ? "Origin: $origin\r\n" : "")
: "OPTIONS /mcp/ HTTP/1.1\r\n"
. (defined $origin ? "Origin: $origin\r\n" : "")
. "Access-Control-Request-Method: POST\r\n"
. "Access-Control-Request-Headers: content-type,mcp-method\r\n\r\n";
$request =~ s{POST /mcp/}{POST /token/mcp/} if $cookie;
$client->push_write ($request);
http_read ($client, sub {
my ($header) = @_;
like $header, qr/HTTP\/1.1 $status /, "CORS $method status";
if (defined $allowed) {
like $header, qr/^Access-Control-Allow-Origin: \Q$allowed\E\r?$/m, "allowed origin";
like $header, qr/^Vary: Origin\r?$/m, "vary by allowed origin" if $allowed ne "*";
t/c06_multibroker.t view on Meta::CPAN
for (1 .. 1500) {
$socket = IO::Socket::INET->new (PeerAddr => "127.0.0.1", PeerPort => $port, Proto => "tcp", Timeout => 1);
last if $socket;
Time::HiRes::sleep (0.02);
}
$socket or die "CORS test socket: $!\n";
print {$socket}
"OPTIONS $path HTTP/1.1\r\nHost: localhost\r\n"
. "Origin: http://localhost:5001\r\n"
. "Access-Control-Request-Method: POST\r\n"
. "Access-Control-Request-Headers: content-type,authorization\r\n\r\n";
my $preflight;
{
local $/ = "\r\n\r\n";
$preflight = <$socket>;
}
close $socket;
my $allowed = "*";
my $preflight_ok = $preflight =~ /HTTP\/1\.1 204/
&& $preflight =~ /^Access-Control-Allow-Origin: \Q$allowed\E\r?$/m;
ok $preflight_ok, "CORS preflight allows any origin by default";
t/c25_http.t view on Meta::CPAN
$r = call_http ({ url => "$base/page", verify_tls => JSON::XS::false });
is $r->{structuredContent}{status}, 200, "verify_tls=false accepted";
$r = call_http ({ url => "$base/page", verify_tls => JSON::XS::true });
is $r->{structuredContent}{status}, 200, "verify_tls=true accepted";
$r = call_http ({ url => "$base/page", verify_tls => "false" });
ok $r->{isError}, "non-boolean verify_tls rejected";
$r = call_http ({
url => "$base/post", method => "POST", cookie_jar => $jar,
headers => { "content-type" => "application/octet-stream" },
body_base64 => MIME::Base64::encode_base64 ("A\0B", ""),
});
is $r->{structuredContent}{body}, "A\0B", "binary request body preserved";
like $seen[-1][2], qr/^cookie:\s*sid=abc/im, "cookie sent from caller-provided jar";
is $seen[-1][0], "POST", "HTTP method forwarded";
$r = call_http ({ url => "$base/page", headers => { "uSeR-aGeNt" => "custom-agent/1" } });
is $r->{structuredContent}{status}, 200, "custom User-Agent request succeeded";
like $seen[-1][2], qr/^user-agent:\s*custom-agent\/1\s*$/im,
"explicit User-Agent is not overridden";
( run in 1.318 second using v1.01-cache-2.11-cpan-99fa8243a42 )