ratacode 0.2.5
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/CREDITS.md +80 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/apreton/README.md +54 -0
- package/apreton/handshake.md +33 -0
- package/apreton/headless.md +58 -0
- package/apreton/mcp.md +221 -0
- package/apreton/navegador.md +58 -0
- package/bin/instalacion.js +148 -0
- package/bin/ratacode.js +1251 -0
- package/fabrica/settings.yaml +247 -0
- package/mcp/README.md +253 -0
- package/mcp/bin/ratacode-mcp.js +215 -0
- package/mcp/lib/actividad.js +49 -0
- package/mcp/lib/casa.js +126 -0
- package/mcp/lib/claves.js +148 -0
- package/mcp/lib/espacios.js +117 -0
- package/mcp/lib/http.js +184 -0
- package/mcp/lib/lectura.js +210 -0
- package/mcp/lib/modelos.js +235 -0
- package/mcp/lib/nucleo.js +332 -0
- package/mcp/lib/registro.js +21 -0
- package/mcp/lib/seguridad.js +283 -0
- package/mcp/lib/servidor.js +397 -0
- package/mcp/lib/tareas.js +406 -0
- package/mcp/package.json +22 -0
- package/mcp/tunel.mjs +209 -0
- package/modos/arquitecto/agent.cordis.yml +120 -0
- package/modos/arquitecto/preset.yml +3 -0
- package/modos/capataz/agent.cordis.yml +120 -0
- package/modos/capataz/preset.yml +3 -0
- package/modos/faro/agent.cordis.yml +120 -0
- package/modos/faro/preset.yml +3 -0
- package/modos/gepeto/agent.cordis.yml +191 -0
- package/modos/gepeto/preset.yml +3 -0
- package/modos/hero/agent.cordis.yml +198 -0
- package/modos/hero/preset.yml +3 -0
- package/modos/modo-rata/agent.cordis.yml +198 -0
- package/modos/modo-rata/preset.yml +3 -0
- package/modos/nex/agent.cordis.yml +213 -0
- package/modos/nex/preset.yml +3 -0
- package/modos/nex/skills/cordis-plugin-development/SKILL.md +420 -0
- package/modos/nex/skills/editing-cordis-compositions/SKILL.md +165 -0
- package/modos/pix/agent.cordis.yml +207 -0
- package/modos/pix/preset.yml +3 -0
- package/modos/tirita/agent.cordis.yml +130 -0
- package/modos/tirita/preset.yml +3 -0
- package/package.json +49 -0
- package/piel/activos/ratacode-emblema.svg +14 -0
- package/piel/activos/ratacode-es.js +1331 -0
- package/piel/activos/ratacode-identidad.css +50 -0
- package/piel/activos/ratacode-piel.css +217 -0
- package/piel/activos/ratacode-piel.js +487 -0
- package/piel/activos/ratacode-vida.js +424 -0
- package/piel/cordis.patch.yml +6 -0
- package/piel/lib/cliente.js +1020 -0
- package/piel/lib/index.js +1230 -0
- package/piel/package.json +36 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* servidor — las siete herramientas MCP de RATACODE.
|
|
3
|
+
*
|
|
4
|
+
* Esto es una capa FINA: aquí no se decide qué modelo usar (salvo el que la
|
|
5
|
+
* casa ya tiene por defecto, y se dice cuál), no se habla con ningún proveedor
|
|
6
|
+
* y no se toca ninguna clave. Todo el trabajo lo hace el core, por medio del
|
|
7
|
+
* puente (`lib/nucleo.js`).
|
|
8
|
+
*
|
|
9
|
+
* Lo que sí vive aquí son las reglas que el humano pidió: espacio cerrado,
|
|
10
|
+
* nada de acciones peligrosas sin autorización, ni una clave en ninguna
|
|
11
|
+
* respuesta, y un tope de tareas por hora para no gastar de más cuando el
|
|
12
|
+
* servidor está expuesto (sobre todo por el túnel de Cloudflare).
|
|
13
|
+
*/
|
|
14
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
import { ajustesMcp } from './casa.js';
|
|
19
|
+
import { catalogo, credencialDeProveedor, resolverRuta } from './modelos.js';
|
|
20
|
+
import { faltaLaClave } from './claves.js';
|
|
21
|
+
import { aviso } from './registro.js';
|
|
22
|
+
import { resolverEspacio, resolverModo } from './seguridad.js';
|
|
23
|
+
import { Tareas } from './tareas.js';
|
|
24
|
+
|
|
25
|
+
/** La ventana del tope de tareas. */
|
|
26
|
+
const VENTANA_MS = 3600_000;
|
|
27
|
+
|
|
28
|
+
/** El envoltorio de toda respuesta: JSON legible para cualquier agente. */
|
|
29
|
+
function comoTexto(dato) {
|
|
30
|
+
return { content: [{ type: 'text', text: JSON.stringify(dato, null, 2) }] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Un error que el agente cliente puede leer y corregir. */
|
|
34
|
+
function comoError(mensaje) {
|
|
35
|
+
return { content: [{ type: 'text', text: 'ERROR: ' + mensaje }], isError: true };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Envolver un manejador para que un fallo salga como error de herramienta. */
|
|
39
|
+
function conRed(funcion) {
|
|
40
|
+
return async (args, extra) => {
|
|
41
|
+
try {
|
|
42
|
+
return await funcion(args, extra);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return comoError(e instanceof Error ? e.message : String(e));
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Las marcas del tope por hora, EN LA CASA (`<casa>\mcp\marcas.json`).
|
|
51
|
+
* Antes vivían sólo en memoria: dos servidores, o un reinicio, multiplicaban el
|
|
52
|
+
* tope. Al arrancar se cuentan las de la última hora; al lanzar una tarea se
|
|
53
|
+
* apunta y se guarda.
|
|
54
|
+
*/
|
|
55
|
+
function cargarMarcas(casa, ahora = Date.now()) {
|
|
56
|
+
try {
|
|
57
|
+
const leidas = JSON.parse(readFileSync(join(casa, 'mcp', 'marcas.json'), 'utf8'));
|
|
58
|
+
if (Array.isArray(leidas)) {
|
|
59
|
+
return leidas
|
|
60
|
+
.filter((t) => typeof t === 'number' && Number.isFinite(t) && ahora - t <= VENTANA_MS)
|
|
61
|
+
.sort((a, b) => a - b);
|
|
62
|
+
}
|
|
63
|
+
} catch { /* sin fichero (o roto): no hay marcas */ }
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Guardar las marcas. Si no se puede escribir, la tarea sigue: no es un requisito. */
|
|
68
|
+
function guardarMarcas(casa, marcas) {
|
|
69
|
+
try {
|
|
70
|
+
mkdirSync(join(casa, 'mcp'), { recursive: true });
|
|
71
|
+
writeFileSync(join(casa, 'mcp', 'marcas.json'), JSON.stringify(marcas) + '\n', { mode: 0o600 });
|
|
72
|
+
} catch { /* el cuaderno no puede tumbar el servicio */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** El texto que el cliente MCP lee al conectar: cómo se usa esto. */
|
|
76
|
+
function instrucciones() {
|
|
77
|
+
return [
|
|
78
|
+
'RATACODE está disponible como servidor MCP: úsalo para delegar trabajos a los modelos configurados en esta máquina.',
|
|
79
|
+
'',
|
|
80
|
+
'Antes de ejecutar una tarea:',
|
|
81
|
+
'1. consulta list_models',
|
|
82
|
+
'2. lanza run_task con el modelo que te hayan pedido',
|
|
83
|
+
'3. consulta get_task_status hasta que termine',
|
|
84
|
+
'4. recoge el resultado con get_task_result',
|
|
85
|
+
'',
|
|
86
|
+
'Si no vas a poder volver (un chat de una sola vuelta, un `claude -p`, un `codex exec`), NO uses ese ciclo: lanza run_task con `esperar_segundos` (1-600) y la MISMA llamada te espera y te devuelve el resultado completo. Si no, no termines tu turno hasta que get_task_status diga `completed` o `failed`.',
|
|
87
|
+
'Por stdio, las tareas viven lo que vive el cliente: si el cliente se cierra, el servidor se va y la tarea muere a medias. Por eso, con clientes de una sola vuelta, espera dentro de la llamada.',
|
|
88
|
+
'',
|
|
89
|
+
'No cambies de modelo automáticamente si el usuario ha indicado uno.',
|
|
90
|
+
'RATACODE nunca devuelve claves: las guarda él y hace las llamadas.',
|
|
91
|
+
'Las claves de los modelos están en UN solo sitio: RATACODE › Ajustes › Models (la casa). Este servidor NO mira el entorno del cliente ni abre ficheros de claves: le pregunta al motor si la credencial de esa ruta está puesta. Si no lo está, lo dirá tal cual («Falta la clave de B.AI. Pégala en RATACODE › Ajustes › Models.») y no arrancará nada.',
|
|
92
|
+
'Cada tarea del MCP trabaja ENCERRADA en las carpetas autorizadas de la casa (`mcp.workspaces`): lee y escribe sólo ahí. Fuera de ahí la herramienta se para y te lo dice («Fuera de la carpeta autorizada: <ruta>»). No hay terminal, ni red, ni subagentes, ni guiones: no hay forma de saltar el cerco. Si necesitas algo de fuera, pídelo al humano.',
|
|
93
|
+
].join('\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Registrar las siete herramientas en un `McpServer`. Extraído de
|
|
98
|
+
* `montarServidor` para poder crear un servidor por sesión HTTP que COMPARTA
|
|
99
|
+
* el mismo registro de tareas (así `get_task_result` ve las tareas de otras
|
|
100
|
+
* sesiones).
|
|
101
|
+
* @param {McpServer} servidor - el servidor donde registrar.
|
|
102
|
+
* @param {object} ctx - casa, dshBin, cwdPorDefecto, tareas, marcasTarea, tareasPorHora, http.
|
|
103
|
+
*/
|
|
104
|
+
export function registrarHerramientas(servidor, ctx) {
|
|
105
|
+
const { casa, dshBin, cwdPorDefecto, tareas, marcasTarea } = ctx;
|
|
106
|
+
const ajustes = ajustesMcp(casa);
|
|
107
|
+
|
|
108
|
+
/** El nombre del cliente que está al otro lado, para el cuaderno. */
|
|
109
|
+
const cliente = () => {
|
|
110
|
+
const quien = servidor.server.getClientVersion();
|
|
111
|
+
return typeof quien?.name === 'string' && quien.name.trim() !== '' ? quien.name : 'MCP';
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
servidor.server.oninitialized = () => {
|
|
115
|
+
const nombre = cliente();
|
|
116
|
+
tareas.verCliente(nombre);
|
|
117
|
+
tareas.resumir();
|
|
118
|
+
aviso('cliente conectado: ' + nombre);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// ── list_providers ────────────────────────────────────────────────────────
|
|
122
|
+
servidor.registerTool(
|
|
123
|
+
'list_providers',
|
|
124
|
+
{
|
|
125
|
+
title: 'Proveedores de RATACODE',
|
|
126
|
+
description: 'Los proveedores configurados en RATACODE, con si tienen la credencial puesta en la casa (RATACODE › Ajustes › Models). Nunca devuelve ninguna clave.',
|
|
127
|
+
inputSchema: {},
|
|
128
|
+
},
|
|
129
|
+
conRed(async () => {
|
|
130
|
+
const { proveedores, porDefecto, avisos } = await catalogo(casa);
|
|
131
|
+
return comoTexto({
|
|
132
|
+
proveedores: proveedores.map((p) => ({
|
|
133
|
+
id: p.id,
|
|
134
|
+
nombre: p.nombre,
|
|
135
|
+
api: p.api,
|
|
136
|
+
base_url: p.base_url,
|
|
137
|
+
tiene_clave: p.tiene_clave,
|
|
138
|
+
credencial: p.credencial,
|
|
139
|
+
...(p.falta === null ? {} : { problema: p.falta }),
|
|
140
|
+
})),
|
|
141
|
+
modelo_por_defecto: porDefecto,
|
|
142
|
+
avisos,
|
|
143
|
+
nota: 'La única fuente de claves es la casa (RATACODE › Ajustes › Models).'
|
|
144
|
+
+ ' Lo que mira este servidor es si la credencial está puesta ahí, preguntándoselo al motor;'
|
|
145
|
+
+ ' nunca lee valores ni mira el entorno del cliente.',
|
|
146
|
+
});
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// ── list_models ───────────────────────────────────────────────────────────
|
|
151
|
+
servidor.registerTool(
|
|
152
|
+
'list_models',
|
|
153
|
+
{
|
|
154
|
+
title: 'Modelos de RATACODE',
|
|
155
|
+
description: 'Los modelos disponibles, con proveedor, id, contexto, capacidades, coste declarado y estado. Úsalo ANTES de run_task para elegir modelo.',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
provider: z.string().optional().describe('Filtra por proveedor (por ejemplo "b-ai").'),
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
conRed(async ({ provider }) => {
|
|
161
|
+
const { modelos, porDefecto, avisos } = await catalogo(casa);
|
|
162
|
+
const filtrados = provider === undefined ? modelos : modelos.filter((m) => m.provider === provider);
|
|
163
|
+
return comoTexto({
|
|
164
|
+
modelos: filtrados,
|
|
165
|
+
total: filtrados.length,
|
|
166
|
+
modelo_por_defecto: porDefecto,
|
|
167
|
+
avisos: provider !== undefined && filtrados.length === 0
|
|
168
|
+
? [...avisos, 'no hay modelos declarados para el proveedor «' + provider + '»; mira list_providers']
|
|
169
|
+
: avisos,
|
|
170
|
+
nota: 'El coste sólo aparece si el humano declaró precios en `mcp.precios` de settings.yaml. RATACODE no se inventa precios.',
|
|
171
|
+
});
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// ── run_task ──────────────────────────────────────────────────────────────
|
|
176
|
+
servidor.registerTool(
|
|
177
|
+
'run_task',
|
|
178
|
+
{
|
|
179
|
+
title: 'Lanzar una tarea en RATACODE',
|
|
180
|
+
description: 'Manda un trabajo a un modelo de RATACODE. Sin `esperar_segundos` devuelve un task_id al momento y el trabajo sigue en segundo plano (consulta get_task_status y recoge con get_task_result). CON `esperar_segundos` (1-600) la llamada ESPERA hasta que la tarea termine o se agote ese tiempo, y si terminó devuelve el resultado completo en la MISMA respuesta, sin más llamadas: úsalo cuando no puedas volver después (una sola vuelta de chat). Por stdio, la tarea vive lo que vive el cliente: si cierras el cliente, se muere. Si no indicas modelo, se usa el de por defecto de la casa y se te dice cuál.',
|
|
181
|
+
inputSchema: {
|
|
182
|
+
prompt: z.string().min(1).describe('El encargo, en texto. Tope: ' + ajustes.promptMaxCaracteres + ' caracteres (configurable en `mcp.prompt_max_caracteres`).'),
|
|
183
|
+
esperar_segundos: z.number().int().min(0).max(600).optional().describe('Si lo das, esta llamada espera hasta ese máximo (segundos, 0-600) a que la tarea termine y devuelve el resultado completo en la misma respuesta. Sin este parámetro, vuelve al momento con el task_id y hay que preguntar con get_task_status.'),
|
|
184
|
+
provider: z.string().optional().describe('Proveedor (por ejemplo "b-ai"). Si lo omites, el de por defecto.'),
|
|
185
|
+
model: z.string().optional().describe('Id del modelo (por ejemplo "deepseek-v4.1-flash").'),
|
|
186
|
+
working_directory: z.string().optional().describe('Carpeta donde trabaja la tarea. Debe estar dentro de los espacios autorizados.'),
|
|
187
|
+
context: z.string().optional().describe('Contexto extra que se pone delante del encargo.'),
|
|
188
|
+
max_tokens: z.number().int().positive().optional().describe('Tope de tokens de salida.'),
|
|
189
|
+
timeout: z.number().int().positive().optional().describe('Tiempo máximo en milisegundos antes de cancelar la tarea. Por defecto ' + ajustes.timeoutPorDefectoMs + ' ms (' + Math.round(ajustes.timeoutPorDefectoMs / 60000) + ' min); máximo ' + ajustes.timeoutMaximoMs + ' ms.'),
|
|
190
|
+
allow_dangerous: z.boolean().optional().describe('Pedir acceso total al disco. Requiere que el humano lo haya permitido en la casa; si no, se deniega.'),
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
conRed(async (args) => {
|
|
194
|
+
// Tope del encargo: un prompt enorme es un gasto enorme y una espera peor.
|
|
195
|
+
if (args.prompt.length > ajustes.promptMaxCaracteres) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
'el encargo es demasiado largo: ' + args.prompt.length + ' caracteres (máximo '
|
|
198
|
+
+ ajustes.promptMaxCaracteres + ', `mcp.prompt_max_caracteres` en ' + casa + '\\settings.yaml).'
|
|
199
|
+
+ ' Pásame el encargo en un fichero dentro del espacio de trabajo y pídeme que lo lea.',
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Tope de tareas EN MARCHA a la vez: la URL abierta no puede ser una
|
|
204
|
+
// fábrica de procesos del motor.
|
|
205
|
+
const enMarcha = tareas.listar().filter((t) => t.estado === 'queued' || t.estado === 'running').length;
|
|
206
|
+
if (enMarcha >= ajustes.tareasALaVez) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
'ya hay ' + enMarcha + ' tareas en marcha (máximo ' + ajustes.tareasALaVez + ' a la vez,'
|
|
209
|
+
+ ' `mcp.tareas_a_la_vez` en ' + casa + '\\settings.yaml). Espera a que termine alguna'
|
|
210
|
+
+ ' (get_task_status) o cancélala (cancel_task).',
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Tope de tareas por hora: falla cerrado antes de lanzar nada. Las marcas
|
|
215
|
+
// viven en la casa, así que un reinicio no regala cuota.
|
|
216
|
+
const ahora = Date.now();
|
|
217
|
+
while (marcasTarea.length > 0 && ahora - marcasTarea[0] > VENTANA_MS) marcasTarea.shift();
|
|
218
|
+
if (marcasTarea.length >= ctx.tareasPorHora) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
'tope de tareas alcanzado: ' + marcasTarea.length + ' en la última hora'
|
|
221
|
+
+ ' (máximo ' + ctx.tareasPorHora + '/h). Espera a que pasen las primeras antes de lanzar más.',
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const ruta = resolverRuta(casa, args.provider, args.model);
|
|
226
|
+
// La credencial: la de la casa (Ajustes › Models), que es la única fuente.
|
|
227
|
+
// Se le pregunta al motor. Si no está, se PARA aquí: nada de arrancar un
|
|
228
|
+
// motor que va a fallar peor y más tarde.
|
|
229
|
+
const credencial = await credencialDeProveedor(casa, ruta.provider);
|
|
230
|
+
if (credencial !== null && credencial.motivo === null && credencial.configurada === false) {
|
|
231
|
+
return comoError(faltaLaClave(credencial.nombreVisible));
|
|
232
|
+
}
|
|
233
|
+
const { espacio, raiz, raices, avisos } = resolverEspacio({ casa, pedido: args.working_directory, cwdPorDefecto, http: ctx.http === true });
|
|
234
|
+
const { modo, motivo } = resolverModo({ casa, allowDangerous: args.allow_dangerous });
|
|
235
|
+
const prompt = args.context === undefined || args.context.trim() === ''
|
|
236
|
+
? args.prompt
|
|
237
|
+
: 'Contexto:\n' + args.context.trim() + '\n\nTarea:\n' + args.prompt;
|
|
238
|
+
|
|
239
|
+
// El tiempo: si el cliente no dice nada, el de la casa (media hora); si
|
|
240
|
+
// dice, se respeta pero nunca por encima del techo.
|
|
241
|
+
const timeoutPedido = typeof args.timeout === 'number' && args.timeout > 0 ? args.timeout : null;
|
|
242
|
+
const timeoutMs = timeoutPedido === null
|
|
243
|
+
? ajustes.timeoutPorDefectoMs
|
|
244
|
+
: Math.min(timeoutPedido, ajustes.timeoutMaximoMs);
|
|
245
|
+
const timeoutRecortado = timeoutPedido !== null && timeoutPedido > ajustes.timeoutMaximoMs;
|
|
246
|
+
|
|
247
|
+
// Sólo contabiliza la tarea si de verdad se lanza (no los intentos
|
|
248
|
+
// denegados por falta de clave o espacio no autorizado).
|
|
249
|
+
marcasTarea.push(Date.now());
|
|
250
|
+
guardarMarcas(casa, marcasTarea);
|
|
251
|
+
const recibo = tareas.crear({
|
|
252
|
+
prompt,
|
|
253
|
+
provider: ruta.provider,
|
|
254
|
+
model: ruta.model,
|
|
255
|
+
espacio,
|
|
256
|
+
raices,
|
|
257
|
+
modo,
|
|
258
|
+
maxTokens: args.max_tokens,
|
|
259
|
+
timeoutMs,
|
|
260
|
+
cliente: cliente(),
|
|
261
|
+
});
|
|
262
|
+
const comun = {
|
|
263
|
+
...recibo,
|
|
264
|
+
ruta_elegida: ruta.origen,
|
|
265
|
+
sandbox: { modo, motivo },
|
|
266
|
+
espacio_autorizado_por: raiz,
|
|
267
|
+
tiempo: {
|
|
268
|
+
timeout_ms: timeoutMs,
|
|
269
|
+
origen: timeoutPedido === null ? 'por_defecto_de_la_casa' : 'peticion',
|
|
270
|
+
...(timeoutRecortado ? { recortado_al_maximo_ms: ajustes.timeoutMaximoMs } : {}),
|
|
271
|
+
},
|
|
272
|
+
avisos,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Sin `esperar_segundos`: recibo y a otra cosa (como siempre).
|
|
276
|
+
if (args.esperar_segundos === undefined) {
|
|
277
|
+
return comoTexto({
|
|
278
|
+
...comun,
|
|
279
|
+
siguiente: 'Consulta get_task_status con este task_id y recoge el resultado con get_task_result. Si no vas a poder volver (una sola vuelta de chat), repite run_task con esperar_segundos (1-600) y te doy el resultado en la misma llamada.',
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Con `esperar_segundos`: se espera aquí, y si terminó se devuelve TODO.
|
|
284
|
+
const espera = await tareas.esperar(recibo.task_id, args.esperar_segundos * 1000);
|
|
285
|
+
if (espera.terminada) {
|
|
286
|
+
return comoTexto({
|
|
287
|
+
...comun,
|
|
288
|
+
estado: espera.estado,
|
|
289
|
+
espera: { pedida_segundos: args.esperar_segundos, esperada_ms: espera.esperada_ms, agotada: false },
|
|
290
|
+
resultado: tareas.resultado(recibo.task_id),
|
|
291
|
+
siguiente: 'La tarea ya terminó: el resultado completo va en «resultado». No hace falta get_task_status.',
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return comoTexto({
|
|
295
|
+
...comun,
|
|
296
|
+
estado: espera.estado,
|
|
297
|
+
espera: { pedida_segundos: args.esperar_segundos, esperada_ms: espera.esperada_ms, agotada: true },
|
|
298
|
+
resultado: null,
|
|
299
|
+
siguiente: 'Se agotaron los ' + args.esperar_segundos + ' s de espera y la tarea sigue en «' + espera.estado + '»: llama a get_task_status (o a get_task_result) hasta que diga completed o failed. Y por stdio la tarea vive lo que vive el cliente: no cierres hasta verla terminada.',
|
|
300
|
+
});
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// ── get_task_status ───────────────────────────────────────────────────────
|
|
305
|
+
servidor.registerTool(
|
|
306
|
+
'get_task_status',
|
|
307
|
+
{
|
|
308
|
+
title: 'Estado de una tarea',
|
|
309
|
+
description: 'Estado de una tarea: queued, running, completed, failed o cancelled.',
|
|
310
|
+
inputSchema: { task_id: z.string().min(1).describe('El task_id que devolvió run_task.') },
|
|
311
|
+
},
|
|
312
|
+
conRed(async ({ task_id }) => comoTexto(tareas.estado(task_id))),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// ── get_task_result ───────────────────────────────────────────────────────
|
|
316
|
+
servidor.registerTool(
|
|
317
|
+
'get_task_result',
|
|
318
|
+
{
|
|
319
|
+
title: 'Resultado de una tarea',
|
|
320
|
+
description: 'La respuesta de una tarea terminada, con modelo, proveedor, tokens, coste (si está declarado), duración y errores.',
|
|
321
|
+
inputSchema: { task_id: z.string().min(1).describe('El task_id que devolvió run_task.') },
|
|
322
|
+
},
|
|
323
|
+
conRed(async ({ task_id }) => comoTexto(tareas.resultado(task_id))),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// ── cancel_task ───────────────────────────────────────────────────────────
|
|
327
|
+
servidor.registerTool(
|
|
328
|
+
'cancel_task',
|
|
329
|
+
{
|
|
330
|
+
title: 'Cancelar una tarea',
|
|
331
|
+
description: 'Detiene una tarea en marcha de inmediato (se mata el proceso que la ejecuta).',
|
|
332
|
+
inputSchema: { task_id: z.string().min(1).describe('El task_id que devolvió run_task.') },
|
|
333
|
+
},
|
|
334
|
+
conRed(async ({ task_id }) => comoTexto(await tareas.cancelar(task_id))),
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// ── ratacode_status (extra de casa, para no depender de la pantalla) ──────
|
|
338
|
+
servidor.registerTool(
|
|
339
|
+
'ratacode_status',
|
|
340
|
+
{
|
|
341
|
+
title: 'Estado de RATACODE MCP',
|
|
342
|
+
description: 'Estado del propio servidor: tareas vivas, tope por hora y espacios autorizados. No devuelve la casa ni la actividad de otros clientes.',
|
|
343
|
+
inputSchema: {},
|
|
344
|
+
},
|
|
345
|
+
conRed(async () => {
|
|
346
|
+
const resumen = tareas.resumir();
|
|
347
|
+
// Ni la casa (el dato que sirve en bandeja para ir a leer
|
|
348
|
+
// `.credentials.yaml`), ni el cuaderno de actividad (lleva los encargos de
|
|
349
|
+
// TODOS los clientes), ni las últimas tareas de otros: cada cliente ve lo
|
|
350
|
+
// suyo y el estado del servidor.
|
|
351
|
+
return comoTexto({
|
|
352
|
+
mcp: resumen.mcp,
|
|
353
|
+
actualizado: resumen.actualizado,
|
|
354
|
+
espacios_autorizados: ajustes.workspaces.length > 0 ? ajustes.workspaces : ['(sin lista; sólo el espacio por defecto)'],
|
|
355
|
+
permitir_peligroso: ajustes.permitirPeligroso,
|
|
356
|
+
topes: {
|
|
357
|
+
tareas_por_hora: ctx.tareasPorHora,
|
|
358
|
+
marcas_ultima_hora: marcasTarea.length,
|
|
359
|
+
tareas_a_la_vez: ajustes.tareasALaVez,
|
|
360
|
+
timeout_por_defecto_ms: ajustes.timeoutPorDefectoMs,
|
|
361
|
+
timeout_maximo_ms: ajustes.timeoutMaximoMs,
|
|
362
|
+
prompt_max_caracteres: ajustes.promptMaxCaracteres,
|
|
363
|
+
},
|
|
364
|
+
nota: 'Para ver el cuaderno de actividad y las tareas de todos los clientes, míralo en la casa (o en el panel), no por MCP.',
|
|
365
|
+
});
|
|
366
|
+
}),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Montar el servidor MCP con sus siete herramientas.
|
|
372
|
+
* @param {{casa: string, dshBin: string, cwdPorDefecto: string, tareasPorHora?: number, http?: boolean}} opciones - la casa, el motor, el cwd, el tope y si se habla por HTTP.
|
|
373
|
+
* @returns {{servidor: McpServer, tareas: Tareas, fabricaServidor: () => McpServer}}
|
|
374
|
+
*/
|
|
375
|
+
export function montarServidor({ casa, dshBin, cwdPorDefecto, tareasPorHora = 30, http = false }) {
|
|
376
|
+
const tareas = new Tareas({ casa, dshBin });
|
|
377
|
+
/** Marcas de tiempo de creación de tareas, para el tope por hora. Viven en la casa. */
|
|
378
|
+
const marcasTarea = cargarMarcas(casa);
|
|
379
|
+
if (marcasTarea.length > 0) {
|
|
380
|
+
aviso('tope de tareas: ' + marcasTarea.length + ' marca(s) de la última hora, contadas al arrancar');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Crear un servidor MCP nuevo que COMPARTE el registro de tareas. Sirve para
|
|
384
|
+
* atender cada sesión HTTP con su propio servidor (el SDK no permite conectar
|
|
385
|
+
* un mismo servidor a varios transportes a la vez) sin perder el estado. */
|
|
386
|
+
function fabricaServidor() {
|
|
387
|
+
const servidor = new McpServer(
|
|
388
|
+
{ name: 'ratacode', version: '0.1.0' },
|
|
389
|
+
{ instructions: instrucciones() },
|
|
390
|
+
);
|
|
391
|
+
registrarHerramientas(servidor, { casa, dshBin, cwdPorDefecto, tareas, marcasTarea, tareasPorHora, http });
|
|
392
|
+
return servidor;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const servidor = fabricaServidor();
|
|
396
|
+
return { servidor, tareas, fabricaServidor };
|
|
397
|
+
}
|