diff --git a/build-py-grpc.sh b/build-py-grpc.sh
index a22be1f3d7e11240ea30f6c7453d7f6a4140e808..4285224aed206b97cc46ebe8d96154628a45d109 100755
--- a/build-py-grpc.sh
+++ b/build-py-grpc.sh
@@ -5,7 +5,7 @@ echo "Packaging python tinode-grpc..."
 pushd ./pbx > /dev/null
 
 # Generate grpc bindings from the proto file.
-./generate-python.sh v=3
+./py-generate.sh v=3
 
 pushd ../py_grpc > /dev/null
 
diff --git a/py_grpc/README.md b/py_grpc/README.md
index 57f123fcb05f3d0fc72a539c0075a7797a2e81e6..a26e08b9e6d98241ac9ad2b3ef8ad1a36c101dc9 100644
--- a/py_grpc/README.md
+++ b/py_grpc/README.md
@@ -17,7 +17,7 @@ pip install tinode_grpc
 
 ## Generating files
 
-Don't modify included files directly. If you want to make changes, you have to install protobuffers tool chain and gRPC the generate the Python bindings from [`pbx/model.proto`](https://github.com/tinode/chat/tree/master/pbx/model.proto) (your path to `model.proto` may be different):
+Don't modify included files directly. If you want to make changes, you have to install protobuffers tool chain and gRPC then generate the Python bindings from [`pbx/model.proto`](https://github.com/tinode/chat/tree/master/pbx/model.proto) (your path to `model.proto` may be different):
 ```
 python -m grpc_tools.protoc -I../pbx --python_out=. --grpc_python_out=. ../pbx/model.proto
 ```
diff --git a/py_grpc/tinode_grpc/model_pb2.py b/py_grpc/tinode_grpc/model_pb2.py
index 3ae626af86804dbad2c92ba7413da62504e364e9..0488db812f1ed9057cd59b687c3d40b1d10b68d7 100644
Binary files a/py_grpc/tinode_grpc/model_pb2.py and b/py_grpc/tinode_grpc/model_pb2.py differ
diff --git a/py_grpc/tinode_grpc/model_pb2_grpc.py b/py_grpc/tinode_grpc/model_pb2_grpc.py
index 927249812e9907a0148b00eedd5b0777d8d2af61..ff913671a374de01598f2c46ae85fa2414eec349 100644
--- a/py_grpc/tinode_grpc/model_pb2_grpc.py
+++ b/py_grpc/tinode_grpc/model_pb2_grpc.py
@@ -1,180 +1,312 @@
 # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
 import grpc
 
 from . import model_pb2 as model__pb2
 
 
 class NodeStub(object):
-  """This is the single method that needs to be implemented by a gRPC client.
-  """
+    """This is the single method that needs to be implemented by a gRPC client.
+    """
 
-  def __init__(self, channel):
-    """Constructor.
+    def __init__(self, channel):
+        """Constructor.
 
-    Args:
-      channel: A grpc.Channel.
-    """
-    self.MessageLoop = channel.stream_stream(
-        '/pbx.Node/MessageLoop',
-        request_serializer=model__pb2.ClientMsg.SerializeToString,
-        response_deserializer=model__pb2.ServerMsg.FromString,
-        )
+        Args:
+            channel: A grpc.Channel.
+        """
+        self.MessageLoop = channel.stream_stream(
+                '/pbx.Node/MessageLoop',
+                request_serializer=model__pb2.ClientMsg.SerializeToString,
+                response_deserializer=model__pb2.ServerMsg.FromString,
+                )
 
 
 class NodeServicer(object):
-  """This is the single method that needs to be implemented by a gRPC client.
-  """
-
-  def MessageLoop(self, request_iterator, context):
-    """Client sends a stream of ClientMsg, server responds with a stream of ServerMsg
+    """This is the single method that needs to be implemented by a gRPC client.
     """
-    context.set_code(grpc.StatusCode.UNIMPLEMENTED)
-    context.set_details('Method not implemented!')
-    raise NotImplementedError('Method not implemented!')
+
+    def MessageLoop(self, request_iterator, context):
+        """Client sends a stream of ClientMsg, server responds with a stream of ServerMsg
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
 
 
 def add_NodeServicer_to_server(servicer, server):
-  rpc_method_handlers = {
-      'MessageLoop': grpc.stream_stream_rpc_method_handler(
-          servicer.MessageLoop,
-          request_deserializer=model__pb2.ClientMsg.FromString,
-          response_serializer=model__pb2.ServerMsg.SerializeToString,
-      ),
-  }
-  generic_handler = grpc.method_handlers_generic_handler(
-      'pbx.Node', rpc_method_handlers)
-  server.add_generic_rpc_handlers((generic_handler,))
+    rpc_method_handlers = {
+            'MessageLoop': grpc.stream_stream_rpc_method_handler(
+                    servicer.MessageLoop,
+                    request_deserializer=model__pb2.ClientMsg.FromString,
+                    response_serializer=model__pb2.ServerMsg.SerializeToString,
+            ),
+    }
+    generic_handler = grpc.method_handlers_generic_handler(
+            'pbx.Node', rpc_method_handlers)
+    server.add_generic_rpc_handlers((generic_handler,))
 
 
-class PluginStub(object):
-  """Plugin interface.
-  """
+ # This class is part of an EXPERIMENTAL API.
+class Node(object):
+    """This is the single method that needs to be implemented by a gRPC client.
+    """
+
+    @staticmethod
+    def MessageLoop(request_iterator,
+            target,
+            options=(),
+            channel_credentials=None,
+            call_credentials=None,
+            insecure=False,
+            compression=None,
+            wait_for_ready=None,
+            timeout=None,
+            metadata=None):
+        return grpc.experimental.stream_stream(request_iterator, target, '/pbx.Node/MessageLoop',
+            model__pb2.ClientMsg.SerializeToString,
+            model__pb2.ServerMsg.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
 
-  def __init__(self, channel):
-    """Constructor.
 
-    Args:
-      channel: A grpc.Channel.
+class PluginStub(object):
+    """Plugin interface.
     """
-    self.FireHose = channel.unary_unary(
-        '/pbx.Plugin/FireHose',
-        request_serializer=model__pb2.ClientReq.SerializeToString,
-        response_deserializer=model__pb2.ServerResp.FromString,
-        )
-    self.Find = channel.unary_unary(
-        '/pbx.Plugin/Find',
-        request_serializer=model__pb2.SearchQuery.SerializeToString,
-        response_deserializer=model__pb2.SearchFound.FromString,
-        )
-    self.Account = channel.unary_unary(
-        '/pbx.Plugin/Account',
-        request_serializer=model__pb2.AccountEvent.SerializeToString,
-        response_deserializer=model__pb2.Unused.FromString,
-        )
-    self.Topic = channel.unary_unary(
-        '/pbx.Plugin/Topic',
-        request_serializer=model__pb2.TopicEvent.SerializeToString,
-        response_deserializer=model__pb2.Unused.FromString,
-        )
-    self.Subscription = channel.unary_unary(
-        '/pbx.Plugin/Subscription',
-        request_serializer=model__pb2.SubscriptionEvent.SerializeToString,
-        response_deserializer=model__pb2.Unused.FromString,
-        )
-    self.Message = channel.unary_unary(
-        '/pbx.Plugin/Message',
-        request_serializer=model__pb2.MessageEvent.SerializeToString,
-        response_deserializer=model__pb2.Unused.FromString,
-        )
+
+    def __init__(self, channel):
+        """Constructor.
+
+        Args:
+            channel: A grpc.Channel.
+        """
+        self.FireHose = channel.unary_unary(
+                '/pbx.Plugin/FireHose',
+                request_serializer=model__pb2.ClientReq.SerializeToString,
+                response_deserializer=model__pb2.ServerResp.FromString,
+                )
+        self.Find = channel.unary_unary(
+                '/pbx.Plugin/Find',
+                request_serializer=model__pb2.SearchQuery.SerializeToString,
+                response_deserializer=model__pb2.SearchFound.FromString,
+                )
+        self.Account = channel.unary_unary(
+                '/pbx.Plugin/Account',
+                request_serializer=model__pb2.AccountEvent.SerializeToString,
+                response_deserializer=model__pb2.Unused.FromString,
+                )
+        self.Topic = channel.unary_unary(
+                '/pbx.Plugin/Topic',
+                request_serializer=model__pb2.TopicEvent.SerializeToString,
+                response_deserializer=model__pb2.Unused.FromString,
+                )
+        self.Subscription = channel.unary_unary(
+                '/pbx.Plugin/Subscription',
+                request_serializer=model__pb2.SubscriptionEvent.SerializeToString,
+                response_deserializer=model__pb2.Unused.FromString,
+                )
+        self.Message = channel.unary_unary(
+                '/pbx.Plugin/Message',
+                request_serializer=model__pb2.MessageEvent.SerializeToString,
+                response_deserializer=model__pb2.Unused.FromString,
+                )
 
 
 class PluginServicer(object):
-  """Plugin interface.
-  """
-
-  def FireHose(self, request, context):
-    """This plugin method is called by Tinode server for every message received from the clients. The
-    method returns a ServerCtrl message. Non-zero ServerCtrl.code indicates that no further
-    processing is needed. The Tinode server will generate a {ctrl} message from the returned ServerCtrl
-    and forward it to the client session.
-    ServerCtrl.code equals to 0 instructs the server to continue with default processing of the client message.
+    """Plugin interface.
     """
-    context.set_code(grpc.StatusCode.UNIMPLEMENTED)
-    context.set_details('Method not implemented!')
-    raise NotImplementedError('Method not implemented!')
 
-  def Find(self, request, context):
-    """An alteranative user and topic discovery mechanism.
-    A search request issued on a 'fnd' topic. This method is called to generate an alternative result set.
-    """
-    context.set_code(grpc.StatusCode.UNIMPLEMENTED)
-    context.set_details('Method not implemented!')
-    raise NotImplementedError('Method not implemented!')
+    def FireHose(self, request, context):
+        """This plugin method is called by Tinode server for every message received from the clients. The
+        method returns a ServerCtrl message. Non-zero ServerCtrl.code indicates that no further
+        processing is needed. The Tinode server will generate a {ctrl} message from the returned ServerCtrl
+        and forward it to the client session.
+        ServerCtrl.code equals to 0 instructs the server to continue with default processing of the client message.
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
 
-  def Account(self, request, context):
-    """The following methods are for the Tinode server to report individual events.
+    def Find(self, request, context):
+        """An alteranative user and topic discovery mechanism.
+        A search request issued on a 'fnd' topic. This method is called to generate an alternative result set.
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
 
-    Account created, updated or deleted
-    """
-    context.set_code(grpc.StatusCode.UNIMPLEMENTED)
-    context.set_details('Method not implemented!')
-    raise NotImplementedError('Method not implemented!')
+    def Account(self, request, context):
+        """The following methods are for the Tinode server to report individual events.
 
-  def Topic(self, request, context):
-    """Topic created, updated [or deleted -- not supported yet]
-    """
-    context.set_code(grpc.StatusCode.UNIMPLEMENTED)
-    context.set_details('Method not implemented!')
-    raise NotImplementedError('Method not implemented!')
+        Account created, updated or deleted
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
 
-  def Subscription(self, request, context):
-    """Subscription created, updated or deleted
-    """
-    context.set_code(grpc.StatusCode.UNIMPLEMENTED)
-    context.set_details('Method not implemented!')
-    raise NotImplementedError('Method not implemented!')
+    def Topic(self, request, context):
+        """Topic created, updated [or deleted -- not supported yet]
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
 
-  def Message(self, request, context):
-    """Message published or deleted
-    """
-    context.set_code(grpc.StatusCode.UNIMPLEMENTED)
-    context.set_details('Method not implemented!')
-    raise NotImplementedError('Method not implemented!')
+    def Subscription(self, request, context):
+        """Subscription created, updated or deleted
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
+
+    def Message(self, request, context):
+        """Message published or deleted
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
 
 
 def add_PluginServicer_to_server(servicer, server):
-  rpc_method_handlers = {
-      'FireHose': grpc.unary_unary_rpc_method_handler(
-          servicer.FireHose,
-          request_deserializer=model__pb2.ClientReq.FromString,
-          response_serializer=model__pb2.ServerResp.SerializeToString,
-      ),
-      'Find': grpc.unary_unary_rpc_method_handler(
-          servicer.Find,
-          request_deserializer=model__pb2.SearchQuery.FromString,
-          response_serializer=model__pb2.SearchFound.SerializeToString,
-      ),
-      'Account': grpc.unary_unary_rpc_method_handler(
-          servicer.Account,
-          request_deserializer=model__pb2.AccountEvent.FromString,
-          response_serializer=model__pb2.Unused.SerializeToString,
-      ),
-      'Topic': grpc.unary_unary_rpc_method_handler(
-          servicer.Topic,
-          request_deserializer=model__pb2.TopicEvent.FromString,
-          response_serializer=model__pb2.Unused.SerializeToString,
-      ),
-      'Subscription': grpc.unary_unary_rpc_method_handler(
-          servicer.Subscription,
-          request_deserializer=model__pb2.SubscriptionEvent.FromString,
-          response_serializer=model__pb2.Unused.SerializeToString,
-      ),
-      'Message': grpc.unary_unary_rpc_method_handler(
-          servicer.Message,
-          request_deserializer=model__pb2.MessageEvent.FromString,
-          response_serializer=model__pb2.Unused.SerializeToString,
-      ),
-  }
-  generic_handler = grpc.method_handlers_generic_handler(
-      'pbx.Plugin', rpc_method_handlers)
-  server.add_generic_rpc_handlers((generic_handler,))
+    rpc_method_handlers = {
+            'FireHose': grpc.unary_unary_rpc_method_handler(
+                    servicer.FireHose,
+                    request_deserializer=model__pb2.ClientReq.FromString,
+                    response_serializer=model__pb2.ServerResp.SerializeToString,
+            ),
+            'Find': grpc.unary_unary_rpc_method_handler(
+                    servicer.Find,
+                    request_deserializer=model__pb2.SearchQuery.FromString,
+                    response_serializer=model__pb2.SearchFound.SerializeToString,
+            ),
+            'Account': grpc.unary_unary_rpc_method_handler(
+                    servicer.Account,
+                    request_deserializer=model__pb2.AccountEvent.FromString,
+                    response_serializer=model__pb2.Unused.SerializeToString,
+            ),
+            'Topic': grpc.unary_unary_rpc_method_handler(
+                    servicer.Topic,
+                    request_deserializer=model__pb2.TopicEvent.FromString,
+                    response_serializer=model__pb2.Unused.SerializeToString,
+            ),
+            'Subscription': grpc.unary_unary_rpc_method_handler(
+                    servicer.Subscription,
+                    request_deserializer=model__pb2.SubscriptionEvent.FromString,
+                    response_serializer=model__pb2.Unused.SerializeToString,
+            ),
+            'Message': grpc.unary_unary_rpc_method_handler(
+                    servicer.Message,
+                    request_deserializer=model__pb2.MessageEvent.FromString,
+                    response_serializer=model__pb2.Unused.SerializeToString,
+            ),
+    }
+    generic_handler = grpc.method_handlers_generic_handler(
+            'pbx.Plugin', rpc_method_handlers)
+    server.add_generic_rpc_handlers((generic_handler,))
+
+
+ # This class is part of an EXPERIMENTAL API.
+class Plugin(object):
+    """Plugin interface.
+    """
+
+    @staticmethod
+    def FireHose(request,
+            target,
+            options=(),
+            channel_credentials=None,
+            call_credentials=None,
+            insecure=False,
+            compression=None,
+            wait_for_ready=None,
+            timeout=None,
+            metadata=None):
+        return grpc.experimental.unary_unary(request, target, '/pbx.Plugin/FireHose',
+            model__pb2.ClientReq.SerializeToString,
+            model__pb2.ServerResp.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+
+    @staticmethod
+    def Find(request,
+            target,
+            options=(),
+            channel_credentials=None,
+            call_credentials=None,
+            insecure=False,
+            compression=None,
+            wait_for_ready=None,
+            timeout=None,
+            metadata=None):
+        return grpc.experimental.unary_unary(request, target, '/pbx.Plugin/Find',
+            model__pb2.SearchQuery.SerializeToString,
+            model__pb2.SearchFound.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+
+    @staticmethod
+    def Account(request,
+            target,
+            options=(),
+            channel_credentials=None,
+            call_credentials=None,
+            insecure=False,
+            compression=None,
+            wait_for_ready=None,
+            timeout=None,
+            metadata=None):
+        return grpc.experimental.unary_unary(request, target, '/pbx.Plugin/Account',
+            model__pb2.AccountEvent.SerializeToString,
+            model__pb2.Unused.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+
+    @staticmethod
+    def Topic(request,
+            target,
+            options=(),
+            channel_credentials=None,
+            call_credentials=None,
+            insecure=False,
+            compression=None,
+            wait_for_ready=None,
+            timeout=None,
+            metadata=None):
+        return grpc.experimental.unary_unary(request, target, '/pbx.Plugin/Topic',
+            model__pb2.TopicEvent.SerializeToString,
+            model__pb2.Unused.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+
+    @staticmethod
+    def Subscription(request,
+            target,
+            options=(),
+            channel_credentials=None,
+            call_credentials=None,
+            insecure=False,
+            compression=None,
+            wait_for_ready=None,
+            timeout=None,
+            metadata=None):
+        return grpc.experimental.unary_unary(request, target, '/pbx.Plugin/Subscription',
+            model__pb2.SubscriptionEvent.SerializeToString,
+            model__pb2.Unused.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+
+    @staticmethod
+    def Message(request,
+            target,
+            options=(),
+            channel_credentials=None,
+            call_credentials=None,
+            insecure=False,
+            compression=None,
+            wait_for_ready=None,
+            timeout=None,
+            metadata=None):
+        return grpc.experimental.unary_unary(request, target, '/pbx.Plugin/Message',
+            model__pb2.MessageEvent.SerializeToString,
+            model__pb2.Unused.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)