# 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:
--------------------------------------------------------------------------------
```
1 | 3.13
2 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Python-generated files
2 | __pycache__/
3 | *.py[oc]
4 | build/
5 | dist/
6 | wheels/
7 | *.egg-info
8 |
9 | # Virtual environments
10 | .venv
11 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # mcp-windows
2 |
3 | MCP server for the windows API.
4 |
5 | ## Installation
6 |
7 | add this to your claude mcp config:
8 |
9 | ```json
10 | {
11 | "mcpServers": {
12 | "windows": {
13 | "command": "uvx",
14 | "args": [
15 | "mcp-windows"
16 | ]
17 | }
18 | }
19 | }
20 | ```
21 |
22 | or locally:
23 |
24 | ```json
25 | {
26 | "mcpServers": {
27 | "windows": {
28 | "command": "uv",
29 | "args": [
30 | "--directory",
31 | "C:\\Users\\{name}\\Documents\\mcp-windows",
32 | "run",
33 | "mcp-windows"
34 | ]
35 | }
36 | }
37 | }
38 | ```
39 |
40 | ## Features
41 |
42 | ### Media
43 |
44 | - get_media_sessions
45 | - pause
46 | - play
47 | - next
48 | - previous
49 |
50 | ### Notifications
51 |
52 | - send_toast
53 |
54 | ### Window Management
55 |
56 | - get_foreground_window_info
57 | - get_window_list
58 | - focus_window
59 | - close_window
60 | - minimize_window
61 |
62 | ### screenshot
63 |
64 | - screenshot_window
65 |
66 | ### Monitors
67 |
68 | - sleep_monitors
69 | - wake_monitors
70 |
71 | ### Theme
72 |
73 | - set_theme_mode (light, dark)
74 | - get_theme_mode
75 |
76 | ### Start Menu
77 |
78 | - open_file
79 | - open_url
80 |
81 | ### Clipboard
82 |
83 | - get_clipboard
84 | - set_clipboard
85 |
86 | ## License
87 |
88 | MIT
```
--------------------------------------------------------------------------------
/src/mcp_windows/__init__.py:
--------------------------------------------------------------------------------
```python
1 | from mcp_windows.main import mcp
2 |
3 | def main() -> None:
4 | mcp.run("stdio")
5 |
```
--------------------------------------------------------------------------------
/src/mcp_windows/clipboard.py:
--------------------------------------------------------------------------------
```python
1 | import win32clipboard
2 | import win32con
3 | from fastmcp import FastMCP
4 |
5 |
6 | mcp: FastMCP = FastMCP(
7 | name="clipboard",
8 | )
9 |
10 | @mcp.tool("get_clipboard")
11 | async def get_clipboard() -> str:
12 | """Get the current clipboard contents."""
13 | win32clipboard.OpenClipboard()
14 | data = win32clipboard.GetClipboardData(win32con.CF_UNICODETEXT)
15 | win32clipboard.CloseClipboard()
16 | return data
17 |
18 | @mcp.tool("set_clipboard")
19 | async def set_clipboard(text: str) -> str:
20 | """Set the clipboard contents."""
21 | win32clipboard.OpenClipboard()
22 | win32clipboard.EmptyClipboard()
23 | win32clipboard.SetClipboardText(text, win32con.CF_UNICODETEXT)
24 | win32clipboard.CloseClipboard()
25 | return "Clipboard set"
26 |
```
--------------------------------------------------------------------------------
/src/mcp_windows/notifications.py:
--------------------------------------------------------------------------------
```python
1 | import asyncio
2 | from fastmcp import FastMCP
3 |
4 | from mcp_windows.appid import APP_ID
5 |
6 | from winrt.windows.ui.notifications import ToastNotificationManager, ToastNotification
7 | from winrt.windows.data.xml.dom import XmlDocument
8 |
9 | mcp: FastMCP = FastMCP(
10 | name="notifications",
11 | )
12 |
13 | @mcp.tool("send_toast")
14 | async def send_toast(title: str, message: str) -> str:
15 | """Send a windows toast notification to the user."""
16 |
17 |
18 | toast_xml_string = f"""
19 | <toast>
20 | <visual>
21 | <binding template="ToastGeneric">
22 | <text>{title}</text>
23 | <text>{message}</text>
24 | </binding>
25 | </visual>
26 | </toast>
27 | """
28 |
29 | xml_doc = XmlDocument()
30 | xml_doc.load_xml(toast_xml_string)
31 |
32 | toast = ToastNotification(xml_doc)
33 |
34 | notifier = ToastNotificationManager.create_toast_notifier_with_id(APP_ID)
35 |
36 | notifier.show(toast)
37 |
38 | return "Toast notification sent"
```
--------------------------------------------------------------------------------
/src/mcp_windows/monitors.py:
--------------------------------------------------------------------------------
```python
1 | import ctypes
2 | import win32con
3 | import win32gui
4 |
5 | from fastmcp import FastMCP
6 |
7 | mcp: FastMCP = FastMCP(
8 | name="monitors",
9 | )
10 |
11 | @mcp.tool("sleep_monitors")
12 | async def sleep_monitors() -> str:
13 | """Put all monitors to sleep."""
14 | try:
15 | ctypes.windll.user32.SendMessageW(
16 | win32con.HWND_BROADCAST,
17 | win32con.WM_SYSCOMMAND,
18 | win32con.SC_MONITORPOWER,
19 | 2 # 2 = power off
20 | )
21 | return "Monitors put to sleep"
22 | except Exception as e:
23 | return f"Failed to sleep monitors: {type(e).__name__}: {e}"
24 |
25 | @mcp.tool("wake_monitors")
26 | async def wake_monitors() -> str:
27 | """Wake up sleeping monitors."""
28 | try:
29 | # This is dumb, but moving the mouse 1px wakes monitors
30 | x, y = win32gui.GetCursorPos()
31 | ctypes.windll.user32.SetCursorPos(x, y + 1)
32 | ctypes.windll.user32.SetCursorPos(x, y)
33 | return "Monitors woken up"
34 | except Exception as e:
35 | return f"Failed to wake monitors: {type(e).__name__}: {e}"
36 |
```
--------------------------------------------------------------------------------
/src/mcp_windows/appid.py:
--------------------------------------------------------------------------------
```python
1 | """this script registers a start menu shortcut for the MCP Windows app with a custom AppUserModelID.
2 | This is necessary for the app to be able to send windows toast notifications due to some legacy UWP API
3 | limitations.
4 |
5 | If you press the windows key and type "mcp" in the start menu, you should see the MCP Windows app icon."""
6 |
7 | import os
8 | import sys
9 | from win32com.client import Dispatch
10 | import pythoncom
11 |
12 | APP_ID = "mcp-windows"
13 | SHORTCUT_PATH = os.path.join(
14 | os.environ["APPDATA"],
15 | r"Microsoft\Windows\Start Menu\Programs\MCP Windows.lnk"
16 | )
17 |
18 | STGM_READWRITE = 0x00000002
19 |
20 | def register_app_id():
21 | shell = Dispatch("WScript.Shell")
22 | shortcut = shell.CreateShortcut(SHORTCUT_PATH)
23 | shortcut.TargetPath = sys.executable
24 | shortcut.WorkingDirectory = os.getcwd()
25 | shortcut.IconLocation = sys.executable
26 | shortcut.Save()
27 |
28 | # Add AppUserModelID
29 | from win32com.propsys import propsys, pscon
30 | property_store = propsys.SHGetPropertyStoreFromParsingName(SHORTCUT_PATH, None, STGM_READWRITE)
31 | property_store.SetValue(pscon.PKEY_AppUserModel_ID, propsys.PROPVARIANTType(APP_ID, pythoncom.VT_LPWSTR))
32 | property_store.Commit()
33 |
34 | register_app_id()
35 |
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [project]
2 | name = "mcp-windows"
3 | version = "0.1.0"
4 | description = "MCP server for the windows API."
5 | readme = "README.md"
6 | authors = [
7 | { name = "TerminalMan", email = "[email protected]" }
8 | ]
9 | requires-python = ">=3.13"
10 | dependencies = [
11 | "comtypes>=1.4.10",
12 | "fastmcp>=2.2.0",
13 | "mcp>=1.6.0",
14 | "pillow>=11.2.1",
15 | "psutil>=7.0.0",
16 | "pycaw>=20240210",
17 | "pywin32>=310",
18 | "winrt-runtime>=3.1.0",
19 | "winrt-windows-data-xml-dom>=3.1.0",
20 | "winrt-windows-foundation>=3.1.0",
21 | "winrt-windows-foundation-collections>=3.1.0",
22 | "winrt-windows-media-control>=3.1.0",
23 | "winrt-windows-storage>=3.1.0",
24 | "winrt-windows-system>=3.1.0",
25 | "winrt-windows-ui-notifications>=3.1.0",
26 | ]
27 |
28 | [project.scripts]
29 | mcp-windows = "mcp_windows:main"
30 |
31 | [build-system]
32 | requires = ["hatchling"]
33 | build-backend = "hatchling.build"
34 |
35 | [project.urls]
36 | Homepage = "https://github.com/SecretiveShell/mcp-windows"
37 | Documentation = "https://github.com/SecretiveShell/mcp-windows"
38 | Repository = "https://github.com/SecretiveShell/mcp-windows.git"
39 | Issues = "https://github.com/SecretiveShell/mcp-windows/issues"
40 | Changelog = "https://github.com/SecretiveShell/mcp-windows/commits/master/"
41 |
```
--------------------------------------------------------------------------------
/src/mcp_windows/startmenu.py:
--------------------------------------------------------------------------------
```python
1 | from fastmcp import FastMCP
2 | import os
3 | from winrt.windows.foundation import Uri
4 | from winrt.windows.system import Launcher
5 | from winrt.windows.storage import StorageFile
6 |
7 | mcp: FastMCP = FastMCP(
8 | name="startmenu",
9 | )
10 |
11 | @mcp.tool("open_file")
12 | async def open_file(path: str) -> str:
13 | """Open a file or folder in the default application."""
14 | path = os.path.expanduser(path)
15 | path = os.path.expandvars(path)
16 | path = os.path.abspath(path)
17 | if not os.path.exists(path):
18 | return f"Path does not exist: {path}"
19 |
20 | file = await StorageFile.get_file_from_path_async(path)
21 | success = await Launcher.launch_file_async(file)
22 |
23 | if success:
24 | return "Opened file"
25 |
26 | # Fallback to os.startfile if the above fails
27 | os.startfile(path)
28 | return "Opened file"
29 |
30 | @mcp.tool("open_url")
31 | async def open_url(url: str) -> str:
32 | """Open a URL in the default browser."""
33 | try:
34 | uri = Uri(url)
35 | success = await Launcher.launch_uri_async(uri)
36 | if success:
37 | return "Opened URL"
38 | except Exception:
39 | pass
40 |
41 | # Fallback to webbrowser if the above fails
42 | import webbrowser
43 | webbrowser.open(url)
44 | return "Opened URL"
```
--------------------------------------------------------------------------------
/src/mcp_windows/main.py:
--------------------------------------------------------------------------------
```python
1 | from fastmcp import FastMCP
2 | from os import environ
3 |
4 | from mcp_windows.media import mcp as media_mcp
5 | from mcp_windows.notifications import mcp as notifications_mcp
6 | from mcp_windows.window_management import mcp as window_management_mcp
7 | from mcp_windows.monitors import mcp as monitors_mcp
8 | from mcp_windows.clipboard import mcp as clipboard_mcp
9 | from mcp_windows.screenshot import mcp as screenshot_mcp
10 | from mcp_windows.theme import mcp as theme_mcp
11 | from mcp_windows.startmenu import mcp as startmenu_mcp
12 | from mcp_windows.keyboard import mcp as keyboard_mcp
13 | from mcp_windows.audio import mcp as audio_mcp
14 |
15 | sep = environ.get("FASTMCP_TOOL_SEPARATOR", "_")
16 |
17 | mcp: FastMCP = FastMCP(
18 | name="windows",
19 | )
20 |
21 | mcp.mount("media", media_mcp, tool_separator=sep)
22 | mcp.mount("notifications", notifications_mcp, tool_separator=sep)
23 | mcp.mount("window_management", window_management_mcp, tool_separator=sep)
24 | mcp.mount("monitors", monitors_mcp, tool_separator=sep)
25 | mcp.mount("clipboard", clipboard_mcp, tool_separator=sep)
26 | mcp.mount("screenshot", screenshot_mcp, tool_separator=sep)
27 | mcp.mount("theme", theme_mcp, tool_separator=sep)
28 | mcp.mount("startmenu", startmenu_mcp, tool_separator=sep)
29 | mcp.mount("keyboard", keyboard_mcp, tool_separator=sep)
30 | mcp.mount("audio", audio_mcp, tool_separator=sep)
```
--------------------------------------------------------------------------------
/src/mcp_windows/theme.py:
--------------------------------------------------------------------------------
```python
1 | from typing import Literal
2 | import winreg
3 | from fastmcp import FastMCP
4 |
5 | mcp: FastMCP = FastMCP(
6 | name="theme",
7 | )
8 |
9 |
10 | def _set_theme_key(name: str, value: int):
11 | key_path = r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
12 | with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_SET_VALUE) as key:
13 | winreg.SetValueEx(key, name, 0, winreg.REG_DWORD, value)
14 |
15 | @mcp.tool("set_theme_mode")
16 | async def set_theme_mode(mode: Literal["dark", "light"]) -> str:
17 | """Set Windows UI theme to 'dark' or 'light'."""
18 |
19 | if mode.lower() not in {"dark", "light"}:
20 | return "Invalid mode. Use 'dark' or 'light'."
21 |
22 | val = 0 if mode == "dark" else 1
23 | try:
24 | _set_theme_key("AppsUseLightTheme", val)
25 | _set_theme_key("SystemUsesLightTheme", val)
26 | return f"Set theme to {mode}"
27 | except Exception as e:
28 | return f"Failed to set theme: {type(e).__name__}: {e}"
29 |
30 |
31 | @mcp.tool("get_theme_mode")
32 | async def get_theme_mode() -> str:
33 | """Get the current Windows UI theme."""
34 |
35 | key_path = r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
36 | try:
37 | with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path) as key:
38 | val, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
39 | return "light" if val else "dark"
40 | except Exception:
41 | return "unknown"
42 |
```
--------------------------------------------------------------------------------
/src/mcp_windows/audio.py:
--------------------------------------------------------------------------------
```python
1 | from fastmcp import FastMCP
2 | import asyncio
3 | from ctypes import POINTER, cast
4 | from comtypes import CLSCTX_ALL
5 | from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume
6 |
7 | mcp: FastMCP = FastMCP(
8 | name="audio"
9 | )
10 |
11 | def _get_volume_interface():
12 | devices = AudioUtilities.GetSpeakers()
13 | interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None)
14 | return cast(interface, POINTER(IAudioEndpointVolume))
15 |
16 | async def run_in_executor(func, *args):
17 | loop = asyncio.get_event_loop()
18 | return await loop.run_in_executor(None, lambda: func(*args))
19 |
20 | @mcp.tool("get_volume")
21 | async def get_volume() -> str:
22 | """Return the master volume level as a percentage (0-100)."""
23 | volume = await run_in_executor(_get_volume_interface)
24 | level = await run_in_executor(volume.GetMasterVolumeLevelScalar)
25 | return f"{int(level * 100)}"
26 |
27 | @mcp.tool("set_volume")
28 | async def set_volume(level: int) -> str:
29 | """Set the master volume level (0-100)."""
30 | if not 0 <= level <= 100:
31 | return "Volume must be between 0 and 100"
32 | volume = await run_in_executor(_get_volume_interface)
33 | await run_in_executor(volume.SetMasterVolumeLevelScalar, level / 100.0, None)
34 | return f"Volume set to {level}%"
35 |
36 | @mcp.tool("mute")
37 | async def mute() -> str:
38 | volume = await run_in_executor(_get_volume_interface)
39 | await run_in_executor(volume.SetMute, 1, None)
40 | return "Muted"
41 |
42 | @mcp.tool("unmute")
43 | async def unmute() -> str:
44 | volume = await run_in_executor(_get_volume_interface)
45 | await run_in_executor(volume.SetMute, 0, None)
46 | return "Unmuted"
47 |
```
--------------------------------------------------------------------------------
/src/mcp_windows/keyboard.py:
--------------------------------------------------------------------------------
```python
1 | import win32api
2 | import win32con
3 | import asyncio
4 | from fastmcp import FastMCP
5 |
6 | mcp: FastMCP = FastMCP(
7 | name="keyboard",
8 | )
9 |
10 | @mcp.tool("type_text")
11 | async def type_text(text: str, delay: float = 0.05):
12 | """Simulate typing text with a delay between each character. ONLY USE THIS AS A LAST RESORT."""
13 | for char in text:
14 | vk = ord(char.upper())
15 | needs_shift = char.isupper() or not char.isalnum() # crude shift detection
16 |
17 | if needs_shift:
18 | win32api.keybd_event(win32con.VK_SHIFT, 0, 0, 0)
19 |
20 | win32api.keybd_event(vk, 0, 0, 0)
21 | win32api.keybd_event(vk, 0, win32con.KEYEVENTF_KEYUP, 0)
22 |
23 | if needs_shift:
24 | win32api.keybd_event(win32con.VK_SHIFT, 0, win32con.KEYEVENTF_KEYUP, 0)
25 |
26 | await asyncio.sleep(delay)
27 |
28 |
29 | VK_LOOKUP = {
30 | 'ctrl': win32con.VK_CONTROL,
31 | 'shift': win32con.VK_SHIFT,
32 | 'alt': win32con.VK_MENU,
33 | 'esc': win32con.VK_ESCAPE,
34 | 'enter': win32con.VK_RETURN,
35 | 'tab': win32con.VK_TAB,
36 | 'space': win32con.VK_SPACE,
37 | 'left': win32con.VK_LEFT,
38 | 'right': win32con.VK_RIGHT,
39 | 'up': win32con.VK_UP,
40 | 'down': win32con.VK_DOWN,
41 | }
42 |
43 | @mcp.tool("send_keyboard_shortcut")
44 | async def press_keys(keys: list[str]):
45 | """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."""
46 | vk_keys = []
47 | for k in keys:
48 | k_lower = k.lower()
49 | vk = VK_LOOKUP.get(k_lower, ord(k.upper()))
50 | vk_keys.append(vk)
51 |
52 | for vk in vk_keys:
53 | win32api.keybd_event(vk, 0, 0, 0)
54 | await asyncio.sleep(0.05)
55 | for vk in reversed(vk_keys):
56 | win32api.keybd_event(vk, 0, win32con.KEYEVENTF_KEYUP, 0)
57 |
```
--------------------------------------------------------------------------------
/src/mcp_windows/screenshot.py:
--------------------------------------------------------------------------------
```python
1 | import io
2 | import win32gui
3 | import win32ui
4 | import win32api
5 | from PIL import Image
6 | import ctypes
7 |
8 | from fastmcp import FastMCP, Image as FastMCPImage
9 |
10 | mcp: FastMCP = FastMCP(
11 | name="screencapture",
12 | )
13 |
14 |
15 | # this was mostly llm generated so if it doesn't work, blame the ai
16 | @mcp.tool("screenshot_window")
17 | async def screenshot_window(hwnd: int) -> str | FastMCPImage:
18 | """Capture a screenshot of the specified window handle (hwnd). Does not require the window to be visible."""
19 | try:
20 | hwnd = int(hwnd)
21 | if not win32gui.IsWindow(hwnd):
22 | return "Invalid window handle"
23 |
24 | # Get window rect
25 | left, top, right, bottom = win32gui.GetWindowRect(hwnd)
26 | width = right - left
27 | height = bottom - top
28 |
29 | # Check for valid dimensions
30 | if width <= 0 or height <= 0:
31 | return "Window has invalid dimensions"
32 |
33 | # Get window device context
34 | hwndDC = win32gui.GetWindowDC(hwnd)
35 | mfcDC = win32ui.CreateDCFromHandle(hwndDC)
36 | saveDC = mfcDC.CreateCompatibleDC()
37 |
38 | saveBitMap = win32ui.CreateBitmap()
39 | saveBitMap.CreateCompatibleBitmap(mfcDC, width, height)
40 | saveDC.SelectObject(saveBitMap)
41 |
42 | # Change PrintWindow flags to capture entire window content including child windows
43 | # PW_RENDERFULLCONTENT = 0x00000002
44 | result = ctypes.windll.user32.PrintWindow(hwnd, saveDC.GetSafeHdc(), 2)
45 |
46 | # Add a small delay to ensure the content is captured
47 | win32api.Sleep(100)
48 |
49 | bmpinfo = saveBitMap.GetInfo()
50 | bmpstr = saveBitMap.GetBitmapBits(True)
51 |
52 | # Check if we have valid bitmap data
53 | if not bmpstr or len(bmpstr) <= 0:
54 | return "Failed to capture window content"
55 |
56 | img = Image.frombuffer(
57 | "RGB",
58 | (bmpinfo['bmWidth'], bmpinfo['bmHeight']),
59 | bmpstr, 'raw', 'BGRX', 0, 1
60 | )
61 |
62 | # Cleanup
63 | win32gui.DeleteObject(saveBitMap.GetHandle())
64 | saveDC.DeleteDC()
65 | mfcDC.DeleteDC()
66 | win32gui.ReleaseDC(hwnd, hwndDC)
67 |
68 | if result:
69 | # Preserve aspect ratio when resizing
70 | if width > 0 and height > 0:
71 | target_width = 1024
72 | target_height = int(height * (target_width / width))
73 |
74 | # Make sure we don't exceed maximum height
75 | if target_height > 2048:
76 | target_height = 2048
77 | target_width = int(width * (target_height / height))
78 |
79 | img = img.resize((target_width, target_height), Image.LANCZOS)
80 |
81 | buffer = io.BytesIO()
82 | img.save(buffer, format="PNG")
83 | buffer.seek(0)
84 |
85 | return FastMCPImage(data=buffer.read(), format="png")
86 | else:
87 | return "Screenshot may be partial or failed due to permissions"
88 | except Exception as e:
89 | return f"Failed to capture screenshot: {type(e).__name__}: {e}"
```
--------------------------------------------------------------------------------
/src/mcp_windows/media.py:
--------------------------------------------------------------------------------
```python
1 | import json
2 |
3 | from fastmcp import FastMCP
4 | from winrt.windows.foundation import IAsyncOperation
5 | from winrt.windows.media.control import (
6 | GlobalSystemMediaTransportControlsSessionManager as MediaManager,
7 | GlobalSystemMediaTransportControlsSessionMediaProperties as MediaProperties,
8 | GlobalSystemMediaTransportControlsSessionPlaybackInfo as PlaybackInfo,
9 | )
10 |
11 | mcp: FastMCP = FastMCP(
12 | name="Media",
13 | )
14 |
15 | PLAYBACK_STATUS = {
16 | 0: "closed",
17 | 1: "opened",
18 | 2: "changing",
19 | 3: "stopped",
20 | 4: "playing",
21 | 5: "paused",
22 | }
23 |
24 |
25 | @mcp.tool("get_media_sessions")
26 | async def get_media_sessions() -> str:
27 | """List all media playback sessions with metadata and control capability info."""
28 |
29 | manager_op: IAsyncOperation = MediaManager.request_async()
30 | manager = await manager_op
31 | sessions = manager.get_sessions()
32 |
33 | output = {}
34 | for session in sessions:
35 | props_op = session.try_get_media_properties_async()
36 | props: MediaProperties = await props_op
37 | playback_info: PlaybackInfo = session.get_playback_info()
38 | controls = playback_info.controls
39 |
40 | app_id = session.source_app_user_model_id
41 |
42 | output[app_id] = {
43 | "title": props.title or "unknown",
44 | "artist": props.artist or "unknown",
45 | "album_title": props.album_title or "unknown",
46 | "playback_status": str(PLAYBACK_STATUS.get(playback_info.playback_status)),
47 | "is_play_enabled": controls.is_play_enabled,
48 | "is_pause_enabled": controls.is_pause_enabled,
49 | "is_next_enabled": controls.is_next_enabled,
50 | "is_previous_enabled": controls.is_previous_enabled,
51 | }
52 |
53 | return json.dumps(output)
54 |
55 |
56 | @mcp.tool("pause")
57 | async def pause(app_id: str) -> str:
58 | """Pause the media playback for a given app_id using windows media control API."""
59 |
60 | manager_op: IAsyncOperation[MediaManager] = MediaManager.request_async()
61 | manager: MediaManager = await manager_op
62 |
63 | sessions = manager.get_sessions()
64 | for session in sessions:
65 | if session.source_app_user_model_id.lower() == app_id.lower():
66 | playback_info = session.get_playback_info()
67 | if playback_info.controls.is_pause_enabled:
68 | await session.try_pause_async()
69 | return "Paused"
70 | else:
71 | return "Pause not available"
72 |
73 | return "Session not found"
74 |
75 |
76 | @mcp.tool("play")
77 | async def play(app_id: str) -> str:
78 | """Play the media playback for a given app_id using windows media control API."""
79 |
80 | manager_op: IAsyncOperation[MediaManager] = MediaManager.request_async()
81 | manager: MediaManager = await manager_op
82 |
83 | sessions = manager.get_sessions()
84 | for session in sessions:
85 | if session.source_app_user_model_id.lower() == app_id.lower():
86 | playback_info = session.get_playback_info()
87 | if playback_info.controls.is_play_enabled:
88 | await session.try_play_async()
89 | return "Playing"
90 | else:
91 | return "Play not available"
92 |
93 | return "Session not found"
94 |
95 |
96 | @mcp.tool("next")
97 | async def next(app_id: str) -> str:
98 | """Skip to the next media item for the given app_id."""
99 |
100 | manager = await MediaManager.request_async()
101 | sessions = manager.get_sessions()
102 |
103 | for session in sessions:
104 | if session.source_app_user_model_id.lower() == app_id.lower():
105 | playback_info = session.get_playback_info()
106 | if playback_info.controls.is_next_enabled:
107 | await session.try_skip_next_async()
108 | return "Skipped to next track"
109 | else:
110 | return "Next track not available"
111 |
112 | return "Session not found"
113 |
114 |
115 | @mcp.tool("previous")
116 | async def previous(app_id: str) -> str:
117 | """Skip to the previous media item for the given app_id."""
118 |
119 | manager = await MediaManager.request_async()
120 | sessions = manager.get_sessions()
121 |
122 | for session in sessions:
123 | if session.source_app_user_model_id.lower() == app_id.lower():
124 | playback_info = session.get_playback_info()
125 | if playback_info.controls.is_previous_enabled:
126 | await session.try_skip_previous_async()
127 | return "Skipped to previous track"
128 | else:
129 | return "Previous track not available"
130 |
131 | return "Session not found"
132 |
```
--------------------------------------------------------------------------------
/src/mcp_windows/window_management.py:
--------------------------------------------------------------------------------
```python
1 | import asyncio
2 | import json
3 | import win32gui
4 | import win32con
5 | import win32process
6 | import win32api
7 | import psutil
8 |
9 | from fastmcp import FastMCP
10 |
11 | mcp: FastMCP = FastMCP(
12 | name="window_management"
13 | )
14 |
15 | def get_process_info(pid: int) -> dict:
16 | try:
17 | proc = psutil.Process(pid)
18 | return {
19 | "pid": pid,
20 | "exe": proc.name(),
21 | }
22 | except psutil.NoSuchProcess:
23 | return {
24 | "pid": pid,
25 | "exe": "<terminated>"
26 | }
27 |
28 | @mcp.tool("get_foreground_window_info")
29 | async def get_foreground_window_info() -> str:
30 | """Return information about the currently focused (foreground) window."""
31 | hwnd = win32gui.GetForegroundWindow()
32 | if hwnd == 0:
33 | return json.dumps({"error": "No active window"})
34 |
35 | _, pid = win32process.GetWindowThreadProcessId(hwnd)
36 | info = get_process_info(pid)
37 | info.update({
38 | "hwnd": hwnd,
39 | "title": win32gui.GetWindowText(hwnd),
40 | "class": win32gui.GetClassName(hwnd),
41 | })
42 | return json.dumps(info, ensure_ascii=False)
43 |
44 | @mcp.tool("get_window_list")
45 | async def list_open_windows() -> str:
46 | """Return a list of all top-level visible windows."""
47 | windows = []
48 |
49 | def callback(hwnd, _):
50 | if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowText(hwnd):
51 | _, pid = win32process.GetWindowThreadProcessId(hwnd)
52 | info = get_process_info(pid)
53 | info.update({
54 | "hwnd": hwnd,
55 | "title": win32gui.GetWindowText(hwnd),
56 | "class": win32gui.GetClassName(hwnd),
57 | })
58 | windows.append(info)
59 |
60 | win32gui.EnumWindows(callback, None)
61 | return json.dumps(windows, ensure_ascii=False)
62 |
63 | @mcp.tool("focus_window")
64 | async def focus_window(hwnd: int) -> str:
65 | """Force focus a window using all known safe tricks (thread attach, fake input, fallback restore)."""
66 | try:
67 | hwnd = int(hwnd)
68 |
69 | if not win32gui.IsWindow(hwnd):
70 | return "Invalid HWND"
71 |
72 | # Step 1: Only restore if minimized (prevent resizing)
73 | if win32gui.IsIconic(hwnd):
74 | win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
75 |
76 | # Step 2: Try normal focus via thread attach
77 | fg_hwnd = win32gui.GetForegroundWindow()
78 | fg_thread = win32process.GetWindowThreadProcessId(fg_hwnd)[0]
79 | current_thread = win32api.GetCurrentThreadId()
80 |
81 | if fg_thread != current_thread:
82 | win32process.AttachThreadInput(fg_thread, current_thread, True)
83 |
84 | try:
85 | win32gui.SetForegroundWindow(hwnd)
86 | except Exception:
87 | pass
88 |
89 | if fg_thread != current_thread:
90 | win32process.AttachThreadInput(fg_thread, current_thread, False)
91 |
92 | # Step 3: Check if it worked
93 | if win32gui.GetForegroundWindow() == hwnd:
94 | return "Focused window successfully"
95 |
96 | # Step 4: Fallback — simulate user input (to defeat foreground lock)
97 | win32api.keybd_event(0, 0, 0, 0)
98 | await asyncio.sleep(0.05)
99 |
100 | # Step 5: Try again
101 | try:
102 | win32gui.SetForegroundWindow(hwnd)
103 | except Exception:
104 | pass
105 |
106 | if win32gui.GetForegroundWindow() == hwnd:
107 | return "Focused window (after simulating input)"
108 |
109 | # Step 6: Hard fallback — minimize + restore
110 | win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE)
111 | await asyncio.sleep(0.2)
112 | win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
113 | win32gui.SetForegroundWindow(hwnd)
114 |
115 | if win32gui.GetForegroundWindow() == hwnd:
116 | return "Focused window (after minimize/restore trick)"
117 |
118 | return "Could not focus window: OS restrictions"
119 |
120 | except Exception as e:
121 | return f"Could not focus window: {type(e).__name__}: {e}"
122 |
123 |
124 | @mcp.tool("close_window")
125 | async def close_window(hwnd: int) -> str:
126 | """Close the specified window."""
127 | try:
128 | win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0)
129 | return "Closed window"
130 | except Exception as e:
131 | return f"Could not close window: {type(e).__name__}: {e}"
132 |
133 | @mcp.tool("minimize_window")
134 | async def minimize_window(hwnd: int) -> str:
135 | """Minimize the specified window."""
136 | try:
137 | win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE)
138 | return "Minimized window"
139 | except Exception as e:
140 | return f"Could not minimize window: {type(e).__name__}: {e}"
141 |
```