JetKVM MCP Server
README.md
# JetKVM MCP Server
JetKVMの公式ローカルWeb UIをPlaywrightで開き、接続先コンピューターの画面取得とHID入力をMCP Toolとして提供するstdioサーバーです。
この文書では、JetKVMに接続されて操作を受けるコンピューターを「PC1」、MCP ServerとPlaywrightを実行するコンピューターを「PC2」と呼びます。HIDは、JetKVMがPC1へ送るマウス・キーボード入力を指します。
## 実装済み機能
- PC1から受信した映像フレームと同じピクセル寸法でのPNG取得
- 絶対マウス移動、クリック、ダブルクリック、スクロール
- 単一キー、macOS向けhotkey、printable ASCII入力
- 複数の画面特徴を必要とするmacOSロック画面判定と、最大1回に制限したロック解除試行
- PC2内で完結するOCR Vision解析と、候補座標・confidenceを含む画面構造化
- risk上限、直列入力、before/after画像差分を一体化したSafe Action Planning
- BrowserContext・WebRTC・HID DataChannelの常駐再利用
- WebRTC切断時の1回限定再接続と、HTML/PNG診断保存
- 出力先制限、ディレクトリ外を指すファイル名の拒否、資格情報のログ抑止
## 調査に基づく方式
2026-08-18時点のJetKVM公式 `jetkvm/kvm` リポジトリ(`dev`、commit `b3c29a44d9e2862b8ff7530830781803ce27b060`)を確認しました。
- ローカル認証UIは `POST /auth/login-local` を使い、成功時にHttpOnlyの `authToken` Cookieを設定します。
- ローカルWebRTC signalingは認証保護された `GET /webrtc/signaling/client` を使います。
- UIは `RTCPeerConnection` に `recvonly` video transceiverを追加し、受信MediaStreamを `<video>` の `srcObject` に設定します。
- 本実装はこの公式UIをそのままPlaywrightで実行し、デコード済みvideo frameをcanvasへ描画してPNG化します。
独自signaling、Developer Mode、独自Firmware、Cloud/Remote Access、JetKVM設定変更は使いません。仮想メディア、Wake on LAN、Terminal、Serial等も公開しません。
## アーキテクチャ
MCP Server起動時にPlaywright Chromium、BrowserContext、pageを各1つだけ作成し、JetKVMへ一度ログインしてWebRTC videoがreadyになるまで待機します。全Toolは同じpageとWebRTC/DataChannelセッションを共有し、同時呼び出しは順番に処理します。通常のTool呼び出しでブラウザ再起動や再ログインは行いません。
入力にはPlaywrightの `page.mouse` / `page.keyboard` を使いません。これらはPC2上のChromiumを操作するだけで、PC1へ届くことを保証できないためです。
マウスとキーボードは、JetKVM公式Web UIがE2Eテスト用に公開している `window.__kvmTestHooks` を優先して呼び出します。hookが利用できない場合は、公式UIが `<video>` と `document` に登録したイベントlistenerへDOMイベントを送ります。スクロールは常に公式UIのvideo用wheel listenerを経由します。この設計により、独自のHIDパケットを実装せず、公式UI内のHID RPC handshake、DataChannel選択、旧バージョン向けfallbackを再利用します。
`__kvmTestHooks` はJetKVMの安定した外部APIではありません。本実装は上記commitで実装を確認しているため、JetKVM更新後は入力系の互換性を再検証してください。
主要コンポーネント:
| ファイル | 責務 | 設計理由 |
|---|---|---|
| `server.ts` | MCP schemaとstdio lifecycle | Playwrightや資格情報をMCP境界へ露出させない |
| `session.ts` | Browser/WebRTC常駐、直列化、再接続 | 競合を避け、全Toolで同一DataChannelを使う |
| `capture.ts` | 受信映像の元のピクセル寸法でframe取得、障害診断 | JetKVM UI全体ではなくPC1映像だけを扱う |
| `input.ts` | 公式HID hook・wheel RPCへのdispatch | PC2ブラウザ操作ではなくPC1へ確実に届ける |
| `keyboard.ts` | MCP key名、KeyboardEvent.code、USB HIDの対応 | キー変換と送信処理を分離する |
| `unlock.ts` | OCR三値判定と最大1回の認証 | 誤判定時の通常アプリへの秘密入力を防ぐ |
| `vision.ts` | ローカルOCR、画面領域、操作候補の抽出 | 画像を外部へ送らず、低confidence時は候補を返さない |
| `planner.ts` | 操作候補、期待結果、riskの構造化 | 入力前に対象と許容riskを明示する |
Tool呼び出しのFLOW:
```text
MCP client
→ Zod引数検証
→ JetKvmSession内の直列実行キュー
→ WebRTC video健全性確認
→ 映像取得、または公式UIのHID/RPC経路
→ MCP response
```
WebRTC切断時は同じpageを1回だけreloadして再接続します。30秒以内に復旧しない場合は診断ファイルを保存し、別のJetKVM WebRTCセッションが存在する可能性を含むエラーを返します。
> JetKVMは同時WebRTCセッションが競合する場合があります。MCP Server利用中は、通常のChrome/Safari等で同じJetKVM KVM画面を開かないでください。
公式資料:
- https://github.com/jetkvm/kvm
- https://github.com/jetkvm/kvm/blob/dev/ui/src/routes/login-local.tsx
- https://github.com/jetkvm/kvm/blob/dev/ui/src/routes/devices.%24id.tsx
- https://github.com/jetkvm/kvm/blob/dev/ui/src/components/WebRTCVideo.tsx
## セットアップ
Node.js 20以上が必要です。
```sh
npm install
npx playwright install chromium
export JETKVM_URL=http://jetkvm.local
export JETKVM_PASSWORD='your-local-password'
export JETKVM_SCREENSHOT_DIR=./screenshots
export JETKVM_PC_PASSWORD='your-pc1-macos-password'
npm run build
```
`.env`を使う場合、サーバー自身はdotenvを自動読込みしないため、起動シェルで読み込ませます。
```sh
cp .env.example .env
# .envへ実値を設定(Gitにはcommitしない)
set -a
source .env
set +a
npm run build
npm start
```
初回のみChromiumを導入します。依存関係を更新しない通常起動では再実行不要です。
```sh
npx playwright install chromium
```
`JETKVM_PC_PASSWORD` はPC1 macOSのロック解除専用です。Tool引数には渡さず、PC2上のローカルな `.env` だけで管理してください。`.env` はgitignore済みですが、誤って別名で複製しないでください。Hermes等の設定ファイルへ平文で記載せず、起動シェルから環境変数を継承する運用を推奨します。
## PNG取得の直接検証
```sh
npm run screenshot -- current-screen.png
```
成功時は `screenshots/current-screen.png` を保存します。ファイル名は `JETKVM_SCREENSHOT_DIR` の外へ出られず、`.png` のみ許可します。
各実行ではSPA初期化のため5秒待機した後、video待機より先に次の診断情報も保存・stderr表示します。videoが取得できない場合も診断ファイルは残ります。
- 現在URL、ページタイトル、本文先頭2000文字
- `video`、password input、`form`、`#root`、`text=JetKVM` の要素数
- `screenshots/debug-page.html`
- `screenshots/debug-page.png`(full-page)
## MCP設定例
```json
{
"mcpServers": {
"jetkvm": {
"command": "node",
"args": ["/path/to/jetkvm-mcp/dist/server.js"],
"env": {
"JETKVM_URL": "http://jetkvm.local",
"JETKVM_PASSWORD": "<local-password>",
"JETKVM_SCREENSHOT_DIR": "/path/to/jetkvm-mcp/screenshots"
}
}
}
}
```
公開ToolとMCP引数:
| Tool | 引数 | 動作 |
|---|---|---|
| `take_screenshot` | `filename?: string` | 受信映像と同じピクセル寸法のPNGを保存・返却 |
| `analyze_screen` | `filename?: string` | ローカルOCRでactive application、visible text、主要領域、操作候補、confidenceを返す |
| `plan_action` | action、target、expected result、各action固有引数 | 最新画面を解析し、候補座標とriskを返す。PC1入力は行わない |
| `execute_action` | plan用引数、`approvedRisk`、`filenamePrefix?` | 計画、risk gate、1操作、before/after差分確認を直列実行 |
| `move_mouse` | `x: int, y: int` | PC1映像座標へ絶対移動 |
| `click` | `x, y, button?: left\|right\|middle` | 指定位置を1回クリック |
| `double_click` | `x: int, y: int` | 左button down/upを2組送信 |
| `scroll` | `dx: number, dy: number` | 公式UIのwheel listener経由でスクロールRPCを送信 |
| `press_key` | `key: string` | 対応キーをdown/up |
| `hotkey` | `keys: string[]` | 順にdown、逆順にup。`META`/`CMD`対応 |
| `type_text` | `text: string` | macOSのABC入力ソースでprintable ASCIIをUS配列として入力 |
| `unlock_pc` | なし | 明確なロック画面だけ認証を最大1回試行 |
| `ensure_unlocked` | なし | 解除済みなら無入力、lockedだけ共通解除処理 |
スクリーンショットの書込み先は、サーバープロセスのカレントディレクトリ直下にある `screenshots/` のみに制限されます。`JETKVM_SCREENSHOT_DIR`を指定する場合も、正規化後にこの場所と一致する必要があります。`../`や絶対パスなど、このディレクトリ外を指すファイル名は拒否します。
## Hermes Integration
2026-08-18時点のHermes Agent公式ドキュメントでは、stdio MCP Serverを `~/.hermes/config.yaml` の `mcp_servers` に登録します。HermesはMCP subprocessへshell環境をすべて継承しないため、必要な環境変数を `env` で明示する必要があります。
公式資料:
- https://hermes-agent.nousresearch.com/docs/user-guide/features/mcp
- https://hermes-agent.nousresearch.com/docs/reference/mcp-config-reference
まずJetKVM MCPをbuildし、Hermesを起動するshellへ資格情報を設定します。
```sh
cd /absolute/path/to/jetkvm-mcp
npm install
npx playwright install chromium
npm run build
export JETKVM_URL=http://jetkvm.local
export JETKVM_PASSWORD='your-local-password'
export JETKVM_SCREENSHOT_DIR=/absolute/path/to/jetkvm-mcp/screenshots
export JETKVM_PC_PASSWORD='your-pc1-macos-password'
```
次の設定を `~/.hermes/config.yaml` の既存内容へmergeします。同じ内容を [`examples/hermes/config.yaml`](examples/hermes/config.yaml) にも収録しています。`command`を実行する際のカレントディレクトリがプロジェクトrootになるよう、Hermes自体もこのディレクトリから起動してください。
```yaml
mcp_servers:
jetkvm:
command: "node"
args: ["/absolute/path/to/jetkvm-mcp/dist/server.js"]
env:
JETKVM_URL: "${JETKVM_URL}"
JETKVM_PASSWORD: "${JETKVM_PASSWORD}"
JETKVM_SCREENSHOT_DIR: "/absolute/path/to/jetkvm-mcp/screenshots"
JETKVM_PC_PASSWORD: "${JETKVM_PC_PASSWORD}"
enabled: true
trust: untrusted
timeout: 120
connect_timeout: 60
idle_timeout_seconds: 0
supports_parallel_tool_calls: false
tools:
include: [take_screenshot, analyze_screen, plan_action, execute_action, move_mouse, click, double_click, scroll, press_key, hotkey, type_text, unlock_pc, ensure_unlocked]
resources: false
prompts: false
```
`tools.include` はHermesが登録するToolのallowlistです。この例は専用検証機で実機確認済みのToolを含みます。一般環境ではread-only Toolから開始し、必要なwrite Toolだけへ絞ってください。
`trust: untrusted` はwrite-capableと判定されたToolにHermesの承認を要求するfail-closed設定です。`idle_timeout_seconds: 0` はHermesによるstdio subprocessのidle再起動を無効化し、JetKVM WebRTC sessionを1つだけ常駐させます。
| JetKVM MCP Tool | Hermesでの登録名 | 今回の確認範囲 |
|---|---|---|
| `take_screenshot` | `mcp__jetkvm__take_screenshot` | 登録・呼出し可能であることを確認対象とする |
| `analyze_screen` | `mcp__jetkvm__analyze_screen` | ローカルOCRで画面状態と操作候補を構造化。PC1入力なし |
| `plan_action` | `mcp__jetkvm__plan_action` | 最新画面から対象座標・期待結果・riskを生成。PC1入力なし |
| `execute_action` | `mcp__jetkvm__execute_action` | 計画・risk承認・入力・before/after差分確認を1つの直列処理で実行 |
| `move_mouse` | `mcp__jetkvm__move_mouse` | PC1映像座標へpointerを移動。クリックなし |
| `click` | `mcp__jetkvm__click` | Visionで確認したPC1映像座標を1回クリック |
| `double_click` | `mcp__jetkvm__double_click` | 確認済み座標で左ダブルクリック |
| `scroll` | `mcp__jetkvm__scroll` | 公式wheel RPC経路でスクロール |
| `press_key` | `mcp__jetkvm__press_key` | 対応キーのdown/upを1組送信 |
| `hotkey` | `mcp__jetkvm__hotkey` | modifierを含むキー列を順序保証して送信 |
| `type_text` | `mcp__jetkvm__type_text` | ABC入力ソース時にprintable ASCIIを公式keypress経路で入力 |
| `unlock_pc` | `mcp__jetkvm__unlock_pc` | 高確度のmacOSロック画面で認証を最大1回試行 |
| `ensure_unlocked` | `mcp__jetkvm__ensure_unlocked` | 解除済みなら無入力、locked時だけ最大1回認証 |
PC1は本プロジェクト専用の検証機として扱い、上記ToolをHermesへ登録します。`trust: untrusted` の実行ゲートと `supports_parallel_tool_calls: false` を維持し、共有WebRTC sessionへの並行入力を防ぎます。認証Toolは1 callにつき最大1試行で、自動retryしません。
実パスへ置換後、プロジェクトrootからHermesを起動します。
```sh
cd /absolute/path/to/jetkvm-mcp
hermes chat
```
Hermes起動中に設定を変更した場合は、Hermes内で `/reload-mcp` を実行します。通常ブラウザで同じJetKVM KVM画面を開いているとWebRTC sessionが競合する可能性があるため、Hermes起動前に閉じてください。
資格情報の実値を `config.yaml` や本Repositoryへ保存しないでください。上記の `${VAR}` はHermes公式がサポートする環境変数参照です。特に `ensure_unlocked` はパスワードをPC1へ送る可能性があるため、Tool登録とTool実行を別の判断として扱ってください。
## PC1ロック解除の安全仕様
ロック状態はPC1映像をPC2上のTesseract.js(WASM、英語・日本語データ同梱)で領域別OCRし、`locked` / `unlocked` / `unknown` の三値で判定します。画像やOCR結果を外部サービスへ送信しません。
OCR実装: https://github.com/naptha/tesseract.js
- `locked`: 所定領域で時刻、日付、パスワード案内の3種類をすべて確認
- `unlocked`: パスワード案内がなく、画面上端で既知のmacOSメニューバー語を3種類以上確認
- `unknown`: 上記の証拠が揃わない状態。パスワードもEnterも送信しない
これはmacOSの状態をOS APIから取得する方式ではなく、画面上の文字配置に基づく保守的な判定です。表示言語、解像度、壁紙、macOSのUI変更によって `unknown` になる可能性があります。誤入力を避けるため、証拠不足時にロック解除を試みないことを優先します。
`unlock_pc()` と `ensure_unlocked()` はMCP引数を取りません。資格情報は `JETKVM_PC_PASSWORD` からのみ読み、ログ、例外、MCP response、ファイル名へ含めません。資格情報入力は診断ログを出さない専用の内部HID経路を使います。1回のTool呼び出しにつきパスワード入力とEnterは最大1回で、自動リトライは行いません。判定用画像は `unlock-before.png`、`ensure-unlocked-before.png`、結果確認は `unlock-after.png` として `screenshots/` 内だけに保存します。
戻り値の `status` は `unlocked`、`already_unlocked`、`not_lock_screen`、`state_unknown`、`unlock_failed` のいずれかです。
## テスト
```sh
npm test
npm run build
```
## 業務アプリ検証と運用上の前提
専用検証機では、Hermes経由のスクリーンショット、ローカルVision、risk gate付き入力、Slackの下書き入力と消去、Jira・Google Sheets・Figmaのread-only表示、VS Code workspaceとCopilot Chatの応答確認まで実施しています。業務データの削除・送信・更新は検証対象にしていません。
- 社内Jira等へ接続する前に、PC1のGlobalProtectが接続済みであることを確認します。MFAや資格情報入力が必要な場合は人間へ引き継ぎます。
- `type_text` は文字列をUSB HID key usageへ変換するため、PC1の現在の入力ソースに従います。ASCIIを確実に入力する前にmacOSの入力ソースを `ABC` にしてください。日本語IMEへの直接入力は対象外です。
- Google Sheetsの実機確認は保護された業務Sheetのread-only表示までです。明示された安全なテストセルがない限り編集しません。
- VS CodeはDockの右クリックメニューから `workspace` を選択し、対象workspaceを明示してからCopilotへ委譲します。
## ロードマップ
今後の候補:
- OCR workerのセッション内再利用による状態判定レイテンシ短縮
- macOSの表示言語・解像度・壁紙バリエーションを増やしたlock判定fixture
- 入力Toolごとの構造化監査イベント(秘密情報を含めない)
- WebRTC/DataChannelの状態を入力なしで確認するread-only health Tool
- Hermes Agent向けの秘密情報を設定ファイルへ直書きしない起動wrapper
- 画面内の命令を信頼できないデータとして扱うPrompt Injection対策
- 複数JetKVMを安全に分離するPC profile
明示的な非目標:
- Developer Mode、独自Firmware、Cloud/Remote Accessの利用
- JetKVM設定変更API、Terminal、Serial、仮想メディア、Wake on LANの公開
- 日本語IMEへの直接文字列注入、認証失敗時の自動リトライ
## 実機検証ログ
- 2026-08-18 STEP 1: 同一WebRTCセッションで `take_screenshot` 3回、`move_mouse` 2回を実行。
- `(100,100)` → HID `(1708,3037)`、`(1700,900)` → HID `(29028,27331)`。
- 両方とも公式E2E HID hook、HID ready、RPC DataChannel open、WebRTC connectedを確認。
- `mouse-a.png` と `mouse-b.png` でPC1カーソルが異なる2地点へ移動したことを確認。
- click、double_click、scroll、press_key、hotkey、type_textの実機呼び出しは0回。
- 2026-08-18 STEP 2: 同一WebRTCセッションで `take_screenshot` 2回、`move_mouse` 1回、left `click` 1回を実行。
- `(960,540)` → HID `(16392,16399)`。move/clickとも公式E2E HID hook、HID ready、RPC DataChannel open、WebRTC connectedを確認。
- 安全なロック画面背景をクリックしたため、カーソル移動以外のPC1 UI変化はなし。
- double_click、right click、scroll、press_key、hotkey、type_textのSTEP 2実機呼び出しは0回。
- 2026-08-18 STEP 3: 同一WebRTCセッションで `take_screenshot` 2回、`press_key("Tab")` 1回(down/up各1回)を実行。
- Tabは公式 `sendKeypress` E2E HID hook(USB HID usage `0x2b`)経由。HID ready、RPC DataChannel open、WebRTC connectedを確認。
- before/after画像ではロック画面の明確なフォーカス変化を判定できなかった。Tab以外のキー、click、double_click、scroll、hotkey、type_textのSTEP 3実機呼び出しは0回。
- 2026-08-18 STEP 4: 同一WebRTCセッションで `take_screenshot` 3回、`type_text("abc")` 1回、`press_key("Backspace")` 3回を実行。
- `abc` とBackspaceは公式 `sendKeypress` E2E HID hook経由。HID ready、RPC DataChannel open、WebRTC connectedを全入力で確認。小文字入力のためShiftは0回、Enterは0回。
- 入力後はパスワード欄に3文字分のマーカーが表示され、Backspace 3回後にすべて消えた。ロック画面からの遷移および追加操作はなし。
- 2026-08-18 STEP 5: 同一WebRTCセッションで `take_screenshot` 2回、`move_mouse(1400,700)` 1回、left `double_click(1400,700)` 1回を実行。
- double-clickは公式 `sendAbsMouseMove` E2E HID hook経由でleft button down/upを各2回送信。HID ready、RPC DataChannel open、WebRTC connectedを確認。
- ロック画面の何もない場所で実施し、画面状態の変更なし。単発clickおよびその他の追加入力は0回。
- 2026-08-18 STEP 6: 同一WebRTCセッションで `take_screenshot` 2回、Slackメッセージ本文領域への `move_mouse(1150,540)` 1回、`scroll(0,500)` 1回を実行。
- scrollは公式video wheel listenerからJetKVMのwheel RPC経路へ送信され、正規化wheel値は `(0,-5)`。HID ready、RPC DataChannel open、WebRTC connectedを確認。
- before/afterでSlackメッセージ本文が上方向へ移動したことを確認。click、double_click、keyboard系Toolおよびその他の追加入力は0回。
- 2026-08-18 STEP 7初回: `hotkey(["SHIFT","TAB"])` は大文字 `TAB` の正規化エラーでHID dispatch前に停止。スクリーンショット2回、PC1へのHID入力0回、画面変化なし。
- `TAB` aliasを `Tab` に正規化する修正とunit testを追加済み。安全条件に従い、この回では実機再試行を行っていない。
- 2026-08-18 STEP 7再試行: 同一WebRTCセッションで `take_screenshot` 2回、`hotkey(["SHIFT","TAB"])` 1回を実行。
- 公式 `sendKeypress` E2E HID hookからShiftLeft down (`0xe1`)、Tab down (`0x2b`)、Tab up、ShiftLeft upの順に送信。HID ready、RPC DataChannel open、WebRTC connectedを確認。
- PC1は壁紙のみの表示からロック画面表示へ変化。その他の入力Toolと追加実機入力は0回。
- 2026-08-19統合検証: Hermesから `take_screenshot` とrisk gate付き `execute_action` を呼び出し、同一WebRTC sessionの維持とbefore/after差分確認を実証。
- ローカルVisionはFinderをactive applicationとして識別し、壁紙上の低confidenceな単独文字をclick候補から除外。改善したmacOS lock判定で `ensure_unlocked` を1回だけ実行し、再試行なしで解除を確認。
- SlackはABC入力ソースで安全な下書きを入力後に消去し、送信操作は未実施。VPN接続後のJira、保護されたGoogle Sheets、Figmaはread-only表示を確認。
- VS CodeはDockの右クリックメニューから `workspace` を選択して開き、Copilot Chatへファイル変更を禁止した確認文だけを送って応答を確認。
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues