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: unaryCall(JsonRpc) → JsonRpccarrying one UTF-8 JSON-RPC 2.0 message asbytes body;Sessionbidi stream reserved, may returnUNIMPLEMENTED) andadmin.proto(ramen.v1.Admin:Reload,Metrics, both returning JSON bytes). Rust stubs via tonic-build; Python stubs via grpcio-tools intoramen_protopackages vendored in the console, the runtime bridge and the test harness (make protoregenerates them). ramen-nodeserves one h2c portRAMEN_NODE_PORT(8080) withramen.v1.Mcp,ramen.v1.Adminandgrpc.health.v1.Health(SERVINGonceruntime.loadsucceeded,NOT_SERVINGbefore).RAMEN_TLS_CERT+RAMEN_TLS_KEYswitch 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.projectIamAdminand project-levelstorage.admindropped;serviceAccountCreator/Deleter/Userplus the custom roleramenConsoleSaIamlimited tosetIamPolicyonramen-*service accounts; bucket- and secret-level bindings instead of project-level. - AWS console role:
wafv2:*narrowed to theramenweb ACL / IP sets by ARN pattern;iam:PutRolePolicylimited 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/configin the bucket copy. - OIDC: PKCE (S256) and
nonce. Node: constant-time key compares.
Logo¶
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.