Skip to main content
Glama
server.py18 kB
""" Notepad++ MCP Server FastMCP 2.12 compliant MCP server for Notepad++ automation and control. Provides comprehensive file operations, text manipulation, and UI control. """ import asyncio import logging import os import subprocess import sys from pathlib import Path from typing import Any, Dict, Optional from fastmcp import FastMCP # Configure structured logging logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) # Create console handler for stderr (safe for FastMCP stdio protocol) console_handler = logging.StreamHandler(sys.stderr) console_handler.setLevel(logging.INFO) # Create formatter formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") console_handler.setFormatter(formatter) # Add handler to logger logger.addHandler(console_handler) # Windows-specific imports try: import win32api import win32con import win32gui WINDOWS_AVAILABLE = True except ImportError: WINDOWS_AVAILABLE = False # Create FastMCP application app = FastMCP("Notepad++ MCP Server") # Configuration NOTEPADPP_TIMEOUT = int(os.getenv("NOTEPADPP_TIMEOUT", "30")) NOTEPADPP_AUTO_START = os.getenv("NOTEPADPP_AUTO_START", "true").lower() == "true" NOTEPADPP_PATH = os.getenv("NOTEPADPP_PATH", None) # Default Notepad++ installation paths DEFAULT_NOTEPADPP_PATHS = [ r"C:\Program Files\Notepad++\notepad++.exe", r"C:\Program Files (x86)\Notepad++\notepad++.exe", r"C:\Users\{}\AppData\Local\Notepad++\notepad++.exe".format( os.getenv("USERNAME", "") ), ] class NotepadPPError(Exception): """Base exception for Notepad++ operations.""" pass class NotepadPPNotFoundError(NotepadPPError): """Raised when Notepad++ is not found or not running.""" pass class NotepadPPController: """Controller for Notepad++ automation via Windows API.""" def __init__(self): if not WINDOWS_AVAILABLE: raise NotepadPPError( "Windows API not available - this server requires Windows" ) self.notepadpp_exe = self._find_notepadpp_exe() self.hwnd = None self.scintilla_hwnd = None def _find_notepadpp_exe(self) -> str: """Find Notepad++ executable path.""" if NOTEPADPP_PATH and Path(NOTEPADPP_PATH).exists(): return NOTEPADPP_PATH for path in DEFAULT_NOTEPADPP_PATHS: if Path(path).exists(): return path raise NotepadPPNotFoundError( "Notepad++ executable not found. Please install Notepad++ or set NOTEPADPP_PATH environment variable." ) def _find_notepadpp_window(self) -> Optional[int]: """Find Notepad++ main window handle.""" def enum_windows_callback(hwnd: int, windows: list[int]) -> bool: if win32gui.IsWindowVisible(hwnd): window_text = win32gui.GetWindowText(hwnd) class_name = win32gui.GetClassName(hwnd) if class_name == "Notepad++" or "Notepad++" in window_text: windows.append(hwnd) return True windows: list[int] = [] win32gui.EnumWindows(enum_windows_callback, windows) return windows[0] if windows else None def _find_scintilla_window(self, main_hwnd: int) -> Optional[int]: """Find Scintilla editor window within Notepad++.""" def enum_child_windows(hwnd: int, scintilla_windows: list[int]) -> bool: class_name = win32gui.GetClassName(hwnd) if class_name == "Scintilla": scintilla_windows.append(hwnd) return True scintilla_windows: list[int] = [] win32gui.EnumChildWindows(main_hwnd, enum_child_windows, scintilla_windows) return scintilla_windows[0] if scintilla_windows else None async def ensure_notepadpp_running(self) -> bool: """Ensure Notepad++ is running, start if needed.""" self.hwnd = self._find_notepadpp_window() if not self.hwnd and NOTEPADPP_AUTO_START: # Start Notepad++ subprocess.Popen([self.notepadpp_exe], shell=False) # Wait for it to start for _ in range(50): # 5 seconds max await asyncio.sleep(0.1) self.hwnd = self._find_notepadpp_window() if self.hwnd: break if not self.hwnd: raise NotepadPPNotFoundError( "Notepad++ is not running and auto-start failed" ) # Find Scintilla editor window self.scintilla_hwnd = self._find_scintilla_window(self.hwnd) if not self.scintilla_hwnd: raise NotepadPPError("Could not find Scintilla editor window") return True async def send_message( self, hwnd: int, msg: int, wparam: int = 0, lparam: int = 0 ) -> int: """Send Windows message to window.""" try: result = win32gui.SendMessage(hwnd, msg, wparam, lparam) return int(result) if result is not None else 0 except Exception as e: raise NotepadPPError(f"Failed to send message: {e}") async def get_window_text(self, hwnd: int) -> str: """Get text from window.""" try: length_result = win32gui.SendMessage(hwnd, win32con.WM_GETTEXTLENGTH, 0, 0) length = int(length_result) if length_result is not None else 0 if length == 0: return "" buffer = win32gui.PyMakeBuffer(length + 1) win32gui.SendMessage(hwnd, win32con.WM_GETTEXT, length + 1, buffer) text = buffer.raw.decode("utf-8", errors="ignore").rstrip("\x00") return text except Exception as e: raise NotepadPPError(f"Failed to get window text: {e}") # Global controller instance controller = NotepadPPController() if WINDOWS_AVAILABLE else None @app.tool() async def get_status() -> Dict[str, Any]: """Get Notepad++ status and information.""" if not controller: return {"error": "Windows API not available"} try: await controller.ensure_notepadpp_running() window_text = await controller.get_window_text(controller.hwnd) return { "status": "running", "window_title": window_text, "main_window_handle": controller.hwnd, "scintilla_handle": controller.scintilla_hwnd, "executable_path": controller.notepadpp_exe, } except Exception as e: return { "status": "error", "error": str(e), "executable_path": controller.notepadpp_exe if controller else None, } @app.tool() async def open_file(file_path: str) -> Dict[str, Any]: """ Open a file in Notepad++. Args: file_path: Path to the file to open Returns: Dictionary with operation status and file information """ if not controller: return {"error": "Windows API not available"} try: await controller.ensure_notepadpp_running() # Convert to absolute path abs_path = os.path.abspath(file_path) if not os.path.exists(abs_path): return {"success": False, "error": f"File not found: {abs_path}"} # Use subprocess to open file (Notepad++ command line) subprocess.Popen( [controller.notepadpp_exe, abs_path], shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) # Wait a moment for file to load await asyncio.sleep(0.5) return { "success": True, "file_path": abs_path, "message": f"Opened file: {abs_path}", } except Exception as e: return {"success": False, "error": f"Failed to open file: {e}"} @app.tool() async def new_file() -> Dict[str, Any]: """ Create a new untitled file in Notepad++. Returns: Dictionary with operation status """ if not controller: return {"error": "Windows API not available"} try: await controller.ensure_notepadpp_running() # Send Ctrl+N to create new file win32gui.SetForegroundWindow(controller.hwnd) await asyncio.sleep(0.1) # Simulate Ctrl+N keybd_event = win32api.keybd_event keybd_event(win32con.VK_CONTROL, 0, 0, 0) keybd_event(ord("N"), 0, 0, 0) keybd_event(ord("N"), 0, win32con.KEYEVENTF_KEYUP, 0) keybd_event(win32con.VK_CONTROL, 0, win32con.KEYEVENTF_KEYUP, 0) await asyncio.sleep(0.2) return {"success": True, "message": "Created new file"} except Exception as e: return {"success": False, "error": f"Failed to create new file: {e}"} @app.tool() async def save_file() -> Dict[str, Any]: """ Save the current file in Notepad++. Returns: Dictionary with operation status """ if not controller: return {"error": "Windows API not available"} try: await controller.ensure_notepadpp_running() # Send Ctrl+S to save file win32gui.SetForegroundWindow(controller.hwnd) await asyncio.sleep(0.1) # Simulate Ctrl+S keybd_event = win32api.keybd_event keybd_event(win32con.VK_CONTROL, 0, 0, 0) keybd_event(ord("S"), 0, 0, 0) keybd_event(ord("S"), 0, win32con.KEYEVENTF_KEYUP, 0) keybd_event(win32con.VK_CONTROL, 0, win32con.KEYEVENTF_KEYUP, 0) await asyncio.sleep(0.3) return {"success": True, "message": "File saved"} except Exception as e: return {"success": False, "error": f"Failed to save file: {e}"} @app.tool() async def get_current_file_info() -> Dict[str, Any]: """ Get information about the currently active file in Notepad++. Returns: Dictionary with current file information """ if not controller: return {"error": "Windows API not available"} try: await controller.ensure_notepadpp_running() # Get window title which usually contains filename window_text = await controller.get_window_text(controller.hwnd) # Parse filename from window title # Notepad++ title format: "filename - Notepad++" filename = "Untitled" if " - Notepad++" in window_text: filename = window_text.split(" - Notepad++")[0] # Check if file is modified (usually indicated by *) is_modified = "*" in window_text return { "success": True, "window_title": window_text, "filename": filename, "is_modified": is_modified, } except Exception as e: return {"success": False, "error": f"Failed to get file info: {e}"} @app.tool() async def insert_text(text: str) -> Dict[str, Any]: """ Insert text at the current cursor position. Args: text: Text to insert Returns: Dictionary with operation status """ if not controller: return {"error": "Windows API not available"} try: await controller.ensure_notepadpp_running() # Focus on Notepad++ win32gui.SetForegroundWindow(controller.hwnd) await asyncio.sleep(0.1) # Use clipboard to insert text (more reliable for longer text) import win32clipboard win32clipboard.OpenClipboard() win32clipboard.EmptyClipboard() win32clipboard.SetClipboardText(text) win32clipboard.CloseClipboard() # Paste text with Ctrl+V keybd_event = win32api.keybd_event keybd_event(win32con.VK_CONTROL, 0, 0, 0) keybd_event(ord("V"), 0, 0, 0) keybd_event(ord("V"), 0, win32con.KEYEVENTF_KEYUP, 0) keybd_event(win32con.VK_CONTROL, 0, win32con.KEYEVENTF_KEYUP, 0) await asyncio.sleep(0.2) return {"success": True, "message": f"Inserted {len(text)} characters"} except Exception as e: return {"success": False, "error": f"Failed to insert text: {e}"} @app.tool() async def find_text(search_text: str, case_sensitive: bool = False) -> Dict[str, Any]: """ Find text in the current document. Args: search_text: Text to search for case_sensitive: Whether search should be case sensitive Returns: Dictionary with search results """ if not controller: return {"error": "Windows API not available"} try: await controller.ensure_notepadpp_running() # Focus on Notepad++ win32gui.SetForegroundWindow(controller.hwnd) await asyncio.sleep(0.1) # Open Find dialog with Ctrl+F keybd_event = win32api.keybd_event keybd_event(win32con.VK_CONTROL, 0, 0, 0) keybd_event(ord("F"), 0, 0, 0) keybd_event(ord("F"), 0, win32con.KEYEVENTF_KEYUP, 0) keybd_event(win32con.VK_CONTROL, 0, win32con.KEYEVENTF_KEYUP, 0) await asyncio.sleep(0.3) # Type search text for char in search_text: if char.isalnum() or char in " .,;:!?": keybd_event(ord(char.upper()), 0, 0, 0) keybd_event(ord(char.upper()), 0, win32con.KEYEVENTF_KEYUP, 0) await asyncio.sleep(0.01) # Press Enter to start search keybd_event(win32con.VK_RETURN, 0, 0, 0) keybd_event(win32con.VK_RETURN, 0, win32con.KEYEVENTF_KEYUP, 0) await asyncio.sleep(0.2) # Close find dialog with Escape keybd_event(win32con.VK_ESCAPE, 0, 0, 0) keybd_event(win32con.VK_ESCAPE, 0, win32con.KEYEVENTF_KEYUP, 0) return { "success": True, "search_text": search_text, "case_sensitive": case_sensitive, "message": f"Searched for: {search_text}", } except Exception as e: return {"success": False, "error": f"Failed to find text: {e}"} @app.tool() async def fix_display_issue() -> Dict[str, Any]: """ Fix Notepad++ display issues like black text on black background. This tool attempts to reset Notepad++ theme and colors to default values. Returns: Dictionary with operation status """ if not controller: return {"error": "Windows API not available"} try: await controller.ensure_notepadpp_running() # Focus on Notepad++ win32gui.SetForegroundWindow(controller.hwnd) await asyncio.sleep(0.1) # Method 1: Try to reset theme via Settings menu # Open Settings menu with Alt+S keybd_event = win32api.keybd_event keybd_event(win32con.VK_MENU, 0, 0, 0) # Alt key keybd_event(ord("S"), 0, 0, 0) keybd_event(ord("S"), 0, win32con.KEYEVENTF_KEYUP, 0) keybd_event(win32con.VK_MENU, 0, win32con.KEYEVENTF_KEYUP, 0) await asyncio.sleep(0.5) # Try to navigate to Style Configurator # Press 'S' for Style Configurator keybd_event(ord("S"), 0, 0, 0) keybd_event(ord("S"), 0, win32con.KEYEVENTF_KEYUP, 0) await asyncio.sleep(1.0) # If Style Configurator opened, try to reset to default theme # Press Tab to navigate to theme selection for _ in range(3): keybd_event(win32con.VK_TAB, 0, 0, 0) keybd_event(win32con.VK_TAB, 0, win32con.KEYEVENTF_KEYUP, 0) await asyncio.sleep(0.1) # Try to select default theme keybd_event(win32con.VK_DOWN, 0, 0, 0) keybd_event(win32con.VK_DOWN, 0, win32con.KEYEVENTF_KEYUP, 0) await asyncio.sleep(0.1) # Press Enter to apply keybd_event(win32con.VK_RETURN, 0, 0, 0) keybd_event(win32con.VK_RETURN, 0, win32con.KEYEVENTF_KEYUP, 0) await asyncio.sleep(0.5) # Close dialog with Escape keybd_event(win32con.VK_ESCAPE, 0, 0, 0) keybd_event(win32con.VK_ESCAPE, 0, win32con.KEYEVENTF_KEYUP, 0) await asyncio.sleep(0.5) # Method 2: Try Ctrl+Shift+P to open Plugin Manager and reset # This is a common shortcut for plugin-related issues keybd_event(win32con.VK_CONTROL, 0, 0, 0) keybd_event(win32con.VK_SHIFT, 0, 0, 0) keybd_event(ord("P"), 0, 0, 0) keybd_event(ord("P"), 0, win32con.KEYEVENTF_KEYUP, 0) keybd_event(win32con.VK_SHIFT, 0, win32con.KEYEVENTF_KEYUP, 0) keybd_event(win32con.VK_CONTROL, 0, win32con.KEYEVENTF_KEYUP, 0) await asyncio.sleep(0.5) # Close any opened dialogs keybd_event(win32con.VK_ESCAPE, 0, 0, 0) keybd_event(win32con.VK_ESCAPE, 0, win32con.KEYEVENTF_KEYUP, 0) # Method 3: Try to force refresh the display # Send WM_PAINT message to force redraw win32gui.SendMessage(controller.hwnd, win32con.WM_PAINT, 0, 0) if controller.scintilla_hwnd: win32gui.SendMessage(controller.scintilla_hwnd, win32con.WM_PAINT, 0, 0) await asyncio.sleep(0.2) return { "success": True, "message": "Display fix attempted. If issue persists, try: 1) Restart Notepad++, 2) Settings > Style Configurator > Select 'Default' theme, 3) Check View > Current View for display issues", "suggestions": [ "Restart Notepad++ completely", "Go to Settings > Style Configurator and select 'Default' theme", "Check View > Current View for any display issues", "Try View > Zoom > Reset Zoom", "Check if any plugins are causing the issue", ], } except Exception as e: return { "success": False, "error": f"Failed to fix display issue: {e}", "manual_fix": "Try manually: Settings > Style Configurator > Select 'Default' theme", } def main() -> None: """Main entry point for the MCP server.""" if not WINDOWS_AVAILABLE: logger.error("This MCP server requires Windows and pywin32") sys.exit(1) app.run() if __name__ == "__main__": main()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sandraschi/notepadpp-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server