R-Kentaren commited on
Commit
f76a56f
Β·
verified Β·
1 Parent(s): 2417726

fix: pure FastAPI serving, auto-load model, rich MCP catalog with 18 servers

Browse files
Files changed (5) hide show
  1. app.py +20 -13
  2. index.html +203 -51
  3. requirements.txt +4 -2
  4. sonicoder/config/__init__.py +230 -0
  5. sonicoder/server/__init__.py +94 -17
app.py CHANGED
@@ -1,9 +1,9 @@
1
  """SoniCoder v2 β€” Entry point.
2
 
3
- gradio.Server (Gradio 6.x) serves index.html from project root
4
- with Gradio's queuing, concurrency, and API infrastructure.
5
 
6
- Blog: https://huggingface.co/blog/introducing-gradio-server
7
  """
8
 
9
  from __future__ import annotations
@@ -27,15 +27,21 @@ logger = logging.getLogger(__name__)
27
  def main():
28
  logger.info("SoniCoder v2 starting...")
29
 
30
- # Start loading default model in background
31
- from sonicoder.model.loader import start_background_load
32
- start_background_load()
33
- logger.info("Background model loading started")
34
-
35
- # Set up MCP servers from config
36
- from sonicoder.mcp import get_mcp_manager
37
  from sonicoder.config import load_settings
38
  settings = load_settings()
 
 
 
 
 
 
 
 
 
 
 
39
  if settings.mcp_servers:
40
  mcp = get_mcp_manager()
41
  mcp.configure(settings.mcp_servers)
@@ -46,12 +52,13 @@ def main():
46
  else:
47
  logger.info(f"MCP server '{name}' connected")
48
 
49
- # Create the server (gradio.Server β€” custom HTML + Gradio backend)
50
  from sonicoder.server import create_app
51
  app = create_app()
52
 
53
- logger.info("Launching...")
54
- app.launch(show_error=True)
 
55
 
56
 
57
  if __name__ == "__main__":
 
1
  """SoniCoder v2 β€” Entry point.
2
 
3
+ Uses FastAPI directly (not gradio.Server) so our custom index.html
4
+ is always served at / without Gradio overriding the root route.
5
 
6
+ Model is loaded BLOCKING before server starts β€” no lazy loading.
7
  """
8
 
9
  from __future__ import annotations
 
27
  def main():
28
  logger.info("SoniCoder v2 starting...")
29
 
30
+ # ── BLOCKING model load β€” model must be ready BEFORE server starts ──
31
+ from sonicoder.model.loader import load_model
 
 
 
 
 
32
  from sonicoder.config import load_settings
33
  settings = load_settings()
34
+ model_key = settings.default_model
35
+ logger.info(f"Loading model {model_key} (blocking)...")
36
+ try:
37
+ load_model(model_key)
38
+ logger.info("Model loaded successfully β€” ready to serve")
39
+ except Exception as e:
40
+ logger.error(f"Failed to load model: {e}")
41
+ logger.info("Server will start anyway β€” model can be loaded later via API")
42
+
43
+ # ── Set up MCP servers from config ─────────────────────────────────
44
+ from sonicoder.mcp import get_mcp_manager
45
  if settings.mcp_servers:
46
  mcp = get_mcp_manager()
47
  mcp.configure(settings.mcp_servers)
 
52
  else:
53
  logger.info(f"MCP server '{name}' connected")
54
 
55
+ # ── Create the FastAPI server ──────────────────────────────────────
56
  from sonicoder.server import create_app
57
  app = create_app()
58
 
59
+ logger.info("Launching on 0.0.0.0:7860...")
60
+ import uvicorn
61
+ uvicorn.run(app, host="0.0.0.0", port=7860, log_level="info")
62
 
63
 
64
  if __name__ == "__main__":
index.html CHANGED
@@ -195,17 +195,46 @@ a:hover{text-decoration:underline;text-shadow:var(--glow-cyan)}
195
  #file-viewer pre{margin:0;font-size:11px;line-height:1.5;color:var(--code-text)}
196
  .file-line-num{color:var(--gray-dim);display:inline-block;width:50px;text-align:right;margin-right:12px;user-select:none;border-right:1px solid var(--border);padding-right:8px}
197
 
198
- /* MCP */
199
- #pane-mcp{padding:16px;gap:12px;overflow-y:auto}
200
- .mcp-server{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;margin-bottom:8px}
201
- .mcp-server-header{display:flex;align-items:center;gap:8px;padding:8px 12px;background:rgba(30,42,58,.5);border-bottom:1px solid var(--border);font-size:12px}
202
- .mcp-server-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
203
- .mcp-server-dot.connected{background:var(--success);box-shadow:0 0 6px var(--success)}
204
- .mcp-server-dot.disconnected{background:var(--red);box-shadow:0 0 6px var(--red)}
205
- .mcp-server-name{font-weight:600;color:var(--gray-light);flex:1}
206
- .mcp-tools{padding:8px 12px;display:flex;flex-wrap:wrap;gap:4px}
207
- .mcp-tool-tag{padding:2px 8px;background:rgba(0,212,255,.08);border:1px solid rgba(0,212,255,.2);border-radius:10px;font-size:10px;color:var(--cyan)}
208
- .mcp-empty{color:var(--gray-dim);font-size:12px;text-align:center;padding:40px 20px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
210
  /* ═══ STATUS BAR ═══ */
211
  #status-bar{display:flex;align-items:center;gap:8px;padding:5px 16px;border-top:1px solid var(--border);background:var(--bg-panel);font-size:11px;flex-shrink:0}
@@ -261,7 +290,7 @@ a:hover{text-decoration:underline;text-shadow:var(--glow-cyan)}
261
  <span id="model-pill-text">Loading...</span>
262
  </div>
263
  <select id="model-select" title="Switch AI model"></select>
264
- <div class="pill mcp-pill" id="mcp-pill">
265
  <span class="dot disconnected" id="mcp-dot"></span>
266
  <span id="mcp-pill-text">MCP</span>
267
  </div>
@@ -338,7 +367,25 @@ a:hover{text-decoration:underline;text-shadow:var(--glow-cyan)}
338
  <div id="file-viewer"><pre id="file-content"></pre></div>
339
  </div>
340
  <div class="tab-pane" id="pane-mcp">
341
- <div id="mcp-content"><div class="mcp-empty">Loading MCP status...</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  </div>
343
  </div>
344
  </div>
@@ -367,6 +414,7 @@ var state = {
367
  isGenerating: false,
368
  modelReady: false,
369
  activeTab: 'preview',
 
370
  toolCalls: {},
371
  currentStreamDiv: null,
372
  lastCode: '',
@@ -374,6 +422,7 @@ var state = {
374
  consoleStdout: '',
375
  consoleStderr: '',
376
  generatedFiles: [],
 
377
  };
378
 
379
  // ═══ INIT ═══
@@ -381,7 +430,7 @@ document.addEventListener('DOMContentLoaded', function() {
381
  loadConfig();
382
  pollModelStatus();
383
  refreshWorkspace();
384
- loadMcpStatus();
385
 
386
  var input = document.getElementById('chat-input');
387
  input.addEventListener('input', autoResize);
@@ -410,6 +459,7 @@ async function loadConfig() {
410
  var config = await resp.json();
411
  var sel = document.getElementById('model-select');
412
  sel.innerHTML = '';
 
413
  (config.models || []).forEach(function(m) {
414
  var opt = document.createElement('option');
415
  opt.value = m.key;
@@ -417,6 +467,7 @@ async function loadConfig() {
417
  if (m.key === config.default_model) opt.selected = true;
418
  sel.appendChild(opt);
419
  });
 
420
  sel.addEventListener('change', function() { switchModel(sel.value); });
421
  renderExamples(config);
422
  } catch(e) { console.error('Config load failed:', e); }
@@ -477,46 +528,139 @@ async function refreshWorkspace() {
477
  } catch(e) { console.error(e); }
478
  }
479
 
480
- async function loadMcpStatus() {
 
 
481
  try {
482
- var resp = await fetch('/api/mcp_status');
483
  var data = await resp.json();
484
- var dot = document.getElementById('mcp-dot');
485
- var text = document.getElementById('mcp-pill-text');
486
- var content = document.getElementById('mcp-content');
487
- var servers = data.servers || {};
488
- var keys = Object.keys(servers);
489
- if (keys.length === 0) {
490
- dot.className = 'dot disconnected';
491
- text.textContent = 'MCP';
492
- content.innerHTML = '<div class="mcp-empty">No MCP servers configured.<br><br>Add MCP servers in your config to extend SoniCoder with external tools.</div>';
493
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
  }
495
- var anyConnected = keys.some(function(k) { return servers[k].status === 'connected'; });
496
- dot.className = anyConnected ? 'dot connected' : 'dot disconnected';
497
- text.textContent = 'MCP (' + keys.length + ')';
498
- var html = '';
499
- keys.forEach(function(name) {
500
- var s = servers[name];
501
- var isConn = s.status === 'connected';
502
- html += '<div class="mcp-server">';
503
- html += '<div class="mcp-server-header">';
504
- html += '<span class="mcp-server-dot ' + (isConn ? 'connected' : 'disconnected') + '"></span>';
505
- html += '<span class="mcp-server-name">' + escHtml(name) + '</span>';
506
- html += '<span class="tool-badge ' + (isConn ? 'success' : 'error') + '">' + s.status + '</span>';
507
- html += '</div>';
508
- if (s.tools && s.tools.length > 0) {
509
- html += '<div class="mcp-tools">';
510
- s.tools.forEach(function(t) { html += '<span class="mcp-tool-tag">' + escHtml(t) + '</span>'; });
511
- html += '</div>';
512
- }
513
- html += '</div>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  });
515
- content.innerHTML = html;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  } catch(e) {
517
- document.getElementById('mcp-dot').className = 'dot disconnected';
518
- document.getElementById('mcp-content').innerHTML = '<div class="mcp-empty">MCP status unavailable.</div>';
 
 
 
 
 
519
  }
 
520
  }
521
 
522
  function renderExamples(config) {
@@ -552,6 +696,7 @@ function switchTab(tab) {
552
  document.querySelectorAll('.tab-pane').forEach(function(p) {
553
  p.classList.toggle('active', p.id === 'pane-' + tab);
554
  });
 
555
  }
556
 
557
  // ═══ FULLSCREEN ═══
@@ -587,7 +732,7 @@ function addMessage(role, content, extra) {
587
  var bodyClass = role === 'assistant' ? '<div class="msg-body">' : '';
588
  var bodyClose = role === 'assistant' ? '</div>' : '';
589
  var iterationHtml = extra.iteration ? '<div class="iteration-badge">Iteration ' + extra.iteration + '/' + extra.max_iterations + '</div>' : '';
590
- div.innerHTML = iterationHtml + prefix + '<span class="msg-content">' + renderMd(content) + '</span>' + bodyClose + bodyClose;
591
  container.appendChild(div);
592
  container.scrollTop = container.scrollHeight;
593
  return div;
@@ -596,7 +741,6 @@ function addMessage(role, content, extra) {
596
  function renderMd(text) {
597
  if (!text) return '';
598
  text = escHtml(text);
599
- // Code blocks
600
  text = text.replace(/```(\w*)\n([\s\S]*?)```/g, function(_, lang, code) {
601
  var id = 'cb_' + Date.now() + Math.random().toString(36).substr(2,4);
602
  return '<div class="code-block-wrap"><div class="code-block-header"><span class="code-lang">' + (lang||'code') + '</span><button class="btn-copy" onclick="copyBlock(\'' + id + '\')">&#128203; Copy</button></div><pre><code id="' + id + '">' + code.trim() + '</code></pre></div>';
@@ -640,6 +784,7 @@ function addToolCall(toolName, args) {
640
  var div = document.createElement('div');
641
  div.className = 'tool-block open';
642
  div.id = id;
 
643
  var argsStr = '';
644
  if (args) {
645
  Object.entries(args).forEach(function(e) {
@@ -647,6 +792,7 @@ function addToolCall(toolName, args) {
647
  argsStr += escHtml(e[0] + ': ' + v) + '\n';
648
  });
649
  }
 
650
  div.innerHTML =
651
  '<div class="tool-header">' +
652
  '<span class="tool-icon">&#9889;</span>' +
@@ -655,6 +801,7 @@ function addToolCall(toolName, args) {
655
  '</div>' +
656
  '<div class="tool-args">' + argsStr + '</div>' +
657
  '<div class="tool-result"></div>';
 
658
  container.appendChild(div);
659
  container.scrollTop = container.scrollHeight;
660
  state.toolCalls[id] = div;
@@ -666,6 +813,7 @@ function updateToolCall(id, success, output) {
666
  if (!div) return;
667
  var badge = div.querySelector('.tool-badge');
668
  var result = div.querySelector('.tool-result');
 
669
  if (success) {
670
  badge.className = 'tool-badge success';
671
  badge.textContent = 'DONE';
@@ -673,6 +821,7 @@ function updateToolCall(id, success, output) {
673
  badge.className = 'tool-badge error';
674
  badge.textContent = 'ERROR';
675
  }
 
676
  if (output) {
677
  result.textContent = output;
678
  result.className = success ? 'tool-result' : 'tool-result error-output';
@@ -726,15 +875,18 @@ async function handleSend() {
726
  body: JSON.stringify({ message: message, session_id: state.sessionId }),
727
  });
728
  if (!resp.ok) throw new Error('HTTP ' + resp.status);
 
729
  var reader = resp.body.getReader();
730
  var decoder = new TextDecoder();
731
  var buffer = '';
 
732
  while (true) {
733
  var result = await reader.read();
734
  if (result.done) break;
735
  buffer += decoder.decode(result.value, {stream: true});
736
  var lines = buffer.split('\n');
737
  buffer = lines.pop() || '';
 
738
  for (var i = 0; i < lines.length; i++) {
739
  var line = lines[i];
740
  if (!line.trim()) continue;
@@ -757,7 +909,6 @@ async function handleSend() {
757
  break;
758
  }
759
  }
760
- // Capture code for Code tab
761
  if (ev.result.output && (ev.result.output.includes('<!DOCTYPE') || ev.result.output.includes('<html'))) {
762
  showPreview(ev.result.output);
763
  }
@@ -790,6 +941,7 @@ async function handleSend() {
790
  setStatus('status-error', 'CONNECTION ERROR');
791
  }
792
  }
 
793
  state.isGenerating = false;
794
  document.getElementById('btn-send').style.display = 'flex';
795
  document.getElementById('btn-stop').style.display = 'none';
 
195
  #file-viewer pre{margin:0;font-size:11px;line-height:1.5;color:var(--code-text)}
196
  .file-line-num{color:var(--gray-dim);display:inline-block;width:50px;text-align:right;margin-right:12px;user-select:none;border-right:1px solid var(--border);padding-right:8px}
197
 
198
+ /* ═══ MCP TAB β€” Rich Catalog UI ═══ */
199
+ #pane-mcp{overflow-y:auto;flex:1}
200
+ .mcp-tab-bar{display:flex;border-bottom:1px solid var(--border);background:rgba(13,17,23,.6);flex-shrink:0;padding:0 8px;gap:2px;overflow-x:auto}
201
+ .mcp-cat-tab{background:transparent;border:none;border-bottom:2px solid transparent;color:var(--gray-dim);font-family:var(--font-mono);font-size:10px;padding:8px 10px;cursor:pointer;transition:all var(--transition);letter-spacing:.5px;text-transform:uppercase;white-space:nowrap}
202
+ .mcp-cat-tab:hover{color:var(--gray-mid)}
203
+ .mcp-cat-tab.active{color:var(--purple);border-bottom-color:var(--purple);text-shadow:var(--glow-purple)}
204
+ .mcp-catalog{padding:10px;display:flex;flex-direction:column;gap:8px}
205
+ .mcp-card{border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-deep);overflow:hidden;transition:border-color var(--transition)}
206
+ .mcp-card:hover{border-color:var(--border-focus)}
207
+ .mcp-card-head{display:flex;align-items:center;gap:10px;padding:10px 12px;cursor:default}
208
+ .mcp-card-icon{font-size:18px;flex-shrink:0;width:24px;text-align:center}
209
+ .mcp-card-info{flex:1;min-width:0}
210
+ .mcp-card-name{font-weight:600;color:var(--gray-light);font-size:12px;margin-bottom:2px}
211
+ .mcp-card-desc{color:var(--gray-dim);font-size:11px;line-height:1.4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
212
+ .mcp-card-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}
213
+ .mcp-card-status{width:7px;height:7px;border-radius:50%;flex-shrink:0}
214
+ .mcp-card-status.on{background:var(--success);box-shadow:0 0 6px var(--success)}
215
+ .mcp-card-status.off{background:var(--gray-dim)}
216
+ .mcp-toggle{position:relative;width:34px;height:18px;flex-shrink:0}
217
+ .mcp-toggle input{opacity:0;width:0;height:0;position:absolute}
218
+ .mcp-toggle-slider{position:absolute;inset:0;background:var(--border);border-radius:9px;cursor:pointer;transition:background var(--transition)}
219
+ .mcp-toggle-slider::before{content:'';position:absolute;left:2px;top:2px;width:14px;height:14px;background:var(--gray-dim);border-radius:50%;transition:all var(--transition)}
220
+ .mcp-toggle input:checked+.mcp-toggle-slider{background:rgba(57,255,20,.3);border:1px solid var(--green-dim)}
221
+ .mcp-toggle input:checked+.mcp-toggle-slider::before{transform:translateX(16px);background:var(--green);box-shadow:0 0 6px var(--green)}
222
+ .mcp-card-details{display:none;padding:0 12px 10px;border-top:1px solid var(--border);margin-top:0;padding-top:8px}
223
+ .mcp-card.open .mcp-card-details{display:block}
224
+ .mcp-detail-row{display:flex;align-items:flex-start;gap:8px;margin-bottom:4px;font-size:11px}
225
+ .mcp-detail-label{color:var(--gray-dim);flex-shrink:0;min-width:60px}
226
+ .mcp-detail-value{color:var(--gray-mid);word-break:break-all}
227
+ .mcp-card-tools{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}
228
+ .mcp-tool-chip{padding:2px 8px;background:rgba(0,212,255,.08);border:1px solid rgba(0,212,255,.2);border-radius:10px;font-size:10px;color:var(--cyan)}
229
+ .mcp-card-error{color:var(--red);font-size:11px;margin-top:4px}
230
+ .mcp-card-expand{background:transparent;border:none;color:var(--gray-dim);font-family:var(--font-mono);font-size:10px;cursor:pointer;padding:2px 6px;border-radius:3px;transition:color var(--transition)}
231
+ .mcp-card-expand:hover{color:var(--cyan)}
232
+ .mcp-connecting{color:var(--amber);font-size:10px;animation:pulse 1s ease infinite}
233
+ .mcp-stats-bar{display:flex;align-items:center;gap:12px;padding:8px 12px;border-bottom:1px solid var(--border);background:rgba(30,42,58,.3);font-size:11px;flex-shrink:0}
234
+ .mcp-stat{color:var(--gray-dim)}
235
+ .mcp-stat strong{color:var(--gray-light);font-weight:600}
236
+ .mcp-stat .green{color:var(--green)}
237
+ .mcp-stat .red{color:var(--red)}
238
 
239
  /* ═══ STATUS BAR ═══ */
240
  #status-bar{display:flex;align-items:center;gap:8px;padding:5px 16px;border-top:1px solid var(--border);background:var(--bg-panel);font-size:11px;flex-shrink:0}
 
290
  <span id="model-pill-text">Loading...</span>
291
  </div>
292
  <select id="model-select" title="Switch AI model"></select>
293
+ <div class="pill mcp-pill" id="mcp-pill" onclick="switchTab('mcp')" style="cursor:pointer" title="Open MCP panel">
294
  <span class="dot disconnected" id="mcp-dot"></span>
295
  <span id="mcp-pill-text">MCP</span>
296
  </div>
 
367
  <div id="file-viewer"><pre id="file-content"></pre></div>
368
  </div>
369
  <div class="tab-pane" id="pane-mcp">
370
+ <div class="mcp-stats-bar" id="mcp-stats-bar">
371
+ <span class="mcp-stat">Servers: <strong id="mcp-total-count">0</strong></span>
372
+ <span class="mcp-stat">Connected: <strong class="green" id="mcp-connected-count">0</strong></span>
373
+ <span class="mcp-stat">Tools: <strong id="mcp-tools-count">0</strong></span>
374
+ </div>
375
+ <div class="mcp-tab-bar" id="mcp-tab-bar">
376
+ <button class="mcp-cat-tab active" data-cat="all" onclick="filterMcpCat('all')">All</button>
377
+ <button class="mcp-cat-tab" data-cat="core" onclick="filterMcpCat('core')">Core</button>
378
+ <button class="mcp-cat-tab" data-cat="reasoning" onclick="filterMcpCat('reasoning')">Reasoning</button>
379
+ <button class="mcp-cat-tab" data-cat="dev-tools" onclick="filterMcpCat('dev-tools')">Dev Tools</button>
380
+ <button class="mcp-cat-tab" data-cat="database" onclick="filterMcpCat('database')">Database</button>
381
+ <button class="mcp-cat-tab" data-cat="search" onclick="filterMcpCat('search')">Search</button>
382
+ <button class="mcp-cat-tab" data-cat="browser" onclick="filterMcpCat('browser')">Browser</button>
383
+ <button class="mcp-cat-tab" data-cat="integrations" onclick="filterMcpCat('integrations')">Integrations</button>
384
+ <button class="mcp-cat-tab" data-cat="utilities" onclick="filterMcpCat('utilities')">Utilities</button>
385
+ </div>
386
+ <div class="mcp-catalog" id="mcp-catalog">
387
+ <div class="mcp-empty" style="color:var(--gray-dim);font-size:12px;text-align:center;padding:40px 20px">Loading MCP catalog...</div>
388
+ </div>
389
  </div>
390
  </div>
391
  </div>
 
414
  isGenerating: false,
415
  modelReady: false,
416
  activeTab: 'preview',
417
+ activeMcpCat: 'all',
418
  toolCalls: {},
419
  currentStreamDiv: null,
420
  lastCode: '',
 
422
  consoleStdout: '',
423
  consoleStderr: '',
424
  generatedFiles: [],
425
+ mcpServers: [],
426
  };
427
 
428
  // ═══ INIT ═══
 
430
  loadConfig();
431
  pollModelStatus();
432
  refreshWorkspace();
433
+ loadMcpCatalog();
434
 
435
  var input = document.getElementById('chat-input');
436
  input.addEventListener('input', autoResize);
 
459
  var config = await resp.json();
460
  var sel = document.getElementById('model-select');
461
  sel.innerHTML = '';
462
+
463
  (config.models || []).forEach(function(m) {
464
  var opt = document.createElement('option');
465
  opt.value = m.key;
 
467
  if (m.key === config.default_model) opt.selected = true;
468
  sel.appendChild(opt);
469
  });
470
+
471
  sel.addEventListener('change', function() { switchModel(sel.value); });
472
  renderExamples(config);
473
  } catch(e) { console.error('Config load failed:', e); }
 
528
  } catch(e) { console.error(e); }
529
  }
530
 
531
+ // ═══ MCP CATALOG (Rich UI) ═══
532
+
533
+ async function loadMcpCatalog() {
534
  try {
535
+ var resp = await fetch('/api/mcp_catalog');
536
  var data = await resp.json();
537
+ state.mcpServers = data.servers || [];
538
+ renderMcpCatalog();
539
+ updateMcpPill();
540
+ } catch(e) {
541
+ document.getElementById('mcp-catalog').innerHTML = '<div class="mcp-empty">MCP catalog unavailable.</div>';
542
+ }
543
+ }
544
+
545
+ function updateMcpPill() {
546
+ var servers = state.mcpServers;
547
+ var connected = servers.filter(function(s) { return s.connected; });
548
+ var dot = document.getElementById('mcp-dot');
549
+ var text = document.getElementById('mcp-pill-text');
550
+ dot.className = connected.length > 0 ? 'dot connected' : 'dot disconnected';
551
+ text.textContent = 'MCP ' + (connected.length > 0 ? '(' + connected.length + ')' : '');
552
+ }
553
+
554
+ function filterMcpCat(cat) {
555
+ state.activeMcpCat = cat;
556
+ document.querySelectorAll('.mcp-cat-tab').forEach(function(t) {
557
+ t.classList.toggle('active', t.dataset.cat === cat);
558
+ });
559
+ renderMcpCatalog();
560
+ }
561
+
562
+ function renderMcpCatalog() {
563
+ var cat = state.activeMcpCat;
564
+ var servers = state.mcpServers.filter(function(s) {
565
+ return cat === 'all' || s.category === cat;
566
+ });
567
+
568
+ // Update stats
569
+ var totalServers = state.mcpServers.length;
570
+ var connectedServers = state.mcpServers.filter(function(s) { return s.connected; }).length;
571
+ var totalTools = state.mcpServers.reduce(function(sum, s) { return sum + (s.tool_count || 0); }, 0);
572
+ document.getElementById('mcp-total-count').textContent = totalServers;
573
+ document.getElementById('mcp-connected-count').textContent = connectedServers;
574
+ document.getElementById('mcp-tools-count').textContent = totalTools;
575
+
576
+ if (servers.length === 0) {
577
+ document.getElementById('mcp-catalog').innerHTML = '<div class="mcp-empty">No servers in this category.</div>';
578
+ return;
579
+ }
580
+
581
+ var html = '';
582
+ servers.forEach(function(s) {
583
+ var isOn = s.connected;
584
+ html += '<div class="mcp-card" id="mcp-card-' + escHtml(s.key) + '">';
585
+ html += '<div class="mcp-card-head">';
586
+ html += '<span class="mcp-card-icon">' + (s.icon || '') + '</span>';
587
+ html += '<div class="mcp-card-info">';
588
+ html += '<div class="mcp-card-name">' + escHtml(s.name) + '</div>';
589
+ html += '<div class="mcp-card-desc">' + escHtml(s.description) + '</div>';
590
+ html += '</div>';
591
+ html += '<div class="mcp-card-actions">';
592
+ html += '<span class="mcp-card-status ' + (isOn ? 'on' : 'off') + '" id="mcp-status-' + escHtml(s.key) + '"></span>';
593
+ html += '<label class="mcp-toggle" title="' + (isOn ? 'Disable' : 'Enable') + ' ' + escHtml(s.name) + '">';
594
+ html += '<input type="checkbox" ' + (isOn ? 'checked' : '') + ' onchange="toggleMcpServer(\'' + escHtml(s.key) + '\', this.checked)">';
595
+ html += '<span class="mcp-toggle-slider"></span>';
596
+ html += '</label>';
597
+ html += '<button class="mcp-card-expand" onclick="toggleMcpCard(\'' + escHtml(s.key) + '\')">&#9660; Info</button>';
598
+ html += '</div>';
599
+ html += '</div>';
600
+ html += '<div class="mcp-card-details" id="mcp-details-' + escHtml(s.key) + '">';
601
+ if (s.install_hint) {
602
+ html += '<div class="mcp-detail-row"><span class="mcp-detail-label">Install:</span><span class="mcp-detail-value" style="color:var(--cyan)">' + escHtml(s.install_hint) + '</span></div>';
603
  }
604
+ html += '<div class="mcp-detail-row"><span class="mcp-detail-label">Transport:</span><span class="mcp-detail-value">' + escHtml(s.transport) + '</span></div>';
605
+ if (s.command) {
606
+ html += '<div class="mcp-detail-row"><span class="mcp-detail-label">Command:</span><span class="mcp-detail-value">' + escHtml(s.command + ' ' + (s.args || []).join(' ')) + '</span></div>';
607
+ }
608
+ if (s.repo_url) {
609
+ html += '<div class="mcp-detail-row"><span class="mcp-detail-label">Repo:</span><span class="mcp-detail-value"><a href="' + escHtml(s.repo_url) + '" target="_blank" rel="noopener">' + escHtml(s.repo_url) + '</a></span></div>';
610
+ }
611
+ if (s.tool_count > 0) {
612
+ html += '<div class="mcp-card-tools"><span class="mcp-tool-chip">' + s.tool_count + ' tools</span></div>';
613
+ }
614
+ html += '<div id="mcp-error-' + escHtml(s.key) + '"></div>';
615
+ html += '</div>';
616
+ html += '</div>';
617
+ });
618
+
619
+ document.getElementById('mcp-catalog').innerHTML = html;
620
+ }
621
+
622
+ function toggleMcpCard(key) {
623
+ var card = document.getElementById('mcp-card-' + key);
624
+ if (card) card.classList.toggle('open');
625
+ }
626
+
627
+ async function toggleMcpServer(key, enabled) {
628
+ var errorEl = document.getElementById('mcp-error-' + key);
629
+ var statusEl = document.getElementById('mcp-status-' + key);
630
+ if (errorEl) errorEl.innerHTML = '<span class="mcp-connecting">' + (enabled ? 'Connecting...' : 'Disconnecting...') + '</span>';
631
+
632
+ try {
633
+ var resp = await fetch('/api/mcp_toggle', {
634
+ method: 'POST',
635
+ headers: {'Content-Type': 'application/json'},
636
+ body: JSON.stringify({ server_key: key, enabled: enabled }),
637
  });
638
+ var data = await resp.json();
639
+ if (data.error) {
640
+ if (errorEl) errorEl.innerHTML = '<div class="mcp-card-error">' + escHtml(data.error) + '</div>';
641
+ // Revert toggle
642
+ var card = document.getElementById('mcp-card-' + key);
643
+ if (card) {
644
+ var cb = card.querySelector('input[type=checkbox]');
645
+ if (cb) cb.checked = !enabled;
646
+ }
647
+ if (statusEl) statusEl.className = 'mcp-card-status off';
648
+ } else {
649
+ if (errorEl) errorEl.innerHTML = '';
650
+ if (statusEl) statusEl.className = 'mcp-card-status ' + (enabled ? 'on' : 'off');
651
+ // Refresh to get updated tool counts
652
+ setTimeout(loadMcpCatalog, 1500);
653
+ }
654
  } catch(e) {
655
+ if (errorEl) errorEl.innerHTML = '<div class="mcp-card-error">Connection failed: ' + escHtml(e.message) + '</div>';
656
+ var card = document.getElementById('mcp-card-' + key);
657
+ if (card) {
658
+ var cb = card.querySelector('input[type=checkbox]');
659
+ if (cb) cb.checked = !enabled;
660
+ }
661
+ if (statusEl) statusEl.className = 'mcp-card-status off';
662
  }
663
+ updateMcpPill();
664
  }
665
 
666
  function renderExamples(config) {
 
696
  document.querySelectorAll('.tab-pane').forEach(function(p) {
697
  p.classList.toggle('active', p.id === 'pane-' + tab);
698
  });
699
+ if (tab === 'mcp') loadMcpCatalog();
700
  }
701
 
702
  // ═══ FULLSCREEN ═══
 
732
  var bodyClass = role === 'assistant' ? '<div class="msg-body">' : '';
733
  var bodyClose = role === 'assistant' ? '</div>' : '';
734
  var iterationHtml = extra.iteration ? '<div class="iteration-badge">Iteration ' + extra.iteration + '/' + extra.max_iterations + '</div>' : '';
735
+ div.innerHTML = iterationHtml + prefix + '<span class="msg-content">' + renderMd(content) + '</span>' + bodyClass + bodyClose;
736
  container.appendChild(div);
737
  container.scrollTop = container.scrollHeight;
738
  return div;
 
741
  function renderMd(text) {
742
  if (!text) return '';
743
  text = escHtml(text);
 
744
  text = text.replace(/```(\w*)\n([\s\S]*?)```/g, function(_, lang, code) {
745
  var id = 'cb_' + Date.now() + Math.random().toString(36).substr(2,4);
746
  return '<div class="code-block-wrap"><div class="code-block-header"><span class="code-lang">' + (lang||'code') + '</span><button class="btn-copy" onclick="copyBlock(\'' + id + '\')">&#128203; Copy</button></div><pre><code id="' + id + '">' + code.trim() + '</code></pre></div>';
 
784
  var div = document.createElement('div');
785
  div.className = 'tool-block open';
786
  div.id = id;
787
+
788
  var argsStr = '';
789
  if (args) {
790
  Object.entries(args).forEach(function(e) {
 
792
  argsStr += escHtml(e[0] + ': ' + v) + '\n';
793
  });
794
  }
795
+
796
  div.innerHTML =
797
  '<div class="tool-header">' +
798
  '<span class="tool-icon">&#9889;</span>' +
 
801
  '</div>' +
802
  '<div class="tool-args">' + argsStr + '</div>' +
803
  '<div class="tool-result"></div>';
804
+
805
  container.appendChild(div);
806
  container.scrollTop = container.scrollHeight;
807
  state.toolCalls[id] = div;
 
813
  if (!div) return;
814
  var badge = div.querySelector('.tool-badge');
815
  var result = div.querySelector('.tool-result');
816
+
817
  if (success) {
818
  badge.className = 'tool-badge success';
819
  badge.textContent = 'DONE';
 
821
  badge.className = 'tool-badge error';
822
  badge.textContent = 'ERROR';
823
  }
824
+
825
  if (output) {
826
  result.textContent = output;
827
  result.className = success ? 'tool-result' : 'tool-result error-output';
 
875
  body: JSON.stringify({ message: message, session_id: state.sessionId }),
876
  });
877
  if (!resp.ok) throw new Error('HTTP ' + resp.status);
878
+
879
  var reader = resp.body.getReader();
880
  var decoder = new TextDecoder();
881
  var buffer = '';
882
+
883
  while (true) {
884
  var result = await reader.read();
885
  if (result.done) break;
886
  buffer += decoder.decode(result.value, {stream: true});
887
  var lines = buffer.split('\n');
888
  buffer = lines.pop() || '';
889
+
890
  for (var i = 0; i < lines.length; i++) {
891
  var line = lines[i];
892
  if (!line.trim()) continue;
 
909
  break;
910
  }
911
  }
 
912
  if (ev.result.output && (ev.result.output.includes('<!DOCTYPE') || ev.result.output.includes('<html'))) {
913
  showPreview(ev.result.output);
914
  }
 
941
  setStatus('status-error', 'CONNECTION ERROR');
942
  }
943
  }
944
+
945
  state.isGenerating = false;
946
  document.getElementById('btn-send').style.display = 'flex';
947
  document.getElementById('btn-stop').style.display = 'none';
requirements.txt CHANGED
@@ -1,6 +1,8 @@
1
  # SoniCoder v2 β€” Optimized AI Code Agent
2
  # Architecture inspired by Gemini CLI and Claude Code
3
 
 
 
4
  gradio==6.19.0
5
  transformers>=4.45.0
6
  torch>=2.1.0
@@ -10,8 +12,8 @@ requests>=2.31.0
10
  beautifulsoup4>=4.12.0
11
  Pillow>=10.0
12
 
13
- # MCP support
14
- mcp>=1.0.0
15
 
16
  # Optional: VLM support
17
  # torchvision>=0.16.0
 
1
  # SoniCoder v2 β€” Optimized AI Code Agent
2
  # Architecture inspired by Gemini CLI and Claude Code
3
 
4
+ fastapi>=0.115.0
5
+ uvicorn[standard]>=0.30.0
6
  gradio==6.19.0
7
  transformers>=4.45.0
8
  torch>=2.1.0
 
12
  beautifulsoup4>=4.12.0
13
  Pillow>=10.0
14
 
15
+ # MCP support (optional β€” servers work without it)
16
+ # mcp>=1.0.0
17
 
18
  # Optional: VLM support
19
  # torchvision>=0.16.0
sonicoder/config/__init__.py CHANGED
@@ -163,6 +163,236 @@ MODEL_REGISTRY: dict[str, ModelConfig] = {
163
  }
164
 
165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  def get_available_models() -> dict[str, ModelConfig]:
167
  """Return all registered models."""
168
  return MODEL_REGISTRY.copy()
 
163
  }
164
 
165
 
166
+ # ── MCP Server Catalog ─────────────────────────────────────────────────
167
+ # Pre-configured MCP servers users can enable from the UI.
168
+
169
+ MCP_CATALOG: list[dict[str, Any]] = [
170
+ {
171
+ "key": "filesystem",
172
+ "name": "Filesystem",
173
+ "description": "Read, write, and manage files on the server. Full file system access with safe path resolution.",
174
+ "category": "core",
175
+ "icon": "πŸ“",
176
+ "transport": "stdio",
177
+ "command": "npx",
178
+ "args": ["-y", "@anthropic/mcp-filesystem", "."],
179
+ "install_hint": "npx -y @anthropic/mcp-filesystem",
180
+ "repo_url": "https://github.com/anthropics/mcp-filesystem",
181
+ },
182
+ {
183
+ "key": "fetch",
184
+ "name": "Fetch / HTTP",
185
+ "description": "Make HTTP requests to any URL. Fetch web pages, APIs, and resources for the agent to read.",
186
+ "category": "core",
187
+ "icon": "🌐",
188
+ "transport": "stdio",
189
+ "command": "npx",
190
+ "args": ["-y", "@anthropic/mcp-fetch"],
191
+ "install_hint": "npx -y @anthropic/mcp-fetch",
192
+ "repo_url": "https://github.com/anthropics/mcp-fetch",
193
+ },
194
+ {
195
+ "key": "memory",
196
+ "name": "Memory / Knowledge Graph",
197
+ "description": "Persistent knowledge graph memory. Store and retrieve facts, entities, and relationships across sessions.",
198
+ "category": "core",
199
+ "icon": "🧠",
200
+ "transport": "stdio",
201
+ "command": "npx",
202
+ "args": ["-y", "@anthropic/mcp-memory"],
203
+ "install_hint": "npx -y @anthropic/mcp-memory",
204
+ "repo_url": "https://github.com/anthropics/mcp-memory",
205
+ },
206
+ {
207
+ "key": "sequential-thinking",
208
+ "name": "Sequential Thinking",
209
+ "description": "Dynamic thinking and reflection. Break down complex problems into ordered reasoning steps.",
210
+ "category": "reasoning",
211
+ "icon": "πŸ’­",
212
+ "transport": "stdio",
213
+ "command": "npx",
214
+ "args": ["-y", "@anthropic/mcp-sequential-thinking"],
215
+ "install_hint": "npx -y @anthropic/mcp-sequential-thinking",
216
+ "repo_url": "https://github.com/anthropics/mcp-sequential-thinking",
217
+ },
218
+ {
219
+ "key": "github",
220
+ "name": "GitHub",
221
+ "description": "Search repos, manage issues/PRs, create branches, and interact with the GitHub API directly.",
222
+ "category": "dev-tools",
223
+ "icon": "πŸ™",
224
+ "transport": "stdio",
225
+ "command": "npx",
226
+ "args": ["-y", "@anthropic/mcp-github"],
227
+ "env": {"GITHUB_TOKEN": ""},
228
+ "install_hint": "Set GITHUB_TOKEN env var first, then: npx -y @anthropic/mcp-github",
229
+ "repo_url": "https://github.com/anthropics/mcp-github",
230
+ },
231
+ {
232
+ "key": "git",
233
+ "name": "Git",
234
+ "description": "Git operations: diff, log, status, branch management, and commit history analysis.",
235
+ "category": "dev-tools",
236
+ "icon": "πŸ”€",
237
+ "transport": "stdio",
238
+ "command": "npx",
239
+ "args": ["-y", "@anthropic/mcp-git"],
240
+ "install_hint": "npx -y @anthropic/mcp-git",
241
+ "repo_url": "https://github.com/anthropics/mcp-git",
242
+ },
243
+ {
244
+ "key": "docker",
245
+ "name": "Docker",
246
+ "description": "Manage Docker containers, images, and compose stacks. Build, run, and inspect containers.",
247
+ "category": "dev-tools",
248
+ "icon": "🐳",
249
+ "transport": "stdio",
250
+ "command": "npx",
251
+ "args": ["-y", "@anthropic/mcp-docker"],
252
+ "install_hint": "npx -y @anthropic/mcp-docker (requires Docker daemon)",
253
+ "repo_url": "https://github.com/anthropics/mcp-docker",
254
+ },
255
+ {
256
+ "key": "postgres",
257
+ "name": "PostgreSQL",
258
+ "description": "Read/write PostgreSQL databases. Run queries, inspect schemas, and manage tables.",
259
+ "category": "database",
260
+ "icon": "🐘",
261
+ "transport": "stdio",
262
+ "command": "npx",
263
+ "args": ["-y", "@anthropic/mcp-postgres"],
264
+ "env": {"DATABASE_URL": "postgresql://localhost:5432/mydb"},
265
+ "install_hint": "Set DATABASE_URL, then: npx -y @anthropic/mcp-postgres",
266
+ "repo_url": "https://github.com/anthropics/mcp-postgres",
267
+ },
268
+ {
269
+ "key": "sqlite",
270
+ "name": "SQLite",
271
+ "description": "Read/write SQLite databases. Query tables, inspect schemas, run migrations.",
272
+ "category": "database",
273
+ "icon": "πŸ—ƒοΈ",
274
+ "transport": "stdio",
275
+ "command": "npx",
276
+ "args": ["-y", "@anthropic/mcp-sqlite", "--db-path", "./workspace/data.db"],
277
+ "install_hint": "npx -y @anthropic/mcp-sqlite --db-path ./data.db",
278
+ "repo_url": "https://github.com/anthropics/mcp-sqlite",
279
+ },
280
+ {
281
+ "key": "redis",
282
+ "name": "Redis",
283
+ "description": "Redis cache and data structure operations. Get/set keys, manage lists, sets, and hashes.",
284
+ "category": "database",
285
+ "icon": "πŸ”΄",
286
+ "transport": "stdio",
287
+ "command": "npx",
288
+ "args": ["-y", "@anthropic/mcp-redis"],
289
+ "env": {"REDIS_URL": "redis://localhost:6379"},
290
+ "install_hint": "Set REDIS_URL, then: npx -y @anthropic/mcp-redis",
291
+ "repo_url": "https://github.com/anthropics/mcp-redis",
292
+ },
293
+ {
294
+ "key": "brave-search",
295
+ "name": "Brave Search",
296
+ "description": "Web search powered by Brave. Search the web, get AI summaries, and fetch page contents.",
297
+ "category": "search",
298
+ "icon": "πŸ”",
299
+ "transport": "stdio",
300
+ "command": "npx",
301
+ "args": ["-y", "@anthropic/mcp-brave-search"],
302
+ "env": {"BRAVE_API_KEY": ""},
303
+ "install_hint": "Set BRAVE_API_KEY, then: npx -y @anthropic/mcp-brave-search",
304
+ "repo_url": "https://github.com/anthropics/mcp-brave-search",
305
+ },
306
+ {
307
+ "key": "puppeteer",
308
+ "name": "Puppeteer / Browser",
309
+ "description": "Headless browser automation. Navigate pages, take screenshots, extract content, and fill forms.",
310
+ "category": "browser",
311
+ "icon": "🎭",
312
+ "transport": "stdio",
313
+ "command": "npx",
314
+ "args": ["-y", "@anthropic/mcp-puppeteer"],
315
+ "install_hint": "npx -y @anthropic/mcp-puppeteer (requires Chromium)",
316
+ "repo_url": "https://github.com/anthropics/mcp-puppeteer",
317
+ },
318
+ {
319
+ "key": "slack",
320
+ "name": "Slack",
321
+ "description": "Send messages, read channels, and manage Slack workspaces via the Slack API.",
322
+ "category": "integrations",
323
+ "icon": "πŸ’¬",
324
+ "transport": "stdio",
325
+ "command": "npx",
326
+ "args": ["-y", "@anthropic/mcp-slack"],
327
+ "env": {"SLACK_BOT_TOKEN": "", "SLACK_TEAM_ID": ""},
328
+ "install_hint": "Set SLACK_BOT_TOKEN + SLACK_TEAM_ID, then: npx -y @anthropic/mcp-slack",
329
+ "repo_url": "https://github.com/anthropics/mcp-slack",
330
+ },
331
+ {
332
+ "key": "context7",
333
+ "name": "Context7 / Docs",
334
+ "description": "Look up library documentation. Get up-to-date docs for npm packages, Python libs, and more.",
335
+ "category": "search",
336
+ "icon": "πŸ“š",
337
+ "transport": "stdio",
338
+ "command": "npx",
339
+ "args": ["-y", "@anthropic/mcp-context7"],
340
+ "install_hint": "npx -y @anthropic/mcp-context7",
341
+ "repo_url": "https://github.com/anthropics/mcp-context7",
342
+ },
343
+ {
344
+ "key": "everything",
345
+ "name": "Everything (Meta)",
346
+ "description": "Combines multiple MCP tools into one server: filesystem, fetch, memory, and more.",
347
+ "category": "core",
348
+ "icon": "⚑",
349
+ "transport": "stdio",
350
+ "command": "npx",
351
+ "args": ["-y", "@anthropic/mcp-everything"],
352
+ "install_hint": "npx -y @anthropic/mcp-everything",
353
+ "repo_url": "https://github.com/anthropics/mcp-everything",
354
+ },
355
+ {
356
+ "key": "sentry",
357
+ "name": "Sentry",
358
+ "description": "Query Sentry for error tracking. List issues, get stack traces, and manage projects.",
359
+ "category": "integrations",
360
+ "icon": "πŸ›‘οΈ",
361
+ "transport": "stdio",
362
+ "command": "npx",
363
+ "args": ["-y", "@anthropic/mcp-sentry"],
364
+ "env": {"SENTRY_TOKEN": "", "SENTRY_ORG": "", "SENTRY_PROJECT": ""},
365
+ "install_hint": "Set SENTRY_TOKEN/ORG/PROJECT, then: npx -y @anthropic/mcp-sentry",
366
+ "repo_url": "https://github.com/anthropics/mcp-sentry",
367
+ },
368
+ {
369
+ "key": "google-maps",
370
+ "name": "Google Maps",
371
+ "description": "Search places, get directions, geocode addresses, and embed maps via Google Maps API.",
372
+ "category": "integrations",
373
+ "icon": "πŸ—ΊοΈ",
374
+ "transport": "stdio",
375
+ "command": "npx",
376
+ "args": ["-y", "@anthropic/mcp-google-maps"],
377
+ "env": {"GOOGLE_MAPS_API_KEY": ""},
378
+ "install_hint": "Set GOOGLE_MAPS_API_KEY, then: npx -y @anthropic/mcp-google-maps",
379
+ "repo_url": "https://github.com/anthropics/mcp-google-maps",
380
+ },
381
+ {
382
+ "key": "time",
383
+ "name": "Time / Timezone",
384
+ "description": "Get current time in any timezone. Convert between timezones and calculate time differences.",
385
+ "category": "utilities",
386
+ "icon": "⏰",
387
+ "transport": "stdio",
388
+ "command": "npx",
389
+ "args": ["-y", "@anthropic/mcp-time"],
390
+ "install_hint": "npx -y @anthropic/mcp-time",
391
+ "repo_url": "https://github.com/anthropics/mcp-time",
392
+ },
393
+ ]
394
+
395
+
396
  def get_available_models() -> dict[str, ModelConfig]:
397
  """Return all registered models."""
398
  return MODEL_REGISTRY.copy()
sonicoder/server/__init__.py CHANGED
@@ -1,13 +1,9 @@
1
- """Server routes β€” using gradio.Server (Gradio 6.x).
2
 
3
- Blog: https://huggingface.co/blog/introducing-gradio-server
 
4
 
5
- Pattern:
6
- - from gradio import Server β†’ app = Server()
7
- - @app.get("/") serves index.html from project root
8
- - All API endpoints use raw FastAPI routes (Server extends FastAPI)
9
- - Streaming chat uses StreamingResponse (text/event-stream)
10
- - Frontend uses plain fetch() β€” no @gradio/client needed
11
  """
12
 
13
  from __future__ import annotations
@@ -18,12 +14,12 @@ import uuid
18
  from pathlib import Path
19
  from typing import Any, Generator
20
 
21
- from fastapi import Request
22
  from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse
23
 
24
  from ..config import (
25
  Settings, get_available_models, load_settings,
26
- WORKSPACE_ROOT, CONFIG_DIR,
27
  )
28
  from ..model.loader import (
29
  get_model_status, start_background_load, switch_model as switch_model_fn,
@@ -87,15 +83,12 @@ def _json_str(data) -> JSONResponse:
87
  # ── Create App ────────────────────────────────────────────────────────
88
 
89
  def create_app():
90
- """Create and return a gradio.Server with custom HTML frontend.
91
 
92
- Blog: https://huggingface.co/blog/introducing-gradio-server
93
- Server extends FastAPI, so standard @app.get/post routes work.
94
- @app.get("/") serves index.html from project root.
95
  """
96
- from gradio import Server
97
-
98
- app = Server()
99
 
100
  # ── Serve index.html from project root ─────────────────────────────
101
 
@@ -275,6 +268,90 @@ def create_app():
275
  async def mcp_status():
276
  return _json(get_mcp_manager().get_status())
277
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  # ── Policy status ──────────────────────────────────────────────────
279
 
280
  @app.get("/api/policy_status")
 
1
+ """Server routes β€” pure FastAPI with custom HTML frontend.
2
 
3
+ Uses FastAPI directly (not gradio.Server) to guarantee our custom
4
+ index.html is served at / without Gradio's default UI taking over.
5
 
6
+ Frontend uses plain fetch() β€” no @gradio/client needed.
 
 
 
 
 
7
  """
8
 
9
  from __future__ import annotations
 
14
  from pathlib import Path
15
  from typing import Any, Generator
16
 
17
+ from fastapi import FastAPI, Request
18
  from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse
19
 
20
  from ..config import (
21
  Settings, get_available_models, load_settings,
22
+ WORKSPACE_ROOT, CONFIG_DIR, MCP_CATALOG,
23
  )
24
  from ..model.loader import (
25
  get_model_status, start_background_load, switch_model as switch_model_fn,
 
83
  # ── Create App ────────────────────────────────────────────────────────
84
 
85
  def create_app():
86
+ """Create and return a FastAPI app with custom HTML frontend.
87
 
88
+ Uses FastAPI directly (not gradio.Server) so our custom index.html
89
+ is always served at / without being overridden by Gradio's routes.
 
90
  """
91
+ app = FastAPI(title="SoniCoder v2", docs_url=None, redoc_url=None)
 
 
92
 
93
  # ── Serve index.html from project root ─────────────────────────────
94
 
 
268
  async def mcp_status():
269
  return _json(get_mcp_manager().get_status())
270
 
271
+ # ── MCP catalog (available servers to enable) ──────────────────────
272
+
273
+ @app.get("/api/mcp_catalog")
274
+ async def mcp_catalog():
275
+ """Return all available MCP servers from the catalog, with their current status."""
276
+ manager = get_mcp_manager()
277
+ status = manager.get_status()
278
+ catalog = []
279
+ for server in MCP_CATALOG:
280
+ key = server["key"]
281
+ s = status.get(key, {"connected": False, "tool_count": 0})
282
+ catalog.append({
283
+ "key": key,
284
+ "name": server["name"],
285
+ "description": server["description"],
286
+ "category": server["category"],
287
+ "icon": server.get("icon", ""),
288
+ "transport": server["transport"],
289
+ "command": server.get("command", ""),
290
+ "args": server.get("args", []),
291
+ "connected": s.get("connected", False),
292
+ "enabled": key in status,
293
+ "tool_count": s.get("tool_count", 0),
294
+ "install_hint": server.get("install_hint", ""),
295
+ "repo_url": server.get("repo_url", ""),
296
+ })
297
+ return JSONResponse(content={"servers": catalog})
298
+
299
+ # ── MCP toggle (enable/disable a server) ───────────────────────────
300
+
301
+ @app.post("/api/mcp_toggle")
302
+ async def mcp_toggle(request: Request):
303
+ body = await request.json()
304
+ server_key = body.get("server_key", "")
305
+ enabled = body.get("enabled", False)
306
+
307
+ # Find in catalog
308
+ server_cfg = None
309
+ for s in MCP_CATALOG:
310
+ if s["key"] == server_key:
311
+ server_cfg = s
312
+ break
313
+
314
+ if not server_cfg:
315
+ return JSONResponse(content={"error": f"Unknown MCP server: {server_key}"})
316
+
317
+ manager = get_mcp_manager()
318
+
319
+ if not enabled:
320
+ # Disable: remove from manager
321
+ manager.remove_server(server_key)
322
+ return JSONResponse(content={"status": "disabled", "server": server_key})
323
+ else:
324
+ # Enable: add and connect
325
+ from ..config import MCPServerConfig
326
+ config = MCPServerConfig(
327
+ name=server_key,
328
+ transport=server_cfg["transport"],
329
+ command=server_cfg.get("command"),
330
+ args=server_cfg.get("args", []),
331
+ env=server_cfg.get("env", {}),
332
+ enabled=True,
333
+ )
334
+ manager.add_server(config)
335
+ error = None
336
+ if config.transport == "stdio" and config.command:
337
+ try:
338
+ error = manager.connect_all().get(server_key)
339
+ except Exception as e:
340
+ error = str(e)
341
+ return JSONResponse(content={
342
+ "status": "enabled" if not error else "error",
343
+ "server": server_key,
344
+ "error": error,
345
+ })
346
+
347
+ # ── MCP connect all ────────────────────────────────────────────────
348
+
349
+ @app.post("/api/mcp_connect_all")
350
+ async def mcp_connect_all():
351
+ manager = get_mcp_manager()
352
+ results = manager.connect_all()
353
+ return JSONResponse(content={"results": results})
354
+
355
  # ── Policy status ──────────────────────────────────────────────────
356
 
357
  @app.get("/api/policy_status")