Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- Avoid advertising OAuth support on MCP endpoints if that entitlement is not actually configured. [#985](https://github.com/sourcebot-dev/sourcebot/pull/985)

## [4.15.1] - 2026-03-06

### Fixed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { apiHandler } from '@/lib/apiHandler';
import { env } from '@sourcebot/shared';
import { env, hasEntitlement } from '@sourcebot/shared';

// RFC 8414: OAuth 2.0 Authorization Server Metadata
// @note: we do not gate on entitlements here. That is handled in the /register,
// /token, and /revoke routes.
// @see: https://datatracker.ietf.org/doc/html/rfc8414
export const GET = apiHandler(async () => {
if (!hasEntitlement('oauth')) {
return Response.json(
{ error: 'not_found', error_description: 'OAuth authorization server metadata is not available on this plan.' },
{ status: 404 }
);
}

const issuer = env.AUTH_URL.replace(/\/$/, '');

return Response.json({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { apiHandler } from '@/lib/apiHandler';
import { env } from '@sourcebot/shared';
import { env, hasEntitlement } from '@sourcebot/shared';
import { NextRequest } from 'next/server';

// RFC 9728: OAuth 2.0 Protected Resource Metadata (path-specific form)
// For a resource at /api/mcp, the well-known URI is /.well-known/oauth-protected-resource/api/mcp.
// @note: we do not gate on entitlements here. That is handled in the /register,
// /token, and /revoke routes.
// @see: https://datatracker.ietf.org/doc/html/rfc9728#section-3
const PROTECTED_RESOURCES = new Set([
'api/mcp'
]);

export const GET = apiHandler(async (_request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) => {
if (!hasEntitlement('oauth')) {
return Response.json(
{ error: 'not_found', error_description: 'OAuth protected resource metadata is not available on this plan.' },
{ status: 404 }
);
}

const { path } = await params;
const resourcePath = path.join('/');

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { apiHandler } from '@/lib/apiHandler';
import { env } from '@sourcebot/shared';
import { env, hasEntitlement } from '@sourcebot/shared';

// RFC 9728: OAuth 2.0 Protected Resource Metadata
// Tells OAuth clients which authorization server protects this resource.
// @see: https://datatracker.ietf.org/doc/html/rfc9728
export const GET = apiHandler(async () => {
if (!hasEntitlement('oauth')) {
return Response.json(
{ error: 'not_found', error_description: 'OAuth protected resource metadata is not available on this plan.' },
{ status: 404 }
);
}

const issuer = env.AUTH_URL.replace(/\/$/, '');

return Response.json({
Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/app/api/(server)/mcp/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { StatusCodes } from 'http-status-codes';
import { NextRequest } from 'next/server';
import { sew } from '@/actions';
import { apiHandler } from '@/lib/apiHandler';
import { env } from '@sourcebot/shared';
import { env, hasEntitlement } from '@sourcebot/shared';

// On 401, tell MCP clients where to find the OAuth protected resource metadata (RFC 9728)
// so they can discover the authorization server and initiate the authorization code flow.
Expand All @@ -18,7 +18,7 @@ import { env } from '@sourcebot/shared';
// @see: https://datatracker.ietf.org/doc/html/rfc9728
function mcpErrorResponse(error: ServiceError): Response {
const response = serviceErrorResponse(error);
if (error.statusCode === StatusCodes.UNAUTHORIZED) {
if (error.statusCode === StatusCodes.UNAUTHORIZED && hasEntitlement('oauth')) {
const issuer = env.AUTH_URL.replace(/\/$/, '');
response.headers.set(
'WWW-Authenticate',
Expand Down