Skip to content

fix: skip pricing confirmation for creating branch #33

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
99 changes: 1 addition & 98 deletions packages/mcp-server-supabase/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
mockOrgs,
mockProjects,
} from '../test/mocks.js';
import { BRANCH_COST_HOURLY, PROJECT_COST_MONTHLY } from './pricing.js';
import { PROJECT_COST_MONTHLY } from './pricing.js';
import { createSupabaseMcpServer } from './server.js';

beforeEach(() => {
Expand Down Expand Up @@ -213,24 +213,6 @@ describe('tools', () => {
);
});

test('get branch cost', async () => {
const { callTool } = await setup();

const paidOrg = mockOrgs.find((org) => org.plan !== 'free')!;

const result = await callTool({
name: 'get_cost',
arguments: {
type: 'branch',
organization_id: paidOrg.id,
},
});

expect(result).toEqual(
`The new branch will cost $${BRANCH_COST_HOURLY} hourly. You must repeat this to the user and confirm their understanding.`
);
});

test('list projects', async () => {
const { callTool } = await setup();

Expand Down Expand Up @@ -637,22 +619,12 @@ describe('tools', () => {
const { callTool } = await setup();
const project = mockProjects.values().next().value!;

const confirm_cost_id = await callTool({
name: 'confirm_cost',
arguments: {
type: 'branch',
recurrence: 'hourly',
amount: BRANCH_COST_HOURLY,
},
});

const branchName = 'test-branch';
const result = await callTool({
name: 'create_branch',
arguments: {
project_id: project.id,
name: branchName,
confirm_cost_id,
},
});

Expand All @@ -673,44 +645,15 @@ describe('tools', () => {
});
});

test('create branch without cost confirmation fails', async () => {
const { callTool } = await setup();

const project = mockProjects.values().next().value!;

const branchName = 'test-branch';
const createBranchPromise = callTool({
name: 'create_branch',
arguments: {
project_id: project.id,
name: branchName,
},
});

await expect(createBranchPromise).rejects.toThrow(
'User must confirm understanding of costs before creating a branch.'
);
});

test('delete branch', async () => {
const { callTool } = await setup();
const project = mockProjects.values().next().value!;

const confirm_cost_id = await callTool({
name: 'confirm_cost',
arguments: {
type: 'branch',
recurrence: 'hourly',
amount: BRANCH_COST_HOURLY,
},
});

const branch = await callTool({
name: 'create_branch',
arguments: {
project_id: project.id,
name: 'test-branch',
confirm_cost_id,
},
});

Expand Down Expand Up @@ -777,21 +720,11 @@ describe('tools', () => {
const { callTool } = await setup();
const project = mockProjects.values().next().value!;

const confirm_cost_id = await callTool({
name: 'confirm_cost',
arguments: {
type: 'branch',
recurrence: 'hourly',
amount: BRANCH_COST_HOURLY,
},
});

const branch = await callTool({
name: 'create_branch',
arguments: {
project_id: project.id,
name: 'test-branch',
confirm_cost_id,
},
});

Expand Down Expand Up @@ -836,21 +769,11 @@ describe('tools', () => {
const { callTool } = await setup();
const project = mockProjects.values().next().value!;

const confirm_cost_id = await callTool({
name: 'confirm_cost',
arguments: {
type: 'branch',
recurrence: 'hourly',
amount: BRANCH_COST_HOURLY,
},
});

const branch = await callTool({
name: 'create_branch',
arguments: {
project_id: project.id,
name: 'test-branch',
confirm_cost_id,
},
});

Expand Down Expand Up @@ -900,21 +823,11 @@ describe('tools', () => {
const { callTool } = await setup();
const project = mockProjects.values().next().value!;

const confirm_cost_id = await callTool({
name: 'confirm_cost',
arguments: {
type: 'branch',
recurrence: 'hourly',
amount: BRANCH_COST_HOURLY,
},
});

const branch = await callTool({
name: 'create_branch',
arguments: {
project_id: project.id,
name: 'test-branch',
confirm_cost_id,
},
});

Expand Down Expand Up @@ -988,21 +901,11 @@ describe('tools', () => {
const { callTool } = await setup();
const project = mockProjects.values().next().value!;

const confirm_cost_id = await callTool({
name: 'confirm_cost',
arguments: {
type: 'branch',
recurrence: 'hourly',
amount: BRANCH_COST_HOURLY,
},
});

const branch = await callTool({
name: 'create_branch',
arguments: {
project_id: project.id,
name: 'test-branch',
confirm_cost_id,
},
});

Expand Down
38 changes: 13 additions & 25 deletions packages/mcp-server-supabase/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import {
type ManagementApiClient,
} from './management-api/index.js';
import { generatePassword } from './password.js';
import { getBranchCost, getNextProjectCost, type Cost } from './pricing.js';
import {
BRANCH_COST_HOURLY,
getBranchCost,
getNextProjectCost,
type Cost,
} from './pricing.js';
import {
AWS_REGION_CODES,
getClosestAwsRegion,
Expand Down Expand Up @@ -124,7 +129,7 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) {
}),
get_cost: tool({
description:
'Gets the cost of creating a new project or branch. Never assume organization as costs can be different for each.',
'Gets the cost of creating a new project. Never assume organization when creating a project as costs can be different for each.',
parameters: z.object({
type: z.enum(['project', 'branch']),
organization_id: z
Expand Down Expand Up @@ -154,7 +159,7 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) {
}),
confirm_cost: tool({
description:
'Ask the user to confirm their understanding of the cost of creating a new project or branch. Call `get_cost` first. Returns a unique ID for this confirmation which should be passed to `create_project` or `create_branch`.',
'Ask the user to confirm their understanding of the cost of creating a new project. Call `get_cost` first. Returns a unique ID for this confirmation which should be passed to `create_project`.',
parameters: z.object({
type: z.enum(['project', 'branch']),
recurrence: z.enum(['hourly', 'monthly']),
Expand Down Expand Up @@ -504,33 +509,16 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) {

// Experimental features
create_branch: tool({
description:
'Creates a development branch on a Supabase project. This will apply all migrations from the main project to a fresh branch database. Note that production data will not carry over. The branch will get its own project_id via the resulting project_ref. Use this ID to execute queries and migrations on the branch.',
description: `Creates a development branch on a Supabase project. This will apply all migrations from the main project to a fresh branch database. Note that production data will not carry over. The branch will get its own project_id via the resulting project_ref. Use this ID to execute queries and migrations on the branch.
The cost of each active branch is $${BRANCH_COST_HOURLY} per hour. Always show this to the user in bold before creating a branch and suggest deleting any unused branches to avoid unnecessary charges.`,
parameters: z.object({
project_id: z.string(),
name: z
.string()
.default('develop')
.describe('Name of the branch to create'),
confirm_cost_id: z
.string()
.describe('The cost confirmation ID. Call `confirm_cost` first.'),
}),
execute: async ({ project_id, name, confirm_cost_id }) => {
if (!confirm_cost_id) {
throw new Error(
'User must confirm understanding of costs before creating a branch.'
);
}

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.'
);
}

execute: async ({ project_id, name }) => {
const createBranchResponse = await managementApiClient.POST(
'/v1/projects/{ref}/branches',
{
Expand Down Expand Up @@ -584,8 +572,8 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) {
},
}),
list_branches: tool({
description:
'Lists all development branches of a Supabase project. This will return branch details including status which you can use to check when operations like merge/rebase/reset complete.',
description: `Lists all development branches of a Supabase project. This will return branch details including status which you can use to check when operations like merge/rebase/reset complete.
The cost of each active branch is $${BRANCH_COST_HOURLY} per hour. Always suggest deleting any unused branches to avoid unnecessary charges.`,
parameters: z.object({
project_id: z.string(),
}),
Expand Down