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.
- 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/engines/lmstudio.js +69 -16
- 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
|
@@ -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 (
|
|
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 (
|
|
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
|
|