Skip to content

Commit 6f84816

Browse files
author
Teryl Taylor
committed
feat: initial revision of adding plugin support.
Signed-off-by: Teryl Taylor <[email protected]>
1 parent 13e447a commit 6f84816

File tree

11 files changed

+1153
-0
lines changed

11 files changed

+1153
-0
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# -*- coding: utf-8 -*-
2+
"""Base plugin implementation.
3+
4+
Copyright 2025
5+
SPDX-License-Identifier: Apache-2.0
6+
Authors: Teryl Taylor
7+
8+
This module implements the base plugin object.
9+
It supports pre and post hooks AI safety, security and business processing
10+
for the following locations in the server:
11+
server_pre_register / server_post_register - for virtual server verification
12+
tool_pre_invoke / tool_post_invoke - for guardrails
13+
prompt_pre_fetch / prompt_post_fetch - for prompt filtering
14+
resource_pre_fetch / resource_post_fetch - for content filtering
15+
auth_pre_check / auth_post_check - for custom auth logic
16+
federation_pre_sync / federation_post_sync - for gateway federation
17+
"""
18+
19+
# Standard
20+
import uuid
21+
22+
# First-Party
23+
from mcpgateway.plugins.framework.models import HookType, PluginCondition, PluginConfig, PluginMode
24+
from mcpgateway.plugins.framework.types import (
25+
PluginContext,
26+
PromptPosthookPayload,
27+
PromptPosthookResult,
28+
PromptPrehookPayload,
29+
PromptPrehookResult,
30+
)
31+
32+
33+
class Plugin:
34+
"""Base plugin object for pre/post processing of inputs and outputs at various locations throughout the server."""
35+
36+
def __init__(self, config: PluginConfig) -> None:
37+
"""Initialize a plugin with a configuration and context.
38+
39+
Args:
40+
config: The plugin configuration
41+
"""
42+
self._config = config
43+
44+
@property
45+
def priority(self) -> int:
46+
"""Return the plugin's priority.
47+
48+
Returns:
49+
Plugin's priority.
50+
"""
51+
return self._config.priority
52+
53+
@property
54+
def mode(self) -> PluginMode:
55+
"""Return the plugin's mode.
56+
57+
Returns:
58+
Plugin's mode.
59+
"""
60+
return self._config.mode
61+
62+
@property
63+
def name(self) -> str:
64+
"""Return the plugin's name.
65+
66+
Returns:
67+
Plugin's name.
68+
"""
69+
return self._config.name
70+
71+
@property
72+
def hooks(self) -> list[HookType]:
73+
"""Return the plugin's currently configured hooks.
74+
75+
Returns:
76+
Plugin's configured hooks.
77+
"""
78+
return self._config.hooks
79+
80+
@property
81+
def tags(self) -> list[str]:
82+
"""Return the plugin's tags.
83+
84+
Returns:
85+
Plugin's tags.
86+
"""
87+
return self._config.tags
88+
89+
@property
90+
def conditions(self) -> list[PluginCondition] | None:
91+
"""Return the plugin's conditions for operation.
92+
93+
Returns:
94+
Plugin's conditions for executing.
95+
"""
96+
return self._config.conditions
97+
98+
async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult:
99+
"""Plugin hook run before a prompt is retrieved and rendered.
100+
101+
Args:
102+
payload: The prompt payload to be analyzed.
103+
context: contextual information about the hook call. Including why it was called.
104+
105+
Raises:
106+
NotImplementedError: needs to be implemented by sub class.
107+
"""
108+
raise NotImplementedError(f"""'prompt_pre_fetch' not implemented for plugin {self._config.name}
109+
of plugin type {type(self)}
110+
""")
111+
112+
async def prompt_post_fetch(self, payload: PromptPosthookPayload, context: PluginContext) -> PromptPosthookResult:
113+
"""Plugin hook run after a prompt is rendered.
114+
115+
Args:
116+
payload: The prompt payload to be analyzed.
117+
context: Contextual information about the hook call.
118+
119+
Raises:
120+
NotImplementedError: needs to be implemented by sub class.
121+
"""
122+
raise NotImplementedError(f"""'prompt_post_fetch' not implemented for plugin {self._config.name}
123+
of plugin type {type(self)}
124+
""")
125+
126+
def shutdown(self) -> None:
127+
"""Plugin cleanup code."""
128+
129+
130+
class PluginRef:
131+
"""Plugin reference which contains a uuid."""
132+
133+
def __init__(self, plugin: Plugin):
134+
"""Initialize a plugin reference.
135+
136+
Args:
137+
plugin: The plugin to reference.
138+
"""
139+
self._plugin = plugin
140+
self._uuid = uuid.uuid4()
141+
142+
@property
143+
def plugin(self) -> Plugin:
144+
"""Return the underlying plugin.
145+
146+
Returns:
147+
The underlying plugin.
148+
"""
149+
return self._plugin
150+
151+
@property
152+
def uuid(self) -> str:
153+
"""Return the plugin's UUID.
154+
155+
Returns:
156+
Plugin's UUID.
157+
"""
158+
return self._uuid.hex
159+
160+
@property
161+
def priority(self) -> int:
162+
"""Returns the plugin's priority.
163+
164+
Returns:
165+
Plugin's priority.
166+
"""
167+
return self._plugin.priority
168+
169+
@property
170+
def name(self) -> str:
171+
"""Return the plugin's name.
172+
173+
Returns:
174+
Plugin's name.
175+
"""
176+
return self._plugin.name
177+
178+
@property
179+
def hooks(self) -> list[HookType]:
180+
"""Returns the plugin's currently configured hooks.
181+
182+
Returns:
183+
Plugin's configured hooks.
184+
"""
185+
return self._plugin.hooks
186+
187+
@property
188+
def tags(self) -> list[str]:
189+
"""Return the plugin's tags.
190+
191+
Returns:
192+
Plugin's tags.
193+
"""
194+
return self._plugin.tags
195+
196+
@property
197+
def conditions(self) -> list[PluginCondition] | None:
198+
"""Return the plugin's conditions for operation.
199+
200+
Returns:
201+
Plugin's conditions for operation.
202+
"""
203+
return self._plugin.conditions
204+
205+
@property
206+
def mode(self) -> PluginMode:
207+
"""Return the plugin's mode.
208+
209+
Returns:
210+
Plugin's mode.
211+
"""
212+
return self.plugin.mode
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# -*- coding: utf-8 -*-
2+
"""Configuration loader implementation.
3+
4+
Copyright 2025
5+
SPDX-License-Identifier: Apache-2.0
6+
Authors: Teryl Taylor
7+
8+
This module loads configurations for plugins.
9+
"""
10+
11+
# Standard
12+
import os
13+
14+
# Third-Party
15+
import jinja2
16+
import yaml
17+
18+
# First-Party
19+
from mcpgateway.plugins.framework.models import Config, PluginConfig, PluginManifest
20+
21+
22+
class ConfigLoader:
23+
"""A configuration loader."""
24+
25+
@staticmethod
26+
def load_config(config: str, use_jinja: bool = True) -> Config:
27+
"""Load the plugin configuration from a file path.
28+
29+
Args:
30+
config: the configuration path.
31+
use_jinja: use jinja to replace env variables if true.
32+
33+
Returns:
34+
The plugin configuration object.
35+
"""
36+
with open(os.path.normpath(config), "r", encoding="utf-8") as file:
37+
template = file.read()
38+
if use_jinja:
39+
jinja_env = jinja2.Environment(loader=jinja2.BaseLoader())
40+
rendered_template = jinja_env.from_string(template).render(env=os.environ)
41+
else:
42+
rendered_template = template
43+
config_data = yaml.safe_load(rendered_template)
44+
return Config(**config_data)
45+
46+
@staticmethod
47+
def dump_config(path: str, config: Config) -> None:
48+
"""Dump plugin configuration to a file.
49+
50+
Args:
51+
path: configuration file path
52+
config: the plugin configuration path
53+
"""
54+
with open(os.path.normpath(path), "w", encoding="utf-8") as file:
55+
yaml.safe_dump(config.model_dump(exclude_none=True), file)
56+
57+
@staticmethod
58+
def load_plugin_config(config: str) -> PluginConfig:
59+
"""Load a plugin configuration from a file path.
60+
61+
This function autoescapes curly brackets in the 'instruction'
62+
and 'examples' keys under the config attribute.
63+
64+
Args:
65+
config: the plugin configuration path
66+
67+
Returns:
68+
The plugin configuration object
69+
"""
70+
with open(os.path.normpath(config), "r", encoding="utf8") as file:
71+
template = file.read()
72+
jinja_env = jinja2.Environment(loader=jinja2.BaseLoader(), autoescape=True)
73+
rendered_template = jinja_env.from_string(template).render(env=os.environ)
74+
config_data = yaml.safe_load(rendered_template)
75+
return PluginConfig(**config_data)
76+
77+
@staticmethod
78+
def load_plugin_manifest(manifest: str) -> PluginManifest:
79+
"""Load a plugin manifest from a file path.
80+
81+
Args:
82+
manifest: the plugin manifest path
83+
84+
Returns:
85+
The plugin manifest object
86+
"""
87+
with open(os.path.normpath(manifest), "r", encoding="utf8") as file:
88+
template = file.read()
89+
jinja_env = jinja2.Environment(loader=jinja2.BaseLoader(), autoescape=True)
90+
rendered_template = jinja_env.from_string(template).render(env=os.environ)
91+
config_data = yaml.safe_load(rendered_template)
92+
return PluginManifest(**config_data)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# -*- coding: utf-8 -*-
2+
"""Plugin loader implementation.
3+
4+
Copyright 2025
5+
SPDX-License-Identifier: Apache-2.0
6+
Authors: Teryl Taylor
7+
8+
This module implements the plugin loader.
9+
"""
10+
11+
# Standard
12+
import logging
13+
from typing import cast, Type
14+
15+
# First-Party
16+
from mcpgateway.plugins.framework.base import Plugin
17+
from mcpgateway.plugins.framework.models import PluginConfig
18+
from mcpgateway.plugins.framework.utils import import_module, parse_class_name
19+
20+
logger = logging.getLogger(__name__)
21+
22+
23+
class PluginLoader(object):
24+
"""A plugin loader object for loading and instantiating plugins."""
25+
26+
def __init__(self) -> None:
27+
"""Initialize the plugin loader."""
28+
self._plugin_types: dict[str, Type[Plugin]] = {}
29+
30+
def __get_plugin_type(self, kind: str) -> Type[Plugin]:
31+
try:
32+
(mod_name, cls_name) = parse_class_name(kind)
33+
module = import_module(mod_name)
34+
class_ = getattr(module, cls_name)
35+
return cast(Type[Plugin], class_)
36+
except Exception:
37+
logger.exception("Unable to instantiate class '%s'", kind)
38+
raise
39+
40+
def __register_plugin_type(self, kind: str) -> None:
41+
if kind not in self._plugin_types:
42+
plugin_type = self.__get_plugin_type(kind)
43+
self._plugin_types[kind] = plugin_type
44+
45+
async def load_and_instantiate_plugin(self, config: PluginConfig) -> Plugin | None:
46+
"""Load and instantiate a plugin, given a configuration.
47+
48+
Args:
49+
config: A plugin configuration.
50+
51+
Returns:
52+
A plugin instance.
53+
"""
54+
if config.kind not in self._plugin_types:
55+
self.__register_plugin_type(config.kind)
56+
plugin_type = self._plugin_types[config.kind]
57+
if plugin_type:
58+
return plugin_type(config)
59+
return None

0 commit comments

Comments
 (0)