diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index 862fcb69b8ed64..7a3fcf85dabefd 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -2099,6 +2099,11 @@
quarkus-grpc-stubs
${project.version}
+
+ io.quarkus
+ quarkus-grpc-reflection
+ ${project.version}
+
io.quarkus
quarkus-grpc-deployment
diff --git a/extensions/grpc/cli/pom.xml b/extensions/grpc/cli/pom.xml
new file mode 100644
index 00000000000000..2c689bbfedacfd
--- /dev/null
+++ b/extensions/grpc/cli/pom.xml
@@ -0,0 +1,68 @@
+
+
+ 4.0.0
+
+ quarkus-grpc-parent
+ io.quarkus
+ 999-SNAPSHOT
+
+
+ quarkus-grpc-cli
+ Quarkus - gRPC - CLI
+ gRPC CLI
+
+
+
+ io.vertx
+ vertx-grpc-client
+
+
+ com.google.protobuf
+ protobuf-java-util
+
+
+ io.quarkus
+ quarkus-grpc-reflection
+
+
+ io.quarkus
+ quarkus-core-deployment
+
+
+ io.quarkus
+ quarkus-picocli
+
+
+ io.quarkus
+ quarkus-arc
+
+
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+ true
+
+
+
+ build
+ generate-code
+ generate-code-tests
+
+
+
+
+
+ maven-compiler-plugin
+
+
+ -parameters
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/DescribeCommand.java b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/DescribeCommand.java
new file mode 100644
index 00000000000000..4103ea238eb6ce
--- /dev/null
+++ b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/DescribeCommand.java
@@ -0,0 +1,44 @@
+package io.quarkus.grpc.cli;
+
+import java.util.List;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.DescriptorProtos;
+import com.google.protobuf.util.JsonFormat;
+
+import io.grpc.reflection.v1.MutinyServerReflectionGrpc;
+import io.grpc.reflection.v1.ServerReflectionRequest;
+import io.grpc.reflection.v1.ServerReflectionResponse;
+import io.smallrye.mutiny.Multi;
+import picocli.CommandLine;
+
+@CommandLine.Command(name = "describe", sortOptions = false, header = "grpc describe")
+public class DescribeCommand extends GcurlBaseCommand {
+
+ public String getAction() {
+ return "describe";
+ }
+
+ @Override
+ protected void execute(MutinyServerReflectionGrpc.MutinyServerReflectionStub stub) {
+ ServerReflectionRequest request = ServerReflectionRequest
+ .newBuilder()
+ .setFileContainingSymbol(unmatched.get(1))
+ .build();
+ Multi response = stub.serverReflectionInfo(Multi.createFrom().item(request));
+ response.toUni().map(r -> {
+ List list = r.getFileDescriptorResponse().getFileDescriptorProtoList();
+ for (ByteString bs : list) {
+ try {
+ DescriptorProtos.FileDescriptorProto fdp = DescriptorProtos.FileDescriptorProto.parseFrom(bs);
+ log(JsonFormat.printer().print(fdp));
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return null;
+ }).await().indefinitely();
+ }
+}
diff --git a/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/GcurlBaseCommand.java b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/GcurlBaseCommand.java
new file mode 100644
index 00000000000000..51be27f15269da
--- /dev/null
+++ b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/GcurlBaseCommand.java
@@ -0,0 +1,132 @@
+package io.quarkus.grpc.cli;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.DescriptorProtos;
+import com.google.protobuf.Descriptors;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.ProtocolStringList;
+
+import io.grpc.Channel;
+import io.grpc.reflection.v1.MutinyServerReflectionGrpc;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.HttpClientOptions;
+import io.vertx.core.net.SocketAddress;
+import io.vertx.grpc.client.GrpcClient;
+import io.vertx.grpc.client.GrpcClientChannel;
+import picocli.CommandLine;
+import picocli.CommandLine.Model.CommandSpec;
+import picocli.CommandLine.Spec;
+
+public abstract class GcurlBaseCommand implements Callable {
+
+ @Spec
+ protected CommandSpec spec;
+
+ @CommandLine.Unmatched
+ protected List unmatched;
+
+ Vertx vertx = Vertx.vertx();
+
+ /**
+ * The grpc subcommand (e.g. list, describe, invoke)
+ *
+ * @return the subcommand
+ */
+ protected abstract String getAction();
+
+ protected abstract void execute(MutinyServerReflectionGrpc.MutinyServerReflectionStub stub);
+
+ protected void log(String msg) {
+ System.out.println(msg);
+ }
+
+ protected void err(String msg) {
+ System.err.println(msg);
+ }
+
+ protected static List getFileDescriptorsFromProtos(List protos) {
+ try {
+ Map all = protos
+ .stream()
+ .map(bs -> {
+ try {
+ return DescriptorProtos.FileDescriptorProto.parseFrom(bs);
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .collect(Collectors.toMap(DescriptorProtos.FileDescriptorProto::getName, Function.identity(), (a, b) -> a));
+ List fds = new ArrayList<>();
+ Map resolved = new HashMap<>();
+ for (DescriptorProtos.FileDescriptorProto fdp : all.values()) {
+ fds.add(toFileDescriptor(fdp, all, resolved));
+ }
+ return fds;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static Descriptors.FileDescriptor toFileDescriptor(DescriptorProtos.FileDescriptorProto fdp,
+ Map all, Map resolved) {
+ int n = fdp.getDependencyCount();
+ ProtocolStringList list = fdp.getDependencyList();
+ Descriptors.FileDescriptor[] fds = new Descriptors.FileDescriptor[n];
+ for (int i = 0; i < n; i++) {
+ String dep = list.get(i);
+ // remember resolved FDs, recursively resolve deps
+ fds[i] = resolved.computeIfAbsent(dep, key -> {
+ DescriptorProtos.FileDescriptorProto proto = all.get(key);
+ return toFileDescriptor(proto, all, resolved);
+ });
+ }
+ try {
+ return Descriptors.FileDescriptor.buildFrom(fdp, fds);
+ } catch (Descriptors.DescriptorValidationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public Integer call() {
+ if (unmatched == null || unmatched.isEmpty()) {
+ log("Missing host:port");
+ return CommandLine.ExitCode.USAGE;
+ }
+
+ return execute(channel -> {
+ try {
+ MutinyServerReflectionGrpc.MutinyServerReflectionStub stub = MutinyServerReflectionGrpc.newMutinyStub(channel);
+ execute(stub);
+ return CommandLine.ExitCode.OK;
+ } catch (Exception e) {
+ err("Failed to execute grpc " + getAction() + ", due to: " + e.getMessage());
+ return CommandLine.ExitCode.SOFTWARE;
+ }
+ });
+ }
+
+ protected X execute(Function fn) {
+ HttpClientOptions options = new HttpClientOptions(); // TODO
+ options.setHttp2ClearTextUpgrade(false);
+
+ GrpcClient client = GrpcClient.client(vertx, options);
+ String[] split = unmatched.get(0).split(":");
+ String host = split[0];
+ int port = Integer.parseInt(split[1]);
+ Channel channel = new GrpcClientChannel(client, SocketAddress.inetSocketAddress(port, host));
+ try {
+ return fn.apply(channel);
+ } finally {
+ client.close().toCompletionStage().toCompletableFuture().join();
+ }
+ }
+}
diff --git a/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/GrpcCommand.java b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/GrpcCommand.java
new file mode 100644
index 00000000000000..18d5b16348eb67
--- /dev/null
+++ b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/GrpcCommand.java
@@ -0,0 +1,27 @@
+package io.quarkus.grpc.cli;
+
+import java.util.concurrent.Callable;
+
+import io.quarkus.picocli.runtime.annotations.TopCommand;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Model.CommandSpec;
+import picocli.CommandLine.Spec;
+
+@TopCommand
+@Command(name = "grpc", sortOptions = false, header = "grpc CLI", subcommands = {
+ ListCommand.class, DescribeCommand.class, InvokeCommand.class })
+public class GrpcCommand implements Callable {
+
+ @Spec
+ protected CommandSpec spec;
+
+ @CommandLine.Option(names = { "-h", "--help" }, usageHelp = true, description = "Display this help message.")
+ public boolean help;
+
+ @Override
+ public Integer call() {
+ CommandLine schemaCommand = spec.subcommands().get("list");
+ return schemaCommand.execute();
+ }
+}
\ No newline at end of file
diff --git a/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/InvokeCommand.java b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/InvokeCommand.java
new file mode 100644
index 00000000000000..08b22962964965
--- /dev/null
+++ b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/InvokeCommand.java
@@ -0,0 +1,120 @@
+package io.quarkus.grpc.cli;
+
+import java.util.List;
+import java.util.Optional;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.Descriptors;
+import com.google.protobuf.DynamicMessage;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.util.JsonFormat;
+
+import io.grpc.CallOptions;
+import io.grpc.MethodDescriptor;
+import io.grpc.protobuf.ProtoUtils;
+import io.grpc.reflection.v1.MutinyServerReflectionGrpc;
+import io.grpc.reflection.v1.ServerReflectionRequest;
+import io.grpc.reflection.v1.ServerReflectionResponse;
+import io.grpc.stub.ClientCalls;
+import io.smallrye.mutiny.Multi;
+import io.smallrye.mutiny.infrastructure.Infrastructure;
+import picocli.CommandLine;
+
+@CommandLine.Command(name = "invoke", sortOptions = false, header = "grpc invoke")
+public class InvokeCommand extends GcurlBaseCommand {
+
+ @CommandLine.Option(names = { "-d" }, description = "Request input")
+ Optional content;
+
+ public String getAction() {
+ return "invoke";
+ }
+
+ @Override
+ protected void execute(MutinyServerReflectionGrpc.MutinyServerReflectionStub stub) {
+ String toInvoke = unmatched.get(1);
+ String[] split = toInvoke.split("/");
+ String serviceName = split[0];
+ String methodName = split[1];
+ ServerReflectionRequest request = ServerReflectionRequest
+ .newBuilder()
+ .setFileContainingSymbol(serviceName)
+ .build();
+ Multi response = stub.serverReflectionInfo(Multi.createFrom().item(request));
+ response.emitOn(Infrastructure.getDefaultWorkerPool()).toUni().map(r -> {
+ ServerReflectionResponse.MessageResponseCase responseCase = r.getMessageResponseCase();
+ if (responseCase == ServerReflectionResponse.MessageResponseCase.FILE_DESCRIPTOR_RESPONSE) {
+ List byteStrings = r.getFileDescriptorResponse().getFileDescriptorProtoList();
+ for (Descriptors.FileDescriptor fd : getFileDescriptorsFromProtos(byteStrings)) {
+ fd.getServices().forEach(
+ sd -> {
+ String fullName = sd.getFullName();
+ if (fullName.equals(serviceName)) {
+ Descriptors.MethodDescriptor md = sd.findMethodByName(methodName);
+ if (md != null) {
+ invokeMethod(md);
+ } else {
+ log("Method not found: " + methodName);
+ }
+ }
+ });
+ }
+ } else {
+ err("Unexpected response from server reflection: " + responseCase);
+ }
+ return null;
+ }).await().indefinitely();
+ }
+
+ private void invokeMethod(Descriptors.MethodDescriptor md) {
+ String fullMethodName = md.getService().getFullName() + "/" + md.getName();
+ log("Invoking method: " + fullMethodName);
+ Descriptors.Descriptor inputType = md.getInputType();
+ DynamicMessage.Builder messageBuilder = DynamicMessage.newBuilder(inputType);
+ try {
+ content.ifPresent(request -> {
+ try {
+ JsonFormat.parser().merge(request, messageBuilder);
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ DynamicMessage msg = messageBuilder.build();
+ MethodDescriptor.MethodType methodType = MethodDescriptor.MethodType.UNARY;
+ if (md.isClientStreaming()) {
+ methodType = MethodDescriptor.MethodType.CLIENT_STREAMING;
+ }
+ if (md.isServerStreaming()) {
+ methodType = MethodDescriptor.MethodType.SERVER_STREAMING;
+ }
+ if (md.isClientStreaming() && md.isServerStreaming()) {
+ methodType = MethodDescriptor.MethodType.BIDI_STREAMING;
+ }
+ MethodDescriptor methodDescriptor = io.grpc.MethodDescriptor
+ . newBuilder()
+ .setType(methodType)
+ .setFullMethodName(fullMethodName)
+ .setRequestMarshaller(ProtoUtils.marshaller(DynamicMessage.getDefaultInstance(inputType)))
+ .setResponseMarshaller(
+ ProtoUtils.marshaller(DynamicMessage.getDefaultInstance(md.getOutputType())))
+ .build();
+
+ execute(channel -> {
+ DynamicMessage response = ClientCalls.blockingUnaryCall(
+ channel,
+ methodDescriptor,
+ CallOptions.DEFAULT,
+ msg);
+
+ try {
+ log(JsonFormat.printer().print(response));
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ return null;
+ });
+ } catch (Exception e) {
+ err("Error creating dynamic message: " + e.getMessage());
+ }
+ }
+}
diff --git a/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/ListCommand.java b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/ListCommand.java
new file mode 100644
index 00000000000000..2999fdb9b08fa4
--- /dev/null
+++ b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/ListCommand.java
@@ -0,0 +1,34 @@
+package io.quarkus.grpc.cli;
+
+import java.util.List;
+
+import io.grpc.reflection.v1.MutinyServerReflectionGrpc;
+import io.grpc.reflection.v1.ServerReflectionRequest;
+import io.grpc.reflection.v1.ServerReflectionResponse;
+import io.grpc.reflection.v1.ServiceResponse;
+import io.smallrye.mutiny.Multi;
+import picocli.CommandLine;
+
+@CommandLine.Command(name = "list", sortOptions = false, header = "grpc list")
+public class ListCommand extends GcurlBaseCommand {
+
+ public String getAction() {
+ return "list";
+ }
+
+ @Override
+ protected void execute(MutinyServerReflectionGrpc.MutinyServerReflectionStub stub) {
+ ServerReflectionRequest request = ServerReflectionRequest
+ .newBuilder()
+ .setListServices("dummy")
+ .build();
+ Multi response = stub.serverReflectionInfo(Multi.createFrom().item(request));
+ response.toUni().map(r -> {
+ List serviceList = r.getListServicesResponse().getServiceList();
+ serviceList.forEach(sr -> {
+ log(sr.getName());
+ });
+ return null;
+ }).await().indefinitely();
+ }
+}
diff --git a/extensions/grpc/cli/src/main/resources/application.properties b/extensions/grpc/cli/src/main/resources/application.properties
new file mode 100644
index 00000000000000..d76bd2c707c29e
--- /dev/null
+++ b/extensions/grpc/cli/src/main/resources/application.properties
@@ -0,0 +1,4 @@
+quarkus.log.level=INFO
+quarkus.banner.enabled=false
+quarkus.package.jar.type=uber-jar
+quarkus.package.jar.add-runner-suffix=false
\ No newline at end of file
diff --git a/extensions/grpc/pom.xml b/extensions/grpc/pom.xml
index da3eb3e1eed285..32c150f5b3c140 100644
--- a/extensions/grpc/pom.xml
+++ b/extensions/grpc/pom.xml
@@ -18,9 +18,11 @@
codegen
api
stubs
+ reflection
deployment
runtime
xds
inprocess
+ cli
\ No newline at end of file
diff --git a/extensions/grpc/reflection/pom.xml b/extensions/grpc/reflection/pom.xml
new file mode 100644
index 00000000000000..c8e6d9fac8e619
--- /dev/null
+++ b/extensions/grpc/reflection/pom.xml
@@ -0,0 +1,75 @@
+
+
+ 4.0.0
+
+ quarkus-grpc-parent
+ io.quarkus
+ 999-SNAPSHOT
+
+
+ quarkus-grpc-reflection
+ Quarkus - gRPC - Reflection
+ Reflection gRPC services and utils
+
+
+ io.quarkus
+ quarkus-grpc-api
+
+
+ io.quarkus
+ quarkus-grpc-stubs
+
+
+ *
+ *
+
+
+
+
+ io.grpc
+ grpc-protobuf
+
+
+ org.codehaus.mojo
+ animal-sniffer-annotations
+
+
+ org.checkerframework
+ checker-qual
+
+
+ com.google.guava
+ guava
+
+
+ com.google.errorprone
+ error_prone_annotations
+
+
+
+
+ io.grpc
+ grpc-stub
+
+
+ com.google.code.findbugs
+ jsr305
+
+
+ org.codehaus.mojo
+ animal-sniffer-annotations
+
+
+ org.checkerframework
+ checker-qual
+
+
+
+
+ io.quarkus
+ quarkus-mutiny
+
+
+
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/GrpcServerIndex.java b/extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/GrpcServerIndex.java
similarity index 99%
rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/GrpcServerIndex.java
rename to extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/GrpcServerIndex.java
index 5c1dece314e527..1786106fb1be46 100644
--- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/GrpcServerIndex.java
+++ b/extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/GrpcServerIndex.java
@@ -1,4 +1,4 @@
-package io.quarkus.grpc.runtime.reflection;
+package io.quarkus.grpc.reflection.service;
import static com.google.protobuf.Descriptors.FileDescriptor;
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionServiceV1.java b/extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/ReflectionServiceV1.java
similarity index 99%
rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionServiceV1.java
rename to extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/ReflectionServiceV1.java
index 0f57ad617c4f01..1c668087de500d 100644
--- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionServiceV1.java
+++ b/extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/ReflectionServiceV1.java
@@ -1,4 +1,4 @@
-package io.quarkus.grpc.runtime.reflection;
+package io.quarkus.grpc.reflection.service;
import static com.google.protobuf.Descriptors.FileDescriptor;
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionServiceV1alpha.java b/extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/ReflectionServiceV1alpha.java
similarity index 99%
rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionServiceV1alpha.java
rename to extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/ReflectionServiceV1alpha.java
index 1caafa1a4425bf..a590fdfe2a95a1 100644
--- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionServiceV1alpha.java
+++ b/extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/ReflectionServiceV1alpha.java
@@ -1,4 +1,4 @@
-package io.quarkus.grpc.runtime.reflection;
+package io.quarkus.grpc.reflection.service;
import static com.google.protobuf.Descriptors.FileDescriptor;
diff --git a/extensions/grpc/runtime/pom.xml b/extensions/grpc/runtime/pom.xml
index 73393d614ff219..35442249814cc2 100644
--- a/extensions/grpc/runtime/pom.xml
+++ b/extensions/grpc/runtime/pom.xml
@@ -25,6 +25,10 @@
io.quarkus
quarkus-grpc-common
+
+ io.quarkus
+ quarkus-grpc-reflection
+
io.quarkus
quarkus-vertx-http
@@ -121,6 +125,12 @@
+
+
+ src/main/resources
+ true
+
+
io.quarkus
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java
index 322cf5eb18db7c..dfcbd00be93d55 100644
--- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java
@@ -42,6 +42,8 @@
import io.quarkus.arc.InstanceHandle;
import io.quarkus.arc.Subclass;
import io.quarkus.grpc.auth.GrpcSecurityInterceptor;
+import io.quarkus.grpc.reflection.service.ReflectionServiceV1;
+import io.quarkus.grpc.reflection.service.ReflectionServiceV1alpha;
import io.quarkus.grpc.runtime.config.GrpcConfiguration;
import io.quarkus.grpc.runtime.config.GrpcServerConfiguration;
import io.quarkus.grpc.runtime.config.GrpcServerNettyConfig;
@@ -49,8 +51,6 @@
import io.quarkus.grpc.runtime.devmode.GrpcHotReplacementInterceptor;
import io.quarkus.grpc.runtime.devmode.GrpcServerReloader;
import io.quarkus.grpc.runtime.health.GrpcHealthStorage;
-import io.quarkus.grpc.runtime.reflection.ReflectionServiceV1;
-import io.quarkus.grpc.runtime.reflection.ReflectionServiceV1alpha;
import io.quarkus.grpc.runtime.supports.CompressionInterceptor;
import io.quarkus.grpc.runtime.supports.blocking.BlockingServerInterceptor;
import io.quarkus.grpc.spi.GrpcBuilderProvider;
diff --git a/extensions/grpc/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/grpc/runtime/src/main/resources/META-INF/quarkus-extension.yaml
index 2a2f8152e8fe75..ad0bc59f0e4ce7 100644
--- a/extensions/grpc/runtime/src/main/resources/META-INF/quarkus-extension.yaml
+++ b/extensions/grpc/runtime/src/main/resources/META-INF/quarkus-extension.yaml
@@ -16,3 +16,5 @@ metadata:
artifact: "io.quarkus:quarkus-project-core-extension-codestarts"
config:
- "quarkus.grpc."
+ cli-plugins:
+ - "${project.groupId}:quarkus-grpc-cli:${project.version}"
\ No newline at end of file
diff --git a/integration-tests/grpc-vertx/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldNewService.java b/integration-tests/grpc-vertx/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldNewService.java
index d41dcca08d0401..ea5d5d38ddf6e3 100644
--- a/integration-tests/grpc-vertx/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldNewService.java
+++ b/integration-tests/grpc-vertx/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldNewService.java
@@ -1,5 +1,7 @@
package io.quarkus.grpc.examples.hello;
+import com.google.protobuf.Empty;
+
import examples.HelloReply;
import examples.HelloRequest;
import examples.MutinyGreeterGrpc;
@@ -16,6 +18,12 @@ public Uni sayHello(HelloRequest request) {
.map(res -> HelloReply.newBuilder().setMessage(res).build());
}
+ @Override
+ public Uni sayJo(Empty request) {
+ return Uni.createFrom().item("Jo!")
+ .map(res -> HelloReply.newBuilder().setMessage(res).build());
+ }
+
@Override
public Uni threadName(HelloRequest request) {
return Uni.createFrom().item(Thread.currentThread().getName())
diff --git a/integration-tests/grpc-vertx/src/main/proto/helloworld.proto b/integration-tests/grpc-vertx/src/main/proto/helloworld.proto
index c798d111640b48..6ae0904fb041d8 100644
--- a/integration-tests/grpc-vertx/src/main/proto/helloworld.proto
+++ b/integration-tests/grpc-vertx/src/main/proto/helloworld.proto
@@ -36,10 +36,14 @@ option objc_class_prefix = "HLW";
package helloworld;
+// Import the empty message definition
+import "google/protobuf/empty.proto";
+
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
+ rpc SayJo (google.protobuf.Empty) returns (HelloReply) {}
rpc ThreadName (HelloRequest) returns (HelloReply) {}
}