diff --git a/src/channels/web/static/app.js b/src/channels/web/static/app.js
index bdf8b454..57ba3947 100644
--- a/src/channels/web/static/app.js
+++ b/src/channels/web/static/app.js
@@ -1204,15 +1204,18 @@ function loadServerLogLevel() {
function loadExtensions() {
const extList = document.getElementById('extensions-list');
+ const wasmList = document.getElementById('available-wasm-list');
+ const mcpList = document.getElementById('mcp-servers-list');
const toolsTbody = document.getElementById('tools-tbody');
const toolsEmpty = document.getElementById('tools-empty');
- // Fetch both in parallel
+ // Fetch all three in parallel
Promise.all([
apiFetch('/api/extensions').catch(() => ({ extensions: [] })),
apiFetch('/api/extensions/tools').catch(() => ({ tools: [] })),
- ]).then(([extData, toolData]) => {
- // Render extensions
+ apiFetch('/api/extensions/registry').catch(function(err) { console.warn('registry fetch failed:', err); return { entries: [] }; }),
+ ]).then(([extData, toolData, registryData]) => {
+ // Render installed extensions
if (extData.extensions.length === 0) {
extList.innerHTML = '
No extensions installed
';
} else {
@@ -1222,6 +1225,31 @@ function loadExtensions() {
}
}
+ // Split registry entries by kind
+ var wasmEntries = registryData.entries.filter(function(e) { return e.kind !== 'mcp_server' && !e.installed; });
+ var mcpEntries = registryData.entries.filter(function(e) { return e.kind === 'mcp_server'; });
+
+ // Available WASM extensions
+ if (wasmEntries.length === 0) {
+ wasmList.innerHTML = 'No additional WASM extensions available
';
+ } else {
+ wasmList.innerHTML = '';
+ for (const entry of wasmEntries) {
+ wasmList.appendChild(renderAvailableExtensionCard(entry));
+ }
+ }
+
+ // MCP servers (show both installed and uninstalled)
+ if (mcpEntries.length === 0) {
+ mcpList.innerHTML = 'No MCP servers available
';
+ } else {
+ mcpList.innerHTML = '';
+ for (const entry of mcpEntries) {
+ var installedExt = extData.extensions.find(function(e) { return e.name === entry.name; });
+ mcpList.appendChild(renderMcpServerCard(entry, installedExt));
+ }
+ }
+
// Render tools
if (toolData.tools.length === 0) {
toolsTbody.innerHTML = '';
@@ -1235,6 +1263,148 @@ function loadExtensions() {
});
}
+function renderAvailableExtensionCard(entry) {
+ const card = document.createElement('div');
+ card.className = 'ext-card ext-available';
+
+ const header = document.createElement('div');
+ header.className = 'ext-header';
+
+ const name = document.createElement('span');
+ name.className = 'ext-name';
+ name.textContent = entry.display_name;
+ header.appendChild(name);
+
+ const kind = document.createElement('span');
+ kind.className = 'ext-kind kind-' + entry.kind;
+ kind.textContent = entry.kind;
+ header.appendChild(kind);
+
+ card.appendChild(header);
+
+ const desc = document.createElement('div');
+ desc.className = 'ext-desc';
+ desc.textContent = entry.description;
+ card.appendChild(desc);
+
+ if (entry.keywords && entry.keywords.length > 0) {
+ const kw = document.createElement('div');
+ kw.className = 'ext-keywords';
+ kw.textContent = entry.keywords.join(', ');
+ card.appendChild(kw);
+ }
+
+ const actions = document.createElement('div');
+ actions.className = 'ext-actions';
+
+ const installBtn = document.createElement('button');
+ installBtn.className = 'btn-ext install';
+ installBtn.textContent = 'Install';
+ installBtn.addEventListener('click', function() {
+ installBtn.disabled = true;
+ installBtn.textContent = 'Installing...';
+ apiFetch('/api/extensions/install', {
+ method: 'POST',
+ body: { name: entry.name, kind: entry.kind },
+ }).then(function(res) {
+ if (res.success) {
+ showToast('Installed ' + entry.display_name, 'success');
+ } else {
+ showToast('Install: ' + (res.message || 'unknown error'), 'error');
+ }
+ loadExtensions();
+ }).catch(function(err) {
+ showToast('Install failed: ' + err.message, 'error');
+ loadExtensions();
+ });
+ });
+ actions.appendChild(installBtn);
+
+ card.appendChild(actions);
+ return card;
+}
+
+function renderMcpServerCard(entry, installedExt) {
+ var card = document.createElement('div');
+ card.className = 'ext-card' + (installedExt ? '' : ' ext-available');
+
+ var header = document.createElement('div');
+ header.className = 'ext-header';
+
+ var name = document.createElement('span');
+ name.className = 'ext-name';
+ name.textContent = entry.display_name;
+ header.appendChild(name);
+
+ var kind = document.createElement('span');
+ kind.className = 'ext-kind kind-mcp_server';
+ kind.textContent = 'mcp_server';
+ header.appendChild(kind);
+
+ if (installedExt) {
+ var authDot = document.createElement('span');
+ authDot.className = 'ext-auth-dot ' + (installedExt.authenticated ? 'authed' : 'unauthed');
+ authDot.title = installedExt.authenticated ? 'Authenticated' : 'Not authenticated';
+ header.appendChild(authDot);
+ }
+
+ card.appendChild(header);
+
+ var desc = document.createElement('div');
+ desc.className = 'ext-desc';
+ desc.textContent = entry.description;
+ card.appendChild(desc);
+
+ var actions = document.createElement('div');
+ actions.className = 'ext-actions';
+
+ if (installedExt) {
+ if (!installedExt.active) {
+ var activateBtn = document.createElement('button');
+ activateBtn.className = 'btn-ext activate';
+ activateBtn.textContent = 'Activate';
+ activateBtn.addEventListener('click', function() { activateExtension(installedExt.name); });
+ actions.appendChild(activateBtn);
+ } else {
+ var activeLabel = document.createElement('span');
+ activeLabel.className = 'ext-active-label';
+ activeLabel.textContent = 'Active';
+ actions.appendChild(activeLabel);
+ }
+ var removeBtn = document.createElement('button');
+ removeBtn.className = 'btn-ext remove';
+ removeBtn.textContent = 'Remove';
+ removeBtn.addEventListener('click', function() { removeExtension(installedExt.name); });
+ actions.appendChild(removeBtn);
+ } else {
+ var installBtn = document.createElement('button');
+ installBtn.className = 'btn-ext install';
+ installBtn.textContent = 'Install';
+ installBtn.addEventListener('click', function() {
+ installBtn.disabled = true;
+ installBtn.textContent = 'Installing...';
+ apiFetch('/api/extensions/install', {
+ method: 'POST',
+ body: { name: entry.name, kind: entry.kind },
+ }).then(function(res) {
+ if (res.success) {
+ showToast('Installed ' + entry.display_name, 'success');
+ } else {
+ showToast('Install: ' + (res.message || 'unknown error'), 'error');
+ }
+ loadExtensions();
+ }).catch(function(err) {
+ showToast('Install failed: ' + err.message, 'error');
+ loadExtensions();
+ });
+ });
+ actions.appendChild(installBtn);
+ }
+
+ card.appendChild(actions);
+ return card;
+}
+
function renderExtensionCard(ext) {
const card = document.createElement('div');
card.className = 'ext-card';
@@ -2195,32 +2365,64 @@ document.getElementById('tee-shield').addEventListener('mouseleave', function()
// --- Extension install ---
-function installExtension() {
- const name = document.getElementById('ext-install-name').value.trim();
+function installWasmExtension() {
+ var name = document.getElementById('wasm-install-name').value.trim();
if (!name) {
showToast('Extension name is required', 'error');
return;
}
- const url = document.getElementById('ext-install-url').value.trim();
- const kind = document.getElementById('ext-install-kind').value;
+ var url = document.getElementById('wasm-install-url').value.trim();
+ if (!url) {
+ showToast('URL to .tar.gz bundle is required', 'error');
+ return;
+ }
apiFetch('/api/extensions/install', {
method: 'POST',
- body: { name, url: url || undefined, kind },
- }).then((res) => {
+ body: { name: name, url: url, kind: 'wasm_tool' },
+ }).then(function(res) {
if (res.success) {
showToast('Installed ' + name, 'success');
- document.getElementById('ext-install-name').value = '';
- document.getElementById('ext-install-url').value = '';
+ document.getElementById('wasm-install-name').value = '';
+ document.getElementById('wasm-install-url').value = '';
loadExtensions();
} else {
showToast('Install failed: ' + (res.message || 'unknown error'), 'error');
}
- }).catch((err) => {
+ }).catch(function(err) {
showToast('Install failed: ' + err.message, 'error');
});
}
+function addMcpServer() {
+ var name = document.getElementById('mcp-install-name').value.trim();
+ if (!name) {
+ showToast('Server name is required', 'error');
+ return;
+ }
+ var url = document.getElementById('mcp-install-url').value.trim();
+ if (!url) {
+ showToast('MCP server URL is required', 'error');
+ return;
+ }
+
+ apiFetch('/api/extensions/install', {
+ method: 'POST',
+ body: { name: name, url: url, kind: 'mcp_server' },
+ }).then(function(res) {
+ if (res.success) {
+ showToast('Added MCP server ' + name, 'success');
+ document.getElementById('mcp-install-name').value = '';
+ document.getElementById('mcp-install-url').value = '';
+ loadExtensions();
+ } else {
+ showToast('Failed to add MCP server: ' + (res.message || 'unknown error'), 'error');
+ }
+ }).catch(function(err) {
+ showToast('Failed to add MCP server: ' + err.message, 'error');
+ });
+}
+
// --- Keyboard shortcuts ---
document.addEventListener('keydown', (e) => {
@@ -2228,10 +2430,10 @@ document.addEventListener('keydown', (e) => {
const tag = (e.target.tagName || '').toLowerCase();
const inInput = tag === 'input' || tag === 'textarea';
- // Mod+1-6: switch tabs
- if (mod && e.key >= '1' && e.key <= '6') {
+ // Mod+1-5: switch tabs
+ if (mod && e.key >= '1' && e.key <= '5') {
e.preventDefault();
- const tabs = ['chat', 'memory', 'jobs', 'routines', 'logs', 'extensions'];
+ const tabs = ['chat', 'memory', 'jobs', 'routines', 'extensions'];
const idx = parseInt(e.key) - 1;
if (tabs[idx]) switchTab(tabs[idx]);
return;
diff --git a/src/channels/web/static/index.html b/src/channels/web/static/index.html
index d7f65025..0c13974d 100644
--- a/src/channels/web/static/index.html
+++ b/src/channels/web/static/index.html
@@ -4,6 +4,9 @@
IronClaw
+
+
+