multi/

directory
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 31, 2026 License: MIT

README

examples/multi

Three separate processes — gateway, greeter, library — wired together at runtime via the control plane. Demonstrates dynamic service registration: the gateway boots empty, services self-register, the GraphQL schema rebuilds in place, and graceful shutdown deregisters.

Run

$ cd examples/multi
$ ./run.sh
control plane listening on :50090
graphql listening on :8080
greeter gRPC listening on :50051
greeter registered with localhost:50090
library gRPC listening on :50052
library registered with localhost:50090

In another terminal:

$ curl -sS -d '{"query":"{ greeter { hello(name: \"alice\") { greeting } } library { listBooks(author: \"\") { books { title year } } } }"}' \
       -H 'Content-Type: application/json' http://localhost:8080/graphql
{"data":{"greeter":{"hello":{"greeting":"Hello, alice!"}},
         "library":{"listBooks":{"books":[
           {"title":"Go Programming","year":2015},
           {"title":"The Go Programming Language","year":2015},
           {"title":"Designing Data-Intensive Applications","year":2017}]}}}}

GraphiQL at http://localhost:8080/graphql in a browser.

What this demonstrates

Boot-empty. Start just the gateway (go run ./cmd/gateway) and introspect — the schema has only the inert _status field. No services have registered yet.

Self-registration. Each service binary starts its own gRPC server, then calls controlclient.SelfRegister against the gateway's control plane (:50090). The gateway rebuilds its schema and atomically swaps in the new one; in-flight requests finish on the old schema, new ones see the additions.

Graceful deregister. SIGTERM either service — its defer reg.Close(ctx) calls Deregister, the gateway rebuilds without it.

Heartbeat-failure eviction. SIGKILL a service (no graceful exit) — the gateway notices missed heartbeats and evicts after TtlSeconds (default 30s). The schema rebuilds the same way.

Single address, multiple services. Not exercised in this example, but supported: one binary that hosts multiple services can register them all with one Register call, sharing a single gRPC connection back from the gateway. See controlplane.v1.RegisterRequest.services (a repeated ServiceBinding).

Parameter / header injection. cmd/gateway/main.go registers two gw.Use(...) injectors before services come in:

  • InjectPath("greeter.Hello.name", …, Hide(false), Nullable(true)) flips name to optional in the external schema and defaults it to the caller's IP when omitted (X-Forwarded-ForX-Real-IPRemoteAddr). Send a value and it passes through.
  • InjectHeader("X-Source-IP", …) stamps the caller's IP on every outbound dispatch — gRPC metadata for proto pools, HTTP header for OpenAPI sources. The greeter logs x-source-ip=… whenever the metadata lands.

Try it:

$ curl -sS -d '{"query":"{ greeter { hello { greeting } } }"}' \
       -H 'Content-Type: application/json' \
       -H 'X-Forwarded-For: 192.0.2.42' \
       http://localhost:8080/api/graphql
{"data":{"greeter":{"hello":{"greeting":"Hello, 192.0.2.42!"}}}}

The third flavor — hide-and-fill via InjectType[*authpb.Context] — lives in examples/auth so the gateway here stays generic (no service-specific proto imports).

MCP integration (worked example)

The gateway publishes its registered GraphQL surface to LLM agents via the Model Context Protocol over Streamable HTTP at /mcp, gated by the admin bearer. Four tools land:

Tool Purpose
schema_list Every op the operator's allowlist exposes (path + kind + namespace + first-line doc).
schema_search Filter by dot-segmented path glob + regex over op name / arg names / doc body.
schema_expand Full structured definition of one op or type, plus its transitive type closure.
query Execute a GraphQL operation in-process. Result wrapped as ResponseWithEvents { response, events }.

The allowlist is operator-curated. cmd/gateway/main.go seeds the example surface at boot:

gateway.WithMCPInclude("greeter.**", "library.**", "admin.**")

Default-deny — agents only see what's explicitly included. Toggle auto_include=true (via WithMCPAutoInclude or /api/admin/mcp/auto-include) for "every public leaf minus the exclude list" mode. Internal _* namespaces are filtered first either way.

./run.sh persists the admin token to /tmp/gwag-multi/admin-token (via --admin-data-dir). Then:

$ cd examples/multi
$ go run ./cmd/mcp-demo
using admin token from /tmp/gwag-multi/admin-token (64 hex chars)

--- step 1 — retune the MCP allowlist (idempotent — already seeded at boot) ---
  → POST /api/admin/mcp/include path=greeter.** ok
  → POST /api/admin/mcp/include path=library.** ok
  → MCPConfig: {"auto_include":false,"include":["greeter.**","library.**","admin.**"],"exclude":[]}

--- step 2 — open MCP client at http://localhost:8080/mcp ---
  → server: gwag 1.0.0

--- step 3 — tools/list ---
  → schema_list — List every operation exposed via the MCP surface…
  → schema_search — Filter the MCP-allowed operation surface.
  → schema_expand — Return the structured definition of one op or type…
  → query — Execute a GraphQL operation against the gateway in-process.

--- step 4 — schema_list ---
  [{"path":"greeter.hello", "kind":"Query", "namespace":"greeter", ...}, ...]
…

The demo chains all four tools: list → search → expand → query. See cmd/mcp-demo/main.go for the full sequence; it's a useful template for wiring your own MCP-aware agent against the gateway.

Retune the allowlist at runtime via the admin OpenAPI routes:

$ TOKEN=$(cat /tmp/gwag-multi/admin-token)
$ curl -sS -H "Authorization: Bearer $TOKEN" \
       -H 'Content-Type: application/json' \
       -d '{"path":"greeter.**"}' \
       http://localhost:8080/api/admin/mcp/include
$ curl -sS -H "Authorization: Bearer $TOKEN" \
       -H 'Content-Type: application/json' \
       -d '{"auto_include":true}' \
       http://localhost:8080/api/admin/mcp/auto-include
$ curl -sS http://localhost:8080/api/admin/mcp   # always public for inspection

The same admin_mcp_* mutations are also surfaced as GraphQL fields (dogfooded via the huma → OpenAPI → self-ingest path), so any GraphQL client can curate the surface without separate REST calls.

Subscriptions are not exposed as MCP tools in v1.

Layout

examples/multi/
  cmd/
    gateway/main.go    GraphQL :8080 + control plane :50090
    greeter/main.go    Greeter gRPC :50051, self-registers as "greeter"
    library/main.go    Library gRPC :50052, self-registers as "library"
  protos/              Service definitions
  gen/                 protoc-generated bindings (committed for convenience)
  run.sh               Spawns all three; Ctrl-C cleans up

The protos and bindings are only needed by the services (which want typed gRPC servers and ship the raw .proto source bytes to the gateway via controlclient.Service.ProtoSource). The gateway itself imports nothing service-specific — it compiles the .proto via protocompile at registration time (preserving comments) and dispatches via dynamicpb.

Cluster mode (3 gateways, KV-backed registry)

run-cluster.sh boots three gateways with embedded NATS + JetStream. Each gateway joins the cluster via --nats-peer; the registry KV bucket replicates across nodes (R bumps monotonically as peers join). Two greeters at v1 and one at v2 register on different gateways:

$ ./run-cluster.sh
Cluster up. GraphQL endpoints:
  n1 → http://localhost:18080/graphql
  n2 → http://localhost:18081/graphql
  n3 → http://localhost:18082/graphql

Every gateway dispatches to every greeter, regardless of which gateway received the registration. The same query against any node returns the same response:

$ for p in 18080 18081 18082; do
    curl -sS -X POST http://localhost:$p/graphql \
      -H 'Content-Type: application/json' \
      -d '{"query":"{ greeter { hello(name:\"world\") { greeting } v2{hello(name:\"v2\"){greeting}} } }"}'
  done

Behind the scenes:

  • Register writes to the gwag-registry KV bucket; it does not touch local pool state.
  • Every gateway watches that bucket and reconciles its pools on each Put/Delete, dialing the advertised addr through its own conn pool.
  • Heartbeat re-Puts the entry, refreshing the bucket TTL. A service that stops heartbeating without graceful Deregister has its key auto-expired by JetStream after peerTTL (30s).
  • Stream replicas auto-bump 1→2→3 as peers join; monotonic — never shrinks automatically. Operator-driven shrink is a separate path (see peer forget once it lands).

Optional mTLS

Pass --tls-cert, --tls-key, and --tls-ca to enable mutual TLS on both the NATS cluster routes (gateway-to-gateway) and the gRPC control plane (service-to-gateway). The same cert pair covers both surfaces; issue one per node from a shared CA:

$ go run ./cmd/gateway \
    --node-name n1 --nats-data /tmp/n1 --nats-peer 127.0.0.1:14249 \
    --tls-cert n1.crt --tls-key n1.key --tls-ca shared-ca.crt

The library's gateway.LoadMTLSConfig(cert, key, ca) returns a *tls.Config with RequireAndVerifyClientCert for true mesh mTLS. Service binaries pass an equivalent TLS dial option to controlclient.SelfRegister(... DialOptions: []grpc.DialOption{...}). The gateway's outbound dial to registered services also uses the same cert when WithTLS is set.

CLI alternative

For static registration without writing Go, the gwag binary takes --proto path=addr flags. Use that when service-discovery is out of scope and the proto + addr list is known at deploy time.

Directories

Path Synopsis
cmd
gateway command
Gateway binary: serves GraphQL on :8080 and a control plane on :50090.
Gateway binary: serves GraphQL on :8080 and a control plane on :50090.
greeter command
Greeter service: starts a real gRPC server on :50051 and self-registers with the gateway's control plane.
Greeter service: starts a real gRPC server on :50051 and self-registers with the gateway's control plane.
hello-graphql command
hello-graphql: a tiny native GraphQL service self-registering with the gateway via the GraphQL stitching path.
hello-graphql: a tiny native GraphQL service self-registering with the gateway via the GraphQL stitching path.
hello-openapi command
hello-openapi: an HTTP/JSON service that exposes a single Hello operation, self-registers with the gateway via the OpenAPI control plane, and exists primarily so bench traffic openapi --direct has a format-native upstream to compare against.
hello-openapi: an HTTP/JSON service that exposes a single Hello operation, self-registers with the gateway via the OpenAPI control plane, and exists primarily so bench traffic openapi --direct has a format-native upstream to compare against.
hello-proto command
hello-proto: a tiny native gRPC service self-registering with the gateway via the proto control plane.
hello-proto: a tiny native gRPC service self-registering with the gateway via the proto control plane.
library command
Library service: real gRPC server on :50052, self-registers with the gateway.
Library service: real gRPC server on :50052, self-registers with the gateway.
mcp-demo command
Worked example for the gateway's MCP integration.
Worked example for the gateway's MCP integration.
gen

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL