Windows 终端控制学习笔记(Agent + MCP)
一、本章概览
本模块演示如何让 AI Agent 通过 MCP 工具控制 Windows 终端:
| 文件 | 内容 |
|---|---|
mcp_server.py | MCP Server:封装 6 个 PowerShell 操作工具 |
test.py | MCP Client + Agent:加载工具后让 Agent 自主操作终端 |
二、架构设计
2.1 为什么用 MCP 而不是直接操作
Agent 本身没有操作操作系统的能力,它只能"思考"和"调用工具"。通过 MCP 协议,我们将终端操作封装为标准化工具,Agent 只需决定"调用哪个工具、传什么参数"。
这带来的好处:
| 优势 | 说明 |
|---|---|
| 安全隔离 | Agent 不直接接触系统调用,所有操作经过 MCP Server |
| 可验证 | 通过日志追踪 Agent 调用了哪些工具,证明是通过工具执行的 |
| 可扩展 | 新增终端能力只需在 MCP Server 注册新工具 |
| 跨平台 | 同一套 Agent 代码,换个 MCP Server 就能控制 Linux/macOS |
2.2 两种 PowerShell 操作方式
MCP Server 提供两种操作 PowerShell 的方式,适用于不同场景:
| 方式 | 技术 | 特点 | 适用场景 |
|---|---|---|---|
| 后台执行 | subprocess | 不可见,直接返回结果 | 执行命令获取输出 |
| GUI 操作 | pyautogui | 模拟键盘操作,可见窗口 | 需要可视化操作 |
三、MCP Server 实现
3.1 工具列表
MCP Server 共注册了 6 个工具:
| 工具名 | 功能 | 底层技术 |
|---|---|---|
get_powershell_processes | 获取所有 PowerShell 进程列表 | psutil |
close_powershell | 关闭所有 PowerShell 进程 | psutil.terminate() |
close_powershell_process | 关闭指定 PID 的进程 | psutil.Process(pid).terminate() |
open_powershell | 打开新的 PowerShell 窗口 | subprocess + Start-Process |
run_powershell_script | 向窗口发送命令(GUI 可见) | pyautogui.write() |
execute_powershell_command | 后台执行命令并返回结果 | subprocess.run() |
3.2 工具注册方式
使用 @mcp.tool() 装饰器注册 MCP 工具,参数描述通过 pydantic.Field 提供:
from mcp.server.fastmcp import FastMCP
from typing import Annotated
from pydantic import Field
mcp = FastMCP("windows-terminal-control")
@mcp.tool()
def open_powershell(
working_directory: Annotated[str, Field(description="可选的工作目录,为空则使用当前目录")] = ""
) -> str:
"""打开新的 PowerShell 窗口"""
# ...
@mcp.tool()
def execute_powershell_command(
command: Annotated[str, Field(description="要执行的 PowerShell 命令")]
) -> str:
"""直接执行 PowerShell 命令并返回结果(不通过 GUI)"""
# ...
3.3 核心实现代码
后台执行命令(subprocess)
import subprocess
def run_powershell_command(command: str, capture_output: bool = True):
"""执行 PowerShell 命令"""
cmd = ["powershell", "-Command", command]
result = subprocess.run(cmd, capture_output=True, text=True, shell=True)
return result.stdout.strip(), result.stderr.strip(), result.returncode
GUI 操作(pyautogui)
import pyautogui
def activate_powershell_window():
"""激活 PowerShell 窗口"""
windows = pyautogui.getWindowsWithTitle('Windows PowerShell')
if windows:
window = windows[0]
window.activate()
time.sleep(0.5)
return True
return False
# 向窗口发送命令
pyautogui.hotkey('ctrl', 'c') # 清空当前输入行
pyautogui.press('end') # 确保光标在末尾
pyautogui.write(script, interval=0.02) # 逐字符输入命令
pyautogui.press('enter') # 按 Enter 执行
进程管理(psutil)
import psutil
def get_powershell_processes():
"""获取所有 PowerShell 进程"""
processes = []
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
if proc.info['name'] and 'powershell' in proc.info['name'].lower():
processes.append({
'pid': proc.info['pid'],
'name': proc.info['name'],
})
return processes
3.4 启动 MCP Server
# stdio 模式(Agent 以子进程启动)
python -m app.terminal_control.mcp_server stdio
# Streamable HTTP 模式(远程访问)
python -m app.terminal_control.mcp_server streamable-http
四、MCP Client + Agent 测试
4.1 完整代码流程
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
async def run():
# 1. 创建 MCP 客户端,通过 stdio 连接 Server
client = MultiServerMCPClient({
"terminal": {
"command": sys.executable,
"args": [MCP_SERVER_PATH, "stdio"],
"transport": "stdio",
}
})
# 2. 加载 MCP 工具(自动转换为 LangChain Tool)
tools = await client.get_tools()
# tools = [get_powershell_processes, close_powershell, open_powershell, ...]
# 3. 创建 Agent(带内存记忆)
memory = MemorySaver()
agent = create_react_agent(
model=llm, tools=tools, checkpointer=memory,
)
# 4. 发送指令,Agent 自主决定调用哪个工具
result = await agent.ainvoke(
{"messages": [("user", "请打开一个新的 PowerShell 窗口")]},
config=RunnableConfig(configurable={"thread_id": "terminal-001"}),
)
4.2 Agent 决策流程
以 "请打开一个新的 PowerShell 窗口" 为例:
以 "执行 Get-Location 命令" 为例:
4.3 预定义测试任务
| 序号 | 任务 | Agent 调用的工具 |
|---|---|---|
| 1 | 打开新的 PowerShell 窗口 | open_powershell |
| 2 | 执行 Get-Location | execute_powershell_command |
| 3 | 执行 Get-Date | execute_powershell_command |
| 4 | 查看 PowerShell 进程 | get_powershell_processes |
| 清理 | 关闭第一个进程 | close_powershell_process |
五、工具调用日志追踪
5.1 为什么需要日志
Agent 操作终端时,用户看不到 Agent 到底做了什么。通过追踪 messages 中的 AIMessage.tool_calls 和 ToolMessage,可以验证 Agent 确实是通过 MCP 工具执行的。
5.2 日志提取原理
Agent 执行后的 result["messages"] 包含完整的消息链:
messages = [
HumanMessage(content="打开 PowerShell"), # 用户输入
AIMessage(tool_calls=[ # Agent 决定调用工具
{"name": "open_powershell", "args": {""}}
]),
ToolMessage(content="PowerShell 已打开"), # 工具返回结果
AIMessage(content="已为您打开了 PowerShell 窗口"), # Agent 最终回复
]
5.3 日志打印代码
from langchain_core.messages import AIMessage, ToolMessage
_printed_indices: set[int] = set()
def print_tool_call_logs(messages):
"""从 messages 中提取工具调用记录"""
print(" [MCP 工具调用日志]")
call_count = 0
for idx, msg in enumerate(messages):
if idx in _printed_indices:
continue
_printed_indices.add(idx)
if isinstance(msg, AIMessage) and msg.tool_calls:
for tc in msg.tool_calls:
call_count += 1
print(f" 调用[{call_count}]: {tc['name']}({tc['args']})")
elif isinstance(msg, ToolMessage):
content = str(msg.content)[:200] # 截断过长内容
print(f" 返回: {content}")
if call_count == 0:
print(" (本次未调用工具,Agent 直接回复)")
5.4 日志输出示例
--- 任务 1/4: 请打开一个新的 PowerShell 窗口 ---
[MCP 工具调用日志]
调用[1]: open_powershell({'working_directory': ''})
返回: PowerShell 已打开,当前运行进程数: 2
[Agent 回复] 已成功打开一个新的 PowerShell 窗口。
--- 任务 2/4: 在 PowerShell 中执行 Get-Location ---
[MCP 工具调用日志]
调用[1]: execute_powershell_command({'command': 'Get-Location'})
返回: 命令执行成功: D:\ap\my-agent-server
[Agent 回复] 当前目录是 D:\ap\my-agent-server。
六、依赖说明
| 依赖 | 作用 | 安装 |
|---|---|---|
pyautogui | GUI 自动化,模拟键盘操作 PowerShell 窗口 | pip install pyautogui |
psutil | 进程管理,获取/关闭 PowerShell 进程 | pip install psutil |
mcp | MCP Server 框架 | pip install mcp |
langchain-mcp-adapters | MCP → LangChain Tool 适配器 | pip install langchain-mcp-adapters |
langgraph | Agent 框架(create_react_agent) | pip install langgraph |
注意:
pyautogui是 GUI 自动化工具,操作时会实际控制鼠标和键盘。运行时请不要手动操作鼠标,避免冲突。可通过pyautogui.FAILSAFE = True启用安全模式(鼠标移到屏幕左上角时中断)。
七、run_powershell_script vs execute_powershell_command
这两个工具都能执行 PowerShell 命令,但实现方式完全不同:
| 维度 | execute_powershell_command | run_powershell_script |
|---|---|---|
| 技术 | subprocess.run 后台执行 | pyautogui GUI 模拟键盘 |
| 可见性 | 不可见,无窗口 | 可见,在 PowerShell 窗口中输入 |
| 返回值 | 直接返回 stdout | 只返回"命令已发送" |
| 获取结果 | 有完整输出 | 无法获取输出 |
| 适用场景 | 需要命令执行结果 | 需要可视化操作 |
Agent 通常会选择 execute_powershell_command,因为它能返回执行结果。
八、关键 API 速查
# ---- MCP Server ----
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("server-name")
@mcp.tool()
def my_tool(param: Annotated[str, Field(description="参数描述")]) -> str:
"""工具描述"""
return "结果"
mcp.run(transport="stdio")
# ---- MCP Client ----
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({
"name": {
"command": sys.executable,
"args": ["mcp_server.py", "stdio"],
"transport": "stdio",
}
})
tools = await client.get_tools()
# ---- Agent ----
from langgraph.prebuilt import create_react_agent
agent = create_react_agent(model=llm, tools=tools, checkpointer=MemorySaver())
result = await agent.ainvoke(
{"messages": [("user", "你的指令")]},
config={"configurable": {"thread_id": "001"}},
)
# ---- 提取工具调用日志 ----
from langchain_core.messages import AIMessage, ToolMessage
for msg in result["messages"]:
if isinstance(msg, AIMessage) and msg.tool_calls:
for tc in msg.tool_calls:
print(f"调用: {tc['name']}({tc['args']})")
elif isinstance(msg, ToolMessage):
print(f"返回: {msg.content}")

952

被折叠的 条评论
为什么被折叠?



