diff --git a/README.md b/README.md
index 1d03dbd..92577e4 100755
--- a/README.md
+++ b/README.md
@@ -214,6 +214,57 @@ All paths are validated against the whitelisted roots (directory-traversal and
symlink-escape attempts are rejected), and uploads stream to disk with a size cap
(`MCPPROXY_MAX_UPLOAD_BYTES`, default 50 MB).
+### Provider status badges
+
+While background setup is running, the left-panel provider list shows live badges:
+
+| Badge | Meaning |
+|---|---|
+| **⏳ initializing** (yellow) | Provider's dependencies are still installing — tools are advertised but return a retry directive until setup finishes. Badge disappears automatically once the provider is ready, however long that takes. |
+| **✗ setup failed** (red, with tooltip) | An actual error occurred during setup — hover the badge to read the error. All other providers are unaffected. |
+| *(no status badge)* | Provider is ready. |
+
+The badges are updated every 4 seconds via `GET /api/provider-status`. A provider stays
+marked **⏳ initializing** for as long as it needs — it is never changed to "failed" because
+of timing alone; only an actual exception during setup produces the red badge.
+
+### Registered tools list
+
+The **📋 Tools** navbar button opens a read-only panel showing every tool currently
+exposed by the proxy, grouped by provider. Each provider section shows:
+
+- A status badge (⏳ initializing / ✓ ready / ✗ failed)
+- The number of tools it contributes
+- Each tool's short name and description
+
+A filter box narrows results by tool name or description. This panel is useful for a
+quick audit of what the LLM can currently see, especially during startup when some
+providers may still be initializing.
+
+Under the hood it reads the same `GET /v1/tools` endpoint as the tool tester, plus
+`GET /api/provider-status` for the readiness information.
+
+```
+GET /api/provider-status
+```
+
+Returns per-provider initialization state:
+
+```json
+{
+ "ok": true,
+ "providers": {
+ "playwright": {"status": "pending", "error": null},
+ "github": {"status": "ready", "error": null},
+ "myflaky": {"status": "failed", "error": "pip install returned exit code 1 …"}
+ }
+}
+```
+
+`status` is one of `"pending"` (still installing), `"ready"` (setup complete), or
+`"failed"` (setup threw an error). Returns `{"ok": true, "providers": {}}` when
+`MCPPROXY_BACKGROUND_SETUP=0` (synchronous mode, nothing to track).
+
### Tool tester
The **🧪 Test Tools** navbar button lists every registered tool, grouped by provider,
diff --git a/frontend/app.py b/frontend/app.py
index 0820319..4747a90 100644
--- a/frontend/app.py
+++ b/frontend/app.py
@@ -22,6 +22,7 @@
POST /api/oauth-bootstrap — begin a provider-declared OAuth consent flow {name}
POST /api/restart — send SIGTERM to restart server
GET /api/config — UI feature flags (e.g. web_terminal)
+GET /api/provider-status — per-provider init state {providers: {name: {status, error}}}
WS /ws/terminal — interactive PTY terminal (optional ?cmd=…)
"""
@@ -1358,6 +1359,30 @@ async def client_config() -> dict:
"""Expose UI feature flags so the front end can hide disabled features."""
return {"ok": True, "web_terminal": _web_terminal_enabled()}
+ @app.get("/api/provider-status")
+ async def provider_status_api() -> dict:
+ """Return per-provider initialization status from the background startup.
+
+ Each entry maps a provider name to its current readiness state:
+ ``status`` is ``"pending"`` (still installing), ``"ready"`` (setup done),
+ or ``"failed"`` (setup threw an error). ``error`` is ``null`` unless
+ ``status == "failed"``, in which case it holds the failure message.
+
+ Returns ``{"ok": True, "providers": {}}`` when the background-startup
+ module is not loaded (e.g. ``MCPPROXY_BACKGROUND_SETUP=0``).
+ """
+ try:
+ import provider_status as _ps # same in-process dict server.py populates
+ return {
+ "ok": True,
+ "providers": {
+ name: {"status": s.status, "error": s.error}
+ for name, s in _ps.all_states().items()
+ },
+ }
+ except ImportError:
+ return {"ok": True, "providers": {}}
+
# ── Interactive web terminal (PTY over WebSocket) ──────────────────────────
@app.websocket("/ws/terminal")
@@ -1541,6 +1566,9 @@ async def index():
.badge-warn{background:var(--yellow);color:#1e1e2e;font-size:.62em;padding:2px 6px;border-radius:3px;font-weight:700}
.badge-err{background:var(--red);color:#1e1e2e;font-size:.62em;padding:2px 6px;border-radius:3px;font-weight:700}
.badge-disabled{background:#45475a;color:var(--muted);font-size:.62em;padding:2px 6px;border-radius:3px;font-weight:700;text-transform:uppercase;letter-spacing:.4px}
+.badge-status-pending{background:var(--yellow);color:#1e1e2e;font-size:.62em;padding:2px 6px;border-radius:3px;font-weight:700}
+.badge-status-ready{background:var(--green);color:#1e1e2e;font-size:.62em;padding:2px 6px;border-radius:3px;font-weight:700}
+.badge-status-failed{background:var(--red);color:#1e1e2e;font-size:.62em;padding:2px 6px;border-radius:3px;font-weight:700}
.tool-card.disabled .tool-card-body{opacity:.55;filter:saturate(.6)}
.tool-card.disabled .tool-card-header{background:#1f1f2c}
.fn-pick-row{display:flex;gap:6px;align-items:stretch}
@@ -1567,6 +1595,8 @@ async def index():
onclick="openTerminal()" title="Open an interactive shell in the server container">🖥 Terminal
+
@@ -2202,6 +2232,23 @@ async def index():
+
+
+
+
+
+
📋 Registered Tools
+
+
+
+
+
+
+
+
+
+
@@ -2246,9 +2293,12 @@ async def index():
let codeEditor = null; // CodeMirror instance for the code block
let secretsModal = null, wizModal = null, termModal = null;
let catalogModal = null, catalogEntries = [];
-let filesModal = null, ttModal = null;
+let filesModal = null, ttModal = null, tlModal = null;
let filesRoot = 'tools', filesPath = '', filesRoots = ['tools', 'files', 'repos'];
let ttTools = [], ttSelected = null; // tool tester: /v1/tools entries + selected name
+let tlTools = [], tlStatus = {}; // tools list: /v1/tools entries + provider status
+let _providerStatus = {}; // name → {status, error} from /api/provider-status
+let _statusPollTimer = null; // setInterval handle; cleared once all providers settle
let webTerminalEnabled = false;
let term = null, termFit = null, termSock = null; // xterm.js terminal state
let wzType = null; // 'code' | 'package' | 'repository' | 'remote' | 'rest'
@@ -2287,6 +2337,7 @@ async def index():
termModal = new bootstrap.Modal('#terminal-modal');
filesModal = new bootstrap.Modal('#files-modal');
ttModal = new bootstrap.Modal('#tooltest-modal');
+ tlModal = new bootstrap.Modal('#toolslist-modal');
document.getElementById('terminal-modal').addEventListener('hidden.bs.modal', closeTerminal);
document.getElementById('files-list').addEventListener('click', filesListClick);
document.getElementById('tt-list').addEventListener('click', ttListClick);
@@ -2298,6 +2349,8 @@ async def index():
loadConfig();
pollPendingAuth();
setInterval(pollPendingAuth, 5000);
+ pollProviderStatus();
+ _statusPollTimer = setInterval(pollProviderStatus, 4000);
loadList();
});
@@ -2329,6 +2382,60 @@ async def index():
banner.style.display = '';
}
+// ─────────────────────────────────────────────────────────────────────────────
+// Provider status polling (background startup badges)
+// ─────────────────────────────────────────────────────────────────────────────
+
+async function pollProviderStatus() {
+ try {
+ const r = await api('GET', '/api/provider-status');
+ _providerStatus = r.providers || {};
+ } catch { return; }
+ // Remove ⏳ badges the moment a provider flips to READY, however long it took.
+ updateStatusBadges();
+ // Stop polling once every provider has settled (ready or failed — no pending).
+ // Providers never go back to pending without a server restart, so no future
+ // ticks would change anything.
+ const hasPending = Object.values(_providerStatus).some(s => s.status === 'pending');
+ if (!hasPending && _statusPollTimer !== null) {
+ clearInterval(_statusPollTimer);
+ _statusPollTimer = null;
+ }
+}
+
+function updateStatusBadges() {
+ document.querySelectorAll('.provider-item[data-name]').forEach(el => {
+ const name = el.dataset.name;
+ const st = _providerStatus[name];
+ let badge = el.querySelector('.status-init-badge');
+ // Provider is READY (or not tracked): remove any existing status badge.
+ if (!st || st.status === 'ready') {
+ if (badge) badge.remove();
+ return;
+ }
+ if (!badge) {
+ badge = document.createElement('span');
+ badge.className = 'status-init-badge';
+ let row = el.querySelector('.alert-row');
+ if (!row) {
+ row = document.createElement('div');
+ row.className = 'd-flex gap-1 flex-wrap mt-1 alert-row';
+ el.querySelector('div').appendChild(row);
+ }
+ row.prepend(badge);
+ }
+ if (st.status === 'pending') {
+ badge.className = 'status-init-badge badge-status-pending';
+ badge.title = 'Provider is still initializing — tools will be available shortly';
+ badge.textContent = '⏳ initializing';
+ } else if (st.status === 'failed') {
+ badge.className = 'status-init-badge badge-status-failed';
+ badge.title = st.error || 'Setup failed';
+ badge.textContent = '✗ setup failed';
+ }
+ });
+}
+
// ─────────────────────────────────────────────────────────────────────────────
// API
// ─────────────────────────────────────────────────────────────────────────────
@@ -2358,7 +2465,11 @@ async def index():
// ─────────────────────────────────────────────────────────────────────────────
async function loadList() {
try {
- const providers = await api('GET', '/api/tools');
+ const [providers, statusRes] = await Promise.all([
+ api('GET', '/api/tools'),
+ api('GET', '/api/provider-status').catch(() => ({providers: {}})),
+ ]);
+ _providerStatus = statusRes.providers || {};
const el = document.getElementById('provider-list');
if (!providers.length) {
el.innerHTML = '