Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
/.idea
/.idea
.envrc
.direnv
build/
*.egg-info/
*__pycache__/
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
19 changes: 19 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
PROTO_SRC = protos/example.proto
PY_OUT = .

.PHONY: all proto clean run-server run-client

all: proto

proto:
python -m grpc_tools.protoc -I=. --python_out=$(PY_OUT) --grpc_python_out=$(PY_OUT) $(PROTO_SRC)
protoc --py-mcp_out=$(PY_OUT) --pyi_out=$(PY_OUT) $(PROTO_SRC)

clean:
find . -type f -name "*_pb2.py" -o -name "*_pb2_grpc.py" -o -name "*_pb2.pyi" -o -name "*_pb2_mpc.py" | xargs rm -f

run-server:
python grpc_server.py

run-client:
python grpc_client.py
36 changes: 27 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
protoc-gen-py-mcp
-----------
# protoc-gen-py-mcp

This is a [Topeka](#topeka) plugin for the [protoc compiler](https://grpc.io/docs/protoc-installation/) that generates a [model-context-protocol(MCP)](https://modelcontextprotocol.io/introduction) server based on a [protocol buffer](https://protobuf.dev/) definition. Conceptually, this allows an AI model to use existing [gRPC](https://grpc.io/) codebases with natural language, allowing for rapid prototyping and usage of LLM capabilities for protobuf based codebases.

#### Prerequisites
## Prerequisites

- [protoc](https://grpc.io/docs/protoc-installation/) 3.20 or later
[TODO: add all prerequisites to successfully build and run the plugin]

#### Running the plugin
## Running the plugin

[TODO: add instructions to run the plugin]
```bash
# Create a virtual environment
uv venv
# Activate the virtual environment
source .venv/bin/activate
# Install the plugin
uv pip install -e .
# Generate python files from proto files
make
```

## Debugging the plugin

#### Debugging the plugin
[TODO: add instructions to debug the plugin]

#### Testing the example
## Testing the example

Install the example `mcp-vibe` server
[TODO: add instructions to run the project specific example server]
Add the `mcp-vibe` server to your mcp servers:

```json
{
"mcpServers": {
Expand All @@ -25,9 +40,11 @@ Add the `mcp-vibe` server to your mcp servers:
}
}
```

and run a client with above mcp server attached (eg. claude desktop)

#### Topeka
## Topeka

[Topeka](https://topeka.ai) is an open source project that provides code-generators for [Model-Context-Protocol (MCP)](https://modelcontextprotocol.io/introduction).
It is designed to facilitate the usage of MCP seamlessly against existing gRPC based applications. This is done via
leveraging code generation using the [protoc compiler](https://grpc.io/docs/protoc-installation/) and installing the relevant Topeka plugin.
Expand All @@ -36,7 +53,8 @@ The plugins follow [Semantic Versioning](https://semver.org/) and any plugin pri
applied to the generated servers, not the plugins themselves, which do not provide public APIs. This project reserves the right to change how code generation is achieved,
while maintaining stable MCP server APIs.

#### Maintainers
## Maintainers

[Stable Kernel](https://stablekernel.com) is the primary maintainer of this project and sponsor of the plugins, though we welcome outside contributions.

[Stable Kernel](https://stablekernel.com) is a digital transformation company building solutions that power LLM enablement for growing businesses. We have a track record of helping our partners solve their biggest challenges on their digital journey, whether they need insights or implementation. Every day, millions of people rely on software that we developed, and our custom software development and technology services have been trusted by some of the most innovative Fortune 500 companies in the world.
[Stable Kernel](https://stablekernel.com) is a digital transformation company building solutions that power LLM enablement for growing businesses. We have a track record of helping our partners solve their biggest challenges on their digital journey, whether they need insights or implementation. Every day, millions of people rely on software that we developed, and our custom software development and technology services have been trusted by some of the most innovative Fortune 500 companies in the world.
61 changes: 61 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {self, nixpkgs, flake-utils}:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
with pkgs;
{
devShells.default = mkShell {
buildInputs = [
python312
python312Packages.pip
python312Packages.virtualenv
uv
protobuf
];
# Make libraries available
LD_LIBRARY_PATH = "${stdenv.cc.cc.lib}/lib";

shellHook = ''
export LD_LIBRARY_PATH=${stdenv.cc.cc.lib}/lib:$LD_LIBRARY_PATH
'';
};
}
);
}
25 changes: 25 additions & 0 deletions grpc_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import grpc
import argparse
from protos import example_pb2
from protos import example_pb2_grpc

def run(vibe='foobar'):
channel = grpc.insecure_channel('localhost:50051')
stub = example_pb2_grpc.VibeServiceStub(channel)

# Set the vibe
print("Setting vibe ...")
set_response = stub.SetVibe(example_pb2.SetVibeRequest(vibe=vibe))
print(f"Previous vibe: {set_response.previous_vibe}, New vibe: {set_response.vibe}")

# Get the vibe
print("Getting current vibe...")
get_response = stub.GetVibe(example_pb2.GetVibeRequest())
print(f"Current vibe: {get_response.vibe}")

if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Set and get the vibe via gRPC')
parser.add_argument('--vibe', type=str, default='foobar', help='The vibe to set (default: foobar)')
args = parser.parse_args()

run(args.vibe)
36 changes: 36 additions & 0 deletions grpc_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import grpc
from concurrent import futures
import time

from protos import example_pb2
from protos import example_pb2_grpc

class VibeService(example_pb2_grpc.VibeServiceServicer):
def __init__(self):
self.current_vibe = "neutral"

def SetVibe(self, request, context):
prev = self.current_vibe
self.current_vibe = request.vibe
print(f"Vibe changed from '{prev}' to '{self.current_vibe}'")
return example_pb2.SetVibeResponse(previous_vibe=prev, vibe=self.current_vibe)

def GetVibe(self, request, context):
print(f"Vibe requested, returning '{self.current_vibe}'")
return example_pb2.GetVibeResponse(vibe=self.current_vibe)

def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
example_pb2_grpc.add_VibeServiceServicer_to_server(VibeService(), server)
server.add_insecure_port('[::]:50051')
server.start()
print("VibeService gRPC server started on port 50051.")
try:
while True:
time.sleep(86400)
except KeyboardInterrupt:
print("Shutting down server...")
server.stop(0)

if __name__ == '__main__':
serve()
Empty file added protos/__init__.py
Empty file.
45 changes: 45 additions & 0 deletions protos/example_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions protos/example_pb2.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Optional as _Optional

DESCRIPTOR: _descriptor.FileDescriptor

class SetVibeRequest(_message.Message):
__slots__ = ("vibe",)
VIBE_FIELD_NUMBER: _ClassVar[int]
vibe: str
def __init__(self, vibe: _Optional[str] = ...) -> None: ...

class SetVibeResponse(_message.Message):
__slots__ = ("previous_vibe", "vibe")
PREVIOUS_VIBE_FIELD_NUMBER: _ClassVar[int]
VIBE_FIELD_NUMBER: _ClassVar[int]
previous_vibe: str
vibe: str
def __init__(self, previous_vibe: _Optional[str] = ..., vibe: _Optional[str] = ...) -> None: ...

class GetVibeRequest(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...

class GetVibeResponse(_message.Message):
__slots__ = ("vibe",)
VIBE_FIELD_NUMBER: _ClassVar[int]
vibe: str
def __init__(self, vibe: _Optional[str] = ...) -> None: ...
Loading