Skip to content

Commit 0e7889d

Browse files
authored
Merge pull request #58 from supabase-community/feat/edge-functions
feat: list and deploy edge functions
2 parents 46cb5c4 + c32fae7 commit 0e7889d

File tree

15 files changed

+1329
-20
lines changed

15 files changed

+1329
-20
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ The following Supabase tools are available to the LLM:
146146
- `execute_sql`: Executes raw SQL in the database. LLMs should use this for regular queries that don't change the schema.
147147
- `get_logs`: Gets logs for a Supabase project by service type (api, postgres, edge functions, auth, storage, realtime). LLMs can use this to help with debugging and monitoring service performance.
148148

149+
#### Edge Function Management
150+
151+
- `list_edge_functions`: Lists all Edge Functions in a Supabase project.
152+
- `deploy_edge_function`: Deploys a new Edge Function to a Supabase project. LLMs can use this to deploy new functions or update existing ones.
153+
149154
#### Project Configuration
150155

151156
- `get_project_url`: Gets the API URL for a project.

package-lock.json

Lines changed: 64 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/mcp-server-supabase/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@
2929
}
3030
},
3131
"dependencies": {
32+
"@deno/eszip": "^0.84.0",
3233
"@modelcontextprotocol/sdk": "^1.4.1",
3334
"@supabase/mcp-utils": "0.1.3",
3435
"common-tags": "^1.8.2",
35-
"openapi-fetch": "^0.13.4",
36+
"openapi-fetch": "^0.13.5",
3637
"zod": "^3.24.1"
3738
},
3839
"devDependencies": {
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { codeBlock } from 'common-tags';
2+
import { fileURLToPath } from 'url';
3+
import { extractFiles } from './eszip.js';
4+
import {
5+
assertSuccess,
6+
type ManagementApiClient,
7+
} from './management-api/index.js';
8+
9+
/**
10+
* Gets the deployment ID for an Edge Function.
11+
*/
12+
export function getDeploymentId(
13+
projectId: string,
14+
functionId: string,
15+
functionVersion: number
16+
): string {
17+
return `${projectId}_${functionId}_${functionVersion}`;
18+
}
19+
20+
/**
21+
* Gets the path prefix applied to each file in an Edge Function.
22+
*/
23+
export function getPathPrefix(deploymentId: string) {
24+
return `/tmp/user_fn_${deploymentId}/`;
25+
}
26+
27+
export const edgeFunctionExample = codeBlock`
28+
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
29+
30+
Deno.serve(async (req: Request) => {
31+
const data = {
32+
message: "Hello there!"
33+
};
34+
35+
return new Response(JSON.stringify(data), {
36+
headers: {
37+
'Content-Type': 'application/json',
38+
'Connection': 'keep-alive'
39+
}
40+
});
41+
});
42+
`;
43+
44+
/**
45+
* Fetches a full Edge Function from the Supabase Management API.
46+
47+
* - Includes both function metadata and the contents of each file.
48+
* - Normalizes file paths to be relative to the project root.
49+
*/
50+
export async function getFullEdgeFunction(
51+
managementApiClient: ManagementApiClient,
52+
projectId: string,
53+
functionSlug: string
54+
) {
55+
const functionResponse = await managementApiClient.GET(
56+
'/v1/projects/{ref}/functions/{function_slug}',
57+
{
58+
params: {
59+
path: {
60+
ref: projectId,
61+
function_slug: functionSlug,
62+
},
63+
},
64+
}
65+
);
66+
67+
if (functionResponse.error) {
68+
return {
69+
data: undefined,
70+
error: functionResponse.error as { message: string },
71+
};
72+
}
73+
74+
assertSuccess(functionResponse, 'Failed to fetch Edge Function');
75+
76+
const edgeFunction = functionResponse.data;
77+
78+
const deploymentId = getDeploymentId(
79+
projectId,
80+
edgeFunction.id,
81+
edgeFunction.version
82+
);
83+
84+
const pathPrefix = getPathPrefix(deploymentId);
85+
86+
const entrypoint_path = edgeFunction.entrypoint_path
87+
? fileURLToPath(edgeFunction.entrypoint_path).replace(pathPrefix, '')
88+
: undefined;
89+
90+
const import_map_path = edgeFunction.import_map_path
91+
? fileURLToPath(edgeFunction.import_map_path).replace(pathPrefix, '')
92+
: undefined;
93+
94+
const eszipResponse = await managementApiClient.GET(
95+
'/v1/projects/{ref}/functions/{function_slug}/body',
96+
{
97+
params: {
98+
path: {
99+
ref: projectId,
100+
function_slug: functionSlug,
101+
},
102+
},
103+
parseAs: 'arrayBuffer',
104+
}
105+
);
106+
107+
assertSuccess(eszipResponse, 'Failed to fetch Edge Function eszip bundle');
108+
109+
const extractedFiles = await extractFiles(
110+
new Uint8Array(eszipResponse.data),
111+
pathPrefix
112+
);
113+
114+
const files = await Promise.all(
115+
extractedFiles.map(async (file) => ({
116+
name: file.name,
117+
content: await file.text(),
118+
}))
119+
);
120+
121+
const normalizedFunction = {
122+
...edgeFunction,
123+
entrypoint_path,
124+
import_map_path,
125+
files,
126+
};
127+
128+
return { data: normalizedFunction, error: undefined };
129+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { codeBlock } from 'common-tags';
2+
import { open, type FileHandle } from 'node:fs/promises';
3+
import { describe, expect, test } from 'vitest';
4+
import { bundleFiles, extractFiles } from './eszip.js';
5+
6+
describe('eszip', () => {
7+
test('extract files', async () => {
8+
const helloContent = codeBlock`
9+
export function hello(): string {
10+
return 'Hello, world!';
11+
}
12+
`;
13+
const helloFile = new File([helloContent], 'hello.ts', {
14+
type: 'application/typescript',
15+
});
16+
17+
const indexContent = codeBlock`
18+
import { hello } from './hello.ts';
19+
20+
Deno.serve(async (req: Request) => {
21+
return new Response(hello(), { headers: { 'Content-Type': 'text/plain' } })
22+
});
23+
`;
24+
const indexFile = new File([indexContent], 'index.ts', {
25+
type: 'application/typescript',
26+
});
27+
28+
const eszip = await bundleFiles([indexFile, helloFile]);
29+
const extractedFiles = await extractFiles(eszip);
30+
31+
expect(extractedFiles).toHaveLength(2);
32+
33+
const extractedIndexFile = extractedFiles.find(
34+
(file) => file.name === 'index.ts'
35+
);
36+
const extractedHelloFile = extractedFiles.find(
37+
(file) => file.name === 'hello.ts'
38+
);
39+
40+
expect(extractedIndexFile).toBeDefined();
41+
expect(extractedIndexFile!.type).toBe('application/typescript');
42+
await expect(extractedIndexFile!.text()).resolves.toBe(indexContent);
43+
44+
expect(extractedHelloFile).toBeDefined();
45+
expect(extractedHelloFile!.type).toBe('application/typescript');
46+
await expect(extractedHelloFile!.text()).resolves.toBe(helloContent);
47+
});
48+
});
49+
50+
export class Source implements UnderlyingSource<Uint8Array> {
51+
type = 'bytes' as const;
52+
autoAllocateChunkSize = 1024;
53+
54+
path: string | URL;
55+
controller?: ReadableByteStreamController;
56+
file?: FileHandle;
57+
58+
constructor(path: string | URL) {
59+
this.path = path;
60+
}
61+
62+
async start(controller: ReadableStreamController<Uint8Array>) {
63+
if (!('byobRequest' in controller)) {
64+
throw new Error('ReadableStreamController does not support byobRequest');
65+
}
66+
67+
this.file = await open(this.path);
68+
this.controller = controller;
69+
}
70+
71+
async pull() {
72+
if (!this.controller || !this.file) {
73+
throw new Error('ReadableStream has not been started');
74+
}
75+
76+
if (!this.controller.byobRequest) {
77+
throw new Error('ReadableStreamController does not support byobRequest');
78+
}
79+
80+
const view = this.controller.byobRequest.view as NodeJS.ArrayBufferView;
81+
82+
if (!view) {
83+
throw new Error('ReadableStreamController does not have a view');
84+
}
85+
86+
const { bytesRead } = await this.file.read({
87+
buffer: view,
88+
offset: view.byteOffset,
89+
length: view.byteLength,
90+
});
91+
92+
if (bytesRead === 0) {
93+
await this.file.close();
94+
this.controller.close();
95+
}
96+
97+
this.controller.byobRequest.respond(view.byteLength);
98+
}
99+
}

0 commit comments

Comments
 (0)