Skip to content

Commit 3640336

Browse files
teryltTeryl Tayloraraujofcrivetimihai
authored
feat: add http hooks (#1394)
* feat: add http hooks. Signed-off-by: Teryl Taylor <[email protected]> * feat: add permissions hook for rbac. Signed-off-by: Teryl Taylor <[email protected]> * fix: http payload object intialization and pylint issues Signed-off-by: Frederico Araujo <[email protected]> * docs: updated docs with new hooks and examples. Signed-off-by: Teryl Taylor <[email protected]> * fix: linting and test case issues Signed-off-by: Teryl Taylor <[email protected]> * fix: correct mock patching in HTTP auth integration test The test was trying to patch get_plugin_manager in the wrong location. Fixed to only patch mcpgateway.auth.get_plugin_manager where it's actually used. Signed-off-by: Mihai Criveti <[email protected]> * fix: move uuid import to top level and remove unnecessary else - Move uuid import from function scope to module scope - Remove unnecessary else after return in permission check logic Signed-off-by: Mihai Criveti <[email protected]> * fix: add missing newline at end of file Signed-off-by: Mihai Criveti <[email protected]> * fix: use keyword argument for HttpHeaderPayload in example plugins Change HttpHeaderPayload(headers) to HttpHeaderPayload(root=headers) to match production code pattern and satisfy pylint-pydantic. Fixes E1121 too-many-function-args errors in: - plugins/examples/custom_auth_example/custom_auth.py (2 places) - plugins/examples/simple_token_auth/simple_token_auth.py (2 places) Signed-off-by: Mihai Criveti <[email protected]> --------- Signed-off-by: Teryl Taylor <[email protected]> Signed-off-by: Frederico Araujo <[email protected]> Signed-off-by: Mihai Criveti <[email protected]> Co-authored-by: Teryl Taylor <[email protected]> Co-authored-by: Frederico Araujo <[email protected]> Co-authored-by: Mihai Criveti <[email protected]>
1 parent 5d0fc3b commit 3640336

File tree

27 files changed

+5802
-300
lines changed

27 files changed

+5802
-300
lines changed

docs/docs/architecture/plugins.md

Lines changed: 177 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ Details of each field are below:
210210

211211
Available hook values for the `hooks` field:
212212

213+
**MCP Protocol Hooks:**
214+
213215
| Hook Value | Description | Timing |
214216
|------------|-------------|--------|
215217
| `"prompt_pre_fetch"` | Process prompt requests before template processing | Before prompt template retrieval |
@@ -219,6 +221,17 @@ Available hook values for the `hooks` field:
219221
| `"resource_pre_fetch"` | Process resource requests before fetching | Before resource retrieval |
220222
| `"resource_post_fetch"` | Process resource content after loading | After resource content loading |
221223

224+
**HTTP Authentication & Middleware Hooks:**
225+
226+
| Hook Value | Description | Timing |
227+
|------------|-------------|--------|
228+
| `"http_pre_request"` | Transform HTTP headers before processing | Before authentication |
229+
| `"http_auth_resolve_user"` | Implement custom authentication | During user authentication |
230+
| `"http_auth_check_permission"` | Custom permission checking logic | Before RBAC checks |
231+
| `"http_post_request"` | Process responses and add audit headers | After request completion |
232+
233+
See the [HTTP Authentication Hooks Guide](../../using/plugins/http-auth-hooks.md) for detailed implementation examples.
234+
222235
#### Plugin Modes
223236

224237
Available values for the `mode` field:
@@ -438,57 +451,101 @@ mcpgateway/plugins/framework/
438451
439452
### Base Plugin Class
440453
441-
The base plugin class, of which developers subclass and implement the hooks that are important for their plugins. Hook points are functions that interpose on existing MCP and agent-based functionality.
454+
The `Plugin` class is an **Abstract Base Class (ABC)** that provides the foundation for all plugins. Developers must subclass it and implement only the hooks they need using one of three registration patterns.
442455
443456
```python
444-
class Plugin:
445-
"""Base plugin class for self-contained, in-process plugins"""
457+
from abc import ABC
458+
459+
class Plugin(ABC):
460+
"""Abstract base class for self-contained, in-process plugins.
461+
462+
Plugins must inherit from this class and implement at least one hook method.
463+
Three hook registration patterns are supported:
464+
465+
1. Convention-based: Name your method to match the hook type
466+
2. Decorator-based: Use @hook decorator with custom method names
467+
3. Custom hooks: Define new hook types with @hook decorator
468+
"""
446469
447470
def __init__(self, config: PluginConfig) -> None:
448-
"""Initialize plugin with configuration"""
471+
"""Initialize plugin with configuration."""
449472
450473
@property
451-
def name(self) -> str: ...
452-
"""Plugin name"""
474+
def name(self) -> str:
475+
"""Plugin name from configuration."""
453476
454477
@property
455-
def priority(self) -> int: ...
456-
"""Plugin execution priority (lower = higher priority)"""
478+
def priority(self) -> int:
479+
"""Plugin execution priority (lower number = higher priority)."""
457480
458481
@property
459-
def mode(self) -> PluginMode: ...
460-
"""Plugin execution mode (enforce/permissive/disabled)"""
482+
def mode(self) -> PluginMode:
483+
"""Plugin execution mode (enforce/enforce_ignore_error/permissive/disabled)."""
461484
462485
@property
463-
def hooks(self) -> list[HookType]: ...
464-
"""Hook points where plugin executes"""
486+
def hooks(self) -> list[str]:
487+
"""Hook points where plugin executes (discovered via introspection)."""
465488
466489
@property
467-
def conditions(self) -> list[PluginCondition] | None: ...
468-
"""Conditions for plugin execution"""
490+
def conditions(self) -> list[PluginCondition] | None:
491+
"""Conditions for plugin execution (optional)."""
469492
470-
async def initialize(self) -> None: ...
471-
"""Initialize plugin resources"""
493+
# Optional lifecycle methods
494+
async def initialize(self) -> None:
495+
"""Initialize plugin resources (called when plugin is loaded)."""
472496
473-
async def shutdown(self) -> None: ...
474-
"""Cleanup plugin resources"""
497+
async def shutdown(self) -> None:
498+
"""Cleanup plugin resources (called on shutdown)."""
499+
```
475500

476-
# Hook methods (implemented by subclasses)
477-
async def prompt_pre_fetch(self, payload: PromptPrehookPayload,
478-
context: PluginContext) -> PromptPrehookResult: ...
479-
async def prompt_post_fetch(self, payload: PromptPosthookPayload,
480-
context: PluginContext) -> PromptPosthookResult: ...
481-
async def tool_pre_invoke(self, payload: ToolPreInvokePayload,
482-
context: PluginContext) -> ToolPreInvokeResult: ...
483-
async def tool_post_invoke(self, payload: ToolPostInvokePayload,
484-
context: PluginContext) -> ToolPostInvokeResult: ...
485-
async def resource_pre_fetch(self, payload: ResourcePreFetchPayload,
486-
context: PluginContext) -> ResourcePreFetchResult: ...
487-
async def resource_post_fetch(self, payload: ResourcePostFetchPayload,
488-
context: PluginContext) -> ResourcePostFetchResult: ...
489-
# ... additional hook methods
501+
**Hook Implementation Patterns:**
502+
503+
Plugins implement hooks using one of three patterns - **they do not need to implement all hooks**, only the ones they need:
504+
505+
**Pattern 1: Convention-Based** (method name matches hook type)
506+
```python
507+
class MyPlugin(Plugin):
508+
async def tool_pre_invoke(
509+
self,
510+
payload: ToolPreInvokePayload,
511+
context: PluginContext
512+
) -> ToolPreInvokeResult:
513+
# Implementation
514+
pass
515+
```
516+
517+
**Pattern 2: Decorator-Based** (custom method names)
518+
```python
519+
from mcpgateway.plugins.framework.decorator import hook
520+
521+
class MyPlugin(Plugin):
522+
@hook(ToolHookType.TOOL_POST_INVOKE)
523+
async def my_custom_handler(
524+
self,
525+
payload: ToolPostInvokePayload,
526+
context: PluginContext
527+
) -> ToolPostInvokeResult:
528+
# Implementation
529+
pass
490530
```
491531

532+
**Pattern 3: Custom Hooks** (new hook types)
533+
```python
534+
from mcpgateway.plugins.framework.decorator import hook
535+
536+
class MyPlugin(Plugin):
537+
@hook("email_pre_send", EmailPayload, EmailResult)
538+
async def validate_email(
539+
self,
540+
payload: EmailPayload,
541+
context: PluginContext
542+
) -> EmailResult:
543+
# Implementation
544+
pass
545+
```
546+
547+
See [Plugin Development Guide](../../using/plugins/) for detailed examples and best practices
548+
492549
### Plugin Manager
493550

494551
The Plugin Manager loads configured plugins and executes them at their designated hook points based on a plugin's priority.
@@ -520,14 +577,61 @@ class PluginManager:
520577
def get_plugin(self, name: str) -> Optional[Plugin]:
521578
"""Get plugin by name"""
522579

523-
# Hook execution methods
524-
async def prompt_pre_fetch(self, payload: PromptPrehookPayload,
525-
global_context: GlobalContext, ...) -> tuple[PromptPrehookResult, PluginContextTable]: ...
526-
async def prompt_post_fetch(self, payload: PromptPosthookPayload, ...) -> tuple[PromptPosthookResult, PluginContextTable]: ...
527-
async def tool_pre_invoke(self, payload: ToolPreInvokePayload, ...) -> tuple[ToolPreInvokeResult, PluginContextTable]: ...
528-
async def tool_post_invoke(self, payload: ToolPostInvokePayload, ...) -> tuple[ToolPostInvokeResult, PluginContextTable]: ...
529-
async def resource_pre_fetch(self, payload: ResourcePreFetchPayload, ...) -> tuple[ResourcePreFetchResult, PluginContextTable]: ...
530-
async def resource_post_fetch(self, payload: ResourcePostFetchPayload, ...) -> tuple[ResourcePostFetchResult, PluginContextTable]: ...
580+
# Unified hook invocation API
581+
async def invoke_hook(
582+
self,
583+
hook_type: str,
584+
payload: PluginPayload,
585+
global_context: GlobalContext,
586+
**kwargs
587+
) -> tuple[PluginResult, PluginContextTable]:
588+
"""
589+
Invoke a specific hook type with the given payload.
590+
591+
This is the primary API for executing plugins at hook points.
592+
Plugins are executed in priority order with conditional filtering.
593+
594+
Args:
595+
hook_type: String identifier for the hook (e.g., HttpHookType.HTTP_AUTH_RESOLVE_USER,
596+
ToolHookType.TOOL_PRE_INVOKE, PromptHookType.PROMPT_PRE_FETCH)
597+
payload: Hook-specific payload (e.g., HttpAuthResolveUserPayload,
598+
ToolPreInvokePayload, PromptPrehookPayload)
599+
global_context: Shared request context across all plugins
600+
**kwargs: Additional hook-specific parameters
601+
602+
Returns:
603+
tuple[PluginResult, PluginContextTable]: Combined plugin result and context table
604+
"""
605+
...
606+
```
607+
608+
**Usage Example:**
609+
610+
```python
611+
# Invoke HTTP authentication hook
612+
result, contexts = await plugin_manager.invoke_hook(
613+
HttpHookType.HTTP_AUTH_RESOLVE_USER,
614+
payload=HttpAuthResolveUserPayload(
615+
credentials=credentials,
616+
headers=HttpHeaderPayload(headers),
617+
client_host=client_host,
618+
),
619+
global_context=GlobalContext(
620+
request_id=request_id,
621+
server_id=None,
622+
tenant_id=None,
623+
)
624+
)
625+
626+
# Invoke MCP protocol hook
627+
result, contexts = await plugin_manager.invoke_hook(
628+
HookType.TOOL_PRE_INVOKE,
629+
payload=ToolPreInvokePayload(
630+
name=tool_name,
631+
args=tool_args,
632+
),
633+
global_context=global_context
634+
)
531635
```
532636

533637
### Plugin Registry
@@ -645,15 +749,33 @@ class PluginMode(str, Enum):
645749
PERMISSIVE = "permissive" # Log violations but allow continuation
646750
DISABLED = "disabled" # Plugin loaded but not executed
647751

648-
class HookType(str, Enum):
649-
"""Available hook points in MCP request lifecycle"""
752+
class PromptHookType(str, Enum):
753+
"""Prompt lifecycle hook points"""
650754
PROMPT_PRE_FETCH = "prompt_pre_fetch" # Before prompt retrieval
651755
PROMPT_POST_FETCH = "prompt_post_fetch" # After prompt rendering
756+
757+
class ToolHookType(str, Enum):
758+
"""Tool invocation hook points"""
652759
TOOL_PRE_INVOKE = "tool_pre_invoke" # Before tool execution
653760
TOOL_POST_INVOKE = "tool_post_invoke" # After tool execution
761+
762+
class ResourceHookType(str, Enum):
763+
"""Resource fetching hook points"""
654764
RESOURCE_PRE_FETCH = "resource_pre_fetch" # Before resource fetching
655765
RESOURCE_POST_FETCH = "resource_post_fetch" # After resource retrieval
656766

767+
class HttpHookType(str, Enum):
768+
"""HTTP authentication and middleware hook points"""
769+
HTTP_PRE_REQUEST = "http_pre_request" # Before authentication
770+
HTTP_AUTH_RESOLVE_USER = "http_auth_resolve_user" # Custom authentication
771+
HTTP_AUTH_CHECK_PERMISSION = "http_auth_check_permission" # Permission checking
772+
HTTP_POST_REQUEST = "http_post_request" # After request completion
773+
774+
class AgentHookType(str, Enum):
775+
"""Agent-to-Agent hook points"""
776+
AGENT_PRE_INVOKE = "agent_pre_invoke" # Before agent invocation
777+
AGENT_POST_INVOKE = "agent_post_invoke" # After agent completion
778+
657779
class TransportType(str, Enum):
658780
"""Supported MCP transport protocols"""
659781
SSE = "sse" # Server-Sent Events
@@ -1569,21 +1691,26 @@ plugins:
15691691

15701692
Legend: ✅ = Completed | 🚧 = In Progress | 📋 = Planned
15711693

1572-
### Planned Hook Points
1694+
### Completed Hook Points
15731695

15741696
```python
1575-
# HTTP hooks
1576-
HTTP_PRE_FORWARDING_CALL = "http_pre_forwarding_call" # Before HTTP forwarding
1577-
HTTP_POST_FORWARDING_CALL = "http_post_forwarding_call" # After HTTP forwarding
1697+
# HTTP Authentication & Middleware Hooks (✅ Implemented)
1698+
class HttpHookType(str, Enum):
1699+
HTTP_PRE_REQUEST = "http_pre_request" # Transform headers before authentication
1700+
HTTP_AUTH_RESOLVE_USER = "http_auth_resolve_user" # Custom user authentication
1701+
HTTP_AUTH_CHECK_PERMISSION = "http_auth_check_permission" # Custom permission checking
1702+
HTTP_POST_REQUEST = "http_post_request" # Response processing and audit logging
1703+
```
15781704

1705+
See the [HTTP Authentication Hooks Guide](../../using/plugins/http-auth-hooks.md) for implementation details and the [Simple Token Auth Plugin](https://github.com/IBM/mcp-context-forge/tree/main/plugins/examples/simple_token_auth) for a complete example.
1706+
1707+
### Planned Hook Points
1708+
1709+
```python
15791710
# Server lifecycle hooks
15801711
SERVER_PRE_REGISTER = "server_pre_register" # Server attestation and validation
15811712
SERVER_POST_REGISTER = "server_post_register" # Post-registration processing
15821713
1583-
# Authentication hooks
1584-
AUTH_PRE_CHECK = "auth_pre_check" # Custom authentication logic
1585-
AUTH_POST_CHECK = "auth_post_check" # Post-authentication processing
1586-
15871714
# Federation hooks
15881715
FEDERATION_PRE_SYNC = "federation_pre_sync" # Pre-federation validation
15891716
FEDERATION_POST_SYNC = "federation_post_sync" # Post-federation processing

0 commit comments

Comments
 (0)