"use strict"; (function () { const modal = document.getElementById("mcp-modal"); const openBtn = document.getElementById("mcp-settings-btn"); const closeBtn = document.getElementById("mcp-modal-close"); const addBtn = document.getElementById("mcp-add-btn"); const saveBtn = document.getElementById("mcp-save-btn"); const serversEl = document.getElementById("mcp-servers"); let currentConfig = { mcpServers: {} }; function csrfHeaders(headers = {}) { const token = document.cookie.split("; ").find((row) => row.startsWith("ollama_web_csrf=")); if (!token) return headers; return { ...headers, "X-CSRF-Token": decodeURIComponent(token.split("=")[1]) }; } async function csrfFetch(url, options = {}) { const method = (options.method || "GET").toUpperCase(); const headers = ["POST", "PUT", "PATCH", "DELETE"].includes(method) ? csrfHeaders(options.headers || {}) : options.headers; const res = await fetch(url, { ...options, headers }); if (res.status === 401) { window.location.assign(`/login?next=${encodeURIComponent(window.location.pathname)}`); } return res; } function createTextarea(value, placeholder, rows) { const el = document.createElement("textarea"); el.rows = rows || 2; el.placeholder = placeholder || ""; el.value = value || ""; return el; } function createInput(value, placeholder, type) { const el = document.createElement("input"); el.type = type || "text"; el.placeholder = placeholder || ""; el.value = value || ""; return el; } function renderServer(name, server) { const card = document.createElement("div"); card.className = "mcp-server-card"; const header = document.createElement("div"); header.className = "mcp-server-header"; const nameInput = createInput(name, "サーバー名", "text"); nameInput.className = "mcp-server-name"; const delBtn = document.createElement("button"); delBtn.type = "button"; delBtn.className = "mcp-server-delete"; delBtn.textContent = "削除"; delBtn.addEventListener("click", () => card.remove()); header.append(nameInput, delBtn); const typeSelect = document.createElement("select"); typeSelect.className = "mcp-server-type"; typeSelect.innerHTML = ` `; const isHttp = "url" in server; typeSelect.value = isHttp ? "http" : "stdio"; const fieldsEl = document.createElement("div"); fieldsEl.className = "mcp-server-fields"; function wrapField(label, control, required) { const wrap = document.createElement("label"); wrap.className = "mcp-field"; const span = document.createElement("span"); span.textContent = `${label} ${required ? "(必須)" : "(任意)"}`; wrap.append(span, control); return wrap; } function renderFields() { fieldsEl.innerHTML = ""; const type = typeSelect.value; if (type === "stdio") { const command = createInput(server.command || "", "例: npx", "text"); command.dataset.key = "command"; const args = createTextarea( (server.args || []).join("\n"), "例: -y @modelcontextprotocol/server-filesystem /tmp", 3 ); args.dataset.key = "args"; const env = createTextarea( Object.entries(server.env || {}) .map(([k, v]) => `${k}=${v}`) .join("\n"), "例: API_KEY=sk-xxx", 3 ); env.dataset.key = "env"; const cwd = createInput(server.cwd || "", "例: C:\\Users\\Home", "text"); cwd.dataset.key = "cwd"; fieldsEl.append( wrapField("command", command, true), wrapField("args", args, false), wrapField("env", env, false), wrapField("cwd", cwd, false) ); } else { const url = createInput(server.url || "", "例: http://127.0.0.1:9000/mcp", "text"); url.dataset.key = "url"; const headers = createTextarea( Object.entries(server.headers || {}) .map(([k, v]) => `${k}=${v}`) .join("\n"), "例: Authorization=Bearer xxx", 3 ); headers.dataset.key = "headers"; const timeout = createInput( server.timeout !== undefined ? String(server.timeout) : "", "例: 30", "number" ); timeout.dataset.key = "timeout"; fieldsEl.append( wrapField("url", url, true), wrapField("headers", headers, false), wrapField("timeout", timeout, false) ); } } typeSelect.addEventListener("change", () => { server = isHttp ? { url: "" } : { command: "" }; renderFields(); }); card.append(header, typeSelect, fieldsEl); renderFields(); return card; } function parseServer(card) { const name = card.querySelector(".mcp-server-name").value.trim(); if (!name) return null; const type = card.querySelector(".mcp-server-type").value; const fields = card.querySelectorAll(".mcp-server-fields [data-key]"); const server = {}; for (const field of fields) { const key = field.dataset.key; const value = field.value.trim(); if (!value) continue; if (type === "stdio") { if (key === "command") server.command = value; if (key === "args") server.args = value.split("\n").map((s) => s.trim()).filter(Boolean); if (key === "env") { server.env = {}; for (const line of value.split("\n")) { const idx = line.indexOf("="); if (idx > 0) server.env[line.slice(0, idx).trim()] = line.slice(idx + 1).trim(); } } if (key === "cwd") server.cwd = value; } else { if (key === "url") server.url = value; if (key === "headers") { server.headers = {}; for (const line of value.split("\n")) { const idx = line.indexOf("="); if (idx > 0) server.headers[line.slice(0, idx).trim()] = line.slice(idx + 1).trim(); } } if (key === "timeout") server.timeout = parseFloat(value); } } return { name, server }; } async function loadConfig() { try { const res = await fetch("/api/mcp/servers"); if (!res.ok) throw new Error(res.statusText); currentConfig = await res.json(); } catch (err) { console.error("failed to load MCP config", err); currentConfig = { mcpServers: {} }; } render(); } function render() { serversEl.innerHTML = ""; const servers = currentConfig.mcpServers || {}; for (const [name, server] of Object.entries(servers)) { if (server && typeof server === "object") { serversEl.appendChild(renderServer(name, server)); } } } function openModal() { loadConfig(); modal.classList.add("open"); } function closeModal() { modal.classList.remove("open"); } openBtn.addEventListener("click", openModal); closeBtn.addEventListener("click", closeModal); modal.addEventListener("click", (e) => { if (e.target === modal) closeModal(); }); addBtn.addEventListener("click", () => { const card = renderServer("new_server", { command: "" }); serversEl.appendChild(card); }); saveBtn.addEventListener("click", async () => { const cards = serversEl.querySelectorAll(".mcp-server-card"); const servers = {}; for (const card of cards) { const parsed = parseServer(card); if (parsed) servers[parsed.name] = parsed.server; } const payload = { mcpServers: servers }; try { const res = await csrfFetch("/api/mcp/servers", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) { const data = await res.json().catch(() => ({})); alert(`保存に失敗しました: ${(data.error || []).join(" / ") || res.statusText}`); return; } currentConfig = await res.json(); alert("MCP設定を保存しました"); closeModal(); } catch (err) { console.error("failed to save MCP config", err); alert("MCP設定の保存に失敗しました"); } }); loadConfig(); })();