Skip to content

Architecture v0.3.1 — gRPC transport

Scope from decision D19 and contract §11 (binding; supersedes §3 /mcp and the HTTP endpoint mentions in §5/§7/§8). Everything from v0.3.0 plus:

Breaking for HTTP MCP clients

The worker's HTTP surface (POST /mcp, /healthz, /readyz, /metrics, /admin/reload) is removed. Standard MCP clients connect through ramen-mcp-bridge. See the migration note.

Transport

  • Protos in proto/ramen/v1/ are the single source: mcp.proto (ramen.v1.Mcp: unary Call(JsonRpc) → JsonRpc carrying one UTF-8 JSON-RPC 2.0 message as bytes body; Session bidi stream reserved, may return UNIMPLEMENTED) and admin.proto (ramen.v1.Admin: Reload, Metrics, both returning JSON bytes). Rust stubs via tonic-build; Python stubs via grpcio-tools into ramen_proto packages vendored in the console, the runtime bridge and the test harness (make proto regenerates them).
  • ramen-node serves one h2c port RAMEN_NODE_PORT (8080) with ramen.v1.Mcp, ramen.v1.Admin and grpc.health.v1.Health (SERVING once runtime.load succeeded, NOT_SERVING before). RAMEN_TLS_CERT + RAMEN_TLS_KEY switch the port to TLS (h2); otherwise the LB terminates TLS.
  • A JSON-RPC notification returns an empty body. Message size limit 4 MiB. RAMEN_MAX_INFLIGHT → RESOURCE_EXHAUSTED.

Security parity on gRPC

Check Where Failure
authorization: Bearer <rmk_key> metadata, constant-time compare against RAMEN_MCP_KEYS (empty set = deny all) Mcp/Call UNAUTHENTICATED
Peer address (or first x-forwarded-for hop with RAMEN_TRUST_PROXY=1) ∈ RAMEN_ALLOWED_CIDRS Mcp/Call PERMISSION_DENIED
x-ramen-admin-key metadata + RAMEN_ADMIN_CIDRS Admin/* UNAUTHENTICATED / PERMISSION_DENIED
RAMEN_BLOCKED names hidden from */list body JSON-RPC -32601 on call
Health Health/Check unauthenticated by design

One JSON access-log line per call with the 0.3.0 fields plus grpc_code.

Routing at the edge

Clients and the bridge send metadata ramen-group and ramen-zone.

GCP (GKE Gateway) AWS (ALB)
Backend protocol worker Service appProtocol: kubernetes.io/h2c (fallback: node TLS + HTTP2 if the Gateway rejects h2c) target group backend-protocol-version: GRPC
Route HTTPRoute matches.headers: [ramen-group=<g>, ramen-zone=<z>], no path rewrite listener rule on the two headers
Health HealthCheckPolicy type GRPC health check gRPC code 0
IP rules Cloud Armor (unchanged) WAFv2 (unchanged)

The console still targets pods directly (<pod-ip>:8080) for reload, metrics and readiness.

Console

ramen_console.grpcclient (grpcio, cached channel per target, 10 s deadline) replaces every HTTP call to a worker: _reload_and_smoke → Admin/Reload + Mcp/Call tools/list; workers() → Admin/Metrics; readiness → Health/Check. The local adapter is identical against worker:8080. Pod-proxy settings (RAMEN_GCP_POD_PROXY, RAMEN_AWS_POD_PROXY) are gone.

Bridge

ramen-mcp-bridge (Python, console script of ramen-runtime[grpc], also in the worker image) is a stdio MCP server: --target <host:port> --key <rmk_…> --group <g> --zone <z> [--tls|--insecure] [--ca <pem>]. Each stdio JSON-RPC message is forwarded to Mcp/Call; notifications are forwarded and produce nothing. It is the documented way to connect Claude Desktop, Cursor and the mcp SDK.

Local stack, harness, CI

Compose exposes the worker's 8080 as h2c; demo.sh and the harness speak gRPC (grpcio); mcp_call.py and deploy/local/mcp-client-config.example.json use the bridge. ramen_tests.mcp_client is a gRPC client; conformance covers UNAUTHENTICATED, PERMISSION_DENIED, blocked -32601, the size limit, health states and the bridge end to end through the mcp SDK stdio client. scripts/cloud_smoke.sh uses grpcurl.

Carried security mediums fixed

  • GCP console GSA: resourcemanager.projectIamAdmin and project-level storage.admin dropped; serviceAccountCreator/Deleter/User plus the custom role ramenConsoleSaIam limited to setIamPolicy on ramen-* service accounts; bucket- and secret-level bindings instead of project-level.
  • AWS console role: wafv2:* narrowed to the ramen web ACL / IP sets by ARN pattern; iam:PutRolePolicy limited to /ramen/ roles.
  • Kubernetes RBAC: the console ClusterRole loses cluster-wide secrets / serviceaccounts; it creates a namespaced Role + RoleBinding for its KSA in every zone namespace it attaches (ClusterRole keeps namespaces, networkpolicies, httproutes and read verbs).
  • Repo sync: the GitHub token goes through http.extraheader / GIT_ASKPASS, never into the remote URL, and credentials are stripped from .git/config in the bucket copy.
  • OIDC: PKCE (S256) and nonce. Node: constant-time key compares.

images/logo_v2.png (same coral #F26B3A on off-white #F4F1EC palette) replaces v1 in the console static files, this site (docs/img/logo.png, logo-mark.png, favicon.png), the README and the launch drafts.