Previously untracked working-tree files. mcp-cli and mcp-master are local build outputs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
6.9 KiB
Edge Routing Design
Status: SUPERSEDED by ARCHITECTURE_V2.md (2026-04-01).
This draft used agent-to-agent communication, a single shared cert with
both SANs, private key transmission over gRPC, and an edge field on
routes. All of these were replaced in the v2 architecture by master-
mediated coordination, two separate certs, edge-local cert provisioning,
and public = true on routes. Retained for historical context only.
Draft design for publicly accessible services via edge nodes.
Problem
Services run on internal nodes (rift, hyperborea) behind mc-proxy. To make a service publicly accessible, an edge node (svc) needs an mc-proxy route pointing back to the internal node. Today this is manual: configure svc's mc-proxy, provision a cert, set up DNS. MCP should automate everything except DNS.
Model
A route in the service definition gains an edge field naming the
public-facing node:
[[components.routes]]
port = 443
mode = "l7"
hostname = "kls.metacircular.net"
edge = "svc"
When edge is set, MCP registers the route on two mc-proxy
instances:
- Local (rift mc-proxy):
kls.metacircular.net→127.0.0.1:<assigned-port>. Internal access. - Edge (svc mc-proxy):
kls.metacircular.net→<rift-tailscale-ip>:<assigned-port>. Public access.
Agent-to-Agent Coordination
mc-proxy is a local concern — each node's MCP agent is the sole authority for its mc-proxy. Cross-node route registration goes through agent-to-agent gRPC, not direct mc-proxy access.
Deploy Flow
mcp deploy kls
│
▼
rift agent (node where container runs)
├── pull image, start container
├── allocate host port
├── provision TLS cert (both SANs, see below)
├── register route on rift mc-proxy (local)
│ hostname: kls.metacircular.net
│ backend: 127.0.0.1:<port>
│ cert: /srv/mc-proxy/certs/kls.pem (internal SAN)
│
└── call svc agent: RegisterEdgeRoute
├── hostname: kls.metacircular.net
├── backend: <rift-tailscale-ip>:<port>
├── mode: l7
└── cert: pushed with request (public SAN)
│
▼
svc agent
├── write cert to /srv/mc-proxy/certs/kls.pem
└── register route on svc mc-proxy (local)
hostname: kls.metacircular.net
backend: <rift-tailscale-ip>:<port>
Undeploy Flow
Reverse of deploy. Rift agent calls svc agent to remove the edge route, then tears down locally.
TLS Certificates
There are two hostnames involved:
| Hostname | Where used | Purpose |
|---|---|---|
kls.svc.mcp.metacircular.net |
rift mc-proxy | Internal platform access |
kls.metacircular.net |
svc mc-proxy | Public internet access |
Options
Option A: Two separate certs. Rift's agent provisions one cert with
SAN kls.svc.mcp.metacircular.net for the internal route. For the edge
route, the rift agent (or svc agent) provisions a second cert with SAN
kls.metacircular.net and pushes it to svc.
Option B: Single cert with both SANs. The agent requests one cert
from Metacrypt with both kls.svc.mcp.metacircular.net and
kls.metacircular.net as SANs. This cert is used on both rift and svc
mc-proxy instances.
Option B is simpler (one cert to manage, one renewal) but means internal certs carry public hostnames. Option A is cleaner separation but doubles the cert management.
Recommendation
Option B (single cert, both SANs) for simplicity. The Metacrypt CA is
internal anyway — both names are under our control. The cert issuance
request already accepts a list of dns_names, so this is a natural fit:
{
"dns_names": [
"kls.svc.mcp.metacircular.net",
"kls.metacircular.net"
]
}
The agent pushes the same cert to both rift and svc mc-proxy cert dirs.
Trust Consideration
The Metacrypt CA cert is not publicly trusted. External clients hitting
kls.metacircular.net will see an untrusted cert. This is acceptable
for now (platform services are operator-facing, -k or CA trust is
expected). Future option: Let's Encrypt / ACME on svc mc-proxy for
publicly trusted certs.
New RPC: RegisterEdgeRoute
rpc RegisterEdgeRoute(RegisterEdgeRouteRequest)
returns (RegisterEdgeRouteResponse);
message RegisterEdgeRouteRequest {
string hostname = 1;
string backend = 2; // e.g. "100.95.252.120:13881"
string mode = 3; // "l4" or "l7"
bytes cert_pem = 4; // TLS cert chain
bytes key_pem = 5; // TLS private key
string service = 6; // service name (for cert file naming)
}
message RegisterEdgeRouteResponse {
bool success = 1;
string error = 2;
}
A corresponding RemoveEdgeRoute RPC for undeploy:
rpc RemoveEdgeRoute(RemoveEdgeRouteRequest)
returns (RemoveEdgeRouteResponse);
message RemoveEdgeRouteRequest {
string hostname = 1;
string service = 2;
int32 port = 3; // mc-proxy listener port
}
message RemoveEdgeRouteResponse {
bool success = 1;
string error = 2;
}
Agent Config Changes
The agent needs to know how to reach other agents for edge route registration. This could be:
- Explicit config — agent config lists peer agents:
[[peers]]
name = "svc"
address = "100.x.x.x:9444"
- Discovered from node registry — the agent already receives node info during sync. If the CLI pushes node addresses to agents, the agent can dial peers without extra config.
Option 2 is preferable (less config duplication) but requires the agent to have MCIAS credentials for authenticating to the peer agent.
Service Definition Changes
Proto: add edge field to RouteSpec:
message RouteSpec {
string name = 1;
int32 port = 2;
string mode = 3;
string hostname = 4;
string edge = 5; // edge node name (e.g. "svc")
}
Servicedef: add Edge field to RouteDef:
type RouteDef struct {
Name string `toml:"name,omitempty"`
Port int `toml:"port"`
Mode string `toml:"mode,omitempty"`
Hostname string `toml:"hostname,omitempty"`
Edge string `toml:"edge,omitempty"`
}
Prerequisites
- MCP agent provisioned on svc.
- mc-proxy running on svc with gRPC admin socket (currently config-only on svc, per svc-deployment-plan.md).
- Agent-to-agent authentication (MCIAS service credentials).
Open Questions
- Should the internal route (
kls.svc.mcp.metacircular.net) also be registered on rift mc-proxy when an edge route exists? Or does the edge route replace the internal one? Probably both — internal for platform use, edge for public. - Port for the public mc-proxy listener: should it always be 443 for
L7, or should the route's
portfield apply? For public services, 443 is the only sensible choice. - Should the edge route backend use TLS to the internal node
(
backend_tls = true)? Yes — traffic crosses Tailscale but defense-in-depth is good practice.