mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
The server routes a mounted path when a dot follows it, so /mcp.json reached the MCP endpoint while panel_request refused only /mcp and /mcp/...; the same held for the provider-settings, credential and model routes. The refusals now match a dot after the route as well. A regression test drives panel_request against a real Vite server. Co-Authored-By: Claude Opus 5.5 <[email protected]>
333 lines
10 KiB
JavaScript
333 lines
10 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { createServer, request as httpRequest } from 'node:http';
|
|
import test from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { isLocalMcpRequest, localMcpPlugin } from '../../server/mcp/plugin.js';
|
|
|
|
test('only direct loopback connections to this server are local', () => {
|
|
const local = {
|
|
remoteAddress: '127.0.0.1',
|
|
localPort: 4173,
|
|
host: 'localhost:4173',
|
|
};
|
|
assert.equal(isLocalMcpRequest(local), true);
|
|
assert.equal(
|
|
isLocalMcpRequest({ ...local, remoteAddress: '::1', host: '[::1]:4173' }),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
isLocalMcpRequest({ ...local, origin: 'http://localhost:4173' }),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
isLocalMcpRequest({ ...local, env: { PINOKIO_SHARE_LOCAL: 'false' } }),
|
|
true,
|
|
);
|
|
for (const request of [
|
|
{ ...local, remoteAddress: '192.168.1.20' },
|
|
{ ...local, host: 'attacker.example:4173' },
|
|
{ ...local, host: 'localhost.attacker.example' },
|
|
{ ...local, host: '' },
|
|
// Another local port, as the Host or as the page's origin.
|
|
{ ...local, host: 'localhost:9000' },
|
|
{ ...local, localPort: 80 },
|
|
{ ...local, origin: 'http://127.0.0.1:5173' },
|
|
{ ...local, origin: 'http://localhost:5173' },
|
|
{ ...local, origin: 'https://attacker.example' },
|
|
{ ...local, origin: 'null' },
|
|
{ ...local, origin: 'file:///tmp/page.html' },
|
|
// Proxied or shared: not from this machine, whatever the socket says.
|
|
{ ...local, headers: { 'x-forwarded-for': '203.0.113.9' } },
|
|
{ ...local, headers: { forwarded: 'for=203.0.113.9' } },
|
|
{ ...local, headers: { 'cf-connecting-ip': '203.0.113.9' } },
|
|
{ ...local, env: { PINOKIO_SHARE_LOCAL: 'true' } },
|
|
{ ...local, env: { PINOKIO_SHARE_VAR: 'GEV_SHARE_URL' } },
|
|
])
|
|
assert.equal(isLocalMcpRequest(request), false, JSON.stringify(request));
|
|
});
|
|
|
|
test('the /mcp route answers local MCP requests and refuses others', async (t) => {
|
|
let middleware;
|
|
const created = [];
|
|
const plugin = localMcpPlugin({
|
|
createServer: ({ apiBase }) => {
|
|
created.push(apiBase);
|
|
return {
|
|
handle: async (message) =>
|
|
message.id === undefined
|
|
? null
|
|
: { jsonrpc: '2.0', id: message.id, result: { apiBase } },
|
|
};
|
|
},
|
|
});
|
|
plugin.configureServer({
|
|
middlewares: { use: (path, handler) => (middleware = handler) },
|
|
});
|
|
const http = createServer((req, res) => middleware(req, res));
|
|
await new Promise((resolve) => http.listen(0, '127.0.0.1', resolve));
|
|
t.after(() => http.close());
|
|
const { port } = http.address();
|
|
const send = (method, body, headers = {}) =>
|
|
new Promise((resolve, reject) => {
|
|
const request = httpRequest(
|
|
{
|
|
host: '127.0.0.1',
|
|
port,
|
|
path: '/mcp',
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Host: `localhost:${port}`,
|
|
...headers,
|
|
},
|
|
},
|
|
(response) => {
|
|
let text = '';
|
|
response.on('data', (chunk) => (text += chunk));
|
|
response.on('end', () =>
|
|
resolve({
|
|
status: response.statusCode,
|
|
json: () => JSON.parse(text),
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
request.on('error', reject);
|
|
request.end(
|
|
body === undefined
|
|
? undefined
|
|
: typeof body === 'string'
|
|
? body
|
|
: JSON.stringify(body),
|
|
);
|
|
});
|
|
const post = (body, headers) => send('POST', body, headers);
|
|
|
|
const ok = await post({ jsonrpc: '2.0', id: 1, method: 'ping' });
|
|
assert.equal(ok.status, 200);
|
|
assert.deepEqual(await ok.json(), {
|
|
jsonrpc: '2.0',
|
|
id: 1,
|
|
result: { apiBase: `http://localhost:${port}` },
|
|
});
|
|
assert.equal(
|
|
(await post({ jsonrpc: '2.0', method: 'notifications/initialized' }))
|
|
.status,
|
|
202,
|
|
);
|
|
await post({ jsonrpc: '2.0', id: 2, method: 'ping' });
|
|
assert.deepEqual(created, [`http://localhost:${port}`]);
|
|
|
|
const foreign = await post(
|
|
{ jsonrpc: '2.0', id: 3, method: 'ping' },
|
|
{ Origin: 'https://attacker.example' },
|
|
);
|
|
assert.equal(foreign.status, 403);
|
|
assert.deepEqual(await foreign.json(), {
|
|
error: 'The MCP server only accepts local requests',
|
|
});
|
|
assert.equal((await post({}, { Host: 'attacker.example' })).status, 403);
|
|
assert.equal(
|
|
(await post({}, { Host: `localhost.attacker.example:${port}` })).status,
|
|
403,
|
|
);
|
|
// A Host naming another local port, a page on another port, and a request
|
|
// a proxy forwarded are all refused, and no server is made for them.
|
|
assert.equal((await post({}, { Host: `localhost:${port + 1}` })).status, 403);
|
|
assert.equal(
|
|
(await post({}, { Origin: `http://localhost:${port + 1}` })).status,
|
|
403,
|
|
);
|
|
assert.equal(
|
|
(await post({}, { 'X-Forwarded-For': '203.0.113.9' })).status,
|
|
403,
|
|
);
|
|
assert.deepEqual(created, [`http://localhost:${port}`]);
|
|
assert.equal((await post('x'.repeat(1024 * 1024 + 1))).status, 413);
|
|
assert.equal(
|
|
(
|
|
await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
headers: { Host: `localhost:${port}` },
|
|
})
|
|
).status,
|
|
405,
|
|
);
|
|
});
|
|
|
|
test('a client that disconnects cancels its tool call', async (t) => {
|
|
let middleware;
|
|
let seen;
|
|
const started = Promise.withResolvers();
|
|
const aborted = Promise.withResolvers();
|
|
const plugin = localMcpPlugin({
|
|
createServer: () => ({
|
|
handle: (message, { signal }) => {
|
|
seen = signal;
|
|
signal.addEventListener('abort', () => aborted.resolve(), {
|
|
once: true,
|
|
});
|
|
started.resolve();
|
|
return new Promise(() => {});
|
|
},
|
|
}),
|
|
});
|
|
plugin.configureServer({
|
|
middlewares: { use: (path, handler) => (middleware = handler) },
|
|
});
|
|
const http = createServer((req, res) => middleware(req, res));
|
|
await new Promise((resolve) => http.listen(0, '127.0.0.1', resolve));
|
|
t.after(() => http.close());
|
|
const { port } = http.address();
|
|
const request = httpRequest({
|
|
host: '127.0.0.1',
|
|
port,
|
|
path: '/mcp',
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Host: `localhost:${port}`,
|
|
},
|
|
});
|
|
request.on('error', () => {});
|
|
request.end(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call' }));
|
|
await started.promise;
|
|
assert.equal(seen.aborted, false);
|
|
request.destroy();
|
|
await aborted.promise;
|
|
assert.equal(seen.aborted, true);
|
|
});
|
|
|
|
test('a client that stops sending its request body is answered with a timeout', async (t) => {
|
|
let middleware;
|
|
const plugin = localMcpPlugin({
|
|
bodyTimeoutMs: 20,
|
|
createServer: () => ({ handle: async () => null }),
|
|
});
|
|
plugin.configureServer({
|
|
middlewares: { use: (path, handler) => (middleware = handler) },
|
|
});
|
|
const http = createServer((req, res) => middleware(req, res));
|
|
await new Promise((resolve) => http.listen(0, '127.0.0.1', resolve));
|
|
t.after(() => http.close());
|
|
const { port } = http.address();
|
|
const status = await new Promise((resolve, reject) => {
|
|
const request = httpRequest(
|
|
{
|
|
host: '127.0.0.1',
|
|
port,
|
|
path: '/mcp',
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': '100',
|
|
Host: `localhost:${port}`,
|
|
},
|
|
},
|
|
(response) => {
|
|
response.resume();
|
|
resolve(response.statusCode);
|
|
request.destroy();
|
|
},
|
|
);
|
|
request.on('error', (error) => {
|
|
if (error.code !== 'ECONNRESET') reject(error);
|
|
});
|
|
// Part of the body, then nothing more.
|
|
request.write('{"jsonrpc"');
|
|
});
|
|
assert.equal(status, 408);
|
|
});
|
|
|
|
test('panel requests cannot reach /mcp through a suffix the server routes to it', async (t) => {
|
|
const { createServer: createViteServer } = await import('vite');
|
|
const vite = await createViteServer({
|
|
root: fileURLToPath(new URL('../../', import.meta.url)),
|
|
configFile: false,
|
|
envFile: false,
|
|
publicDir: false,
|
|
logLevel: 'silent',
|
|
plugins: [localMcpPlugin()],
|
|
optimizeDeps: { noDiscovery: true, include: [] },
|
|
server: { host: '127.0.0.1', port: 0, hmr: false, watch: null },
|
|
});
|
|
await vite.listen();
|
|
t.after(() => vite.close());
|
|
const { port } = vite.httpServer.address();
|
|
const rpc = (path, message) =>
|
|
new Promise((resolve, reject) => {
|
|
const body = JSON.stringify(message);
|
|
const request = httpRequest(
|
|
{
|
|
host: '127.0.0.1',
|
|
port,
|
|
path,
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Host: `localhost:${port}`,
|
|
},
|
|
},
|
|
(response) => {
|
|
let text = '';
|
|
response.on('data', (chunk) => (text += chunk));
|
|
response.on('end', () =>
|
|
resolve({ status: response.statusCode, text }),
|
|
);
|
|
},
|
|
);
|
|
request.on('error', reject);
|
|
request.end(body);
|
|
});
|
|
|
|
// The hazard is real: the server routes /mcp.json to the MCP endpoint.
|
|
const direct = await rpc('/mcp.json', {
|
|
jsonrpc: '2.0',
|
|
id: 1,
|
|
method: 'ping',
|
|
});
|
|
assert.equal(direct.status, 200);
|
|
assert.deepEqual(JSON.parse(direct.text), {
|
|
jsonrpc: '2.0',
|
|
id: 1,
|
|
result: {},
|
|
});
|
|
|
|
const page = JSON.parse(
|
|
(
|
|
await rpc('/mcp', {
|
|
jsonrpc: '2.0',
|
|
id: 2,
|
|
method: 'resources/read',
|
|
params: { uri: 'ui://gods-eye-view/globe' },
|
|
})
|
|
).text,
|
|
);
|
|
const [, key] = page.result.contents[0].text.match(/"panelKey":"([^"]+)"/);
|
|
const body = Buffer.from(
|
|
JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'ping' }),
|
|
).toString('base64');
|
|
for (const path of ['/mcp', '/mcp.json', '/MCP.json', '/mcp.anything']) {
|
|
const answer = JSON.parse(
|
|
(
|
|
await rpc('/mcp', {
|
|
jsonrpc: '2.0',
|
|
id: 4,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'panel_request',
|
|
arguments: {
|
|
key,
|
|
path,
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body,
|
|
},
|
|
},
|
|
})
|
|
).text,
|
|
);
|
|
assert.equal(answer.result.isError, true, path);
|
|
assert.match(answer.result.content[0].text, /is not available/, path);
|
|
}
|
|
});
|