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:
+9
-7
@@ -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 server(stdio)——不自己開 port,經 named pipe 橋接 GUI
|
└── ETTerms.SerialMcp/ # ✅ Serial MCP server(stdio)——不自己開 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 `
|
||||||
@@ -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`**。
|
2. **主 exe 改名**:`dotnet publish` 產生的 `ETTerms.exe` 重新命名為 **`ETTerms v{Version}.exe`**。
|
||||||
- 可安全改名:.NET apphost 內部記錄要載入的 `ETTerms.dll`,**不靠自身檔名**,改名後仍正常啟動。
|
- 可安全改名:.NET apphost 內部記錄要載入的 `ETTerms.dll`,**不靠自身檔名**,改名後仍正常啟動。
|
||||||
3. **框架相依**:`--self-contained false -r win-x64`(目標機需已裝 .NET 8 Desktop Runtime)。
|
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"
|
$root = "src\ETTerms\Publish\ETTerms_v$ver"
|
||||||
if (Test-Path $root) { Remove-Item $root -Recurse -Force }
|
if (Test-Path $root) { Remove-Item $root -Recurse -Force }
|
||||||
|
|
||||||
# 1) GUI
|
# 發佈 GUI;ETTerms.SerialMcp 由 PublishSerialMcp target 自動發到 $root\ETTerms.SerialMcp\
|
||||||
dotnet publish src\ETTerms\ETTerms.csproj -c Release -r win-x64 --self-contained false -o $root
|
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"
|
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")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ ETTerms 是一個 **C# .NET 8 WinForms** 的原生 Windows 終端機工作台,
|
|||||||
|
|
||||||
**進度:** Phase 1–5 ✅、Phase 6 ✅(TTL 引擎 + Group 同步,SSH 待驗收)、Phase 7 ✅(Settings/About)、Phase 8 ✅(PDU + Shell/ConPTY + SFTP + Settings 擴充)、Phase 9 ✅(Serial MCP server:**GUI 持有 COM port,MCP 經本機 named pipe 橋接**,AI 收發的資料即時以 `[AI]` 標色顯示在 GUI)。打包待指示。
|
**進度:** Phase 1–5 ✅、Phase 6 ✅(TTL 引擎 + Group 同步,SSH 待驗收)、Phase 7 ✅(Settings/About)、Phase 8 ✅(PDU + Shell/ConPTY + SFTP + Settings 擴充)、Phase 9 ✅(Serial MCP server:**GUI 持有 COM port,MCP 經本機 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 server,read-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)才接受並取「最後一次」出現,排除輸出中途的指令回顯(避免腳本搶跑)。
|
||||||
@@ -39,13 +41,16 @@ 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}\)
|
# 打包(見「Publish / 打包慣例」,輸出固定到 src\ETTerms\Publish\ETTerms_v{Version}\)
|
||||||
|
# GUI publish 會「自動」把 ETTerms.SerialMcp 一併發到 \ETTerms.SerialMcp\ 子資料夾
|
||||||
|
# (ETTerms.csproj 的 PublishSerialMcp target,AfterTargets=Publish),不必再單獨發 MCP。
|
||||||
$ver = ([regex]::Match((Get-Content src\ETTerms\ETTerms.csproj -Raw), '<Version>([^<]+)</Version>')).Groups[1].Value
|
$ver = ([regex]::Match((Get-Content src\ETTerms\ETTerms.csproj -Raw), '<Version>([^<]+)</Version>')).Groups[1].Value
|
||||||
$root = "src\ETTerms\Publish\ETTerms_v$ver"
|
$root = "src\ETTerms\Publish\ETTerms_v$ver"
|
||||||
dotnet publish src\ETTerms\ETTerms.csproj -c Release -r win-x64 --self-contained false -o $root
|
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"
|
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)
|
# 註冊 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"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
       
|
       
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -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 1–8 complete: window shell, connection sidebar, tiling workspace, Serial / SSH / local Shell (ConPTY) / SFTP
|
- Phases 1–8 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
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
> 原生 Windows 終端機工作台(C# .NET 8 WinForms)—— 一個視窗整合 **SSH**、**Serial Port**、**本機 Shell (ConPTY)** 連線,內建從 MyTeraTerm 移植的 **TTL 腳本引擎**做自動化,並提供選用的 **Serial MCP server**,讓 AI agent(Kiro CLI / Claude CLI)直接操作 serial port。單機、無雲、無登入系統。
|
> 原生 Windows 終端機工作台(C# .NET 8 WinForms)—— 一個視窗整合 **SSH**、**Serial Port**、**本機 Shell (ConPTY)** 連線,內建從 MyTeraTerm 移植的 **TTL 腳本引擎**做自動化,並提供選用的 **Serial MCP server**,讓 AI agent(Kiro CLI / Claude CLI)直接操作 serial port。單機、無雲、無登入系統。
|
||||||
|
|
||||||
       
|
       
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -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 server(stdio)—— 讓 AI agent 直接操作 serial port
|
└── src/ETTerms.SerialMcp/ # Serial MCP server(stdio)—— 讓 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 9:Serial MCP server**(`ETTerms.SerialMcp`)—— AI agent(Kiro 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 9:Serial MCP server(`ETTerms.SerialMcp`)—— 讓 AI agent(Kiro CLI / Claude CLI)直接操作 serial port
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -177,15 +177,15 @@ 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",
|
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.",
|
"AI agents can now 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 (never opens the port itself).",
|
"New app icon and version shown in the title bar.",
|
||||||
"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!",
|
|
||||||
]),
|
]),
|
||||||
new("0.1.0", new DateOnly(2026, 6, 3), "Initial Release",
|
new("0.1.0", new DateOnly(2026, 6, 3), "Initial Release",
|
||||||
[
|
[
|
||||||
|
|||||||
+174
-18
@@ -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; // 所有輸入框統一寬度
|
||||||
|
|||||||
@@ -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 "$(MSBuildThisFileDirectory)..\ETTerms.SerialMcp\ETTerms.SerialMcp.csproj" -c $(Configuration) -r $(_McpRid) --self-contained false -o "$(PublishDir)ETTerms.SerialMcp"" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -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"), // 同層
|
||||||
|
};
|
||||||
|
|
||||||
|
// 開發版 fallback:src\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 Code:stdio server 需 type 欄位
|
||||||
|
McpTarget.Claude => new JsonObject
|
||||||
|
{
|
||||||
|
["type"] = "stdio",
|
||||||
|
["command"] = exe,
|
||||||
|
["args"] = new JsonArray()
|
||||||
|
},
|
||||||
|
// Kiro:local 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user