NAME

Langertha::Skeid::Protocol::MCP - The MCP gateway's wire -- relayed headers, upstream URLs and the server listing

VERSION

version 0.004

DESCRIPTION

Skeid relays MCP's "Streamable HTTP" transport to the MCP servers a customer key was assigned (ADR 0022): POST, GET and DELETE on /mcp/<name>, and a listing of a key's servers on /v1/mcp/servers. It does not read MCP -- no JSON-RPC, no tool list, no session state. What it has to know of the wire is small and is all here: which transports a server entry may name, which request headers travel upstream, how a sub-path becomes an upstream URL, and what a listing entry looks like.

{ "object": "list", "data": [
  { "name": "bookrag", "transport": "streamable-http",
    "url": "https://llm.example.com/mcp/bookrag", "description": "..." },
  { "name": "files", "transport": "stdio", "command": "mcp-files",
    "args": ["--root", "/srv/data"], "env": {}, "description": "..." } ] }

Everything here is a class method. The config section is "mcp" in Langertha::Skeid, the routes are "MCP routes" in Langertha::Skeid::Proxy.

response_header_allowed

my $passes = Langertha::Skeid::Protocol::MCP->response_header_allowed($name);

Whether a response header of the MCP server is relayed to the client: Content-Type, Cache-Control, Retry-After and every Mcp-* header. An allowlist, not a filter: a header the server adds tomorrow does not reach the client until it is named here.

transports

my @names = Langertha::Skeid::Protocol::MCP->transports;   # ('stdio', 'streamable-http')

The transports a server entry may name, sorted.

default_transport

The transport of an entry that names none: streamable-http.

is_transport

my $known = Langertha::Skeid::Protocol::MCP->is_transport($name);

Whether a server entry may name this transport.

is_relayed

my $proxied = Langertha::Skeid::Protocol::MCP->is_relayed($transport);

Whether Skeid relays a server of this transport. True for streamable-http; a stdio server runs on the customer's machine, and its entry is a hint in the listing and nothing else.

listing_path

The client path of the listing: /v1/mcp/servers.

endpoint

my $path = Langertha::Skeid::Protocol::MCP->endpoint('bookrag');   # /mcp/bookrag

The client path of a server, which is also the endpoint its usage events carry -- whatever sub-path a request named.

name_problem

my $problem = Langertha::Skeid::Protocol::MCP->name_problem($name);   # undef when it is fine

A server name is one segment of a URL path and a part of a usage event's endpoint: it starts with a letter or digit and holds only letters, digits, ., _ and -, at most 64 characters. Returns what is wrong with a name, or nothing.

url_problem

my $problem = Langertha::Skeid::Protocol::MCP->url_problem($url);   # undef when it is fine

What is wrong with a configured URL -- a server's url or the section's public_url -- or nothing: it has to be an absolute http or https URL, and it may not carry credentials (user:password@), which would be a secret in the config. The message never repeats the URL.

config_header_problem

my $problem = Langertha::Skeid::Protocol::MCP->config_header_problem('Authorization');

Why a header may not be among a server's static headers, or nothing when it may. Refused are the credential headers (Authorization, Proxy-Authorization, X-Api-Key, Cookie), the headers "request_headers" passes on from the client, and the ones that belong to the connection.

request_headers

my %headers = Langertha::Skeid::Protocol::MCP->request_headers($c->req->headers);

The client's headers that go upstream, by the name the client used: Content-Type, Accept, Last-Event-ID and every header whose name starts with Mcp- (Mcp-Session-Id, Mcp-Protocol-Version, whatever the protocol adds there). An allowlist: the client's Authorization, X-Api-Key, cookies and forwarding headers are not on it.

upstream_url

my $url = Langertha::Skeid::Protocol::MCP->upstream_url($server->{url}, $rest, $c->req->url->query);

The Mojo::URL a request is relayed to: the server's configured URL, with the sub-path the client named after /mcp/<name>/ appended to its path segment by segment and the client's query parameters appended to its query. $rest is the decoded sub-path, or nothing.

Returns nothing for a sub-path with a . or .. segment or a backslash in it. Such a path is refused rather than normalised: a customer key must not climb out of the path the operator configured, whatever the upstream's own idea of path normalisation is.

listing_entry

my $entry = Langertha::Skeid::Protocol::MCP->listing_entry($server, 'https://llm.example.com');

What the listing says about one resolved server ("mcp_servers" in Langertha::Skeid): name, transport and description always; for a relayed server its public url, {base}/mcp/{name}; for a stdio one command, args and env. Never the upstream URL, the key reference, the variable name or the static headers.

is_event_stream

my $open_ended = Langertha::Skeid::Protocol::MCP->is_event_stream($res->headers->content_type);

Whether an answer is a Server-Sent Events stream, by its media type. Such an answer has no end Skeid could check without reading MCP, so however it ends is how it ends.

SEE ALSO

"mcp" in Langertha::Skeid (the config section), "MCP routes" in Langertha::Skeid::Proxy

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/langertha-skeid/issues.

IRC

Join #langertha on irc.perl.org or message Getty directly.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.