llm-tuner 0.3.0 โ†’ 0.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.
@@ -0,0 +1,121 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ export const SUPPORTED_AGENTS = [
5
+ {
6
+ id: 'claude-desktop',
7
+ name: 'Claude Desktop',
8
+ format: 'json',
9
+ rootKey: 'mcpServers'
10
+ },
11
+ {
12
+ id: 'claude-code',
13
+ name: 'Claude Code',
14
+ format: 'json',
15
+ rootKey: 'mcpServers'
16
+ },
17
+ {
18
+ id: 'opencode',
19
+ name: 'OpenCode',
20
+ format: 'json',
21
+ rootKey: 'mcp',
22
+ isOpenCode: true
23
+ },
24
+ {
25
+ id: 'antigravity',
26
+ name: 'Antigravity',
27
+ format: 'json',
28
+ rootKey: 'mcpServers'
29
+ },
30
+ {
31
+ id: 'vscode',
32
+ name: 'VS Code',
33
+ format: 'json',
34
+ rootKey: 'servers'
35
+ },
36
+ {
37
+ id: 'chatgpt',
38
+ name: 'ChatGPT / Codex',
39
+ format: 'toml'
40
+ }
41
+ ];
42
+
43
+ export function resolveConfigPath(agentId, { platform = process.platform, env = process.env, homedir = os.homedir() } = {}) {
44
+ const p = platform === 'win32' ? path.win32 : path.posix;
45
+
46
+ switch (agentId) {
47
+ case 'claude-desktop': {
48
+ if (platform === 'darwin') {
49
+ return p.join(homedir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
50
+ }
51
+ if (platform === 'win32') {
52
+ const appData = env.APPDATA || p.join(homedir, 'AppData', 'Roaming');
53
+ return p.join(appData, 'Claude', 'claude_desktop_config.json');
54
+ }
55
+ const configDir = env.XDG_CONFIG_HOME || p.join(homedir, '.config');
56
+ return p.join(configDir, 'Claude', 'claude_desktop_config.json');
57
+ }
58
+ case 'claude-code': {
59
+ const userProfile = (platform === 'win32' && env.USERPROFILE) ? env.USERPROFILE : homedir;
60
+ return p.join(userProfile, '.claude.json');
61
+ }
62
+ case 'opencode': {
63
+ if (platform === 'darwin') {
64
+ return p.join(homedir, 'Library', 'Application Support', 'opencode', 'opencode.json');
65
+ }
66
+ if (platform === 'win32') {
67
+ const appData = env.APPDATA || p.join(homedir, 'AppData', 'Roaming');
68
+ return p.join(appData, 'opencode', 'opencode.json');
69
+ }
70
+ const configDir = env.XDG_CONFIG_HOME || p.join(homedir, '.config');
71
+ return p.join(configDir, 'opencode', 'opencode.json');
72
+ }
73
+ case 'antigravity': {
74
+ const userProfile = (platform === 'win32' && env.USERPROFILE) ? env.USERPROFILE : homedir;
75
+ return p.join(userProfile, '.gemini', 'config', 'mcp_config.json');
76
+ }
77
+ case 'vscode': {
78
+ if (platform === 'darwin') {
79
+ return p.join(homedir, 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
80
+ }
81
+ if (platform === 'win32') {
82
+ const appData = env.APPDATA || p.join(homedir, 'AppData', 'Roaming');
83
+ return p.join(appData, 'Code', 'User', 'mcp.json');
84
+ }
85
+ const configDir = env.XDG_CONFIG_HOME || p.join(homedir, '.config');
86
+ return p.join(configDir, 'Code', 'User', 'mcp.json');
87
+ }
88
+ case 'chatgpt': {
89
+ const userProfile = (platform === 'win32' && env.USERPROFILE) ? env.USERPROFILE : homedir;
90
+ return p.join(userProfile, '.codex', 'config.toml');
91
+ }
92
+ default:
93
+ throw new Error(`Unsupported agent ID: ${agentId}`);
94
+ }
95
+ }
96
+
97
+ export function buildMcpPayload(engineId, { customName, host, apiKey } = {}) {
98
+ switch (engineId) {
99
+ case 'lmstudio': {
100
+ return {
101
+ command: 'npx',
102
+ args: ['-y', '@mzxrai/mcp-openai@latest'],
103
+ env: {
104
+ OPENAI_BASE_URL: host || 'http://127.0.0.1:1234/v1',
105
+ OPENAI_API_KEY: apiKey || 'sk-lm-local'
106
+ }
107
+ };
108
+ }
109
+ case 'ollama': {
110
+ return {
111
+ command: 'npx',
112
+ args: ['-y', 'ollama-mcp-server@latest'],
113
+ env: {
114
+ OLLAMA_HOST: host || 'http://127.0.0.1:11434'
115
+ }
116
+ };
117
+ }
118
+ default:
119
+ throw new Error(`Unsupported engine ID: ${engineId}`);
120
+ }
121
+ }
package/src/i18n/en.js CHANGED
@@ -95,24 +95,34 @@ export default {
95
95
  'common.longPrompt': 'long prompt (~{tokens} tokens, 50% context)โ€ฆ',
96
96
  'common.presetSaved': 'Preset saved: {file}',
97
97
  'common.backups': 'Backups: {list}',
98
+ 'common.back': 'Back',
98
99
 
99
100
  // CLI
100
101
  'cli.help': `llm-tuner โ€” load your local LLM with the fastest configuration for the context you need
101
102
 
102
- Usage: llm-tuner [options]
103
+ Usage: llm-tuner [command] [options]
104
+
105
+ Commands:
106
+ /load Tune and load a model with optimal hardware settings
107
+ /mcp Connect local engine to AI coding agents as MCP server
108
+ /presets List saved presets
109
+ /settings Interactive settings menu (language and theme)
110
+ /help Show this help message
111
+ /exit Exit the interactive menu
112
+
113
+ Options:
103
114
  --engine <lmstudio|ollama> Engine
104
115
  --model <key> Model (LM Studio key or Ollama name)
105
116
  --ctx <tokens> Context (e.g. 16384)
106
- --profile <speed|balanced|quality> What to optimize for (asked in the wizard; balanced with --yes, --json, or when engine, model and context are all given)
117
+ --profile <speed|balanced|quality> What to optimize for (speed, balanced, quality)
107
118
  --force Measure again even if a preset exists
108
119
  --candidates <n> Configurations to test when measuring (3)
109
120
  --yes Do not ask for confirmation
110
121
  --dry-run Measure and show the changes without applying anything or saving a preset
111
- --presets List saved presets
122
+ --presets List saved presets (same as /presets)
112
123
  --json Final output as JSON
113
124
  --lang <en|es> Interface language (saved for next time)
114
125
  --theme <system|light|dark> Interface theme (saved for next time)
115
- /settings Interactive settings menu
116
126
 
117
127
  Presets are saved in {presetsDir}`,
118
128
  'cli.desktopStartFailed': 'Could not start the desktop app: {error}\nThe desktop runtime (Electron) is downloaded the first time you run llm-tuner-desktop, so that first run needs a network connection.\nThe terminal interface works without it: run llm-tuner.',
@@ -249,5 +259,37 @@ Presets are saved in {presetsDir}`,
249
259
  'web.updateAvailable': 'Update available: v{latest}',
250
260
  'web.updateLink': 'View release notes on GitHub',
251
261
  'web.version': 'Version {version}',
262
+ 'errors.missingParameter': 'Missing required parameter: {param}',
263
+ 'web.addAsMcp': 'Add as MCP',
264
+ 'web.addAsMcpTooltip': 'Configure this engine as an MCP server for your AI agents (Claude, OpenCode, VS Code, ChatGPT)',
265
+ 'web.mcpModalTitle': 'Add {engine} as MCP Server',
266
+ 'web.mcpServerName': 'MCP Server Name',
267
+ 'web.mcpAdvancedOptions': 'Advanced connection options',
268
+ 'web.mcpBaseUrl': 'Base URL / Host',
269
+ 'web.mcpApiKey': 'API Key',
270
+ 'web.mcpSelectAgents': 'Select AI agents to configure:',
271
+ 'web.mcpDetected': 'Detected',
272
+ 'web.mcpNotDetected': 'Available',
273
+ 'web.mcpAlreadyConfigured': 'Configured',
274
+ 'web.mcpSuccess': 'Successfully configured MCP in: {agents}',
275
+ 'web.mcpSaveBtn': 'Save & Configure',
276
+
277
+ // CLI Slash Commands
278
+ 'cli.menuTitle': 'Select a command:',
279
+ 'cli.cmdLoadDesc': 'Tune and load a model with optimal hardware settings',
280
+ 'cli.cmdMcpDesc': 'Connect local engine to AI coding agents as MCP server',
281
+ 'cli.cmdPresetsDesc': 'View saved tuning presets',
282
+ 'cli.cmdSettingsDesc': 'Configure interface language and visual theme',
283
+ 'cli.cmdHelpDesc': 'View usage help and CLI flags',
284
+ 'cli.cmdExitDesc': 'Exit',
285
+ 'cli.mcpSelectEngine': 'Which engine do you want to configure as an MCP server?',
286
+ 'cli.mcpSelectAgents': 'Select AI agents to configure (space to toggle, enter to confirm):',
287
+ 'cli.mcpServerNamePrompt': 'MCP server name:',
288
+ 'cli.mcpNoAgentsSelected': 'No agents selected. Exiting.',
289
+ 'cli.mcpSuccess': "Configured MCP server '{server}' in: {agents}",
290
+ 'cli.mcpFailed': 'Failed to configure MCP in: {agents}',
291
+ 'cli.mcpStatusInstalled': 'Installed',
292
+ 'cli.mcpStatusDetected': 'Detected',
293
+ 'cli.mcpStatusAvailable': 'Available',
252
294
  };
253
295
 
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 (se pregunta en el asistente; balanced con --yes, --json, o si se indican motor, modelo y contexto)
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
 
@@ -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
- box.appendChild(btn);
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>
@@ -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