#!/usr/bin/env node import process from 'node:process'; const runtimeUrl = process.env.SHD_MCP_RUNTIME_URL || process.env.MCP_RUNTIME_URL || ''; const accessToken = process.env.SHD_MCP_ACCESS_TOKEN || process.env.MCP_ACCESS_TOKEN || ''; const requireAuth = process.argv.includes('--require-auth'); const jsonOutput = process.argv.includes('--json'); if (!runtimeUrl) { fail('Set SHD_MCP_RUNTIME_URL to the approved MCP endpoint; no endpoint is assumed.'); process.exit(1); } const mcpUrl = new URL(runtimeUrl); const origin = mcpUrl.origin; const checks = []; function fail(message) { console.error(`MCP runtime smoke: ERROR: ${message}`); process.exitCode = 1; } function addCheck(name, ok, detail = '') { checks.push({ name, ok, detail }); if (!ok) throw new Error(`${name}${detail ? `: ${detail}` : ''}`); } function safeErrorBody(value) { if (!value) return ''; return String(value).replace(/Bearer\s+[^\s]+/gi, 'Bearer [redacted]').replace(/(access_token|refresh_token|code|client_secret|cookie)=?[^&\s]*/gi, '$1=[redacted]').slice(0, 240); } async function request(url, options = {}) { const response = await fetch(url, { ...options, headers: { Accept: 'application/json', ...(options.body ? { 'Content-Type': 'application/json' } : {}), ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), ...(options.headers || {}), }, }); const text = await response.text(); let body = null; try { body = text ? JSON.parse(text) : null; } catch { body = null; } return { response, body, text }; } async function expectJson(url, name, expectedStatus = 200) { const result = await request(url); addCheck(`${name} HTTP`, result.response.status === expectedStatus, `received ${result.response.status}`); addCheck(`${name} JSON`, result.body && typeof result.body === 'object', safeErrorBody(result.text)); return result.body; } async function rpc(method, params, id, sessionId) { const headers = { Accept: 'application/json, text/event-stream' }; if (sessionId) headers['MCP-Session-Id'] = sessionId; const result = await request(mcpUrl, { method: 'POST', headers, body: JSON.stringify({ jsonrpc: '2.0', id, method, params }), }); addCheck(`${method} HTTP`, result.response.ok, `received ${result.response.status}`); addCheck(`${method} JSON`, result.body && typeof result.body === 'object', safeErrorBody(result.text)); if (result.body?.error) throw new Error(`${method}: MCP returned an error`); return { result: result.body?.result || {}, sessionId: result.response.headers.get('MCP-Session-Id') || sessionId }; } async function notifyRpc(method, params, sessionId) { const headers = { Accept: 'application/json, text/event-stream' }; if (sessionId) headers['MCP-Session-Id'] = sessionId; const result = await request(mcpUrl, { method: 'POST', headers, body: JSON.stringify({ jsonrpc: '2.0', method, params }), }); addCheck(`${method} HTTP`, result.response.ok, `received ${result.response.status}`); } async function run() { const authorization = await expectJson(`${origin}/.well-known/oauth-authorization-server`, 'OAuth authorization metadata'); addCheck('OAuth issuer', typeof authorization.issuer === 'string' && authorization.issuer.length > 0); addCheck('OAuth authorization endpoint', typeof authorization.authorization_endpoint === 'string'); addCheck('OAuth token endpoint', typeof authorization.token_endpoint === 'string'); addCheck('OAuth PKCE S256', Array.isArray(authorization.code_challenge_methods_supported) && authorization.code_challenge_methods_supported.includes('S256')); const protectedResource = await expectJson(`${origin}/.well-known/oauth-protected-resource/mcp`, 'OAuth protected-resource metadata'); addCheck('Protected resource', typeof protectedResource.resource === 'string' && protectedResource.resource.startsWith(origin)); addCheck('Bearer header support', Array.isArray(protectedResource.bearer_methods_supported) && protectedResource.bearer_methods_supported.includes('header')); if (!accessToken) { addCheck('Authenticated MCP checks', !requireAuth, 'SHD_MCP_ACCESS_TOKEN is not set'); return; } let sessionId; const initialized = await rpc('initialize', { protocolVersion: '2025-11-25', capabilities: {}, clientInfo: { name: 'shd-mcp-plugin-runtime-smoke', version: '1.0.0' }, }, 1); sessionId = initialized.sessionId; addCheck('MCP protocol', initialized.result.protocolVersion === '2025-11-25'); await notifyRpc('notifications/initialized', {}, sessionId); const tools = (await rpc('tools/list', {}, 2, sessionId)).result.tools || []; const toolNames = new Set(tools.map((tool) => tool.name)); for (const name of ['shd_list_projects', 'shd_render_projects_widget', 'shd_render_documents_widget', 'shd_render_tasks_widget', 'shd_render_finance_widget', 'shd_render_crm_widget', 'shd_render_discussions_widget', 'shd_render_notifications_widget', 'shd_render_scheduling_widget', 'shd_render_inventory_widget', 'shd_render_agents_widget', 'shd_render_status_page_widget', 'shd_render_project_db_widget', 'shd_render_notes_widget', 'shd_render_project_overview_widget', 'shd_render_files_widget', 'shd_render_proposals_widget', 'shd_render_terms_widget', 'shd_render_activity_widget', 'shd_render_organizations_widget', 'shd_render_gitea_widget', 'shd_render_schemes_widget', 'shd_render_task_kanban_widget', 'shd_render_team_workload_widget', 'shd_render_project_timeline_widget', 'shd_render_crm_funnel_widget', 'shd_render_finance_dashboard_widget', 'shd_render_proposal_approvals_widget', 'shd_render_documents_completeness_widget', 'shd_render_agents_health_widget', 'shd_render_schedule_calendar_widget', 'shd_render_acl_matrix_widget', 'shd_render_inventory_warnings_widget', 'shd_render_gitea_board_widget', 'shd_render_schemes_progress_widget', 'shd_render_notes_tree_widget', 'shd_render_project_db_preview_widget']) { addCheck(`Tool ${name}`, toolNames.has(name)); } for (const [name, items] of [ ['shd_render_projects_widget', { projects: [{ id: 1, code: 'runtime-smoke', display_name: 'Runtime smoke', status: 'check', deadline: null }] }], ['shd_render_documents_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'check', date: null, value: null }] }], ['shd_render_tasks_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'check', date: null, value: null }] }], ['shd_render_finance_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'check', date: null, value: null }] }], ['shd_render_crm_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'check', date: null, value: null }] }], ['shd_render_discussions_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'open', date: null, value: null }] }], ['shd_render_notifications_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'unread', date: null, value: null }] }], ['shd_render_scheduling_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'scheduled', date: null, value: null }] }], ['shd_render_inventory_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'available', date: null, value: null }] }], ['shd_render_agents_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'online', date: null, value: null }] }], ['shd_render_status_page_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'up', date: null, value: '100%' }] }], ['shd_render_project_db_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'ready', date: null, value: null }] }], ['shd_render_notes_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'published', date: null, value: null }] }], ['shd_render_project_overview_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'ready', date: null, value: null }] }], ['shd_render_files_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'available', date: null, value: null }] }], ['shd_render_proposals_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'draft', date: null, value: null }] }], ['shd_render_terms_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'ready', date: null, value: null }] }], ['shd_render_activity_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'new', date: null, value: null }] }], ['shd_render_organizations_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'active', date: null, value: null }] }], ['shd_render_gitea_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'open', date: null, value: null }] }], ['shd_render_schemes_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'ready', date: null, value: null }] }], ['shd_render_task_kanban_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'todo', date: null, value: null }] }], ['shd_render_team_workload_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'normal', date: null, value: '8' }] }], ['shd_render_project_timeline_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'planned', date: null, value: null }] }], ['shd_render_crm_funnel_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'qualified', date: null, value: null }] }], ['shd_render_finance_dashboard_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'draft', date: null, value: '100' }] }], ['shd_render_proposal_approvals_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'pending', date: null, value: null }] }], ['shd_render_documents_completeness_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'complete', date: null, value: '100' }] }], ['shd_render_agents_health_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'online', date: null, value: null }] }], ['shd_render_schedule_calendar_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'scheduled', date: null, value: null }] }], ['shd_render_acl_matrix_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'member', date: null, value: null }] }], ['shd_render_inventory_warnings_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'low_stock', date: null, value: '1' }] }], ['shd_render_gitea_board_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'open', date: null, value: null }] }], ['shd_render_schemes_progress_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'in_progress', date: null, value: '50' }] }], ['shd_render_notes_tree_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'active', date: null, value: null }] }], ['shd_render_project_db_preview_widget', { items: [{ id: 'runtime-smoke', title: 'Runtime smoke', status: 'ready', date: null, value: null }] }], ]) { const render = await rpc('tools/call', { name, arguments: items }, 20 + checks.length, sessionId); const output = render.result.structuredContent; addCheck(`${name} output`, output?.ok === true && Array.isArray(output.data)); } const resources = (await rpc('resources/list', {}, 3, sessionId)).result.resources || []; for (const uri of ['ui://shd/active-projects/v1.html', 'ui://shd/documents/v1.html', 'ui://shd/tasks/v1.html', 'ui://shd/finance/v1.html', 'ui://shd/crm/v1.html', 'ui://shd/discussions/v1.html', 'ui://shd/notifications/v1.html', 'ui://shd/scheduling/v1.html', 'ui://shd/inventory/v1.html', 'ui://shd/agents/v1.html', 'ui://shd/status-page/v1.html', 'ui://shd/project-db/v1.html', 'ui://shd/notes/v1.html', 'ui://shd/project-overview/v1.html', 'ui://shd/files/v1.html', 'ui://shd/proposals/v1.html', 'ui://shd/terms/v1.html', 'ui://shd/activity/v1.html', 'ui://shd/organizations/v1.html', 'ui://shd/gitea/v1.html', 'ui://shd/schemes/v1.html', 'ui://shd/task-kanban/v1.html', 'ui://shd/team-workload/v1.html', 'ui://shd/project-timeline/v1.html', 'ui://shd/crm-funnel/v1.html', 'ui://shd/finance-dashboard/v1.html', 'ui://shd/proposal-approvals/v1.html', 'ui://shd/documents-completeness/v1.html', 'ui://shd/agents-health/v1.html', 'ui://shd/schedule-calendar/v1.html', 'ui://shd/acl-matrix/v1.html', 'ui://shd/inventory-warnings/v1.html', 'ui://shd/gitea-board/v1.html', 'ui://shd/schemes-progress/v1.html', 'ui://shd/notes-tree/v1.html', 'ui://shd/project-db-preview/v1.html']) { addCheck(`Resource ${uri}`, resources.some((resource) => resource.uri === uri)); const resourceIndex = resources.findIndex((resource) => resource.uri === uri); if (resourceIndex < 0) continue; const content = (await rpc('resources/read', { uri }, 10 + resourceIndex, sessionId)).result.contents?.[0]; addCheck(`${uri} MIME`, content?.mimeType === 'text/html;profile=mcp-app'); addCheck(`${uri} bridge`, ['ui/initialize', 'ui/notifications/initialized', 'tools/call'].every((marker) => content?.text?.includes(marker))); } addCheck('Authenticated MCP resource checks', true); } try { await run(); const result = { status: 'ok', authenticated: Boolean(accessToken), checks }; if (jsonOutput) console.log(JSON.stringify(result, null, 2)); else { console.log(`MCP runtime smoke: ok (${checks.length} checks; authenticated=${Boolean(accessToken)})`); for (const check of checks) console.log(`- ${check.name}`); } } catch (error) { const message = error instanceof Error ? error.message : 'unknown error'; if (jsonOutput) console.log(JSON.stringify({ status: 'error', authenticated: Boolean(accessToken), checks, error: message }, null, 2)); else fail(message); process.exitCode = 1; }