New scripts/build-mcpb.mjs packs a Claude Desktop extension bundle (scripts/mcpb-wrapper.mjs + manifest + icon) into dist-mcpb/tiletopia.mcpb. The wrapper reads the bearer token from %APPDATA% at launch and execs `npx -y mcp-remote`, so no secrets are baked in and Regenerate keeps working transparently. Run via `pnpm run build:mcpb`. McpPanel gets a "Download .mcpb" button linking to the releases page; the help-overlay tip and README MCP section both lead with the bundle install path and keep the .mcp.json shim recipe as the Claude Code fallback. Session-log entry in memory.md covers the design choices, especially why the wrapper-script approach beat the alternatives (user_config prompt would defeat one-click; baked-in token would be wrong for everyone else).
110 lines
3.6 KiB
JavaScript
110 lines
3.6 KiB
JavaScript
#!/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 */
|
|
}
|
|
});
|
|
}
|