@better-auth/mcp 1.5.0-beta.8 → 1.7.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,1039 +1,214 @@
1
- #!/usr/bin/env node
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import * as z from "zod";
5
-
6
- //#region src/lib/parser.ts
7
- function parseExistingSetup(existingSetup) {
8
- const detected = { features: [] };
9
- if (existingSetup.authConfig) {
10
- const config = existingSetup.authConfig;
11
- detected.database = detectDatabase(config);
12
- detected.orm = detectORM(config);
13
- detected.features = detectFeatures(config, existingSetup.authClientConfig);
14
- }
15
- return detected;
16
- }
17
- function detectDatabase(config) {
18
- if (/provider:\s*["']postgresql["']/.test(config) || /provider:\s*["']pg["']/.test(config)) return "postgres";
19
- if (/provider:\s*["']mysql["']/.test(config)) return "mysql";
20
- if (/provider:\s*["']sqlite["']/.test(config)) return "sqlite";
21
- if (/provider:\s*["']mongodb["']/.test(config)) return "mongodb";
22
- }
23
- function detectORM(config) {
24
- if (/prismaAdapter\(/.test(config)) return "prisma";
25
- if (/drizzleAdapter\(/.test(config)) return "drizzle";
26
- if (/database:\s*\{[\s\S]*provider:/.test(config)) return "none";
27
- }
28
- function detectFeatures(serverConfig, clientConfig) {
29
- const features = [];
30
- if (/emailAndPassword:\s*\{[\s\S]*enabled:\s*true/.test(serverConfig)) features.push("email-password");
31
- for (const provider of [
32
- "google",
33
- "github",
34
- "apple",
35
- "discord",
36
- "twitter",
37
- "facebook",
38
- "microsoft",
39
- "linkedin"
40
- ]) if ((/* @__PURE__ */ new RegExp(`${provider}:\\s*\\{`)).test(serverConfig)) features.push(provider);
41
- for (const [pattern, feature] of [
42
- [/twoFactor\(/, "2fa"],
43
- [/organization\(/, "organization"],
44
- [/admin\(/, "admin"],
45
- [/username\(/, "username"],
46
- [/multiSession\(/, "multi-session"],
47
- [/apiKey\(/, "api-key"],
48
- [/bearer\(/, "bearer"],
49
- [/jwt\(/, "jwt"],
50
- [/magicLink\(/, "magic-link"],
51
- [/phoneNumber\(/, "phone-number"],
52
- [/passkey\(/, "passkey"],
53
- [/anonymous\(/, "anonymous"],
54
- [/captcha\(/, "captcha"]
55
- ]) if (pattern.test(serverConfig)) features.push(feature);
56
- return features;
57
- }
58
- function computeFeatureDiff(existing, requested) {
59
- return {
60
- toAdd: requested.filter((f) => !existing.includes(f)),
61
- existing: requested.filter((f) => existing.includes(f))
1
+ import { ResourceUriSchema, getIssuer, metadataResponse, oauthProvider, raiseResourceServerChallenge } from "@better-auth/oauth-provider";
2
+ import { DPOP_SIGNING_ALGORITHMS, createDpopReplayStore, requestToResourceInput, verifyAccessTokenRequest } from "better-auth/oauth2";
3
+ import { APIError } from "better-call";
4
+ //#region src/handler.ts
5
+ /**
6
+ * A request middleware handler that verifies an MCP access token and responds
7
+ * with an RFC 9728 `WWW-Authenticate` header for unauthenticated requests.
8
+ *
9
+ * @external
10
+ */
11
+ const mcpHandler = (verifyOptions, handler, opts) => {
12
+ return async (req) => {
13
+ try {
14
+ return handler(req, await verifyAccessTokenRequest(requestToResourceInput(req), verifyOptions));
15
+ } catch (error) {
16
+ try {
17
+ raiseResourceServerChallenge(error, verifyOptions.verifyOptions.audience, {
18
+ ...opts,
19
+ dpopSigningAlgorithms: verifyOptions.dpop?.signingAlgorithms
20
+ });
21
+ } catch (err) {
22
+ if (err instanceof APIError) {
23
+ const headers = new Headers(err.headers);
24
+ headers.set("Content-Type", "application/json");
25
+ return new Response(JSON.stringify({
26
+ jsonrpc: "2.0",
27
+ error: {
28
+ code: -32e3,
29
+ message: err.message
30
+ },
31
+ id: null
32
+ }), {
33
+ status: err.statusCode,
34
+ headers
35
+ });
36
+ }
37
+ throw new Error(String(err));
38
+ }
39
+ throw new Error(String(error));
40
+ }
62
41
  };
63
- }
64
-
65
- //#endregion
66
- //#region src/lib/templates/database.ts
67
- const DATABASE_CONFIGS = {
68
- postgres: {
69
- provider: "postgresql",
70
- envVarName: "DATABASE_URL",
71
- connectionStringExample: "postgresql://user:password@localhost:5432/mydb"
72
- },
73
- mysql: {
74
- provider: "mysql",
75
- envVarName: "DATABASE_URL",
76
- connectionStringExample: "mysql://user:password@localhost:3306/mydb"
77
- },
78
- sqlite: {
79
- provider: "sqlite",
80
- envVarName: "DATABASE_URL",
81
- connectionStringExample: "file:./dev.db"
82
- },
83
- mongodb: {
84
- provider: "mongodb",
85
- envVarName: "DATABASE_URL",
86
- connectionStringExample: "mongodb://localhost:27017/mydb"
87
- }
88
42
  };
89
- const ORM_CONFIGS = {
90
- prisma: {
91
- adapterImport: `import { prismaAdapter } from "better-auth/adapters/prisma";
92
- import { PrismaClient } from "@prisma/client";`,
93
- adapterSetup: (dbProvider) => `prismaAdapter(prisma, {
94
- provider: "${dbProvider}",
95
- })`,
96
- schemaCommand: "npx @better-auth/cli generate --output prisma/schema.prisma",
97
- pushCommand: "npx prisma db push"
98
- },
99
- drizzle: {
100
- adapterImport: `import { drizzleAdapter } from "better-auth/adapters/drizzle";
101
- import { db } from "./db";`,
102
- adapterSetup: (dbProvider) => `drizzleAdapter(db, {
103
- provider: "${getShortProvider(dbProvider)}",
104
- })`,
105
- schemaCommand: "npx @better-auth/cli generate --output src/lib/schema.ts",
106
- pushCommand: "npx drizzle-kit push"
107
- },
108
- none: {
109
- adapterImport: "",
110
- adapterSetup: (dbProvider) => `{
111
- provider: "${getShortProvider(dbProvider)}",
112
- url: process.env.DATABASE_URL,
113
- }`,
114
- pushCommand: "npx @better-auth/cli migrate"
115
- }
43
+ //#endregion
44
+ //#region src/plugin.ts
45
+ const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
46
+ const AUTHORIZATION_SERVER_ONLY_SCOPES = new Set([
47
+ "openid",
48
+ "profile",
49
+ "email",
50
+ "phone",
51
+ "address",
52
+ "offline_access"
53
+ ]);
54
+ const resourceIdentifier = (resource) => typeof resource === "string" ? resource : resource.identifier;
55
+ const appendProtectedResource = (resources, resource) => {
56
+ const configuredResources = resources ?? [];
57
+ if (configuredResources.some((configuredResource) => resourceIdentifier(configuredResource) === resource)) return configuredResources;
58
+ return [...configuredResources, resource];
116
59
  };
117
- function getShortProvider(provider) {
118
- switch (provider) {
119
- case "postgresql": return "pg";
120
- case "mysql": return "mysql";
121
- case "sqlite": return "sqlite";
122
- case "mongodb": return "mongodb";
123
- default: return provider;
124
- }
125
- }
126
- function generateDatabaseConfig(database, orm) {
127
- 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.");
128
- const dbConfig = DATABASE_CONFIGS[database];
129
- const ormConfig = ORM_CONFIGS[orm];
130
- let imports = "";
131
- let prismaInstance = "";
132
- if (orm === "prisma") {
133
- imports = ormConfig.adapterImport;
134
- prismaInstance = "\nconst prisma = new PrismaClient();\n";
135
- } else if (orm === "drizzle") imports = ormConfig.adapterImport;
136
- const config = ormConfig.adapterSetup(dbConfig.provider);
137
- return {
138
- imports,
139
- config,
140
- prismaInstance
141
- };
142
- }
143
- function getDatabaseEnvVar(database) {
144
- const config = DATABASE_CONFIGS[database];
145
- return {
146
- name: config.envVarName,
147
- description: `${database.charAt(0).toUpperCase() + database.slice(1)} connection string`,
148
- required: true,
149
- example: config.connectionStringExample
60
+ /**
61
+ * Build the RFC 9728 Protected Resource Metadata document. The MCP server is the
62
+ * resource server, and its authorization server is this same provider, so
63
+ * `authorization_servers` reuses the provider issuer. Resource metadata only
64
+ * advertises scopes that apply to the protected resource itself; OIDC identity
65
+ * scopes and refresh-token scopes stay authorization-server metadata.
66
+ */
67
+ const buildResourceServerMetadata = (ctx, providerOptions, resource) => {
68
+ const resourceScopes = (providerOptions.advertisedMetadata?.scopes_supported ?? providerOptions.scopes ?? []).filter((scope) => !AUTHORIZATION_SERVER_ONLY_SCOPES.has(scope));
69
+ const configuredResource = providerOptions.resources?.find((configuredResource) => resourceIdentifier(configuredResource) === resource);
70
+ const dpopBoundAccessTokensRequired = typeof configuredResource === "object" && configuredResource.dpopBoundAccessTokensRequired === true;
71
+ const metadata = {
72
+ resource,
73
+ authorization_servers: [getIssuer(ctx, providerOptions)],
74
+ bearer_methods_supported: ["header"],
75
+ dpop_signing_alg_values_supported: [...providerOptions.dpop?.signingAlgorithms ?? DPOP_SIGNING_ALGORITHMS]
150
76
  };
151
- }
152
- function getDatabaseCommands(orm) {
153
- const ormConfig = ORM_CONFIGS[orm];
154
- const commands = [];
155
- if (ormConfig.schemaCommand) commands.push({
156
- command: ormConfig.schemaCommand,
157
- description: `Generate ${orm === "prisma" ? "Prisma" : "Drizzle"} schema for auth tables`
158
- });
159
- commands.push({
160
- command: ormConfig.pushCommand,
161
- description: orm === "none" ? "Run database migrations for auth tables" : `Push ${orm === "prisma" ? "Prisma" : "Drizzle"} schema to database`,
162
- when: "After setting DATABASE_URL"
163
- });
164
- return commands;
165
- }
166
-
167
- //#endregion
168
- //#region src/lib/templates/features.ts
169
- function generateSocialProviderConfig(provider) {
170
- const upperName = provider.toUpperCase();
171
- return `${provider}: {
172
- clientId: process.env.${upperName}_CLIENT_ID!,
173
- clientSecret: process.env.${upperName}_CLIENT_SECRET!,
174
- }`;
175
- }
176
- function getSocialProviderEnvVars(provider) {
177
- const upperName = provider.toUpperCase();
178
- const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
179
- return [{
180
- name: `${upperName}_CLIENT_ID`,
181
- description: `${providerName} OAuth client ID`,
182
- required: true,
183
- howToGet: `Create OAuth app at ${providerName} developer console`
184
- }, {
185
- name: `${upperName}_CLIENT_SECRET`,
186
- description: `${providerName} OAuth client secret`,
187
- required: true,
188
- howToGet: `Create OAuth app at ${providerName} developer console`
189
- }];
190
- }
191
- const PLUGIN_CONFIGS = {
192
- "2fa": {
193
- serverImport: "import { twoFactor } from \"better-auth/plugins\";",
194
- clientImport: "import { twoFactorClient } from \"better-auth/client/plugins\";",
195
- serverPlugin: () => `twoFactor()`,
196
- clientPlugin: () => `twoFactorClient()`
197
- },
198
- organization: {
199
- serverImport: "import { organization } from \"better-auth/plugins\";",
200
- clientImport: "import { organizationClient } from \"better-auth/client/plugins\";",
201
- serverPlugin: () => `organization()`,
202
- clientPlugin: () => `organizationClient()`
203
- },
204
- admin: {
205
- serverImport: "import { admin } from \"better-auth/plugins\";",
206
- clientImport: "import { adminClient } from \"better-auth/client/plugins\";",
207
- serverPlugin: () => `admin()`,
208
- clientPlugin: () => `adminClient()`
209
- },
210
- username: {
211
- serverImport: "import { username } from \"better-auth/plugins\";",
212
- clientImport: "import { usernameClient } from \"better-auth/client/plugins\";",
213
- serverPlugin: () => `username()`,
214
- clientPlugin: () => `usernameClient()`
215
- },
216
- "multi-session": {
217
- serverImport: "import { multiSession } from \"better-auth/plugins\";",
218
- clientImport: "import { multiSessionClient } from \"better-auth/client/plugins\";",
219
- serverPlugin: () => `multiSession()`,
220
- clientPlugin: () => `multiSessionClient()`
221
- },
222
- "api-key": {
223
- serverImport: "import { apiKey } from \"better-auth/plugins\";",
224
- clientImport: "import { apiKeyClient } from \"better-auth/client/plugins\";",
225
- serverPlugin: () => `apiKey()`,
226
- clientPlugin: () => `apiKeyClient()`
227
- },
228
- bearer: {
229
- serverImport: "import { bearer } from \"better-auth/plugins\";",
230
- serverPlugin: () => `bearer()`
231
- },
232
- jwt: {
233
- serverImport: "import { jwt } from \"better-auth/plugins\";",
234
- serverPlugin: () => `jwt()`
235
- },
236
- "magic-link": {
237
- serverImport: "import { magicLink } from \"better-auth/plugins\";",
238
- clientImport: "import { magicLinkClient } from \"better-auth/client/plugins\";",
239
- serverPlugin: () => `magicLink({
240
- sendMagicLink: async ({ email, url }) => {
241
- // TODO: Send magic link email
242
- console.log("Send magic link to", email, url);
243
- },
244
- })`,
245
- clientPlugin: () => `magicLinkClient()`
246
- },
247
- "phone-number": {
248
- serverImport: "import { phoneNumber } from \"better-auth/plugins\";",
249
- clientImport: "import { phoneNumberClient } from \"better-auth/client/plugins\";",
250
- serverPlugin: () => `phoneNumber({
251
- sendOTP: async ({ phoneNumber, code }) => {
252
- // TODO: Send OTP via SMS
253
- console.log("Send OTP", code, "to", phoneNumber);
254
- },
255
- })`,
256
- clientPlugin: () => `phoneNumberClient()`
257
- },
258
- passkey: {
259
- serverImport: "import { passkey } from \"better-auth/plugins\";",
260
- clientImport: "import { passkeyClient } from \"better-auth/client/plugins\";",
261
- serverPlugin: () => `passkey()`,
262
- clientPlugin: () => `passkeyClient()`
263
- },
264
- anonymous: {
265
- serverImport: "import { anonymous } from \"better-auth/plugins\";",
266
- clientImport: "import { anonymousClient } from \"better-auth/client/plugins\";",
267
- serverPlugin: () => `anonymous()`,
268
- clientPlugin: () => `anonymousClient()`
269
- },
270
- captcha: {
271
- serverImport: "import { captcha } from \"better-auth/plugins\";",
272
- serverPlugin: () => `captcha({
273
- provider: "cloudflare-turnstile", // or "recaptcha" or "hcaptcha"
274
- secretKey: process.env.CAPTCHA_SECRET_KEY!,
275
- })`,
276
- envVars: [{
277
- name: "CAPTCHA_SECRET_KEY",
278
- description: "Captcha provider secret key",
279
- required: true
280
- }]
281
- }
77
+ if (dpopBoundAccessTokensRequired) metadata.dpop_bound_access_tokens_required = true;
78
+ if (resourceScopes.length) metadata.scopes_supported = [...resourceScopes];
79
+ return metadata;
282
80
  };
283
- function generatePluginImports(plugins) {
284
- const serverImports = [];
285
- const clientImports = [];
286
- for (const plugin of plugins) {
287
- const config = PLUGIN_CONFIGS[plugin];
288
- if (config) {
289
- serverImports.push(config.serverImport);
290
- if (config.clientImport) clientImports.push(config.clientImport);
291
- }
292
- }
81
+ /**
82
+ * Model Context Protocol authorization server.
83
+ *
84
+ * `mcp()` is the OAuth 2.1 / OIDC provider ({@link oauthProvider}) configured for
85
+ * MCP: it enables dynamic client registration, binds issued tokens to the MCP
86
+ * `resource`, and, as the resource server, serves the RFC 9728 protected resource
87
+ * metadata so MCP clients discover and use it through standard OAuth discovery.
88
+ * Because it is the OAuth provider, it cannot be combined with a separate
89
+ * {@link oauthProvider}.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * import { betterAuth } from "better-auth";
94
+ * import { jwt } from "better-auth/plugins";
95
+ * import { mcp } from "@better-auth/mcp";
96
+ *
97
+ * export const auth = betterAuth({
98
+ * plugins: [
99
+ * jwt(),
100
+ * mcp({
101
+ * loginPage: "/login",
102
+ * consentPage: "/consent",
103
+ * resource: "https://api.example.com/mcp",
104
+ * }),
105
+ * ],
106
+ * });
107
+ * ```
108
+ */
109
+ const mcp = (options) => {
110
+ const { resource, ...oauthOptions } = options;
111
+ ResourceUriSchema.parse(resource);
112
+ const provider = oauthProvider({
113
+ allowDynamicClientRegistration: true,
114
+ allowUnauthenticatedClientRegistration: true,
115
+ ...oauthOptions,
116
+ resources: appendProtectedResource(oauthOptions.resources, resource)
117
+ });
118
+ const serveProviderDiscovery = provider.onRequest;
293
119
  return {
294
- serverImports,
295
- clientImports
296
- };
297
- }
298
- function generatePluginSetup(plugins) {
299
- const serverPlugins = [];
300
- const clientPlugins = [];
301
- for (const plugin of plugins) {
302
- const config = PLUGIN_CONFIGS[plugin];
303
- if (config) {
304
- serverPlugins.push(config.serverPlugin());
305
- if (config.clientPlugin) clientPlugins.push(config.clientPlugin());
120
+ ...provider,
121
+ onRequest: async (request, ctx) => {
122
+ const handledByProvider = await serveProviderDiscovery?.(request, ctx);
123
+ if (handledByProvider) return handledByProvider;
124
+ const pathname = new URL(request.url).pathname;
125
+ const requestPath = ctx.options.advanced?.skipTrailingSlashes ? pathname.replace(/\/+$/, "") || "/" : pathname;
126
+ let resourcePath = "";
127
+ try {
128
+ resourcePath = new URL(resource).pathname.replace(/\/$/, "");
129
+ } catch {
130
+ resourcePath = "";
131
+ }
132
+ if (!new Set([PROTECTED_RESOURCE_METADATA_PATH, `${PROTECTED_RESOURCE_METADATA_PATH}${resourcePath}`]).has(requestPath)) return;
133
+ if (request.method !== "GET" && request.method !== "HEAD") return { response: new Response(null, {
134
+ status: 405,
135
+ headers: { Allow: "GET, HEAD" }
136
+ }) };
137
+ const response = metadataResponse(buildResourceServerMetadata({ context: ctx }, oauthOptions, resource));
138
+ if (request.method === "HEAD") return { response: new Response(null, {
139
+ status: response.status,
140
+ headers: response.headers
141
+ }) };
142
+ return { response };
306
143
  }
307
- }
308
- return {
309
- serverPlugins,
310
- clientPlugins
311
- };
312
- }
313
- function getPluginEnvVars(plugins) {
314
- const envVars = [];
315
- for (const plugin of plugins) {
316
- const config = PLUGIN_CONFIGS[plugin];
317
- if (config?.envVars) envVars.push(...config.envVars);
318
- }
319
- return envVars;
320
- }
321
- function categorizeFeatures(features) {
322
- const socialProviders = [];
323
- const plugins = [];
324
- let hasEmailPassword = false;
325
- for (const feature of features) if (feature === "email-password") hasEmailPassword = true;
326
- else if (feature in PLUGIN_CONFIGS) plugins.push(feature);
327
- else socialProviders.push(feature);
328
- return {
329
- socialProviders,
330
- plugins,
331
- hasEmailPassword
332
144
  };
333
- }
334
-
335
- //#endregion
336
- //#region src/lib/templates/frameworks.ts
337
- const FRAMEWORK_CONFIGS = {
338
- "next-app-router": {
339
- name: "Next.js (App Router)",
340
- defaultSrcDir: false,
341
- defaultAuthPath: "lib/auth",
342
- defaultApiPath: "app/api/auth/[...all]",
343
- clientImport: "import { createAuthClient } from \"better-auth/react\";",
344
- handlerImport: "import { toNextJsHandler } from \"better-auth/next-js\";",
345
- handlerFunction: "toNextJsHandler",
346
- apiRouteTemplate: (authPath) => `import { auth } from "@/${authPath}";
347
- import { toNextJsHandler } from "better-auth/next-js";
348
-
349
- export const { GET, POST } = toNextJsHandler(auth);`,
350
- defaultPort: 3e3
351
- },
352
- "next-pages-router": {
353
- name: "Next.js (Pages Router)",
354
- defaultSrcDir: false,
355
- defaultAuthPath: "lib/auth",
356
- defaultApiPath: "pages/api/auth/[...all]",
357
- clientImport: "import { createAuthClient } from \"better-auth/react\";",
358
- handlerImport: "import { toNodeHandler } from \"better-auth/node\";",
359
- handlerFunction: "toNodeHandler",
360
- apiRouteTemplate: (authPath) => `import { toNodeHandler } from "better-auth/node";
361
- import { auth } from "@/${authPath}";
362
-
363
- export const config = { api: { bodyParser: false } };
364
- export default toNodeHandler(auth);`,
365
- defaultPort: 3e3
366
- },
367
- sveltekit: {
368
- name: "SvelteKit",
369
- defaultSrcDir: true,
370
- defaultAuthPath: "lib/auth",
371
- defaultApiPath: "routes/api/auth/[...all]",
372
- clientImport: "import { createAuthClient } from \"better-auth/svelte\";",
373
- handlerImport: "import { svelteKitHandler } from \"better-auth/svelte-kit\";",
374
- handlerFunction: "svelteKitHandler",
375
- apiRouteTemplate: () => "",
376
- hooksTemplate: (authPath) => `import { auth } from "$${authPath}";
377
- import { svelteKitHandler } from "better-auth/svelte-kit";
378
- import type { Handle } from "@sveltejs/kit";
379
-
380
- export const handle: Handle = async ({ event, resolve }) => {
381
- return svelteKitHandler({ event, resolve, auth });
382
- };`,
383
- defaultPort: 5173
384
- },
385
- astro: {
386
- name: "Astro",
387
- defaultSrcDir: true,
388
- defaultAuthPath: "lib/auth",
389
- defaultApiPath: "pages/api/auth/[...all]",
390
- clientImport: "import { createAuthClient } from \"better-auth/client\";",
391
- handlerImport: "",
392
- handlerFunction: "",
393
- apiRouteTemplate: (authPath) => `import type { APIRoute } from "astro";
394
- import { auth } from "@/${authPath}";
395
-
396
- export const GET: APIRoute = async (ctx) => {
397
- return auth.handler(ctx.request);
398
- };
399
-
400
- export const POST: APIRoute = async (ctx) => {
401
- return auth.handler(ctx.request);
402
- };`,
403
- defaultPort: 4321
404
- },
405
- remix: {
406
- name: "Remix",
407
- defaultSrcDir: true,
408
- defaultAuthPath: "lib/auth",
409
- defaultApiPath: "routes/api.auth.$",
410
- clientImport: "import { createAuthClient } from \"better-auth/react\";",
411
- handlerImport: "",
412
- handlerFunction: "",
413
- apiRouteTemplate: (authPath) => `import { auth } from "~/${authPath}";
414
- import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
415
-
416
- export async function loader({ request }: LoaderFunctionArgs) {
417
- return auth.handler(request);
418
- }
419
-
420
- export async function action({ request }: ActionFunctionArgs) {
421
- return auth.handler(request);
422
- }`,
423
- defaultPort: 3e3
424
- },
425
- nuxt: {
426
- name: "Nuxt 3",
427
- defaultSrcDir: false,
428
- defaultAuthPath: "lib/auth",
429
- defaultApiPath: "server/api/auth/[...all]",
430
- clientImport: "import { createAuthClient } from \"better-auth/vue\";",
431
- handlerImport: "",
432
- handlerFunction: "",
433
- apiRouteTemplate: (authPath) => `import { auth } from "~/${authPath}";
434
-
435
- export default defineEventHandler((event) => {
436
- return auth.handler(toWebRequest(event));
437
- });`,
438
- defaultPort: 3e3
439
- },
440
- "solid-start": {
441
- name: "SolidStart",
442
- defaultSrcDir: true,
443
- defaultAuthPath: "lib/auth",
444
- defaultApiPath: "routes/api/auth/[...all]",
445
- clientImport: "import { createAuthClient } from \"better-auth/solid\";",
446
- handlerImport: "",
447
- handlerFunction: "",
448
- apiRouteTemplate: (authPath) => `import { auth } from "~/${authPath}";
449
- import type { APIEvent } from "@solidjs/start/server";
450
-
451
- export async function GET(event: APIEvent) {
452
- return auth.handler(event.request);
453
- }
454
-
455
- export async function POST(event: APIEvent) {
456
- return auth.handler(event.request);
457
- }`,
458
- defaultPort: 3e3
459
- },
460
- hono: {
461
- name: "Hono",
462
- defaultSrcDir: true,
463
- defaultAuthPath: "lib/auth",
464
- defaultApiPath: "",
465
- clientImport: "import { createAuthClient } from \"better-auth/client\";",
466
- handlerImport: "",
467
- handlerFunction: "",
468
- apiRouteTemplate: (authPath) => `import { Hono } from "hono";
469
- import { auth } from "./${authPath}";
470
-
471
- const app = new Hono();
472
-
473
- app.on(["POST", "GET"], "/api/auth/**", (c) => {
474
- return auth.handler(c.req.raw);
475
- });
476
-
477
- export default app;`,
478
- defaultPort: 3e3
479
- },
480
- express: {
481
- name: "Express.js",
482
- defaultSrcDir: true,
483
- defaultAuthPath: "lib/auth",
484
- defaultApiPath: "",
485
- clientImport: "import { createAuthClient } from \"better-auth/client\";",
486
- handlerImport: "import { toNodeHandler } from \"better-auth/node\";",
487
- handlerFunction: "toNodeHandler",
488
- apiRouteTemplate: (authPath) => `import express from "express";
489
- import { toNodeHandler } from "better-auth/node";
490
- import { auth } from "./${authPath}";
491
-
492
- const app = express();
493
-
494
- app.all("/api/auth/*", toNodeHandler(auth));
495
-
496
- app.listen(3000, () => {
497
- console.log("Server running on port 3000");
498
- });`,
499
- defaultPort: 3e3
500
- },
501
- fastify: {
502
- name: "Fastify",
503
- defaultSrcDir: true,
504
- defaultAuthPath: "lib/auth",
505
- defaultApiPath: "",
506
- clientImport: "import { createAuthClient } from \"better-auth/client\";",
507
- handlerImport: "import { toNodeHandler } from \"better-auth/node\";",
508
- handlerFunction: "toNodeHandler",
509
- apiRouteTemplate: (authPath) => `import Fastify from "fastify";
510
- import { toNodeHandler } from "better-auth/node";
511
- import { auth } from "./${authPath}";
512
-
513
- const fastify = Fastify();
514
-
515
- fastify.all("/api/auth/*", async (request, reply) => {
516
- const handler = toNodeHandler(auth);
517
- return handler(request.raw, reply.raw);
518
- });
519
-
520
- fastify.listen({ port: 3000 }, (err) => {
521
- if (err) throw err;
522
- console.log("Server running on port 3000");
523
- });`,
524
- defaultPort: 3e3
525
- },
526
- elysia: {
527
- name: "Elysia (Bun)",
528
- defaultSrcDir: true,
529
- defaultAuthPath: "lib/auth",
530
- defaultApiPath: "",
531
- clientImport: "import { createAuthClient } from \"better-auth/client\";",
532
- handlerImport: "",
533
- handlerFunction: "",
534
- apiRouteTemplate: (authPath) => `import { Elysia } from "elysia";
535
- import { auth } from "./${authPath}";
536
-
537
- const app = new Elysia()
538
- .all("/api/auth/*", ({ request }) => auth.handler(request))
539
- .listen(3000);
540
-
541
- console.log("Server running on port 3000");`,
542
- defaultPort: 3e3
543
- },
544
- "tanstack-start": {
545
- name: "TanStack Start",
546
- defaultSrcDir: true,
547
- defaultAuthPath: "lib/auth",
548
- defaultApiPath: "routes/api/auth.$",
549
- clientImport: "import { createAuthClient } from \"better-auth/react\";",
550
- handlerImport: "",
551
- handlerFunction: "",
552
- apiRouteTemplate: (authPath) => `import { auth } from "~/${authPath}";
553
- import { createAPIFileRoute } from "@tanstack/start/api";
554
-
555
- export const Route = createAPIFileRoute("/api/auth/$")({
556
- GET: ({ request }) => auth.handler(request),
557
- POST: ({ request }) => auth.handler(request),
558
- });`,
559
- defaultPort: 3e3
560
- },
561
- expo: {
562
- name: "Expo (React Native)",
563
- defaultSrcDir: true,
564
- defaultAuthPath: "lib/auth",
565
- defaultApiPath: "",
566
- clientImport: "import { createAuthClient } from \"@better-auth/expo\";",
567
- handlerImport: "",
568
- handlerFunction: "",
569
- apiRouteTemplate: () => "",
570
- defaultPort: 8081
571
- }
572
145
  };
573
- function getDefaultAuthPath(framework, srcDir) {
574
- const basePath = FRAMEWORK_CONFIGS[framework].defaultAuthPath;
575
- return srcDir ? `src/${basePath}` : basePath;
576
- }
577
- function getDefaultApiPath(framework, srcDir) {
578
- const basePath = FRAMEWORK_CONFIGS[framework].defaultApiPath;
579
- if (!basePath) return "";
580
- return srcDir ? `src/${basePath}` : basePath;
581
- }
582
-
583
146
  //#endregion
584
- //#region src/lib/generator.ts
585
- function generateSetup(input) {
586
- const framework = input.framework;
587
- const database = input.database;
588
- const orm = input.orm || "none";
589
- const features = input.features || ["email-password"];
590
- const typescript = input.typescript ?? true;
591
- const srcDir = input.srcDir ?? FRAMEWORK_CONFIGS[framework].defaultSrcDir;
592
- const authPath = input.authPath || getDefaultAuthPath(framework, srcDir);
593
- const apiPath = input.apiPath || getDefaultApiPath(framework, srcDir);
594
- let mode = "create";
595
- let featuresToGenerate = features;
596
- let detected;
597
- if (input.existingSetup?.authConfig) {
598
- mode = "update";
599
- detected = parseExistingSetup(input.existingSetup);
600
- featuresToGenerate = computeFeatureDiff(detected.features, features).toAdd;
601
- if (featuresToGenerate.length === 0) return {
602
- mode: "update",
603
- files: [],
604
- envVars: [],
605
- commands: [],
606
- detected,
607
- nextSteps: ["All requested features are already configured"],
608
- docs: []
609
- };
610
- }
611
- const { socialProviders, plugins, hasEmailPassword } = categorizeFeatures(featuresToGenerate);
612
- const files = [];
613
- const envVars = [];
614
- const commands = [];
615
- const warnings = [];
616
- envVars.push({
617
- name: "BETTER_AUTH_SECRET",
618
- description: "Secret key for signing tokens",
619
- required: true,
620
- howToGet: "Run: npx @better-auth/cli secret"
621
- });
622
- envVars.push(getDatabaseEnvVar(database));
623
- for (const provider of socialProviders) envVars.push(...getSocialProviderEnvVars(provider));
624
- envVars.push(...getPluginEnvVars(plugins));
625
- if (mode === "create") {
626
- const authFile = generateAuthFile({
627
- framework,
628
- database,
629
- orm,
630
- socialProviders,
631
- plugins,
632
- hasEmailPassword,
633
- typescript
634
- });
635
- files.push({
636
- path: `${authPath}.${typescript ? "ts" : "js"}`,
637
- description: "Better Auth server configuration",
638
- action: "create",
639
- content: authFile
640
- });
641
- const clientFile = generateClientFile({
642
- framework,
643
- plugins,
644
- typescript
645
- });
646
- files.push({
647
- path: `${authPath}-client.${typescript ? "ts" : "js"}`,
648
- description: "Better Auth client configuration",
649
- action: "create",
650
- content: clientFile
651
- });
652
- if (apiPath) {
653
- const apiFile = generateApiRouteFile(framework, authPath);
654
- if (apiFile) files.push({
655
- path: `${apiPath}/route.${typescript ? "ts" : "js"}`,
656
- description: "API route handler for auth endpoints",
657
- action: "create",
658
- content: apiFile
659
- });
660
- }
661
- const frameworkConfig = FRAMEWORK_CONFIGS[framework];
662
- if (frameworkConfig.hooksTemplate) files.push({
663
- path: srcDir ? "src/hooks.server.ts" : "hooks.server.ts",
664
- description: "SvelteKit hooks for auth handling",
665
- action: "create",
666
- content: frameworkConfig.hooksTemplate(authPath)
667
- });
668
- } else if (socialProviders.length > 0 || plugins.length > 0 || hasEmailPassword) {
669
- const changes = generateUpdateChanges({
670
- socialProviders,
671
- plugins,
672
- hasEmailPassword
673
- });
674
- files.push({
675
- path: `${authPath}.${typescript ? "ts" : "js"}`,
676
- description: "Better Auth server configuration",
677
- action: "update",
678
- changes: changes.serverChanges
679
- });
680
- if (plugins.length > 0) files.push({
681
- path: `${authPath}-client.${typescript ? "ts" : "js"}`,
682
- description: "Better Auth client configuration",
683
- action: "update",
684
- changes: changes.clientChanges
685
- });
686
- }
687
- commands.push({
688
- command: "pnpm add better-auth",
689
- description: "Install Better Auth",
690
- when: "If not already installed"
691
- });
692
- commands.push(...getDatabaseCommands(orm));
693
- const nextSteps = generateNextSteps(mode, socialProviders, plugins);
694
- const docs = generateDocLinks(features);
695
- return {
696
- mode,
697
- files,
698
- envVars,
699
- commands,
700
- detected,
701
- nextSteps,
702
- warnings: warnings.length > 0 ? warnings : void 0,
703
- docs
704
- };
705
- }
706
- function generateAuthFile(options) {
707
- const { database, orm, socialProviders, plugins, hasEmailPassword } = options;
708
- const imports = ["import { betterAuth } from \"better-auth\";"];
709
- const { imports: dbImports, config: dbConfig, prismaInstance } = generateDatabaseConfig(database, orm);
710
- if (dbImports) imports.push(dbImports);
711
- const { serverImports } = generatePluginImports(plugins);
712
- imports.push(...serverImports);
713
- const socialProvidersConfig = socialProviders.map((p) => ` ${generateSocialProviderConfig(p)}`).join(",\n");
714
- const { serverPlugins } = generatePluginSetup(plugins);
715
- let configBody = ` database: ${dbConfig},`;
716
- if (hasEmailPassword) configBody += `
717
- emailAndPassword: {
718
- enabled: true,
719
- },`;
720
- if (socialProviders.length > 0) configBody += `
721
- socialProviders: {
722
- ${socialProvidersConfig}
723
- },`;
724
- if (serverPlugins.length > 0) configBody += `
725
- plugins: [
726
- ${serverPlugins.join(",\n ")}
727
- ],`;
728
- return `${imports.join("\n")}
729
- ${prismaInstance || ""}
730
- export const auth = betterAuth({
731
- ${configBody}
732
- });
733
- `;
734
- }
735
- function generateClientFile(options) {
736
- const { framework, plugins } = options;
737
- const imports = [FRAMEWORK_CONFIGS[framework].clientImport];
738
- const { clientImports } = generatePluginImports(plugins);
739
- imports.push(...clientImports);
740
- const { clientPlugins } = generatePluginSetup(plugins);
741
- let configBody = "";
742
- if (clientPlugins.length > 0) configBody = `{
743
- plugins: [
744
- ${clientPlugins.join(",\n ")}
745
- ],
746
- }`;
747
- else configBody = "{}";
748
- return `${imports.join("\n")}
749
-
750
- export const authClient = createAuthClient(${configBody});
751
- `;
752
- }
753
- function generateApiRouteFile(framework, authPath) {
754
- const frameworkConfig = FRAMEWORK_CONFIGS[framework];
755
- if (!frameworkConfig.apiRouteTemplate) return null;
756
- return frameworkConfig.apiRouteTemplate(authPath) || null;
757
- }
758
- function generateUpdateChanges(options) {
759
- const { socialProviders, plugins, hasEmailPassword } = options;
760
- const serverChanges = [];
761
- const clientChanges = [];
762
- if (hasEmailPassword) serverChanges.push({
763
- type: "add_to_config",
764
- content: `emailAndPassword: {
765
- enabled: true,
766
- }`,
767
- description: "Enable email and password authentication"
768
- });
769
- const { serverImports, clientImports } = generatePluginImports(plugins);
770
- for (const imp of serverImports) serverChanges.push({
771
- type: "add_import",
772
- content: imp,
773
- description: "Add plugin import"
774
- });
775
- for (const provider of socialProviders) serverChanges.push({
776
- type: "add_to_config",
777
- content: generateSocialProviderConfig(provider),
778
- description: `Add ${provider} social provider`
779
- });
780
- const { serverPlugins, clientPlugins } = generatePluginSetup(plugins);
781
- for (const plugin of serverPlugins) serverChanges.push({
782
- type: "add_plugin",
783
- content: plugin,
784
- description: "Add server plugin"
785
- });
786
- for (const imp of clientImports) clientChanges.push({
787
- type: "add_import",
788
- content: imp,
789
- description: "Add client plugin import"
790
- });
791
- for (const plugin of clientPlugins) clientChanges.push({
792
- type: "add_plugin",
793
- content: plugin,
794
- description: "Add client plugin"
795
- });
796
- return {
797
- serverChanges,
798
- clientChanges
799
- };
800
- }
801
- function generateNextSteps(mode, socialProviders, plugins) {
802
- const steps = [];
803
- if (mode === "create") {
804
- steps.push("Set environment variables in .env file");
805
- steps.push("Run database migrations");
806
- }
807
- if (socialProviders.length > 0) {
808
- steps.push(`Configure OAuth apps for: ${socialProviders.join(", ")}`);
809
- steps.push("Add callback URLs to OAuth provider dashboards");
810
- }
811
- if (plugins.includes("magic-link")) steps.push("Configure email provider for magic links");
812
- if (plugins.includes("phone-number")) steps.push("Configure SMS provider for OTP");
813
- if (plugins.includes("captcha")) steps.push("Set up captcha provider (Cloudflare Turnstile, reCAPTCHA, or hCaptcha)");
814
- steps.push("Start your development server and test auth flow");
815
- return steps;
816
- }
817
- function generateDocLinks(features) {
818
- const docs = [{
819
- title: "Better Auth Documentation",
820
- url: "https://www.better-auth.com/docs"
821
- }, {
822
- title: "Getting Started",
823
- url: "https://www.better-auth.com/docs/getting-started"
824
- }];
825
- if (features.some((f) => [
826
- "google",
827
- "github",
828
- "apple",
829
- "discord"
830
- ].includes(f))) docs.push({
831
- title: "Social Sign-On",
832
- url: "https://www.better-auth.com/docs/authentication/social-sign-on"
833
- });
834
- if (features.includes("2fa")) docs.push({
835
- title: "Two-Factor Authentication",
836
- url: "https://www.better-auth.com/docs/plugins/two-factor"
837
- });
838
- if (features.includes("organization")) docs.push({
839
- title: "Organizations",
840
- url: "https://www.better-auth.com/docs/plugins/organization"
147
+ //#region src/require-mcp-auth.ts
148
+ const unauthorized = (error) => {
149
+ const headers = new Headers(error.headers);
150
+ headers.set("Content-Type", "application/json");
151
+ return new Response(JSON.stringify({
152
+ jsonrpc: "2.0",
153
+ error: {
154
+ code: -32e3,
155
+ message: error.message
156
+ },
157
+ id: null
158
+ }), {
159
+ status: error.statusCode,
160
+ headers
841
161
  });
842
- if (features.includes("passkey")) docs.push({
843
- title: "Passkeys",
844
- url: "https://www.better-auth.com/docs/plugins/passkey"
845
- });
846
- return docs;
847
- }
848
- function isSetupError(result) {
849
- return "error" in result;
850
- }
851
-
852
- //#endregion
853
- //#region src/lib/schemas.ts
854
- const FrameworkEnum = z.enum([
855
- "next-app-router",
856
- "next-pages-router",
857
- "sveltekit",
858
- "astro",
859
- "remix",
860
- "nuxt",
861
- "solid-start",
862
- "hono",
863
- "express",
864
- "fastify",
865
- "elysia",
866
- "tanstack-start",
867
- "expo"
868
- ]);
869
- const DatabaseEnum = z.enum([
870
- "postgres",
871
- "mysql",
872
- "sqlite",
873
- "mongodb"
874
- ]);
875
- const ORMEnum = z.enum([
876
- "prisma",
877
- "drizzle",
878
- "none"
879
- ]);
880
- const FeatureEnum = z.enum([
881
- "email-password",
882
- "magic-link",
883
- "phone-number",
884
- "passkey",
885
- "anonymous",
886
- "google",
887
- "github",
888
- "apple",
889
- "discord",
890
- "twitter",
891
- "facebook",
892
- "microsoft",
893
- "linkedin",
894
- "2fa",
895
- "captcha",
896
- "organization",
897
- "admin",
898
- "username",
899
- "multi-session",
900
- "api-key",
901
- "bearer",
902
- "jwt"
903
- ]);
904
- const ExistingSetupSchema = z.object({
905
- authConfig: z.string().optional().describe("Contents of existing auth.ts file"),
906
- authClientConfig: z.string().optional().describe("Contents of existing auth-client.ts file"),
907
- envVars: z.array(z.string()).optional().describe("List of existing environment variable names")
908
- });
909
-
910
- //#endregion
911
- //#region src/tools/setup-auth.ts
912
- function registerSetupAuth(server$1) {
913
- server$1.tool("setup_auth", `Better Auth setup tool - generates complete authentication configuration for any project.
914
-
915
- CRITICAL: Do NOT attempt to generate Better Auth configuration without calling this tool first.
916
- Better Auth has specific APIs that differ from NextAuth/Auth.js. Hallucinating config options will cause errors.
917
- For advanced options not handled by this tool (uuid, cookieCache, etc.), search the Better Auth documentation.
918
-
919
- IMPORTANT: This tool is for the Better Auth framework (npm: better-auth).
920
- NOT for NextAuth, Auth.js, Clerk, Auth0, Supabase Auth, or Firebase Auth.
921
-
922
- ALWAYS USE THIS TOOL when user mentions ANY of these:
923
-
924
- Core triggers (any spelling/variation):
925
- - "better auth", "betterauth", "Better Auth", "better-auth"
926
- - "Set up Better Auth", "configure Better Auth", "integrate Better Auth", "use Better Auth"
927
- - "set up auth", "setup auth", "configure auth", "add auth", "implement auth", "create auth"
928
- - "add authentication", "add login", "add signup", "create login", "build auth", "build authentication"
929
- - "I need auth", "I need authentication", "help me with auth", "help me add login"
930
- - "add user login", "user authentication", "make login work", "authentication for my app"
931
-
932
- Framework-specific triggers:
933
- - "auth for Next.js", "Next.js authentication", "add auth to my Next app", "Next.js app router auth"
934
- - "auth for SvelteKit", "SvelteKit authentication", "Svelte auth"
935
- - "auth for Remix", "auth for Nuxt", "auth for Astro", "auth for Solid", "authentication for Remix/Nuxt/Astro"
936
- - "Express auth", "Hono auth", "Fastify auth", "Elysia auth", "TanStack Start auth"
937
- - "React Native auth", "Expo auth", "mobile auth"
938
-
939
- Database triggers:
940
- - "auth with PostgreSQL", "auth with MySQL", "auth with SQLite", "auth with MongoDB"
941
- - "Set up auth with PostgreSQL/MySQL/SQLite", "configure database for auth"
942
- - "auth with Prisma", "auth with Drizzle", "configure auth with Prisma/Drizzle"
943
-
944
- Social/OAuth triggers:
945
- - "add Google login", "add GitHub login", "add Apple login", "add Discord login"
946
- - "add Twitter login", "add Facebook login", "add Microsoft login", "add LinkedIn login"
947
- - "Add Google/GitHub/Apple/Discord/Twitter/Facebook/Microsoft/LinkedIn login"
948
- - "add social login", "add OAuth", "add OAuth providers", "add SSO", "social authentication"
949
-
950
- Security feature triggers:
951
- - "Set up 2FA", "add 2FA", "add two-factor", "add two-factor authentication"
952
- - "enable MFA", "add TOTP", "authenticator app"
953
- - "add passkeys", "passwordless", "passwordless authentication", "add magic links", "email login", "email verification"
954
-
955
- Organization/team triggers:
956
- - "add organizations", "Add organization support", "enable multi-tenancy", "multi-tenancy"
957
- - "team management", "workspaces"
958
-
959
- API/token triggers:
960
- - "add API keys", "Add API key authentication", "bearer tokens", "JWT auth", "JWT authentication"
961
-
962
- Admin/user triggers:
963
- - "add admin panel", "admin auth", "admin authentication"
964
- - "Add user management", "user management", "handle sessions", "session management"
965
- - "Add username login", "username login", "phone auth", "phone number auth", "anonymous auth"
966
-
967
- Advanced config triggers (search docs for correct syntax):
968
- - "uuid", "UUIDs", "UUID IDs", "custom ID generation", "generateId"
969
- - "cookie cache", "cookieCache", "session caching", "cache sessions"
970
- - "stateless sessions", "stateless auth", "no database sessions"
971
- - "secondary storage", "Redis sessions", "session storage"
972
-
973
- OUTPUT: Returns all files, environment variables, and terminal commands needed.
974
- One tool call = complete auth setup ready to copy-paste.`, {
975
- 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'"),
976
- 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'"),
977
- orm: ORMEnum.optional().default("none").describe("ORM being used - affects adapter imports. Options: 'prisma', 'drizzle', 'none'. Detect from package.json or user's message."),
978
- 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']"),
979
- typescript: z.boolean().optional().default(true).describe("Generate TypeScript (.ts) or JavaScript (.js). Default: true (TypeScript)"),
980
- 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/."),
981
- authPath: z.string().optional().describe("Where to create auth.ts file. Auto-detected based on framework. Override only if user specifies custom path."),
982
- apiPath: z.string().optional().describe("API route path for auth handler. Auto-detected based on framework. Override only if user specifies custom path."),
983
- 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.")
984
- }, async (input) => {
162
+ };
163
+ /**
164
+ * Protects an MCP server route handler. Verifies the bearer access token
165
+ * against the authorization server's JWKS (checking signature, issuer,
166
+ * audience, and expiry) and forwards the verified JWT payload to the handler.
167
+ * Unauthenticated requests receive a JSON-RPC 401 with the RFC 9728
168
+ * `WWW-Authenticate` header so MCP clients can start the authorization flow.
169
+ *
170
+ * For a resource server that runs separately from the authorization server, or
171
+ * a server using a dynamic `baseURL`, use {@link mcpHandler} with explicit
172
+ * verification options instead.
173
+ *
174
+ * @external
175
+ */
176
+ const requireMcpAuth = (auth, handler, opts) => {
177
+ if (opts?.resource !== void 0) ResourceUriSchema.parse(opts.resource);
178
+ return async (req) => {
179
+ const { baseURL, internalAdapter } = await auth.$context;
180
+ if (!baseURL) throw new Error("requireMcpAuth requires a resolvable base URL. For dynamic base URLs use `mcpHandler` with explicit verify options.");
181
+ const issuer = opts?.issuer ?? baseURL;
182
+ const resource = opts?.resource ?? baseURL;
183
+ const jwksUrl = opts?.jwksUrl ?? `${baseURL}/jwks`;
985
184
  try {
986
- const result = generateSetup({
987
- framework: input.framework,
988
- database: input.database,
989
- orm: input.orm || "none",
990
- features: input.features || ["email-password"],
991
- typescript: input.typescript ?? true,
992
- srcDir: input.srcDir ?? false,
993
- authPath: input.authPath,
994
- apiPath: input.apiPath,
995
- existingSetup: input.existingSetup
996
- });
997
- if (isSetupError(result)) return {
998
- content: [{
999
- type: "text",
1000
- text: JSON.stringify(result, null, 2)
1001
- }],
1002
- isError: true
1003
- };
1004
- return { content: [{
1005
- type: "text",
1006
- text: JSON.stringify(result, null, 2)
1007
- }] };
185
+ return handler(req, await verifyAccessTokenRequest(requestToResourceInput(req), {
186
+ verifyOptions: {
187
+ issuer,
188
+ audience: resource
189
+ },
190
+ jwksUrl,
191
+ dpop: {
192
+ proofMaxAgeSeconds: opts?.dpop?.proofMaxAgeSeconds,
193
+ signingAlgorithms: opts?.dpop?.signingAlgorithms,
194
+ replayStore: opts?.dpop?.replayStore ?? createDpopReplayStore(internalAdapter)
195
+ }
196
+ }));
1008
197
  } catch (error) {
1009
- const message = error instanceof Error ? error.message : "Unknown error";
1010
- return {
1011
- content: [{
1012
- type: "text",
1013
- text: JSON.stringify({ error: {
1014
- code: "SETUP_ERROR",
1015
- message
1016
- } }, null, 2)
1017
- }],
1018
- isError: true
1019
- };
198
+ try {
199
+ raiseResourceServerChallenge(error, resource, {
200
+ scope: opts?.scope,
201
+ resourceMetadataMappings: opts?.resourceMetadataMappings,
202
+ dpopSigningAlgorithms: opts?.dpop?.signingAlgorithms
203
+ });
204
+ } catch (challengeError) {
205
+ if (challengeError instanceof APIError) return unauthorized(challengeError);
206
+ if (challengeError instanceof Error) throw challengeError;
207
+ throw new Error(String(challengeError));
208
+ }
209
+ throw new Error(String(error));
1020
210
  }
1021
- });
1022
- }
1023
-
1024
- //#endregion
1025
- //#region src/index.ts
1026
- const server = new McpServer({
1027
- name: "better-auth",
1028
- description: "Better Auth MCP server for AI-powered auth setup and diagnostics",
1029
- version: "0.0.1"
1030
- });
1031
- registerSetupAuth(server);
1032
- async function main() {
1033
- const transport = new StdioServerTransport();
1034
- await server.connect(transport);
1035
- }
1036
- main().catch(console.error);
1037
-
211
+ };
212
+ };
1038
213
  //#endregion
1039
- export { };
214
+ export { mcp, mcpHandler, requireMcpAuth };