windows-server-mcp 1.0.0__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.
Files changed (39) hide show
  1. windows_server_mcp/__init__.py +1 -0
  2. windows_server_mcp/registry.py +118 -0
  3. windows_server_mcp/server.py +116 -0
  4. windows_server_mcp/shell.py +200 -0
  5. windows_server_mcp/tools/__init__.py +1 -0
  6. windows_server_mcp/tools/advanced_process.py +184 -0
  7. windows_server_mcp/tools/developer_environment.py +143 -0
  8. windows_server_mcp/tools/disk_health.py +68 -0
  9. windows_server_mcp/tools/driver_maintenance.py +67 -0
  10. windows_server_mcp/tools/event_diagnostics.py +68 -0
  11. windows_server_mcp/tools/execute_powershell.py +135 -0
  12. windows_server_mcp/tools/hardware_drivers.py +217 -0
  13. windows_server_mcp/tools/manage_network.py +56 -0
  14. windows_server_mcp/tools/manage_registry.py +74 -0
  15. windows_server_mcp/tools/media_management.py +131 -0
  16. windows_server_mcp/tools/network_diagnostics.py +198 -0
  17. windows_server_mcp/tools/optimization.py +79 -0
  18. windows_server_mcp/tools/peripherals.py +84 -0
  19. windows_server_mcp/tools/power_hardware.py +84 -0
  20. windows_server_mcp/tools/process_management.py +66 -0
  21. windows_server_mcp/tools/restore_update_controls.py +150 -0
  22. windows_server_mcp/tools/safe_file_operations.py +520 -0
  23. windows_server_mcp/tools/security_updates.py +99 -0
  24. windows_server_mcp/tools/services.py +47 -0
  25. windows_server_mcp/tools/shell_helpers.py +91 -0
  26. windows_server_mcp/tools/software_audit.py +65 -0
  27. windows_server_mcp/tools/software_maintenance.py +119 -0
  28. windows_server_mcp/tools/startup_tasks.py +143 -0
  29. windows_server_mcp/tools/storage_analysis.py +122 -0
  30. windows_server_mcp/tools/system_info.py +69 -0
  31. windows_server_mcp/tools/task_manager.py +98 -0
  32. windows_server_mcp/tools/text_files.py +113 -0
  33. windows_server_mcp/tools/virtualization.py +71 -0
  34. windows_server_mcp/tools/window_management.py +109 -0
  35. windows_server_mcp-1.0.0.dist-info/METADATA +136 -0
  36. windows_server_mcp-1.0.0.dist-info/RECORD +39 -0
  37. windows_server_mcp-1.0.0.dist-info/WHEEL +4 -0
  38. windows_server_mcp-1.0.0.dist-info/entry_points.txt +2 -0
  39. windows_server_mcp-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1 @@
1
+ # This file makes windows_server_mcp a Python package
@@ -0,0 +1,118 @@
1
+ import functools
2
+ import inspect
3
+ import logging
4
+ import time
5
+
6
+ log = logging.getLogger("windows_server_mcp")
7
+
8
+ registered_tools = []
9
+
10
+ #: Hard ceiling (seconds) for a single tool call. Commands have their own,
11
+ #: shorter timeouts (see shell.py); this only frees the server when something
12
+ #: blocks inside Python itself (a stuck syscall, a hung handle query...).
13
+ DEFAULT_TOOL_DEADLINE = 180
14
+ LONG_TOOL_DEADLINE = 3700 # update/driver installs
15
+
16
+
17
+ def _deadline_for(name: str, kwargs: dict) -> int:
18
+ if name == "execute_powershell":
19
+ try:
20
+ return min(int(kwargs.get("timeout_seconds", 60)), 3600) + 30
21
+ except (TypeError, ValueError):
22
+ return 90
23
+ if "install" in name:
24
+ return LONG_TOOL_DEADLINE
25
+ return DEFAULT_TOOL_DEADLINE
26
+
27
+
28
+ def _run_in_thread(func):
29
+ """Run a blocking tool in a worker thread with a hard deadline.
30
+
31
+ * With ``mcp`` 1.x plain sync tools run on the event loop, so one slow call
32
+ froze the whole server. A thread keeps the server responsive.
33
+ * If the thread blocks forever (e.g. a stuck syscall), the call is
34
+ abandoned after the deadline and an error is returned, instead of the
35
+ client waiting until its own timeout.
36
+ * Every call is logged (start/finish/abandon) so a hang can be traced to a
37
+ specific tool: a ``start`` line with no ``finished`` line is the culprit.
38
+ """
39
+ if inspect.iscoroutinefunction(func):
40
+ return func
41
+
42
+ @functools.wraps(func)
43
+ async def wrapper(*args, **kwargs):
44
+ import anyio
45
+
46
+ name = func.__name__
47
+ deadline = _deadline_for(name, kwargs)
48
+ started = time.monotonic()
49
+ log.info("tool start: %s (deadline %ss)", name, deadline)
50
+
51
+ def target():
52
+ try:
53
+ return func(*args, **kwargs)
54
+ finally:
55
+ log.info("tool finished: %s in %.1fs", name, time.monotonic() - started)
56
+
57
+ try:
58
+ with anyio.fail_after(deadline):
59
+ return await anyio.to_thread.run_sync(target, abandon_on_cancel=True)
60
+ except TimeoutError:
61
+ log.error("tool ABANDONED: %s still running after %ss", name, deadline)
62
+ return (
63
+ f"Error: tool '{name}' did not finish within {deadline}s and was abandoned. "
64
+ "The server is still responsive; try a narrower request."
65
+ )
66
+ except Exception:
67
+ log.exception("tool failed: %s", name)
68
+ raise
69
+
70
+ return wrapper
71
+
72
+
73
+ def windows_server_mcp_tool():
74
+ """
75
+ Decorator to register a function as a Windows MCP tool.
76
+ """
77
+ def decorator(func):
78
+ registered_tools.append(func)
79
+ return func
80
+ return decorator
81
+
82
+ def register_all_tools(mcp):
83
+ """
84
+ Imports all tool modules and registers them with the server instance
85
+ (``MCPServer`` on mcp 2.x, ``FastMCP`` on mcp 1.x).
86
+ """
87
+ import windows_server_mcp.tools.execute_powershell
88
+ import windows_server_mcp.tools.manage_registry
89
+ import windows_server_mcp.tools.manage_network
90
+ import windows_server_mcp.tools.task_manager
91
+ import windows_server_mcp.tools.software_audit
92
+ import windows_server_mcp.tools.storage_analysis
93
+ import windows_server_mcp.tools.system_info
94
+ import windows_server_mcp.tools.process_management
95
+ import windows_server_mcp.tools.optimization
96
+ import windows_server_mcp.tools.services
97
+ import windows_server_mcp.tools.hardware_drivers
98
+ import windows_server_mcp.tools.media_management
99
+ import windows_server_mcp.tools.advanced_process
100
+ import windows_server_mcp.tools.window_management
101
+ import windows_server_mcp.tools.startup_tasks
102
+ import windows_server_mcp.tools.event_diagnostics
103
+ import windows_server_mcp.tools.network_diagnostics
104
+ import windows_server_mcp.tools.security_updates
105
+ import windows_server_mcp.tools.power_hardware
106
+ import windows_server_mcp.tools.safe_file_operations
107
+ import windows_server_mcp.tools.developer_environment
108
+ import windows_server_mcp.tools.shell_helpers
109
+ import windows_server_mcp.tools.disk_health
110
+ import windows_server_mcp.tools.driver_maintenance
111
+ import windows_server_mcp.tools.software_maintenance
112
+ import windows_server_mcp.tools.peripherals
113
+ import windows_server_mcp.tools.virtualization
114
+ import windows_server_mcp.tools.restore_update_controls
115
+ import windows_server_mcp.tools.text_files
116
+
117
+ for func in registered_tools:
118
+ mcp.tool()(_run_in_thread(func))
@@ -0,0 +1,116 @@
1
+ """Entry point for the Windows Management MCP server.
2
+
3
+ This module works with both generations of the official MCP Python SDK:
4
+
5
+ * ``mcp >= 2.0`` -> ``MCPServer`` (``mcp.server.mcpserver``).
6
+ In v2 the old ``FastMCP`` class was renamed to ``MCPServer`` and the
7
+ ``mcp.server.fastmcp`` module was removed without a deprecation shim.
8
+ * ``mcp 1.x`` -> ``FastMCP`` (``mcp.server.fastmcp``).
9
+
10
+ Migration guide:
11
+ https://py.sdk.modelcontextprotocol.io/v2/migration/#fastmcp-renamed-to-mcpserver
12
+ """
13
+
14
+ import logging
15
+ import logging.handlers
16
+ import os
17
+ import sys
18
+ import tempfile
19
+ from importlib.metadata import PackageNotFoundError, version as _dist_version
20
+
21
+ from windows_server_mcp.registry import register_all_tools
22
+
23
+ try: # mcp 2.x
24
+ from mcp.server.mcpserver import MCPServer as _Server
25
+
26
+ _SDK_GENERATION = 2
27
+ except ImportError: # mcp 1.x
28
+ from mcp.server.fastmcp import FastMCP as _Server
29
+
30
+ _SDK_GENERATION = 1
31
+
32
+ SERVER_NAME = "Windows Management"
33
+
34
+
35
+ def _package_version() -> str:
36
+ try:
37
+ return _dist_version("windows-server-mcp")
38
+ except PackageNotFoundError: # running from a source checkout
39
+ return "0.0.0+local"
40
+
41
+
42
+ def create_server():
43
+ """Build the MCP server and register every Windows tool on it."""
44
+ if _SDK_GENERATION == 2:
45
+ # In v2 only `name` is positional-safe: pass everything else by keyword.
46
+ server = _Server(SERVER_NAME, version=_package_version())
47
+ else:
48
+ server = _Server(SERVER_NAME)
49
+ register_all_tools(server)
50
+ return server
51
+
52
+
53
+ mcp = create_server()
54
+
55
+
56
+ def _log_dir() -> str:
57
+ base = os.environ.get("LOCALAPPDATA") or tempfile.gettempdir()
58
+ return os.path.join(base, "windows-server-mcp")
59
+
60
+
61
+ def _harden_stdio() -> None:
62
+ """Make sure logging can never block the server.
63
+
64
+ When the server is started with ``pythonw.exe`` by a bridge/launcher,
65
+ ``stderr`` is often a pipe nobody reads. Once ~64 KB of log lines fill it,
66
+ the next ``write`` blocks forever and every tool call "hangs" until the
67
+ process is killed. We send our own logs and ``stderr`` to files instead.
68
+ ``stdout`` is left alone: it carries the MCP protocol.
69
+ """
70
+ try:
71
+ directory = _log_dir()
72
+ os.makedirs(directory, exist_ok=True)
73
+ log_file = os.path.join(directory, "server.log")
74
+ err_file = os.path.join(directory, "stderr.log")
75
+
76
+ handler = logging.handlers.RotatingFileHandler(
77
+ log_file, maxBytes=1_000_000, backupCount=2, encoding="utf-8"
78
+ )
79
+ handler.setFormatter(logging.Formatter("%(asctime)s %(threadName)s %(levelname)s %(message)s"))
80
+ package_logger = logging.getLogger("windows_server_mcp")
81
+ package_logger.setLevel(logging.INFO)
82
+ package_logger.addHandler(handler)
83
+ package_logger.propagate = False
84
+
85
+ if os.path.exists(err_file) and os.path.getsize(err_file) > 5_000_000:
86
+ os.remove(err_file)
87
+ err_stream = open(err_file, "a", buffering=1, encoding="utf-8", errors="replace")
88
+ previous_stderr = sys.stderr
89
+ sys.stderr = err_stream
90
+ loggers = [logging.getLogger()] + [
91
+ lg for lg in logging.root.manager.loggerDict.values() if isinstance(lg, logging.Logger)
92
+ ]
93
+ for lg in loggers:
94
+ for existing in lg.handlers:
95
+ # Only re-point handlers that write to the old stderr; leave
96
+ # foreign handlers (e.g. pytest's capture handlers) untouched.
97
+ if (
98
+ isinstance(existing, logging.StreamHandler)
99
+ and not isinstance(existing, logging.FileHandler)
100
+ and getattr(existing, "stream", None) in (previous_stderr, sys.__stderr__)
101
+ ):
102
+ existing.setStream(err_stream)
103
+ package_logger.info("server starting (python %s, sdk generation %s)", sys.executable, _SDK_GENERATION)
104
+ except Exception:
105
+ # Never let logging setup prevent the server from starting.
106
+ pass
107
+
108
+
109
+ def main():
110
+ """Run the server over stdio (the default transport)."""
111
+ _harden_stdio()
112
+ mcp.run()
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()
@@ -0,0 +1,200 @@
1
+ """Safe subprocess helpers shared by every tool.
2
+
3
+ Why this module exists
4
+ ----------------------
5
+ The MCP server talks to the client over stdio. A child process that
6
+
7
+ * inherits the server's ``stdin`` (the MCP channel) can block waiting for input
8
+ (``pause``, installers, ``Read-Host``...), and
9
+ * has no ``timeout`` never returns if PowerShell hangs (``Get-Counter``,
10
+ Windows Update COM calls, a UAC prompt nobody can see...).
11
+
12
+ When that happens the tool call never completes and the MCP client eventually
13
+ gives up with its own timeout. Every external command should go through
14
+ :func:`run` / :func:`run_powershell` so it always has a deadline, never reads
15
+ from the MCP channel, never pops a console window and is killed together with
16
+ all of its children when the deadline expires.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import subprocess
23
+ import sys
24
+ from typing import Any, Sequence
25
+
26
+ #: Used when a caller does not pass an explicit ``timeout``.
27
+ DEFAULT_TIMEOUT = 60
28
+
29
+ #: Exit code reported when a command was killed because it timed out
30
+ #: (same convention as GNU ``timeout``).
31
+ TIMEOUT_RETURNCODE = 124
32
+
33
+ _IS_WINDOWS = sys.platform == "win32"
34
+ _CREATE_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0)
35
+ _CREATE_NEW_PROCESS_GROUP = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
36
+
37
+
38
+ #: Prepended to every PowerShell ``-Command`` script. Windows PowerShell 5.1
39
+ #: writes to a pipe using the console's OEM code page (cp437/cp850...), which
40
+ #: turns accents and emojis into garbage ("é", "?", Chinese-looking text) once
41
+ #: Python decodes it. Forcing UTF-8 (without BOM) on both sides fixes that.
42
+ UTF8_PREAMBLE = (
43
+ "try { [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false) } catch {}; "
44
+ "try { [Console]::InputEncoding = New-Object System.Text.UTF8Encoding($false) } catch {}; "
45
+ "$OutputEncoding = New-Object System.Text.UTF8Encoding($false); "
46
+ )
47
+
48
+
49
+ def _is_powershell_command(args: Any) -> bool:
50
+ if isinstance(args, (str, bytes)) or not args:
51
+ return False
52
+ exe = os.path.basename(str(args[0])).lower()
53
+ if exe not in ("powershell", "powershell.exe", "pwsh", "pwsh.exe"):
54
+ return False
55
+ return any(str(a).lower() in ("-command", "-c") for a in args[:-1])
56
+
57
+
58
+ def with_utf8_preamble(args: Sequence[str]) -> list[str]:
59
+ """Return *args* with the UTF-8 preamble added to the PowerShell script."""
60
+ argv = list(args)
61
+ for index, value in enumerate(argv[:-1]):
62
+ if str(value).lower() in ("-command", "-c") and not argv[index + 1].startswith(UTF8_PREAMBLE):
63
+ argv[index + 1] = UTF8_PREAMBLE + argv[index + 1]
64
+ break
65
+ return argv
66
+
67
+
68
+ def powershell_command(script: str, *, non_interactive: bool = True) -> list[str]:
69
+ """Build the argv used to run *script* with Windows PowerShell."""
70
+ argv = ["powershell", "-NoProfile"]
71
+ if non_interactive:
72
+ argv.append("-NonInteractive")
73
+ argv += ["-Command", UTF8_PREAMBLE + script]
74
+ return argv
75
+
76
+
77
+ def kill_process_tree(proc: subprocess.Popen) -> None:
78
+ """Kill *proc* and every process it spawned."""
79
+ try:
80
+ if _IS_WINDOWS:
81
+ subprocess.run(
82
+ ["taskkill", "/PID", str(proc.pid), "/T", "/F"],
83
+ stdin=subprocess.DEVNULL,
84
+ stdout=subprocess.DEVNULL,
85
+ stderr=subprocess.DEVNULL,
86
+ timeout=10,
87
+ creationflags=_CREATE_NO_WINDOW,
88
+ )
89
+ else: # pragma: no cover - only used when developing on Linux/macOS
90
+ import signal
91
+
92
+ os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
93
+ except Exception:
94
+ pass
95
+ try:
96
+ proc.kill()
97
+ except Exception:
98
+ pass
99
+
100
+
101
+ def _timeout_message(args: Any, timeout: float) -> str:
102
+ return (
103
+ f"Command timed out after {timeout:g}s and was terminated. "
104
+ "Try a narrower query, or raise the timeout if the task is expected to be slow."
105
+ )
106
+
107
+
108
+ def run(
109
+ args: Sequence[str] | str,
110
+ *,
111
+ timeout: float | None = DEFAULT_TIMEOUT,
112
+ capture_output: bool = True,
113
+ text: bool = True,
114
+ check: bool = False,
115
+ **kwargs: Any,
116
+ ) -> subprocess.CompletedProcess:
117
+ """Drop-in replacement for :func:`subprocess.run` that cannot hang.
118
+
119
+ Differences from ``subprocess.run``:
120
+
121
+ * ``timeout`` defaults to :data:`DEFAULT_TIMEOUT`. Pass ``None`` only if
122
+ you really want to wait forever.
123
+ * ``stdin`` is always closed (``DEVNULL``) so the child can never read from
124
+ the MCP stdio channel.
125
+ * No console window is created on Windows.
126
+ * On timeout the whole process tree is killed and a ``CompletedProcess``
127
+ with ``returncode == 124`` is *returned* (not raised). ``stderr``
128
+ contains an explanatory message, so the existing
129
+ ``if result.returncode != 0: return result.stderr`` code paths report the
130
+ timeout to the model instead of blowing up.
131
+ """
132
+ kwargs.pop("stdin", None)
133
+ if _is_powershell_command(args):
134
+ args = with_utf8_preamble(args)
135
+ if text:
136
+ kwargs.setdefault("encoding", "utf-8")
137
+ if capture_output:
138
+ kwargs.setdefault("stdout", subprocess.PIPE)
139
+ kwargs.setdefault("stderr", subprocess.PIPE)
140
+ if text:
141
+ kwargs.setdefault("errors", "replace")
142
+ flags = kwargs.pop("creationflags", 0) | _CREATE_NO_WINDOW
143
+ if not _IS_WINDOWS: # pragma: no cover
144
+ kwargs.setdefault("start_new_session", True)
145
+ flags = 0
146
+ else:
147
+ flags |= _CREATE_NEW_PROCESS_GROUP
148
+
149
+ proc = subprocess.Popen(
150
+ args,
151
+ stdin=subprocess.DEVNULL,
152
+ text=text,
153
+ creationflags=flags,
154
+ **kwargs,
155
+ )
156
+ try:
157
+ stdout, stderr = proc.communicate(timeout=timeout)
158
+ except subprocess.TimeoutExpired:
159
+ kill_process_tree(proc)
160
+ try:
161
+ stdout, stderr = proc.communicate(timeout=5)
162
+ except Exception:
163
+ stdout, stderr = ("" if text else b""), ("" if text else b"")
164
+ message = _timeout_message(args, timeout or 0)
165
+ if text:
166
+ stderr = f"{(stderr or '').rstrip()}\n{message}".strip()
167
+ else:
168
+ stderr = (stderr or b"") + b"\n" + message.encode()
169
+ return subprocess.CompletedProcess(args, TIMEOUT_RETURNCODE, stdout, stderr)
170
+ except BaseException:
171
+ kill_process_tree(proc)
172
+ raise
173
+
174
+ result = subprocess.CompletedProcess(args, proc.returncode, stdout, stderr)
175
+ if check and result.returncode != 0:
176
+ raise subprocess.CalledProcessError(
177
+ result.returncode, args, output=stdout, stderr=stderr
178
+ )
179
+ return result
180
+
181
+
182
+ def run_powershell(
183
+ script: str,
184
+ *,
185
+ timeout: float | None = DEFAULT_TIMEOUT,
186
+ non_interactive: bool = True,
187
+ ) -> subprocess.CompletedProcess:
188
+ """Run a PowerShell script with a deadline and a closed stdin."""
189
+ return run(powershell_command(script, non_interactive=non_interactive), timeout=timeout)
190
+
191
+
192
+ def spawn_detached(args: Sequence[str]) -> subprocess.Popen:
193
+ """Start a GUI program (e.g. Explorer) without waiting for it or sharing stdio."""
194
+ return subprocess.Popen(
195
+ args,
196
+ stdin=subprocess.DEVNULL,
197
+ stdout=subprocess.DEVNULL,
198
+ stderr=subprocess.DEVNULL,
199
+ creationflags=_CREATE_NO_WINDOW if _IS_WINDOWS else 0,
200
+ )
@@ -0,0 +1 @@
1
+ # Tools package
@@ -0,0 +1,184 @@
1
+ import datetime
2
+ import os
3
+ import sys
4
+ from windows_server_mcp.shell import TIMEOUT_RETURNCODE, run
5
+ from typing import Optional
6
+
7
+ import psutil
8
+
9
+ from windows_server_mcp.registry import windows_server_mcp_tool
10
+
11
+
12
+ def _format_time(timestamp: float) -> str:
13
+ return datetime.datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
14
+
15
+
16
+ def _signature_status(exe_path: Optional[str]) -> str:
17
+ if not exe_path or not os.path.exists(exe_path):
18
+ return "Unknown"
19
+
20
+ escaped_path = exe_path.replace("'", "''")
21
+ ps_cmd = (
22
+ "Get-AuthenticodeSignature -LiteralPath "
23
+ f"'{escaped_path}' | "
24
+ "Select-Object Status,SignerCertificate | ConvertTo-Json -Depth 3"
25
+ )
26
+ try:
27
+ result = run(
28
+ ["powershell", "-NoProfile", "-Command", ps_cmd],
29
+ capture_output=True,
30
+ text=True,
31
+ timeout=10,
32
+ )
33
+ if result.returncode != 0 or not result.stdout.strip():
34
+ return "Unknown"
35
+ return result.stdout.strip()
36
+ except Exception:
37
+ return "Unknown"
38
+
39
+
40
+ @windows_server_mcp_tool()
41
+ def get_process_details(pid: int) -> str:
42
+ """
43
+ Return detailed process metadata for a PID, including command line, parent,
44
+ executable path, resource usage, open files, network connections, and signature status.
45
+ """
46
+ try:
47
+ proc = psutil.Process(pid)
48
+ with proc.oneshot():
49
+ parent = proc.parent()
50
+ memory = proc.memory_info()
51
+ cpu_times = proc.cpu_times()
52
+ exe_path = proc.exe() if proc.exe else ""
53
+ lines = [
54
+ f"Name: {proc.name()}",
55
+ f"PID: {proc.pid}",
56
+ f"Status: {proc.status()}",
57
+ f"User: {proc.username()}",
58
+ f"Created: {_format_time(proc.create_time())}",
59
+ f"Parent: {parent.name()} (PID: {parent.pid})" if parent else "Parent: None",
60
+ f"Executable: {exe_path}",
61
+ f"Command Line: {' '.join(proc.cmdline())}",
62
+ f"Working Directory: {proc.cwd()}",
63
+ f"Threads: {proc.num_threads()}",
64
+ f"Handles: {proc.num_handles() if hasattr(proc, 'num_handles') else 'Unknown'}",
65
+ f"CPU Time: user={cpu_times.user:.2f}s system={cpu_times.system:.2f}s",
66
+ f"Memory RSS: {memory.rss / (1024 ** 2):.2f} MB",
67
+ f"Memory VMS: {memory.vms / (1024 ** 2):.2f} MB",
68
+ f"Signature: {_signature_status(exe_path)}",
69
+ ]
70
+
71
+ try:
72
+ open_files = proc.open_files()
73
+ lines.append("\nOpen Files:")
74
+ lines.extend(f" {file.path}" for file in open_files[:20])
75
+ if len(open_files) > 20:
76
+ lines.append(f" ... and {len(open_files) - 20} more")
77
+ except Exception as exc:
78
+ lines.append(f"\nOpen Files: unavailable ({exc})")
79
+
80
+ try:
81
+ connections = proc.net_connections(kind="inet")
82
+ lines.append("\nNetwork Connections:")
83
+ for conn in connections[:20]:
84
+ laddr = f"{conn.laddr.ip}:{conn.laddr.port}" if conn.laddr else ""
85
+ raddr = f"{conn.raddr.ip}:{conn.raddr.port}" if conn.raddr else ""
86
+ lines.append(f" {laddr} -> {raddr} [{conn.status}]")
87
+ if len(connections) > 20:
88
+ lines.append(f" ... and {len(connections) - 20} more")
89
+ except Exception as exc:
90
+ lines.append(f"\nNetwork Connections: unavailable ({exc})")
91
+
92
+ return "\n".join(lines)
93
+ except psutil.NoSuchProcess:
94
+ return f"Error: No process found with PID {pid}."
95
+ except psutil.AccessDenied:
96
+ return f"Error: Access denied reading process PID {pid}."
97
+ except Exception as exc:
98
+ return f"Error getting process details: {exc}"
99
+
100
+
101
+ @windows_server_mcp_tool()
102
+ def get_process_tree(pid: int = 0) -> str:
103
+ """
104
+ Show parent-child process relationships.
105
+ Args:
106
+ pid: Root PID to inspect. Use 0 to show top-level process trees.
107
+ """
108
+ try:
109
+ processes = {proc.pid: proc for proc in psutil.process_iter(["pid", "name", "ppid"])}
110
+ children: dict[int, list[psutil.Process]] = {}
111
+ for proc in processes.values():
112
+ try:
113
+ children.setdefault(proc.ppid(), []).append(proc)
114
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
115
+ continue
116
+
117
+ roots = [processes[pid]] if pid else [
118
+ proc for proc in processes.values() if proc.ppid() not in processes
119
+ ]
120
+
121
+ lines: list[str] = []
122
+
123
+ def walk(proc: psutil.Process, depth: int) -> None:
124
+ try:
125
+ indent = " " * depth
126
+ lines.append(f"{indent}- {proc.name()} (PID: {proc.pid})")
127
+ for child in sorted(children.get(proc.pid, []), key=lambda item: item.name().lower()):
128
+ walk(child, depth + 1)
129
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
130
+ return
131
+
132
+ for root in sorted(roots, key=lambda item: item.name().lower()):
133
+ walk(root, 0)
134
+
135
+ return "\n".join(lines[:500]) if lines else "No process tree data found."
136
+ except KeyError:
137
+ return f"Error: No process found with PID {pid}."
138
+ except Exception as exc:
139
+ return f"Error building process tree: {exc}"
140
+
141
+
142
+ _LOCK_SCAN_CODE = r"""
143
+ import os, sys, psutil
144
+ target = os.path.abspath(sys.argv[1]).lower()
145
+ for proc in psutil.process_iter(["pid", "name"]):
146
+ try:
147
+ for f in proc.open_files():
148
+ if os.path.abspath(f.path).lower() == target:
149
+ print(f"PID: {proc.pid} | Name: {proc.name()} | Path: {f.path}", flush=True)
150
+ break
151
+ except Exception:
152
+ continue
153
+ """
154
+
155
+
156
+ def _console_python() -> str:
157
+ """Return python.exe even when the server itself runs under pythonw.exe."""
158
+ exe = sys.executable
159
+ if os.path.basename(exe).lower() == "pythonw.exe":
160
+ candidate = os.path.join(os.path.dirname(exe), "python.exe")
161
+ if os.path.exists(candidate):
162
+ return candidate
163
+ return exe
164
+
165
+
166
+ @windows_server_mcp_tool()
167
+ def find_process_locking_file(file_path: str) -> str:
168
+ """
169
+ Identify processes that appear to have a file open.
170
+ Note: Windows may hide some handles without Administrator privileges.
171
+ The scan runs in a separate process with a 45s limit, because
172
+ psutil's open_files() can block forever on some Windows handles.
173
+ """
174
+ result = run([_console_python(), "-c", _LOCK_SCAN_CODE, file_path], timeout=45)
175
+ matches = [line for line in (result.stdout or "").splitlines() if line.startswith("PID:")]
176
+ if matches:
177
+ return "\n".join(matches)
178
+ if result.returncode == TIMEOUT_RETURNCODE:
179
+ return (
180
+ "The handle scan timed out (some process handle blocked it). "
181
+ f"No match found before the limit for '{file_path}'. "
182
+ "Try PowerShell/handle.exe or Resource Monitor as an alternative."
183
+ )
184
+ return f"No visible process handles found for '{file_path}'. Try running as Administrator for deeper handle visibility."