@darwinium/portal-mcp 0.0.2 → 0.0.3
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/README.md +8 -8
- package/dist/bin/install-BhzwhhI-.js.map +1 -1
- package/dist/bin/portal-mcp.js +1 -1
- package/dist/bin/portal-mcp.js.map +1 -1
- package/extension-bundle/extension.zip +0 -0
- package/package.json +2 -3
- package/TESTING.md +0 -478
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Connect Claude Desktop or Claude Code to your active `*.darwinium.com` portal tab. Three tools, zero backend connections, OOB-paired to your existing portal session.
|
|
4
4
|
|
|
5
|
-
For the full guide, visit **https://darwinium.com/portal-mcp
|
|
5
|
+
For the full guide, visit **https://www.darwinium.com**. (A dedicated docs site at `darwinium.com/portal-mcp` is coming; until then this README is the reference.)
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -30,8 +30,7 @@ The install is two short steps: register the MCP server with your LLM client, th
|
|
|
30
30
|
|
|
31
31
|
Pick the path your environment allows:
|
|
32
32
|
|
|
33
|
-
- **Chrome Web Store** (recommended for unmanaged Chrome): listing pending review
|
|
34
|
-
https://darwinium.com/portal-mcp/chrome-extension for the current link
|
|
33
|
+
- **Chrome Web Store** (recommended for unmanaged Chrome): listing pending review
|
|
35
34
|
- **Unpacked** (locked corporate Chrome): run the installer in Step 2 first; it extracts a versioned copy of the extension to your platform's app-data directory and prints the exact path. Then in Chrome:
|
|
36
35
|
1. Visit `chrome://extensions`
|
|
37
36
|
2. Enable **Developer Mode** (top right)
|
|
@@ -145,7 +144,7 @@ The MCP server returns four user-actionable error responses:
|
|
|
145
144
|
|
|
146
145
|
## Privacy Policy
|
|
147
146
|
|
|
148
|
-
Full policy: **https://darwinium.com/
|
|
147
|
+
Full policy: **https://www.darwinium.com/privacy-policy**. Summary below.
|
|
149
148
|
|
|
150
149
|
### What this software collects
|
|
151
150
|
|
|
@@ -203,7 +202,7 @@ Privacy questions: **privacy@darwinium.com**. Issues:
|
|
|
203
202
|
- **"`claude_desktop_config.json` is not valid JSON"** — fix the syntax (or delete the file) and re-run `install`. Your previous config is at `claude_desktop_config.json.bak`.
|
|
204
203
|
- **"`port 9224` is already in use"** (`port.9224.bindable` doctor check fails) — another `portal-mcp serve` instance is running, or another tool has bound the port. Quit the other instance, or `lsof -i :9224` (macOS / Linux) / `netstat -ano | findstr :9224` (Windows) to find the offender.
|
|
205
204
|
|
|
206
|
-
For more,
|
|
205
|
+
For more, contact us at **https://www.darwinium.com/contact-us**.
|
|
207
206
|
|
|
208
207
|
## Building the macOS hand-over bundle (internal)
|
|
209
208
|
|
|
@@ -220,9 +219,10 @@ Requires [bun](https://bun.sh) on PATH. Output lands in `build/` (gitignored):
|
|
|
220
219
|
|
|
221
220
|
- `DarwiniumPortalMCP-<version>-macos-<arch>.zip` — the file you hand over
|
|
222
221
|
- `DarwiniumPortalMCP/1 - Darwinium Portal MCP.mcpb` — double-click installs the MCP server
|
|
223
|
-
into Claude Desktop.
|
|
224
|
-
|
|
225
|
-
|
|
222
|
+
into Claude Desktop. The hand-over bundle carries a `bun build --compile` standalone binary
|
|
223
|
+
so it is entirely self-contained for a non-technical recipient. The bundle published to the
|
|
224
|
+
extension directory (`yarn package:mcpb`) uses `server.type: "node"` instead: Claude Desktop
|
|
225
|
+
ships its own Node runtime, so one ~320KB file installs on macOS, Windows and Linux.
|
|
226
226
|
- `DarwiniumPortalMCP/2 - chrome-extension/` — the Load Unpacked target, at a space-free path
|
|
227
227
|
- `DarwiniumPortalMCP/START-HERE.html` — the end-user guide
|
|
228
228
|
- `Check-Setup.command` / `Fix-Permissions.command` — support escape hatches
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install-BhzwhhI-.js","names":["winAcl.lockAcl","extract.extractBundledExtension","oobPair.run"],"sources":["../../src/install/extract.ts","../../src/install/configMerge.ts","../../src/install/install.ts"],"sourcesContent":["/**\n * Bundled-extension extractor.\n *\n * The npm-published binary ships with a copy of the WXT-built portal-extension\n * zip at `extension-bundle/extension.zip` (placed there by\n * `scripts/bundle-extension.ts` during prebuild). At install time we extract it\n * to `<env-paths data dir>/extension/` so customers can load it via Chrome's\n * \"Load Unpacked\" path on locked corporate machines that can't use the Web\n * Store.\n *\n * Idempotency: if the target directory already contains a manifest.json\n * with the same version as the bundled zip, we skip extraction so re-running\n * `install` doesn't churn the extracted dir. Drift (different version) is\n * handled by overwriting (with `extractAllTo(dir, true, false)`).\n *\n * Security: adm-zip's `extractAllTo(target, true, false)` does NOT preserve\n * original permissions and resolves entry paths relative to the extraction\n * target — no traversal beyond `targetDir`. A symlink pre-planted at the\n * target is risk-accepted.\n */\nimport AdmZip from 'adm-zip';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\n/** Outcome of `extractBundledExtension(zip, target)`. */\nexport interface ExtractResult {\n /** True if the target dir was actually written (false = idempotent skip). */\n extracted: boolean;\n /** Manifest version of the bundled extension (always populated). */\n version: string;\n}\n\n// Validate that `json` parses to an object whose `version` field is a non-empty\n// string. Without this, an `as string` cast lets `version: undefined` flow into\n// the idempotency check, where `undefined === undefined` is true and every\n// re-install silently skips extraction.\nfunction readManifestVersion(json: string, source: string): string {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n throw new Error(`${source}: manifest.json is not valid JSON`);\n }\n if (!parsed || typeof parsed !== 'object') {\n throw new Error(`${source}: manifest.json is not a JSON object`);\n }\n const v = (parsed as { version?: unknown }).version;\n if (typeof v !== 'string' || v.length === 0) {\n throw new Error(`${source}: manifest.json missing version string`);\n }\n return v;\n}\n\n/**\n * Extract `zipPath` into `targetDir`, skipping when the existing manifest\n * version matches the incoming version.\n *\n * Throws if the bundled zip is missing `manifest.json` or its version field\n * is missing/non-string (catastrophic build-time mistake — caller maps to\n * the `extract failed: ...` line and exits 1).\n */\nexport function extractBundledExtension(zipPath: string, targetDir: string): ExtractResult {\n // Read the incoming zip's version BEFORE deciding to extract, so we can\n // implement the idempotency check without writing any bytes.\n const zip = new AdmZip(zipPath);\n const manifestEntry = zip.getEntry('manifest.json');\n if (!manifestEntry) {\n throw new Error(`bundled zip ${zipPath} missing manifest.json`);\n }\n const incomingVersion = readManifestVersion(manifestEntry.getData().toString('utf8'), `bundled zip ${zipPath}`);\n\n const existingManifest = path.join(targetDir, 'manifest.json');\n if (fs.existsSync(existingManifest)) {\n try {\n const existingVersion = readManifestVersion(\n fs.readFileSync(existingManifest, 'utf8'),\n `existing ${existingManifest}`,\n );\n if (existingVersion === incomingVersion) {\n return { extracted: false, version: incomingVersion };\n }\n } catch {\n // Existing manifest is malformed or missing version — fall through and overwrite.\n }\n }\n\n fs.mkdirSync(targetDir, { recursive: true });\n zip.extractAllTo(targetDir, /* overwrite */ true, /* keepOriginalPermission */ false);\n return { extracted: true, version: incomingVersion };\n}\n","/**\n * Pure additive merge for `claude_desktop_config.json`.\n *\n * Customers may already have other MCP servers configured in their\n * `claude_desktop_config.json`. We must add or update ONLY our own\n * `mcpServers[\"darwinium-portal-mcp\"]` entry, preserving everything else.\n *\n * To guard against config corruption, the caller (`install.ts`) writes a\n * `.bak` before any modification and uses temp+rename for atomicity. This\n * module is pure — no fs reads/writes, no side effects — so it's trivially\n * testable and cannot leak partial state.\n *\n * Pre-existing malformed JSON triggers a hard refusal: if we can't parse it,\n * we don't touch it. The caller maps the thrown error to the user-facing\n * fail line.\n */\n\n/** Shape of the per-server entry written under `mcpServers[<name>]`. */\nexport interface McpServerEntry {\n command: string;\n args: string[];\n env?: Record<string, string>;\n}\n\n/** Result of a merge: the new full JSON object plus diff metadata. */\nexport interface MergeResult {\n /** Full top-level JSON object after merge. Caller serializes + writes. */\n merged: object;\n /** True if the merged JSON differs from `current` (warrants a write). */\n changed: boolean;\n /** True if `current` already had a `mcpServers[serverName]` entry. */\n existed: boolean;\n}\n\n/**\n * Returns a merged JSON object with `mcpServers[serverName]` set to `entry`,\n * preserving all other top-level keys and other `mcpServers.*` siblings.\n *\n * `current` semantics:\n * - `null` → treated as empty starting point (file did not exist on disk).\n * - non-null non-object (e.g. array, string, number) → throws.\n *\n * Idempotency: if the existing entry is byte-equal (via JSON.stringify of\n * `args` and `env`) to the proposed entry, returns `{changed: false}` so the\n * caller can skip the `.bak` write and the temp-rename.\n */\nexport function mergeMcpServersEntry(current: unknown, serverName: string, entry: McpServerEntry): MergeResult {\n // Treat null (file absent) as empty starting point. Any other non-object is\n // a malformed file and we refuse to write.\n if (current !== null && (typeof current !== 'object' || Array.isArray(current))) {\n throw new Error('claude_desktop_config.json is not a JSON object');\n }\n const base: Record<string, unknown> = (current ?? {}) as Record<string, unknown>;\n\n // `mcpServers` must be a plain object or absent. Anything else (a string, an\n // array) is malformed, and spreading it would scatter its indices across the\n // rewritten config — so it takes the same hard refusal as malformed JSON\n // rather than being silently coerced.\n const rawServers = base.mcpServers;\n if (rawServers !== undefined && (typeof rawServers !== 'object' || rawServers === null || Array.isArray(rawServers))) {\n throw new Error('claude_desktop_config.json has a malformed \"mcpServers\" value');\n }\n const mcpServers: Record<string, unknown> = (rawServers ?? {}) as Record<string, unknown>;\n\n const existing = mcpServers[serverName];\n const existed = existing !== undefined;\n\n // Narrow before reading fields: a non-object entry simply fails the match and\n // gets overwritten, which is what the old `any` did implicitly.\n const existingEntry =\n typeof existing === 'object' && existing !== null ? (existing as Partial<McpServerEntry>) : undefined;\n\n // Drift detection: compare command, args, env via stable JSON encoding. We\n // intentionally use JSON.stringify(args) instead of array equality so order\n // and nested structures are compared deeply.\n const isMatch =\n existingEntry !== undefined &&\n existingEntry.command === entry.command &&\n JSON.stringify(existingEntry.args) === JSON.stringify(entry.args) &&\n JSON.stringify(existingEntry.env ?? {}) === JSON.stringify(entry.env ?? {});\n\n if (isMatch) {\n return { merged: base, changed: false, existed: true };\n }\n\n return {\n merged: { ...base, mcpServers: { ...mcpServers, [serverName]: entry } },\n changed: true,\n existed,\n };\n}\n","/**\n * `npx @darwinium/portal-mcp install` — orchestrator.\n *\n * Sequence:\n * 1. migrate.maybeMigrate() — legacy token-path move\n * 2. token: skip if exists, else generateAndWrite({silent:true})\n * 3. winAcl.lockAcl(TOKEN_PATH) AND winAcl.lockAcl(TOKEN_DIR)\n * 4. extract.extractBundledExtension(zip, EXTENSION_DIR)\n * 5. configMerge → write `.bak` then atomic temp+rename\n * 6. Print Claude Code marketplace + Chrome extension instructions\n * 7. oobPair.run({mode:'install', expectedToken: token})\n * 8. Success summary + process.exit(0)\n *\n * Security:\n * - Token never echoed — generateAndWrite({silent:true}).\n * - configMerge entry contains only `npx @darwinium/portal-mcp serve` —\n * no token, no env.\n * - `.bak` written via copyFileSync BEFORE temp-write; atomic fs.renameSync\n * replaces target. Malformed pre-existing JSON → refusal.\n */\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { TOKEN_DIR, TOKEN_PATH, EXTENSION_DIR } from '../token/paths.js';\nimport { generateAndWrite } from '../token/store.js';\nimport * as term from './term.js';\nimport * as migrate from './migrate.js';\nimport * as winAcl from './winAcl.js';\nimport * as extract from './extract.js';\nimport { bundledExtensionZipPath } from './bundlePaths.js';\nimport { mergeMcpServersEntry, type McpServerEntry } from './configMerge.js';\nimport * as oobPair from './oobPair.js';\n\n/** Config entry — never includes the token; only the cli invocation. */\nconst MCP_ENTRY: McpServerEntry = {\n command: 'npx',\n args: ['-y', '@darwinium/portal-mcp', 'serve'],\n};\n\nconst SERVER_NAME = 'darwinium-portal-mcp';\n\n/** Resolve the platform-correct claude_desktop_config.json path. */\nfunction resolveClaudeDesktopConfigPath(): string {\n if (process.platform === 'darwin') {\n return path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');\n }\n if (process.platform === 'win32') {\n const appData = process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming');\n return path.join(appData, 'Claude', 'claude_desktop_config.json');\n }\n // Linux fallback — Anthropic's documented location matches XDG_CONFIG_HOME default.\n return path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json');\n}\n\n/** Read + parse claude_desktop_config.json. Returns null on ENOENT. */\nfunction readConfigJson(configPath: string): unknown {\n if (!fs.existsSync(configPath)) return null;\n const raw = fs.readFileSync(configPath, 'utf8');\n try {\n return JSON.parse(raw);\n } catch (err) {\n // Re-throw as a tagged error the caller can map to the user-facing fail line.\n throw new Error(`claude_desktop_config.json is not valid JSON: ${(err as Error).message}`);\n }\n}\n\n/** Atomic write: `.bak` (if pre-existing) → temp → rename. */\nfunction writeConfigJsonAtomic(configPath: string, merged: object, existed: boolean): void {\n if (existed) {\n fs.copyFileSync(configPath, `${configPath}.bak`);\n }\n const tmpPath = `${configPath}.tmp.${process.pid}`;\n fs.writeFileSync(tmpPath, JSON.stringify(merged, null, 2), { mode: 0o644 });\n fs.renameSync(tmpPath, configPath);\n}\n\n/**\n * Apply the configMerge to claude_desktop_config.json. Emits exactly one\n * summary-line variant for the config row.\n */\nfunction applyClaudeDesktopMerge(): void {\n const configPath = resolveClaudeDesktopConfigPath();\n const configDir = path.dirname(configPath);\n\n if (!fs.existsSync(configDir)) {\n // Claude Desktop not installed — warn and continue.\n term.warn(\n `config (Claude Desktop config dir not found at ${configDir} — install Claude Desktop or use the Claude Code marketplace path).`,\n );\n return;\n }\n\n let current: unknown;\n try {\n current = readConfigJson(configPath);\n } catch {\n // Refuse to overwrite invalid JSON.\n term.fail(`config (refusing to write — ${configPath} is not valid JSON; please fix or delete and re-run)`);\n process.exit(1);\n }\n\n let result: ReturnType<typeof mergeMcpServersEntry>;\n try {\n result = mergeMcpServersEntry(current, SERVER_NAME, MCP_ENTRY);\n } catch (err) {\n // mergeMcpServersEntry throws on non-object top-level — same fail line.\n term.fail(`config (refusing to write — ${configPath} is not a JSON object: ${(err as Error).message})`);\n process.exit(1);\n }\n\n const fileExisted = current !== null;\n if (!result.changed) {\n term.ok('config (existing match)');\n return;\n }\n\n writeConfigJsonAtomic(configPath, result.merged, fileExisted);\n if (result.existed) {\n term.ok(`config (updated darwinium-portal-mcp entry; .bak written)`);\n } else if (fileExisted) {\n term.ok(`config (added darwinium-portal-mcp entry to ${configPath}; .bak written)`);\n } else {\n // First-ever config file — there's no pre-existing content to back up.\n term.ok(`config (added darwinium-portal-mcp entry to ${configPath}; new file)`);\n }\n}\n\n/** Print the Claude Code + Chrome extension instructions block. */\nfunction printPostInstallInstructions(): void {\n console.error('');\n term.info('Claude Code: run `/plugin marketplace add darwinium-com/portal-mcp-marketplace`');\n console.error(' then `/plugin install portal-mcp` from inside Claude Code.');\n console.error('');\n term.info('Chrome extension — pick one:');\n console.error(' (a) Web Store: https://chromewebstore.google.com/detail/<placeholder-listing-url>');\n console.error(' (b) Unpacked: chrome://extensions → enable Developer Mode →');\n console.error(` Load Unpacked → point at ${EXTENSION_DIR}`);\n console.error('');\n}\n\n/** Public entry point — invoked from the lazy-loaded `install` action in bin. */\nexport async function runInstall(_opts: { json?: boolean; yes?: boolean } = {}): Promise<void> {\n term.info('install starting...');\n\n // 1. Legacy token-path migration (silent on no-op).\n migrate.maybeMigrate(TOKEN_PATH);\n\n // 2. Token — skip generation if existing, else write silently.\n let token: string;\n if (fs.existsSync(TOKEN_PATH)) {\n token = fs.readFileSync(TOKEN_PATH, 'utf8').trim();\n const aclSuffix = process.platform === 'win32' ? ', ACL locked' : '';\n term.ok(`token (existing at ${TOKEN_PATH}, mode 0600${aclSuffix})`);\n } else {\n token = generateAndWrite({ silent: true });\n const aclSuffix = process.platform === 'win32' ? ', ACL locked' : '';\n term.ok(`token (added at ${TOKEN_PATH}, mode 0600${aclSuffix})`);\n }\n\n // 3. Windows ACL on token + parent dir. Non-Windows: no-op.\n for (const p of [TOKEN_PATH, TOKEN_DIR]) {\n const r = winAcl.lockAcl(p);\n if (!r.ok) {\n term.warn(`Could not restrict ACLs on ${p}: ${r.error}. Run \\`${r.cmd}\\` manually.`);\n }\n }\n\n // 4. Extract bundled extension (idempotent).\n const extZip = bundledExtensionZipPath();\n try {\n const extResult = extract.extractBundledExtension(extZip, EXTENSION_DIR);\n if (extResult.extracted) {\n term.ok(`extension (extracted v${extResult.version} to ${EXTENSION_DIR})`);\n } else {\n term.ok(`extension (existing v${extResult.version})`);\n }\n } catch (err) {\n term.fail(\n `extension (extract failed: ${(err as Error).message}; bundled extension at ${extZip} is intact — re-run install)`,\n );\n process.exit(1);\n }\n\n // 5. claude_desktop_config.json additive merge.\n applyClaudeDesktopMerge();\n\n // 6. Post-install instructions.\n printPostInstallInstructions();\n\n // 7. Foreground OOB pair — blocks until paired or SIGINT.\n await oobPair.run({ mode: 'install', expectedToken: token });\n\n // 8. Success summary. Read the manifest of the just-extracted extension to\n // get its version for the success line. If it's missing for any reason,\n // emit the version-less variant.\n let extVersionForSuccess = '';\n const manifestPath = path.join(EXTENSION_DIR, 'manifest.json');\n if (fs.existsSync(manifestPath)) {\n try {\n extVersionForSuccess = JSON.parse(fs.readFileSync(manifestPath, 'utf8')).version as string;\n } catch {\n /* fall through to versionless variant */\n }\n }\n if (extVersionForSuccess) {\n term.ok(`paired with extension (extension version ${extVersionForSuccess})`);\n } else {\n term.ok('paired with extension');\n }\n term.info('install complete. You can close this terminal.');\n process.exit(0);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"install-BhzwhhI-.js","names":["winAcl.lockAcl","extract.extractBundledExtension","oobPair.run"],"sources":["../../src/install/extract.ts","../../src/install/configMerge.ts","../../src/install/install.ts"],"sourcesContent":["/**\n * Bundled-extension extractor.\n *\n * The npm-published binary ships with a copy of the WXT-built portal-extension\n * zip at `extension-bundle/extension.zip` (placed there by\n * `scripts/bundle-extension.ts` during prebuild). At install time we extract it\n * to `<env-paths data dir>/extension/` so customers can load it via Chrome's\n * \"Load Unpacked\" path on locked corporate machines that can't use the Web\n * Store.\n *\n * Idempotency: if the target directory already contains a manifest.json\n * with the same version as the bundled zip, we skip extraction so re-running\n * `install` doesn't churn the extracted dir. Drift (different version) is\n * handled by overwriting (with `extractAllTo(dir, true, false)`).\n *\n * Security: adm-zip's `extractAllTo(target, true, false)` does NOT preserve\n * original permissions and resolves entry paths relative to the extraction\n * target — no traversal beyond `targetDir`. A symlink pre-planted at the\n * target is risk-accepted.\n */\nimport AdmZip from 'adm-zip';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\n/** Outcome of `extractBundledExtension(zip, target)`. */\nexport interface ExtractResult {\n /** True if the target dir was actually written (false = idempotent skip). */\n extracted: boolean;\n /** Manifest version of the bundled extension (always populated). */\n version: string;\n}\n\n// Validate that `json` parses to an object whose `version` field is a non-empty\n// string. Without this, an `as string` cast lets `version: undefined` flow into\n// the idempotency check, where `undefined === undefined` is true and every\n// re-install silently skips extraction.\nfunction readManifestVersion(json: string, source: string): string {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n throw new Error(`${source}: manifest.json is not valid JSON`);\n }\n if (!parsed || typeof parsed !== 'object') {\n throw new Error(`${source}: manifest.json is not a JSON object`);\n }\n const v = (parsed as { version?: unknown }).version;\n if (typeof v !== 'string' || v.length === 0) {\n throw new Error(`${source}: manifest.json missing version string`);\n }\n return v;\n}\n\n/**\n * Extract `zipPath` into `targetDir`, skipping when the existing manifest\n * version matches the incoming version.\n *\n * Throws if the bundled zip is missing `manifest.json` or its version field\n * is missing/non-string (catastrophic build-time mistake — caller maps to\n * the `extract failed: ...` line and exits 1).\n */\nexport function extractBundledExtension(zipPath: string, targetDir: string): ExtractResult {\n // Read the incoming zip's version BEFORE deciding to extract, so we can\n // implement the idempotency check without writing any bytes.\n const zip = new AdmZip(zipPath);\n const manifestEntry = zip.getEntry('manifest.json');\n if (!manifestEntry) {\n throw new Error(`bundled zip ${zipPath} missing manifest.json`);\n }\n const incomingVersion = readManifestVersion(manifestEntry.getData().toString('utf8'), `bundled zip ${zipPath}`);\n\n const existingManifest = path.join(targetDir, 'manifest.json');\n if (fs.existsSync(existingManifest)) {\n try {\n const existingVersion = readManifestVersion(\n fs.readFileSync(existingManifest, 'utf8'),\n `existing ${existingManifest}`,\n );\n if (existingVersion === incomingVersion) {\n return { extracted: false, version: incomingVersion };\n }\n } catch {\n // Existing manifest is malformed or missing version — fall through and overwrite.\n }\n }\n\n fs.mkdirSync(targetDir, { recursive: true });\n zip.extractAllTo(targetDir, /* overwrite */ true, /* keepOriginalPermission */ false);\n return { extracted: true, version: incomingVersion };\n}\n","/**\n * Pure additive merge for `claude_desktop_config.json`.\n *\n * Customers may already have other MCP servers configured in their\n * `claude_desktop_config.json`. We must add or update ONLY our own\n * `mcpServers[\"darwinium-portal-mcp\"]` entry, preserving everything else.\n *\n * To guard against config corruption, the caller (`install.ts`) writes a\n * `.bak` before any modification and uses temp+rename for atomicity. This\n * module is pure — no fs reads/writes, no side effects — so it's trivially\n * testable and cannot leak partial state.\n *\n * Pre-existing malformed JSON triggers a hard refusal: if we can't parse it,\n * we don't touch it. The caller maps the thrown error to the user-facing\n * fail line.\n */\n\n/** Shape of the per-server entry written under `mcpServers[<name>]`. */\nexport interface McpServerEntry {\n command: string;\n args: string[];\n env?: Record<string, string>;\n}\n\n/** Result of a merge: the new full JSON object plus diff metadata. */\nexport interface MergeResult {\n /** Full top-level JSON object after merge. Caller serializes + writes. */\n merged: object;\n /** True if the merged JSON differs from `current` (warrants a write). */\n changed: boolean;\n /** True if `current` already had a `mcpServers[serverName]` entry. */\n existed: boolean;\n}\n\n/**\n * Returns a merged JSON object with `mcpServers[serverName]` set to `entry`,\n * preserving all other top-level keys and other `mcpServers.*` siblings.\n *\n * `current` semantics:\n * - `null` → treated as empty starting point (file did not exist on disk).\n * - non-null non-object (e.g. array, string, number) → throws.\n *\n * Idempotency: if the existing entry is byte-equal (via JSON.stringify of\n * `args` and `env`) to the proposed entry, returns `{changed: false}` so the\n * caller can skip the `.bak` write and the temp-rename.\n */\nexport function mergeMcpServersEntry(current: unknown, serverName: string, entry: McpServerEntry): MergeResult {\n // Treat null (file absent) as empty starting point. Any other non-object is\n // a malformed file and we refuse to write.\n if (current !== null && (typeof current !== 'object' || Array.isArray(current))) {\n throw new Error('claude_desktop_config.json is not a JSON object');\n }\n const base: Record<string, unknown> = (current ?? {}) as Record<string, unknown>;\n\n // `mcpServers` must be a plain object or absent. Anything else (a string, an\n // array) is malformed, and spreading it would scatter its indices across the\n // rewritten config — so it takes the same hard refusal as malformed JSON\n // rather than being silently coerced.\n const rawServers = base.mcpServers;\n if (\n rawServers !== undefined &&\n (typeof rawServers !== 'object' || rawServers === null || Array.isArray(rawServers))\n ) {\n throw new Error('claude_desktop_config.json has a malformed \"mcpServers\" value');\n }\n const mcpServers: Record<string, unknown> = (rawServers ?? {}) as Record<string, unknown>;\n\n const existing = mcpServers[serverName];\n const existed = existing !== undefined;\n\n // Narrow before reading fields: a non-object entry simply fails the match and\n // gets overwritten, which is what the old `any` did implicitly.\n const existingEntry =\n typeof existing === 'object' && existing !== null ? (existing as Partial<McpServerEntry>) : undefined;\n\n // Drift detection: compare command, args, env via stable JSON encoding. We\n // intentionally use JSON.stringify(args) instead of array equality so order\n // and nested structures are compared deeply.\n const isMatch =\n existingEntry !== undefined &&\n existingEntry.command === entry.command &&\n JSON.stringify(existingEntry.args) === JSON.stringify(entry.args) &&\n JSON.stringify(existingEntry.env ?? {}) === JSON.stringify(entry.env ?? {});\n\n if (isMatch) {\n return { merged: base, changed: false, existed: true };\n }\n\n return {\n merged: { ...base, mcpServers: { ...mcpServers, [serverName]: entry } },\n changed: true,\n existed,\n };\n}\n","/**\n * `npx @darwinium/portal-mcp install` — orchestrator.\n *\n * Sequence:\n * 1. migrate.maybeMigrate() — legacy token-path move\n * 2. token: skip if exists, else generateAndWrite({silent:true})\n * 3. winAcl.lockAcl(TOKEN_PATH) AND winAcl.lockAcl(TOKEN_DIR)\n * 4. extract.extractBundledExtension(zip, EXTENSION_DIR)\n * 5. configMerge → write `.bak` then atomic temp+rename\n * 6. Print Claude Code marketplace + Chrome extension instructions\n * 7. oobPair.run({mode:'install', expectedToken: token})\n * 8. Success summary + process.exit(0)\n *\n * Security:\n * - Token never echoed — generateAndWrite({silent:true}).\n * - configMerge entry contains only `npx @darwinium/portal-mcp serve` —\n * no token, no env.\n * - `.bak` written via copyFileSync BEFORE temp-write; atomic fs.renameSync\n * replaces target. Malformed pre-existing JSON → refusal.\n */\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { TOKEN_DIR, TOKEN_PATH, EXTENSION_DIR } from '../token/paths.js';\nimport { generateAndWrite } from '../token/store.js';\nimport * as term from './term.js';\nimport * as migrate from './migrate.js';\nimport * as winAcl from './winAcl.js';\nimport * as extract from './extract.js';\nimport { bundledExtensionZipPath } from './bundlePaths.js';\nimport { mergeMcpServersEntry, type McpServerEntry } from './configMerge.js';\nimport * as oobPair from './oobPair.js';\n\n/** Config entry — never includes the token; only the cli invocation. */\nconst MCP_ENTRY: McpServerEntry = {\n command: 'npx',\n args: ['-y', '@darwinium/portal-mcp', 'serve'],\n};\n\nconst SERVER_NAME = 'darwinium-portal-mcp';\n\n/** Resolve the platform-correct claude_desktop_config.json path. */\nfunction resolveClaudeDesktopConfigPath(): string {\n if (process.platform === 'darwin') {\n return path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');\n }\n if (process.platform === 'win32') {\n const appData = process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming');\n return path.join(appData, 'Claude', 'claude_desktop_config.json');\n }\n // Linux fallback — Anthropic's documented location matches XDG_CONFIG_HOME default.\n return path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json');\n}\n\n/** Read + parse claude_desktop_config.json. Returns null on ENOENT. */\nfunction readConfigJson(configPath: string): unknown {\n if (!fs.existsSync(configPath)) return null;\n const raw = fs.readFileSync(configPath, 'utf8');\n try {\n return JSON.parse(raw);\n } catch (err) {\n // Re-throw as a tagged error the caller can map to the user-facing fail line.\n throw new Error(`claude_desktop_config.json is not valid JSON: ${(err as Error).message}`);\n }\n}\n\n/** Atomic write: `.bak` (if pre-existing) → temp → rename. */\nfunction writeConfigJsonAtomic(configPath: string, merged: object, existed: boolean): void {\n if (existed) {\n fs.copyFileSync(configPath, `${configPath}.bak`);\n }\n const tmpPath = `${configPath}.tmp.${process.pid}`;\n fs.writeFileSync(tmpPath, JSON.stringify(merged, null, 2), { mode: 0o644 });\n fs.renameSync(tmpPath, configPath);\n}\n\n/**\n * Apply the configMerge to claude_desktop_config.json. Emits exactly one\n * summary-line variant for the config row.\n */\nfunction applyClaudeDesktopMerge(): void {\n const configPath = resolveClaudeDesktopConfigPath();\n const configDir = path.dirname(configPath);\n\n if (!fs.existsSync(configDir)) {\n // Claude Desktop not installed — warn and continue.\n term.warn(\n `config (Claude Desktop config dir not found at ${configDir} — install Claude Desktop or use the Claude Code marketplace path).`,\n );\n return;\n }\n\n let current: unknown;\n try {\n current = readConfigJson(configPath);\n } catch {\n // Refuse to overwrite invalid JSON.\n term.fail(`config (refusing to write — ${configPath} is not valid JSON; please fix or delete and re-run)`);\n process.exit(1);\n }\n\n let result: ReturnType<typeof mergeMcpServersEntry>;\n try {\n result = mergeMcpServersEntry(current, SERVER_NAME, MCP_ENTRY);\n } catch (err) {\n // mergeMcpServersEntry throws on non-object top-level — same fail line.\n term.fail(`config (refusing to write — ${configPath} is not a JSON object: ${(err as Error).message})`);\n process.exit(1);\n }\n\n const fileExisted = current !== null;\n if (!result.changed) {\n term.ok('config (existing match)');\n return;\n }\n\n writeConfigJsonAtomic(configPath, result.merged, fileExisted);\n if (result.existed) {\n term.ok(`config (updated darwinium-portal-mcp entry; .bak written)`);\n } else if (fileExisted) {\n term.ok(`config (added darwinium-portal-mcp entry to ${configPath}; .bak written)`);\n } else {\n // First-ever config file — there's no pre-existing content to back up.\n term.ok(`config (added darwinium-portal-mcp entry to ${configPath}; new file)`);\n }\n}\n\n/** Print the Claude Code + Chrome extension instructions block. */\nfunction printPostInstallInstructions(): void {\n console.error('');\n term.info('Claude Code: run `/plugin marketplace add darwinium-com/portal-mcp-marketplace`');\n console.error(' then `/plugin install portal-mcp` from inside Claude Code.');\n console.error('');\n term.info('Chrome extension — pick one:');\n console.error(' (a) Web Store: https://chromewebstore.google.com/detail/<placeholder-listing-url>');\n console.error(' (b) Unpacked: chrome://extensions → enable Developer Mode →');\n console.error(` Load Unpacked → point at ${EXTENSION_DIR}`);\n console.error('');\n}\n\n/** Public entry point — invoked from the lazy-loaded `install` action in bin. */\nexport async function runInstall(_opts: { json?: boolean; yes?: boolean } = {}): Promise<void> {\n term.info('install starting...');\n\n // 1. Legacy token-path migration (silent on no-op).\n migrate.maybeMigrate(TOKEN_PATH);\n\n // 2. Token — skip generation if existing, else write silently.\n let token: string;\n if (fs.existsSync(TOKEN_PATH)) {\n token = fs.readFileSync(TOKEN_PATH, 'utf8').trim();\n const aclSuffix = process.platform === 'win32' ? ', ACL locked' : '';\n term.ok(`token (existing at ${TOKEN_PATH}, mode 0600${aclSuffix})`);\n } else {\n token = generateAndWrite({ silent: true });\n const aclSuffix = process.platform === 'win32' ? ', ACL locked' : '';\n term.ok(`token (added at ${TOKEN_PATH}, mode 0600${aclSuffix})`);\n }\n\n // 3. Windows ACL on token + parent dir. Non-Windows: no-op.\n for (const p of [TOKEN_PATH, TOKEN_DIR]) {\n const r = winAcl.lockAcl(p);\n if (!r.ok) {\n term.warn(`Could not restrict ACLs on ${p}: ${r.error}. Run \\`${r.cmd}\\` manually.`);\n }\n }\n\n // 4. Extract bundled extension (idempotent).\n const extZip = bundledExtensionZipPath();\n try {\n const extResult = extract.extractBundledExtension(extZip, EXTENSION_DIR);\n if (extResult.extracted) {\n term.ok(`extension (extracted v${extResult.version} to ${EXTENSION_DIR})`);\n } else {\n term.ok(`extension (existing v${extResult.version})`);\n }\n } catch (err) {\n term.fail(\n `extension (extract failed: ${(err as Error).message}; bundled extension at ${extZip} is intact — re-run install)`,\n );\n process.exit(1);\n }\n\n // 5. claude_desktop_config.json additive merge.\n applyClaudeDesktopMerge();\n\n // 6. Post-install instructions.\n printPostInstallInstructions();\n\n // 7. Foreground OOB pair — blocks until paired or SIGINT.\n await oobPair.run({ mode: 'install', expectedToken: token });\n\n // 8. Success summary. Read the manifest of the just-extracted extension to\n // get its version for the success line. If it's missing for any reason,\n // emit the version-less variant.\n let extVersionForSuccess = '';\n const manifestPath = path.join(EXTENSION_DIR, 'manifest.json');\n if (fs.existsSync(manifestPath)) {\n try {\n extVersionForSuccess = JSON.parse(fs.readFileSync(manifestPath, 'utf8')).version as string;\n } catch {\n /* fall through to versionless variant */\n }\n }\n if (extVersionForSuccess) {\n term.ok(`paired with extension (extension version ${extVersionForSuccess})`);\n } else {\n term.ok('paired with extension');\n }\n term.info('install complete. You can close this terminal.');\n process.exit(0);\n}\n"],"mappings":"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"}
|
package/dist/bin/portal-mcp.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"portal-mcp.js","names":[],"sources":["../../src/version.ts","../../src/bin/portal-mcp.ts"],"sourcesContent":["// AUTO-GENERATED by scripts/gen-version.mjs — do not edit by hand.\n// Regenerate with `yarn gen-version`; `yarn build` does it for you.\n//\n// This is the RELEASE version. For the bridge frame-format version see\n// PROTOCOL_VERSION in src/bridge/wireProtocol.ts — they are independent.\nexport const VERSION = '0.0.
|
|
1
|
+
{"version":3,"file":"portal-mcp.js","names":[],"sources":["../../src/version.ts","../../src/bin/portal-mcp.ts"],"sourcesContent":["// AUTO-GENERATED by scripts/gen-version.mjs — do not edit by hand.\n// Regenerate with `yarn gen-version`; `yarn build` does it for you.\n//\n// This is the RELEASE version. For the bridge frame-format version see\n// PROTOCOL_VERSION in src/bridge/wireProtocol.ts — they are independent.\nexport const VERSION = '0.0.3';\n","#!/usr/bin/env node\n\n// STDOUT DISCIPLINE — MUST be the FIRST executable statements before any imports\n// that may log on load. Any console.log / .info / .debug / .warn is re-routed to\n// stderr to keep stdout pure JSON-RPC for MCP host parsing.\nconsole.log = console.error;\nconsole.info = console.error;\nconsole.debug = console.error;\nconsole.warn = console.error;\n\nimport { Command } from 'commander';\n\nimport { VERSION } from '../version.js';\n\nconst program = new Command();\nprogram.name('portal-mcp').version(VERSION);\n\nprogram\n .command('serve', { isDefault: true })\n .description(\n 'Run as MCP stdio server (default subcommand). Binds WS server on 127.0.0.1:9224 for the extension bridge.',\n )\n .action(async () => {\n const { runServer } = await import('../server/index.js');\n await runServer();\n });\n\nprogram\n .command('install')\n .description('Install Darwinium Portal MCP — writes binary, token, config, and OOB-pairs the extension')\n .action(async () => {\n const { runInstall } = await import('../install/install.js');\n await runInstall({});\n });\n\nprogram\n .command('doctor')\n .description('Self-diagnostic: pass/fail across binary, token, config, extension, port')\n .option('--json', 'Emit machine-readable JSON for support tickets')\n .action(async (opts: { json?: boolean }) => {\n const { runDoctor } = await import('../install/doctor.js');\n await runDoctor(opts);\n });\n\nprogram\n .command('rotate-token')\n .description('Generate a new token + fresh OOB pairing code; old token rejects with WS 4401')\n .action(async () => {\n const { runRotateToken } = await import('../install/rotate.js');\n await runRotateToken();\n });\n\nprogram.parseAsync(process.argv).catch((err) => {\n // Unhandled errors must go to stderr so stdout stays clean.\n console.error(`portal-mcp fatal: ${(err as Error).message}`);\n process.exit(1);\n});\n"],"mappings":";;;;AAKA,MAAa,UAAU;;;;ACAvB,QAAQ,MAAM,QAAQ;AACtB,QAAQ,OAAO,QAAQ;AACvB,QAAQ,QAAQ,QAAQ;AACxB,QAAQ,OAAO,QAAQ;AAMvB,MAAM,UAAU,IAAI,SAAS;AAC7B,QAAQ,KAAK,aAAa,CAAC,QAAQ,QAAQ;AAE3C,QACG,QAAQ,SAAS,EAAE,WAAW,MAAM,CAAC,CACrC,YACC,4GACD,CACA,OAAO,YAAY;CAClB,MAAM,EAAE,cAAc,MAAM,OAAO;AACnC,OAAM,WAAW;EACjB;AAEJ,QACG,QAAQ,UAAU,CAClB,YAAY,2FAA2F,CACvG,OAAO,YAAY;CAClB,MAAM,EAAE,eAAe,MAAM,OAAO;AACpC,OAAM,WAAW,EAAE,CAAC;EACpB;AAEJ,QACG,QAAQ,SAAS,CACjB,YAAY,2EAA2E,CACvF,OAAO,UAAU,iDAAiD,CAClE,OAAO,OAAO,SAA6B;CAC1C,MAAM,EAAE,cAAc,MAAM,OAAO;AACnC,OAAM,UAAU,KAAK;EACrB;AAEJ,QACG,QAAQ,eAAe,CACvB,YAAY,gFAAgF,CAC5F,OAAO,YAAY;CAClB,MAAM,EAAE,mBAAmB,MAAM,OAAO;AACxC,OAAM,gBAAgB;EACtB;AAEJ,QAAQ,WAAW,QAAQ,KAAK,CAAC,OAAO,QAAQ;AAE9C,SAAQ,MAAM,qBAAsB,IAAc,UAAU;AAC5D,SAAQ,KAAK,EAAE;EACf"}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@darwinium/portal-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Darwinium Portal MCP — local stdio MCP server bridging Claude Desktop / Claude Code to the active *.darwinium.com tab via a Chrome MV3 extension.",
|
|
5
5
|
"author": "Darwinium",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
"dist",
|
|
39
39
|
"docs",
|
|
40
40
|
"extension-bundle",
|
|
41
|
-
"TESTING.md",
|
|
42
41
|
"README.md",
|
|
43
42
|
"LICENSE",
|
|
44
43
|
"NOTICE"
|
|
@@ -47,7 +46,7 @@
|
|
|
47
46
|
"clean": "rimraf dist",
|
|
48
47
|
"bundle-extension": "node scripts/bundle-extension.mjs",
|
|
49
48
|
"build": "rimraf dist && node scripts/gen-version.mjs && node scripts/vendor-instructions.mjs && tsdown && node -e \"require('fs').chmodSync('dist/bin/portal-mcp.js', 0o755)\"",
|
|
50
|
-
"package:macos": "node scripts/build-mcpb.mjs --target=darwin-arm64 --handover",
|
|
49
|
+
"package:macos": "node scripts/build-mcpb.mjs --runtime=bun --target=darwin-arm64 --handover",
|
|
51
50
|
"smoke-test": "node --experimental-strip-types scripts/smoke-test.ts",
|
|
52
51
|
"smoke-test:eaddrinuse": "node --experimental-strip-types scripts/smoke-test-eaddrinuse.ts",
|
|
53
52
|
"test": "npm run build && npm run smoke-test",
|
package/TESTING.md
DELETED
|
@@ -1,478 +0,0 @@
|
|
|
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).
|