ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/AnyEvent-MCP/lib/AnyEvent/MCP/Server/HTTP.pm
Revision: 1.15
Committed: Sat Oct 17 08:07:02 2026 UTC (2 weeks, 2 days ago) by root
Branch: MAIN
Changes since 1.14: +57 -3 lines
Log Message:
*** empty log message ***

File Contents

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