Skip to content

Commit 8ce8664

Browse files
authored
feat: add get_edge_function, condense list_edge_functions (#140)
* feat: add `get_edge_function` tool, omit body from `list_edge_functions` * test: `get_edge_function` and `list_edge_functions` * chore: biome formatting * docs: `get_edge_function` readme description * test: remove `.only`
1 parent a173777 commit 8ce8664

File tree

6 files changed

+163
-57
lines changed

6 files changed

+163
-57
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ Enabled by default. Use `development` to target this group of tools with the [`-
222222
Enabled by default. Use `functions` to target this group of tools with the [`--features`](#feature-groups) option.
223223

224224
- `list_edge_functions`: Lists all Edge Functions in a Supabase project.
225+
- `get_edge_function`: Retrieves file contents for an Edge Function in a Supabase project.
225226
- `deploy_edge_function`: Deploys a new Edge Function to a Supabase project. LLMs can use this to deploy new functions or update existing ones.
226227

227228
#### Branching (Experimental, requires a paid plan)

packages/mcp-server-supabase/src/platform/api-platform.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
type DevelopmentOperations,
3232
type EdgeFunction,
3333
type EdgeFunctionsOperations,
34+
type EdgeFunctionWithBody,
3435
type ExecuteSqlOptions,
3536
type GetLogsOptions,
3637
type ResetBranchOptions,
@@ -351,15 +352,35 @@ export function createSupabaseApiPlatform(
351352

352353
assertSuccess(response, 'Failed to fetch Edge Functions');
353354

354-
// Fetch files for each Edge Function
355-
return await Promise.all(
356-
response.data.map(async (listedFunction) => {
357-
return await functions.getEdgeFunction(
358-
projectId,
359-
listedFunction.slug
360-
);
361-
})
362-
);
355+
return response.data.map((edgeFunction) => {
356+
const deploymentId = getDeploymentId(
357+
projectId,
358+
edgeFunction.id,
359+
edgeFunction.version
360+
);
361+
362+
const pathPrefix = getPathPrefix(deploymentId);
363+
364+
const entrypoint_path = edgeFunction.entrypoint_path
365+
? relative(
366+
pathPrefix,
367+
fileURLToPath(edgeFunction.entrypoint_path, { windows: false })
368+
)
369+
: undefined;
370+
371+
const import_map_path = edgeFunction.import_map_path
372+
? relative(
373+
pathPrefix,
374+
fileURLToPath(edgeFunction.import_map_path, { windows: false })
375+
)
376+
: undefined;
377+
378+
return {
379+
...edgeFunction,
380+
entrypoint_path,
381+
import_map_path,
382+
};
383+
});
363384
},
364385
async getEdgeFunction(projectId: string, functionSlug: string) {
365386
const functionResponse = await managementApiClient.GET(
@@ -440,7 +461,7 @@ export function createSupabaseApiPlatform(
440461
throw new Error('No data received from Edge Function body');
441462
}
442463

443-
const files: EdgeFunction['files'] = [];
464+
const files: EdgeFunctionWithBody['files'] = [];
444465
const parts = parseMultipartStream(bodyResponse.data, { boundary });
445466

446467
for await (const part of parts) {

packages/mcp-server-supabase/src/platform/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ export const edgeFunctionSchema = z.object({
7070
import_map: z.boolean().optional(),
7171
import_map_path: z.string().optional(),
7272
entrypoint_path: z.string().optional(),
73+
});
74+
75+
export const edgeFunctionWithBodySchema = edgeFunctionSchema.extend({
7376
files: z.array(
7477
z.object({
7578
name: z.string(),
@@ -134,6 +137,7 @@ export type Organization = z.infer<typeof organizationSchema>;
134137
export type Project = z.infer<typeof projectSchema>;
135138
export type Branch = z.infer<typeof branchSchema>;
136139
export type EdgeFunction = z.infer<typeof edgeFunctionSchema>;
140+
export type EdgeFunctionWithBody = z.infer<typeof edgeFunctionWithBodySchema>;
137141

138142
export type CreateProjectOptions = z.infer<typeof createProjectOptionsSchema>;
139143
export type CreateBranchOptions = z.infer<typeof createBranchOptionsSchema>;
@@ -179,7 +183,7 @@ export type EdgeFunctionsOperations = {
179183
getEdgeFunction(
180184
projectId: string,
181185
functionSlug: string
182-
): Promise<EdgeFunction>;
186+
): Promise<EdgeFunctionWithBody>;
183187
deployEdgeFunction(
184188
projectId: string,
185189
options: DeployEdgeFunctionOptions

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

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,16 +1221,77 @@ describe('tools', () => {
12211221
updated_at: expect.stringMatching(
12221222
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/
12231223
),
1224-
files: [
1225-
{
1226-
name: 'index.ts',
1227-
content: indexContent,
1228-
},
1229-
],
12301224
},
12311225
]);
12321226
});
12331227

1228+
test('get edge function', async () => {
1229+
const { callTool } = await setup();
1230+
1231+
const org = await createOrganization({
1232+
name: 'My Org',
1233+
plan: 'free',
1234+
allowed_release_channels: ['ga'],
1235+
});
1236+
1237+
const project = await createProject({
1238+
name: 'Project 1',
1239+
region: 'us-east-1',
1240+
organization_id: org.id,
1241+
});
1242+
project.status = 'ACTIVE_HEALTHY';
1243+
1244+
const indexContent = codeBlock`
1245+
Deno.serve(async (req: Request) => {
1246+
return new Response('Hello world!', { headers: { 'Content-Type': 'text/plain' } })
1247+
});
1248+
`;
1249+
1250+
const edgeFunction = await project.deployEdgeFunction(
1251+
{
1252+
name: 'hello-world',
1253+
entrypoint_path: 'index.ts',
1254+
},
1255+
[
1256+
new File([indexContent], 'index.ts', {
1257+
type: 'application/typescript',
1258+
}),
1259+
]
1260+
);
1261+
1262+
const result = await callTool({
1263+
name: 'get_edge_function',
1264+
arguments: {
1265+
project_id: project.id,
1266+
function_slug: edgeFunction.slug,
1267+
},
1268+
});
1269+
1270+
expect(result).toEqual({
1271+
id: edgeFunction.id,
1272+
slug: edgeFunction.slug,
1273+
version: edgeFunction.version,
1274+
name: edgeFunction.name,
1275+
status: edgeFunction.status,
1276+
entrypoint_path: 'index.ts',
1277+
import_map_path: undefined,
1278+
import_map: false,
1279+
verify_jwt: true,
1280+
created_at: expect.stringMatching(
1281+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/
1282+
),
1283+
updated_at: expect.stringMatching(
1284+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/
1285+
),
1286+
files: [
1287+
{
1288+
name: 'index.ts',
1289+
content: indexContent,
1290+
},
1291+
],
1292+
});
1293+
});
1294+
12341295
test('deploy new edge function', async () => {
12351296
const { callTool } = await setup();
12361297

@@ -2145,7 +2206,11 @@ describe('feature groups', () => {
21452206
const { tools } = await client.listTools();
21462207
const toolNames = tools.map((tool) => tool.name);
21472208

2148-
expect(toolNames).toEqual(['list_edge_functions', 'deploy_edge_function']);
2209+
expect(toolNames).toEqual([
2210+
'list_edge_functions',
2211+
'get_edge_function',
2212+
'deploy_edge_function',
2213+
]);
21492214
});
21502215

21512216
test('branching tools', async () => {

packages/mcp-server-supabase/src/tools/edge-function-tools.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ export function getEdgeFunctionTools({
2525
return await functions.listEdgeFunctions(project_id);
2626
},
2727
}),
28+
get_edge_function: injectableTool({
29+
description:
30+
'Retrieves file contents for an Edge Function in a Supabase project.',
31+
parameters: z.object({
32+
project_id: z.string(),
33+
function_slug: z.string(),
34+
}),
35+
inject: { project_id },
36+
execute: async ({ project_id, function_slug }) => {
37+
return await functions.getEdgeFunction(project_id, function_slug);
38+
},
39+
}),
2840
deploy_edge_function: injectableTool({
2941
description: `Deploys an Edge Function to a Supabase project. If the function already exists, this will create a new version. Example:\n\n${edgeFunctionExample}`,
3042
parameters: z.object({

packages/mcp-server-supabase/test/e2e/functions.e2e.ts

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
/// <reference types="../extensions.d.ts" />
22

3-
import { generateText, type ToolCallUnion, type ToolSet } from "ai";
4-
import { codeBlock } from "common-tags";
5-
import { describe, expect, test } from "vitest";
6-
import { createOrganization, createProject } from "../mocks.js";
7-
import { join } from "node:path/posix";
8-
import { getTestModel, setup } from "./utils.js";
9-
10-
describe("edge function e2e tests", () => {
11-
test("deploys an edge function", async () => {
3+
import { generateText, type ToolCallUnion, type ToolSet } from 'ai';
4+
import { codeBlock } from 'common-tags';
5+
import { describe, expect, test } from 'vitest';
6+
import { createOrganization, createProject } from '../mocks.js';
7+
import { join } from 'node:path/posix';
8+
import { getTestModel, setup } from './utils.js';
9+
10+
describe('edge function e2e tests', () => {
11+
test('deploys an edge function', async () => {
1212
const { client } = await setup();
1313
const model = getTestModel();
1414

1515
const org = await createOrganization({
16-
name: "My Org",
17-
plan: "free",
18-
allowed_release_channels: ["ga"],
16+
name: 'My Org',
17+
plan: 'free',
18+
allowed_release_channels: ['ga'],
1919
});
2020

2121
const project = await createProject({
22-
name: "todos-app",
23-
region: "us-east-1",
22+
name: 'todos-app',
23+
region: 'us-east-1',
2424
organization_id: org.id,
2525
});
2626

@@ -32,12 +32,12 @@ describe("edge function e2e tests", () => {
3232
tools,
3333
messages: [
3434
{
35-
role: "system",
35+
role: 'system',
3636
content:
37-
"You are a coding assistant. The current working directory is /home/user/projects/todos-app.",
37+
'You are a coding assistant. The current working directory is /home/user/projects/todos-app.',
3838
},
3939
{
40-
role: "user",
40+
role: 'user',
4141
content: `Deploy an edge function to project with ref ${project.id} that returns the current time in UTC.`,
4242
},
4343
],
@@ -48,27 +48,27 @@ describe("edge function e2e tests", () => {
4848
});
4949

5050
expect(toolCalls).toContainEqual(
51-
expect.objectContaining({ toolName: "deploy_edge_function" }),
51+
expect.objectContaining({ toolName: 'deploy_edge_function' })
5252
);
5353

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

59-
test("modifies an edge function", async () => {
59+
test('modifies an edge function', async () => {
6060
const { client } = await setup();
6161
const model = getTestModel();
6262

6363
const org = await createOrganization({
64-
name: "My Org",
65-
plan: "free",
66-
allowed_release_channels: ["ga"],
64+
name: 'My Org',
65+
plan: 'free',
66+
allowed_release_channels: ['ga'],
6767
});
6868

6969
const project = await createProject({
70-
name: "todos-app",
71-
region: "us-east-1",
70+
name: 'todos-app',
71+
region: 'us-east-1',
7272
organization_id: org.id,
7373
});
7474

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

8181
const edgeFunction = await project.deployEdgeFunction(
8282
{
83-
name: "hello-world",
84-
entrypoint_path: "index.ts",
83+
name: 'hello-world',
84+
entrypoint_path: 'index.ts',
8585
},
8686
[
87-
new File([code], "index.ts", {
88-
type: "application/typescript",
87+
new File([code], 'index.ts', {
88+
type: 'application/typescript',
8989
}),
90-
],
90+
]
9191
);
9292

9393
const toolCalls: ToolCallUnion<ToolSet>[] = [];
@@ -98,36 +98,39 @@ describe("edge function e2e tests", () => {
9898
tools,
9999
messages: [
100100
{
101-
role: "system",
101+
role: 'system',
102102
content:
103-
"You are a coding assistant. The current working directory is /home/user/projects/todos-app.",
103+
'You are a coding assistant. The current working directory is /home/user/projects/todos-app.',
104104
},
105105
{
106-
role: "user",
106+
role: 'user',
107107
content: `Change my edge function (project id ${project.id}) to replace "world" with "Earth".`,
108108
},
109109
],
110-
maxSteps: 3,
110+
maxSteps: 4,
111111
async onStepFinish({ toolCalls: tools }) {
112112
toolCalls.push(...tools);
113113
},
114114
});
115115

116-
expect(toolCalls).toHaveLength(2);
116+
expect(toolCalls).toHaveLength(3);
117117
expect(toolCalls[0]).toEqual(
118-
expect.objectContaining({ toolName: "list_edge_functions" }),
118+
expect.objectContaining({ toolName: 'list_edge_functions' })
119119
);
120120
expect(toolCalls[1]).toEqual(
121-
expect.objectContaining({ toolName: "deploy_edge_function" }),
121+
expect.objectContaining({ toolName: 'get_edge_function' })
122+
);
123+
expect(toolCalls[2]).toEqual(
124+
expect.objectContaining({ toolName: 'deploy_edge_function' })
122125
);
123126

124127
await expect(text).toMatchCriteria(
125-
"Confirms the successful modification of an Edge Function.",
128+
'Confirms the successful modification of an Edge Function.'
126129
);
127130

128131
expect(edgeFunction.files).toHaveLength(1);
129132
expect(edgeFunction.files[0].name).toBe(
130-
join(edgeFunction.pathPrefix, "index.ts"),
133+
join(edgeFunction.pathPrefix, 'index.ts')
131134
);
132135
await expect(edgeFunction.files[0].text()).resolves.toEqual(codeBlock`
133136
Deno.serve(async (req: Request) => {

0 commit comments

Comments
 (0)