The target here is ProtoMolt’s own gRPC service, so one process is both the toolkit and the demo target — but nothing is specific to it. Any reflection-enabled server works identically, and servers without reflection work from a registered schema instead.
Start ProtoMolt
docker run -p 8080:8080 -p 9090:9090 ghcr.io/ai-pipestream/protomolt-serve --demo
--demo seeds a sample order-management schema, so every call
below has material to work with.
Reflect the server
Its address is all we know:
import requests
BASE = "http://localhost:8080"
def verb(name, body):
r = requests.post(f"{BASE}/grpc-json/ProtoMoltService/{name}", json=body)
r.raise_for_status()
return r.json()
reflected = verb("Reflect", {"target": "localhost:9090"})
print(reflected["services"])
output
['ai.pipestream.protomolt.v1.ProtoMoltService', 'grpc.reflection.v1.ServerReflection']
Generate the Python message modules
The reflected descriptor set feeds the code generator, which runs protoc’s own Python generator as WebAssembly on the server:
import pathlib
generated = verb("GenerateStubs", {
"schema": {"descriptorSetBase64": reflected["descriptorSetBase64"]},
"generators": ["python"],
})
out = pathlib.Path("gen")
for f in generated["files"]:
path = out / f["name"]
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(f["content"])
output
gen/ai/pipestream/protomolt/v1/protomolt_service_pb2.py
Real protoc --python_out output, from a server, on demand.
Call the server with plain grpcio
grpcio can invoke any method given the path and the message
classes — the *_pb2_grpc.py convenience stubs are
optional sugar, not a requirement:
import sys
sys.path.insert(0, "gen")
import grpc
from ai.pipestream.protomolt.v1 import protomolt_service_pb2 as pb2
channel = grpc.insecure_channel("localhost:9090")
list_types = channel.unary_unary(
"/ai.pipestream.protomolt.v1.ProtoMoltService/ListTypes",
request_serializer=pb2.ListTypesRequest.SerializeToString,
response_deserializer=pb2.ListTypesResponse.FromString,
)
response = list_types(pb2.ListTypesRequest(filter="demo.shop"))
print([t.full_name for t in response.types])
output
['demo.shop.v1.Customer', 'demo.shop.v1.LineItem', 'demo.shop.v1.Order',
'demo.shop.v1.Order.Status', 'demo.shop.v1.GetOrderRequest',
'demo.shop.v1.ListOrdersRequest', 'demo.shop.v1.OrderService']
Native protobuf over gRPC, typed end to end, from Python — and the message classes were minted moments ago from a reflected schema.
One honesty note on service stubs
protoc’s built-in Python generator produces message modules
(*_pb2.py); the *_pb2_grpc.py service stubs
come from the separate grpc_python plugin, which ProtoMolt
does not ship yet. The channel.unary_unary(...) pattern
above needs only the message classes and is exactly what those stubs
wrap.
Where you land
The same channel drives every verb: compile new protos through the typed surface, validate messages against declared rules, diff schema versions, render JSON Schema. All thirteen verbs speak the same envelopes whether you arrive over gRPC, REST, or MCP.