@discord-mcp/cli
Advanced tools
| import { t as version } from "./package-Cmu8WQp-.js"; | ||
| import { a as profileExists, c as saveProfile, i as normalizeProfileName } from "./profiles-BOWWDiOo.js"; | ||
| import { t as emitResult } from "./output-BGKgg-RQ.js"; | ||
| import { i as isInteractive, n as askChoice, r as askYesNo } from "./prompt-B6jM7zuq.js"; | ||
| import { existsSync, writeFileSync } from "node:fs"; | ||
| import { dirname, resolve } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| //#region src/lib/client-snippets/_shared.ts | ||
| /** | ||
| * Build the `{ command, args, env }` payload for a single MCP server | ||
| * entry. Args are merged: caller-provided `serverArgs` first, then | ||
| * `--gateway` appended when `cfg.gateway === true`. Env includes | ||
| * `DISCORD_TOKEN` when supplied and merges any extra `cfg.envVars`. | ||
| */ | ||
| function renderServerEntry(cfg) { | ||
| const args = [...cfg.serverArgs ?? []]; | ||
| if (cfg.gateway === true) args.push("--gateway"); | ||
| const env = { | ||
| ...cfg.discordToken === void 0 ? {} : { DISCORD_TOKEN: cfg.discordToken }, | ||
| ...cfg.envVars ?? {} | ||
| }; | ||
| return { | ||
| command: cfg.serverPath, | ||
| args, | ||
| ...Object.keys(env).length === 0 ? {} : { env } | ||
| }; | ||
| } | ||
| /** | ||
| * Render the full top-level JSON document with a single `discord-mcp` | ||
| * server registered under `mcpServers`. | ||
| * | ||
| * The output is pretty-printed with 2-space indent (matches Anthropic's | ||
| * sample configs) and trailing newline. | ||
| */ | ||
| function renderMcpServersJson(cfg) { | ||
| const doc = { mcpServers: { "discord-mcp": renderServerEntry(cfg) } }; | ||
| return `${JSON.stringify(doc, null, 2)}\n`; | ||
| } | ||
| //#endregion | ||
| //#region src/lib/client-snippets/claude-code.ts | ||
| /** | ||
| * Claude Code (Anthropic CLI) MCP server snippet generator. | ||
| * | ||
| * Claude Code uses the same `mcpServers` JSON schema as Claude Desktop | ||
| * but stores it in different locations and exposes a `claude mcp add` | ||
| * subcommand for managed configuration. We emit the JSON snippet for | ||
| * users who prefer manual editing and document both options. | ||
| * | ||
| * Path order matches Anthropic CLI's lookup: project-local takes | ||
| * priority over user-level. We document the user-level path here since | ||
| * `init` is typically run once per machine. | ||
| */ | ||
| const CONFIG_PATH$4 = [ | ||
| "User-level (preferred): ~/.claude.json", | ||
| "Project-level: <project>/.mcp.json", | ||
| "Modern CLI form: claude mcp add discord-mcp -- <command> [args...]" | ||
| ].join("\n"); | ||
| const INSTRUCTIONS$4 = "Easiest: `claude mcp add discord-mcp -- <command> [args...]`. Manual: merge into the `mcpServers` object in ~/.claude.json (or the project-level .mcp.json)."; | ||
| const claudeCodeGenerator = { | ||
| id: "claude-code", | ||
| displayName: "Claude Code", | ||
| generate(cfg) { | ||
| return { | ||
| format: "json", | ||
| content: renderMcpServersJson(cfg), | ||
| configFilePath: CONFIG_PATH$4, | ||
| instructions: INSTRUCTIONS$4 | ||
| }; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/lib/client-snippets/claude-desktop.ts | ||
| /** | ||
| * Claude Desktop MCP server snippet generator. | ||
| * | ||
| * Claude Desktop reads `mcpServers` from a JSON file. The path differs | ||
| * by OS; we document all three so users on any platform can find it. | ||
| * | ||
| * Restart Claude Desktop after editing - the file is read once at app | ||
| * startup and not watched. | ||
| */ | ||
| const CONFIG_PATH$3 = [ | ||
| "macOS: ~/Library/Application Support/Claude/claude_desktop_config.json", | ||
| "Windows: %APPDATA%\\Claude\\claude_desktop_config.json", | ||
| "Linux: ~/.config/Claude/claude_desktop_config.json" | ||
| ].join("\n"); | ||
| const INSTRUCTIONS$3 = "Merge into your existing `mcpServers` object in claude_desktop_config.json (paths above), then fully restart Claude Desktop."; | ||
| const claudeDesktopGenerator = { | ||
| id: "claude-desktop", | ||
| displayName: "Claude Desktop", | ||
| generate(cfg) { | ||
| return { | ||
| format: "json", | ||
| content: renderMcpServersJson(cfg), | ||
| configFilePath: CONFIG_PATH$3, | ||
| instructions: INSTRUCTIONS$3 | ||
| }; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/lib/client-snippets/codex.ts | ||
| const CONFIG_PATH$2 = "User-level: ~/.codex/config.toml"; | ||
| const INSTRUCTIONS$2 = "Merge this TOML fragment into ~/.codex/config.toml. Set DISCORD_TOKEN in the environment before starting Codex; the default fragment forwards it without storing the token in config.toml."; | ||
| const TOKEN_PLACEHOLDER$1 = "${env:DISCORD_TOKEN}"; | ||
| function tomlString(value) { | ||
| return JSON.stringify(value); | ||
| } | ||
| function renderTomlStringArray(values) { | ||
| return `[${values.map(tomlString).join(", ")}]`; | ||
| } | ||
| function renderCodexToml(cfg) { | ||
| const args = [...cfg.serverArgs ?? []]; | ||
| if (cfg.gateway === true) args.push("--gateway"); | ||
| const lines = [ | ||
| "[mcp_servers.discord-mcp]", | ||
| `command = ${tomlString(cfg.serverPath)}`, | ||
| `args = ${renderTomlStringArray(args)}` | ||
| ]; | ||
| if (cfg.serverPath === "npx") lines.push("startup_timeout_sec = 90"); | ||
| if (cfg.discordToken === void 0 || cfg.discordToken === TOKEN_PLACEHOLDER$1) lines.push("env_vars = [\"DISCORD_TOKEN\"]"); | ||
| const env = { | ||
| ...cfg.discordToken === void 0 || cfg.discordToken === TOKEN_PLACEHOLDER$1 ? {} : { DISCORD_TOKEN: cfg.discordToken }, | ||
| ...cfg.envVars ?? {} | ||
| }; | ||
| if (Object.keys(env).length > 0) { | ||
| lines.push("", "[mcp_servers.discord-mcp.env]"); | ||
| for (const [key, value] of Object.entries(env)) lines.push(`${key} = ${tomlString(value)}`); | ||
| } | ||
| return `${lines.join("\n")}\n`; | ||
| } | ||
| const codexGenerator = { | ||
| id: "codex", | ||
| displayName: "Codex", | ||
| generate(cfg) { | ||
| return { | ||
| format: "toml", | ||
| content: renderCodexToml(cfg), | ||
| configFilePath: CONFIG_PATH$2, | ||
| instructions: INSTRUCTIONS$2 | ||
| }; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/lib/client-snippets/cursor.ts | ||
| /** | ||
| * Cursor MCP server snippet generator. | ||
| * | ||
| * Cursor adopted the standard `mcpServers` schema. Two scopes are | ||
| * supported: global (`~/.cursor/mcp.json`) and per-project | ||
| * (`<project>/.cursor/mcp.json`). The schema is identical, only the | ||
| * file location differs. Restart Cursor after editing. | ||
| */ | ||
| const CONFIG_PATH$1 = ["Global: ~/.cursor/mcp.json", "Per-project: <project>/.cursor/mcp.json"].join("\n"); | ||
| const INSTRUCTIONS$1 = "Place under `~/.cursor/mcp.json` for global access, or `.cursor/mcp.json` in your project root for per-project. Restart Cursor for changes to take effect."; | ||
| const cursorGenerator = { | ||
| id: "cursor", | ||
| displayName: "Cursor", | ||
| generate(cfg) { | ||
| return { | ||
| format: "json", | ||
| content: renderMcpServersJson(cfg), | ||
| configFilePath: CONFIG_PATH$1, | ||
| instructions: INSTRUCTIONS$1 | ||
| }; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/lib/client-snippets/generic.ts | ||
| /** | ||
| * Generic MCP client snippet generator. | ||
| * | ||
| * Catch-all for clients that aren't first-class supported here but | ||
| * implement the standard MCP server config schema. We emit the same | ||
| * JSON shape with no client-specific path and direct the user to their | ||
| * client's docs for placement. | ||
| */ | ||
| const CONFIG_PATH = "(check your MCP client docs for the config file location)"; | ||
| const INSTRUCTIONS = "This is the standard MCP server config block. Place it under your client's `mcpServers` object as documented by the client."; | ||
| //#endregion | ||
| //#region src/lib/client-snippets/index.ts | ||
| /** | ||
| * Registry of all supported MCP client snippet generators - Plan 9 Phase D. | ||
| * | ||
| * Order is intentional: Claude Desktop first (most common entry point | ||
| * for new users), Claude Code second (Anthropic CLI), Codex third, | ||
| * Cursor fourth, Generic last (fallback). The numeric | ||
| * order also drives the default index in interactive `init` choice | ||
| * prompts. | ||
| * | ||
| * To add a new client: implement {@link ClientGenerator} in a new file | ||
| * under this directory, register the singleton here, and add a test. | ||
| * No other surface needs to change - `init` reads from this array. | ||
| */ | ||
| const ALL_GENERATORS = [ | ||
| claudeDesktopGenerator, | ||
| claudeCodeGenerator, | ||
| codexGenerator, | ||
| cursorGenerator, | ||
| { | ||
| id: "generic", | ||
| displayName: "Generic MCP client", | ||
| generate(cfg) { | ||
| return { | ||
| format: "json", | ||
| content: renderMcpServersJson(cfg), | ||
| configFilePath: CONFIG_PATH, | ||
| instructions: INSTRUCTIONS | ||
| }; | ||
| } | ||
| } | ||
| ]; | ||
| //#endregion | ||
| //#region src/commands/init.ts | ||
| /** | ||
| * `discord-mcp init` - Plan 9 Phase D. | ||
| * | ||
| * Replaces the Phase A placeholder. Bootstraps an MCP client config | ||
| * snippet that the user can paste into their client's config file (or | ||
| * have us write directly via `--output`). | ||
| * | ||
| * Flow: | ||
| * 1. Resolve which client (`--client <id>` OR interactive choice OR | ||
| * 'generic' as the silent default for non-interactive runs). | ||
| * 2. Resolve the Discord token (`--token` OR the | ||
| * `${env:DISCORD_TOKEN}` placeholder so users don't accidentally bake a | ||
| * real secret into a committed file). | ||
| * 3. Resolve the gateway flag (`--gateway` OR interactive yes/no OR | ||
| * false by default). | ||
| * 4. Validate the advertised tool surface (`full` by default, or the | ||
| * opt-in `progressive` search + risk-specific dispatcher surface). | ||
| * 5. Validate and normalize the optional server-side guild allowlist. With | ||
| * `--discover-guilds`, verify the current bot identity, enumerate its | ||
| * real guilds, and select or validate the allowlist before generation. | ||
| * 6. Pick a serverPath/serverArgs strategy. Stateless `init` uses the | ||
| * current Node binary + the resolved CLI script, which works for any | ||
| * installation at the cost of an absolute path. Guided profile setup | ||
| * instead emits a pinned `npx` package launcher so its client fragment | ||
| * does not depend on an installation or cache path. | ||
| * 7. Generate the snippet via the chosen ClientGenerator. | ||
| * 8. Either write to `--output <path>` (with `--force` for overwrite | ||
| * protection) or print to stdout / structured payload. | ||
| * | ||
| * Token redaction: in pretty mode the snippet text contains whatever | ||
| * `--token` was passed - including raw secrets. The CLI flag's help | ||
| * text warns about this. The placeholder default avoids the issue | ||
| * entirely. We do NOT echo the token in any other log line. | ||
| */ | ||
| const TOKEN_PLACEHOLDER = "${env:DISCORD_TOKEN}"; | ||
| const DISCORD_API_DEFAULT = "https://discord.com/api/v10"; | ||
| const DISCORD_REQUEST_TIMEOUT_MS = 5e3; | ||
| const SNOWFLAKE = /^\d{17,20}$/; | ||
| const ADMINISTRATOR_PERMISSION = 8n; | ||
| function safeDisplay(value) { | ||
| return value.replace(/[\p{Cc}\p{Cf}]/gu, " ").trim(); | ||
| } | ||
| function discordAuthHeader(token) { | ||
| return token.startsWith("Bot ") ? token : `Bot ${token}`; | ||
| } | ||
| async function discordGet(path, token) { | ||
| const baseUrl = process.env.DISCORD_API_BASE_URL ?? DISCORD_API_DEFAULT; | ||
| const response = await fetch(`${baseUrl}${path}`, { | ||
| method: "GET", | ||
| headers: { | ||
| Authorization: discordAuthHeader(token), | ||
| "User-Agent": "discord-mcp-init (https://github.com/cappyeo/discord-mcp)" | ||
| }, | ||
| signal: AbortSignal.timeout(DISCORD_REQUEST_TIMEOUT_MS) | ||
| }); | ||
| if (!response.ok) { | ||
| if (response.status === 401) throw new Error("Discord rejected DISCORD_TOKEN (401)"); | ||
| if (response.status === 403) throw new Error("Discord denied access for this bot (403)"); | ||
| if (response.status === 429) throw new Error("Discord rate-limited guild discovery (429)"); | ||
| throw new Error(`Discord returned HTTP ${response.status}`); | ||
| } | ||
| try { | ||
| return await response.json(); | ||
| } catch { | ||
| throw new Error("Discord returned an invalid JSON response"); | ||
| } | ||
| } | ||
| function parseGuild(raw) { | ||
| if (raw === null || typeof raw !== "object") throw new Error("Discord returned an invalid guild entry"); | ||
| const guild = raw; | ||
| if (typeof guild.id !== "string" || !SNOWFLAKE.test(guild.id)) throw new Error("Discord returned a guild with an invalid id"); | ||
| if (typeof guild.name !== "string" || typeof guild.permissions !== "string") throw new Error(`Discord returned incomplete metadata for guild ${guild.id}`); | ||
| let permissions; | ||
| try { | ||
| permissions = BigInt(guild.permissions); | ||
| } catch { | ||
| throw new Error(`Discord returned invalid permissions for guild ${guild.id}`); | ||
| } | ||
| return { | ||
| id: guild.id, | ||
| name: safeDisplay(guild.name) || "(unnamed guild)", | ||
| administrator: (permissions & ADMINISTRATOR_PERMISSION) === ADMINISTRATOR_PERMISSION | ||
| }; | ||
| } | ||
| async function discoverDiscordSetup(token) { | ||
| const rawUser = await discordGet("/users/@me", token); | ||
| if (rawUser === null || typeof rawUser !== "object") throw new Error("Discord returned an invalid bot identity"); | ||
| const user = rawUser; | ||
| if (typeof user.id !== "string" || !SNOWFLAKE.test(user.id) || typeof user.username !== "string" || user.bot !== true) throw new Error("DISCORD_TOKEN must identify a Discord bot account"); | ||
| const guilds = []; | ||
| const seen = /* @__PURE__ */ new Set(); | ||
| let after; | ||
| for (;;) { | ||
| const query = new URLSearchParams({ | ||
| limit: "200", | ||
| with_counts: "false" | ||
| }); | ||
| if (after !== void 0) query.set("after", after); | ||
| const rawPage = await discordGet(`/users/@me/guilds?${query.toString()}`, token); | ||
| if (!Array.isArray(rawPage)) throw new Error("Discord returned an invalid guild list"); | ||
| const page = rawPage.map(parseGuild); | ||
| for (const guild of page) { | ||
| if (seen.has(guild.id)) throw new Error("Discord returned a duplicate guild page"); | ||
| seen.add(guild.id); | ||
| guilds.push(guild); | ||
| } | ||
| if (page.length < 200) break; | ||
| after = page.at(-1)?.id; | ||
| if (after === void 0) throw new Error("Discord guild pagination did not advance"); | ||
| } | ||
| return { | ||
| bot: { | ||
| id: user.id, | ||
| username: safeDisplay(user.username) || "(unnamed bot)" | ||
| }, | ||
| guilds | ||
| }; | ||
| } | ||
| /** | ||
| * Resolve the absolute path to the running CLI script. Used as the | ||
| * second `serverArgs` element when emitting `node <cli.js>`. | ||
| * | ||
| * Uses `import.meta.url` (Node 20+ ESM-stable) via `fileURLToPath`. | ||
| * `import.meta.dirname` would be slightly cleaner but tsdown's bundle | ||
| * output may reshape directory layout; resolving via URL is portable | ||
| * across both source-mode (vitest) and bundled-mode (production). | ||
| * | ||
| * `fileURLToPath` - NOT `URL.pathname`. A pathname is percent-encoded | ||
| * (a path with a space or a non-ASCII segment comes out as `%20` / | ||
| * `%C3%B6`) and on Windows it carries a leading slash with forward | ||
| * slashes (`/C:/Users/...`). Both forms are unspawnable by the MCP | ||
| * client we are generating the config for. | ||
| * | ||
| * The bundled init command is a sibling chunk of `dist/cli.js`, whereas | ||
| * source-mode init lives under `src/commands/`. Prefer a real sibling | ||
| * `cli.js` first, then retain the source-mode fallback. This keeps emitted | ||
| * configs executable after packaging instead of pointing at the package root. | ||
| * | ||
| * `moduleUrl` is a parameter only so tests can exercise this against | ||
| * paths the repo checkout doesn't have; production always uses the default. | ||
| */ | ||
| function resolveCliPath(moduleUrl = import.meta.url) { | ||
| const bundledCliPath = resolve(dirname(fileURLToPath(moduleUrl)), "cli.js"); | ||
| if (existsSync(bundledCliPath)) return bundledCliPath; | ||
| return fileURLToPath(new URL("../cli.js", moduleUrl)); | ||
| } | ||
| async function initAction(opts) { | ||
| const asJson = opts.json === true; | ||
| const profileLocation = opts.profile?.directory === void 0 ? {} : { directory: opts.profile.directory }; | ||
| let profileName; | ||
| if (opts.profile !== void 0) { | ||
| try { | ||
| profileName = normalizeProfileName(opts.profile.name); | ||
| } catch (error) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "invalid profile name", | ||
| errors: [error instanceof Error ? error.message : String(error)] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| if (opts.discoverGuilds !== true) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "profile setup requires live Discord discovery", | ||
| errors: ["Use the guided setup command so the bot identity and guild scope are verified."] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| if (opts.token !== void 0) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "profile setup does not accept token arguments", | ||
| errors: ["Set DISCORD_TOKEN in the launch environment instead of passing --token."] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| if (opts.force !== true && profileExists(profileName, profileLocation)) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: `profile ${profileName} already exists`, | ||
| errors: ["Rerun setup with --force to update the same bot, or choose another profile name."] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| } | ||
| if (opts.output !== void 0 && existsSync(opts.output) && opts.force !== true) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: `${opts.output} exists; use --force to overwrite` | ||
| }, asJson); | ||
| return; | ||
| } | ||
| let clientId = opts.client; | ||
| if (clientId === void 0) if (isInteractive()) clientId = await askChoice("Which MCP client?", ALL_GENERATORS.map((g) => g.id), 0); | ||
| else clientId = "generic"; | ||
| const generator = ALL_GENERATORS.find((g) => g.id === clientId); | ||
| if (!generator) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: `unknown client: ${clientId}`, | ||
| errors: [`Available clients: ${ALL_GENERATORS.map((g) => g.id).join(", ")}`] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| let token = opts.token ?? TOKEN_PLACEHOLDER; | ||
| if (token === "" || token === TOKEN_PLACEHOLDER) token = TOKEN_PLACEHOLDER; | ||
| let gateway = opts.gateway; | ||
| if (gateway === void 0) if (isInteractive()) gateway = await askYesNo("Enable Discord Gateway resource subscriptions?", false); | ||
| else gateway = false; | ||
| const toolSurface = opts.toolSurface ?? "full"; | ||
| if (toolSurface !== "full" && toolSurface !== "progressive") { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: `unknown tool surface: ${toolSurface}`, | ||
| errors: ["Available tool surfaces: full, progressive"] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| let allowedGuilds = opts.allowedGuilds?.split(",").map((guildId) => guildId.trim()); | ||
| if (allowedGuilds !== void 0 && (allowedGuilds.length === 0 || allowedGuilds.some((guildId) => !SNOWFLAKE.test(guildId)))) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "invalid allowed guild list", | ||
| errors: ["--allowed-guilds must be a comma-separated list of Discord snowflake IDs"] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| let discord; | ||
| const warnings = []; | ||
| if (opts.discoverGuilds === true) { | ||
| const discoveryToken = token === TOKEN_PLACEHOLDER ? process.env.DISCORD_TOKEN : token; | ||
| if (discoveryToken === void 0 || discoveryToken === "") { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "cannot discover Discord guilds without a token", | ||
| errors: ["Set DISCORD_TOKEN in this terminal, then rerun init --discover-guilds. The default config forwards the environment variable without persisting it."] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| try { | ||
| discord = await discoverDiscordSetup(discoveryToken); | ||
| } catch (error) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "Discord guild discovery failed", | ||
| errors: [error instanceof Error ? error.message : String(error)] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| if (discord.guilds.length === 0) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: `verified ${discord.bot.username}, but the bot is not installed in any guild`, | ||
| errors: ["Invite the bot to the intended Discord server and rerun init --discover-guilds."], | ||
| data: { discord } | ||
| }, asJson); | ||
| return; | ||
| } | ||
| if (allowedGuilds !== void 0) { | ||
| const visibleIds = new Set(discord.guilds.map((guild) => guild.id)); | ||
| const missing = allowedGuilds.filter((guildId) => !visibleIds.has(guildId)); | ||
| if (missing.length > 0) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "the requested guild allowlist is not visible to this bot", | ||
| errors: missing.map((guildId) => `${guildId} is not visible to the verified bot`), | ||
| data: { discord } | ||
| }, asJson); | ||
| return; | ||
| } | ||
| } else if (discord.guilds.length === 1) allowedGuilds = [discord.guilds[0].id]; | ||
| else if (isInteractive()) { | ||
| const cancelChoice = "Cancel setup without selecting a guild"; | ||
| const choices = [cancelChoice, ...discord.guilds.map((guild) => `${guild.name} (${guild.id})${guild.administrator ? " [Administrator]" : ""}`)]; | ||
| const choice = await askChoice("Which Discord guild should this config allow?", choices, 0); | ||
| if (choice === cancelChoice) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "guild selection cancelled", | ||
| errors: ["Rerun init --discover-guilds and explicitly choose the intended guild."], | ||
| data: { discord } | ||
| }, asJson); | ||
| return; | ||
| } | ||
| const choiceIndex = choices.indexOf(choice) - 1; | ||
| allowedGuilds = [discord.guilds[choiceIndex].id]; | ||
| } else { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "the verified bot can see multiple guilds", | ||
| details: discord.guilds.map((guild) => `${guild.id} ${guild.name}${guild.administrator ? " [Administrator]" : ""}`), | ||
| errors: ["Pass --allowed-guilds <id,id,...> with the intended target, then keep --discover-guilds to verify it."], | ||
| data: { discord } | ||
| }, asJson); | ||
| return; | ||
| } | ||
| const selectedIds = new Set(allowedGuilds); | ||
| for (const guild of discord.guilds) if (selectedIds.has(guild.id) && guild.administrator) warnings.push(`Bot has Administrator in ${guild.name} (${guild.id}); remove it and grant only the Discord permissions required by your workflows.`); | ||
| } | ||
| const serverPath = profileName === void 0 ? process.execPath : "npx"; | ||
| const serverArgs = profileName === void 0 ? [resolveCliPath()] : [ | ||
| "--yes", | ||
| "--loglevel=error", | ||
| `@discord-mcp/cli@${version}`, | ||
| "serve", | ||
| "--profile", | ||
| profileName | ||
| ]; | ||
| const envVars = {}; | ||
| if (profileName === void 0) { | ||
| if (toolSurface === "progressive") envVars.MCP_TOOL_SURFACE = "progressive"; | ||
| if (allowedGuilds !== void 0) envVars.ALLOWED_GUILDS = allowedGuilds.join(","); | ||
| if (discord !== void 0) envVars.DISCORD_EXPECTED_BOT_ID = discord.bot.id; | ||
| } | ||
| const snippet = generator.generate({ | ||
| serverPath, | ||
| serverArgs, | ||
| ...profileName === void 0 ? { discordToken: token } : {}, | ||
| gateway: profileName === void 0 ? gateway : false, | ||
| ...Object.keys(envVars).length > 0 ? { envVars } : {} | ||
| }); | ||
| let savedProfilePath; | ||
| if (profileName !== void 0 && discord !== void 0 && allowedGuilds !== void 0) { | ||
| const profile = { | ||
| version: 1, | ||
| name: profileName, | ||
| bot: discord.bot, | ||
| credential: { | ||
| provider: "env", | ||
| variable: "DISCORD_TOKEN" | ||
| }, | ||
| allowedGuilds, | ||
| client: generator.id, | ||
| toolSurface, | ||
| gateway | ||
| }; | ||
| try { | ||
| savedProfilePath = saveProfile(profile, { | ||
| ...profileLocation, | ||
| overwrite: opts.force === true | ||
| }); | ||
| } catch (error) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: `could not save profile ${profileName}`, | ||
| errors: [error instanceof Error ? error.message : String(error)] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| } | ||
| let writtenTo; | ||
| if (opts.output !== void 0) { | ||
| writeFileSync(opts.output, snippet.content, "utf8"); | ||
| writtenTo = opts.output; | ||
| } | ||
| const portabilityNote = profileName === void 0 ? generator.id === "codex" ? "For a portable Codex configuration, set command = \"npx\", args = [\"-y\", \"@discord-mcp/cli\"], and startup_timeout_sec = 90 in the TOML fragment." : "Adjust the `command` field if you install discord-mcp globally (e.g. set command=\"npx\" args=[\"@discord-mcp/cli\"])." : `This profile uses a pinned npx launcher (@discord-mcp/cli@${version}) instead of this installation's absolute CLI path. The non-secret profile itself remains local to this operating-system user.`; | ||
| const exitCode = warnings.length > 0 ? 1 : 0; | ||
| const discordDetails = discord === void 0 ? [] : [ | ||
| `Verified Discord bot: ${discord.bot.username} (${discord.bot.id})`, | ||
| `Allowed Discord guilds: ${allowedGuilds?.join(", ") ?? "none"}`, | ||
| "" | ||
| ]; | ||
| emitResult({ | ||
| ok: exitCode === 0, | ||
| exitCode, | ||
| summary: writtenTo !== void 0 ? `wrote ${generator.displayName} config to ${writtenTo}` : `generated ${generator.displayName} config (use --output <path> to write to a file)`, | ||
| data: { | ||
| client: generator.id, | ||
| configFilePath: snippet.configFilePath, | ||
| content: snippet.content, | ||
| instructions: snippet.instructions, | ||
| gateway, | ||
| toolSurface, | ||
| allowedGuilds: allowedGuilds ?? [], | ||
| ...discord === void 0 ? {} : { discord }, | ||
| ...savedProfilePath === void 0 ? {} : { profile: { | ||
| name: profileName, | ||
| path: savedProfilePath, | ||
| credentialProvider: "env:DISCORD_TOKEN" | ||
| } } | ||
| }, | ||
| ...warnings.length > 0 ? { warnings } : {}, | ||
| details: writtenTo !== void 0 ? [ | ||
| ...discordDetails, | ||
| snippet.instructions, | ||
| "", | ||
| `Suggested config path:`, | ||
| snippet.configFilePath, | ||
| "", | ||
| portabilityNote, | ||
| ...savedProfilePath === void 0 ? [] : [ | ||
| "", | ||
| `Profile: ${profileName}`, | ||
| savedProfilePath, | ||
| "Credential: inherited env:DISCORD_TOKEN (not stored)", | ||
| `Verify: discord-mcp doctor --profile ${profileName} --online`, | ||
| `Smoke: discord-mcp smoke --profile ${profileName}` | ||
| ] | ||
| ] : [ | ||
| ...discordDetails, | ||
| snippet.instructions, | ||
| "", | ||
| `Suggested config path:`, | ||
| snippet.configFilePath, | ||
| "", | ||
| portabilityNote, | ||
| ...savedProfilePath === void 0 ? [] : [ | ||
| "", | ||
| `Profile: ${profileName}`, | ||
| savedProfilePath, | ||
| "Credential: inherited env:DISCORD_TOKEN (not stored)", | ||
| `Verify: discord-mcp doctor --profile ${profileName} --online`, | ||
| `Smoke: discord-mcp smoke --profile ${profileName}` | ||
| ], | ||
| "", | ||
| "Snippet:", | ||
| snippet.content.trimEnd() | ||
| ] | ||
| }, asJson); | ||
| } | ||
| //#endregion | ||
| export { initAction as n, resolveCliPath as r, discoverDiscordSetup as t }; | ||
| //# sourceMappingURL=init-BVNHIp9n.js.map |
| {"version":3,"file":"init-BVNHIp9n.js","names":["CONFIG_PATH","INSTRUCTIONS","CONFIG_PATH","INSTRUCTIONS","CONFIG_PATH","INSTRUCTIONS","TOKEN_PLACEHOLDER","CONFIG_PATH","INSTRUCTIONS","packageJson.version"],"sources":["../src/lib/client-snippets/_shared.ts","../src/lib/client-snippets/claude-code.ts","../src/lib/client-snippets/claude-desktop.ts","../src/lib/client-snippets/codex.ts","../src/lib/client-snippets/cursor.ts","../src/lib/client-snippets/generic.ts","../src/lib/client-snippets/index.ts","../src/commands/init.ts"],"sourcesContent":["/**\n * Internal shared rendering for MCP client snippets.\n *\n * The JSON-configured clients (Claude Desktop, Claude Code, Cursor, and\n * Generic) converge on the same `mcpServers.<id>.{command,args,env}` schema.\n * Codex has its own TOML renderer because its config supports secure\n * environment forwarding through `env_vars`.\n */\nimport type { SnippetConfig } from './types.js';\n\n/**\n * Build the `{ command, args, env }` payload for a single MCP server\n * entry. Args are merged: caller-provided `serverArgs` first, then\n * `--gateway` appended when `cfg.gateway === true`. Env includes\n * `DISCORD_TOKEN` when supplied and merges any extra `cfg.envVars`.\n */\nfunction renderServerEntry(cfg: SnippetConfig): {\n command: string;\n args: string[];\n env?: Record<string, string>;\n} {\n const args = [...(cfg.serverArgs ?? [])];\n if (cfg.gateway === true) {\n args.push('--gateway');\n }\n\n const env: Record<string, string> = {\n ...(cfg.discordToken === undefined ? {} : { DISCORD_TOKEN: cfg.discordToken }),\n ...(cfg.envVars ?? {}),\n };\n\n return {\n command: cfg.serverPath,\n args,\n ...(Object.keys(env).length === 0 ? {} : { env }),\n };\n}\n\n/**\n * Render the full top-level JSON document with a single `discord-mcp`\n * server registered under `mcpServers`.\n *\n * The output is pretty-printed with 2-space indent (matches Anthropic's\n * sample configs) and trailing newline.\n */\nexport function renderMcpServersJson(cfg: SnippetConfig): string {\n const doc = {\n mcpServers: {\n 'discord-mcp': renderServerEntry(cfg),\n },\n };\n return `${JSON.stringify(doc, null, 2)}\\n`;\n}\n","/**\n * Claude Code (Anthropic CLI) MCP server snippet generator.\n *\n * Claude Code uses the same `mcpServers` JSON schema as Claude Desktop\n * but stores it in different locations and exposes a `claude mcp add`\n * subcommand for managed configuration. We emit the JSON snippet for\n * users who prefer manual editing and document both options.\n *\n * Path order matches Anthropic CLI's lookup: project-local takes\n * priority over user-level. We document the user-level path here since\n * `init` is typically run once per machine.\n */\nimport { renderMcpServersJson } from './_shared.js';\nimport type { ClientGenerator, Snippet, SnippetConfig } from './types.js';\n\nconst CONFIG_PATH = [\n 'User-level (preferred): ~/.claude.json',\n 'Project-level: <project>/.mcp.json',\n 'Modern CLI form: claude mcp add discord-mcp -- <command> [args...]',\n].join('\\n');\n\nconst INSTRUCTIONS =\n 'Easiest: `claude mcp add discord-mcp -- <command> [args...]`. Manual: merge into the `mcpServers` object in ~/.claude.json (or the project-level .mcp.json).';\n\nexport const claudeCodeGenerator: ClientGenerator = {\n id: 'claude-code',\n displayName: 'Claude Code',\n generate(cfg: SnippetConfig): Snippet {\n return {\n format: 'json',\n content: renderMcpServersJson(cfg),\n configFilePath: CONFIG_PATH,\n instructions: INSTRUCTIONS,\n };\n },\n};\n","/**\n * Claude Desktop MCP server snippet generator.\n *\n * Claude Desktop reads `mcpServers` from a JSON file. The path differs\n * by OS; we document all three so users on any platform can find it.\n *\n * Restart Claude Desktop after editing - the file is read once at app\n * startup and not watched.\n */\nimport { renderMcpServersJson } from './_shared.js';\nimport type { ClientGenerator, Snippet, SnippetConfig } from './types.js';\n\nconst CONFIG_PATH = [\n 'macOS: ~/Library/Application Support/Claude/claude_desktop_config.json',\n 'Windows: %APPDATA%\\\\Claude\\\\claude_desktop_config.json',\n 'Linux: ~/.config/Claude/claude_desktop_config.json',\n].join('\\n');\n\nconst INSTRUCTIONS =\n 'Merge into your existing `mcpServers` object in claude_desktop_config.json (paths above), then fully restart Claude Desktop.';\n\nexport const claudeDesktopGenerator: ClientGenerator = {\n id: 'claude-desktop',\n displayName: 'Claude Desktop',\n generate(cfg: SnippetConfig): Snippet {\n return {\n format: 'json',\n content: renderMcpServersJson(cfg),\n configFilePath: CONFIG_PATH,\n instructions: INSTRUCTIONS,\n };\n },\n};\n","/**\n * Codex MCP server configuration generator.\n *\n * Codex configures local stdio servers in `~/.codex/config.toml`. The safe\n * default forwards `DISCORD_TOKEN` from the environment with `env_vars`\n * instead of persisting a token in Codex's configuration file.\n */\nimport type { ClientGenerator, Snippet, SnippetConfig } from './types.js';\n\nconst CONFIG_PATH = 'User-level: ~/.codex/config.toml';\n\nconst INSTRUCTIONS =\n 'Merge this TOML fragment into ~/.codex/config.toml. Set DISCORD_TOKEN in the environment before starting Codex; the default fragment forwards it without storing the token in config.toml.';\n\n// biome-ignore lint/suspicious/noTemplateCurlyInString: literal placeholder passed by init\nconst TOKEN_PLACEHOLDER = '${env:DISCORD_TOKEN}';\n\nfunction tomlString(value: string): string {\n return JSON.stringify(value);\n}\n\nfunction renderTomlStringArray(values: readonly string[]): string {\n return `[${values.map(tomlString).join(', ')}]`;\n}\n\nfunction renderCodexToml(cfg: SnippetConfig): string {\n const args = [...(cfg.serverArgs ?? [])];\n if (cfg.gateway === true) {\n args.push('--gateway');\n }\n\n const lines = [\n '[mcp_servers.discord-mcp]',\n `command = ${tomlString(cfg.serverPath)}`,\n `args = ${renderTomlStringArray(args)}`,\n ];\n\n if (cfg.serverPath === 'npx') {\n lines.push('startup_timeout_sec = 90');\n }\n\n if (cfg.discordToken === undefined || cfg.discordToken === TOKEN_PLACEHOLDER) {\n lines.push('env_vars = [\"DISCORD_TOKEN\"]');\n }\n\n const env = {\n ...(cfg.discordToken === undefined || cfg.discordToken === TOKEN_PLACEHOLDER\n ? {}\n : { DISCORD_TOKEN: cfg.discordToken }),\n ...(cfg.envVars ?? {}),\n };\n\n if (Object.keys(env).length > 0) {\n lines.push('', '[mcp_servers.discord-mcp.env]');\n for (const [key, value] of Object.entries(env)) {\n lines.push(`${key} = ${tomlString(value)}`);\n }\n }\n\n return `${lines.join('\\n')}\\n`;\n}\n\nexport const codexGenerator: ClientGenerator = {\n id: 'codex',\n displayName: 'Codex',\n generate(cfg: SnippetConfig): Snippet {\n return {\n format: 'toml',\n content: renderCodexToml(cfg),\n configFilePath: CONFIG_PATH,\n instructions: INSTRUCTIONS,\n };\n },\n};\n","/**\n * Cursor MCP server snippet generator.\n *\n * Cursor adopted the standard `mcpServers` schema. Two scopes are\n * supported: global (`~/.cursor/mcp.json`) and per-project\n * (`<project>/.cursor/mcp.json`). The schema is identical, only the\n * file location differs. Restart Cursor after editing.\n */\nimport { renderMcpServersJson } from './_shared.js';\nimport type { ClientGenerator, Snippet, SnippetConfig } from './types.js';\n\nconst CONFIG_PATH = [\n 'Global: ~/.cursor/mcp.json',\n 'Per-project: <project>/.cursor/mcp.json',\n].join('\\n');\n\nconst INSTRUCTIONS =\n 'Place under `~/.cursor/mcp.json` for global access, or `.cursor/mcp.json` in your project root for per-project. Restart Cursor for changes to take effect.';\n\nexport const cursorGenerator: ClientGenerator = {\n id: 'cursor',\n displayName: 'Cursor',\n generate(cfg: SnippetConfig): Snippet {\n return {\n format: 'json',\n content: renderMcpServersJson(cfg),\n configFilePath: CONFIG_PATH,\n instructions: INSTRUCTIONS,\n };\n },\n};\n","/**\n * Generic MCP client snippet generator.\n *\n * Catch-all for clients that aren't first-class supported here but\n * implement the standard MCP server config schema. We emit the same\n * JSON shape with no client-specific path and direct the user to their\n * client's docs for placement.\n */\nimport { renderMcpServersJson } from './_shared.js';\nimport type { ClientGenerator, Snippet, SnippetConfig } from './types.js';\n\nconst CONFIG_PATH = '(check your MCP client docs for the config file location)';\n\nconst INSTRUCTIONS =\n \"This is the standard MCP server config block. Place it under your client's `mcpServers` object as documented by the client.\";\n\nexport const genericGenerator: ClientGenerator = {\n id: 'generic',\n displayName: 'Generic MCP client',\n generate(cfg: SnippetConfig): Snippet {\n return {\n format: 'json',\n content: renderMcpServersJson(cfg),\n configFilePath: CONFIG_PATH,\n instructions: INSTRUCTIONS,\n };\n },\n};\n","/**\n * Registry of all supported MCP client snippet generators - Plan 9 Phase D.\n *\n * Order is intentional: Claude Desktop first (most common entry point\n * for new users), Claude Code second (Anthropic CLI), Codex third,\n * Cursor fourth, Generic last (fallback). The numeric\n * order also drives the default index in interactive `init` choice\n * prompts.\n *\n * To add a new client: implement {@link ClientGenerator} in a new file\n * under this directory, register the singleton here, and add a test.\n * No other surface needs to change - `init` reads from this array.\n */\nimport { claudeCodeGenerator } from './claude-code.js';\nimport { claudeDesktopGenerator } from './claude-desktop.js';\nimport { codexGenerator } from './codex.js';\nimport { cursorGenerator } from './cursor.js';\nimport { genericGenerator } from './generic.js';\nimport type { ClientGenerator } from './types.js';\n\nexport type { ClientGenerator, Snippet, SnippetConfig } from './types.js';\n\nexport const ALL_GENERATORS: readonly ClientGenerator[] = [\n claudeDesktopGenerator,\n claudeCodeGenerator,\n codexGenerator,\n cursorGenerator,\n genericGenerator,\n];\n","/**\n * `discord-mcp init` - Plan 9 Phase D.\n *\n * Replaces the Phase A placeholder. Bootstraps an MCP client config\n * snippet that the user can paste into their client's config file (or\n * have us write directly via `--output`).\n *\n * Flow:\n * 1. Resolve which client (`--client <id>` OR interactive choice OR\n * 'generic' as the silent default for non-interactive runs).\n * 2. Resolve the Discord token (`--token` OR the\n * `${env:DISCORD_TOKEN}` placeholder so users don't accidentally bake a\n * real secret into a committed file).\n * 3. Resolve the gateway flag (`--gateway` OR interactive yes/no OR\n * false by default).\n * 4. Validate the advertised tool surface (`full` by default, or the\n * opt-in `progressive` search + risk-specific dispatcher surface).\n * 5. Validate and normalize the optional server-side guild allowlist. With\n * `--discover-guilds`, verify the current bot identity, enumerate its\n * real guilds, and select or validate the allowlist before generation.\n * 6. Pick a serverPath/serverArgs strategy. Stateless `init` uses the\n * current Node binary + the resolved CLI script, which works for any\n * installation at the cost of an absolute path. Guided profile setup\n * instead emits a pinned `npx` package launcher so its client fragment\n * does not depend on an installation or cache path.\n * 7. Generate the snippet via the chosen ClientGenerator.\n * 8. Either write to `--output <path>` (with `--force` for overwrite\n * protection) or print to stdout / structured payload.\n *\n * Token redaction: in pretty mode the snippet text contains whatever\n * `--token` was passed - including raw secrets. The CLI flag's help\n * text warns about this. The placeholder default avoids the issue\n * entirely. We do NOT echo the token in any other log line.\n */\nimport { existsSync, writeFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport packageJson from '../../package.json' with { type: 'json' };\nimport { ALL_GENERATORS } from '../lib/client-snippets/index.js';\nimport { emitResult } from '../lib/output.js';\nimport {\n type DiscordMcpProfile,\n normalizeProfileName,\n profileExists,\n saveProfile,\n} from '../lib/profiles.js';\nimport { askChoice, askYesNo, isInteractive } from '../lib/prompt.js';\n\nexport interface InitOptions {\n token?: string;\n client?: string;\n output?: string;\n force?: boolean;\n gateway?: boolean;\n toolSurface?: string;\n allowedGuilds?: string;\n discoverGuilds?: boolean;\n json?: boolean;\n profile?: {\n name: string;\n directory?: string;\n };\n}\n\n// Literal placeholder string used when the user opts out of supplying a\n// real token. Clients that support env-var interpolation (Claude Desktop,\n// Cursor, etc.) will resolve this at startup; clients that don't will\n// flag it as a missing token so the user notices.\n//\n// biome-ignore lint/suspicious/noTemplateCurlyInString: literal placeholder for MCP client env interpolation\nconst TOKEN_PLACEHOLDER = '${env:DISCORD_TOKEN}';\nconst DISCORD_API_DEFAULT = 'https://discord.com/api/v10';\nconst DISCORD_REQUEST_TIMEOUT_MS = 5000;\nconst SNOWFLAKE = /^\\d{17,20}$/;\nconst ADMINISTRATOR_PERMISSION = 8n;\n\ninterface DiscordSetupGuild {\n readonly id: string;\n readonly name: string;\n readonly administrator: boolean;\n}\n\ninterface DiscordSetupDiscovery {\n readonly bot: {\n readonly id: string;\n readonly username: string;\n };\n readonly guilds: DiscordSetupGuild[];\n}\n\nfunction safeDisplay(value: string): string {\n return value.replace(/[\\p{Cc}\\p{Cf}]/gu, ' ').trim();\n}\n\nfunction discordAuthHeader(token: string): string {\n return token.startsWith('Bot ') ? token : `Bot ${token}`;\n}\n\nasync function discordGet(path: string, token: string): Promise<unknown> {\n const baseUrl = process.env.DISCORD_API_BASE_URL ?? DISCORD_API_DEFAULT;\n const response = await fetch(`${baseUrl}${path}`, {\n method: 'GET',\n headers: {\n Authorization: discordAuthHeader(token),\n 'User-Agent': 'discord-mcp-init (https://github.com/cappyeo/discord-mcp)',\n },\n signal: AbortSignal.timeout(DISCORD_REQUEST_TIMEOUT_MS),\n });\n\n if (!response.ok) {\n if (response.status === 401) throw new Error('Discord rejected DISCORD_TOKEN (401)');\n if (response.status === 403) throw new Error('Discord denied access for this bot (403)');\n if (response.status === 429) throw new Error('Discord rate-limited guild discovery (429)');\n throw new Error(`Discord returned HTTP ${response.status}`);\n }\n\n try {\n return await response.json();\n } catch {\n throw new Error('Discord returned an invalid JSON response');\n }\n}\n\nfunction parseGuild(raw: unknown): DiscordSetupGuild {\n if (raw === null || typeof raw !== 'object') {\n throw new Error('Discord returned an invalid guild entry');\n }\n const guild = raw as { id?: unknown; name?: unknown; permissions?: unknown };\n if (typeof guild.id !== 'string' || !SNOWFLAKE.test(guild.id)) {\n throw new Error('Discord returned a guild with an invalid id');\n }\n if (typeof guild.name !== 'string' || typeof guild.permissions !== 'string') {\n throw new Error(`Discord returned incomplete metadata for guild ${guild.id}`);\n }\n\n let permissions: bigint;\n try {\n permissions = BigInt(guild.permissions);\n } catch {\n throw new Error(`Discord returned invalid permissions for guild ${guild.id}`);\n }\n\n return {\n id: guild.id,\n name: safeDisplay(guild.name) || '(unnamed guild)',\n administrator: (permissions & ADMINISTRATOR_PERMISSION) === ADMINISTRATOR_PERMISSION,\n };\n}\n\nexport async function discoverDiscordSetup(token: string): Promise<DiscordSetupDiscovery> {\n const rawUser = await discordGet('/users/@me', token);\n if (rawUser === null || typeof rawUser !== 'object') {\n throw new Error('Discord returned an invalid bot identity');\n }\n const user = rawUser as { id?: unknown; username?: unknown; bot?: unknown };\n if (\n typeof user.id !== 'string' ||\n !SNOWFLAKE.test(user.id) ||\n typeof user.username !== 'string' ||\n user.bot !== true\n ) {\n throw new Error('DISCORD_TOKEN must identify a Discord bot account');\n }\n\n const guilds: DiscordSetupGuild[] = [];\n const seen = new Set<string>();\n let after: string | undefined;\n\n for (;;) {\n const query = new URLSearchParams({ limit: '200', with_counts: 'false' });\n if (after !== undefined) query.set('after', after);\n const rawPage = await discordGet(`/users/@me/guilds?${query.toString()}`, token);\n if (!Array.isArray(rawPage)) {\n throw new Error('Discord returned an invalid guild list');\n }\n\n const page = rawPage.map(parseGuild);\n for (const guild of page) {\n if (seen.has(guild.id)) {\n throw new Error('Discord returned a duplicate guild page');\n }\n seen.add(guild.id);\n guilds.push(guild);\n }\n\n if (page.length < 200) break;\n after = page.at(-1)?.id;\n if (after === undefined) throw new Error('Discord guild pagination did not advance');\n }\n\n return {\n bot: { id: user.id, username: safeDisplay(user.username) || '(unnamed bot)' },\n guilds,\n };\n}\n\n/**\n * Resolve the absolute path to the running CLI script. Used as the\n * second `serverArgs` element when emitting `node <cli.js>`.\n *\n * Uses `import.meta.url` (Node 20+ ESM-stable) via `fileURLToPath`.\n * `import.meta.dirname` would be slightly cleaner but tsdown's bundle\n * output may reshape directory layout; resolving via URL is portable\n * across both source-mode (vitest) and bundled-mode (production).\n *\n * `fileURLToPath` - NOT `URL.pathname`. A pathname is percent-encoded\n * (a path with a space or a non-ASCII segment comes out as `%20` /\n * `%C3%B6`) and on Windows it carries a leading slash with forward\n * slashes (`/C:/Users/...`). Both forms are unspawnable by the MCP\n * client we are generating the config for.\n *\n * The bundled init command is a sibling chunk of `dist/cli.js`, whereas\n * source-mode init lives under `src/commands/`. Prefer a real sibling\n * `cli.js` first, then retain the source-mode fallback. This keeps emitted\n * configs executable after packaging instead of pointing at the package root.\n *\n * `moduleUrl` is a parameter only so tests can exercise this against\n * paths the repo checkout doesn't have; production always uses the default.\n */\nexport function resolveCliPath(moduleUrl: string = import.meta.url): string {\n const modulePath = fileURLToPath(moduleUrl);\n const bundledCliPath = resolve(dirname(modulePath), 'cli.js');\n if (existsSync(bundledCliPath)) {\n return bundledCliPath;\n }\n\n // commands/init.js → ../cli.js in source mode.\n return fileURLToPath(new URL('../cli.js', moduleUrl));\n}\n\nexport async function initAction(opts: InitOptions): Promise<void> {\n const asJson = opts.json === true;\n const profileLocation =\n opts.profile?.directory === undefined ? {} : { directory: opts.profile.directory };\n let profileName: string | undefined;\n if (opts.profile !== undefined) {\n try {\n profileName = normalizeProfileName(opts.profile.name);\n } catch (error) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'invalid profile name',\n errors: [error instanceof Error ? error.message : String(error)],\n },\n asJson,\n );\n return;\n }\n if (opts.discoverGuilds !== true) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'profile setup requires live Discord discovery',\n errors: [\n 'Use the guided setup command so the bot identity and guild scope are verified.',\n ],\n },\n asJson,\n );\n return;\n }\n if (opts.token !== undefined) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'profile setup does not accept token arguments',\n errors: ['Set DISCORD_TOKEN in the launch environment instead of passing --token.'],\n },\n asJson,\n );\n return;\n }\n if (opts.force !== true && profileExists(profileName, profileLocation)) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: `profile ${profileName} already exists`,\n errors: [\n 'Rerun setup with --force to update the same bot, or choose another profile name.',\n ],\n },\n asJson,\n );\n return;\n }\n }\n\n if (opts.output !== undefined && existsSync(opts.output) && opts.force !== true) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: `${opts.output} exists; use --force to overwrite`,\n },\n asJson,\n );\n return;\n }\n\n // 1. Resolve client.\n let clientId = opts.client;\n if (clientId === undefined) {\n if (isInteractive()) {\n clientId = await askChoice(\n 'Which MCP client?',\n ALL_GENERATORS.map((g) => g.id),\n 0,\n );\n } else {\n clientId = 'generic';\n }\n }\n const generator = ALL_GENERATORS.find((g) => g.id === clientId);\n if (!generator) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: `unknown client: ${clientId}`,\n errors: [`Available clients: ${ALL_GENERATORS.map((g) => g.id).join(', ')}`],\n },\n asJson,\n );\n return;\n }\n\n // 2. Resolve token. Omission always means environment forwarding. A real\n // token only enters generated output through the explicit --token flag.\n let token = opts.token ?? TOKEN_PLACEHOLDER;\n if (token === '' || token === TOKEN_PLACEHOLDER) {\n token = TOKEN_PLACEHOLDER;\n }\n\n // 3. Resolve gateway flag.\n let gateway = opts.gateway;\n if (gateway === undefined) {\n if (isInteractive()) {\n gateway = await askYesNo('Enable Discord Gateway resource subscriptions?', false);\n } else {\n gateway = false;\n }\n }\n\n // 4. Resolve advertised tool surface.\n const toolSurface = opts.toolSurface ?? 'full';\n if (toolSurface !== 'full' && toolSurface !== 'progressive') {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: `unknown tool surface: ${toolSurface}`,\n errors: ['Available tool surfaces: full, progressive'],\n },\n asJson,\n );\n return;\n }\n\n let allowedGuilds = opts.allowedGuilds?.split(',').map((guildId) => guildId.trim());\n if (\n allowedGuilds !== undefined &&\n (allowedGuilds.length === 0 || allowedGuilds.some((guildId) => !SNOWFLAKE.test(guildId)))\n ) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'invalid allowed guild list',\n errors: ['--allowed-guilds must be a comma-separated list of Discord snowflake IDs'],\n },\n asJson,\n );\n return;\n }\n\n let discord: DiscordSetupDiscovery | undefined;\n const warnings: string[] = [];\n if (opts.discoverGuilds === true) {\n const discoveryToken = token === TOKEN_PLACEHOLDER ? process.env.DISCORD_TOKEN : token;\n if (discoveryToken === undefined || discoveryToken === '') {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'cannot discover Discord guilds without a token',\n errors: [\n 'Set DISCORD_TOKEN in this terminal, then rerun init --discover-guilds. The default config forwards the environment variable without persisting it.',\n ],\n },\n asJson,\n );\n return;\n }\n\n try {\n discord = await discoverDiscordSetup(discoveryToken);\n } catch (error) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'Discord guild discovery failed',\n errors: [error instanceof Error ? error.message : String(error)],\n },\n asJson,\n );\n return;\n }\n\n if (discord.guilds.length === 0) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: `verified ${discord.bot.username}, but the bot is not installed in any guild`,\n errors: [\n 'Invite the bot to the intended Discord server and rerun init --discover-guilds.',\n ],\n data: { discord },\n },\n asJson,\n );\n return;\n }\n\n if (allowedGuilds !== undefined) {\n const visibleIds = new Set(discord.guilds.map((guild) => guild.id));\n const missing = allowedGuilds.filter((guildId) => !visibleIds.has(guildId));\n if (missing.length > 0) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'the requested guild allowlist is not visible to this bot',\n errors: missing.map((guildId) => `${guildId} is not visible to the verified bot`),\n data: { discord },\n },\n asJson,\n );\n return;\n }\n } else if (discord.guilds.length === 1) {\n allowedGuilds = [discord.guilds[0]!.id];\n } else if (isInteractive()) {\n const cancelChoice = 'Cancel setup without selecting a guild';\n const choices = [\n cancelChoice,\n ...discord.guilds.map(\n (guild) => `${guild.name} (${guild.id})${guild.administrator ? ' [Administrator]' : ''}`,\n ),\n ];\n const choice = await askChoice('Which Discord guild should this config allow?', choices, 0);\n if (choice === cancelChoice) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'guild selection cancelled',\n errors: ['Rerun init --discover-guilds and explicitly choose the intended guild.'],\n data: { discord },\n },\n asJson,\n );\n return;\n }\n const choiceIndex = choices.indexOf(choice) - 1;\n allowedGuilds = [discord.guilds[choiceIndex]!.id];\n } else {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'the verified bot can see multiple guilds',\n details: discord.guilds.map(\n (guild) => `${guild.id} ${guild.name}${guild.administrator ? ' [Administrator]' : ''}`,\n ),\n errors: [\n 'Pass --allowed-guilds <id,id,...> with the intended target, then keep --discover-guilds to verify it.',\n ],\n data: { discord },\n },\n asJson,\n );\n return;\n }\n\n const selectedIds = new Set(allowedGuilds);\n for (const guild of discord.guilds) {\n if (selectedIds.has(guild.id) && guild.administrator) {\n warnings.push(\n `Bot has Administrator in ${guild.name} (${guild.id}); remove it and grant only the Discord permissions required by your workflows.`,\n );\n }\n }\n }\n\n // 6. Resolve the launcher. Guided profile setup must not persist the\n // installation-specific path of an npx cache or local checkout. Pin the\n // npm package version so the profile runs against the version that\n // created it; stateless init intentionally keeps its legacy local path.\n const serverPath = profileName === undefined ? process.execPath : 'npx';\n const serverArgs: string[] =\n profileName === undefined\n ? [resolveCliPath()]\n : [\n '--yes',\n '--loglevel=error',\n `@discord-mcp/cli@${packageJson.version}`,\n 'serve',\n '--profile',\n profileName,\n ];\n\n // 7. Generate snippet.\n const envVars: Record<string, string> = {};\n if (profileName === undefined) {\n if (toolSurface === 'progressive') envVars.MCP_TOOL_SURFACE = 'progressive';\n if (allowedGuilds !== undefined) envVars.ALLOWED_GUILDS = allowedGuilds.join(',');\n if (discord !== undefined) envVars.DISCORD_EXPECTED_BOT_ID = discord.bot.id;\n }\n\n const snippet = generator.generate({\n serverPath,\n serverArgs,\n ...(profileName === undefined ? { discordToken: token } : {}),\n gateway: profileName === undefined ? gateway : false,\n ...(Object.keys(envVars).length > 0 ? { envVars } : {}),\n });\n\n let savedProfilePath: string | undefined;\n if (profileName !== undefined && discord !== undefined && allowedGuilds !== undefined) {\n const profile: DiscordMcpProfile = {\n version: 1,\n name: profileName,\n bot: discord.bot,\n credential: { provider: 'env', variable: 'DISCORD_TOKEN' },\n allowedGuilds,\n client: generator.id as DiscordMcpProfile['client'],\n toolSurface: toolSurface as DiscordMcpProfile['toolSurface'],\n gateway,\n };\n try {\n savedProfilePath = saveProfile(profile, {\n ...profileLocation,\n overwrite: opts.force === true,\n });\n } catch (error) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: `could not save profile ${profileName}`,\n errors: [error instanceof Error ? error.message : String(error)],\n },\n asJson,\n );\n return;\n }\n }\n\n // 8. Write or print.\n let writtenTo: string | undefined;\n if (opts.output !== undefined) {\n writeFileSync(opts.output, snippet.content, 'utf8');\n writtenTo = opts.output;\n }\n\n const portabilityNote =\n profileName === undefined\n ? generator.id === 'codex'\n ? 'For a portable Codex configuration, set command = \"npx\", args = [\"-y\", \"@discord-mcp/cli\"], and startup_timeout_sec = 90 in the TOML fragment.'\n : 'Adjust the `command` field if you install discord-mcp globally (e.g. set command=\"npx\" args=[\"@discord-mcp/cli\"]).'\n : `This profile uses a pinned npx launcher (@discord-mcp/cli@${packageJson.version}) instead of this installation's absolute CLI path. The non-secret profile itself remains local to this operating-system user.`;\n\n const exitCode = warnings.length > 0 ? 1 : 0;\n const discordDetails =\n discord === undefined\n ? []\n : [\n `Verified Discord bot: ${discord.bot.username} (${discord.bot.id})`,\n `Allowed Discord guilds: ${allowedGuilds?.join(', ') ?? 'none'}`,\n '',\n ];\n\n emitResult(\n {\n ok: exitCode === 0,\n exitCode,\n summary:\n writtenTo !== undefined\n ? `wrote ${generator.displayName} config to ${writtenTo}`\n : `generated ${generator.displayName} config (use --output <path> to write to a file)`,\n data: {\n client: generator.id,\n configFilePath: snippet.configFilePath,\n content: snippet.content,\n instructions: snippet.instructions,\n gateway,\n toolSurface,\n allowedGuilds: allowedGuilds ?? [],\n ...(discord === undefined ? {} : { discord }),\n ...(savedProfilePath === undefined\n ? {}\n : {\n profile: {\n name: profileName,\n path: savedProfilePath,\n credentialProvider: 'env:DISCORD_TOKEN',\n },\n }),\n },\n ...(warnings.length > 0 ? { warnings } : {}),\n details:\n writtenTo !== undefined\n ? [\n ...discordDetails,\n snippet.instructions,\n '',\n `Suggested config path:`,\n snippet.configFilePath,\n '',\n portabilityNote,\n ...(savedProfilePath === undefined\n ? []\n : [\n '',\n `Profile: ${profileName}`,\n savedProfilePath,\n 'Credential: inherited env:DISCORD_TOKEN (not stored)',\n `Verify: discord-mcp doctor --profile ${profileName} --online`,\n `Smoke: discord-mcp smoke --profile ${profileName}`,\n ]),\n ]\n : [\n ...discordDetails,\n snippet.instructions,\n '',\n `Suggested config path:`,\n snippet.configFilePath,\n '',\n portabilityNote,\n ...(savedProfilePath === undefined\n ? []\n : [\n '',\n `Profile: ${profileName}`,\n savedProfilePath,\n 'Credential: inherited env:DISCORD_TOKEN (not stored)',\n `Verify: discord-mcp doctor --profile ${profileName} --online`,\n `Smoke: discord-mcp smoke --profile ${profileName}`,\n ]),\n '',\n 'Snippet:',\n snippet.content.trimEnd(),\n ],\n },\n asJson,\n );\n}\n"],"mappings":"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"} |
| import { n as initAction } from "./init-BVNHIp9n.js"; | ||
| export { initAction }; |
| //#region package.json | ||
| var version = "0.16.6"; | ||
| //#endregion | ||
| export { version as t }; | ||
| //# sourceMappingURL=package-Cmu8WQp-.js.map |
| {"version":3,"file":"package-Cmu8WQp-.js","names":[],"sources":["../package.json"],"sourcesContent":[""],"mappings":""} |
| import { t as emitResult } from "./output-BGKgg-RQ.js"; | ||
| import { n as initAction } from "./init-BVNHIp9n.js"; | ||
| import { i as isInteractive, t as ask } from "./prompt-B6jM7zuq.js"; | ||
| //#region src/commands/setup.ts | ||
| async function setupAction(options) { | ||
| let profileName = options.profile; | ||
| if (profileName === void 0) { | ||
| if (!isInteractive()) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "non-interactive setup requires --profile <name>", | ||
| errors: ["Choose a stable lowercase profile name, for example: discord-mcp setup --profile devbot --client codex"] | ||
| }, options.json === true); | ||
| return; | ||
| } | ||
| profileName = await ask("Profile name", "default"); | ||
| } | ||
| await initAction({ | ||
| ...options.client === void 0 ? {} : { client: options.client }, | ||
| ...options.output === void 0 ? {} : { output: options.output }, | ||
| ...options.force === void 0 ? {} : { force: options.force }, | ||
| ...options.gateway === void 0 ? {} : { gateway: options.gateway }, | ||
| toolSurface: options.toolSurface ?? "progressive", | ||
| ...options.allowedGuilds === void 0 ? {} : { allowedGuilds: options.allowedGuilds }, | ||
| ...options.json === void 0 ? {} : { json: options.json }, | ||
| discoverGuilds: true, | ||
| profile: { | ||
| name: profileName, | ||
| ...options.profileDirectory === void 0 ? {} : { directory: options.profileDirectory } | ||
| } | ||
| }); | ||
| } | ||
| //#endregion | ||
| export { setupAction }; | ||
| //# sourceMappingURL=setup-B4WXPU_e.js.map |
| {"version":3,"file":"setup-B4WXPU_e.js","names":[],"sources":["../src/commands/setup.ts"],"sourcesContent":["import { emitResult } from '../lib/output.js';\nimport { ask, isInteractive } from '../lib/prompt.js';\nimport { initAction } from './init.js';\n\nexport interface SetupOptions {\n profile?: string;\n client?: string;\n output?: string;\n force?: boolean;\n gateway?: boolean;\n toolSurface?: string;\n allowedGuilds?: string;\n json?: boolean;\n profileDirectory?: string;\n}\n\nexport async function setupAction(options: SetupOptions): Promise<void> {\n let profileName = options.profile;\n if (profileName === undefined) {\n if (!isInteractive()) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'non-interactive setup requires --profile <name>',\n errors: [\n 'Choose a stable lowercase profile name, for example: discord-mcp setup --profile devbot --client codex',\n ],\n },\n options.json === true,\n );\n return;\n }\n profileName = await ask('Profile name', 'default');\n }\n\n await initAction({\n ...(options.client === undefined ? {} : { client: options.client }),\n ...(options.output === undefined ? {} : { output: options.output }),\n ...(options.force === undefined ? {} : { force: options.force }),\n ...(options.gateway === undefined ? {} : { gateway: options.gateway }),\n toolSurface: options.toolSurface ?? 'progressive',\n ...(options.allowedGuilds === undefined ? {} : { allowedGuilds: options.allowedGuilds }),\n ...(options.json === undefined ? {} : { json: options.json }),\n discoverGuilds: true,\n profile: {\n name: profileName,\n ...(options.profileDirectory === undefined ? {} : { directory: options.profileDirectory }),\n },\n });\n}\n"],"mappings":";;;;AAgBA,eAAsB,YAAY,SAAsC;CACtE,IAAI,cAAc,QAAQ;AAC1B,KAAI,gBAAgB,KAAA,GAAW;AAC7B,MAAI,CAAC,eAAe,EAAE;AACpB,cACE;IACE,IAAI;IACJ,UAAU;IACV,SAAS;IACT,QAAQ,CACN,yGACD;IACF,EACD,QAAQ,SAAS,KAClB;AACD;;AAEF,gBAAc,MAAM,IAAI,gBAAgB,UAAU;;AAGpD,OAAM,WAAW;EACf,GAAI,QAAQ,WAAW,KAAA,IAAY,EAAE,GAAG,EAAE,QAAQ,QAAQ,QAAQ;EAClE,GAAI,QAAQ,WAAW,KAAA,IAAY,EAAE,GAAG,EAAE,QAAQ,QAAQ,QAAQ;EAClE,GAAI,QAAQ,UAAU,KAAA,IAAY,EAAE,GAAG,EAAE,OAAO,QAAQ,OAAO;EAC/D,GAAI,QAAQ,YAAY,KAAA,IAAY,EAAE,GAAG,EAAE,SAAS,QAAQ,SAAS;EACrE,aAAa,QAAQ,eAAe;EACpC,GAAI,QAAQ,kBAAkB,KAAA,IAAY,EAAE,GAAG,EAAE,eAAe,QAAQ,eAAe;EACvF,GAAI,QAAQ,SAAS,KAAA,IAAY,EAAE,GAAG,EAAE,MAAM,QAAQ,MAAM;EAC5D,gBAAgB;EAChB,SAAS;GACP,MAAM;GACN,GAAI,QAAQ,qBAAqB,KAAA,IAAY,EAAE,GAAG,EAAE,WAAW,QAAQ,kBAAkB;GAC1F;EACF,CAAC"} |
+3
-3
| #!/usr/bin/env node | ||
| import { t as version } from "./package-CLICWxai.js"; | ||
| import { t as version } from "./package-Cmu8WQp-.js"; | ||
| import { t as activateProfile } from "./profiles-BOWWDiOo.js"; | ||
@@ -423,3 +423,3 @@ import { Command } from "commander"; | ||
| program.command("setup").description("Guided setup for one caller-owned Discord bot profile").option("--profile <name>", "Stable local profile name (required when not interactive)").option("--client <id>", "MCP client (claude-desktop|claude-code|codex|cursor|generic). Default: prompt if TTY, else \"generic\".").option("--gateway", "Enable Discord Gateway resource subscriptions for this profile").option("--tool-surface <mode>", "Advertised tool surface (full|progressive). Default: progressive", "progressive").option("--allowed-guilds <ids>", "Comma-separated guild IDs to verify and allow").option("--output <path>", "Write the generated client snippet to this path").option("--force", "Update the same bot profile and overwrite --output if needed").option("--json", "Emit machine-readable JSON instead of pretty output").action(async (options) => { | ||
| const { setupAction } = await import("./setup-DhA5_G_-.js"); | ||
| const { setupAction } = await import("./setup-B4WXPU_e.js"); | ||
| await captureCliActivity({ command: "setup" }, async () => setupAction(options)); | ||
@@ -449,3 +449,3 @@ }); | ||
| program.command("init").description("Generate an MCP client config snippet (Claude Desktop / Claude Code / Codex / Cursor / Generic)").option("--client <id>", "MCP client (claude-desktop|claude-code|codex|cursor|generic). Default: prompt if TTY, else \"generic\".").option("--token <token>", "Discord bot token. WARNING: writes the value into the config file unredacted. Omit to use the ${env:DISCORD_TOKEN} placeholder.").option("--gateway", "Append --gateway to the snippet so the server enables Discord Gateway resource subscriptions").option("--tool-surface <mode>", "Advertised tool surface (full|progressive). Default: full", "full").option("--allowed-guilds <ids>", "Comma-separated guild IDs enforced by the server (recommended for bot safety)").option("--discover-guilds", "Verify DISCORD_TOKEN online and safely select or validate the guild allowlist").option("--output <path>", "Write the snippet to this path instead of stdout").option("--force", "Overwrite the --output path if it already exists").option("--json", "Emit machine-readable JSON instead of pretty output").action(async (options) => { | ||
| const { initAction } = await import("./init-CqBs6sFV.js"); | ||
| const { initAction } = await import("./init-DzqRAZqR.js"); | ||
| await initAction(options); | ||
@@ -452,0 +452,0 @@ }); |
+3
-3
| { | ||
| "name": "@discord-mcp/cli", | ||
| "version": "0.16.5", | ||
| "version": "0.16.6", | ||
| "private": false, | ||
@@ -64,3 +64,3 @@ "type": "module", | ||
| "zod": "^4.0.0", | ||
| "@discord-mcp/core": "0.16.5" | ||
| "@discord-mcp/core": "0.16.6" | ||
| }, | ||
@@ -72,3 +72,3 @@ "devDependencies": { | ||
| "vitest": "^3.2.0", | ||
| "@discord-mcp/server-mocks": "0.16.5" | ||
| "@discord-mcp/server-mocks": "0.16.6" | ||
| }, | ||
@@ -75,0 +75,0 @@ "scripts": { |
+16
-1
@@ -22,2 +22,3 @@ <p align="center"> | ||
| · <a href="https://cappyeo.github.io/discord-mcp/tools/"><strong>Browse 201 tools</strong></a> | ||
| · <a href="https://cappyeo.github.io/discord-mcp/showcase/live-gaming-server/"><strong>Watch live demo</strong></a> | ||
| · <a href="https://www.npmjs.com/package/@discord-mcp/cli"><strong>View on npm</strong></a> | ||
@@ -27,2 +28,16 @@ · <a href="https://cappyeo.github.io/discord-mcp/"><strong>Documentation</strong></a> | ||
| ## Live demo | ||
| <p align="center"> | ||
| <a href="https://cappyeo.github.io/discord-mcp/showcase/live-gaming-server/"> | ||
| <img src="https://raw.githubusercontent.com/cappyeo/discord-mcp/main/site/public/demo/live-gaming-server-build.webp" alt="Discord gaming-server onboarding and final verification, built live through discord-mcp" width="960" /> | ||
| </a> | ||
| </p> | ||
| An 87-second live walkthrough of an AI agent building a complete gaming | ||
| community from a fresh Discord server through its caller-owned bot. It covers | ||
| channels, safe role permissions, Community, Welcome Screen, onboarding, | ||
| AutoMod, Components V2 cards, and final API readback. Watch the | ||
| [full demo in the docs](https://cappyeo.github.io/discord-mcp/showcase/live-gaming-server/). | ||
| ## How it works | ||
@@ -179,3 +194,3 @@ | ||
| `discord-mcp` is pre-1.0. The current public release is **v0.16.5**; this source tree's core exports, CLI surface, environment schema, and 201-tool registry are covered by contract tests. See the [changelog](https://cappyeo.github.io/discord-mcp/reference/changelog/) and [v1.0 readiness checklist](https://cappyeo.github.io/discord-mcp/reference/v1-readiness/) before depending on an unstable surface. | ||
| `discord-mcp` is pre-1.0. The current public release is **v0.16.6**; this source tree's core exports, CLI surface, environment schema, and 201-tool registry are covered by contract tests. See the [changelog](https://cappyeo.github.io/discord-mcp/reference/changelog/) and [v1.0 readiness checklist](https://cappyeo.github.io/discord-mcp/reference/v1-readiness/) before depending on an unstable surface. | ||
@@ -182,0 +197,0 @@ ## License |
| import { n as initAction } from "./init-CyaxsfDl.js"; | ||
| export { initAction }; |
| import { t as version } from "./package-CLICWxai.js"; | ||
| import { a as profileExists, c as saveProfile, i as normalizeProfileName } from "./profiles-BOWWDiOo.js"; | ||
| import { t as emitResult } from "./output-BGKgg-RQ.js"; | ||
| import { i as isInteractive, n as askChoice, r as askYesNo } from "./prompt-B6jM7zuq.js"; | ||
| import { existsSync, writeFileSync } from "node:fs"; | ||
| import { dirname, resolve } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| //#region src/lib/client-snippets/_shared.ts | ||
| /** | ||
| * Build the `{ command, args, env }` payload for a single MCP server | ||
| * entry. Args are merged: caller-provided `serverArgs` first, then | ||
| * `--gateway` appended when `cfg.gateway === true`. Env includes | ||
| * `DISCORD_TOKEN` when supplied and merges any extra `cfg.envVars`. | ||
| */ | ||
| function renderServerEntry(cfg) { | ||
| const args = [...cfg.serverArgs ?? []]; | ||
| if (cfg.gateway === true) args.push("--gateway"); | ||
| const env = { | ||
| ...cfg.discordToken === void 0 ? {} : { DISCORD_TOKEN: cfg.discordToken }, | ||
| ...cfg.envVars ?? {} | ||
| }; | ||
| return { | ||
| command: cfg.serverPath, | ||
| args, | ||
| ...Object.keys(env).length === 0 ? {} : { env } | ||
| }; | ||
| } | ||
| /** | ||
| * Render the full top-level JSON document with a single `discord-mcp` | ||
| * server registered under `mcpServers`. | ||
| * | ||
| * The output is pretty-printed with 2-space indent (matches Anthropic's | ||
| * sample configs) and trailing newline. | ||
| */ | ||
| function renderMcpServersJson(cfg) { | ||
| const doc = { mcpServers: { "discord-mcp": renderServerEntry(cfg) } }; | ||
| return `${JSON.stringify(doc, null, 2)}\n`; | ||
| } | ||
| //#endregion | ||
| //#region src/lib/client-snippets/claude-code.ts | ||
| /** | ||
| * Claude Code (Anthropic CLI) MCP server snippet generator. | ||
| * | ||
| * Claude Code uses the same `mcpServers` JSON schema as Claude Desktop | ||
| * but stores it in different locations and exposes a `claude mcp add` | ||
| * subcommand for managed configuration. We emit the JSON snippet for | ||
| * users who prefer manual editing and document both options. | ||
| * | ||
| * Path order matches Anthropic CLI's lookup: project-local takes | ||
| * priority over user-level. We document the user-level path here since | ||
| * `init` is typically run once per machine. | ||
| */ | ||
| const CONFIG_PATH$4 = [ | ||
| "User-level (preferred): ~/.claude.json", | ||
| "Project-level: <project>/.mcp.json", | ||
| "Modern CLI form: claude mcp add discord-mcp -- <command> [args...]" | ||
| ].join("\n"); | ||
| const INSTRUCTIONS$4 = "Easiest: `claude mcp add discord-mcp -- <command> [args...]`. Manual: merge into the `mcpServers` object in ~/.claude.json (or the project-level .mcp.json)."; | ||
| const claudeCodeGenerator = { | ||
| id: "claude-code", | ||
| displayName: "Claude Code", | ||
| generate(cfg) { | ||
| return { | ||
| format: "json", | ||
| content: renderMcpServersJson(cfg), | ||
| configFilePath: CONFIG_PATH$4, | ||
| instructions: INSTRUCTIONS$4 | ||
| }; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/lib/client-snippets/claude-desktop.ts | ||
| /** | ||
| * Claude Desktop MCP server snippet generator. | ||
| * | ||
| * Claude Desktop reads `mcpServers` from a JSON file. The path differs | ||
| * by OS; we document all three so users on any platform can find it. | ||
| * | ||
| * Restart Claude Desktop after editing - the file is read once at app | ||
| * startup and not watched. | ||
| */ | ||
| const CONFIG_PATH$3 = [ | ||
| "macOS: ~/Library/Application Support/Claude/claude_desktop_config.json", | ||
| "Windows: %APPDATA%\\Claude\\claude_desktop_config.json", | ||
| "Linux: ~/.config/Claude/claude_desktop_config.json" | ||
| ].join("\n"); | ||
| const INSTRUCTIONS$3 = "Merge into your existing `mcpServers` object in claude_desktop_config.json (paths above), then fully restart Claude Desktop."; | ||
| const claudeDesktopGenerator = { | ||
| id: "claude-desktop", | ||
| displayName: "Claude Desktop", | ||
| generate(cfg) { | ||
| return { | ||
| format: "json", | ||
| content: renderMcpServersJson(cfg), | ||
| configFilePath: CONFIG_PATH$3, | ||
| instructions: INSTRUCTIONS$3 | ||
| }; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/lib/client-snippets/codex.ts | ||
| const CONFIG_PATH$2 = "User-level: ~/.codex/config.toml"; | ||
| const INSTRUCTIONS$2 = "Merge this TOML fragment into ~/.codex/config.toml. Set DISCORD_TOKEN in the environment before starting Codex; the default fragment forwards it without storing the token in config.toml."; | ||
| const TOKEN_PLACEHOLDER$1 = "${env:DISCORD_TOKEN}"; | ||
| function tomlString(value) { | ||
| return JSON.stringify(value); | ||
| } | ||
| function renderTomlStringArray(values) { | ||
| return `[${values.map(tomlString).join(", ")}]`; | ||
| } | ||
| function renderCodexToml(cfg) { | ||
| const args = [...cfg.serverArgs ?? []]; | ||
| if (cfg.gateway === true) args.push("--gateway"); | ||
| const lines = [ | ||
| "[mcp_servers.discord-mcp]", | ||
| `command = ${tomlString(cfg.serverPath)}`, | ||
| `args = ${renderTomlStringArray(args)}` | ||
| ]; | ||
| if (cfg.discordToken === void 0 || cfg.discordToken === TOKEN_PLACEHOLDER$1) lines.push("env_vars = [\"DISCORD_TOKEN\"]"); | ||
| const env = { | ||
| ...cfg.discordToken === void 0 || cfg.discordToken === TOKEN_PLACEHOLDER$1 ? {} : { DISCORD_TOKEN: cfg.discordToken }, | ||
| ...cfg.envVars ?? {} | ||
| }; | ||
| if (Object.keys(env).length > 0) { | ||
| lines.push("", "[mcp_servers.discord-mcp.env]"); | ||
| for (const [key, value] of Object.entries(env)) lines.push(`${key} = ${tomlString(value)}`); | ||
| } | ||
| return `${lines.join("\n")}\n`; | ||
| } | ||
| const codexGenerator = { | ||
| id: "codex", | ||
| displayName: "Codex", | ||
| generate(cfg) { | ||
| return { | ||
| format: "toml", | ||
| content: renderCodexToml(cfg), | ||
| configFilePath: CONFIG_PATH$2, | ||
| instructions: INSTRUCTIONS$2 | ||
| }; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/lib/client-snippets/cursor.ts | ||
| /** | ||
| * Cursor MCP server snippet generator. | ||
| * | ||
| * Cursor adopted the standard `mcpServers` schema. Two scopes are | ||
| * supported: global (`~/.cursor/mcp.json`) and per-project | ||
| * (`<project>/.cursor/mcp.json`). The schema is identical, only the | ||
| * file location differs. Restart Cursor after editing. | ||
| */ | ||
| const CONFIG_PATH$1 = ["Global: ~/.cursor/mcp.json", "Per-project: <project>/.cursor/mcp.json"].join("\n"); | ||
| const INSTRUCTIONS$1 = "Place under `~/.cursor/mcp.json` for global access, or `.cursor/mcp.json` in your project root for per-project. Restart Cursor for changes to take effect."; | ||
| const cursorGenerator = { | ||
| id: "cursor", | ||
| displayName: "Cursor", | ||
| generate(cfg) { | ||
| return { | ||
| format: "json", | ||
| content: renderMcpServersJson(cfg), | ||
| configFilePath: CONFIG_PATH$1, | ||
| instructions: INSTRUCTIONS$1 | ||
| }; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/lib/client-snippets/generic.ts | ||
| /** | ||
| * Generic MCP client snippet generator. | ||
| * | ||
| * Catch-all for clients that aren't first-class supported here but | ||
| * implement the standard MCP server config schema. We emit the same | ||
| * JSON shape with no client-specific path and direct the user to their | ||
| * client's docs for placement. | ||
| */ | ||
| const CONFIG_PATH = "(check your MCP client docs for the config file location)"; | ||
| const INSTRUCTIONS = "This is the standard MCP server config block. Place it under your client's `mcpServers` object as documented by the client."; | ||
| //#endregion | ||
| //#region src/lib/client-snippets/index.ts | ||
| /** | ||
| * Registry of all supported MCP client snippet generators - Plan 9 Phase D. | ||
| * | ||
| * Order is intentional: Claude Desktop first (most common entry point | ||
| * for new users), Claude Code second (Anthropic CLI), Codex third, | ||
| * Cursor fourth, Generic last (fallback). The numeric | ||
| * order also drives the default index in interactive `init` choice | ||
| * prompts. | ||
| * | ||
| * To add a new client: implement {@link ClientGenerator} in a new file | ||
| * under this directory, register the singleton here, and add a test. | ||
| * No other surface needs to change - `init` reads from this array. | ||
| */ | ||
| const ALL_GENERATORS = [ | ||
| claudeDesktopGenerator, | ||
| claudeCodeGenerator, | ||
| codexGenerator, | ||
| cursorGenerator, | ||
| { | ||
| id: "generic", | ||
| displayName: "Generic MCP client", | ||
| generate(cfg) { | ||
| return { | ||
| format: "json", | ||
| content: renderMcpServersJson(cfg), | ||
| configFilePath: CONFIG_PATH, | ||
| instructions: INSTRUCTIONS | ||
| }; | ||
| } | ||
| } | ||
| ]; | ||
| //#endregion | ||
| //#region src/commands/init.ts | ||
| /** | ||
| * `discord-mcp init` - Plan 9 Phase D. | ||
| * | ||
| * Replaces the Phase A placeholder. Bootstraps an MCP client config | ||
| * snippet that the user can paste into their client's config file (or | ||
| * have us write directly via `--output`). | ||
| * | ||
| * Flow: | ||
| * 1. Resolve which client (`--client <id>` OR interactive choice OR | ||
| * 'generic' as the silent default for non-interactive runs). | ||
| * 2. Resolve the Discord token (`--token` OR the | ||
| * `${env:DISCORD_TOKEN}` placeholder so users don't accidentally bake a | ||
| * real secret into a committed file). | ||
| * 3. Resolve the gateway flag (`--gateway` OR interactive yes/no OR | ||
| * false by default). | ||
| * 4. Validate the advertised tool surface (`full` by default, or the | ||
| * opt-in `progressive` search + risk-specific dispatcher surface). | ||
| * 5. Validate and normalize the optional server-side guild allowlist. With | ||
| * `--discover-guilds`, verify the current bot identity, enumerate its | ||
| * real guilds, and select or validate the allowlist before generation. | ||
| * 6. Pick a serverPath/serverArgs strategy. Stateless `init` uses the | ||
| * current Node binary + the resolved CLI script, which works for any | ||
| * installation at the cost of an absolute path. Guided profile setup | ||
| * instead emits a pinned `npx` package launcher so its client fragment | ||
| * does not depend on an installation or cache path. | ||
| * 7. Generate the snippet via the chosen ClientGenerator. | ||
| * 8. Either write to `--output <path>` (with `--force` for overwrite | ||
| * protection) or print to stdout / structured payload. | ||
| * | ||
| * Token redaction: in pretty mode the snippet text contains whatever | ||
| * `--token` was passed - including raw secrets. The CLI flag's help | ||
| * text warns about this. The placeholder default avoids the issue | ||
| * entirely. We do NOT echo the token in any other log line. | ||
| */ | ||
| const TOKEN_PLACEHOLDER = "${env:DISCORD_TOKEN}"; | ||
| const DISCORD_API_DEFAULT = "https://discord.com/api/v10"; | ||
| const DISCORD_REQUEST_TIMEOUT_MS = 5e3; | ||
| const SNOWFLAKE = /^\d{17,20}$/; | ||
| const ADMINISTRATOR_PERMISSION = 8n; | ||
| function safeDisplay(value) { | ||
| return value.replace(/[\p{Cc}\p{Cf}]/gu, " ").trim(); | ||
| } | ||
| function discordAuthHeader(token) { | ||
| return token.startsWith("Bot ") ? token : `Bot ${token}`; | ||
| } | ||
| async function discordGet(path, token) { | ||
| const baseUrl = process.env.DISCORD_API_BASE_URL ?? DISCORD_API_DEFAULT; | ||
| const response = await fetch(`${baseUrl}${path}`, { | ||
| method: "GET", | ||
| headers: { | ||
| Authorization: discordAuthHeader(token), | ||
| "User-Agent": "discord-mcp-init (https://github.com/cappyeo/discord-mcp)" | ||
| }, | ||
| signal: AbortSignal.timeout(DISCORD_REQUEST_TIMEOUT_MS) | ||
| }); | ||
| if (!response.ok) { | ||
| if (response.status === 401) throw new Error("Discord rejected DISCORD_TOKEN (401)"); | ||
| if (response.status === 403) throw new Error("Discord denied access for this bot (403)"); | ||
| if (response.status === 429) throw new Error("Discord rate-limited guild discovery (429)"); | ||
| throw new Error(`Discord returned HTTP ${response.status}`); | ||
| } | ||
| try { | ||
| return await response.json(); | ||
| } catch { | ||
| throw new Error("Discord returned an invalid JSON response"); | ||
| } | ||
| } | ||
| function parseGuild(raw) { | ||
| if (raw === null || typeof raw !== "object") throw new Error("Discord returned an invalid guild entry"); | ||
| const guild = raw; | ||
| if (typeof guild.id !== "string" || !SNOWFLAKE.test(guild.id)) throw new Error("Discord returned a guild with an invalid id"); | ||
| if (typeof guild.name !== "string" || typeof guild.permissions !== "string") throw new Error(`Discord returned incomplete metadata for guild ${guild.id}`); | ||
| let permissions; | ||
| try { | ||
| permissions = BigInt(guild.permissions); | ||
| } catch { | ||
| throw new Error(`Discord returned invalid permissions for guild ${guild.id}`); | ||
| } | ||
| return { | ||
| id: guild.id, | ||
| name: safeDisplay(guild.name) || "(unnamed guild)", | ||
| administrator: (permissions & ADMINISTRATOR_PERMISSION) === ADMINISTRATOR_PERMISSION | ||
| }; | ||
| } | ||
| async function discoverDiscordSetup(token) { | ||
| const rawUser = await discordGet("/users/@me", token); | ||
| if (rawUser === null || typeof rawUser !== "object") throw new Error("Discord returned an invalid bot identity"); | ||
| const user = rawUser; | ||
| if (typeof user.id !== "string" || !SNOWFLAKE.test(user.id) || typeof user.username !== "string" || user.bot !== true) throw new Error("DISCORD_TOKEN must identify a Discord bot account"); | ||
| const guilds = []; | ||
| const seen = /* @__PURE__ */ new Set(); | ||
| let after; | ||
| for (;;) { | ||
| const query = new URLSearchParams({ | ||
| limit: "200", | ||
| with_counts: "false" | ||
| }); | ||
| if (after !== void 0) query.set("after", after); | ||
| const rawPage = await discordGet(`/users/@me/guilds?${query.toString()}`, token); | ||
| if (!Array.isArray(rawPage)) throw new Error("Discord returned an invalid guild list"); | ||
| const page = rawPage.map(parseGuild); | ||
| for (const guild of page) { | ||
| if (seen.has(guild.id)) throw new Error("Discord returned a duplicate guild page"); | ||
| seen.add(guild.id); | ||
| guilds.push(guild); | ||
| } | ||
| if (page.length < 200) break; | ||
| after = page.at(-1)?.id; | ||
| if (after === void 0) throw new Error("Discord guild pagination did not advance"); | ||
| } | ||
| return { | ||
| bot: { | ||
| id: user.id, | ||
| username: safeDisplay(user.username) || "(unnamed bot)" | ||
| }, | ||
| guilds | ||
| }; | ||
| } | ||
| /** | ||
| * Resolve the absolute path to the running CLI script. Used as the | ||
| * second `serverArgs` element when emitting `node <cli.js>`. | ||
| * | ||
| * Uses `import.meta.url` (Node 20+ ESM-stable) via `fileURLToPath`. | ||
| * `import.meta.dirname` would be slightly cleaner but tsdown's bundle | ||
| * output may reshape directory layout; resolving via URL is portable | ||
| * across both source-mode (vitest) and bundled-mode (production). | ||
| * | ||
| * `fileURLToPath` - NOT `URL.pathname`. A pathname is percent-encoded | ||
| * (a path with a space or a non-ASCII segment comes out as `%20` / | ||
| * `%C3%B6`) and on Windows it carries a leading slash with forward | ||
| * slashes (`/C:/Users/...`). Both forms are unspawnable by the MCP | ||
| * client we are generating the config for. | ||
| * | ||
| * The bundled init command is a sibling chunk of `dist/cli.js`, whereas | ||
| * source-mode init lives under `src/commands/`. Prefer a real sibling | ||
| * `cli.js` first, then retain the source-mode fallback. This keeps emitted | ||
| * configs executable after packaging instead of pointing at the package root. | ||
| * | ||
| * `moduleUrl` is a parameter only so tests can exercise this against | ||
| * paths the repo checkout doesn't have; production always uses the default. | ||
| */ | ||
| function resolveCliPath(moduleUrl = import.meta.url) { | ||
| const bundledCliPath = resolve(dirname(fileURLToPath(moduleUrl)), "cli.js"); | ||
| if (existsSync(bundledCliPath)) return bundledCliPath; | ||
| return fileURLToPath(new URL("../cli.js", moduleUrl)); | ||
| } | ||
| async function initAction(opts) { | ||
| const asJson = opts.json === true; | ||
| const profileLocation = opts.profile?.directory === void 0 ? {} : { directory: opts.profile.directory }; | ||
| let profileName; | ||
| if (opts.profile !== void 0) { | ||
| try { | ||
| profileName = normalizeProfileName(opts.profile.name); | ||
| } catch (error) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "invalid profile name", | ||
| errors: [error instanceof Error ? error.message : String(error)] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| if (opts.discoverGuilds !== true) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "profile setup requires live Discord discovery", | ||
| errors: ["Use the guided setup command so the bot identity and guild scope are verified."] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| if (opts.token !== void 0) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "profile setup does not accept token arguments", | ||
| errors: ["Set DISCORD_TOKEN in the launch environment instead of passing --token."] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| if (opts.force !== true && profileExists(profileName, profileLocation)) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: `profile ${profileName} already exists`, | ||
| errors: ["Rerun setup with --force to update the same bot, or choose another profile name."] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| } | ||
| if (opts.output !== void 0 && existsSync(opts.output) && opts.force !== true) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: `${opts.output} exists; use --force to overwrite` | ||
| }, asJson); | ||
| return; | ||
| } | ||
| let clientId = opts.client; | ||
| if (clientId === void 0) if (isInteractive()) clientId = await askChoice("Which MCP client?", ALL_GENERATORS.map((g) => g.id), 0); | ||
| else clientId = "generic"; | ||
| const generator = ALL_GENERATORS.find((g) => g.id === clientId); | ||
| if (!generator) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: `unknown client: ${clientId}`, | ||
| errors: [`Available clients: ${ALL_GENERATORS.map((g) => g.id).join(", ")}`] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| let token = opts.token ?? TOKEN_PLACEHOLDER; | ||
| if (token === "" || token === TOKEN_PLACEHOLDER) token = TOKEN_PLACEHOLDER; | ||
| let gateway = opts.gateway; | ||
| if (gateway === void 0) if (isInteractive()) gateway = await askYesNo("Enable Discord Gateway resource subscriptions?", false); | ||
| else gateway = false; | ||
| const toolSurface = opts.toolSurface ?? "full"; | ||
| if (toolSurface !== "full" && toolSurface !== "progressive") { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: `unknown tool surface: ${toolSurface}`, | ||
| errors: ["Available tool surfaces: full, progressive"] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| let allowedGuilds = opts.allowedGuilds?.split(",").map((guildId) => guildId.trim()); | ||
| if (allowedGuilds !== void 0 && (allowedGuilds.length === 0 || allowedGuilds.some((guildId) => !SNOWFLAKE.test(guildId)))) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "invalid allowed guild list", | ||
| errors: ["--allowed-guilds must be a comma-separated list of Discord snowflake IDs"] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| let discord; | ||
| const warnings = []; | ||
| if (opts.discoverGuilds === true) { | ||
| const discoveryToken = token === TOKEN_PLACEHOLDER ? process.env.DISCORD_TOKEN : token; | ||
| if (discoveryToken === void 0 || discoveryToken === "") { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "cannot discover Discord guilds without a token", | ||
| errors: ["Set DISCORD_TOKEN in this terminal, then rerun init --discover-guilds. The default config forwards the environment variable without persisting it."] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| try { | ||
| discord = await discoverDiscordSetup(discoveryToken); | ||
| } catch (error) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "Discord guild discovery failed", | ||
| errors: [error instanceof Error ? error.message : String(error)] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| if (discord.guilds.length === 0) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: `verified ${discord.bot.username}, but the bot is not installed in any guild`, | ||
| errors: ["Invite the bot to the intended Discord server and rerun init --discover-guilds."], | ||
| data: { discord } | ||
| }, asJson); | ||
| return; | ||
| } | ||
| if (allowedGuilds !== void 0) { | ||
| const visibleIds = new Set(discord.guilds.map((guild) => guild.id)); | ||
| const missing = allowedGuilds.filter((guildId) => !visibleIds.has(guildId)); | ||
| if (missing.length > 0) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "the requested guild allowlist is not visible to this bot", | ||
| errors: missing.map((guildId) => `${guildId} is not visible to the verified bot`), | ||
| data: { discord } | ||
| }, asJson); | ||
| return; | ||
| } | ||
| } else if (discord.guilds.length === 1) allowedGuilds = [discord.guilds[0].id]; | ||
| else if (isInteractive()) { | ||
| const cancelChoice = "Cancel setup without selecting a guild"; | ||
| const choices = [cancelChoice, ...discord.guilds.map((guild) => `${guild.name} (${guild.id})${guild.administrator ? " [Administrator]" : ""}`)]; | ||
| const choice = await askChoice("Which Discord guild should this config allow?", choices, 0); | ||
| if (choice === cancelChoice) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "guild selection cancelled", | ||
| errors: ["Rerun init --discover-guilds and explicitly choose the intended guild."], | ||
| data: { discord } | ||
| }, asJson); | ||
| return; | ||
| } | ||
| const choiceIndex = choices.indexOf(choice) - 1; | ||
| allowedGuilds = [discord.guilds[choiceIndex].id]; | ||
| } else { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "the verified bot can see multiple guilds", | ||
| details: discord.guilds.map((guild) => `${guild.id} ${guild.name}${guild.administrator ? " [Administrator]" : ""}`), | ||
| errors: ["Pass --allowed-guilds <id,id,...> with the intended target, then keep --discover-guilds to verify it."], | ||
| data: { discord } | ||
| }, asJson); | ||
| return; | ||
| } | ||
| const selectedIds = new Set(allowedGuilds); | ||
| for (const guild of discord.guilds) if (selectedIds.has(guild.id) && guild.administrator) warnings.push(`Bot has Administrator in ${guild.name} (${guild.id}); remove it and grant only the Discord permissions required by your workflows.`); | ||
| } | ||
| const serverPath = profileName === void 0 ? process.execPath : "npx"; | ||
| const serverArgs = profileName === void 0 ? [resolveCliPath()] : [ | ||
| "--yes", | ||
| "--loglevel=error", | ||
| `@discord-mcp/cli@${version}`, | ||
| "serve", | ||
| "--profile", | ||
| profileName | ||
| ]; | ||
| const envVars = {}; | ||
| if (profileName === void 0) { | ||
| if (toolSurface === "progressive") envVars.MCP_TOOL_SURFACE = "progressive"; | ||
| if (allowedGuilds !== void 0) envVars.ALLOWED_GUILDS = allowedGuilds.join(","); | ||
| if (discord !== void 0) envVars.DISCORD_EXPECTED_BOT_ID = discord.bot.id; | ||
| } | ||
| const snippet = generator.generate({ | ||
| serverPath, | ||
| serverArgs, | ||
| ...profileName === void 0 ? { discordToken: token } : {}, | ||
| gateway: profileName === void 0 ? gateway : false, | ||
| ...Object.keys(envVars).length > 0 ? { envVars } : {} | ||
| }); | ||
| let savedProfilePath; | ||
| if (profileName !== void 0 && discord !== void 0 && allowedGuilds !== void 0) { | ||
| const profile = { | ||
| version: 1, | ||
| name: profileName, | ||
| bot: discord.bot, | ||
| credential: { | ||
| provider: "env", | ||
| variable: "DISCORD_TOKEN" | ||
| }, | ||
| allowedGuilds, | ||
| client: generator.id, | ||
| toolSurface, | ||
| gateway | ||
| }; | ||
| try { | ||
| savedProfilePath = saveProfile(profile, { | ||
| ...profileLocation, | ||
| overwrite: opts.force === true | ||
| }); | ||
| } catch (error) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: `could not save profile ${profileName}`, | ||
| errors: [error instanceof Error ? error.message : String(error)] | ||
| }, asJson); | ||
| return; | ||
| } | ||
| } | ||
| let writtenTo; | ||
| if (opts.output !== void 0) { | ||
| writeFileSync(opts.output, snippet.content, "utf8"); | ||
| writtenTo = opts.output; | ||
| } | ||
| const portabilityNote = profileName === void 0 ? generator.id === "codex" ? "For a portable Codex configuration, set command = \"npx\" and args = [\"-y\", \"@discord-mcp/cli\"] in the TOML fragment." : "Adjust the `command` field if you install discord-mcp globally (e.g. set command=\"npx\" args=[\"@discord-mcp/cli\"])." : `This profile uses a pinned npx launcher (@discord-mcp/cli@${version}) instead of this installation's absolute CLI path. The non-secret profile itself remains local to this operating-system user.`; | ||
| const exitCode = warnings.length > 0 ? 1 : 0; | ||
| const discordDetails = discord === void 0 ? [] : [ | ||
| `Verified Discord bot: ${discord.bot.username} (${discord.bot.id})`, | ||
| `Allowed Discord guilds: ${allowedGuilds?.join(", ") ?? "none"}`, | ||
| "" | ||
| ]; | ||
| emitResult({ | ||
| ok: exitCode === 0, | ||
| exitCode, | ||
| summary: writtenTo !== void 0 ? `wrote ${generator.displayName} config to ${writtenTo}` : `generated ${generator.displayName} config (use --output <path> to write to a file)`, | ||
| data: { | ||
| client: generator.id, | ||
| configFilePath: snippet.configFilePath, | ||
| content: snippet.content, | ||
| instructions: snippet.instructions, | ||
| gateway, | ||
| toolSurface, | ||
| allowedGuilds: allowedGuilds ?? [], | ||
| ...discord === void 0 ? {} : { discord }, | ||
| ...savedProfilePath === void 0 ? {} : { profile: { | ||
| name: profileName, | ||
| path: savedProfilePath, | ||
| credentialProvider: "env:DISCORD_TOKEN" | ||
| } } | ||
| }, | ||
| ...warnings.length > 0 ? { warnings } : {}, | ||
| details: writtenTo !== void 0 ? [ | ||
| ...discordDetails, | ||
| snippet.instructions, | ||
| "", | ||
| `Suggested config path:`, | ||
| snippet.configFilePath, | ||
| "", | ||
| portabilityNote, | ||
| ...savedProfilePath === void 0 ? [] : [ | ||
| "", | ||
| `Profile: ${profileName}`, | ||
| savedProfilePath, | ||
| "Credential: inherited env:DISCORD_TOKEN (not stored)", | ||
| `Verify: discord-mcp doctor --profile ${profileName} --online`, | ||
| `Smoke: discord-mcp smoke --profile ${profileName}` | ||
| ] | ||
| ] : [ | ||
| ...discordDetails, | ||
| snippet.instructions, | ||
| "", | ||
| `Suggested config path:`, | ||
| snippet.configFilePath, | ||
| "", | ||
| portabilityNote, | ||
| ...savedProfilePath === void 0 ? [] : [ | ||
| "", | ||
| `Profile: ${profileName}`, | ||
| savedProfilePath, | ||
| "Credential: inherited env:DISCORD_TOKEN (not stored)", | ||
| `Verify: discord-mcp doctor --profile ${profileName} --online`, | ||
| `Smoke: discord-mcp smoke --profile ${profileName}` | ||
| ], | ||
| "", | ||
| "Snippet:", | ||
| snippet.content.trimEnd() | ||
| ] | ||
| }, asJson); | ||
| } | ||
| //#endregion | ||
| export { initAction as n, resolveCliPath as r, discoverDiscordSetup as t }; | ||
| //# sourceMappingURL=init-CyaxsfDl.js.map |
| {"version":3,"file":"init-CyaxsfDl.js","names":["CONFIG_PATH","INSTRUCTIONS","CONFIG_PATH","INSTRUCTIONS","CONFIG_PATH","INSTRUCTIONS","TOKEN_PLACEHOLDER","CONFIG_PATH","INSTRUCTIONS","packageJson.version"],"sources":["../src/lib/client-snippets/_shared.ts","../src/lib/client-snippets/claude-code.ts","../src/lib/client-snippets/claude-desktop.ts","../src/lib/client-snippets/codex.ts","../src/lib/client-snippets/cursor.ts","../src/lib/client-snippets/generic.ts","../src/lib/client-snippets/index.ts","../src/commands/init.ts"],"sourcesContent":["/**\n * Internal shared rendering for MCP client snippets.\n *\n * The JSON-configured clients (Claude Desktop, Claude Code, Cursor, and\n * Generic) converge on the same `mcpServers.<id>.{command,args,env}` schema.\n * Codex has its own TOML renderer because its config supports secure\n * environment forwarding through `env_vars`.\n */\nimport type { SnippetConfig } from './types.js';\n\n/**\n * Build the `{ command, args, env }` payload for a single MCP server\n * entry. Args are merged: caller-provided `serverArgs` first, then\n * `--gateway` appended when `cfg.gateway === true`. Env includes\n * `DISCORD_TOKEN` when supplied and merges any extra `cfg.envVars`.\n */\nfunction renderServerEntry(cfg: SnippetConfig): {\n command: string;\n args: string[];\n env?: Record<string, string>;\n} {\n const args = [...(cfg.serverArgs ?? [])];\n if (cfg.gateway === true) {\n args.push('--gateway');\n }\n\n const env: Record<string, string> = {\n ...(cfg.discordToken === undefined ? {} : { DISCORD_TOKEN: cfg.discordToken }),\n ...(cfg.envVars ?? {}),\n };\n\n return {\n command: cfg.serverPath,\n args,\n ...(Object.keys(env).length === 0 ? {} : { env }),\n };\n}\n\n/**\n * Render the full top-level JSON document with a single `discord-mcp`\n * server registered under `mcpServers`.\n *\n * The output is pretty-printed with 2-space indent (matches Anthropic's\n * sample configs) and trailing newline.\n */\nexport function renderMcpServersJson(cfg: SnippetConfig): string {\n const doc = {\n mcpServers: {\n 'discord-mcp': renderServerEntry(cfg),\n },\n };\n return `${JSON.stringify(doc, null, 2)}\\n`;\n}\n","/**\n * Claude Code (Anthropic CLI) MCP server snippet generator.\n *\n * Claude Code uses the same `mcpServers` JSON schema as Claude Desktop\n * but stores it in different locations and exposes a `claude mcp add`\n * subcommand for managed configuration. We emit the JSON snippet for\n * users who prefer manual editing and document both options.\n *\n * Path order matches Anthropic CLI's lookup: project-local takes\n * priority over user-level. We document the user-level path here since\n * `init` is typically run once per machine.\n */\nimport { renderMcpServersJson } from './_shared.js';\nimport type { ClientGenerator, Snippet, SnippetConfig } from './types.js';\n\nconst CONFIG_PATH = [\n 'User-level (preferred): ~/.claude.json',\n 'Project-level: <project>/.mcp.json',\n 'Modern CLI form: claude mcp add discord-mcp -- <command> [args...]',\n].join('\\n');\n\nconst INSTRUCTIONS =\n 'Easiest: `claude mcp add discord-mcp -- <command> [args...]`. Manual: merge into the `mcpServers` object in ~/.claude.json (or the project-level .mcp.json).';\n\nexport const claudeCodeGenerator: ClientGenerator = {\n id: 'claude-code',\n displayName: 'Claude Code',\n generate(cfg: SnippetConfig): Snippet {\n return {\n format: 'json',\n content: renderMcpServersJson(cfg),\n configFilePath: CONFIG_PATH,\n instructions: INSTRUCTIONS,\n };\n },\n};\n","/**\n * Claude Desktop MCP server snippet generator.\n *\n * Claude Desktop reads `mcpServers` from a JSON file. The path differs\n * by OS; we document all three so users on any platform can find it.\n *\n * Restart Claude Desktop after editing - the file is read once at app\n * startup and not watched.\n */\nimport { renderMcpServersJson } from './_shared.js';\nimport type { ClientGenerator, Snippet, SnippetConfig } from './types.js';\n\nconst CONFIG_PATH = [\n 'macOS: ~/Library/Application Support/Claude/claude_desktop_config.json',\n 'Windows: %APPDATA%\\\\Claude\\\\claude_desktop_config.json',\n 'Linux: ~/.config/Claude/claude_desktop_config.json',\n].join('\\n');\n\nconst INSTRUCTIONS =\n 'Merge into your existing `mcpServers` object in claude_desktop_config.json (paths above), then fully restart Claude Desktop.';\n\nexport const claudeDesktopGenerator: ClientGenerator = {\n id: 'claude-desktop',\n displayName: 'Claude Desktop',\n generate(cfg: SnippetConfig): Snippet {\n return {\n format: 'json',\n content: renderMcpServersJson(cfg),\n configFilePath: CONFIG_PATH,\n instructions: INSTRUCTIONS,\n };\n },\n};\n","/**\n * Codex MCP server configuration generator.\n *\n * Codex configures local stdio servers in `~/.codex/config.toml`. The safe\n * default forwards `DISCORD_TOKEN` from the environment with `env_vars`\n * instead of persisting a token in Codex's configuration file.\n */\nimport type { ClientGenerator, Snippet, SnippetConfig } from './types.js';\n\nconst CONFIG_PATH = 'User-level: ~/.codex/config.toml';\n\nconst INSTRUCTIONS =\n 'Merge this TOML fragment into ~/.codex/config.toml. Set DISCORD_TOKEN in the environment before starting Codex; the default fragment forwards it without storing the token in config.toml.';\n\n// biome-ignore lint/suspicious/noTemplateCurlyInString: literal placeholder passed by init\nconst TOKEN_PLACEHOLDER = '${env:DISCORD_TOKEN}';\n\nfunction tomlString(value: string): string {\n return JSON.stringify(value);\n}\n\nfunction renderTomlStringArray(values: readonly string[]): string {\n return `[${values.map(tomlString).join(', ')}]`;\n}\n\nfunction renderCodexToml(cfg: SnippetConfig): string {\n const args = [...(cfg.serverArgs ?? [])];\n if (cfg.gateway === true) {\n args.push('--gateway');\n }\n\n const lines = [\n '[mcp_servers.discord-mcp]',\n `command = ${tomlString(cfg.serverPath)}`,\n `args = ${renderTomlStringArray(args)}`,\n ];\n\n if (cfg.discordToken === undefined || cfg.discordToken === TOKEN_PLACEHOLDER) {\n lines.push('env_vars = [\"DISCORD_TOKEN\"]');\n }\n\n const env = {\n ...(cfg.discordToken === undefined || cfg.discordToken === TOKEN_PLACEHOLDER\n ? {}\n : { DISCORD_TOKEN: cfg.discordToken }),\n ...(cfg.envVars ?? {}),\n };\n\n if (Object.keys(env).length > 0) {\n lines.push('', '[mcp_servers.discord-mcp.env]');\n for (const [key, value] of Object.entries(env)) {\n lines.push(`${key} = ${tomlString(value)}`);\n }\n }\n\n return `${lines.join('\\n')}\\n`;\n}\n\nexport const codexGenerator: ClientGenerator = {\n id: 'codex',\n displayName: 'Codex',\n generate(cfg: SnippetConfig): Snippet {\n return {\n format: 'toml',\n content: renderCodexToml(cfg),\n configFilePath: CONFIG_PATH,\n instructions: INSTRUCTIONS,\n };\n },\n};\n","/**\n * Cursor MCP server snippet generator.\n *\n * Cursor adopted the standard `mcpServers` schema. Two scopes are\n * supported: global (`~/.cursor/mcp.json`) and per-project\n * (`<project>/.cursor/mcp.json`). The schema is identical, only the\n * file location differs. Restart Cursor after editing.\n */\nimport { renderMcpServersJson } from './_shared.js';\nimport type { ClientGenerator, Snippet, SnippetConfig } from './types.js';\n\nconst CONFIG_PATH = [\n 'Global: ~/.cursor/mcp.json',\n 'Per-project: <project>/.cursor/mcp.json',\n].join('\\n');\n\nconst INSTRUCTIONS =\n 'Place under `~/.cursor/mcp.json` for global access, or `.cursor/mcp.json` in your project root for per-project. Restart Cursor for changes to take effect.';\n\nexport const cursorGenerator: ClientGenerator = {\n id: 'cursor',\n displayName: 'Cursor',\n generate(cfg: SnippetConfig): Snippet {\n return {\n format: 'json',\n content: renderMcpServersJson(cfg),\n configFilePath: CONFIG_PATH,\n instructions: INSTRUCTIONS,\n };\n },\n};\n","/**\n * Generic MCP client snippet generator.\n *\n * Catch-all for clients that aren't first-class supported here but\n * implement the standard MCP server config schema. We emit the same\n * JSON shape with no client-specific path and direct the user to their\n * client's docs for placement.\n */\nimport { renderMcpServersJson } from './_shared.js';\nimport type { ClientGenerator, Snippet, SnippetConfig } from './types.js';\n\nconst CONFIG_PATH = '(check your MCP client docs for the config file location)';\n\nconst INSTRUCTIONS =\n \"This is the standard MCP server config block. Place it under your client's `mcpServers` object as documented by the client.\";\n\nexport const genericGenerator: ClientGenerator = {\n id: 'generic',\n displayName: 'Generic MCP client',\n generate(cfg: SnippetConfig): Snippet {\n return {\n format: 'json',\n content: renderMcpServersJson(cfg),\n configFilePath: CONFIG_PATH,\n instructions: INSTRUCTIONS,\n };\n },\n};\n","/**\n * Registry of all supported MCP client snippet generators - Plan 9 Phase D.\n *\n * Order is intentional: Claude Desktop first (most common entry point\n * for new users), Claude Code second (Anthropic CLI), Codex third,\n * Cursor fourth, Generic last (fallback). The numeric\n * order also drives the default index in interactive `init` choice\n * prompts.\n *\n * To add a new client: implement {@link ClientGenerator} in a new file\n * under this directory, register the singleton here, and add a test.\n * No other surface needs to change - `init` reads from this array.\n */\nimport { claudeCodeGenerator } from './claude-code.js';\nimport { claudeDesktopGenerator } from './claude-desktop.js';\nimport { codexGenerator } from './codex.js';\nimport { cursorGenerator } from './cursor.js';\nimport { genericGenerator } from './generic.js';\nimport type { ClientGenerator } from './types.js';\n\nexport type { ClientGenerator, Snippet, SnippetConfig } from './types.js';\n\nexport const ALL_GENERATORS: readonly ClientGenerator[] = [\n claudeDesktopGenerator,\n claudeCodeGenerator,\n codexGenerator,\n cursorGenerator,\n genericGenerator,\n];\n","/**\n * `discord-mcp init` - Plan 9 Phase D.\n *\n * Replaces the Phase A placeholder. Bootstraps an MCP client config\n * snippet that the user can paste into their client's config file (or\n * have us write directly via `--output`).\n *\n * Flow:\n * 1. Resolve which client (`--client <id>` OR interactive choice OR\n * 'generic' as the silent default for non-interactive runs).\n * 2. Resolve the Discord token (`--token` OR the\n * `${env:DISCORD_TOKEN}` placeholder so users don't accidentally bake a\n * real secret into a committed file).\n * 3. Resolve the gateway flag (`--gateway` OR interactive yes/no OR\n * false by default).\n * 4. Validate the advertised tool surface (`full` by default, or the\n * opt-in `progressive` search + risk-specific dispatcher surface).\n * 5. Validate and normalize the optional server-side guild allowlist. With\n * `--discover-guilds`, verify the current bot identity, enumerate its\n * real guilds, and select or validate the allowlist before generation.\n * 6. Pick a serverPath/serverArgs strategy. Stateless `init` uses the\n * current Node binary + the resolved CLI script, which works for any\n * installation at the cost of an absolute path. Guided profile setup\n * instead emits a pinned `npx` package launcher so its client fragment\n * does not depend on an installation or cache path.\n * 7. Generate the snippet via the chosen ClientGenerator.\n * 8. Either write to `--output <path>` (with `--force` for overwrite\n * protection) or print to stdout / structured payload.\n *\n * Token redaction: in pretty mode the snippet text contains whatever\n * `--token` was passed - including raw secrets. The CLI flag's help\n * text warns about this. The placeholder default avoids the issue\n * entirely. We do NOT echo the token in any other log line.\n */\nimport { existsSync, writeFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport packageJson from '../../package.json' with { type: 'json' };\nimport { ALL_GENERATORS } from '../lib/client-snippets/index.js';\nimport { emitResult } from '../lib/output.js';\nimport {\n type DiscordMcpProfile,\n normalizeProfileName,\n profileExists,\n saveProfile,\n} from '../lib/profiles.js';\nimport { askChoice, askYesNo, isInteractive } from '../lib/prompt.js';\n\nexport interface InitOptions {\n token?: string;\n client?: string;\n output?: string;\n force?: boolean;\n gateway?: boolean;\n toolSurface?: string;\n allowedGuilds?: string;\n discoverGuilds?: boolean;\n json?: boolean;\n profile?: {\n name: string;\n directory?: string;\n };\n}\n\n// Literal placeholder string used when the user opts out of supplying a\n// real token. Clients that support env-var interpolation (Claude Desktop,\n// Cursor, etc.) will resolve this at startup; clients that don't will\n// flag it as a missing token so the user notices.\n//\n// biome-ignore lint/suspicious/noTemplateCurlyInString: literal placeholder for MCP client env interpolation\nconst TOKEN_PLACEHOLDER = '${env:DISCORD_TOKEN}';\nconst DISCORD_API_DEFAULT = 'https://discord.com/api/v10';\nconst DISCORD_REQUEST_TIMEOUT_MS = 5000;\nconst SNOWFLAKE = /^\\d{17,20}$/;\nconst ADMINISTRATOR_PERMISSION = 8n;\n\ninterface DiscordSetupGuild {\n readonly id: string;\n readonly name: string;\n readonly administrator: boolean;\n}\n\ninterface DiscordSetupDiscovery {\n readonly bot: {\n readonly id: string;\n readonly username: string;\n };\n readonly guilds: DiscordSetupGuild[];\n}\n\nfunction safeDisplay(value: string): string {\n return value.replace(/[\\p{Cc}\\p{Cf}]/gu, ' ').trim();\n}\n\nfunction discordAuthHeader(token: string): string {\n return token.startsWith('Bot ') ? token : `Bot ${token}`;\n}\n\nasync function discordGet(path: string, token: string): Promise<unknown> {\n const baseUrl = process.env.DISCORD_API_BASE_URL ?? DISCORD_API_DEFAULT;\n const response = await fetch(`${baseUrl}${path}`, {\n method: 'GET',\n headers: {\n Authorization: discordAuthHeader(token),\n 'User-Agent': 'discord-mcp-init (https://github.com/cappyeo/discord-mcp)',\n },\n signal: AbortSignal.timeout(DISCORD_REQUEST_TIMEOUT_MS),\n });\n\n if (!response.ok) {\n if (response.status === 401) throw new Error('Discord rejected DISCORD_TOKEN (401)');\n if (response.status === 403) throw new Error('Discord denied access for this bot (403)');\n if (response.status === 429) throw new Error('Discord rate-limited guild discovery (429)');\n throw new Error(`Discord returned HTTP ${response.status}`);\n }\n\n try {\n return await response.json();\n } catch {\n throw new Error('Discord returned an invalid JSON response');\n }\n}\n\nfunction parseGuild(raw: unknown): DiscordSetupGuild {\n if (raw === null || typeof raw !== 'object') {\n throw new Error('Discord returned an invalid guild entry');\n }\n const guild = raw as { id?: unknown; name?: unknown; permissions?: unknown };\n if (typeof guild.id !== 'string' || !SNOWFLAKE.test(guild.id)) {\n throw new Error('Discord returned a guild with an invalid id');\n }\n if (typeof guild.name !== 'string' || typeof guild.permissions !== 'string') {\n throw new Error(`Discord returned incomplete metadata for guild ${guild.id}`);\n }\n\n let permissions: bigint;\n try {\n permissions = BigInt(guild.permissions);\n } catch {\n throw new Error(`Discord returned invalid permissions for guild ${guild.id}`);\n }\n\n return {\n id: guild.id,\n name: safeDisplay(guild.name) || '(unnamed guild)',\n administrator: (permissions & ADMINISTRATOR_PERMISSION) === ADMINISTRATOR_PERMISSION,\n };\n}\n\nexport async function discoverDiscordSetup(token: string): Promise<DiscordSetupDiscovery> {\n const rawUser = await discordGet('/users/@me', token);\n if (rawUser === null || typeof rawUser !== 'object') {\n throw new Error('Discord returned an invalid bot identity');\n }\n const user = rawUser as { id?: unknown; username?: unknown; bot?: unknown };\n if (\n typeof user.id !== 'string' ||\n !SNOWFLAKE.test(user.id) ||\n typeof user.username !== 'string' ||\n user.bot !== true\n ) {\n throw new Error('DISCORD_TOKEN must identify a Discord bot account');\n }\n\n const guilds: DiscordSetupGuild[] = [];\n const seen = new Set<string>();\n let after: string | undefined;\n\n for (;;) {\n const query = new URLSearchParams({ limit: '200', with_counts: 'false' });\n if (after !== undefined) query.set('after', after);\n const rawPage = await discordGet(`/users/@me/guilds?${query.toString()}`, token);\n if (!Array.isArray(rawPage)) {\n throw new Error('Discord returned an invalid guild list');\n }\n\n const page = rawPage.map(parseGuild);\n for (const guild of page) {\n if (seen.has(guild.id)) {\n throw new Error('Discord returned a duplicate guild page');\n }\n seen.add(guild.id);\n guilds.push(guild);\n }\n\n if (page.length < 200) break;\n after = page.at(-1)?.id;\n if (after === undefined) throw new Error('Discord guild pagination did not advance');\n }\n\n return {\n bot: { id: user.id, username: safeDisplay(user.username) || '(unnamed bot)' },\n guilds,\n };\n}\n\n/**\n * Resolve the absolute path to the running CLI script. Used as the\n * second `serverArgs` element when emitting `node <cli.js>`.\n *\n * Uses `import.meta.url` (Node 20+ ESM-stable) via `fileURLToPath`.\n * `import.meta.dirname` would be slightly cleaner but tsdown's bundle\n * output may reshape directory layout; resolving via URL is portable\n * across both source-mode (vitest) and bundled-mode (production).\n *\n * `fileURLToPath` - NOT `URL.pathname`. A pathname is percent-encoded\n * (a path with a space or a non-ASCII segment comes out as `%20` /\n * `%C3%B6`) and on Windows it carries a leading slash with forward\n * slashes (`/C:/Users/...`). Both forms are unspawnable by the MCP\n * client we are generating the config for.\n *\n * The bundled init command is a sibling chunk of `dist/cli.js`, whereas\n * source-mode init lives under `src/commands/`. Prefer a real sibling\n * `cli.js` first, then retain the source-mode fallback. This keeps emitted\n * configs executable after packaging instead of pointing at the package root.\n *\n * `moduleUrl` is a parameter only so tests can exercise this against\n * paths the repo checkout doesn't have; production always uses the default.\n */\nexport function resolveCliPath(moduleUrl: string = import.meta.url): string {\n const modulePath = fileURLToPath(moduleUrl);\n const bundledCliPath = resolve(dirname(modulePath), 'cli.js');\n if (existsSync(bundledCliPath)) {\n return bundledCliPath;\n }\n\n // commands/init.js → ../cli.js in source mode.\n return fileURLToPath(new URL('../cli.js', moduleUrl));\n}\n\nexport async function initAction(opts: InitOptions): Promise<void> {\n const asJson = opts.json === true;\n const profileLocation =\n opts.profile?.directory === undefined ? {} : { directory: opts.profile.directory };\n let profileName: string | undefined;\n if (opts.profile !== undefined) {\n try {\n profileName = normalizeProfileName(opts.profile.name);\n } catch (error) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'invalid profile name',\n errors: [error instanceof Error ? error.message : String(error)],\n },\n asJson,\n );\n return;\n }\n if (opts.discoverGuilds !== true) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'profile setup requires live Discord discovery',\n errors: [\n 'Use the guided setup command so the bot identity and guild scope are verified.',\n ],\n },\n asJson,\n );\n return;\n }\n if (opts.token !== undefined) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'profile setup does not accept token arguments',\n errors: ['Set DISCORD_TOKEN in the launch environment instead of passing --token.'],\n },\n asJson,\n );\n return;\n }\n if (opts.force !== true && profileExists(profileName, profileLocation)) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: `profile ${profileName} already exists`,\n errors: [\n 'Rerun setup with --force to update the same bot, or choose another profile name.',\n ],\n },\n asJson,\n );\n return;\n }\n }\n\n if (opts.output !== undefined && existsSync(opts.output) && opts.force !== true) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: `${opts.output} exists; use --force to overwrite`,\n },\n asJson,\n );\n return;\n }\n\n // 1. Resolve client.\n let clientId = opts.client;\n if (clientId === undefined) {\n if (isInteractive()) {\n clientId = await askChoice(\n 'Which MCP client?',\n ALL_GENERATORS.map((g) => g.id),\n 0,\n );\n } else {\n clientId = 'generic';\n }\n }\n const generator = ALL_GENERATORS.find((g) => g.id === clientId);\n if (!generator) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: `unknown client: ${clientId}`,\n errors: [`Available clients: ${ALL_GENERATORS.map((g) => g.id).join(', ')}`],\n },\n asJson,\n );\n return;\n }\n\n // 2. Resolve token. Omission always means environment forwarding. A real\n // token only enters generated output through the explicit --token flag.\n let token = opts.token ?? TOKEN_PLACEHOLDER;\n if (token === '' || token === TOKEN_PLACEHOLDER) {\n token = TOKEN_PLACEHOLDER;\n }\n\n // 3. Resolve gateway flag.\n let gateway = opts.gateway;\n if (gateway === undefined) {\n if (isInteractive()) {\n gateway = await askYesNo('Enable Discord Gateway resource subscriptions?', false);\n } else {\n gateway = false;\n }\n }\n\n // 4. Resolve advertised tool surface.\n const toolSurface = opts.toolSurface ?? 'full';\n if (toolSurface !== 'full' && toolSurface !== 'progressive') {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: `unknown tool surface: ${toolSurface}`,\n errors: ['Available tool surfaces: full, progressive'],\n },\n asJson,\n );\n return;\n }\n\n let allowedGuilds = opts.allowedGuilds?.split(',').map((guildId) => guildId.trim());\n if (\n allowedGuilds !== undefined &&\n (allowedGuilds.length === 0 || allowedGuilds.some((guildId) => !SNOWFLAKE.test(guildId)))\n ) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'invalid allowed guild list',\n errors: ['--allowed-guilds must be a comma-separated list of Discord snowflake IDs'],\n },\n asJson,\n );\n return;\n }\n\n let discord: DiscordSetupDiscovery | undefined;\n const warnings: string[] = [];\n if (opts.discoverGuilds === true) {\n const discoveryToken = token === TOKEN_PLACEHOLDER ? process.env.DISCORD_TOKEN : token;\n if (discoveryToken === undefined || discoveryToken === '') {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'cannot discover Discord guilds without a token',\n errors: [\n 'Set DISCORD_TOKEN in this terminal, then rerun init --discover-guilds. The default config forwards the environment variable without persisting it.',\n ],\n },\n asJson,\n );\n return;\n }\n\n try {\n discord = await discoverDiscordSetup(discoveryToken);\n } catch (error) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'Discord guild discovery failed',\n errors: [error instanceof Error ? error.message : String(error)],\n },\n asJson,\n );\n return;\n }\n\n if (discord.guilds.length === 0) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: `verified ${discord.bot.username}, but the bot is not installed in any guild`,\n errors: [\n 'Invite the bot to the intended Discord server and rerun init --discover-guilds.',\n ],\n data: { discord },\n },\n asJson,\n );\n return;\n }\n\n if (allowedGuilds !== undefined) {\n const visibleIds = new Set(discord.guilds.map((guild) => guild.id));\n const missing = allowedGuilds.filter((guildId) => !visibleIds.has(guildId));\n if (missing.length > 0) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'the requested guild allowlist is not visible to this bot',\n errors: missing.map((guildId) => `${guildId} is not visible to the verified bot`),\n data: { discord },\n },\n asJson,\n );\n return;\n }\n } else if (discord.guilds.length === 1) {\n allowedGuilds = [discord.guilds[0]!.id];\n } else if (isInteractive()) {\n const cancelChoice = 'Cancel setup without selecting a guild';\n const choices = [\n cancelChoice,\n ...discord.guilds.map(\n (guild) => `${guild.name} (${guild.id})${guild.administrator ? ' [Administrator]' : ''}`,\n ),\n ];\n const choice = await askChoice('Which Discord guild should this config allow?', choices, 0);\n if (choice === cancelChoice) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'guild selection cancelled',\n errors: ['Rerun init --discover-guilds and explicitly choose the intended guild.'],\n data: { discord },\n },\n asJson,\n );\n return;\n }\n const choiceIndex = choices.indexOf(choice) - 1;\n allowedGuilds = [discord.guilds[choiceIndex]!.id];\n } else {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'the verified bot can see multiple guilds',\n details: discord.guilds.map(\n (guild) => `${guild.id} ${guild.name}${guild.administrator ? ' [Administrator]' : ''}`,\n ),\n errors: [\n 'Pass --allowed-guilds <id,id,...> with the intended target, then keep --discover-guilds to verify it.',\n ],\n data: { discord },\n },\n asJson,\n );\n return;\n }\n\n const selectedIds = new Set(allowedGuilds);\n for (const guild of discord.guilds) {\n if (selectedIds.has(guild.id) && guild.administrator) {\n warnings.push(\n `Bot has Administrator in ${guild.name} (${guild.id}); remove it and grant only the Discord permissions required by your workflows.`,\n );\n }\n }\n }\n\n // 6. Resolve the launcher. Guided profile setup must not persist the\n // installation-specific path of an npx cache or local checkout. Pin the\n // npm package version so the profile runs against the version that\n // created it; stateless init intentionally keeps its legacy local path.\n const serverPath = profileName === undefined ? process.execPath : 'npx';\n const serverArgs: string[] =\n profileName === undefined\n ? [resolveCliPath()]\n : [\n '--yes',\n '--loglevel=error',\n `@discord-mcp/cli@${packageJson.version}`,\n 'serve',\n '--profile',\n profileName,\n ];\n\n // 7. Generate snippet.\n const envVars: Record<string, string> = {};\n if (profileName === undefined) {\n if (toolSurface === 'progressive') envVars.MCP_TOOL_SURFACE = 'progressive';\n if (allowedGuilds !== undefined) envVars.ALLOWED_GUILDS = allowedGuilds.join(',');\n if (discord !== undefined) envVars.DISCORD_EXPECTED_BOT_ID = discord.bot.id;\n }\n\n const snippet = generator.generate({\n serverPath,\n serverArgs,\n ...(profileName === undefined ? { discordToken: token } : {}),\n gateway: profileName === undefined ? gateway : false,\n ...(Object.keys(envVars).length > 0 ? { envVars } : {}),\n });\n\n let savedProfilePath: string | undefined;\n if (profileName !== undefined && discord !== undefined && allowedGuilds !== undefined) {\n const profile: DiscordMcpProfile = {\n version: 1,\n name: profileName,\n bot: discord.bot,\n credential: { provider: 'env', variable: 'DISCORD_TOKEN' },\n allowedGuilds,\n client: generator.id as DiscordMcpProfile['client'],\n toolSurface: toolSurface as DiscordMcpProfile['toolSurface'],\n gateway,\n };\n try {\n savedProfilePath = saveProfile(profile, {\n ...profileLocation,\n overwrite: opts.force === true,\n });\n } catch (error) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: `could not save profile ${profileName}`,\n errors: [error instanceof Error ? error.message : String(error)],\n },\n asJson,\n );\n return;\n }\n }\n\n // 8. Write or print.\n let writtenTo: string | undefined;\n if (opts.output !== undefined) {\n writeFileSync(opts.output, snippet.content, 'utf8');\n writtenTo = opts.output;\n }\n\n const portabilityNote =\n profileName === undefined\n ? generator.id === 'codex'\n ? 'For a portable Codex configuration, set command = \"npx\" and args = [\"-y\", \"@discord-mcp/cli\"] in the TOML fragment.'\n : 'Adjust the `command` field if you install discord-mcp globally (e.g. set command=\"npx\" args=[\"@discord-mcp/cli\"]).'\n : `This profile uses a pinned npx launcher (@discord-mcp/cli@${packageJson.version}) instead of this installation's absolute CLI path. The non-secret profile itself remains local to this operating-system user.`;\n\n const exitCode = warnings.length > 0 ? 1 : 0;\n const discordDetails =\n discord === undefined\n ? []\n : [\n `Verified Discord bot: ${discord.bot.username} (${discord.bot.id})`,\n `Allowed Discord guilds: ${allowedGuilds?.join(', ') ?? 'none'}`,\n '',\n ];\n\n emitResult(\n {\n ok: exitCode === 0,\n exitCode,\n summary:\n writtenTo !== undefined\n ? `wrote ${generator.displayName} config to ${writtenTo}`\n : `generated ${generator.displayName} config (use --output <path> to write to a file)`,\n data: {\n client: generator.id,\n configFilePath: snippet.configFilePath,\n content: snippet.content,\n instructions: snippet.instructions,\n gateway,\n toolSurface,\n allowedGuilds: allowedGuilds ?? [],\n ...(discord === undefined ? {} : { discord }),\n ...(savedProfilePath === undefined\n ? {}\n : {\n profile: {\n name: profileName,\n path: savedProfilePath,\n credentialProvider: 'env:DISCORD_TOKEN',\n },\n }),\n },\n ...(warnings.length > 0 ? { warnings } : {}),\n details:\n writtenTo !== undefined\n ? [\n ...discordDetails,\n snippet.instructions,\n '',\n `Suggested config path:`,\n snippet.configFilePath,\n '',\n portabilityNote,\n ...(savedProfilePath === undefined\n ? []\n : [\n '',\n `Profile: ${profileName}`,\n savedProfilePath,\n 'Credential: inherited env:DISCORD_TOKEN (not stored)',\n `Verify: discord-mcp doctor --profile ${profileName} --online`,\n `Smoke: discord-mcp smoke --profile ${profileName}`,\n ]),\n ]\n : [\n ...discordDetails,\n snippet.instructions,\n '',\n `Suggested config path:`,\n snippet.configFilePath,\n '',\n portabilityNote,\n ...(savedProfilePath === undefined\n ? []\n : [\n '',\n `Profile: ${profileName}`,\n savedProfilePath,\n 'Credential: inherited env:DISCORD_TOKEN (not stored)',\n `Verify: discord-mcp doctor --profile ${profileName} --online`,\n `Smoke: discord-mcp smoke --profile ${profileName}`,\n ]),\n '',\n 'Snippet:',\n snippet.content.trimEnd(),\n ],\n },\n asJson,\n );\n}\n"],"mappings":"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"} |
| //#region package.json | ||
| var version = "0.16.5"; | ||
| //#endregion | ||
| export { version as t }; | ||
| //# sourceMappingURL=package-CLICWxai.js.map |
| {"version":3,"file":"package-CLICWxai.js","names":[],"sources":["../package.json"],"sourcesContent":[""],"mappings":""} |
| import { t as emitResult } from "./output-BGKgg-RQ.js"; | ||
| import { n as initAction } from "./init-CyaxsfDl.js"; | ||
| import { i as isInteractive, t as ask } from "./prompt-B6jM7zuq.js"; | ||
| //#region src/commands/setup.ts | ||
| async function setupAction(options) { | ||
| let profileName = options.profile; | ||
| if (profileName === void 0) { | ||
| if (!isInteractive()) { | ||
| emitResult({ | ||
| ok: false, | ||
| exitCode: 2, | ||
| summary: "non-interactive setup requires --profile <name>", | ||
| errors: ["Choose a stable lowercase profile name, for example: discord-mcp setup --profile devbot --client codex"] | ||
| }, options.json === true); | ||
| return; | ||
| } | ||
| profileName = await ask("Profile name", "default"); | ||
| } | ||
| await initAction({ | ||
| ...options.client === void 0 ? {} : { client: options.client }, | ||
| ...options.output === void 0 ? {} : { output: options.output }, | ||
| ...options.force === void 0 ? {} : { force: options.force }, | ||
| ...options.gateway === void 0 ? {} : { gateway: options.gateway }, | ||
| toolSurface: options.toolSurface ?? "progressive", | ||
| ...options.allowedGuilds === void 0 ? {} : { allowedGuilds: options.allowedGuilds }, | ||
| ...options.json === void 0 ? {} : { json: options.json }, | ||
| discoverGuilds: true, | ||
| profile: { | ||
| name: profileName, | ||
| ...options.profileDirectory === void 0 ? {} : { directory: options.profileDirectory } | ||
| } | ||
| }); | ||
| } | ||
| //#endregion | ||
| export { setupAction }; | ||
| //# sourceMappingURL=setup-DhA5_G_-.js.map |
| {"version":3,"file":"setup-DhA5_G_-.js","names":[],"sources":["../src/commands/setup.ts"],"sourcesContent":["import { emitResult } from '../lib/output.js';\nimport { ask, isInteractive } from '../lib/prompt.js';\nimport { initAction } from './init.js';\n\nexport interface SetupOptions {\n profile?: string;\n client?: string;\n output?: string;\n force?: boolean;\n gateway?: boolean;\n toolSurface?: string;\n allowedGuilds?: string;\n json?: boolean;\n profileDirectory?: string;\n}\n\nexport async function setupAction(options: SetupOptions): Promise<void> {\n let profileName = options.profile;\n if (profileName === undefined) {\n if (!isInteractive()) {\n emitResult(\n {\n ok: false,\n exitCode: 2,\n summary: 'non-interactive setup requires --profile <name>',\n errors: [\n 'Choose a stable lowercase profile name, for example: discord-mcp setup --profile devbot --client codex',\n ],\n },\n options.json === true,\n );\n return;\n }\n profileName = await ask('Profile name', 'default');\n }\n\n await initAction({\n ...(options.client === undefined ? {} : { client: options.client }),\n ...(options.output === undefined ? {} : { output: options.output }),\n ...(options.force === undefined ? {} : { force: options.force }),\n ...(options.gateway === undefined ? {} : { gateway: options.gateway }),\n toolSurface: options.toolSurface ?? 'progressive',\n ...(options.allowedGuilds === undefined ? {} : { allowedGuilds: options.allowedGuilds }),\n ...(options.json === undefined ? {} : { json: options.json }),\n discoverGuilds: true,\n profile: {\n name: profileName,\n ...(options.profileDirectory === undefined ? {} : { directory: options.profileDirectory }),\n },\n });\n}\n"],"mappings":";;;;AAgBA,eAAsB,YAAY,SAAsC;CACtE,IAAI,cAAc,QAAQ;AAC1B,KAAI,gBAAgB,KAAA,GAAW;AAC7B,MAAI,CAAC,eAAe,EAAE;AACpB,cACE;IACE,IAAI;IACJ,UAAU;IACV,SAAS;IACT,QAAQ,CACN,yGACD;IACF,EACD,QAAQ,SAAS,KAClB;AACD;;AAEF,gBAAc,MAAM,IAAI,gBAAgB,UAAU;;AAGpD,OAAM,WAAW;EACf,GAAI,QAAQ,WAAW,KAAA,IAAY,EAAE,GAAG,EAAE,QAAQ,QAAQ,QAAQ;EAClE,GAAI,QAAQ,WAAW,KAAA,IAAY,EAAE,GAAG,EAAE,QAAQ,QAAQ,QAAQ;EAClE,GAAI,QAAQ,UAAU,KAAA,IAAY,EAAE,GAAG,EAAE,OAAO,QAAQ,OAAO;EAC/D,GAAI,QAAQ,YAAY,KAAA,IAAY,EAAE,GAAG,EAAE,SAAS,QAAQ,SAAS;EACrE,aAAa,QAAQ,eAAe;EACpC,GAAI,QAAQ,kBAAkB,KAAA,IAAY,EAAE,GAAG,EAAE,eAAe,QAAQ,eAAe;EACvF,GAAI,QAAQ,SAAS,KAAA,IAAY,EAAE,GAAG,EAAE,MAAM,QAAQ,MAAM;EAC5D,gBAAgB;EAChB,SAAS;GACP,MAAM;GACN,GAAI,QAAQ,qBAAqB,KAAA,IAAY,EAAE,GAAG,EAAE,WAAW,QAAQ,kBAAkB;GAC1F;EACF,CAAC"} |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
553228
0.2%5475
0.02%197
8.24%+ Added
- Removed
Updated