scripts/verify-management-mcp.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
import { readFileSync } from 'node:fs';
const REQUEST_TIMEOUT_MS = 15_000;
const AGENT_AUTHORING_GUIDE_URI = 'chickpea://guide/agent-authoring/v1';
const AGENT_AUTHORING_GUIDE_VERSION = '1.0.49';
const MANAGEMENT_MCP_SERVER_VERSION = '4.5.0';
const MANAGEMENT_MCP_TOOL_NAMES = [
'inspect_workspace',
'prepare_connector_setup',
'prepare_provider_setup',
'discover_slack_channels',
'test_connection',
'save_connection_key',
'try_connection',
'inspect_memory',
'inspect_routines',
'export_workspace_recipe',
'preview_workspace_recipe',
'import_skill',
'upload_skills',
'manage_agent_skill',
'apply_workspace_changes',
'confirm_workspace_change',
'undo_workspace_change',
'get_operation',
'revoke_setup_link',
'list_mcp_clients',
'revoke_mcp_client',
'get_account',
'get_workspace',
'list_agents',
'get_agent',
'list_services',
'get_service',
'list_models',
'list_members',
'list_slack_channels',
'get_agent_memory',
'list_schedules',
'get_setup_status',
'export_agents',
'list_connected_apps',
'list_agent_runs',
'get_agent_run',
'propose_agent',
'propose_agent_change',
'get_creation_status',
'practice_agent',
'get_practice_run',
'list_practice_runs',
'try_agent_once',
'get_test_run',
'explain_agent',
'check_agent',
'create_agent',
'update_agent',
'update_agent_memory',
'archive_agent',
'restore_agent',
'delete_agent',
'add_agent_to_channel',
'remove_agent_from_channel',
'save_channel_settings',
'import_skill_from_github',
'upload_skill_files',
'set_agent_skill',
'connect_service',
'reconnect_service',
'update_connection',
'remove_connection',
'remove_website_login',
'set_agent_repositories',
'save_schedule',
'pause_schedule',
'resume_schedule',
'run_schedule_now',
'take_over_schedule',
'delete_schedule',
'set_workspace_model',
'connect_model_provider',
'remove_model_provider_key',
'update_member',
'cancel_setup_link',
'disconnect_app',
'confirm_change',
'decline_change',
'get_confirmation',
'undo_change',
];
function fetchWithDeadline(input, init = {}) {
return fetch(input, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
}
const rawBase = process.env.MANAGEMENT_MCP_BASE_URL?.trim();
if (!rawBase) {
console.error('Set MANAGEMENT_MCP_BASE_URL to a disposable Chickpea deployment.');
process.exitCode = 2;
} else {
await main(rawBase);
}
async function main(rawBaseUrl) {
const base = new URL(rawBaseUrl);
if (base.protocol !== 'https:' && !['localhost', '127.0.0.1', '::1'].includes(base.hostname)) {
throw new Error('MANAGEMENT_MCP_BASE_URL must use HTTPS or loopback HTTP.');
}
base.pathname = '/';
base.search = '';
base.hash = '';
const resourceUrl = new URL('/.well-known/oauth-protected-resource/mcp', base);
const resourceResponse = await fetchWithDeadline(resourceUrl, { redirect: 'error' });
requireStatus(resourceResponse, 200, 'protected-resource discovery');
const resource = await resourceResponse.json();
requireEqual(resource.resource, new URL('/mcp', base).href, 'protected resource');
if (!Array.isArray(resource.authorization_servers) || resource.authorization_servers.length !== 1) {
throw new Error('Authorization-server metadata is not exact.');
}
const authorizationUrl = new URL('/.well-known/oauth-authorization-server/api/auth', base);
const authorizationResponse = await fetchWithDeadline(authorizationUrl, { redirect: 'error' });
requireStatus(authorizationResponse, 200, 'authorization-server discovery');
const authorization = await authorizationResponse.json();
requireEqual(authorization.registration_endpoint, new URL('/api/auth/oauth2/register', base).href, 'DCR endpoint');
requireEqual(authorization.token_endpoint, new URL('/api/auth/oauth2/token', base).href, 'token endpoint');
const challenge = await fetchWithDeadline(new URL('/mcp', base), {
method: 'POST',
redirect: 'error',
headers: mcpHeaders('tools/list', {}),
body: rpcBody('tools/list', {}),
});
requireStatus(challenge, 401, 'unauthenticated MCP challenge');
if (!(challenge.headers.get('www-authenticate') ?? '').includes('resource_metadata=')) {
throw new Error('MCP challenge omitted protected-resource metadata.');
}
console.log('ok discovery and unauthenticated challenge');
const token = process.env.MANAGEMENT_MCP_BEARER_TOKEN?.trim();
if (!token) {
console.log('skip authenticated checks: MANAGEMENT_MCP_BEARER_TOKEN is unset');
return;
}
if (protocolVersion() !== '2026-07-28') {
const initialized = await mcpCall(base, token, 'initialize', {
protocolVersion: protocolVersion(),
capabilities: {},
clientInfo: { name: 'chickpea-management-canary', version: '1.0.0' },
});
requireEqual(
initialized?.serverInfo?.version,
MANAGEMENT_MCP_SERVER_VERSION,
'workspace-management MCP server version',
);
}
const listed = await mcpCall(base, token, 'tools/list', {});
const names = listed?.tools?.map?.((tool) => tool?.name).filter(Boolean) ?? [];
requireEqual(names.join(','), MANAGEMENT_MCP_TOOL_NAMES.join(','), 'MCP tool inventory');
const resources = await mcpCall(base, token, 'resources/list', {});
if (!(resources?.resources ?? []).some((resource) => resource?.uri === AGENT_AUTHORING_GUIDE_URI)) {
throw new Error('MCP resource inventory omitted the Agent-authoring guide.');
}
const guideResult = await mcpCall(base, token, 'resources/read', {
uri: AGENT_AUTHORING_GUIDE_URI,
});
const guide = JSON.parse(guideResult?.contents?.[0]?.text ?? '{}');
requireEqual(guide.version, AGENT_AUTHORING_GUIDE_VERSION, 'Agent-authoring guide version');
if (typeof guide.digest !== 'string' || !/^[a-f0-9]{64}$/.test(guide.digest) ||
typeof guide.guide !== 'string' || guide.guide.length < 1 ||
typeof guide.files?.['skill-creation.md'] !== 'string' ||
guide.files['skill-creation.md'].length < 1) {
throw new Error('Agent-authoring guide content or digest is invalid.');
}
const inspected = await mcpCall(base, token, 'tools/call', {
name: 'inspect_workspace',
arguments: {},
});
const snapshot = assertSuccessfulToolResult(inspected, 'workspace inspection');
const reinspected = assertSuccessfulToolResult(await mcpCall(base, token, 'tools/call', {
name: 'inspect_workspace',
arguments: {},
}), 'repeat workspace inspection');
requireEqual(
reinspected?.effectiveRevision,
snapshot?.effectiveRevision,
'configuration revision across read-only verification',
);
// A second stateless call exercises disconnect/reconnect semantics without
// retaining a server session or printing the private workspace snapshot.
const relisted = await mcpCall(base, token, 'tools/list', {});
if ((relisted?.tools?.length ?? 0) !== names.length) {
throw new Error('Stateless reconnect returned a different tool inventory.');
}
console.log(`ok authenticated guide, read-only inspection, and stateless reconnect (${names.length} tools)`);
const applyPath = process.env.MANAGEMENT_MCP_CANARY_APPLY_PATH?.trim();
if (!applyPath) return;
if (process.env.MANAGEMENT_MCP_ALLOW_MUTATION !== '1') {
throw new Error('Set MANAGEMENT_MCP_ALLOW_MUTATION=1 to authorize the supplied canary apply payload.');
}
const argumentsValue = JSON.parse(readFileSync(applyPath, 'utf8'));
const applied = await mcpCall(base, token, 'tools/call', {
name: 'apply_workspace_changes',
arguments: argumentsValue,
});
assertSuccessfulToolResult(applied, 'authorized canary mutation');
console.log('ok authorized canary mutation (response body intentionally suppressed)');
}
async function mcpCall(base, token, method, params) {
const response = await fetchWithDeadline(new URL('/mcp', base), {
method: 'POST',
redirect: 'error',
headers: { ...mcpHeaders(method, params), authorization: `Bearer ${token}` },
body: rpcBody(method, params),
});
requireStatus(response, 200, `MCP ${method}`);
const body = await response.text();
if (body.trimStart().startsWith('{')) {
const envelope = JSON.parse(body);
if (envelope.error) throw new Error(`MCP ${method} returned a protocol error.`);
return envelope.result;
}
const event = body.split('\n').find((line) => line.startsWith('data: '));
if (!event) throw new Error(`MCP ${method} returned no SSE data event.`);
const envelope = JSON.parse(event.slice('data: '.length));
if (envelope.error) throw new Error(`MCP ${method} returned a protocol error.`);
return envelope.result;
}
function assertSuccessfulToolResult(result, label) {
const text = result?.content?.[0]?.text;
if (typeof text !== 'string') throw new Error(`${label} returned no text result.`);
const envelope = JSON.parse(text);
if (envelope?.ok !== true) throw new Error(`${label} returned a tool error.`);
return envelope.result;
}
function mcpHeaders(method, params) {
const protocol = protocolVersion();
return {
accept: 'application/json, text/event-stream',
'content-type': 'application/json',
'mcp-protocol-version': protocol,
...(protocol === '2026-07-28' ? { 'mcp-method': method } : {}),
...(protocol === '2026-07-28' && method === 'tools/call' && typeof params.name === 'string'
? { 'mcp-name': params.name }
: {}),
...(protocol === '2026-07-28' && method === 'resources/read' && typeof params.uri === 'string'
? { 'mcp-name': params.uri }
: {}),
};
}
function rpcBody(method, params) {
const protocol = protocolVersion();
const bodyParams = protocol === '2026-07-28' ? {
...params,
_meta: {
'io.modelcontextprotocol/protocolVersion': protocol,
'io.modelcontextprotocol/clientCapabilities': {},
},
} : params;
return JSON.stringify({ jsonrpc: '2.0', id: `${method}:canary`, method, params: bodyParams });
}
function protocolVersion() {
return process.env.MANAGEMENT_MCP_PROTOCOL_VERSION ?? '2025-11-25';
}
function requireStatus(response, expected, label) {
if (response.status !== expected) {
throw new Error(`${label} returned HTTP ${response.status}; expected ${expected}.`);
}
}
function requireEqual(actual, expected, label) {
if (actual !== expected) throw new Error(`${label} did not match the deployment origin.`);
}