Kora grpc client
Skill kora-projects/kora-skills/plugins/kora-v1/skills/kora-grpc-client
Builds gRPC clients in Kora via GrpcClientModule, the protobuf Gradle plugin, and generated stubs injected directly into components. Covers grpcClient.<ServiceName> HOCON/YAML config, plaintext vs TLS through the URL scheme, custom ClientInterceptor scoped with @Tag(ServiceGrpc.class) for metadata auth and logging, and unary plus server/client/bidirectional streaming with blocking and async stubs. Use when wiring an outbound gRPC call, injecting a *BlockingStub / *FutureStub / *Stub, adding authorization headers via gRPC metadata, configuring keepAliveTime/timeout/loadBalancingPolicy, or debugging UNAVAILABLE/UNAUTHENTICATED and missing-stub graph errors.From its SKILL.md
npx -y skills add kora-projects/kora-skills --skill kora-grpc-clientAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
- runs commandsInstructs the agent to run 1 command, including `run `./gradlew classes``.
SKILL.md
9.6 KB, ~2.2k tokens by cl100k_base, as published. Nobody here has run it
Kora gRPC Client
Generate gRPC client stubs from a .proto contract and inject them as components through GrpcClientModule. Kora wires the configured channel and the generated stubs into the application graph; your code just builds protobuf requests and calls stub methods.
Read this first when:
- enabling
GrpcClientModuleand injecting a generated*BlockingStub/*FutureStub/*Stub, - configuring a client under
grpcClient.<ServiceName>.*, - adding a
ClientInterceptorfor metadata headers, auth, or logging, - making unary or streaming calls.
Key facts (do not get these wrong)
- Stubs are injected directly by type — no
@Tagon the constructor parameter. Kora produces one stub per generated*Grpcclass. InjectingUserServiceGrpc.UserServiceBlockingStubis enough. @Tag(ServiceGrpc.class)belongs on aClientInterceptor, to scope that interceptor to one service's client. It is not used for stub injection.- Annotations come from
ru.tinkoff.kora.common.*(@Component,@Tag,@KoraApp), not from anyannotation.processor.*package. - Plaintext vs TLS is chosen by the URL scheme (
http://= plaintext,https:///grpc://per transport). There is nousePlaintextconfig key. - The mandatory annotation processor must be present: Java
annotationProcessor "ru.tinkoff.kora:annotation-processors", Kotlinksp "ru.tinkoff.kora:symbol-processors".
Quick Start
1. Dependencies
Pin the BOM (kora-parent); never version individual ru.tinkoff.kora:* artifacts.
plugins {
id "application"
id "com.google.protobuf" version "0.9.4"
}
configurations {
koraBom
annotationProcessor.extendsFrom(koraBom)
implementation.extendsFrom(koraBom)
}
dependencies {
koraBom platform("ru.tinkoff.kora:kora-parent:1.2.17")
annotationProcessor "ru.tinkoff.kora:annotation-processors"
implementation "ru.tinkoff.kora:grpc-client"
implementation "ru.tinkoff.kora:config-hocon"
implementation "ru.tinkoff.kora:logging-logback"
implementation "io.grpc:grpc-protobuf:1.74.0"
compileOnly "javax.annotation:javax.annotation-api:1.3.2"
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:3.25.3" }
plugins {
grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.74.0" }
}
generateProtoTasks {
all()*.plugins { grpc {} }
}
}
sourceSets {
main {
java {
srcDirs "build/generated/source/proto/main/grpc"
srcDirs "build/generated/source/proto/main/java"
}
}
}
2. Define the protobuf service
src/main/proto/user_service.proto:
syntax = "proto3";
package ru.tinkoff.kora.example.grpc;
option java_multiple_files = true;
service UserService {
rpc GetUser (GetUserRequest) returns (UserResponse) {}
}
message GetUserRequest { string user_id = 1; }
message UserResponse {
string id = 1;
string name = 2;
string email = 3;
}
3. Enable the module
import ru.tinkoff.grpc.client.GrpcClientModule;
import ru.tinkoff.kora.application.graph.KoraApplication;
import ru.tinkoff.kora.common.KoraApp;
import ru.tinkoff.kora.config.hocon.HoconConfigModule;
import ru.tinkoff.kora.logging.logback.LogbackModule;
@KoraApp
public interface Application extends HoconConfigModule, LogbackModule, GrpcClientModule {
static void main(String[] args) {
KoraApplication.run(ApplicationGraph::graph);
}
}
4. Inject the stub and wrap it in a service
Inject the generated stub directly by type. Wrap it so protobuf builders and gRPC status handling stay at the transport boundary.
import ru.tinkoff.kora.common.Component;
import ru.tinkoff.kora.example.grpc.UserServiceGrpc;
import ru.tinkoff.kora.example.grpc.GetUserRequest;
@Component
public final class UserClientService {
private final UserServiceGrpc.UserServiceBlockingStub userService;
public UserClientService(UserServiceGrpc.UserServiceBlockingStub userService) {
this.userService = userService;
}
public UserDto getUser(String userId) {
var response = userService.getUser(GetUserRequest.newBuilder()
.setUserId(userId)
.build());
return new UserDto(response.getId(), response.getName(), response.getEmail());
}
}
5. Configure the client
A service named UserService is configured under grpcClient.UserService. Use the URL scheme to control plaintext vs TLS; externalize the URL with ${?VAR}.
grpcClient {
UserService {
url = "http://localhost:8090" // http:// => plaintext for local
url = ${?GRPC_SERVER_URL}
timeout = "10s"
telemetry.logging.enabled = true
}
}
Stub types
| Stub | Use for |
|---|---|
*BlockingStub | Unary calls and server-streaming reads (returns Iterator<Response>) |
*FutureStub | Unary calls returning ListenableFuture<Response> |
*Stub (async) | Client-streaming and bidirectional streaming via StreamObserver |
A service may inject several stub types at once (e.g. a *BlockingStub for reads and a *Stub for client streaming). See grpc-client-stubs-reference.md.
Client interceptors
Register a ClientInterceptor as a @Component and scope it to one service with @Tag(ServiceGrpc.class). Kora applies it to that service's channel automatically; it does not change stub injection.
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import ru.tinkoff.kora.common.Component;
import ru.tinkoff.kora.common.Tag;
import ru.tinkoff.kora.example.grpc.UserServiceGrpc;
@Tag(UserServiceGrpc.class)
@Component
public final class AuthInterceptor implements ClientInterceptor {
private static final Metadata.Key<String> AUTHORIZATION =
Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
private final AuthConfig authConfig;
public AuthInterceptor(AuthConfig authConfig) {
this.authConfig = authConfig;
}
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
return new ForwardingClientCall.SimpleForwardingClientCall<>(next.newCall(method, callOptions)) {
@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
headers.put(AUTHORIZATION, authConfig.value());
super.start(responseListener, headers);
}
};
}
}
The default GrpcClientConfigInterceptor (applies grpcClient.* config) is always present. See grpc-client-interceptors-reference.md for logging, metadata, and GraphInterceptor patterns.
Streaming
Server streaming can be consumed with a *BlockingStub (returns an Iterator) or asynchronously with a *Stub. Client and bidirectional streaming require the async *Stub and a StreamObserver. See grpc-client-streaming-reference.md.
Configuration
grpcClient.<ServiceName>.* keys: url (required), timeout, keepAliveTime, keepAliveTimeout, loadBalancingPolicy, maxInboundMessageSize, and telemetry.{logging,metrics,tracing}. Full reference (HOCON + YAML, defaults, env substitution): grpc-client-config-reference.md.
Common pitfalls
| Symptom | Fix |
|---|---|
Required dependency not found: ...BlockingStub | Enable GrpcClientModule on @KoraApp; run ./gradlew classes so protobuf + processors run |
| Stub class does not exist | Run protobuf generation and add the build/generated/source/proto/... source dirs to sourceSets |
Tried to add @Tag(...) on a stub parameter | Remove it — stubs inject by type; @Tag is only for interceptors |
UNAVAILABLE | Check grpcClient.<ServiceName>.url host/port and that the server is up |
UNAUTHENTICATED | Add an AuthInterceptor (above) putting credentials into Metadata |
| Interceptor never runs | Ensure @Component + @Tag(ServiceGrpc.class) matches the generated class exactly |
| Wanted plaintext but got TLS | Use an http:// URL; there is no usePlaintext key |
References & assets
| File | Purpose |
|---|---|
| references/grpc-client-stubs-reference.md | Stub types, direct injection, service wrappers |
| references/grpc-client-config-reference.md | Full config + protobuf plugin setup |
| references/grpc-client-interceptors-reference.md | Interceptor patterns, metadata, GraphInterceptor |
| references/grpc-client-streaming-reference.md | Server/client/bidirectional streaming |
| assets/README.md | Template catalog and usage |
What ships with it: 24 files
43.5 KB alongside SKILL.md
assets/
- application.client.conf.template516 B
- Application.client.java.template475 B
- Application.client.kt.template419 B
- application.server.conf.template416 B
- Application.server.java.template480 B
- Application.server.kt.template424 B
- build.gradle.client.kt.template1.2 KB
- build.gradle.client.template1.2 KB
- build.gradle.server.kt.template1.3 KB
- build.gradle.server.template1.3 KB
- client-interceptor.client.java.template1.2 KB
- client-interceptor.client.kt.template1.2 KB
- client-wrapper.client.java.template804 B
- client-wrapper.client.kt.template678 B
- server-interceptor.server.java.template780 B
- server-interceptor.server.kt.template734 B
- service-impl.server.java.template856 B
- service-impl.server.kt.template1.1 KB
- service.proto.template560 B
evals/
- evals.json5.9 KB