mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-25 16:36:34 -05:00
feat(mcp): add setup_auth tool (#7307)
This commit is contained in:
committed by
Alex Yang
parent
5643f3caec
commit
33aaacef8e
@@ -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
53
packages/mcp/package.json
Normal 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
20
packages/mcp/src/index.ts
Normal 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);
|
||||
467
packages/mcp/src/lib/generator.ts
Normal file
467
packages/mcp/src/lib/generator.ts
Normal 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;
|
||||
}
|
||||
111
packages/mcp/src/lib/parser.ts
Normal file
111
packages/mcp/src/lib/parser.ts
Normal 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 };
|
||||
}
|
||||
61
packages/mcp/src/lib/schemas.ts
Normal file
61
packages/mcp/src/lib/schemas.ts
Normal 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"),
|
||||
});
|
||||
140
packages/mcp/src/lib/templates/database.ts
Normal file
140
packages/mcp/src/lib/templates/database.ts
Normal 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;
|
||||
}
|
||||
224
packages/mcp/src/lib/templates/features.ts
Normal file
224
packages/mcp/src/lib/templates/features.ts
Normal 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 };
|
||||
}
|
||||
274
packages/mcp/src/lib/templates/frameworks.ts
Normal file
274
packages/mcp/src/lib/templates/frameworks.ts
Normal 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;
|
||||
}
|
||||
167
packages/mcp/src/lib/types.ts
Normal file
167
packages/mcp/src/lib/types.ts
Normal 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;
|
||||
}
|
||||
1
packages/mcp/src/tools/index.ts
Normal file
1
packages/mcp/src/tools/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { registerSetupAuth } from "./setup-auth.js";
|
||||
174
packages/mcp/src/tools/setup-auth.ts
Normal file
174
packages/mcp/src/tools/setup-auth.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
6
packages/mcp/tsconfig.json
Normal file
6
packages/mcp/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext"]
|
||||
}
|
||||
}
|
||||
7
packages/mcp/tsdown.config.ts
Normal file
7
packages/mcp/tsdown.config.ts
Normal 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
56
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user