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.

In plain terms. You built a service, and now three kinds of clients want to use it: a backend that speaks gRPC, a browser page that only speaks HTTP and JSON, and an AI assistant that speaks MCP. Normally that means writing and maintaining an adapter for each. ProtoMolt generates the adapters: describe an operation once, as a pair of protobuf messages, and the platform serves it over all three protocols (and five more) from the same code, with a test that fails the build if any front starts answering differently.
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 catalog, eight fronts. Every front serves the same verbs; nothing is re-implemented per protocol.

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.

One verb, 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
The schema a caller reads is generated from the message the verb enforces, and a parity test pins the fronts together.

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.

schema built-ins: the 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"]
The 44/28 asymmetry. Typed verbs ride every surface; contributed verbs ride the catalog surfaces.

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.

ModuleStackKind
protomolt-server-jdkcom.sun.net.httpserver, virtual threadsreal host, engine id jdk, the default
protomolt-server-vertxVert.x 5 (createRouter() for embedding)real host, engine id vertx
protomolt-server-nettyNetty 4.2 with a virtual-thread invoker poolreal host, engine id netty
protomolt-server-springSpring MVC @RestControllerfacade; Spring binds it
protomolt-server-micronautplain facade, no Micronaut annotationsfacade, no DI module by design
protomolt-server-quarkusCDI bean plus JAX-RSfacade 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 by protomolt-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 with curl and an agent does over MCP.
  • Search console (the search-console role): a working, deliberately thin browser over the search service.
  • Playground (the playground role, port 8095): a streaming parser playground for trying parsing against live input.