Skip to content

Commit e2960f3

Browse files
authored
feat: self-hosted MCP development tools (supabase#39022)
* feat: stub `getDevelopmentOperations` * feat: `getProjectUrl` * feat: `getAnonKey` * feat: `generateTypescriptTypes` * chore: comment for consistency * fix: "development" in supported feature group schema * fix: `ResponseError` checks * chore: rename `typescript.ts` to `generate-types.ts` * chore: unused import
1 parent 9949916 commit e2960f3

File tree

6 files changed

+157
-68
lines changed

6 files changed

+157
-68
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { fetchGet } from 'data/fetchers'
2+
import { PG_META_URL } from 'lib/constants'
3+
import { assertSelfHosted } from './util'
4+
import { ResponseError } from 'types'
5+
6+
export type GenerateTypescriptTypesOptions = {
7+
headers?: HeadersInit
8+
}
9+
10+
type GenerateTypescriptTypesResult = {
11+
types: string
12+
}
13+
14+
/**
15+
* Generates TypeScript types for the self-hosted Postgres instance via pg-meta service.
16+
*
17+
* _Only call this from server-side self-hosted code._
18+
*/
19+
export async function generateTypescriptTypes({
20+
headers,
21+
}: GenerateTypescriptTypesOptions): Promise<GenerateTypescriptTypesResult | ResponseError> {
22+
assertSelfHosted()
23+
24+
const includedSchema = ['public', 'graphql_public', 'storage'].join(',')
25+
26+
const excludedSchema = [
27+
'auth',
28+
'cron',
29+
'extensions',
30+
'graphql',
31+
'net',
32+
'pgsodium',
33+
'pgsodium_masks',
34+
'realtime',
35+
'supabase_functions',
36+
'supabase_migrations',
37+
'vault',
38+
'_analytics',
39+
'_realtime',
40+
].join(',')
41+
42+
const response = await fetchGet<GenerateTypescriptTypesResult>(
43+
`${PG_META_URL}/generators/typescript?included_schema=${includedSchema}&excluded_schemas=${excludedSchema}`,
44+
{ headers }
45+
)
46+
47+
return response
48+
}

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
import {
22
ApplyMigrationOptions,
33
DatabaseOperations,
4+
DevelopmentOperations,
45
ExecuteSqlOptions,
56
} from '@supabase/mcp-server-supabase/platform'
67
import { applyAndTrackMigrations, listMigrationVersions } from './migrations'
78
import { executeQuery } from './query'
9+
import { getProjectSettings } from './settings'
10+
import { generateTypescriptTypes } from './generate-types'
11+
import { ResponseError } from 'types'
812

913
export type GetDatabaseOperationsOptions = {
1014
headers?: HeadersInit
1115
}
1216

17+
export type GetDevelopmentOperationsOptions = {
18+
headers?: HeadersInit
19+
}
20+
1321
export function getDatabaseOperations({
1422
headers,
1523
}: GetDatabaseOperationsOptions): DatabaseOperations {
@@ -43,3 +51,33 @@ export function getDatabaseOperations({
4351
},
4452
}
4553
}
54+
55+
export function getDevelopmentOperations({
56+
headers,
57+
}: GetDevelopmentOperationsOptions): DevelopmentOperations {
58+
return {
59+
async getProjectUrl(_projectRef) {
60+
const settings = getProjectSettings()
61+
return `${settings.app_config.protocol}://${settings.app_config.endpoint}`
62+
},
63+
async getAnonKey(_projectRef) {
64+
const settings = getProjectSettings()
65+
const anonKey = settings.service_api_keys.find((key) => key.name === 'anon key')
66+
67+
if (!anonKey) {
68+
throw new Error('Anon key not found in project settings')
69+
}
70+
71+
return anonKey.api_key
72+
},
73+
async generateTypescriptTypes(_projectRef) {
74+
const response = await generateTypescriptTypes({ headers })
75+
76+
if (response instanceof ResponseError) {
77+
throw response
78+
}
79+
80+
return response
81+
},
82+
}
83+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { components } from 'api-types'
2+
import { PROJECT_ENDPOINT, PROJECT_ENDPOINT_PROTOCOL } from 'lib/constants/api'
3+
import { assertSelfHosted } from './util'
4+
5+
type ProjectAppConfig = components['schemas']['ProjectSettingsResponse']['app_config'] & {
6+
protocol?: string
7+
}
8+
9+
export type ProjectSettings = components['schemas']['ProjectSettingsResponse'] & {
10+
app_config?: ProjectAppConfig
11+
}
12+
13+
/**
14+
* Gets self-hosted project settings
15+
*
16+
* _Only call this from server-side self-hosted code._
17+
*/
18+
export function getProjectSettings() {
19+
assertSelfHosted()
20+
21+
const response = {
22+
app_config: {
23+
db_schema: 'public',
24+
endpoint: PROJECT_ENDPOINT,
25+
storage_endpoint: PROJECT_ENDPOINT,
26+
// manually added to force the frontend to use the correct URL
27+
protocol: PROJECT_ENDPOINT_PROTOCOL,
28+
},
29+
cloud_provider: 'AWS',
30+
db_dns_name: '-',
31+
db_host: 'localhost',
32+
db_ip_addr_config: 'legacy' as const,
33+
db_name: 'postgres',
34+
db_port: 5432,
35+
db_user: 'postgres',
36+
inserted_at: '2021-08-02T06:40:40.646Z',
37+
jwt_secret:
38+
process.env.AUTH_JWT_SECRET ?? 'super-secret-jwt-token-with-at-least-32-characters-long',
39+
name: process.env.DEFAULT_PROJECT_NAME || 'Default Project',
40+
ref: 'default',
41+
region: 'ap-southeast-1',
42+
service_api_keys: [
43+
{
44+
api_key: process.env.SUPABASE_SERVICE_KEY ?? '',
45+
name: 'service_role key',
46+
tags: 'service_role',
47+
},
48+
{
49+
api_key: process.env.SUPABASE_ANON_KEY ?? '',
50+
name: 'anon key',
51+
tags: 'anon',
52+
},
53+
],
54+
ssl_enforced: false,
55+
status: 'ACTIVE_HEALTHY',
56+
} satisfies ProjectSettings
57+
58+
return response
59+
}

apps/studio/pages/api/mcp/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
22
import { createSupabaseMcpServer, SupabasePlatform } from '@supabase/mcp-server-supabase'
33
import { stripIndent } from 'common-tags'
44
import { commaSeparatedStringIntoArray, fromNodeHeaders } from 'lib/api/apiHelpers'
5-
import { getDatabaseOperations } from 'lib/api/self-hosted/mcp'
5+
import { getDatabaseOperations, getDevelopmentOperations } from 'lib/api/self-hosted/mcp'
66
import { DEFAULT_PROJECT } from 'lib/constants/api'
77
import { NextApiRequest, NextApiResponse } from 'next'
88
import { z } from 'zod'
99

10-
const supportedFeatureGroupSchema = z.enum(['docs', 'database'])
10+
const supportedFeatureGroupSchema = z.enum(['docs', 'database', 'development'])
1111

1212
const mcpQuerySchema = z.object({
1313
features: z
@@ -46,6 +46,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) {
4646

4747
const platform: SupabasePlatform = {
4848
database: getDatabaseOperations({ headers }),
49+
development: getDevelopmentOperations({ headers }),
4950
}
5051

5152
try {

apps/studio/pages/api/platform/projects/[ref]/settings.ts

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

33
import { components } from 'api-types'
44
import apiWrapper from 'lib/api/apiWrapper'
5-
import { PROJECT_ENDPOINT, PROJECT_ENDPOINT_PROTOCOL } from 'lib/constants/api'
5+
import { getProjectSettings } from 'lib/api/self-hosted/settings'
66

77
type ProjectAppConfig = components['schemas']['ProjectSettingsResponse']['app_config'] & {
88
protocol?: string
@@ -26,42 +26,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
2626
}
2727

2828
const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
29-
const response: ProjectSettings = {
30-
app_config: {
31-
db_schema: 'public',
32-
endpoint: PROJECT_ENDPOINT,
33-
storage_endpoint: PROJECT_ENDPOINT,
34-
// manually added to force the frontend to use the correct URL
35-
protocol: PROJECT_ENDPOINT_PROTOCOL,
36-
},
37-
cloud_provider: 'AWS',
38-
db_dns_name: '-',
39-
db_host: 'localhost',
40-
db_ip_addr_config: 'legacy' as const,
41-
db_name: 'postgres',
42-
db_port: 5432,
43-
db_user: 'postgres',
44-
inserted_at: '2021-08-02T06:40:40.646Z',
45-
jwt_secret:
46-
process.env.AUTH_JWT_SECRET ?? 'super-secret-jwt-token-with-at-least-32-characters-long',
47-
name: process.env.DEFAULT_PROJECT_NAME || 'Default Project',
48-
ref: 'default',
49-
region: 'ap-southeast-1',
50-
service_api_keys: [
51-
{
52-
api_key: process.env.SUPABASE_SERVICE_KEY ?? '',
53-
name: 'service_role key',
54-
tags: 'service_role',
55-
},
56-
{
57-
api_key: process.env.SUPABASE_ANON_KEY ?? '',
58-
name: 'anon key',
59-
tags: 'anon',
60-
},
61-
],
62-
ssl_enforced: false,
63-
status: 'ACTIVE_HEALTHY',
64-
}
29+
const response = getProjectSettings()
6530

6631
return res.status(200).json(response)
6732
}
Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { NextApiRequest, NextApiResponse } from 'next'
22

3-
import { fetchGet } from 'data/fetchers'
43
import { constructHeaders } from 'lib/api/apiHelpers'
54
import apiWrapper from 'lib/api/apiWrapper'
6-
import { PG_META_URL } from 'lib/constants'
5+
import { generateTypescriptTypes } from 'lib/api/self-hosted/generate-types'
6+
import { ResponseError } from 'types'
77

88
export default (req: NextApiRequest, res: NextApiResponse) =>
99
apiWrapper(req, res, handler, { withAuth: true })
@@ -21,35 +21,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
2121
}
2222

2323
const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
24-
const includedSchema = ['public', 'graphql_public', 'storage'].join(',')
25-
26-
const excludedSchema = [
27-
'auth',
28-
'cron',
29-
'extensions',
30-
'graphql',
31-
'net',
32-
'pgsodium',
33-
'pgsodium_masks',
34-
'realtime',
35-
'supabase_functions',
36-
'supabase_migrations',
37-
'vault',
38-
'_analytics',
39-
'_realtime',
40-
].join(',')
41-
4224
const headers = constructHeaders(req.headers)
4325

44-
const response = await fetchGet(
45-
`${PG_META_URL}/generators/typescript?included_schema=${includedSchema}&excluded_schemas=${excludedSchema}`,
46-
{ headers }
47-
)
26+
const response = await generateTypescriptTypes({ headers })
4827

49-
if (response.error) {
50-
const { code, message } = response.error
51-
return res.status(code).json({ message })
52-
} else {
53-
return res.status(200).json(response)
28+
if (response instanceof ResponseError) {
29+
return res.status(response.code ?? 500).json({ message: response.message })
5430
}
31+
32+
return res.status(200).json(response)
5533
}

0 commit comments

Comments
 (0)