llm-tuner 0.3.0 → 0.4.0
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.
- package/README.es.md +19 -3
- package/README.md +19 -3
- package/package.json +1 -1
- package/src/cli/animation.js +85 -0
- package/src/cli/colors.js +12 -0
- package/src/cli/commands/help.js +7 -0
- package/src/cli/commands/load.js +338 -0
- package/src/cli/commands/mcp.js +118 -0
- package/src/cli/commands/presets.js +21 -0
- package/src/cli/commands/settings.js +65 -0
- package/src/cli/index.js +10 -367
- package/src/cli/nav.js +82 -0
- package/src/cli/router.js +243 -0
- package/src/core/index.js +1 -0
- package/src/core/mcp/adapters/json-adapter.js +37 -0
- package/src/core/mcp/adapters/toml-adapter.js +82 -0
- package/src/core/mcp/index.js +82 -0
- package/src/core/mcp/registry.js +121 -0
- package/src/i18n/en.js +46 -4
- package/src/i18n/es.js +46 -4
- package/src/web/public/app.js +172 -1
- package/src/web/public/index.html +33 -0
- package/src/web/public/style.css +29 -0
- package/src/web/server.js +20 -1
package/src/i18n/es.js
CHANGED
|
@@ -95,24 +95,34 @@ export default {
|
|
|
95
95
|
'common.longPrompt': 'prompt largo (~{tokens} tokens, 50% del contexto)…',
|
|
96
96
|
'common.presetSaved': 'Preset guardado: {file}',
|
|
97
97
|
'common.backups': 'Copias de seguridad: {list}',
|
|
98
|
+
'common.back': 'Volver',
|
|
98
99
|
|
|
99
100
|
// CLI
|
|
100
101
|
'cli.help': `llm-tuner — carga tu LLM local con la configuración más rápida para el contexto que necesitas
|
|
101
102
|
|
|
102
|
-
Uso: llm-tuner [opciones]
|
|
103
|
+
Uso: llm-tuner [comando] [opciones]
|
|
104
|
+
|
|
105
|
+
Comandos:
|
|
106
|
+
/load Ajustar y cargar un modelo con la configuración óptima
|
|
107
|
+
/mcp Conectar motor local con agentes de IA como servidor MCP
|
|
108
|
+
/presets Listar presets guardados
|
|
109
|
+
/settings Menú interactivo de configuración (idioma y tema)
|
|
110
|
+
/help Mostrar esta ayuda
|
|
111
|
+
/exit Salir del menú interactivo
|
|
112
|
+
|
|
113
|
+
Opciones:
|
|
103
114
|
--engine <lmstudio|ollama> Motor
|
|
104
115
|
--model <clave> Modelo (clave de LM Studio o nombre de Ollama)
|
|
105
116
|
--ctx <tokens> Contexto (p. ej. 16384)
|
|
106
|
-
--profile <speed|balanced|quality> Qué optimizar (
|
|
117
|
+
--profile <speed|balanced|quality> Qué optimizar (velocidad, equilibrado, calidad)
|
|
107
118
|
--force Volver a medir aunque exista un preset
|
|
108
119
|
--candidates <n> Configuraciones a probar al medir (3)
|
|
109
120
|
--yes No pedir confirmación
|
|
110
121
|
--dry-run Medir y mostrar cambios sin aplicar nada ni guardar preset
|
|
111
|
-
--presets Listar presets guardados
|
|
122
|
+
--presets Listar presets guardados (igual que /presets)
|
|
112
123
|
--json Salida final en JSON
|
|
113
124
|
--lang <en|es> Idioma de la interfaz (se guarda para la próxima vez)
|
|
114
125
|
--theme <system|light|dark> Tema de la interfaz (se guarda para la próxima)
|
|
115
|
-
/settings Menú interactivo de configuración
|
|
116
126
|
|
|
117
127
|
Los presets se guardan en {presetsDir}`,
|
|
118
128
|
'cli.desktopStartFailed': 'No se pudo iniciar la aplicación de escritorio: {error}\nEl entorno de escritorio (Electron) se descarga la primera vez que ejecutas llm-tuner-desktop, así que esa primera ejecución necesita conexión a la red.\nLa interfaz de terminal funciona sin él: ejecuta llm-tuner.',
|
|
@@ -249,5 +259,37 @@ Los presets se guardan en {presetsDir}`,
|
|
|
249
259
|
'web.updateAvailable': 'Actualización disponible: v{latest}',
|
|
250
260
|
'web.updateLink': 'Ver notas de la versión en GitHub',
|
|
251
261
|
'web.version': 'Versión {version}',
|
|
262
|
+
'errors.missingParameter': 'Falta el parámetro requerido: {param}',
|
|
263
|
+
'web.addAsMcp': 'Agregar como MCP',
|
|
264
|
+
'web.addAsMcpTooltip': 'Configurar este engine como servidor MCP para tus agentes de IA (Claude, OpenCode, VS Code, ChatGPT)',
|
|
265
|
+
'web.mcpModalTitle': 'Agregar {engine} como servidor MCP',
|
|
266
|
+
'web.mcpServerName': 'Nombre del servidor MCP',
|
|
267
|
+
'web.mcpAdvancedOptions': 'Opciones avanzadas de conexión',
|
|
268
|
+
'web.mcpBaseUrl': 'URL base / Host',
|
|
269
|
+
'web.mcpApiKey': 'API Key',
|
|
270
|
+
'web.mcpSelectAgents': 'Seleccionar agentes de IA para configurar:',
|
|
271
|
+
'web.mcpDetected': 'Detectado',
|
|
272
|
+
'web.mcpNotDetected': 'Disponible',
|
|
273
|
+
'web.mcpAlreadyConfigured': 'Configurado',
|
|
274
|
+
'web.mcpSuccess': 'MCP configurado con éxito en: {agents}',
|
|
275
|
+
'web.mcpSaveBtn': 'Guardar y configurar',
|
|
276
|
+
|
|
277
|
+
// CLI Slash Commands
|
|
278
|
+
'cli.menuTitle': 'Selecciona un comando:',
|
|
279
|
+
'cli.cmdLoadDesc': 'Ajustar y cargar un modelo con la configuración óptima',
|
|
280
|
+
'cli.cmdMcpDesc': 'Conectar motor local con agentes de IA como servidor MCP',
|
|
281
|
+
'cli.cmdPresetsDesc': 'Ver presets de configuración guardados',
|
|
282
|
+
'cli.cmdSettingsDesc': 'Configurar idioma y tema de la interfaz',
|
|
283
|
+
'cli.cmdHelpDesc': 'Ver ayuda de uso y opciones',
|
|
284
|
+
'cli.cmdExitDesc': 'Salir',
|
|
285
|
+
'cli.mcpSelectEngine': '¿Qué motor quieres configurar como servidor MCP?',
|
|
286
|
+
'cli.mcpSelectAgents': 'Selecciona los agentes de IA a configurar (espacio para marcar, enter para confirmar):',
|
|
287
|
+
'cli.mcpServerNamePrompt': 'Nombre del servidor MCP:',
|
|
288
|
+
'cli.mcpNoAgentsSelected': 'No se seleccionó ningún agente. Saliendo.',
|
|
289
|
+
'cli.mcpSuccess': "Servidor MCP '{server}' configurado en: {agents}",
|
|
290
|
+
'cli.mcpFailed': 'Error al configurar MCP en: {agents}',
|
|
291
|
+
'cli.mcpStatusInstalled': 'Instalado',
|
|
292
|
+
'cli.mcpStatusDetected': 'Detectado',
|
|
293
|
+
'cli.mcpStatusAvailable': 'Disponible',
|
|
252
294
|
};
|
|
253
295
|
|
package/src/web/public/app.js
CHANGED
|
@@ -129,6 +129,160 @@ function setBusy(busy) {
|
|
|
129
129
|
$('#theme').disabled = busy;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// ---------- MCP Modal ----------
|
|
133
|
+
let activeMcpEngine = null;
|
|
134
|
+
let mcpAgentsCache = null;
|
|
135
|
+
|
|
136
|
+
function setupMcpModal() {
|
|
137
|
+
const modal = $('#mcp-modal');
|
|
138
|
+
const backdrop = $('#mcp-backdrop');
|
|
139
|
+
const closeBtn = $('#mcp-close');
|
|
140
|
+
const cancelBtn = $('#mcp-cancel');
|
|
141
|
+
const form = $('#mcp-form');
|
|
142
|
+
const nameInput = $('#mcp-server-name');
|
|
143
|
+
|
|
144
|
+
if (!modal || !backdrop) return;
|
|
145
|
+
|
|
146
|
+
function closeModal() {
|
|
147
|
+
modal.hidden = true;
|
|
148
|
+
backdrop.hidden = true;
|
|
149
|
+
activeMcpEngine = null;
|
|
150
|
+
const statusEl = $('#mcp-status');
|
|
151
|
+
if (statusEl) statusEl.hidden = true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (closeBtn) closeBtn.onclick = closeModal;
|
|
155
|
+
if (cancelBtn) cancelBtn.onclick = closeModal;
|
|
156
|
+
if (backdrop) backdrop.onclick = closeModal;
|
|
157
|
+
|
|
158
|
+
if (nameInput) {
|
|
159
|
+
nameInput.oninput = () => {
|
|
160
|
+
updateMcpAgentsList();
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (form) {
|
|
165
|
+
form.onsubmit = async (ev) => {
|
|
166
|
+
ev.preventDefault();
|
|
167
|
+
if (!activeMcpEngine) return;
|
|
168
|
+
|
|
169
|
+
const customName = (nameInput?.value || '').trim();
|
|
170
|
+
if (!customName) return;
|
|
171
|
+
|
|
172
|
+
const checkedBoxes = Array.from(document.querySelectorAll('.mcp-agent-check:checked'));
|
|
173
|
+
const agentIds = checkedBoxes.map((cb) => cb.value);
|
|
174
|
+
|
|
175
|
+
const host = $('#mcp-host')?.value.trim() || undefined;
|
|
176
|
+
const apiKey = $('#mcp-api-key')?.value.trim() || undefined;
|
|
177
|
+
const saveBtn = $('#mcp-save-btn');
|
|
178
|
+
const statusEl = $('#mcp-status');
|
|
179
|
+
|
|
180
|
+
if (saveBtn) saveBtn.disabled = true;
|
|
181
|
+
if (statusEl) {
|
|
182
|
+
statusEl.hidden = false;
|
|
183
|
+
statusEl.className = 'mcp-status';
|
|
184
|
+
statusEl.textContent = t('status.applying');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const res = await api('/api/mcp/install', {
|
|
189
|
+
engineId: activeMcpEngine.id,
|
|
190
|
+
customName,
|
|
191
|
+
agentIds,
|
|
192
|
+
host,
|
|
193
|
+
apiKey
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (res.success) {
|
|
197
|
+
if (statusEl) {
|
|
198
|
+
statusEl.className = 'mcp-status good';
|
|
199
|
+
statusEl.textContent = t('web.mcpSuccess', { agents: res.updated.join(', ') || 'none' });
|
|
200
|
+
}
|
|
201
|
+
setTimeout(() => {
|
|
202
|
+
closeModal();
|
|
203
|
+
if (saveBtn) saveBtn.disabled = false;
|
|
204
|
+
}, 1500);
|
|
205
|
+
} else {
|
|
206
|
+
const errMsgs = res.errors.map((e) => `${e.agentId}: ${e.error}`).join('; ');
|
|
207
|
+
if (statusEl) {
|
|
208
|
+
statusEl.className = 'mcp-status bad';
|
|
209
|
+
statusEl.textContent = `${t('web.error')}: ${errMsgs}`;
|
|
210
|
+
}
|
|
211
|
+
if (saveBtn) saveBtn.disabled = false;
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
if (statusEl) {
|
|
215
|
+
statusEl.className = 'mcp-status bad';
|
|
216
|
+
statusEl.textContent = err.message;
|
|
217
|
+
}
|
|
218
|
+
if (saveBtn) saveBtn.disabled = false;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function openMcpModal(engine) {
|
|
225
|
+
activeMcpEngine = engine;
|
|
226
|
+
const modal = $('#mcp-modal');
|
|
227
|
+
const backdrop = $('#mcp-backdrop');
|
|
228
|
+
const title = $('#mcp-title');
|
|
229
|
+
const nameInput = $('#mcp-server-name');
|
|
230
|
+
const hostInput = $('#mcp-host');
|
|
231
|
+
const apiKeyGroup = $('#mcp-api-key-group');
|
|
232
|
+
const apiKeyInput = $('#mcp-api-key');
|
|
233
|
+
const statusEl = $('#mcp-status');
|
|
234
|
+
|
|
235
|
+
if (title) title.textContent = t('web.mcpModalTitle', { engine: engine.name });
|
|
236
|
+
if (nameInput) nameInput.value = engine.id === 'lmstudio' ? 'lmstudio' : 'ollama';
|
|
237
|
+
|
|
238
|
+
if (engine.id === 'lmstudio') {
|
|
239
|
+
if (hostInput) hostInput.placeholder = 'http://127.0.0.1:1234/v1';
|
|
240
|
+
if (apiKeyGroup) apiKeyGroup.hidden = false;
|
|
241
|
+
if (apiKeyInput) {
|
|
242
|
+
apiKeyInput.value = '';
|
|
243
|
+
apiKeyInput.placeholder = 'sk-lm-local';
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
if (hostInput) hostInput.placeholder = 'http://127.0.0.1:11434';
|
|
247
|
+
if (apiKeyGroup) apiKeyGroup.hidden = true;
|
|
248
|
+
if (apiKeyInput) apiKeyInput.value = '';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (statusEl) statusEl.hidden = true;
|
|
252
|
+
if (backdrop) backdrop.hidden = false;
|
|
253
|
+
if (modal) modal.hidden = false;
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const data = await api('/api/mcp/agents');
|
|
257
|
+
mcpAgentsCache = data.agents || [];
|
|
258
|
+
updateMcpAgentsList();
|
|
259
|
+
} catch {
|
|
260
|
+
mcpAgentsCache = [];
|
|
261
|
+
}
|
|
262
|
+
if (nameInput) nameInput.focus();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function updateMcpAgentsList() {
|
|
266
|
+
const container = $('#mcp-agents-list');
|
|
267
|
+
if (!container || !mcpAgentsCache) return;
|
|
268
|
+
|
|
269
|
+
const currentName = ($('#mcp-server-name')?.value || '').trim();
|
|
270
|
+
container.innerHTML = '';
|
|
271
|
+
|
|
272
|
+
for (const agent of mcpAgentsCache) {
|
|
273
|
+
const isAlreadyConfigured = agent.configuredServers?.includes(currentName);
|
|
274
|
+
const row = document.createElement('label');
|
|
275
|
+
row.className = 'mcp-agent-item';
|
|
276
|
+
row.innerHTML = `
|
|
277
|
+
<input type="checkbox" class="mcp-agent-check" value="${esc(agent.id)}" ${agent.detected ? 'checked' : ''}>
|
|
278
|
+
<span class="mcp-agent-name">${esc(agent.name)}</span>
|
|
279
|
+
<span class="tag ${agent.detected ? 'prio' : ''}">${esc(agent.detected ? t('web.mcpDetected') : t('web.mcpNotDetected'))}</span>
|
|
280
|
+
${isAlreadyConfigured ? `<span class="tag bad">${esc(t('web.mcpAlreadyConfigured'))}</span>` : ''}
|
|
281
|
+
`;
|
|
282
|
+
container.appendChild(row);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
132
286
|
async function boot() {
|
|
133
287
|
const s = await api('/api/settings');
|
|
134
288
|
applyTheme(s.theme);
|
|
@@ -136,6 +290,7 @@ async function boot() {
|
|
|
136
290
|
document.documentElement.lang = s.lang;
|
|
137
291
|
applyStatic();
|
|
138
292
|
setupDrawer();
|
|
293
|
+
setupMcpModal();
|
|
139
294
|
api('/api/update').then((up) => {
|
|
140
295
|
if (up?.currentVersion) {
|
|
141
296
|
const vDisp = $('#version-display');
|
|
@@ -198,6 +353,8 @@ async function init() {
|
|
|
198
353
|
box.innerHTML = '';
|
|
199
354
|
for (const e of s.engines) {
|
|
200
355
|
const d = e.detection;
|
|
356
|
+
const card = document.createElement('div');
|
|
357
|
+
card.className = 'engine-card';
|
|
201
358
|
const btn = document.createElement('button');
|
|
202
359
|
btn.className = 'engine';
|
|
203
360
|
btn.type = 'button';
|
|
@@ -205,7 +362,21 @@ async function init() {
|
|
|
205
362
|
btn.innerHTML = `<b>${esc(e.name)}${e.id === s.recommended ? ` <span class="tag prio">${esc(t('web.recommended'))}</span>` : ''}</b>
|
|
206
363
|
<small>${d.installed ? `${esc(t('web.installed'))}${d.version ? ' · v' + esc(d.version) : ''}` : esc(t('web.notInstalled'))}</small>`;
|
|
207
364
|
btn.onclick = () => chooseEngine(e);
|
|
208
|
-
|
|
365
|
+
card.appendChild(btn);
|
|
366
|
+
|
|
367
|
+
if (d.installed) {
|
|
368
|
+
const mcpBtn = document.createElement('button');
|
|
369
|
+
mcpBtn.className = 'btn mcp-btn';
|
|
370
|
+
mcpBtn.type = 'button';
|
|
371
|
+
mcpBtn.title = t('web.addAsMcpTooltip');
|
|
372
|
+
mcpBtn.textContent = `🔗 ${t('web.addAsMcp')}`;
|
|
373
|
+
mcpBtn.onclick = (ev) => {
|
|
374
|
+
ev.stopPropagation();
|
|
375
|
+
openMcpModal(e);
|
|
376
|
+
};
|
|
377
|
+
card.appendChild(mcpBtn);
|
|
378
|
+
}
|
|
379
|
+
box.appendChild(card);
|
|
209
380
|
}
|
|
210
381
|
}
|
|
211
382
|
|
|
@@ -153,6 +153,39 @@
|
|
|
153
153
|
</div>
|
|
154
154
|
</div>
|
|
155
155
|
</aside>
|
|
156
|
+
<div id="mcp-backdrop" class="modal-backdrop" hidden></div>
|
|
157
|
+
<dialog id="mcp-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="mcp-title" hidden>
|
|
158
|
+
<div class="modal-header">
|
|
159
|
+
<h3 id="mcp-title" data-i18n="web.mcpModalTitle">Add as MCP Server</h3>
|
|
160
|
+
<button id="mcp-close" class="modal-close" type="button" aria-label="Close">✕</button>
|
|
161
|
+
</div>
|
|
162
|
+
<form id="mcp-form" class="modal-body">
|
|
163
|
+
<div class="field">
|
|
164
|
+
<label for="mcp-server-name" data-i18n="web.mcpServerName">MCP Server Name</label>
|
|
165
|
+
<input id="mcp-server-name" type="text" required pattern="[A-Za-z0-9_-]+" placeholder="e.g. pumbastudio">
|
|
166
|
+
</div>
|
|
167
|
+
<details class="mcp-advanced">
|
|
168
|
+
<summary data-i18n="web.mcpAdvancedOptions">Advanced connection options</summary>
|
|
169
|
+
<div class="field">
|
|
170
|
+
<label for="mcp-host" data-i18n="web.mcpBaseUrl">Base URL / Host</label>
|
|
171
|
+
<input id="mcp-host" type="text" placeholder="Default port">
|
|
172
|
+
</div>
|
|
173
|
+
<div class="field" id="mcp-api-key-group">
|
|
174
|
+
<label for="mcp-api-key" data-i18n="web.mcpApiKey">API Key</label>
|
|
175
|
+
<input id="mcp-api-key" type="password" placeholder="sk-lm-local">
|
|
176
|
+
</div>
|
|
177
|
+
</details>
|
|
178
|
+
<div class="field">
|
|
179
|
+
<label data-i18n="web.mcpSelectAgents">Select AI agents to configure:</label>
|
|
180
|
+
<div id="mcp-agents-list" class="mcp-agents-list"></div>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="modal-actions">
|
|
183
|
+
<button id="mcp-cancel" class="btn secondary" type="button" data-i18n="web.close">Cancel</button>
|
|
184
|
+
<button id="mcp-save-btn" class="btn primary" type="submit" data-i18n="web.mcpSaveBtn">Save & Configure</button>
|
|
185
|
+
</div>
|
|
186
|
+
<p id="mcp-status" class="mcp-status" hidden></p>
|
|
187
|
+
</form>
|
|
188
|
+
</dialog>
|
|
156
189
|
<script src="app.js" type="module"></script>
|
|
157
190
|
</body>
|
|
158
191
|
</html>
|
package/src/web/public/style.css
CHANGED
|
@@ -370,3 +370,32 @@ td .err { color: var(--bad); white-space: normal; }
|
|
|
370
370
|
font-size: .75rem;
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
/* MCP integration styles */
|
|
374
|
+
.engine-card { display: flex; flex-direction: column; gap: 6px; }
|
|
375
|
+
.mcp-btn { font-size: .8rem; padding: 6px 10px; width: 100%; border-radius: 6px; border: 1px dashed var(--line); color: var(--muted); background: var(--bg); cursor: pointer; }
|
|
376
|
+
.mcp-btn:hover { color: var(--ink); border-color: var(--accent); border-style: solid; }
|
|
377
|
+
|
|
378
|
+
.modal-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, .5); z-index: 90; }
|
|
379
|
+
.modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--bg); border: 1px solid var(--line); border-radius: 10px; width: 90%; max-width: 520px; z-index: 100; box-shadow: 0 10px 30px rgba(0, 0, 0, .3); padding: 0; display: flex; flex-direction: column; color: var(--ink); }
|
|
380
|
+
.modal[hidden] { display: none; }
|
|
381
|
+
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-bottom: 1px solid var(--line); }
|
|
382
|
+
.modal-header h3 { margin: 0; font-size: 1.1rem; }
|
|
383
|
+
.modal-close { background: transparent; border: none; font-size: 1.2rem; cursor: pointer; color: var(--muted); padding: 4px; }
|
|
384
|
+
.modal-close:hover { color: var(--ink); }
|
|
385
|
+
.modal-body { padding: 20px; display: grid; gap: 14px; overflow-y: auto; max-height: calc(85vh - 70px); }
|
|
386
|
+
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px; }
|
|
387
|
+
|
|
388
|
+
.mcp-advanced { border: 1px solid var(--line); border-radius: 6px; padding: 8px 12px; font-size: .85rem; }
|
|
389
|
+
.mcp-advanced summary { cursor: pointer; color: var(--muted); outline: none; margin-bottom: 6px; }
|
|
390
|
+
.mcp-advanced summary:hover { color: var(--ink); }
|
|
391
|
+
.mcp-advanced .field { margin-top: 8px; }
|
|
392
|
+
|
|
393
|
+
.mcp-agents-list { display: grid; gap: 8px; margin-top: 4px; max-height: 200px; overflow-y: auto; border: 1px solid var(--line); border-radius: 6px; padding: 10px; background: var(--code-bg); }
|
|
394
|
+
.mcp-agent-item { display: flex; align-items: center; gap: 10px; font-size: .9rem; cursor: pointer; }
|
|
395
|
+
.mcp-agent-item input[type=checkbox] { width: 16px; height: 16px; margin: 0; }
|
|
396
|
+
.mcp-agent-name { flex: 1; }
|
|
397
|
+
|
|
398
|
+
.mcp-status { margin: 4px 0 0; font-size: .85rem; }
|
|
399
|
+
.mcp-status.good { color: var(--accent); }
|
|
400
|
+
.mcp-status.bad { color: var(--bad); }
|
|
401
|
+
|
package/src/web/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
|
-
import { detectHardware, detectEngines, Tuner, ENGINES, installPlans, runInstall, slim, listPresets, readSettings, writeSettings, TunerError, PROFILES, checkUpdate } from '../core/index.js';
|
|
6
|
+
import { detectHardware, detectEngines, Tuner, ENGINES, installPlans, runInstall, slim, listPresets, readSettings, writeSettings, TunerError, PROFILES, checkUpdate, detectAgents, installMcpServer } from '../core/index.js';
|
|
7
7
|
import { THEMES } from '../core/settings.js';
|
|
8
8
|
import { LOCALES, messagesFor } from '../i18n/index.js';
|
|
9
9
|
import { openUrl } from '../core/installer.js';
|
|
@@ -141,6 +141,25 @@ const routes = {
|
|
|
141
141
|
const tuner = new Tuner(engine, detection);
|
|
142
142
|
return tuner.chat(model, messages);
|
|
143
143
|
},
|
|
144
|
+
|
|
145
|
+
'GET /api/mcp/agents': async () => {
|
|
146
|
+
const agents = await detectAgents();
|
|
147
|
+
return { agents };
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
'POST /api/mcp/install': async (req) => {
|
|
151
|
+
const payload = await body(req);
|
|
152
|
+
if (!payload?.engineId || !payload?.customName) {
|
|
153
|
+
throw new TunerError('errors.missingParameter', { param: 'engineId, customName' });
|
|
154
|
+
}
|
|
155
|
+
return installMcpServer({
|
|
156
|
+
engineId: payload.engineId,
|
|
157
|
+
customName: payload.customName,
|
|
158
|
+
agentIds: Array.isArray(payload.agentIds) ? payload.agentIds : [],
|
|
159
|
+
host: payload.host,
|
|
160
|
+
apiKey: payload.apiKey
|
|
161
|
+
});
|
|
162
|
+
},
|
|
144
163
|
};
|
|
145
164
|
|
|
146
165
|
|