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 twenty-three verbs speak the same envelopes whether you arrive over gRPC, REST, or MCP.