44Copyright 2025
55SPDX-License-Identifier: Apache-2.0
66Authors: Teryl Taylor
7+ Fred Araujo
78
89Module that contains plugin MCP server code to serve external plugins.
910"""
11+
1012# Standard
1113import asyncio
1214import logging
1315import os
16+ from typing import Any , Callable , Dict , Type , TypeVar
1417
1518# Third-Party
1619from chuk_mcp_runtime .common .mcp_tool_decorator import mcp_tool
1720from chuk_mcp_runtime .entry import main_async
21+ from pydantic import BaseModel
1822
1923# First-Party
24+ from mcpgateway .plugins .framework import Plugin
2025from mcpgateway .plugins .framework .errors import convert_exception_to_error
2126from mcpgateway .plugins .framework .loader .config import ConfigLoader
2227from mcpgateway .plugins .framework .manager import DEFAULT_PLUGIN_TIMEOUT , PluginManager
2328from mcpgateway .plugins .framework .models import (
2429 PluginContext ,
2530 PluginErrorModel ,
31+ PluginResult ,
2632 PromptPosthookPayload ,
33+ PromptPosthookResult ,
2734 PromptPrehookPayload ,
35+ PromptPrehookResult ,
36+ ResourcePostFetchPayload ,
37+ ResourcePostFetchResult ,
38+ ResourcePreFetchPayload ,
39+ ResourcePreFetchResult ,
40+ ToolPostInvokePayload ,
41+ ToolPostInvokeResult ,
42+ ToolPreInvokePayload ,
43+ ToolPreInvokeResult ,
2844)
2945
46+ P = TypeVar ("P" , bound = BaseModel )
47+
3048logger = logging .getLogger (__name__ )
3149
3250config_file = os .environ .get ("PLUGINS_CONFIG_PATH" , os .path .join ("." , "resources" , "plugins" , "config.yaml" ))
@@ -75,11 +93,14 @@ async def get_plugin_config(name: str) -> dict:
7593 return None
7694
7795
78- @mcp_tool (name = "prompt_pre_fetch" , description = "Execute prompt prefetch hook for a plugin" )
79- async def prompt_pre_fetch (plugin_name : str , payload : dict , context : dict ) -> dict :
80- """Invoke the prompt pre fetch hook for a particular plugin.
96+ async def _invoke_hook (
97+ payload_model : Type [P ], hook_function : Callable [[Plugin ], Callable [[P , PluginContext ], PluginResult ]], plugin_name : str , payload : Dict [str , Any ], context : Dict [str , Any ]
98+ ) -> dict :
99+ """Invoke a plugin hook.
81100
82101 Args:
102+ payload_model: the type of the payload accepted for the hook.
103+ hook_function: the hook function to be invoked.
83104 plugin_name: the name of the plugin to execute.
84105 payload: the prompt name and arguments to be analyzed.
85106 context: the contextual and state information required for the execution of the hook.
@@ -94,9 +115,9 @@ async def prompt_pre_fetch(plugin_name: str, payload: dict, context: dict) -> di
94115 plugin = global_plugin_manager .get_plugin (plugin_name )
95116 try :
96117 if plugin :
97- prepayload = PromptPrehookPayload .model_validate (payload )
98- precontext = PluginContext .model_validate (context )
99- result = await asyncio .wait_for (plugin . prompt_pre_fetch ( prepayload , precontext ), plugin_timeout )
118+ _payload = payload_model .model_validate (payload )
119+ _context = PluginContext .model_validate (context )
120+ result = await asyncio .wait_for (hook_function ( plugin , _payload , _context ), plugin_timeout )
100121 return result .model_dump ()
101122 raise ValueError (f"Unable to retrieve plugin { plugin_name } to execute." )
102123 except asyncio .TimeoutError :
@@ -106,8 +127,30 @@ async def prompt_pre_fetch(plugin_name: str, payload: dict, context: dict) -> di
106127 return convert_exception_to_error (ex , plugin_name = plugin_name ).model_dump ()
107128
108129
130+ @mcp_tool (name = "prompt_pre_fetch" , description = "Execute prompt prefetch hook for a plugin" )
131+ async def prompt_pre_fetch (plugin_name : str , payload : Dict [str , Any ], context : Dict [str , Any ]) -> dict :
132+ """Invoke the prompt pre fetch hook for a particular plugin.
133+
134+ Args:
135+ plugin_name: the name of the plugin to execute.
136+ payload: the prompt name and arguments to be analyzed.
137+ context: the contextual and state information required for the execution of the hook.
138+
139+ Raises:
140+ ValueError: if unable to retrieve a plugin.
141+
142+ Returns:
143+ The transformed or filtered response from the plugin hook.
144+ """
145+
146+ def prompt_pre_fetch_func (plugin : Plugin , payload : PromptPrehookPayload , context : PluginContext ) -> PromptPrehookResult :
147+ return plugin .prompt_pre_fetch (payload , context )
148+
149+ return await _invoke_hook (PromptPrehookPayload , prompt_pre_fetch_func , plugin_name , payload , context )
150+
151+
109152@mcp_tool (name = "prompt_post_fetch" , description = "Execute prompt postfetch hook for a plugin" )
110- async def prompt_post_fetch (plugin_name : str , payload : dict , context : dict ) -> dict :
153+ async def prompt_post_fetch (plugin_name : str , payload : Dict [ str , Any ], context : Dict [ str , Any ] ) -> dict :
111154 """Call plugin's prompt post-fetch hook.
112155
113156 Args:
@@ -121,20 +164,99 @@ async def prompt_post_fetch(plugin_name: str, payload: dict, context: dict) -> d
121164 Returns:
122165 The result of the plugin execution.
123166 """
124- plugin_timeout = global_plugin_manager .config .plugin_settings .plugin_timeout if global_plugin_manager .config else DEFAULT_PLUGIN_TIMEOUT
125- plugin = global_plugin_manager .get_plugin (plugin_name )
126- try :
127- if plugin :
128- postpayload = PromptPosthookPayload .model_validate (payload )
129- postcontext = PluginContext .model_validate (context )
130- result = await asyncio .wait_for (plugin .prompt_post_fetch (postpayload , postcontext ), plugin_timeout )
131- return result .model_dump ()
132- raise ValueError (f"Unable to retrieve plugin { plugin_name } to execute." )
133- except asyncio .TimeoutError :
134- return PluginErrorModel (message = f"Plugin { plugin_name } timed out from execution after { plugin_timeout } seconds." , plugin_name = plugin_name ).model_dump ()
135- except Exception as ex :
136- logger .exception (ex )
137- return convert_exception_to_error (ex , plugin_name = plugin_name ).model_dump ()
167+
168+ def prompt_post_fetch_func (plugin : Plugin , payload : PromptPosthookPayload , context : PluginContext ) -> PromptPosthookResult :
169+ return plugin .prompt_post_fetch (payload , context )
170+
171+ return await _invoke_hook (PromptPosthookPayload , prompt_post_fetch_func , plugin_name , payload , context )
172+
173+
174+ @mcp_tool (name = "tool_pre_invoke" , description = "Execute tool pre-invoke hook for a plugin" )
175+ async def tool_pre_invoke (plugin_name : str , payload : Dict [str , Any ], context : Dict [str , Any ]) -> dict :
176+ """Invoke the tool pre-invoke hook for a particular plugin.
177+
178+ Args:
179+ plugin_name: the name of the plugin to execute.
180+ payload: the tool name and arguments to be analyzed.
181+ context: the contextual and state information required for the execution of the hook.
182+
183+ Raises:
184+ ValueError: if unable to retrieve a plugin.
185+
186+ Returns:
187+ The transformed or filtered response from the plugin hook.
188+ """
189+
190+ def tool_pre_invoke_func (plugin : Plugin , payload : ToolPreInvokePayload , context : PluginContext ) -> ToolPreInvokeResult :
191+ return plugin .tool_pre_invoke (payload , context )
192+
193+ return await _invoke_hook (ToolPreInvokePayload , tool_pre_invoke_func , plugin_name , payload , context )
194+
195+
196+ @mcp_tool (name = "tool_post_invoke" , description = "Execute tool post-invoke hook for a plugin" )
197+ async def tool_post_invoke (plugin_name : str , payload : Dict [str , Any ], context : Dict [str , Any ]) -> dict :
198+ """Invoke the tool post-invoke hook for a particular plugin.
199+
200+ Args:
201+ plugin_name: the name of the plugin to execute.
202+ payload: the tool name and arguments to be analyzed.
203+ context: the contextual and state information required for the execution of the hook.
204+
205+ Raises:
206+ ValueError: if unable to retrieve a plugin.
207+
208+ Returns:
209+ The transformed or filtered response from the plugin hook.
210+ """
211+
212+ def tool_post_invoke_func (plugin : Plugin , payload : ToolPostInvokePayload , context : PluginContext ) -> ToolPostInvokeResult :
213+ return plugin .tool_post_invoke (payload , context )
214+
215+ return await _invoke_hook (ToolPostInvokePayload , tool_post_invoke_func , plugin_name , payload , context )
216+
217+
218+ @mcp_tool (name = "resource_pre_fetch" , description = "Execute resource prefetch hook for a plugin" )
219+ async def resource_pre_fetch (plugin_name : str , payload : Dict [str , Any ], context : Dict [str , Any ]) -> dict :
220+ """Invoke the resource pre fetch hook for a particular plugin.
221+
222+ Args:
223+ plugin_name: the name of the plugin to execute.
224+ payload: the resource name and arguments to be analyzed.
225+ context: the contextual and state information required for the execution of the hook.
226+
227+ Raises:
228+ ValueError: if unable to retrieve a plugin.
229+
230+ Returns:
231+ The transformed or filtered response from the plugin hook.
232+ """
233+
234+ def resource_pre_fetch_func (plugin : Plugin , payload : ResourcePreFetchPayload , context : PluginContext ) -> ResourcePreFetchResult :
235+ return plugin .resource_pre_fetch (payload , context )
236+
237+ return await _invoke_hook (ResourcePreFetchPayload , resource_pre_fetch_func , plugin_name , payload , context )
238+
239+
240+ @mcp_tool (name = "resource_post_fetch" , description = "Execute resource postfetch hook for a plugin" )
241+ async def resource_post_fetch (plugin_name : str , payload : Dict [str , Any ], context : Dict [str , Any ]) -> dict :
242+ """Call plugin's resource post-fetch hook.
243+
244+ Args:
245+ plugin_name: The name of the plugin to execute.
246+ payload: The resource payload to be analyzed.
247+ context: Contextual information about the hook call.
248+
249+ Raises:
250+ ValueError: if unable to retrieve a plugin.
251+
252+ Returns:
253+ The result of the plugin execution.
254+ """
255+
256+ def resource_post_fetch_func (plugin : Plugin , payload : ResourcePostFetchPayload , context : PluginContext ) -> ResourcePostFetchResult :
257+ return plugin .resource_post_fetch (payload , context )
258+
259+ return await _invoke_hook (ResourcePostFetchPayload , resource_post_fetch_func , plugin_name , payload , context )
138260
139261
140262async def run_plugin_mcp_server ():
0 commit comments