Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docker-compose.phoenix-simple.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Simplified Phoenix Observability Stack for MCP Gateway
#
#
# Usage:
# Start Phoenix: docker-compose -f docker-compose.phoenix-simple.yml up -d
# Stop Phoenix: docker-compose -f docker-compose.phoenix-simple.yml down
Expand Down Expand Up @@ -34,4 +34,4 @@ services:

volumes:
phoenix-data:
driver: local
driver: local
2 changes: 1 addition & 1 deletion docker-compose.with-phoenix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ services:

volumes:
phoenix-data:
driver: local
driver: local
4 changes: 1 addition & 3 deletions mcpgateway/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,9 +557,7 @@ def validate_database(self) -> None:
db_dir.mkdir(parents=True)

# Validation patterns for safe display (configurable)
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)>"
)
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)>"

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

Expand Down
13 changes: 3 additions & 10 deletions mcpgateway/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,7 @@
"""

from mcpgateway.plugins.framework.manager import PluginManager
from mcpgateway.plugins.framework.models import PluginViolation
from mcpgateway.plugins.framework.plugin_types import GlobalContext, PluginViolationError, PromptPosthookPayload, PromptPrehookPayload
from mcpgateway.plugins.framework.models import GlobalContext, PluginViolation, PromptPosthookPayload, PromptPrehookPayload, ToolPostInvokePayload, ToolPreInvokePayload
from mcpgateway.plugins.framework.errors import PluginViolationError, PluginError

__all__ = [
"GlobalContext",
"PluginManager",
"PluginViolation",
"PluginViolationError",
"PromptPosthookPayload",
"PromptPrehookPayload",
]
__all__ = ["GlobalContext", "PluginError", "PluginManager", "PluginViolation", "PluginViolationError", "PromptPosthookPayload", "PromptPrehookPayload", "ToolPostInvokePayload", "ToolPreInvokePayload"]
101 changes: 45 additions & 56 deletions mcpgateway/plugins/framework/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
import uuid

# First-Party
from mcpgateway.plugins.framework.models import HookType, PluginCondition, PluginConfig, PluginMode
from mcpgateway.plugins.framework.plugin_types import (
from mcpgateway.plugins.framework.models import (
HookType,
PluginCondition,
PluginConfig,
PluginContext,
PluginMode,
PromptPosthookPayload,
PromptPosthookResult,
PromptPrehookPayload,
Expand Down Expand Up @@ -93,6 +96,15 @@ def priority(self) -> int:
"""
return self._config.priority

@property
def config(self) -> PluginConfig:
"""Return the plugin's configuration.

Returns:
Plugin's configuration.
"""
return self._config

@property
def mode(self) -> PluginMode:
"""Return the plugin's mode.
Expand Down Expand Up @@ -138,6 +150,9 @@ def conditions(self) -> list[PluginCondition] | None:
"""
return self._config.conditions

async def initialize(self) -> None:
"""Initialize the plugin."""

async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult:
"""Plugin hook run before a prompt is retrieved and rendered.

Expand Down Expand Up @@ -177,18 +192,14 @@ async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginCo
payload: The tool payload to be analyzed.
context: Contextual information about the hook call.

Returns:
ToolPreInvokeResult with processing status and modified payload.

Examples:
>>> from mcpgateway.plugins.framework.plugin_types import ToolPreInvokePayload, PluginContext, GlobalContext
>>> payload = ToolPreInvokePayload("calculator", {"operation": "add", "a": 5, "b": 3})
>>> context = PluginContext(GlobalContext(request_id="123"))
>>> # In async context:
>>> # result = await plugin.tool_pre_invoke(payload, context)
Raises:
NotImplementedError: needs to be implemented by sub class.
"""
# Default pass-through implementation
return ToolPreInvokeResult(continue_processing=True, modified_payload=payload)
raise NotImplementedError(
f"""'tool_pre_invoke' not implemented for plugin {self._config.name}
of plugin type {type(self)}
"""
)

async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult:
"""Plugin hook run after a tool is invoked.
Expand All @@ -197,18 +208,14 @@ async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: Plugin
payload: The tool result payload to be analyzed.
context: Contextual information about the hook call.

Returns:
ToolPostInvokeResult with processing status and modified result.

Examples:
>>> from mcpgateway.plugins.framework.plugin_types import ToolPostInvokePayload, PluginContext, GlobalContext
>>> payload = ToolPostInvokePayload("calculator", {"result": 8, "status": "success"})
>>> context = PluginContext(GlobalContext(request_id="123"))
>>> # In async context:
>>> # result = await plugin.tool_post_invoke(payload, context)
Raises:
NotImplementedError: needs to be implemented by sub class.
"""
# Default pass-through implementation
return ToolPostInvokeResult(continue_processing=True, modified_payload=payload)
raise NotImplementedError(
f"""'tool_post_invoke' not implemented for plugin {self._config.name}
of plugin type {type(self)}
"""
)

async def resource_pre_fetch(self, payload, context):
"""Plugin hook run before a resource is fetched.
Expand All @@ -217,22 +224,14 @@ async def resource_pre_fetch(self, payload, context):
payload: The resource payload to be analyzed.
context: Contextual information about the hook call.

Returns:
ResourcePreFetchResult with processing status and modified payload.

Examples:
>>> from mcpgateway.plugins.framework.plugin_types import ResourcePreFetchPayload, PluginContext, GlobalContext
>>> payload = ResourcePreFetchPayload("file:///data.txt", {"cache": True})
>>> context = PluginContext(GlobalContext(request_id="123"))
>>> # In async context:
>>> # result = await plugin.resource_pre_fetch(payload, context)
Raises:
NotImplementedError: needs to be implemented by sub class.
"""
# Import here to avoid circular dependency
# First-Party
from mcpgateway.plugins.framework.plugin_types import ResourcePreFetchResult

# Default pass-through implementation
return ResourcePreFetchResult(continue_processing=True, modified_payload=payload)
raise NotImplementedError(
f"""'resource_pre_fetch' not implemented for plugin {self._config.name}
of plugin type {type(self)}
"""
)

async def resource_post_fetch(self, payload, context):
"""Plugin hook run after a resource is fetched.
Expand All @@ -241,24 +240,14 @@ async def resource_post_fetch(self, payload, context):
payload: The resource content payload to be analyzed.
context: Contextual information about the hook call.

Returns:
ResourcePostFetchResult with processing status and modified content.

Examples:
>>> from mcpgateway.plugins.framework.plugin_types import ResourcePostFetchPayload, PluginContext, GlobalContext
>>> from mcpgateway.models import ResourceContent
>>> content = ResourceContent(type="resource", uri="file:///data.txt", text="Data")
>>> payload = ResourcePostFetchPayload("file:///data.txt", content)
>>> context = PluginContext(GlobalContext(request_id="123"))
>>> # In async context:
>>> # result = await plugin.resource_post_fetch(payload, context)
Raises:
NotImplementedError: needs to be implemented by sub class.
"""
# Import here to avoid circular dependency
# First-Party
from mcpgateway.plugins.framework.plugin_types import ResourcePostFetchResult

# Default pass-through implementation
return ResourcePostFetchResult(continue_processing=True, modified_payload=payload)
raise NotImplementedError(
f"""'resource_post_fetch' not implemented for plugin {self._config.name}
of plugin type {type(self)}
"""
)

async def shutdown(self) -> None:
"""Plugin cleanup code."""
Expand Down
27 changes: 27 additions & 0 deletions mcpgateway/plugins/framework/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
"""Plugins constants file.

Copyright 2025
SPDX-License-Identifier: Apache-2.0
Authors: Teryl Taylor

This module stores a collection of plugin constants used throughout the framework.
"""

# Model constants.
# Specialized plugin types.
EXTERNAL_PLUGIN_TYPE = "external"

# MCP related constants.
PYTHON_SUFFIX = ".py"
URL = "url"
SCRIPT = "script"
AFTER = "after"

NAME = "name"
PYTHON = "python"
PLUGIN_NAME = "plugin_name"
PAYLOAD = "payload"
CONTEXT = "context"
GET_PLUGIN_CONFIG = "get_plugin_config"
IGNORE_CONFIG_EXTERNAL = "ignore_config_external"
63 changes: 63 additions & 0 deletions mcpgateway/plugins/framework/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
"""Pydantic models for plugins.

Copyright 2025
SPDX-License-Identifier: Apache-2.0
Authors: Teryl Taylor

This module implements the pydantic models associated with
the base plugin layer including configurations, and contexts.
"""

# First-Party
from mcpgateway.plugins.framework.models import PluginErrorModel, PluginViolation


class PluginViolationError(Exception):
"""A plugin violation error.

Attributes:
violation (PluginViolation): the plugin violation.
message (str): the plugin violation reason.
"""

def __init__(self, message: str, violation: PluginViolation | None = None):
"""Initialize a plugin violation error.

Args:
message: the reason for the violation error.
violation: the plugin violation object details.
"""
self.message = message
self.violation = violation
super().__init__(self.message)


class PluginError(Exception):
"""A plugin error object for errors internal to the plugin.

Attributes:
error (PluginErrorModel): the plugin error object.
"""

def __init__(self, error: PluginErrorModel):
"""Initialize a plugin violation error.

Args:
error: the plugin error details.
"""
self.error = error
super().__init__(self.error.message)


def convert_exception_to_error(exception: Exception, plugin_name: str) -> PluginErrorModel:
"""Converts an exception object into a PluginErrorModel. Primarily used for external plugin error handling.

Args:
exception: The exception to be converted.
plugin_name: The name of the plugin on which the exception occurred.

Returns:
A plugin error pydantic object that can be sent over HTTP.
"""
return PluginErrorModel(message=repr(exception), plugin_name=plugin_name)
Loading
Loading