"""
Plugin management tools for FiftyOne MCP server.
| Copyright 2017-2026, Voxel51, Inc.
| `voxel51.com <https://voxel51.com/>`_
|
"""
import json
import logging
import fiftyone.plugins as fop
from mcp.types import Tool, TextContent
from .utils import format_response, safe_serialize
logger = logging.getLogger(__name__)
def list_plugins(enabled=None):
"""Lists available FiftyOne plugins.
Args:
enabled (None): whether to list only enabled plugins. If None,
lists all downloaded plugins
Returns:
a dict with success status and plugin data
"""
try:
if enabled is None:
plugins = fop.list_downloaded_plugins()
else:
plugins = fop.list_plugins(enabled=enabled)
plugin_list = []
for item in plugins:
plugin_name = item if isinstance(item, str) else item.name
try:
plugin = fop.get_plugin(plugin_name)
plugin_list.append(
{
"name": plugin.name,
"version": plugin.version,
"description": plugin.description,
"operators": plugin.operators or [],
"author": getattr(plugin, "author", None),
"license": getattr(plugin, "license", None),
"builtin": plugin.builtin,
}
)
except Exception as e:
logger.warning("Error getting plugin %s: %s", plugin_name, e)
plugin_list.append({"name": str(plugin_name), "error": str(e)})
return format_response(
{"plugins": plugin_list, "count": len(plugin_list)}, success=True
)
except Exception as e:
logger.error("Error listing plugins: %s", e)
return format_response(None, success=False, error=str(e))
def get_plugin_info(plugin_name):
"""Gets detailed information about a specific plugin.
Args:
plugin_name: the name of the plugin (e.g., "@voxel51/brain")
Returns:
a dict with success status and plugin info
"""
try:
plugin = fop.get_plugin(plugin_name)
info = {
"name": plugin.name,
"version": plugin.version,
"description": plugin.description,
"operators": plugin.operators or [],
"author": getattr(plugin, "author", None),
"license": getattr(plugin, "license", None),
"builtin": plugin.builtin,
"directory": str(plugin.directory),
"has_python": plugin.has_py,
"has_javascript": plugin.has_js,
}
return format_response({"plugin": info}, success=True)
except Exception as e:
logger.error("Error getting plugin info for %s: %s", plugin_name, e)
return format_response(None, success=False, error=str(e))
def download_plugin(url_or_repo, plugin_names=None, overwrite=False):
"""Downloads and installs a FiftyOne plugin.
Args:
url_or_repo: a GitHub repository URL or "user/repo" string
plugin_names (None): optional list of specific plugin names to
download from the repo. If None, downloads all plugins
overwrite (False): whether to overwrite existing plugins
Returns:
a dict with success status and installation result
"""
try:
fop.download_plugin(
url_or_repo, plugin_names=plugin_names, overwrite=overwrite
)
downloaded = fop.list_downloaded_plugins()
return format_response(
{
"message": "Plugin(s) downloaded successfully",
"url": url_or_repo,
"plugin_names": plugin_names,
"total_downloaded": len(downloaded),
},
success=True,
)
except Exception as e:
logger.error("Error downloading plugin from %s: %s", url_or_repo, e)
return format_response(None, success=False, error=str(e))
def enable_plugin(plugin_name):
"""Enables a downloaded plugin.
Args:
plugin_name: the name of the plugin to enable
Returns:
a dict with success status
"""
try:
fop.enable_plugin(plugin_name)
return format_response(
{"message": "Plugin %s enabled successfully" % plugin_name},
success=True,
)
except Exception as e:
logger.error("Error enabling plugin %s: %s", plugin_name, e)
return format_response(None, success=False, error=str(e))
def disable_plugin(plugin_name):
"""Disables a plugin.
Args:
plugin_name: the name of the plugin to disable
Returns:
a dict with success status
"""
try:
fop.disable_plugin(plugin_name)
return format_response(
{"message": f"Plugin {plugin_name} disabled successfully"},
success=True,
)
except Exception as e:
logger.error("Error disabling plugin %s: %s", plugin_name, e)
return format_response(None, success=False, error=str(e))
def get_plugin_tools():
"""Returns the list of plugin management MCP tools.
Returns:
list of :class:`mcp.types.Tool`
"""
return [
Tool(
name="list_plugins",
description="Lists available FiftyOne plugins and their operators. Plugins extend functionality by providing additional operators. Key plugins: @voxel51/brain (16 operators for similarity/duplicates/visualization), @voxel51/utils (12 operators for dataset CRUD), @voxel51/io (5 operators for import/export), @voxel51/evaluation (5 operators), @voxel51/annotation (6 operators), @voxel51/zoo (2 operators). Use this to discover what plugins are installed and what operators they provide.",
inputSchema={
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"description": "If true, list only enabled plugins. If false, list only disabled plugins. If not specified, lists all downloaded plugins",
}
},
},
),
Tool(
name="get_plugin_info",
description="Gets detailed information about a specific FiftyOne plugin including its operators, version, and metadata.",
inputSchema={
"type": "object",
"properties": {
"plugin_name": {
"type": "string",
"description": "The name of the plugin (e.g., '@voxel51/brain')",
}
},
"required": ["plugin_name"],
},
),
Tool(
name="download_plugin",
description="Downloads and installs a FiftyOne plugin from GitHub. Plugins immediately add new operators to the system (accessible via list_operators and execute_operator). Common repo: 'voxel51/fiftyone-plugins' contains all official plugins. After installation, use enable_plugin to activate the plugin's operators.",
inputSchema={
"type": "object",
"properties": {
"url_or_repo": {
"type": "string",
"description": "GitHub repository URL or 'user/repo' string (e.g., 'voxel51/fiftyone-plugins' or 'https://github.com/voxel51/fiftyone-plugins')",
},
"plugin_names": {
"type": "array",
"items": {"type": "string"},
"description": "Optional list of specific plugin names to download from the repo. If not specified, downloads all plugins in the repo",
},
"overwrite": {
"type": "boolean",
"description": "Whether to overwrite existing plugins. Default is false",
"default": False,
},
},
"required": ["url_or_repo"],
},
),
Tool(
name="enable_plugin",
description="Enables a downloaded FiftyOne plugin, making its operators immediately available through list_operators and execute_operator. Required after download_plugin to activate the plugin's functionality.",
inputSchema={
"type": "object",
"properties": {
"plugin_name": {
"type": "string",
"description": "The name of the plugin to enable",
}
},
"required": ["plugin_name"],
},
),
Tool(
name="disable_plugin",
description="Disables a FiftyOne plugin, removing its operators from availability.",
inputSchema={
"type": "object",
"properties": {
"plugin_name": {
"type": "string",
"description": "The name of the plugin to disable",
}
},
"required": ["plugin_name"],
},
),
]
async def handle_tool_call(name, arguments):
"""Handles plugin management tool calls.
Args:
name: the tool name
arguments: dict of arguments for the tool
Returns:
list of TextContent with the result
"""
if name == "list_plugins":
enabled = arguments.get("enabled")
result = list_plugins(enabled=enabled)
elif name == "get_plugin_info":
plugin_name = arguments["plugin_name"]
result = get_plugin_info(plugin_name)
elif name == "download_plugin":
url_or_repo = arguments["url_or_repo"]
plugin_names = arguments.get("plugin_names")
overwrite = arguments.get("overwrite", False)
result = download_plugin(url_or_repo, plugin_names, overwrite)
elif name == "enable_plugin":
plugin_name = arguments["plugin_name"]
result = enable_plugin(plugin_name)
elif name == "disable_plugin":
plugin_name = arguments["plugin_name"]
result = disable_plugin(plugin_name)
else:
result = format_response(
None, success=False, error=f"Unknown tool: {name}"
)
return [TextContent(type="text", text=json.dumps(result, indent=2))]