3 Commits
Author SHA1 Message Date
etwen 2ac260489f merge: AI MCP one-click setup (v0.2.1) 2026-06-05 08:58:20 +08:00
etwen 7ba3fadb87 feat: AI MCP one-click setup in Settings (v0.2.1)
Add Settings -> AI MCP tab with McpRegistrar to one-click register/remove the Serial MCP server into Claude Code (~/.claude.json) and Kiro (~/.kiro/settings/mcp.json); read-modify-write preserves other servers, atomic write-back. Each card shows a CLI verify command.

Add PublishSerialMcp MSBuild target so a single GUI publish auto-bundles ETTerms.SerialMcp into the \ETTerms.SerialMcp\ subfolder, aligning with McpRegistrar.ResolveServerExe(). Bump version to 0.2.1. Update ARCHITECTURE/CLAUDE/README/README.zh-TW and About changelog.
2026-06-05 08:58:02 +08:00
etwenandClaude Opus 4.8 10ec900386 docs: add v0.2.0 changelog & publish convention; drop "Version" label from title
- About: add v0.2.0 "Beta Version Release" changelog entry.
- MainForm: title bar now shows "ETTerms  vX.Y.Z" (drop the word "Version").
- ARCHITECTURE.md + CLAUDE.md: document the publish convention --
  output to src\ETTerms\Publish\ETTerms_v{Version}\, main exe renamed to
  "ETTerms v{Version}.exe", ETTerms.SerialMcp published into a subfolder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 21:39:38 +08:00
9 changed files with 480 additions and 49 deletions
+48 -3
View File
@@ -160,6 +160,7 @@ ETTerms/
└── Infrastructure/ └── Infrastructure/
├── AppLogger.cs # 日誌 (port 自 MyTeraTerm) ├── AppLogger.cs # 日誌 (port 自 MyTeraTerm)
├── AppSettings.cs # 使用者偏好 (JSON, %LocalAppData%\ETTerms\settings.json) ├── AppSettings.cs # 使用者偏好 (JSON, %LocalAppData%\ETTerms\settings.json)
├── McpRegistrar.cs # ✅ 一鍵把 Serial MCP 註冊/移除到 Claude Code / Kiro 設定檔(Settings → AI MCP
└── NativeTheme.cs # 深色標題列 (DWM) └── NativeTheme.cs # 深色標題列 (DWM)
└── ETTerms.SerialMcp/ # ✅ Serial MCP serverstdio)——不自己開 port,經 named pipe 橋接 GUI └── ETTerms.SerialMcp/ # ✅ Serial MCP serverstdio)——不自己開 port,經 named pipe 橋接 GUI
@@ -411,7 +412,11 @@ ETTerms GUI(單一行程,唯一開 COM3 的人)
- **可視性:** AI 的 TX 在 GUI 以 `[AI]` 標色,與使用者手打的輸入區分;RX 兩邊同源。 - **可視性:** AI 的 TX 在 GUI 以 `[AI]` 標色,與使用者手打的輸入區分;RX 兩邊同源。
- **安全:** 本機、無雲、不碰 credential;pipe 僅限本機行程,只搬 serial bytes。 - **安全:** 本機、無雲、不碰 credential;pipe 僅限本機行程,只搬 serial bytes。
### 註冊(Kiro CLI ### 註冊(兩種方式
**方式 A — 一鍵設定(推薦,v0.2.1):** GUI **Settings → AI MCP** 分頁,對 Claude Code / Kiro 各按 **Setup** 即可。`McpRegistrar` 以 read-modify-write 把 `etterms-serial` 寫進該 CLI 的使用者層級設定檔(Claude Code:`~/.claude.json`Kiro`~/.kiro/settings/mcp.json`),保留檔內其他既有 MCP server,原子寫回避免壞檔。註冊的執行檔路徑指向 `<ETTerms.exe>\ETTerms.SerialMcp\ETTerms.SerialMcp.exe`(與 publish 慣例對齊,必定存在);卡片同時顯示 CLI 驗證指令,按 **Remove** 可移除。
**方式 B — 手動 CLI**
```powershell ```powershell
kiro-cli mcp add --name serial --command dotnet ` kiro-cli mcp add --name serial --command dotnet `
@@ -480,13 +485,53 @@ cd F:\10_AI\ETTerms
dotnet build dotnet build
dotnet run --project src\ETTerms\ETTerms.csproj dotnet run --project src\ETTerms\ETTerms.csproj
# 5. 打包(後期 Phase # 5. 打包(見下方「Publish / 打包慣例」
dotnet publish src\ETTerms\ETTerms.csproj -c Release -r win-x64 --self-contained false dotnet publish src\ETTerms\ETTerms.csproj -c Release -r win-x64 --self-contained false `
-o src\ETTerms\Publish\ETTerms_v<Version>
# 後續可用 Inno Setup / MSIX 做安裝程式 # 後續可用 Inno Setup / MSIX 做安裝程式
``` ```
--- ---
## Publish / 打包慣例
> 每次發 Release 都依此規則產出,方便辨識版本與一併攜帶 MCP server。
**輸出位置(固定):** `src\ETTerms\Publish\ETTerms_v{Version}\`
- `{Version}` 取自 `src\ETTerms\ETTerms.csproj``<Version>`(例:`0.2.0`)。
- 資料夾命名:`ETTerms_v{Version}`(前綴 `v`,例:`ETTerms_v0.2.0`)。
- `Publish/` 已 gitignore`publish/`),**不入 git**。
**內容結構:**
```
ETTerms_v0.2.0\
├── ETTerms v0.2.0.exe # 主程式 apphost,改名為「ETTerms v{Version}.exe」
├── ETTerms.dll + 各相依 dll # SSH.NET / SQLite / SnmpSharpNet / System.IO.Ports …
└── ETTerms.SerialMcp\ # Serial MCP server,獨立發佈到子資料夾(相依 dll 與 GUI 隔離)
├── ETTerms.SerialMcp.exe
└── ETTerms.SerialMcp.dll + 相依
```
**規則:**
1. **GUI publish 會自動帶上 MCP**`ETTerms.csproj``PublishSerialMcp` target`AfterTargets="Publish"`),會把 `ETTerms.SerialMcp` 一併發佈到 `ETTerms_v{Version}\ETTerms.SerialMcp\` **子資料夾**(與 GUI 相依 dll 隔離)。因此**只要發佈 GUI 一個指令**即可,不必再單獨發 MCP。
- 對齊 `McpRegistrar.ResolveServerExe()`:它解析的 `<ETTerms.exe>\ETTerms.SerialMcp\ETTerms.SerialMcp.exe` 因此**必定存在**,AI MCP 一鍵設定寫進去的路徑才不會落空。
2. **主 exe 改名**`dotnet publish` 產生的 `ETTerms.exe` 重新命名為 **`ETTerms v{Version}.exe`**。
- 可安全改名:.NET apphost 內部記錄要載入的 `ETTerms.dll`,**不靠自身檔名**,改名後仍正常啟動。
3. **框架相依**`--self-contained false -r win-x64`(目標機需已裝 .NET 8 Desktop Runtime)。
**PowerShell 範例:**
```powershell
$ver = ([regex]::Match((Get-Content src\ETTerms\ETTerms.csproj -Raw), '<Version>([^<]+)</Version>')).Groups[1].Value
$root = "src\ETTerms\Publish\ETTerms_v$ver"
if (Test-Path $root) { Remove-Item $root -Recurse -Force }
# 發佈 GUIETTerms.SerialMcp 由 PublishSerialMcp target 自動發到 $root\ETTerms.SerialMcp\
dotnet publish src\ETTerms\ETTerms.csproj -c Release -r win-x64 --self-contained false -o $root
Rename-Item (Join-Path $root "ETTerms.exe") "ETTerms v$ver.exe"
```
---
## Development Phases ## Development Phases
> 依使用者要求「**前面先把 GUI 做出來,後面再慢慢補功能**」排序: > 依使用者要求「**前面先把 GUI 做出來,後面再慢慢補功能**」排序:
+12 -2
View File
@@ -10,6 +10,8 @@ ETTerms 是一個 **C# .NET 8 WinForms** 的原生 Windows 終端機工作台,
**進度:** Phase 15 ✅、Phase 6 ✅(TTL 引擎 + Group 同步,SSH 待驗收)、Phase 7 ✅(Settings/About)、Phase 8 ✅(PDU + Shell/ConPTY + SFTP + Settings 擴充)、Phase 9 ✅(Serial MCP server**GUI 持有 COM portMCP 經本機 named pipe 橋接**,AI 收發的資料即時以 `[AI]` 標色顯示在 GUI)。打包待指示。 **進度:** Phase 15 ✅、Phase 6 ✅(TTL 引擎 + Group 同步,SSH 待驗收)、Phase 7 ✅(Settings/About)、Phase 8 ✅(PDU + Shell/ConPTY + SFTP + Settings 擴充)、Phase 9 ✅(Serial MCP server**GUI 持有 COM portMCP 經本機 named pipe 橋接**,AI 收發的資料即時以 `[AI]` 標色顯示在 GUI)。打包待指示。
**v0.2.1** 新增 GUI **Settings → AI MCP** 分頁(`McpRegistrar`):對 Claude Code`~/.claude.json`)與 Kiro`~/.kiro/settings/mcp.json`**一鍵 Setup / Remove** 註冊 `etterms-serial` MCP serverread-modify-write 保留檔內其他設定、原子寫回;卡片附 CLI 驗證指令。`ETTerms.csproj``PublishSerialMcp` target`AfterTargets=Publish`),GUI publish 會自動把 `ETTerms.SerialMcp` 帶到 `\ETTerms.SerialMcp\` 子資料夾,與 `McpRegistrar.ResolveServerExe()` 解析路徑對齊。
**v0.2.0** Phase 9 完成 — `ETTerms.SerialMcp`stdio MCP server+ GUI `SerialBridgeServer`named pipe `\\.\pipe\etterms-serial`)上線,提供 `serial_list` / `serial_attach` / `serial_write` / `serial_read` / `serial_detach` 五個工具,AI 的 TX 在 GUI 以 `[AI]` 標色即時 echo;視窗 / 工作列 / About 改用 Choco 圖示,標題列顯示版本號。見 [docs/serial-mcp-guide.md](docs/serial-mcp-guide.md)。 **v0.2.0** Phase 9 完成 — `ETTerms.SerialMcp`stdio MCP server+ GUI `SerialBridgeServer`named pipe `\\.\pipe\etterms-serial`)上線,提供 `serial_list` / `serial_attach` / `serial_write` / `serial_read` / `serial_detach` 五個工具,AI 的 TX 在 GUI 以 `[AI]` 標色即時 echo;視窗 / 工作列 / About 改用 Choco 圖示,標題列顯示版本號。見 [docs/serial-mcp-guide.md](docs/serial-mcp-guide.md)。
**v0.1.2** 新增 `sprintf2`TeraTerm 相容 C printf 格式化);`wait` 改為命中關鍵字後須等裝置安靜(`SettleMs` 預設 300ms)才接受並取「最後一次」出現,排除輸出中途的指令回顯(避免腳本搶跑)。 **v0.1.2** 新增 `sprintf2`TeraTerm 相容 C printf 格式化);`wait` 改為命中關鍵字後須等裝置安靜(`SettleMs` 預設 300ms)才接受並取「最後一次」出現,排除輸出中途的指令回顯(避免腳本搶跑)。
@@ -38,16 +40,24 @@ dotnet run --project src\ETTerms\ETTerms.csproj
# 加套件 # 加套件
dotnet add src\ETTerms package SSH.NET dotnet add src\ETTerms package SSH.NET
# 打包 # 打包(見「Publish / 打包慣例」,輸出固定到 src\ETTerms\Publish\ETTerms_v{Version}\
dotnet publish src\ETTerms\ETTerms.csproj -c Release -r win-x64 --self-contained false # GUI publish 會「自動」把 ETTerms.SerialMcp 一併發到 \ETTerms.SerialMcp\ 子資料夾
# ETTerms.csproj 的 PublishSerialMcp targetAfterTargets=Publish),不必再單獨發 MCP。
$ver = ([regex]::Match((Get-Content src\ETTerms\ETTerms.csproj -Raw), '<Version>([^<]+)</Version>')).Groups[1].Value
$root = "src\ETTerms\Publish\ETTerms_v$ver"
dotnet publish src\ETTerms\ETTerms.csproj -c Release -r win-x64 --self-contained false -o $root
Rename-Item (Join-Path $root "ETTerms.exe") "ETTerms v$ver.exe"
# 註冊 Serial MCP server(給 AI agent 操作 serial # 註冊 Serial MCP server(給 AI agent 操作 serial
# 推薦:GUI Settings → AI MCP 分頁,對 Claude Code / Kiro 按 Setup 一鍵註冊(McpRegistrar)。
# 或手動 CLI
kiro-cli mcp add --name serial --command dotnet --args "run --project src\ETTerms.SerialMcp\ETTerms.SerialMcp.csproj" kiro-cli mcp add --name serial --command dotnet --args "run --project src\ETTerms.SerialMcp\ETTerms.SerialMcp.csproj"
``` ```
## 開發慣例 ## 開發慣例
- **命名:** PascalCase 類別 / 方法,`_camelCase` 私有欄位;檔名 = 類別名。 - **命名:** PascalCase 類別 / 方法,`_camelCase` 私有欄位;檔名 = 類別名。
- **Publish / 打包:** 固定輸出到 `src\ETTerms\Publish\ETTerms_v{Version}\``{Version}` 取自 csproj `<Version>`);主 exe 改名為 `ETTerms v{Version}.exe``ETTerms.SerialMcp` 一併發到其下 `ETTerms.SerialMcp\` 子資料夾。框架相依(`--self-contained false -r win-x64`)。詳見 [ARCHITECTURE.md](ARCHITECTURE.md#publish--打包慣例)。
- **分層:** UI`App/`)只認 `ISessionChannel` 抽象,不直接相依 SSH.NET / SerialPort。 - **分層:** UI`App/`)只認 `ISessionChannel` 抽象,不直接相依 SSH.NET / SerialPort。
- **執行緒:** channel I/O 在背景;所有 UI 更新一律 `Control.Invoke` 回 UI thread。 - **執行緒:** channel I/O 在背景;所有 UI 更新一律 `Control.Invoke` 回 UI thread。
- **commit** 走 Conventional Commits`feat:` / `fix:` / `refactor:` …)。 - **commit** 走 Conventional Commits`feat:` / `fix:` / `refactor:` …)。
+24 -12
View File
@@ -4,7 +4,7 @@
> A native Windows terminal workspace (C# .NET 8 WinForms) — **SSH**, **Serial Port**, and **local Shell (ConPTY)** in one window, with a **TTL scripting engine** ported from MyTeraTerm for automation, plus an optional **Serial MCP server** that lets AI agents (Kiro CLI / Claude CLI) drive the serial port directly. Standalone, no cloud, no login. > A native Windows terminal workspace (C# .NET 8 WinForms) — **SSH**, **Serial Port**, and **local Shell (ConPTY)** in one window, with a **TTL scripting engine** ported from MyTeraTerm for automation, plus an optional **Serial MCP server** that lets AI agents (Kiro CLI / Claude CLI) drive the serial port directly. Standalone, no cloud, no login.
![version](https://img.shields.io/badge/version-0.1.0-blue.svg) ![platform](https://img.shields.io/badge/platform-Windows-0078D6.svg?logo=windows&logoColor=white) ![.NET](https://img.shields.io/badge/.NET-8.0-512BD4.svg?logo=dotnet&logoColor=white) ![UI](https://img.shields.io/badge/UI-WinForms-5C2D91.svg) ![SSH](https://img.shields.io/badge/SSH-SSH.NET-success.svg) ![Serial](https://img.shields.io/badge/Serial-System.IO.Ports-success.svg) ![status](https://img.shields.io/badge/status-WIP-orange.svg) ![license](https://img.shields.io/badge/license-MIT-green.svg) ![version](https://img.shields.io/badge/version-0.2.1-blue.svg) ![platform](https://img.shields.io/badge/platform-Windows-0078D6.svg?logo=windows&logoColor=white) ![.NET](https://img.shields.io/badge/.NET-8.0-512BD4.svg?logo=dotnet&logoColor=white) ![UI](https://img.shields.io/badge/UI-WinForms-5C2D91.svg) ![SSH](https://img.shields.io/badge/SSH-SSH.NET-success.svg) ![Serial](https://img.shields.io/badge/Serial-System.IO.Ports-success.svg) ![status](https://img.shields.io/badge/status-beta-yellow.svg) ![license](https://img.shields.io/badge/license-MIT-green.svg)
--- ---
@@ -65,10 +65,11 @@
- Control PDU outlets over SNMP (`SnmpSharpNet`) to power-cycle devices during tests - Control PDU outlets over SNMP (`SnmpSharpNet`) to power-cycle devices during tests
- Script commands: `pduconnect` / `pductrl` - Script commands: `pduconnect` / `pductrl`
- 🤖 **AI / MCP integration (optional, planned)** - 🤖 **AI / MCP integration (optional)**
- The **GUI owns the COM port**; a standalone stdio **Serial MCP server** bridges to it over a local named pipe for **Kiro CLI / Claude CLI** - The **GUI owns the COM port**; a standalone stdio **Serial MCP server** bridges to it over a local named pipe for **Kiro CLI / Claude CLI**
- Tools: `serial_list` / `serial_attach` / `serial_write` / `serial_read` / `serial_detach` - Tools: `serial_list` / `serial_attach` / `serial_write` / `serial_read` / `serial_detach`
- AI's serial TX/RX shows live in the GUI tagged `[AI]` — open the port in the GUI first, then let the AI attach - AI's serial TX/RX shows live in the GUI tagged `[AI]` — open the port in the GUI first, then let the AI attach
- **One-click setup** in **Settings → AI MCP**: register the server into Claude Code / Kiro with a single button
- 📊 **Session RX logging** - 📊 **Session RX logging**
- `logopen` / `logwrite` / `logclose` write session output to file - `logopen` / `logwrite` / `logclose` write session output to file
@@ -271,7 +272,7 @@ wait 'login: '
## 🤖 AI / MCP Integration ## 🤖 AI / MCP Integration
> 🔜 **Planned (Phase 9).** Let AI agents (**Kiro CLI / Claude CLI**) send/receive on a serial port **while you watch it live in the ETTerms GUI**. > Let AI agents (**Kiro CLI / Claude CLI**) send/receive on a serial port **while you watch it live in the ETTerms GUI**.
**Key design: the GUI owns the COM port; the MCP server never opens it.** A COM port can be opened by only one process at a time, so rather than the MCP server grabbing it, the GUI is the sole owner and runs a local **named pipe server** (`SerialBridgeServer`). The standalone `ETTerms.SerialMcp` (launched by Kiro/Claude CLI) is a thin client: every `write` / `read` is forwarded over the pipe to the GUI, which does the real port I/O. AI's TX is echoed into the terminal tagged `[AI]`, so RX/TX flow through the GUI channel and what you see is exactly what the AI sees. **Key design: the GUI owns the COM port; the MCP server never opens it.** A COM port can be opened by only one process at a time, so rather than the MCP server grabbing it, the GUI is the sole owner and runs a local **named pipe server** (`SerialBridgeServer`). The standalone `ETTerms.SerialMcp` (launched by Kiro/Claude CLI) is a thin client: every `write` / `read` is forwarded over the pipe to the GUI, which does the real port I/O. AI's TX is echoed into the terminal tagged `[AI]`, so RX/TX flow through the GUI channel and what you see is exactly what the AI sees.
@@ -287,7 +288,11 @@ Full closed loop — all inside the ETTerms GUI:
| `serial_read` | waitFor?, timeoutMs? | Drain accumulated RX; optionally wait for a substring / timeout | | `serial_read` | waitFor?, timeoutMs? | Drain accumulated RX; optionally wait for a substring / timeout |
| `serial_detach` | — | Unbind (does **not** close the GUI's port) | | `serial_detach` | — | Unbind (does **not** close the GUI's port) |
Register in Kiro CLI: ### One-click setup (recommended)
In **Settings → AI MCP**, click **Setup** on the Claude Code or Kiro card to register the Serial MCP server into that CLI's user-level config (`~/.claude.json` / `~/.kiro/settings/mcp.json`) with a single button. Your other MCP servers are preserved, and each card shows the CLI command to verify the connection. **Remove** unregisters it.
### Manual setup
```powershell ```powershell
kiro-cli mcp add --name serial --command dotnet --args "run --project src\ETTerms.SerialMcp\ETTerms.SerialMcp.csproj" kiro-cli mcp add --name serial --command dotnet --args "run --project src\ETTerms.SerialMcp\ETTerms.SerialMcp.csproj"
@@ -377,8 +382,8 @@ ETTerms/
│ ├── Sessions/ # Connection abstraction: ISessionChannel / SshChannel / SerialChannel / ShellChannel │ ├── Sessions/ # Connection abstraction: ISessionChannel / SshChannel / SerialChannel / ShellChannel
│ ├── Connections/ # Connection data: Connection / ConnectionStore(SQLite) / CredentialVault │ ├── Connections/ # Connection data: Connection / ConnectionStore(SQLite) / CredentialVault
│ ├── Scripting/ # TTL engine: TTLInterpreter / ScriptRunner / GroupSyncContext / Pdu │ ├── Scripting/ # TTL engine: TTLInterpreter / ScriptRunner / GroupSyncContext / Pdu
│ └── Infrastructure/ # AppLogger / AppSettings / NativeTheme │ └── Infrastructure/ # AppLogger / AppSettings / McpRegistrar / NativeTheme
└── src/ETTerms.SerialMcp/ # 🔜 Serial MCP server (stdio) — lets AI agents drive the serial port └── src/ETTerms.SerialMcp/ # Serial MCP server (stdio) — lets AI agents drive the serial port
``` ```
**Core design:** every connection implements `ISessionChannel` (`Write(byte[])` + `event DataReceived`). `TerminalView` and `TTLInterpreter` only know this abstraction, so SSH / Serial / Shell look identical to upper layers — the key to "one script engine driving multiple connection types." See [ARCHITECTURE.md](ARCHITECTURE.md). **Core design:** every connection implements `ISessionChannel` (`Write(byte[])` + `event DataReceived`). `TerminalView` and `TTLInterpreter` only know this abstraction, so SSH / Serial / Shell look identical to upper layers — the key to "one script engine driving multiple connection types." See [ARCHITECTURE.md](ARCHITECTURE.md).
@@ -397,18 +402,25 @@ ETTerms/
## 📜 Version History ## 📜 Version History
### v0.1.0 (in development) ### v0.2.1
- **AI MCP one-click setup** — new **Settings → AI MCP** tab registers the Serial MCP server into Claude Code (`~/.claude.json`) or Kiro (`~/.kiro/settings/mcp.json`) with a single button; existing MCP servers are preserved, and each card shows the verify command
- Publish now auto-bundles `ETTerms.SerialMcp` into the app folder, so the registered path always resolves
### v0.2.0
- **Phase 9: Serial MCP server** (`ETTerms.SerialMcp`) — AI agents (Kiro CLI / Claude CLI) send/receive on the serial port while you watch it live in the GUI
- GUI is the sole COM-port owner; the MCP server bridges over a local named pipe (`SerialBridgeServer`) and never opens the port itself
- Tools: `serial_list` / `serial_attach` / `serial_write` / `serial_read` / `serial_detach`; AI's TX echoed into the terminal tagged `[AI]`
- New app icon (window / taskbar / executable); version shown in the title bar
### v0.1.0
- Phases 18 complete: window shell, connection sidebar, tiling workspace, Serial / SSH / local Shell (ConPTY) / SFTP - Phases 18 complete: window shell, connection sidebar, tiling workspace, Serial / SSH / local Shell (ConPTY) / SFTP
- Owner-drawn VT100 terminal rendering - Owner-drawn VT100 terminal rendering
- TTL scripting engine (ported from MyTeraTerm) + Group sync execution - TTL scripting engine (ported from MyTeraTerm) + Group sync execution
- PDU power control (SNMP) - PDU power control (SNMP)
- Settings / About - Settings / About
- Packaging TBD
**Planned**
- Phase 9: Serial MCP server (`ETTerms.SerialMcp`) — let AI agents (Kiro CLI / Claude CLI) drive the serial port directly
--- ---
+24 -12
View File
@@ -4,7 +4,7 @@
> 原生 Windows 終端機工作台(C# .NET 8 WinForms)—— 一個視窗整合 **SSH**、**Serial Port**、**本機 Shell (ConPTY)** 連線,內建從 MyTeraTerm 移植的 **TTL 腳本引擎**做自動化,並提供選用的 **Serial MCP server**,讓 AI agentKiro CLI / Claude CLI)直接操作 serial port。單機、無雲、無登入系統。 > 原生 Windows 終端機工作台(C# .NET 8 WinForms)—— 一個視窗整合 **SSH**、**Serial Port**、**本機 Shell (ConPTY)** 連線,內建從 MyTeraTerm 移植的 **TTL 腳本引擎**做自動化,並提供選用的 **Serial MCP server**,讓 AI agentKiro CLI / Claude CLI)直接操作 serial port。單機、無雲、無登入系統。
![version](https://img.shields.io/badge/version-0.1.0-blue.svg) ![platform](https://img.shields.io/badge/platform-Windows-0078D6.svg?logo=windows&logoColor=white) ![.NET](https://img.shields.io/badge/.NET-8.0-512BD4.svg?logo=dotnet&logoColor=white) ![UI](https://img.shields.io/badge/UI-WinForms-5C2D91.svg) ![SSH](https://img.shields.io/badge/SSH-SSH.NET-success.svg) ![Serial](https://img.shields.io/badge/Serial-System.IO.Ports-success.svg) ![status](https://img.shields.io/badge/status-WIP-orange.svg) ![license](https://img.shields.io/badge/license-MIT-green.svg) ![version](https://img.shields.io/badge/version-0.2.1-blue.svg) ![platform](https://img.shields.io/badge/platform-Windows-0078D6.svg?logo=windows&logoColor=white) ![.NET](https://img.shields.io/badge/.NET-8.0-512BD4.svg?logo=dotnet&logoColor=white) ![UI](https://img.shields.io/badge/UI-WinForms-5C2D91.svg) ![SSH](https://img.shields.io/badge/SSH-SSH.NET-success.svg) ![Serial](https://img.shields.io/badge/Serial-System.IO.Ports-success.svg) ![status](https://img.shields.io/badge/status-beta-yellow.svg) ![license](https://img.shields.io/badge/license-MIT-green.svg)
--- ---
@@ -65,10 +65,11 @@
- 透過 SNMP`SnmpSharpNet`)控制 PDU 插座,測試中遠端電源循環 - 透過 SNMP`SnmpSharpNet`)控制 PDU 插座,測試中遠端電源循環
- 腳本指令:`pduconnect` / `pductrl` - 腳本指令:`pduconnect` / `pductrl`
- 🤖 **AI / MCP 整合(選用,規劃中** - 🤖 **AI / MCP 整合(選用)**
- **COM port 由 GUI 持有**;獨立的 stdio **Serial MCP server** 經本機 named pipe 橋接過去,供 **Kiro CLI / Claude CLI** 使用 - **COM port 由 GUI 持有**;獨立的 stdio **Serial MCP server** 經本機 named pipe 橋接過去,供 **Kiro CLI / Claude CLI** 使用
- 工具:`serial_list` / `serial_attach` / `serial_write` / `serial_read` / `serial_detach` - 工具:`serial_list` / `serial_attach` / `serial_write` / `serial_read` / `serial_detach`
- AI 的 serial 收發會即時顯示在 GUI(標 `[AI]`)——先在 GUI 開好 port,再讓 AI attach - AI 的 serial 收發會即時顯示在 GUI(標 `[AI]`)——先在 GUI 開好 port,再讓 AI attach
- **一鍵設定**:在 **設定 → AI MCP** 一個按鈕就把 server 註冊進 Claude Code / Kiro
- 📊 **連線 RX 日誌** - 📊 **連線 RX 日誌**
- `logopen` / `logwrite` / `logclose` 將工作階段輸出寫檔 - `logopen` / `logwrite` / `logclose` 將工作階段輸出寫檔
@@ -271,7 +272,7 @@ wait 'login: '
## 🤖 AI / MCP 整合 ## 🤖 AI / MCP 整合
> 🔜 **規劃中(Phase 9)。** 讓 AI agent**Kiro CLI / Claude CLI**)收發 serial**而且你能在 ETTerms GUI 即時看到 AI 的每筆收發**。 > 讓 AI agent**Kiro CLI / Claude CLI**)收發 serial**而且你能在 ETTerms GUI 即時看到 AI 的每筆收發**。
**關鍵設計:COM port 由 GUI 唯一持有,MCP server 不自己開 port。** 一個 COM port 同時只能被一個行程開啟;因此不讓 MCP server 自己開,而是 **GUI 當 port 的唯一擁有者**,在 GUI 內跑一支本機 **named pipe server**`SerialBridgeServer`)。獨立的 `ETTerms.SerialMcp`(由 Kiro/Claude CLI 啟動)退化成瘦客戶端:所有 `write` / `read` 都經 pipe 轉發給 GUI,由 GUI 代為讀寫實體 port。AI 的 TX 會以 `[AI]` 標色 echo 進終端機,RX/TX 都流經 GUI channel**你看到的就是 AI 看到的**。 **關鍵設計:COM port 由 GUI 唯一持有,MCP server 不自己開 port。** 一個 COM port 同時只能被一個行程開啟;因此不讓 MCP server 自己開,而是 **GUI 當 port 的唯一擁有者**,在 GUI 內跑一支本機 **named pipe server**`SerialBridgeServer`)。獨立的 `ETTerms.SerialMcp`(由 Kiro/Claude CLI 啟動)退化成瘦客戶端:所有 `write` / `read` 都經 pipe 轉發給 GUI,由 GUI 代為讀寫實體 port。AI 的 TX 會以 `[AI]` 標色 echo 進終端機,RX/TX 都流經 GUI channel**你看到的就是 AI 看到的**。
@@ -287,7 +288,11 @@ wait 'login: '
| `serial_read` | waitFor?, timeoutMs? | 取出累積的 RX;可等待子字串或逾時 | | `serial_read` | waitFor?, timeoutMs? | 取出累積的 RX;可等待子字串或逾時 |
| `serial_detach` | — | 解除綁定(**不會**關閉 GUI 的 port | | `serial_detach` | — | 解除綁定(**不會**關閉 GUI 的 port |
在 Kiro CLI 註冊: ### 一鍵設定(推薦)
**設定 → AI MCP**,對 Claude Code 或 Kiro 卡片按 **Setup**,一個按鈕就把 Serial MCP server 寫進該 CLI 的使用者層級設定檔(`~/.claude.json` / `~/.kiro/settings/mcp.json`)。你既有的其他 MCP server 會被保留,卡片並顯示用來驗證連線的 CLI 指令。按 **Remove** 即可移除。
### 手動設定
```powershell ```powershell
kiro-cli mcp add --name serial --command dotnet --args "run --project src\ETTerms.SerialMcp\ETTerms.SerialMcp.csproj" kiro-cli mcp add --name serial --command dotnet --args "run --project src\ETTerms.SerialMcp\ETTerms.SerialMcp.csproj"
@@ -377,8 +382,8 @@ ETTerms/
│ ├── Sessions/ # 連線抽象:ISessionChannel / SshChannel / SerialChannel / ShellChannel │ ├── Sessions/ # 連線抽象:ISessionChannel / SshChannel / SerialChannel / ShellChannel
│ ├── Connections/ # 連線資料:Connection / ConnectionStore(SQLite) / CredentialVault │ ├── Connections/ # 連線資料:Connection / ConnectionStore(SQLite) / CredentialVault
│ ├── Scripting/ # TTL 引擎:TTLInterpreter / ScriptRunner / GroupSyncContext / Pdu │ ├── Scripting/ # TTL 引擎:TTLInterpreter / ScriptRunner / GroupSyncContext / Pdu
│ └── Infrastructure/ # AppLogger / AppSettings / NativeTheme │ └── Infrastructure/ # AppLogger / AppSettings / McpRegistrar / NativeTheme
└── src/ETTerms.SerialMcp/ # 🔜 Serial MCP serverstdio)—— 讓 AI agent 直接操作 serial port └── src/ETTerms.SerialMcp/ # Serial MCP serverstdio)—— 讓 AI agent 直接操作 serial port
``` ```
**核心設計:** 所有連線都實作 `ISessionChannel``Write(byte[])` + `event DataReceived`)。`TerminalView``TTLInterpreter` 只認得這個抽象,因此 SSH / Serial / Shell 對上層完全一致——這是「同一套腳本引擎驅動多種連線」的關鍵。詳見 [ARCHITECTURE.md](ARCHITECTURE.md)。 **核心設計:** 所有連線都實作 `ISessionChannel``Write(byte[])` + `event DataReceived`)。`TerminalView``TTLInterpreter` 只認得這個抽象,因此 SSH / Serial / Shell 對上層完全一致——這是「同一套腳本引擎驅動多種連線」的關鍵。詳見 [ARCHITECTURE.md](ARCHITECTURE.md)。
@@ -397,18 +402,25 @@ ETTerms/
## 📜 版本紀錄 ## 📜 版本紀錄
### v0.1.0(開發中) ### v0.2.1
- **AI MCP 一鍵設定** —— 新增 **設定 → AI MCP** 分頁,一個按鈕就把 Serial MCP server 註冊進 Claude Code`~/.claude.json`)或 Kiro`~/.kiro/settings/mcp.json`);保留檔內其他既有 MCP server,卡片並顯示驗證指令
- Publish 會自動把 `ETTerms.SerialMcp` 一併打包進 app 資料夾,註冊寫入的路徑必定存在
### v0.2.0
- **Phase 9Serial MCP server**`ETTerms.SerialMcp`)—— AI agentKiro CLI / Claude CLI)可在 serial port 收發,而你在 GUI 即時看著
- COM port 由 GUI 唯一持有;MCP server 經本機 named pipe`SerialBridgeServer`)橋接,自己不開 port
- 工具:`serial_list` / `serial_attach` / `serial_write` / `serial_read` / `serial_detach`AI 的 TX 在終端機以 `[AI]` 標色 echo
- 新增 app 圖示(視窗 / 工作列 / 執行檔);標題列顯示版本號
### v0.1.0
- Phase 1–8 完成:視窗外殼、連線側欄、平鋪工作區、Serial / SSH / 本機 Shell (ConPTY) / SFTP - Phase 1–8 完成:視窗外殼、連線側欄、平鋪工作區、Serial / SSH / 本機 Shell (ConPTY) / SFTP
- 自繪 VT100 終端機渲染 - 自繪 VT100 終端機渲染
- TTL 腳本引擎(移植自 MyTeraTerm+ Group 同步執行 - TTL 腳本引擎(移植自 MyTeraTerm+ Group 同步執行
- PDU 電源控制(SNMP - PDU 電源控制(SNMP
- Settings / About - Settings / About
- 打包待規劃
**規劃中**
- Phase 9Serial MCP server`ETTerms.SerialMcp`)—— 讓 AI agentKiro CLI / Claude CLI)直接操作 serial port
--- ---
+10
View File
@@ -177,6 +177,16 @@ public sealed class AboutView : UserControl
private static readonly ChangelogEntry[] Changelog = private static readonly ChangelogEntry[] Changelog =
[ [
new("0.2.1", new DateOnly(2026, 6, 4), "AI MCP one-click setup",
[
"New Settings → AI MCP tab: register the Serial MCP server into Claude Code or Kiro with one click.",
"Lets AI agents drive the serial port for you.",
]),
new("0.2.0", new DateOnly(2026, 6, 4), "Beta Version Release",
[
"AI agents can now send/receive on the serial port while you watch it live in the GUI.",
"New app icon and version shown in the title bar.",
]),
new("0.1.0", new DateOnly(2026, 6, 3), "Initial Release", new("0.1.0", new DateOnly(2026, 6, 3), "Initial Release",
[ [
"Beta Version: expect bugs and missing features. Feedback welcome!", "Beta Version: expect bugs and missing features. Feedback welcome!",
+1 -1
View File
@@ -39,7 +39,7 @@ public partial class MainForm : Form
var v = Assembly.GetExecutingAssembly().GetName().Version; var v = Assembly.GetExecutingAssembly().GetName().Version;
var versionStr = v != null ? $"{v.Major}.{v.Minor}.{v.Build}" : "0.1.0"; var versionStr = v != null ? $"{v.Major}.{v.Minor}.{v.Build}" : "0.1.0";
Text = $"ETTerms Version v{versionStr}"; Text = $"ETTerms v{versionStr}";
} }
private void BuildLayout() private void BuildLayout()
+174 -18
View File
@@ -5,7 +5,7 @@ using ETTerms.Scripting.Pdu;
namespace ETTerms.App; namespace ETTerms.App;
/// <summary>Settings page with tabs: Terminal / PDU.</summary> /// <summary>Settings page with tabs: Terminal / PDU / AI MCP.</summary>
public sealed class SettingsView : UserControl public sealed class SettingsView : UserControl
{ {
public SettingsView() public SettingsView()
@@ -20,19 +20,17 @@ public sealed class SettingsView : UserControl
Padding = new Padding(4, 4, 4, 0), WrapContents = false Padding = new Padding(4, 4, 4, 0), WrapContents = false
}; };
var terminalPage = BuildTerminalTab();
var pduPage = BuildPduTab();
terminalPage.Dock = DockStyle.Fill;
pduPage.Dock = DockStyle.Fill;
pduPage.Visible = false;
var body = new Panel { Dock = DockStyle.Fill, BackColor = Theme.WorkspaceBack }; var body = new Panel { Dock = DockStyle.Fill, BackColor = Theme.WorkspaceBack };
body.Controls.Add(terminalPage); var pages = new List<Panel>();
body.Controls.Add(pduPage);
Button? activeBtn = null; Button? activeBtn = null;
Button MakeTab(string text, Panel page) Button MakeTab(string text, Panel page)
{ {
page.Dock = DockStyle.Fill;
page.Visible = false;
body.Controls.Add(page);
pages.Add(page);
var b = new Button var b = new Button
{ {
Text = text, AutoSize = false, Width = 80, Height = 26, FlatStyle = FlatStyle.Flat, Text = text, AutoSize = false, Width = 80, Height = 26, FlatStyle = FlatStyle.Flat,
@@ -43,8 +41,7 @@ public sealed class SettingsView : UserControl
b.FlatAppearance.MouseOverBackColor = Theme.Hover; b.FlatAppearance.MouseOverBackColor = Theme.Hover;
b.Click += (_, _) => b.Click += (_, _) =>
{ {
terminalPage.Visible = page == terminalPage; foreach (var p in pages) p.Visible = p == page;
pduPage.Visible = page == pduPage;
if (activeBtn != null) activeBtn.BackColor = Theme.TabBack; if (activeBtn != null) activeBtn.BackColor = Theme.TabBack;
b.BackColor = Theme.TabActiveBack; b.BackColor = Theme.TabActiveBack;
activeBtn = b; activeBtn = b;
@@ -52,17 +49,16 @@ public sealed class SettingsView : UserControl
return b; return b;
} }
var termBtn = MakeTab("Terminal", terminalPage); var termBtn = MakeTab("Terminal", BuildTerminalTab());
var pduBtn = MakeTab("PDU", pduPage);
tabBar.Controls.Add(termBtn); tabBar.Controls.Add(termBtn);
tabBar.Controls.Add(pduBtn); tabBar.Controls.Add(MakeTab("PDU", BuildPduTab()));
tabBar.Controls.Add(MakeTab("AI MCP", BuildAiMcpTab()));
// Set initial active
termBtn.BackColor = Theme.TabActiveBack;
activeBtn = termBtn;
Controls.Add(body); Controls.Add(body);
Controls.Add(tabBar); Controls.Add(tabBar);
// Set initial active tab
termBtn.PerformClick();
} }
// ═══ Terminal Tab ═══ // ═══ Terminal Tab ═══
@@ -275,6 +271,166 @@ public sealed class SettingsView : UserControl
} }
} }
// ═══ AI MCP Tab ═══
private Panel BuildAiMcpTab()
{
var page = new Panel { BackColor = Theme.WorkspaceBack, Padding = new Padding(20) };
var flow = new FlowLayoutPanel
{
Dock = DockStyle.Fill, FlowDirection = FlowDirection.TopDown,
WrapContents = false, BackColor = Theme.WorkspaceBack, AutoScroll = true
};
flow.Controls.Add(new Label
{
Text = "AI MCP Integration", AutoSize = true,
ForeColor = Theme.Accent, Font = Theme.UiFontBold, Margin = new Padding(0, 0, 0, 4)
});
flow.Controls.Add(new Label
{
Text = "One-click register the ETTerms Serial MCP server into your AI CLI's user-level\n" +
"config. ETTerms keeps sole ownership of the COM port; the AI drives serial through\n" +
"a local named pipe. Open a Serial session in ETTerms first, then the AI can attach.",
AutoSize = false, Width = 600, Height = 56,
ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 8)
});
// Resolved MCP server exe
var exe = McpRegistrar.ResolveServerExe();
var exists = McpRegistrar.ServerExeExists();
flow.Controls.Add(new Label
{
Text = $"MCP server: {exe}",
AutoSize = false, Width = 600, Height = 20,
ForeColor = exists ? Theme.SerialColor : Color.FromArgb(210, 150, 120),
Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 2)
});
if (!exists)
{
flow.Controls.Add(new Label
{
Text = "⚠ Not found yet — publish the app (or build ETTerms.SerialMcp). Setup still writes this expected path.",
AutoSize = false, Width = 600, Height = 20,
ForeColor = Color.FromArgb(210, 150, 120), Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 4)
});
}
flow.Controls.Add(MakeSpacer(10));
flow.Controls.Add(BuildMcpTargetCard(McpTarget.Claude));
flow.Controls.Add(MakeSpacer(10));
flow.Controls.Add(BuildMcpTargetCard(McpTarget.Kiro));
page.Controls.Add(flow);
return page;
}
/// <summary>單一 AI 目標(Claude / Kiro)的設定卡:狀態 + Setup / Remove + CLI 驗證指令。</summary>
private Panel BuildMcpTargetCard(McpTarget target)
{
var card = new Panel
{
Width = 600, Height = 196, BackColor = Theme.TabBack,
Padding = new Padding(14), Margin = new Padding(0, 0, 0, 4)
};
var col = new FlowLayoutPanel
{
Dock = DockStyle.Fill, FlowDirection = FlowDirection.TopDown,
WrapContents = false, BackColor = Theme.TabBack, AutoSize = false
};
var title = new Label
{
Text = McpRegistrar.DisplayName(target), AutoSize = true,
ForeColor = Theme.Text, Font = Theme.UiFontBold, Margin = new Padding(0, 0, 0, 2)
};
var status = new Label { AutoSize = true, Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 2) };
var pathLbl = new Label
{
Text = $"Config: {McpRegistrar.ConfigPath(target)}",
AutoSize = false, Width = 560, Height = 18,
ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 6)
};
var setupBtn = MakeButton("Setup", Theme.Accent);
var removeBtn = MakeButton("Remove", Color.FromArgb(210, 120, 120));
setupBtn.Margin = new Padding(0, 0, 8, 0);
var btnRow = new FlowLayoutPanel
{
FlowDirection = FlowDirection.LeftToRight, AutoSize = true,
WrapContents = false, BackColor = Theme.TabBack, Margin = new Padding(0, 0, 0, 8)
};
btnRow.Controls.Add(setupBtn);
btnRow.Controls.Add(removeBtn);
var verifyLbl = new Label
{
Text = "Verify in your CLI:", AutoSize = true,
ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 2)
};
var verifyBox = new TextBox
{
Multiline = true, ReadOnly = true, Width = 560, Height = 56,
BackColor = Color.FromArgb(20, 20, 24), ForeColor = Color.FromArgb(200, 200, 200),
BorderStyle = BorderStyle.FixedSingle, Font = new Font("Cascadia Mono", 9f),
Text = McpRegistrar.VerifyHint(target)
};
void Refresh()
{
bool reg = McpRegistrar.IsRegistered(target);
status.Text = reg ? "● Configured" : "○ Not configured";
status.ForeColor = reg ? Theme.SerialColor : Theme.TextDim;
removeBtn.Enabled = reg;
}
setupBtn.Click += (_, _) =>
{
try
{
McpRegistrar.Register(target);
Refresh();
MessageBox.Show(this,
$"{McpRegistrar.DisplayName(target)} is now configured.\n\n" +
"Restart your AI CLI (or open a new session), then run the verify command shown below.",
"AI MCP", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show(this, $"Failed to write config:\n{ex.Message}",
"AI MCP", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
};
removeBtn.Click += (_, _) =>
{
try
{
McpRegistrar.Unregister(target);
Refresh();
MessageBox.Show(this,
$"Removed from {McpRegistrar.DisplayName(target)}.\nRestart your AI CLI for it to take effect.",
"AI MCP", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show(this, $"Failed to update config:\n{ex.Message}",
"AI MCP", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
};
Refresh();
col.Controls.Add(title);
col.Controls.Add(status);
col.Controls.Add(pathLbl);
col.Controls.Add(btnRow);
col.Controls.Add(verifyLbl);
col.Controls.Add(verifyBox);
card.Controls.Add(col);
return card;
}
// ── Helpers ── // ── Helpers ──
private const int LabelWidth = 150; // 標籤欄固定寬度 private const int LabelWidth = 150; // 標籤欄固定寬度
private const int InputWidth = 200; // 所有輸入框統一寬度 private const int InputWidth = 200; // 所有輸入框統一寬度
+16 -1
View File
@@ -10,7 +10,7 @@
<AssemblyName>ETTerms</AssemblyName> <AssemblyName>ETTerms</AssemblyName>
<!-- 版本資訊 --> <!-- 版本資訊 -->
<Version>0.2.0</Version> <Version>0.2.1</Version>
<Product>ETTerms</Product> <Product>ETTerms</Product>
<Company>ETTerms Project</Company> <Company>ETTerms Project</Company>
@@ -32,4 +32,19 @@
<PackageReference Include="System.IO.Ports" Version="8.0.0" /> <PackageReference Include="System.IO.Ports" Version="8.0.0" />
</ItemGroup> </ItemGroup>
<!--
發佈 GUI 時,自動把 Serial MCP server 一併發佈到 <publish>\ETTerms.SerialMcp\ 子資料夾。
這樣單一 `dotnet publish src\ETTerms` 就會產生完整自洽的 bundle,
且 McpRegistrar.ResolveServerExe() 解析的 <ETTerms.exe>\ETTerms.SerialMcp\ETTerms.SerialMcp.exe 必定存在。
刻意放子資料夾:與 GUI 的相依 dll 隔離,避免互相覆蓋。
-->
<Target Name="PublishSerialMcp" AfterTargets="Publish">
<PropertyGroup>
<_McpRid Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier)</_McpRid>
<_McpRid Condition="'$(RuntimeIdentifier)' == ''">win-x64</_McpRid>
</PropertyGroup>
<Message Importance="high" Text="[ETTerms] Publishing ETTerms.SerialMcp -> $(PublishDir)ETTerms.SerialMcp" />
<Exec Command="dotnet publish &quot;$(MSBuildThisFileDirectory)..\ETTerms.SerialMcp\ETTerms.SerialMcp.csproj&quot; -c $(Configuration) -r $(_McpRid) --self-contained false -o &quot;$(PublishDir)ETTerms.SerialMcp&quot;" />
</Target>
</Project> </Project>
+171
View File
@@ -0,0 +1,171 @@
using System.IO;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace ETTerms.Infrastructure;
/// <summary>支援一鍵設定 MCP 的 AI CLI 目標。</summary>
public enum McpTarget { Claude, Kiro }
/// <summary>
/// 把 ETTerms 的 Serial MCP server<c>ETTerms.SerialMcp</c>)一鍵註冊 / 移除到
/// 各 AI CLI 的「使用者層級」MCP 設定檔。採 read-modify-write,保留檔內其他既有伺服器。
///
/// - Claude Code<c>~/.claude.json</c> 頂層 <c>mcpServers</c>entry 需 <c>type:"stdio"</c>。
/// - Kiro<c>%USERPROFILE%\.kiro\settings\mcp.json</c> 頂層 <c>mcpServers</c>。
/// </summary>
public static class McpRegistrar
{
/// <summary>註冊到各 CLI 時用的 MCP server 名稱。</summary>
public const string ServerName = "etterms-serial";
public static string DisplayName(McpTarget t) => t switch
{
McpTarget.Claude => "Claude Code",
McpTarget.Kiro => "Kiro",
_ => t.ToString()
};
/// <summary>該 AI CLI 的使用者層級 MCP 設定檔路徑。</summary>
public static string ConfigPath(McpTarget t)
{
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return t switch
{
McpTarget.Claude => Path.Combine(home, ".claude.json"),
McpTarget.Kiro => Path.Combine(home, ".kiro", "settings", "mcp.json"),
_ => throw new ArgumentOutOfRangeException(nameof(t))
};
}
/// <summary>給使用者在 CLI 確認是否設定成功的指令(多行)。</summary>
public static string VerifyHint(McpTarget t) => t switch
{
McpTarget.Claude =>
"claude mcp list\r\n" +
$"# 應看到:{ServerName} ✓ Connected\r\n" +
$"# 細節: claude mcp get {ServerName}",
McpTarget.Kiro =>
"kiro-cli mcp list\r\n" +
$"kiro-cli mcp status --name {ServerName}\r\n" +
"# 或在 Kiro IDE:點 ghost 圖示開 MCP Servers 面板查看狀態",
_ => ""
};
/// <summary>找出 ETTerms.SerialMcp 執行檔路徑(找不到回傳最可能的位置作為註冊值)。</summary>
public static string ResolveServerExe()
{
var baseDir = AppContext.BaseDirectory;
var candidates = new List<string>
{
Path.Combine(baseDir, "ETTerms.SerialMcp", "ETTerms.SerialMcp.exe"), // 發佈版(子資料夾)
Path.Combine(baseDir, "ETTerms.SerialMcp.exe"), // 同層
};
// 開發版 fallbacksrc\ETTerms\bin\<cfg>\net8.0-windows → src\ETTerms.SerialMcp\bin\<cfg>\net8.0
try
{
var binCfg = new DirectoryInfo(baseDir); // ...\net8.0-windows
var config = binCfg.Parent?.Name ?? "Debug"; // Debug / Release
var srcDir = binCfg.Parent?.Parent?.Parent?.Parent; // ...\src
if (srcDir != null)
candidates.Add(Path.Combine(srcDir.FullName, "ETTerms.SerialMcp", "bin", config, "net8.0", "ETTerms.SerialMcp.exe"));
}
catch { /* 路徑推導失敗就略過開發版 fallback */ }
foreach (var c in candidates)
if (File.Exists(c)) return c;
return candidates[0]; // 都找不到 → 回發佈版預期位置
}
public static bool ServerExeExists() => File.Exists(ResolveServerExe());
/// <summary>該目標是否已註冊 etterms-serial。</summary>
public static bool IsRegistered(McpTarget t)
{
try
{
var path = ConfigPath(t);
if (!File.Exists(path)) return false;
var root = JsonNode.Parse(File.ReadAllText(path)) as JsonObject;
return (root?["mcpServers"] as JsonObject)?[ServerName] != null;
}
catch { return false; }
}
/// <summary>註冊(或更新)etterms-serial 到該目標設定檔。</summary>
public static void Register(McpTarget t)
{
var path = ConfigPath(t);
var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
var root = LoadRoot(path);
if (root["mcpServers"] is not JsonObject servers)
{
servers = new JsonObject();
root["mcpServers"] = servers;
}
servers[ServerName] = BuildEntry(t);
WriteRoot(path, root);
AppLogger.Info($"MCP registered to {DisplayName(t)} at {path}");
}
/// <summary>從該目標設定檔移除 etterms-serial。</summary>
public static void Unregister(McpTarget t)
{
var path = ConfigPath(t);
if (!File.Exists(path)) return;
var root = LoadRoot(path);
if (root["mcpServers"] is JsonObject servers && servers.Remove(ServerName))
{
WriteRoot(path, root);
AppLogger.Info($"MCP unregistered from {DisplayName(t)} at {path}");
}
}
private static JsonObject BuildEntry(McpTarget t)
{
var exe = ResolveServerExe();
return t switch
{
// Claude Codestdio server 需 type 欄位
McpTarget.Claude => new JsonObject
{
["type"] = "stdio",
["command"] = exe,
["args"] = new JsonArray()
},
// Kirolocal server,附 env / disabled / autoApprove 預設
McpTarget.Kiro => new JsonObject
{
["command"] = exe,
["args"] = new JsonArray(),
["env"] = new JsonObject(),
["disabled"] = false,
["autoApprove"] = new JsonArray()
},
_ => throw new ArgumentOutOfRangeException(nameof(t))
};
}
/// <summary>讀入設定檔為可變 JSON 物件;不存在回空物件。檔案存在但格式錯誤則丟例外(不覆蓋使用者資料)。</summary>
private static JsonObject LoadRoot(string path)
{
if (!File.Exists(path)) return new JsonObject();
var text = File.ReadAllText(path);
if (string.IsNullOrWhiteSpace(text)) return new JsonObject();
if (JsonNode.Parse(text) is JsonObject obj) return obj;
throw new InvalidDataException($"{path} 不是有效的 JSON 物件,為避免覆蓋資料已中止。請手動檢查該檔。");
}
/// <summary>原子寫回(先寫 .tmp 再 replace),避免半寫壞檔。</summary>
private static void WriteRoot(string path, JsonObject root)
{
var json = root.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
var tmp = path + ".tmp";
File.WriteAllText(tmp, json);
if (File.Exists(path)) File.Replace(tmp, path, null);
else File.Move(tmp, path);
}
}