Ctrl K
ComunidadGuías…

scripts/verify-management-mcp.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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.`);
}