Skip to content

Commit d397277

Browse files
committed
fix: address local HTTP review findings
1 parent 9ff5239 commit d397277

4 files changed

Lines changed: 76 additions & 8 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pnpm dev
2929
Then start the server with `--http`, which serves the build the same way `mcp.supabase.com` does:
3030

3131
```bash
32-
node packages/mcp-server-supabase/dist/cli.js --http --project-ref <your project ref>
32+
node dist/cli.js --http --project-ref <your project ref>
3333
```
3434

3535
It binds `127.0.0.1` only and prints a ready-to-paste `.mcp.json` snippet:

packages/mcp-server-supabase/src/transports/local-http-entry.test.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,14 +159,30 @@ describe('startLocalHttpEntry', () => {
159159
test('rejects a foreign Host header and accepts loopback hosts', async () => {
160160
const { port } = new URL(entry.url);
161161
// node:http, because fetch forbids overriding the Host header.
162+
const body = JSON.stringify({
163+
jsonrpc: '2.0',
164+
id: 1,
165+
method: 'initialize',
166+
params: {
167+
protocolVersion: '2025-06-18',
168+
capabilities: {},
169+
clientInfo: { name: 'host-probe', version: '1.0.0' },
170+
},
171+
});
162172
const probe = (host: string) =>
163173
new Promise<{ status: number; body: string }>((resolve, reject) => {
164174
const req = httpRequest(
165175
{
166176
host: '127.0.0.1',
167177
port,
168178
path: '/mcp',
169-
headers: { ...AUTH_HEADERS, host },
179+
method: 'POST',
180+
headers: {
181+
...AUTH_HEADERS,
182+
accept: 'application/json, text/event-stream',
183+
'content-type': 'application/json',
184+
host,
185+
},
170186
},
171187
(res) => {
172188
let body = '';
@@ -176,7 +192,7 @@ describe('startLocalHttpEntry', () => {
176192
}
177193
);
178194
req.on('error', reject);
179-
req.end();
195+
req.end(body);
180196
});
181197

182198
const rejected = await probe('evil.example');
@@ -191,10 +207,16 @@ describe('startLocalHttpEntry', () => {
191207

192208
for (const host of [`127.0.0.1:${port}`, `localhost:${port}`]) {
193209
const response = await probe(host);
194-
// Past host validation, path, and auth: the SDK answers a bare GET.
195-
expect(response.status).not.toBe(403);
196-
expect(response.status).not.toBe(401);
197-
expect(response.status).not.toBe(404);
210+
expect(response.status).toBe(200);
211+
const dataLine = response.body
212+
.split('\n')
213+
.find((line) => line.startsWith('data: '));
214+
if (!dataLine) throw new Error('expected an SSE data line');
215+
expect(JSON.parse(dataLine.slice('data: '.length))).toMatchObject({
216+
jsonrpc: '2.0',
217+
id: 1,
218+
result: { protocolVersion: expect.any(String) },
219+
});
198220
}
199221
});
200222

packages/mcp-server-supabase/src/transports/node-bridge.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,34 @@ describe('toNodeListener', () => {
8787
]);
8888
});
8989

90+
test('rejects request bodies larger than 4 MiB before calling the handler', async () => {
91+
let handled = false;
92+
const { origin } = await listen(async () => {
93+
handled = true;
94+
return new Response();
95+
});
96+
97+
const response = await new Promise<{ status: number; body: string }>(
98+
(resolve, reject) => {
99+
const req = httpRequest(origin, { method: 'POST' }, (res) => {
100+
let body = '';
101+
res.setEncoding('utf8');
102+
res.on('data', (chunk) => (body += chunk));
103+
res.on('end', () => resolve({ status: res.statusCode!, body }));
104+
});
105+
req.on('error', reject);
106+
req.write(Buffer.alloc(4 * 1024 * 1024 + 1));
107+
req.end(Buffer.alloc(1024 * 1024));
108+
}
109+
);
110+
111+
expect(response.status).toBe(413);
112+
expect(JSON.parse(response.body)).toEqual({
113+
error: 'payload too large',
114+
});
115+
expect(handled).toBe(false);
116+
});
117+
90118
test('aborts the signal when the client drops mid-request and keeps serving', async () => {
91119
const handling = deferred();
92120
const release = deferred();

packages/mcp-server-supabase/src/transports/node-bridge.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { RequestListener } from 'node:http';
22

3+
const MAX_REQUEST_BODY_BYTES = 4 * 1024 * 1024;
4+
35
// Bridge from the SDK's fetch-shaped handler to node:http. Bodies are buffered
46
// on both sides on purpose: under `legacy: 'stateless'` every response is
57
// request-scoped, so a workstation entry gains nothing from streaming, and
@@ -24,7 +26,23 @@ export function toNodeListener(handle: FetchHandler): RequestListener {
2426
return async (req, res) => {
2527
try {
2628
const chunks: Buffer[] = [];
27-
for await (const chunk of req) chunks.push(chunk as Buffer);
29+
let bodyBytes = 0;
30+
let bodyTooLarge = false;
31+
for await (const chunk of req.iterator({ destroyOnReturn: false })) {
32+
const buffer = chunk as Buffer;
33+
bodyBytes += buffer.length;
34+
if (bodyBytes > MAX_REQUEST_BODY_BYTES) {
35+
bodyTooLarge = true;
36+
break;
37+
}
38+
chunks.push(buffer);
39+
}
40+
if (bodyTooLarge) {
41+
req.resume();
42+
res.writeHead(413, { 'content-type': 'application/json' });
43+
res.end(JSON.stringify({ error: 'payload too large' }));
44+
return;
45+
}
2846
const body = chunks.length > 0 ? Buffer.concat(chunks) : undefined;
2947

3048
const headers = new Headers();

0 commit comments

Comments
 (0)