summaryrefslogtreecommitdiff
path: root/src/ollama_web/static/mcp.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/ollama_web/static/mcp.js')
-rw-r--r--src/ollama_web/static/mcp.js263
1 files changed, 263 insertions, 0 deletions
diff --git a/src/ollama_web/static/mcp.js b/src/ollama_web/static/mcp.js
new file mode 100644
index 0000000..ff3a27b
--- /dev/null
+++ b/src/ollama_web/static/mcp.js
@@ -0,0 +1,263 @@
+"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 = `
+ <option value="stdio">stdio</option>
+ <option value="http">HTTP Stream</option>
+ `;
+ 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();
+})(); \ No newline at end of file