Skip to main content
Glama
portel-dev

NCP - Natural Context Provider

by portel-dev
test-native-dialog.cjs10 kB
#!/usr/bin/env node /** * Test: Native Dialog Fallback * * Simulates an MCP client (like Claude Desktop without elicitation support) * calling ncp:add to trigger the native OS dialog confirmation. * * Expected behavior: * 1. Client calls tools/call with ncp:add * 2. Server attempts elicitation (will fail - client doesn't support it) * 3. Server falls back to native OS dialog * 4. Native macOS dialog appears with Approve/Cancel buttons * 5. User clicks Approve/Cancel * 6. Server returns success/failure based on user choice */ const { spawn } = require('child_process'); const fs = require('fs'); const path = require('path'); const os = require('os'); // Test configuration const NCP_DIR = path.join(os.homedir(), '.ncp'); const PROFILES_DIR = path.join(NCP_DIR, 'profiles'); const TEST_PROFILE = 'dialog-test'; // ANSI colors for output const colors = { green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m', reset: '\x1b[0m' }; function log(emoji, message, color = 'reset') { console.log(`${emoji} ${colors[color]}${message}${colors.reset}`); } function logError(message) { log('❌', message, 'red'); } function logSuccess(message) { log('✅', message, 'green'); } function logInfo(message) { log('ℹ️', message, 'blue'); } function logWarning(message) { log('⚠️', message, 'yellow'); } // Setup test profile function setupTestProfile() { if (!fs.existsSync(PROFILES_DIR)) { fs.mkdirSync(PROFILES_DIR, { recursive: true }); } const profilePath = path.join(PROFILES_DIR, `${TEST_PROFILE}.json`); const testProfile = { mcpServers: { // Start with empty profile - we'll add time MCP via the test } }; fs.writeFileSync(profilePath, JSON.stringify(testProfile, null, 2)); logInfo(`Created test profile at ${profilePath}`); } class MCPClientSimulator { constructor() { this.ncp = null; this.responses = []; this.responseBuffer = ''; this.requestId = 0; } start() { return new Promise((resolve, reject) => { logInfo('Starting NCP MCP server in extension mode...'); this.ncp = spawn('node', ['dist/index-mcp.js', '--profile', TEST_PROFILE], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, NCP_MODE: 'extension', // Run in extension mode (like .dxt) NCP_CONFIG_PATH: NCP_DIR, NCP_PROFILE: TEST_PROFILE, NCP_CONFIRM_BEFORE_RUN: 'true', // Enable confirmations NO_COLOR: 'true', NCP_DEBUG: 'true' } }); this.ncp.stdout.on('data', (data) => { this.responseBuffer += data.toString(); const lines = this.responseBuffer.split('\n'); lines.slice(0, -1).forEach(line => { if (line.trim()) { try { const response = JSON.parse(line); this.responses.push(response); // Log responses for debugging if (response.error) { logWarning(`Response ${response.id}: ERROR ${response.error.code} - ${response.error.message}`); } else if (response.result) { logInfo(`Response ${response.id}: ${JSON.stringify(response.result).substring(0, 100)}...`); } } catch (e) { // Ignore non-JSON lines (logs, etc.) } } }); this.responseBuffer = lines[lines.length - 1]; }); this.ncp.stderr.on('data', (data) => { const msg = data.toString(); console.log(colors.cyan + msg.trim() + colors.reset); }); this.ncp.on('error', reject); this.ncp.on('exit', (code) => { if (code !== 0 && code !== null) { logError(`NCP exited with code ${code}`); } }); // Give it a moment to start setTimeout(resolve, 500); }); } sendRequest(method, params = {}) { this.requestId++; const request = { jsonrpc: '2.0', id: this.requestId, method, params }; logInfo(`Sending: ${method} (id=${this.requestId})`); this.ncp.stdin.write(JSON.stringify(request) + '\n'); return this.requestId; } waitForResponse(id, timeoutMs = 60000) { return new Promise((resolve, reject) => { const startTime = Date.now(); const checkResponse = () => { const response = this.responses.find(r => r.id === id); if (response) { resolve(response); return; } if (Date.now() - startTime > timeoutMs) { reject(new Error(`Timeout waiting for response to request ${id} after ${timeoutMs}ms`)); return; } setTimeout(checkResponse, 100); }; checkResponse(); }); } async stop() { if (this.ncp) { this.ncp.kill(); await new Promise(resolve => setTimeout(resolve, 500)); } } } async function testNativeDialogFallback() { console.log('\n' + '='.repeat(70)); console.log('🧪 Testing Native Dialog Fallback for Confirmations'); console.log('='.repeat(70) + '\n'); setupTestProfile(); const client = new MCPClientSimulator(); await client.start(); try { // Initialize the connection logInfo('Step 1: Initializing MCP connection...'); const initId = client.sendRequest('initialize', { protocolVersion: '2024-11-05', capabilities: { // Note: NOT including elicitation capability // This simulates Claude Desktop without elicitation support }, clientInfo: { name: 'test-client-no-elicitation', version: '1.0.0' } }); const initResponse = await client.waitForResponse(initId, 5000); if (initResponse.error) { logError(`Initialize failed: ${initResponse.error.message}`); await client.stop(); return false; } logSuccess('Initialized successfully'); // Send initialized notification client.ncp.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n'); await new Promise(resolve => setTimeout(resolve, 1000)); // List available tools logInfo('\nStep 2: Listing available tools...'); const listId = client.sendRequest('tools/list'); const listResponse = await client.waitForResponse(listId, 5000); if (listResponse.error) { logError(`tools/list failed: ${listResponse.error.message}`); await client.stop(); return false; } const tools = listResponse.result?.tools || []; const runTool = tools.find(t => t.name === 'run'); if (!runTool) { logError('run tool not found in tools list'); logInfo('Available tools: ' + tools.map(t => t.name).join(', ')); await client.stop(); return false; } logSuccess(`Found run tool (used to call internal management tools)`); // Try to add a test MCP - this should trigger native dialog console.log('\n' + '='.repeat(70)); logWarning('⏰ WATCH FOR NATIVE DIALOG BOX!'); console.log(' A macOS dialog should appear asking to confirm MCP installation.'); console.log(' You have 45 seconds to click Approve or Cancel.'); console.log('='.repeat(70) + '\n'); logInfo('Step 3: Calling ncp:add via run tool (triggering native dialog)...'); const addId = client.sendRequest('tools/call', { name: 'run', arguments: { tool: 'ncp:add', parameters: { mcp_name: 'time', command: 'npx', args: ['-y', '@modelcontextprotocol/server-time'], profile: TEST_PROFILE } } }); logInfo('Waiting for response (up to 60 seconds)...'); logWarning('Check your screen for a dialog box!'); const addResponse = await client.waitForResponse(addId, 60000); console.log('\n' + '='.repeat(70)); if (addResponse.error) { // Check if it's a timeout/cancellation error const errorMsg = addResponse.error.message || ''; if (errorMsg.includes('⏳ Waiting for user confirmation')) { logWarning('TIMEOUT: Dialog timed out waiting for user response'); logInfo('This is expected if you didn\'t click within 45 seconds'); logInfo('Error message:\n' + errorMsg); console.log('='.repeat(70) + '\n'); logInfo('You can now test RETRY by running this test again within 60 seconds'); logInfo('If you clicked Approve already, the retry should proceed immediately'); await client.stop(); return true; // Test passed (timeout behavior is correct) } else { logError(`ncp:add failed: ${errorMsg}`); console.log('='.repeat(70) + '\n'); await client.stop(); return false; } } const result = addResponse.result?.content?.[0]?.text || ''; if (result.includes('✅') || result.includes('added')) { logSuccess('MCP added successfully!'); logInfo('User clicked APPROVE in the native dialog'); console.log('\nResult:\n' + result); } else if (result.includes('cancelled') || result.includes('⛔')) { logWarning('MCP addition cancelled by user'); logInfo('User clicked CANCEL in the native dialog'); console.log('\nResult:\n' + result); } else { logError('Unexpected result from ncp:add'); console.log('\nResult:\n' + result); await client.stop(); return false; } console.log('='.repeat(70) + '\n'); await client.stop(); return true; } catch (error) { logError(`Test failed with error: ${error.message}`); console.error(error); await client.stop(); return false; } } // Run the test testNativeDialogFallback().then(success => { if (success) { console.log('✅ Native dialog fallback test completed successfully\n'); process.exit(0); } else { console.log('❌ Native dialog fallback test failed\n'); process.exit(1); } }).catch(error => { logError(`Test crashed: ${error.message}`); console.error(error); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/portel-dev/ncp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server