NAME

AnyEvent::MCP::Server::HTTP - an optional MCP HTTP listener and socket handler

DESCRIPTION

A subclass of AnyEvent::MCP::Server that handles HTTP POST requests. It can open its own listener, or you can give it sockets accepted elsewhere. If your application already parses HTTP, you can pass decoded requests to AnyEvent::MCP::Server instead of running another HTTP implementation.

Requests sharing one connection are dispatched in sequence, preserving their HTTP response order; requests on other connections are independent. This implementation returns JSON and drops notifications. It does not support SSE or subscriptions.

Request bodies need Content-Length: chunked transfer encoding is not supported. The server does not assign protocol sessions. Legacy clients use the 2025 request/response subset.

Mcp-Param-* headers from x-mcp-header tool schema annotations are not currently validated against the body. The built-in protocol headers Mcp-Protocol-Version, Mcp-Method and, where applicable, Mcp-Name must agree with modern request metadata and parameters; a mismatch is rejected. Custom schema-derived headers do not receive this check.

FIRST USE

For an application that already registers tools in AnyEvent::MCP::Server::Toolset, start an HTTP listener:

use AnyEvent;
use AnyEvent::MCP::Server::HTTP;

my $server = AnyEvent::MCP::Server::HTTP->new (
   toolsets => [$tools], tokens => [$secret],
   host => "127.0.0.1", port => 31740,
);
$server->listen;
print $server->url, "\n";
AE::cv->recv;

$tools is a Toolset and $secret is a previously generated private URL-safe token. Do not use a sample or hardcoded token in a real service. This object handles sockets and HTTP; AnyEvent::MCP::Server dispatches requests, while the Toolset runs tool callbacks. For a standalone program with automatic configuration and startup, prefer AnyEvent::MCP::Server::Simple.

AUTHENTICATION

Unlike "mcp_start" in AnyEvent::MCP::Server::Simple, this low-level object does not load global configuration or derive credentials. Supply tokens explicitly, for example:

my $server = AnyEvent::MCP::Server::HTTP->new (
   host   => "127.0.0.1",
   port   => 31740,
   path   => "/mcp/",
   tokens => ["a-long-random-secret", "another-long-random-secret"],
);
$server->listen;

Each token authorizes the same requests. For either token, a client may POST to /TOKEN/mcp/ without an Authorization header, or POST to /mcp/ with Authorization: Bearer TOKEN. URL credentials are path components, not HTTP cookies. url advertises the first token; url (0) returns the clean endpoint.

The accepted tokens are static, URL-safe strings. HTTP Basic and OAuth access-token validation are not implemented here. There is no automatic fallback to the master-secret-derived token: that belongs to mcp_start or "http_settings" in AnyEvent::MCP::Config.

Authentication defaults to tokens, and construction fails unless a nonempty array of valid tokens is provided. To deliberately delegate authentication to a trusted embedding layer (for example, a reverse proxy that rejects unauthorized requests before they reach MCP), specify auth => "none" instead, without tokens. This is an explicit opt-out, not a permissive default. The embedding layer then owns the entire authentication boundary. The origins setting filters browser origins; it is not authentication. URLs can leak through logs and histories, and HTTP alone does not encrypt tokens. Use TLS when credentials cross an untrusted network. A local address is a sensible default, but not a credential.

METHODS

$server = AnyEvent::MCP::Server::HTTP->new (%config)

Construction does not start a listener. In addition to Server options, the following configuration options are supported:

host => $host

The listening address, defaulting to 127.0.0.1.

port => $port

The listening port, defaulting to zero for an automatically assigned port.

path => $path

The MCP endpoint path, defaulting to /mcp/. Bearer authentication uses $path directly; URL tokens prefix it with /TOKEN.

connection_close => $boolean

When true, disables persistent connections, sends Connection: close and closes the socket after draining each response. Defaults to false. This is useful when an existing listener routes sockets by their first request and cannot route subsequent requests independently.

max_size => $bytes

Limits incoming headers and request bodies, defaulting to 1 MiB.

origins => [$origin...]

Allowed Origin header values. Defaults to ["*"], permitting browser requests from any origin. An empty array rejects requests with an Origin header, while requests without one are accepted. Allowed origins receive CORS response headers, and nonempty origin lists enable OPTIONS preflight for POST. Preflight does not require authentication; the subsequent POST still does.

handle => { %handle }

AnyEvent::Handle constructor options, such as tls, tls_ctx, timeout or on_starttls. The socket and the read/error/EOF handlers belong to this class.

tokens => [$token, ...]

Ordered tokens accepted either as /TOKEN before the configured path or in an Authorization: Bearer TOKEN header. The first token is used in advertised URLs. Token strings must be URL-safe and nonempty.

auth => "tokens" | "none"

Select authentication mode. tokens is the default and requires at least one token; none explicitly disables HTTP authentication and cannot be combined with tokens. Other values are rejected. Browser origins are not a replacement for authentication.

name => $name

Identifies the service. Use "http_settings" in AnyEvent::MCP::Config to resolve named listener configuration before constructing this transport.

url_host => $hostname

Overrides the advertised hostname, for example when listening on a wildcard address. Otherwise a wildcard uses the nodename from uname.

url_scheme => $scheme

Overrides the advertised scheme; otherwise TLS handle options select https, and plain handles select http.

Every configured token authenticates in either the URL or the Authorization header. Without tokens, the constructor requires auth => "none".

$url = $server->url ($with_token)

Returns the advertised URL with the actual listening port. $with_token defaults to true, including the first configured token. Pass false for the clean URL to use with an Authorization bearer token.

The port must be known. Wildcard hostname resolution and reverse-proxy routing remain the caller's responsibility.

$server->listen ($host, $port, %handle)

Starts a listener, replacing any previous listener. Omitted arguments use the configured host and port. The actual bound port is stored in $server->{port}, including when requesting an ephemeral port.

%handle overrides the configured handle options for connections accepted by this listener.

$handle = $server->accept ($socket, %handle)

Starts handling an already accepted socket. The server takes ownership of $socket. No listener is needed.

%handle overrides the configured handle options for this connection.

$server->close

Stops listening, closes accepted sockets and cancels outstanding requests. The same object can subsequently listen or accept again.

EVENTS

on_extension => $callback->($server, $handle, $hello)

Accept raw extension connections on the HTTP listener. A first byte of [ selects a bounded, newline-terminated JSON hello whose first two fields are the protocol name and an accepted listener token. Authenticate before invoking this callback, which validates the protocol-specific fields and takes ownership of the handle, preserving buffered input. Throw an exception to reject it. HTTP connections continue through the normal parser. This dispatch examines decrypted bytes when TLS is configured, but the built-in reverse connector currently supports only plain http:// endpoints.

on_http_request => $callback->($server, $request, $reply)

Called for every complete, syntactically valid HTTP request before MCP routing or authentication. $request is a hash containing the parsed HTTP fields (including method, uri, and http_version) and a body field holding the raw request body. Return false to use the normal MCP handling, or true to handle the request yourself.

$reply->($status, $reason, \%headers, $body) sends a response, either immediately or asynchronously after the callback returns. %headers is optional; Content-Type defaults to application/json, and the server generates Content-Length and manages persistent connections. The body is an octet string. Reply at most once, and return true when taking ownership of the request, even if the response will be sent later. The next request on the same connection is not read until that response is sent.

The hook runs before MCP authentication and CORS checks. Application endpoints must perform their own authentication and browser request checks. It does not replace or bypass the normal authentication of unhandled MCP requests. Malformed or oversized requests are rejected before the hook.

on_connection_error => $callback->($server, $handle, $message)

Called for socket errors before closing the connection.

on_close => $callback->($server)

Called once after close stops the listener and closes all connections. Calling listen or accept again starts a new lifetime.