Windows Telnet MCP
# Windows Telnet MCP(Telnet + SSH + 串口服务器 TCP)
一个只在 Windows 上运行的本地 MCP Server,用来启动和实时操作真正可见的 Windows Telnet、OpenSSH 或串口服务器原始 TCP 客户端窗口。
它不是把客户端放进匿名管道或 ConPTY 的无头封装:每个会话都由系统 `conhost.exe` 承载,用户能在桌面上看到窗口、手工输入,并与 MCP 交替接管同一个会话。MCP 通过 Windows Console API 读取屏幕缓冲区、写入键盘事件。三种协议共用控制台 worker、工具处理函数与进程回收实现。
## 环境要求
- Windows 10/11 或带桌面会话的 Windows Server
- Node.js 22 或更高版本
- Windows PowerShell 5.1 (`powershell.exe`) 或 PowerShell 7 (`pwsh.exe`)
- Windows Telnet Client 可选功能(使用 Telnet 时)
- Windows OpenSSH Client,路径为 `%SystemRoot%\System32\OpenSSH\ssh.exe`(使用 SSH 时,无需安装 sshd 服务端)
- Windows 自带 .NET Framework 4.x 及其 `csc.exe`(使用串口 TCP 时;无需 .NET SDK、Visual Studio 或第三方终端)
本机尚未安装 Telnet 时,在“管理员”终端中执行:
```powershell
dism /online /Enable-Feature /FeatureName:TelnetClient
```
然后安装依赖:
```powershell
cd D:\work\windows-telnet-mcp
npm install
```
## 配置 MCP 客户端
通用 stdio 配置:
```json
{
"mcpServers": {
"windows-telnet": {
"command": "C:\\Program Files\\nodejs\\node.exe",
"args": ["D:\\work\\windows-telnet-mcp\\src\\index.js"]
}
}
}
```
启动器优先使用 PowerShell 7;未安装时会自动回退到 Windows 自带的 PowerShell 5.1。也可用环境变量显式指定:
```json
{
"env": {
"TELNET_MCP_PWSH": "C:\\Program Files\\PowerShell\\7\\pwsh.exe"
}
}
```
VS Code 可把同一项放到 `.vscode/mcp.json` 的 `servers` 下,并补上 `"type": "stdio"`。
## 提供的工具
| 工具 | 作用 |
| --- | --- |
| `telnet_check` | 检查 Windows、Telnet、conhost 和 PowerShell 依赖 |
| `telnet_start` | 打开可见 Telnet 窗口;可直接连接主机,也可停在 `Microsoft Telnet>` |
| `telnet_list` | 列出当前 MCP 进程创建的会话 |
| `telnet_status` | 查询进程和窗口状态 |
| `telnet_read` | 读取用户当前可见区域或最近的屏幕缓冲区 |
| `telnet_send` | 以控制台键盘事件输入文本,可选择追加 Enter |
| `telnet_key` | 输入 Enter、方向键、`CTRL+]`、`CTRL+C` 等按键 |
| `telnet_wait_for_text` | 等待提示符或其他文本出现 |
| `telnet_focus` | 恢复并前置真实 Telnet 窗口 |
| `telnet_close` | 优雅退出;必要时可强制关闭 |
建议调用顺序:
1. `telnet_check`
2. `telnet_start`
3. `telnet_wait_for_text` 或 `telnet_read`
4. `telnet_send` / `telnet_key`
5. 重复读写
6. `telnet_close`
`telnet_start` 不传 `host` 时不会建立网络连接,适合先确认窗口和 Telnet 命令提示符。传入 `host` 后会发起真实的出站连接。
## SSH 用法
同一个 MCP 配置现在同时提供 `ssh_check`、`ssh_start`、`ssh_list`、`ssh_status`、`ssh_read`、`ssh_send`、`ssh_key`、`ssh_wait_for_text`、`ssh_focus` 和 `ssh_close`。原来的服务名称、配置路径、`TELNET_MCP_PWSH` 环境变量以及 `telnet_*` 参数保持兼容。两种协议分别维护会话列表,不允许用 `telnet_*` 操作 SSH 会话,反之亦然。
`ssh_start` 参数示例:
```json
{
"host": "192.0.2.10",
"port": 22,
"username": "admin",
"identityFile": "C:\\Users\\you\\.ssh\\id_ed25519",
"title": "MCP Windows SSH"
}
```
只有 `host` 必填,支持主机名、IP(IPv6 使用不带方括号的形式)或 SSH config 别名;用户名用单独的 `username` 字段。可选 `identityFile` 指向现有私钥,`configFile` 指向可信的 OpenSSH 配置;省略时沿用 OpenSSH 的常规用户/系统配置。主机密钥检查遵循 OpenSSH 和所选配置,不自动添加跳过校验的参数。首次连接如出现指纹确认,应先由用户核验。
`ssh_start` 成功只表示客户端窗口启动,`ssh_status.running` 也只表示进程存活;二者不代表已经登录。接着使用 `ssh_read` / `ssh_wait_for_text` 检查主机指纹、密码、密钥口令或远端提示符,再用 `ssh_send` / `ssh_key` 交互,也可以直接在可见窗口中操作。
优先使用密钥、ssh-agent 或在窗口中手动输入密码。通过 `ssh_send` 发送的内容不会在工具结果中回显,但 MCP 主机仍可能记录工具参数,不能视为不留痕的密码通道。
`ssh_close` 只接受 `sessionId`:关闭本地客户端并在必要时强制终止,不发送 Telnet 的 `quit` 或假定的远端 `exit`,因此在密码提示或远端应用中也不会误提交输入。这会断开连接,但不保证远端后台任务结束。若需要远端程序正常退出,应先主动执行该程序的退出操作。
## 串口服务器:原始 TCP 透传
适用于设备已配置为 **TCP Server / Raw TCP** 的串口服务器。Windows 作为 TCP 客户端连接它的 IP 和端口;不是在本机把 COM 口发布为服务器,也不创建虚拟 COM 口。波特率、数据位、校验位、停止位、流控及端口映射需要在串口服务器侧预先配置。
正式客户端是一个可见的 C#/.NET Framework 控制台程序,网络直接 P/Invoke 调用系统 `ws2_32.dll` 的 Winsock,不使用 `TcpClient`。这里“原生 Winsock”指网络接口,不代表整个程序是无 CLR 的 C++ 二进制。没有新增 npm 依赖;沿用项目已有 Node/MCP 运行环境,不安装 PuTTY、串口驱动或额外编译器。
首次使用或更新 C# 源码后,在项目目录构建一次(重建前关闭串口会话):
```powershell
npm run build:serial
```
构建产物位于 `.build/windows-serial-client.exe`,不提交到仓库。构建脚本兼容 PowerShell 5.1/7,使用系统 .NET Framework 编译器;没有下载操作或后台自动编译。缺少产物时,`serial_start` 会给出构建提示。
同一个 MCP 配置新增 `serial_check/start/list/status/read/send/key/wait_for_text/focus/close` 共 10 个工具;会话与 Telnet、SSH 隔离。`serial_start` 示例(替换为设备实际地址):
```json
{
"host": "192.0.2.20",
"port": 4001,
"encoding": "utf8",
"newline": "crlf",
"connectTimeoutMs": 10000,
"title": "Serial console"
}
```
`host`、`port` 必填;IPv6 不带方括号。编码支持 `utf8`(默认)、`gbk`、`ascii`、`latin1`,回车支持 `crlf`(默认)、`cr`、`lf`。`connectTimeoutMs` 限制 DNS/建立 TCP 连接,总计 100–120000 ms;它不是远端设备响应超时,也不是 `serial_start` 的等待时长。
`serial_start` 成功仅表示可见进程启动,`serial_status.running` 仅表示进程存活。先用 `serial_read` 或 `serial_wait_for_text` 检查 `[serial] CONNECTED`(仅表示 TCP 已建立),再检查设备提示符;需要唤醒设备时,由调用者明确发送 Enter。连接失败或超时、远端断开后客户端退出,会话自动移除;没有自动重连或重发命令。
`serial_send` / `serial_key` 仍操作真实控制台。输入即时编码发送,默认没有本地回显,远端回显原样显示;`appendEnter` 和 ENTER 按 `newline` 转换,方向/导航键发送 ANSI 序列,`CTRL+C`/`CTRL+]` 发送对应控制字节,不进入 Telnet 命令模式。编码器、解码器保留跨字符/分包状态。
这是文本终端接口,不是任意二进制/十六进制文件传输接口;不可表示的字符使用编码替代字符。无 Telnet 协商/IAC 转义,无 RFC 2217、TLS、Modbus RTU 帧处理或远程串口配置;`serial_key` 不提供 `CTRL+BREAK`,原始 TCP 没有通用的串口 BREAK、DTR/RTS 控制协议。需要这些能力时必须先确认设备协议,不能把控制指令猜作透传字节。
`serial_close` 只接受 `sessionId`,关闭本地窗口和连接,不发送 `quit`/`exit`。强制关闭不保证尚在缓冲区的字节送达;关键命令应先等待设备确认。MCP EOF、强杀、繁忙输入中的回收使用原有 Job Object 机制。
### 用 PowerShell TcpClient 调试
下面脚本仅作有界的一次性连通/收发调试,兼容 5.1/7;正式 MCP 不调用它。默认只观察数据,不主动发字节。示例显式发送 `help` 和回车,应换成设备支持且安全的命令:
```powershell
.\scripts\debug-serial-tcp.ps1 -ServerHost 192.0.2.20 -Port 4001 -Text 'help' -AppendEnter -TimeoutMs 3000
```
连接和观察阶段各有 `TimeoutMs` 上限,观察期间持续收取 TCP 分片,不把一次 `DataAvailable=false` 当作响应结束;退出始终关闭连接。可选 `-Encoding gbk -Newline cr`。请先关闭占用端口的其他会话再调试,部分串口服务器只允许单客户端连接。原始 TCP 是明文,只应连接授权、可信网络内的设备。
## 统一连接管理
原有 30 个协议工具保持兼容,新增 7 个 `connection_*` 工具。**连接配置**保存设备入口,重启后保留;**运行会话**对应当前可见窗口,只属于创建它的 MCP 进程。保存或删除配置不操作设备,不关闭已打开的窗口。
| 工具 | 参数与用途 |
| --- | --- |
| `connection_list` | 可选 `query`、`alias`、`group`、`tag`、`protocol`;搜索配置,返回候选 ID,不按重名或重复别名自动选择 |
| `connection_get` | `connectionId`;读取完整配置和当前 `revision` |
| `connection_save` | `profile`;新建时不带 ID,更新时同时传 `connectionId` 和 `expectedRevision` |
| `connection_delete` | `connectionId`、`expectedRevision`;仅删除配置 |
| `connection_open` | `connectionId`、可选 `mode: "reuse"`(默认)或 `"new"` |
| `connection_sessions` | 可选 `connectionId`;统一列出当前实例的三种协议会话,包括旧工具直接打开的会话 |
| `connection_close` | `sessionId`、可选 `force`;自动分发到正确协议,不删除配置 |
### 保存、打开和编辑
调用 `connection_save`,例如保存 SSH 入口:
```json
{
"profile": {
"name": "机房核心交换机 SSH",
"aliases": ["核心交换机", "Core-SW"],
"group": "机房 A",
"tags": ["核心", "网络设备"],
"notes": "管理口入口,不在备注中填写密码",
"protocol": "ssh",
"options": {
"host": "192.0.2.10",
"port": 22,
"username": "admin"
}
}
}
```
返回的 `connection` 包含 `connectionId`、`revision`、时间和完整 `profile`。把该 ID 传给 `connection_open`,再使用返回的 `sessionId` 和 `tools.read/send/key/...` 操作窗口。初次 SSH 指纹仍需核验;管理层不自动填密码或发送初始化命令,客户端仍沿用原有认证机制(例如 OpenSSH 密钥认证)。
每条连接都可保存多个别名 `aliases` 和多个标签 `tags`,均为字符串数组,默认 `[]`;各最多 32 项,每项去掉首尾空格后为 1–120 字符,不允许控制字符。旧配置没有 `aliases` 时按空数组读取,不会因此改写文件或增加版本。
`connection_list` 搜索示例:
```json
{ "alias": "core-sw" }
```
```json
{ "tag": "核心" }
```
```json
{ "alias": "核心交换机", "tag": "网络设备", "protocol": "ssh" }
```
`alias` 和 `tag` 分别按完整别名、完整标签匹配,不区分大小写;组合条件要求同时满足。`query` 是包含匹配,也会搜索别名和标签,例如 `{ "query": "core" }` 可匹配别名 `Core-SW`。多个连接可以使用相同别名或标签,搜索返回所有候选,确定后仍按唯一 `connectionId` 打开,避免误连。
`protocol` 取 `ssh`、`telnet` 或 `serial`;`options` 复用相应 `*_start` 参数。保存的 Telnet 配置必须指定有效主机,直接调用 `telnet_start` 仍可不传主机;串口仍是 TCP Server / Raw TCP 模式,不是本地 COM,波特率等在设备侧配置。一条配置对应一种协议,同设备的不同入口可放在同一分组。
更新采用**完整替换**:先 `connection_get`,保留需要的 `profile` 字段,修改后连同 ID 和读取到的 `revision`(作为 `expectedRevision`)提交。省略的可选字段恢复默认值,不是局部合并。版本冲突时重新读取、核对变更,再提交;不自动强制覆盖。删除也必须提供当前版本。
不提供密码、私钥内容或启动命令字段,未知字段会被拒绝;可以保存私钥/SSH 配置的文件路径。备注等自由文本不是密码保险箱,切勿手工填入秘密。配置文件和它引用的 SSH 配置应视为可信本地配置;不要直接使用来源不明的文件。
### 会话行为与边界
- 默认 `reuse` 返回该配置在**当前实例**中最早找到的可用会话,不重新连接、不自动切换焦点;可用返回的 `tools.focus` 前置窗口。需要另开一个窗口时显式用 `mode: "new"`。
- 同一配置的并发打开按顺序执行,复用请求会等待正在启动的窗口。不同配置可独立启动;不同 ID 即使指向同一地址也不自动合并。
- 已开的会话保留启动时的配置快照。修改名称、地址甚至协议后,默认复用仍返回原窗口和原协议,并以 `configurationChanged` 提醒;用 `new` 才采用新配置,不暗中替换旧连接。
- `connection_sessions` 返回启动版本和当前版本;配置被删除后,原窗口仍可操作、关闭,标记 `configurationDeleted`。不关联配置的直接会话,其 `connectionId` 为 `null`。
- `readiness: "unconfirmed"` 表示未确认远端连接/登录就绪;配置列表不是在线探测结果。仍应读屏确认,不把可见窗口或存活进程当成登录成功。
- 统一关闭沿用协议语义:Telnet 默认尝试 CTRL+]/quit,`force: true` 强制关闭;SSH/串口只关闭本地客户端,不向设备注入 quit/exit。
- 多个 MCP 实例共享配置文件,但不共享运行窗口,不能凭别的实例的会话 ID 接管或关闭它。串口服务器可能只允许单连接,需要自行避免跨实例占用。
- MCP 退出回收自己的全部会话;再次启动只读取配置,不恢复旧 PID、不自动重连。配置损坏时统一会话列表仍尽力返回运行会话并附 `configurationError`,关闭入口不依赖配置文件。
- 对同一会话的重复关闭只执行一次;强制关闭请求可在优雅关闭未成功时升级。关闭进行中拒绝新读写,复用请求等待关闭结果,不把即将退出的窗口当成有效会话。
### 配置文件和写入保护
默认文件:`%LOCALAPPDATA%\windows-telnet-mcp\connections.json`。可以在 MCP 启动环境中设置 `TELNET_MCP_CONNECTIONS_FILE` 为其他绝对路径;环境配置改变后需要重启 MCP。只读列表不会创建文件,第一次保存时才创建目录与文件。不增加数据库或 npm 依赖,建议文件留在当前用户的本地私有目录,不要提交到 Git。
文件格式为 `schemaVersion: 1` 和 `connections` 数组,最多 1000 条、10 MiB。保存时使用独占 `.lock` 文件串行化跨实例更新,在锁内重新读取并检查版本,然后同步写入同目录临时文件并替换目标文件;不先删除旧文件。坏 JSON、未知格式版本、重复 ID、校验错误均拒绝覆盖原文件。
读取接受 UTF-8(含 BOM),严格拒绝损坏的 UTF-8 编码;即使读取期间文件增长,也不会突破 10 MiB 的读取上限。协议启动参数在创建 worker 前校验,主机不能是命令行选项,用户名、路径及窗口标题不允许 NUL 或换行;IPv6 会话目标以 `[地址]:端口` 显示。
普通退出等待正在进行的保存完成;若写入时进程被强杀,可能留下 `.lock` 或 `.tmp` 文件。下次会提示写锁超时,**不会依据锁文件年龄或 PID 自动删锁**。恢复步骤:停止使用该配置路径的所有实例,备份并检查配置文件,确认没有写入者后再手动移除对应 `.lock`,必要时处理同目录遗留 `.tmp`;重新启动后核对版本再操作。只对本地文件系统使用此方案,不把网络共享盘当作已验证的并发存储。
## 实现边界
- MCP Server 的 stdout 只用于 JSON-RPC;Telnet 不继承该管道。
- 每个会话有独立 worker,因此可以并行打开多个 Telnet 窗口。
- worker 在启动控制台前加入独立的 Windows Job Object;其唯一句柄关闭时,系统回收该会话的进程树。因此 worker 被强杀也不依赖 PowerShell 清理代码来回收 `telnet.exe` 和 `conhost.exe`。
- 客户端发现后先固定进程句柄并验证属于当前 Job,再允许附着或进入清理状态;不只凭 PID/进程名决定要操作的进程。
- MCP 退出时同时停止已启动和正在启动的 worker,并禁止新会话;worker 另外监听 MCP 父进程退出,覆盖父进程突然终止、工作线程忙碌等情况。
- Telnet 自行退出时,worker 自动释放控制台并结束,会话随后从 `telnet_list` 移除。
- 如果宿主的 Job Object 限制不允许建立上述生命周期保护,启动会失败,不会退回到可能遗留进程的启动方式。
- 读取的是字符屏幕缓冲区,不是 OCR,速度快且不会受窗口遮挡影响。
- 目前不抓取像素截图,也不支持鼠标事件;Windows Telnet 本身是字符终端,这两项不是必要能力。
- Telnet 协议通常是明文的。不要通过不可信网络发送密码或敏感数据;有条件时应使用 SSH。
## 验证
快速单元回归(不打开终端窗口):
```powershell
npm run test:unit
```
完整回归(会打开测试用可见窗口):
```powershell
npm run check
```
worker 控制通道最多允许 64 个未完成请求,单条回复上限为 16 Mi 字符。请求超时、管道故障、非法回复或超限回复会停止对应会话并回收其进程树,不让已排队的命令继续在失去反馈的情况下执行;输入可能已部分送达,因此不得自动重试。错误不回显非法协议片段中的屏幕/凭据内容。实际终端发送、真实超时回收、UTF-8 分片、重复关闭和复用竞态均有回归测试。
该命令先用系统编译器构建串口客户端。串口集成测试连接临时本机 TCP 服务(IPv4/IPv6),在两种 PowerShell 下验证可见性、UTF-8/GBK 分片解码、编码/换行字节、控制键、无 Telnet 协商、远端断开和 MCP EOF/强杀回收。无需串口硬件或额外服务。回环测试不等于已验证具体硬件的端口映射、串口参数、时序或设备协议。
集成测试会使用检测到的 PowerShell 7 和 Windows PowerShell 5.1,短暂打开真正可见的 `cmd.exe` 控制台,覆盖读写、EOF 清理、客户端自行退出、worker 强杀、32,768 字符输入中断,以及读屏字符边界。会话管理单元测试覆盖启动/关闭竞态、关闭幂等性和失败清理。
安装 Telnet 后,还会通过 MCP 工具测试真实 `telnet.exe`:连接测试临时创建的 `127.0.0.1` 随机端口,验证收发、优雅关闭、自行退出、长输入期间的 MCP EOF,以及 MCP 强杀后的回收。只连接本机,不需要账号或外部服务,也不需要安装 Telnet 服务端。未安装 Telnet 时,这组测试会明确标记为 skipped。
SSH 集成测试同样使用真实的 Windows `ssh.exe`,连接仅绑定 `127.0.0.1` 的临时 SSH 测试服务,验证密码交互、键盘输入、备用屏幕、密码提示下关闭、远端退出、MCP 崩溃回收以及 Telnet/SSH 混合会话退出。测试主机密钥在内存中生成,只在独立临时目录写测试配置和公开的 known_hosts,结束后删除;不改用户的 SSH 配置,不注册系统服务。`ssh2` 仅为测试开发依赖,实际 MCP 运行时依赖不变;未安装 OpenSSH Client 时 SSH 测试标记为 skipped。
## 文件结构
- `src/index.js`:MCP 工具、会话管理和 MCP stdio transport
- `src/session-options.js`:旧协议工具与连接配置共用的参数校验
- `src/connection-store.js`:连接配置持久化、搜索、跨实例写锁与版本检查
- `src/connection-manager.js`:协议分发、重复打开保护和统一会话入口
- `src/windows-console-worker.ps1`:Win32 Console API、可见进程启动及实时读写
- `src/windows-serial-client.cs`:可见串口服务器 Raw TCP 终端,直接使用原生 Winsock
- `scripts/build-serial.ps1`:使用 Windows 自带编译器构建串口客户端
- `scripts/debug-serial-tcp.ps1`:仅用于调试的 PowerShell TcpClient 收发脚本
- `test/mcp.test.js`:MCP 握手和真实 Telnet/SSH/串口 TCP 本机集成测试
- `test/serial-debug.test.js`:TcpClient 调试脚本在 PowerShell 5.1/7 下的 IPv4/IPv6 收发和断开测试
- `test/worker.test.js`:真实可见控制台读写和进程回收回归测试
- `test/session.test.js`:会话生命周期与竞态单元测试
- `test/worker-protocol.test.js`:控制通道分帧、非法回复、超时、排队限额及错误脱敏测试
- `test/connections.test.js`:持久化、并发更新、损坏保护、版本冲突和统一管理回归测试
- `test-support/connection-fixture.js`:独立临时配置目录,不读写真实用户连接配置
- `test-support/windows.js`:测试用 PowerShell 检测和进程退出检查
- `test-support/ssh-peer.js`:仅供测试的临时本机 SSH 服务
- `docs/design.md`:方案研究与取舍
TDQS
Scored across 10 tools
Each tool targets a distinct operation: status/check/list cover different aspects of session state and capability, while read/wait_for_text/send/key separate reading, polling, text injection, and named key injection. There is no meaningful overlap that would confuse an agent.
All tools share a consistent telnet_ prefix followed by a clear verb or verb phrase, such as start, close, send, read, list, and wait_for_text. The naming pattern is uniform and predictable across the entire set.
Ten tools is well-scoped for controlling a visible Windows Telnet client: session lifecycle, I/O, waiting, focusing, and diagnostics are all represented without unnecessary duplication. Each tool contributes a distinct capability.
The toolset covers the full interaction lifecycle: starting a session, connecting, sending input, reading output, waiting for expected text, sending special keys, listing sessions, checking readiness, focusing, and closing. No obvious gap prevents an agent from automating a Telnet workflow.