godcrm/scripts/smoke/public-mcp-transport.mjs
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

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);