Let me re-establish somethings just to make sure we are on the same page.
1. Servers are always async. Server Stubs are always passed a Response observer. For Bidi and Client Streaming, they also return a Request Observer.
2. Servers are usually on the receiving end of cancellation. Servers find out about cancellation by inspecting the Context.current().isCancelled() value.
3. Servers can also check for early termination of the RPC for Bidi and ClientStreaming by getting onError() called on the Request observer.
4. Clients usually don't check for cancellation. Client's find out the RPC is prematurely terminated by getting an onError() callback with a StatusRuntimeException or StatusException with a status Code. From the client's POV, server cancellation and regular failure look the same.
5. Clients using the blocking API usually use Context cancellation to end the call early. (because there is no other way)
6. A blocking client will take the current context, wrap a cancellable context around it, and install the new ctx back into the current. (This was the attach() suggestion I made above). When the RPC is complete, they swap the old context back into place.
7. ClientResponseObserver is probably not what you want here. It's meant to modify flow control on the client stub. It's rarely used.
With these in mind, It seems like your server should just do responseObserver.onError(Status.CANCELLED.withDescription("Server is cancelling").asRuntimeException()); The client be notified by the onError() invocation on it's response observer. (I think you said it's bidi, so this would be the observer "returned" from your client stub.
Does this make sense?