diff --git a/tools/distrib/python/grpcio_tools/grpc/tools/protoc_compiler.pyx b/tools/distrib/python/grpcio_tools/grpc/tools/_protoc_compiler.pyx
similarity index 100%
rename from tools/distrib/python/grpcio_tools/grpc/tools/protoc_compiler.pyx
rename to tools/distrib/python/grpcio_tools/grpc/tools/_protoc_compiler.pyx
diff --git a/tools/distrib/python/grpcio_tools/grpc/tools/command.py b/tools/distrib/python/grpcio_tools/grpc/tools/command.py
new file mode 100644
index 0000000000000000000000000000000000000000..ccf38b7d569e066f314b057214903356f144a84b
--- /dev/null
+++ b/tools/distrib/python/grpcio_tools/grpc/tools/command.py
@@ -0,0 +1,70 @@
+# Copyright 2016, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import sys
+
+import setuptools
+
+from grpc.tools import protoc
+
+
+class BuildProtoModules(setuptools.Command):
+  """Command to generate project *_pb2.py modules from proto files."""
+
+  description = 'build grpc protobuf modules'
+  user_options = []
+
+  def initialize_options(self):
+    pass
+
+  def finalize_options(self):
+    pass
+
+  def run(self):
+    # due to limitations of the proto generator, we require that only *one*
+    # directory is provided as an 'include' directory. We assume it's the '' key
+    # to `self.distribution.package_dir` (and get a key error if it's not
+    # there).
+    proto_files = []
+    inclusion_root = os.path.abspath(self.distribution.package_dir[''])
+    for root, _, files in os.walk(inclusion_root):
+      for filename in files:
+        if filename.endswith('.proto'):
+          proto_files.append(os.path.abspath(os.path.join(root, filename)))
+
+    for proto_file in proto_files:
+      command = [
+          'grpc.tools.protoc',
+          '--proto_path={}'.format(inclusion_root),
+          '--python_out={}'.format(inclusion_root),
+          '--grpc_python_out={}'.format(inclusion_root),
+      ] + [proto_file]
+      if protoc.main(command) != 0:
+        sys.stderr.write('warning: {} failed'.format(command))
diff --git a/tools/distrib/python/grpcio_tools/grpc/tools/protoc.py b/tools/distrib/python/grpcio_tools/grpc/tools/protoc.py
index 1c69e78920c65da7d1193011fa5aa31d37e7dc96..931fda27d263442da9c1f66f45ba215cafae5ff7 100644
--- a/tools/distrib/python/grpcio_tools/grpc/tools/protoc.py
+++ b/tools/distrib/python/grpcio_tools/grpc/tools/protoc.py
@@ -32,10 +32,17 @@
 import pkg_resources
 import sys
 
-from grpc.tools import protoc_compiler
+from grpc.tools import _protoc_compiler
 
+def main(command_arguments):
+  """Run the protocol buffer compiler with the given command-line arguments.
+
+  Args:
+    command_arguments: a list of strings representing command line arguments to
+        `protoc`.
+  """
+  return _protoc_compiler.run_main(command_arguments)
 
 if __name__ == '__main__':
   proto_include = pkg_resources.resource_filename('grpc.tools', '_proto')
-  protoc_compiler.run_main(
-      sys.argv + ['-I{}'.format(proto_include)])
+  sys.exit(main(sys.argv + ['-I{}'.format(proto_include)]))
diff --git a/tools/distrib/python/grpcio_tools/setup.py b/tools/distrib/python/grpcio_tools/setup.py
index f9cb0534d54294c080f62a6522fd626ff4b8d894..afb6063906e7a6e3f146d6c99262000b973eb94a 100644
--- a/tools/distrib/python/grpcio_tools/setup.py
+++ b/tools/distrib/python/grpcio_tools/setup.py
@@ -99,8 +99,8 @@ def protoc_ext_module():
       os.path.join(protoc_lib_deps.CC_INCLUDE, cc_file)
       for cc_file in protoc_lib_deps.CC_FILES]
   plugin_ext = extension.Extension(
-      name='grpc.tools.protoc_compiler',
-      sources=['grpc/tools/protoc_compiler.pyx'] + plugin_sources,
+      name='grpc.tools._protoc_compiler',
+      sources=['grpc/tools/_protoc_compiler.pyx'] + plugin_sources,
       include_dirs=[
           '.',
           'grpc_root',