# 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}"
```