mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-27 06:14:48 -06:00
feat(console): the MCP family onto the shelf — create/edit merge, detail, import, install
The four MCP surfaces leave their legacy overlays. The add/edit server editor collapses into ONE lg pane-scoped shelf (#mcp-shelf): a hidden mcp-edit-id decides POST vs PUT, and the title/tag/data-kind/submit-label flip between "Add MCP server"/MCP-NEW/create/Create and "Edit MCP server — name"/MCP-EDIT/ edit/Save. The transport-conditional stdio/http field groups and the OAuth subfield block toggle on the hidden attribute instead of style.display (which .hatch [hidden] enforces); the multitenant-auth segmented radio control carries over verbatim. The transport/auth onchange and submit move out of inline markup into _mcpWire (which also installs the audience-autofill listener once). mcp-import becomes a create shelf with the JSON paste as an sh-mono textarea; mcp-detail an inspect shelf carrying its setSafeHtml two-column population. The registry install flow moves onto the document-modal dialog tier per the confirm precedent: a STATIC #mcp-install-dialog whose summary/source/fields containers _showInstallMcpModal still populates — the dynamic overlay-shell construction is gone. _doRegistryInstall is shared by the one-click card path and the dialog submit, so its busy lock and inline-vs-toast error branch now key off the dialog's .open. Submits go busy via the LED lock; legacy trap/Escape/ID-list entries and the trap-handler lets are deleted.
This commit is contained in:
@@ -2633,10 +2633,6 @@ function _installTrap(overlayId, boxId, trapRef) {
|
||||
hideCreateTemplateModal();
|
||||
else if (overlayId === "edit-template-overlay") hideEditTemplateModal();
|
||||
else if (overlayId === "memory-detail-overlay") hideMemoryDetailModal();
|
||||
else if (overlayId === "mcp-create-overlay") hideCreateMcpModal();
|
||||
else if (overlayId === "mcp-import-overlay") hideImportMcpModal();
|
||||
else if (overlayId === "mcp-detail-overlay") hideMcpDetailModal();
|
||||
else if (overlayId === "mcp-install-overlay") hideInstallMcpModal();
|
||||
else if (overlayId === "github-import-overlay") hideGitHubImportModal();
|
||||
else if (overlayId === "create-ppolicy-overlay")
|
||||
hideCreatePromptPolicyModal();
|
||||
@@ -2678,10 +2674,6 @@ document.addEventListener("keydown", function (e) {
|
||||
["create-template-overlay", hideCreateTemplateModal],
|
||||
["edit-template-overlay", hideEditTemplateModal],
|
||||
["memory-detail-overlay", hideMemoryDetailModal],
|
||||
["mcp-install-overlay", hideInstallMcpModal],
|
||||
["mcp-detail-overlay", hideMcpDetailModal],
|
||||
["mcp-import-overlay", hideImportMcpModal],
|
||||
["mcp-create-overlay", hideCreateMcpModal],
|
||||
["github-import-overlay", hideGitHubImportModal],
|
||||
["create-ppolicy-overlay", hideCreatePromptPolicyModal],
|
||||
["edit-ppolicy-overlay", hideEditPromptPolicyModal],
|
||||
@@ -3623,14 +3615,13 @@ function _showModalError(el, msg) {
|
||||
/* ── MCP Servers tab ─────────────────────────────────────────────────────── */
|
||||
|
||||
let _mcpServers = [];
|
||||
let _mcpCreateTrap = null;
|
||||
let _mcpCreateTrigger = null;
|
||||
let _mcpImportTrap = null;
|
||||
let _mcpImportTrigger = null;
|
||||
let _mcpDetailTrap = null;
|
||||
let _mcpDetailTrigger = null;
|
||||
let _mcpInstallTrap = null;
|
||||
let _mcpInstallTrigger = null;
|
||||
let _mcpWired = false;
|
||||
let _mcpShelfHandle = null;
|
||||
let _mcpImportWired = false;
|
||||
let _mcpImportShelfHandle = null;
|
||||
let _mcpDetailShelfHandle = null;
|
||||
let _mcpInstallWired = false;
|
||||
let _mcpInstallHandle = null;
|
||||
let _mcpInstallServer = null;
|
||||
let _mcpCurrentView = "servers";
|
||||
let _registryResults = [];
|
||||
@@ -3997,10 +3988,8 @@ function _renderMcpServers(items) {
|
||||
|
||||
function toggleMcpTransport() {
|
||||
const v = document.getElementById("mcp-transport").value;
|
||||
document.getElementById("mcp-stdio-fields").style.display =
|
||||
v === "stdio" ? "" : "none";
|
||||
document.getElementById("mcp-http-fields").style.display =
|
||||
v === "streamable-http" ? "" : "none";
|
||||
document.getElementById("mcp-stdio-fields").hidden = v !== "stdio";
|
||||
document.getElementById("mcp-http-fields").hidden = v !== "streamable-http";
|
||||
// Re-evaluate auth-field visibility because the headers row lives
|
||||
// inside mcp-http-fields and gets toggled there.
|
||||
toggleMcpAuthFields();
|
||||
@@ -4018,7 +4007,7 @@ function toggleMcpAuthFields() {
|
||||
const authType = _selectedMcpAuthType();
|
||||
const oauthDiv = document.getElementById("mcp-oauth-fields");
|
||||
if (oauthDiv) {
|
||||
oauthDiv.style.display = authType === "oauth_user" ? "" : "none";
|
||||
oauthDiv.hidden = authType !== "oauth_user";
|
||||
}
|
||||
// The "Headers" textarea (inside mcp-http-fields) is only meaningful
|
||||
// for static auth; hide it for 'none' / 'oauth_user' so operators
|
||||
@@ -4027,8 +4016,8 @@ function toggleMcpAuthFields() {
|
||||
if (headersInput) {
|
||||
const headersLabel = document.querySelector('label[for="mcp-headers"]');
|
||||
const show = authType === "static";
|
||||
headersInput.style.display = show ? "" : "none";
|
||||
if (headersLabel) headersLabel.style.display = show ? "" : "none";
|
||||
headersInput.hidden = !show;
|
||||
if (headersLabel) headersLabel.hidden = !show;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4043,13 +4032,34 @@ function _wireMcpAudienceAutofill() {
|
||||
});
|
||||
}
|
||||
|
||||
function showCreateMcpModal() {
|
||||
_mcpCreateTrigger = document.activeElement;
|
||||
const ov = document.getElementById("mcp-create-overlay");
|
||||
ov.style.display = "flex";
|
||||
function _mcpWire() {
|
||||
if (_mcpWired) return;
|
||||
_mcpWired = true;
|
||||
document
|
||||
.getElementById("mcp-transport")
|
||||
.addEventListener("change", toggleMcpTransport);
|
||||
const authRadios = document.getElementsByName("mcp-auth-type");
|
||||
for (let i = 0; i < authRadios.length; i++) {
|
||||
authRadios[i].addEventListener("change", toggleMcpAuthFields);
|
||||
}
|
||||
document
|
||||
.getElementById("mcp-create-submit")
|
||||
.addEventListener("click", submitCreateMcp);
|
||||
_wireMcpAudienceAutofill();
|
||||
}
|
||||
|
||||
function _mcpOpen(title, tag, kind, submitLabel) {
|
||||
const shelf = document.getElementById("mcp-shelf");
|
||||
document.getElementById("mcp-shelf-title").textContent = title;
|
||||
document.getElementById("mcp-shelf-tag").textContent = tag;
|
||||
shelf.setAttribute("data-kind", kind);
|
||||
document.getElementById("mcp-create-submit").textContent = submitLabel;
|
||||
_mcpShelfHandle = window.TurnstoneHatch.openShelf(shelf);
|
||||
document.getElementById("mcp-name").focus();
|
||||
}
|
||||
|
||||
function _mcpResetForm() {
|
||||
document.getElementById("mcp-edit-id").value = "";
|
||||
document.getElementById("mcp-create-title").textContent = "Add MCP Server";
|
||||
document.getElementById("mcp-create-submit").textContent = "Create";
|
||||
document.getElementById("mcp-name").value = "";
|
||||
document.getElementById("mcp-transport").value = "stdio";
|
||||
document.getElementById("mcp-command").value = "";
|
||||
@@ -4072,12 +4082,16 @@ function showCreateMcpModal() {
|
||||
document.getElementById("mcp-create-error").classList.remove("is-visible");
|
||||
toggleMcpTransport();
|
||||
toggleMcpAuthFields();
|
||||
_wireMcpAudienceAutofill();
|
||||
document.getElementById("mcp-name").focus();
|
||||
_mcpCreateTrap = _installTrap("mcp-create-overlay", "mcp-create-box");
|
||||
}
|
||||
|
||||
function showCreateMcpModal() {
|
||||
_mcpWire();
|
||||
_mcpResetForm();
|
||||
_mcpOpen("Add MCP server", "MCP-NEW", "create", "Create");
|
||||
}
|
||||
|
||||
function showEditMcpModal(serverId) {
|
||||
_mcpWire();
|
||||
// Fetch with reveal=true to get actual secret values for editing
|
||||
authFetch("/v1/api/admin/mcp-servers/" + serverId + "?reveal=true")
|
||||
.then(function (r) {
|
||||
@@ -4085,11 +4099,8 @@ function showEditMcpModal(serverId) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function (s) {
|
||||
showCreateMcpModal();
|
||||
_mcpResetForm();
|
||||
document.getElementById("mcp-edit-id").value = serverId;
|
||||
document.getElementById("mcp-create-title").textContent =
|
||||
"Edit MCP Server";
|
||||
document.getElementById("mcp-create-submit").textContent = "Save";
|
||||
document.getElementById("mcp-name").value = s.name;
|
||||
document.getElementById("mcp-transport").value = s.transport;
|
||||
document.getElementById("mcp-command").value = s.command || "";
|
||||
@@ -4142,6 +4153,7 @@ function showEditMcpModal(serverId) {
|
||||
s.oauth_audience || "";
|
||||
toggleMcpTransport();
|
||||
toggleMcpAuthFields();
|
||||
_mcpOpen("Edit MCP server — " + s.name, "MCP-EDIT", "edit", "Save");
|
||||
})
|
||||
.catch(function () {
|
||||
showToast("Failed to load server details");
|
||||
@@ -4149,10 +4161,8 @@ function showEditMcpModal(serverId) {
|
||||
}
|
||||
|
||||
function hideCreateMcpModal() {
|
||||
document.getElementById("mcp-create-overlay").style.display = "none";
|
||||
_mcpCreateTrap = _removeTrap(_mcpCreateTrap);
|
||||
if (_mcpCreateTrigger && _mcpCreateTrigger.focus) _mcpCreateTrigger.focus();
|
||||
_mcpCreateTrigger = null;
|
||||
window.TurnstoneHatch.closeShelf(document.getElementById("mcp-shelf"));
|
||||
_mcpShelfHandle = null;
|
||||
}
|
||||
|
||||
function _parseMcpForm() {
|
||||
@@ -4239,11 +4249,12 @@ function _parseMcpForm() {
|
||||
}
|
||||
|
||||
function submitCreateMcp() {
|
||||
const shelf = document.getElementById("mcp-shelf");
|
||||
const errEl = document.getElementById("mcp-create-error");
|
||||
const form = _parseMcpForm();
|
||||
if (form.error) {
|
||||
const e = document.getElementById("mcp-create-error");
|
||||
e.textContent = form.error;
|
||||
e.classList.add("is-visible");
|
||||
errEl.textContent = form.error;
|
||||
errEl.classList.add("is-visible");
|
||||
return;
|
||||
}
|
||||
const editId = document.getElementById("mcp-edit-id").value;
|
||||
@@ -4252,7 +4263,8 @@ function submitCreateMcp() {
|
||||
? "/v1/api/admin/mcp-servers/" + editId
|
||||
: "/v1/api/admin/mcp-servers";
|
||||
|
||||
document.getElementById("mcp-create-submit").disabled = true;
|
||||
errEl.classList.remove("is-visible");
|
||||
window.TurnstoneHatch.setBusy(shelf, true);
|
||||
authFetch(url, {
|
||||
method: method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -4266,18 +4278,16 @@ function submitCreateMcp() {
|
||||
return r.json();
|
||||
})
|
||||
.then(function () {
|
||||
window.TurnstoneHatch.setBusy(shelf, false);
|
||||
hideCreateMcpModal();
|
||||
showToast(editId ? "Server updated" : "Server created");
|
||||
_flagMcpSyncPending();
|
||||
loadAdminMcp();
|
||||
})
|
||||
.catch(function (e) {
|
||||
const el = document.getElementById("mcp-create-error");
|
||||
el.textContent = e.message;
|
||||
el.classList.add("is-visible");
|
||||
})
|
||||
.finally(function () {
|
||||
document.getElementById("mcp-create-submit").disabled = false;
|
||||
window.TurnstoneHatch.setBusy(shelf, false);
|
||||
errEl.textContent = e.message;
|
||||
errEl.classList.add("is-visible");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4337,7 +4347,6 @@ function showMcpDetailModal(serverId) {
|
||||
|
||||
function _openMcpDetail(s) {
|
||||
if (!s) return;
|
||||
_mcpDetailTrigger = document.activeElement;
|
||||
|
||||
let html = '<div class="modal-columns">';
|
||||
html += '<div class="modal-col">';
|
||||
@@ -4441,31 +4450,36 @@ function _openMcpDetail(s) {
|
||||
|
||||
document.getElementById("mcp-detail-title").textContent = s.name;
|
||||
setSafeHtml(document.getElementById("mcp-detail-content"), html);
|
||||
document.getElementById("mcp-detail-overlay").style.display = "flex";
|
||||
_mcpDetailTrap = _installTrap("mcp-detail-overlay", "mcp-detail-box");
|
||||
_mcpDetailShelfHandle = window.TurnstoneHatch.openShelf(
|
||||
document.getElementById("mcp-detail-shelf"),
|
||||
);
|
||||
}
|
||||
|
||||
function hideMcpDetailModal() {
|
||||
document.getElementById("mcp-detail-overlay").style.display = "none";
|
||||
_mcpDetailTrap = _removeTrap(_mcpDetailTrap);
|
||||
if (_mcpDetailTrigger && _mcpDetailTrigger.focus) _mcpDetailTrigger.focus();
|
||||
_mcpDetailTrigger = null;
|
||||
window.TurnstoneHatch.closeShelf(document.getElementById("mcp-detail-shelf"));
|
||||
_mcpDetailShelfHandle = null;
|
||||
}
|
||||
|
||||
function _mcpImportWire() {
|
||||
if (_mcpImportWired) return;
|
||||
_mcpImportWired = true;
|
||||
document
|
||||
.getElementById("mcp-import-submit")
|
||||
.addEventListener("click", submitImportMcp);
|
||||
}
|
||||
|
||||
function showImportMcpModal() {
|
||||
_mcpImportTrigger = document.activeElement;
|
||||
document.getElementById("mcp-import-overlay").style.display = "flex";
|
||||
_mcpImportWire();
|
||||
const shelf = document.getElementById("mcp-import-shelf");
|
||||
document.getElementById("mcp-import-json").value = "";
|
||||
document.getElementById("mcp-import-error").classList.remove("is-visible");
|
||||
_mcpImportShelfHandle = window.TurnstoneHatch.openShelf(shelf);
|
||||
document.getElementById("mcp-import-json").focus();
|
||||
_mcpImportTrap = _installTrap("mcp-import-overlay", "mcp-import-box");
|
||||
}
|
||||
|
||||
function hideImportMcpModal() {
|
||||
document.getElementById("mcp-import-overlay").style.display = "none";
|
||||
_mcpImportTrap = _removeTrap(_mcpImportTrap);
|
||||
if (_mcpImportTrigger && _mcpImportTrigger.focus) _mcpImportTrigger.focus();
|
||||
_mcpImportTrigger = null;
|
||||
window.TurnstoneHatch.closeShelf(document.getElementById("mcp-import-shelf"));
|
||||
_mcpImportShelfHandle = null;
|
||||
}
|
||||
|
||||
function submitImportMcp() {
|
||||
@@ -4491,7 +4505,10 @@ function submitImportMcp() {
|
||||
e3.classList.add("is-visible");
|
||||
return;
|
||||
}
|
||||
document.getElementById("mcp-import-submit").disabled = true;
|
||||
const shelf = document.getElementById("mcp-import-shelf");
|
||||
const errEl = document.getElementById("mcp-import-error");
|
||||
errEl.classList.remove("is-visible");
|
||||
window.TurnstoneHatch.setBusy(shelf, true);
|
||||
authFetch("/v1/api/admin/mcp-servers/import", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -4505,6 +4522,7 @@ function submitImportMcp() {
|
||||
return r.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
window.TurnstoneHatch.setBusy(shelf, false);
|
||||
hideImportMcpModal();
|
||||
let msg = "Imported " + (data.imported || []).length;
|
||||
if ((data.skipped || []).length)
|
||||
@@ -4516,12 +4534,9 @@ function submitImportMcp() {
|
||||
loadAdminMcp();
|
||||
})
|
||||
.catch(function (e) {
|
||||
const el = document.getElementById("mcp-import-error");
|
||||
el.textContent = e.message;
|
||||
el.classList.add("is-visible");
|
||||
})
|
||||
.finally(function () {
|
||||
document.getElementById("mcp-import-submit").disabled = false;
|
||||
window.TurnstoneHatch.setBusy(shelf, false);
|
||||
errEl.textContent = e.message;
|
||||
errEl.classList.add("is-visible");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4795,10 +4810,16 @@ function _initiateRegistryInstall(srv) {
|
||||
_showInstallMcpModal(srv, hasRemote, hasPackage);
|
||||
}
|
||||
|
||||
function _mcpInstallWire() {
|
||||
if (_mcpInstallWired) return;
|
||||
_mcpInstallWired = true;
|
||||
document
|
||||
.getElementById("mcp-install-submit")
|
||||
.addEventListener("click", submitInstallMcp);
|
||||
}
|
||||
|
||||
function _showInstallMcpModal(srv, hasRemote, hasPackage) {
|
||||
_mcpInstallTrigger = document.activeElement;
|
||||
const ov = document.getElementById("mcp-install-overlay");
|
||||
ov.style.display = "flex";
|
||||
_mcpInstallWire();
|
||||
document.getElementById("mcp-install-error").classList.remove("is-visible");
|
||||
|
||||
// Summary
|
||||
@@ -4846,7 +4867,14 @@ function _showInstallMcpModal(srv, hasRemote, hasPackage) {
|
||||
}
|
||||
|
||||
_updateInstallFields();
|
||||
_mcpInstallTrap = _installTrap("mcp-install-overlay", "mcp-install-box");
|
||||
_mcpInstallHandle = window.TurnstoneHatch.openDialog(
|
||||
document.getElementById("mcp-install-dialog"),
|
||||
{
|
||||
onClose: function () {
|
||||
_mcpInstallServer = null;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function _updateInstallFields() {
|
||||
@@ -4995,12 +5023,8 @@ function _updateInstallFields() {
|
||||
}
|
||||
|
||||
function hideInstallMcpModal() {
|
||||
document.getElementById("mcp-install-overlay").style.display = "none";
|
||||
_mcpInstallTrap = _removeTrap(_mcpInstallTrap);
|
||||
if (_mcpInstallTrigger && _mcpInstallTrigger.focus)
|
||||
_mcpInstallTrigger.focus();
|
||||
_mcpInstallTrigger = null;
|
||||
_mcpInstallServer = null;
|
||||
const d = document.getElementById("mcp-install-dialog");
|
||||
if (d.open) d.close();
|
||||
}
|
||||
|
||||
function submitInstallMcp() {
|
||||
@@ -5035,8 +5059,10 @@ function _doRegistryInstall(
|
||||
env,
|
||||
headers,
|
||||
) {
|
||||
const submitBtn = document.getElementById("mcp-install-submit");
|
||||
if (submitBtn) submitBtn.disabled = true;
|
||||
// Two entry points share this: the one-click card path (no dialog) and the
|
||||
// install dialog's submit. The dialog's busy lock only applies when it's open.
|
||||
const dialog = document.getElementById("mcp-install-dialog");
|
||||
if (dialog.open) window.TurnstoneHatch.setBusy(dialog, true);
|
||||
|
||||
authFetch("/v1/api/admin/mcp-registry/install", {
|
||||
method: "POST",
|
||||
@@ -5058,10 +5084,8 @@ function _doRegistryInstall(
|
||||
return r.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
const overlay = document.getElementById("mcp-install-overlay");
|
||||
if (overlay && overlay.style.display !== "none") {
|
||||
hideInstallMcpModal();
|
||||
}
|
||||
window.TurnstoneHatch.setBusy(dialog, false);
|
||||
if (dialog.open) hideInstallMcpModal();
|
||||
const serverName = data.name || registryName;
|
||||
showToast("Installed " + serverName + " — connecting to nodes\u2026");
|
||||
// Re-search to update installed status
|
||||
@@ -5074,9 +5098,9 @@ function _doRegistryInstall(
|
||||
}
|
||||
})
|
||||
.catch(function (e) {
|
||||
const overlay = document.getElementById("mcp-install-overlay");
|
||||
window.TurnstoneHatch.setBusy(dialog, false);
|
||||
const errEl = document.getElementById("mcp-install-error");
|
||||
if (errEl && overlay && overlay.style.display !== "none") {
|
||||
if (errEl && dialog.open) {
|
||||
errEl.textContent = e.message;
|
||||
errEl.classList.add("is-visible");
|
||||
} else {
|
||||
@@ -5084,9 +5108,6 @@ function _doRegistryInstall(
|
||||
// Re-render to reset card button states
|
||||
_renderRegistryResults();
|
||||
}
|
||||
})
|
||||
.finally(function () {
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+322
-306
@@ -2712,6 +2712,292 @@
|
||||
<button class="sh-btn" data-close>Close</button>
|
||||
</footer>
|
||||
</dialog>
|
||||
|
||||
<!-- MCP server shelf (create + edit) — pane-scoped. Transport-
|
||||
conditional field groups toggle on the hidden attribute; the
|
||||
multitenant-auth fieldset carries over as-is. -->
|
||||
<dialog
|
||||
class="hatch hatch--shelf hatch--lg"
|
||||
id="mcp-shelf"
|
||||
data-kind="create"
|
||||
aria-labelledby="mcp-shelf-title"
|
||||
>
|
||||
<header class="sh-head">
|
||||
<span class="sh-led" aria-hidden="true"></span>
|
||||
<h2 class="sh-title" id="mcp-shelf-title">Add MCP server</h2>
|
||||
<span class="sh-tag" aria-hidden="true" id="mcp-shelf-tag"
|
||||
>MCP-NEW</span
|
||||
>
|
||||
<button class="sh-x" data-close aria-label="Close">✕</button>
|
||||
</header>
|
||||
<div class="sh-body">
|
||||
<div
|
||||
id="mcp-create-error"
|
||||
class="sh-alert"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
></div>
|
||||
<input type="hidden" id="mcp-edit-id" value="" />
|
||||
<label for="mcp-name">Server name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="mcp-name"
|
||||
class="sh-mono"
|
||||
placeholder="e.g. filesystem"
|
||||
maxlength="64"
|
||||
pattern="[a-zA-Z0-9._\-]+"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<label for="mcp-transport">Transport</label>
|
||||
<select id="mcp-transport">
|
||||
<option value="stdio">stdio</option>
|
||||
<option value="streamable-http">streamable-http</option>
|
||||
</select>
|
||||
<div id="mcp-stdio-fields">
|
||||
<label for="mcp-command">Command</label>
|
||||
<input
|
||||
type="text"
|
||||
id="mcp-command"
|
||||
class="sh-mono"
|
||||
placeholder="e.g. npx"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<label for="mcp-args"
|
||||
>Arguments <span class="label-hint">one per line</span></label
|
||||
>
|
||||
<textarea
|
||||
id="mcp-args"
|
||||
class="sh-mono"
|
||||
rows="3"
|
||||
placeholder="-y @modelcontextprotocol/server-filesystem /tmp"
|
||||
></textarea>
|
||||
<label for="mcp-env"
|
||||
>Environment variables
|
||||
<span class="label-hint">KEY=VALUE, one per line</span></label
|
||||
>
|
||||
<textarea
|
||||
id="mcp-env"
|
||||
class="sh-mono"
|
||||
rows="2"
|
||||
placeholder="API_KEY=..."
|
||||
></textarea>
|
||||
</div>
|
||||
<div id="mcp-http-fields" hidden>
|
||||
<label for="mcp-url">URL</label>
|
||||
<input
|
||||
type="text"
|
||||
id="mcp-url"
|
||||
class="sh-mono"
|
||||
placeholder="https://..."
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<label for="mcp-headers"
|
||||
>Headers
|
||||
<span class="label-hint"
|
||||
>KEY: VALUE, one per line</span
|
||||
></label
|
||||
>
|
||||
<textarea
|
||||
id="mcp-headers"
|
||||
class="sh-mono"
|
||||
rows="2"
|
||||
placeholder="Authorization: Bearer ..."
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="sh-section">Multitenant authorization</div>
|
||||
<div
|
||||
class="segmented-control"
|
||||
role="radiogroup"
|
||||
aria-label="Multitenant Authorization"
|
||||
>
|
||||
<label class="segmented-option">
|
||||
<input
|
||||
type="radio"
|
||||
name="mcp-auth-type"
|
||||
id="mcp-auth-none"
|
||||
value="none"
|
||||
/>
|
||||
<span class="segmented-indicator" aria-hidden="true"></span>
|
||||
<span class="segmented-text">No authorization</span>
|
||||
</label>
|
||||
<label class="segmented-option">
|
||||
<input
|
||||
type="radio"
|
||||
name="mcp-auth-type"
|
||||
id="mcp-auth-static"
|
||||
value="static"
|
||||
checked
|
||||
/>
|
||||
<span class="segmented-indicator" aria-hidden="true"></span>
|
||||
<span class="segmented-text"
|
||||
>Static headers
|
||||
<span class="segmented-hint"
|
||||
>single shared identity</span
|
||||
></span
|
||||
>
|
||||
</label>
|
||||
<label class="segmented-option">
|
||||
<input
|
||||
type="radio"
|
||||
name="mcp-auth-type"
|
||||
id="mcp-auth-oauth"
|
||||
value="oauth_user"
|
||||
/>
|
||||
<span class="segmented-indicator" aria-hidden="true"></span>
|
||||
<span class="segmented-text"
|
||||
>Per-user OAuth 2.1
|
||||
<span class="segmented-hint">recommended</span></span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div id="mcp-oauth-fields" hidden>
|
||||
<label for="mcp-oauth-as-url"
|
||||
>Authorization server URL
|
||||
<span class="label-hint"
|
||||
>optional — override discovery when the MCP server URL is
|
||||
not the OAuth issuer</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="mcp-oauth-as-url"
|
||||
class="sh-mono"
|
||||
placeholder="https://auth.example.com"
|
||||
/>
|
||||
<label for="mcp-oauth-registration">Client registration</label>
|
||||
<select id="mcp-oauth-registration">
|
||||
<option value="preregistered">preregistered</option>
|
||||
<option value="dcr">dcr (Dynamic Client Registration)</option>
|
||||
</select>
|
||||
<label for="mcp-oauth-client-id">Client ID</label>
|
||||
<input
|
||||
type="text"
|
||||
id="mcp-oauth-client-id"
|
||||
class="sh-mono"
|
||||
placeholder="(operator-issued client_id)"
|
||||
/>
|
||||
<label for="mcp-oauth-client-secret"
|
||||
>Client secret
|
||||
<span class="label-hint"
|
||||
>write-only, never displayed</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
id="mcp-oauth-client-secret"
|
||||
placeholder="***"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<label for="mcp-oauth-scopes"
|
||||
>Scopes <span class="label-hint">space-separated</span></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="mcp-oauth-scopes"
|
||||
class="sh-mono"
|
||||
placeholder="openid profile"
|
||||
/>
|
||||
<label for="mcp-oauth-audience"
|
||||
>Audience
|
||||
<span class="label-hint">auto-populated from URL</span></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="mcp-oauth-audience"
|
||||
class="sh-mono"
|
||||
placeholder="https://mcp.example.com"
|
||||
/>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="mcp-auto-approve" />
|
||||
<span class="toggle-track" aria-hidden="true"></span>
|
||||
<span class="toggle-label">Auto-approve tools</span>
|
||||
</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="mcp-enabled" checked />
|
||||
<span class="toggle-track" aria-hidden="true"></span>
|
||||
<span class="toggle-label">Enabled</span>
|
||||
</label>
|
||||
</div>
|
||||
<footer class="sh-foot">
|
||||
<div class="sh-foot-meta"></div>
|
||||
<button class="sh-btn" data-close>Cancel</button>
|
||||
<button id="mcp-create-submit" class="sh-btn sh-btn--primary">
|
||||
Create
|
||||
</button>
|
||||
</footer>
|
||||
</dialog>
|
||||
|
||||
<!-- MCP import shelf — pane-scoped JSON paste. -->
|
||||
<dialog
|
||||
class="hatch hatch--shelf"
|
||||
id="mcp-import-shelf"
|
||||
data-kind="create"
|
||||
aria-labelledby="mcp-import-shelf-title"
|
||||
>
|
||||
<header class="sh-head">
|
||||
<span class="sh-led" aria-hidden="true"></span>
|
||||
<h2 class="sh-title" id="mcp-import-shelf-title">
|
||||
Import MCP config
|
||||
</h2>
|
||||
<span class="sh-tag" aria-hidden="true">MCP-IMPORT</span>
|
||||
<button class="sh-x" data-close aria-label="Close">✕</button>
|
||||
</header>
|
||||
<div class="sh-body">
|
||||
<div
|
||||
id="mcp-import-error"
|
||||
class="sh-alert"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
></div>
|
||||
<label for="mcp-import-json">Paste JSON</label>
|
||||
<textarea
|
||||
id="mcp-import-json"
|
||||
class="sh-mono"
|
||||
rows="10"
|
||||
placeholder='{"mcpServers":{"filesystem":{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/tmp"]}}}'
|
||||
></textarea>
|
||||
<p class="sh-prose" style="margin-top: 10px">
|
||||
Paste a JSON object with a <code>mcpServers</code> key (Claude
|
||||
Desktop / VS Code / Cursor format). Existing servers with the
|
||||
same name will be skipped.
|
||||
</p>
|
||||
</div>
|
||||
<footer class="sh-foot">
|
||||
<div class="sh-foot-meta"></div>
|
||||
<button class="sh-btn" data-close>Cancel</button>
|
||||
<button id="mcp-import-submit" class="sh-btn sh-btn--primary">
|
||||
Import
|
||||
</button>
|
||||
</footer>
|
||||
</dialog>
|
||||
|
||||
<!-- MCP detail shelf — pane-scoped, read-only server info. -->
|
||||
<dialog
|
||||
class="hatch hatch--shelf hatch--lg"
|
||||
id="mcp-detail-shelf"
|
||||
data-kind="inspect"
|
||||
aria-labelledby="mcp-detail-title"
|
||||
>
|
||||
<header class="sh-head">
|
||||
<span class="sh-led" aria-hidden="true"></span>
|
||||
<h2 class="sh-title" id="mcp-detail-title">MCP server detail</h2>
|
||||
<span class="sh-tag" aria-hidden="true">MCP-INFO</span>
|
||||
<button class="sh-x" data-close aria-label="Close">✕</button>
|
||||
</header>
|
||||
<div class="sh-body">
|
||||
<div id="mcp-detail-content"></div>
|
||||
</div>
|
||||
<footer class="sh-foot">
|
||||
<div class="sh-foot-meta"></div>
|
||||
<button class="sh-btn" data-close>Close</button>
|
||||
</footer>
|
||||
</dialog>
|
||||
</div>
|
||||
<!-- /admin-layout -->
|
||||
</div>
|
||||
@@ -2890,6 +3176,42 @@
|
||||
</footer>
|
||||
</dialog>
|
||||
|
||||
<!-- Install MCP Server (document-modal) — the summary/source/fields
|
||||
containers are populated by _showInstallMcpModal from the registry
|
||||
entry; the dialog shell is static. -->
|
||||
<dialog
|
||||
class="hatch hatch--dialog"
|
||||
id="mcp-install-dialog"
|
||||
data-kind="create"
|
||||
aria-labelledby="mcp-install-title"
|
||||
>
|
||||
<header class="sh-head">
|
||||
<span class="sh-led" aria-hidden="true"></span>
|
||||
<h2 class="sh-title" id="mcp-install-title">Install MCP server</h2>
|
||||
<span class="sh-tag" aria-hidden="true">MCP-INSTALL</span>
|
||||
<button class="sh-x" data-close aria-label="Close">✕</button>
|
||||
</header>
|
||||
<div class="sh-body">
|
||||
<div
|
||||
id="mcp-install-error"
|
||||
class="sh-alert"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
></div>
|
||||
<div id="mcp-install-summary"></div>
|
||||
<div id="mcp-install-source-select"></div>
|
||||
<div id="mcp-install-fields"></div>
|
||||
</div>
|
||||
<footer class="sh-foot">
|
||||
<div class="sh-foot-meta"></div>
|
||||
<button class="sh-btn" data-close>Cancel</button>
|
||||
<button id="mcp-install-submit" class="sh-btn sh-btn--primary">
|
||||
Install
|
||||
</button>
|
||||
</footer>
|
||||
</dialog>
|
||||
|
||||
<!-- Create Role Modal -->
|
||||
<div
|
||||
id="create-role-overlay"
|
||||
@@ -4019,312 +4341,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="mcp-create-overlay"
|
||||
style="display: none"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="mcp-create-title"
|
||||
>
|
||||
<div id="mcp-create-box" class="admin-modal">
|
||||
<h2 id="mcp-create-title">Add MCP Server</h2>
|
||||
<div
|
||||
id="mcp-create-error"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
></div>
|
||||
<input type="hidden" id="mcp-edit-id" value="" />
|
||||
<label for="mcp-name">Server Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="mcp-name"
|
||||
placeholder="e.g. filesystem"
|
||||
maxlength="64"
|
||||
pattern="[a-zA-Z0-9._\-]+"
|
||||
/>
|
||||
<label for="mcp-transport">Transport</label>
|
||||
<select id="mcp-transport" onchange="toggleMcpTransport()">
|
||||
<option value="stdio">stdio</option>
|
||||
<option value="streamable-http">streamable-http</option>
|
||||
</select>
|
||||
<div id="mcp-stdio-fields">
|
||||
<label for="mcp-command">Command</label>
|
||||
<input type="text" id="mcp-command" placeholder="e.g. npx" />
|
||||
<label for="mcp-args"
|
||||
>Arguments
|
||||
<span style="font-weight: 400; text-transform: none"
|
||||
>(one per line)</span
|
||||
></label
|
||||
>
|
||||
<textarea
|
||||
id="mcp-args"
|
||||
rows="3"
|
||||
placeholder="-y @modelcontextprotocol/server-filesystem /tmp"
|
||||
></textarea>
|
||||
<label for="mcp-env"
|
||||
>Environment Variables
|
||||
<span style="font-weight: 400; text-transform: none"
|
||||
>(KEY=VALUE, one per line)</span
|
||||
></label
|
||||
>
|
||||
<textarea id="mcp-env" rows="2" placeholder="API_KEY=..."></textarea>
|
||||
</div>
|
||||
<div id="mcp-http-fields" style="display: none">
|
||||
<label for="mcp-url">URL</label>
|
||||
<input type="text" id="mcp-url" placeholder="https://..." />
|
||||
<label for="mcp-headers"
|
||||
>Headers
|
||||
<span style="font-weight: 400; text-transform: none"
|
||||
>(KEY: VALUE, one per line)</span
|
||||
></label
|
||||
>
|
||||
<textarea
|
||||
id="mcp-headers"
|
||||
rows="2"
|
||||
placeholder="Authorization: Bearer ..."
|
||||
></textarea>
|
||||
</div>
|
||||
<fieldset
|
||||
id="mcp-auth-section"
|
||||
style="
|
||||
border: 1px solid var(--border);
|
||||
padding: 10px 12px;
|
||||
margin-top: 12px;
|
||||
"
|
||||
>
|
||||
<legend style="font-size: 12px; padding: 0 6px">
|
||||
Multitenant Authorization
|
||||
</legend>
|
||||
<div
|
||||
class="segmented-control"
|
||||
role="radiogroup"
|
||||
aria-label="Multitenant Authorization"
|
||||
>
|
||||
<label class="segmented-option">
|
||||
<input
|
||||
type="radio"
|
||||
name="mcp-auth-type"
|
||||
id="mcp-auth-none"
|
||||
value="none"
|
||||
onchange="toggleMcpAuthFields()"
|
||||
/>
|
||||
<span class="segmented-indicator" aria-hidden="true"></span>
|
||||
<span class="segmented-text">No authorization</span>
|
||||
</label>
|
||||
<label class="segmented-option">
|
||||
<input
|
||||
type="radio"
|
||||
name="mcp-auth-type"
|
||||
id="mcp-auth-static"
|
||||
value="static"
|
||||
onchange="toggleMcpAuthFields()"
|
||||
checked
|
||||
/>
|
||||
<span class="segmented-indicator" aria-hidden="true"></span>
|
||||
<span class="segmented-text"
|
||||
>Static headers
|
||||
<span class="segmented-hint">single shared identity</span></span
|
||||
>
|
||||
</label>
|
||||
<label class="segmented-option">
|
||||
<input
|
||||
type="radio"
|
||||
name="mcp-auth-type"
|
||||
id="mcp-auth-oauth"
|
||||
value="oauth_user"
|
||||
onchange="toggleMcpAuthFields()"
|
||||
/>
|
||||
<span class="segmented-indicator" aria-hidden="true"></span>
|
||||
<span class="segmented-text"
|
||||
>Per-user OAuth 2.1
|
||||
<span class="segmented-hint">recommended</span></span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div id="mcp-oauth-fields" style="display: none; margin-top: 8px">
|
||||
<label for="mcp-oauth-as-url"
|
||||
>Authorization Server URL
|
||||
<span style="font-weight: 400; text-transform: none"
|
||||
>(optional — override discovery when MCP server URL is not the
|
||||
OAuth issuer)</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="mcp-oauth-as-url"
|
||||
placeholder="https://auth.example.com"
|
||||
/>
|
||||
<label for="mcp-oauth-registration">Client Registration</label>
|
||||
<select id="mcp-oauth-registration">
|
||||
<option value="preregistered">preregistered</option>
|
||||
<option value="dcr">dcr (Dynamic Client Registration)</option>
|
||||
</select>
|
||||
<label for="mcp-oauth-client-id">Client ID</label>
|
||||
<input
|
||||
type="text"
|
||||
id="mcp-oauth-client-id"
|
||||
placeholder="(operator-issued client_id)"
|
||||
/>
|
||||
<label for="mcp-oauth-client-secret"
|
||||
>Client Secret
|
||||
<span style="font-weight: 400; text-transform: none"
|
||||
>(write-only, never displayed)</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
id="mcp-oauth-client-secret"
|
||||
placeholder="***"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<label for="mcp-oauth-scopes"
|
||||
>Scopes
|
||||
<span style="font-weight: 400; text-transform: none"
|
||||
>(space-separated)</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="mcp-oauth-scopes"
|
||||
placeholder="openid profile"
|
||||
/>
|
||||
<label for="mcp-oauth-audience"
|
||||
>Audience
|
||||
<span style="font-weight: 400; text-transform: none"
|
||||
>(auto-populated from URL)</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="mcp-oauth-audience"
|
||||
placeholder="https://mcp.example.com"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="toggle-stack">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="mcp-auto-approve" />
|
||||
<span class="toggle-track" aria-hidden="true"></span>
|
||||
<span class="toggle-label">Auto-approve tools</span>
|
||||
</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="mcp-enabled" checked />
|
||||
<span class="toggle-track" aria-hidden="true"></span>
|
||||
<span class="toggle-label">Enabled</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-buttons">
|
||||
<button class="modal-cancel" onclick="hideCreateMcpModal()">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
id="mcp-create-submit"
|
||||
class="modal-submit"
|
||||
onclick="submitCreateMcp()"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="mcp-import-overlay"
|
||||
style="display: none"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="mcp-import-title"
|
||||
>
|
||||
<div id="mcp-import-box" class="admin-modal">
|
||||
<h2 id="mcp-import-title">Import MCP Config</h2>
|
||||
<div
|
||||
id="mcp-import-error"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
></div>
|
||||
<label for="mcp-import-json">Paste JSON</label>
|
||||
<textarea
|
||||
id="mcp-import-json"
|
||||
rows="10"
|
||||
placeholder='{"mcpServers":{"filesystem":{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/tmp"]}}}'
|
||||
style="font-family: var(--font-mono); font-size: 11px"
|
||||
></textarea>
|
||||
<p style="font-size: 11px; color: var(--fg-dim); margin-top: 8px">
|
||||
Paste a JSON object with a <code>mcpServers</code> key (Claude Desktop
|
||||
/ VS Code / Cursor format). Existing servers with the same name will
|
||||
be skipped.
|
||||
</p>
|
||||
<div class="modal-buttons">
|
||||
<button class="modal-cancel" onclick="hideImportMcpModal()">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
id="mcp-import-submit"
|
||||
class="modal-submit"
|
||||
onclick="submitImportMcp()"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="mcp-detail-overlay"
|
||||
style="display: none"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="mcp-detail-title"
|
||||
>
|
||||
<div
|
||||
id="mcp-detail-box"
|
||||
class="admin-modal admin-modal-wide mcp-detail-modal"
|
||||
>
|
||||
<h2 id="mcp-detail-title">MCP Server Detail</h2>
|
||||
<div id="mcp-detail-content"></div>
|
||||
<div class="modal-buttons">
|
||||
<button class="modal-cancel" onclick="hideMcpDetailModal()">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="mcp-install-overlay"
|
||||
style="display: none"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="mcp-install-title"
|
||||
>
|
||||
<div id="mcp-install-box" class="admin-modal">
|
||||
<h2 id="mcp-install-title">Install MCP Server</h2>
|
||||
<div
|
||||
id="mcp-install-error"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
></div>
|
||||
<div id="mcp-install-summary"></div>
|
||||
<div id="mcp-install-source-select"></div>
|
||||
<div id="mcp-install-fields"></div>
|
||||
<div class="modal-buttons">
|
||||
<button class="modal-cancel" onclick="hideInstallMcpModal()">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
id="mcp-install-submit"
|
||||
class="modal-submit"
|
||||
onclick="submitInstallMcp()"
|
||||
>
|
||||
Install
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete coordinators confirmation modal (batch) -->
|
||||
<div
|
||||
id="coord-delete-overlay"
|
||||
|
||||
@@ -1780,10 +1780,6 @@ textarea.skill-content-area {
|
||||
#create-template-overlay,
|
||||
#edit-template-overlay,
|
||||
#memory-detail-overlay,
|
||||
#mcp-create-overlay,
|
||||
#mcp-import-overlay,
|
||||
#mcp-detail-overlay,
|
||||
#mcp-install-overlay,
|
||||
#github-import-overlay,
|
||||
#create-hr-overlay,
|
||||
#edit-hr-overlay,
|
||||
|
||||
Reference in New Issue
Block a user