"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, t("mcp.server_name"), "text");
nameInput.className = "mcp-server-name";
const delBtn = document.createElement("button");
delBtn.type = "button";
delBtn.className = "mcp-server-delete";
delBtn.textContent = t("common.delete");
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 ? t("mcp.required") : t("mcp.optional")}`;
wrap.append(span, control);
return wrap;
}
function renderFields() {
fieldsEl.innerHTML = "";
const type = typeSelect.value;
if (type === "stdio") {
const command = createInput(server.command || "", t("mcp.examples.command"), "text");
command.dataset.key = "command";
const args = createTextarea(
(server.args || []).join("\n"),
t("mcp.examples.args"),
3
);
args.dataset.key = "args";
const env = createTextarea(
Object.entries(server.env || {})
.map(([k, v]) => `${k}=${v}`)
.join("\n"),
t("mcp.examples.env"),
3
);
env.dataset.key = "env";
const cwd = createInput(server.cwd || "", t("mcp.examples.cwd"), "text");
cwd.dataset.key = "cwd";
fieldsEl.append(
wrapField(t("mcp.command"), command, true),
wrapField(t("mcp.args"), args, false),
wrapField(t("mcp.env"), env, false),
wrapField(t("mcp.cwd"), cwd, false)
);
} else {
const url = createInput(server.url || "", t("mcp.examples.url"), "text");
url.dataset.key = "url";
const headers = createTextarea(
Object.entries(server.headers || {})
.map(([k, v]) => `${k}=${v}`)
.join("\n"),
t("mcp.examples.headers"),
3
);
headers.dataset.key = "headers";
const timeout = createInput(
server.timeout !== undefined ? String(server.timeout) : "",
t("mcp.examples.timeout"),
"number"
);
timeout.dataset.key = "timeout";
fieldsEl.append(
wrapField(t("mcp.url"), url, true),
wrapField(t("mcp.headers"), headers, false),
wrapField(t("mcp.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(() => ({}));
const errorText = (data.error || []).join(" / ") || res.statusText;
alert(t("mcp_messages.save_failed", "Failed to save: {error}").replace("{error}", errorText));
return;
}
currentConfig = await res.json();
alert(t("mcp_messages.saved"));
closeModal();
} catch (err) {
console.error("failed to save MCP config", err);
alert(t("mcp_messages.save_error"));
}
});
loadConfig();
})();