Pipestream AI / ProtoMolt / tutorial

Tutorial

Python clients without protoc

From “there’s a gRPC server at this address” to “my Python code calls it natively” — no protoc installed, no grpcio-tools, no generated stubs checked in. ProtoMolt discovers the schema and mints the Python message modules; your side needs two pip packages. Every command and output below ran against a live server.

pip install grpcio requests reflect → generate → call real outputs, not mockups

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.

1

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.

2

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']
3

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.

4

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.