Skip to content

Commit a447b89

Browse files
author
Teryl Taylor
committed
feat: add support for external plugins
Signed-off-by: Teryl Taylor <[email protected]>
1 parent 7cac6f8 commit a447b89

File tree

29 files changed

+913
-571
lines changed

29 files changed

+913
-571
lines changed

mcpgateway/config.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -557,9 +557,7 @@ def validate_database(self) -> None:
557557
db_dir.mkdir(parents=True)
558558

559559
# Validation patterns for safe display (configurable)
560-
validation_dangerous_html_pattern: str = (
561-
r"<(script|iframe|object|embed|link|meta|base|form|img|svg|video|audio|source|track|area|map|canvas|applet|frame|frameset|html|head|body|style)\b|</*(script|iframe|object|embed|link|meta|base|form|img|svg|video|audio|source|track|area|map|canvas|applet|frame|frameset|html|head|body|style)>"
562-
)
560+
validation_dangerous_html_pattern: str = r"<(script|iframe|object|embed|link|meta|base|form|img|svg|video|audio|source|track|area|map|canvas|applet|frame|frameset|html|head|body|style)\b|</*(script|iframe|object|embed|link|meta|base|form|img|svg|video|audio|source|track|area|map|canvas|applet|frame|frameset|html|head|body|style)>"
563561

564562
validation_dangerous_js_pattern: str = r"(?i)(?:^|\s|[\"'`<>=])(javascript:|vbscript:|data:\s*[^,]*[;\s]*(javascript|vbscript)|\bon[a-z]+\s*=|<\s*script\b)"
565563

mcpgateway/plugins/__init__.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,7 @@
1313
"""
1414

1515
from mcpgateway.plugins.framework.manager import PluginManager
16-
from mcpgateway.plugins.framework.models import PluginViolation
17-
from mcpgateway.plugins.framework.plugin_types import GlobalContext, PluginViolationError, PromptPosthookPayload, PromptPrehookPayload
16+
from mcpgateway.plugins.framework.models import GlobalContext, PluginViolation, PromptPosthookPayload, PromptPrehookPayload, ToolPostInvokePayload, ToolPreInvokePayload
17+
from mcpgateway.plugins.framework.errors import PluginViolationError, PluginError
1818

19-
__all__ = [
20-
"GlobalContext",
21-
"PluginManager",
22-
"PluginViolation",
23-
"PluginViolationError",
24-
"PromptPosthookPayload",
25-
"PromptPrehookPayload",
26-
]
19+
__all__ = ["GlobalContext", "PluginError", "PluginManager", "PluginViolation", "PluginViolationError", "PromptPosthookPayload", "PromptPrehookPayload", "ToolPostInvokePayload", "ToolPreInvokePayload"]

mcpgateway/plugins/framework/base.py

Lines changed: 36 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
import uuid
2121

2222
# First-Party
23-
from mcpgateway.plugins.framework.models import HookType, PluginCondition, PluginConfig, PluginMode
24-
from mcpgateway.plugins.framework.plugin_types import (
23+
from mcpgateway.plugins.framework.models import (
24+
HookType,
25+
PluginCondition,
26+
PluginConfig,
2527
PluginContext,
28+
PluginMode,
2629
PromptPosthookPayload,
2730
PromptPosthookResult,
2831
PromptPrehookPayload,
@@ -138,6 +141,9 @@ def conditions(self) -> list[PluginCondition] | None:
138141
"""
139142
return self._config.conditions
140143

144+
async def initialize(self) -> None:
145+
"""Initialize the plugin."""
146+
141147
async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult:
142148
"""Plugin hook run before a prompt is retrieved and rendered.
143149
@@ -177,18 +183,14 @@ async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginCo
177183
payload: The tool payload to be analyzed.
178184
context: Contextual information about the hook call.
179185
180-
Returns:
181-
ToolPreInvokeResult with processing status and modified payload.
182-
183-
Examples:
184-
>>> from mcpgateway.plugins.framework.plugin_types import ToolPreInvokePayload, PluginContext, GlobalContext
185-
>>> payload = ToolPreInvokePayload("calculator", {"operation": "add", "a": 5, "b": 3})
186-
>>> context = PluginContext(GlobalContext(request_id="123"))
187-
>>> # In async context:
188-
>>> # result = await plugin.tool_pre_invoke(payload, context)
186+
Raises:
187+
NotImplementedError: needs to be implemented by sub class.
189188
"""
190-
# Default pass-through implementation
191-
return ToolPreInvokeResult(continue_processing=True, modified_payload=payload)
189+
raise NotImplementedError(
190+
f"""'tool_pre_invoke' not implemented for plugin {self._config.name}
191+
of plugin type {type(self)}
192+
"""
193+
)
192194

193195
async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult:
194196
"""Plugin hook run after a tool is invoked.
@@ -197,18 +199,14 @@ async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: Plugin
197199
payload: The tool result payload to be analyzed.
198200
context: Contextual information about the hook call.
199201
200-
Returns:
201-
ToolPostInvokeResult with processing status and modified result.
202-
203-
Examples:
204-
>>> from mcpgateway.plugins.framework.plugin_types import ToolPostInvokePayload, PluginContext, GlobalContext
205-
>>> payload = ToolPostInvokePayload("calculator", {"result": 8, "status": "success"})
206-
>>> context = PluginContext(GlobalContext(request_id="123"))
207-
>>> # In async context:
208-
>>> # result = await plugin.tool_post_invoke(payload, context)
202+
Raises:
203+
NotImplementedError: needs to be implemented by sub class.
209204
"""
210-
# Default pass-through implementation
211-
return ToolPostInvokeResult(continue_processing=True, modified_payload=payload)
205+
raise NotImplementedError(
206+
f"""'tool_post_invoke' not implemented for plugin {self._config.name}
207+
of plugin type {type(self)}
208+
"""
209+
)
212210

213211
async def resource_pre_fetch(self, payload, context):
214212
"""Plugin hook run before a resource is fetched.
@@ -217,22 +215,14 @@ async def resource_pre_fetch(self, payload, context):
217215
payload: The resource payload to be analyzed.
218216
context: Contextual information about the hook call.
219217
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)
218+
Raises:
219+
NotImplementedError: needs to be implemented by sub class.
229220
"""
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)
221+
raise NotImplementedError(
222+
f"""'resource_pre_fetch' not implemented for plugin {self._config.name}
223+
of plugin type {type(self)}
224+
"""
225+
)
236226

237227
async def resource_post_fetch(self, payload, context):
238228
"""Plugin hook run after a resource is fetched.
@@ -241,24 +231,14 @@ async def resource_post_fetch(self, payload, context):
241231
payload: The resource content payload to be analyzed.
242232
context: Contextual information about the hook call.
243233
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)
234+
Raises:
235+
NotImplementedError: needs to be implemented by sub class.
255236
"""
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)
237+
raise NotImplementedError(
238+
f"""'resource_post_fetch' not implemented for plugin {self._config.name}
239+
of plugin type {type(self)}
240+
"""
241+
)
262242

263243
async def shutdown(self) -> None:
264244
"""Plugin cleanup code."""
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# -*- coding: utf-8 -*-
2+
"""Plugins constants file.
3+
4+
Copyright 2025
5+
SPDX-License-Identifier: Apache-2.0
6+
Authors: Teryl Taylor
7+
8+
This module stores a collection of plugin constants used throughout the framework.
9+
"""
10+
11+
# Model constants.
12+
# Specialized plugin types.
13+
EXTERNAL_PLUGIN_TYPE = "external"
14+
15+
# MCP related constants.
16+
PYTHON_SUFFIX = ".py"
17+
URL = "url"
18+
SCRIPT = "script"
19+
AFTER = "after"
20+
21+
NAME = "name"
22+
PYTHON = "python"
23+
PLUGIN_NAME = "plugin_name"
24+
PAYLOAD = "payload"
25+
CONTEXT = "context"
26+
GET_PLUGIN_CONFIG = "get_plugin_config"
27+
IGNORE_CONFIG_EXTERNAL = "ignore_config_external"
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# -*- coding: utf-8 -*-
2+
"""Pydantic models for plugins.
3+
4+
Copyright 2025
5+
SPDX-License-Identifier: Apache-2.0
6+
Authors: Teryl Taylor
7+
8+
This module implements the pydantic models associated with
9+
the base plugin layer including configurations, and contexts.
10+
"""
11+
12+
# First-Party
13+
from mcpgateway.plugins.framework.models import PluginErrorModel, PluginViolation
14+
15+
16+
class PluginViolationError(Exception):
17+
"""A plugin violation error.
18+
19+
Attributes:
20+
violation (PluginViolation): the plugin violation.
21+
message (str): the plugin violation reason.
22+
"""
23+
24+
def __init__(self, message: str, violation: PluginViolation | None = None):
25+
"""Initialize a plugin violation error.
26+
27+
Args:
28+
message: the reason for the violation error.
29+
violation: the plugin violation object details.
30+
"""
31+
self.message = message
32+
self.violation = violation
33+
super().__init__(self.message)
34+
35+
36+
class PluginError(Exception):
37+
"""A plugin error object for errors internal to the plugin.
38+
39+
Attributes:
40+
error (PluginErrorModel): the plugin error object.
41+
"""
42+
43+
def __init__(self, error: PluginErrorModel):
44+
"""Initialize a plugin violation error.
45+
46+
Args:
47+
error: the plugin error details.
48+
"""
49+
self.error = error
50+
super().__init__(self.error.message)
51+
52+
53+
def convert_exception_to_error(exception: Exception, plugin_name: str) -> PluginErrorModel:
54+
"""Converts an exception object into a PluginErrorModel. Primarily used for external plugin error handling.
55+
56+
Args:
57+
exception: The exception to be converted.
58+
plugin_name: The name of the plugin on which the exception occurred.
59+
60+
Returns:
61+
A plugin error pydantic object that can be sent over HTTP.
62+
"""
63+
return PluginErrorModel(message=repr(exception), plugin_name=plugin_name)

0 commit comments

Comments
 (0)