#
tokens: 28208/50000 1/95 files (page 3/4)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 3 of 4. Use http://codebase.md/supabase-community/supabase-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .github
│   └── workflows
│       └── tests.yml
├── .gitignore
├── .nvmrc
├── .vscode
│   └── settings.json
├── biome.json
├── CONTRIBUTING.md
├── docs
│   └── production.md
├── LICENSE
├── package.json
├── packages
│   ├── mcp-server-postgrest
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── index.ts
│   │   │   ├── server.test.ts
│   │   │   ├── server.ts
│   │   │   ├── stdio.ts
│   │   │   └── util.ts
│   │   ├── tsconfig.json
│   │   └── tsup.config.ts
│   ├── mcp-server-supabase
│   │   ├── .gitignore
│   │   ├── package.json
│   │   ├── scripts
│   │   │   └── registry
│   │   │       ├── login.sh
│   │   │       └── update-version.ts
│   │   ├── server.json
│   │   ├── src
│   │   │   ├── content-api
│   │   │   │   ├── graphql.test.ts
│   │   │   │   ├── graphql.ts
│   │   │   │   └── index.ts
│   │   │   ├── edge-function.test.ts
│   │   │   ├── edge-function.ts
│   │   │   ├── index.test.ts
│   │   │   ├── index.ts
│   │   │   ├── logs.ts
│   │   │   ├── management-api
│   │   │   │   ├── index.ts
│   │   │   │   └── types.ts
│   │   │   ├── password.test.ts
│   │   │   ├── password.ts
│   │   │   ├── pg-meta
│   │   │   │   ├── columns.sql
│   │   │   │   ├── extensions.sql
│   │   │   │   ├── index.ts
│   │   │   │   ├── tables.sql
│   │   │   │   └── types.ts
│   │   │   ├── platform
│   │   │   │   ├── api-platform.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── types.ts
│   │   │   ├── pricing.ts
│   │   │   ├── regions.ts
│   │   │   ├── server.test.ts
│   │   │   ├── server.ts
│   │   │   ├── tools
│   │   │   │   ├── account-tools.ts
│   │   │   │   ├── branching-tools.ts
│   │   │   │   ├── database-operation-tools.ts
│   │   │   │   ├── debugging-tools.ts
│   │   │   │   ├── development-tools.ts
│   │   │   │   ├── docs-tools.ts
│   │   │   │   ├── edge-function-tools.ts
│   │   │   │   ├── storage-tools.ts
│   │   │   │   └── util.ts
│   │   │   ├── transports
│   │   │   │   ├── stdio.ts
│   │   │   │   ├── util.test.ts
│   │   │   │   └── util.ts
│   │   │   ├── types
│   │   │   │   └── sql.d.ts
│   │   │   ├── types.test.ts
│   │   │   ├── types.ts
│   │   │   ├── util.test.ts
│   │   │   └── util.ts
│   │   ├── test
│   │   │   ├── e2e
│   │   │   │   ├── functions.e2e.ts
│   │   │   │   ├── projects.e2e.ts
│   │   │   │   ├── prompt-injection.e2e.ts
│   │   │   │   ├── setup.ts
│   │   │   │   └── utils.ts
│   │   │   ├── extensions.d.ts
│   │   │   ├── extensions.ts
│   │   │   ├── mocks.ts
│   │   │   ├── plugins
│   │   │   │   └── text-loader.ts
│   │   │   └── stdio.integration.ts
│   │   ├── tsconfig.json
│   │   ├── tsup.config.ts
│   │   ├── vitest.config.ts
│   │   ├── vitest.setup.ts
│   │   └── vitest.workspace.ts
│   └── mcp-utils
│       ├── package.json
│       ├── README.md
│       ├── src
│       │   ├── index.ts
│       │   ├── server.test.ts
│       │   ├── server.ts
│       │   ├── stream-transport.ts
│       │   ├── types.ts
│       │   ├── util.test.ts
│       │   └── util.ts
│       ├── tsconfig.json
│       └── tsup.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
└── supabase
    ├── config.toml
    ├── migrations
    │   ├── 20241220232417_todos.sql
    │   └── 20250109000000_add_todo_policies.sql
    └── seed.sql
```

# Files

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

```typescript
   1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
   2 | import {
   3 |   CallToolResultSchema,
   4 |   type CallToolRequest,
   5 | } from '@modelcontextprotocol/sdk/types.js';
   6 | import { StreamTransport } from '@supabase/mcp-utils';
   7 | import { codeBlock, stripIndent } from 'common-tags';
   8 | import { setupServer } from 'msw/node';
   9 | import { beforeEach, describe, expect, test } from 'vitest';
  10 | import {
  11 |   ACCESS_TOKEN,
  12 |   API_URL,
  13 |   contentApiMockSchema,
  14 |   mockContentApiSchemaLoadCount,
  15 |   createOrganization,
  16 |   createProject,
  17 |   createBranch,
  18 |   MCP_CLIENT_NAME,
  19 |   MCP_CLIENT_VERSION,
  20 |   mockBranches,
  21 |   mockContentApi,
  22 |   mockManagementApi,
  23 |   mockOrgs,
  24 |   mockProjects,
  25 | } from '../test/mocks.js';
  26 | import { createSupabaseApiPlatform } from './platform/api-platform.js';
  27 | import { BRANCH_COST_HOURLY, PROJECT_COST_MONTHLY } from './pricing.js';
  28 | import { createSupabaseMcpServer } from './server.js';
  29 | import type { SupabasePlatform } from './platform/types.js';
  30 | 
  31 | beforeEach(async () => {
  32 |   mockOrgs.clear();
  33 |   mockProjects.clear();
  34 |   mockBranches.clear();
  35 |   mockContentApiSchemaLoadCount.value = 0;
  36 | 
  37 |   const server = setupServer(...mockContentApi, ...mockManagementApi);
  38 |   server.listen({ onUnhandledRequest: 'error' });
  39 | });
  40 | 
  41 | type SetupOptions = {
  42 |   accessToken?: string;
  43 |   projectId?: string;
  44 |   platform?: SupabasePlatform;
  45 |   readOnly?: boolean;
  46 |   features?: string[];
  47 | };
  48 | 
  49 | /**
  50 |  * Sets up an MCP client and server for testing.
  51 |  */
  52 | async function setup(options: SetupOptions = {}) {
  53 |   const { accessToken = ACCESS_TOKEN, projectId, readOnly, features } = options;
  54 |   const clientTransport = new StreamTransport();
  55 |   const serverTransport = new StreamTransport();
  56 | 
  57 |   clientTransport.readable.pipeTo(serverTransport.writable);
  58 |   serverTransport.readable.pipeTo(clientTransport.writable);
  59 | 
  60 |   const client = new Client(
  61 |     {
  62 |       name: MCP_CLIENT_NAME,
  63 |       version: MCP_CLIENT_VERSION,
  64 |     },
  65 |     {
  66 |       capabilities: {},
  67 |     }
  68 |   );
  69 | 
  70 |   const platform =
  71 |     options.platform ??
  72 |     createSupabaseApiPlatform({
  73 |       accessToken,
  74 |       apiUrl: API_URL,
  75 |     });
  76 | 
  77 |   const server = createSupabaseMcpServer({
  78 |     platform,
  79 |     projectId,
  80 |     readOnly,
  81 |     features,
  82 |   });
  83 | 
  84 |   await server.connect(serverTransport);
  85 |   await client.connect(clientTransport);
  86 | 
  87 |   /**
  88 |    * Calls a tool with the given parameters.
  89 |    *
  90 |    * Wrapper around the `client.callTool` method to handle the response and errors.
  91 |    */
  92 |   async function callTool(params: CallToolRequest['params']) {
  93 |     const output = await client.callTool(params);
  94 |     const { content } = CallToolResultSchema.parse(output);
  95 |     const [textContent] = content;
  96 | 
  97 |     if (!textContent) {
  98 |       return undefined;
  99 |     }
 100 | 
 101 |     if (textContent.type !== 'text') {
 102 |       throw new Error('tool result content is not text');
 103 |     }
 104 | 
 105 |     if (textContent.text === '') {
 106 |       throw new Error('tool result content is empty');
 107 |     }
 108 | 
 109 |     const result = JSON.parse(textContent.text);
 110 | 
 111 |     if (output.isError) {
 112 |       throw new Error(result.error.message);
 113 |     }
 114 | 
 115 |     return result;
 116 |   }
 117 | 
 118 |   return { client, clientTransport, callTool, server, serverTransport };
 119 | }
 120 | 
 121 | describe('tools', () => {
 122 |   test('list organizations', async () => {
 123 |     const { callTool } = await setup();
 124 | 
 125 |     const org1 = await createOrganization({
 126 |       name: 'Org 1',
 127 |       plan: 'free',
 128 |       allowed_release_channels: ['ga'],
 129 |     });
 130 |     const org2 = await createOrganization({
 131 |       name: 'Org 2',
 132 |       plan: 'free',
 133 |       allowed_release_channels: ['ga'],
 134 |     });
 135 | 
 136 |     const result = await callTool({
 137 |       name: 'list_organizations',
 138 |       arguments: {},
 139 |     });
 140 | 
 141 |     expect(result).toEqual([
 142 |       { id: org1.id, name: org1.name },
 143 |       { id: org2.id, name: org2.name },
 144 |     ]);
 145 |   });
 146 | 
 147 |   test('get organization', async () => {
 148 |     const { callTool } = await setup();
 149 | 
 150 |     const org = await createOrganization({
 151 |       name: 'My Org',
 152 |       plan: 'free',
 153 |       allowed_release_channels: ['ga'],
 154 |     });
 155 | 
 156 |     const result = await callTool({
 157 |       name: 'get_organization',
 158 |       arguments: {
 159 |         id: org.id,
 160 |       },
 161 |     });
 162 | 
 163 |     expect(result).toEqual(org);
 164 |   });
 165 | 
 166 |   test('get next project cost for free org', async () => {
 167 |     const { callTool } = await setup();
 168 | 
 169 |     const freeOrg = await createOrganization({
 170 |       name: 'Free Org',
 171 |       plan: 'free',
 172 |       allowed_release_channels: ['ga'],
 173 |     });
 174 | 
 175 |     const result = await callTool({
 176 |       name: 'get_cost',
 177 |       arguments: {
 178 |         type: 'project',
 179 |         organization_id: freeOrg.id,
 180 |       },
 181 |     });
 182 | 
 183 |     expect(result).toEqual(
 184 |       'The new project will cost $0 monthly. You must repeat this to the user and confirm their understanding.'
 185 |     );
 186 |   });
 187 | 
 188 |   test('get next project cost for paid org with 0 projects', async () => {
 189 |     const { callTool } = await setup();
 190 | 
 191 |     const paidOrg = await createOrganization({
 192 |       name: 'Paid Org',
 193 |       plan: 'pro',
 194 |       allowed_release_channels: ['ga'],
 195 |     });
 196 | 
 197 |     const result = await callTool({
 198 |       name: 'get_cost',
 199 |       arguments: {
 200 |         type: 'project',
 201 |         organization_id: paidOrg.id,
 202 |       },
 203 |     });
 204 | 
 205 |     expect(result).toEqual(
 206 |       'The new project will cost $0 monthly. You must repeat this to the user and confirm their understanding.'
 207 |     );
 208 |   });
 209 | 
 210 |   test('get next project cost for paid org with > 0 active projects', async () => {
 211 |     const { callTool } = await setup();
 212 | 
 213 |     const paidOrg = await createOrganization({
 214 |       name: 'Paid Org',
 215 |       plan: 'pro',
 216 |       allowed_release_channels: ['ga'],
 217 |     });
 218 | 
 219 |     const priorProject = await createProject({
 220 |       name: 'Project 1',
 221 |       region: 'us-east-1',
 222 |       organization_id: paidOrg.id,
 223 |     });
 224 |     priorProject.status = 'ACTIVE_HEALTHY';
 225 | 
 226 |     const result = await callTool({
 227 |       name: 'get_cost',
 228 |       arguments: {
 229 |         type: 'project',
 230 |         organization_id: paidOrg.id,
 231 |       },
 232 |     });
 233 | 
 234 |     expect(result).toEqual(
 235 |       `The new project will cost $${PROJECT_COST_MONTHLY} monthly. You must repeat this to the user and confirm their understanding.`
 236 |     );
 237 |   });
 238 | 
 239 |   test('get next project cost for paid org with > 0 inactive projects', async () => {
 240 |     const { callTool } = await setup();
 241 | 
 242 |     const paidOrg = await createOrganization({
 243 |       name: 'Paid Org',
 244 |       plan: 'pro',
 245 |       allowed_release_channels: ['ga'],
 246 |     });
 247 | 
 248 |     const priorProject = await createProject({
 249 |       name: 'Project 1',
 250 |       region: 'us-east-1',
 251 |       organization_id: paidOrg.id,
 252 |     });
 253 |     priorProject.status = 'INACTIVE';
 254 | 
 255 |     const result = await callTool({
 256 |       name: 'get_cost',
 257 |       arguments: {
 258 |         type: 'project',
 259 |         organization_id: paidOrg.id,
 260 |       },
 261 |     });
 262 | 
 263 |     expect(result).toEqual(
 264 |       `The new project will cost $0 monthly. You must repeat this to the user and confirm their understanding.`
 265 |     );
 266 |   });
 267 | 
 268 |   test('get branch cost', async () => {
 269 |     const { callTool } = await setup();
 270 | 
 271 |     const paidOrg = await createOrganization({
 272 |       name: 'Paid Org',
 273 |       plan: 'pro',
 274 |       allowed_release_channels: ['ga'],
 275 |     });
 276 | 
 277 |     const result = await callTool({
 278 |       name: 'get_cost',
 279 |       arguments: {
 280 |         type: 'branch',
 281 |         organization_id: paidOrg.id,
 282 |       },
 283 |     });
 284 | 
 285 |     expect(result).toEqual(
 286 |       `The new branch will cost $${BRANCH_COST_HOURLY} hourly. You must repeat this to the user and confirm their understanding.`
 287 |     );
 288 |   });
 289 | 
 290 |   test('list projects', async () => {
 291 |     const { callTool } = await setup();
 292 | 
 293 |     const org = await createOrganization({
 294 |       name: 'My Org',
 295 |       plan: 'free',
 296 |       allowed_release_channels: ['ga'],
 297 |     });
 298 | 
 299 |     const project1 = await createProject({
 300 |       name: 'Project 1',
 301 |       region: 'us-east-1',
 302 |       organization_id: org.id,
 303 |     });
 304 | 
 305 |     const project2 = await createProject({
 306 |       name: 'Project 2',
 307 |       region: 'us-east-1',
 308 |       organization_id: org.id,
 309 |     });
 310 | 
 311 |     const result = await callTool({
 312 |       name: 'list_projects',
 313 |       arguments: {},
 314 |     });
 315 | 
 316 |     expect(result).toEqual([project1.details, project2.details]);
 317 |   });
 318 | 
 319 |   test('get project', async () => {
 320 |     const { callTool } = await setup();
 321 | 
 322 |     const org = await createOrganization({
 323 |       name: 'My Org',
 324 |       plan: 'free',
 325 |       allowed_release_channels: ['ga'],
 326 |     });
 327 | 
 328 |     const project = await createProject({
 329 |       name: 'Project 1',
 330 |       region: 'us-east-1',
 331 |       organization_id: org.id,
 332 |     });
 333 | 
 334 |     const result = await callTool({
 335 |       name: 'get_project',
 336 |       arguments: {
 337 |         id: project.id,
 338 |       },
 339 |     });
 340 | 
 341 |     expect(result).toEqual(project.details);
 342 |   });
 343 | 
 344 |   test('create project', async () => {
 345 |     const { callTool } = await setup();
 346 | 
 347 |     const freeOrg = await createOrganization({
 348 |       name: 'Free Org',
 349 |       plan: 'free',
 350 |       allowed_release_channels: ['ga'],
 351 |     });
 352 | 
 353 |     const confirm_cost_id = await callTool({
 354 |       name: 'confirm_cost',
 355 |       arguments: {
 356 |         type: 'project',
 357 |         recurrence: 'monthly',
 358 |         amount: 0,
 359 |       },
 360 |     });
 361 | 
 362 |     const newProject = {
 363 |       name: 'New Project',
 364 |       region: 'us-east-1',
 365 |       organization_id: freeOrg.id,
 366 |       confirm_cost_id,
 367 |     };
 368 | 
 369 |     const result = await callTool({
 370 |       name: 'create_project',
 371 |       arguments: newProject,
 372 |     });
 373 | 
 374 |     const { confirm_cost_id: _, ...projectInfo } = newProject;
 375 | 
 376 |     expect(result).toEqual({
 377 |       ...projectInfo,
 378 |       id: expect.stringMatching(/^.+$/),
 379 |       created_at: expect.stringMatching(
 380 |         /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/
 381 |       ),
 382 |       status: 'UNKNOWN',
 383 |     });
 384 |   });
 385 | 
 386 |   test('create project in read-only mode throws an error', async () => {
 387 |     const { callTool } = await setup({ readOnly: true });
 388 | 
 389 |     const freeOrg = await createOrganization({
 390 |       name: 'Free Org',
 391 |       plan: 'free',
 392 |       allowed_release_channels: ['ga'],
 393 |     });
 394 | 
 395 |     const confirm_cost_id = await callTool({
 396 |       name: 'confirm_cost',
 397 |       arguments: {
 398 |         type: 'project',
 399 |         recurrence: 'monthly',
 400 |         amount: 0,
 401 |       },
 402 |     });
 403 | 
 404 |     const newProject = {
 405 |       name: 'New Project',
 406 |       region: 'us-east-1',
 407 |       organization_id: freeOrg.id,
 408 |       confirm_cost_id,
 409 |     };
 410 | 
 411 |     const result = callTool({
 412 |       name: 'create_project',
 413 |       arguments: newProject,
 414 |     });
 415 | 
 416 |     await expect(result).rejects.toThrow(
 417 |       'Cannot create a project in read-only mode.'
 418 |     );
 419 |   });
 420 | 
 421 |   test('create project without region fails', async () => {
 422 |     const { callTool } = await setup();
 423 | 
 424 |     const freeOrg = await createOrganization({
 425 |       name: 'Free Org',
 426 |       plan: 'free',
 427 |       allowed_release_channels: ['ga'],
 428 |     });
 429 | 
 430 |     const confirm_cost_id = await callTool({
 431 |       name: 'confirm_cost',
 432 |       arguments: {
 433 |         type: 'project',
 434 |         recurrence: 'monthly',
 435 |         amount: 0,
 436 |       },
 437 |     });
 438 | 
 439 |     const newProject = {
 440 |       name: 'New Project',
 441 |       organization_id: freeOrg.id,
 442 |       confirm_cost_id,
 443 |     };
 444 | 
 445 |     const createProjectPromise = callTool({
 446 |       name: 'create_project',
 447 |       arguments: newProject,
 448 |     });
 449 | 
 450 |     await expect(createProjectPromise).rejects.toThrow();
 451 |   });
 452 | 
 453 |   test('create project without cost confirmation fails', async () => {
 454 |     const { callTool } = await setup();
 455 | 
 456 |     const org = await createOrganization({
 457 |       name: 'Paid Org',
 458 |       plan: 'pro',
 459 |       allowed_release_channels: ['ga'],
 460 |     });
 461 | 
 462 |     const newProject = {
 463 |       name: 'New Project',
 464 |       region: 'us-east-1',
 465 |       organization_id: org.id,
 466 |     };
 467 | 
 468 |     const createProjectPromise = callTool({
 469 |       name: 'create_project',
 470 |       arguments: newProject,
 471 |     });
 472 | 
 473 |     await expect(createProjectPromise).rejects.toThrow(
 474 |       'User must confirm understanding of costs before creating a project.'
 475 |     );
 476 |   });
 477 | 
 478 |   test('pause project', async () => {
 479 |     const { callTool } = await setup();
 480 | 
 481 |     const org = await createOrganization({
 482 |       name: 'My Org',
 483 |       plan: 'free',
 484 |       allowed_release_channels: ['ga'],
 485 |     });
 486 | 
 487 |     const project = await createProject({
 488 |       name: 'Project 1',
 489 |       region: 'us-east-1',
 490 |       organization_id: org.id,
 491 |     });
 492 |     project.status = 'ACTIVE_HEALTHY';
 493 | 
 494 |     await callTool({
 495 |       name: 'pause_project',
 496 |       arguments: {
 497 |         project_id: project.id,
 498 |       },
 499 |     });
 500 | 
 501 |     expect(project.status).toEqual('INACTIVE');
 502 |   });
 503 | 
 504 |   test('pause project in read-only mode throws an error', async () => {
 505 |     const { callTool } = await setup({ readOnly: true });
 506 | 
 507 |     const org = await createOrganization({
 508 |       name: 'My Org',
 509 |       plan: 'free',
 510 |       allowed_release_channels: ['ga'],
 511 |     });
 512 | 
 513 |     const project = await createProject({
 514 |       name: 'Project 1',
 515 |       region: 'us-east-1',
 516 |       organization_id: org.id,
 517 |     });
 518 |     project.status = 'ACTIVE_HEALTHY';
 519 | 
 520 |     const result = callTool({
 521 |       name: 'pause_project',
 522 |       arguments: {
 523 |         project_id: project.id,
 524 |       },
 525 |     });
 526 | 
 527 |     await expect(result).rejects.toThrow(
 528 |       'Cannot pause a project in read-only mode.'
 529 |     );
 530 |   });
 531 | 
 532 |   test('restore project', async () => {
 533 |     const { callTool } = await setup();
 534 | 
 535 |     const org = await createOrganization({
 536 |       name: 'My Org',
 537 |       plan: 'free',
 538 |       allowed_release_channels: ['ga'],
 539 |     });
 540 | 
 541 |     const project = await createProject({
 542 |       name: 'Project 1',
 543 |       region: 'us-east-1',
 544 |       organization_id: org.id,
 545 |     });
 546 |     project.status = 'INACTIVE';
 547 | 
 548 |     await callTool({
 549 |       name: 'restore_project',
 550 |       arguments: {
 551 |         project_id: project.id,
 552 |       },
 553 |     });
 554 | 
 555 |     expect(project.status).toEqual('ACTIVE_HEALTHY');
 556 |   });
 557 | 
 558 |   test('restore project in read-only mode throws an error', async () => {
 559 |     const { callTool } = await setup({ readOnly: true });
 560 | 
 561 |     const org = await createOrganization({
 562 |       name: 'My Org',
 563 |       plan: 'free',
 564 |       allowed_release_channels: ['ga'],
 565 |     });
 566 | 
 567 |     const project = await createProject({
 568 |       name: 'Project 1',
 569 |       region: 'us-east-1',
 570 |       organization_id: org.id,
 571 |     });
 572 |     project.status = 'INACTIVE';
 573 | 
 574 |     const result = callTool({
 575 |       name: 'restore_project',
 576 |       arguments: {
 577 |         project_id: project.id,
 578 |       },
 579 |     });
 580 | 
 581 |     await expect(result).rejects.toThrow(
 582 |       'Cannot restore a project in read-only mode.'
 583 |     );
 584 |   });
 585 | 
 586 |   test('get project url', async () => {
 587 |     const { callTool } = await setup();
 588 | 
 589 |     const org = await createOrganization({
 590 |       name: 'My Org',
 591 |       plan: 'free',
 592 |       allowed_release_channels: ['ga'],
 593 |     });
 594 | 
 595 |     const project = await createProject({
 596 |       name: 'Project 1',
 597 |       region: 'us-east-1',
 598 |       organization_id: org.id,
 599 |     });
 600 |     project.status = 'ACTIVE_HEALTHY';
 601 | 
 602 |     const result = await callTool({
 603 |       name: 'get_project_url',
 604 |       arguments: {
 605 |         project_id: project.id,
 606 |       },
 607 |     });
 608 |     expect(result).toEqual(`https://${project.id}.supabase.co`);
 609 |   });
 610 | 
 611 |   test('get anon or publishable keys', async () => {
 612 |     const { callTool } = await setup();
 613 |     const org = await createOrganization({
 614 |       name: 'My Org',
 615 |       plan: 'free',
 616 |       allowed_release_channels: ['ga'],
 617 |     });
 618 |     const project = await createProject({
 619 |       name: 'Project 1',
 620 |       region: 'us-east-1',
 621 |       organization_id: org.id,
 622 |     });
 623 |     project.status = 'ACTIVE_HEALTHY';
 624 | 
 625 |     const result = await callTool({
 626 |       name: 'get_publishable_keys',
 627 |       arguments: {
 628 |         project_id: project.id,
 629 |       },
 630 |     });
 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');
 651 |   });
 652 | 
 653 |   test('list storage buckets', async () => {
 654 |     const { callTool } = await setup({ features: ['storage'] });
 655 | 
 656 |     const org = await createOrganization({
 657 |       name: 'My Org',
 658 |       plan: 'free',
 659 |       allowed_release_channels: ['ga'],
 660 |     });
 661 | 
 662 |     const project = await createProject({
 663 |       name: 'Project 1',
 664 |       region: 'us-east-1',
 665 |       organization_id: org.id,
 666 |     });
 667 |     project.status = 'ACTIVE_HEALTHY';
 668 | 
 669 |     project.createStorageBucket('bucket1', true);
 670 |     project.createStorageBucket('bucket2', false);
 671 | 
 672 |     const result = await callTool({
 673 |       name: 'list_storage_buckets',
 674 |       arguments: {
 675 |         project_id: project.id,
 676 |       },
 677 |     });
 678 | 
 679 |     expect(Array.isArray(result)).toBe(true);
 680 |     expect(result.length).toBe(2);
 681 |     expect(result[0]).toEqual(
 682 |       expect.objectContaining({
 683 |         name: 'bucket1',
 684 |         public: true,
 685 |         created_at: expect.any(String),
 686 |         updated_at: expect.any(String),
 687 |       })
 688 |     );
 689 |     expect(result[1]).toEqual(
 690 |       expect.objectContaining({
 691 |         name: 'bucket2',
 692 |         public: false,
 693 |         created_at: expect.any(String),
 694 |         updated_at: expect.any(String),
 695 |       })
 696 |     );
 697 |   });
 698 | 
 699 |   test('get storage config', async () => {
 700 |     const { callTool } = await setup({ features: ['storage'] });
 701 | 
 702 |     const org = await createOrganization({
 703 |       name: 'My Org',
 704 |       plan: 'free',
 705 |       allowed_release_channels: ['ga'],
 706 |     });
 707 | 
 708 |     const project = await createProject({
 709 |       name: 'Project 1',
 710 |       region: 'us-east-1',
 711 |       organization_id: org.id,
 712 |     });
 713 |     project.status = 'ACTIVE_HEALTHY';
 714 | 
 715 |     const result = await callTool({
 716 |       name: 'get_storage_config',
 717 |       arguments: {
 718 |         project_id: project.id,
 719 |       },
 720 |     });
 721 | 
 722 |     expect(result).toEqual({
 723 |       fileSizeLimit: expect.any(Number),
 724 |       features: {
 725 |         imageTransformation: { enabled: expect.any(Boolean) },
 726 |         s3Protocol: { enabled: expect.any(Boolean) },
 727 |       },
 728 |     });
 729 |   });
 730 | 
 731 |   test('update storage config', async () => {
 732 |     const { callTool } = await setup({ features: ['storage'] });
 733 | 
 734 |     const org = await createOrganization({
 735 |       name: 'My Org',
 736 |       plan: 'free',
 737 |       allowed_release_channels: ['ga'],
 738 |     });
 739 | 
 740 |     const project = await createProject({
 741 |       name: 'Project 1',
 742 |       region: 'us-east-1',
 743 |       organization_id: org.id,
 744 |     });
 745 |     project.status = 'ACTIVE_HEALTHY';
 746 | 
 747 |     const config = {
 748 |       fileSizeLimit: 50,
 749 |       features: {
 750 |         imageTransformation: { enabled: true },
 751 |         s3Protocol: { enabled: false },
 752 |       },
 753 |     };
 754 | 
 755 |     const result = await callTool({
 756 |       name: 'update_storage_config',
 757 |       arguments: {
 758 |         project_id: project.id,
 759 |         config,
 760 |       },
 761 |     });
 762 | 
 763 |     expect(result).toEqual({ success: true });
 764 |   });
 765 | 
 766 |   test('update storage config in read-only mode throws an error', async () => {
 767 |     const { callTool } = await setup({ readOnly: true, features: ['storage'] });
 768 | 
 769 |     const org = await createOrganization({
 770 |       name: 'My Org',
 771 |       plan: 'free',
 772 |       allowed_release_channels: ['ga'],
 773 |     });
 774 | 
 775 |     const project = await createProject({
 776 |       name: 'Project 1',
 777 |       region: 'us-east-1',
 778 |       organization_id: org.id,
 779 |     });
 780 |     project.status = 'ACTIVE_HEALTHY';
 781 | 
 782 |     const config = {
 783 |       fileSizeLimit: 50,
 784 |       features: {
 785 |         imageTransformation: { enabled: true },
 786 |         s3Protocol: { enabled: false },
 787 |       },
 788 |     };
 789 | 
 790 |     const result = callTool({
 791 |       name: 'update_storage_config',
 792 |       arguments: {
 793 |         project_id: project.id,
 794 |         config,
 795 |       },
 796 |     });
 797 | 
 798 |     await expect(result).rejects.toThrow(
 799 |       'Cannot update storage config in read-only mode.'
 800 |     );
 801 |   });
 802 | 
 803 |   test('execute sql', async () => {
 804 |     const { callTool } = await setup();
 805 | 
 806 |     const org = await createOrganization({
 807 |       name: 'My Org',
 808 |       plan: 'free',
 809 |       allowed_release_channels: ['ga'],
 810 |     });
 811 | 
 812 |     const project = await createProject({
 813 |       name: 'Project 1',
 814 |       region: 'us-east-1',
 815 |       organization_id: org.id,
 816 |     });
 817 |     project.status = 'ACTIVE_HEALTHY';
 818 | 
 819 |     const query = 'select 1+1 as sum';
 820 | 
 821 |     const result = await callTool({
 822 |       name: 'execute_sql',
 823 |       arguments: {
 824 |         project_id: project.id,
 825 |         query,
 826 |       },
 827 |     });
 828 | 
 829 |     expect(result).toContain('untrusted user data');
 830 |     expect(result).toMatch(/<untrusted-data-\w{8}-\w{4}-\w{4}-\w{4}-\w{12}>/);
 831 |     expect(result).toContain(JSON.stringify([{ sum: 2 }]));
 832 |     expect(result).toMatch(/<\/untrusted-data-\w{8}-\w{4}-\w{4}-\w{4}-\w{12}>/);
 833 |   });
 834 | 
 835 |   test('can run read queries in read-only mode', async () => {
 836 |     const { callTool } = await setup({ readOnly: true });
 837 | 
 838 |     const org = await createOrganization({
 839 |       name: 'My Org',
 840 |       plan: 'free',
 841 |       allowed_release_channels: ['ga'],
 842 |     });
 843 | 
 844 |     const project = await createProject({
 845 |       name: 'Project 1',
 846 |       region: 'us-east-1',
 847 |       organization_id: org.id,
 848 |     });
 849 |     project.status = 'ACTIVE_HEALTHY';
 850 | 
 851 |     const query = 'select 1+1 as sum';
 852 | 
 853 |     const result = await callTool({
 854 |       name: 'execute_sql',
 855 |       arguments: {
 856 |         project_id: project.id,
 857 |         query,
 858 |       },
 859 |     });
 860 | 
 861 |     expect(result).toContain('untrusted user data');
 862 |     expect(result).toMatch(/<untrusted-data-\w{8}-\w{4}-\w{4}-\w{4}-\w{12}>/);
 863 |     expect(result).toContain(JSON.stringify([{ sum: 2 }]));
 864 |     expect(result).toMatch(/<\/untrusted-data-\w{8}-\w{4}-\w{4}-\w{4}-\w{12}>/);
 865 |   });
 866 | 
 867 |   test('cannot run write queries in read-only mode', async () => {
 868 |     const { callTool } = await setup({ readOnly: true });
 869 | 
 870 |     const org = await createOrganization({
 871 |       name: 'My Org',
 872 |       plan: 'free',
 873 |       allowed_release_channels: ['ga'],
 874 |     });
 875 | 
 876 |     const project = await createProject({
 877 |       name: 'Project 1',
 878 |       region: 'us-east-1',
 879 |       organization_id: org.id,
 880 |     });
 881 |     project.status = 'ACTIVE_HEALTHY';
 882 | 
 883 |     const query =
 884 |       'create table test (id integer generated always as identity primary key)';
 885 | 
 886 |     const resultPromise = callTool({
 887 |       name: 'execute_sql',
 888 |       arguments: {
 889 |         project_id: project.id,
 890 |         query,
 891 |       },
 892 |     });
 893 | 
 894 |     await expect(resultPromise).rejects.toThrow(
 895 |       'permission denied for schema public'
 896 |     );
 897 |   });
 898 | 
 899 |   test('apply migration, list migrations, check tables', async () => {
 900 |     const { callTool } = await setup();
 901 | 
 902 |     const org = await createOrganization({
 903 |       name: 'My Org',
 904 |       plan: 'free',
 905 |       allowed_release_channels: ['ga'],
 906 |     });
 907 | 
 908 |     const project = await createProject({
 909 |       name: 'Project 1',
 910 |       region: 'us-east-1',
 911 |       organization_id: org.id,
 912 |     });
 913 |     project.status = 'ACTIVE_HEALTHY';
 914 | 
 915 |     const name = 'test_migration';
 916 |     const query =
 917 |       'create table test (id integer generated always as identity primary key)';
 918 | 
 919 |     const result = await callTool({
 920 |       name: 'apply_migration',
 921 |       arguments: {
 922 |         project_id: project.id,
 923 |         name,
 924 |         query,
 925 |       },
 926 |     });
 927 | 
 928 |     expect(result).toEqual({ success: true });
 929 | 
 930 |     const listMigrationsResult = await callTool({
 931 |       name: 'list_migrations',
 932 |       arguments: {
 933 |         project_id: project.id,
 934 |       },
 935 |     });
 936 | 
 937 |     expect(listMigrationsResult).toEqual([
 938 |       {
 939 |         name,
 940 |         version: expect.stringMatching(/^\d{14}$/),
 941 |       },
 942 |     ]);
 943 | 
 944 |     const listTablesResult = await callTool({
 945 |       name: 'list_tables',
 946 |       arguments: {
 947 |         project_id: project.id,
 948 |         schemas: ['public'],
 949 |       },
 950 |     });
 951 | 
 952 |     expect(listTablesResult).toEqual([
 953 |       {
 954 |         schema: 'public',
 955 |         name: 'test',
 956 |         rls_enabled: false,
 957 |         rows: 0,
 958 |         columns: [
 959 |           {
 960 |             name: 'id',
 961 |             data_type: 'integer',
 962 |             format: 'int4',
 963 |             options: ['identity', 'updatable'],
 964 |             identity_generation: 'ALWAYS',
 965 |           },
 966 |         ],
 967 |         primary_keys: ['id'],
 968 |       },
 969 |     ]);
 970 |   });
 971 | 
 972 |   test('cannot apply migration in read-only mode', async () => {
 973 |     const { callTool } = await setup({ readOnly: true });
 974 | 
 975 |     const org = await createOrganization({
 976 |       name: 'My Org',
 977 |       plan: 'free',
 978 |       allowed_release_channels: ['ga'],
 979 |     });
 980 | 
 981 |     const project = await createProject({
 982 |       name: 'Project 1',
 983 |       region: 'us-east-1',
 984 |       organization_id: org.id,
 985 |     });
 986 |     project.status = 'ACTIVE_HEALTHY';
 987 | 
 988 |     const name = 'test-migration';
 989 |     const query =
 990 |       'create table test (id integer generated always as identity primary key)';
 991 | 
 992 |     const resultPromise = callTool({
 993 |       name: 'apply_migration',
 994 |       arguments: {
 995 |         project_id: project.id,
 996 |         name,
 997 |         query,
 998 |       },
 999 |     });
1000 | 
1001 |     await expect(resultPromise).rejects.toThrow(
1002 |       'Cannot apply migration in read-only mode.'
1003 |     );
1004 |   });
1005 | 
1006 |   test('list tables only under a specific schema', async () => {
1007 |     const { callTool } = await setup();
1008 | 
1009 |     const org = await createOrganization({
1010 |       name: 'My Org',
1011 |       plan: 'free',
1012 |       allowed_release_channels: ['ga'],
1013 |     });
1014 | 
1015 |     const project = await createProject({
1016 |       name: 'Project 1',
1017 |       region: 'us-east-1',
1018 |       organization_id: org.id,
1019 |     });
1020 |     project.status = 'ACTIVE_HEALTHY';
1021 | 
1022 |     await project.db.exec('create schema test;');
1023 |     await project.db.exec(
1024 |       'create table public.test_1 (id serial primary key);'
1025 |     );
1026 |     await project.db.exec('create table test.test_2 (id serial primary key);');
1027 | 
1028 |     const result = await callTool({
1029 |       name: 'list_tables',
1030 |       arguments: {
1031 |         project_id: project.id,
1032 |         schemas: ['test'],
1033 |       },
1034 |     });
1035 | 
1036 |     expect(result).toEqual(
1037 |       expect.arrayContaining([expect.objectContaining({ name: 'test_2' })])
1038 |     );
1039 |     expect(result).not.toEqual(
1040 |       expect.arrayContaining([expect.objectContaining({ name: 'test_1' })])
1041 |     );
1042 |   });
1043 | 
1044 |   test('listing all tables excludes system schemas', async () => {
1045 |     const { callTool } = await setup();
1046 | 
1047 |     const org = await createOrganization({
1048 |       name: 'My Org',
1049 |       plan: 'free',
1050 |       allowed_release_channels: ['ga'],
1051 |     });
1052 | 
1053 |     const project = await createProject({
1054 |       name: 'Project 1',
1055 |       region: 'us-east-1',
1056 |       organization_id: org.id,
1057 |     });
1058 |     project.status = 'ACTIVE_HEALTHY';
1059 | 
1060 |     const result = await callTool({
1061 |       name: 'list_tables',
1062 |       arguments: {
1063 |         project_id: project.id,
1064 |       },
1065 |     });
1066 | 
1067 |     expect(result).not.toEqual(
1068 |       expect.arrayContaining([
1069 |         expect.objectContaining({ schema: 'pg_catalog' }),
1070 |       ])
1071 |     );
1072 | 
1073 |     expect(result).not.toEqual(
1074 |       expect.arrayContaining([
1075 |         expect.objectContaining({ schema: 'information_schema' }),
1076 |       ])
1077 |     );
1078 | 
1079 |     expect(result).not.toEqual(
1080 |       expect.arrayContaining([expect.objectContaining({ schema: 'pg_toast' })])
1081 |     );
1082 |   });
1083 | 
1084 |   test('list_tables is not vulnerable to SQL injection via schemas parameter', async () => {
1085 |     const { callTool } = await setup();
1086 | 
1087 |     const org = await createOrganization({
1088 |       name: 'SQLi Org',
1089 |       plan: 'free',
1090 |       allowed_release_channels: ['ga'],
1091 |     });
1092 | 
1093 |     const project = await createProject({
1094 |       name: 'SQLi Project',
1095 |       region: 'us-east-1',
1096 |       organization_id: org.id,
1097 |     });
1098 |     project.status = 'ACTIVE_HEALTHY';
1099 | 
1100 |     // Attempt SQL injection via schemas parameter using payload from HackerOne report
1101 |     // This payload attempts to break out of the string and inject a division by zero expression
1102 |     // Reference: https://linear.app/supabase/issue/AI-139
1103 |     const maliciousSchema = "public') OR (SELECT 1)=1/0--";
1104 | 
1105 |     // With proper parameterization, this should NOT throw "division by zero" error
1106 |     // The literal schema name doesn't exist, so it should return empty array
1107 |     // WITHOUT parameterization, this would throw: "division by zero" error
1108 |     const maliciousResult = await callTool({
1109 |       name: 'list_tables',
1110 |       arguments: {
1111 |         project_id: project.id,
1112 |         schemas: [maliciousSchema],
1113 |       },
1114 |     });
1115 | 
1116 |     // Should return empty array without errors, proving the SQL injection was prevented
1117 |     expect(maliciousResult).toEqual([]);
1118 |   });
1119 | 
1120 |   test('list extensions', async () => {
1121 |     const { callTool } = await setup();
1122 | 
1123 |     const org = await createOrganization({
1124 |       name: 'My Org',
1125 |       plan: 'free',
1126 |       allowed_release_channels: ['ga'],
1127 |     });
1128 | 
1129 |     const project = await createProject({
1130 |       name: 'Project 1',
1131 |       region: 'us-east-1',
1132 |       organization_id: org.id,
1133 |     });
1134 |     project.status = 'ACTIVE_HEALTHY';
1135 | 
1136 |     const result = await callTool({
1137 |       name: 'list_extensions',
1138 |       arguments: {
1139 |         project_id: project.id,
1140 |       },
1141 |     });
1142 | 
1143 |     expect(result).toMatchInlineSnapshot(`
1144 |       [
1145 |         {
1146 |           "comment": "PL/pgSQL procedural language",
1147 |           "default_version": "1.0",
1148 |           "installed_version": "1.0",
1149 |           "name": "plpgsql",
1150 |           "schema": "pg_catalog",
1151 |         },
1152 |       ]
1153 |     `);
1154 |   });
1155 | 
1156 |   test('invalid access token', async () => {
1157 |     const { callTool } = await setup({ accessToken: 'bad-token' });
1158 | 
1159 |     const listOrganizationsPromise = callTool({
1160 |       name: 'list_organizations',
1161 |       arguments: {},
1162 |     });
1163 | 
1164 |     await expect(listOrganizationsPromise).rejects.toThrow('Unauthorized.');
1165 |   });
1166 | 
1167 |   test('invalid sql for apply_migration', async () => {
1168 |     const { callTool } = await setup();
1169 | 
1170 |     const org = await createOrganization({
1171 |       name: 'My Org',
1172 |       plan: 'free',
1173 |       allowed_release_channels: ['ga'],
1174 |     });
1175 | 
1176 |     const project = await createProject({
1177 |       name: 'Project 1',
1178 |       region: 'us-east-1',
1179 |       organization_id: org.id,
1180 |     });
1181 |     project.status = 'ACTIVE_HEALTHY';
1182 | 
1183 |     const name = 'test-migration';
1184 |     const query = 'invalid sql';
1185 | 
1186 |     const applyMigrationPromise = callTool({
1187 |       name: 'apply_migration',
1188 |       arguments: {
1189 |         project_id: project.id,
1190 |         name,
1191 |         query,
1192 |       },
1193 |     });
1194 | 
1195 |     await expect(applyMigrationPromise).rejects.toThrow(
1196 |       'syntax error at or near "invalid"'
1197 |     );
1198 |   });
1199 | 
1200 |   test('invalid sql for execute_sql', async () => {
1201 |     const { callTool } = await setup();
1202 | 
1203 |     const org = await createOrganization({
1204 |       name: 'My Org',
1205 |       plan: 'free',
1206 |       allowed_release_channels: ['ga'],
1207 |     });
1208 | 
1209 |     const project = await createProject({
1210 |       name: 'Project 1',
1211 |       region: 'us-east-1',
1212 |       organization_id: org.id,
1213 |     });
1214 |     project.status = 'ACTIVE_HEALTHY';
1215 | 
1216 |     const query = 'invalid sql';
1217 | 
1218 |     const executeSqlPromise = callTool({
1219 |       name: 'execute_sql',
1220 |       arguments: {
1221 |         project_id: project.id,
1222 |         query,
1223 |       },
1224 |     });
1225 | 
1226 |     await expect(executeSqlPromise).rejects.toThrow(
1227 |       'syntax error at or near "invalid"'
1228 |     );
1229 |   });
1230 | 
1231 |   test('get logs for each service type', async () => {
1232 |     const { callTool } = await setup();
1233 | 
1234 |     const org = await createOrganization({
1235 |       name: 'My Org',
1236 |       plan: 'free',
1237 |       allowed_release_channels: ['ga'],
1238 |     });
1239 | 
1240 |     const project = await createProject({
1241 |       name: 'Project 1',
1242 |       region: 'us-east-1',
1243 |       organization_id: org.id,
1244 |     });
1245 |     project.status = 'ACTIVE_HEALTHY';
1246 | 
1247 |     const services = [
1248 |       'api',
1249 |       'branch-action',
1250 |       'postgres',
1251 |       'edge-function',
1252 |       'auth',
1253 |       'storage',
1254 |       'realtime',
1255 |     ] as const;
1256 | 
1257 |     for (const service of services) {
1258 |       const result = await callTool({
1259 |         name: 'get_logs',
1260 |         arguments: {
1261 |           project_id: project.id,
1262 |           service,
1263 |         },
1264 |       });
1265 | 
1266 |       expect(result).toEqual([]);
1267 |     }
1268 |   });
1269 | 
1270 |   test('get security advisors', async () => {
1271 |     const { callTool } = await setup();
1272 | 
1273 |     const org = await createOrganization({
1274 |       name: 'My Org',
1275 |       plan: 'free',
1276 |       allowed_release_channels: ['ga'],
1277 |     });
1278 | 
1279 |     const project = await createProject({
1280 |       name: 'Project 1',
1281 |       region: 'us-east-1',
1282 |       organization_id: org.id,
1283 |     });
1284 |     project.status = 'ACTIVE_HEALTHY';
1285 | 
1286 |     const result = await callTool({
1287 |       name: 'get_advisors',
1288 |       arguments: {
1289 |         project_id: project.id,
1290 |         type: 'security',
1291 |       },
1292 |     });
1293 | 
1294 |     expect(result).toEqual({ lints: [] });
1295 |   });
1296 | 
1297 |   test('get performance advisors', async () => {
1298 |     const { callTool } = await setup();
1299 | 
1300 |     const org = await createOrganization({
1301 |       name: 'My Org',
1302 |       plan: 'free',
1303 |       allowed_release_channels: ['ga'],
1304 |     });
1305 | 
1306 |     const project = await createProject({
1307 |       name: 'Project 1',
1308 |       region: 'us-east-1',
1309 |       organization_id: org.id,
1310 |     });
1311 |     project.status = 'ACTIVE_HEALTHY';
1312 | 
1313 |     const result = await callTool({
1314 |       name: 'get_advisors',
1315 |       arguments: {
1316 |         project_id: project.id,
1317 |         type: 'performance',
1318 |       },
1319 |     });
1320 | 
1321 |     expect(result).toEqual({ lints: [] });
1322 |   });
1323 | 
1324 |   test('get logs for invalid service type', async () => {
1325 |     const { callTool } = await setup();
1326 | 
1327 |     const org = await createOrganization({
1328 |       name: 'My Org',
1329 |       plan: 'free',
1330 |       allowed_release_channels: ['ga'],
1331 |     });
1332 | 
1333 |     const project = await createProject({
1334 |       name: 'Project 1',
1335 |       region: 'us-east-1',
1336 |       organization_id: org.id,
1337 |     });
1338 |     project.status = 'ACTIVE_HEALTHY';
1339 | 
1340 |     const invalidService = 'invalid-service';
1341 |     const getLogsPromise = callTool({
1342 |       name: 'get_logs',
1343 |       arguments: {
1344 |         project_id: project.id,
1345 |         service: invalidService,
1346 |       },
1347 |     });
1348 |     await expect(getLogsPromise).rejects.toThrow('Invalid enum value');
1349 |   });
1350 | 
1351 |   test('list edge functions', async () => {
1352 |     const { callTool } = await setup();
1353 | 
1354 |     const org = await createOrganization({
1355 |       name: 'My Org',
1356 |       plan: 'free',
1357 |       allowed_release_channels: ['ga'],
1358 |     });
1359 | 
1360 |     const project = await createProject({
1361 |       name: 'Project 1',
1362 |       region: 'us-east-1',
1363 |       organization_id: org.id,
1364 |     });
1365 |     project.status = 'ACTIVE_HEALTHY';
1366 | 
1367 |     const indexContent = codeBlock`
1368 |       Deno.serve(async (req: Request) => {
1369 |         return new Response('Hello world!', { headers: { 'Content-Type': 'text/plain' } })
1370 |       });
1371 |     `;
1372 | 
1373 |     const edgeFunction = await project.deployEdgeFunction(
1374 |       {
1375 |         name: 'hello-world',
1376 |         entrypoint_path: 'index.ts',
1377 |       },
1378 |       [
1379 |         new File([indexContent], 'index.ts', {
1380 |           type: 'application/typescript',
1381 |         }),
1382 |       ]
1383 |     );
1384 | 
1385 |     const result = await callTool({
1386 |       name: 'list_edge_functions',
1387 |       arguments: {
1388 |         project_id: project.id,
1389 |       },
1390 |     });
1391 | 
1392 |     expect(result).toEqual([
1393 |       {
1394 |         id: edgeFunction.id,
1395 |         slug: edgeFunction.slug,
1396 |         version: edgeFunction.version,
1397 |         name: edgeFunction.name,
1398 |         status: edgeFunction.status,
1399 |         entrypoint_path: 'index.ts',
1400 |         import_map_path: undefined,
1401 |         import_map: false,
1402 |         verify_jwt: true,
1403 |         created_at: expect.stringMatching(
1404 |           /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/
1405 |         ),
1406 |         updated_at: expect.stringMatching(
1407 |           /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/
1408 |         ),
1409 |       },
1410 |     ]);
1411 |   });
1412 | 
1413 |   test('get edge function', async () => {
1414 |     const { callTool } = await setup();
1415 | 
1416 |     const org = await createOrganization({
1417 |       name: 'My Org',
1418 |       plan: 'free',
1419 |       allowed_release_channels: ['ga'],
1420 |     });
1421 | 
1422 |     const project = await createProject({
1423 |       name: 'Project 1',
1424 |       region: 'us-east-1',
1425 |       organization_id: org.id,
1426 |     });
1427 |     project.status = 'ACTIVE_HEALTHY';
1428 | 
1429 |     const indexContent = codeBlock`
1430 |       Deno.serve(async (req: Request) => {
1431 |         return new Response('Hello world!', { headers: { 'Content-Type': 'text/plain' } })
1432 |       });
1433 |     `;
1434 | 
1435 |     const edgeFunction = await project.deployEdgeFunction(
1436 |       {
1437 |         name: 'hello-world',
1438 |         entrypoint_path: 'index.ts',
1439 |       },
1440 |       [
1441 |         new File([indexContent], 'index.ts', {
1442 |           type: 'application/typescript',
1443 |         }),
1444 |       ]
1445 |     );
1446 | 
1447 |     const result = await callTool({
1448 |       name: 'get_edge_function',
1449 |       arguments: {
1450 |         project_id: project.id,
1451 |         function_slug: edgeFunction.slug,
1452 |       },
1453 |     });
1454 | 
1455 |     expect(result).toEqual({
1456 |       id: edgeFunction.id,
1457 |       slug: edgeFunction.slug,
1458 |       version: edgeFunction.version,
1459 |       name: edgeFunction.name,
1460 |       status: edgeFunction.status,
1461 |       entrypoint_path: 'index.ts',
1462 |       import_map_path: undefined,
1463 |       import_map: false,
1464 |       verify_jwt: true,
1465 |       created_at: expect.stringMatching(
1466 |         /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/
1467 |       ),
1468 |       updated_at: expect.stringMatching(
1469 |         /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/
1470 |       ),
1471 |       files: [
1472 |         {
1473 |           name: 'index.ts',
1474 |           content: indexContent,
1475 |         },
1476 |       ],
1477 |     });
1478 |   });
1479 | 
1480 |   test('deploy new edge function', async () => {
1481 |     const { callTool } = await setup();
1482 | 
1483 |     const org = await createOrganization({
1484 |       name: 'My Org',
1485 |       plan: 'free',
1486 |       allowed_release_channels: ['ga'],
1487 |     });
1488 | 
1489 |     const project = await createProject({
1490 |       name: 'Project 1',
1491 |       region: 'us-east-1',
1492 |       organization_id: org.id,
1493 |     });
1494 |     project.status = 'ACTIVE_HEALTHY';
1495 | 
1496 |     const functionName = 'hello-world';
1497 |     const functionCode = 'console.log("Hello, world!");';
1498 | 
1499 |     const result = await callTool({
1500 |       name: 'deploy_edge_function',
1501 |       arguments: {
1502 |         project_id: project.id,
1503 |         name: functionName,
1504 |         files: [
1505 |           {
1506 |             name: 'index.ts',
1507 |             content: functionCode,
1508 |           },
1509 |         ],
1510 |       },
1511 |     });
1512 | 
1513 |     expect(result).toEqual({
1514 |       id: expect.stringMatching(/^.+$/),
1515 |       slug: functionName,
1516 |       version: 1,
1517 |       name: functionName,
1518 |       status: 'ACTIVE',
1519 |       entrypoint_path: expect.stringMatching(/index\.ts$/),
1520 |       import_map_path: undefined,
1521 |       import_map: false,
1522 |       verify_jwt: true,
1523 |       created_at: expect.stringMatching(
1524 |         /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/
1525 |       ),
1526 |       updated_at: expect.stringMatching(
1527 |         /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/
1528 |       ),
1529 |     });
1530 |   });
1531 | 
1532 |   test('deploy edge function in read-only mode throws an error', async () => {
1533 |     const { callTool } = await setup({ readOnly: true });
1534 | 
1535 |     const org = await createOrganization({
1536 |       name: 'test-org',
1537 |       plan: 'free',
1538 |       allowed_release_channels: ['ga'],
1539 |     });
1540 | 
1541 |     const project = await createProject({
1542 |       name: 'test-app',
1543 |       region: 'us-east-1',
1544 |       organization_id: org.id,
1545 |     });
1546 |     project.status = 'ACTIVE_HEALTHY';
1547 | 
1548 |     const functionName = 'hello-world';
1549 |     const functionCode = 'console.log("Hello, world!");';
1550 | 
1551 |     const result = callTool({
1552 |       name: 'deploy_edge_function',
1553 |       arguments: {
1554 |         project_id: project.id,
1555 |         name: functionName,
1556 |         files: [
1557 |           {
1558 |             name: 'index.ts',
1559 |             content: functionCode,
1560 |           },
1561 |         ],
1562 |       },
1563 |     });
1564 | 
1565 |     await expect(result).rejects.toThrow(
1566 |       'Cannot deploy an edge function in read-only mode.'
1567 |     );
1568 |   });
1569 | 
1570 |   test('deploy new version of existing edge function', async () => {
1571 |     const { callTool } = await setup();
1572 |     const org = await createOrganization({
1573 |       name: 'My Org',
1574 |       plan: 'free',
1575 |       allowed_release_channels: ['ga'],
1576 |     });
1577 | 
1578 |     const project = await createProject({
1579 |       name: 'Project 1',
1580 |       region: 'us-east-1',
1581 |       organization_id: org.id,
1582 |     });
1583 |     project.status = 'ACTIVE_HEALTHY';
1584 | 
1585 |     const functionName = 'hello-world';
1586 | 
1587 |     const edgeFunction = await project.deployEdgeFunction(
1588 |       {
1589 |         name: functionName,
1590 |         entrypoint_path: 'index.ts',
1591 |       },
1592 |       [
1593 |         new File(['console.log("Hello, world!");'], 'index.ts', {
1594 |           type: 'application/typescript',
1595 |         }),
1596 |       ]
1597 |     );
1598 | 
1599 |     expect(edgeFunction.version).toEqual(1);
1600 | 
1601 |     const originalCreatedAt = edgeFunction.created_at.getTime();
1602 |     const originalUpdatedAt = edgeFunction.updated_at.getTime();
1603 | 
1604 |     const result = await callTool({
1605 |       name: 'deploy_edge_function',
1606 |       arguments: {
1607 |         project_id: project.id,
1608 |         name: functionName,
1609 |         files: [
1610 |           {
1611 |             name: 'index.ts',
1612 |             content: 'console.log("Hello, world! v2");',
1613 |           },
1614 |         ],
1615 |       },
1616 |     });
1617 | 
1618 |     expect(result).toEqual({
1619 |       id: edgeFunction.id,
1620 |       slug: functionName,
1621 |       version: 2,
1622 |       name: functionName,
1623 |       status: 'ACTIVE',
1624 |       entrypoint_path: expect.stringMatching(/index\.ts$/),
1625 |       import_map_path: undefined,
1626 |       import_map: false,
1627 |       verify_jwt: true,
1628 |       created_at: expect.stringMatching(
1629 |         /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/
1630 |       ),
1631 |       updated_at: expect.stringMatching(
1632 |         /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/
1633 |       ),
1634 |     });
1635 | 
1636 |     expect(new Date(result.created_at).getTime()).toEqual(originalCreatedAt);
1637 |     expect(new Date(result.updated_at).getTime()).toBeGreaterThan(
1638 |       originalUpdatedAt
1639 |     );
1640 |   });
1641 | 
1642 |   test('custom edge function import map', async () => {
1643 |     const { callTool } = await setup();
1644 | 
1645 |     const org = await createOrganization({
1646 |       name: 'My Org',
1647 |       plan: 'free',
1648 |       allowed_release_channels: ['ga'],
1649 |     });
1650 | 
1651 |     const project = await createProject({
1652 |       name: 'Project 1',
1653 |       region: 'us-east-1',
1654 |       organization_id: org.id,
1655 |     });
1656 | 
1657 |     const functionName = 'hello-world';
1658 |     const functionCode = 'console.log("Hello, world!");';
1659 | 
1660 |     const result = await callTool({
1661 |       name: 'deploy_edge_function',
1662 |       arguments: {
1663 |         project_id: project.id,
1664 |         name: functionName,
1665 |         import_map_path: 'custom-map.json',
1666 |         files: [
1667 |           {
1668 |             name: 'index.ts',
1669 |             content: functionCode,
1670 |           },
1671 |           {
1672 |             name: 'custom-map.json',
1673 |             content: '{}',
1674 |           },
1675 |         ],
1676 |       },
1677 |     });
1678 | 
1679 |     expect(result.import_map).toBe(true);
1680 |     expect(result.import_map_path).toMatch(/custom-map\.json$/);
1681 |   });
1682 | 
1683 |   test('default edge function import map to deno.json', async () => {
1684 |     const { callTool } = await setup();
1685 | 
1686 |     const org = await createOrganization({
1687 |       name: 'My Org',
1688 |       plan: 'free',
1689 |       allowed_release_channels: ['ga'],
1690 |     });
1691 | 
1692 |     const project = await createProject({
1693 |       name: 'Project 1',
1694 |       region: 'us-east-1',
1695 |       organization_id: org.id,
1696 |     });
1697 | 
1698 |     const functionName = 'hello-world';
1699 |     const functionCode = 'console.log("Hello, world!");';
1700 | 
1701 |     const result = await callTool({
1702 |       name: 'deploy_edge_function',
1703 |       arguments: {
1704 |         project_id: project.id,
1705 |         name: functionName,
1706 |         files: [
1707 |           {
1708 |             name: 'index.ts',
1709 |             content: functionCode,
1710 |           },
1711 |           {
1712 |             name: 'deno.json',
1713 |             content: '{}',
1714 |           },
1715 |         ],
1716 |       },
1717 |     });
1718 | 
1719 |     expect(result.import_map).toBe(true);
1720 |     expect(result.import_map_path).toMatch(/deno\.json$/);
1721 |   });
1722 | 
1723 |   test('default edge function import map to import_map.json', async () => {
1724 |     const { callTool } = await setup();
1725 | 
1726 |     const org = await createOrganization({
1727 |       name: 'My Org',
1728 |       plan: 'free',
1729 |       allowed_release_channels: ['ga'],
1730 |     });
1731 | 
1732 |     const project = await createProject({
1733 |       name: 'Project 1',
1734 |       region: 'us-east-1',
1735 |       organization_id: org.id,
1736 |     });
1737 | 
1738 |     const functionName = 'hello-world';
1739 |     const functionCode = 'console.log("Hello, world!");';
1740 | 
1741 |     const result = await callTool({
1742 |       name: 'deploy_edge_function',
1743 |       arguments: {
1744 |         project_id: project.id,
1745 |         name: functionName,
1746 |         files: [
1747 |           {
1748 |             name: 'index.ts',
1749 |             content: functionCode,
1750 |           },
1751 |           {
1752 |             name: 'import_map.json',
1753 |             content: '{}',
1754 |           },
1755 |         ],
1756 |       },
1757 |     });
1758 | 
1759 |     expect(result.import_map).toBe(true);
1760 |     expect(result.import_map_path).toMatch(/import_map\.json$/);
1761 |   });
1762 | 
1763 |   test('updating edge function with missing import_map_path defaults to previous value', async () => {
1764 |     const { callTool } = await setup();
1765 |     const org = await createOrganization({
1766 |       name: 'My Org',
1767 |       plan: 'free',
1768 |       allowed_release_channels: ['ga'],
1769 |     });
1770 | 
1771 |     const project = await createProject({
1772 |       name: 'Project 1',
1773 |       region: 'us-east-1',
1774 |       organization_id: org.id,
1775 |     });
1776 |     project.status = 'ACTIVE_HEALTHY';
1777 | 
1778 |     const functionName = 'hello-world';
1779 | 
1780 |     const edgeFunction = await project.deployEdgeFunction(
1781 |       {
1782 |         name: functionName,
1783 |         entrypoint_path: 'index.ts',
1784 |         import_map_path: 'custom-map.json',
1785 |       },
1786 |       [
1787 |         new File(['console.log("Hello, world!");'], 'index.ts', {
1788 |           type: 'application/typescript',
1789 |         }),
1790 |         new File(['{}'], 'custom-map.json', {
1791 |           type: 'application/json',
1792 |         }),
1793 |       ]
1794 |     );
1795 | 
1796 |     const result = await callTool({
1797 |       name: 'deploy_edge_function',
1798 |       arguments: {
1799 |         project_id: project.id,
1800 |         name: functionName,
1801 |         files: [
1802 |           {
1803 |             name: 'index.ts',
1804 |             content: 'console.log("Hello, world! v2");',
1805 |           },
1806 |           {
1807 |             name: 'custom-map.json',
1808 |             content: '{}',
1809 |           },
1810 |         ],
1811 |       },
1812 |     });
1813 | 
1814 |     expect(result.import_map).toBe(true);
1815 |     expect(result.import_map_path).toMatch(/custom-map\.json$/);
1816 |   });
1817 | 
1818 |   test('create branch', async () => {
1819 |     const { callTool } = await setup({
1820 |       features: ['account', 'branching'],
1821 |     });
1822 | 
1823 |     const org = await createOrganization({
1824 |       name: 'My Org',
1825 |       plan: 'free',
1826 |       allowed_release_channels: ['ga'],
1827 |     });
1828 | 
1829 |     const project = await createProject({
1830 |       name: 'Project 1',
1831 |       region: 'us-east-1',
1832 |       organization_id: org.id,
1833 |     });
1834 |     project.status = 'ACTIVE_HEALTHY';
1835 | 
1836 |     const confirm_cost_id = await callTool({
1837 |       name: 'confirm_cost',
1838 |       arguments: {
1839 |         type: 'branch',
1840 |         recurrence: 'hourly',
1841 |         amount: BRANCH_COST_HOURLY,
1842 |       },
1843 |     });
1844 | 
1845 |     const branchName = 'test-branch';
1846 |     const result = await callTool({
1847 |       name: 'create_branch',
1848 |       arguments: {
1849 |         project_id: project.id,
1850 |         name: branchName,
1851 |         confirm_cost_id,
1852 |       },
1853 |     });
1854 | 
1855 |     expect(result).toEqual({
1856 |       id: expect.stringMatching(/^.+$/),
1857 |       name: branchName,
1858 |       project_ref: expect.stringMatching(/^.+$/),
1859 |       parent_project_ref: project.id,
1860 |       is_default: false,
1861 |       persistent: false,
1862 |       status: 'CREATING_PROJECT',
1863 |       created_at: expect.stringMatching(
1864 |         /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/
1865 |       ),
1866 |       updated_at: expect.stringMatching(
1867 |         /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/
1868 |       ),
1869 |     });
1870 |   });
1871 | 
1872 |   test('create branch in read-only mode throws an error', async () => {
1873 |     const { callTool } = await setup({
1874 |       readOnly: true,
1875 |       features: ['account', 'branching'],
1876 |     });
1877 | 
1878 |     const org = await createOrganization({
1879 |       name: 'My Org',
1880 |       plan: 'free',
1881 |       allowed_release_channels: ['ga'],
1882 |     });
1883 | 
1884 |     const project = await createProject({
1885 |       name: 'Project 1',
1886 |       region: 'us-east-1',
1887 |       organization_id: org.id,
1888 |     });
1889 |     project.status = 'ACTIVE_HEALTHY';
1890 | 
1891 |     const confirm_cost_id = await callTool({
1892 |       name: 'confirm_cost',
1893 |       arguments: {
1894 |         type: 'branch',
1895 |         recurrence: 'hourly',
1896 |         amount: BRANCH_COST_HOURLY,
1897 |       },
1898 |     });
1899 | 
1900 |     const branchName = 'test-branch';
1901 |     const result = callTool({
1902 |       name: 'create_branch',
1903 |       arguments: {
1904 |         project_id: project.id,
1905 |         name: branchName,
1906 |         confirm_cost_id,
1907 |       },
1908 |     });
1909 | 
1910 |     await expect(result).rejects.toThrow(
1911 |       'Cannot create a branch in read-only mode.'
1912 |     );
1913 |   });
1914 | 
1915 |   test('create branch without cost confirmation fails', async () => {
1916 |     const { callTool } = await setup({ features: ['branching'] });
1917 | 
1918 |     const org = await createOrganization({
1919 |       name: 'Paid Org',
1920 |       plan: 'pro',
1921 |       allowed_release_channels: ['ga'],
1922 |     });
1923 | 
1924 |     const project = await createProject({
1925 |       name: 'Project 1',
1926 |       region: 'us-east-1',
1927 |       organization_id: org.id,
1928 |     });
1929 |     project.status = 'ACTIVE_HEALTHY';
1930 | 
1931 |     const branchName = 'test-branch';
1932 |     const createBranchPromise = callTool({
1933 |       name: 'create_branch',
1934 |       arguments: {
1935 |         project_id: project.id,
1936 |         name: branchName,
1937 |       },
1938 |     });
1939 | 
1940 |     await expect(createBranchPromise).rejects.toThrow(
1941 |       'User must confirm understanding of costs before creating a branch.'
1942 |     );
1943 |   });
1944 | 
1945 |   test('delete branch', async () => {
1946 |     const { callTool } = await setup({
1947 |       features: ['account', 'branching'],
1948 |     });
1949 | 
1950 |     const org = await createOrganization({
1951 |       name: 'My Org',
1952 |       plan: 'free',
1953 |       allowed_release_channels: ['ga'],
1954 |     });
1955 | 
1956 |     const project = await createProject({
1957 |       name: 'Project 1',
1958 |       region: 'us-east-1',
1959 |       organization_id: org.id,
1960 |     });
1961 |     project.status = 'ACTIVE_HEALTHY';
1962 | 
1963 |     const confirm_cost_id = await callTool({
1964 |       name: 'confirm_cost',
1965 |       arguments: {
1966 |         type: 'branch',
1967 |         recurrence: 'hourly',
1968 |         amount: BRANCH_COST_HOURLY,
1969 |       },
1970 |     });
1971 | 
1972 |     const branch = await callTool({
1973 |       name: 'create_branch',
1974 |       arguments: {
1975 |         project_id: project.id,
1976 |         name: 'test-branch',
1977 |         confirm_cost_id,
1978 |       },
1979 |     });
1980 | 
1981 |     const listBranchesResult = await callTool({
1982 |       name: 'list_branches',
1983 |       arguments: {
1984 |         project_id: project.id,
1985 |       },
1986 |     });
1987 | 
1988 |     expect(listBranchesResult).toContainEqual(
1989 |       expect.objectContaining({ id: branch.id })
1990 |     );
1991 |     expect(listBranchesResult).toHaveLength(2);
1992 | 
1993 |     await callTool({
1994 |       name: 'delete_branch',
1995 |       arguments: {
1996 |         branch_id: branch.id,
1997 |       },
1998 |     });
1999 | 
2000 |     const listBranchesResultAfterDelete = await callTool({
2001 |       name: 'list_branches',
2002 |       arguments: {
2003 |         project_id: project.id,
2004 |       },
2005 |     });
2006 | 
2007 |     expect(listBranchesResultAfterDelete).not.toContainEqual(
2008 |       expect.objectContaining({ id: branch.id })
2009 |     );
2010 |     expect(listBranchesResultAfterDelete).toHaveLength(1);
2011 | 
2012 |     const mainBranch = listBranchesResultAfterDelete[0];
2013 | 
2014 |     const deleteBranchPromise = callTool({
2015 |       name: 'delete_branch',
2016 |       arguments: {
2017 |         branch_id: mainBranch.id,
2018 |       },
2019 |     });
2020 | 
2021 |     await expect(deleteBranchPromise).rejects.toThrow(
2022 |       'Cannot delete the default branch.'
2023 |     );
2024 |   });
2025 | 
2026 |   test('delete branch in read-only mode throws an error', async () => {
2027 |     const { callTool } = await setup({
2028 |       readOnly: true,
2029 |       features: ['account', 'branching'],
2030 |     });
2031 | 
2032 |     const org = await createOrganization({
2033 |       name: 'My Org',
2034 |       plan: 'free',
2035 |       allowed_release_channels: ['ga'],
2036 |     });
2037 | 
2038 |     const project = await createProject({
2039 |       name: 'Project 1',
2040 |       region: 'us-east-1',
2041 |       organization_id: org.id,
2042 |     });
2043 |     project.status = 'ACTIVE_HEALTHY';
2044 | 
2045 |     const branch = await createBranch({
2046 |       name: 'test-branch',
2047 |       parent_project_ref: project.id,
2048 |     });
2049 | 
2050 |     const listBranchesResult = await callTool({
2051 |       name: 'list_branches',
2052 |       arguments: {
2053 |         project_id: project.id,
2054 |       },
2055 |     });
2056 | 
2057 |     expect(listBranchesResult).toHaveLength(1);
2058 |     expect(listBranchesResult).toContainEqual(
2059 |       expect.objectContaining({ id: branch.id })
2060 |     );
2061 | 
2062 |     const result = callTool({
2063 |       name: 'delete_branch',
2064 |       arguments: {
2065 |         branch_id: branch.id,
2066 |       },
2067 |     });
2068 | 
2069 |     await expect(result).rejects.toThrow(
2070 |       'Cannot delete a branch in read-only mode.'
2071 |     );
2072 |   });
2073 | 
2074 |   test('list branches', async () => {
2075 |     const { callTool } = await setup({ features: ['branching'] });
2076 | 
2077 |     const org = await createOrganization({
2078 |       name: 'My Org',
2079 |       plan: 'free',
2080 |       allowed_release_channels: ['ga'],
2081 |     });
2082 | 
2083 |     const project = await createProject({
2084 |       name: 'Project 1',
2085 |       region: 'us-east-1',
2086 |       organization_id: org.id,
2087 |     });
2088 |     project.status = 'ACTIVE_HEALTHY';
2089 | 
2090 |     const result = await callTool({
2091 |       name: 'list_branches',
2092 |       arguments: {
2093 |         project_id: project.id,
2094 |       },
2095 |     });
2096 | 
2097 |     expect(result).toStrictEqual([]);
2098 |   });
2099 | 
2100 |   test('merge branch', async () => {
2101 |     const { callTool } = await setup({
2102 |       features: ['account', 'branching', 'database'],
2103 |     });
2104 | 
2105 |     const org = await createOrganization({
2106 |       name: 'My Org',
2107 |       plan: 'free',
2108 |       allowed_release_channels: ['ga'],
2109 |     });
2110 | 
2111 |     const project = await createProject({
2112 |       name: 'Project 1',
2113 |       region: 'us-east-1',
2114 |       organization_id: org.id,
2115 |     });
2116 |     project.status = 'ACTIVE_HEALTHY';
2117 | 
2118 |     const confirm_cost_id = await callTool({
2119 |       name: 'confirm_cost',
2120 |       arguments: {
2121 |         type: 'branch',
2122 |         recurrence: 'hourly',
2123 |         amount: BRANCH_COST_HOURLY,
2124 |       },
2125 |     });
2126 | 
2127 |     const branch = await callTool({
2128 |       name: 'create_branch',
2129 |       arguments: {
2130 |         project_id: project.id,
2131 |         name: 'test-branch',
2132 |         confirm_cost_id,
2133 |       },
2134 |     });
2135 | 
2136 |     const migrationName = 'sample_migration';
2137 |     const migrationQuery =
2138 |       'create table sample (id integer generated always as identity primary key)';
2139 |     await callTool({
2140 |       name: 'apply_migration',
2141 |       arguments: {
2142 |         project_id: branch.project_ref,
2143 |         name: migrationName,
2144 |         query: migrationQuery,
2145 |       },
2146 |     });
2147 | 
2148 |     await callTool({
2149 |       name: 'merge_branch',
2150 |       arguments: {
2151 |         branch_id: branch.id,
2152 |       },
2153 |     });
2154 | 
2155 |     // Check that the migration was applied to the parent project
2156 |     const listResult = await callTool({
2157 |       name: 'list_migrations',
2158 |       arguments: {
2159 |         project_id: project.id,
2160 |       },
2161 |     });
2162 | 
2163 |     expect(listResult).toContainEqual({
2164 |       name: migrationName,
2165 |       version: expect.stringMatching(/^\d{14}$/),
2166 |     });
2167 |   });
2168 | 
2169 |   test('merge branch in read-only mode throws an error', async () => {
2170 |     const { callTool } = await setup({
2171 |       readOnly: true,
2172 |       features: ['account', 'branching', 'database'],
2173 |     });
2174 | 
2175 |     const org = await createOrganization({
2176 |       name: 'My Org',
2177 |       plan: 'free',
2178 |       allowed_release_channels: ['ga'],
2179 |     });
2180 | 
2181 |     const project = await createProject({
2182 |       name: 'Project 1',
2183 |       region: 'us-east-1',
2184 |       organization_id: org.id,
2185 |     });
2186 |     project.status = 'ACTIVE_HEALTHY';
2187 | 
2188 |     const branch = await createBranch({
2189 |       name: 'test-branch',
2190 |       parent_project_ref: project.id,
2191 |     });
2192 | 
2193 |     const result = callTool({
2194 |       name: 'merge_branch',
2195 |       arguments: {
2196 |         branch_id: branch.id,
2197 |       },
2198 |     });
2199 | 
2200 |     await expect(result).rejects.toThrow(
2201 |       'Cannot merge a branch in read-only mode.'
2202 |     );
2203 |   });
2204 | 
2205 |   test('reset branch', async () => {
2206 |     const { callTool } = await setup({
2207 |       features: ['account', 'branching', 'database'],
2208 |     });
2209 | 
2210 |     const org = await createOrganization({
2211 |       name: 'My Org',
2212 |       plan: 'free',
2213 |       allowed_release_channels: ['ga'],
2214 |     });
2215 | 
2216 |     const project = await createProject({
2217 |       name: 'Project 1',
2218 |       region: 'us-east-1',
2219 |       organization_id: org.id,
2220 |     });
2221 |     project.status = 'ACTIVE_HEALTHY';
2222 | 
2223 |     const confirm_cost_id = await callTool({
2224 |       name: 'confirm_cost',
2225 |       arguments: {
2226 |         type: 'branch',
2227 |         recurrence: 'hourly',
2228 |         amount: BRANCH_COST_HOURLY,
2229 |       },
2230 |     });
2231 | 
2232 |     const branch = await callTool({
2233 |       name: 'create_branch',
2234 |       arguments: {
2235 |         project_id: project.id,
2236 |         name: 'test-branch',
2237 |         confirm_cost_id,
2238 |       },
2239 |     });
2240 | 
2241 |     // Create a table via execute_sql so that it is untracked
2242 |     const query =
2243 |       'create table test_untracked (id integer generated always as identity primary key)';
2244 |     await callTool({
2245 |       name: 'execute_sql',
2246 |       arguments: {
2247 |         project_id: branch.project_ref,
2248 |         query,
2249 |       },
2250 |     });
2251 | 
2252 |     const firstTablesResult = await callTool({
2253 |       name: 'list_tables',
2254 |       arguments: {
2255 |         project_id: branch.project_ref,
2256 |       },
2257 |     });
2258 | 
2259 |     expect(firstTablesResult).toContainEqual(
2260 |       expect.objectContaining({ name: 'test_untracked' })
2261 |     );
2262 | 
2263 |     await callTool({
2264 |       name: 'reset_branch',
2265 |       arguments: {
2266 |         branch_id: branch.id,
2267 |       },
2268 |     });
2269 | 
2270 |     const secondTablesResult = await callTool({
2271 |       name: 'list_tables',
2272 |       arguments: {
2273 |         project_id: branch.project_ref,
2274 |       },
2275 |     });
2276 | 
2277 |     // Expect the untracked table to be removed after reset
2278 |     expect(secondTablesResult).not.toContainEqual(
2279 |       expect.objectContaining({ name: 'test_untracked' })
2280 |     );
2281 |   });
2282 | 
2283 |   test('reset branch in read-only mode throws an error', async () => {
2284 |     const { callTool } = await setup({
2285 |       readOnly: true,
2286 |       features: ['account', 'branching', 'database'],
2287 |     });
2288 | 
2289 |     const org = await createOrganization({
2290 |       name: 'My Org',
2291 |       plan: 'free',
2292 |       allowed_release_channels: ['ga'],
2293 |     });
2294 | 
2295 |     const project = await createProject({
2296 |       name: 'Project 1',
2297 |       region: 'us-east-1',
2298 |       organization_id: org.id,
2299 |     });
2300 |     project.status = 'ACTIVE_HEALTHY';
2301 | 
2302 |     const branch = await createBranch({
2303 |       name: 'test-branch',
2304 |       parent_project_ref: project.id,
2305 |     });
2306 | 
2307 |     const result = callTool({
2308 |       name: 'reset_branch',
2309 |       arguments: {
2310 |         branch_id: branch.id,
2311 |       },
2312 |     });
2313 | 
2314 |     await expect(result).rejects.toThrow(
2315 |       'Cannot reset a branch in read-only mode.'
2316 |     );
2317 |   });
2318 | 
2319 |   test('revert migrations', async () => {
2320 |     const { callTool } = await setup({
2321 |       features: ['account', 'branching', 'database'],
2322 |     });
2323 | 
2324 |     const org = await createOrganization({
2325 |       name: 'My Org',
2326 |       plan: 'free',
2327 |       allowed_release_channels: ['ga'],
2328 |     });
2329 | 
2330 |     const project = await createProject({
2331 |       name: 'Project 1',
2332 |       region: 'us-east-1',
2333 |       organization_id: org.id,
2334 |     });
2335 |     project.status = 'ACTIVE_HEALTHY';
2336 | 
2337 |     const confirm_cost_id = await callTool({
2338 |       name: 'confirm_cost',
2339 |       arguments: {
2340 |         type: 'branch',
2341 |         recurrence: 'hourly',
2342 |         amount: BRANCH_COST_HOURLY,
2343 |       },
2344 |     });
2345 | 
2346 |     const branch = await callTool({
2347 |       name: 'create_branch',
2348 |       arguments: {
2349 |         project_id: project.id,
2350 |         name: 'test-branch',
2351 |         confirm_cost_id,
2352 |       },
2353 |     });
2354 | 
2355 |     const migrationName = 'sample_migration';
2356 |     const migrationQuery =
2357 |       'create table sample (id integer generated always as identity primary key)';
2358 |     await callTool({
2359 |       name: 'apply_migration',
2360 |       arguments: {
2361 |         project_id: branch.project_ref,
2362 |         name: migrationName,
2363 |         query: migrationQuery,
2364 |       },
2365 |     });
2366 | 
2367 |     // Check that migration has been applied to the branch
2368 |     const firstListResult = await callTool({
2369 |       name: 'list_migrations',
2370 |       arguments: {
2371 |         project_id: branch.project_ref,
2372 |       },
2373 |     });
2374 | 
2375 |     expect(firstListResult).toContainEqual({
2376 |       name: migrationName,
2377 |       version: expect.stringMatching(/^\d{14}$/),
2378 |     });
2379 | 
2380 |     const firstTablesResult = await callTool({
2381 |       name: 'list_tables',
2382 |       arguments: {
2383 |         project_id: branch.project_ref,
2384 |       },
2385 |     });
2386 | 
2387 |     expect(firstTablesResult).toContainEqual(
2388 |       expect.objectContaining({ name: 'sample' })
2389 |     );
2390 | 
2391 |     await callTool({
2392 |       name: 'reset_branch',
2393 |       arguments: {
2394 |         branch_id: branch.id,
2395 |         migration_version: '0',
2396 |       },
2397 |     });
2398 | 
2399 |     // Check that all migrations have been reverted
2400 |     const secondListResult = await callTool({
2401 |       name: 'list_migrations',
2402 |       arguments: {
2403 |         project_id: branch.project_ref,
2404 |       },
2405 |     });
2406 | 
2407 |     expect(secondListResult).toStrictEqual([]);
2408 | 
2409 |     const secondTablesResult = await callTool({
2410 |       name: 'list_tables',
2411 |       arguments: {
2412 |         project_id: branch.project_ref,
2413 |       },
2414 |     });
2415 | 
2416 |     expect(secondTablesResult).not.toContainEqual(
2417 |       expect.objectContaining({ name: 'sample' })
2418 |     );
2419 |   });
2420 | 
2421 |   test('rebase branch', async () => {
2422 |     const { callTool } = await setup({
2423 |       features: ['account', 'branching', 'database'],
2424 |     });
2425 | 
2426 |     const org = await createOrganization({
2427 |       name: 'My Org',
2428 |       plan: 'free',
2429 |       allowed_release_channels: ['ga'],
2430 |     });
2431 | 
2432 |     const project = await createProject({
2433 |       name: 'Project 1',
2434 |       region: 'us-east-1',
2435 |       organization_id: org.id,
2436 |     });
2437 |     project.status = 'ACTIVE_HEALTHY';
2438 | 
2439 |     const confirm_cost_id = await callTool({
2440 |       name: 'confirm_cost',
2441 |       arguments: {
2442 |         type: 'branch',
2443 |         recurrence: 'hourly',
2444 |         amount: BRANCH_COST_HOURLY,
2445 |       },
2446 |     });
2447 | 
2448 |     const branch = await callTool({
2449 |       name: 'create_branch',
2450 |       arguments: {
2451 |         project_id: project.id,
2452 |         name: 'test-branch',
2453 |         confirm_cost_id,
2454 |       },
2455 |     });
2456 | 
2457 |     const migrationName = 'sample_migration';
2458 |     const migrationQuery =
2459 |       'create table sample (id integer generated always as identity primary key)';
2460 |     await callTool({
2461 |       name: 'apply_migration',
2462 |       arguments: {
2463 |         project_id: project.id,
2464 |         name: migrationName,
2465 |         query: migrationQuery,
2466 |       },
2467 |     });
2468 | 
2469 |     await callTool({
2470 |       name: 'rebase_branch',
2471 |       arguments: {
2472 |         branch_id: branch.id,
2473 |       },
2474 |     });
2475 | 
2476 |     // Check that the production migration was applied to the branch
2477 |     const listResult = await callTool({
2478 |       name: 'list_migrations',
2479 |       arguments: {
2480 |         project_id: branch.project_ref,
2481 |       },
2482 |     });
2483 | 
2484 |     expect(listResult).toContainEqual({
2485 |       name: migrationName,
2486 |       version: expect.stringMatching(/^\d{14}$/),
2487 |     });
2488 |   });
2489 | 
2490 |   test('rebase branch in read-only mode throws an error', async () => {
2491 |     const { callTool } = await setup({
2492 |       readOnly: true,
2493 |       features: ['account', 'branching', 'database'],
2494 |     });
2495 | 
2496 |     const org = await createOrganization({
2497 |       name: 'My Org',
2498 |       plan: 'free',
2499 |       allowed_release_channels: ['ga'],
2500 |     });
2501 | 
2502 |     const project = await createProject({
2503 |       name: 'Project 1',
2504 |       region: 'us-east-1',
2505 |       organization_id: org.id,
2506 |     });
2507 |     project.status = 'ACTIVE_HEALTHY';
2508 | 
2509 |     const branch = await createBranch({
2510 |       name: 'test-branch',
2511 |       parent_project_ref: project.id,
2512 |     });
2513 | 
2514 |     const result = callTool({
2515 |       name: 'rebase_branch',
2516 |       arguments: {
2517 |         branch_id: branch.id,
2518 |       },
2519 |     });
2520 | 
2521 |     await expect(result).rejects.toThrow(
2522 |       'Cannot rebase a branch in read-only mode.'
2523 |     );
2524 |   });
2525 | 
2526 |   // We use snake_case because it aligns better with most MCP clients
2527 |   test('all tools follow snake_case naming convention', async () => {
2528 |     const { client } = await setup();
2529 | 
2530 |     const { tools } = await client.listTools();
2531 | 
2532 |     for (const tool of tools) {
2533 |       expect(tool.name, 'expected tool name to be snake_case').toMatch(
2534 |         /^[a-z0-9_]+$/
2535 |       );
2536 | 
2537 |       const parameterNames = Object.keys(tool.inputSchema.properties ?? {});
2538 |       for (const name of parameterNames) {
2539 |         expect(name, 'expected parameter to be snake_case').toMatch(
2540 |           /^[a-z0-9_]+$/
2541 |         );
2542 |       }
2543 |     }
2544 |   });
2545 | 
2546 |   test('all tools provide annotations', async () => {
2547 |     const { client } = await setup();
2548 | 
2549 |     const { tools } = await client.listTools();
2550 | 
2551 |     for (const tool of tools) {
2552 |       expect(tool.annotations, `${tool.name} tool`).toBeDefined();
2553 |       expect(tool.annotations!.title, `${tool.name} tool`).toBeDefined();
2554 |       expect(tool.annotations!.readOnlyHint, `${tool.name} tool`).toBeDefined();
2555 |       expect(
2556 |         tool.annotations!.destructiveHint,
2557 |         `${tool.name} tool`
2558 |       ).toBeDefined();
2559 |       expect(
2560 |         tool.annotations!.idempotentHint,
2561 |         `${tool.name} tool`
2562 |       ).toBeDefined();
2563 |       expect(
2564 |         tool.annotations!.openWorldHint,
2565 |         `${tool.name} tool`
2566 |       ).toBeDefined();
2567 |     }
2568 |   });
2569 | });
2570 | 
2571 | describe('feature groups', () => {
2572 |   test('account tools', async () => {
2573 |     const { client } = await setup({
2574 |       features: ['account'],
2575 |     });
2576 | 
2577 |     const { tools } = await client.listTools();
2578 |     const toolNames = tools.map((tool) => tool.name);
2579 | 
2580 |     expect(toolNames).toEqual([
2581 |       'list_organizations',
2582 |       'get_organization',
2583 |       'list_projects',
2584 |       'get_project',
2585 |       'get_cost',
2586 |       'confirm_cost',
2587 |       'create_project',
2588 |       'pause_project',
2589 |       'restore_project',
2590 |     ]);
2591 |   });
2592 | 
2593 |   test('database tools', async () => {
2594 |     const { client } = await setup({
2595 |       features: ['database'],
2596 |     });
2597 | 
2598 |     const { tools } = await client.listTools();
2599 |     const toolNames = tools.map((tool) => tool.name);
2600 | 
2601 |     expect(toolNames).toEqual([
2602 |       'list_tables',
2603 |       'list_extensions',
2604 |       'list_migrations',
2605 |       'apply_migration',
2606 |       'execute_sql',
2607 |     ]);
2608 |   });
2609 | 
2610 |   test('debugging tools', async () => {
2611 |     const { client } = await setup({
2612 |       features: ['debugging'],
2613 |     });
2614 | 
2615 |     const { tools } = await client.listTools();
2616 |     const toolNames = tools.map((tool) => tool.name);
2617 | 
2618 |     expect(toolNames).toEqual(['get_logs', 'get_advisors']);
2619 |   });
2620 | 
2621 |   test('development tools', async () => {
2622 |     const { client } = await setup({
2623 |       features: ['development'],
2624 |     });
2625 | 
2626 |     const { tools } = await client.listTools();
2627 |     const toolNames = tools.map((tool) => tool.name);
2628 | 
2629 |     expect(toolNames).toEqual([
2630 |       'get_project_url',
2631 |       'get_publishable_keys',
2632 |       'generate_typescript_types',
2633 |     ]);
2634 |   });
2635 | 
2636 |   test('docs tools', async () => {
2637 |     const { client } = await setup({
2638 |       features: ['docs'],
2639 |     });
2640 | 
2641 |     const { tools } = await client.listTools();
2642 |     const toolNames = tools.map((tool) => tool.name);
2643 | 
2644 |     expect(toolNames).toEqual(['search_docs']);
2645 |   });
2646 | 
2647 |   test('functions tools', async () => {
2648 |     const { client } = await setup({
2649 |       features: ['functions'],
2650 |     });
2651 | 
2652 |     const { tools } = await client.listTools();
2653 |     const toolNames = tools.map((tool) => tool.name);
2654 | 
2655 |     expect(toolNames).toEqual([
2656 |       'list_edge_functions',
2657 |       'get_edge_function',
2658 |       'deploy_edge_function',
2659 |     ]);
2660 |   });
2661 | 
2662 |   test('branching tools', async () => {
2663 |     const { client } = await setup({
2664 |       features: ['branching'],
2665 |     });
2666 | 
2667 |     const { tools } = await client.listTools();
2668 |     const toolNames = tools.map((tool) => tool.name);
2669 | 
2670 |     expect(toolNames).toEqual([
2671 |       'create_branch',
2672 |       'list_branches',
2673 |       'delete_branch',
2674 |       'merge_branch',
2675 |       'reset_branch',
2676 |       'rebase_branch',
2677 |     ]);
2678 |   });
2679 | 
2680 |   test('storage tools', async () => {
2681 |     const { client } = await setup({
2682 |       features: ['storage'],
2683 |     });
2684 | 
2685 |     const { tools } = await client.listTools();
2686 |     const toolNames = tools.map((tool) => tool.name);
2687 | 
2688 |     expect(toolNames).toEqual([
2689 |       'list_storage_buckets',
2690 |       'get_storage_config',
2691 |       'update_storage_config',
2692 |     ]);
2693 |   });
2694 | 
2695 |   test('invalid group fails', async () => {
2696 |     const setupPromise = setup({
2697 |       features: ['my-invalid-group'],
2698 |     });
2699 | 
2700 |     await expect(setupPromise).rejects.toThrow('Invalid enum value');
2701 |   });
2702 | 
2703 |   test('duplicate group behaves like single group', async () => {
2704 |     const { client: duplicateClient } = await setup({
2705 |       features: ['account', 'account'],
2706 |     });
2707 | 
2708 |     const { tools } = await duplicateClient.listTools();
2709 |     const toolNames = tools.map((tool) => tool.name);
2710 | 
2711 |     expect(toolNames).toEqual([
2712 |       'list_organizations',
2713 |       'get_organization',
2714 |       'list_projects',
2715 |       'get_project',
2716 |       'get_cost',
2717 |       'confirm_cost',
2718 |       'create_project',
2719 |       'pause_project',
2720 |       'restore_project',
2721 |     ]);
2722 |   });
2723 | 
2724 |   test('tools filtered to available platform operations', async () => {
2725 |     const platform: SupabasePlatform = {
2726 |       database: {
2727 |         executeSql() {
2728 |           throw new Error('Not implemented');
2729 |         },
2730 |         listMigrations() {
2731 |           throw new Error('Not implemented');
2732 |         },
2733 |         applyMigration() {
2734 |           throw new Error('Not implemented');
2735 |         },
2736 |       },
2737 |     };
2738 | 
2739 |     const { client } = await setup({ platform });
2740 |     const { tools } = await client.listTools();
2741 |     const toolNames = tools.map((tool) => tool.name);
2742 | 
2743 |     expect(toolNames).toEqual([
2744 |       'search_docs',
2745 |       'list_tables',
2746 |       'list_extensions',
2747 |       'list_migrations',
2748 |       'apply_migration',
2749 |       'execute_sql',
2750 |     ]);
2751 |   });
2752 | 
2753 |   test('unimplemented feature group produces custom error message', async () => {
2754 |     const platform: SupabasePlatform = {
2755 |       database: {
2756 |         executeSql() {
2757 |           throw new Error('Not implemented');
2758 |         },
2759 |         listMigrations() {
2760 |           throw new Error('Not implemented');
2761 |         },
2762 |         applyMigration() {
2763 |           throw new Error('Not implemented');
2764 |         },
2765 |       },
2766 |     };
2767 | 
2768 |     const setupPromise = setup({ platform, features: ['account'] });
2769 | 
2770 |     await expect(setupPromise).rejects.toThrow(
2771 |       "This platform does not support the 'account' feature group"
2772 |     );
2773 |   });
2774 | });
2775 | 
2776 | describe('project scoped tools', () => {
2777 |   test('no account level tools should exist', async () => {
2778 |     const org = await createOrganization({
2779 |       name: 'My Org',
2780 |       plan: 'free',
2781 |       allowed_release_channels: ['ga'],
2782 |     });
2783 | 
2784 |     const project = await createProject({
2785 |       name: 'Project 1',
2786 |       region: 'us-east-1',
2787 |       organization_id: org.id,
2788 |     });
2789 | 
2790 |     const { client } = await setup({ projectId: project.id });
2791 | 
2792 |     const result = await client.listTools();
2793 | 
2794 |     const accountLevelToolNames = [
2795 |       'list_organizations',
2796 |       'get_organization',
2797 |       'list_projects',
2798 |       'get_project',
2799 |       'get_cost',
2800 |       'confirm_cost',
2801 |       'create_project',
2802 |       'pause_project',
2803 |       'restore_project',
2804 |     ];
2805 | 
2806 |     const toolNames = result.tools.map((tool) => tool.name);
2807 | 
2808 |     for (const accountLevelToolName of accountLevelToolNames) {
2809 |       expect(
2810 |         toolNames,
2811 |         `tool ${accountLevelToolName} should not be available in project scope`
2812 |       ).not.toContain(accountLevelToolName);
2813 |     }
2814 |   });
2815 | 
2816 |   test('no tool should accept a project_id', async () => {
2817 |     const org = await createOrganization({
2818 |       name: 'My Org',
2819 |       plan: 'free',
2820 |       allowed_release_channels: ['ga'],
2821 |     });
2822 | 
2823 |     const project = await createProject({
2824 |       name: 'Project 1',
2825 |       region: 'us-east-1',
2826 |       organization_id: org.id,
2827 |     });
2828 | 
2829 |     const { client } = await setup({ projectId: project.id });
2830 | 
2831 |     const result = await client.listTools();
2832 | 
2833 |     expect(result.tools).toBeDefined();
2834 |     expect(Array.isArray(result.tools)).toBe(true);
2835 | 
2836 |     for (const tool of result.tools) {
2837 |       const schemaProperties = tool.inputSchema.properties ?? {};
2838 |       expect(
2839 |         'project_id' in schemaProperties,
2840 |         `tool ${tool.name} should not accept a project_id`
2841 |       ).toBe(false);
2842 |     }
2843 |   });
2844 | 
2845 |   test('invalid project ID should throw an error', async () => {
2846 |     const { callTool } = await setup({ projectId: 'invalid-project-id' });
2847 | 
2848 |     const listTablesPromise = callTool({
2849 |       name: 'list_tables',
2850 |       arguments: {
2851 |         schemas: ['public'],
2852 |       },
2853 |     });
2854 | 
2855 |     await expect(listTablesPromise).rejects.toThrow('Project not found');
2856 |   });
2857 | 
2858 |   test('passing project_id to a tool should throw an error', async () => {
2859 |     const org = await createOrganization({
2860 |       name: 'My Org',
2861 |       plan: 'free',
2862 |       allowed_release_channels: ['ga'],
2863 |     });
2864 | 
2865 |     const project = await createProject({
2866 |       name: 'Project 1',
2867 |       region: 'us-east-1',
2868 |       organization_id: org.id,
2869 |     });
2870 |     project.status = 'ACTIVE_HEALTHY';
2871 | 
2872 |     const { callTool } = await setup({ projectId: project.id });
2873 | 
2874 |     const listTablesPromise = callTool({
2875 |       name: 'list_tables',
2876 |       arguments: {
2877 |         project_id: 'my-project-id',
2878 |         schemas: ['public'],
2879 |       },
2880 |     });
2881 | 
2882 |     await expect(listTablesPromise).rejects.toThrow('Unrecognized key');
2883 |   });
2884 | 
2885 |   test('listing tables implicitly uses the scoped project_id', async () => {
2886 |     const org = await createOrganization({
2887 |       name: 'My Org',
2888 |       plan: 'free',
2889 |       allowed_release_channels: ['ga'],
2890 |     });
2891 | 
2892 |     const project = await createProject({
2893 |       name: 'Project 1',
2894 |       region: 'us-east-1',
2895 |       organization_id: org.id,
2896 |     });
2897 |     project.status = 'ACTIVE_HEALTHY';
2898 | 
2899 |     project.db
2900 |       .sql`create table test (id integer generated always as identity primary key)`;
2901 | 
2902 |     const { callTool } = await setup({ projectId: project.id });
2903 | 
2904 |     const result = await callTool({
2905 |       name: 'list_tables',
2906 |       arguments: {
2907 |         schemas: ['public'],
2908 |       },
2909 |     });
2910 | 
2911 |     expect(result).toEqual([
2912 |       expect.objectContaining({
2913 |         name: 'test',
2914 |         schema: 'public',
2915 |         columns: [
2916 |           expect.objectContaining({
2917 |             name: 'id',
2918 |             options: expect.arrayContaining(['identity']),
2919 |           }),
2920 |         ],
2921 |       }),
2922 |     ]);
2923 |   });
2924 | });
2925 | 
2926 | describe('docs tools', () => {
2927 |   test('gets content', async () => {
2928 |     const { callTool } = await setup();
2929 |     const query = stripIndent`
2930 |       query ContentQuery {
2931 |         searchDocs(query: "typescript") {
2932 |           nodes {
2933 |             title
2934 |             href
2935 |           }
2936 |         }
2937 |       }
2938 |     `;
2939 | 
2940 |     const result = await callTool({
2941 |       name: 'search_docs',
2942 |       arguments: {
2943 |         graphql_query: query,
2944 |       },
2945 |     });
2946 | 
2947 |     expect(result).toEqual({ dummy: true });
2948 |   });
2949 | 
2950 |   test('tool description contains schema', async () => {
2951 |     const { client } = await setup();
2952 | 
2953 |     const { tools } = await client.listTools();
2954 | 
2955 |     const tool = tools.find((tool) => tool.name === 'search_docs');
2956 | 
2957 |     if (!tool) {
2958 |       throw new Error('tool not found');
2959 |     }
2960 | 
2961 |     if (!tool.description) {
2962 |       throw new Error('tool description not found');
2963 |     }
2964 | 
2965 |     expect(tool.description.includes(contentApiMockSchema)).toBe(true);
2966 |   });
2967 | 
2968 |   test('schema is only loaded when listing tools', async () => {
2969 |     const { client, callTool } = await setup();
2970 | 
2971 |     expect(mockContentApiSchemaLoadCount.value).toBe(0);
2972 | 
2973 |     // "tools/list" requests fetch the schema
2974 |     await client.listTools();
2975 |     expect(mockContentApiSchemaLoadCount.value).toBe(1);
2976 | 
2977 |     // "tools/call" should not fetch the schema again
2978 |     await callTool({
2979 |       name: 'search_docs',
2980 |       arguments: {
2981 |         graphql_query: '{ searchDocs(query: "test") { nodes { title } } }',
2982 |       },
2983 |     });
2984 |     expect(mockContentApiSchemaLoadCount.value).toBe(1);
2985 | 
2986 |     // Additional "tools/list" requests fetch the schema again
2987 |     await client.listTools();
2988 |     expect(mockContentApiSchemaLoadCount.value).toBe(2);
2989 |   });
2990 | });
2991 | 
```
Page 3/4FirstPrevNextLast