fix(mcp): share one panel key across an install's stdio servers

Fixes bilawalsidhu/gods-eye-view#927. Each stdio MCP server made its own
random panel key, and panel_request accepts only the key in the panel
page. A client that starts two server processes for one connection and
reads the page from one while sending the page's requests to the other
(seen in Claude Desktop on Windows) therefore always got "Only the God's
Eye View panel may make this request".

The stdio entry now passes a key shared by the install, created once in
.gev-cache/mcp-panel-key. The file is staged 0600 and published with
link(), so processes starting together agree on one key and never read a
half-written one. If it cannot be stored, the process keeps a key of its
own, as before. createLocalMcpServer still makes a fresh key unless given
one, so in-process servers (the /mcp HTTP plugin, tests) are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
jibraaanandClaude Opus 5.5 committed 2026-10-07 18:00:38 +05:00
1 parent e685449a52
commit e363b6a14f
6 files changed
+229 -5

No files matched your search

+124
View File
@@ -0,0 +1,124 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { sharedPanelKey } from '../../server/mcp/panelKey.js';
import { createLocalMcpServer } from '../../server/mcp/server.js';
const PANEL_KEY_MODULE = new URL(
'../../server/mcp/panelKey.js',
import.meta.url,
).href;
function tempKeyFile(t) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gev-panel-key-'));
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
return path.join(dir, 'state', 'mcp-panel-key');
}
const readPage = async (server) => {
const page = await server.handle({
jsonrpc: '2.0',
id: 1,
method: 'resources/read',
params: { uri: 'ui://gods-eye-view/globe' },
});
return page.result.contents[0].text.match(/"panelKey":"([^"]+)"/)[1];
};
const panelRequest = (server, key) =>
server.handle({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'panel_request',
arguments: { key, path: '/panel/index.html' },
},
});
const serverWith = (panelKey) =>
createLocalMcpServer({
apiBase: 'http://127.0.0.1:5000',
fetchImpl: async () => new Response('ok'),
...(panelKey ? { panelKey } : {}),
});
test('a panel page from one server process is refused by another without a shared key (#927)', async () => {
const pageServer = serverWith();
const requestServer = serverWith();
const key = await readPage(pageServer);
const refused = await panelRequest(requestServer, key);
assert.equal(refused.result.isError, true);
assert.match(
refused.result.content[0].text,
/Only the God's Eye View panel may make this request/,
);
});
test('servers sharing the install key accept each other’s panel page', async (t) => {
const file = tempKeyFile(t);
const pageServer = serverWith(sharedPanelKey({ file }));
const requestServer = serverWith(sharedPanelKey({ file }));
const key = await readPage(pageServer);
const opened = await panelRequest(requestServer, key);
assert.equal(opened.result.isError, false);
});
test('the shared key is created once, kept, and private to the user', (t) => {
const file = tempKeyFile(t);
const first = sharedPanelKey({ file });
assert.match(first, /^[A-Za-z0-9_-]{43}$/);
assert.equal(sharedPanelKey({ file }), first);
assert.equal(fs.readFileSync(file, 'utf8').trim(), first);
if (process.platform !== 'win32')
assert.equal(fs.statSync(file).mode & 0o777, 0o600);
// No staging files are left beside it.
assert.deepEqual(fs.readdirSync(path.dirname(file)), ['mcp-panel-key']);
});
test('a malformed key file is replaced', (t) => {
const file = tempKeyFile(t);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, 'not a key\n');
const key = sharedPanelKey({ file });
assert.match(key, /^[A-Za-z0-9_-]{43}$/);
assert.equal(sharedPanelKey({ file }), key);
});
test('when the key cannot be stored, the process uses a key of its own', (t) => {
const file = tempKeyFile(t);
// A file where the directory should be makes every write fail.
fs.writeFileSync(path.dirname(path.dirname(file)) + '/state', 'blocker');
const logged = [];
const a = sharedPanelKey({ file, log: (line) => logged.push(line) });
const b = sharedPanelKey({ file, log: (line) => logged.push(line) });
assert.match(a, /^[A-Za-z0-9_-]{43}$/);
assert.notEqual(a, b);
assert.equal(logged.length, 2);
assert.match(logged[0], /panel key not shared/);
});
test('processes starting at the same moment agree on one key', async (t) => {
const file = tempKeyFile(t);
const script =
`const { sharedPanelKey } = await import(${JSON.stringify(PANEL_KEY_MODULE)});` +
`process.stdout.write(sharedPanelKey({ file: ${JSON.stringify(file)} }));`;
const run = () =>
new Promise((resolve, reject) => {
const child = spawn(
process.execPath,
['--input-type=module', '-e', script],
{ stdio: ['ignore', 'pipe', 'inherit'] },
);
let out = '';
child.stdout.on('data', (chunk) => (out += chunk));
child.on('error', reject);
child.on('close', (code) =>
code === 0 ? resolve(out) : reject(new Error(`exit ${code}`)),
);
});
const keys = await Promise.all(Array.from({ length: 8 }, run));
assert.equal(new Set(keys).size, 1);
assert.equal(fs.readFileSync(file, 'utf8').trim(), keys[0]);
});