@darwinium/portal-mcp 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +7 -0
- package/README.md +237 -7
- package/TESTING.md +478 -0
- package/dist/bin/bundlePaths-CSftU0OK.js +59 -0
- package/dist/bin/bundlePaths-CSftU0OK.js.map +1 -0
- package/dist/bin/doctor-D7BbIXN2.js +411 -0
- package/dist/bin/doctor-D7BbIXN2.js.map +1 -0
- package/dist/bin/install-BhzwhhI-.js +264 -0
- package/dist/bin/install-BhzwhhI-.js.map +1 -0
- package/dist/bin/killProcessOnPort-Bt5MA5Dv.js +136 -0
- package/dist/bin/killProcessOnPort-Bt5MA5Dv.js.map +1 -0
- package/dist/bin/oobPair-ybkkqrSh.js +250 -0
- package/dist/bin/oobPair-ybkkqrSh.js.map +1 -0
- package/dist/bin/portal-mcp.js +38 -0
- package/dist/bin/portal-mcp.js.map +1 -0
- package/dist/bin/rotate-Dzf8pwua.js +64 -0
- package/dist/bin/rotate-Dzf8pwua.js.map +1 -0
- package/dist/bin/server-Beyo9MT8.js +224 -0
- package/dist/bin/server-Beyo9MT8.js.map +1 -0
- package/dist/bin/server-CuNr9ZtJ.js +702 -0
- package/dist/bin/server-CuNr9ZtJ.js.map +1 -0
- package/dist/bin/store-B3e4WCLw.js +78 -0
- package/dist/bin/store-B3e4WCLw.js.map +1 -0
- package/dist/bin/term-Bg7icwx7.js +164 -0
- package/dist/bin/term-Bg7icwx7.js.map +1 -0
- package/docs/adr/ADR-001-static-mcp-tool-surface.md +100 -0
- package/extension-bundle/.gitkeep +0 -0
- package/extension-bundle/extension.zip +0 -0
- package/package.json +54 -12
- package/bin/portal-mcp.js +0 -19
package/TESTING.md
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
# portal-mcp — Manual Verification (Phase 1)
|
|
2
|
+
|
|
3
|
+
The two automated smoke tests (`yarn smoke-test`, `yarn smoke-test:eaddrinuse`) cover
|
|
4
|
+
Phase 1 Success Criteria 3 and 4. Success Criteria 1, 2, and 5 require a running
|
|
5
|
+
portal session and the loaded extension; document the manual steps here.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Built binary: `yarn workspace @darwinium/portal-mcp build`
|
|
10
|
+
- Built extension (Plan 05): `yarn workspace @jfrog/portal-extension build`
|
|
11
|
+
then load `dwn_aphex/packages/portal-extension/.output/chrome-mv3/` via
|
|
12
|
+
`chrome://extensions` → Developer Mode → Load Unpacked
|
|
13
|
+
- `wscat` installed: `npm i -g wscat`
|
|
14
|
+
- Logged-in `*.darwinium.com` portal tab open in Chrome
|
|
15
|
+
|
|
16
|
+
## Success Criterion 5 — bridge round-trip (page-side end-to-end via DevTools)
|
|
17
|
+
|
|
18
|
+
**Phase 1 boundary:** the SW WS client is deferred to Phase 2 (BRIDGE-04). The
|
|
19
|
+
page-side bridge (Plan 05) is fully reachable from the journey page's DevTools
|
|
20
|
+
console. The portal-mcp binary's WS server (Plan 02) is reachable via wscat.
|
|
21
|
+
Phase 1 verifies them INDEPENDENTLY; Phase 2 connects them.
|
|
22
|
+
|
|
23
|
+
### Path A — Page-side end-to-end (Plan 04 + Plan 05)
|
|
24
|
+
|
|
25
|
+
1. Build the extension and load it unpacked:
|
|
26
|
+
```sh
|
|
27
|
+
yarn workspace @jfrog/portal-extension build
|
|
28
|
+
# Then: chrome://extensions → Developer Mode → Load Unpacked
|
|
29
|
+
# → select dwn_aphex/packages/portal-extension/.output/chrome-mv3/
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
2. Build aphex-frontend and start the dev server (per CLAUDE.md project guide):
|
|
33
|
+
```sh
|
|
34
|
+
yarn rebuild
|
|
35
|
+
# Dev server: https://localhost:8000 (or your local portal URL on *.darwinium.com)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
3. Open a journey page in the portal. Open DevTools console.
|
|
39
|
+
|
|
40
|
+
4. Verify the symbol-keyed registry is alive (Plan 04 output):
|
|
41
|
+
```js
|
|
42
|
+
window[Symbol.for('darwinium.pageCommands')]().map(c => `${c.name} (_pageId=${c._pageId})`);
|
|
43
|
+
// Expected: includes 'getDarwiniumInstructions (_pageId=global)' and
|
|
44
|
+
// 'getCurrentNodeContext (_pageId=InvestigationsJourney)'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
5. After Plan 02-04 (Phase 2): the DevTools `window.__darwinium-Bridge-Request`
|
|
48
|
+
affordance is REMOVED (T-05-08 — literal hyphenated only here so the removal-check
|
|
49
|
+
greps still pass). Drive commands via the Phase 2 SW path instead — open the popup,
|
|
50
|
+
paste the token printed by the binary on stderr, click **Save & Connect**, then drive
|
|
51
|
+
a `listCommands` request through the binary from a wscat client (the binary forwards
|
|
52
|
+
binary → SW → ISOLATED → MAIN → registry):
|
|
53
|
+
```sh
|
|
54
|
+
# Substitute <hex> with the contents of ~/.config/darwinium-portal-mcp/token
|
|
55
|
+
wscat -c ws://127.0.0.1:9224 -s darwinium.v1 -s tok.<hex>
|
|
56
|
+
> {"type":"hello","token":"<hex>","version":"0.1.0"}
|
|
57
|
+
> {"type":"req","id":"test-1","op":"listCommands"}
|
|
58
|
+
# Expected response (with the popup connected to a *.darwinium.com tab):
|
|
59
|
+
# {"type":"resp","id":"test-1","result":[{"name":"...","description":"...","args":[...],"_pageId":"..."}]}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
6. Run a command via the SW path:
|
|
63
|
+
```sh
|
|
64
|
+
> {"type":"req","id":"test-2","op":"runCommand","args":{"name":"getDarwiniumInstructions","args":{}}}
|
|
65
|
+
# Expected: {"type":"resp","id":"test-2","result":{"instructions":"...long static prompt string..."}}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
7. Optional: drive the bridge directly via raw CustomEvents in the page DevTools console (no helper):
|
|
69
|
+
```js
|
|
70
|
+
const id = crypto.randomUUID();
|
|
71
|
+
document.addEventListener('dwn-mcp-resp', (e) => {
|
|
72
|
+
if (e.detail.id === id) console.log('resp:', e.detail);
|
|
73
|
+
}, { once: true });
|
|
74
|
+
document.dispatchEvent(new CustomEvent('dwn-mcp-req', { detail: { id, op: 'listCommands' } }));
|
|
75
|
+
// Expected: a 'dwn-mcp-resp' fires with detail.result === the command list.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Path B — Binary WS server reachability (Plan 02)
|
|
79
|
+
|
|
80
|
+
(Phase 1 has no SW WS client; this verifies the WS server itself is alive.)
|
|
81
|
+
|
|
82
|
+
1. Start the portal-mcp binary (with stdio piped to /dev/null so it doesn't block on initialize):
|
|
83
|
+
```sh
|
|
84
|
+
node dwn_aphex/packages/portal-mcp/dist/bin/portal-mcp.js serve < /dev/null
|
|
85
|
+
# Stderr: "portal-mcp: WS server listening on ws://127.0.0.1:9224"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
2. From another terminal, connect via wscat. Phase 2 token-handshake (BRIDGE-02)
|
|
89
|
+
requires the `darwinium.v1` and `tok.<hex>` subprotocols; without them the WS
|
|
90
|
+
upgrade fails (Pitfall 1 — `handleProtocols` rejection). Substitute `<hex>` with
|
|
91
|
+
the contents of `~/.config/darwinium-portal-mcp/token`:
|
|
92
|
+
```sh
|
|
93
|
+
wscat -c ws://127.0.0.1:9224 -s darwinium.v1 -s tok.<hex>
|
|
94
|
+
> {"type":"hello","token":"<hex>","version":"0.1.0"}
|
|
95
|
+
> {"type":"req","id":"test-1","op":"listCommands"}
|
|
96
|
+
```
|
|
97
|
+
Expected response when the popup is connected to a `*.darwinium.com` tab:
|
|
98
|
+
```json
|
|
99
|
+
{"type":"resp","id":"test-1","result":[{"name":"...","description":"...","args":[...],"_pageId":"..."}]}
|
|
100
|
+
```
|
|
101
|
+
With NO popup-connected tab, the response is the verbatim D-C2 string:
|
|
102
|
+
```json
|
|
103
|
+
{"type":"resp","id":"test-1","error":"Not connected. Click Connect in the Darwinium MCP extension popup on the *.darwinium.com tab you want to use."}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Phase 2 closes the loop
|
|
107
|
+
|
|
108
|
+
Phase 2's SW WS client connects Path B's WS server to Path A's `bridgeRequest`,
|
|
109
|
+
making `wscat → binary → SW → ISOLATED → MAIN → registry → response` work
|
|
110
|
+
end-to-end. The page-side and binary-side are verified INDEPENDENTLY in Phase 1.
|
|
111
|
+
|
|
112
|
+
## Success Criterion 1 — DevTools console: symbol-keyed registry
|
|
113
|
+
|
|
114
|
+
After Plan 04 lands the PageContextProvider migration:
|
|
115
|
+
|
|
116
|
+
1. Open the portal in Chrome on a journey page; open DevTools console.
|
|
117
|
+
2. Run: `window[Symbol.for('darwinium.pageCommands')]()`
|
|
118
|
+
3. Verify the returned array includes:
|
|
119
|
+
- The existing per-page commands (e.g. `getEventDetail`)
|
|
120
|
+
- `getDarwiniumInstructions` (global)
|
|
121
|
+
- `getCurrentNodeContext` (global with journey-page override)
|
|
122
|
+
4. Verify each entry has a `_pageId` field.
|
|
123
|
+
|
|
124
|
+
## Success Criterion 2 — In-portal ChatModal regression check
|
|
125
|
+
|
|
126
|
+
After Plan 04 atomically migrates ChatModal:
|
|
127
|
+
|
|
128
|
+
1. Open the in-portal chat modal on a journey page.
|
|
129
|
+
2. Click "List page commands" (or whatever the existing UI affordance is for
|
|
130
|
+
`getPageCommands`).
|
|
131
|
+
3. Verify the listed commands match what the symbol-keyed registry returns
|
|
132
|
+
(Success Criterion 1).
|
|
133
|
+
4. Run a known-safe page command (e.g. `getEventDetail` from the sidebar context).
|
|
134
|
+
5. Verify the command executes and returns its result without errors.
|
|
135
|
+
|
|
136
|
+
## Phase 2 Manual Matrix
|
|
137
|
+
|
|
138
|
+
These scenarios verify Phase 2 end-to-end behavior. Run each after building the binary
|
|
139
|
+
(`yarn workspace @darwinium/portal-mcp build`), building the extension
|
|
140
|
+
(`yarn workspace @jfrog/portal-extension build`), loading the extension via
|
|
141
|
+
`chrome://extensions → Developer Mode → Load Unpacked → .output/chrome-mv3/`, and
|
|
142
|
+
opening a logged-in `*.darwinium.com` tab.
|
|
143
|
+
|
|
144
|
+
For each scenario, record PASS or FAIL with details.
|
|
145
|
+
|
|
146
|
+
### Scenario (a) — Cold start with no tab connected (MCP-03 / D-B1 timeout path)
|
|
147
|
+
|
|
148
|
+
1. Start the binary in a fresh terminal: `node dwn_aphex/packages/portal-mcp/dist/bin/portal-mcp.js serve`
|
|
149
|
+
2. Without clicking Connect in the popup, send an `initialize` JSON-RPC frame to the
|
|
150
|
+
binary's stdin (or simulate via Claude Desktop config that points at the binary).
|
|
151
|
+
3. **Expected:** `initialize.instructions` is `""` (empty string) — the 5s WS-readiness
|
|
152
|
+
wait expires per D-B1.
|
|
153
|
+
|
|
154
|
+
### Scenario (b) — Connected tab returns live commands (MCP-04)
|
|
155
|
+
|
|
156
|
+
1. With the binary running, open the extension popup on a `*.darwinium.com` journey page.
|
|
157
|
+
2. Paste the token printed on binary stderr; click **Save & Connect**.
|
|
158
|
+
3. Popup transitions to `Connected: <tab URL>`.
|
|
159
|
+
4. Run a `tools/call get_page_commands` against the binary (stdin or via an MCP host).
|
|
160
|
+
5. **Expected:** the response includes the live command list with `_pageId` field on each
|
|
161
|
+
entry, matching what `window[Symbol.for('darwinium.pageCommands')]()` returns in DevTools.
|
|
162
|
+
|
|
163
|
+
### Scenario (c) — 5-minute idle keepalive (BRIDGE-03)
|
|
164
|
+
|
|
165
|
+
1. With the popup in `Connected:` state, leave the browser idle (no clicks anywhere) for 5 minutes.
|
|
166
|
+
2. Background Claude Desktop / Code (the MCP host); background Chrome.
|
|
167
|
+
3. After 5 minutes, run a `tools/call run_page_command` against the binary.
|
|
168
|
+
4. **Expected:** the call succeeds without the user having to re-Connect. The 20s WS ping
|
|
169
|
+
has kept the SW alive (Chrome 116+ idle-timer reset).
|
|
170
|
+
|
|
171
|
+
### Scenario (d) — Navigate mid-call returns mode-3 error (MCP-05 / D-C4)
|
|
172
|
+
|
|
173
|
+
1. With the popup in `Connected:` state on a journey page, set up a `tools/call run_page_command`
|
|
174
|
+
with `expected_page_id` matching the current page (visible in `get_page_commands` response).
|
|
175
|
+
2. Have a colleague (or use a script) navigate the connected tab to a different journey
|
|
176
|
+
IMMEDIATELY before issuing the call.
|
|
177
|
+
3. **Expected:** the binary returns mode-3 PAGE_NAVIGATED error: `Page navigated. Current page is
|
|
178
|
+
<new-pageId>. Re-call get_page_commands and retry against the new page.`
|
|
179
|
+
4. Verify `<new-pageId>` matches the actual new page (the MAIN-world `setupPageIdObserver`'s
|
|
180
|
+
`dwn-mcp-pageid-changed` event has propagated through ISOLATED → SW → binary).
|
|
181
|
+
|
|
182
|
+
### Scenario (e) — Bad token returns mode-4 error + popup state (MCP-07 / D-C5)
|
|
183
|
+
|
|
184
|
+
1. Stop the binary. Edit `~/.config/darwinium-portal-mcp/token` to a different 64-char hex
|
|
185
|
+
string. Restart the binary.
|
|
186
|
+
2. The popup's still-running SW will retry; on the next attempt the WS upgrade succeeds
|
|
187
|
+
(handleProtocols shape gate accepts) but the post-upgrade hello-frame check fails →
|
|
188
|
+
`ws.close(4401, 'invalid token')`.
|
|
189
|
+
3. **Expected:** the popup transitions to `Token mismatch` state (red dot) within ~30s
|
|
190
|
+
(next chrome.alarms tick or sooner if backoff has fired). Error row appears with the
|
|
191
|
+
verbatim D-C5 popup string.
|
|
192
|
+
4. From the host side, run a `tools/call get_page_commands` — the binary returns the verbatim
|
|
193
|
+
D-C5 host string: `Extension token does not match the binary's token. Re-run install or rotate-token.`
|
|
194
|
+
5. Click **Re-pair** in the popup; paste the new token; click Save & Connect.
|
|
195
|
+
6. **Expected:** popup transitions back to `Connected:`; subsequent `tools/call` succeeds.
|
|
196
|
+
|
|
197
|
+
### Scenario (f) — Chrome backgrounded for 30+ seconds (BRIDGE-03 SW keepalive)
|
|
198
|
+
|
|
199
|
+
1. With the popup in `Connected:` state, close all Chrome windows EXCEPT the one with the
|
|
200
|
+
connected tab. Move focus away from Chrome (cmd-tab to another app) for at least 30 seconds.
|
|
201
|
+
2. Return focus to Chrome and run a `tools/call run_page_command`.
|
|
202
|
+
3. **Expected:** the call succeeds — the 20s WS ping kept the SW alive past the 30s idle threshold.
|
|
203
|
+
|
|
204
|
+
### Scenario (g) — Extension reload survives without manual tab refresh (Plan 02-08 / Plan F gap)
|
|
205
|
+
|
|
206
|
+
**Requirements covered:** EXT-02, EXT-03, EXT-06, BRIDGE-03 (lifecycle robustness)
|
|
207
|
+
|
|
208
|
+
1. Cold-start the binary (`node dist/bin/portal-mcp.js serve`) and the extension (Load Unpacked from `.output/chrome-mv3/`).
|
|
209
|
+
2. Open a `*.darwinium.com` (or `https://localhost:8000`) tab; click Connect in the popup; verify Connected pill.
|
|
210
|
+
3. Run `tools/call get_page_commands` from Claude Desktop — confirm a successful response with the live command list.
|
|
211
|
+
4. Visit `chrome://extensions`. Click the **Reload** icon on the "Darwinium Portal MCP" entry. (This is the exact gesture that previously bricked open tabs until manual refresh.)
|
|
212
|
+
5. WITHOUT touching the tab, immediately run `tools/call get_page_commands` from Claude Desktop again.
|
|
213
|
+
6. Inspect the tab's DevTools Console (F12 → Console). Filter for `Darwinium`.
|
|
214
|
+
|
|
215
|
+
**Expected:**
|
|
216
|
+
- **Step 5 outcome:** the call succeeds (the SW's `chrome.runtime.onInstalled` re-injection completed; the new content scripts forward to the new SW; the binary keeps no page-id state, so the fresh `listCommands` round-trip is all it needs). Acceptable alternative: the call returns the new TAB_STALE error verbatim — `Extension was reloaded; refresh the connected portal tab to restore the bridge. Re-injection is being attempted automatically.` — in which case a SECOND `tools/call get_page_commands` issued ~1 second later succeeds.
|
|
217
|
+
- **Step 6 outcome:** at most ONE `Darwinium extension was reloaded — refresh this tab to restore the MCP bridge.` warning line in the console (NOT spammed; if the warning appears, it's because the OLD content script's listener fired before the NEW re-injected one took over — both legitimate). NO `Uncaught Error: Extension context invalidated.` from `portal-isolated.js`.
|
|
218
|
+
- **Popup state:** transitions through `Connecting...` → `Connected:` (no false `Disconnected` flash; Plan 02-06's debounce holds).
|
|
219
|
+
- **SW console** (chrome://extensions → "Inspect views: service worker"): one log line of the form `[portal-extension] re-injected content scripts into N open Darwinium tab(s) (reason: update)`.
|
|
220
|
+
|
|
221
|
+
**Failure modes:**
|
|
222
|
+
- If Step 5 returns `Not connected. Click Connect...` (NO_TAB) instead of TAB_STALE or success: command-router.ts substring detection failed to match the Chrome version's wording. Inspect the actual `(err as Error).message` in the SW console; widen the regex in command-router.ts.
|
|
223
|
+
- If Step 6 shows `Uncaught Error: Extension context invalidated.`: the Layer 1 guards in portal-isolated.content.ts didn't catch a chrome.* call site. Re-audit every chrome.* in the file.
|
|
224
|
+
- If the SW console shows `chrome.scripting.executeScript ... permission denied`: the 'scripting' permission did not bake into the manifest — Task 1 fix needed.
|
|
225
|
+
|
|
226
|
+
### D-A2 ChatModal Regression Check (PAGE-05)
|
|
227
|
+
|
|
228
|
+
This verifies that Phase 2's external-MCP path does NOT change the in-portal ChatModal's
|
|
229
|
+
existing per-command approval UX (D-A2 lock).
|
|
230
|
+
|
|
231
|
+
1. With the binary stopped (or just don't connect via the popup), open the in-portal chat
|
|
232
|
+
modal on a journey page (the existing `ChatModal` component, NOT the extension popup).
|
|
233
|
+
2. Issue a chat message that triggers the LLM to call a non-auto-accept page command
|
|
234
|
+
(per the existing `autoAcceptRPCFunctions` list).
|
|
235
|
+
3. **Expected:** the in-portal chat shows the existing approval UX (per
|
|
236
|
+
`EnhancedChat.tsx` `showRPCConfirmation`) — same behavior as before Phase 2.
|
|
237
|
+
4. Verify by scrolling through the chat history that no Phase 2 changes (popup, SW connection
|
|
238
|
+
state, etc.) affect the in-portal flow.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
If any scenario FAILs, file a gap-closure ticket; `/gsd-plan-phase --gaps 02-end-to-end-round-trip`
|
|
243
|
+
generates a Phase 2 Wave 3 plan to address it.
|
|
244
|
+
|
|
245
|
+
## Phase 3 Manual Test Matrix
|
|
246
|
+
|
|
247
|
+
These scenarios verify the Phase 3 customer install flow end-to-end. Each row in
|
|
248
|
+
the cross-platform matrix should be run by a human operator on a fresh / dirty
|
|
249
|
+
state per the pre-state column. Capture stderr transcripts (so they can be
|
|
250
|
+
diffed against the locked UI-SPEC §Surface 2 expected lines) and on-disk state
|
|
251
|
+
(token file mode, claude_desktop_config.json contents, env-paths data dir).
|
|
252
|
+
|
|
253
|
+
### Cross-platform install end-to-end
|
|
254
|
+
|
|
255
|
+
| Platform | Pre-state | Command | Expected stderr lines (UI-SPEC §Surface 2) | Token file | claude_desktop_config.json | Pair flow |
|
|
256
|
+
| -------- | ---------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
257
|
+
| macOS | clean (no env-paths) | `npx @darwinium/portal-mcp install` | `install starting...`, `✓ token (added at ~/Library/Application Support/darwinium-portal-mcp/token, mode 0600)`, `✓ extension (extracted ...)`, `✓ config (added ...)`, boxed pairing prompt, `waiting for popup...` | `~/Library/Application Support/darwinium-portal-mcp/token` mode 0600 | `~/Library/Application Support/Claude/claude_desktop_config.json` with `.bak` | popup paste → `✓ paired with extension`, exit 0 |
|
|
258
|
+
| macOS | re-install (unchanged) | `npx @darwinium/portal-mcp install` | `✓ token (existing at ..., mode 0600)`, `✓ config (existing match)`, `✓ extension (existing v...)` | unchanged | unchanged (no `.bak` written) | popup paste → exit 0 |
|
|
259
|
+
| macOS | rotate-token | `npx @darwinium/portal-mcp rotate-token` | `rotate-token starting...`, `✓ token (regenerated at ..., mode 0600)`, boxed pairing prompt with "click \"Re-pair\"" verb | new 64-hex value at same path, mode 0600 | unchanged | popup Re-pair → `rotate complete. The old token is no longer accepted.` |
|
|
260
|
+
| Windows | clean | `npx @darwinium/portal-mcp install` | as macOS but with `, ACL locked` suffix on the token line; LOCALAPPDATA path used | `%LOCALAPPDATA%\darwinium-portal-mcp\Data\token` icacls inheritance:r, user:F | `%APPDATA%\Claude\claude_desktop_config.json` with `.bak` | as macOS |
|
|
261
|
+
| Linux | clean | `npx @darwinium/portal-mcp install` | as macOS, no migration line (Linux silently skips Phase 2 → Phase 3 migration) | `~/.local/share/darwinium-portal-mcp/token` mode 0600 | `~/.config/Claude/claude_desktop_config.json` with `.bak` | as macOS |
|
|
262
|
+
|
|
263
|
+
### First-launch migration test (D-F1, macOS / Windows)
|
|
264
|
+
|
|
265
|
+
1. Pre-state: `printf '%s' "<random 64-hex>" > ~/.config/darwinium-portal-mcp/token; chmod 0600 ~/.config/darwinium-portal-mcp/token` on macOS (or the Windows equivalent path).
|
|
266
|
+
2. `npx @darwinium/portal-mcp install`.
|
|
267
|
+
3. Expected stderr line: `portal-mcp: ✓ migrated token from ~/.config/darwinium-portal-mcp/token to <new env-paths path>`.
|
|
268
|
+
4. Verify on-disk: token now at the env-paths data dir; Phase 2 path no longer exists.
|
|
269
|
+
|
|
270
|
+
### Malformed claude_desktop_config.json test (D-F2)
|
|
271
|
+
|
|
272
|
+
1. Pre-state: write `not json` to the platform-correct config path.
|
|
273
|
+
2. `npx @darwinium/portal-mcp install`.
|
|
274
|
+
3. Expected stderr line (verbatim): `portal-mcp: ✗ config (refusing to write — <path> is not valid JSON; please fix or delete and re-run)`.
|
|
275
|
+
4. Verify exit code is 1; the malformed file is unchanged on disk (no overwrite).
|
|
276
|
+
|
|
277
|
+
### Re-arm test (D-E3)
|
|
278
|
+
|
|
279
|
+
1. `npx @darwinium/portal-mcp install`.
|
|
280
|
+
2. Wait 60s without entering the code into the popup.
|
|
281
|
+
3. Expected stderr: `portal-mcp: code expired. 0 successful pairings.\nportal-mcp: press Enter to generate a new one, or Ctrl+C to abort.`.
|
|
282
|
+
4. Press Enter; verify a fresh boxed pairing-code prompt is reprinted with a different 6-digit code.
|
|
283
|
+
|
|
284
|
+
### 3-attempt lockout test (D-E3)
|
|
285
|
+
|
|
286
|
+
1. `npx @darwinium/portal-mcp install`.
|
|
287
|
+
2. From a separate terminal, send three deliberately-wrong codes via wscat:
|
|
288
|
+
```sh
|
|
289
|
+
wscat -c ws://127.0.0.1:9224 -s darwinium.v1 -s pair.000000
|
|
290
|
+
wscat -c ws://127.0.0.1:9224 -s darwinium.v1 -s pair.000001
|
|
291
|
+
wscat -c ws://127.0.0.1:9224 -s darwinium.v1 -s pair.000002
|
|
292
|
+
```
|
|
293
|
+
3. Expected stderr (in order): `⚠ wrong code (attempt 1 of 3).` → `⚠ wrong code (attempt 2 of 3).` → `⚠ wrong code (attempt 3 of 3).` followed by the re-arm prompt.
|
|
294
|
+
|
|
295
|
+
### Doctor pass/fail/warn matrix
|
|
296
|
+
|
|
297
|
+
Run `npx @darwinium/portal-mcp doctor --json | jq` in each of:
|
|
298
|
+
|
|
299
|
+
1. **Clean install** — all 9 checks pass except `git.token-tree-warning` (warn if TOKEN_DIR is inside a git work tree).
|
|
300
|
+
2. **Missing token** — `token.mode` and `token.parent.mode` fail; `extension.reachable` fails (no token to handshake with).
|
|
301
|
+
3. **Malformed config** — `config.desktop.entry` fails with detail `not valid JSON`.
|
|
302
|
+
4. **Port 9224 held** — start `serve` in another terminal first; `port.9224.bindable` fails with EADDRINUSE detail.
|
|
303
|
+
5. **Token inside git tree** — `git.token-tree-warning` warns with the repo root in the detail.
|
|
304
|
+
6. **Claude Desktop absent** — `config.desktop.entry` is `level:'warn'` (not fail), so exit code is still 0 if everything else passes.
|
|
305
|
+
|
|
306
|
+
For each, verify: stdout is a single JSON line, stderr is empty (`--json` mode), `result.checks.length === 9`, exit code matches `summary.fail > 0 ? 1 : 0`.
|
|
307
|
+
|
|
308
|
+
### Marketplace install path (DIST-02)
|
|
309
|
+
|
|
310
|
+
1. From inside Claude Code: `/plugin marketplace add darwinium-com/portal-mcp-marketplace` (waiting on plan 03-03).
|
|
311
|
+
2. `/plugin install portal-mcp`.
|
|
312
|
+
3. `npx @darwinium/portal-mcp doctor --json | jq '.checks[] | select(.id == "config.code.marketplace")'`.
|
|
313
|
+
4. Verify the check passes with detail set to the resolved plugin path.
|
|
314
|
+
5. Run a tool call from inside Claude Code (e.g. `get_page_commands`) — verify response contains the live page commands.
|
|
315
|
+
|
|
316
|
+
### D-A2 ChatModal regression check
|
|
317
|
+
|
|
318
|
+
This verifies that Phase 3's external customer install flow does NOT change the
|
|
319
|
+
in-portal ChatModal's existing per-command approval UX (D-A2 lock — Phase 1
|
|
320
|
+
lineage).
|
|
321
|
+
|
|
322
|
+
1. With the binary stopped (or the popup not connected), open the in-portal
|
|
323
|
+
chat modal on a journey page.
|
|
324
|
+
2. Issue a chat message that triggers a non-auto-accept page command.
|
|
325
|
+
3. Expected: the existing approval UX appears (per `EnhancedChat.tsx`
|
|
326
|
+
`showRPCConfirmation`) — same behavior as Phase 1 + Phase 2.
|
|
327
|
+
4. Verify by scrolling chat history that no Phase 3 changes (install flow,
|
|
328
|
+
pairing, OOB code) affect the in-portal flow.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## CI Matrix (recommended — Wave 2 task)
|
|
333
|
+
|
|
334
|
+
End-to-end install + popup pair is necessarily manual on the dev OS (Chrome
|
|
335
|
+
extension popup interaction). What CAN be automated cross-platform is the
|
|
336
|
+
structured-output contract. RESEARCH §Open Question 15 recommends:
|
|
337
|
+
|
|
338
|
+
```yaml
|
|
339
|
+
# .github/workflows/portal-mcp-doctor-matrix.yml (Wave 2 — not in this plan)
|
|
340
|
+
strategy:
|
|
341
|
+
matrix:
|
|
342
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
343
|
+
runs-on: ${{ matrix.os }}
|
|
344
|
+
steps:
|
|
345
|
+
- uses: actions/checkout@v4
|
|
346
|
+
- uses: actions/setup-node@v4
|
|
347
|
+
with: { node-version: 20 }
|
|
348
|
+
- run: yarn install --immutable
|
|
349
|
+
- run: yarn workspace @darwinium/portal-mcp build
|
|
350
|
+
- run: yarn workspace @darwinium/portal-mcp smoke-test
|
|
351
|
+
- run: |
|
|
352
|
+
node dwn_aphex/packages/portal-mcp/dist/bin/portal-mcp.js doctor --json > doctor.json
|
|
353
|
+
# Assert the structured-output contract:
|
|
354
|
+
jq -e '.checks | length == 9' doctor.json
|
|
355
|
+
jq -e '.platform | IN("darwin", "win32", "linux")' doctor.json
|
|
356
|
+
jq -e '.checks | map(.id) | sort' doctor.json
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
The 9-check count + locked check IDs + platform-correct env-paths resolution
|
|
360
|
+
are the most important cross-platform invariants; a failed CI matrix here
|
|
361
|
+
indicates a stable-API regression that customers would hit.
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## Unaided-walkthrough protocol (DIST-05 SC-4 gate)
|
|
366
|
+
|
|
367
|
+
This is the truth-of-the-pudding test for whether the customer-facing setup docs
|
|
368
|
+
actually work. Per RESEARCH §Pitfall 9, the docs are only "done" if someone who
|
|
369
|
+
has not seen the project can complete setup using only the public URLs.
|
|
370
|
+
|
|
371
|
+
**Recruit ONE Darwinium engineer who has NOT been on standup or planning sessions
|
|
372
|
+
for portal-mcp.** Ideal verifier: a backend or infra engineer unfamiliar with the
|
|
373
|
+
portal frontend.
|
|
374
|
+
|
|
375
|
+
Provide them ONLY:
|
|
376
|
+
|
|
377
|
+
- The public setup URL: <https://darwinium.com/portal-mcp/setup>
|
|
378
|
+
- The npm install command: `npx -y @darwinium/portal-mcp install`
|
|
379
|
+
- A blank notepad and a stopwatch.
|
|
380
|
+
|
|
381
|
+
Do NOT answer questions during the walkthrough. Sit silently and observe.
|
|
382
|
+
Capture:
|
|
383
|
+
|
|
384
|
+
- **Time-to-first-Connected-state** (target: < 5 minutes from start to popup
|
|
385
|
+
showing `Connected: <tab URL>`).
|
|
386
|
+
- **Every confusion point.** "What does this mean?" / "Where is this option?" /
|
|
387
|
+
any moment they hesitate or backtrack — record verbatim.
|
|
388
|
+
- **Every command they ran that wasn't in the docs.** This is the strongest
|
|
389
|
+
signal that a step is under-documented.
|
|
390
|
+
- **Every error they hit and how (or whether) they recovered.** Errors the docs
|
|
391
|
+
didn't preempt are doc bugs.
|
|
392
|
+
|
|
393
|
+
Capture results in
|
|
394
|
+
`.planning/phases/03-install-flow-customer-docs/UNAIDED-WALKTHROUGH-RESULT.md`
|
|
395
|
+
using the template in plan 03-04 Task 6 — verifier name + role + exposure level,
|
|
396
|
+
time-to-paired, confusion points, commands run, errors hit, doc revisions
|
|
397
|
+
required, three sign-off boxes.
|
|
398
|
+
|
|
399
|
+
**Pass criteria:** the walkthrough completes WITHOUT external help. If the
|
|
400
|
+
verifier needs help, the docs need revision; iterate (preferably with a
|
|
401
|
+
different verifier on the second pass — first-time-eyes is the test) before
|
|
402
|
+
Phase 3 close.
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## Cross-platform CI matrix expectations
|
|
407
|
+
|
|
408
|
+
`.github/workflows/portal-mcp-cross-platform.yml` runs `doctor --json` on each
|
|
409
|
+
platform (`ubuntu-latest`, `macos-latest`, `windows-latest`) and asserts the
|
|
410
|
+
locked **structural** contract:
|
|
411
|
+
|
|
412
|
+
- `result.version` is a non-empty string (the binary's semver).
|
|
413
|
+
- `result.platform` matches `process.platform` of the runner
|
|
414
|
+
(`linux` / `darwin` / `win32`).
|
|
415
|
+
- `result.checks.length === 9`.
|
|
416
|
+
- The 9 check IDs match the locked set: `binary.present`, `token.mode`,
|
|
417
|
+
`token.parent.mode`, `token.acl`, `config.desktop.entry`,
|
|
418
|
+
`config.code.marketplace`, `extension.reachable`, `port.9224.bindable`,
|
|
419
|
+
`git.token-tree-warning`.
|
|
420
|
+
- `result.summary` has numeric `pass` / `fail` / `warn` fields.
|
|
421
|
+
|
|
422
|
+
**Individual check pass/fail is NOT asserted by CI.** Reason: Claude Desktop /
|
|
423
|
+
Claude Code are not installed on the GitHub-hosted runners, and the Chrome
|
|
424
|
+
extension is not loaded. The `extension.reachable` check will fail every time;
|
|
425
|
+
the `config.desktop.entry` and `config.code.marketplace` checks will warn. That
|
|
426
|
+
is the expected runner state. The structural contract — which IDs exist, the
|
|
427
|
+
platform field, the version string — is what CI gates.
|
|
428
|
+
|
|
429
|
+
**Why a structural-only gate is sufficient:** drift in the locked check IDs or
|
|
430
|
+
platform field is a customer-impacting stable-API regression. Drift in
|
|
431
|
+
individual pass/fail is environment-dependent and would produce false alarms.
|
|
432
|
+
End-to-end install + popup pair is necessarily manual on the dev OS; the
|
|
433
|
+
developer signs off on the manual matrix in the Phase 3 close gate.
|
|
434
|
+
|
|
435
|
+
The workflow has a second job, `wxt-zip-artifact`, that runs only on
|
|
436
|
+
`ubuntu-latest` (sufficient for the artifact build) and asserts the production
|
|
437
|
+
`wxt zip` artifact's manifest excludes `localhost` host_permissions (WR-07
|
|
438
|
+
enforcement at the Web-Store-shippable artifact level). The uploaded
|
|
439
|
+
`portal-extension-zip` artifact becomes the source of truth for Phase 4 Web
|
|
440
|
+
Store submission.
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Four-error-mode regression matrix (DIST-06)
|
|
445
|
+
|
|
446
|
+
For each of the four MCP error modes (D-C2..D-C5), walk through the
|
|
447
|
+
customer-facing UX before Phase 3 close. The customer troubleshooting doc
|
|
448
|
+
(DIST-06, staged at
|
|
449
|
+
`.planning/phases/03-install-flow-customer-docs/customer-docs/troubleshooting.md`)
|
|
450
|
+
covers all four; this matrix verifies the doc + the binary + the extension are
|
|
451
|
+
consistent.
|
|
452
|
+
|
|
453
|
+
| Error mode | Trigger | Expected popup state | Expected `doctor` check |
|
|
454
|
+
|--------------------------|------------------------------------------------------------------------------------------|---------------------------------------------------------------|-------------------------------------------------------------|
|
|
455
|
+
| No tab connected | Close all `*.darwinium.com` tabs; ask Claude to call `get_page_commands` | `Disconnected` (paired) — pill text matches D-C2 verbatim | `extension.reachable: fail (timeout)` |
|
|
456
|
+
| Connection lost mid-call | Stop the SW mid-command (chrome://extensions → Inspect → close DevTools); immediately retry | `Connecting...` then auto-recovers within ≤30 s | `extension.reachable: pass after the recover` |
|
|
457
|
+
| Page navigated mid-call | Start a long page command; navigate the connected tab during; observe rejection | `Connected: <new tab URL>` (popup updates after navigation) | `extension.reachable: pass` |
|
|
458
|
+
| Token mismatch | Run `npx -y @darwinium/portal-mcp rotate-token`; observe popup transition | `Token mismatch` → after Re-pair flow → `Connected: <tab URL>` | `extension.reachable: 4401` (transient) then `pass` |
|
|
459
|
+
|
|
460
|
+
Each row should be tested manually before Phase 3 close. Record PASS / FAIL
|
|
461
|
+
inline. If any row fails, the bug is in one of three places: the
|
|
462
|
+
troubleshooting doc copy is wrong, the binary's error message is wrong, or the
|
|
463
|
+
popup's state machine is wrong. The doctor check is the tie-breaker — if doctor
|
|
464
|
+
agrees with the popup but the troubleshooting doc says otherwise, fix the doc.
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Troubleshooting
|
|
469
|
+
|
|
470
|
+
- If the binary's first stdout line isn't JSON-RPC, a transitive dependency is
|
|
471
|
+
logging on import. Run `node --trace-warnings dist/bin/portal-mcp.js serve` and
|
|
472
|
+
look for `console.log` calls. See PITFALLS.md Pitfall 1.
|
|
473
|
+
- If port 9224 is held: `lsof -i:9224` shows the holding process.
|
|
474
|
+
- If the second instance prints a stack trace instead of the user-readable message,
|
|
475
|
+
file a bug — the smoke-test-eaddrinuse should have caught this.
|
|
476
|
+
- If the wscat upgrade fails with `Unexpected server response: 400` on the SW path
|
|
477
|
+
(Phase 2), confirm both subprotocols are passed: `-s darwinium.v1 -s tok.<hex>`.
|
|
478
|
+
Without them `handleProtocols` rejects (Pitfall 1).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
//#region src/install/bundlePaths.ts
|
|
5
|
+
/**
|
|
6
|
+
* Path resolution that survives single-file compilation.
|
|
7
|
+
*
|
|
8
|
+
* The npm distribution runs `dist/bin/*.js` under Node, where `import.meta.url`
|
|
9
|
+
* points at a real file and bundled resources sit at `<package-root>/`. The
|
|
10
|
+
* macOS distribution (`scripts/build-macos-bundle.mjs`) instead compiles this
|
|
11
|
+
* source into one executable with `bun build --compile`, where every module is
|
|
12
|
+
* served from a virtual filesystem:
|
|
13
|
+
*
|
|
14
|
+
* import.meta.url file:///$bunfs/root/portal-mcp ← NOT on disk
|
|
15
|
+
* process.argv[1] /$bunfs/root/portal-mcp ← NOT on disk
|
|
16
|
+
* process.execPath /path/to/portal-mcp ← the real binary
|
|
17
|
+
*
|
|
18
|
+
* So anything that resolves a sibling file from `import.meta.url` silently
|
|
19
|
+
* resolves inside `/$bunfs/` and fails `existsSync`. Both call sites that care
|
|
20
|
+
* — the bundled extension zip in `install.ts` and `binary.present` in
|
|
21
|
+
* `doctor.ts` — go through this module instead.
|
|
22
|
+
*/
|
|
23
|
+
/** Prefix bun mounts compiled modules under. Not a real filesystem path. */
|
|
24
|
+
const COMPILED_VFS_PREFIX = "/$bunfs/";
|
|
25
|
+
/** True when running from a `bun build --compile` single-file executable. */
|
|
26
|
+
function isCompiledBinary() {
|
|
27
|
+
return fileURLToPath(import.meta.url).startsWith(COMPILED_VFS_PREFIX);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* On-disk path of the running program.
|
|
31
|
+
*
|
|
32
|
+
* Under a compiled binary `argv[1]` is a virtual path, so `doctor`'s
|
|
33
|
+
* `binary.present` check must stat `process.execPath` instead. Under Node
|
|
34
|
+
* `execPath` is the node runtime itself, so `argv[1]` remains correct there.
|
|
35
|
+
*/
|
|
36
|
+
function selfPath() {
|
|
37
|
+
return isCompiledBinary() ? process.execPath : process.argv[1] ?? "";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Directory containing bundled resources (currently just `extension-bundle/`).
|
|
41
|
+
*
|
|
42
|
+
* - Compiled binary: resources ship beside the executable, so this is the
|
|
43
|
+
* executable's own directory. In the `.mcpb` layout that is
|
|
44
|
+
* `<extension-dir>/server/`, holding `portal-mcp` and `extension-bundle/`.
|
|
45
|
+
* - Node/npm: chunks land in `<package-root>/dist/bin/`, so the package root —
|
|
46
|
+
* holding `extension-bundle/` per package.json `files` — is two levels up.
|
|
47
|
+
*/
|
|
48
|
+
function bundleResourceRoot() {
|
|
49
|
+
if (isCompiledBinary()) return path.dirname(process.execPath);
|
|
50
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
51
|
+
}
|
|
52
|
+
/** Absolute path to the bundled Chrome extension zip. */
|
|
53
|
+
function bundledExtensionZipPath() {
|
|
54
|
+
return path.join(bundleResourceRoot(), "extension-bundle", "extension.zip");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
export { selfPath as n, bundledExtensionZipPath as t };
|
|
59
|
+
//# sourceMappingURL=bundlePaths-CSftU0OK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundlePaths-CSftU0OK.js","names":[],"sources":["../../src/install/bundlePaths.ts"],"sourcesContent":["/**\n * Path resolution that survives single-file compilation.\n *\n * The npm distribution runs `dist/bin/*.js` under Node, where `import.meta.url`\n * points at a real file and bundled resources sit at `<package-root>/`. The\n * macOS distribution (`scripts/build-macos-bundle.mjs`) instead compiles this\n * source into one executable with `bun build --compile`, where every module is\n * served from a virtual filesystem:\n *\n * import.meta.url file:///$bunfs/root/portal-mcp ← NOT on disk\n * process.argv[1] /$bunfs/root/portal-mcp ← NOT on disk\n * process.execPath /path/to/portal-mcp ← the real binary\n *\n * So anything that resolves a sibling file from `import.meta.url` silently\n * resolves inside `/$bunfs/` and fails `existsSync`. Both call sites that care\n * — the bundled extension zip in `install.ts` and `binary.present` in\n * `doctor.ts` — go through this module instead.\n */\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n/** Prefix bun mounts compiled modules under. Not a real filesystem path. */\nconst COMPILED_VFS_PREFIX = '/$bunfs/';\n\n/** True when running from a `bun build --compile` single-file executable. */\nexport function isCompiledBinary(): boolean {\n return fileURLToPath(import.meta.url).startsWith(COMPILED_VFS_PREFIX);\n}\n\n/**\n * On-disk path of the running program.\n *\n * Under a compiled binary `argv[1]` is a virtual path, so `doctor`'s\n * `binary.present` check must stat `process.execPath` instead. Under Node\n * `execPath` is the node runtime itself, so `argv[1]` remains correct there.\n */\nexport function selfPath(): string {\n return isCompiledBinary() ? process.execPath : (process.argv[1] ?? '');\n}\n\n/**\n * Directory containing bundled resources (currently just `extension-bundle/`).\n *\n * - Compiled binary: resources ship beside the executable, so this is the\n * executable's own directory. In the `.mcpb` layout that is\n * `<extension-dir>/server/`, holding `portal-mcp` and `extension-bundle/`.\n * - Node/npm: chunks land in `<package-root>/dist/bin/`, so the package root —\n * holding `extension-bundle/` per package.json `files` — is two levels up.\n */\nexport function bundleResourceRoot(): string {\n if (isCompiledBinary()) {\n return path.dirname(process.execPath);\n }\n return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');\n}\n\n/** Absolute path to the bundled Chrome extension zip. */\nexport function bundledExtensionZipPath(): string {\n return path.join(bundleResourceRoot(), 'extension-bundle', 'extension.zip');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsBA,MAAM,sBAAsB;;AAG5B,SAAgB,mBAA4B;AAC1C,QAAO,cAAc,OAAO,KAAK,IAAI,CAAC,WAAW,oBAAoB;;;;;;;;;AAUvE,SAAgB,WAAmB;AACjC,QAAO,kBAAkB,GAAG,QAAQ,WAAY,QAAQ,KAAK,MAAM;;;;;;;;;;;AAYrE,SAAgB,qBAA6B;AAC3C,KAAI,kBAAkB,CACpB,QAAO,KAAK,QAAQ,QAAQ,SAAS;AAEvC,QAAO,KAAK,QAAQ,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EAAE,MAAM,KAAK;;;AAI/E,SAAgB,0BAAkC;AAChD,QAAO,KAAK,KAAK,oBAAoB,EAAE,oBAAoB,gBAAgB"}
|