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:
Patrick Buckley
2026-06-09 23:05:48 -07:00
parent 4adf223dd9
commit f2d76bbec6
3 changed files with 435 additions and 402 deletions
+113 -92
View File
@@ -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
View File
@@ -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&#10;@modelcontextprotocol/server-filesystem&#10;/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&#10;@modelcontextprotocol/server-filesystem&#10;/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"
-4
View File
@@ -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,