@thotischner/observability-mcp 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -122,6 +122,7 @@
122
122
  .badge::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; box-shadow: 0 0 6px currentColor; }
123
123
  .badge-ok { background: var(--success-soft); color: var(--success); border: 1px solid rgba(74,222,128,0.25); }
124
124
  .badge-err { background: var(--danger-soft); color: var(--danger); border: 1px solid rgba(239,91,110,0.30); }
125
+ .hidden { display: none !important; }
125
126
 
126
127
  .nav { display: flex; gap: 2px; margin-left: var(--sp-6); }
127
128
  .nav-btn {
@@ -490,6 +491,7 @@
490
491
  <button class="nav-btn active" data-page="dashboard" onclick="showPage('dashboard')">Dashboard</button>
491
492
  <button class="nav-btn" data-page="sources" onclick="showPage('sources')">Sources</button>
492
493
  <button class="nav-btn" data-page="services" onclick="showPage('services')">Services</button>
494
+ <button class="nav-btn" data-page="connectors" onclick="showPage('connectors')">Connectors</button>
493
495
  <button class="nav-btn" data-page="health" onclick="showPage('health')">Health</button>
494
496
  <button class="nav-btn" data-page="settings" onclick="showPage('settings')">Settings</button>
495
497
  </div>
@@ -549,6 +551,40 @@
549
551
  <div class="card"><div class="card-header"><h2>Discovered Services</h2></div><div id="services-list"><div class="empty">Loading...</div></div></div>
550
552
  </div>
551
553
 
554
+ <!-- ===== Connectors ===== -->
555
+ <div class="page" id="page-connectors">
556
+ <div class="card" style="padding:0;">
557
+ <div class="tabs">
558
+ <button class="tab-btn active" onclick="showTab('installed')">Installed</button>
559
+ <button class="tab-btn" onclick="showTab('hub')">Connector Hub</button>
560
+ <button class="tab-btn" onclick="showTab('upload')">Upload bundle</button>
561
+ </div>
562
+
563
+ <!-- Installed Tab -->
564
+ <div class="tab-content active" id="tab-installed" style="padding:20px;">
565
+ <div class="card-header" style="margin-bottom:12px"><h2>Installed connectors</h2><button class="btn btn-ghost btn-sm" onclick="loadConnectors()">Refresh</button></div>
566
+ <div id="conn-installed"><div class="empty">Loading...</div></div>
567
+ </div>
568
+
569
+ <!-- Connector Hub Tab -->
570
+ <div class="tab-content" id="tab-hub" style="padding:20px;">
571
+ <div class="card-header" style="margin-bottom:12px"><h2>Available from the Connector Hub</h2>
572
+ <a class="btn btn-ghost btn-sm" href="https://thotischner.github.io/observability-mcp/hub/" target="_blank" rel="noopener">Open Hub ↗</a></div>
573
+ <div id="conn-hub"><div class="empty">Loading...</div></div>
574
+ </div>
575
+
576
+ <!-- Upload bundle Tab -->
577
+ <div class="tab-content" id="tab-upload" style="padding:20px;">
578
+ <div class="card-header" style="margin-bottom:12px"><h2>Upload a connector bundle</h2></div>
579
+ <p class="empty" style="margin:0 0 12px">Install a signed connector <code>.tgz</code> directly — handy for air-gapped environments. The bundle is always verified against the configured trust root before it is loaded.</p>
580
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
581
+ <input type="file" id="conn-upload-file" accept=".tgz,.tar.gz,application/octet-stream">
582
+ <button class="btn btn-primary btn-sm" onclick="uploadConnector(this)">Upload &amp; install</button>
583
+ </div>
584
+ </div>
585
+ </div>
586
+ </div>
587
+
552
588
  <!-- ===== Health ===== -->
553
589
  <div class="page" id="page-health">
554
590
  <div id="health-cards" style="display:grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 16px;">
@@ -744,12 +780,88 @@ function showPage(name) {
744
780
  document.querySelector(`.nav-btn[data-page="${name}"]`).classList.add('active');
745
781
  if(name==='settings') loadSettingsData();
746
782
  if(name==='health') loadHealthData();
783
+ if(name==='connectors') loadConnectors();
784
+ }
785
+
786
+ function escHtml(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
787
+
788
+ async function loadConnectors(){
789
+ const inst=document.getElementById('conn-installed');
790
+ const hub=document.getElementById('conn-hub');
791
+ try {
792
+ const d=await(await fetch('/api/connectors')).json();
793
+ const cs=(d.connectors||[]);
794
+ inst.innerHTML = cs.length ? cs.map(c=>{
795
+ const caps=Object.entries(c.capabilities||{}).filter(([,v])=>v).map(([k])=>k).join(', ')||'—';
796
+ return `<div class="card" style="margin:0 0 10px">
797
+ <div class="card-header"><h2 style="font-size:15px">${escHtml(c.displayName)}
798
+ <span class="badge">${escHtml(c.source)}</span>
799
+ ${c.version?`<span class="badge">v${escHtml(c.version)}</span>`:''}</h2></div>
800
+ <div style="color:var(--text-muted);font-size:13px">${escHtml(c.description)||'<em>no description</em>'}</div>
801
+ <div style="font-size:12px;color:var(--text-muted);margin-top:6px">type <code>${escHtml(c.name)}</code> · signals ${(c.signalTypes||[]).map(escHtml).join(', ')||'—'} · ${escHtml(caps)}</div>
802
+ </div>`;
803
+ }).join('') : '<div class="empty">No connectors loaded.</div>';
804
+ } catch(e){ inst.innerHTML='<div class="empty">Failed to load connectors.</div>'; }
805
+
806
+ try {
807
+ const d=await(await fetch('/api/hub/catalog')).json();
808
+ if(d.error){ hub.innerHTML=`<div class="empty">Hub catalog unreachable (${escHtml(d.url)}).<br>Set <code>HUB_CATALOG_URL</code> for a mirror. ${escHtml(d.error)}</div>`; return; }
809
+ const cs=(d.connectors||[]);
810
+ hub.innerHTML = cs.length ? cs.map(c=>{
811
+ const v=(c.versions&&c.versions[0])||{};
812
+ const ver=c.latest||v.version||'';
813
+ const status = c.installed
814
+ ? `<span class="badge badge-ok">installed${c.installedVersion?` v${escHtml(c.installedVersion)}`:''}</span>`
815
+ : (c.builtin?'<span class="badge">builtin</span>':'<span class="badge">available</span>');
816
+ const cmd = c.builtin
817
+ ? `# ${escHtml(c.displayName)} ships in the server image — no install needed.`
818
+ : `curl -fsSL -o plugin-signing.pub.pem https://raw.githubusercontent.com/ThoTischner/observability-mcp/main/docs/plugin-signing.pub.pem\nomcp plugin install ${escHtml(c.name)}@${escHtml(ver)} --trust-root plugin-signing.pub.pem`;
819
+ const id='ci-'+c.name;
820
+ return `<div class="card" style="margin:0 0 10px">
821
+ <div class="card-header"><h2 style="font-size:15px">${escHtml(c.displayName)}
822
+ <span class="badge">${escHtml(c.tier)}</span> ${status}</h2>
823
+ ${c.installed||c.builtin?'':`<span style="display:flex;gap:6px"><button class="btn btn-primary btn-sm" onclick="installConnector('${escHtml(c.name)}',this)">Install</button><button class="btn btn-ghost btn-sm" onclick="document.getElementById('${id}').classList.toggle('hidden')">CLI…</button></span>`}</div>
824
+ <div style="color:var(--text-muted);font-size:13px">${escHtml(c.description)}</div>
825
+ <div style="font-size:12px;color:var(--text-muted);margin-top:6px">type <code>${escHtml(c.name)}</code> · signals ${(c.signalTypes||[]).map(escHtml).join(', ')||'—'} · latest <code>${escHtml(ver)||'—'}</code></div>
826
+ <pre id="${id}" class="hidden" style="margin-top:8px;background:var(--surface-2);padding:10px;border-radius:6px;font-size:12px;overflow:auto">${escHtml(cmd)}</pre>
827
+ </div>`;
828
+ }).join('') : '<div class="empty">Catalog empty.</div>';
829
+ } catch(e){ hub.innerHTML='<div class="empty">Failed to reach the hub catalog.</div>'; }
830
+ }
831
+
832
+ async function installConnector(name, btn){
833
+ if(btn){ btn.disabled=true; btn.textContent='Installing…'; }
834
+ try {
835
+ const r=await fetch('/api/connectors/install',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name})});
836
+ const d=await r.json().catch(()=>({}));
837
+ if(r.ok){ toast(`Installed ${name}${d.version?(' v'+d.version):''}`); loadConnectors(); loadTypes(); }
838
+ else if(r.status===403){ toast('UI install disabled — use the CLI tab, or set ENABLE_UI_INSTALL=true + PLUGIN_TRUST_ROOT.'); if(btn){btn.disabled=false;btn.textContent='Install';} }
839
+ else { toast('Install failed: '+(d.error||r.status)); if(btn){btn.disabled=false;btn.textContent='Install';} }
840
+ } catch(e){ toast('Install error: '+e.message); if(btn){btn.disabled=false;btn.textContent='Install';} }
841
+ }
842
+ async function uploadConnector(btn){
843
+ const inp=document.getElementById('conn-upload-file');
844
+ const f=inp&&inp.files&&inp.files[0];
845
+ if(!f){ toast('Choose a connector .tgz first'); return; }
846
+ if(btn){ btn.disabled=true; btn.textContent='Uploading…'; }
847
+ try {
848
+ const r=await fetch('/api/connectors/upload',{method:'POST',headers:{'Content-Type':'application/octet-stream'},body:f});
849
+ const d=await r.json().catch(()=>({}));
850
+ if(r.ok){ toast(`Installed ${d.name||'connector'}${d.version?(' v'+d.version):''}`); inp.value=''; loadConnectors(); loadTypes(); }
851
+ else if(r.status===403){ toast('UI install disabled — set ENABLE_UI_INSTALL=true + PLUGIN_TRUST_ROOT.'); }
852
+ else { toast('Upload failed: '+(d.error||r.status)); }
853
+ } catch(e){ toast('Upload error: '+e.message); }
854
+ finally { if(btn){btn.disabled=false;btn.textContent='Upload & install';} }
747
855
  }
748
856
  function showTab(name) {
749
- document.querySelectorAll('.tab-content').forEach(t=>t.classList.remove('active'));
750
- document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
751
- document.getElementById('tab-'+name).classList.add('active');
752
- event.target.classList.add('active');
857
+ // Scope to the tab group's card so independent tab sets (Settings,
858
+ // Connectors) don't reset each other.
859
+ const scope = (event && event.target.closest('.card')) || document;
860
+ scope.querySelectorAll('.tab-content').forEach(t=>t.classList.remove('active'));
861
+ scope.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
862
+ const el = document.getElementById('tab-'+name);
863
+ if (el) el.classList.add('active');
864
+ if (event && event.target) event.target.classList.add('active');
753
865
  if(name==='metrics') populateMetricsSourceSelect();
754
866
  }
755
867
  function closeModal(id) { document.getElementById(id).classList.remove('open'); }
@@ -1124,6 +1236,9 @@ async function loadInfo(){
1124
1236
  }catch{/* server too old or /api/info disabled */}
1125
1237
  }
1126
1238
 
1239
+ // Show the endpoint the user is actually reaching the server through
1240
+ // (localhost, a port-forward, or an ingress) — not a hardcoded host.
1241
+ document.getElementById('mcp-url').textContent = window.location.origin + '/mcp';
1127
1242
  (async()=>{await loadTypes();await refresh();await loadInfo();setInterval(refresh,15000);})();
1128
1243
  </script>
1129
1244
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thotischner/observability-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Unified observability gateway for AI agents — one MCP server for Prometheus, Loki, and any backend",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,6 +9,10 @@
9
9
  "type": "git",
10
10
  "url": "https://github.com/ThoTischner/observability-mcp"
11
11
  },
12
+ "homepage": "https://github.com/ThoTischner/observability-mcp#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/ThoTischner/observability-mcp/issues"
15
+ },
12
16
  "keywords": [
13
17
  "mcp",
14
18
  "observability",
@@ -18,7 +22,8 @@
18
22
  "anomaly-detection"
19
23
  ],
20
24
  "bin": {
21
- "observability-mcp": "./dist/index.js"
25
+ "observability-mcp": "./dist/index.js",
26
+ "omcp": "./dist/cli/index.js"
22
27
  },
23
28
  "files": [
24
29
  "dist",
@@ -26,7 +31,7 @@
26
31
  ],
27
32
  "scripts": {
28
33
  "dev": "tsx watch src/index.ts",
29
- "build": "tsc && cp -r src/ui dist/ui",
34
+ "build": "tsc && rm -rf dist/ui && cp -r src/ui dist/ui",
30
35
  "start": "node dist/index.js"
31
36
  },
32
37
  "dependencies": {
@@ -41,7 +46,7 @@
41
46
  "@types/js-yaml": "^4.0.9",
42
47
  "@types/node": "^25.7.0",
43
48
  "openapi-types": "^12.1.3",
44
- "tsx": "^4.19.0",
49
+ "tsx": "^4.22.0",
45
50
  "typescript": "^6.0.3"
46
51
  },
47
52
  "overrides": {