Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
86 lines
4.3 KiB
JavaScript
86 lines
4.3 KiB
JavaScript
/**
|
|
* DB-free transport smoke for the public MCP mount (NO-SIGNUP-MCP §B).
|
|
* Mounts mcpPublicHandler behind a stub that pins req.publicSpace, then drives
|
|
* a real JSON-RPC handshake through StreamableHTTPServerTransport:
|
|
* initialize → tools/list (expect EXACTLY the 9-tool read-safe allow-set,
|
|
* MCP-DOOR-LEGIBILITY Option A) → tools/call list_spaces
|
|
* (locked → account_required upsell, NOT cross-tenant data).
|
|
* No loader is exercised, so no DB is touched.
|
|
*/
|
|
import express from 'express';
|
|
import request from 'supertest';
|
|
import { mcpPublicHandler } from '../../backend/services/mcp/publicMcp.js';
|
|
|
|
const app = express();
|
|
app.use(express.json());
|
|
app.all('/s/:slug/mcp', (req, _res, next) => {
|
|
req.publicSpace = { id: 7773, name: 'GOD CRM Public', settings: null, public_slug: 'help' };
|
|
next();
|
|
}, mcpPublicHandler);
|
|
|
|
const HEADERS = { Accept: 'application/json, text/event-stream', 'Content-Type': 'application/json' };
|
|
const rpc = (id, method, params = {}) => ({ jsonrpc: '2.0', id, method, params });
|
|
|
|
let failed = false;
|
|
const check = (label, cond, detail) => {
|
|
console.log(`${cond ? 'PASS' : 'FAIL'} ${label}${cond ? '' : ' — ' + detail}`);
|
|
if (!cond) failed = true;
|
|
};
|
|
|
|
// Streamable-HTTP may answer as SSE-framed text even with enableJsonResponse;
|
|
// extract the JSON-RPC payload from either shape.
|
|
const parseBody = (res) => {
|
|
const t = res.text || (res.body ? JSON.stringify(res.body) : '');
|
|
const line = t.split('\n').find(l => l.startsWith('data:')) || t;
|
|
try { return JSON.parse(line.replace(/^data:\s*/, '')); } catch { return res.body; }
|
|
};
|
|
|
|
const init = await request(app).post('/s/help/mcp').set(HEADERS).send(
|
|
rpc(1, 'initialize', {
|
|
protocolVersion: '2024-11-05',
|
|
capabilities: {},
|
|
clientInfo: { name: 'smoke', version: '0' },
|
|
})
|
|
);
|
|
const initBody = parseBody(init);
|
|
check('initialize returns a result', init.status === 200 && !!initBody?.result, `status=${init.status} body=${JSON.stringify(initBody).slice(0,200)}`);
|
|
|
|
const list = await request(app).post('/s/help/mcp').set(HEADERS).send(rpc(2, 'tools/list'));
|
|
const listBody = parseBody(list);
|
|
const tools = listBody?.result?.tools || [];
|
|
const toolNames = tools.map(t => t.name).sort();
|
|
const ALLOWSET = [
|
|
'get_dashboard_widgets', 'get_document_content', 'get_table_row', 'get_table_schema',
|
|
'list_documents', 'list_projects', 'list_tables', 'memory_recall', 'query_table_data',
|
|
].sort();
|
|
check('tools/list advertises EXACTLY the 9 read-safe tools (Option A)',
|
|
toolNames.length === ALLOWSET.length && toolNames.every((n, i) => n === ALLOWSET[i]),
|
|
`got ${tools.length}: ${toolNames.join(',')}`);
|
|
check('tools/list advertises NO write/destructive verbs',
|
|
!tools.some(t => /^(delete_|add_|update_|create_|send_|batch_|move_|copy_|printer_)/.test(t.name)),
|
|
toolNames.filter(n => /^(delete_|add_|update_|create_|send_|batch_|move_|copy_|printer_)/.test(n)).join(','));
|
|
check('tools/list NOT a "Server not initialized" error', !listBody?.error, JSON.stringify(listBody?.error));
|
|
|
|
// Lever 1: serverInfo signals read-only + intentionally public.
|
|
check('serverInfo.name signals read-only', initBody?.result?.serverInfo?.name === 'godcrm-public-readonly',
|
|
JSON.stringify(initBody?.result?.serverInfo));
|
|
check('serverInfo.instructions carries "intentionally public" clause',
|
|
/intentionally public/i.test(initBody?.result?.instructions || ''),
|
|
(initBody?.result?.instructions || '').slice(0, 120));
|
|
|
|
// Lever 2: GET probe → benign banner, not a transport error.
|
|
const probe = await request(app).get('/s/help/mcp');
|
|
const probeBody = probe.body || {};
|
|
check('GET probe → 200 benign banner', probe.status === 200 && probeBody.intentional === true,
|
|
`status=${probe.status} body=${JSON.stringify(probeBody).slice(0,160)}`);
|
|
|
|
const locked = await request(app).post('/s/help/mcp').set(HEADERS).send(
|
|
rpc(3, 'tools/call', { name: 'list_spaces', arguments: {} })
|
|
);
|
|
const lockedBody = parseBody(locked);
|
|
const lockedText = lockedBody?.result?.content?.[0]?.text || '';
|
|
check('locked tool list_spaces → account_required upsell', lockedText.includes('account_required'), lockedText.slice(0, 160));
|
|
check('locked tool did NOT return tenant data', !/space/i.test(lockedText) || lockedText.includes('account_required'), lockedText.slice(0, 160));
|
|
|
|
console.log(failed ? '\nSMOKE FAILED' : '\nSMOKE OK');
|
|
process.exit(failed ? 1 : 0);
|