mcp-windows 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_windows/__init__.py +4 -0
- mcp_windows/appid.py +34 -0
- mcp_windows/main.py +18 -0
- mcp_windows/media.py +70 -0
- mcp_windows/monitors.py +35 -0
- mcp_windows/notifications.py +38 -0
- mcp_windows/window_management.py +140 -0
- mcp_windows-0.0.1.dist-info/METADATA +84 -0
- mcp_windows-0.0.1.dist-info/RECORD +11 -0
- mcp_windows-0.0.1.dist-info/WHEEL +4 -0
- mcp_windows-0.0.1.dist-info/entry_points.txt +2 -0
mcp_windows/__init__.py
ADDED
mcp_windows/appid.py
ADDED
@@ -0,0 +1,34 @@
|
|
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()
|
mcp_windows/main.py
ADDED
@@ -0,0 +1,18 @@
|
|
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
|
+
|
9
|
+
sep = environ.get("FASTMCP_TOOL_SEPARATOR", "_")
|
10
|
+
|
11
|
+
mcp: FastMCP = FastMCP(
|
12
|
+
name="windows",
|
13
|
+
)
|
14
|
+
|
15
|
+
mcp.mount("media", media_mcp, tool_separator=sep)
|
16
|
+
mcp.mount("notifications", notifications_mcp, tool_separator=sep)
|
17
|
+
mcp.mount("window_management", window_management_mcp, tool_separator=sep)
|
18
|
+
mcp.mount("monitors", monitors_mcp, tool_separator=sep)
|
mcp_windows/media.py
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
import json
|
2
|
+
from fastmcp import FastMCP
|
3
|
+
from winrt.windows.media.control import GlobalSystemMediaTransportControlsSessionManager as MediaManager, GlobalSystemMediaTransportControlsSessionMediaProperties as MediaProperties
|
4
|
+
from winrt.windows.foundation import IAsyncOperation
|
5
|
+
|
6
|
+
mcp: FastMCP = FastMCP(
|
7
|
+
name="Media",
|
8
|
+
)
|
9
|
+
|
10
|
+
@mcp.tool("get_media_sessions")
|
11
|
+
async def get_media_sessions() -> str:
|
12
|
+
"""List all media playback sessions using windows media control API."""
|
13
|
+
|
14
|
+
manager_op: IAsyncOperation = MediaManager.request_async()
|
15
|
+
manager = await manager_op
|
16
|
+
sessions = manager.get_sessions()
|
17
|
+
|
18
|
+
output = {}
|
19
|
+
for session in sessions:
|
20
|
+
props_op = session.try_get_media_properties_async()
|
21
|
+
props: MediaProperties = await props_op
|
22
|
+
app_id = session.source_app_user_model_id
|
23
|
+
|
24
|
+
output[app_id] = {
|
25
|
+
"title": props.title or "unknown",
|
26
|
+
"artist": props.artist or "unknown",
|
27
|
+
"album_title": props.album_title or "unknown",
|
28
|
+
}
|
29
|
+
|
30
|
+
return json.dumps(output)
|
31
|
+
|
32
|
+
@mcp.tool("pause")
|
33
|
+
async def pause(app_id: str) -> str:
|
34
|
+
"""Pause the media playback for a given app_id using windows media control API."""
|
35
|
+
|
36
|
+
manager_op: IAsyncOperation[MediaManager] = \
|
37
|
+
MediaManager.request_async()
|
38
|
+
manager: MediaManager = await manager_op
|
39
|
+
|
40
|
+
sessions = manager.get_sessions()
|
41
|
+
for session in sessions:
|
42
|
+
if session.source_app_user_model_id.lower() == app_id.lower():
|
43
|
+
playback_info = session.get_playback_info()
|
44
|
+
if playback_info.controls.is_pause_enabled:
|
45
|
+
await session.try_pause_async()
|
46
|
+
return "Paused"
|
47
|
+
else:
|
48
|
+
return "Pause not available"
|
49
|
+
|
50
|
+
return "Session not found"
|
51
|
+
|
52
|
+
@mcp.tool("play")
|
53
|
+
async def play(app_id: str) -> str:
|
54
|
+
"""Play the media playback for a given app_id using windows media control API."""
|
55
|
+
|
56
|
+
manager_op: IAsyncOperation[MediaManager] = \
|
57
|
+
MediaManager.request_async()
|
58
|
+
manager: MediaManager = await manager_op
|
59
|
+
|
60
|
+
sessions = manager.get_sessions()
|
61
|
+
for session in sessions:
|
62
|
+
if session.source_app_user_model_id.lower() == app_id.lower():
|
63
|
+
playback_info = session.get_playback_info()
|
64
|
+
if playback_info.controls.is_play_enabled:
|
65
|
+
await session.try_play_async()
|
66
|
+
return "Playing"
|
67
|
+
else:
|
68
|
+
return "Play not available"
|
69
|
+
|
70
|
+
return "Session not found"
|
mcp_windows/monitors.py
ADDED
@@ -0,0 +1,35 @@
|
|
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}"
|
@@ -0,0 +1,38 @@
|
|
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"
|
@@ -0,0 +1,140 @@
|
|
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}"
|
@@ -0,0 +1,84 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: mcp-windows
|
3
|
+
Version: 0.0.1
|
4
|
+
Summary: Add your description here
|
5
|
+
Author-email: TerminalMan <84923604+SecretiveShell@users.noreply.github.com>
|
6
|
+
Requires-Python: >=3.13
|
7
|
+
Requires-Dist: fastmcp>=2.2.0
|
8
|
+
Requires-Dist: mcp>=1.6.0
|
9
|
+
Requires-Dist: psutil>=7.0.0
|
10
|
+
Requires-Dist: pywin32>=310
|
11
|
+
Requires-Dist: winrt-runtime>=3.1.0
|
12
|
+
Requires-Dist: winrt-windows-data-xml-dom>=3.1.0
|
13
|
+
Requires-Dist: winrt-windows-foundation-collections>=3.1.0
|
14
|
+
Requires-Dist: winrt-windows-foundation>=3.1.0
|
15
|
+
Requires-Dist: winrt-windows-media-control>=3.1.0
|
16
|
+
Requires-Dist: winrt-windows-ui-notifications>=3.1.0
|
17
|
+
Description-Content-Type: text/markdown
|
18
|
+
|
19
|
+
# mcp-windows
|
20
|
+
|
21
|
+
MCP server for the windows API.
|
22
|
+
|
23
|
+
## Installation
|
24
|
+
|
25
|
+
add this to your claude mcp config:
|
26
|
+
|
27
|
+
```json
|
28
|
+
{
|
29
|
+
"mcpServers": {
|
30
|
+
"windows": {
|
31
|
+
"command": "uvx",
|
32
|
+
"args": [
|
33
|
+
"mcp-windows"
|
34
|
+
]
|
35
|
+
}
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
or locally:
|
40
|
+
|
41
|
+
```json
|
42
|
+
{
|
43
|
+
"mcpServers": {
|
44
|
+
"windows": {
|
45
|
+
"command": "uv",
|
46
|
+
"args": [
|
47
|
+
"--directory",
|
48
|
+
"C:\\Users\\{name}\\Documents\\mcp-windows",
|
49
|
+
"run",
|
50
|
+
"mcp-windows"
|
51
|
+
]
|
52
|
+
}
|
53
|
+
}
|
54
|
+
}
|
55
|
+
```
|
56
|
+
|
57
|
+
## Features
|
58
|
+
|
59
|
+
### Media
|
60
|
+
|
61
|
+
- get_media_sessions
|
62
|
+
- pause
|
63
|
+
- play
|
64
|
+
|
65
|
+
### Notifications
|
66
|
+
|
67
|
+
- send_toast
|
68
|
+
|
69
|
+
### Window Management
|
70
|
+
|
71
|
+
- get_foreground_window_info
|
72
|
+
- get_window_list
|
73
|
+
- focus_window
|
74
|
+
- close_window
|
75
|
+
- minimize_window
|
76
|
+
|
77
|
+
### Monitors
|
78
|
+
|
79
|
+
- sleep_monitors
|
80
|
+
- wake_monitors
|
81
|
+
|
82
|
+
## License
|
83
|
+
|
84
|
+
MIT
|
@@ -0,0 +1,11 @@
|
|
1
|
+
mcp_windows/__init__.py,sha256=csYpM8A238IdEljN4FLTcQUFLfNDM7xOAI4ZlyyMTrY,75
|
2
|
+
mcp_windows/appid.py,sha256=gJN9Ug4S-mdZkpe1447_E7N33pzhPiKnR24dIHMHyZo,1201
|
3
|
+
mcp_windows/main.py,sha256=p8MddWAGvZfhgLu_lklzfqfC2f9qbC6FsCWWSKIOqzk,648
|
4
|
+
mcp_windows/media.py,sha256=Kf8bvNpd15kt3yZQJ8hUKnGl0ZLq2lg4ximZZZHPpX8,2518
|
5
|
+
mcp_windows/monitors.py,sha256=ffFMvp6qEEd84wyPKrEipNSfL81jLAOPPYMORF5wBug,1037
|
6
|
+
mcp_windows/notifications.py,sha256=UPsqsmiOvIVak4BC6ol6SrcSl1IC17oGo7h4oXluRO8,940
|
7
|
+
mcp_windows/window_management.py,sha256=j_aNIdviNf75xZAPP0DacDAokjUrz17bFRQ_iXa-6sQ,4515
|
8
|
+
mcp_windows-0.0.1.dist-info/METADATA,sha256=IQtr3HerrCvSZiefDETiQIpx_S7Dym7Z-ljKEisj9NE,1380
|
9
|
+
mcp_windows-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
10
|
+
mcp_windows-0.0.1.dist-info/entry_points.txt,sha256=aTryo6W9hFBSP_7zE0aL0IWlIJ2eDJX7d4qIqHQ76W4,49
|
11
|
+
mcp_windows-0.0.1.dist-info/RECORD,,
|