Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/mcp-server-postgrest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,14 @@ pnpm add @supabase/mcp-server-postgrest

#### Example

The following example uses the [`StreamTransport`](../mcp-utils#streamtransport) to connect directly between an MCP client and server.
The following example uses the [`StreamTransport`](../mcp-utils#streamtransport) to connect directly between an MCP client and server. It also needs `@modelcontextprotocol/client`, which is a separate package from the `@modelcontextprotocol/server` peer dependency and is not installed for you:

```bash
npm i @modelcontextprotocol/client
```

```ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { Client } from '@modelcontextprotocol/client';
import { StreamTransport } from '@supabase/mcp-utils';
import { createPostgrestMcpServer } from '@supabase/mcp-server-postgrest';

Expand Down
5 changes: 3 additions & 2 deletions packages/mcp-server-postgrest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@
"@supabase/sql-to-rest": "^0.1.8"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": "catalog:",
"@modelcontextprotocol/server": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@modelcontextprotocol/sdk": "catalog:",
"@modelcontextprotocol/client": "catalog:",
"@modelcontextprotocol/server": "catalog:",
"@supabase/auth-js": "^2.67.3",
"@total-typescript/tsconfig": "^1.0.4",
"@types/node": "^22.8.6",
Expand Down
3 changes: 2 additions & 1 deletion packages/mcp-server-postgrest/src/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { Client } from '@modelcontextprotocol/client';
import { AuthClient } from '@supabase/auth-js';
import { StreamTransport } from '@supabase/mcp-utils';
import { describe, expect, test } from 'vitest';

import { createPostgrestMcpServer } from './server.js';

// Requires local Supabase stack running
Expand Down
4 changes: 2 additions & 2 deletions packages/mcp-server-postgrest/src/stdio.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node

import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { parseArgs } from 'node:util';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';

import { createPostgrestMcpServer } from './server.js';

async function main() {
Expand Down
5 changes: 3 additions & 2 deletions packages/mcp-server-supabase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,15 @@
"openapi-fetch": "^0.13.5"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": "catalog:",
"@modelcontextprotocol/server": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@ai-sdk/anthropic": "catalog:",
"@ai-sdk/mcp": "catalog:",
"@electric-sql/pglite": "^0.2.17",
"@modelcontextprotocol/sdk": "catalog:",
"@modelcontextprotocol/client": "catalog:",
"@modelcontextprotocol/server": "catalog:",
"@total-typescript/tsconfig": "^1.0.4",
"@types/common-tags": "^1.8.4",
"@types/node": "^22.8.6",
Expand Down
3 changes: 2 additions & 1 deletion packages/mcp-server-supabase/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { Client } from '@modelcontextprotocol/client';
import { StreamTransport } from '@supabase/mcp-utils';
import { describe, expect, test } from 'vitest';

import {
ACCESS_TOKEN,
API_URL,
Expand Down
14 changes: 6 additions & 8 deletions packages/mcp-server-supabase/src/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import {
CallToolResultSchema,
type CallToolRequest,
} from '@modelcontextprotocol/sdk/types.js';
import { Client } from '@modelcontextprotocol/client';
import type { CallToolRequestParams } from '@modelcontextprotocol/client';
import { StreamTransport } from '@supabase/mcp-utils';
import { codeBlock, stripIndent } from 'common-tags';
import gqlmin from 'gqlmin';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { globalRegistry } from 'zod/v4';

import {
ACCESS_TOKEN,
API_URL,
Expand Down Expand Up @@ -102,9 +100,9 @@ async function setup(options: SetupOptions = {}) {
*
* Wrapper around the `client.callTool` method to handle the response and errors.
*/
async function callTool(params: CallToolRequest['params']) {
async function callTool(params: CallToolRequestParams) {
const output = await client.callTool(params);
const { content } = CallToolResultSchema.parse(output);
const { content } = output;
const [textContent] = content;

if (!textContent) {
Expand Down Expand Up @@ -3893,7 +3891,7 @@ describe('tools', () => {
arguments: { schemas: ['public'] },
});

const result = CallToolResultSchema.parse(resultUntyped);
const result = resultUntyped;
const firstContent = result.content.at(0);
if (!firstContent) {
throw new Error('Expected content in tool response');
Expand Down
11 changes: 4 additions & 7 deletions packages/mcp-server-supabase/src/tools/util.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import {
CallToolResultSchema,
type CallToolRequest,
} from '@modelcontextprotocol/sdk/types.js';
import { Client } from '@modelcontextprotocol/client';
import type { CallToolRequestParams } from '@modelcontextprotocol/client';
import { createMcpServer, StreamTransport } from '@supabase/mcp-utils';
import { describe, expect, test } from 'vitest';
import { z } from 'zod/v4';
Expand Down Expand Up @@ -38,9 +35,9 @@ async function setup(tools: Record<string, ReturnType<typeof injectableTool>>) {
*
* Wrapper around the `client.callTool` method to handle the response and errors.
*/
async function callTool(params: CallToolRequest['params']) {
async function callTool(params: CallToolRequestParams) {
const output = await client.callTool(params);
const { content } = CallToolResultSchema.parse(output);
const { content } = output;
const [textContent] = content;

if (!textContent || textContent.type !== 'text') {
Expand Down
4 changes: 2 additions & 2 deletions packages/mcp-server-supabase/src/transports/stdio.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node

import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { parseArgs } from 'node:util';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';

import packageJson from '../../package.json' with { type: 'json' };
import { createSupabaseApiPlatform } from '../platform/api-platform.js';
import { createSupabaseMcpServer } from '../server.js';
Expand Down
180 changes: 172 additions & 8 deletions packages/mcp-server-supabase/test/stdio.integration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { LoggingMessageNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
import gqlmin from 'gqlmin';
import { createServer, type Server } from 'node:http';
import { afterEach, describe, expect, test } from 'vitest';
Expand All @@ -9,13 +8,15 @@ import {
contentApiMockSchema,
MCP_CLIENT_NAME,
MCP_CLIENT_VERSION,
MCP_SERVER_VERSION,
} from './mocks.js';

type SetupOptions = {
accessToken?: string;
projectId?: string;
readOnly?: boolean;
contentApiUrl?: string;
apiUrl?: string;
env?: Record<string, string>;
};

Expand All @@ -24,6 +25,7 @@ async function setup(options: SetupOptions = {}) {
accessToken = ACCESS_TOKEN,
projectId,
readOnly,
apiUrl,
contentApiUrl,
env,
} = options;
Expand All @@ -38,7 +40,7 @@ async function setup(options: SetupOptions = {}) {
}
);

client.setNotificationHandler(LoggingMessageNotificationSchema, (message) => {
client.setNotificationHandler('notifications/message', (message) => {
const { level, data } = message.params;
if (level === 'error') {
console.error(data);
Expand All @@ -62,6 +64,10 @@ async function setup(options: SetupOptions = {}) {
args.push('--read-only');
}

if (apiUrl) {
args.push('--api-url', apiUrl);
}

if (contentApiUrl) {
args.push('--content-api-url', contentApiUrl);
}
Expand Down Expand Up @@ -108,19 +114,177 @@ async function createContentApiStub() {
};
}

async function createManagementApiStub() {
const hits: Array<{ method: string | undefined; url: URL }> = [];

const server: Server = createServer((req, res) => {
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
hits.push({ method: req.method, url });
res.setHeader('Content-Type', 'application/json');

if (
req.method === 'GET' &&
url.pathname === '/v1/projects' &&
url.search === ''
) {
res.end(
JSON.stringify([
{
id: 'abcdefghijklmnopqrst',
ref: 'abcdefghijklmnopqrst',
organization_id: 'tsrqponmlkjihgfedcba',
organization_slug: 'tsrqponmlkjihgfedcba',
name: 'Example project',
region: 'us-east-1',
created_at: '2024-01-02T03:04:05.000Z',
status: 'ACTIVE_HEALTHY',
database: {
host: 'db.abcdefghijklmnopqrst.supabase.co',
version: '15.1.0.147',
postgres_engine: '15',
release_channel: 'ga',
},
},
])
);
return;
}

res.statusCode = 404;
res.end(JSON.stringify({ error: 'not found' }));
});

await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));

const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('failed to bind management API stub');
}

return {
url: `http://127.0.0.1:${address.port}`,
hits,
close: () => new Promise<void>((resolve) => server.close(() => resolve())),
};
}

describe('stdio', () => {
const stubs: Array<{ close: () => Promise<void> }> = [];

afterEach(async () => {
await Promise.all(stubs.splice(0).map((stub) => stub.close()));
});

test('server connects and lists tools', async () => {
const { client } = await setup();
const managementApiStub = await createManagementApiStub();
const contentApiStub = await createContentApiStub();
stubs.push(managementApiStub, contentApiStub);

const { tools } = await client.listTools();
const { client } = await setup({
apiUrl: managementApiStub.url,
contentApiUrl: contentApiStub.url,
});

expect(tools.length).toBeGreaterThan(0);
try {
const { tools } = await client.listTools();
const toolResult = await client.callTool({
name: 'list_projects',
arguments: {},
});

expect(tools.map((tool) => tool.name).sort()).toEqual([
'apply_migration',
'confirm_cost',
'create_branch',
'create_project',
'delete_branch',
'deploy_edge_function',
'execute_sql',
'generate_typescript_types',
'get_advisors',
'get_cost',
'get_edge_function',
'get_organization',
'get_project',
'get_project_url',
'get_publishable_keys',
'list_branches',
'list_edge_functions',
'list_extensions',
'list_migrations',
'list_organizations',
'list_projects',
'list_tables',
'merge_branch',
'pause_project',
'query_logs',
'rebase_branch',
'reset_branch',
'restore_project',
'search_docs',
]);
expect(client.getServerVersion()).toEqual({
name: 'supabase',
title: 'Supabase',
version: MCP_SERVER_VERSION,
});
expect(client.getServerCapabilities()).toEqual({
tools: {},
});
expect(toolResult).toEqual({
content: [
{
type: 'text',
text: JSON.stringify({
projects: [
{
id: 'abcdefghijklmnopqrst',
ref: 'abcdefghijklmnopqrst',
organization_id: 'tsrqponmlkjihgfedcba',
organization_slug: 'tsrqponmlkjihgfedcba',
name: 'Example project',
region: 'us-east-1',
created_at: '2024-01-02T03:04:05.000Z',
status: 'ACTIVE_HEALTHY',
database: {
host: 'db.abcdefghijklmnopqrst.supabase.co',
version: '15.1.0.147',
postgres_engine: '15',
release_channel: 'ga',
},
},
],
}),
},
],
});
expect(
managementApiStub.hits.map(({ method, url }) => ({
method,
pathname: url.pathname,
search: url.search,
}))
).toEqual([
{
method: 'GET',
pathname: '/v1/projects',
search: '',
},
]);
expect(contentApiStub.hits.length).toBeGreaterThan(0);
} finally {
await client.close();
}
});

test('missing access token fails', async () => {
const setupPromise = setup({ accessToken: null as any });

await expect(setupPromise).rejects.toThrow('MCP error -32000');
// The server is unchanged here: it still exits before completing the handshake.
// Only the message this test's own client renders changed, from v1's
// 'MCP error -32000: Connection closed' to v2's 'Connection closed'. Held against a
// fixed v1 client, both the pre- and post-migration builds return the v1 string.
await expect(setupPromise).rejects.toThrow('Connection closed');
});
});

Expand Down
Loading
Loading