// integrations.js — 웹훅 설정 + MCP 서버 연동 관리.
// extracted from app.js (v4.3.19-3 split).
let _integTab = 'webhooks';
function setIntegTab(tab) {
_integTab = tab;
['webhooks', 'mcp'].forEach(t => {
const btn = document.getElementById('itab-' + t);
const panel = document.getElementById('itab-panel-' + t);
if (btn) {
btn.style.background = t === tab ? '#eaf2ff' : '#fff';
btn.style.color = t === tab ? '#0d4faf' : 'var(--muted)';
btn.style.borderColor = t === tab ? '#bdd3f6' : 'var(--line)';
}
if (panel) panel.style.display = t === tab ? 'block' : 'none';
});
}
async function loadIntegrationsTab() {
setIntegTab(_integTab);
await loadWebhookSettings();
await loadMCPServers();
}
// --- Webhooks -----------------------------------------------------------------
async function loadWebhookSettings() {
try {
const data = await api('/api/settings/hooks');
const h = data.hooks || {};
const cb = document.getElementById('wh-enabled');
const inp = document.getElementById('wh-token');
const pathInp = document.getElementById('wh-path');
if (cb) cb.checked = h.enabled === true;
if (inp) inp.value = h.tokenSet ? '••••••••' : '';
if (pathInp) pathInp.value = h.path || '/hooks';
updateWebhookStatus(h.enabled, h.tokenSet);
updateWebhookUrlDisplay(h.enabled, h.tokenSet, h.path || '/hooks');
} catch(e) {
console.warn('loadWebhookSettings:', e);
}
}
function updateWebhookStatus(enabled, tokenSet) {
const dot = document.getElementById('wh-status-dot');
const txt = document.getElementById('wh-status-text');
if (!dot || !txt) return;
if (enabled && tokenSet) {
dot.style.background = '#22c55e';
txt.textContent = '활성 — 엔드포인트 실행 중';
txt.style.color = '#166534';
} else if (!enabled) {
dot.style.background = '#ccc';
txt.textContent = '비활성';
txt.style.color = 'var(--muted)';
} else {
dot.style.background = '#f59e0b';
txt.textContent = '활성화됨 — 토큰 미설정';
txt.style.color = '#92400e';
}
}
function updateWebhookUrlDisplay(enabled, tokenSet, hookPath) {
const el = document.getElementById('wh-url-display');
if (!el) return;
if (enabled && tokenSet) {
const origin = window.location.origin || 'http://localhost:18789';
el.style.display = 'block';
el.textContent = origin + hookPath + '/agent';
} else {
el.style.display = 'none';
}
}
function generateWebhookToken() {
const arr = new Uint8Array(24);
crypto.getRandomValues(arr);
const token = Array.from(arr).map(b => b.toString(16).padStart(2,'0')).join('');
const inp = document.getElementById('wh-token');
if (inp) { inp.value = token; inp.type = 'text'; setTimeout(() => { inp.type = 'password'; }, 4000); }
}
async function saveWebhookSettings() {
const enabled = document.getElementById('wh-enabled')?.checked ?? false;
const rawToken = document.getElementById('wh-token')?.value?.trim() || '';
const hookPath = document.getElementById('wh-path')?.value?.trim() || '/hooks';
try {
const r = await api('/api/settings/hooks', {
method: 'POST',
body: JSON.stringify({ enabled, token: rawToken, path: hookPath }),
});
if (r.success) {
await loadWebhookSettings();
showIntegMsg('? Webhook settings saved — restart gateway to apply', '#166534', '#f0fdf4');
} else {
showIntegMsg('Error: ' + (r.error || 'Unknown error'), '#991b1b', '#fef2f2');
}
} catch(e) {
showIntegMsg('Save failed: ' + e.message, '#991b1b', '#fef2f2');
}
}
async function testWebhookEndpoint() {
try {
const r = await api('/api/settings/hooks/test', { method: 'POST' });
if (r.success) {
showIntegMsg('? ' + (r.message || 'Endpoint is active'), '#166534', '#f0fdf4');
} else {
showIntegMsg('? ' + (r.error || 'Test failed'), '#991b1b', '#fef2f2');
}
} catch(e) {
showIntegMsg('Test failed: ' + e.message, '#991b1b', '#fef2f2');
}
}
function copyWebhookCurl() {
const origin = window.location.origin || 'http://localhost:18789';
const hookPath = document.getElementById('wh-path')?.value?.trim() || '/hooks';
const curl = `curl -X POST ${origin}${hookPath}/agent \\
-H "X-Gateway-Token: YOUR_TOKEN" \\
-H "Content-Type: application/json" \\
-d '{"message": "Hello from webhook", "name": "MyApp", "deliver": true}'`;
navigator.clipboard?.writeText(curl).then(() => {
showIntegMsg('cURL example copied to clipboard', '#1e40af', '#eff6ff');
}).catch(() => {
showIntegMsg('Could not copy — check browser permissions', '#92400e', '#fffbeb');
});
}
// --- MCP ---------------------------------------------------------------------
let _mcpEditingId = null;
const MCP_PRESETS = {
filesystem: { name: 'Filesystem', transport: 'stdio', command: 'npx', args: '-y\n@modelcontextprotocol/server-filesystem\nC:\\Users', env: '', description: 'Read/write local files' },
github: { name: 'GitHub', transport: 'stdio', command: 'npx', args: '-y\n@modelcontextprotocol/server-github', env: 'GITHUB_PERSONAL_ACCESS_TOKEN=', description: 'Repos, PRs, issues' },
postgres: { name: 'PostgreSQL', transport: 'stdio', command: 'npx', args: '-y\n@modelcontextprotocol/server-postgres\npostgresql://localhost/mydb', env: '', description: 'Query PostgreSQL' },
sqlite: { name: 'SQLite', transport: 'stdio', command: 'npx', args: '-y\n@modelcontextprotocol/server-sqlite\n--db-path\nC:\\path\\to\\db.sqlite', env: '', description: 'Local database' },
brave: { name: 'Brave Search', transport: 'stdio', command: 'npx', args: '-y\n@modelcontextprotocol/server-brave-search', env: 'BRAVE_API_KEY=', description: 'Web search' },
memory: { name: 'Memory', transport: 'stdio', command: 'npx', args: '-y\n@modelcontextprotocol/server-memory', env: '', description: 'Persistent knowledge store' },
};
async function loadMCPServers() {
const el = document.getElementById('mcp-server-list');
if (!el) return;
try {
const data = await api('/api/mcp/servers');
const servers = data.servers || [];
if (servers.length === 0) {
el.innerHTML = '
No MCP servers configured yet.
Add one above or click a preset on the right.
';
return;
}
el.innerHTML = servers.map(s => {
const statusColor = s.status === 'connected' ? '#22c55e' : s.status === 'error' ? '#ef4444' : s.status === 'connecting' ? '#f59e0b' : '#9ca3af';
const statusLabel = s.status === 'connected' ? `Connected · ${s.toolCount} tool${s.toolCount !== 1 ? 's' : ''}` : s.status === 'error' ? 'Error' : s.status === 'connecting' ? 'Connecting…' : 'Disconnected';
const toolTip = s.toolNames?.length ? s.toolNames.join(', ') : '';
return `
${escHtml(s.name)} (${escHtml(s.id)})
${statusLabel}${s.error ? ` — ${escHtml(s.error.slice(0,60))}` : ''}
${toolTip ? `
${escHtml(toolTip.slice(0,80))}${toolTip.length>80?'…':''}
` : ''}
${s.status === 'connected'
? ``
: ``
}
`;
}).join('');
} catch(e) {
el.innerHTML = `Failed to load: ${escHtml(e.message)}
`;
}
}
function showMCPAddForm(editData) {
_mcpEditingId = editData?.id || null;
document.getElementById('mcp-add-form').style.display = 'block';
document.getElementById('mcp-form-title').textContent = _mcpEditingId ? 'MCP 서버 편집' : 'MCP 서버 추가';
document.getElementById('mcp-form-msg').textContent = '';
if (editData) {
const v = (id, val) => { const el = document.getElementById(id); if (el && val !== undefined) el.value = val; };
v('mcp-f-id', editData.id); v('mcp-f-name', editData.name);
document.getElementById('mcp-f-transport').value = editData.transport || 'stdio';
onMCPTransportChange();
v('mcp-f-command', editData.command || '');
v('mcp-f-args', (editData.args || []).join('\n'));
v('mcp-f-env', Object.entries(editData.env || {}).map(([k,v]) => `${k}=${v}`).join('\n'));
v('mcp-f-url', editData.url || '');
v('mcp-f-headers', Object.entries(editData.headers || {}).map(([k,v]) => `${k}: ${v}`).join('\n'));
const cb = document.getElementById('mcp-f-enabled');
if (cb) cb.checked = editData.enabled !== false;
} else {
['mcp-f-id','mcp-f-name','mcp-f-command','mcp-f-args','mcp-f-env','mcp-f-url','mcp-f-headers'].forEach(id => {
const el = document.getElementById(id); if (el) el.value = '';
});
document.getElementById('mcp-f-transport').value = 'stdio';
onMCPTransportChange();
const cb = document.getElementById('mcp-f-enabled'); if (cb) cb.checked = true;
}
document.getElementById('mcp-f-id').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function hideMCPAddForm() {
document.getElementById('mcp-add-form').style.display = 'none';
_mcpEditingId = null;
}
function onMCPTransportChange() {
const t = document.getElementById('mcp-f-transport')?.value;
document.getElementById('mcp-stdio-fields').style.display = t === 'stdio' ? 'block' : 'none';
document.getElementById('mcp-sse-fields').style.display = t === 'sse' ? 'block' : 'none';
}
function prefillMCPServer(presetKey) {
const p = MCP_PRESETS[presetKey];
if (!p) return;
showMCPAddForm();
const v = (id, val) => { const el = document.getElementById(id); if (el) el.value = val; };
v('mcp-f-id', presetKey);
v('mcp-f-name', p.name);
document.getElementById('mcp-f-transport').value = p.transport;
onMCPTransportChange();
v('mcp-f-command', p.command || '');
v('mcp-f-args', p.args || '');
v('mcp-f-env', p.env || '');
v('mcp-f-url', p.url || '');
setIntegTab('mcp');
}
async function saveMCPServer() {
const id = document.getElementById('mcp-f-id')?.value?.trim();
const name = document.getElementById('mcp-f-name')?.value?.trim();
const transport = document.getElementById('mcp-f-transport')?.value || 'stdio';
const enabled = document.getElementById('mcp-f-enabled')?.checked ?? true;
const msgEl = document.getElementById('mcp-form-msg');
if (!id || !name) { if (msgEl) msgEl.textContent = 'ID와 이름은 필수입니다.'; return; }
const cfg = { id, name, transport, enabled };
if (transport === 'stdio') {
cfg.command = document.getElementById('mcp-f-command')?.value?.trim() || '';
const argsRaw = document.getElementById('mcp-f-args')?.value || '';
cfg.args = argsRaw.split('\n').map(s => s.trim()).filter(Boolean);
const envRaw = document.getElementById('mcp-f-env')?.value || '';
cfg.env = {};
envRaw.split('\n').map(l => l.trim()).filter(Boolean).forEach(line => {
const eq = line.indexOf('='); if (eq > 0) cfg.env[line.slice(0,eq).trim()] = line.slice(eq+1);
});
} else {
cfg.url = document.getElementById('mcp-f-url')?.value?.trim() || '';
const headersRaw = document.getElementById('mcp-f-headers')?.value || '';
cfg.headers = {};
headersRaw.split('\n').map(l => l.trim()).filter(Boolean).forEach(line => {
const colon = line.indexOf(':'); if (colon > 0) cfg.headers[line.slice(0,colon).trim()] = line.slice(colon+1).trim();
});
}
try {
const r = await api('/api/mcp/servers', { method: 'POST', body: JSON.stringify(cfg) });
if (r.success) {
if (msgEl) msgEl.textContent = '? Saved';
setTimeout(() => hideMCPAddForm(), 600);
await loadMCPServers();
// Auto-connect if enabled
if (enabled) connectMCPServer(id);
} else {
if (msgEl) msgEl.textContent = '오류: ' + (r.error || '알 수 없음');
}
} catch(e) {
if (msgEl) msgEl.textContent = '저장 실패: ' + e.message;
}
}
async function editMCPServer(id) {
try {
const data = await api('/api/mcp/servers');
const server = (data.servers || []).find(s => s.id === id);
if (server) showMCPAddForm(server);
} catch(e) { console.warn('editMCPServer:', e); }
}
async function deleteMCPServer(id) {
if (!confirm(`Delete MCP server "${id}"?`)) return;
try {
await api(`/api/mcp/servers/${id}`, { method: 'DELETE' });
await loadMCPServers();
} catch(e) { showIntegMsg('Delete failed: ' + e.message, '#991b1b', '#fef2f2'); }
}
async function connectMCPServer(id) {
showIntegMsg('Connecting to ' + id + '…', '#1e40af', '#eff6ff');
try {
const r = await api(`/api/mcp/servers/${id}/connect`, { method: 'POST' });
if (r.success) {
showIntegMsg(`? Connected — ${(r.tools||[]).length} tool(s) available`, '#166534', '#f0fdf4');
} else {
showIntegMsg('? Connection failed: ' + (r.error || 'Unknown'), '#991b1b', '#fef2f2');
}
await loadMCPServers();
} catch(e) {
showIntegMsg('Connect error: ' + e.message, '#991b1b', '#fef2f2');
await loadMCPServers();
}
}
async function disconnectMCPServer(id) {
try {
await api(`/api/mcp/servers/${id}/disconnect`, { method: 'POST' });
await loadMCPServers();
showIntegMsg('Disconnected from ' + id, '#475569', '#f8fafc');
} catch(e) { showIntegMsg('Disconnect error: ' + e.message, '#991b1b', '#fef2f2'); }
}
// --- Shared helper ------------------------------------------------------------
function showIntegMsg(msg, color, bg) {
let el = document.getElementById('integ-toast');
if (!el) {
el = document.createElement('div');
el.id = 'integ-toast';
el.style.cssText = 'position:fixed;bottom:80px;right:24px;z-index:9999;padding:10px 16px;border-radius:10px;font-size:12px;font-weight:600;max-width:320px;box-shadow:0 4px 16px rgba(0,0,0,0.12);transition:opacity .3s';
document.body.appendChild(el);
}
el.style.background = bg || '#f0fdf4';
el.style.color = color || '#166534';
el.style.border = `1px solid ${color || '#166534'}33`;
el.style.opacity = '1';
el.textContent = msg;
clearTimeout(el._timeout);
el._timeout = setTimeout(() => { el.style.opacity = '0'; }, 3500);
}