Skip to content

Commit 80d45cf

Browse files
committed
feat: add tools and resources support
Signed-off-by: Frederico Araujo <[email protected]>
1 parent e31b838 commit 80d45cf

File tree

2 files changed

+146
-23
lines changed

2 files changed

+146
-23
lines changed

mcpgateway/plugins/framework/base.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
PromptPosthookResult,
3131
PromptPrehookPayload,
3232
PromptPrehookResult,
33+
ResourcePreFetchResult,
3334
ToolPostInvokePayload,
3435
ToolPostInvokeResult,
3536
ToolPreInvokePayload,
@@ -217,7 +218,7 @@ async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: Plugin
217218
"""
218219
)
219220

220-
async def resource_pre_fetch(self, payload, context):
221+
async def resource_pre_fetch(self, payload, context) -> ResourcePreFetchResult:
221222
"""Plugin hook run before a resource is fetched.
222223
223224
Args:
@@ -233,7 +234,7 @@ async def resource_pre_fetch(self, payload, context):
233234
"""
234235
)
235236

236-
async def resource_post_fetch(self, payload, context):
237+
async def resource_post_fetch(self, payload, context) -> ResourcePreFetchResult:
237238
"""Plugin hook run after a resource is fetched.
238239
239240
Args:

mcpgateway/plugins/framework/external/mcp/server.py

Lines changed: 143 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,47 @@
44
Copyright 2025
55
SPDX-License-Identifier: Apache-2.0
66
Authors: Teryl Taylor
7+
Fred Araujo
78
89
Module that contains plugin MCP server code to serve external plugins.
910
"""
11+
1012
# Standard
1113
import asyncio
1214
import logging
1315
import os
16+
from typing import Any, Callable, Dict, Type, TypeVar
1417

1518
# Third-Party
1619
from chuk_mcp_runtime.common.mcp_tool_decorator import mcp_tool
1720
from chuk_mcp_runtime.entry import main_async
21+
from pydantic import BaseModel
1822

1923
# First-Party
24+
from mcpgateway.plugins.framework import Plugin
2025
from mcpgateway.plugins.framework.errors import convert_exception_to_error
2126
from mcpgateway.plugins.framework.loader.config import ConfigLoader
2227
from mcpgateway.plugins.framework.manager import DEFAULT_PLUGIN_TIMEOUT, PluginManager
2328
from 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+
3048
logger = logging.getLogger(__name__)
3149

3250
config_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

140262
async def run_plugin_mcp_server():

0 commit comments

Comments
 (0)