Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,352 changes: 430 additions & 922 deletions docs/site/src/lib/mcp-catalog.snapshot.json

Large diffs are not rendered by default.

24 changes: 16 additions & 8 deletions web/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ import {
accounts,
jwks,
oauthAccessTokens,
oauthClientAssertions,
oauthClientResources,
oauthClients,
oauthConsents,
oauthRefreshTokens,
oauthResources,
sessions,
users,
verifications,
Expand Down Expand Up @@ -140,6 +143,9 @@ export const authInstance = betterAuth({
oauthRefreshToken: oauthRefreshTokens,
oauthAccessToken: oauthAccessTokens,
oauthConsent: oauthConsents,
oauthResource: oauthResources,
oauthClientResource: oauthClientResources,
oauthClientAssertion: oauthClientAssertions,
},
}),
...(googleClientId && googleClientSecret
Expand Down Expand Up @@ -258,9 +264,12 @@ export const authInstance = betterAuth({
loginPage: "/login",
consentPage: "/consent",
scopes: [...oauthScopes],
// One audience per deployment: GHSA-p2fr-6hmx-4528 lets a client pick any
// allow-listed `resource` at the token endpoint until 1.7 ships stable.
validAudiences: [mcpResourceAudience],
// One audience per deployment, seeded into `oauth_resource` at boot.
resources: [mcpResourceAudience],
// Clients registered under Better Auth 1.6 have no `oauth_client_resource`
// link and would get `invalid_target`. With a single resource, linking
// each client restricts nothing.
enforcePerClientResources: false,
// Cursor / mcp-remote still require RFC 7591 Dynamic Client Registration.
// Open registration only creates OAuth clients — Google Workspace still
// gates who can sign in.
Expand All @@ -274,11 +283,10 @@ export const authInstance = betterAuth({
allowUnauthenticatedClientRegistration: true,
clientRegistrationDefaultScopes: [...oauthScopes],
clientRegistrationAllowedScopes: [...oauthScopes],
// Path-aware AS metadata lives at
// `/.well-known/oauth-authorization-server/api/auth` (issuer path).
silenceWarnings: {
oauthAuthServerConfig: true,
},
// Cursor and Claude Code omit `application_type`, and the `web` default
// rejects their `cursor://` and loopback callbacks. "infer" registers
// them as `native`; clients with only https callbacks stay `web`.
clientRegistrationDefaultApplicationType: "infer",
}),
// Must be last so Set-Cookie from server actions (sign-in / sign-out)
// reaches the browser via Next's `cookies()` helper.
Expand Down
5 changes: 3 additions & 2 deletions web/lib/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,9 @@ export const accounts = pgTable(
.references(() => users.id, { onDelete: "cascade" }),
// Provider's account ID, or the user id for `credential` accounts.
accountId: text("accountId").notNull(),
// Written by Better Auth 1.7 only. Left nullable so the 1.6 runtime,
// which does not know this column, can still insert account rows.
// Only Better Auth 1.7.0–1.7.2 wrote this; 1.7.3+ identifies accounts by
// `providerId` and `accountId` again. Must stay nullable, because new rows
// leave it empty.
issuer: text("issuer"),
// OAuth provider id (e.g. `google`) or `credential` for email/password.
providerId: text("providerId").notNull(),
Expand Down
31 changes: 27 additions & 4 deletions web/lib/db/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,15 @@ export const SEED_WATCHER_VERSION = "1.0.0";

// ---------------------------------------------------------------------------
// clearAll — schema-driven TRUNCATE of every `pgTable` declared in
// `lib/db/schema.ts`. Uses CASCADE so FK ordering doesn't matter; the
// previous static `TRUNCATE_ORDER` list was a maintenance hazard (every
// new table required a manual edit) and is replaced by this.
// `lib/db/schema.ts` except `jwks`. Uses CASCADE so FK ordering doesn't
// matter; the previous static `TRUNCATE_ORDER` list was a maintenance hazard
// (every new table required a manual edit) and is replaced by this.
// ---------------------------------------------------------------------------

// The MCP route caches the published signing keys and rate-limits refetches,
// so a running server would reject tokens signed by a replacement key.
const KEPT_TABLES = new Set<string>([getTableName(schema.jwks)]);

export async function clearAll(db: Db): Promise<void> {
// Cast through `unknown[]` so the `isTable` type predicate can narrow
// away the `PgEnum`s and other non-table exports in the schema module
Expand All @@ -64,7 +68,8 @@ export async function clearAll(db: Db): Promise<void> {
// but TS won't accept the union as-is).
const tableNames = (Object.values(schema) as unknown[])
.filter(isTable)
.map((t) => getTableName(t));
.map((t) => getTableName(t))
.filter((name) => !KEPT_TABLES.has(name));

if (tableNames.length === 0) {
return;
Expand Down Expand Up @@ -199,6 +204,24 @@ export async function seedSlackChannelConfig(
);
}

// The MCP audience row that `resources` in `lib/auth.ts` declares. Better Auth
// inserts it only once per server process, so a server that outlives a reset
// rejects every MCP token request with `invalid_target` until this restores it.
export async function seedMcpResource(db: Db, origin: string): Promise<void> {
const identifier = `${origin}/mcp/v1`;
const now = new Date();
await db
.insert(schema.oauthResources)
.values({
id: crypto.randomUUID(),
identifier,
name: identifier,
createdAt: now,
updatedAt: now,
})
.onConflictDoNothing({ target: schema.oauthResources.identifier });
}

// ---------------------------------------------------------------------------
// Instruments — mirrors the production catalog (ids + display names) so docs
// screenshots look real, plus one pending instrument for the activate-
Expand Down
11 changes: 5 additions & 6 deletions web/lib/mcp/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,11 @@ export async function verifyMcpToken(
}

try {
// better-auth 1.6 exposes `verifyAccessToken(token)`; 1.7's
// `verifyAccessTokenRequest(req)` (DPoP-aware) is not available yet.
// Opaque tokens (no `resource` at token exchange) are rejected — JWKS
// verification requires a JWT, and we intentionally do not hand-roll
// DB lookups that skip audience checks.
const payload = await resourceClient.verifyAccessToken(bearerToken, {
// MCP already extracted the Bearer token, so DPoP-bound tokens are
// rejected here. Opaque tokens (no `resource` at token exchange) fail
// JWKS verification, and we do not hand-roll DB lookups that skip
// audience checks.
const payload = await resourceClient.verifyBearerToken(bearerToken, {
verifyOptions: {
audience: mcpResourceAudience,
// Must match JWT `iss` / AS metadata issuer (`{origin}/api/auth`).
Expand Down
Loading
Loading