"""
Plugin Operations Portmanteau Tool
Consolidates plugin operations (discover, install, list, execute) into a unified interface.
"""
import asyncio
from typing import Any, Dict, Literal, Optional
from fastmcp import FastMCP
# Windows-specific imports
try:
import win32api
import win32con
import win32gui
WINDOWS_AVAILABLE = True
except ImportError:
WINDOWS_AVAILABLE = False
win32api = None
win32con = None
win32gui = None
class PluginOperationsTool:
"""Portmanteau tool for plugin operations in Notepad++."""
def __init__(self, app: FastMCP, controller=None):
"""Initialize the plugin operations tool."""
self.app = app
self.controller = controller
def register_tools(self):
"""Register plugin operations portmanteau tool."""
@self.app.tool()
async def plugin_ops(
operation: Literal["discover", "install", "list", "execute"],
plugin_name: Optional[str] = None,
command: Optional[str] = None,
category: Optional[str] = None,
search_term: Optional[str] = None,
limit: int = 20,
) -> Dict[str, Any]:
"""Manage Notepad++ plugins with comprehensive discovery, installation, and execution.
PORTMANTEAU PATTERN RATIONALE:
Instead of creating 4 separate tools (discover, install, list, execute), this tool consolidates
plugin management operations into a single interface. Prevents tool explosion (4 tools -> 1 tool) while maintaining
full functionality and improving discoverability. Follows FastMCP 2.14.1+ SOTA standards.
Supported Operations:
- Discover available plugins from official repositories
- Install plugins to Notepad++ plugin directory
- List installed and available plugins
- Execute plugin commands and functions
Operations Detail:
**Plugin Discovery:**
- "discover": Browse official Notepad++ plugin repositories with filtering and search
**Plugin Installation:**
- "install": Download and install plugins to Notepad++ plugins directory
**Plugin Inventory:**
- "list": Show installed plugins and their status
**Plugin Execution:**
- "execute": Run plugin commands and access plugin functionality
Prerequisites:
- Windows OS with Notepad++ installed
- Internet access for plugin discovery and download
- File system write access to Notepad++ plugins directory
- pywin32 package for Windows API access (for plugin execution)
- requests library for HTTP operations (`pip install requests`)
Args:
operation (Literal, required): The plugin operation to perform. Must be one of: "discover", "install", "list", "execute".
- "discover": Browse available plugins (optional filters and search)
- "install": Install plugin (requires plugin_name)
- "list": Show installed plugins (no additional parameters required)
- "execute": Run plugin command (requires plugin_name and command)
plugin_name (str | None): Plugin identifier for install/execute operations. Required for: install, execute operations.
Must match official plugin naming conventions.
command (str | None): Plugin command or function to execute. Required for: execute operation.
Command format depends on specific plugin API.
category (str | None): Plugin category filter for discovery. Used by: discover operation.
Common categories: "editing", "development", "productivity", "utilities".
Default: None (no category filtering).
search_term (str | None): Text search filter for plugin discovery. Used by: discover operation.
Searches plugin names, descriptions, and tags.
Default: None (no text filtering).
limit (int): Maximum results for discovery operations. Used by: discover operation.
Default: 20. Valid range: 1-100.
Returns:
Dictionary following FastMCP 2.14.1+ enhanced response patterns:
```json
{
"success": true,
"operation": "discover",
"summary": "Found 15 plugins matching criteria",
"result": {
"plugins": [
{
"name": "XMLTools",
"version": "3.0.2",
"description": "XML editing tools",
"category": "development",
"download_url": "https://...",
"size_bytes": 245760
}
],
"total_found": 15,
"filtered_by": {
"category": "development",
"search_term": "xml"
}
},
"next_steps": ["Install desired plugins", "Execute plugin commands"],
"context": {
"operation_type": "plugin_discovery"
}
}
```
**Success Response Structure:**
- success (bool): Operation success status
- operation (str): Plugin operation that was performed
- summary (str): Human-readable result summary
- result (dict): Plugin-specific data (plugin list, install status, execution results)
- next_steps (list[str]): Suggested next actions
- context (dict): Additional operation context
**Error Response Structure:**
- success (bool): Always false for errors
- error (str): Error type (network_error, plugin_not_found, etc.)
- operation (str): Failed operation
- summary (str): Human-readable error summary
- recovery_options (list[str]): Suggested recovery actions
Examples:
# Discover plugins with search
result = await plugin_ops("discover", search_term="xml", limit=10)
# Returns: {"success": true, "result": {"plugins": [...], "total_found": 5}, ...}
# Install specific plugin
result = await plugin_ops("install", plugin_name="XMLTools")
# Returns: {"success": true, "summary": "Plugin XMLTools installed", ...}
# List installed plugins
result = await plugin_ops("list")
# Returns: {"success": true, "result": {"installed_plugins": [...]}, ...}
# Execute plugin command
result = await plugin_ops("execute", plugin_name="XMLTools", command="format")
# Returns: {"success": true, "summary": "XML formatting completed", ...}
Errors:
**Common Errors:**
- "Network error": Internet connection required for discovery/installation
- "Plugin not found": Specified plugin_name doesn't exist in repositories
- "Download failed": Unable to download plugin package
- "Installation failed": No write access to Notepad++ plugins directory
- "Plugin execution failed": Plugin not installed or command not supported
- "Invalid plugin name": Plugin name doesn't match official naming conventions
**Recovery Options:**
- Check internet connection for discovery and download operations
- Verify plugin name spelling and availability with "discover" operation
- Ensure write permissions to Notepad++ plugins directory
- Install plugins before attempting to execute them
- Use "list" operation to verify plugin installation status
- Check plugin documentation for supported commands
"""
if operation == "discover":
try:
import requests
import json
# Official Notepad++ Plugin List URLs
plugin_list_urls = [
"https://raw.githubusercontent.com/notepad-plus-plus/nppPluginList/master/src/pluginList.json",
"https://api.github.com/repos/notepad-plus-plus/nppPluginList/contents/src/pluginList.json",
]
plugins_data = None
# Try to fetch plugin list from GitHub
for url in plugin_list_urls:
try:
response = requests.get(url, timeout=10)
if response.status_code == 200:
if "api.github.com" in url:
# GitHub API returns base64 encoded content
import base64
content = base64.b64decode(
response.json()["content"]
).decode("utf-8")
plugins_data = json.loads(content)
else:
plugins_data = response.json()
break
except Exception:
continue
if not plugins_data:
return {
"success": False,
"error": "Could not fetch plugin list from any source",
"operation": operation,
"summary": "Plugin discovery failed - unable to fetch plugin list",
"recovery_options": [
"Check internet connection",
"Try again later",
"Check GitHub status",
],
"context": {"sources_tried": plugin_list_urls},
}
# Process plugin data
plugins = []
if "plugins" in plugins_data:
for plugin in plugins_data["plugins"]:
# Apply filters
if (
category
and plugin.get("category", "").lower()
!= category.lower()
):
continue
if (
search_term
and search_term.lower()
not in plugin.get("display-name", "").lower()
and search_term.lower()
not in plugin.get("description", "").lower()
):
continue
plugins.append(
{
"name": plugin.get("display-name", ""),
"description": plugin.get("description", ""),
"category": plugin.get("category", ""),
"version": plugin.get("version", ""),
"author": plugin.get("author", ""),
"homepage": plugin.get("homepage", ""),
}
)
# Limit results
plugins = plugins[:limit]
return {
"success": True,
"operation": operation,
"summary": f"Discovered {len(plugins)} plugins from official list",
"result": {
"plugins": plugins,
"total_found": len(plugins),
"limit": limit,
},
"next_steps": [
"Use plugin_ops install to install desired plugins"
],
"context": {
"source": "official_notepad_plugin_list",
"filters_applied": {
"category": category,
"search_term": search_term,
},
"total_available": len(plugins_data.get("plugins", [])),
},
}
except Exception as e:
return {
"success": False,
"error": f"Plugin discovery failed: {e}",
"operation": operation,
"summary": "Failed to discover plugins from official list",
"recovery_options": [
"Check internet connection",
"Verify requests library is installed",
],
"diagnostic_info": {"exception_type": type(e).__name__},
}
elif operation == "install":
if not plugin_name:
return {
"success": False,
"error": "plugin_name required for install operation",
"operation": operation,
"summary": "Plugin install failed - missing plugin name",
"clarification_options": {
"plugin_name": {
"description": "What plugin would you like to install?",
"type": "string",
}
},
}
if not self.controller:
return {
"success": False,
"error": "Windows API not available",
"operation": operation,
"summary": "Plugin install failed - Windows API unavailable",
"recovery_options": [
"Ensure pywin32 is installed",
"Restart the MCP server",
],
}
try:
await self.controller.ensure_notepadpp_running()
# Focus on Notepad++
win32gui.SetForegroundWindow(self.controller.hwnd)
await asyncio.sleep(0.1)
# Open Plugins menu with Alt+P
keybd_event = win32api.keybd_event
keybd_event(win32con.VK_MENU, 0, 0, 0) # Alt key
keybd_event(ord("P"), 0, 0, 0)
keybd_event(ord("P"), 0, win32con.KEYEVENTF_KEYUP, 0)
keybd_event(win32con.VK_MENU, 0, win32con.KEYEVENTF_KEYUP, 0)
await asyncio.sleep(0.5)
# Navigate to Plugin Admin (usually first option)
keybd_event(ord("P"), 0, 0, 0) # Press 'P' for Plugin Admin
keybd_event(ord("P"), 0, win32con.KEYEVENTF_KEYUP, 0)
await asyncio.sleep(1.0)
# In Plugin Admin, search for the plugin
# (This is a simplified version - full implementation would need more complex UI automation)
return {
"success": True,
"operation": operation,
"summary": f"Attempted to install plugin '{plugin_name}'",
"result": {
"plugin_name": plugin_name,
"install_attempted": True,
},
"next_steps": [
"Check Plugin Admin dialog in Notepad++",
"Verify plugin appears in Plugins menu",
],
"context": {
"method": "plugin_admin_dialog",
"manual_steps": f"Plugins > Plugin Admin > Search for '{plugin_name}' > Install",
"note": "Full automation requires Plugin Admin API integration",
},
}
except Exception as e:
return {
"success": False,
"error": f"Plugin install failed: {e}",
"operation": operation,
"plugin_name": plugin_name,
"summary": f"Failed to install plugin '{plugin_name}'",
"recovery_options": [
"Try manual installation via Plugin Admin",
"Check plugin name spelling",
],
"diagnostic_info": {"exception_type": type(e).__name__},
}
elif operation == "list":
if not self.controller:
return {
"success": False,
"error": "Windows API not available",
"operation": operation,
"summary": "Plugin list failed - Windows API unavailable",
"recovery_options": [
"Ensure pywin32 is installed",
"Restart the MCP server",
],
}
try:
await self.controller.ensure_notepadpp_running()
# Focus on Notepad++
win32gui.SetForegroundWindow(self.controller.hwnd)
await asyncio.sleep(0.1)
# Open Plugins menu with Alt+P to see installed plugins
# (This is a simplified approach - full enumeration would need menu parsing)
return {
"success": True,
"operation": operation,
"summary": "Plugin list requested - check Notepad++ Plugins menu",
"result": {"plugins_menu_accessed": True},
"next_steps": [
"Check Plugins menu in Notepad++ for installed plugins"
],
"context": {
"method": "menu_inspection",
"manual_check": "View: Plugins menu in Notepad++",
"limitation": "Full plugin enumeration requires menu parsing API",
},
}
except Exception as e:
return {
"success": False,
"error": f"Plugin list failed: {e}",
"operation": operation,
"summary": "Failed to access plugin list",
"recovery_options": [
"Check Notepad++ is running",
"Try manual menu inspection",
],
"diagnostic_info": {"exception_type": type(e).__name__},
}
elif operation == "execute":
if not plugin_name or not command:
missing = []
if not plugin_name:
missing.append("plugin_name")
if not command:
missing.append("command")
return {
"success": False,
"error": f"Missing required parameters: {', '.join(missing)}",
"operation": operation,
"summary": f"Plugin execute failed - missing {', '.join(missing)}",
"clarification_options": {
param: {
"description": f"What {param.replace('_', ' ')} would you like to use?",
"type": "string",
}
for param in missing
},
}
if not self.controller:
return {
"success": False,
"error": "Windows API not available",
"operation": operation,
"summary": "Plugin execute failed - Windows API unavailable",
"recovery_options": [
"Ensure pywin32 is installed",
"Restart the MCP server",
],
}
try:
await self.controller.ensure_notepadpp_running()
# Focus on Notepad++
win32gui.SetForegroundWindow(self.controller.hwnd)
await asyncio.sleep(0.1)
# Open Plugins menu with Alt+P
keybd_event = win32api.keybd_event
keybd_event(win32con.VK_MENU, 0, 0, 0) # Alt key
keybd_event(ord("P"), 0, 0, 0)
keybd_event(ord("P"), 0, win32con.KEYEVENTF_KEYUP, 0)
keybd_event(win32con.VK_MENU, 0, win32con.KEYEVENTF_KEYUP, 0)
await asyncio.sleep(0.5)
# Navigate to the plugin submenu
# (This is a simplified version - full navigation would need menu structure knowledge)
# Type the command name
for char in command:
keybd_event(ord(char.upper()), 0, 0, 0)
keybd_event(ord(char.upper()), 0, win32con.KEYEVENTF_KEYUP, 0)
await asyncio.sleep(0.1)
await asyncio.sleep(0.5)
# Press Enter to execute
keybd_event(win32con.VK_RETURN, 0, 0, 0)
keybd_event(win32con.VK_RETURN, 0, win32con.KEYEVENTF_KEYUP, 0)
await asyncio.sleep(1.0)
return {
"success": True,
"operation": operation,
"summary": f"Attempted to execute '{command}' from plugin '{plugin_name}'",
"result": {
"plugin_name": plugin_name,
"command": command,
"execution_attempted": True,
},
"next_steps": ["Check Notepad++ for command execution results"],
"context": {
"method": "menu_navigation",
"manual_alternative": f"Plugins > {plugin_name} > {command}",
"limitation": "Full automation requires plugin menu structure knowledge",
},
}
except Exception as e:
return {
"success": False,
"error": f"Plugin execute failed: {e}",
"operation": operation,
"plugin_name": plugin_name,
"command": command,
"summary": "Failed to execute plugin command",
"recovery_options": [
"Try manual execution via Plugins menu",
"Verify plugin and command names",
],
"diagnostic_info": {"exception_type": type(e).__name__},
}
else:
return {
"success": False,
"error": f"Unknown operation: {operation}",
"operation": operation,
"summary": f"Plugin operation failed - unknown operation '{operation}'",
"recovery_options": [
"Use 'discover', 'install', 'list', or 'execute' operations"
],
"clarification_options": {
"operation": {
"description": "What plugin operation would you like to perform?",
"options": ["discover", "install", "list", "execute"],
}
},
}