From efbbac29b4e7e0bbd7e3ff433dd90493c6e8974b Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Mon, 7 Apr 2025 09:01:22 +0800 Subject: [PATCH 1/3] fix: skip pricing confirmation for creating branch --- .../mcp-server-supabase/src/server.test.ts | 99 +------------------ packages/mcp-server-supabase/src/server.ts | 34 +++---- 2 files changed, 12 insertions(+), 121 deletions(-) diff --git a/packages/mcp-server-supabase/src/server.test.ts b/packages/mcp-server-supabase/src/server.test.ts index f9d0ed3..9d8f9eb 100644 --- a/packages/mcp-server-supabase/src/server.test.ts +++ b/packages/mcp-server-supabase/src/server.test.ts @@ -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(() => { @@ -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(); @@ -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, }, }); @@ -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, }, }); @@ -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, }, }); @@ -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, }, }); @@ -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, }, }); @@ -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, }, }); diff --git a/packages/mcp-server-supabase/src/server.ts b/packages/mcp-server-supabase/src/server.ts index 0f5f7f7..a3bd499 100644 --- a/packages/mcp-server-supabase/src/server.ts +++ b/packages/mcp-server-supabase/src/server.ts @@ -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, @@ -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 @@ -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']), @@ -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 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', { From 4e3e0133d093617494ab5a0069074e8c0f9e4931 Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Mon, 7 Apr 2025 09:59:23 +0800 Subject: [PATCH 2/3] chore: add cost reminder to list branches tool --- packages/mcp-server-supabase/src/server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mcp-server-supabase/src/server.ts b/packages/mcp-server-supabase/src/server.ts index a3bd499..4ea0c6d 100644 --- a/packages/mcp-server-supabase/src/server.ts +++ b/packages/mcp-server-supabase/src/server.ts @@ -572,8 +572,8 @@ The cost of each active branch is $${BRANCH_COST_HOURLY} per hour. Always show t }, }), 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(), }), From 58509cb2fa868a67a3e9059daec51ea7ad24fc8b Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Mon, 7 Apr 2025 10:14:41 +0800 Subject: [PATCH 3/3] chore: bold suggestion if possible --- packages/mcp-server-supabase/src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp-server-supabase/src/server.ts b/packages/mcp-server-supabase/src/server.ts index 4ea0c6d..bb7de59 100644 --- a/packages/mcp-server-supabase/src/server.ts +++ b/packages/mcp-server-supabase/src/server.ts @@ -510,7 +510,7 @@ 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. -The cost of each active branch is $${BRANCH_COST_HOURLY} per hour. Always show this to the user before creating a branch and suggest deleting any unused branches to avoid unnecessary charges.`, +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