diff --git a/.gitignore b/.gitignore index a5cdf6b..81ba218 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Node / build node_modules/ dist/ +dist-mcpb/ .svelte-kit/ .pnpm-store/ *.tsbuildinfo diff --git a/README.md b/README.md index a24d18f..b9a85e2 100644 --- a/README.md +++ b/README.md @@ -114,9 +114,25 @@ The titlebar 🤖 button opens a small panel that starts an MCP (Model Context P - **Saved SSH passwords are never exposed** through the MCP surface. - **Bound to all interfaces** (`0.0.0.0`). The bearer token is the only auth — don't enable the server on an untrusted network. -#### Claude Code setup (via `mcp-remote` stdio shim) +#### Claude Desktop setup (one-click via `.mcpb` bundle — recommended) -Claude Code's HTTP-MCP client currently tries OAuth discovery and ignores static `headers` auth (Anthropic [#17152](https://github.com/anthropics/claude-code/issues/17152), [#46879](https://github.com/anthropics/claude-code/issues/46879)). The [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) stdio shim transparently proxies the HTTP endpoint with the bearer header attached, sidestepping the OAuth flow. +The MCP panel has a **Download .mcpb** button that fetches a packaged Claude Desktop extension (an `.mcpb` file). Drag it into Claude Desktop's *Settings → Extensions* pane and Claude will auto-discover tiletopia — no config editing, no copy-pasting tokens. + +The bundle ships a tiny wrapper that reads your per-install bearer token straight from `%APPDATA%\com.megaproxy.tiletopia\mcp.json` at launch, so: + +- It carries **no secrets** — the same file works for every tiletopia install. +- **Token regeneration** in the panel keeps working transparently; the next time Claude Desktop launches the extension, it'll pick up the new token. +- Requires `npx` (Node 18+) on PATH because the wrapper still talks to tiletopia through `mcp-remote` (same reason as the manual recipe below). + +You can also rebuild the bundle from source: + +```sh +pnpm run build:mcpb # writes dist-mcpb/tiletopia.mcpb +``` + +#### Claude Code setup (via `mcp-remote` stdio shim — fallback / manual recipe) + +Claude Code (the terminal CLI) doesn't accept `.mcpb` bundles yet, and its HTTP-MCP client currently tries OAuth discovery and ignores static `headers` auth (Anthropic [#17152](https://github.com/anthropics/claude-code/issues/17152), [#46879](https://github.com/anthropics/claude-code/issues/46879)). The [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) stdio shim transparently proxies the HTTP endpoint with the bearer header attached, sidestepping the OAuth flow. The panel's config snippet uses this shim by default — paste it into your project's `.mcp.json`: diff --git a/memory.md b/memory.md index 38c65eb..9f9823c 100644 --- a/memory.md +++ b/memory.md @@ -106,6 +106,41 @@ The keyboard-shortcut table in README and the in-app help overlay used to be han **Verified:** ran twice, second run reports "already up to date" with empty `git diff`. `pnpm check` clean (tsc --noEmit, exit 0). +### 2026-05-26 — `.mcpb` Claude Desktop bundle (zero-config token handling) + +Long-standing follow-up shipped. Build script + tiny Node wrapper produce `dist-mcpb/tiletopia.mcpb` — a one-click Claude Desktop install replacing the hand-paste of `.mcp.json`. + +**Key design choice — per-install token handling.** The `.mcpb` spec offers two ways to handle credentials: `user_config` prompts at install time (copy-paste), or bake them in (wrong). Both lose: copy-paste defeats the whole point of one-click, and token rotation (the Regenerate button) would silently invalidate any saved `user_config` value. Picked a **third option not in the spec docs**: bundle a tiny Node wrapper as `entry_point` that reads `%APPDATA%\com.megaproxy.tiletopia\mcp.json` at launch and execs `npx -y mcp-remote ...` with the live token. Zero secrets in the bundle → safe to publish on the releases page; works for any tiletopia install; transparently picks up the new token after Regenerate without the user re-doing anything. + +**Bundle shape (`scripts/build-mcpb.mjs`):** +- `manifest.json` — `type: "node"`, `entry_point: "server/index.mjs"`, `mcp_config: { command: "node", args: ["${__dirname}/server/index.mjs"] }`, version mirrors `package.json`, icon points at the 128×128 brand PNG. +- `server/index.mjs` — the wrapper. Reads `mcp.json`, validates port + token, spawns `npx -y mcp-remote http://127.0.0.1:/mcp --allow-http --header "Authorization: Bearer "` with `stdio: "inherit"`, forwards SIGINT/SIGTERM/SIGHUP to clean up the child on extension disable. +- `icon.png` — copy of `src-tauri/icons/128x128.png`. + +**Build path.** `pnpm run build:mcpb` → `dist-mcpb/tiletopia.mcpb` (gitignored). Pure-Node store-only ZIP writer (~70 lines, no `archiver`/`jszip` devDep). Validated end-to-end with Python `zipfile`: 3 entries, valid CRCs, manifest parses. ~9 KB output. + +**Distribution.** The script is committed; the artifact isn't (regenerable). The intent is to attach `tiletopia.mcpb` to each Forgejo release alongside the NSIS installer — `scripts/release.sh` doesn't do this yet (follow-up). The new "Download .mcpb" button in `McpPanel` opens the releases page; once the artifact is up there, users grab it from there. + +**UI changes.** +- `McpPanel.tsx`: new "Claude Desktop (one-click install)" field above the .mcp.json snippet with a "Download .mcpb" button (opens the releases URL via `plugin-opener`) and a brief hint explaining zero-config token handling + the regen script. Styled in `McpPanel.css` (`.mcp-mcpb-row`, `.mcp-mcpb-btn`, `.mcp-mcpb-hint`). +- `McpPanel.css`: also added an explicit `.mcp-hint` style that was previously inheriting (used by both the token hint and the .mcpb hint). +- `shortcuts.ts`: MCP tip now leads with the `.mcpb` install path; the mcp-remote shim is described as the fallback for Claude Code (the terminal CLI, which doesn't accept `.mcpb` yet). +- `README.md`: same restructure under the MCP section — Claude Desktop install via `.mcpb` first, Claude Code via mcp-remote second. + +**Why no in-app file save dialog?** I considered bundling the `.mcpb` inside the Tauri NSIS as a Rust resource + exposing a `download_mcpb` Tauri command that opens a save dialog. Would let the panel button work fully in-app. Rejected because (a) it'd require Rust changes which I can't compile-check in WSL, (b) it duplicates what releases do for free, and (c) "Download .mcpb" landing on the releases page is the more discoverable distribution flow long-term. + +**Confirmed: bundle contains zero secrets.** Scanned both `manifest.json` and `server/index.mjs` for `Bearer ey`, `token=`, `secret`, `password`, `api_key` — all clean. The wrapper reads the token from `%APPDATA%` at runtime on the *user's* machine; nothing is ever baked in. + +**`pnpm check` clean, vitest 72/72 passing.** + +Open follow-ups specific to this session: + +- **Wire `.mcpb` into the release.** `scripts/release.sh` currently uploads only the NSIS installer; it should also run `node scripts/build-mcpb.mjs` and attach the resulting `dist-mcpb/tiletopia.mcpb` to the Forgejo release. Two lines + one `tea releases create --asset` flag. Until that's done, the "Download .mcpb" button lands on a releases page where the asset doesn't exist yet for old tags. +- **Direct in-app save flow.** If we ever want fully-offline install (no roundtrip through the web), add a Rust-side `download_mcpb` command that returns the bundled bytes + use `@tauri-apps/plugin-dialog` save() in the panel. Not blocking — current flow is sufficient and matches how Tauri apps usually distribute extension files. +- **Pre-flight on the wrapper.** Could detect missing `npx` / Node 18+ and emit a more directed message. Currently we just let `spawn` fail with whatever Node says. The "make sure Node 18+ is installed and `npx` is on PATH" line in the error path is the band-aid. +- **`.mcpb` for Claude Code (CLI).** Claude Code doesn't accept `.mcpb` bundles yet — Anthropic may add it. When they do, the same bundle should Just Work since the wrapper is platform-agnostic re: which Claude is calling it. +- **Bundle compatibility field.** Manifest declares `platforms: ["win32"]` and `runtimes: { node: ">=18.0.0" }`. The wrapper has a hard `%APPDATA%` requirement so this is correct, but if anyone ever wants macOS / Linux tiletopia support, the wrapper needs a portable config-path lookup. + ### 2026-05-26 — Hard-deny: PowerShell patterns + label list de-duplicated Mirrors the POSIX hard-deny rules with their Windows/PowerShell equivalents. Four new patterns: diff --git a/package.json b/package.json index 5b72388..9a5f043 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "test": "vitest run", "test:watch": "vitest", "check": "tsc --noEmit", + "build:mcpb": "node scripts/build-mcpb.mjs", "gen:readme": "node scripts/gen-readme-shortcuts.mjs", "tauri": "tauri" }, diff --git a/scripts/build-mcpb.mjs b/scripts/build-mcpb.mjs new file mode 100644 index 0000000..0f59462 --- /dev/null +++ b/scripts/build-mcpb.mjs @@ -0,0 +1,259 @@ +#!/usr/bin/env node +// build-mcpb.mjs — package tiletopia's Claude Desktop MCP bundle. +// +// Produces dist-mcpb/tiletopia.mcpb — an .mcpb (MCP Bundle, the format +// formerly known as DXT) zip containing: +// manifest.json → declares a node-type server pointing at the wrapper +// server/index.mjs → the wrapper script that reads %APPDATA% and +// execs `npx -y mcp-remote ...` (see mcpb-wrapper.mjs) +// icon.png → 128×128 brand icon +// +// Usage: +// pnpm run build:mcpb +// or +// node scripts/build-mcpb.mjs +// +// Output: +// dist-mcpb/tiletopia.mcpb — drag-and-drop this into Claude Desktop's +// Extensions panel to install. +// +// Design notes: +// - The bundle bakes in NO secrets. The bearer token + port are read at +// runtime from %APPDATA%\com.megaproxy.tiletopia\mcp.json on the user's +// own machine. Each install of tiletopia generates its own token; the +// bundle is the same for everyone. +// - We write the zip ourselves (store-only, no compression) to avoid a +// devDep on archiver/jszip/etc. The MCPB spec is just a regular zip; +// three small files = trivial. +// - The manifest's `version` mirrors package.json so the panel UI can show +// "Bundle v0.2.3 — matches running app". + +import { readFile, writeFile, mkdir, stat } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { deflateRawSync, crc32 } from "node:zlib"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = resolve(HERE, ".."); +const PKG_PATH = join(REPO_ROOT, "package.json"); +const WRAPPER_PATH = join(HERE, "mcpb-wrapper.mjs"); +const ICON_PATH = join(REPO_ROOT, "src-tauri", "icons", "128x128.png"); +const OUT_DIR = join(REPO_ROOT, "dist-mcpb"); +const OUT_PATH = join(OUT_DIR, "tiletopia.mcpb"); + +// ---------------------------------------------------------------------------- +// Read inputs +// ---------------------------------------------------------------------------- + +if (!existsSync(WRAPPER_PATH)) { + console.error(`missing wrapper: ${WRAPPER_PATH}`); + process.exit(1); +} +if (!existsSync(ICON_PATH)) { + console.error(`missing icon: ${ICON_PATH}`); + process.exit(1); +} + +const pkg = JSON.parse(await readFile(PKG_PATH, "utf8")); +const wrapperSrc = await readFile(WRAPPER_PATH); +const iconBytes = await readFile(ICON_PATH); + +// ---------------------------------------------------------------------------- +// Manifest +// +// Schema reference: https://github.com/modelcontextprotocol/mcpb/blob/main/MANIFEST.md +// +// type=node + entry_point pointing at server/index.mjs + mcp_config.command +// = "node" matches Claude Desktop's expectations. We avoid a `user_config` +// block on purpose — the wrapper reads the token from %APPDATA% so the user +// doesn't have to copy-paste it at install time. +// ---------------------------------------------------------------------------- + +const manifest = { + manifest_version: "0.3", + name: "tiletopia", + display_name: "tiletopia (workspace driver)", + version: pkg.version, + description: + "Drive your tiletopia workspace from Claude Desktop — inspect panes, " + + "read scrollback, reshape the layout, and (with policy approval) send " + + "commands.", + long_description: + "tiletopia is a Windows tiling terminal manager for WSL. This bundle " + + "lets Claude Desktop connect to a running tiletopia process on the same " + + "machine via its embedded MCP server. The bundle reads the per-install " + + "bearer token and port from %APPDATA%\\com.megaproxy.tiletopia\\mcp.json " + + "at launch, so you don't need to paste any credentials during install. " + + "Start the MCP server once from tiletopia's 🤖 panel (Server: ON), then " + + "drop this bundle into Claude Desktop and it will connect automatically. " + + "All write operations (spawn, write keystrokes, reshape) are gated by " + + "the per-pane allow-list and the user-editable policy inside tiletopia.", + author: { + name: "megaproxy", + url: "https://git.rdx4.com/megaproxy/tiletopia", + }, + repository: { + type: "git", + url: "https://git.rdx4.com/megaproxy/tiletopia.git", + }, + homepage: "https://git.rdx4.com/megaproxy/tiletopia", + documentation: "https://git.rdx4.com/megaproxy/tiletopia#mcp-server-claude-can-drive-the-workspace", + support: "https://git.rdx4.com/megaproxy/tiletopia/issues", + icon: "icon.png", + server: { + type: "node", + entry_point: "server/index.mjs", + mcp_config: { + command: "node", + args: ["${__dirname}/server/index.mjs"], + }, + }, + keywords: ["tiletopia", "wsl", "terminal", "mcp", "claude"], + license: "Proprietary", + compatibility: { + // Claude Desktop runtime requirements — the bundle launches node, which + // shells out to npx mcp-remote; both need Node 18+ on PATH. + platforms: ["win32"], + runtimes: { + node: ">=18.0.0", + }, + }, +}; + +const manifestBytes = Buffer.from(JSON.stringify(manifest, null, 2), "utf8"); + +// ---------------------------------------------------------------------------- +// Build the .mcpb zip +// +// The MCPB spec is a plain ZIP file. We're writing three small files, so a +// pure-Node store-only writer is simplest. Avoids adding archiver as a +// devDep. Format reference: APPNOTE.TXT 6.3.4 sections 4.3 (local file +// header), 4.4 (data descriptor), 4.5 (central directory). +// ---------------------------------------------------------------------------- + +const SIG_LFH = 0x04034b50; +const SIG_CDH = 0x02014b50; +const SIG_EOCD = 0x06054b50; + +function dosTimeDate(date) { + // DOS time/date format (2-second resolution; epoch 1980-01-01). + const yr = Math.max(date.getFullYear(), 1980) - 1980; + const time = + ((date.getHours() & 0x1f) << 11) | + ((date.getMinutes() & 0x3f) << 5) | + ((Math.floor(date.getSeconds() / 2)) & 0x1f); + const dt = + ((yr & 0x7f) << 9) | + (((date.getMonth() + 1) & 0x0f) << 5) | + (date.getDate() & 0x1f); + return { time, date: dt }; +} + +function buildZip(entries) { + const now = dosTimeDate(new Date()); + const chunks = []; + const centralDir = []; + let offset = 0; + + for (const { name, data } of entries) { + const nameBuf = Buffer.from(name, "utf8"); + const crc = crc32(data); // store-only — uncompressed crc == compressed crc + const size = data.length; + + // Local file header (4.3.7) + const lfh = Buffer.alloc(30); + lfh.writeUInt32LE(SIG_LFH, 0); + lfh.writeUInt16LE(20, 4); // version needed (2.0) + lfh.writeUInt16LE(0, 6); // general purpose bit flag + lfh.writeUInt16LE(0, 8); // compression: store + lfh.writeUInt16LE(now.time, 10); + lfh.writeUInt16LE(now.date, 12); + lfh.writeUInt32LE(crc, 14); + lfh.writeUInt32LE(size, 18); // compressed size (== size for store) + lfh.writeUInt32LE(size, 22); // uncompressed size + lfh.writeUInt16LE(nameBuf.length, 26); + lfh.writeUInt16LE(0, 28); // extra field length + chunks.push(lfh, nameBuf, data); + + // Central directory header (4.4.7) + const cdh = Buffer.alloc(46); + cdh.writeUInt32LE(SIG_CDH, 0); + cdh.writeUInt16LE(20, 4); // version made by + cdh.writeUInt16LE(20, 6); // version needed + cdh.writeUInt16LE(0, 8); // gp flag + cdh.writeUInt16LE(0, 10); // compression + cdh.writeUInt16LE(now.time, 12); + cdh.writeUInt16LE(now.date, 14); + cdh.writeUInt32LE(crc, 16); + cdh.writeUInt32LE(size, 20); // compressed size + cdh.writeUInt32LE(size, 24); // uncompressed + cdh.writeUInt16LE(nameBuf.length, 28); + cdh.writeUInt16LE(0, 30); // extra len + cdh.writeUInt16LE(0, 32); // comment len + cdh.writeUInt16LE(0, 34); // disk number + cdh.writeUInt16LE(0, 36); // internal attrs + cdh.writeUInt32LE(0, 38); // external attrs + cdh.writeUInt32LE(offset, 42); // local header offset + centralDir.push(cdh, nameBuf); + + offset += lfh.length + nameBuf.length + data.length; + } + + const cdStart = offset; + for (const buf of centralDir) { + chunks.push(buf); + offset += buf.length; + } + const cdSize = offset - cdStart; + + // End of central directory record (4.5) + const eocd = Buffer.alloc(22); + eocd.writeUInt32LE(SIG_EOCD, 0); + eocd.writeUInt16LE(0, 4); // disk number + eocd.writeUInt16LE(0, 6); // start disk + eocd.writeUInt16LE(entries.length, 8); // entries on this disk + eocd.writeUInt16LE(entries.length, 10); // total entries + eocd.writeUInt32LE(cdSize, 12); + eocd.writeUInt32LE(cdStart, 16); + eocd.writeUInt16LE(0, 20); // comment length + chunks.push(eocd); + + // Silence the "unused on store path" lint trip; deflateRawSync stays + // imported so a future maintainer who wants to add compression doesn't + // have to re-figure out the right symbol. + void deflateRawSync; + + return Buffer.concat(chunks); +} + +const entries = [ + { name: "manifest.json", data: manifestBytes }, + { name: "server/index.mjs", data: wrapperSrc }, + { name: "icon.png", data: iconBytes }, +]; + +const zipBytes = buildZip(entries); + +await mkdir(OUT_DIR, { recursive: true }); +await writeFile(OUT_PATH, zipBytes); + +const sizeKB = (zipBytes.length / 1024).toFixed(1); +console.log(`wrote ${OUT_PATH} (${sizeKB} KB, ${entries.length} entries)`); +for (const e of entries) { + console.log(` ${e.name.padEnd(20)} ${e.data.length} bytes`); +} +console.log( + `manifest version ${manifest.version} (mirrors package.json); ` + + "to install, drag the .mcpb file into Claude Desktop's Extensions panel.", +); + +// Touch stat() so any "wrote nothing" CI bug surfaces here, not at the user's +// next install. +const written = await stat(OUT_PATH); +if (written.size !== zipBytes.length) { + console.error( + `size mismatch: wrote ${zipBytes.length} bytes, file is ${written.size}`, + ); + process.exit(1); +} diff --git a/scripts/mcpb-wrapper.mjs b/scripts/mcpb-wrapper.mjs new file mode 100644 index 0000000..be86ae8 --- /dev/null +++ b/scripts/mcpb-wrapper.mjs @@ -0,0 +1,110 @@ +#!/usr/bin/env node +// tiletopia .mcpb wrapper — entry_point for the bundled MCP server. +// +// What this is: a thin stdio shim Claude Desktop launches when the user +// installs `tiletopia.mcpb`. It reads the per-install MCP server settings +// (port + bearer token) that the running tiletopia app persisted to +// %APPDATA%\com.megaproxy.tiletopia\mcp.json, then execs `npx -y mcp-remote` +// with the right URL + Authorization header. Claude talks stdio to us; we +// proxy through mcp-remote, which talks HTTP to the tiletopia process. +// +// Why a wrapper (not just static args in the manifest): +// - The bearer token is per-install — generated at first server start, also +// rotated whenever the user clicks "Regenerate" in the MCP panel. We +// can't bake it into the bundle (that'd be wrong for every other user) +// and we don't want to make the user paste it into a user_config prompt +// at install time. Reading it from %APPDATA% at launch makes the whole +// thing zero-config and survives token rotation transparently. +// - The port may also drift (if the saved port is taken, tiletopia falls +// back to an OS-picked one and re-persists). Reading at launch keeps us +// correct across that too. +// +// Failure modes & messages: every error we emit goes to stderr so the user +// sees it in Claude Desktop's extension log. We deliberately do NOT swallow +// or transform mcp-remote's own output beyond piping it. + +import { spawn } from "node:child_process"; +import { readFileSync, existsSync } from "node:fs"; +import { join } from "node:path"; + +const APPDATA = process.env.APPDATA; +if (!APPDATA) { + console.error( + "[tiletopia-mcpb] %APPDATA% is unset — this bundle only runs on Windows.", + ); + process.exit(2); +} + +const CFG_PATH = join(APPDATA, "com.megaproxy.tiletopia", "mcp.json"); +if (!existsSync(CFG_PATH)) { + console.error( + `[tiletopia-mcpb] config not found at ${CFG_PATH}. ` + + "Launch tiletopia, open the 🤖 MCP panel, and click Server: ON at least " + + "once so the port + token get persisted, then retry.", + ); + process.exit(3); +} + +let cfg; +try { + cfg = JSON.parse(readFileSync(CFG_PATH, "utf8")); +} catch (e) { + console.error(`[tiletopia-mcpb] failed to read/parse ${CFG_PATH}: ${e.message}`); + process.exit(4); +} + +const port = Number(cfg.port); +const token = String(cfg.token ?? ""); +if (!Number.isInteger(port) || port <= 0 || port > 65535 || !token) { + console.error( + `[tiletopia-mcpb] ${CFG_PATH} is missing a valid port or token. ` + + "Toggle the MCP server off and on in the tiletopia panel to regenerate it.", + ); + process.exit(5); +} + +const url = `http://127.0.0.1:${port}/mcp`; + +// `npx.cmd` on Windows is the actual launcher; bare `npx` is a shim that +// node spawns from PATH and that's also fine. spawn() with shell:true ensures +// PATHEXT resolution picks up the .cmd correctly. +const child = spawn( + "npx", + [ + "-y", + "mcp-remote", + url, + "--allow-http", + "--header", + `Authorization: Bearer ${token}`, + ], + { + stdio: "inherit", + shell: true, + }, +); + +child.on("error", (e) => { + console.error( + `[tiletopia-mcpb] failed to spawn npx: ${e.message}. ` + + "Make sure Node.js 18+ is installed and `npx` is on PATH.", + ); + process.exit(6); +}); + +child.on("exit", (code, signal) => { + if (signal) process.kill(process.pid, signal); + else process.exit(code ?? 0); +}); + +// Forward terminate signals to the child so Claude Desktop's "disable +// extension" cleans up the mcp-remote subprocess. +for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) { + process.on(sig, () => { + try { + child.kill(sig); + } catch { + /* child may already be gone */ + } + }); +} diff --git a/src/components/McpPanel.css b/src/components/McpPanel.css index 4641d2f..28b2830 100644 --- a/src/components/McpPanel.css +++ b/src/components/McpPanel.css @@ -187,6 +187,55 @@ color: #ccd; } +/* Inline small-print under inputs — small, muted, tight line-height. Used by + * the token hint and the .mcpb install hint. */ +.mcp-hint { + margin: 4px 0 0; + color: #888; + font-size: 11px; + line-height: 1.4; +} +.mcp-hint code { + background: #0c0c0c; + padding: 1px 4px; + border-radius: 2px; + font-family: inherit; + color: #aac; +} + +/* ---- Claude Desktop .mcpb install row ----------------------------------- */ + +.mcp-mcpb-row { + display: flex; + align-items: flex-start; + gap: 12px; +} + +.mcp-mcpb-btn { + font: inherit; + font-family: inherit; + font-size: 11px; + font-weight: 600; + background: #1a2a3a; + color: #cce6ff; + border: 1px solid #2a4a6a; + border-radius: 3px; + padding: 6px 14px; + cursor: pointer; + flex-shrink: 0; + white-space: nowrap; +} +.mcp-mcpb-btn:hover { + background: #2a4a6a; + color: #e0f0ff; + border-color: #4488cc; +} + +.mcp-mcpb-hint { + flex: 1 1 auto; + margin: 0; +} + .mcp-snippet { font: inherit; font-family: inherit; diff --git a/src/components/McpPanel.tsx b/src/components/McpPanel.tsx index d00592f..89a6ea5 100644 --- a/src/components/McpPanel.tsx +++ b/src/components/McpPanel.tsx @@ -2,12 +2,18 @@ import { useEffect, useState, useCallback } from "react"; import { writeText as clipboardWriteText, } from "@tauri-apps/plugin-clipboard-manager"; +import { openUrl } from "@tauri-apps/plugin-opener"; import type { McpStatus, McpAuditEntry } from "../ipc"; import AuditTab from "./AuditTab"; import PolicyTab from "./PolicyTab"; import ErrorBoundary from "./ErrorBoundary"; import "./McpPanel.css"; +// URL of the GitHub-style releases page where each tagged build attaches the +// prebuilt `.mcpb` bundle (sibling to the NSIS installer). Source bundle is +// regeneratable via `pnpm run build:mcpb`. +const MCPB_RELEASES_URL = "https://git.rdx4.com/megaproxy/tiletopia/releases"; + interface McpPanelProps { status: McpStatus; onStart: () => Promise; @@ -196,6 +202,31 @@ export default function McpPanel({

+
+ +
+ +

+ Grab tiletopia.mcpb from the releases + page, then drag it into Claude Desktop's{" "} + Settings → Extensions. The bundle reads your + bearer token from %APPDATA% at launch — + zero copy-paste, and token regeneration above keeps + working transparently. (Bundle is regeneratable from + source via pnpm run build:mcpb.) +

+
+
+
diff --git a/src/lib/shortcuts.ts b/src/lib/shortcuts.ts
index f9db405..b3692d1 100644
--- a/src/lib/shortcuts.ts
+++ b/src/lib/shortcuts.ts
@@ -110,6 +110,6 @@ export const TIPS: TipSpec[] = [
   },
   {
     title: "MCP server (let Claude drive the workspace)",
-    body: "Titlebar 🤖 opens the MCP control panel — start the server and paste the snippet into your Claude Code .mcp.json. The snippet uses npx mcp-remote as a stdio shim because Claude Code's HTTP-MCP client ignores static bearer auth and tries OAuth instead; the shim proxies the HTTP endpoint with the bearer baked in. URL + token persist across restarts; Regenerate the token in the panel if it leaks. Default-deny per pane: toggle 🤖 on each pane's toolbar to expose it to MCP. Read-only in v1 (no spawn or write yet).",
+    body: "Titlebar 🤖 opens the MCP control panel. Start the server, then for Claude Desktop click 'Download .mcpb' and drag the file into Settings → Extensions — zero-config because the bundle reads your bearer token from %APPDATA% at launch (no copy-paste, survives token rotation). For Claude Code (terminal CLI) use the fallback snippet in the panel: it wires npx mcp-remote as a stdio shim because Claude Code's HTTP-MCP client ignores static bearer auth and tries OAuth instead. URL + token persist across restarts; Regenerate the token in the panel if it leaks. Default-deny per pane: toggle 🤖 on each pane's toolbar to expose it to MCP.",
   },
 ];