ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/AnyEvent-MCP/lib/AnyEvent/MCP/Server/HTTP.pm
Revision: 1.20
Committed: Sun Nov 1 23:30:55 2026 UTC (4 hours, 32 minutes ago) by root
Branch: MAIN
CVS Tags: HEAD
Changes since 1.19: +50 -14 lines
Log Message:
*** empty log message ***

File Contents

# Content
1 =head1 NAME
2
3 AnyEvent::MCP::Server::HTTP - an optional MCP HTTP listener and socket handler
4
5 =head1 DESCRIPTION
6
7 A subclass of L<AnyEvent::MCP::Server> that handles HTTP POST requests.
8 It can open its own listener, or you can give it sockets accepted elsewhere.
9 If your application already parses HTTP, you can pass decoded requests to
10 L<AnyEvent::MCP::Server> instead of running another HTTP implementation.
11
12 Requests sharing one connection are dispatched in sequence, preserving their
13 HTTP response order; requests on other connections are independent. This
14 implementation returns JSON and drops notifications. It does not support
15 SSE or subscriptions.
16
17 Request bodies need C<Content-Length>: chunked transfer encoding is not
18 supported. The server does not assign protocol sessions. Legacy clients
19 use the 2025 request/response subset.
20
21 C<Mcp-Param-*> headers from C<x-mcp-header> tool schema annotations are not
22 currently validated against the body. The built-in protocol headers
23 C<Mcp-Protocol-Version>, C<Mcp-Method> and, where applicable, C<Mcp-Name>
24 must agree with modern request metadata and parameters; a mismatch is
25 rejected. Custom schema-derived headers do not receive this check.
26
27 =head1 FIRST USE
28
29 For an application that already registers tools in
30 L<AnyEvent::MCP::Server::Toolset>, start an HTTP listener:
31
32 use AnyEvent;
33 use AnyEvent::MCP::Server::HTTP;
34
35 my $server = AnyEvent::MCP::Server::HTTP->new (
36 toolsets => [$tools], tokens => [$secret],
37 host => "127.0.0.1", port => 31740,
38 );
39 $server->listen;
40 print $server->url, "\n";
41 AE::cv->recv;
42
43 C<$tools> is a Toolset and C<$secret> is a previously generated private
44 URL-safe token. Do not use a sample or hardcoded token in a real service.
45 This object handles sockets and HTTP; L<AnyEvent::MCP::Server> dispatches
46 requests, while the Toolset runs tool callbacks. For a standalone program
47 with automatic configuration and startup, prefer
48 L<AnyEvent::MCP::Server::Simple>.
49
50 =head1 AUTHENTICATION
51
52 Unlike L<AnyEvent::MCP::Server::Simple/mcp_start>, this low-level object
53 does not load global configuration or derive credentials. Supply C<tokens>
54 explicitly, for example:
55
56 my $server = AnyEvent::MCP::Server::HTTP->new (
57 host => "127.0.0.1",
58 port => 31740,
59 path => "/mcp/",
60 tokens => ["a-long-random-secret", "another-long-random-secret"],
61 );
62 $server->listen;
63
64 Each token authorizes the same requests. For either token, a client may
65 POST to C</TOKEN/mcp/> without an Authorization header, or POST to
66 C</mcp/> with C<Authorization: Bearer TOKEN>. URL credentials are path
67 components, B<not> HTTP cookies. C<url> advertises the first token;
68 C<url (0)> returns the clean endpoint. A trailing slash may be omitted
69 from either request path when the configured endpoint path ends in one;
70 this accommodates clients that strip it from a configured URL.
71
72 The accepted tokens are static, URL-safe strings. HTTP Basic and OAuth
73 access-token validation are not implemented here. There is no automatic
74 fallback to the master-secret-derived token: that belongs to C<mcp_start>
75 or L<AnyEvent::MCP::Config/http_settings>.
76
77 Authentication defaults to C<tokens>, and construction B<fails> unless a
78 nonempty array of valid tokens is provided. To deliberately delegate
79 authentication to a trusted embedding layer (for example, a reverse proxy
80 that rejects unauthorized requests before they reach MCP), specify
81 C<auth =E<gt> "none"> instead, B<without> C<tokens>. This is an explicit
82 opt-out, not a permissive default. The embedding layer then owns the
83 entire authentication boundary.
84 The C<origins> setting filters browser origins; it is not authentication.
85 URLs can leak through logs and histories, and HTTP alone does not encrypt
86 tokens. Use TLS when credentials cross an untrusted network. A local address
87 is a sensible default, but not a credential.
88
89 =head1 METHODS
90
91 =over
92
93 =item $server = AnyEvent::MCP::Server::HTTP->new (%config)
94
95 Construction does not start a listener. In addition to Server options,
96 the following configuration options are supported:
97
98 =over
99
100 =item host => $host
101
102 The listening address, defaulting to C<127.0.0.1>.
103
104 =item port => $port
105
106 The listening port, defaulting to zero for an automatically assigned port.
107
108 =item path => $path
109
110 The MCP endpoint path, defaulting to C</mcp/>. Bearer authentication uses
111 C<$path> directly; URL tokens prefix it with C</TOKEN>. If this path ends
112 in C</>, requests without that final slash are also accepted. Other
113 path differences are not normalized.
114
115 =item connection_close => $boolean
116
117 When true, disables persistent connections, sends C<Connection: close> and
118 closes the socket after draining each response. Defaults to false.
119 This is useful when an existing listener routes sockets by their first
120 request and cannot route subsequent requests independently.
121
122 =item max_size => $bytes
123
124 Limits incoming headers and request bodies, defaulting to 8 MiB.
125
126 =item origins => [$origin...]
127
128 Allowed Origin header values. Defaults to C<["*"]>, permitting browser requests
129 from any origin. An empty array rejects requests with an Origin header, while
130 requests without one are accepted. Allowed origins receive CORS response headers,
131 and nonempty origin lists enable OPTIONS preflight for POST. Preflight does not
132 require authentication; the subsequent POST still does.
133
134 =item handle => { %handle }
135
136 AnyEvent::Handle constructor options, such as C<tls>, C<tls_ctx>, C<timeout>
137 or C<on_starttls>. The socket and the read/error/EOF handlers belong to this
138 class.
139
140 =item tls_cert => $file
141
142 Serve HTTP and HTTPS opportunistically on the same port. FILE contains
143 a PEM certificate chain and optionally its private key. The TLS context
144 is loaded once when listening starts. TLS needs L<Net::SSLeay>.
145
146 =item tls_key => $file
147
148 PEM private key for C<tls_cert>. If only one TLS option is supplied, its
149 file must contain both certificate and private key. On accepted connections,
150 C<< $handle->{tls} >> identifies encrypted traffic. Plain HTTP remains
151 available; TLS is not required of clients.
152
153 =item tokens => [$token, ...]
154
155 Ordered tokens accepted either as C</TOKEN> before the configured
156 C<path> or in an C<Authorization: Bearer TOKEN> header. The first token
157 is used in advertised URLs. Token strings must be URL-safe and nonempty.
158
159 =item auth => "tokens" | "none"
160
161 Select authentication mode. C<tokens> is the default and requires at least
162 one token; C<none> explicitly disables HTTP authentication and cannot be
163 combined with tokens. Other values are rejected. Browser origins are
164 not a replacement for authentication.
165
166 =item name => $name
167
168 Identifies the service. Use L<AnyEvent::MCP::Config/http_settings>
169 to resolve named listener configuration before constructing this transport.
170
171 =item url_host => $hostname
172
173 Overrides the advertised hostname, for example when listening on a wildcard
174 address. Otherwise a wildcard uses the nodename from C<uname>.
175
176 =item url_scheme => $scheme
177
178 Overrides the advertised scheme; otherwise TLS credentials or handle options
179 select C<https>, and plain handles select C<http>.
180
181 =back
182
183 Every configured token authenticates in either the URL or the Authorization
184 header. Without tokens, the constructor requires C<auth =E<gt> "none">.
185
186 =item $url = $server->url ($with_token)
187
188 Returns the advertised URL with the actual listening port. C<$with_token>
189 defaults to true, including the first configured token. Pass false for
190 the clean URL to use with an Authorization bearer token.
191
192 The port must be known. Wildcard hostname resolution and reverse-proxy
193 routing remain the caller's responsibility.
194
195 =item $server->listen ($host, $port, %handle)
196
197 Starts a listener, replacing any previous listener. Omitted arguments use
198 the configured host and port. The actual bound port is stored in
199 C<< $server->{port} >>, including when requesting an ephemeral port.
200
201 C<%handle> overrides the configured C<handle> options for connections
202 accepted by this listener.
203
204 =item $handle = $server->accept ($socket, %handle)
205
206 Starts handling an already accepted socket. The server takes ownership of
207 C<$socket>. No listener is needed.
208
209 C<%handle> overrides the configured C<handle> options for this connection.
210
211 =item $server->close
212
213 Stops listening, closes accepted sockets and cancels outstanding requests.
214 The same object can subsequently listen or accept again.
215
216 =back
217
218 =head1 EVENTS
219
220 =over
221
222 =item on_extension => $callback->($server, $handle, $hello)
223
224 Accept raw extension connections on the HTTP listener. A first byte of C<[>
225 selects a bounded, newline-terminated JSON hello whose first two fields are
226 the protocol name and an accepted listener token. Authenticate before invoking
227 this callback, which validates the protocol-specific fields and takes ownership
228 of the handle, preserving buffered input. Throw an exception to reject it.
229 HTTP connections continue through the normal parser. This dispatch examines
230 decrypted bytes when TLS is configured, but the built-in reverse connector
231 currently supports only plain C<http://> endpoints.
232
233 =item on_http_request => $callback->($server, $request, $reply)
234
235 Called for every complete, syntactically valid HTTP request before MCP routing
236 or authentication. C<$request> is a hash containing the parsed HTTP fields
237 (including C<method>, C<uri>, and C<http_version>) and a C<body> field holding
238 the raw request body. Return false to use the normal MCP handling, or true to
239 handle the request yourself.
240
241 C<$reply-E<gt>($status, $reason, \%headers, $body)> sends a response, either
242 immediately or asynchronously after the callback returns. C<%headers> is
243 optional; C<Content-Type> defaults to C<application/json>, and the server
244 generates C<Content-Length> and manages persistent connections. The body is
245 an octet string. Reply at most once, and return true when taking ownership
246 of the request, even if the response will be sent later. The next request on
247 the same connection is not read until that response is sent.
248
249 The hook runs B<before> MCP authentication and CORS checks. Application
250 endpoints must perform their own authentication and browser request checks.
251 It does not replace or bypass the normal authentication of unhandled MCP
252 requests. Malformed or oversized requests are rejected before the hook.
253
254 =item on_connection_error => $callback->($server, $handle, $message)
255
256 Called for socket errors before closing the connection.
257
258 =item on_close => $callback->($server)
259
260 Called once after C<close> stops the listener and closes all connections.
261 Calling C<listen> or C<accept> again starts a new lifetime.
262
263 =back
264
265 =cut
266
267 package AnyEvent::MCP::Server::HTTP;
268
269 use common::sense;
270 use parent "AnyEvent::MCP::Server";
271 use AnyEvent::Handle ();
272 use AnyEvent::Socket ();
273 use JSON::XS ();
274 use Scalar::Util qw(weaken);
275 use POSIX ();
276
277 sub new {
278 my ($class, %kv) = @_;
279
280 !grep (exists $kv{$_}, qw(cookie bearer users))
281 or die "Legacy HTTP authentication options are unsupported; use tokens\n";
282
283 $kv{auth} //= "tokens";
284 if ($kv{auth} eq "tokens") {
285 ref $kv{tokens} eq "ARRAY" && @{$kv{tokens}}
286 or die "HTTP server requires nonempty tokens\n";
287 /\A[A-Za-z0-9_.~-]+\z/ or die "Invalid HTTP server token\n"
288 for @{$kv{tokens}};
289 } elsif ($kv{auth} eq "none") {
290 !exists $kv{tokens}
291 or die "Unauthenticated HTTP server must not configure tokens\n";
292 } else {
293 die "Unsupported HTTP authentication type '$kv{auth}'\n";
294 }
295
296 $class->SUPER::new (host => "127.0.0.1", port => 0, path => "/mcp/", origins => ["*"], %kv)
297 }
298
299 sub _tls_context {
300 my ($self) = @_;
301
302 return unless defined $self->{tls_cert} || defined $self->{tls_key};
303
304 $self->{_tls_context} ||= do {
305 require AnyEvent::TLS;
306 AnyEvent::TLS->new (
307 cert_file => $self->{tls_cert} // $self->{tls_key},
308 (defined $self->{tls_key} ? (key_file => $self->{tls_key}) : ()),
309 )
310 }
311 }
312
313 sub listen {
314 my ($self, $host, $port, %handle) = @_;
315
316 $self->_tls_context;
317 delete $self->{_closed};
318 $host = $self->{host} unless defined $host;
319 $port = $self->{port} unless defined $port;
320
321 $self->{listen_host} = $host;
322 $self->{listen_handle} = { %{$self->{handle} || {}}, %handle };
323
324 delete $self->{listener};
325 weaken $self;
326
327 $self->{listener} = AnyEvent::Socket::tcp_server $host, $port, sub {
328 return unless $self;
329 my ($socket, $host, $port) = @_;
330 my $handle = $self->accept ($socket, %handle);
331 $handle->{mcp_remote} = AnyEvent::Socket::format_hostport ($host, $port);
332 }, sub {
333 $self->{port} = $_[2] if $self;
334 ()
335 };
336 }
337
338 sub _token_path {
339 my ($self, $token) = @_;
340
341 "/$token$self->{path}"
342 }
343
344 sub url {
345 my ($self, $with_token) = @_;
346
347 $with_token = 1 unless defined $with_token;
348
349 my $host = $self->{url_host}
350 || (defined $self->{listen_host} ? $self->{listen_host} : $self->{host});
351 $host = (POSIX::uname)[1] if !length $host || $host eq "0.0.0.0" || $host eq "::";
352 $host =~ s/\A\[(.*)\]\z/$1/; # allow already bracketed IPv6 URL hosts
353
354 my $port = $self->{port} or die "HTTP port is not known\n";
355 my $scheme = $self->{url_scheme}
356 || (defined $self->{tls_cert} || defined $self->{tls_key}
357 || ($self->{listen_handle} || $self->{handle} || {})->{tls} ? "https" : "http");
358 my $path = $with_token && $self->{tokens} && @{$self->{tokens}}
359 ? $self->_token_path ($self->{tokens}[0]) : $self->{path};
360
361 "$scheme://" . AnyEvent::Socket::format_hostport ($host, $port) . $path
362 }
363
364 sub accept {
365 my ($self, $socket, %handle) = @_;
366
367 delete $self->{_closed};
368 weaken $self;
369
370 my $handle = AnyEvent::Handle->new (
371 rbuf_max => $self->{max_size} || 8 * 1024 * 1024,
372 %{$self->{handle}},
373 %handle,
374 fh => $socket,
375 on_read => undef,
376 on_error => sub {
377 $self->event (connection_error => $_[0], $_[2]) if $self;
378 $self->_close ($_[0]) if $self;
379 },
380 on_eof => sub { $self->_close ($_[0]) if $self },
381 );
382
383 $handle->{mcp_peer} = {};
384 $self->{connections}{$handle} = $handle;
385 $handle->push_read (tls_autostart => $self->_tls_context, "accept")
386 if (defined $self->{tls_cert} || defined $self->{tls_key}) && !$handle->{tls};
387
388 if ($self->{on_extension}) {
389 $handle->rtimeout (15);
390 $handle->on_read (sub {
391 my ($handle) = @_;
392
393 $handle->on_read (undef);
394 return unless $self;
395
396 if (substr ($handle->{rbuf}, 0, 1) eq "[") {
397 require AnyEvent::MCP::Extension;
398 AnyEvent::MCP::Extension::accept ($handle, $self->{tokens} || [], sub {
399 my ($handle, $hello) = @_;
400 return $handle->destroy unless $self;
401 delete $self->{connections}{$handle};
402 $self->{on_extension}->($self, $handle, $hello);
403 }, sub { $self->_close ($_[0]) if $self });
404 } else {
405 $handle->rtimeout (0);
406 $self->_read ($handle, 1);
407 }
408 });
409 } else {
410 $self->_read ($handle);
411 }
412
413 $handle
414 }
415
416 # parse a request line and HTTP fields, using the same comma-combining
417 # approach as AnyEvent::HTTP::_parse_hdr. The caller already bounds headers.
418 sub _parse_request_headers {
419 local $_ = $_[0] . "\r\n";
420 /\A([!#$%&'*+.^_\x60|~0-9A-Za-z-]+) [ ]+
421 ([^\x00-\x20]+) [ ]+ HTTP\/0*([0-9.]+)\r\n/gcix
422 or return;
423 my ($method, $uri, $version) = ($1, $2, $3);
424 return unless $version =~ /\A1\.[0-9]+\z/;
425 my %hdr;
426
427 $hdr{lc $1} .= ",$2"
428 while /\G
429 ([!\#$%&'*+.^_\x60|~0-9A-Za-z-]+):
430 [\t ]*
431 ([^\x00-\x08\x0a-\x1f\x7f]*)
432 \r\n
433 /gcx;
434
435 /\G\z/gc or return;
436
437 # discard the prefix used for comma-combining repeated field values.
438 substr $_, 0, 1, "" for values %hdr;
439 s/[\t ]+\z// for values %hdr;
440
441 # request-line fields override any colliding HTTP header names.
442 +{ %hdr, method => $method, uri => $uri, http_version => $version }
443 }
444
445 sub _read {
446 my ($self, $handle, $immediate) = @_;
447
448 weaken $self;
449
450 if ($immediate) {
451 $handle->push_read (line => "\r\n\r\n", sub {
452 my ($handle, $header) = @_;
453
454 return unless $self;
455
456 my $hdr = _parse_request_headers ($header);
457 $handle->{mcp_close} = $self->{connection_close}
458 || !$hdr || $hdr->{http_version} =~ m{\A1\.0+\z}
459 || $hdr->{connection} =~ /\bclose\b/i;
460
461 return $self->_respond ($handle, 400, "Bad Request", "") unless $hdr;
462
463 $hdr->{"content-length"} = 0 unless defined $hdr->{"content-length"};
464 return $self->_respond ($handle, 400, "Bad Request", "")
465 if exists $hdr->{"transfer-encoding"}
466 || $hdr->{"content-length"} !~ /\A[0-9]+\z/;
467
468 return $self->_respond ($handle, 413, "Content Too Large", "")
469 if $hdr->{"content-length"} > ($self->{max_size} || 8 * 1024 * 1024);
470
471 if ($hdr->{expect}) {
472 return $self->_respond ($handle, 417, "Expectation Failed", "")
473 unless lc $hdr->{expect} eq "100-continue";
474
475 $handle->push_write ("HTTP/1.1 100 Continue\r\n\r\n");
476 }
477
478 $handle->push_read (chunk => $hdr->{"content-length"}, sub {
479 my ($handle, $body) = @_;
480
481 $self->_request ($handle, $hdr, $body) if $self;
482 });
483 });
484 } else {
485 $handle->on_read (sub {
486 my ($handle) = @_;
487 $handle->on_read (undef);
488 $self->_read ($handle, 1) if $self;
489 });
490 }
491 }
492
493 sub _request {
494 my ($self, $handle, $hdr, $body) = @_;
495
496 delete $handle->{mcp_origin};
497
498 if (my $hook = $self->{on_http_request}) {
499 weaken $self;
500 my $replied;
501 my $reply = sub {
502 return if $replied++ || !$self || $handle->{mcp_closed};
503 my ($status, $reason, $headers, $content) = @_;
504 $self->_respond ($handle, $status, $reason, $content, $headers);
505 };
506
507 return if $hook->($self, { %$hdr, body => $body }, $reply);
508 }
509
510 # Some MCP clients strip a trailing slash from the configured URL.
511 my $uri = $hdr->{uri};
512 $uri .= "/" if $self->{path} =~ m{/$} && $uri !~ m{/$};
513 my $token_path = $self->{tokens}
514 && grep $uri eq $self->_token_path ($_), @{$self->{tokens}};
515
516 return $self->_respond ($handle, 404, "Not Found", "")
517 unless $uri eq $self->{path} || $token_path;
518
519 if (exists $hdr->{origin}) {
520 my $wildcard = grep $_ eq "*", @{$self->{origins}};
521
522 return $self->_respond ($handle, 403, "Forbidden", "")
523 unless $wildcard || grep $_ eq $hdr->{origin}, @{$self->{origins}};
524
525 $handle->{mcp_origin} = $wildcard ? "*" : $hdr->{origin};
526 }
527
528 if ($hdr->{method} eq "OPTIONS" && @{$self->{origins}}) {
529 return $self->_respond ($handle, 405, "Method Not Allowed", "")
530 if exists $hdr->{"access-control-request-method"}
531 && $hdr->{"access-control-request-method"} ne "POST";
532
533 return $self->_respond ($handle, 204, "No Content", "");
534 }
535
536 return $self->_respond ($handle, 405, "Method Not Allowed", "")
537 unless $hdr->{method} eq "POST";
538
539 if ($self->{tokens}) {
540 my $authenticated = $token_path;
541 my $authorization = $hdr->{authorization};
542
543 if ($authorization =~ /\ABearer ([^\s]+)\z/i) {
544 my $token = $1;
545 $authenticated ||= grep $_ eq $token, @{$self->{tokens}};
546 }
547
548 return $self->_respond ($handle, 401, "Unauthorized", "")
549 unless $authenticated;
550 }
551
552 return $self->_respond ($handle, 415, "Unsupported Media Type", "")
553 unless $hdr->{"content-type"} =~ m{\Aapplication/json(?:\s*;|\z)}i;
554
555 my $message = eval { JSON::XS::decode_json $body };
556
557 if ($@) {
558 return $self->_rpc_error (sub {
559 $self->_respond ($handle, 400, "Bad Request", JSON::XS::encode_json $_[0]);
560 }, undef, -32700, "Parse error");
561 }
562
563 my $params = ref $message eq "HASH" && ref $message->{params} eq "HASH" ? $message->{params} : {};
564 my $meta = ref $params->{_meta} eq "HASH" ? $params->{_meta} : {};
565 my $modern = exists $meta->{"io.modelcontextprotocol/protocolVersion"}
566 || $hdr->{"mcp-protocol-version"} ge "2026";
567
568 if ($modern && ref $message eq "HASH" && exists $message->{id}) {
569 my %expected = (
570 "mcp-protocol-version" => $meta->{"io.modelcontextprotocol/protocolVersion"},
571 "mcp-method" => $message->{method},
572 );
573
574 $expected{"mcp-name"} = exists $params->{name} ? $params->{name} : $params->{uri}
575 if $message->{method} =~ /\A(?:tools\/call|prompts\/get|resources\/read)\z/;
576
577 for my $name (keys %expected) {
578 my $value = $hdr->{$name};
579
580 if ($name eq "mcp-name" && $value =~ /\A=\?base64\?([A-Za-z0-9+\/=]*)\?=\z/) {
581 $value = eval { AnyEvent::MCP::utf8dec (MIME::Base64::decode_base64 ($1)) };
582 }
583
584 unless (defined $value && defined $expected{$name} && $value eq $expected{$name}) {
585 return $self->_rpc_error (sub {
586 $self->_respond ($handle, 400, "Bad Request", JSON::XS::encode_json $_[0]);
587 }, $message->{id}, -32020, "Header mismatch: $name");
588 }
589 }
590 }
591
592 weaken $self;
593
594 $self->rpc ($message, sub {
595 my ($response) = @_;
596
597 return unless $self && !$handle->{mcp_closed};
598
599 my ($status, $reason) = defined $response ? (200, "OK") : (202, "Accepted");
600
601 if ($modern && defined $response && $response->{error}) {
602 ($status, $reason) = $response->{error}{code} == -32601
603 ? (404, "Not Found") : (400, "Bad Request");
604 }
605
606 $self->_respond ($handle, $status, $reason, defined $response ? JSON::XS::encode_json $response : "");
607 },
608 peer => $handle->{mcp_peer},
609 protocol_version => $hdr->{"mcp-protocol-version"} || "2025-03-26",
610 );
611 }
612
613 sub _respond {
614 my ($self, $handle, $status, $reason, $body, $headers) = @_;
615
616 return if $handle->{mcp_closed};
617
618 $body = "" unless defined $body;
619 my $content_type = "application/json";
620 my $extra = "";
621
622 if ($headers) {
623 ref $headers eq "HASH" or die "HTTP response headers must be a hash\n";
624 while (my ($name, $value) = each %$headers) {
625 $name =~ /\A[!#\$%&'*+.^_`|~0-9A-Za-z-]+\z/
626 && defined $value && !ref $value && $value !~ /[\r\n\0]/
627 or die "Invalid HTTP response header\n";
628 $name !~ /\A(?:content-length|connection|transfer-encoding)\z/i
629 or die "HTTP response framing headers are managed by the server\n";
630 if (lc $name eq "content-type") {
631 $content_type = $value;
632 } else {
633 $extra .= "$name: $value\r\n";
634 }
635 }
636 }
637
638 my $close = $handle->{mcp_close} || $status != 200 && $status != 202;
639
640 $handle->push_write (
641 "HTTP/1.1 $status $reason\r\n"
642 . "Content-Type: $content_type\r\n"
643 . "Content-Length: " . (length $body) . "\r\n"
644 . $extra
645 . (defined $handle->{mcp_origin} ? "Access-Control-Allow-Origin: $handle->{mcp_origin}\r\n"
646 . ($handle->{mcp_origin} ne "*" ? "Vary: Origin\r\n" : "") : "")
647 . ($status == 204 ? "Access-Control-Allow-Methods: POST, OPTIONS\r\n"
648 . "Access-Control-Allow-Headers: content-type,authorization,mcp-protocol-version,mcp-method,mcp-name\r\n"
649 . "Access-Control-Max-Age: 86400\r\n" : "")
650 . ($status == 405 ? "Allow: POST" . (@{$self->{origins}} ? ", OPTIONS" : "") . "\r\n" : "")
651 . ($status == 401 && $self->{tokens} ? "WWW-Authenticate: Bearer\r\n" : "")
652 . ($close ? "Connection: close\r\n" : "")
653 . "\r\n$body"
654 );
655
656 if ($close) {
657 weaken $self;
658 $handle->on_drain (sub { $self->_close ($_[0]) if $self });
659 } else {
660 $self->_read ($handle);
661 }
662 }
663
664 sub _close {
665 my ($self, $handle) = @_;
666
667 return if $handle->{mcp_closed};
668 $handle->{mcp_closed} = 1;
669
670 my $requests = delete $handle->{mcp_peer}{requests} || {};
671 $_->cancel ("Connection closed") for grep defined, values %$requests;
672
673 delete $self->{connections}{$handle};
674 $handle->destroy;
675 }
676
677 sub close {
678 my ($self) = @_;
679
680 return if $self->{_closed}++;
681 delete $self->{listener};
682 $self->_close ($_) for values %{$self->{connections} || {}};
683 $self->event (close =>);
684 }
685
686 sub DESTROY {
687 $_[0]->close
688 }
689
690 1