winterm-mcp 0.1.4__tar.gz → 0.1.5__tar.gz
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-0.1.4/src/winterm_mcp.egg-info → winterm_mcp-0.1.5}/PKG-INFO +1 -1
- {winterm_mcp-0.1.4 → winterm_mcp-0.1.5}/pyproject.toml +1 -1
- winterm_mcp-0.1.5/src/winterm_mcp/__init__.py +9 -0
- winterm_mcp-0.1.5/src/winterm_mcp/__main__.py +42 -0
- {winterm_mcp-0.1.4 → winterm_mcp-0.1.5}/src/winterm_mcp/server.py +48 -1
- {winterm_mcp-0.1.4 → winterm_mcp-0.1.5}/src/winterm_mcp/service.py +111 -2
- {winterm_mcp-0.1.4 → winterm_mcp-0.1.5/src/winterm_mcp.egg-info}/PKG-INFO +1 -1
- winterm_mcp-0.1.4/src/winterm_mcp/__init__.py +0 -6
- winterm_mcp-0.1.4/src/winterm_mcp/__main__.py +0 -19
- {winterm_mcp-0.1.4 → winterm_mcp-0.1.5}/LICENSE +0 -0
- {winterm_mcp-0.1.4 → winterm_mcp-0.1.5}/README.md +0 -0
- {winterm_mcp-0.1.4 → winterm_mcp-0.1.5}/setup.cfg +0 -0
- {winterm_mcp-0.1.4 → winterm_mcp-0.1.5}/src/winterm_mcp.egg-info/SOURCES.txt +0 -0
- {winterm_mcp-0.1.4 → winterm_mcp-0.1.5}/src/winterm_mcp.egg-info/dependency_links.txt +0 -0
- {winterm_mcp-0.1.4 → winterm_mcp-0.1.5}/src/winterm_mcp.egg-info/entry_points.txt +0 -0
- {winterm_mcp-0.1.4 → winterm_mcp-0.1.5}/src/winterm_mcp.egg-info/requires.txt +0 -0
- {winterm_mcp-0.1.4 → winterm_mcp-0.1.5}/src/winterm_mcp.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "winterm-mcp"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.5"
|
|
8
8
|
description = "A Model Context Protocol (MCP) service for executing Windows terminal commands asynchronously"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
winterm-mcp 主入口
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .server import app, init_service
|
|
6
|
+
from .service import RunCmdService, setup_logging, __version__
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import tempfile
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
"""
|
|
14
|
+
主函数,启动 MCP 服务器
|
|
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
|
+
|
|
34
|
+
service = RunCmdService()
|
|
35
|
+
init_service(service)
|
|
36
|
+
|
|
37
|
+
logger.info("Service initialized, starting MCP server...")
|
|
38
|
+
app.run()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
main()
|
|
@@ -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[
|
|
@@ -124,3 +124,50 @@ def query_command_status(token: str) -> Dict[str, Any]:
|
|
|
124
124
|
return result
|
|
125
125
|
except Exception as e:
|
|
126
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__}
|
|
@@ -8,9 +8,65 @@ import uuid
|
|
|
8
8
|
import time
|
|
9
9
|
import os
|
|
10
10
|
import shutil
|
|
11
|
+
import logging
|
|
11
12
|
from datetime import datetime
|
|
12
13
|
from typing import Dict, Optional, Any, List
|
|
13
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
|
+
|
|
14
70
|
|
|
15
71
|
# PowerShell 可执行文件的标准路径(按优先级排序)
|
|
16
72
|
POWERSHELL_PATHS: List[str] = [
|
|
@@ -44,40 +100,67 @@ def _find_powershell() -> str:
|
|
|
44
100
|
Raises:
|
|
45
101
|
FileNotFoundError: 如果找不到 PowerShell
|
|
46
102
|
"""
|
|
103
|
+
logger.debug("Starting PowerShell path discovery...")
|
|
104
|
+
|
|
47
105
|
# 1. 检查用户配置的环境变量
|
|
48
106
|
custom_path = os.environ.get(ENV_POWERSHELL_PATH)
|
|
49
107
|
if custom_path:
|
|
108
|
+
logger.debug(f"Found env var {ENV_POWERSHELL_PATH}={custom_path}")
|
|
50
109
|
if os.path.isfile(custom_path):
|
|
110
|
+
logger.info(f"Using custom PowerShell path: {custom_path}")
|
|
51
111
|
return custom_path
|
|
52
|
-
|
|
112
|
+
else:
|
|
113
|
+
logger.warning(
|
|
114
|
+
f"Custom PowerShell path not found: {custom_path}, "
|
|
115
|
+
"falling back to standard paths"
|
|
116
|
+
)
|
|
53
117
|
|
|
54
118
|
# 2. 检查 Windows PowerShell 标准路径
|
|
55
119
|
for path in POWERSHELL_PATHS:
|
|
120
|
+
logger.debug(f"Checking standard path: {path}")
|
|
56
121
|
if os.path.isfile(path):
|
|
122
|
+
logger.info(f"Found Windows PowerShell: {path}")
|
|
57
123
|
return path
|
|
58
124
|
|
|
59
125
|
# 3. 检查 PowerShell Core 标准路径
|
|
60
126
|
for path in PWSH_PATHS:
|
|
127
|
+
logger.debug(f"Checking PowerShell Core path: {path}")
|
|
61
128
|
if os.path.isfile(path):
|
|
129
|
+
logger.info(f"Found PowerShell Core: {path}")
|
|
62
130
|
return path
|
|
63
131
|
|
|
64
132
|
# 4. 尝试 PATH 环境变量(兼容正常环境)
|
|
133
|
+
logger.debug("Checking PATH environment variable...")
|
|
65
134
|
ps_path = shutil.which("powershell")
|
|
66
135
|
if ps_path:
|
|
136
|
+
logger.info(f"Found PowerShell in PATH: {ps_path}")
|
|
67
137
|
return ps_path
|
|
68
138
|
|
|
69
139
|
pwsh_path = shutil.which("pwsh")
|
|
70
140
|
if pwsh_path:
|
|
141
|
+
logger.info(f"Found pwsh in PATH: {pwsh_path}")
|
|
71
142
|
return pwsh_path
|
|
72
143
|
|
|
73
144
|
# 所有方法都失败
|
|
74
145
|
checked_paths = POWERSHELL_PATHS + PWSH_PATHS
|
|
75
|
-
|
|
146
|
+
error_msg = (
|
|
76
147
|
f"PowerShell not found. "
|
|
77
148
|
f"Set {ENV_POWERSHELL_PATH} environment variable or "
|
|
78
149
|
f"ensure PowerShell is installed. "
|
|
79
150
|
f"Checked paths: {', '.join(checked_paths)}"
|
|
80
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__
|
|
81
164
|
|
|
82
165
|
|
|
83
166
|
class RunCmdService:
|
|
@@ -104,6 +187,7 @@ class RunCmdService:
|
|
|
104
187
|
"""
|
|
105
188
|
if self._powershell_path is None:
|
|
106
189
|
self._powershell_path = _find_powershell()
|
|
190
|
+
logger.debug(f"PowerShell path cached: {self._powershell_path}")
|
|
107
191
|
return self._powershell_path
|
|
108
192
|
|
|
109
193
|
def run_command(
|
|
@@ -126,6 +210,12 @@ class RunCmdService:
|
|
|
126
210
|
命令执行的token
|
|
127
211
|
"""
|
|
128
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 ''}")
|
|
129
219
|
|
|
130
220
|
cmd_info = {
|
|
131
221
|
"token": token,
|
|
@@ -167,6 +257,7 @@ class RunCmdService:
|
|
|
167
257
|
"""
|
|
168
258
|
try:
|
|
169
259
|
start_time = time.time()
|
|
260
|
+
logger.debug(f"[{token}] Starting command execution...")
|
|
170
261
|
|
|
171
262
|
with self.lock:
|
|
172
263
|
if token in self.commands:
|
|
@@ -177,6 +268,7 @@ class RunCmdService:
|
|
|
177
268
|
if shell_type == "powershell":
|
|
178
269
|
# 使用绝对路径调用 PowerShell,避免 PATH 环境变量限制
|
|
179
270
|
ps_path = self._get_powershell_path()
|
|
271
|
+
logger.info(f"[{token}] Using PowerShell: {ps_path}")
|
|
180
272
|
cmd_args = [
|
|
181
273
|
ps_path,
|
|
182
274
|
"-NoProfile",
|
|
@@ -188,8 +280,11 @@ class RunCmdService:
|
|
|
188
280
|
command,
|
|
189
281
|
]
|
|
190
282
|
else:
|
|
283
|
+
logger.debug(f"[{token}] Using cmd.exe")
|
|
191
284
|
cmd_args = ["cmd", "/c", command]
|
|
192
285
|
|
|
286
|
+
logger.debug(f"[{token}] Executing: {cmd_args}")
|
|
287
|
+
|
|
193
288
|
result = subprocess.run(
|
|
194
289
|
cmd_args,
|
|
195
290
|
capture_output=True,
|
|
@@ -201,6 +296,13 @@ class RunCmdService:
|
|
|
201
296
|
)
|
|
202
297
|
|
|
203
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)'}")
|
|
204
306
|
|
|
205
307
|
with self.lock:
|
|
206
308
|
if token in self.commands:
|
|
@@ -216,6 +318,7 @@ class RunCmdService:
|
|
|
216
318
|
|
|
217
319
|
except FileNotFoundError as e:
|
|
218
320
|
execution_time = time.time() - start_time
|
|
321
|
+
logger.error(f"[{token}] PowerShell not found: {e}")
|
|
219
322
|
with self.lock:
|
|
220
323
|
if token in self.commands:
|
|
221
324
|
self.commands[token].update(
|
|
@@ -230,6 +333,7 @@ class RunCmdService:
|
|
|
230
333
|
)
|
|
231
334
|
except subprocess.TimeoutExpired:
|
|
232
335
|
execution_time = time.time() - start_time
|
|
336
|
+
logger.warning(f"[{token}] Command timed out after {timeout}s")
|
|
233
337
|
with self.lock:
|
|
234
338
|
if token in self.commands:
|
|
235
339
|
self.commands[token].update(
|
|
@@ -246,6 +350,7 @@ class RunCmdService:
|
|
|
246
350
|
)
|
|
247
351
|
except Exception as e:
|
|
248
352
|
execution_time = time.time() - start_time
|
|
353
|
+
logger.error(f"[{token}] Command failed with exception: {e}")
|
|
249
354
|
with self.lock:
|
|
250
355
|
if token in self.commands:
|
|
251
356
|
self.commands[token].update(
|
|
@@ -269,8 +374,11 @@ class RunCmdService:
|
|
|
269
374
|
Returns:
|
|
270
375
|
包含命令状态的字典
|
|
271
376
|
"""
|
|
377
|
+
logger.debug(f"Querying status for token: {token}")
|
|
378
|
+
|
|
272
379
|
with self.lock:
|
|
273
380
|
if token not in self.commands:
|
|
381
|
+
logger.warning(f"Token not found: {token}")
|
|
274
382
|
return {
|
|
275
383
|
"token": token,
|
|
276
384
|
"status": "not_found",
|
|
@@ -278,6 +386,7 @@ class RunCmdService:
|
|
|
278
386
|
}
|
|
279
387
|
|
|
280
388
|
cmd_info = self.commands[token].copy()
|
|
389
|
+
logger.debug(f"[{token}] Status: {cmd_info['status']}")
|
|
281
390
|
|
|
282
391
|
if cmd_info["status"] == "running":
|
|
283
392
|
return {"token": cmd_info["token"], "status": "running"}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
winterm-mcp 主入口
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from .server import app, init_service
|
|
6
|
-
from .service import RunCmdService
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def main():
|
|
10
|
-
"""
|
|
11
|
-
主函数,启动 MCP 服务器
|
|
12
|
-
"""
|
|
13
|
-
service = RunCmdService()
|
|
14
|
-
init_service(service)
|
|
15
|
-
app.run()
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if __name__ == "__main__":
|
|
19
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|