feat(mcp): add setup_auth tool (#7307)

This commit is contained in:
Paola Estefanía de Campos
2026-01-13 11:05:39 -08:00
committed by Alex Yang
parent 5643f3caec
commit 33aaacef8e
15 changed files with 1965 additions and 83 deletions

View File

@@ -11,101 +11,178 @@ interface MCPOptions {
claudeCode?: boolean;
openCode?: boolean;
manual?: boolean;
localOnly?: boolean;
remoteOnly?: boolean;
}
const REMOTE_MCP_URL =
"https://mcp.chonkie.ai/better-auth/better-auth-builder/mcp";
const LOCAL_MCP_COMMAND = "npx @better-auth/mcp";
async function mcpAction(options: MCPOptions) {
const mcpUrl = "https://mcp.chonkie.ai/better-auth/better-auth-builder/mcp";
const mcpName = "better-auth";
const installLocal = !options.remoteOnly;
const installRemote = !options.localOnly;
if (options.cursor) {
await handleCursorAction(mcpUrl, mcpName);
await handleCursorAction(installLocal, installRemote);
} else if (options.claudeCode) {
handleClaudeCodeAction(mcpUrl);
handleClaudeCodeAction(installLocal, installRemote);
} else if (options.openCode) {
handleOpenCodeAction(mcpUrl);
handleOpenCodeAction(installLocal, installRemote);
} else if (options.manual) {
handleManualAction(mcpUrl, mcpName);
handleManualAction(installLocal, installRemote);
} else {
showAllOptions(mcpUrl, mcpName);
showAllOptions();
}
}
async function handleCursorAction(mcpUrl: string, mcpName: string) {
const mcpConfig = {
url: mcpUrl,
};
const encodedConfig = base64.encode(
new TextEncoder().encode(JSON.stringify(mcpConfig)),
);
const deeplinkUrl = `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(mcpName)}&config=${encodedConfig}`;
async function installMcpServers(
client: "cursor" | "claude-code" | "open-code" | "manual" | string,
installLocal: boolean = true,
installRemote: boolean = true,
) {
switch (client) {
case "cursor":
await handleCursorAction(installLocal, installRemote);
break;
case "claude-code":
handleClaudeCodeAction(installLocal, installRemote);
break;
case "open-code":
handleOpenCodeAction(installLocal, installRemote);
break;
case "manual":
handleManualAction(installLocal, installRemote);
break;
}
}
async function handleCursorAction(
installLocal: boolean,
installRemote: boolean,
) {
console.log(chalk.bold.blue("🚀 Adding Better Auth MCP to Cursor..."));
try {
const platform = os.platform();
let command: string;
const platform = os.platform();
let openCommand: string;
switch (platform) {
case "darwin":
command = `open "${deeplinkUrl}"`;
break;
case "win32":
command = `start "" "${deeplinkUrl}"`;
break;
case "linux":
command = `xdg-open "${deeplinkUrl}"`;
break;
default:
throw new Error(`Unsupported platform: ${platform}`);
switch (platform) {
case "darwin":
openCommand = "open";
break;
case "win32":
openCommand = "start";
break;
case "linux":
openCommand = "xdg-open";
break;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
const installed: string[] = [];
if (installRemote) {
const remoteConfig = { url: REMOTE_MCP_URL };
const encodedRemote = base64.encode(
new TextEncoder().encode(JSON.stringify(remoteConfig)),
);
const remoteDeeplink = `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent("better-auth-docs")}&config=${encodedRemote}`;
try {
const cmd =
platform === "win32"
? `start "" "${remoteDeeplink}"`
: `${openCommand} "${remoteDeeplink}"`;
execSync(cmd, { stdio: "inherit" });
installed.push("better-auth-docs (remote - documentation & search)");
} catch {
console.log(
chalk.yellow("\n⚠ Could not automatically open Cursor for remote MCP."),
);
}
}
execSync(command, { stdio: "inherit" });
console.log(chalk.green("\n✓ Cursor MCP installed successfully!"));
} catch {
console.log(
chalk.yellow(
"\n⚠ Could not automatically open Cursor. Please copy the deeplink URL above and open it manually.",
),
if (installLocal) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const localConfig = { command: LOCAL_MCP_COMMAND };
const encodedLocal = base64.encode(
new TextEncoder().encode(JSON.stringify(localConfig)),
);
console.log(
chalk.gray(
"\nYou can also manually add this configuration to your Cursor MCP settings:",
),
);
console.log(chalk.gray(JSON.stringify(mcpConfig, null, 2)));
const localDeeplink = `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent("better-auth")}&config=${encodedLocal}`;
try {
const cmd =
platform === "win32"
? `start "" "${localDeeplink}"`
: `${openCommand} "${localDeeplink}"`;
execSync(cmd, { stdio: "inherit" });
installed.push("better-auth (local - setup & diagnostics)");
} catch {
console.log(
chalk.yellow("\n⚠ Could not automatically open Cursor for local MCP."),
);
}
}
if (installed.length > 0) {
console.log(chalk.green("\n✓ Cursor MCP servers installed:"));
for (const name of installed) {
console.log(chalk.green(`${name}`));
}
}
console.log(chalk.bold.white("\n✨ Next Steps:"));
console.log(
chalk.gray("• The MCP server will be added to your Cursor configuration"),
chalk.gray("• The MCP servers will be added to your Cursor configuration"),
);
console.log(
chalk.gray("• You can now use Better Auth features directly in Cursor"),
);
console.log(
chalk.gray(
'• Try: "Set up Better Auth with Google login" or "Help me debug my auth"',
),
);
}
function handleClaudeCodeAction(mcpUrl: string) {
function handleClaudeCodeAction(installLocal: boolean, installRemote: boolean) {
console.log(chalk.bold.blue("🤖 Adding Better Auth MCP to Claude Code..."));
const command = `claude mcp add --transport http better-auth ${mcpUrl}`;
const commands: string[] = [];
try {
execSync(command, { stdio: "inherit" });
console.log(chalk.green("\n✓ Claude Code MCP installed successfully!"));
} catch {
console.log(
chalk.yellow(
"\n⚠ Could not automatically add to Claude Code. Please run this command manually:",
),
if (installRemote) {
commands.push(
`claude mcp add --transport http better-auth-docs ${REMOTE_MCP_URL}`,
);
console.log(chalk.cyan(command));
}
if (installLocal) {
commands.push(`claude mcp add better-auth -- ${LOCAL_MCP_COMMAND}`);
}
let anySucceeded = false;
for (const command of commands) {
try {
execSync(command, { stdio: "inherit" });
anySucceeded = true;
} catch {
console.log(
chalk.yellow(
"\n⚠ Could not automatically add to Claude Code. Please run this command manually:",
),
);
console.log(chalk.cyan(command));
}
}
if (anySucceeded) {
console.log(chalk.green("\n✓ Claude Code MCP configured!"));
}
console.log(chalk.bold.white("\n✨ Next Steps:"));
console.log(
chalk.gray(
"• The MCP server will be added to your Claude Code configuration",
"• The MCP servers will be added to your Claude Code configuration",
),
);
console.log(
@@ -115,18 +192,30 @@ function handleClaudeCodeAction(mcpUrl: string) {
);
}
function handleOpenCodeAction(mcpUrl: string) {
function handleOpenCodeAction(installLocal: boolean, installRemote: boolean) {
console.log(chalk.bold.blue("🔧 Adding Better Auth MCP to Open Code..."));
const mcpConfig: Record<string, unknown> = {};
if (installRemote) {
mcpConfig["better-auth-docs"] = {
type: "remote",
url: REMOTE_MCP_URL,
enabled: true,
};
}
if (installLocal) {
mcpConfig["better-auth"] = {
type: "stdio",
command: LOCAL_MCP_COMMAND,
enabled: true,
};
}
const openCodeConfig = {
$schema: "https://opencode.ai/config.json",
mcp: {
"better-auth": {
type: "remote",
url: mcpUrl,
enabled: true,
},
},
mcp: mcpConfig,
};
const configPath = path.join(process.cwd(), "opencode.json");
@@ -154,7 +243,7 @@ function handleOpenCodeAction(mcpUrl: string) {
console.log(
chalk.green(`\n✓ Open Code configuration written to ${configPath}`),
);
console.log(chalk.green("✓ Better Auth MCP added successfully!"));
console.log(chalk.green("✓ Better Auth MCP servers added successfully!"));
} catch {
console.log(
chalk.yellow(
@@ -165,20 +254,28 @@ function handleOpenCodeAction(mcpUrl: string) {
}
console.log(chalk.bold.white("\n✨ Next Steps:"));
console.log(chalk.gray("• Restart Open Code to load the new MCP server"));
console.log(chalk.gray("• Restart Open Code to load the new MCP servers"));
console.log(
chalk.gray("• You can now use Better Auth features directly in Open Code"),
);
}
function handleManualAction(mcpUrl: string, mcpName: string) {
console.log(chalk.bold.blue("📝 Adding Better Auth MCP Configuration..."));
function handleManualAction(installLocal: boolean, installRemote: boolean) {
console.log(chalk.bold.blue("📝 Better Auth MCP Configuration..."));
const manualConfig = {
[mcpName]: {
url: mcpUrl,
},
};
const manualConfig: Record<string, unknown> = {};
if (installRemote) {
manualConfig["better-auth-docs"] = {
url: REMOTE_MCP_URL,
};
}
if (installLocal) {
manualConfig["better-auth"] = {
command: LOCAL_MCP_COMMAND,
};
}
const configPath = path.join(process.cwd(), "mcp.json");
@@ -196,7 +293,7 @@ function handleManualAction(mcpUrl: string, mcpName: string) {
fs.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2));
console.log(chalk.green(`\n✓ MCP configuration written to ${configPath}`));
console.log(chalk.green("✓ Better Auth MCP added successfully!"));
console.log(chalk.green("✓ Better Auth MCP servers added successfully!"));
} catch {
console.log(
chalk.yellow(
@@ -207,7 +304,7 @@ function handleManualAction(mcpUrl: string, mcpName: string) {
}
console.log(chalk.bold.white("\n✨ Next Steps:"));
console.log(chalk.gray("• Restart your MCP client to load the new server"));
console.log(chalk.gray("• Restart your MCP client to load the new servers"));
console.log(
chalk.gray(
"• You can now use Better Auth features directly in your MCP client",
@@ -215,12 +312,12 @@ function handleManualAction(mcpUrl: string, mcpName: string) {
);
}
function showAllOptions(mcpUrl: string, mcpName: string) {
console.log(chalk.bold.blue("🔌 Better Auth MCP Server"));
function showAllOptions() {
console.log(chalk.bold.blue("🔌 Better Auth MCP Servers"));
console.log(chalk.gray("Choose your MCP client to get started:"));
console.log();
console.log(chalk.bold.white("Available Commands:"));
console.log(chalk.bold.white("MCP Clients:"));
console.log(chalk.cyan(" --cursor ") + chalk.gray("Add to Cursor"));
console.log(
chalk.cyan(" --claude-code ") + chalk.gray("Add to Claude Code"),
@@ -230,12 +327,42 @@ function showAllOptions(mcpUrl: string, mcpName: string) {
chalk.cyan(" --manual ") + chalk.gray("Manual configuration"),
);
console.log();
console.log(chalk.bold.white("Server Selection:"));
console.log(
chalk.cyan(" --local-only ") +
chalk.gray("Install only local MCP (setup & diagnostics)"),
);
console.log(
chalk.cyan(" --remote-only ") +
chalk.gray("Install only remote MCP (documentation & search)"),
);
console.log(chalk.gray(" (default: install both servers)"));
console.log();
console.log(chalk.bold.white("Servers:"));
console.log(
chalk.gray(" • ") +
chalk.white("better-auth") +
chalk.gray(" (local) - Setup auth, diagnose issues, validate config"),
);
console.log(
chalk.gray(" • ") +
chalk.white("better-auth-docs") +
chalk.gray(" (remote) - Search documentation, code examples"),
);
console.log();
}
export const mcp = new Command("mcp")
.description("Add Better Auth MCP server to MCP Clients")
.description("Add Better Auth MCP servers to MCP Clients")
.option("--cursor", "Automatically open Cursor with the MCP configuration")
.option("--claude-code", "Show Claude Code MCP configuration command")
.option("--open-code", "Show Open Code MCP configuration")
.option("--manual", "Show manual MCP configuration for mcp.json")
.option("--local-only", "Install only local MCP server (setup & diagnostics)")
.option(
"--remote-only",
"Install only remote MCP server (documentation & search)",
)
.action(mcpAction);

53
packages/mcp/package.json Normal file
View File

@@ -0,0 +1,53 @@
{
"name": "@better-auth/mcp",
"version": "1.5.0-beta.6",
"type": "module",
"description": "Better Auth MCP server for AI-powered auth setup and diagnostics",
"module": "dist/index.mjs",
"repository": {
"type": "git",
"url": "git+https://github.com/better-auth/better-auth.git",
"directory": "packages/mcp"
},
"homepage": "https://www.better-auth.com/docs/concepts/cli#mcp",
"main": "./dist/index.mjs",
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"typecheck": "tsc --project tsconfig.json"
},
"publishConfig": {
"access": "public",
"executableFiles": [
"./dist/index.mjs"
]
},
"license": "MIT",
"keywords": [
"auth",
"mcp",
"ai",
"model-context-protocol",
"better-auth"
],
"exports": {
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
},
"bin": {
"better-auth-mcp": "./dist/index.mjs"
},
"devDependencies": {
"tsdown": "catalog:",
"typescript": "catalog:"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.8.0",
"zod": "^3.24.2"
},
"files": [
"dist"
]
}

20
packages/mcp/src/index.ts Normal file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerSetupAuth } from "./tools/index.js";
const server = new McpServer({
name: "better-auth",
description:
"Better Auth MCP server for AI-powered auth setup and diagnostics",
version: "0.0.1",
});
registerSetupAuth(server);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);

View File

@@ -0,0 +1,467 @@
import { computeFeatureDiff, parseExistingSetup } from "./parser.js";
import {
generateDatabaseConfig,
getDatabaseCommands,
getDatabaseEnvVar,
} from "./templates/database.js";
import {
categorizeFeatures,
generatePluginImports,
generatePluginSetup,
generateSocialProviderConfig,
getPluginEnvVars,
getSocialProviderEnvVars,
} from "./templates/features.js";
import {
FRAMEWORK_CONFIGS,
getDefaultApiPath,
getDefaultAuthPath,
} from "./templates/frameworks.js";
import type {
Command,
Database,
DocLink,
EnvVar,
Feature,
Framework,
ORM,
OutputFile,
SetupAuthError,
SetupAuthInput,
SetupAuthOutput,
} from "./types.js";
export function generateSetup(
input: SetupAuthInput,
): SetupAuthOutput | SetupAuthError {
const framework = input.framework;
const database = input.database;
const orm = input.orm || "none";
const features = input.features || ["email-password"];
const typescript = input.typescript ?? true;
const srcDir = input.srcDir ?? FRAMEWORK_CONFIGS[framework].defaultSrcDir;
const authPath = input.authPath || getDefaultAuthPath(framework, srcDir);
const apiPath = input.apiPath || getDefaultApiPath(framework, srcDir);
let mode: "create" | "update" = "create";
let featuresToGenerate = features;
let detected: ReturnType<typeof parseExistingSetup> | undefined;
if (input.existingSetup?.authConfig) {
mode = "update";
detected = parseExistingSetup(input.existingSetup);
const diff = computeFeatureDiff(detected.features as Feature[], features);
featuresToGenerate = diff.toAdd;
if (featuresToGenerate.length === 0) {
return {
mode: "update",
files: [],
envVars: [],
commands: [],
detected,
nextSteps: ["All requested features are already configured"],
docs: [],
};
}
}
const { socialProviders, plugins, hasEmailPassword } =
categorizeFeatures(featuresToGenerate);
const files: OutputFile[] = [];
const envVars: EnvVar[] = [];
const commands: Command[] = [];
const warnings: string[] = [];
envVars.push({
name: "BETTER_AUTH_SECRET",
description: "Secret key for signing tokens",
required: true,
howToGet: "Run: npx @better-auth/cli secret",
});
envVars.push(getDatabaseEnvVar(database));
for (const provider of socialProviders) {
envVars.push(...getSocialProviderEnvVars(provider));
}
envVars.push(...getPluginEnvVars(plugins));
if (mode === "create") {
const authFile = generateAuthFile({
framework,
database,
orm,
socialProviders,
plugins,
hasEmailPassword,
typescript,
});
files.push({
path: `${authPath}.${typescript ? "ts" : "js"}`,
description: "Better Auth server configuration",
action: "create",
content: authFile,
});
const clientFile = generateClientFile({
framework,
plugins,
typescript,
});
files.push({
path: `${authPath}-client.${typescript ? "ts" : "js"}`,
description: "Better Auth client configuration",
action: "create",
content: clientFile,
});
if (apiPath) {
const apiFile = generateApiRouteFile(framework, authPath);
if (apiFile) {
files.push({
path: `${apiPath}/route.${typescript ? "ts" : "js"}`,
description: "API route handler for auth endpoints",
action: "create",
content: apiFile,
});
}
}
const frameworkConfig = FRAMEWORK_CONFIGS[framework];
if (frameworkConfig.hooksTemplate) {
files.push({
path: srcDir ? "src/hooks.server.ts" : "hooks.server.ts",
description: "SvelteKit hooks for auth handling",
action: "create",
content: frameworkConfig.hooksTemplate(authPath),
});
}
} else {
if (socialProviders.length > 0 || plugins.length > 0 || hasEmailPassword) {
const changes = generateUpdateChanges({
socialProviders,
plugins,
hasEmailPassword,
});
files.push({
path: `${authPath}.${typescript ? "ts" : "js"}`,
description: "Better Auth server configuration",
action: "update",
changes: changes.serverChanges,
});
if (plugins.length > 0) {
files.push({
path: `${authPath}-client.${typescript ? "ts" : "js"}`,
description: "Better Auth client configuration",
action: "update",
changes: changes.clientChanges,
});
}
}
}
commands.push({
command: "pnpm add better-auth",
description: "Install Better Auth",
when: "If not already installed",
});
commands.push(...getDatabaseCommands(orm));
const nextSteps = generateNextSteps(mode, socialProviders, plugins);
const docs = generateDocLinks(features);
return {
mode,
files,
envVars,
commands,
detected,
nextSteps,
warnings: warnings.length > 0 ? warnings : undefined,
docs,
};
}
function generateAuthFile(options: {
framework: Framework;
database: Database;
orm: ORM;
socialProviders: string[];
plugins: string[];
hasEmailPassword: boolean;
typescript: boolean;
}): string {
const { database, orm, socialProviders, plugins, hasEmailPassword } = options;
const imports: string[] = ['import { betterAuth } from "better-auth";'];
const {
imports: dbImports,
config: dbConfig,
prismaInstance,
} = generateDatabaseConfig(database, orm);
if (dbImports) {
imports.push(dbImports);
}
const { serverImports } = generatePluginImports(plugins);
imports.push(...serverImports);
const socialProvidersConfig = socialProviders
.map((p) => ` ${generateSocialProviderConfig(p)}`)
.join(",\n");
const { serverPlugins } = generatePluginSetup(plugins);
let configBody = ` database: ${dbConfig},`;
if (hasEmailPassword) {
configBody += `
emailAndPassword: {
enabled: true,
},`;
}
if (socialProviders.length > 0) {
configBody += `
socialProviders: {
${socialProvidersConfig}
},`;
}
if (serverPlugins.length > 0) {
configBody += `
plugins: [
${serverPlugins.join(",\n ")}
],`;
}
return `${imports.join("\n")}
${prismaInstance || ""}
export const auth = betterAuth({
${configBody}
});
`;
}
function generateClientFile(options: {
framework: Framework;
plugins: string[];
typescript: boolean;
}): string {
const { framework, plugins } = options;
const frameworkConfig = FRAMEWORK_CONFIGS[framework];
const imports: string[] = [frameworkConfig.clientImport];
const { clientImports } = generatePluginImports(plugins);
imports.push(...clientImports);
const { clientPlugins } = generatePluginSetup(plugins);
let configBody = "";
if (clientPlugins.length > 0) {
configBody = `{
plugins: [
${clientPlugins.join(",\n ")}
],
}`;
} else {
configBody = "{}";
}
return `${imports.join("\n")}
export const authClient = createAuthClient(${configBody});
`;
}
function generateApiRouteFile(
framework: Framework,
authPath: string,
): string | null {
const frameworkConfig = FRAMEWORK_CONFIGS[framework];
if (!frameworkConfig.apiRouteTemplate) {
return null;
}
const template = frameworkConfig.apiRouteTemplate(authPath);
return template || null;
}
function generateUpdateChanges(options: {
socialProviders: string[];
plugins: string[];
hasEmailPassword: boolean;
}): {
serverChanges: { type: string; content: string; description: string }[];
clientChanges: { type: string; content: string; description: string }[];
} {
const { socialProviders, plugins, hasEmailPassword } = options;
const serverChanges: {
type: string;
content: string;
description: string;
}[] = [];
const clientChanges: {
type: string;
content: string;
description: string;
}[] = [];
if (hasEmailPassword) {
serverChanges.push({
type: "add_to_config",
content: `emailAndPassword: {
enabled: true,
}`,
description: "Enable email and password authentication",
});
}
const { serverImports, clientImports } = generatePluginImports(plugins);
for (const imp of serverImports) {
serverChanges.push({
type: "add_import",
content: imp,
description: "Add plugin import",
});
}
for (const provider of socialProviders) {
serverChanges.push({
type: "add_to_config",
content: generateSocialProviderConfig(provider),
description: `Add ${provider} social provider`,
});
}
const { serverPlugins, clientPlugins } = generatePluginSetup(plugins);
for (const plugin of serverPlugins) {
serverChanges.push({
type: "add_plugin",
content: plugin,
description: "Add server plugin",
});
}
for (const imp of clientImports) {
clientChanges.push({
type: "add_import",
content: imp,
description: "Add client plugin import",
});
}
for (const plugin of clientPlugins) {
clientChanges.push({
type: "add_plugin",
content: plugin,
description: "Add client plugin",
});
}
return { serverChanges, clientChanges };
}
function generateNextSteps(
mode: "create" | "update",
socialProviders: string[],
plugins: string[],
): string[] {
const steps: string[] = [];
if (mode === "create") {
steps.push("Set environment variables in .env file");
steps.push("Run database migrations");
}
if (socialProviders.length > 0) {
steps.push(`Configure OAuth apps for: ${socialProviders.join(", ")}`);
steps.push("Add callback URLs to OAuth provider dashboards");
}
if (plugins.includes("magic-link")) {
steps.push("Configure email provider for magic links");
}
if (plugins.includes("phone-number")) {
steps.push("Configure SMS provider for OTP");
}
if (plugins.includes("captcha")) {
steps.push(
"Set up captcha provider (Cloudflare Turnstile, reCAPTCHA, or hCaptcha)",
);
}
steps.push("Start your development server and test auth flow");
return steps;
}
function generateDocLinks(features: Feature[]): DocLink[] {
const docs: DocLink[] = [
{
title: "Better Auth Documentation",
url: "https://www.better-auth.com/docs",
},
{
title: "Getting Started",
url: "https://www.better-auth.com/docs/getting-started",
},
];
if (
features.some((f) => ["google", "github", "apple", "discord"].includes(f))
) {
docs.push({
title: "Social Sign-On",
url: "https://www.better-auth.com/docs/authentication/social-sign-on",
});
}
if (features.includes("2fa")) {
docs.push({
title: "Two-Factor Authentication",
url: "https://www.better-auth.com/docs/plugins/two-factor",
});
}
if (features.includes("organization")) {
docs.push({
title: "Organizations",
url: "https://www.better-auth.com/docs/plugins/organization",
});
}
if (features.includes("passkey")) {
docs.push({
title: "Passkeys",
url: "https://www.better-auth.com/docs/plugins/passkey",
});
}
return docs;
}
export function isSetupError(
result: SetupAuthOutput | SetupAuthError,
): result is SetupAuthError {
return "error" in result;
}

View File

@@ -0,0 +1,111 @@
import type { DetectedConfig, ExistingSetup, Feature } from "./types.js";
export function parseExistingSetup(
existingSetup: ExistingSetup,
): DetectedConfig {
const detected: DetectedConfig = {
features: [],
};
if (existingSetup.authConfig) {
const config = existingSetup.authConfig;
detected.database = detectDatabase(config);
detected.orm = detectORM(config);
detected.features = detectFeatures(config, existingSetup.authClientConfig);
}
return detected;
}
function detectDatabase(config: string): string | undefined {
if (
/provider:\s*["']postgresql["']/.test(config) ||
/provider:\s*["']pg["']/.test(config)
) {
return "postgres";
}
if (/provider:\s*["']mysql["']/.test(config)) {
return "mysql";
}
if (/provider:\s*["']sqlite["']/.test(config)) {
return "sqlite";
}
if (/provider:\s*["']mongodb["']/.test(config)) {
return "mongodb";
}
return undefined;
}
function detectORM(config: string): string | undefined {
if (/prismaAdapter\(/.test(config)) {
return "prisma";
}
if (/drizzleAdapter\(/.test(config)) {
return "drizzle";
}
if (/database:\s*\{[\s\S]*provider:/.test(config)) {
return "none";
}
return undefined;
}
function detectFeatures(
serverConfig: string,
clientConfig?: string,
): Feature[] {
const features: Feature[] = [];
if (/emailAndPassword:\s*\{[\s\S]*enabled:\s*true/.test(serverConfig)) {
features.push("email-password");
}
const socialProviders: Feature[] = [
"google",
"github",
"apple",
"discord",
"twitter",
"facebook",
"microsoft",
"linkedin",
];
for (const provider of socialProviders) {
if (new RegExp(`${provider}:\\s*\\{`).test(serverConfig)) {
features.push(provider);
}
}
const pluginPatterns: [RegExp, Feature][] = [
[/twoFactor\(/, "2fa"],
[/organization\(/, "organization"],
[/admin\(/, "admin"],
[/username\(/, "username"],
[/multiSession\(/, "multi-session"],
[/apiKey\(/, "api-key"],
[/bearer\(/, "bearer"],
[/jwt\(/, "jwt"],
[/magicLink\(/, "magic-link"],
[/phoneNumber\(/, "phone-number"],
[/passkey\(/, "passkey"],
[/anonymous\(/, "anonymous"],
[/captcha\(/, "captcha"],
];
for (const [pattern, feature] of pluginPatterns) {
if (pattern.test(serverConfig)) {
features.push(feature);
}
}
return features;
}
export function computeFeatureDiff(
existing: Feature[],
requested: Feature[],
): { toAdd: Feature[]; existing: Feature[] } {
const toAdd = requested.filter((f) => !existing.includes(f));
const existingFeatures = requested.filter((f) => existing.includes(f));
return { toAdd, existing: existingFeatures };
}

View File

@@ -0,0 +1,61 @@
import * as z from "zod";
export const FrameworkEnum = z.enum([
"next-app-router",
"next-pages-router",
"sveltekit",
"astro",
"remix",
"nuxt",
"solid-start",
"hono",
"express",
"fastify",
"elysia",
"tanstack-start",
"expo",
]);
export const DatabaseEnum = z.enum(["postgres", "mysql", "sqlite", "mongodb"]);
export const ORMEnum = z.enum(["prisma", "drizzle", "none"]);
export const FeatureEnum = z.enum([
"email-password",
"magic-link",
"phone-number",
"passkey",
"anonymous",
"google",
"github",
"apple",
"discord",
"twitter",
"facebook",
"microsoft",
"linkedin",
"2fa",
"captcha",
"organization",
"admin",
"username",
"multi-session",
"api-key",
"bearer",
"jwt",
]);
export const ExistingSetupSchema = z.object({
authConfig: z
.string()
.optional()
.describe("Contents of existing auth.ts file"),
authClientConfig: z
.string()
.optional()
.describe("Contents of existing auth-client.ts file"),
envVars: z
.array(z.string())
.optional()
.describe("List of existing environment variable names"),
});

View File

@@ -0,0 +1,140 @@
import type {
Database,
DatabaseConfig,
EnvVar,
ORM,
ORMConfig,
} from "../types.js";
// cspell:ignore mydb
const DATABASE_CONFIGS: Record<Database, DatabaseConfig> = {
postgres: {
provider: "postgresql",
envVarName: "DATABASE_URL",
connectionStringExample: "postgresql://user:password@localhost:5432/mydb",
},
mysql: {
provider: "mysql",
envVarName: "DATABASE_URL",
connectionStringExample: "mysql://user:password@localhost:3306/mydb",
},
sqlite: {
provider: "sqlite",
envVarName: "DATABASE_URL",
connectionStringExample: "file:./dev.db",
},
mongodb: {
provider: "mongodb",
envVarName: "DATABASE_URL",
connectionStringExample: "mongodb://localhost:27017/mydb",
},
};
const ORM_CONFIGS: Record<ORM, ORMConfig> = {
prisma: {
adapterImport: `import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@prisma/client";`,
adapterSetup: (dbProvider: string) => `prismaAdapter(prisma, {
provider: "${dbProvider}",
})`,
schemaCommand:
"npx @better-auth/cli generate --output prisma/schema.prisma",
pushCommand: "npx prisma db push",
},
drizzle: {
adapterImport: `import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "./db";`,
adapterSetup: (dbProvider: string) => `drizzleAdapter(db, {
provider: "${getShortProvider(dbProvider)}",
})`,
schemaCommand: "npx @better-auth/cli generate --output src/lib/schema.ts",
pushCommand: "npx drizzle-kit push",
},
none: {
adapterImport: "",
adapterSetup: (dbProvider: string) => `{
provider: "${getShortProvider(dbProvider)}",
url: process.env.DATABASE_URL,
}`,
pushCommand: "npx @better-auth/cli migrate",
},
};
function getShortProvider(provider: string): string {
switch (provider) {
case "postgresql":
return "pg";
case "mysql":
return "mysql";
case "sqlite":
return "sqlite";
case "mongodb":
return "mongodb";
default:
return provider;
}
}
export function generateDatabaseConfig(
database: Database,
orm: ORM,
): { imports: string; config: string; prismaInstance?: string } {
if (orm === "drizzle" && database === "mongodb") {
throw new Error(
"Drizzle ORM does not support MongoDB. Please select Prisma or use the built-in MongoDB adapter instead.",
);
}
const dbConfig = DATABASE_CONFIGS[database];
const ormConfig = ORM_CONFIGS[orm];
let imports = "";
let prismaInstance = "";
if (orm === "prisma") {
imports = ormConfig.adapterImport;
prismaInstance = "\nconst prisma = new PrismaClient();\n";
} else if (orm === "drizzle") {
imports = ormConfig.adapterImport;
}
const config = ormConfig.adapterSetup(dbConfig.provider);
return { imports, config, prismaInstance };
}
export function getDatabaseEnvVar(database: Database): EnvVar {
const config = DATABASE_CONFIGS[database];
return {
name: config.envVarName,
description: `${database.charAt(0).toUpperCase() + database.slice(1)} connection string`,
required: true,
example: config.connectionStringExample,
};
}
export function getDatabaseCommands(
orm: ORM,
): { command: string; description: string; when?: string }[] {
const ormConfig = ORM_CONFIGS[orm];
const commands: { command: string; description: string; when?: string }[] =
[];
if (ormConfig.schemaCommand) {
commands.push({
command: ormConfig.schemaCommand,
description: `Generate ${orm === "prisma" ? "Prisma" : "Drizzle"} schema for auth tables`,
});
}
commands.push({
command: ormConfig.pushCommand,
description:
orm === "none"
? "Run database migrations for auth tables"
: `Push ${orm === "prisma" ? "Prisma" : "Drizzle"} schema to database`,
when: "After setting DATABASE_URL",
});
return commands;
}

View File

@@ -0,0 +1,224 @@
import type { EnvVar, Feature, PluginConfig } from "../types.js";
export function generateSocialProviderConfig(provider: string): string {
const upperName = provider.toUpperCase();
return `${provider}: {
clientId: process.env.${upperName}_CLIENT_ID!,
clientSecret: process.env.${upperName}_CLIENT_SECRET!,
}`;
}
export function getSocialProviderEnvVars(provider: string): EnvVar[] {
const upperName = provider.toUpperCase();
const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
return [
{
name: `${upperName}_CLIENT_ID`,
description: `${providerName} OAuth client ID`,
required: true,
howToGet: `Create OAuth app at ${providerName} developer console`,
},
{
name: `${upperName}_CLIENT_SECRET`,
description: `${providerName} OAuth client secret`,
required: true,
howToGet: `Create OAuth app at ${providerName} developer console`,
},
];
}
const KNOWN_SOCIAL_PROVIDERS = [
"google",
"github",
"apple",
"discord",
"twitter",
"facebook",
"microsoft",
"linkedin",
"spotify",
"twitch",
"slack",
"gitlab",
"reddit",
"dropbox",
"tiktok",
] as const;
const PLUGIN_CONFIGS: Record<string, PluginConfig> = {
"2fa": {
serverImport: 'import { twoFactor } from "better-auth/plugins";',
clientImport:
'import { twoFactorClient } from "better-auth/client/plugins";',
serverPlugin: () => `twoFactor()`,
clientPlugin: () => `twoFactorClient()`,
},
organization: {
serverImport: 'import { organization } from "better-auth/plugins";',
clientImport:
'import { organizationClient } from "better-auth/client/plugins";',
serverPlugin: () => `organization()`,
clientPlugin: () => `organizationClient()`,
},
admin: {
serverImport: 'import { admin } from "better-auth/plugins";',
clientImport: 'import { adminClient } from "better-auth/client/plugins";',
serverPlugin: () => `admin()`,
clientPlugin: () => `adminClient()`,
},
username: {
serverImport: 'import { username } from "better-auth/plugins";',
clientImport:
'import { usernameClient } from "better-auth/client/plugins";',
serverPlugin: () => `username()`,
clientPlugin: () => `usernameClient()`,
},
"multi-session": {
serverImport: 'import { multiSession } from "better-auth/plugins";',
clientImport:
'import { multiSessionClient } from "better-auth/client/plugins";',
serverPlugin: () => `multiSession()`,
clientPlugin: () => `multiSessionClient()`,
},
"api-key": {
serverImport: 'import { apiKey } from "better-auth/plugins";',
clientImport: 'import { apiKeyClient } from "better-auth/client/plugins";',
serverPlugin: () => `apiKey()`,
clientPlugin: () => `apiKeyClient()`,
},
bearer: {
serverImport: 'import { bearer } from "better-auth/plugins";',
serverPlugin: () => `bearer()`,
},
jwt: {
serverImport: 'import { jwt } from "better-auth/plugins";',
serverPlugin: () => `jwt()`,
},
"magic-link": {
serverImport: 'import { magicLink } from "better-auth/plugins";',
clientImport:
'import { magicLinkClient } from "better-auth/client/plugins";',
serverPlugin: () => `magicLink({
sendMagicLink: async ({ email, url }) => {
// TODO: Send magic link email
console.log("Send magic link to", email, url);
},
})`,
clientPlugin: () => `magicLinkClient()`,
},
"phone-number": {
serverImport: 'import { phoneNumber } from "better-auth/plugins";',
clientImport:
'import { phoneNumberClient } from "better-auth/client/plugins";',
serverPlugin: () => `phoneNumber({
sendOTP: async ({ phoneNumber, code }) => {
// TODO: Send OTP via SMS
console.log("Send OTP", code, "to", phoneNumber);
},
})`,
clientPlugin: () => `phoneNumberClient()`,
},
passkey: {
serverImport: 'import { passkey } from "better-auth/plugins";',
clientImport: 'import { passkeyClient } from "better-auth/client/plugins";',
serverPlugin: () => `passkey()`,
clientPlugin: () => `passkeyClient()`,
},
anonymous: {
serverImport: 'import { anonymous } from "better-auth/plugins";',
clientImport:
'import { anonymousClient } from "better-auth/client/plugins";',
serverPlugin: () => `anonymous()`,
clientPlugin: () => `anonymousClient()`,
},
captcha: {
serverImport: 'import { captcha } from "better-auth/plugins";',
serverPlugin: () => `captcha({
provider: "cloudflare-turnstile", // or "recaptcha" or "hcaptcha"
secretKey: process.env.CAPTCHA_SECRET_KEY!,
})`,
envVars: [
{
name: "CAPTCHA_SECRET_KEY",
description: "Captcha provider secret key",
required: true,
},
],
},
};
export function generatePluginImports(plugins: string[]): {
serverImports: string[];
clientImports: string[];
} {
const serverImports: string[] = [];
const clientImports: string[] = [];
for (const plugin of plugins) {
const config = PLUGIN_CONFIGS[plugin];
if (config) {
serverImports.push(config.serverImport);
if (config.clientImport) {
clientImports.push(config.clientImport);
}
}
}
return { serverImports, clientImports };
}
export function generatePluginSetup(plugins: string[]): {
serverPlugins: string[];
clientPlugins: string[];
} {
const serverPlugins: string[] = [];
const clientPlugins: string[] = [];
for (const plugin of plugins) {
const config = PLUGIN_CONFIGS[plugin];
if (config) {
serverPlugins.push(config.serverPlugin());
if (config.clientPlugin) {
clientPlugins.push(config.clientPlugin());
}
}
}
return { serverPlugins, clientPlugins };
}
export function getPluginEnvVars(plugins: string[]): EnvVar[] {
const envVars: EnvVar[] = [];
for (const plugin of plugins) {
const config = PLUGIN_CONFIGS[plugin];
if (config?.envVars) {
envVars.push(...config.envVars);
}
}
return envVars;
}
export function categorizeFeatures(features: Feature[]): {
socialProviders: string[];
plugins: string[];
hasEmailPassword: boolean;
} {
const socialProviders: string[] = [];
const plugins: string[] = [];
let hasEmailPassword = false;
for (const feature of features) {
if (feature === "email-password") {
hasEmailPassword = true;
} else if (feature in PLUGIN_CONFIGS) {
plugins.push(feature);
} else {
socialProviders.push(feature);
}
}
return { socialProviders, plugins, hasEmailPassword };
}

View File

@@ -0,0 +1,274 @@
import type { Framework, FrameworkConfig } from "../types.js";
export const FRAMEWORK_CONFIGS: Record<Framework, FrameworkConfig> = {
"next-app-router": {
name: "Next.js (App Router)",
defaultSrcDir: false,
defaultAuthPath: "lib/auth",
defaultApiPath: "app/api/auth/[...all]",
clientImport: 'import { createAuthClient } from "better-auth/react";',
handlerImport: 'import { toNextJsHandler } from "better-auth/next-js";',
handlerFunction: "toNextJsHandler",
apiRouteTemplate: (
authPath: string,
) => `import { auth } from "@/${authPath}";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);`,
defaultPort: 3000,
},
"next-pages-router": {
name: "Next.js (Pages Router)",
defaultSrcDir: false,
defaultAuthPath: "lib/auth",
defaultApiPath: "pages/api/auth/[...all]",
clientImport: 'import { createAuthClient } from "better-auth/react";',
handlerImport: 'import { toNodeHandler } from "better-auth/node";',
handlerFunction: "toNodeHandler",
apiRouteTemplate: (
authPath: string,
) => `import { toNodeHandler } from "better-auth/node";
import { auth } from "@/${authPath}";
export const config = { api: { bodyParser: false } };
export default toNodeHandler(auth);`,
defaultPort: 3000,
},
sveltekit: {
name: "SvelteKit",
defaultSrcDir: true,
defaultAuthPath: "lib/auth",
defaultApiPath: "routes/api/auth/[...all]",
clientImport: 'import { createAuthClient } from "better-auth/svelte";',
handlerImport: 'import { svelteKitHandler } from "better-auth/svelte-kit";',
handlerFunction: "svelteKitHandler",
apiRouteTemplate: () => "",
hooksTemplate: (authPath: string) => `import { auth } from "$${authPath}";
import { svelteKitHandler } from "better-auth/svelte-kit";
import type { Handle } from "@sveltejs/kit";
export const handle: Handle = async ({ event, resolve }) => {
return svelteKitHandler({ event, resolve, auth });
};`,
defaultPort: 5173,
},
astro: {
name: "Astro",
defaultSrcDir: true,
defaultAuthPath: "lib/auth",
defaultApiPath: "pages/api/auth/[...all]",
clientImport: 'import { createAuthClient } from "better-auth/client";',
handlerImport: "",
handlerFunction: "",
apiRouteTemplate: (
authPath: string,
) => `import type { APIRoute } from "astro";
import { auth } from "@/${authPath}";
export const GET: APIRoute = async (ctx) => {
return auth.handler(ctx.request);
};
export const POST: APIRoute = async (ctx) => {
return auth.handler(ctx.request);
};`,
defaultPort: 4321,
},
remix: {
name: "Remix",
defaultSrcDir: true,
defaultAuthPath: "lib/auth",
defaultApiPath: "routes/api.auth.$",
clientImport: 'import { createAuthClient } from "better-auth/react";',
handlerImport: "",
handlerFunction: "",
apiRouteTemplate: (
authPath: string,
) => `import { auth } from "~/${authPath}";
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
export async function loader({ request }: LoaderFunctionArgs) {
return auth.handler(request);
}
export async function action({ request }: ActionFunctionArgs) {
return auth.handler(request);
}`,
defaultPort: 3000,
},
nuxt: {
name: "Nuxt 3",
defaultSrcDir: false,
defaultAuthPath: "lib/auth",
defaultApiPath: "server/api/auth/[...all]",
clientImport: 'import { createAuthClient } from "better-auth/vue";',
handlerImport: "",
handlerFunction: "",
apiRouteTemplate: (
authPath: string,
) => `import { auth } from "~/${authPath}";
export default defineEventHandler((event) => {
return auth.handler(toWebRequest(event));
});`,
defaultPort: 3000,
},
"solid-start": {
name: "SolidStart",
defaultSrcDir: true,
defaultAuthPath: "lib/auth",
defaultApiPath: "routes/api/auth/[...all]",
clientImport: 'import { createAuthClient } from "better-auth/solid";',
handlerImport: "",
handlerFunction: "",
apiRouteTemplate: (
authPath: string,
) => `import { auth } from "~/${authPath}";
import type { APIEvent } from "@solidjs/start/server";
export async function GET(event: APIEvent) {
return auth.handler(event.request);
}
export async function POST(event: APIEvent) {
return auth.handler(event.request);
}`,
defaultPort: 3000,
},
hono: {
name: "Hono",
defaultSrcDir: true,
defaultAuthPath: "lib/auth",
defaultApiPath: "",
clientImport: 'import { createAuthClient } from "better-auth/client";',
handlerImport: "",
handlerFunction: "",
apiRouteTemplate: (authPath: string) => `import { Hono } from "hono";
import { auth } from "./${authPath}";
const app = new Hono();
app.on(["POST", "GET"], "/api/auth/**", (c) => {
return auth.handler(c.req.raw);
});
export default app;`,
defaultPort: 3000,
},
express: {
name: "Express.js",
defaultSrcDir: true,
defaultAuthPath: "lib/auth",
defaultApiPath: "",
clientImport: 'import { createAuthClient } from "better-auth/client";',
handlerImport: 'import { toNodeHandler } from "better-auth/node";',
handlerFunction: "toNodeHandler",
apiRouteTemplate: (authPath: string) => `import express from "express";
import { toNodeHandler } from "better-auth/node";
import { auth } from "./${authPath}";
const app = express();
app.all("/api/auth/*", toNodeHandler(auth));
app.listen(3000, () => {
console.log("Server running on port 3000");
});`,
defaultPort: 3000,
},
fastify: {
name: "Fastify",
defaultSrcDir: true,
defaultAuthPath: "lib/auth",
defaultApiPath: "",
clientImport: 'import { createAuthClient } from "better-auth/client";',
handlerImport: 'import { toNodeHandler } from "better-auth/node";',
handlerFunction: "toNodeHandler",
apiRouteTemplate: (authPath: string) => `import Fastify from "fastify";
import { toNodeHandler } from "better-auth/node";
import { auth } from "./${authPath}";
const fastify = Fastify();
fastify.all("/api/auth/*", async (request, reply) => {
const handler = toNodeHandler(auth);
return handler(request.raw, reply.raw);
});
fastify.listen({ port: 3000 }, (err) => {
if (err) throw err;
console.log("Server running on port 3000");
});`,
defaultPort: 3000,
},
elysia: {
name: "Elysia (Bun)",
defaultSrcDir: true,
defaultAuthPath: "lib/auth",
defaultApiPath: "",
clientImport: 'import { createAuthClient } from "better-auth/client";',
handlerImport: "",
handlerFunction: "",
apiRouteTemplate: (authPath: string) => `import { Elysia } from "elysia";
import { auth } from "./${authPath}";
const app = new Elysia()
.all("/api/auth/*", ({ request }) => auth.handler(request))
.listen(3000);
console.log("Server running on port 3000");`,
defaultPort: 3000,
},
"tanstack-start": {
name: "TanStack Start",
defaultSrcDir: true,
defaultAuthPath: "lib/auth",
defaultApiPath: "routes/api/auth.$",
clientImport: 'import { createAuthClient } from "better-auth/react";',
handlerImport: "",
handlerFunction: "",
apiRouteTemplate: (
authPath: string,
) => `import { auth } from "~/${authPath}";
import { createAPIFileRoute } from "@tanstack/start/api";
export const Route = createAPIFileRoute("/api/auth/$")({
GET: ({ request }) => auth.handler(request),
POST: ({ request }) => auth.handler(request),
});`,
defaultPort: 3000,
},
expo: {
name: "Expo (React Native)",
defaultSrcDir: true,
defaultAuthPath: "lib/auth",
defaultApiPath: "",
clientImport: 'import { createAuthClient } from "@better-auth/expo";',
handlerImport: "",
handlerFunction: "",
apiRouteTemplate: () => "",
defaultPort: 8081,
},
};
export function getDefaultAuthPath(
framework: Framework,
srcDir: boolean,
): string {
const config = FRAMEWORK_CONFIGS[framework];
const basePath = config.defaultAuthPath;
return srcDir ? `src/${basePath}` : basePath;
}
export function getDefaultApiPath(
framework: Framework,
srcDir: boolean,
): string {
const config = FRAMEWORK_CONFIGS[framework];
const basePath = config.defaultApiPath;
if (!basePath) return "";
return srcDir ? `src/${basePath}` : basePath;
}

View File

@@ -0,0 +1,167 @@
export type Framework =
| "next-app-router"
| "next-pages-router"
| "sveltekit"
| "astro"
| "remix"
| "nuxt"
| "solid-start"
| "hono"
| "express"
| "fastify"
| "elysia"
| "tanstack-start"
| "expo";
export type Database = "postgres" | "mysql" | "sqlite" | "mongodb";
export type ORM = "prisma" | "drizzle" | "none";
export type Feature =
| "email-password"
| "magic-link"
| "phone-number"
| "passkey"
| "anonymous"
| "google"
| "github"
| "apple"
| "discord"
| "twitter"
| "facebook"
| "microsoft"
| "linkedin"
| "2fa"
| "captcha"
| "organization"
| "admin"
| "username"
| "multi-session"
| "api-key"
| "bearer"
| "jwt";
export interface ExistingSetup {
authConfig?: string;
authClientConfig?: string;
envVars?: string[];
}
export interface SetupAuthInput {
framework: Framework;
database: Database;
orm?: ORM;
features?: Feature[];
typescript?: boolean;
srcDir?: boolean;
authPath?: string;
apiPath?: string;
existingSetup?: ExistingSetup;
}
export type FileAction = "create" | "update" | "no_change";
export type ChangeType =
| "add_import"
| "add_to_config"
| "add_plugin"
| "replace";
export type EnvVarStatus = "new" | "existing";
export interface FileChange {
type: ChangeType;
location?: string;
content: string;
description: string;
}
export interface OutputFile {
path: string;
description: string;
action: FileAction;
content?: string;
changes?: FileChange[];
}
export interface EnvVar {
name: string;
description: string;
required: boolean;
status?: EnvVarStatus;
defaultValue?: string;
howToGet?: string;
example?: string;
}
export interface Command {
command: string;
description: string;
when?: string;
}
export interface DocLink {
title: string;
url: string;
}
export interface DetectedConfig {
framework?: string;
database?: string;
orm?: string;
features: string[];
}
export interface SetupAuthOutput {
mode: "create" | "update";
files: OutputFile[];
envVars: EnvVar[];
commands: Command[];
detected?: DetectedConfig;
nextSteps: string[];
warnings?: string[];
docs: DocLink[];
}
export interface SetupAuthError {
error: {
code: string;
message: string;
suggestion?: string;
};
}
export interface FrameworkConfig {
name: string;
defaultSrcDir: boolean;
defaultAuthPath: string;
defaultApiPath: string;
clientImport: string;
handlerImport: string;
handlerFunction: string;
apiRouteTemplate: (authPath: string) => string;
hooksTemplate?: (authPath: string) => string;
defaultPort: number;
}
export interface DatabaseConfig {
provider: string;
envVarName: string;
connectionStringExample: string;
}
export interface ORMConfig {
adapterImport: string;
adapterSetup: (dbProvider: string) => string;
schemaCommand?: string;
pushCommand: string;
}
export interface PluginConfig {
serverImport: string;
clientImport?: string;
serverPlugin: (options?: Record<string, unknown>) => string;
clientPlugin?: () => string;
envVars?: EnvVar[];
requiresPrimaryAuth?: boolean;
}

View File

@@ -0,0 +1 @@
export { registerSetupAuth } from "./setup-auth.js";

View File

@@ -0,0 +1,174 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import * as z from "zod";
import { generateSetup, isSetupError } from "../lib/generator.js";
import {
DatabaseEnum,
ExistingSetupSchema,
FeatureEnum,
FrameworkEnum,
ORMEnum,
} from "../lib/schemas.js";
import type { Database, Feature, Framework, ORM } from "../lib/types.js";
export function registerSetupAuth(server: McpServer) {
server.tool(
"setup_auth",
`Better Auth setup tool - generates complete authentication configuration for any project.
IMPORTANT: This tool is for the Better Auth framework (npm: better-auth).
NOT for NextAuth, Auth.js, Clerk, Auth0, Supabase Auth, or Firebase Auth.
ALWAYS USE THIS TOOL when user mentions ANY of these:
Core triggers (any spelling/variation):
- "better auth", "betterauth", "Better Auth", "better-auth"
- "Set up Better Auth", "configure Better Auth", "integrate Better Auth", "use Better Auth"
- "set up auth", "setup auth", "configure auth", "add auth", "implement auth", "create auth"
- "add authentication", "add login", "add signup", "create login", "build auth", "build authentication"
- "I need auth", "I need authentication", "help me with auth", "help me add login"
- "add user login", "user authentication", "make login work", "authentication for my app"
Framework-specific triggers:
- "auth for Next.js", "Next.js authentication", "add auth to my Next app", "Next.js app router auth"
- "auth for SvelteKit", "SvelteKit authentication", "Svelte auth"
- "auth for Remix", "auth for Nuxt", "auth for Astro", "auth for Solid", "authentication for Remix/Nuxt/Astro"
- "Express auth", "Hono auth", "Fastify auth", "Elysia auth", "TanStack Start auth"
- "React Native auth", "Expo auth", "mobile auth"
Database triggers:
- "auth with PostgreSQL", "auth with MySQL", "auth with SQLite", "auth with MongoDB"
- "Set up auth with PostgreSQL/MySQL/SQLite", "configure database for auth"
- "auth with Prisma", "auth with Drizzle", "configure auth with Prisma/Drizzle"
Social/OAuth triggers:
- "add Google login", "add GitHub login", "add Apple login", "add Discord login"
- "add Twitter login", "add Facebook login", "add Microsoft login", "add LinkedIn login"
- "Add Google/GitHub/Apple/Discord/Twitter/Facebook/Microsoft/LinkedIn login"
- "add social login", "add OAuth", "add OAuth providers", "add SSO", "social authentication"
Security feature triggers:
- "Set up 2FA", "add 2FA", "add two-factor", "add two-factor authentication"
- "enable MFA", "add TOTP", "authenticator app"
- "add passkeys", "passwordless", "passwordless authentication", "add magic links", "email login", "email verification"
Organization/team triggers:
- "add organizations", "Add organization support", "enable multi-tenancy", "multi-tenancy"
- "team management", "workspaces"
API/token triggers:
- "add API keys", "Add API key authentication", "bearer tokens", "JWT auth", "JWT authentication"
Admin/user triggers:
- "add admin panel", "admin auth", "admin authentication"
- "Add user management", "user management", "handle sessions", "session management"
- "Add username login", "username login", "phone auth", "phone number auth", "anonymous auth"
OUTPUT: Returns all files, environment variables, and terminal commands needed.
One tool call = complete auth setup ready to copy-paste.`,
{
framework: FrameworkEnum.describe(
"The web framework being used. Detect from package.json or user's message. Examples: 'next-app-router' for Next.js 13+, 'next-pages-router' for Next.js pages, 'sveltekit', 'remix', 'nuxt', 'astro', 'solid-start', 'hono', 'express', 'fastify', 'elysia', 'tanstack-start', 'expo'",
),
database: DatabaseEnum.describe(
"The database type. Detect from user's message or project config. Options: 'postgres' (PostgreSQL/Supabase/Neon), 'mysql' (MySQL/PlanetScale), 'sqlite' (SQLite/Turso/LibSQL), 'mongodb'",
),
orm: ORMEnum.optional()
.default("none")
.describe(
"ORM being used - affects adapter imports. Options: 'prisma', 'drizzle', 'none'. Detect from package.json or user's message.",
),
features: z
.array(FeatureEnum)
.optional()
.default(["email-password"])
.describe(
"Auth features to enable. Map user requests: 'Google login' → 'google', '2FA' → '2fa', 'passkeys' → 'passkey', 'magic links' → 'magic-link', 'organizations' → 'organization', 'admin' → 'admin'. Default: ['email-password']",
),
typescript: z
.boolean()
.optional()
.default(true)
.describe(
"Generate TypeScript (.ts) or JavaScript (.js). Default: true (TypeScript)",
),
srcDir: z
.boolean()
.optional()
.describe(
"Whether project uses src/ directory structure. Check if src/ folder exists. Default: false. Provide true for frameworks that store app files in src/.",
),
authPath: z
.string()
.optional()
.describe(
"Where to create auth.ts file. Auto-detected based on framework. Override only if user specifies custom path.",
),
apiPath: z
.string()
.optional()
.describe(
"API route path for auth handler. Auto-detected based on framework. Override only if user specifies custom path.",
),
existingSetup: ExistingSetupSchema.optional().describe(
"For INCREMENTAL updates only. Pass existing auth.ts and auth-client.ts contents to add new features without overwriting. Read these files first if they exist.",
),
},
async (input) => {
try {
const result = generateSetup({
framework: input.framework as Framework,
database: input.database as Database,
orm: (input.orm || "none") as ORM,
features: (input.features || ["email-password"]) as Feature[],
typescript: input.typescript ?? true,
srcDir: input.srcDir ?? false,
authPath: input.authPath,
apiPath: input.apiPath,
existingSetup: input.existingSetup,
});
if (isSetupError(result)) {
return {
content: [
{
type: "text" as const,
text: JSON.stringify(result, null, 2),
},
],
isError: true,
};
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const message =
error instanceof Error ? error.message : "Unknown error";
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
error: {
code: "SETUP_ERROR",
message,
},
},
null,
2,
),
},
],
isError: true,
};
}
},
);
}

View File

@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": ["esnext"]
}
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from "tsdown";
export default defineConfig({
dts: { build: true },
format: ["esm"],
entry: ["./src/index.ts"],
});

56
pnpm-lock.yaml generated
View File

@@ -1367,6 +1367,22 @@ importers:
specifier: 'catalog:'
version: 0.17.2(@arethetypeswrong/core@0.18.2)(oxc-resolver@11.16.2)(publint@0.3.16)(synckit@0.11.11)(typescript@5.9.3)
packages/mcp:
dependencies:
'@modelcontextprotocol/sdk':
specifier: ^1.8.0
version: 1.25.2(hono@4.11.3)(zod@3.25.76)
zod:
specifier: ^3.24.2
version: 3.25.76
devDependencies:
tsdown:
specifier: 'catalog:'
version: 0.17.2(@arethetypeswrong/core@0.18.2)(oxc-resolver@11.16.2)(publint@0.3.16)(synckit@0.11.11)(typescript@5.9.3)
typescript:
specifier: 'catalog:'
version: 5.9.3
packages/oauth-provider:
dependencies:
'@better-auth/utils':
@@ -10211,6 +10227,10 @@ packages:
resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==}
engines: {node: '>=0.10.0'}
iconv-lite@0.7.2:
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
engines: {node: '>=0.10.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
@@ -17884,6 +17904,28 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@modelcontextprotocol/sdk@1.25.2(hono@4.11.3)(zod@3.25.76)':
dependencies:
'@hono/node-server': 1.19.7(hono@4.11.3)
ajv: 8.17.1
ajv-formats: 3.0.1(ajv@8.17.1)
content-type: 1.0.5
cors: 2.8.5
cross-spawn: 7.0.6
eventsource: 3.0.7
eventsource-parser: 3.0.6
express: 5.2.1
express-rate-limit: 7.5.1(express@5.2.1)
jose: 6.1.3
json-schema-typed: 8.0.2
pkce-challenge: 5.0.1
raw-body: 3.0.2
zod: 3.25.76
zod-to-json-schema: 3.25.1(zod@3.25.76)
transitivePeerDependencies:
- hono
- supports-color
'@modelcontextprotocol/sdk@1.25.2(hono@4.11.3)(zod@4.3.4)':
dependencies:
'@hono/node-server': 1.19.7(hono@4.11.3)
@@ -22025,7 +22067,7 @@ snapshots:
content-type: 1.0.5
debug: 4.4.3
http-errors: 2.0.1
iconv-lite: 0.7.1
iconv-lite: 0.7.2
on-finished: 2.4.1
qs: 6.14.1
raw-body: 3.0.2
@@ -24825,6 +24867,10 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
iconv-lite@0.7.2:
dependencies:
safer-buffer: 2.1.2
ieee754@1.2.1: {}
ignore@5.3.2: {}
@@ -26740,7 +26786,7 @@ snapshots:
aws-ssl-profiles: 1.1.2
denque: 2.1.0
generate-function: 2.3.1
iconv-lite: 0.7.1
iconv-lite: 0.7.2
long: 5.3.2
lru.min: 1.1.3
named-placeholders: 1.1.6
@@ -27838,7 +27884,7 @@ snapshots:
dependencies:
bytes: 3.1.2
http-errors: 2.0.1
iconv-lite: 0.7.1
iconv-lite: 0.7.2
unpipe: 1.0.0
rc9@2.1.2:
@@ -30600,6 +30646,10 @@ snapshots:
compress-commons: 6.0.2
readable-stream: 4.7.0
zod-to-json-schema@3.25.1(zod@3.25.76):
dependencies:
zod: 3.25.76
zod-to-json-schema@3.25.1(zod@4.3.4):
dependencies:
zod: 4.3.4