Files
mcp/docs/edge-routing-design.md
kyleandClaude Fable 5.1 d5d3004518 Add edge routing design doc and deploy-agent script; ignore built binaries
Previously untracked working-tree files. mcp-cli and mcp-master are local
build outputs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 15:04:15 -07:00

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:

  1. Local (rift mc-proxy): kls.metacircular.net127.0.0.1:<assigned-port>. Internal access.
  2. 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:

  1. Explicit config — agent config lists peer agents:
[[peers]]
name = "svc"
address = "100.x.x.x:9444"
  1. 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

  1. MCP agent provisioned on svc.
  2. mc-proxy running on svc with gRPC admin socket (currently config-only on svc, per svc-deployment-plan.md).
  3. 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 port field 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.