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.
This commit is contained in:
2026-06-05 08:58:02 +08:00
parent 10ec900386
commit 7ba3fadb87
8 changed files with 432 additions and 59 deletions
+10 -8
View File
@@ -160,6 +160,7 @@ ETTerms/
└── Infrastructure/
├── AppLogger.cs # 日誌 (port 自 MyTeraTerm)
├── AppSettings.cs # 使用者偏好 (JSON, %LocalAppData%\ETTerms\settings.json)
├── McpRegistrar.cs # ✅ 一鍵把 Serial MCP 註冊/移除到 Claude Code / Kiro 設定檔(Settings → AI MCP
└── NativeTheme.cs # 深色標題列 (DWM)
└── ETTerms.SerialMcp/ # ✅ Serial MCP serverstdio)——不自己開 port,經 named pipe 橋接 GUI
@@ -411,7 +412,11 @@ ETTerms GUI(單一行程,唯一開 COM3 的人)
- **可視性:** AI 的 TX 在 GUI 以 `[AI]` 標色,與使用者手打的輸入區分;RX 兩邊同源。
- **安全:** 本機、無雲、不碰 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
kiro-cli mcp add --name serial --command dotnet `
@@ -508,7 +513,8 @@ ETTerms_v0.2.0\
```
**規則:**
1. **GUI 與 MCP 都發佈**:主程式發到 `ETTerms_v{Version}\``ETTerms.SerialMcp` 發到其下 **`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)。
@@ -519,13 +525,9 @@ $ver = ([regex]::Match((Get-Content src\ETTerms\ETTerms.csproj -Raw), '<Versi
$root = "src\ETTerms\Publish\ETTerms_v$ver"
if (Test-Path $root) { Remove-Item $root -Recurse -Force }
# 1) GUI
dotnet publish src\ETTerms\ETTerms.csproj -c Release -r win-x64 --self-contained false -o $root
# 發佈 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"
# 2) Serial MCP server → 子資料夾
dotnet publish src\ETTerms.SerialMcp\ETTerms.SerialMcp.csproj -c Release -r win-x64 --self-contained false `
-o (Join-Path $root "ETTerms.SerialMcp")
```
---
+6 -1
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)。打包待指示。
**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.1.2** 新增 `sprintf2`TeraTerm 相容 C printf 格式化);`wait` 改為命中關鍵字後須等裝置安靜(`SettleMs` 預設 300ms)才接受並取「最後一次」出現,排除輸出中途的指令回顯(避免腳本搶跑)。
@@ -39,13 +41,16 @@ dotnet run --project src\ETTerms\ETTerms.csproj
dotnet add src\ETTerms package SSH.NET
# 打包(見「Publish / 打包慣例」,輸出固定到 src\ETTerms\Publish\ETTerms_v{Version}\
# 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"
dotnet publish src\ETTerms.SerialMcp\ETTerms.SerialMcp.csproj -c Release -r win-x64 --self-contained false -o (Join-Path $root "ETTerms.SerialMcp")
# 註冊 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"
```
+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.
![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
- 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**
- 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
- **One-click setup** in **Settings → AI MCP**: register the server into Claude Code / Kiro with a single button
- 📊 **Session RX logging**
- `logopen` / `logwrite` / `logclose` write session output to file
@@ -271,7 +272,7 @@ wait 'login: '
## 🤖 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.
@@ -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_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
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
│ ├── Connections/ # Connection data: Connection / ConnectionStore(SQLite) / CredentialVault
│ ├── Scripting/ # TTL engine: TTLInterpreter / ScriptRunner / GroupSyncContext / Pdu
│ └── Infrastructure/ # AppLogger / AppSettings / NativeTheme
└── src/ETTerms.SerialMcp/ # 🔜 Serial MCP server (stdio) — lets AI agents drive the serial port
│ └── Infrastructure/ # AppLogger / AppSettings / McpRegistrar / NativeTheme
└── 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).
@@ -397,18 +402,25 @@ ETTerms/
## 📜 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
- Owner-drawn VT100 terminal rendering
- TTL scripting engine (ported from MyTeraTerm) + Group sync execution
- PDU power control (SNMP)
- 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。單機、無雲、無登入系統。
![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 插座,測試中遠端電源循環
- 腳本指令:`pduconnect` / `pductrl`
- 🤖 **AI / MCP 整合(選用,規劃中**
- 🤖 **AI / MCP 整合(選用)**
- **COM port 由 GUI 持有**;獨立的 stdio **Serial MCP server** 經本機 named pipe 橋接過去,供 **Kiro CLI / Claude CLI** 使用
- 工具:`serial_list` / `serial_attach` / `serial_write` / `serial_read` / `serial_detach`
- AI 的 serial 收發會即時顯示在 GUI(標 `[AI]`)——先在 GUI 開好 port,再讓 AI attach
- **一鍵設定**:在 **設定 → AI MCP** 一個按鈕就把 server 註冊進 Claude Code / Kiro
- 📊 **連線 RX 日誌**
- `logopen` / `logwrite` / `logclose` 將工作階段輸出寫檔
@@ -271,7 +272,7 @@ wait 'login: '
## 🤖 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 看到的**。
@@ -287,7 +288,11 @@ wait 'login: '
| `serial_read` | waitFor?, timeoutMs? | 取出累積的 RX;可等待子字串或逾時 |
| `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
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
│ ├── Connections/ # 連線資料:Connection / ConnectionStore(SQLite) / CredentialVault
│ ├── Scripting/ # TTL 引擎:TTLInterpreter / ScriptRunner / GroupSyncContext / Pdu
│ └── Infrastructure/ # AppLogger / AppSettings / NativeTheme
└── src/ETTerms.SerialMcp/ # 🔜 Serial MCP serverstdio)—— 讓 AI agent 直接操作 serial port
│ └── Infrastructure/ # AppLogger / AppSettings / McpRegistrar / NativeTheme
└── src/ETTerms.SerialMcp/ # Serial MCP serverstdio)—— 讓 AI agent 直接操作 serial port
```
**核心設計:** 所有連線都實作 `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
- 自繪 VT100 終端機渲染
- TTL 腳本引擎(移植自 MyTeraTerm+ Group 同步執行
- PDU 電源控制(SNMP
- Settings / About
- 打包待規劃
**規劃中**
- Phase 9Serial MCP server`ETTerms.SerialMcp`)—— 讓 AI agentKiro CLI / Claude CLI)直接操作 serial port
---
+7 -7
View File
@@ -177,15 +177,15 @@ public sealed class AboutView : UserControl
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",
[
"Phase 9: Serial MCP server — AI agents can send/receive serial while you watch live in the GUI.",
"GUI is the sole COM-port owner; the MCP server bridges over a local named pipe (never opens the port itself).",
"MCP tools: serial_list / serial_attach / serial_write / serial_read / serial_detach.",
"AI-sourced output is echoed into the terminal tagged [AI] so you see exactly what the AI sees.",
"New app icon for the window title bar, taskbar and executable; version now shown in the title bar.",
"About page now displays the app icon.",
"Beta Version: expect bugs and missing features. Feedback welcome!",
"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",
[
+174 -18
View File
@@ -5,7 +5,7 @@ using ETTerms.Scripting.Pdu;
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 SettingsView()
@@ -20,19 +20,17 @@ public sealed class SettingsView : UserControl
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 };
body.Controls.Add(terminalPage);
body.Controls.Add(pduPage);
var pages = new List<Panel>();
Button? activeBtn = null;
Button MakeTab(string text, Panel page)
{
page.Dock = DockStyle.Fill;
page.Visible = false;
body.Controls.Add(page);
pages.Add(page);
var b = new Button
{
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.Click += (_, _) =>
{
terminalPage.Visible = page == terminalPage;
pduPage.Visible = page == pduPage;
foreach (var p in pages) p.Visible = p == page;
if (activeBtn != null) activeBtn.BackColor = Theme.TabBack;
b.BackColor = Theme.TabActiveBack;
activeBtn = b;
@@ -52,17 +49,16 @@ public sealed class SettingsView : UserControl
return b;
}
var termBtn = MakeTab("Terminal", terminalPage);
var pduBtn = MakeTab("PDU", pduPage);
var termBtn = MakeTab("Terminal", BuildTerminalTab());
tabBar.Controls.Add(termBtn);
tabBar.Controls.Add(pduBtn);
// Set initial active
termBtn.BackColor = Theme.TabActiveBack;
activeBtn = termBtn;
tabBar.Controls.Add(MakeTab("PDU", BuildPduTab()));
tabBar.Controls.Add(MakeTab("AI MCP", BuildAiMcpTab()));
Controls.Add(body);
Controls.Add(tabBar);
// Set initial active tab
termBtn.PerformClick();
}
// ═══ 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 ──
private const int LabelWidth = 150; // 標籤欄固定寬度
private const int InputWidth = 200; // 所有輸入框統一寬度
+16 -1
View File
@@ -10,7 +10,7 @@
<AssemblyName>ETTerms</AssemblyName>
<!-- 版本資訊 -->
<Version>0.2.0</Version>
<Version>0.2.1</Version>
<Product>ETTerms</Product>
<Company>ETTerms Project</Company>
@@ -32,4 +32,19 @@
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
</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>
+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);
}
}