// perm-mcp — serveur MCP stdio minimal exposant l'outil `approve` utilisé par // `claude --permission-prompt-tool mcp__adminka__approve`. Chaque demande de // permission est relayée au backend admin-ka (long-poll HTTP local) qui la // transforme en carte Autoriser/Refuser dans le chat. import readline from 'node:readline'; import dns from 'node:dns'; const PERM_URL = process.env.ADMIN_KA_PERM_URL || 'http://127.0.0.1:3300/internal/perm'; const PERM_TOKEN = process.env.ADMIN_KA_PERM_TOKEN || ''; // fetch (undici) échoue sur les noms Bonjour .local qui résolvent en IPv6 // link-local — on force la résolution IPv4 avant l'appel. async function resolvedPermUrl() { const u = new URL(PERM_URL); if (/^\d+\.\d+\.\d+\.\d+$/.test(u.hostname) || u.hostname === 'localhost') return PERM_URL; const { address } = await dns.promises.lookup(u.hostname, { family: 4 }); u.hostname = address; return u.toString(); } const rl = readline.createInterface({ input: process.stdin }); function send(obj) { process.stdout.write(JSON.stringify(obj) + '\n'); } rl.on('line', async (line) => { line = line.trim(); if (!line) return; let msg; try { msg = JSON.parse(line); } catch { return; } const { id, method, params } = msg; if (method === 'initialize') { send({ jsonrpc: '2.0', id, result: { protocolVersion: (params && params.protocolVersion) || '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'adminka', version: '1.0.0' }, }, }); return; } if (method === 'notifications/initialized') return; if (method === 'ping') { send({ jsonrpc: '2.0', id, result: {} }); return; } if (method === 'tools/list') { send({ jsonrpc: '2.0', id, result: { tools: [{ name: 'approve', description: 'Demande de permission relayée à administration-ka.com', inputSchema: { type: 'object', properties: { tool_name: { type: 'string' }, input: { type: 'object' }, tool_use_id: { type: 'string' }, }, required: ['tool_name', 'input'], }, }], }, }); return; } if (method === 'tools/call' && params && params.name === 'approve') { const args = params.arguments || {}; let decision; try { const url = await resolvedPermUrl(); const resp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: PERM_TOKEN, tool_name: args.tool_name, input: args.input, tool_use_id: args.tool_use_id }), }); decision = await resp.json(); if (decision.error) decision = { behavior: 'deny', message: 'Erreur backend: ' + decision.error }; } catch (e) { decision = { behavior: 'deny', message: 'Backend admin-ka injoignable: ' + e.message }; } send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(decision) }] }, }); return; } if (id !== undefined) { send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'method not found: ' + method } }); } });