Skip to content

Commit 471195a

Browse files
Rodriguespnclaude
andcommitted
fix: pass exposedSchemas to getLints in MCP advisor operations
The MCP `getSecurityAdvisors` and `getPerformanceAdvisors` operations called `getLints` without `exposedSchemas`, causing all schema-filtered lints to return empty results in local/self-hosted environments. Extracts `DEFAULT_EXPOSED_SCHEMAS` constant shared between the MCP handler and the `run-lints` API route. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d47fd24 commit 471195a

5 files changed

Lines changed: 146 additions & 3 deletions

File tree

apps/studio/lib/api/self-hosted/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
// Constants specific to self-hosted environments
22

3+
// Schemas exposed via PostgREST Data API.
4+
// In hosted Supabase, PostgREST sets the `pgrst.db_schemas` GUC on its own connections.
5+
// In self-hosted/local environments this GUC isn't available to other services (e.g. Studio),
6+
// so we hardcode the default here until a dynamic config source is wired up.
7+
// See: https://github.com/supabase/supabase/blob/master/docker/docker-compose.yml#L183
8+
export const DEFAULT_EXPOSED_SCHEMAS = 'public, storage'
9+
310
export const ENCRYPTION_KEY = process.env.PG_META_CRYPTO_KEY || 'SAMPLE_KEY'
411
export const POSTGRES_PORT = parseInt(process.env.POSTGRES_PORT || '5432', 10)
512
export const POSTGRES_HOST = process.env.POSTGRES_HOST || 'db'

apps/studio/lib/api/self-hosted/mcp.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
GetLogsOptions,
1010
} from '@supabase/mcp-server-supabase/platform'
1111
import { ResponseError } from 'types'
12+
import { DEFAULT_EXPOSED_SCHEMAS } from './constants'
1213
import { generateTypescriptTypes } from './generate-types'
1314
import { getLints } from './lints'
1415
import { getLogQuery, retrieveAnalyticsData } from './logs'
@@ -128,7 +129,10 @@ export function getDebuggingOperations({
128129
return data
129130
},
130131
async getSecurityAdvisors(_projectRef) {
131-
const { data, error } = await getLints({ headers })
132+
const { data, error } = await getLints({
133+
headers,
134+
exposedSchemas: DEFAULT_EXPOSED_SCHEMAS,
135+
})
132136

133137
if (error) {
134138
throw error
@@ -137,7 +141,10 @@ export function getDebuggingOperations({
137141
return data.filter((lint) => lint.categories.includes('SECURITY'))
138142
},
139143
async getPerformanceAdvisors(_projectRef) {
140-
const { data, error } = await getLints({ headers })
144+
const { data, error } = await getLints({
145+
headers,
146+
exposedSchemas: DEFAULT_EXPOSED_SCHEMAS,
147+
})
141148

142149
if (error) {
143150
throw error

apps/studio/pages/api/platform/projects/[ref]/run-lints.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { NextApiRequest, NextApiResponse } from 'next'
22

33
import { constructHeaders } from 'lib/api/apiHelpers'
44
import apiWrapper from 'lib/api/apiWrapper'
5+
import { DEFAULT_EXPOSED_SCHEMAS } from 'lib/api/self-hosted/constants'
56
import { getLints } from 'lib/api/self-hosted/lints'
67

78
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
@@ -22,7 +23,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
2223
*/
2324
const { data, error } = await getLints({
2425
headers: constructHeaders(req.headers),
25-
exposedSchemas: 'public, storage',
26+
exposedSchemas: DEFAULT_EXPOSED_SCHEMAS,
2627
})
2728

2829
if (error) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { enrichLintsQuery } from 'lib/api/self-hosted/lints'
3+
4+
describe('enrichLintsQuery', () => {
5+
const dummyQuery = 'SELECT 1'
6+
7+
it('should include SET LOCAL pgrst.db_schemas when exposedSchemas is provided', () => {
8+
const result = enrichLintsQuery(dummyQuery, 'public, storage')
9+
expect(result).toContain("set local pgrst.db_schemas = 'public, storage';")
10+
})
11+
12+
it('should NOT include SET LOCAL pgrst.db_schemas when exposedSchemas is undefined', () => {
13+
const result = enrichLintsQuery(dummyQuery, undefined)
14+
expect(result).not.toContain('pgrst.db_schemas')
15+
})
16+
17+
it('should NOT include SET LOCAL pgrst.db_schemas when exposedSchemas is empty string', () => {
18+
const result = enrichLintsQuery(dummyQuery, '')
19+
expect(result).not.toContain('pgrst.db_schemas')
20+
})
21+
22+
it('should always include the query', () => {
23+
const result = enrichLintsQuery(dummyQuery)
24+
expect(result).toContain(dummyQuery)
25+
})
26+
})
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
import { DEFAULT_EXPOSED_SCHEMAS } from 'lib/api/self-hosted/constants'
3+
4+
// Mock getLints to capture what arguments the MCP operations pass
5+
const mockGetLints = vi.fn()
6+
vi.mock('lib/api/self-hosted/lints', () => ({
7+
getLints: (...args: unknown[]) => mockGetLints(...args),
8+
}))
9+
10+
// Mock getProjectSettings to avoid assertSelfHosted() check
11+
vi.mock('lib/api/self-hosted/settings', () => ({
12+
getProjectSettings: () => ({
13+
app_config: {
14+
db_schema: 'public',
15+
endpoint: 'localhost',
16+
protocol: 'http',
17+
},
18+
service_api_keys: [{ api_key: 'test', name: 'anon key', tags: 'anon' }],
19+
}),
20+
}))
21+
22+
// Must import after mock setup
23+
import { getDebuggingOperations } from 'lib/api/self-hosted/mcp'
24+
25+
describe('MCP advisor operations pass exposedSchemas to getLints', () => {
26+
const headers = { Authorization: 'Bearer test' }
27+
28+
beforeEach(() => {
29+
vi.clearAllMocks()
30+
mockGetLints.mockResolvedValue({
31+
data: [
32+
{
33+
name: 'rls_disabled_in_public',
34+
title: 'RLS Disabled in Public',
35+
level: 'ERROR',
36+
categories: ['SECURITY'],
37+
description: 'test',
38+
detail: 'test',
39+
remediation: 'test',
40+
metadata: {},
41+
cache_key: 'test',
42+
},
43+
{
44+
name: 'unindexed_foreign_keys',
45+
title: 'Unindexed foreign keys',
46+
level: 'INFO',
47+
categories: ['PERFORMANCE'],
48+
description: 'test',
49+
detail: 'test',
50+
remediation: 'test',
51+
metadata: {},
52+
cache_key: 'test',
53+
},
54+
],
55+
error: undefined,
56+
})
57+
})
58+
59+
it('getSecurityAdvisors should pass exposedSchemas to getLints', async () => {
60+
const ops = getDebuggingOperations({ headers })
61+
await ops.getSecurityAdvisors('test-project')
62+
63+
expect(mockGetLints).toHaveBeenCalledOnce()
64+
const callArgs = mockGetLints.mock.calls[0][0]
65+
expect(callArgs).toHaveProperty('exposedSchemas')
66+
expect(callArgs.exposedSchemas).toBeTruthy()
67+
})
68+
69+
it('getPerformanceAdvisors should pass exposedSchemas to getLints', async () => {
70+
const ops = getDebuggingOperations({ headers })
71+
await ops.getPerformanceAdvisors('test-project')
72+
73+
expect(mockGetLints).toHaveBeenCalledOnce()
74+
const callArgs = mockGetLints.mock.calls[0][0]
75+
expect(callArgs).toHaveProperty('exposedSchemas')
76+
expect(callArgs.exposedSchemas).toBeTruthy()
77+
})
78+
79+
it('should use DEFAULT_EXPOSED_SCHEMAS constant', async () => {
80+
const ops = getDebuggingOperations({ headers })
81+
await ops.getSecurityAdvisors('test-project')
82+
83+
const callArgs = mockGetLints.mock.calls[0][0]
84+
expect(callArgs.exposedSchemas).toBe(DEFAULT_EXPOSED_SCHEMAS)
85+
})
86+
87+
it('getSecurityAdvisors should filter to SECURITY category', async () => {
88+
const ops = getDebuggingOperations({ headers })
89+
const result = await ops.getSecurityAdvisors('test-project')
90+
91+
expect(result).toHaveLength(1)
92+
expect((result as Array<{ name: string }>)[0].name).toBe('rls_disabled_in_public')
93+
})
94+
95+
it('getPerformanceAdvisors should filter to PERFORMANCE category', async () => {
96+
const ops = getDebuggingOperations({ headers })
97+
const result = await ops.getPerformanceAdvisors('test-project')
98+
99+
expect(result).toHaveLength(1)
100+
expect((result as Array<{ name: string }>)[0].name).toBe('unindexed_foreign_keys')
101+
})
102+
})

0 commit comments

Comments
 (0)