🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@discord-mcp/cli

Package Overview
Dependencies
Maintainers
1
Versions
27
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@discord-mcp/cli - npm Package Compare versions

Comparing version
0.16.5
to
0.16.6
+653
dist/init-BVNHIp9n.js
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 @@ });

{
"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": {

@@ -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"}