Skip to content
Draft
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
10 changes: 2 additions & 8 deletions packages/mcp-server-supabase/src/platform/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,7 @@ export type DevelopmentOperations = {

export type StorageOperations = {
getStorageConfig(projectId: string): Promise<StorageConfig>;
updateStorageConfig(
projectId: string,
config: StorageConfig
): Promise<void>;
updateStorageConfig(projectId: string, config: StorageConfig): Promise<void>;
listAllBuckets(projectId: string): Promise<StorageBucket[]>;
};

Expand All @@ -249,10 +246,7 @@ export type BranchingOperations = {
): Promise<Branch>;
deleteBranch(branchId: string): Promise<void>;
mergeBranch(branchId: string): Promise<void>;
resetBranch(
branchId: string,
options: ResetBranchOptions
): Promise<void>;
resetBranch(branchId: string, options: ResetBranchOptions): Promise<void>;
rebaseBranch(branchId: string): Promise<void>;
};

Expand Down
3 changes: 2 additions & 1 deletion packages/mcp-server-supabase/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) {
}

if (!projectId && account && enabledFeatures.has('account')) {
Object.assign(tools, getAccountTools({ account, readOnly }));
Object.assign(tools, getAccountTools({ account, readOnly, server }));
}

if (database && enabledFeatures.has('database')) {
Expand All @@ -144,6 +144,7 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) {
database,
projectId,
readOnly,
server,
})
);
}
Expand Down
71 changes: 55 additions & 16 deletions packages/mcp-server-supabase/src/tools/account-tools.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { tool } from '@supabase/mcp-utils';
import { tool, type ToolExecuteContext } from '@supabase/mcp-utils';
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { z } from 'zod';
import type { AccountOperations } from '../platform/types.js';
import { type Cost, getBranchCost, getNextProjectCost } from '../pricing.js';
Expand All @@ -10,9 +11,14 @@ const SUCCESS_RESPONSE = { success: true };
export type AccountToolsOptions = {
account: AccountOperations;
readOnly?: boolean;
server?: Server;
};

export function getAccountTools({ account, readOnly }: AccountToolsOptions) {
export function getAccountTools({
account,
readOnly,
server,
}: AccountToolsOptions) {
return {
list_organizations: tool({
description: 'Lists all organizations that the user is a member of.',
Expand Down Expand Up @@ -120,6 +126,7 @@ export function getAccountTools({ account, readOnly }: AccountToolsOptions) {
idempotentHint: true,
openWorldHint: false,
},
isSupported: (clientCapabilities) => !clientCapabilities?.elicitation,
parameters: z.object({
type: z.enum(['project', 'branch']),
recurrence: z.enum(['hourly', 'monthly']),
Expand All @@ -131,7 +138,7 @@ export function getAccountTools({ account, readOnly }: AccountToolsOptions) {
}),
create_project: tool({
description:
'Creates a new Supabase project. Always ask the user which organization to create the project in. The project can take a few minutes to initialize - use `get_project` to check the status.',
'Creates a new Supabase project. Always ask the user which organization to create the project in. If there is a cost involved, the user will be asked to confirm before creation. The project can take a few minutes to initialize - use `get_project` to check the status.',
annotations: {
title: 'Create project',
readOnlyHint: false,
Expand All @@ -145,31 +152,63 @@ export function getAccountTools({ account, readOnly }: AccountToolsOptions) {
.enum(AWS_REGION_CODES)
.describe('The region to create the project in.'),
organization_id: z.string(),
confirm_cost_id: z
.string({
required_error:
'User must confirm understanding of costs before creating a project.',
})
.describe('The cost confirmation ID. Call `confirm_cost` first.'),
}),
execute: async ({ name, region, organization_id, confirm_cost_id }) => {
execute: async ({ name, region, organization_id }, context) => {
if (readOnly) {
throw new Error('Cannot create a project in read-only mode.');
}

// Calculate cost inline
const cost = await getNextProjectCost(account, organization_id);
const costHash = await hashObject(cost);
if (costHash !== confirm_cost_id) {
throw new Error(
'Cost confirmation ID does not match the expected cost of creating a project.'
);

// Only request confirmation if there's a cost AND server supports elicitation
if (cost.amount > 0 && context?.server?.elicitInput) {
const costMessage = `$${cost.amount} per ${cost.recurrence}`;

const result = await context.server.elicitInput({
message: `You are about to create project "${name}" in region ${region}.\n\n💰 Cost: ${costMessage}\n\nDo you want to proceed with this billable project?`,
requestedSchema: {
type: 'object',
properties: {
confirm: {
type: 'boolean',
title: 'Confirm billable project creation',
description: `I understand this will cost ${costMessage} and want to proceed`,
},
},
required: ['confirm'],
},
});

// Handle user response
if (result.action === 'decline' || result.action === 'cancel') {
throw new Error('Project creation cancelled by user.');
}

if (result.action === 'accept' && !result.content?.confirm) {
throw new Error(
'You must confirm understanding of the cost to create a billable project.'
);
}
}

return await account.createProject({
// Create the project (either free or confirmed)
const project = await account.createProject({
name,
region,
organization_id,
});

// Return appropriate message based on cost
const costInfo =
cost.amount > 0
? `Cost: $${cost.amount}/${cost.recurrence}`
: 'Cost: Free';

return {
...project,
message: `Project "${name}" created successfully. ${costInfo}`,
};
},
}),
pause_project: tool({
Expand Down
70 changes: 59 additions & 11 deletions packages/mcp-server-supabase/src/tools/branching-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,73 @@ export function getBranchingTools({
.string()
.default('develop')
.describe('Name of the branch to create'),
// When the client supports elicitation, we will ask the user to confirm the
// branch cost interactively and this parameter is not required. For clients
// without elicitation support, this confirmation ID is required.
confirm_cost_id: z
.string({
required_error:
'User must confirm understanding of costs before creating a branch.',
})
.describe('The cost confirmation ID. Call `confirm_cost` first.'),
.string()
.optional()
.describe(
'The cost confirmation ID. Call `confirm_cost` first if elicitation is not supported.'
),
}),
inject: { project_id },
execute: async ({ project_id, name, confirm_cost_id }) => {
execute: async ({ project_id, name, confirm_cost_id }, context) => {
if (readOnly) {
throw new Error('Cannot create a branch in read-only mode.');
}

const cost = getBranchCost();
const costHash = await hashObject(cost);
if (costHash !== confirm_cost_id) {
throw new Error(
'Cost confirmation ID does not match the expected cost of creating a branch.'
);

// If the server and client support elicitation, request explicit confirmation
const caps = context?.server?.getClientCapabilities?.();
const supportsElicitation = Boolean(caps && (caps as any).elicitation);

if (
cost.amount > 0 &&
supportsElicitation &&
context?.server?.elicitInput
) {
const costMessage = `$${cost.amount} per ${cost.recurrence}`;

const result = await context.server.elicitInput({
message: `You are about to create branch "${name}" on project ${project_id}.\n\n💰 Cost: ${costMessage}\n\nDo you want to proceed with this billable branch?`,
requestedSchema: {
type: 'object',
properties: {
confirm: {
type: 'boolean',
title: 'Confirm billable branch creation',
description: `I understand this will cost ${costMessage} and want to proceed`,
},
},
required: ['confirm'],
},
});

if (result.action === 'decline' || result.action === 'cancel') {
throw new Error('Branch creation cancelled by user.');
}

if (result.action === 'accept' && !result.content?.confirm) {
throw new Error(
'You must confirm understanding of the cost to create a billable branch.'
);
}
} else {
// Fallback path (no elicitation support): require confirm_cost_id
if (!confirm_cost_id) {
throw new Error(
'User must confirm understanding of costs before creating a branch.'
);
}

const costHash = await hashObject(cost);
if (costHash !== confirm_cost_id) {
throw new Error(
'Cost confirmation ID does not match the expected cost of creating a branch.'
);
}
}
return await branching.createBranch(project_id, { name });
},
Expand Down
45 changes: 45 additions & 0 deletions packages/mcp-server-supabase/src/tools/database-operation-tools.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { source } from 'common-tags';
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { z } from 'zod';
import { listExtensionsSql, listTablesSql } from '../pg-meta/index.js';
import {
Expand All @@ -14,12 +15,14 @@ export type DatabaseOperationToolsOptions = {
database: DatabaseOperations;
projectId?: string;
readOnly?: boolean;
server?: Server;
};

export function getDatabaseTools({
database,
projectId,
readOnly,
server,
}: DatabaseOperationToolsOptions) {
const project_id = projectId;

Expand Down Expand Up @@ -215,6 +218,48 @@ export function getDatabaseTools({
throw new Error('Cannot apply migration in read-only mode.');
}

// Try to request user confirmation via elicitation
if (server) {
try {
const result = (await server.request(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if we can use the elicitInput helper here

{
method: 'elicitation/create',
params: {
message: `You are about to apply migration "${name}" to project ${project_id}. This will modify your database schema.\n\nPlease review the SQL:\n\n${query}\n\nDo you want to proceed?`,
requestedSchema: {
type: 'object',
properties: {
confirm: {
type: 'boolean',
title: 'Confirm Migration',
description:
'I have reviewed the SQL and approve this migration',
},
},
required: ['confirm'],
},
},
},
// @ts-ignore - elicitation types might not be available
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using @ts-ignore suppresses all type checking for the next line. Consider using @ts-expect-error instead, which will fail if the error no longer exists, helping track when types become available.

Suggested change
// @ts-ignore - elicitation types might not be available
// @ts-expect-error - elicitation types might not be available

Copilot uses AI. Check for mistakes.
{ elicitation: true }
)) as {
action: 'accept' | 'decline' | 'cancel';
content?: { confirm?: boolean };
};

// User declined or cancelled
if (result.action !== 'accept' || !result.content?.confirm) {
throw new Error('Migration cancelled by user');
}
} catch (error) {
// If elicitation fails (client doesn't support it), proceed without confirmation
// This maintains backwards compatibility
console.warn(
'Elicitation not supported by client, proceeding with migration without confirmation'
);
Comment on lines +255 to +259
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch block silently proceeds with the migration after any elicitation error, which could include network failures or other issues unrelated to client support. Consider checking the specific error type to distinguish between 'unsupported' vs other failures, and potentially fail fast for unexpected errors rather than always proceeding.

Suggested change
// If elicitation fails (client doesn't support it), proceed without confirmation
// This maintains backwards compatibility
console.warn(
'Elicitation not supported by client, proceeding with migration without confirmation'
);
// Only proceed if the error is due to unsupported elicitation; otherwise, fail fast
const errorMessage =
typeof error === 'string'
? error
: error instanceof Error
? error.message
: '';
if (
errorMessage &&
(
errorMessage.includes('elicitation not supported') ||
errorMessage.includes('Elicitation not supported') ||
errorMessage.includes('not implemented') ||
errorMessage.includes('unsupported')
)
) {
console.warn(
'Elicitation not supported by client, proceeding with migration without confirmation'
);
} else {
// Unexpected error, fail fast
throw error;
}

Copilot uses AI. Check for mistakes.
}
}

await database.applyMigration(project_id, {
name,
query,
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server-supabase/src/tools/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,6 @@ export function injectableTool<
description,
annotations,
parameters: parameters.omit(mask),
execute: (args) => execute({ ...args, ...inject }),
execute: (args, context) => execute({ ...args, ...inject }, context),
}) as Tool<z.ZodObject<any, any, any, CleanParams>, Result>;
}
Loading
Loading