Skip to content

Commit b739cfe

Browse files
committed
merge get_cost and confirm_cost tools
1 parent 0aed187 commit b739cfe

File tree

3 files changed

+116
-131
lines changed

3 files changed

+116
-131
lines changed

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

Lines changed: 100 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { tool, type ToolExecuteContext } from '@supabase/mcp-utils';
22
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
3+
import { ElicitResultSchema } from '@modelcontextprotocol/sdk/types.js';
34
import { z } from 'zod';
45
import type { AccountOperations } from '../platform/types.js';
56
import { type Cost, getBranchCost, getNextProjectCost } from '../pricing.js';
@@ -82,11 +83,16 @@ export function getAccountTools({
8283
return await account.getProject(id);
8384
},
8485
}),
85-
get_cost: tool({
86-
description:
87-
'Gets the cost of creating a new project or branch. Never assume organization as costs can be different for each.',
86+
get_and_confirm_cost: tool({
87+
description: async () => {
88+
const clientCapabilities = server?.getClientCapabilities();
89+
if (clientCapabilities?.elicitation) {
90+
return 'Gets the cost of creating a new project or branch and requests user confirmation. Returns a unique ID for this confirmation which must be passed to `create_project` or `create_branch`. Never assume organization as costs can be different for each.';
91+
}
92+
return 'Gets the cost of creating a new project or branch. You must repeat the cost to the user and confirm their understanding before calling `create_project` or `create_branch`. Returns a unique ID for this confirmation which must be passed to `create_project` or `create_branch`. Never assume organization as costs can be different for each.';
93+
},
8894
annotations: {
89-
title: 'Get cost of new resources',
95+
title: 'Get and confirm cost',
9096
readOnlyHint: true,
9197
destructiveHint: false,
9298
idempotentHint: true,
@@ -96,49 +102,94 @@ export function getAccountTools({
96102
type: z.enum(['project', 'branch']),
97103
organization_id: z
98104
.string()
99-
.describe('The organization ID. Always ask the user.'),
105+
.describe('The organization ID. Always ask the user.')
106+
.optional(),
100107
}),
101108
execute: async ({ type, organization_id }) => {
102-
function generateResponse(cost: Cost) {
103-
return `The new ${type} will cost $${cost.amount} ${cost.recurrence}. You must repeat this to the user and confirm their understanding.`;
104-
}
109+
// Get the cost
110+
let cost: Cost;
105111
switch (type) {
106112
case 'project': {
107-
const cost = await getNextProjectCost(account, organization_id);
108-
return generateResponse(cost);
113+
if (!organization_id) {
114+
throw new Error(
115+
'organization_id is required for project cost calculation'
116+
);
117+
}
118+
cost = await getNextProjectCost(account, organization_id);
119+
break;
109120
}
110121
case 'branch': {
111-
const cost = getBranchCost();
112-
return generateResponse(cost);
122+
cost = getBranchCost();
123+
break;
113124
}
114125
default:
115126
throw new Error(`Unknown cost type: ${type}`);
116127
}
117-
},
118-
}),
119-
confirm_cost: tool({
120-
description:
121-
'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`.',
122-
annotations: {
123-
title: 'Confirm cost understanding',
124-
readOnlyHint: true,
125-
destructiveHint: false,
126-
idempotentHint: true,
127-
openWorldHint: false,
128-
},
129-
isSupported: (clientCapabilities) => !clientCapabilities?.elicitation,
130-
parameters: z.object({
131-
type: z.enum(['project', 'branch']),
132-
recurrence: z.enum(['hourly', 'monthly']),
133-
amount: z.number(),
134-
}),
135-
execute: async (cost) => {
136-
return await hashObject(cost);
128+
129+
let userDeclinedCost = false;
130+
131+
// Request confirmation via elicitation if supported
132+
const clientCapabilities = server?.getClientCapabilities();
133+
if (server && clientCapabilities?.elicitation) {
134+
try {
135+
const costMessage =
136+
cost.amount > 0 ? `$${cost.amount} ${cost.recurrence}` : 'Free';
137+
138+
const result = await server.request(
139+
{
140+
method: 'elicitation/create',
141+
params: {
142+
message: `You are about to create a new ${type}.\n\nCost: ${costMessage}\n\nDo you want to proceed?`,
143+
requestedSchema: {
144+
type: 'object',
145+
properties: {
146+
confirm: {
147+
type: 'boolean',
148+
title: 'Confirm Cost',
149+
description: `I understand the cost and want to create the ${type}`,
150+
},
151+
},
152+
required: ['confirm'],
153+
},
154+
},
155+
},
156+
ElicitResultSchema
157+
);
158+
159+
if (result.action !== 'accept' || !result.content?.confirm) {
160+
userDeclinedCost = true;
161+
}
162+
} catch (error) {
163+
// If elicitation fails (client doesn't support it), return cost info for manual confirmation
164+
console.warn(
165+
'Elicitation not supported by client, returning cost for manual confirmation'
166+
);
167+
console.warn(error);
168+
}
169+
}
170+
171+
if (userDeclinedCost) {
172+
throw new Error(
173+
'The user declined to confirm the cost. Ask the user to confirm if they want to proceed with the operation or do something else.'
174+
);
175+
}
176+
177+
// Generate and return confirmation ID
178+
const confirmationId = await hashObject(cost);
179+
180+
return {
181+
...cost,
182+
confirm_cost_id: confirmationId,
183+
message:
184+
cost.amount > 0
185+
? `The new ${type} will cost $${cost.amount} ${cost.recurrence}. ${clientCapabilities?.elicitation ? 'User has confirmed.' : 'You must confirm this cost with the user before proceeding.'}`
186+
: `The new ${type} is free. ${clientCapabilities?.elicitation ? 'User has confirmed.' : 'You may proceed with creation.'}`,
187+
};
137188
},
138189
}),
139190
create_project: tool({
140191
description:
141-
'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.',
192+
'Creates a new Supabase project. Always ask the user which organization to create the project in. Call `get_and_confirm_cost` first to verify the cost and get user confirmation. The project can take a few minutes to initialize - use `get_project` to check the status.',
142193
annotations: {
143194
title: 'Create project',
144195
readOnlyHint: false,
@@ -152,47 +203,30 @@ export function getAccountTools({
152203
.enum(AWS_REGION_CODES)
153204
.describe('The region to create the project in.'),
154205
organization_id: z.string(),
206+
confirm_cost_id: z
207+
.string({
208+
required_error:
209+
'User must confirm understanding of costs before creating a project.',
210+
})
211+
.describe(
212+
'The cost confirmation ID. Call `get_and_confirm_cost` first.'
213+
),
155214
}),
156-
execute: async ({ name, region, organization_id }, context) => {
215+
execute: async ({ name, region, organization_id, confirm_cost_id }) => {
157216
if (readOnly) {
158217
throw new Error('Cannot create a project in read-only mode.');
159218
}
160219

161-
// Calculate cost inline
220+
// Verify the confirmation ID matches the expected cost
162221
const cost = await getNextProjectCost(account, organization_id);
163-
164-
// Only request confirmation if there's a cost AND server supports elicitation
165-
if (cost.amount > 0 && context?.server?.elicitInput) {
166-
const costMessage = `$${cost.amount} per ${cost.recurrence}`;
167-
168-
const result = await context.server.elicitInput({
169-
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?`,
170-
requestedSchema: {
171-
type: 'object',
172-
properties: {
173-
confirm: {
174-
type: 'boolean',
175-
title: 'Confirm billable project creation',
176-
description: `I understand this will cost ${costMessage} and want to proceed`,
177-
},
178-
},
179-
required: ['confirm'],
180-
},
181-
});
182-
183-
// Handle user response
184-
if (result.action === 'decline' || result.action === 'cancel') {
185-
throw new Error('Project creation cancelled by user.');
186-
}
187-
188-
if (result.action === 'accept' && !result.content?.confirm) {
189-
throw new Error(
190-
'You must confirm understanding of the cost to create a billable project.'
191-
);
192-
}
222+
const costHash = await hashObject(cost);
223+
if (costHash !== confirm_cost_id) {
224+
throw new Error(
225+
'Cost confirmation ID does not match the expected cost of creating a project.'
226+
);
193227
}
194228

195-
// Create the project (either free or confirmed)
229+
// Create the project
196230
const project = await account.createProject({
197231
name,
198232
region,

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

Lines changed: 12 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function getBranchingTools({
2323
return {
2424
create_branch: injectableTool({
2525
description:
26-
'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.',
26+
'Creates a development branch on a Supabase project. Call `get_and_confirm_cost` first to verify the cost and get user confirmation. 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.',
2727
annotations: {
2828
title: 'Create branch',
2929
readOnlyHint: false,
@@ -37,73 +37,27 @@ export function getBranchingTools({
3737
.string()
3838
.default('develop')
3939
.describe('Name of the branch to create'),
40-
// When the client supports elicitation, we will ask the user to confirm the
41-
// branch cost interactively and this parameter is not required. For clients
42-
// without elicitation support, this confirmation ID is required.
4340
confirm_cost_id: z
44-
.string()
45-
.optional()
41+
.string({
42+
required_error:
43+
'User must confirm understanding of costs before creating a branch.',
44+
})
4645
.describe(
47-
'The cost confirmation ID. Call `confirm_cost` first if elicitation is not supported.'
46+
'The cost confirmation ID. Call `get_and_confirm_cost` first.'
4847
),
4948
}),
5049
inject: { project_id },
51-
execute: async ({ project_id, name, confirm_cost_id }, context) => {
50+
execute: async ({ project_id, name, confirm_cost_id }) => {
5251
if (readOnly) {
5352
throw new Error('Cannot create a branch in read-only mode.');
5453
}
5554

5655
const cost = getBranchCost();
57-
58-
// If the server and client support elicitation, request explicit confirmation
59-
const caps = context?.server?.getClientCapabilities?.();
60-
const supportsElicitation = Boolean(caps && (caps as any).elicitation);
61-
62-
if (
63-
cost.amount > 0 &&
64-
supportsElicitation &&
65-
context?.server?.elicitInput
66-
) {
67-
const costMessage = `$${cost.amount} per ${cost.recurrence}`;
68-
69-
const result = await context.server.elicitInput({
70-
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?`,
71-
requestedSchema: {
72-
type: 'object',
73-
properties: {
74-
confirm: {
75-
type: 'boolean',
76-
title: 'Confirm billable branch creation',
77-
description: `I understand this will cost ${costMessage} and want to proceed`,
78-
},
79-
},
80-
required: ['confirm'],
81-
},
82-
});
83-
84-
if (result.action === 'decline' || result.action === 'cancel') {
85-
throw new Error('Branch creation cancelled by user.');
86-
}
87-
88-
if (result.action === 'accept' && !result.content?.confirm) {
89-
throw new Error(
90-
'You must confirm understanding of the cost to create a billable branch.'
91-
);
92-
}
93-
} else {
94-
// Fallback path (no elicitation support): require confirm_cost_id
95-
if (!confirm_cost_id) {
96-
throw new Error(
97-
'User must confirm understanding of costs before creating a branch.'
98-
);
99-
}
100-
101-
const costHash = await hashObject(cost);
102-
if (costHash !== confirm_cost_id) {
103-
throw new Error(
104-
'Cost confirmation ID does not match the expected cost of creating a branch.'
105-
);
106-
}
56+
const costHash = await hashObject(cost);
57+
if (costHash !== confirm_cost_id) {
58+
throw new Error(
59+
'Cost confirmation ID does not match the expected cost of creating a branch.'
60+
);
10761
}
10862
return await branching.createBranch(project_id, { name });
10963
},

packages/mcp-server-supabase/src/tools/database-operation-tools.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { source } from 'common-tags';
22
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
3+
import { ElicitResultSchema } from '@modelcontextprotocol/sdk/types.js';
34
import { z } from 'zod';
45
import { listExtensionsSql, listTablesSql } from '../pg-meta/index.js';
56
import {
@@ -221,7 +222,7 @@ export function getDatabaseTools({
221222
// Try to request user confirmation via elicitation
222223
if (server) {
223224
try {
224-
const result = (await server.request(
225+
const result = await server.request(
225226
{
226227
method: 'elicitation/create',
227228
params: {
@@ -240,12 +241,8 @@ export function getDatabaseTools({
240241
},
241242
},
242243
},
243-
// @ts-ignore - elicitation types might not be available
244-
{ elicitation: true }
245-
)) as {
246-
action: 'accept' | 'decline' | 'cancel';
247-
content?: { confirm?: boolean };
248-
};
244+
ElicitResultSchema
245+
);
249246

250247
// User declined or cancelled
251248
if (result.action !== 'accept' || !result.content?.confirm) {

0 commit comments

Comments
 (0)