Skip to content

Commit 0640c56

Browse files
committed
Add examples
1 parent 83bd6e9 commit 0640c56

File tree

15 files changed

+627
-0
lines changed

15 files changed

+627
-0
lines changed

examples/auth_plugin/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Auth Plugin Example
2+
3+
A plugin that validates Bearer token authentication for incoming HTTP requests.
4+
5+
## What it does
6+
7+
- Checks for the `Authorization` header in requests
8+
- Validates that it contains a Bearer token
9+
- Rejects requests with invalid or missing tokens (returns 401 Unauthorized)
10+
- Allows valid requests to continue
11+
12+
## Running the example
13+
14+
```bash
15+
# Set the expected token (optional, defaults to "secret-token-123")
16+
export AUTH_TOKEN="my-secret-token"
17+
18+
# From the repository root
19+
cd examples/auth_plugin
20+
uv run python main.py
21+
```
22+
23+
## Configuration
24+
25+
The plugin uses the `AUTH_TOKEN` environment variable to set the expected token (default: `secret-token-123`).
26+
27+
## Key concepts demonstrated
28+
29+
- Request validation and rejection
30+
- Environment variable configuration
31+
- Returning error responses with `continue_=False`
32+
- Setting HTTP status codes and response bodies
33+
- Using class initialization for plugin state

examples/auth_plugin/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Authentication plugin example."""

examples/auth_plugin/main.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Authentication plugin that validates Bearer tokens.
2+
3+
This example demonstrates how to reject requests that don't meet security requirements.
4+
"""
5+
6+
import asyncio
7+
import logging
8+
import os
9+
10+
from google.protobuf.empty_pb2 import Empty
11+
from grpc import ServicerContext
12+
13+
from mcpd_plugins import BasePlugin, serve
14+
from mcpd_plugins.v1.plugins.plugin_pb2 import (
15+
FLOW_REQUEST,
16+
Capabilities,
17+
HTTPRequest,
18+
HTTPResponse,
19+
Metadata,
20+
)
21+
22+
logging.basicConfig(level=logging.INFO)
23+
logger = logging.getLogger(__name__)
24+
25+
26+
class AuthPlugin(BasePlugin):
27+
"""Plugin that validates Bearer token authentication."""
28+
29+
def __init__(self):
30+
"""Initialize the auth plugin with expected token."""
31+
super().__init__()
32+
self.expected_token = os.getenv("AUTH_TOKEN", "secret-token-123")
33+
34+
async def GetMetadata(self, request: Empty, context: ServicerContext) -> Metadata:
35+
"""Return plugin metadata."""
36+
return Metadata(
37+
name="auth-plugin",
38+
version="1.0.0",
39+
description="Validates Bearer token authentication",
40+
)
41+
42+
async def GetCapabilities(self, request: Empty, context: ServicerContext) -> Capabilities:
43+
"""Declare support for request flow."""
44+
return Capabilities(flows=[FLOW_REQUEST])
45+
46+
async def HandleRequest(self, request: HTTPRequest, context: ServicerContext) -> HTTPResponse:
47+
"""Validate Bearer token in Authorization header."""
48+
logger.info(f"Authenticating request: {request.method} {request.url}")
49+
50+
# Check for Authorization header.
51+
auth_header = request.headers.get("Authorization", "")
52+
53+
if not auth_header.startswith("Bearer "):
54+
logger.warning("Missing or invalid Authorization header")
55+
return self._unauthorized_response("Missing or invalid Authorization header")
56+
57+
# Extract and validate token.
58+
token = auth_header[7:] # Remove "Bearer " prefix
59+
if token != self.expected_token:
60+
logger.warning("Invalid token")
61+
return self._unauthorized_response("Invalid token")
62+
63+
# Token is valid, allow request to continue.
64+
logger.info("Authentication successful")
65+
return HTTPResponse(**{"continue": True})
66+
67+
def _unauthorized_response(self, message: str) -> HTTPResponse:
68+
"""Create a 401 Unauthorized response."""
69+
response = HTTPResponse(
70+
status_code=401,
71+
body=f'{{"error": "{message}"}}'.encode(),
72+
**{"continue": False},
73+
)
74+
response.headers["Content-Type"] = "application/json"
75+
response.headers["WWW-Authenticate"] = "Bearer"
76+
return response
77+
78+
79+
if __name__ == "__main__":
80+
asyncio.run(serve(AuthPlugin()))

examples/logging_plugin/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Logging Plugin Example
2+
3+
A plugin that logs HTTP request and response details for observability and debugging.
4+
5+
## What it does
6+
7+
- Logs details of incoming HTTP requests (method, URL, headers, body size)
8+
- Logs details of outgoing HTTP responses (status code, headers, body size)
9+
- Redacts sensitive headers (Authorization, Cookie) for security
10+
- Supports both REQUEST and RESPONSE flows
11+
12+
## Running the example
13+
14+
```bash
15+
# From the repository root
16+
cd examples/logging_plugin
17+
uv run python main.py
18+
```
19+
20+
## Key concepts demonstrated
21+
22+
- Implementing both `HandleRequest()` and `HandleResponse()` methods
23+
- Declaring multiple flows in `GetCapabilities()`
24+
- Structured logging for observability
25+
- Handling sensitive data (header redaction)
26+
- Pass-through behavior while gathering telemetry
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Logging plugin example."""

examples/logging_plugin/main.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""Logging plugin that logs HTTP requests and responses.
2+
3+
This example demonstrates implementing both request and response flows
4+
for observability purposes.
5+
"""
6+
7+
import asyncio
8+
import logging
9+
10+
from google.protobuf.empty_pb2 import Empty
11+
from grpc import ServicerContext
12+
13+
from mcpd_plugins import BasePlugin, serve
14+
from mcpd_plugins.v1.plugins.plugin_pb2 import (
15+
FLOW_REQUEST,
16+
FLOW_RESPONSE,
17+
Capabilities,
18+
HTTPRequest,
19+
HTTPResponse,
20+
Metadata,
21+
)
22+
23+
logging.basicConfig(
24+
level=logging.INFO,
25+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
26+
)
27+
logger = logging.getLogger(__name__)
28+
29+
30+
class LoggingPlugin(BasePlugin):
31+
"""Plugin that logs HTTP request and response details."""
32+
33+
async def GetMetadata(self, request: Empty, context: ServicerContext) -> Metadata:
34+
"""Return plugin metadata."""
35+
return Metadata(
36+
name="logging-plugin",
37+
version="1.0.0",
38+
description="Logs HTTP request and response details for observability",
39+
)
40+
41+
async def GetCapabilities(self, request: Empty, context: ServicerContext) -> Capabilities:
42+
"""Declare support for both request and response flows."""
43+
return Capabilities(flows=[FLOW_REQUEST, FLOW_RESPONSE])
44+
45+
async def HandleRequest(self, request: HTTPRequest, context: ServicerContext) -> HTTPResponse:
46+
"""Log incoming request details."""
47+
logger.info("=" * 80)
48+
logger.info("INCOMING REQUEST")
49+
logger.info(f"Method: {request.method}")
50+
logger.info(f"URL: {request.url}")
51+
logger.info(f"Path: {request.path}")
52+
logger.info(f"Remote Address: {request.remote_addr}")
53+
54+
# Log headers.
55+
logger.info("Headers:")
56+
for key, value in request.headers.items():
57+
# Mask sensitive headers.
58+
if key.lower() in ("authorization", "cookie"):
59+
value = "***REDACTED***"
60+
logger.info(f" {key}: {value}")
61+
62+
# Log body size.
63+
if request.body:
64+
logger.info(f"Body size: {len(request.body)} bytes")
65+
66+
logger.info("=" * 80)
67+
68+
# Continue processing.
69+
return HTTPResponse(**{"continue": True})
70+
71+
async def HandleResponse(self, request: HTTPResponse, context: ServicerContext) -> HTTPResponse:
72+
"""Log outgoing response details."""
73+
logger.info("=" * 80)
74+
logger.info("OUTGOING RESPONSE")
75+
logger.info(f"Status Code: {request.status_code}")
76+
77+
# Log headers.
78+
logger.info("Headers:")
79+
for key, value in request.headers.items():
80+
logger.info(f" {key}: {value}")
81+
82+
# Log body size.
83+
if request.body:
84+
logger.info(f"Body size: {len(request.body)} bytes")
85+
86+
logger.info("=" * 80)
87+
88+
# Continue processing.
89+
return HTTPResponse(**{"continue": True})
90+
91+
92+
if __name__ == "__main__":
93+
asyncio.run(serve(LoggingPlugin()))
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Rate Limit Plugin Example
2+
3+
A plugin that implements rate limiting using a token bucket algorithm to prevent clients from overwhelming the server.
4+
5+
## What it does
6+
7+
- Tracks requests per client IP address
8+
- Limits each client to a configurable number of requests per minute (default: 60)
9+
- Returns 429 Too Many Requests when limit is exceeded
10+
- Adds rate limit headers to responses (`X-RateLimit-Limit`, `X-RateLimit-Remaining`)
11+
- Includes `Retry-After` header when rate limit is exceeded
12+
13+
## Running the example
14+
15+
```bash
16+
# From the repository root
17+
cd examples/rate_limit_plugin
18+
uv run python main.py
19+
```
20+
21+
## Configuration
22+
23+
You can customize the rate limit by modifying the `requests_per_minute` parameter when creating the plugin:
24+
25+
```python
26+
asyncio.run(serve(RateLimitPlugin(requests_per_minute=100)))
27+
```
28+
29+
## Key concepts demonstrated
30+
31+
- Stateful plugin implementation (tracking client buckets)
32+
- Token bucket algorithm for rate limiting
33+
- Custom response headers
34+
- Calculating and returning `Retry-After` header
35+
- Per-client tracking using remote IP address
36+
- Request rejection with appropriate HTTP status codes
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Rate limiting plugin example."""

examples/rate_limit_plugin/main.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""Rate limiting plugin using a simple token bucket algorithm.
2+
3+
This example demonstrates stateful request processing and rate limiting.
4+
"""
5+
6+
import asyncio
7+
import logging
8+
import time
9+
from collections import defaultdict
10+
11+
from google.protobuf.empty_pb2 import Empty
12+
from grpc import ServicerContext
13+
14+
from mcpd_plugins import BasePlugin, serve
15+
from mcpd_plugins.v1.plugins.plugin_pb2 import (
16+
FLOW_REQUEST,
17+
Capabilities,
18+
HTTPRequest,
19+
HTTPResponse,
20+
Metadata,
21+
)
22+
23+
logging.basicConfig(level=logging.INFO)
24+
logger = logging.getLogger(__name__)
25+
26+
27+
class RateLimitPlugin(BasePlugin):
28+
"""Plugin that implements rate limiting using token bucket algorithm."""
29+
30+
def __init__(self, requests_per_minute: int = 60):
31+
"""Initialize the rate limiter.
32+
33+
Args:
34+
requests_per_minute: Maximum requests allowed per minute per client.
35+
"""
36+
super().__init__()
37+
self.requests_per_minute = requests_per_minute
38+
self.rate_per_second = requests_per_minute / 60.0
39+
40+
# Track tokens for each client IP.
41+
self.buckets: dict[str, float] = defaultdict(lambda: float(requests_per_minute))
42+
self.last_update: dict[str, float] = defaultdict(time.time)
43+
44+
async def GetMetadata(self, request: Empty, context: ServicerContext) -> Metadata:
45+
"""Return plugin metadata."""
46+
return Metadata(
47+
name="rate-limit-plugin",
48+
version="1.0.0",
49+
description=f"Rate limits requests to {self.requests_per_minute} per minute per client",
50+
)
51+
52+
async def GetCapabilities(self, request: Empty, context: ServicerContext) -> Capabilities:
53+
"""Declare support for request flow."""
54+
return Capabilities(flows=[FLOW_REQUEST])
55+
56+
async def HandleRequest(self, request: HTTPRequest, context: ServicerContext) -> HTTPResponse:
57+
"""Apply rate limiting based on client IP."""
58+
client_ip = request.remote_addr or "unknown"
59+
logger.info(f"Rate limit check for {client_ip}: {request.method} {request.url}")
60+
61+
# Refill tokens based on time elapsed.
62+
now = time.time()
63+
elapsed = now - self.last_update[client_ip]
64+
self.buckets[client_ip] = min(
65+
self.requests_per_minute,
66+
self.buckets[client_ip] + elapsed * self.rate_per_second,
67+
)
68+
self.last_update[client_ip] = now
69+
70+
# Check if client has tokens available.
71+
if self.buckets[client_ip] < 1.0:
72+
logger.warning(f"Rate limit exceeded for {client_ip}")
73+
return self._rate_limit_response(client_ip)
74+
75+
# Consume one token.
76+
self.buckets[client_ip] -= 1.0
77+
logger.info(f"Request allowed for {client_ip} (tokens remaining: {self.buckets[client_ip]:.2f})")
78+
79+
# Add rate limit headers to response.
80+
response = HTTPResponse(**{"continue": True})
81+
response.modified_request.CopyFrom(request)
82+
response.headers["X-RateLimit-Limit"] = str(self.requests_per_minute)
83+
response.headers["X-RateLimit-Remaining"] = str(int(self.buckets[client_ip]))
84+
85+
return response
86+
87+
def _rate_limit_response(self, client_ip: str) -> HTTPResponse:
88+
"""Create a 429 Too Many Requests response."""
89+
# Calculate retry-after in seconds.
90+
tokens_needed = 1.0 - self.buckets[client_ip]
91+
retry_after = int(tokens_needed / self.rate_per_second) + 1
92+
93+
response = HTTPResponse(
94+
**{"continue": False},
95+
status_code=429,
96+
body=b'{"error": "Rate limit exceeded"}',
97+
)
98+
response.headers["Content-Type"] = "application/json"
99+
response.headers["X-RateLimit-Limit"] = str(self.requests_per_minute)
100+
response.headers["X-RateLimit-Remaining"] = "0"
101+
response.headers["Retry-After"] = str(retry_after)
102+
103+
return response
104+
105+
106+
if __name__ == "__main__":
107+
asyncio.run(serve(RateLimitPlugin(requests_per_minute=60)))

0 commit comments

Comments
 (0)