Skip to content
Draft
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
9,768 changes: 5,149 additions & 4,619 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"workspaces": ["packages/*"],
"workspaces": [
"packages/*"
],
"scripts": {
"build": "npm run build --workspace @supabase/mcp-utils --workspace @supabase/mcp-server-supabase",
"test": "npm run test --workspace @supabase/mcp-utils --workspace @supabase/mcp-server-supabase",
Expand All @@ -10,5 +12,8 @@
"devDependencies": {
"@biomejs/biome": "1.9.4",
"supabase": "^2.1.1"
},
"dependencies": {
"axios": "^1.11.0"
}
}
6 changes: 3 additions & 3 deletions packages/mcp-server-supabase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@
"test:coverage": "vitest --coverage",
"generate:management-api-types": "openapi-typescript https://api.supabase.com/api/v1-json -o ./src/management-api/types.ts"
},
"files": [
"dist/**/*"
],
"files": ["dist/**/*"],
"bin": {
"mcp-server-supabase": "./dist/transports/stdio.js"
},
Expand All @@ -36,11 +34,13 @@
}
},
"dependencies": {
"@hono/node-server": "^1.14.1",
"@mjackson/multipart-parser": "^0.10.1",
"@modelcontextprotocol/sdk": "^1.11.0",
"@supabase/mcp-utils": "0.2.1",
"common-tags": "^1.8.2",
"graphql": "^16.11.0",
"hono": "^4.7.8",
"openapi-fetch": "^0.13.5",
"zod": "^3.24.1"
},
Expand Down
122 changes: 122 additions & 0 deletions packages/mcp-server-supabase/src/transports/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Hono } from 'hono';
import axios from 'axios';
import { createSupabaseMcpServer } from '../server.js';
import { StatelessHttpServerTransport } from '@supabase/mcp-utils';
import { serve } from '@hono/node-server';
import { createSupabaseApiPlatform } from '../platform/api-platform.js';
import { cors } from 'hono/cors';

const managementApiUrl =
process.env.SUPABASE_API_URL ?? 'https://api.supabase.com';

const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;

const app = new Hono();

//
app.use(
cors({
origin: ['dev', 'test'].includes((process.env.ENV ?? '').toLowerCase())
? '*'
: 'https://api.supabase.io/mcp',
})
);

/**
* Stateless HTTP transport for the Supabase MCP server.
*/
app.post('/mcp', async (c) => {
const projectId = c.req.query('project-ref');
const readOnly = c.req.query('read-only') === 'true';
const apiUrl = c.req.query('api-url');

const accessToken = c.req.header('Authorization')?.replace('Bearer ', '');
if (!accessToken) {
console.error(
'Please provide a personal access token (PAT) in the Authorization header'
);
return c.json({ error: 'Access token is required' }, 401);
}

const platform = createSupabaseApiPlatform({
accessToken,
apiUrl,
});

const server = createSupabaseMcpServer({
platform,
projectId,
readOnly,
});

const transport = new StatelessHttpServerTransport();
await server.connect(transport);
return await transport.handleRequest(c.req.raw);
});

// SSE notifications not supported in stateless mode
app.get('/mcp', async (c) => {
console.log('Received GET MCP request');
c.json(
{
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Method not allowed.',
},
id: null,
},
405
);
});

// Session termination not needed in stateless mode
app.delete('/mcp', async (c) => {
console.log('Received DELETE MCP request');
c.json(
{
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Method not allowed.',
},
id: null,
},
405
);
});

const fetchOauthMetadata = async () => {
const response = await axios.get(
`${managementApiUrl}/.well-known/oauth-authorization-server`
);
if (response.status != 200) {
throw new Error('Failed to fetch OAuth metadata');
}

return response.data;
};

const oauthMetadata = await fetchOauthMetadata();

const protectedResourceMetadata = {
resource: `http://localhost:${port}`, // "https://api.supabase.io/mcp",
authorization_servers: [oauthMetadata.issuer],
scopes_supported: oauthMetadata.scopes_supported,
};
app.get('/.well-known/oauth-protected-resource', (c) =>
c.json(protectedResourceMetadata)
);
app.get('/.well-known/oauth-authorization-server', (c) =>
c.json(oauthMetadata)
);

serve(
{
fetch: app.fetch,
port: port,
},
() => {
console.log('Server is running on port', port);
}
);
74 changes: 37 additions & 37 deletions packages/mcp-server-supabase/test/e2e/functions.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
/// <reference types="../extensions.d.ts" />

import { generateText, type ToolCallUnion, type ToolSet } from "ai";
import { codeBlock } from "common-tags";
import { describe, expect, test } from "vitest";
import { createOrganization, createProject } from "../mocks.js";
import { join } from "node:path/posix";
import { getTestModel, setup } from "./utils.js";

describe("edge function e2e tests", () => {
test("deploys an edge function", async () => {
import { generateText, type ToolCallUnion, type ToolSet } from 'ai';
import { codeBlock } from 'common-tags';
import { describe, expect, test } from 'vitest';
import { createOrganization, createProject } from '../mocks.js';
import { join } from 'node:path/posix';
import { getTestModel, setup } from './utils.js';

describe('edge function e2e tests', () => {
test('deploys an edge function', async () => {
const { client } = await setup();
const model = getTestModel();

const org = await createOrganization({
name: "My Org",
plan: "free",
allowed_release_channels: ["ga"],
name: 'My Org',
plan: 'free',
allowed_release_channels: ['ga'],
});

const project = await createProject({
name: "todos-app",
region: "us-east-1",
name: 'todos-app',
region: 'us-east-1',
organization_id: org.id,
});

Expand All @@ -32,12 +32,12 @@ describe("edge function e2e tests", () => {
tools,
messages: [
{
role: "system",
role: 'system',
content:
"You are a coding assistant. The current working directory is /home/user/projects/todos-app.",
'You are a coding assistant. The current working directory is /home/user/projects/todos-app.',
},
{
role: "user",
role: 'user',
content: `Deploy an edge function to project with ref ${project.id} that returns the current time in UTC.`,
},
],
Expand All @@ -48,27 +48,27 @@ describe("edge function e2e tests", () => {
});

expect(toolCalls).toContainEqual(
expect.objectContaining({ toolName: "deploy_edge_function" }),
expect.objectContaining({ toolName: 'deploy_edge_function' })
);

await expect(text).toMatchCriteria(
"Confirms the successful deployment of an edge function that will return the current time in UTC. It describes steps to test the function.",
'Confirms the successful deployment of an edge function that will return the current time in UTC. It describes steps to test the function.'
);
});

test("modifies an edge function", async () => {
test('modifies an edge function', async () => {
const { client } = await setup();
const model = getTestModel();

const org = await createOrganization({
name: "My Org",
plan: "free",
allowed_release_channels: ["ga"],
name: 'My Org',
plan: 'free',
allowed_release_channels: ['ga'],
});

const project = await createProject({
name: "todos-app",
region: "us-east-1",
name: 'todos-app',
region: 'us-east-1',
organization_id: org.id,
});

Expand All @@ -80,14 +80,14 @@ describe("edge function e2e tests", () => {

const edgeFunction = await project.deployEdgeFunction(
{
name: "hello-world",
entrypoint_path: "index.ts",
name: 'hello-world',
entrypoint_path: 'index.ts',
},
[
new File([code], "index.ts", {
type: "application/typescript",
new File([code], 'index.ts', {
type: 'application/typescript',
}),
],
]
);

const toolCalls: ToolCallUnion<ToolSet>[] = [];
Expand All @@ -98,12 +98,12 @@ describe("edge function e2e tests", () => {
tools,
messages: [
{
role: "system",
role: 'system',
content:
"You are a coding assistant. The current working directory is /home/user/projects/todos-app.",
'You are a coding assistant. The current working directory is /home/user/projects/todos-app.',
},
{
role: "user",
role: 'user',
content: `Change my edge function (project id ${project.id}) to replace "world" with "Earth".`,
},
],
Expand All @@ -115,19 +115,19 @@ describe("edge function e2e tests", () => {

expect(toolCalls).toHaveLength(2);
expect(toolCalls[0]).toEqual(
expect.objectContaining({ toolName: "list_edge_functions" }),
expect.objectContaining({ toolName: 'list_edge_functions' })
);
expect(toolCalls[1]).toEqual(
expect.objectContaining({ toolName: "deploy_edge_function" }),
expect.objectContaining({ toolName: 'deploy_edge_function' })
);

await expect(text).toMatchCriteria(
"Confirms the successful modification of an Edge Function.",
'Confirms the successful modification of an Edge Function.'
);

expect(edgeFunction.files).toHaveLength(1);
expect(edgeFunction.files[0].name).toBe(
join(edgeFunction.pathPrefix, "index.ts"),
join(edgeFunction.pathPrefix, 'index.ts')
);
await expect(edgeFunction.files[0].text()).resolves.toEqual(codeBlock`
Deno.serve(async (req: Request) => {
Expand Down
6 changes: 5 additions & 1 deletion packages/mcp-server-supabase/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { defineConfig } from 'tsup';

export default defineConfig([
{
entry: ['src/index.ts', 'src/transports/stdio.ts', 'src/platform/index.ts'],
entry: [
'src/index.ts',
'src/transports/stdio.ts',
'src/transports/http.ts',
],
format: ['cjs', 'esm'],
outDir: 'dist',
sourcemap: true,
Expand Down
1 change: 1 addition & 0 deletions packages/mcp-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './server.js';
export * from './stateless-http-transport.js';
export * from './stream-transport.js';
export * from './types.js';
Loading