Skip to content

Commit b2ba302

Browse files
committed
Add project code
1 parent bd51495 commit b2ba302

File tree

11 files changed

+890
-0
lines changed

11 files changed

+890
-0
lines changed

Makefile

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
.PHONY: help
2+
help: ## Show this help message
3+
@echo "Available targets:"
4+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
5+
6+
.PHONY: ensure-scripts-exec
7+
ensure-scripts-exec: ## Make scripts executable
8+
chmod +x scripts/*
9+
10+
.PHONY: setup
11+
setup: ensure-scripts-exec ## Setup development environment (installs uv and syncs dependencies)
12+
./scripts/setup_uv.sh
13+
14+
.PHONY: test
15+
test: ## Run tests with pytest
16+
uv run pytest tests/ -v
17+
18+
.PHONY: lint
19+
lint: ## Run pre-commit hooks on all files
20+
uv run pre-commit run --all-files
21+
22+
.PHONY: generate-protos
23+
generate-protos: ensure-scripts-exec ## Download proto files and generate Python code
24+
./scripts/generate_protos.sh
25+
26+
.PHONY: clean
27+
clean: ## Clean generated files and caches
28+
rm -rf tmp/
29+
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
30+
find . -type f -name "*.pyc" -delete
31+
find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
32+
find . -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true
33+
find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true
34+
rm -rf build/ dist/

pyproject.toml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
[project]
2+
name = "mcpd-plugins"
3+
dynamic = ["version"]
4+
description = "mcpd plugins Python SDK"
5+
readme = "README.md"
6+
license = { text = "Apache-2.0" }
7+
requires-python = ">=3.11"
8+
dependencies = [
9+
"grpcio>=1.68.0",
10+
"protobuf>=5.29.0",
11+
]
12+
13+
[dependency-groups]
14+
dev = [
15+
"grpcio-tools>=1.68.0",
16+
"debugpy>=1.8.14",
17+
]
18+
lint = [
19+
"pre-commit>=4.2.0",
20+
"ruff>=0.11.13",
21+
]
22+
tests = [
23+
"pytest>=8.3.5",
24+
"pytest-asyncio>=0.25.2",
25+
"setuptools>=80.9.0",
26+
"setuptools-scm>=8.3.1",
27+
]
28+
all = [
29+
{ include-group = "dev" },
30+
{ include-group = "lint" },
31+
{ include-group = "tests" },
32+
]
33+
34+
[build-system]
35+
requires = ["setuptools>=48", "setuptools_scm[toml]>=6.3.1"]
36+
build-backend = "setuptools.build_meta"
37+
38+
[tool.setuptools]
39+
package-dir = {"" = "src"}
40+
41+
[tool.setuptools.packages.find]
42+
where = ["src"]
43+
exclude = ["tests", "tests.*"]
44+
namespaces = false
45+
46+
[tool.setuptools.package-data]
47+
mcpd_plugins = ["py.typed"]
48+
49+
[tool.setuptools_scm]
50+
write_to = "src/mcpd_plugins/_version.py"

src/mcpd_plugins/__init__.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""mcpd-plugins: Python SDK for building mcpd plugins.
2+
3+
This SDK provides a simple way to create plugins for the mcpd plugin system using gRPC.
4+
Plugins extend the BasePlugin class and override only the methods they need.
5+
6+
Example:
7+
```python
8+
import asyncio
9+
from mcpd_plugins import BasePlugin, serve
10+
from mcpd_plugins.v1.plugins.plugin_pb2 import Capabilities, Flow, Metadata
11+
12+
class MyPlugin(BasePlugin):
13+
async def GetMetadata(self, request, context):
14+
return Metadata(
15+
name="my-plugin",
16+
version="1.0.0",
17+
description="A simple example plugin"
18+
)
19+
20+
async def GetCapabilities(self, request, context):
21+
return Capabilities(flows=[Flow.FLOW_REQUEST])
22+
23+
async def HandleRequest(self, request, context):
24+
# Add custom header
25+
response = HTTPResponse(**{"continue": True})
26+
response.headers["X-My-Plugin"] = "processed"
27+
return response
28+
29+
if __name__ == "__main__":
30+
asyncio.run(serve(MyPlugin()))
31+
```
32+
"""
33+
34+
from mcpd_plugins.base_plugin import BasePlugin
35+
from mcpd_plugins.exceptions import ConfigurationError, PluginError, ServerError
36+
from mcpd_plugins.server import serve
37+
38+
try:
39+
from mcpd_plugins._version import version as __version__
40+
except ImportError:
41+
__version__ = "0.0.0+unknown"
42+
43+
__all__ = [
44+
"BasePlugin",
45+
"serve",
46+
"PluginError",
47+
"ConfigurationError",
48+
"ServerError",
49+
"__version__",
50+
]

src/mcpd_plugins/base_plugin.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""BasePlugin class providing default implementations for all plugin methods."""
2+
3+
from google.protobuf.empty_pb2 import Empty
4+
from grpc import ServicerContext
5+
6+
from mcpd_plugins.v1.plugins.plugin_pb2 import (
7+
Capabilities,
8+
HTTPRequest,
9+
HTTPResponse,
10+
Metadata,
11+
PluginConfig,
12+
)
13+
from mcpd_plugins.v1.plugins.plugin_pb2_grpc import PluginServicer
14+
15+
16+
class BasePlugin(PluginServicer):
17+
"""Base class for mcpd plugins with sensible default implementations.
18+
19+
Developers should extend this class and override only the methods they need.
20+
All methods are async (using async/await pattern) to support asynchronous operations.
21+
22+
Example:
23+
```python
24+
class MyPlugin(BasePlugin):
25+
async def GetMetadata(self, request: Empty, context: ServicerContext) -> Metadata:
26+
return Metadata(
27+
name="my-plugin",
28+
version="1.0.0",
29+
description="My custom plugin"
30+
)
31+
32+
async def HandleRequest(self, request: HTTPRequest, context: ServicerContext) -> HTTPResponse:
33+
# Process the request
34+
response = HTTPResponse(**{"continue": True})
35+
response.headers["X-My-Plugin"] = "processed"
36+
return response
37+
```
38+
"""
39+
40+
async def Configure(self, request: PluginConfig, context: ServicerContext) -> Empty:
41+
"""Configure the plugin with the provided settings.
42+
43+
Default implementation does nothing. Override to handle configuration.
44+
45+
Args:
46+
request: Configuration settings from the host.
47+
context: gRPC context for the request.
48+
49+
Returns:
50+
Empty message indicating successful configuration.
51+
"""
52+
return Empty()
53+
54+
async def Stop(self, request: Empty, context: ServicerContext) -> Empty:
55+
"""Stop the plugin and clean up resources.
56+
57+
Default implementation does nothing. Override to handle cleanup.
58+
59+
Args:
60+
request: Empty request message.
61+
context: gRPC context for the request.
62+
63+
Returns:
64+
Empty message indicating successful shutdown.
65+
"""
66+
return Empty()
67+
68+
async def GetMetadata(self, request: Empty, context: ServicerContext) -> Metadata:
69+
"""Get plugin metadata (name, version, description).
70+
71+
Default implementation returns basic metadata. Override to provide actual values.
72+
73+
Args:
74+
request: Empty request message.
75+
context: gRPC context for the request.
76+
77+
Returns:
78+
Metadata containing plugin information.
79+
"""
80+
return Metadata(
81+
name="base-plugin",
82+
version="0.0.0",
83+
description="Base plugin implementation",
84+
)
85+
86+
async def GetCapabilities(self, request: Empty, context: ServicerContext) -> Capabilities:
87+
"""Get plugin capabilities (supported flows).
88+
89+
Default implementation returns no capabilities. Must override to declare supported flows.
90+
91+
Args:
92+
request: Empty request message.
93+
context: gRPC context for the request.
94+
95+
Returns:
96+
Capabilities message listing supported flows.
97+
"""
98+
return Capabilities()
99+
100+
async def CheckHealth(self, request: Empty, context: ServicerContext) -> Empty:
101+
"""Health check endpoint.
102+
103+
Default implementation returns healthy status. Override if custom health checks needed.
104+
105+
Args:
106+
request: Empty request message.
107+
context: gRPC context for the request.
108+
109+
Returns:
110+
Empty message indicating healthy status.
111+
"""
112+
return Empty()
113+
114+
async def CheckReady(self, request: Empty, context: ServicerContext) -> Empty:
115+
"""Readiness check endpoint.
116+
117+
Default implementation returns ready status. Override if custom readiness checks needed.
118+
119+
Args:
120+
request: Empty request message.
121+
context: gRPC context for the request.
122+
123+
Returns:
124+
Empty message indicating ready status.
125+
"""
126+
return Empty()
127+
128+
async def HandleRequest(self, request: HTTPRequest, context: ServicerContext) -> HTTPResponse:
129+
"""Handle incoming HTTP request.
130+
131+
Default implementation passes through unchanged (continue=True).
132+
133+
Args:
134+
request: The incoming HTTP request to process.
135+
context: gRPC context for the request.
136+
137+
Returns:
138+
HTTPResponse indicating how to proceed (continue, modify, or reject).
139+
"""
140+
return HTTPResponse(**{"continue": True})
141+
142+
async def HandleResponse(self, request: HTTPResponse, context: ServicerContext) -> HTTPResponse:
143+
"""Handle outgoing HTTP response.
144+
145+
Default implementation passes through unchanged (continue=True).
146+
147+
Args:
148+
request: The outgoing HTTP response to process.
149+
context: gRPC context for the request.
150+
151+
Returns:
152+
HTTPResponse indicating how to proceed (continue or modify).
153+
"""
154+
return HTTPResponse(**{"continue": True})

src/mcpd_plugins/exceptions.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""Custom exception classes for mcpd-plugins SDK."""
2+
3+
4+
class PluginError(Exception):
5+
"""Base exception class for all plugin-related errors."""
6+
7+
pass
8+
9+
10+
class ConfigurationError(PluginError):
11+
"""Exception raised for configuration-related errors."""
12+
13+
pass
14+
15+
16+
class ServerError(PluginError):
17+
"""Exception raised for server startup or shutdown errors."""
18+
19+
pass

src/mcpd_plugins/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)