winterm-mcp 0.1.3__py3-none-any.whl → 0.1.5__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.
winterm_mcp/__init__.py CHANGED
@@ -2,5 +2,8 @@
2
2
  winterm-mcp - Windows Terminal MCP Service
3
3
  """
4
4
 
5
- __version__ = "0.1.1"
5
+ from .service import __version__, get_version, setup_logging
6
+
6
7
  __author__ = "winterm-mcp contributors"
8
+
9
+ __all__ = ["__version__", "get_version", "setup_logging"]
winterm_mcp/__main__.py CHANGED
@@ -3,15 +3,38 @@ winterm-mcp 主入口
3
3
  """
4
4
 
5
5
  from .server import app, init_service
6
- from .service import RunCmdService
6
+ from .service import RunCmdService, setup_logging, __version__
7
+ import logging
8
+ import os
9
+ import tempfile
7
10
 
8
11
 
9
12
  def main():
10
13
  """
11
14
  主函数,启动 MCP 服务器
12
15
  """
16
+ # 初始化日志
17
+ setup_logging(logging.INFO)
18
+
19
+ logger = logging.getLogger("winterm-mcp")
20
+
21
+ # 获取日志文件路径并记录
22
+ log_file = os.environ.get("WINTERM_LOG_FILE") or os.path.join(
23
+ tempfile.gettempdir(), "winterm-mcp.log"
24
+ )
25
+
26
+ logger.info("=" * 60)
27
+ logger.info(f"winterm-mcp v{__version__} starting...")
28
+ logger.info(f"Log file: {log_file}")
29
+ logger.info(f"Temp dir: {tempfile.gettempdir()}")
30
+ logger.info(f"Working dir: {os.getcwd()}")
31
+ logger.info(f"WINTERM_POWERSHELL_PATH: {os.environ.get('WINTERM_POWERSHELL_PATH', '(not set)')}")
32
+ logger.info("=" * 60)
33
+
13
34
  service = RunCmdService()
14
35
  init_service(service)
36
+
37
+ logger.info("Service initialized, starting MCP server...")
15
38
  app.run()
16
39
 
17
40
 
winterm_mcp/server.py CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Annotated, Optional, Dict, Any
4
4
  from mcp.server.fastmcp import FastMCP
5
- from .service import RunCmdService
5
+ from .service import RunCmdService, get_version, setup_logging, __version__
6
6
  from pydantic import Field
7
7
 
8
8
  CommandStr = Annotated[
@@ -54,8 +54,7 @@ def init_service(service: RunCmdService) -> None:
54
54
  def _svc() -> RunCmdService:
55
55
  if _service is None:
56
56
  raise RuntimeError(
57
- "Service not initialized. "
58
- "Call init_service() before running the server."
57
+ "Service not initialized. " "Call init_service() before running the server."
59
58
  )
60
59
  return _service
61
60
 
@@ -93,9 +92,7 @@ def run_command(
93
92
  包含token和状态信息的字典
94
93
  """
95
94
  try:
96
- token = _svc().run_command(
97
- command, shell_type, timeout, working_directory
98
- )
95
+ token = _svc().run_command(command, shell_type, timeout, working_directory)
99
96
  return {"token": token, "status": "pending", "message": "submitted"}
100
97
  except Exception as e:
101
98
  return {"error": str(e)}
@@ -127,3 +124,50 @@ def query_command_status(token: str) -> Dict[str, Any]:
127
124
  return result
128
125
  except Exception as e:
129
126
  return {"error": str(e)}
127
+
128
+
129
+ @app.tool(
130
+ name="get_version",
131
+ description="获取 winterm-mcp 服务的版本信息和运行状态。",
132
+ annotations={
133
+ "title": "版本信息",
134
+ "readOnlyHint": True,
135
+ "destructiveHint": False,
136
+ "idempotentHint": True,
137
+ "openWorldHint": False,
138
+ },
139
+ )
140
+ def get_version_tool() -> Dict[str, Any]:
141
+ """
142
+ 获取 winterm-mcp 版本信息
143
+
144
+ Returns:
145
+ 包含版本号和服务状态的字典
146
+ """
147
+ import os
148
+ import sys
149
+
150
+ try:
151
+ # 尝试获取 PowerShell 路径信息
152
+ ps_path = None
153
+ ps_error = None
154
+ try:
155
+ from .service import _find_powershell
156
+ ps_path = _find_powershell()
157
+ except FileNotFoundError as e:
158
+ ps_error = str(e)
159
+
160
+ return {
161
+ "version": get_version(),
162
+ "service_status": "running",
163
+ "python_version": sys.version,
164
+ "platform": sys.platform,
165
+ "powershell_path": ps_path,
166
+ "powershell_error": ps_error,
167
+ "env": {
168
+ "WINTERM_POWERSHELL_PATH": os.environ.get("WINTERM_POWERSHELL_PATH"),
169
+ "WINTERM_LOG_LEVEL": os.environ.get("WINTERM_LOG_LEVEL"),
170
+ }
171
+ }
172
+ except Exception as e:
173
+ return {"error": str(e), "version": __version__}
winterm_mcp/service.py CHANGED
@@ -6,8 +6,161 @@ import subprocess
6
6
  import threading
7
7
  import uuid
8
8
  import time
9
+ import os
10
+ import shutil
11
+ import logging
9
12
  from datetime import datetime
10
- from typing import Dict, Optional, Any
13
+ from typing import Dict, Optional, Any, List
14
+
15
+ # 版本号
16
+ __version__ = "0.1.5"
17
+
18
+ # 配置日志
19
+ logger = logging.getLogger("winterm-mcp")
20
+
21
+
22
+ def setup_logging(level: int = logging.INFO) -> None:
23
+ """
24
+ 配置日志输出
25
+
26
+ Args:
27
+ level: 日志级别,默认 INFO
28
+
29
+ 日志输出位置:
30
+ 1. 控制台 (stderr)
31
+ 2. 文件: %TEMP%/winterm-mcp.log 或 /tmp/winterm-mcp.log
32
+
33
+ 可通过环境变量配置:
34
+ - WINTERM_LOG_LEVEL: 日志级别 (DEBUG/INFO/WARNING/ERROR/CRITICAL)
35
+ - WINTERM_LOG_FILE: 自定义日志文件路径
36
+ """
37
+ import tempfile
38
+
39
+ formatter = logging.Formatter(
40
+ "[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s",
41
+ datefmt="%Y-%m-%d %H:%M:%S"
42
+ )
43
+
44
+ # 控制台输出
45
+ console_handler = logging.StreamHandler()
46
+ console_handler.setFormatter(formatter)
47
+ logger.addHandler(console_handler)
48
+
49
+ # 文件输出
50
+ log_file = os.environ.get("WINTERM_LOG_FILE")
51
+ if not log_file:
52
+ # 默认日志文件路径
53
+ log_file = os.path.join(tempfile.gettempdir(), "winterm-mcp.log")
54
+
55
+ try:
56
+ file_handler = logging.FileHandler(log_file, encoding="utf-8")
57
+ file_handler.setFormatter(formatter)
58
+ logger.addHandler(file_handler)
59
+ logger.info(f"Log file: {log_file}")
60
+ except Exception as e:
61
+ logger.warning(f"Failed to create log file {log_file}: {e}")
62
+
63
+ logger.setLevel(level)
64
+
65
+ # 检查环境变量设置日志级别
66
+ env_level = os.environ.get("WINTERM_LOG_LEVEL", "").upper()
67
+ if env_level in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"):
68
+ logger.setLevel(getattr(logging, env_level))
69
+
70
+
71
+ # PowerShell 可执行文件的标准路径(按优先级排序)
72
+ POWERSHELL_PATHS: List[str] = [
73
+ r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe",
74
+ r"C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe",
75
+ ]
76
+
77
+ # PowerShell Core (pwsh) 的常见路径
78
+ PWSH_PATHS: List[str] = [
79
+ r"C:\Program Files\PowerShell\7\pwsh.exe",
80
+ r"C:\Program Files (x86)\PowerShell\7\pwsh.exe",
81
+ ]
82
+
83
+ # 环境变量名称
84
+ ENV_POWERSHELL_PATH = "WINTERM_POWERSHELL_PATH"
85
+
86
+
87
+ def _find_powershell() -> str:
88
+ """
89
+ 查找可用的 PowerShell 可执行文件路径
90
+
91
+ 查找顺序:
92
+ 1. 环境变量 WINTERM_POWERSHELL_PATH(用户自定义)
93
+ 2. Windows PowerShell 标准路径
94
+ 3. PowerShell Core 标准路径
95
+ 4. PATH 环境变量中的 powershell/pwsh(兼容正常环境)
96
+
97
+ Returns:
98
+ PowerShell 可执行文件的绝对路径
99
+
100
+ Raises:
101
+ FileNotFoundError: 如果找不到 PowerShell
102
+ """
103
+ logger.debug("Starting PowerShell path discovery...")
104
+
105
+ # 1. 检查用户配置的环境变量
106
+ custom_path = os.environ.get(ENV_POWERSHELL_PATH)
107
+ if custom_path:
108
+ logger.debug(f"Found env var {ENV_POWERSHELL_PATH}={custom_path}")
109
+ if os.path.isfile(custom_path):
110
+ logger.info(f"Using custom PowerShell path: {custom_path}")
111
+ return custom_path
112
+ else:
113
+ logger.warning(
114
+ f"Custom PowerShell path not found: {custom_path}, "
115
+ "falling back to standard paths"
116
+ )
117
+
118
+ # 2. 检查 Windows PowerShell 标准路径
119
+ for path in POWERSHELL_PATHS:
120
+ logger.debug(f"Checking standard path: {path}")
121
+ if os.path.isfile(path):
122
+ logger.info(f"Found Windows PowerShell: {path}")
123
+ return path
124
+
125
+ # 3. 检查 PowerShell Core 标准路径
126
+ for path in PWSH_PATHS:
127
+ logger.debug(f"Checking PowerShell Core path: {path}")
128
+ if os.path.isfile(path):
129
+ logger.info(f"Found PowerShell Core: {path}")
130
+ return path
131
+
132
+ # 4. 尝试 PATH 环境变量(兼容正常环境)
133
+ logger.debug("Checking PATH environment variable...")
134
+ ps_path = shutil.which("powershell")
135
+ if ps_path:
136
+ logger.info(f"Found PowerShell in PATH: {ps_path}")
137
+ return ps_path
138
+
139
+ pwsh_path = shutil.which("pwsh")
140
+ if pwsh_path:
141
+ logger.info(f"Found pwsh in PATH: {pwsh_path}")
142
+ return pwsh_path
143
+
144
+ # 所有方法都失败
145
+ checked_paths = POWERSHELL_PATHS + PWSH_PATHS
146
+ error_msg = (
147
+ f"PowerShell not found. "
148
+ f"Set {ENV_POWERSHELL_PATH} environment variable or "
149
+ f"ensure PowerShell is installed. "
150
+ f"Checked paths: {', '.join(checked_paths)}"
151
+ )
152
+ logger.error(error_msg)
153
+ raise FileNotFoundError(error_msg)
154
+
155
+
156
+ def get_version() -> str:
157
+ """
158
+ 获取 winterm-mcp 版本号
159
+
160
+ Returns:
161
+ 版本号字符串
162
+ """
163
+ return __version__
11
164
 
12
165
 
13
166
  class RunCmdService:
@@ -18,6 +171,24 @@ class RunCmdService:
18
171
  def __init__(self):
19
172
  self.commands: Dict[str, Dict[str, Any]] = {}
20
173
  self.lock = threading.Lock()
174
+ self._powershell_path: Optional[str] = None
175
+
176
+ def _get_powershell_path(self) -> str:
177
+ """
178
+ 获取 PowerShell 可执行文件路径(带缓存)
179
+
180
+ 首次调用时查找并缓存路径,后续调用直接返回缓存值。
181
+
182
+ Returns:
183
+ PowerShell 可执行文件的绝对路径
184
+
185
+ Raises:
186
+ FileNotFoundError: 如果找不到 PowerShell
187
+ """
188
+ if self._powershell_path is None:
189
+ self._powershell_path = _find_powershell()
190
+ logger.debug(f"PowerShell path cached: {self._powershell_path}")
191
+ return self._powershell_path
21
192
 
22
193
  def run_command(
23
194
  self,
@@ -39,6 +210,12 @@ class RunCmdService:
39
210
  命令执行的token
40
211
  """
41
212
  token = str(uuid.uuid4())
213
+
214
+ logger.info(
215
+ f"Submitting command: token={token}, shell={shell_type}, "
216
+ f"timeout={timeout}, cwd={working_directory}"
217
+ )
218
+ logger.debug(f"Command content: {command[:100]}{'...' if len(command) > 100 else ''}")
42
219
 
43
220
  cmd_info = {
44
221
  "token": token,
@@ -80,6 +257,7 @@ class RunCmdService:
80
257
  """
81
258
  try:
82
259
  start_time = time.time()
260
+ logger.debug(f"[{token}] Starting command execution...")
83
261
 
84
262
  with self.lock:
85
263
  if token in self.commands:
@@ -88,19 +266,25 @@ class RunCmdService:
88
266
  encoding = "gbk"
89
267
 
90
268
  if shell_type == "powershell":
91
- # 添加 -ExecutionPolicy Bypass 避免执行策略阻塞
92
- # 添加 -NoLogo 减少启动输出
269
+ # 使用绝对路径调用 PowerShell,避免 PATH 环境变量限制
270
+ ps_path = self._get_powershell_path()
271
+ logger.info(f"[{token}] Using PowerShell: {ps_path}")
93
272
  cmd_args = [
94
- "powershell",
273
+ ps_path,
95
274
  "-NoProfile",
96
275
  "-NoLogo",
97
276
  "-NonInteractive",
98
- "-ExecutionPolicy", "Bypass",
99
- "-Command", command
277
+ "-ExecutionPolicy",
278
+ "Bypass",
279
+ "-Command",
280
+ command,
100
281
  ]
101
282
  else:
283
+ logger.debug(f"[{token}] Using cmd.exe")
102
284
  cmd_args = ["cmd", "/c", command]
103
285
 
286
+ logger.debug(f"[{token}] Executing: {cmd_args}")
287
+
104
288
  result = subprocess.run(
105
289
  cmd_args,
106
290
  capture_output=True,
@@ -112,6 +296,13 @@ class RunCmdService:
112
296
  )
113
297
 
114
298
  execution_time = time.time() - start_time
299
+
300
+ logger.info(
301
+ f"[{token}] Command completed: exit_code={result.returncode}, "
302
+ f"time={execution_time:.3f}s"
303
+ )
304
+ logger.debug(f"[{token}] stdout: {result.stdout[:200] if result.stdout else '(empty)'}")
305
+ logger.debug(f"[{token}] stderr: {result.stderr[:200] if result.stderr else '(empty)'}")
115
306
 
116
307
  with self.lock:
117
308
  if token in self.commands:
@@ -125,8 +316,24 @@ class RunCmdService:
125
316
  }
126
317
  )
127
318
 
319
+ except FileNotFoundError as e:
320
+ execution_time = time.time() - start_time
321
+ logger.error(f"[{token}] PowerShell not found: {e}")
322
+ with self.lock:
323
+ if token in self.commands:
324
+ self.commands[token].update(
325
+ {
326
+ "status": "completed",
327
+ "stdout": "",
328
+ "stderr": f"PowerShell not found: {e}",
329
+ "exit_code": -2,
330
+ "execution_time": execution_time,
331
+ "timeout_occurred": False,
332
+ }
333
+ )
128
334
  except subprocess.TimeoutExpired:
129
335
  execution_time = time.time() - start_time
336
+ logger.warning(f"[{token}] Command timed out after {timeout}s")
130
337
  with self.lock:
131
338
  if token in self.commands:
132
339
  self.commands[token].update(
@@ -143,6 +350,7 @@ class RunCmdService:
143
350
  )
144
351
  except Exception as e:
145
352
  execution_time = time.time() - start_time
353
+ logger.error(f"[{token}] Command failed with exception: {e}")
146
354
  with self.lock:
147
355
  if token in self.commands:
148
356
  self.commands[token].update(
@@ -166,8 +374,11 @@ class RunCmdService:
166
374
  Returns:
167
375
  包含命令状态的字典
168
376
  """
377
+ logger.debug(f"Querying status for token: {token}")
378
+
169
379
  with self.lock:
170
380
  if token not in self.commands:
381
+ logger.warning(f"Token not found: {token}")
171
382
  return {
172
383
  "token": token,
173
384
  "status": "not_found",
@@ -175,6 +386,7 @@ class RunCmdService:
175
386
  }
176
387
 
177
388
  cmd_info = self.commands[token].copy()
389
+ logger.debug(f"[{token}] Status: {cmd_info['status']}")
178
390
 
179
391
  if cmd_info["status"] == "running":
180
392
  return {"token": cmd_info["token"], "status": "running"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: winterm-mcp
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: A Model Context Protocol (MCP) service for executing Windows terminal commands asynchronously
5
5
  Author-email: winterm-mcp contributors <maintainer@example.com>
6
6
  License: MIT
@@ -27,8 +27,8 @@ Dynamic: license-file
27
27
 
28
28
  # winterm-mcp
29
29
 
30
- **更新日期**: 2026-01-16
31
- **版本**: 0.1.1
30
+ **更新日期**: 2026-01-16
31
+ **版本**: 0.1.4
32
32
 
33
33
  Windows Terminal MCP Service - 专门支持 Windows 终端的异步命令执行工具。
34
34
 
@@ -58,8 +58,50 @@ pip install -e .
58
58
  ```json
59
59
  {
60
60
  "mcpServers": {
61
- "winterm-mcp": {
62
- "command": "winterm-mcp"
61
+ "winterm": {
62
+ "command": "uvx",
63
+ "args": [
64
+ "winterm-mcp"
65
+ ]
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ ### 环境配置
72
+
73
+ #### PowerShell 路径配置
74
+
75
+ 在某些受限环境(如沙箱环境)中,系统 PATH 可能不包含 PowerShell 路径。winterm-mcp 会自动探测以下位置:
76
+
77
+ 1. `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`
78
+ 2. `C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe`
79
+ 3. `C:\Program Files\PowerShell\7\pwsh.exe`(PowerShell Core)
80
+ 4. `C:\Program Files (x86)\PowerShell\7\pwsh.exe`
81
+
82
+ 如果 PowerShell 安装在非标准位置,可通过环境变量指定:
83
+
84
+ **方式一:系统环境变量**
85
+
86
+ ```bash
87
+ # Windows CMD
88
+ set WINTERM_POWERSHELL_PATH=D:\CustomPath\powershell.exe
89
+
90
+ # Windows PowerShell
91
+ $env:WINTERM_POWERSHELL_PATH = "D:\CustomPath\powershell.exe"
92
+ ```
93
+
94
+ **方式二:MCP 配置**
95
+
96
+ ```json
97
+ {
98
+ "mcpServers": {
99
+ "winterm": {
100
+ "command": "uvx",
101
+ "args": ["winterm-mcp"],
102
+ "env": {
103
+ "WINTERM_POWERSHELL_PATH": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
104
+ }
63
105
  }
64
106
  }
65
107
  }
@@ -0,0 +1,10 @@
1
+ winterm_mcp/__init__.py,sha256=SKtzW0lc0ixl5FUdEaJA6J7tryL-JYTvsPG8wUi5Dxk,222
2
+ winterm_mcp/__main__.py,sha256=5K6N5K-ooaAb1X17EmEQUid8zne-A457NAGCT33qGIg,1097
3
+ winterm_mcp/server.py,sha256=wKoCItWQBG4b2cODluWfk8ABjmXlDlFxVyK4tD7NBiY,4541
4
+ winterm_mcp/service.py,sha256=T7nlrKZDTqudScm-eEhDi72JRN82qxU3C9l_rOzMpOw,13213
5
+ winterm_mcp-0.1.5.dist-info/licenses/LICENSE,sha256=GPZ4VAbf_gxeFMSUHCmNQING30GK0kNduK8EJIO5-gc,1081
6
+ winterm_mcp-0.1.5.dist-info/METADATA,sha256=PCRDUAN9ZtgRcxl-9PkH44jDBxwP6IAiHKBBsHyCd5I,5106
7
+ winterm_mcp-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ winterm_mcp-0.1.5.dist-info/entry_points.txt,sha256=0OGioH1DKGxuxvSFD1OCRcbLoutfqdEPmJy_j2GhJTA,113
9
+ winterm_mcp-0.1.5.dist-info/top_level.txt,sha256=S7w96DR3MB-CMIuXLnrMOvs5ApqPF-2PsAcfVCphCHw,12
10
+ winterm_mcp-0.1.5.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- winterm_mcp/__init__.py,sha256=2Z5_ooE56FyBhidVPwkvcWPyl4ONfzi5ZRmmg2RvUm4,120
2
- winterm_mcp/__main__.py,sha256=aCm888vzPOYtBDLtV02zAKkjIhrFAd2xB-TZAl4qHCc,300
3
- winterm_mcp/server.py,sha256=JEZc6NJeOrMuZejV6aho1UINZEOTDWNor3YYEdzPWCI,3204
4
- winterm_mcp/service.py,sha256=x0Ve8VS_ZodSNaZbDkO-pkFGui_6v_ooam_3WezcKxc,5995
5
- winterm_mcp-0.1.3.dist-info/licenses/LICENSE,sha256=GPZ4VAbf_gxeFMSUHCmNQING30GK0kNduK8EJIO5-gc,1081
6
- winterm_mcp-0.1.3.dist-info/METADATA,sha256=43DUZ9PYyPZXGJ4Qo8_0CiH8YwBNLH80NN7jjHDdcKg,4052
7
- winterm_mcp-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- winterm_mcp-0.1.3.dist-info/entry_points.txt,sha256=0OGioH1DKGxuxvSFD1OCRcbLoutfqdEPmJy_j2GhJTA,113
9
- winterm_mcp-0.1.3.dist-info/top_level.txt,sha256=S7w96DR3MB-CMIuXLnrMOvs5ApqPF-2PsAcfVCphCHw,12
10
- winterm_mcp-0.1.3.dist-info/RECORD,,