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