Skip to content

Commit d319653

Browse files
authored
feat: Replace get_anon_key with get_publishable_keys (#167)
* feat: replace get_anon_key with get_anon_or_publishable_keys - Replace get_anon_key tool with get_anon_or_publishable_keys to support both legacy and modern API keys - Add ApiKeyType type and apiKeyTypeSchema for proper type safety - Update getAnonOrPublishableKeys to return array of structured ApiKey objects - Filter for client-safe keys (legacy 'anon' or 'publishable' type) - Update tests to verify both legacy and publishable keys are returned - Update README documentation to reflect new tool - Improve error messages and provide guidance on key types Fixes AI-166 * feat: indicate disabled legacy API keys to LLMs - Add 'disabled' field to ApiKey type to track key status - Check legacy keys endpoint to determine if legacy keys are disabled - Mark legacy keys with disabled: true when they are disabled - Gracefully handle legacy endpoint failures by omitting disabled field - Update tool description to inform LLMs about disabled keys - Add test coverage for disabled legacy keys scenario Addresses feedback from PR #167 about disabled legacy keys not being indicated to LLMs * fix: correct path parameter in legacy keys mock endpoint Use :ref instead of :projectId to match the actual API endpoint path parameter * fix test * refactor: rename get_anon_or_publishable_keys to get_publishable_keys and update description for LLM compatibility * refactor: rename getAnonOrPublishableKeys to getPublishableKeys in DevelopmentOperations implementation
1 parent 24602bf commit d319653

File tree

6 files changed

+109
-15
lines changed

6 files changed

+109
-15
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ Enabled by default. Use `debugging` to target this group of tools with the [`fea
195195
Enabled by default. Use `development` to target this group of tools with the [`features`](#feature-groups) option.
196196

197197
- `get_project_url`: Gets the API URL for a project.
198-
- `get_anon_key`: Gets the anonymous API key for a project.
198+
- `get_anon_or_publishable_keys`: Gets the anonymous API keys for a project. Returns an array of client-safe API keys including legacy anon keys and modern publishable keys. Publishable keys are recommended for new applications.
199199
- `generate_typescript_types`: Generates TypeScript types based on the database schema. LLMs can save this to a file and use it in their code.
200200

201201
#### Edge Functions

packages/mcp-server-supabase/src/platform/api-platform.ts

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
getLogsOptionsSchema,
2222
resetBranchOptionsSchema,
2323
type AccountOperations,
24+
type ApiKey,
25+
type ApiKeyType,
2426
type ApplyMigrationOptions,
2527
type BranchingOperations,
2628
type CreateBranchOptions,
@@ -301,7 +303,7 @@ export function createSupabaseApiPlatform(
301303
const apiUrl = new URL(managementApiUrl);
302304
return `https://${projectId}.${getProjectDomain(apiUrl.hostname)}`;
303305
},
304-
async getAnonKey(projectId: string): Promise<string> {
306+
async getPublishableKeys(projectId: string): Promise<ApiKey[]> {
305307
const response = await managementApiClient.GET(
306308
'/v1/projects/{ref}/api-keys',
307309
{
@@ -318,13 +320,54 @@ export function createSupabaseApiPlatform(
318320

319321
assertSuccess(response, 'Failed to fetch API keys');
320322

321-
const anonKey = response.data?.find((key) => key.name === 'anon');
323+
// Try to check if legacy JWT-based keys are enabled
324+
// If this fails, we'll continue without the disabled field
325+
let legacyKeysEnabled: boolean | undefined = undefined;
326+
try {
327+
const legacyKeysResponse = await managementApiClient.GET(
328+
'/v1/projects/{ref}/api-keys/legacy',
329+
{
330+
params: {
331+
path: {
332+
ref: projectId,
333+
},
334+
},
335+
}
336+
);
337+
338+
if (legacyKeysResponse.response.ok) {
339+
legacyKeysEnabled = legacyKeysResponse.data?.enabled ?? true;
340+
}
341+
} catch (error) {
342+
// If we can't fetch legacy key status, continue without it
343+
legacyKeysEnabled = undefined;
344+
}
345+
346+
// Filter for client-safe keys: legacy 'anon' or publishable type
347+
const clientKeys =
348+
response.data?.filter(
349+
(key) => key.name === 'anon' || key.type === 'publishable'
350+
) ?? [];
322351

323-
if (!anonKey?.api_key) {
324-
throw new Error('Anonymous key not found');
352+
if (clientKeys.length === 0) {
353+
throw new Error(
354+
'No client-safe API keys (anon or publishable) found. Please create a publishable key in your project settings.'
355+
);
325356
}
326357

327-
return anonKey.api_key;
358+
return clientKeys.map((key) => ({
359+
api_key: key.api_key!,
360+
name: key.name,
361+
type: (key.type === 'publishable'
362+
? 'publishable'
363+
: 'legacy') satisfies ApiKeyType,
364+
// Only include disabled field if we successfully fetched legacy key status
365+
...(legacyKeysEnabled !== undefined && {
366+
disabled: key.type === 'legacy' && !legacyKeysEnabled,
367+
}),
368+
description: key.description ?? undefined,
369+
id: key.id ?? undefined,
370+
}));
328371
},
329372
async generateTypescriptTypes(projectId: string) {
330373
const response = await managementApiClient.GET(

packages/mcp-server-supabase/src/platform/types.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,21 @@ export type DebuggingOperations = {
212212
getPerformanceAdvisors(projectId: string): Promise<unknown>;
213213
};
214214

215+
export const apiKeyTypeSchema = z.enum(['legacy', 'publishable']);
216+
export type ApiKeyType = z.infer<typeof apiKeyTypeSchema>;
217+
218+
export type ApiKey = {
219+
api_key: string;
220+
name: string;
221+
type: ApiKeyType;
222+
description?: string;
223+
id?: string;
224+
disabled?: boolean;
225+
};
226+
215227
export type DevelopmentOperations = {
216228
getProjectUrl(projectId: string): Promise<string>;
217-
getAnonKey(projectId: string): Promise<string>;
229+
getPublishableKeys(projectId: string): Promise<ApiKey[]>;
218230
generateTypescriptTypes(
219231
projectId: string
220232
): Promise<GenerateTypescriptTypesResult>;

packages/mcp-server-supabase/src/server.test.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ describe('tools', () => {
608608
expect(result).toEqual(`https://${project.id}.supabase.co`);
609609
});
610610

611-
test('get anon key', async () => {
611+
test('get anon or publishable keys', async () => {
612612
const { callTool } = await setup();
613613
const org = await createOrganization({
614614
name: 'My Org',
@@ -623,12 +623,31 @@ describe('tools', () => {
623623
project.status = 'ACTIVE_HEALTHY';
624624

625625
const result = await callTool({
626-
name: 'get_anon_key',
626+
name: 'get_publishable_keys',
627627
arguments: {
628628
project_id: project.id,
629629
},
630630
});
631-
expect(result).toEqual('dummy-anon-key');
631+
632+
expect(result).toBeInstanceOf(Array);
633+
expect(result.length).toBe(2);
634+
635+
// Check legacy anon key
636+
const anonKey = result.find((key: any) => key.name === 'anon');
637+
expect(anonKey).toBeDefined();
638+
expect(anonKey.api_key).toEqual('dummy-anon-key');
639+
expect(anonKey.type).toEqual('legacy');
640+
expect(anonKey.id).toEqual('anon-key-id');
641+
expect(anonKey.disabled).toBe(true);
642+
643+
// Check publishable key
644+
const publishableKey = result.find(
645+
(key: any) => key.type === 'publishable'
646+
);
647+
expect(publishableKey).toBeDefined();
648+
expect(publishableKey.api_key).toEqual('sb_publishable_dummy_key_1');
649+
expect(publishableKey.type).toEqual('publishable');
650+
expect(publishableKey.description).toEqual('Main publishable key');
632651
});
633652

634653
test('list storage buckets', async () => {
@@ -2609,7 +2628,7 @@ describe('feature groups', () => {
26092628

26102629
expect(toolNames).toEqual([
26112630
'get_project_url',
2612-
'get_anon_key',
2631+
'get_publishable_keys',
26132632
'generate_typescript_types',
26142633
]);
26152634
});

packages/mcp-server-supabase/src/tools/development-tools.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ export function getDevelopmentTools({
3131
return development.getProjectUrl(project_id);
3232
},
3333
}),
34-
get_anon_key: injectableTool({
35-
description: 'Gets the anonymous API key for a project.',
34+
get_publishable_keys: injectableTool({
35+
description:
36+
'Gets all publishable API keys for a project, including legacy anon keys (JWT-based) and modern publishable keys (format: sb_publishable_...). Publishable keys are recommended for new applications due to better security and independent rotation. Legacy anon keys are included for compatibility, as many LLMs are pretrained on them. Disabled keys are indicated by the "disabled" field; only use keys where disabled is false or undefined.',
3637
annotations: {
37-
title: 'Get anon key',
38+
title: 'Get publishable keys',
3839
readOnlyHint: true,
3940
destructiveHint: false,
4041
idempotentHint: true,
@@ -45,7 +46,7 @@ export function getDevelopmentTools({
4546
}),
4647
inject: { project_id },
4748
execute: async ({ project_id }) => {
48-
return development.getAnonKey(project_id);
49+
return development.getPublishableKeys(project_id);
4950
},
5051
}),
5152
generate_typescript_types: injectableTool({

packages/mcp-server-supabase/test/mocks.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,10 +254,29 @@ export const mockManagementApi = [
254254
{
255255
name: 'anon',
256256
api_key: 'dummy-anon-key',
257+
type: 'legacy',
258+
id: 'anon-key-id',
259+
},
260+
{
261+
name: 'publishable-key-1',
262+
api_key: 'sb_publishable_dummy_key_1',
263+
type: 'publishable',
264+
id: 'publishable-key-1-id',
265+
description: 'Main publishable key',
257266
},
258267
]);
259268
}),
260269

270+
/**
271+
* Check if legacy API keys are enabled
272+
*/
273+
http.get(
274+
`${API_URL}/v1/projects/:projectId/api-keys/legacy`,
275+
({ params }) => {
276+
return HttpResponse.json({ enabled: false });
277+
}
278+
),
279+
261280
/**
262281
* Execute a SQL query on a project's database
263282
*/

0 commit comments

Comments
 (0)