Surface: every verb, every protocol
Every capability in ProtoMolt is a verb: a named operation that takes one JSON message in and returns one JSON message out. Each verb is served, identically, over eight protocol fronts at once: gRPC, REST, a generated OpenAPI document, a browsable docs page, two flavors of MCP, an IDE agent protocol, and a command line. Point ProtoMolt at any running gRPC service and its methods become verbs too, which means an existing service can grow an HTTP endpoint and an agent endpoint with no glue code written by hand.
flowchart TB
pa["ProtoAction: a name, a description, a required scope, request and response Descriptors, execute()"] --> cat["Action catalog: 72 verbs"]
cat --> f1["gRPC with server reflection"]
cat --> f2["JSON/REST gateway"]
cat --> f3["OpenAPI 3 document"]
cat --> f4["Swagger UI"]
cat --> f5["MCP over stdio"]
cat --> f6["MCP over streamable HTTP"]
cat --> f7["ACP over stdio, for AI-native IDEs"]
cat --> f8["CLI and interactive console"]
One verb, eight fronts
Start with the vocabulary. A protobuf message is a structured record whose shape is
declared in a .proto schema file. A Descriptor is the
in-memory form of that declaration: field names, types, and nesting, with no data in it.
ProtoMolt's integration primitive, ProtoAction, is one verb of the
platform. It has a name such as infer-schema, a one-line description, a
required authorization scope, and a request and a response, each declared as a protobuf
Descriptor. Its entire job is to take one request message and return one response message.
Because the contract is a protobuf message, every front can speak it for free. gRPC sends the message in its compact binary form. REST and the CLI accept the message's canonical proto3 JSON form, which is the standard JSON spelling protobuf defines for every message. MCP and ACP advertise the verb as a tool and take that same JSON as its arguments. The proto file for the typed service states the design rule in its header comment:
surface/grpc service contract, protomolt_service.proto: the parity rule
Show the actual definition
// The canonical proto3 JSON form of each message is exactly the action's
// JSON envelope, so the same call works identically over gRPC, over the
// JSON/REST gateway, and as an MCP tool. There are no generated stubs on
// the server side. One consequence worth holding onto: JSON lives only at the edges. Underneath, every action wraps a descriptor-native library, and the machine-to-machine paths can prefer the binary endpoints, which are smaller and cheaper to parse.
infer-schema, shown over five of the eight fronts. It is one of
the 44 typed verbs: hand it sample JSON records and it proposes a protobuf schema.
Switch tabs and watch the highlighted payload: the JSON never changes, only the framing
around it.
Typed gRPC, discovered by reflection
$ grpcurl -plaintext \
-d '{"samples": [{"name": "x", "n": 1}]}' \
localhost:9090 \
ai.pipestream.proto.grpc.service.v1.ProtoMoltService/InferSchema The binary call. grpcurl learns the service's shape from server reflection, so the client needs no stubs or generated code either.
JSON/REST gateway
$ curl -s -H 'content-type: application/json' \
-d '{"samples": [{"name": "x", "n": 1}]}' \
http://localhost:8080/grpc-json/ProtoMoltService/InferSchema The same message in its canonical proto3 JSON spelling, POSTed to the gateway route. One route shape serves every typed verb.
MCP tool call
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "infer-schema",
"arguments": {"samples": [{"name": "x", "n": 1}]}
}
} What an agent client sends. The catalog manifest a verb publishes is already exactly MCP's tool shape, so there is no translation layer.
ACP session in an IDE
# one line inside a JetBrains or Zed chat session:
infer-schema {"samples": [{"name": "x", "n": 1}]} An ACP session is a console: the verb name and its JSON payload on one line, with results streaming back as message chunks.
Native CLI
$ echo '{"samples": [{"name": "x", "n": 1}]}' \
| protomolt-cli infer-schema Piped to the GraalVM-native binary, which starts in under 10 milliseconds. The same 44-verb catalog the gRPC service serves.
Parity is derived, then pinned by a test
The first parity trick is that nobody hand-writes the JSON Schema. A verb's request type is a protobuf Descriptor, and the published input schema is derived from that message. The javadoc on ProtoAction states the property this buys: the bounds a caller reads are the bounds the verb applies. When the message changes, the schema changes with it; the two cannot drift apart, because one is generated from the other.
The second trick is on the server. There are no generated stubs: at startup, the server
compiles protomolt_service.proto with ProtoMolt's own runtime compiler and
binds one dynamic-message handler per RPC method. Server reflection lists the service
exactly like a stub-generated one, which is why grpcurl works with zero
client setup. The docs put it like this: the tool that manages the format is defined in
the format, served through its own machinery, and discoverable by its own
reflect verb.
The third trick is a test. SurfaceParityTest executes the same verb twice,
once over JSON/REST and once over gRPC, and asserts the response messages match.
Its working assumption is the sentence it encodes: a verb answers the same thing
whichever surface asked it. If a front ever drifts, the parity test fails.
flowchart LR
d["Verb declares request and response as protobuf Descriptors"] --> s["Input JSON Schema derived from the message"]
s --> e["execute() applies exactly those bounds"]
t["SurfaceParityTest: run one verb over JSON and over gRPC"] -. "must answer identically" .-> e
surface/actions ProtoAction.java, abridged: the entire contract a verb declares
Show the actual definition
public interface ProtoAction {
String name(); // "infer-schema"
String description();
String requiredScope(); // one of the ten closed scopes
Descriptor requestType(); // the request message's Descriptor
Descriptor responseType();
Message execute(Message request, ActionContext context);
} 72 verbs: 44 typed, 28 contributed
The full catalog has 72 verbs. 44 of them are typed RPCs of a single
proto service, ai.pipestream.proto.grpc.service.v1.ProtoMoltService, one RPC
per verb. These are the schema and core operations: compile,
list-types, validate-message, diff-schemas,
render-json-schema, infer-schema, and the workflow family
(run-workflow, check-workflow, replay-workflow,
and friends), plus the service-workspace, jobs, and inference verbs. Because they are
RPCs of one service, they appear on every typed surface: gRPC, the JSON/REST gateway, the
generated OpenAPI document, and Swagger UI.
The other 28 are contributed at wire time by the modules that own them:
12 delegation verbs (offering work to agents and reviewing what comes back), 6 mesh verbs
(cluster membership and capacity), 3 metric, 2 search, 3 registry, and 2 acquire verbs.
They are not RPCs of ProtoMoltService, so they have no /grpc-json route and
no OpenAPI entry. Over HTTP they are reached through the registry's actions route,
POST /protomolt/actions/{name}, and they are on MCP, ACP, and the
CLI like everything else. Their envelopes follow the same rule: each family's request
message is declared in its own service proto, and the published schema is derived from
that message.
ActionCatalog.defaults 34 verbs: standalone MCP stdio binary 44 typed verbs: RPCs of ProtoMoltService 72 verbs: everything, once all modules are wired
flowchart TB
cat["Action catalog: 72 verbs"] --> typed["44 typed verbs, RPCs of ProtoMoltService"]
cat --> contrib["28 contributed verbs, added by their modules at wire time"]
typed --> tall["gRPC + reflection, JSON/REST, OpenAPI, Swagger UI, MCP stdio and HTTP, ACP, CLI"]
contrib --> call["registry actions route, MCP, ACP, CLI"]
Point it at any gRPC service
Here is the "no glue code" pitch in one paragraph. Register a running gRPC endpoint with
the service-workspace verb service-register and ProtoMolt reflects it: it
reads the endpoint's descriptor set over server reflection and binds every unary method as
a catalog verb named <profile>-<method> in kebab case. The docs'
example: a ListOrders method on the billing profile answers to
billing-list-orders. No codegen, no restart; the new verbs join the catalog
the moment registration completes, and from there an agent can call them over MCP or an
IDE over ACP. Client-streaming methods get no verb, because a verb takes exactly one
request.
Mechanically, that is what turning a gRPC service into an agent endpoint means:
reflection in, catalog verbs out. A model connected over MCP gains typed, schema-checked
tools for a service its authors never heard of. If you control the service's Java code,
the REST gateway can also expose its methods as typed REST endpoints with OpenAPI
entries; exposure is opt-in via a @ProtoRestExposed annotation.
docs/surface and apps/serve: connecting an agent to a running server, and the one-command demo
claude mcp add --transport http protomolt http://localhost:8080/mcp
docker run -p 8080:8080 -p 9090:9090 \
ghcr.io/ai-pipestream/protomolt-serve --demo
The --demo flag seeds a throwaway git registry, a sample
demo.shop.v1.Order schema, and a sample workflow, so every front below is
exercisable in one command.
The eight fronts, one by one
Whatever the transport, the same authorization check runs at every front: catalog
dispatch, the gRPC interceptor, the REST mount, and the registry actions route all
evaluate the verb's required scope against the caller. The scope vocabulary is a closed
set of ten: schema-read, schema-write,
service-invoke, workflow-run, artifact-access,
worker-coordinate, search-query, search-index,
metrics-query, metrics-rebuild. A policy or caller naming
anything else is refused by name, so a typo is a loud failure rather than a silently dead
grant.
1. Typed gRPC with server reflection
The 44 typed verbs as RPCs of ProtoMoltService, served stub-free and compiled from the
proto at startup. Reflection is always on, and each call runs on its own virtual thread,
so blocking work inside a verb parks without occupying a platform worker. The README's
self-hosting proof: the GrpcInvoke RPC of one ProtoMolt server can call the
ListTypes RPC of another, a case the test suite pins.
2. JSON/REST
POST /grpc-json/{Service}/{Method} through
ProtoRestGateway, a framework-agnostic dispatcher any host can mount. Auth
is fail-closed: without an explicitly supplied token validator, tokens are rejected. The
typed and dynamically compiled messages take the same transcoding path.
3. OpenAPI 3
GET /openapi.json returns a generated OpenAPI 3.0.3 document built from the
method registry and the descriptors. Paths are mechanical,
{prefix}/{Service}/{Method}; there are no
google.api.http annotations anywhere in the tree. The generator does not
model oneofs or emit field descriptions from the metadata options yet.
4. Swagger UI
GET /docs serves Swagger UI over that document: a browsable, try-it console
with no frontend build. Security schemes are derived from the token requirements, so the
Authorize button works against a protected server.
5. MCP over stdio
The protomolt-mcp binary is a standalone agent server speaking JSON-RPC 2.0,
newline-delimited over standard input and output, built on Jackson and the JDK with no
framework and no reactive runtime. It registers 34 verbs and negotiates MCP protocol
versions 2025-06-18, 2025-03-26, and 2024-11-05, settling on an older one when the client
demands it. Agents can also browse protomolt:// resources instead of
spending tool calls: protomolt://workspace is always present and lists the
exact tool names plus a SHA-256 fingerprint of the catalog.
6. MCP over streamable HTTP
The serve process mounts the full catalog at /mcp with real session
discipline: initialize returns an Mcp-Session-Id header that
later calls must carry, DELETE closes a session, and a
notifications/cancelled message cancels an in-flight tool. Sessions are
capped at 256, and non-local browser origins are refused as a DNS-rebinding guard.
7. ACP over stdio
protomolt-acp-agent exposes the catalog to ACP-capable IDEs such as JetBrains
AI chat and Zed, on virtual threads with the same newline-delimited JSON-RPC style. A
session is a console: typing a verb name and a JSON payload runs the verb, and results
stream back as message chunks, one per result for streaming verbs. The agent declares no
file, terminal, or permission capabilities; it is read-only.
8. CLI and interactive console
protomolt-cli runs the same 44-verb catalog as the typed service and adds no
verbs of its own. It is built as a GraalVM native image: under 10 milliseconds to start,
about 35 MB of resident memory, no JRE on the target. Run with no arguments it drops into
an interactive console with a protomolt> prompt, where a failing verb
prints its error and the session continues. Exit codes are 0, 1, and 2 for success, verb
failure, and usage errors, and errors print stable kebab-case codes so a shell script can
branch on the code instead of parsing prose.
apps/serve startup banner: one process announcing every front
ProtoMolt serving:
gRPC 0.0.0.0:9090 ai.pipestream.proto.grpc.service.v1.ProtoMoltService (reflection on)
REST http://0.0.0.0:8080/grpc-json/ProtoMoltService/{Method}
API http://0.0.0.0:8080/openapi.json
Docs http://0.0.0.0:8080/docs
MCP http://0.0.0.0:8080/mcp (streamable HTTP) Host it in your own server
The gateway and verb catalog are plain libraries, and host/ provides
adapters for six server stacks. Three are real
hosts: they bind a port themselves. Three are facades: your
framework binds the port and the facade plugs the gateway into it. Quarkus stays a facade
because it still runs on Vert.x 4 and cannot reuse the Vert.x 5 host.
| Module | Stack | Kind |
|---|---|---|
protomolt-server-jdk | com.sun.net.httpserver, virtual threads | real host, engine id jdk, the default |
protomolt-server-vertx | Vert.x 5 (createRouter() for embedding) | real host, engine id vertx |
protomolt-server-netty | Netty 4.2 with a virtual-thread invoker pool | real host, engine id netty |
protomolt-server-spring | Spring MVC @RestController | facade; Spring binds it |
protomolt-server-micronaut | plain facade, no Micronaut annotations | facade, no DI module by design |
protomolt-server-quarkus | CDI bean plus JAX-RS | facade on Vert.x 4 |
On top of the facades sit two framework integrations. protomolt-integration-spring
is a Spring Boot 3 auto-configuration that produces the descriptor registry, transcoder,
method registry, fail-closed token validator, and gateway as beans, each one backing off
when your app defines its own. protomolt-integration-quarkus is a proper
Quarkus extension with the same producers; it is experimental. Micronaut stays at the
facade: construct the gateway objects directly, or wrap them in your own factories.
README, library embedding: the gateway mounted on the JDK host
var gateway = new ProtoRestGateway(methods, transcoder, tokenValidator);
var server = new JdkProtoRestServer(config, gateway);
server.start();
// POST /grpc-json/{service}/{method}, GET /openapi.json, GET /health The consoles
Four browser surfaces ship with the platform, all thin clients over the same verbs. There
is no console-only API, so anything a button does, curl or an agent can do
too.
- Task console (
/console/tasks, served byprotomolt-serve): the delegation coordinator's browser face. A worker directory, task timelines built from recorded protocol frames, the offer dialog, and a review panel where accepting or revising requires a written verdict. Updates arrive by long-poll, and with a receipt key configured it hands over signed work records. - Browser console (
/console, a Vue 3 app riding inside the serve jar): sections for Tasks, Schemas, Workflows, Services, Search, Metrics, and Receipts. Its design stance, quoted from its own README: the console is a thin client over verbs any caller can invoke. What the console does with a button, a shell script does withcurland an agent does over MCP. - Search console (the
search-consolerole): a working, deliberately thin browser over the search service. - Playground (the
playgroundrole, port 8095): a streaming parser playground for trying parsing against live input.