#
tokens: 6628/50000 17/17 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── .python-version
├── LICENSE
├── pyproject.toml
├── README.md
├── src
│   └── mcp_windows
│       ├── __init__.py
│       ├── appid.py
│       ├── audio.py
│       ├── clipboard.py
│       ├── keyboard.py
│       ├── main.py
│       ├── media.py
│       ├── monitors.py
│       ├── notifications.py
│       ├── screenshot.py
│       ├── startmenu.py
│       ├── theme.py
│       └── window_management.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
3.13

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

# Virtual environments
.venv

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# mcp-windows

MCP server for the windows API.

## Installation

add this to your claude mcp config:

```json
{
  "mcpServers": {
    "windows": {
      "command": "uvx",
      "args": [
        "mcp-windows"
      ]
    }
  }
}
```

or locally:

```json
{
  "mcpServers": {
    "windows": {
      "command": "uv",
      "args": [
        "--directory",
        "C:\\Users\\{name}\\Documents\\mcp-windows",
        "run",
        "mcp-windows"
      ]
    }
  }
}
```

## Features

### Media

- get_media_sessions
- pause
- play
- next
- previous

### Notifications

- send_toast

### Window Management

- get_foreground_window_info
- get_window_list
- focus_window
- close_window
- minimize_window

### screenshot

- screenshot_window

### Monitors

- sleep_monitors
- wake_monitors

### Theme

- set_theme_mode (light, dark)
- get_theme_mode

### Start Menu

- open_file
- open_url

### Clipboard

- get_clipboard
- set_clipboard

## License

MIT
```

--------------------------------------------------------------------------------
/src/mcp_windows/__init__.py:
--------------------------------------------------------------------------------

```python
from mcp_windows.main import mcp

def main() -> None:
    mcp.run("stdio")

```

--------------------------------------------------------------------------------
/src/mcp_windows/clipboard.py:
--------------------------------------------------------------------------------

```python
import win32clipboard
import win32con
from fastmcp import FastMCP


mcp: FastMCP = FastMCP(
    name="clipboard",
)

@mcp.tool("get_clipboard")
async def get_clipboard() -> str:
    """Get the current clipboard contents."""
    win32clipboard.OpenClipboard()
    data = win32clipboard.GetClipboardData(win32con.CF_UNICODETEXT)
    win32clipboard.CloseClipboard()
    return data

@mcp.tool("set_clipboard")
async def set_clipboard(text: str) -> str:
    """Set the clipboard contents."""
    win32clipboard.OpenClipboard()
    win32clipboard.EmptyClipboard()
    win32clipboard.SetClipboardText(text, win32con.CF_UNICODETEXT)
    win32clipboard.CloseClipboard()
    return "Clipboard set"

```

--------------------------------------------------------------------------------
/src/mcp_windows/notifications.py:
--------------------------------------------------------------------------------

```python
import asyncio
from fastmcp import FastMCP

from mcp_windows.appid import APP_ID

from winrt.windows.ui.notifications import ToastNotificationManager, ToastNotification
from winrt.windows.data.xml.dom import XmlDocument

mcp: FastMCP = FastMCP(
    name="notifications",
)

@mcp.tool("send_toast")
async def send_toast(title: str, message: str) -> str:
    """Send a windows toast notification to the user."""


    toast_xml_string = f"""
    <toast>
        <visual>
            <binding template="ToastGeneric">
                <text>{title}</text>
                <text>{message}</text>
            </binding>
        </visual>
    </toast>
    """

    xml_doc = XmlDocument()
    xml_doc.load_xml(toast_xml_string)

    toast = ToastNotification(xml_doc)

    notifier = ToastNotificationManager.create_toast_notifier_with_id(APP_ID)

    notifier.show(toast)

    return "Toast notification sent"
```

--------------------------------------------------------------------------------
/src/mcp_windows/monitors.py:
--------------------------------------------------------------------------------

```python
import ctypes
import win32con
import win32gui

from fastmcp import FastMCP

mcp: FastMCP = FastMCP(
    name="monitors",
)

@mcp.tool("sleep_monitors")
async def sleep_monitors() -> str:
    """Put all monitors to sleep."""
    try:
        ctypes.windll.user32.SendMessageW(
            win32con.HWND_BROADCAST,
            win32con.WM_SYSCOMMAND,
            win32con.SC_MONITORPOWER,
            2  # 2 = power off
        )
        return "Monitors put to sleep"
    except Exception as e:
        return f"Failed to sleep monitors: {type(e).__name__}: {e}"

@mcp.tool("wake_monitors")
async def wake_monitors() -> str:
    """Wake up sleeping monitors."""
    try:
        # This is dumb, but moving the mouse 1px wakes monitors
        x, y = win32gui.GetCursorPos()
        ctypes.windll.user32.SetCursorPos(x, y + 1)
        ctypes.windll.user32.SetCursorPos(x, y)
        return "Monitors woken up"
    except Exception as e:
        return f"Failed to wake monitors: {type(e).__name__}: {e}"

```

--------------------------------------------------------------------------------
/src/mcp_windows/appid.py:
--------------------------------------------------------------------------------

```python
"""this script registers a start menu shortcut for the MCP Windows app with a custom AppUserModelID.
This is necessary for the app to be able to send windows toast notifications due to some legacy UWP API
limitations.

If you press the windows key and type "mcp" in the start menu, you should see the MCP Windows app icon."""

import os
import sys
from win32com.client import Dispatch
import pythoncom

APP_ID = "mcp-windows"
SHORTCUT_PATH = os.path.join(
    os.environ["APPDATA"],
    r"Microsoft\Windows\Start Menu\Programs\MCP Windows.lnk"
)

STGM_READWRITE = 0x00000002

def register_app_id():
    shell = Dispatch("WScript.Shell")
    shortcut = shell.CreateShortcut(SHORTCUT_PATH)
    shortcut.TargetPath = sys.executable
    shortcut.WorkingDirectory = os.getcwd()
    shortcut.IconLocation = sys.executable
    shortcut.Save()

    # Add AppUserModelID
    from win32com.propsys import propsys, pscon
    property_store = propsys.SHGetPropertyStoreFromParsingName(SHORTCUT_PATH, None, STGM_READWRITE)
    property_store.SetValue(pscon.PKEY_AppUserModel_ID, propsys.PROPVARIANTType(APP_ID, pythoncom.VT_LPWSTR))
    property_store.Commit()

register_app_id()

```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[project]
name = "mcp-windows"
version = "0.1.0" 
description = "MCP server for the windows API."
readme = "README.md"
authors = [
    { name = "TerminalMan", email = "[email protected]" }
]
requires-python = ">=3.13"
dependencies = [
    "comtypes>=1.4.10",
    "fastmcp>=2.2.0",
    "mcp>=1.6.0",
    "pillow>=11.2.1",
    "psutil>=7.0.0",
    "pycaw>=20240210",
    "pywin32>=310",
    "winrt-runtime>=3.1.0",
    "winrt-windows-data-xml-dom>=3.1.0",
    "winrt-windows-foundation>=3.1.0",
    "winrt-windows-foundation-collections>=3.1.0",
    "winrt-windows-media-control>=3.1.0",
    "winrt-windows-storage>=3.1.0",
    "winrt-windows-system>=3.1.0",
    "winrt-windows-ui-notifications>=3.1.0",
]

[project.scripts]
mcp-windows = "mcp_windows:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project.urls]
Homepage = "https://github.com/SecretiveShell/mcp-windows"
Documentation = "https://github.com/SecretiveShell/mcp-windows"
Repository = "https://github.com/SecretiveShell/mcp-windows.git"
Issues = "https://github.com/SecretiveShell/mcp-windows/issues"
Changelog = "https://github.com/SecretiveShell/mcp-windows/commits/master/"

```

--------------------------------------------------------------------------------
/src/mcp_windows/startmenu.py:
--------------------------------------------------------------------------------

```python
from fastmcp import FastMCP
import os
from winrt.windows.foundation import Uri
from winrt.windows.system import Launcher
from winrt.windows.storage import StorageFile

mcp: FastMCP = FastMCP(
    name="startmenu",
)

@mcp.tool("open_file")
async def open_file(path: str) -> str:
    """Open a file or folder in the default application."""
    path = os.path.expanduser(path)
    path = os.path.expandvars(path)
    path = os.path.abspath(path)
    if not os.path.exists(path):
        return f"Path does not exist: {path}"
    
    file = await StorageFile.get_file_from_path_async(path)
    success = await Launcher.launch_file_async(file)

    if success:
        return "Opened file"

    # Fallback to os.startfile if the above fails
    os.startfile(path)
    return "Opened file"

@mcp.tool("open_url")
async def open_url(url: str) -> str:
    """Open a URL in the default browser."""
    try:
        uri = Uri(url)
        success = await Launcher.launch_uri_async(uri)
        if success:
            return "Opened URL"
    except Exception:
        pass
    
    # Fallback to webbrowser if the above fails
    import webbrowser
    webbrowser.open(url)
    return "Opened URL"
```

--------------------------------------------------------------------------------
/src/mcp_windows/main.py:
--------------------------------------------------------------------------------

```python
from fastmcp import FastMCP
from os import environ

from mcp_windows.media import mcp as media_mcp
from mcp_windows.notifications import mcp as notifications_mcp
from mcp_windows.window_management import mcp as window_management_mcp
from mcp_windows.monitors import mcp as monitors_mcp
from mcp_windows.clipboard import mcp as clipboard_mcp
from mcp_windows.screenshot import mcp as screenshot_mcp
from mcp_windows.theme import mcp as theme_mcp
from mcp_windows.startmenu import mcp as startmenu_mcp
from mcp_windows.keyboard import mcp as keyboard_mcp
from mcp_windows.audio import mcp as audio_mcp

sep = environ.get("FASTMCP_TOOL_SEPARATOR", "_")

mcp: FastMCP = FastMCP(
    name="windows",
)

mcp.mount("media", media_mcp, tool_separator=sep)
mcp.mount("notifications", notifications_mcp, tool_separator=sep)
mcp.mount("window_management", window_management_mcp, tool_separator=sep)
mcp.mount("monitors", monitors_mcp, tool_separator=sep)
mcp.mount("clipboard", clipboard_mcp, tool_separator=sep)
mcp.mount("screenshot", screenshot_mcp, tool_separator=sep)
mcp.mount("theme", theme_mcp, tool_separator=sep)
mcp.mount("startmenu", startmenu_mcp, tool_separator=sep)
mcp.mount("keyboard", keyboard_mcp, tool_separator=sep)
mcp.mount("audio", audio_mcp, tool_separator=sep)
```

--------------------------------------------------------------------------------
/src/mcp_windows/theme.py:
--------------------------------------------------------------------------------

```python
from typing import Literal
import winreg
from fastmcp import FastMCP

mcp: FastMCP = FastMCP(
    name="theme",
)


def _set_theme_key(name: str, value: int):
    key_path = r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
    with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_SET_VALUE) as key:
        winreg.SetValueEx(key, name, 0, winreg.REG_DWORD, value)

@mcp.tool("set_theme_mode")
async def set_theme_mode(mode: Literal["dark", "light"]) -> str:
    """Set Windows UI theme to 'dark' or 'light'."""

    if mode.lower() not in {"dark", "light"}:
        return "Invalid mode. Use 'dark' or 'light'."

    val = 0 if mode == "dark" else 1
    try:
        _set_theme_key("AppsUseLightTheme", val)
        _set_theme_key("SystemUsesLightTheme", val)
        return f"Set theme to {mode}"
    except Exception as e:
        return f"Failed to set theme: {type(e).__name__}: {e}"


@mcp.tool("get_theme_mode")
async def get_theme_mode() -> str:
    """Get the current Windows UI theme."""

    key_path = r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
    try:
        with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path) as key:
            val, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
            return "light" if val else "dark"
    except Exception:
        return "unknown"

```

--------------------------------------------------------------------------------
/src/mcp_windows/audio.py:
--------------------------------------------------------------------------------

```python
from fastmcp import FastMCP
import asyncio
from ctypes import POINTER, cast
from comtypes import CLSCTX_ALL
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume

mcp: FastMCP = FastMCP(
    name="audio"
)

def _get_volume_interface():
    devices = AudioUtilities.GetSpeakers()
    interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None)
    return cast(interface, POINTER(IAudioEndpointVolume))

async def run_in_executor(func, *args):
    loop = asyncio.get_event_loop()
    return await loop.run_in_executor(None, lambda: func(*args))

@mcp.tool("get_volume")
async def get_volume() -> str:
    """Return the master volume level as a percentage (0-100)."""
    volume = await run_in_executor(_get_volume_interface)
    level = await run_in_executor(volume.GetMasterVolumeLevelScalar)
    return f"{int(level * 100)}"

@mcp.tool("set_volume")
async def set_volume(level: int) -> str:
    """Set the master volume level (0-100)."""
    if not 0 <= level <= 100:
        return "Volume must be between 0 and 100"
    volume = await run_in_executor(_get_volume_interface)
    await run_in_executor(volume.SetMasterVolumeLevelScalar, level / 100.0, None)
    return f"Volume set to {level}%"

@mcp.tool("mute")
async def mute() -> str:
    volume = await run_in_executor(_get_volume_interface)
    await run_in_executor(volume.SetMute, 1, None)
    return "Muted"

@mcp.tool("unmute")
async def unmute() -> str:
    volume = await run_in_executor(_get_volume_interface)
    await run_in_executor(volume.SetMute, 0, None)
    return "Unmuted"

```

--------------------------------------------------------------------------------
/src/mcp_windows/keyboard.py:
--------------------------------------------------------------------------------

```python
import win32api
import win32con
import asyncio
from fastmcp import FastMCP

mcp: FastMCP = FastMCP(
    name="keyboard",
)

@mcp.tool("type_text")
async def type_text(text: str, delay: float = 0.05):
    """Simulate typing text with a delay between each character. ONLY USE THIS AS A LAST RESORT."""
    for char in text:
        vk = ord(char.upper())
        needs_shift = char.isupper() or not char.isalnum()  # crude shift detection

        if needs_shift:
            win32api.keybd_event(win32con.VK_SHIFT, 0, 0, 0)

        win32api.keybd_event(vk, 0, 0, 0)
        win32api.keybd_event(vk, 0, win32con.KEYEVENTF_KEYUP, 0)

        if needs_shift:
            win32api.keybd_event(win32con.VK_SHIFT, 0, win32con.KEYEVENTF_KEYUP, 0)

        await asyncio.sleep(delay)


VK_LOOKUP = {
    'ctrl': win32con.VK_CONTROL,
    'shift': win32con.VK_SHIFT,
    'alt': win32con.VK_MENU,
    'esc': win32con.VK_ESCAPE,
    'enter': win32con.VK_RETURN,
    'tab': win32con.VK_TAB,
    'space': win32con.VK_SPACE,
    'left': win32con.VK_LEFT,
    'right': win32con.VK_RIGHT,
    'up': win32con.VK_UP,
    'down': win32con.VK_DOWN,
}

@mcp.tool("send_keyboard_shortcut")
async def press_keys(keys: list[str]):
    """Simulate pressing a combination of keys. Use any of the following special keys: ctrl, shift, alt, esc, enter, tab, space, left, right, up, down as well the regular characters. ONLY USE THIS AS A LAST RESORT."""
    vk_keys = []
    for k in keys:
        k_lower = k.lower()
        vk = VK_LOOKUP.get(k_lower, ord(k.upper()))
        vk_keys.append(vk)

    for vk in vk_keys:
        win32api.keybd_event(vk, 0, 0, 0)
    await asyncio.sleep(0.05)
    for vk in reversed(vk_keys):
        win32api.keybd_event(vk, 0, win32con.KEYEVENTF_KEYUP, 0)

```

--------------------------------------------------------------------------------
/src/mcp_windows/screenshot.py:
--------------------------------------------------------------------------------

```python
import io
import win32gui
import win32ui
import win32api
from PIL import Image
import ctypes

from fastmcp import FastMCP, Image as FastMCPImage

mcp: FastMCP = FastMCP(
    name="screencapture",
)


# this was mostly llm generated so if it doesn't work, blame the ai
@mcp.tool("screenshot_window")
async def screenshot_window(hwnd: int) -> str | FastMCPImage:
    """Capture a screenshot of the specified window handle (hwnd). Does not require the window to be visible."""
    try:
        hwnd = int(hwnd)
        if not win32gui.IsWindow(hwnd):
            return "Invalid window handle"

        # Get window rect
        left, top, right, bottom = win32gui.GetWindowRect(hwnd)
        width = right - left
        height = bottom - top
        
        # Check for valid dimensions
        if width <= 0 or height <= 0:
            return "Window has invalid dimensions"

        # Get window device context
        hwndDC = win32gui.GetWindowDC(hwnd)
        mfcDC = win32ui.CreateDCFromHandle(hwndDC)
        saveDC = mfcDC.CreateCompatibleDC()

        saveBitMap = win32ui.CreateBitmap()
        saveBitMap.CreateCompatibleBitmap(mfcDC, width, height)
        saveDC.SelectObject(saveBitMap)

        # Change PrintWindow flags to capture entire window content including child windows
        # PW_RENDERFULLCONTENT = 0x00000002
        result = ctypes.windll.user32.PrintWindow(hwnd, saveDC.GetSafeHdc(), 2)

        # Add a small delay to ensure the content is captured
        win32api.Sleep(100)
        
        bmpinfo = saveBitMap.GetInfo()
        bmpstr = saveBitMap.GetBitmapBits(True)
        
        # Check if we have valid bitmap data
        if not bmpstr or len(bmpstr) <= 0:
            return "Failed to capture window content"
            
        img = Image.frombuffer(
            "RGB",
            (bmpinfo['bmWidth'], bmpinfo['bmHeight']),
            bmpstr, 'raw', 'BGRX', 0, 1
        )

        # Cleanup
        win32gui.DeleteObject(saveBitMap.GetHandle())
        saveDC.DeleteDC()
        mfcDC.DeleteDC()
        win32gui.ReleaseDC(hwnd, hwndDC)

        if result:
            # Preserve aspect ratio when resizing
            if width > 0 and height > 0:
                target_width = 1024
                target_height = int(height * (target_width / width))
                
                # Make sure we don't exceed maximum height
                if target_height > 2048:
                    target_height = 2048
                    target_width = int(width * (target_height / height))
                
                img = img.resize((target_width, target_height), Image.LANCZOS)
            
            buffer = io.BytesIO()
            img.save(buffer, format="PNG")
            buffer.seek(0)

            return FastMCPImage(data=buffer.read(), format="png")
        else:
            return "Screenshot may be partial or failed due to permissions"
    except Exception as e:
        return f"Failed to capture screenshot: {type(e).__name__}: {e}"
```

--------------------------------------------------------------------------------
/src/mcp_windows/media.py:
--------------------------------------------------------------------------------

```python
import json

from fastmcp import FastMCP
from winrt.windows.foundation import IAsyncOperation
from winrt.windows.media.control import (
    GlobalSystemMediaTransportControlsSessionManager as MediaManager,
    GlobalSystemMediaTransportControlsSessionMediaProperties as MediaProperties,
    GlobalSystemMediaTransportControlsSessionPlaybackInfo as PlaybackInfo,
)

mcp: FastMCP = FastMCP(
    name="Media",
)

PLAYBACK_STATUS = {
    0: "closed",
    1: "opened",
    2: "changing",
    3: "stopped",
    4: "playing",
    5: "paused",
}


@mcp.tool("get_media_sessions")
async def get_media_sessions() -> str:
    """List all media playback sessions with metadata and control capability info."""

    manager_op: IAsyncOperation = MediaManager.request_async()
    manager = await manager_op
    sessions = manager.get_sessions()

    output = {}
    for session in sessions:
        props_op = session.try_get_media_properties_async()
        props: MediaProperties = await props_op
        playback_info: PlaybackInfo = session.get_playback_info()
        controls = playback_info.controls

        app_id = session.source_app_user_model_id

        output[app_id] = {
            "title": props.title or "unknown",
            "artist": props.artist or "unknown",
            "album_title": props.album_title or "unknown",
            "playback_status": str(PLAYBACK_STATUS.get(playback_info.playback_status)),
            "is_play_enabled": controls.is_play_enabled,
            "is_pause_enabled": controls.is_pause_enabled,
            "is_next_enabled": controls.is_next_enabled,
            "is_previous_enabled": controls.is_previous_enabled,
        }

    return json.dumps(output)


@mcp.tool("pause")
async def pause(app_id: str) -> str:
    """Pause the media playback for a given app_id using windows media control API."""

    manager_op: IAsyncOperation[MediaManager] = MediaManager.request_async()
    manager: MediaManager = await manager_op

    sessions = manager.get_sessions()
    for session in sessions:
        if session.source_app_user_model_id.lower() == app_id.lower():
            playback_info = session.get_playback_info()
            if playback_info.controls.is_pause_enabled:
                await session.try_pause_async()
                return "Paused"
            else:
                return "Pause not available"

    return "Session not found"


@mcp.tool("play")
async def play(app_id: str) -> str:
    """Play the media playback for a given app_id using windows media control API."""

    manager_op: IAsyncOperation[MediaManager] = MediaManager.request_async()
    manager: MediaManager = await manager_op

    sessions = manager.get_sessions()
    for session in sessions:
        if session.source_app_user_model_id.lower() == app_id.lower():
            playback_info = session.get_playback_info()
            if playback_info.controls.is_play_enabled:
                await session.try_play_async()
                return "Playing"
            else:
                return "Play not available"

    return "Session not found"


@mcp.tool("next")
async def next(app_id: str) -> str:
    """Skip to the next media item for the given app_id."""

    manager = await MediaManager.request_async()
    sessions = manager.get_sessions()

    for session in sessions:
        if session.source_app_user_model_id.lower() == app_id.lower():
            playback_info = session.get_playback_info()
            if playback_info.controls.is_next_enabled:
                await session.try_skip_next_async()
                return "Skipped to next track"
            else:
                return "Next track not available"

    return "Session not found"


@mcp.tool("previous")
async def previous(app_id: str) -> str:
    """Skip to the previous media item for the given app_id."""

    manager = await MediaManager.request_async()
    sessions = manager.get_sessions()

    for session in sessions:
        if session.source_app_user_model_id.lower() == app_id.lower():
            playback_info = session.get_playback_info()
            if playback_info.controls.is_previous_enabled:
                await session.try_skip_previous_async()
                return "Skipped to previous track"
            else:
                return "Previous track not available"

    return "Session not found"

```

--------------------------------------------------------------------------------
/src/mcp_windows/window_management.py:
--------------------------------------------------------------------------------

```python
import asyncio
import json
import win32gui
import win32con
import win32process
import win32api
import psutil

from fastmcp import FastMCP

mcp: FastMCP = FastMCP(
    name="window_management"
)

def get_process_info(pid: int) -> dict:
    try:
        proc = psutil.Process(pid)
        return {
            "pid": pid,
            "exe": proc.name(),
        }
    except psutil.NoSuchProcess:
        return {
            "pid": pid,
            "exe": "<terminated>"
        }

@mcp.tool("get_foreground_window_info")
async def get_foreground_window_info() -> str:
    """Return information about the currently focused (foreground) window."""
    hwnd = win32gui.GetForegroundWindow()
    if hwnd == 0:
        return json.dumps({"error": "No active window"})

    _, pid = win32process.GetWindowThreadProcessId(hwnd)
    info = get_process_info(pid)
    info.update({
        "hwnd": hwnd,
        "title": win32gui.GetWindowText(hwnd),
        "class": win32gui.GetClassName(hwnd),
    })
    return json.dumps(info, ensure_ascii=False)

@mcp.tool("get_window_list")
async def list_open_windows() -> str:
    """Return a list of all top-level visible windows."""
    windows = []

    def callback(hwnd, _):
        if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowText(hwnd):
            _, pid = win32process.GetWindowThreadProcessId(hwnd)
            info = get_process_info(pid)
            info.update({
                "hwnd": hwnd,
                "title": win32gui.GetWindowText(hwnd),
                "class": win32gui.GetClassName(hwnd),
            })
            windows.append(info)

    win32gui.EnumWindows(callback, None)
    return json.dumps(windows, ensure_ascii=False)

@mcp.tool("focus_window")
async def focus_window(hwnd: int) -> str:
    """Force focus a window using all known safe tricks (thread attach, fake input, fallback restore)."""
    try:
        hwnd = int(hwnd)

        if not win32gui.IsWindow(hwnd):
            return "Invalid HWND"

        # Step 1: Only restore if minimized (prevent resizing)
        if win32gui.IsIconic(hwnd):
            win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)

        # Step 2: Try normal focus via thread attach
        fg_hwnd = win32gui.GetForegroundWindow()
        fg_thread = win32process.GetWindowThreadProcessId(fg_hwnd)[0]
        current_thread = win32api.GetCurrentThreadId()

        if fg_thread != current_thread:
            win32process.AttachThreadInput(fg_thread, current_thread, True)

        try:
            win32gui.SetForegroundWindow(hwnd)
        except Exception:
            pass

        if fg_thread != current_thread:
            win32process.AttachThreadInput(fg_thread, current_thread, False)

        # Step 3: Check if it worked
        if win32gui.GetForegroundWindow() == hwnd:
            return "Focused window successfully"

        # Step 4: Fallback — simulate user input (to defeat foreground lock)
        win32api.keybd_event(0, 0, 0, 0)
        await asyncio.sleep(0.05)

        # Step 5: Try again
        try:
            win32gui.SetForegroundWindow(hwnd)
        except Exception:
            pass

        if win32gui.GetForegroundWindow() == hwnd:
            return "Focused window (after simulating input)"

        # Step 6: Hard fallback — minimize + restore
        win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE)
        await asyncio.sleep(0.2)
        win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
        win32gui.SetForegroundWindow(hwnd)

        if win32gui.GetForegroundWindow() == hwnd:
            return "Focused window (after minimize/restore trick)"

        return "Could not focus window: OS restrictions"

    except Exception as e:
        return f"Could not focus window: {type(e).__name__}: {e}"


@mcp.tool("close_window")
async def close_window(hwnd: int) -> str:
    """Close the specified window."""
    try:
        win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0)
        return "Closed window"
    except Exception as e:
        return f"Could not close window: {type(e).__name__}: {e}"

@mcp.tool("minimize_window")
async def minimize_window(hwnd: int) -> str:
    """Minimize the specified window."""
    try:
        win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE)
        return "Minimized window"
    except Exception as e:
        return f"Could not minimize window: {type(e).__name__}: {e}"

```