|
1 |
| -Plugins go here.. |
| 1 | +# MCP Context Forge Plugin Framework |
| 2 | + |
| 3 | +The MCP Context Forge Plugin Framework provides a powerful, production-ready system for AI safety middleware, content security, policy enforcement, and operational excellence. Plugins run as middleware components that can intercept and transform requests and responses at various points in the gateway lifecycle. |
| 4 | + |
| 5 | +## Quick Start |
| 6 | + |
| 7 | +### Enable Plugins |
| 8 | + |
| 9 | +1. Set environment variables in `.env`: |
| 10 | +```bash |
| 11 | +PLUGINS_ENABLED=true |
| 12 | +PLUGIN_CONFIG_FILE=plugins/config.yaml |
| 13 | +PLUGINS_CLI_COMPLETION=false |
| 14 | +PLUGINS_CLI_MARKUP_MODE=rich |
| 15 | +``` |
| 16 | + |
| 17 | +2. Configure plugins in `plugins/config.yaml` (see [Configuration](#configuration) section) |
| 18 | + |
| 19 | +3. Restart the gateway: `make dev` |
| 20 | + |
| 21 | +## Plugin Architecture |
| 22 | + |
| 23 | +The framework supports two types of plugins: |
| 24 | + |
| 25 | +### 1. Self-Contained Plugins |
| 26 | +- Written in Python and run directly in the gateway process |
| 27 | +- Sub-millisecond latency (<1ms) |
| 28 | +- Perfect for high-frequency operations like PII filtering and regex transformations |
| 29 | +- Examples: `pii_filter`, `regex_filter`, `deny_filter`, `resource_filter` |
| 30 | + |
| 31 | +### 2. External Service Plugins |
| 32 | +- Call external AI safety services via HTTP/MCP |
| 33 | +- Support microservice integrations with authentication |
| 34 | +- 10-100ms latency depending on service |
| 35 | +- Examples: LlamaGuard, OpenAI Moderation, custom safety services |
| 36 | + |
| 37 | +## Available Hooks |
| 38 | + |
| 39 | +Plugins can implement hooks at these lifecycle points: |
| 40 | + |
| 41 | +| Hook | Description | Payload Type | Use Cases | |
| 42 | +|------|-------------|--------------|-----------| |
| 43 | +| `prompt_pre_fetch` | Before prompt template retrieval | `PromptPrehookPayload` | Input validation, access control | |
| 44 | +| `prompt_post_fetch` | After prompt template retrieval | `PromptPosthookPayload` | Content filtering, transformation | |
| 45 | +| `tool_pre_invoke` | Before tool execution | `ToolPreInvokePayload` | Parameter validation, safety checks | |
| 46 | +| `tool_post_invoke` | After tool execution | `ToolPostInvokeResult` | Result filtering, audit logging | |
| 47 | +| `resource_pre_fetch` | Before resource retrieval | `ResourcePreFetchPayload` | Protocol/domain validation | |
| 48 | +| `resource_post_fetch` | After resource retrieval | `ResourcePostFetchResult` | Content scanning, size limits | |
| 49 | + |
| 50 | +Future hooks (in development): |
| 51 | +- `server_pre_register` / `server_post_register` - Virtual server verification |
| 52 | +- `auth_pre_check` / `auth_post_check` - Custom authentication logic |
| 53 | +- `federation_pre_sync` / `federation_post_sync` - Gateway federation |
| 54 | + |
| 55 | +## Configuration |
| 56 | + |
| 57 | +### Main Configuration File (`plugins/config.yaml`) |
| 58 | + |
| 59 | +```yaml |
| 60 | +plugins: |
| 61 | + - name: "PIIFilterPlugin" |
| 62 | + kind: "plugins.pii_filter.pii_filter.PIIFilterPlugin" |
| 63 | + description: "Detects and masks Personally Identifiable Information" |
| 64 | + version: "0.1.0" |
| 65 | + author: "Your Name" |
| 66 | + hooks: ["prompt_pre_fetch", "tool_pre_invoke"] |
| 67 | + tags: ["security", "pii", "compliance"] |
| 68 | + mode: "enforce" # enforce | permissive | disabled |
| 69 | + priority: 50 # Lower number = higher priority (runs first) |
| 70 | + conditions: |
| 71 | + - prompts: [] # Empty = apply to all prompts |
| 72 | + server_ids: [] # Apply to specific servers |
| 73 | + tenant_ids: [] # Apply to specific tenants |
| 74 | + config: |
| 75 | + detect_ssn: true |
| 76 | + detect_email: true |
| 77 | + default_mask_strategy: "partial" |
| 78 | + |
| 79 | +# Global settings |
| 80 | +plugin_settings: |
| 81 | + parallel_execution_within_band: true |
| 82 | + plugin_timeout: 30 |
| 83 | + fail_on_plugin_error: false |
| 84 | + plugin_health_check_interval: 60 |
| 85 | +``` |
| 86 | +
|
| 87 | +### Plugin Modes |
| 88 | +
|
| 89 | +- **`enforce`**: Blocks violations and prevents request processing |
| 90 | +- **`permissive`**: Logs violations but allows request to continue |
| 91 | +- **`disabled`**: Plugin is not executed (useful for temporary disabling) |
| 92 | + |
| 93 | +### Plugin Priority |
| 94 | + |
| 95 | +Lower priority numbers run first (higher priority). Recommended ranges: |
| 96 | +- **1-50**: Critical security plugins (PII, access control) |
| 97 | +- **51-100**: Content filtering and validation |
| 98 | +- **101-200**: Transformations and enhancements |
| 99 | +- **201+**: Logging and monitoring |
| 100 | + |
| 101 | +## Built-in Plugins |
| 102 | + |
| 103 | +### PII Filter Plugin |
| 104 | +Detects and masks Personally Identifiable Information (PII): |
| 105 | + |
| 106 | +```yaml |
| 107 | +- name: "PIIFilterPlugin" |
| 108 | + kind: "plugins.pii_filter.pii_filter.PIIFilterPlugin" |
| 109 | + config: |
| 110 | + detect_ssn: true |
| 111 | + detect_credit_card: true |
| 112 | + detect_email: true |
| 113 | + detect_phone: true |
| 114 | + detect_aws_keys: true |
| 115 | + default_mask_strategy: "partial" # redact | partial | hash | tokenize |
| 116 | + block_on_detection: false |
| 117 | + whitelist_patterns: |
| 118 | + |
| 119 | +``` |
| 120 | + |
| 121 | +### Regex Filter Plugin |
| 122 | +Find and replace text patterns: |
| 123 | + |
| 124 | +```yaml |
| 125 | +- name: "ReplaceBadWordsPlugin" |
| 126 | + kind: "plugins.regex_filter.search_replace.SearchReplacePlugin" |
| 127 | + config: |
| 128 | + words: |
| 129 | + - search: "inappropriate_word" |
| 130 | + replace: "[FILTERED]" |
| 131 | +``` |
| 132 | + |
| 133 | +### Deny List Plugin |
| 134 | +Block requests containing specific terms: |
| 135 | + |
| 136 | +```yaml |
| 137 | +- name: "DenyListPlugin" |
| 138 | + kind: "plugins.deny_filter.deny.DenyListPlugin" |
| 139 | + config: |
| 140 | + words: |
| 141 | + - "blocked_term" |
| 142 | + - "another_blocked_term" |
| 143 | +``` |
| 144 | + |
| 145 | +### Resource Filter Plugin |
| 146 | +Validate and filter resource requests: |
| 147 | + |
| 148 | +```yaml |
| 149 | +- name: "ResourceFilterExample" |
| 150 | + kind: "plugins.resource_filter.resource_filter.ResourceFilterPlugin" |
| 151 | + config: |
| 152 | + max_content_size: 1048576 # 1MB |
| 153 | + allowed_protocols: ["http", "https"] |
| 154 | + blocked_domains: ["malicious.example.com"] |
| 155 | + content_filters: |
| 156 | + - pattern: "password\\s*[:=]\\s*\\S+" |
| 157 | + replacement: "password: [REDACTED]" |
| 158 | +``` |
| 159 | + |
| 160 | +## Writing Custom Plugins |
| 161 | + |
| 162 | +### 1. Plugin Structure |
| 163 | + |
| 164 | +Create a new directory under `plugins/`: |
| 165 | + |
| 166 | +``` |
| 167 | +plugins/my_plugin/ |
| 168 | +├── __init__.py |
| 169 | +├── plugin-manifest.yaml |
| 170 | +├── my_plugin.py |
| 171 | +└── README.md |
| 172 | +``` |
| 173 | + |
| 174 | +### 2. Plugin Manifest (`plugin-manifest.yaml`) |
| 175 | + |
| 176 | +```yaml |
| 177 | +description: "My custom plugin" |
| 178 | +author: "Your Name" |
| 179 | +version: "1.0.0" |
| 180 | +available_hooks: |
| 181 | + - "tool_pre_invoke" |
| 182 | + - "tool_post_invoke" |
| 183 | +default_configs: |
| 184 | + my_setting: true |
| 185 | + threshold: 0.8 |
| 186 | +``` |
| 187 | + |
| 188 | +### 3. Plugin Implementation |
| 189 | + |
| 190 | +```python |
| 191 | +# my_plugin.py |
| 192 | +from mcpgateway.plugins.framework.base import Plugin |
| 193 | +from mcpgateway.plugins.framework.models import ( |
| 194 | + ToolPreInvokePayload, |
| 195 | + ToolPreInvokeResult, |
| 196 | + PluginResult |
| 197 | +) |
| 198 | +
|
| 199 | +class MyPlugin(Plugin): |
| 200 | + """Custom plugin implementation.""" |
| 201 | +
|
| 202 | + async def tool_pre_invoke(self, payload: ToolPreInvokePayload) -> ToolPreInvokeResult: |
| 203 | + """Process tool invocation before execution.""" |
| 204 | +
|
| 205 | + # Get plugin configuration |
| 206 | + my_setting = self.config.get("my_setting", False) |
| 207 | + threshold = self.config.get("threshold", 0.5) |
| 208 | +
|
| 209 | + # Implement your logic |
| 210 | + if my_setting and self._should_block(payload): |
| 211 | + return ToolPreInvokeResult( |
| 212 | + result=PluginResult.BLOCK, |
| 213 | + message="Request blocked by custom logic", |
| 214 | + modified_payload=payload |
| 215 | + ) |
| 216 | +
|
| 217 | + # Modify payload if needed |
| 218 | + modified_payload = self._transform_payload(payload) |
| 219 | +
|
| 220 | + return ToolPreInvokeResult( |
| 221 | + result=PluginResult.CONTINUE, |
| 222 | + modified_payload=modified_payload |
| 223 | + ) |
| 224 | +
|
| 225 | + def _should_block(self, payload: ToolPreInvokePayload) -> bool: |
| 226 | + """Custom blocking logic.""" |
| 227 | + # Implement your validation logic here |
| 228 | + return False |
| 229 | +
|
| 230 | + def _transform_payload(self, payload: ToolPreInvokePayload) -> ToolPreInvokePayload: |
| 231 | + """Transform payload if needed.""" |
| 232 | + return payload |
| 233 | +``` |
| 234 | + |
| 235 | +### 4. Register Your Plugin |
| 236 | + |
| 237 | +Add to `plugins/config.yaml`: |
| 238 | + |
| 239 | +```yaml |
| 240 | +plugins: |
| 241 | + - name: "MyCustomPlugin" |
| 242 | + kind: "plugins.my_plugin.my_plugin.MyPlugin" |
| 243 | + description: "My custom plugin description" |
| 244 | + version: "1.0.0" |
| 245 | + author: "Your Name" |
| 246 | + hooks: ["tool_pre_invoke"] |
| 247 | + mode: "enforce" |
| 248 | + priority: 100 |
| 249 | + config: |
| 250 | + my_setting: true |
| 251 | + threshold: 0.8 |
| 252 | +``` |
| 253 | + |
| 254 | +## Plugin Development Best Practices |
| 255 | + |
| 256 | +### Error Handling |
| 257 | +```python |
| 258 | +async def tool_pre_invoke(self, payload: ToolPreInvokePayload) -> ToolPreInvokeResult: |
| 259 | + try: |
| 260 | + # Your plugin logic |
| 261 | + result = await self._process_payload(payload) |
| 262 | + return ToolPreInvokeResult(result=PluginResult.CONTINUE) |
| 263 | + except Exception as e: |
| 264 | + self.logger.error(f"Plugin error: {e}") |
| 265 | + if self.mode == PluginMode.ENFORCE: |
| 266 | + return ToolPreInvokeResult( |
| 267 | + result=PluginResult.BLOCK, |
| 268 | + message=f"Plugin failed: {e}" |
| 269 | + ) |
| 270 | + return ToolPreInvokeResult(result=PluginResult.CONTINUE) |
| 271 | +``` |
| 272 | + |
| 273 | +### Logging and Monitoring |
| 274 | +```python |
| 275 | +def __init__(self, config: PluginConfig): |
| 276 | + super().__init__(config) |
| 277 | + self.logger.info(f"Initialized {self.name} v{self.version}") |
| 278 | +
|
| 279 | +async def tool_pre_invoke(self, payload: ToolPreInvokePayload) -> ToolPreInvokeResult: |
| 280 | + self.logger.debug(f"Processing tool: {payload.tool_name}") |
| 281 | + # ... plugin logic |
| 282 | + self.metrics.increment("requests_processed") |
| 283 | +``` |
| 284 | + |
| 285 | +### Configuration Validation |
| 286 | +```python |
| 287 | +def validate_config(self) -> None: |
| 288 | + """Validate plugin configuration.""" |
| 289 | + required_keys = ["threshold", "api_key"] |
| 290 | + for key in required_keys: |
| 291 | + if key not in self.config: |
| 292 | + raise ValueError(f"Missing required config key: {key}") |
| 293 | +
|
| 294 | + if not 0 <= self.config["threshold"] <= 1: |
| 295 | + raise ValueError("threshold must be between 0 and 1") |
| 296 | +``` |
| 297 | + |
| 298 | +## Performance Considerations |
| 299 | + |
| 300 | +### Latency Guidelines |
| 301 | +- **Self-contained plugins**: <1ms target |
| 302 | +- **External service plugins**: <100ms target |
| 303 | +- Use async/await for I/O operations |
| 304 | +- Implement timeouts for external calls |
| 305 | + |
| 306 | +### Resource Management |
| 307 | +```python |
| 308 | +class MyPlugin(Plugin): |
| 309 | + def __init__(self, config: PluginConfig): |
| 310 | + super().__init__(config) |
| 311 | + self._session = None |
| 312 | +
|
| 313 | + async def __aenter__(self): |
| 314 | + self._session = aiohttp.ClientSession() |
| 315 | + return self |
| 316 | +
|
| 317 | + async def __aexit__(self, exc_type, exc_val, exc_tb): |
| 318 | + if self._session: |
| 319 | + await self._session.close() |
| 320 | +``` |
| 321 | + |
| 322 | +## Testing Plugins |
| 323 | + |
| 324 | +### Unit Testing |
| 325 | +```python |
| 326 | +import pytest |
| 327 | +from mcpgateway.plugins.framework.models import ToolPreInvokePayload, PluginConfig |
| 328 | +from plugins.my_plugin.my_plugin import MyPlugin |
| 329 | +
|
| 330 | +@pytest.fixture |
| 331 | +def plugin(): |
| 332 | + config = PluginConfig( |
| 333 | + name="test_plugin", |
| 334 | + config={"my_setting": True} |
| 335 | + ) |
| 336 | + return MyPlugin(config) |
| 337 | +
|
| 338 | +async def test_tool_pre_invoke(plugin): |
| 339 | + payload = ToolPreInvokePayload( |
| 340 | + tool_name="test_tool", |
| 341 | + arguments={"arg1": "value1"} |
| 342 | + ) |
| 343 | +
|
| 344 | + result = await plugin.tool_pre_invoke(payload) |
| 345 | + assert result.result == PluginResult.CONTINUE |
| 346 | +``` |
| 347 | + |
| 348 | +### Integration Testing |
| 349 | +```bash |
| 350 | +# Test with live gateway |
| 351 | +make dev |
| 352 | +curl -X POST http://localhost:4444/tools/invoke \ |
| 353 | + -H "Content-Type: application/json" \ |
| 354 | + -d '{"name": "test_tool", "arguments": {}}' |
| 355 | +``` |
| 356 | + |
| 357 | +## Troubleshooting |
| 358 | + |
| 359 | +### Common Issues |
| 360 | + |
| 361 | +1. **Plugin not loading**: Check `plugin_dirs` in config and Python import paths |
| 362 | +2. **Configuration errors**: Validate YAML syntax and required fields |
| 363 | +3. **Performance issues**: Profile plugin execution time and optimize bottlenecks |
| 364 | +4. **Hook not triggering**: Verify hook name matches available hooks in manifest |
| 365 | + |
| 366 | +### Debug Mode |
| 367 | +```bash |
| 368 | +LOG_LEVEL=DEBUG make serve # port 4444 |
| 369 | +# Or with reloading dev server: |
| 370 | +LOG_LEVEL=DEBUG make dev # port 8000 |
| 371 | +``` |
| 372 | + |
| 373 | +## Documentation Links |
| 374 | + |
| 375 | +- **Plugin Usage Guide**: https://ibm.github.io/mcp-context-forge/using/plugins/ |
| 376 | +- **Plugin Lifecycle**: https://ibm.github.io/mcp-context-forge/using/plugins/lifecycle/ |
| 377 | +- **API Reference**: Generated from code docstrings |
| 378 | +- **Examples**: See `plugins/` directory for complete implementations |
| 379 | + |
| 380 | +## Performance Metrics |
| 381 | + |
| 382 | +The framework supports high-performance operations: |
| 383 | +- **1,000+ requests/second** with 5 active plugins |
| 384 | +- **Sub-millisecond latency** for self-contained plugins |
| 385 | +- **Parallel execution** within priority bands |
| 386 | +- **Resource isolation** and timeout protection |
| 387 | + |
| 388 | +## Security Features |
| 389 | + |
| 390 | +- Input validation and sanitization |
| 391 | +- Timeout protection for external calls |
| 392 | +- Resource limits and quota enforcement |
| 393 | +- Error isolation between plugins |
| 394 | +- Comprehensive audit logging |
| 395 | +- Plugin configuration validation |
0 commit comments