Skip to content

Commit 3cde1e8

Browse files
author
Teryl Taylor
committed
feat: add support for external plugins
Signed-off-by: Teryl Taylor <[email protected]>
1 parent 15fd2bd commit 3cde1e8

File tree

30 files changed

+918
-576
lines changed

30 files changed

+918
-576
lines changed

docs/docs/architecture/roadmap.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
- ✅ [**#159**](https://github.com/IBM/mcp-context-forge/issues/159) - Add auto activation of mcp-server, when it goes up back again
8181
- ✅ [**#154**](https://github.com/IBM/mcp-context-forge/issues/154) - Export connection strings to various clients from UI and via API
8282
- ✅ [**#135**](https://github.com/IBM/mcp-context-forge/issues/135) - Dynamic UI Picker for Tool, Resource, and Prompt Associations
83-
- ✅ [**#116**](https://github.com/IBM/mcp-context-forge/issues/116) - Namespace Composite Key & UUIDs for Tool Identity
83+
- ✅ [**#116**](https://github.com/IBM/mcp-context-forge/issues/116) - Namespace Composite Key & UUIDs for Tool Identity
8484
- ✅ [**#100**](https://github.com/IBM/mcp-context-forge/issues/100) - Add path parameter or replace value in input payload for a REST API?
8585
- ✅ [**#26**](https://github.com/IBM/mcp-context-forge/issues/26) - Add dark mode toggle to Admin UI
8686

@@ -259,7 +259,7 @@
259259
- [**#276**](https://github.com/IBM/mcp-context-forge/issues/276) - Terraform Module – "mcp-gateway-ibm-cloud" supporting IKS, ROKS, Code Engine targets
260260
- [**#275**](https://github.com/IBM/mcp-context-forge/issues/275) - Terraform Module - "mcp-gateway-gcp" supporting GKE and Cloud Run
261261
- [**#274**](https://github.com/IBM/mcp-context-forge/issues/274) - Terraform Module - "mcp-gateway-azure" supporting AKS and ACA
262-
- [**#273**](https://github.com/IBM/mcp-context-forge/issues/273) - Terraform Module - mcp-gateway-aws supporting both EKS and ECS Fargate targets
262+
- [**#273**](https://github.com/IBM/mcp-context-forge/issues/273) - Terraform Module - "mcp-gateway-aws" supporting both EKS and ECS Fargate targets
263263
- [**#258**](https://github.com/IBM/mcp-context-forge/issues/258) - Universal Client Retry Mechanisms with Exponential Backoff & Random Jitter
264264
- [**#234**](https://github.com/IBM/mcp-context-forge/issues/234) - 🧠 Protocol Feature – Elicitation Support (MCP 2025-06-18)
265265
- [**#217**](https://github.com/IBM/mcp-context-forge/issues/217) - Graceful-Shutdown Hooks for API & Worker Containers (SIGTERM-safe rollouts, DB-pool cleanup, zero-drop traffic)
@@ -276,7 +276,7 @@
276276
- ✅ [**#666**](https://github.com/IBM/mcp-context-forge/issues/666) - [Bug]:Vague/Unclear Error Message "Validation Failed" When Adding a REST Tool
277277
- ✅ [**#661**](https://github.com/IBM/mcp-context-forge/issues/661) - Database migration runs during doctest execution
278278
- ✅ [**#649**](https://github.com/IBM/mcp-context-forge/issues/649) - Duplicate Gateway Registration with Equivalent URLs Bypasses Uniqueness Check
279-
- ✅ [**#646**](https://github.com/IBM/mcp-context-forge/issues/646) - MCP Server/Federated Gateway Registration is failing
279+
- ✅ [**#646**](https://github.com/IBM/mcp-context-forge/issues/646) - MCP Server/Federated Gateway Registration is failing
280280
- [**#625**](https://github.com/IBM/mcp-context-forge/issues/625) - Gateway unable to register gateway or call tools on MacOS
281281
- [**#587**](https://github.com/IBM/mcp-context-forge/issues/587) - REST Tool giving error
282282
- ✅ [**#557**](https://github.com/IBM/mcp-context-forge/issues/557) - [BUG] Cleanup tool descriptions to remove newlines and truncate text
@@ -337,7 +337,7 @@
337337
- [**#270**](https://github.com/IBM/mcp-context-forge/issues/270) - MCP Server – Go Implementation ("libreoffice-server")
338338
- [**#269**](https://github.com/IBM/mcp-context-forge/issues/269) - MCP Server - Go Implementation (LaTeX Service)
339339
- [**#263**](https://github.com/IBM/mcp-context-forge/issues/263) - Sample Agent - CrewAI Integration (OpenAI & A2A Endpoints)
340-
- [**#262**](https://github.com/IBM/mcp-context-forge/issues/262) - Sample Agent - LangChain Integration (OpenAI & A2A Endpoints)
340+
- [**#262**](https://github.com/IBM/mcp-context-forge/issues/262) - Sample Agent - LangChain Integration (OpenAI & A2A Endpoints)
341341
- [**#218**](https://github.com/IBM/mcp-context-forge/issues/218) - Prometheus Metrics Instrumentation using prometheus-fastapi-instrumentator
342342
- [**#186**](https://github.com/IBM/mcp-context-forge/issues/186) - Granular Configuration Export & Import (via UI & API)
343343
- [**#185**](https://github.com/IBM/mcp-context-forge/issues/185) - Portable Configuration Export & Import CLI (registry, virtual servers and prompts)
@@ -549,4 +549,4 @@
549549
-**Completed** - Issue has been resolved and closed
550550

551551
!!! tip "Contributing"
552-
Want to contribute to any of these features? Check out the individual GitHub issues for more details and discussion!
552+
Want to contribute to any of these features? Check out the individual GitHub issues for more details and discussion!

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)