winterm-mcp 0.1.4__py3-none-any.whl → 0.1.6__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 +4 -1
- winterm_mcp/__main__.py +27 -1
- winterm_mcp/server.py +179 -126
- winterm_mcp/service.py +427 -295
- {winterm_mcp-0.1.4.dist-info → winterm_mcp-0.1.6.dist-info}/METADATA +2 -2
- winterm_mcp-0.1.6.dist-info/RECORD +10 -0
- winterm_mcp-0.1.4.dist-info/RECORD +0 -10
- {winterm_mcp-0.1.4.dist-info → winterm_mcp-0.1.6.dist-info}/WHEEL +0 -0
- {winterm_mcp-0.1.4.dist-info → winterm_mcp-0.1.6.dist-info}/entry_points.txt +0 -0
- {winterm_mcp-0.1.4.dist-info → winterm_mcp-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {winterm_mcp-0.1.4.dist-info → winterm_mcp-0.1.6.dist-info}/top_level.txt +0 -0
winterm_mcp/__init__.py
CHANGED
winterm_mcp/__main__.py
CHANGED
|
@@ -3,15 +3,41 @@ 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(
|
|
32
|
+
f"WINTERM_POWERSHELL_PATH: "
|
|
33
|
+
f"{os.environ.get('WINTERM_POWERSHELL_PATH', '(not set)')}"
|
|
34
|
+
)
|
|
35
|
+
logger.info("=" * 60)
|
|
36
|
+
|
|
13
37
|
service = RunCmdService()
|
|
14
38
|
init_service(service)
|
|
39
|
+
|
|
40
|
+
logger.info("Service initialized, starting MCP server...")
|
|
15
41
|
app.run()
|
|
16
42
|
|
|
17
43
|
|
winterm_mcp/server.py
CHANGED
|
@@ -1,126 +1,179 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Annotated, Optional, Dict, Any
|
|
4
|
-
from mcp.server.fastmcp import FastMCP
|
|
5
|
-
from .service import RunCmdService
|
|
6
|
-
from pydantic import Field
|
|
7
|
-
|
|
8
|
-
CommandStr = Annotated[
|
|
9
|
-
str,
|
|
10
|
-
Field(
|
|
11
|
-
description="要执行的命令字符串",
|
|
12
|
-
min_length=1,
|
|
13
|
-
max_length=1000,
|
|
14
|
-
),
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
ShellTypeStr = Annotated[
|
|
18
|
-
str,
|
|
19
|
-
Field(
|
|
20
|
-
description="Shell 类型 (powershell 或 cmd),默认 powershell",
|
|
21
|
-
pattern="^(powershell|cmd)$",
|
|
22
|
-
),
|
|
23
|
-
]
|
|
24
|
-
|
|
25
|
-
TimeoutInt = Annotated[
|
|
26
|
-
Optional[int],
|
|
27
|
-
Field(
|
|
28
|
-
description="超时秒数 (1-3600),默认 30 秒",
|
|
29
|
-
ge=1,
|
|
30
|
-
le=3600,
|
|
31
|
-
default=30,
|
|
32
|
-
),
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
WorkingDirectoryStr = Annotated[
|
|
36
|
-
Optional[str],
|
|
37
|
-
Field(
|
|
38
|
-
description="工作目录(可选,默认为当前目录)",
|
|
39
|
-
default=None,
|
|
40
|
-
max_length=1000,
|
|
41
|
-
),
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
app = FastMCP("winterm-mcp")
|
|
45
|
-
|
|
46
|
-
_service: Optional[RunCmdService] = None
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def init_service(service: RunCmdService) -> None:
|
|
50
|
-
global _service
|
|
51
|
-
_service = service
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _svc() -> RunCmdService:
|
|
55
|
-
if _service is None:
|
|
56
|
-
raise RuntimeError(
|
|
57
|
-
"Service not initialized. "
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Optional, Dict, Any
|
|
4
|
+
from mcp.server.fastmcp import FastMCP
|
|
5
|
+
from .service import RunCmdService, get_version, __version__
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
CommandStr = Annotated[
|
|
9
|
+
str,
|
|
10
|
+
Field(
|
|
11
|
+
description="要执行的命令字符串",
|
|
12
|
+
min_length=1,
|
|
13
|
+
max_length=1000,
|
|
14
|
+
),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
ShellTypeStr = Annotated[
|
|
18
|
+
str,
|
|
19
|
+
Field(
|
|
20
|
+
description="Shell 类型 (powershell 或 cmd),默认 powershell",
|
|
21
|
+
pattern="^(powershell|cmd)$",
|
|
22
|
+
),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
TimeoutInt = Annotated[
|
|
26
|
+
Optional[int],
|
|
27
|
+
Field(
|
|
28
|
+
description="超时秒数 (1-3600),默认 30 秒",
|
|
29
|
+
ge=1,
|
|
30
|
+
le=3600,
|
|
31
|
+
default=30,
|
|
32
|
+
),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
WorkingDirectoryStr = Annotated[
|
|
36
|
+
Optional[str],
|
|
37
|
+
Field(
|
|
38
|
+
description="工作目录(可选,默认为当前目录)",
|
|
39
|
+
default=None,
|
|
40
|
+
max_length=1000,
|
|
41
|
+
),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
app = FastMCP("winterm-mcp")
|
|
45
|
+
|
|
46
|
+
_service: Optional[RunCmdService] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def init_service(service: RunCmdService) -> None:
|
|
50
|
+
global _service
|
|
51
|
+
_service = service
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _svc() -> RunCmdService:
|
|
55
|
+
if _service is None:
|
|
56
|
+
raise RuntimeError(
|
|
57
|
+
"Service not initialized. "
|
|
58
|
+
"Call init_service() before running the server."
|
|
59
|
+
)
|
|
60
|
+
return _service
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.tool(
|
|
64
|
+
name="run_command",
|
|
65
|
+
description=(
|
|
66
|
+
"异步执行Windows终端命令,立即返回 token。"
|
|
67
|
+
"命令将在后台执行,可通过 query_command_status 查询结果。"
|
|
68
|
+
),
|
|
69
|
+
annotations={
|
|
70
|
+
"title": "异步命令执行器",
|
|
71
|
+
"readOnlyHint": False,
|
|
72
|
+
"destructiveHint": True,
|
|
73
|
+
"idempotentHint": False,
|
|
74
|
+
"openWorldHint": True,
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
def run_command(
|
|
78
|
+
command: CommandStr,
|
|
79
|
+
shell_type: ShellTypeStr = "powershell",
|
|
80
|
+
timeout: TimeoutInt = 30,
|
|
81
|
+
working_directory: WorkingDirectoryStr = None,
|
|
82
|
+
) -> Dict[str, Any]:
|
|
83
|
+
"""
|
|
84
|
+
异步执行Windows终端命令
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
command: 要执行的命令
|
|
88
|
+
shell_type: Shell 类型 (powershell 或 cmd),默认 powershell
|
|
89
|
+
timeout: 超时秒数 (1-3600),默认 30 秒
|
|
90
|
+
working_directory: 工作目录(可选,默认为当前目录)
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
包含token和状态信息的字典
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
token = _svc().run_command(
|
|
97
|
+
command, shell_type, timeout, working_directory
|
|
98
|
+
)
|
|
99
|
+
return {"token": token, "status": "pending", "message": "submitted"}
|
|
100
|
+
except Exception as e:
|
|
101
|
+
return {"error": str(e)}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@app.tool(
|
|
105
|
+
name="query_command_status",
|
|
106
|
+
description=("查询命令执行状态和结果。" "返回命令的当前状态、退出码、输出等信息。"),
|
|
107
|
+
annotations={
|
|
108
|
+
"title": "命令状态查询器",
|
|
109
|
+
"readOnlyHint": True,
|
|
110
|
+
"destructiveHint": False,
|
|
111
|
+
"idempotentHint": True,
|
|
112
|
+
"openWorldHint": False,
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
def query_command_status(token: str) -> Dict[str, Any]:
|
|
116
|
+
"""
|
|
117
|
+
查询命令执行状态和结果
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
token: 任务 token (GUID 字符串)
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
包含命令状态和结果的字典
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
result = _svc().query_command_status(token)
|
|
127
|
+
return result
|
|
128
|
+
except Exception as e:
|
|
129
|
+
return {"error": str(e)}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@app.tool(
|
|
133
|
+
name="get_version",
|
|
134
|
+
description="获取 winterm-mcp 服务的版本信息和运行状态。",
|
|
135
|
+
annotations={
|
|
136
|
+
"title": "版本信息",
|
|
137
|
+
"readOnlyHint": True,
|
|
138
|
+
"destructiveHint": False,
|
|
139
|
+
"idempotentHint": True,
|
|
140
|
+
"openWorldHint": False,
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
def get_version_tool() -> Dict[str, Any]:
|
|
144
|
+
"""
|
|
145
|
+
获取 winterm-mcp 版本信息
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
包含版本号和服务状态的字典
|
|
149
|
+
"""
|
|
150
|
+
import os
|
|
151
|
+
import sys
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
# 尝试获取 PowerShell 路径信息
|
|
155
|
+
ps_path = None
|
|
156
|
+
ps_error = None
|
|
157
|
+
try:
|
|
158
|
+
from .service import _find_powershell
|
|
159
|
+
ps_path = _find_powershell()
|
|
160
|
+
except FileNotFoundError as e:
|
|
161
|
+
ps_error = str(e)
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
"version": get_version(),
|
|
165
|
+
"service_status": "running",
|
|
166
|
+
"python_version": sys.version,
|
|
167
|
+
"platform": sys.platform,
|
|
168
|
+
"powershell_path": ps_path,
|
|
169
|
+
"powershell_error": ps_error,
|
|
170
|
+
"env": {
|
|
171
|
+
"WINTERM_POWERSHELL_PATH": os.environ.get(
|
|
172
|
+
"WINTERM_POWERSHELL_PATH"
|
|
173
|
+
),
|
|
174
|
+
"WINTERM_PYTHON_PATH": os.environ.get("WINTERM_PYTHON_PATH"),
|
|
175
|
+
"WINTERM_LOG_LEVEL": os.environ.get("WINTERM_LOG_LEVEL"),
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
except Exception as e:
|
|
179
|
+
return {"error": str(e), "version": __version__}
|
winterm_mcp/service.py
CHANGED
|
@@ -1,295 +1,427 @@
|
|
|
1
|
-
"""
|
|
2
|
-
winterm服务模块 - 异步执行Windows终端命令服务
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import subprocess
|
|
6
|
-
import threading
|
|
7
|
-
import uuid
|
|
8
|
-
import time
|
|
9
|
-
import os
|
|
10
|
-
import shutil
|
|
11
|
-
|
|
12
|
-
from
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
""
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
"
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
1
|
+
"""
|
|
2
|
+
winterm服务模块 - 异步执行Windows终端命令服务
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import threading
|
|
7
|
+
import uuid
|
|
8
|
+
import time
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Dict, Optional, Any, List
|
|
14
|
+
|
|
15
|
+
# 版本号
|
|
16
|
+
__version__ = "0.1.6"
|
|
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
|
+
ENV_PYTHON_PATH = "WINTERM_PYTHON_PATH"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _find_powershell() -> str:
|
|
89
|
+
"""
|
|
90
|
+
查找可用的 PowerShell 可执行文件路径
|
|
91
|
+
|
|
92
|
+
查找顺序:
|
|
93
|
+
1. 环境变量 WINTERM_POWERSHELL_PATH(用户自定义)
|
|
94
|
+
2. Windows PowerShell 标准路径
|
|
95
|
+
3. PowerShell Core 标准路径
|
|
96
|
+
4. PATH 环境变量中的 powershell/pwsh(兼容正常环境)
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
PowerShell 可执行文件的绝对路径
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
FileNotFoundError: 如果找不到 PowerShell
|
|
103
|
+
"""
|
|
104
|
+
logger.debug("Starting PowerShell path discovery...")
|
|
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__
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class RunCmdService:
|
|
167
|
+
"""
|
|
168
|
+
异步命令执行服务类,管理所有异步命令的执行和状态
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __init__(self):
|
|
172
|
+
self.commands: Dict[str, Dict[str, Any]] = {}
|
|
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
|
|
192
|
+
|
|
193
|
+
def run_command(
|
|
194
|
+
self,
|
|
195
|
+
command: str,
|
|
196
|
+
shell_type: str = "powershell",
|
|
197
|
+
timeout: int = 30,
|
|
198
|
+
working_directory: Optional[str] = None,
|
|
199
|
+
) -> str:
|
|
200
|
+
"""
|
|
201
|
+
异步运行命令
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
command: 要执行的命令
|
|
205
|
+
shell_type: Shell 类型 (powershell 或 cmd)
|
|
206
|
+
timeout: 超时时间(秒)
|
|
207
|
+
working_directory: 工作目录
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
命令执行的token
|
|
211
|
+
"""
|
|
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(
|
|
219
|
+
f"Command content: {command[:100]}"
|
|
220
|
+
f"{'...' if len(command) > 100 else ''}"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
cmd_info = {
|
|
224
|
+
"token": token,
|
|
225
|
+
"command": command,
|
|
226
|
+
"shell_type": shell_type,
|
|
227
|
+
"status": "pending",
|
|
228
|
+
"start_time": datetime.now(),
|
|
229
|
+
"timeout": timeout,
|
|
230
|
+
"working_directory": working_directory,
|
|
231
|
+
"stdout": "",
|
|
232
|
+
"stderr": "",
|
|
233
|
+
"exit_code": None,
|
|
234
|
+
"execution_time": None,
|
|
235
|
+
"timeout_occurred": False,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
with self.lock:
|
|
239
|
+
self.commands[token] = cmd_info
|
|
240
|
+
|
|
241
|
+
thread = threading.Thread(
|
|
242
|
+
target=self._execute_command,
|
|
243
|
+
args=(token, command, shell_type, timeout, working_directory),
|
|
244
|
+
)
|
|
245
|
+
thread.daemon = True
|
|
246
|
+
thread.start()
|
|
247
|
+
|
|
248
|
+
return token
|
|
249
|
+
|
|
250
|
+
def _execute_command(
|
|
251
|
+
self,
|
|
252
|
+
token: str,
|
|
253
|
+
command: str,
|
|
254
|
+
shell_type: str,
|
|
255
|
+
timeout: int,
|
|
256
|
+
working_directory: Optional[str],
|
|
257
|
+
):
|
|
258
|
+
"""
|
|
259
|
+
在单独线程中执行命令
|
|
260
|
+
"""
|
|
261
|
+
try:
|
|
262
|
+
start_time = time.time()
|
|
263
|
+
logger.debug(f"[{token}] Starting command execution...")
|
|
264
|
+
|
|
265
|
+
with self.lock:
|
|
266
|
+
if token in self.commands:
|
|
267
|
+
self.commands[token]["status"] = "running"
|
|
268
|
+
|
|
269
|
+
encoding = "gbk"
|
|
270
|
+
|
|
271
|
+
if shell_type == "powershell":
|
|
272
|
+
# 使用绝对路径调用 PowerShell,避免 PATH 环境变量限制
|
|
273
|
+
ps_path = self._get_powershell_path()
|
|
274
|
+
logger.info(f"[{token}] Using PowerShell: {ps_path}")
|
|
275
|
+
cmd_args = [
|
|
276
|
+
ps_path,
|
|
277
|
+
"-NoProfile",
|
|
278
|
+
"-NoLogo",
|
|
279
|
+
"-NonInteractive",
|
|
280
|
+
"-ExecutionPolicy",
|
|
281
|
+
"Bypass",
|
|
282
|
+
"-Command",
|
|
283
|
+
command,
|
|
284
|
+
]
|
|
285
|
+
else:
|
|
286
|
+
logger.debug(f"[{token}] Using cmd.exe")
|
|
287
|
+
cmd_args = ["cmd", "/c", command]
|
|
288
|
+
|
|
289
|
+
logger.debug(f"[{token}] Executing: {cmd_args}")
|
|
290
|
+
|
|
291
|
+
# 处理 Python 路径环境变量
|
|
292
|
+
env = None
|
|
293
|
+
python_path = os.environ.get(ENV_PYTHON_PATH)
|
|
294
|
+
if python_path and os.path.isfile(python_path):
|
|
295
|
+
env = os.environ.copy()
|
|
296
|
+
python_dir = os.path.dirname(python_path)
|
|
297
|
+
env["PATH"] = (
|
|
298
|
+
f"{python_dir}{os.pathsep}{env.get('PATH', '')}"
|
|
299
|
+
)
|
|
300
|
+
logger.debug(
|
|
301
|
+
f"[{token}] Using custom Python path: {python_path}"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
result = subprocess.run(
|
|
305
|
+
cmd_args,
|
|
306
|
+
capture_output=True,
|
|
307
|
+
text=True,
|
|
308
|
+
timeout=timeout,
|
|
309
|
+
cwd=working_directory,
|
|
310
|
+
encoding=encoding,
|
|
311
|
+
stdin=subprocess.DEVNULL, # 防止等待输入导致挂起
|
|
312
|
+
env=env,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
execution_time = time.time() - start_time
|
|
316
|
+
|
|
317
|
+
logger.info(
|
|
318
|
+
f"[{token}] Command completed: exit_code={result.returncode}, "
|
|
319
|
+
f"time={execution_time:.3f}s"
|
|
320
|
+
)
|
|
321
|
+
logger.debug(
|
|
322
|
+
f"[{token}] stdout: "
|
|
323
|
+
f"{result.stdout[:200] if result.stdout else '(empty)'}"
|
|
324
|
+
)
|
|
325
|
+
logger.debug(
|
|
326
|
+
f"[{token}] stderr: "
|
|
327
|
+
f"{result.stderr[:200] if result.stderr else '(empty)'}"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
with self.lock:
|
|
331
|
+
if token in self.commands:
|
|
332
|
+
self.commands[token].update(
|
|
333
|
+
{
|
|
334
|
+
"status": "completed",
|
|
335
|
+
"stdout": result.stdout,
|
|
336
|
+
"stderr": result.stderr,
|
|
337
|
+
"exit_code": result.returncode,
|
|
338
|
+
"execution_time": execution_time,
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
except FileNotFoundError as e:
|
|
343
|
+
execution_time = time.time() - start_time
|
|
344
|
+
logger.error(f"[{token}] PowerShell not found: {e}")
|
|
345
|
+
with self.lock:
|
|
346
|
+
if token in self.commands:
|
|
347
|
+
self.commands[token].update(
|
|
348
|
+
{
|
|
349
|
+
"status": "completed",
|
|
350
|
+
"stdout": "",
|
|
351
|
+
"stderr": f"PowerShell not found: {e}",
|
|
352
|
+
"exit_code": -2,
|
|
353
|
+
"execution_time": execution_time,
|
|
354
|
+
"timeout_occurred": False,
|
|
355
|
+
}
|
|
356
|
+
)
|
|
357
|
+
except subprocess.TimeoutExpired:
|
|
358
|
+
execution_time = time.time() - start_time
|
|
359
|
+
logger.warning(f"[{token}] Command timed out after {timeout}s")
|
|
360
|
+
with self.lock:
|
|
361
|
+
if token in self.commands:
|
|
362
|
+
self.commands[token].update(
|
|
363
|
+
{
|
|
364
|
+
"status": "completed",
|
|
365
|
+
"stdout": "",
|
|
366
|
+
"stderr": (
|
|
367
|
+
f"Command timed out after {timeout} seconds"
|
|
368
|
+
),
|
|
369
|
+
"exit_code": -1,
|
|
370
|
+
"execution_time": execution_time,
|
|
371
|
+
"timeout_occurred": True,
|
|
372
|
+
}
|
|
373
|
+
)
|
|
374
|
+
except Exception as e:
|
|
375
|
+
execution_time = time.time() - start_time
|
|
376
|
+
logger.error(f"[{token}] Command failed with exception: {e}")
|
|
377
|
+
with self.lock:
|
|
378
|
+
if token in self.commands:
|
|
379
|
+
self.commands[token].update(
|
|
380
|
+
{
|
|
381
|
+
"status": "completed",
|
|
382
|
+
"stdout": "",
|
|
383
|
+
"stderr": str(e),
|
|
384
|
+
"exit_code": -1,
|
|
385
|
+
"execution_time": execution_time,
|
|
386
|
+
"timeout_occurred": False,
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
def query_command_status(self, token: str) -> Dict[str, Any]:
|
|
391
|
+
"""
|
|
392
|
+
查询命令执行状态
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
token: 命令的token
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
包含命令状态的字典
|
|
399
|
+
"""
|
|
400
|
+
logger.debug(f"Querying status for token: {token}")
|
|
401
|
+
|
|
402
|
+
with self.lock:
|
|
403
|
+
if token not in self.commands:
|
|
404
|
+
logger.warning(f"Token not found: {token}")
|
|
405
|
+
return {
|
|
406
|
+
"token": token,
|
|
407
|
+
"status": "not_found",
|
|
408
|
+
"message": "Token not found",
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
cmd_info = self.commands[token].copy()
|
|
412
|
+
logger.debug(f"[{token}] Status: {cmd_info['status']}")
|
|
413
|
+
|
|
414
|
+
if cmd_info["status"] == "running":
|
|
415
|
+
return {"token": cmd_info["token"], "status": "running"}
|
|
416
|
+
elif cmd_info["status"] in ["completed", "pending"]:
|
|
417
|
+
return {
|
|
418
|
+
"token": cmd_info["token"],
|
|
419
|
+
"status": cmd_info["status"],
|
|
420
|
+
"exit_code": cmd_info["exit_code"],
|
|
421
|
+
"stdout": cmd_info["stdout"],
|
|
422
|
+
"stderr": cmd_info["stderr"],
|
|
423
|
+
"execution_time": cmd_info["execution_time"],
|
|
424
|
+
"timeout_occurred": cmd_info["timeout_occurred"],
|
|
425
|
+
}
|
|
426
|
+
else:
|
|
427
|
+
return cmd_info
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: winterm-mcp
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
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
|
|
@@ -28,7 +28,7 @@ Dynamic: license-file
|
|
|
28
28
|
# winterm-mcp
|
|
29
29
|
|
|
30
30
|
**更新日期**: 2026-01-16
|
|
31
|
-
**版本**: 0.1.
|
|
31
|
+
**版本**: 0.1.6
|
|
32
32
|
|
|
33
33
|
Windows Terminal MCP Service - 专门支持 Windows 终端的异步命令执行工具。
|
|
34
34
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
winterm_mcp/__init__.py,sha256=SKtzW0lc0ixl5FUdEaJA6J7tryL-JYTvsPG8wUi5Dxk,222
|
|
2
|
+
winterm_mcp/__main__.py,sha256=u-6txqlMEl-1TP5yQq5LEvGM8ppY4cODmMiTPGlllcQ,1106
|
|
3
|
+
winterm_mcp/server.py,sha256=5DFC9bKBdePtf1GK6iCXC3QFBpQIjwpxNe_ov7Dlvhg,4839
|
|
4
|
+
winterm_mcp/service.py,sha256=GyE3cBgKheWRviB-y7RtTwZuTj1zA4vX3-nznQA4slc,14281
|
|
5
|
+
winterm_mcp-0.1.6.dist-info/licenses/LICENSE,sha256=GPZ4VAbf_gxeFMSUHCmNQING30GK0kNduK8EJIO5-gc,1081
|
|
6
|
+
winterm_mcp-0.1.6.dist-info/METADATA,sha256=6Hyewzr6asiHtQkSdtMSfVr21HCw7yCZUjOzHwsam88,5106
|
|
7
|
+
winterm_mcp-0.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
winterm_mcp-0.1.6.dist-info/entry_points.txt,sha256=0OGioH1DKGxuxvSFD1OCRcbLoutfqdEPmJy_j2GhJTA,113
|
|
9
|
+
winterm_mcp-0.1.6.dist-info/top_level.txt,sha256=S7w96DR3MB-CMIuXLnrMOvs5ApqPF-2PsAcfVCphCHw,12
|
|
10
|
+
winterm_mcp-0.1.6.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=t5YVVDd0PUTlAUJRW5zw2vhPqQ8nTtO9oQsfC_vG9Xc,3170
|
|
4
|
-
winterm_mcp/service.py,sha256=0I-ICxalugK1yElB02dEPztDQCBFniJHfsTIVBbas4U,9233
|
|
5
|
-
winterm_mcp-0.1.4.dist-info/licenses/LICENSE,sha256=GPZ4VAbf_gxeFMSUHCmNQING30GK0kNduK8EJIO5-gc,1081
|
|
6
|
-
winterm_mcp-0.1.4.dist-info/METADATA,sha256=9OsaFqDGHGTy41CndwKkOY77z03casq-Xsjs01xtqSA,5106
|
|
7
|
-
winterm_mcp-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
winterm_mcp-0.1.4.dist-info/entry_points.txt,sha256=0OGioH1DKGxuxvSFD1OCRcbLoutfqdEPmJy_j2GhJTA,113
|
|
9
|
-
winterm_mcp-0.1.4.dist-info/top_level.txt,sha256=S7w96DR3MB-CMIuXLnrMOvs5ApqPF-2PsAcfVCphCHw,12
|
|
10
|
-
winterm_mcp-0.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|