Coverage for mcpgateway / plugins / framework / loader / plugin.py: 100%
52 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-11 07:10 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-11 07:10 +0000
1# -*- coding: utf-8 -*-
2"""Location: ./mcpgateway/plugins/framework/loader/plugin.py
3Copyright 2025
4SPDX-License-Identifier: Apache-2.0
5Authors: Teryl Taylor, Mihai Criveti
7Plugin loader implementation.
8This module implements the plugin loader.
9"""
11# Standard
12import logging
13from typing import cast, Type
15# First-Party
16from mcpgateway.plugins.framework.base import Plugin
17from mcpgateway.plugins.framework.constants import EXTERNAL_PLUGIN_TYPE
18from mcpgateway.plugins.framework.external.mcp.client import ExternalPlugin
19from mcpgateway.plugins.framework.models import PluginConfig
20from mcpgateway.plugins.framework.utils import import_module, parse_class_name
22# Use standard logging to avoid circular imports (plugins -> services -> plugins)
23logger = logging.getLogger(__name__)
26class PluginLoader:
27 """A plugin loader object for loading and instantiating plugins.
29 Examples:
30 >>> loader = PluginLoader()
31 >>> isinstance(loader._plugin_types, dict)
32 True
33 >>> len(loader._plugin_types)
34 0
35 """
37 def __init__(self) -> None:
38 """Initialize the plugin loader.
40 Examples:
41 >>> loader = PluginLoader()
42 >>> loader._plugin_types
43 {}
44 """
45 self._plugin_types: dict[str, Type[Plugin]] = {}
47 def __get_plugin_type(self, kind: str) -> Type[Plugin]:
48 """Import a plugin type from a python module.
50 Args:
51 kind: The fully-qualified type of the plugin to be registered.
53 Raises:
54 Exception: if unable to import a module.
56 Returns:
57 A plugin type.
58 """
59 try:
60 (mod_name, cls_name) = parse_class_name(kind)
61 module = import_module(mod_name)
62 class_ = getattr(module, cls_name)
63 return cast(Type[Plugin], class_)
64 except Exception:
65 logger.exception("Unable to import plugin type '%s'", kind)
66 raise
68 def __register_plugin_type(self, kind: str) -> None:
69 """Register a plugin type.
71 Args:
72 kind: The fully-qualified type of the plugin to be registered.
73 """
74 if kind not in self._plugin_types:
75 plugin_type: Type[Plugin]
76 if kind == EXTERNAL_PLUGIN_TYPE:
77 plugin_type = ExternalPlugin
78 else:
79 plugin_type = self.__get_plugin_type(kind)
80 self._plugin_types[kind] = plugin_type
82 async def load_and_instantiate_plugin(self, config: PluginConfig) -> Plugin | None:
83 """Load and instantiate a plugin, given a configuration.
85 For external plugins, the transport type is determined by the presence
86 of 'mcp', 'grpc', or 'unix_socket' configuration:
87 - If 'grpc' is set, uses GrpcExternalPlugin for gRPC transport
88 - If 'mcp' is set, uses ExternalPlugin for MCP transport
89 - If 'unix_socket' is set, uses UnixSocketExternalPlugin for raw Unix socket transport
91 Args:
92 config: A plugin configuration.
94 Returns:
95 A plugin instance.
97 Raises:
98 ValueError: If an external plugin has no transport configured.
99 """
100 # Handle external plugins with transport selection
101 if config.kind == EXTERNAL_PLUGIN_TYPE:
102 plugin: Plugin
103 if config.grpc:
104 # Use gRPC transport
105 # Import here to avoid circular dependency and to make grpc optional
106 # First-Party
107 from mcpgateway.plugins.framework.external.grpc.client import GrpcExternalPlugin # pylint: disable=import-outside-toplevel
109 plugin = GrpcExternalPlugin(config)
110 logger.info("Loading external plugin '%s' with gRPC transport", config.name)
111 elif config.unix_socket:
112 # Use raw Unix socket transport (high-performance local IPC)
113 # First-Party
114 from mcpgateway.plugins.framework.external.unix.client import UnixSocketExternalPlugin # pylint: disable=import-outside-toplevel
116 plugin = UnixSocketExternalPlugin(config)
117 logger.info("Loading external plugin '%s' with Unix socket transport", config.name)
118 elif config.mcp:
119 # Use MCP transport
120 plugin = ExternalPlugin(config)
121 logger.info("Loading external plugin '%s' with MCP transport", config.name)
122 else:
123 # Defensive fallback: PluginConfig validation should prevent this path.
124 raise ValueError(f"External plugin '{config.name}' must have 'mcp', 'grpc', or 'unix_socket' configuration") # pragma: no cover
126 await plugin.initialize()
127 return plugin
129 # Handle other plugin types
130 if config.kind not in self._plugin_types:
131 self.__register_plugin_type(config.kind)
132 plugin_type = self._plugin_types[config.kind]
133 if plugin_type:
134 plugin = plugin_type(config)
135 await plugin.initialize()
136 return plugin
137 return None
139 async def shutdown(self) -> None:
140 """Shutdown and cleanup plugin loader.
142 Examples:
143 >>> import asyncio
144 >>> loader = PluginLoader()
145 >>> asyncio.run(loader.shutdown())
146 >>> loader._plugin_types
147 {}
148 """
149 if self._plugin_types:
150 self._plugin_types.clear()