mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-14 10:48:35 +02:00
MCP : catalogue embarqué de serveurs connus
Ajouter un serveur MCP demandait d'écrire soi-même `npx -y @scope/paquet …` dans la modale, en devinant le nom du paquet. Le catalogue liste une vingtaine de serveurs connus, commande déjà renseignée, classés par catégorie. - internal/loki/mcp_catalog.json est EMBARQUÉ dans le binaire (go:embed), pas interrogé sur le réseau : Loki tourne hors ligne, et rien de ce qui s'exécute sur la machine ne vient d'un annuaire distant. Pour proposer un serveur de plus : éditer le fichier et recompiler. - Choisir une entrée n'installe rien. Ça préremplit la modale d'ajout existante et l'utilisateur relit la commande avant d'enregistrer — un serveur stdio exécute un process arbitraire, au même titre que l'outil bash du mode agent. Aucune ligne de mcp_client.go n'est touchée : le catalogue s'arrête à l'affichage, tout le reste passe par l'API MCP déjà en place. - Une entrée dont le runtime manque (npx ou uvx absent) le signale dans la liste, plutôt que de laisser l'utilisateur découvrir l'échec au démarrage du serveur. - Les entrées qui réclament une clé d'API la rappellent avant l'enregistrement, au lieu d'enregistrer un serveur qui ne peut pas se connecter. - Les entrées Python publiées avant le SDK MCP 2.0 sont épinglées avec `uvx --with "mcp<2"` : sans cela elles plantent sur ImportError: McpError. - Le Dockerfile gagne uv/uvx (~35 Mo). Sans lui, 9 des 21 entrées s'affichent sans pouvoir démarrer dans le conteneur. Intensité du raisonnement (REASONING_EFFORT) L'interrupteur Raisonnement était binaire. llama-server accepte `reasoning_effort` dans /v1/chat/completions : `none` coupe le raisonnement, toute autre valeur est passée au gabarit jinja du modèle. - Nouvelle clé de preset REASONING_EFFORT — donc réglée par modèle, comme le reste du preset. Vide = on n'envoie rien et le gabarit garde son comportement. - Pas de repli à prévoir : un gabarit qui ne lit pas la valeur l'ignore sans erreur. En pratique seuls gpt-oss et apparentés changent de comportement, et le sous-titre du réglage le dit — promettre un effet universel serait faux. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e602adbbee
commit
847aad217b
15 files changed
+914
-4
No files matched your search
@@ -168,7 +168,10 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
<!-- Même texte que celui rendu après chargement quand il n'y a aucun
|
||||
serveur : sinon la carte partait vide puis grandissait d'une ligne. -->
|
||||
<div id="mcp-list" class="mcp-list"><div class="muted" style="font-size:12px">(aucun serveur — ajoute un serveur fichiers, une base de données, une API…)</div></div>
|
||||
<button class="addbtn" style="margin-top:6px" onclick="openMcp('')">+ ajouter un serveur</button>
|
||||
<div class="mcp-actions">
|
||||
<button class="addbtn" onclick="openMcpCatalog()">catalogue</button>
|
||||
<button class="addbtn" onclick="openMcp('')">+ ajouter un serveur</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -574,6 +577,23 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mcp-catalog-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeMcpCatalog()">
|
||||
<div class="modal-card" style="max-width:640px">
|
||||
<div class="modal-head">
|
||||
<strong>Catalogue MCP</strong>
|
||||
<button class="modal-x" onclick="closeMcpCatalog()" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="muted" style="font-size:12px;margin-bottom:8px">
|
||||
Serveurs connus, embarqués dans Loki — aucun appel réseau pour cette liste.
|
||||
« Utiliser » remplit le formulaire d'ajout : la commande s'exécutera sur
|
||||
cette machine, relis-la avant d'enregistrer.
|
||||
</div>
|
||||
<div id="mcp-catalog-tabs" class="mcp-cat-tabs"></div>
|
||||
<div id="mcp-catalog-list" class="mcp-cat-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mcp-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeMcp()">
|
||||
<div class="modal-card" style="max-width:560px">
|
||||
<div class="modal-head">
|
||||
@@ -821,6 +841,18 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
<span class="pe-row-l">Raisonnement<span class="pe-sub" id="s-reasoning-sub">réflexion étape par étape</span></span>
|
||||
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-reasoning" onchange="cfgWriteKey('REASONING', this.checked?'on':'off')"><span class="pe-sl"></span></label></span>
|
||||
</div>
|
||||
<div class="pe-row" id="s-reasoning-effort-row">
|
||||
<span class="pe-row-l">Intensité du raisonnement<span class="pe-sub" id="s-reasoning-effort-sub">envoyée au gabarit du modèle — auto = ne rien imposer</span></span>
|
||||
<span class="pe-row-c">
|
||||
<select id="s-reasoning-effort" class="pe-val" onchange="cfgWriteKey('REASONING_EFFORT', this.value)">
|
||||
<option value="">auto</option>
|
||||
<option value="none">aucune</option>
|
||||
<option value="low">basse</option>
|
||||
<option value="medium">moyenne</option>
|
||||
<option value="high">haute</option>
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Flash attention<span class="pe-sub">plus rapide, moins de VRAM</span></span>
|
||||
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-flash" onchange="eaSetValued('--flash-attn', this.checked?'on':'')"><span class="pe-sl"></span></label></span>
|
||||
|
||||
@@ -746,6 +746,20 @@ function populateSettings(){
|
||||
rzSub.textContent = rz ? 'réflexion étape par étape'
|
||||
: 'réflexion étape par étape — non précisé : le modèle décide';
|
||||
}
|
||||
// Intensité : passée telle quelle au gabarit jinja du modèle via
|
||||
// `reasoning_effort`. Seuls les gabarits qui la lisent (gpt-oss et apparentés)
|
||||
// changent de comportement ; ailleurs elle est ignorée sans erreur — le
|
||||
// sous-titre le dit pour ne pas laisser croire à un effet garanti.
|
||||
const eff = document.getElementById('s-reasoning-effort');
|
||||
if(eff){
|
||||
eff.value = (cfgReadKey('REASONING_EFFORT')||'').toLowerCase();
|
||||
const effSub = document.getElementById('s-reasoning-effort-sub');
|
||||
if(effSub){
|
||||
effSub.textContent = eff.value
|
||||
? 'envoyée au gabarit du modèle — sans effet si son gabarit l\'ignore'
|
||||
: 'envoyée au gabarit du modèle — auto = ne rien imposer';
|
||||
}
|
||||
}
|
||||
chk('s-flash', eaHasFlag('--flash-attn') && !/^off$/i.test(eaGetValued('--flash-attn')));
|
||||
chk('s-mlock', eaHasFlag('--mlock'));
|
||||
chk('s-nommap', eaHasFlag('--no-mmap'));
|
||||
|
||||
@@ -78,6 +78,118 @@ async function toggleMcp(name, on){
|
||||
toast(on ? 'serveur activé' : 'serveur désactivé');
|
||||
}
|
||||
|
||||
// ─── Catalogue ───────────────────────────────────────────────────────────────
|
||||
// Liste embarquée de serveurs connus, servie par /api/mcp/catalog. Choisir une
|
||||
// entrée n'installe RIEN : ça ouvre la modale d'ajout préremplie, l'utilisateur
|
||||
// relit la commande (qui s'exécutera sur cette machine) puis enregistre.
|
||||
let mcpCatalog = [];
|
||||
let mcpCatalogFilter = 'Tous';
|
||||
|
||||
async function openMcpCatalog(){
|
||||
showModal('mcp-catalog-modal');
|
||||
const list = document.getElementById('mcp-catalog-list');
|
||||
if(!mcpCatalog.length){
|
||||
list.innerHTML = '<div class="muted" style="font-size:12px">chargement…</div>';
|
||||
const r = await jget('/api/mcp/catalog');
|
||||
mcpCatalog = (r && r.catalog) || [];
|
||||
}
|
||||
renderMcpCatalog();
|
||||
}
|
||||
|
||||
function closeMcpCatalog(){ hideModal('mcp-catalog-modal'); }
|
||||
|
||||
function renderMcpCatalog(){
|
||||
const list = document.getElementById('mcp-catalog-list');
|
||||
const tabs = document.getElementById('mcp-catalog-tabs');
|
||||
list.textContent = '';
|
||||
tabs.textContent = '';
|
||||
|
||||
if(!mcpCatalog.length){
|
||||
list.innerHTML = '<div class="muted" style="font-size:12px">(catalogue vide)</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const cats = ['Tous'].concat([...new Set(mcpCatalog.map(e=>e.category))].sort());
|
||||
cats.forEach(c=>{
|
||||
const b = document.createElement('button');
|
||||
b.className = 'mcp-cat'+(c===mcpCatalogFilter?' on':'');
|
||||
b.textContent = c;
|
||||
b.onclick = ()=>{ mcpCatalogFilter = c; renderMcpCatalog(); };
|
||||
tabs.appendChild(b);
|
||||
});
|
||||
|
||||
mcpCatalog
|
||||
.filter(e=>mcpCatalogFilter==='Tous' || e.category===mcpCatalogFilter)
|
||||
.forEach(e=>{
|
||||
const row = document.createElement('div');
|
||||
row.className = 'mcp-cat-row';
|
||||
|
||||
const info = document.createElement('div'); info.className = 'mcp-info';
|
||||
const nm = document.createElement('div'); nm.className = 'mcp-name';
|
||||
nm.textContent = e.name;
|
||||
if(e.installed){
|
||||
const tag = document.createElement('span'); tag.className = 'mcp-tag';
|
||||
tag.textContent = 'installé'; tag.style.marginLeft = '6px';
|
||||
nm.appendChild(tag);
|
||||
}
|
||||
const desc = document.createElement('div'); desc.className = 'mcp-cat-desc';
|
||||
desc.textContent = e.description;
|
||||
const meta = document.createElement('div'); meta.className = 'mcp-meta';
|
||||
const cmd = document.createElement('code'); cmd.className = 'mcp-cat-cmd';
|
||||
cmd.textContent = [e.command].concat(e.args||[]).join(' ');
|
||||
cmd.title = cmd.textContent;
|
||||
meta.appendChild(cmd);
|
||||
if(!e.runtimeOk){
|
||||
const w = document.createElement('span'); w.className = 'mcp-err';
|
||||
w.textContent = 'runtime '+(e.runtime||'?')+' absent';
|
||||
w.title = e.runtime==='python'
|
||||
? "uv/uvx n'est pas installé sur cette machine : ce serveur ne pourra pas démarrer."
|
||||
: "npx n'est pas installé sur cette machine : ce serveur ne pourra pas démarrer.";
|
||||
meta.appendChild(w);
|
||||
}
|
||||
if((e.secrets||[]).length){
|
||||
const s = document.createElement('span'); s.className = 'mcp-tag';
|
||||
s.textContent = 'clé requise'; s.title = e.secrets.join(', ');
|
||||
meta.appendChild(s);
|
||||
}
|
||||
info.appendChild(nm); info.appendChild(desc); info.appendChild(meta);
|
||||
|
||||
const use = document.createElement('button');
|
||||
use.className = 'addbtn mcp-cat-use';
|
||||
use.textContent = e.installed ? 'revoir' : 'utiliser';
|
||||
use.onclick = ()=>{ closeMcpCatalog(); openMcpFromCatalog(e); };
|
||||
|
||||
row.appendChild(info); row.appendChild(use);
|
||||
list.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Préremplit la modale d'ajout à partir d'une entrée du catalogue. Le nom du
|
||||
// serveur = l'id de l'entrée, pour que « déjà installé » se détecte ensuite.
|
||||
function openMcpFromCatalog(e){
|
||||
openMcp(mcpServers.some(s=>s.name===e.id) ? e.id : '');
|
||||
if(!mcpServers.some(s=>s.name===e.id)){
|
||||
document.getElementById('mcp-name').value = e.id;
|
||||
document.querySelector('input[name="mcp-transport"][value="stdio"]').checked = true;
|
||||
mcpTransportUI('stdio');
|
||||
document.getElementById('mcp-command').value = e.command || '';
|
||||
document.getElementById('mcp-args').value = (e.args||[]).join('\n');
|
||||
document.getElementById('mcp-env').value =
|
||||
Object.entries(e.env||{}).map(([k,v])=>k+'='+v).join('\n');
|
||||
document.getElementById('mcp-url').value = '';
|
||||
document.getElementById('mcp-headers').value = '';
|
||||
document.getElementById('mcp-modal-title').textContent = e.name+' — depuis le catalogue';
|
||||
}
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
if((e.secrets||[]).length){
|
||||
st.textContent = 'renseigne '+e.secrets.join(', ')+' avant d\'enregistrer';
|
||||
st.style.color = 'var(--err)';
|
||||
} else if(!e.runtimeOk){
|
||||
st.textContent = '⚠ runtime '+(e.runtime||'?')+' absent : le serveur ne démarrera pas ici';
|
||||
st.style.color = 'var(--err)';
|
||||
}
|
||||
}
|
||||
|
||||
// Ouvre la modale d'ajout (name='') ou d'édition (nom existant).
|
||||
function openMcp(name){
|
||||
mcpEditing = name || '';
|
||||
|
||||
@@ -1007,6 +1007,17 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5}
|
||||
.mcp-edit{flex:none;padding:2px 5px;font-size:13px;line-height:1;background:transparent;border:none;opacity:.5}
|
||||
.mcp-edit:hover{opacity:1;color:var(--accent)}
|
||||
/* Liste des outils MCP dans la modale */
|
||||
.mcp-actions{display:flex;gap:6px;margin-top:6px}
|
||||
.mcp-cat-tabs{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px}
|
||||
.mcp-cat{font-size:11px;padding:2px 9px;border-radius:9px;border:1px solid var(--border);background:transparent;color:var(--dim);cursor:pointer}
|
||||
.mcp-cat:hover{border-color:var(--accent);color:var(--text)}
|
||||
.mcp-cat.on{border-color:var(--accent);color:var(--accent)}
|
||||
.mcp-cat-list{display:flex;flex-direction:column;gap:4px;max-height:52vh;overflow:auto}
|
||||
.mcp-cat-row{display:flex;align-items:center;gap:8px;padding:7px 10px;border:1px solid var(--border);border-radius:6px;background:var(--panel)}
|
||||
.mcp-cat-row:hover{border-color:var(--accent)}
|
||||
.mcp-cat-desc{font-size:11px;color:var(--dim);margin-top:2px;line-height:1.35}
|
||||
.mcp-cat-cmd{font-size:10px;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:340px}
|
||||
.mcp-cat-use{flex:none}
|
||||
.mcp-tools-list{display:flex;flex-direction:column;gap:2px;max-height:240px;overflow:auto;border:1px solid var(--border);border-radius:6px;padding:6px}
|
||||
.mcp-tool-row{display:flex;align-items:center;gap:10px;padding:4px 6px;border-radius:5px;cursor:pointer}
|
||||
.mcp-tool-row:hover{background:var(--panel)}
|
||||
|
||||
Reference in new issue
Block a user