Skip to content

Commit 80ac298

Browse files
authored
Plugins now support resources pre/post hooks (#719)
* Plugins now support resources pre/post Signed-off-by: Mihai Criveti <[email protected]> * Plugins now support resources pre/post Signed-off-by: Mihai Criveti <[email protected]> * Plugins now support resources pre/post Signed-off-by: Mihai Criveti <[email protected]> * Plugins now support resources pre/post Signed-off-by: Mihai Criveti <[email protected]> * Plugins now support resources pre/post Signed-off-by: Mihai Criveti <[email protected]> * Plugins now support resources pre/post Signed-off-by: Mihai Criveti <[email protected]> --------- Signed-off-by: Mihai Criveti <[email protected]>
1 parent 8e6567b commit 80ac298

File tree

19 files changed

+2681
-36
lines changed

19 files changed

+2681
-36
lines changed

docs/docs/using/plugins/index.md

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Plugin Framework
22

33
!!! warning "Experimental Feature"
4-
The plugin framework is currently in **MVP stage** and marked as experimental. Only prompt hooks (`prompt_pre_fetch` and `prompt_post_fetch`) are implemented. Additional hooks for tools, resources, authentication, and server registration are planned for future releases.
4+
The plugin framework is currently in **MVP stage** and marked as experimental. Prompt, tool, and resource hooks are implemented. Additional hooks for authentication and server registration are planned for future releases.
55

66
## Overview
77

@@ -94,7 +94,7 @@ are defined as follows:
9494
| **description** | The description of the plugin configuration. | A plugin for replacing bad words. |
9595
| **version** | The version of the plugin configuration. | 0.1 |
9696
| **author** | The team that wrote the plugin. | MCP Context Forge |
97-
| **hooks** | A list of hooks for which the plugin will be executed. Supported hooks: "prompt_pre_fetch", "prompt_post_fetch", "tool_pre_invoke", "tool_post_invoke" | ["prompt_pre_fetch", "prompt_post_fetch", "tool_pre_invoke", "tool_post_invoke"] |
97+
| **hooks** | A list of hooks for which the plugin will be executed. Supported hooks: "prompt_pre_fetch", "prompt_post_fetch", "tool_pre_invoke", "tool_post_invoke", "resource_pre_fetch", "resource_post_fetch" | ["prompt_pre_fetch", "prompt_post_fetch", "tool_pre_invoke", "tool_post_invoke", "resource_pre_fetch", "resource_post_fetch"] |
9898
| **tags** | Descriptive keywords that make the configuration searchable. | ["security", "filter"] |
9999
| **mode** | Mode of operation of the plugin. - enforce (stops during a violation), permissive (audits a violation but doesn't stop), disabled (disabled) | permissive |
100100
| **priority** | The priority in which the plugin will run - 0 is higher priority | 100 |
@@ -152,6 +152,7 @@ Users may only want plugins to be invoked on specific servers, tools, and prompt
152152
| **server_ids** | The list of MCP servers on which the plugin will trigger |
153153
| **tools** | The list of tools on which the plugin will be applied. |
154154
| **prompts** | The list of prompts on which the plugin will be applied. |
155+
| **resources** | The list of resource URIs on which the plugin will be applied. |
155156
| **user_patterns** | The list of users on which the plugin will be applied. |
156157
| **content_types** | The list of content types on which the plugin will trigger. |
157158

@@ -165,6 +166,8 @@ Currently implemented hooks:
165166
| `prompt_post_fetch` | After prompt rendering | Filter/transform rendered prompts |
166167
| `tool_pre_invoke` | Before tool invocation | Validate/modify tool arguments, block dangerous operations |
167168
| `tool_post_invoke` | After tool execution | Filter/transform tool results, audit tool usage |
169+
| `resource_pre_fetch` | Before resource fetching | Validate URIs, check protocols, add metadata |
170+
| `resource_post_fetch` | After resource fetching | Filter content, redact sensitive data, validate size |
168171

169172
### Tool Hooks Details
170173

@@ -180,9 +183,23 @@ Example use cases:
180183
- Input validation and sanitization
181184
- Output filtering and transformation
182185

186+
### Resource Hooks Details
187+
188+
The resource hooks enable plugins to intercept and modify resource fetching:
189+
190+
- **`resource_pre_fetch`**: Receives the resource URI and metadata before fetching. Can modify the URI, add metadata, or block the fetch entirely.
191+
- **`resource_post_fetch`**: Receives the resource content after fetching. Can modify the content, redact sensitive information, or block it from being returned.
192+
193+
Example use cases:
194+
- Protocol validation (block non-HTTPS resources)
195+
- Domain blocklisting/allowlisting
196+
- Content size limiting
197+
- Sensitive data redaction
198+
- Content transformation and filtering
199+
- Resource caching metadata
200+
183201
Planned hooks (not yet implemented):
184202

185-
- `resource_pre_fetch` / `resource_post_fetch` - Resource content filtering
186203
- `server_pre_register` / `server_post_register` - Server validation
187204
- `auth_pre_check` / `auth_post_check` - Custom authentication
188205
- `federation_pre_sync` / `federation_post_sync` - Gateway federation
@@ -203,7 +220,11 @@ from mcpgateway.plugins.framework.plugin_types import (
203220
ToolPreInvokePayload,
204221
ToolPreInvokeResult,
205222
ToolPostInvokePayload,
206-
ToolPostInvokeResult
223+
ToolPostInvokeResult,
224+
ResourcePreFetchPayload,
225+
ResourcePreFetchResult,
226+
ResourcePostFetchPayload,
227+
ResourcePostFetchResult
207228
)
208229
209230
class MyPlugin(Plugin):
@@ -326,6 +347,62 @@ class MyPlugin(Plugin):
326347
327348
return ToolPostInvokeResult()
328349
350+
async def resource_pre_fetch(
351+
self,
352+
payload: ResourcePreFetchPayload,
353+
context: PluginContext
354+
) -> ResourcePreFetchResult:
355+
"""Process resource before fetching."""
356+
357+
# Access resource URI and metadata
358+
uri = payload.uri
359+
metadata = payload.metadata
360+
361+
# Example: Block certain protocols
362+
from urllib.parse import urlparse
363+
parsed = urlparse(uri)
364+
if parsed.scheme not in ["http", "https", "file"]:
365+
return ResourcePreFetchResult(
366+
continue_processing=False,
367+
violation=PluginViolation(
368+
plugin_name=self.name,
369+
description=f"Protocol {parsed.scheme} not allowed",
370+
violation_code="PROTOCOL_BLOCKED",
371+
details={"uri": uri, "protocol": parsed.scheme}
372+
)
373+
)
374+
375+
# Example: Add metadata
376+
metadata["validated_by"] = self.name
377+
return ResourcePreFetchResult(
378+
modified_payload=ResourcePreFetchPayload(uri, metadata)
379+
)
380+
381+
async def resource_post_fetch(
382+
self,
383+
payload: ResourcePostFetchPayload,
384+
context: PluginContext
385+
) -> ResourcePostFetchResult:
386+
"""Process resource after fetching."""
387+
388+
# Access resource content
389+
uri = payload.uri
390+
content = payload.content
391+
392+
# Example: Redact sensitive patterns from text content
393+
if hasattr(content, 'text') and content.text:
394+
# Redact email addresses
395+
import re
396+
content.text = re.sub(
397+
r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
398+
'[EMAIL_REDACTED]',
399+
content.text
400+
)
401+
402+
return ResourcePostFetchResult(
403+
modified_payload=ResourcePostFetchPayload(uri, content)
404+
)
405+
329406
async def shutdown(self):
330407
"""Cleanup when plugin shuts down."""
331408
# Close connections, save state, etc.

mcpgateway/main.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import time
3333
from typing import Any, AsyncIterator, Dict, List, Optional, Union
3434
from urllib.parse import urlparse, urlunparse
35+
import uuid
3536

3637
# Third-Party
3738
from fastapi import APIRouter, Body, Depends, FastAPI, HTTPException, Request, status, WebSocket, WebSocketDisconnect
@@ -1499,12 +1500,13 @@ async def create_resource(
14991500

15001501

15011502
@resource_router.get("/{uri:path}")
1502-
async def read_resource(uri: str, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> ResourceContent:
1503+
async def read_resource(uri: str, request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> ResourceContent:
15031504
"""
1504-
Read a resource by its URI.
1505+
Read a resource by its URI with plugin support.
15051506
15061507
Args:
15071508
uri (str): URI of the resource.
1509+
request (Request): FastAPI request object for context.
15081510
db (Session): Database session.
15091511
user (str): Authenticated user.
15101512
@@ -1514,14 +1516,23 @@ async def read_resource(uri: str, db: Session = Depends(get_db), user: str = Dep
15141516
Raises:
15151517
HTTPException: If the resource cannot be found or read.
15161518
"""
1517-
logger.debug(f"User {user} requested resource with URI {uri}")
1519+
# Get request ID from headers or generate one
1520+
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
1521+
server_id = request.headers.get("X-Server-ID")
1522+
1523+
logger.debug(f"User {user} requested resource with URI {uri} (request_id: {request_id})")
1524+
1525+
# Check cache
15181526
if cached := resource_cache.get(uri):
15191527
return cached
1528+
15201529
try:
1521-
content: ResourceContent = await resource_service.read_resource(db, uri)
1530+
# Call service with context for plugin support
1531+
content: ResourceContent = await resource_service.read_resource(db, uri, request_id=request_id, user=user, server_id=server_id)
15221532
except (ResourceNotFoundError, ResourceError) as exc:
15231533
# Translate to FastAPI HTTP error
15241534
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
1535+
15251536
resource_cache.set(uri, content)
15261537
return content
15271538

@@ -1759,10 +1770,12 @@ async def get_prompt(
17591770
except Exception as ex:
17601771
error_message = str(ex)
17611772
logger.error(f"Could not retrieve prompt {name}: {ex}")
1762-
if isinstance(ex, (ValueError, PromptError)):
1763-
result = JSONResponse(content={"message": "Prompt execution arguments contains HTML tags that may cause security issues"}, status_code=422)
1764-
elif isinstance(ex, PluginViolationError):
1765-
result = JSONResponse(content={"message": "Prompt execution arguments contains HTML tags that may cause security issues", "details": ex.message}, status_code=422)
1773+
if isinstance(ex, PluginViolationError):
1774+
# Return the actual plugin violation message
1775+
result = JSONResponse(content={"message": ex.message, "details": str(ex.violation) if hasattr(ex, "violation") else None}, status_code=422)
1776+
elif isinstance(ex, (ValueError, PromptError)):
1777+
# Return the actual error message
1778+
result = JSONResponse(content={"message": str(ex)}, status_code=422)
17661779
else:
17671780
raise
17681781

mcpgateway/plugins/framework/base.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
Copyright 2025
55
SPDX-License-Identifier: Apache-2.0
6-
Authors: Teryl Taylor
6+
Authors: Teryl Taylor, Mihai Criveti
77
88
This module implements the base plugin object.
99
It supports pre and post hooks AI safety, security and business processing
@@ -210,6 +210,56 @@ async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: Plugin
210210
# Default pass-through implementation
211211
return ToolPostInvokeResult(continue_processing=True, modified_payload=payload)
212212

213+
async def resource_pre_fetch(self, payload, context):
214+
"""Plugin hook run before a resource is fetched.
215+
216+
Args:
217+
payload: The resource payload to be analyzed.
218+
context: Contextual information about the hook call.
219+
220+
Returns:
221+
ResourcePreFetchResult with processing status and modified payload.
222+
223+
Examples:
224+
>>> from mcpgateway.plugins.framework.plugin_types import ResourcePreFetchPayload, PluginContext, GlobalContext
225+
>>> payload = ResourcePreFetchPayload("file:///data.txt", {"cache": True})
226+
>>> context = PluginContext(GlobalContext(request_id="123"))
227+
>>> # In async context:
228+
>>> # result = await plugin.resource_pre_fetch(payload, context)
229+
"""
230+
# Import here to avoid circular dependency
231+
# First-Party
232+
from mcpgateway.plugins.framework.plugin_types import ResourcePreFetchResult
233+
234+
# Default pass-through implementation
235+
return ResourcePreFetchResult(continue_processing=True, modified_payload=payload)
236+
237+
async def resource_post_fetch(self, payload, context):
238+
"""Plugin hook run after a resource is fetched.
239+
240+
Args:
241+
payload: The resource content payload to be analyzed.
242+
context: Contextual information about the hook call.
243+
244+
Returns:
245+
ResourcePostFetchResult with processing status and modified content.
246+
247+
Examples:
248+
>>> from mcpgateway.plugins.framework.plugin_types import ResourcePostFetchPayload, PluginContext, GlobalContext
249+
>>> from mcpgateway.models import ResourceContent
250+
>>> content = ResourceContent(type="resource", uri="file:///data.txt", text="Data")
251+
>>> payload = ResourcePostFetchPayload("file:///data.txt", content)
252+
>>> context = PluginContext(GlobalContext(request_id="123"))
253+
>>> # In async context:
254+
>>> # result = await plugin.resource_post_fetch(payload, context)
255+
"""
256+
# Import here to avoid circular dependency
257+
# First-Party
258+
from mcpgateway.plugins.framework.plugin_types import ResourcePostFetchResult
259+
260+
# Default pass-through implementation
261+
return ResourcePostFetchResult(continue_processing=True, modified_payload=payload)
262+
213263
async def shutdown(self) -> None:
214264
"""Plugin cleanup code."""
215265

mcpgateway/plugins/framework/loader/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
Copyright 2025
55
SPDX-License-Identifier: Apache-2.0
6-
Authors: Teryl Taylor
6+
Authors: Teryl Taylor, Mihai Criveti
77
88
This module loads configurations for plugins.
99
"""

mcpgateway/plugins/framework/loader/plugin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
Copyright 2025
55
SPDX-License-Identifier: Apache-2.0
6-
Authors: Teryl Taylor
6+
Authors: Teryl Taylor, Mihai Criveti
77
88
This module implements the plugin loader.
99
"""

0 commit comments

Comments
 (0)