mcp-phase-gate 2.1.0 → 2.2.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 +125 -57
- package/README.md +109 -50
- package/build/cli.d.ts +14 -0
- package/build/cli.js +98 -0
- package/build/config.d.ts +27 -2
- package/build/config.js +151 -22
- package/build/confirmacion.d.ts +15 -0
- package/build/confirmacion.js +100 -0
- package/build/diagnostico.d.ts +83 -0
- package/build/diagnostico.js +394 -0
- package/build/index.js +28 -388
- package/build/log.d.ts +6 -0
- package/build/log.js +27 -0
- package/build/paths.js +3 -3
- package/build/phases.d.ts +10 -1
- package/build/phases.js +48 -17
- package/build/prompts-mcp.d.ts +33 -0
- package/build/prompts-mcp.js +194 -0
- package/build/recursos.d.ts +8 -0
- package/build/recursos.js +306 -0
- package/build/schemas.d.ts +105 -51
- package/build/schemas.js +151 -78
- package/build/servidor.d.ts +3 -0
- package/build/servidor.js +372 -0
- package/build/tasks.d.ts +2 -0
- package/build/tasks.js +114 -2
- package/package.json +1 -2
- package/prompts/comandos/f1.md +14 -0
- package/prompts/comandos/f2.md +12 -0
- package/prompts/comandos/f3.md +9 -0
- package/prompts/comandos/f4.md +10 -0
- package/prompts/comandos/f5.md +12 -0
package/build/index.js
CHANGED
|
@@ -2,413 +2,53 @@
|
|
|
2
2
|
// El shebang es lo que vuelve ejecutable este archivo como `bin` del paquete. Sin él,
|
|
3
3
|
// `npx mcp-phase-gate` falla en Linux y macOS: el sistema no sabe con qué interpretarlo.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// tareas en tasks.ts. Este archivo solo traduce entre el protocolo y esas funciones.
|
|
8
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
// El punto de entrada: con argumentos, la línea de comandos (cli.ts); sin ellos, el servidor MCP
|
|
6
|
+
// (servidor.ts) conectado por stdio. Aquí no se define ninguna tool.
|
|
9
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { describePhases, discoverPhases, loadPhasePrompt, nombreContextoInicial, } from "./phases.js";
|
|
16
|
-
import { avisoDeCompuerta, bloqueDeRecibos, describeEstado, guardarPuntero, limpiarEstadoViejo, listarTareas, motivoDeProteccion, tareaActiva, tareaDelPrompt, } from "./tasks.js";
|
|
17
|
-
import { validarLlamada } from "./schemas.js";
|
|
18
|
-
const server = new Server({
|
|
19
|
-
name: SERVER_NAME,
|
|
20
|
-
version: SERVER_VERSION,
|
|
21
|
-
}, {
|
|
22
|
-
capabilities: {
|
|
23
|
-
tools: {},
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
// La lista de tools es estática, así que estas descripciones se arman una sola vez al
|
|
27
|
-
// arrancar. La de proyectos puede quedarse vieja si creas una carpeta con el cliente abierto:
|
|
28
|
-
// es orientativa, y la lista que manda es la que devuelven las tools al rechazar un proyecto.
|
|
29
|
-
const PHASES_DESCRIPTION = describePhases();
|
|
30
|
-
const PROJECTS_DESCRIPTION = describeProjects();
|
|
31
|
-
// ─── Formato de las respuestas de tarea ─────────────────────────────────────
|
|
32
|
-
function lineaDeTarea(estado, phases) {
|
|
33
|
-
return `- ${estado.taskName} — ${describeEstado(estado, phases)}`;
|
|
34
|
-
}
|
|
35
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
36
|
-
return {
|
|
37
|
-
tools: [
|
|
38
|
-
{
|
|
39
|
-
name: "start_task",
|
|
40
|
-
description: "Inicia una tarea NUEVA creando su carpeta en la Documentación Central y guardando el contexto inicial. Para volver a una tarea que ya existe usa 'switch_task': start_task reescribiría su contexto inicial.",
|
|
41
|
-
inputSchema: {
|
|
42
|
-
type: "object",
|
|
43
|
-
properties: {
|
|
44
|
-
project: { type: "string", description: `Proyecto. Tiene que ser uno de los que ya existen: ${PROJECTS_DESCRIPTION}. Si no lo es, la tool te devuelve la lista actualizada.` },
|
|
45
|
-
task_name: { type: "string", description: "Nombre de la tarea (creará una carpeta con este nombre)" },
|
|
46
|
-
initial_context: { type: "string", description: "El contexto, correo o requerimiento inicial completo" },
|
|
47
|
-
crear_proyecto: { type: "boolean", description: "Solo para ESTRENAR un proyecto que todavía no existe. Déjalo sin poner salvo que el usuario te haya dicho explícitamente que quiere crear un proyecto nuevo: sirve para que un nombre mal escrito no cree una carpeta suelta en la Documentación." }
|
|
48
|
-
},
|
|
49
|
-
required: ["project", "task_name", "initial_context"],
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
name: "get_phase_prompt",
|
|
54
|
-
description: `Obtiene el prompt maestro de comportamiento según la fase. Fases disponibles — ${PHASES_DESCRIPTION}. Comprueba además que no te estés saltando una fase: si faltan documentos anteriores, el prompt llega con un aviso al principio.`,
|
|
55
|
-
inputSchema: {
|
|
56
|
-
type: "object",
|
|
57
|
-
properties: {
|
|
58
|
-
phase: { type: "number", description: `Número de la fase. Disponibles: ${PHASES_DESCRIPTION}.` },
|
|
59
|
-
project: { type: "string", description: `Proyecto en el que estás trabajando: ${PROJECTS_DESCRIPTION}. Pásalo siempre que lo sepas —lo sabes por el repositorio que tienes abierto— para que se pueda comprobar que no te saltas una fase. La tarea se toma de la activa de ese proyecto.` },
|
|
60
|
-
task_name: { type: "string", description: "Solo si quieres comprobar contra una tarea distinta de la activa del proyecto. Normalmente se omite." }
|
|
61
|
-
},
|
|
62
|
-
required: ["phase"],
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
name: "get_active_task",
|
|
67
|
-
description: "Devuelve la tarea activa y en qué fase va, deducida de los documentos que ya tiene. Úsalo al iniciar un chat nuevo o si perdiste el contexto. Sin 'project' devuelve la activa de CADA proyecto.",
|
|
68
|
-
inputSchema: {
|
|
69
|
-
type: "object",
|
|
70
|
-
properties: {
|
|
71
|
-
project: { type: "string", description: `Proyecto del que quieres la tarea activa: ${PROJECTS_DESCRIPTION}. Si lo omites, se listan las de todos los proyectos.` }
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
name: "list_tasks",
|
|
77
|
-
description: "Lista las tareas que existen y la fase en la que va cada una. Úsalo para retomar una tarea vieja o para saber qué hay pendiente.",
|
|
78
|
-
inputSchema: {
|
|
79
|
-
type: "object",
|
|
80
|
-
properties: {
|
|
81
|
-
project: { type: "string", description: `Proyecto a listar: ${PROJECTS_DESCRIPTION}. Si lo omites, se listan todos.` }
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
name: "switch_task",
|
|
87
|
-
description: "Cambia cuál es la tarea activa de un proyecto, sin tocar ningún archivo. Es la forma correcta de retomar una tarea que ya existe.",
|
|
88
|
-
inputSchema: {
|
|
89
|
-
type: "object",
|
|
90
|
-
properties: {
|
|
91
|
-
project: { type: "string", description: `Proyecto de la tarea: ${PROJECTS_DESCRIPTION}.` },
|
|
92
|
-
task_name: { type: "string", description: "Nombre de la carpeta de la tarea a activar. Si no existe, la tool te devuelve las que sí." }
|
|
93
|
-
},
|
|
94
|
-
required: ["project", "task_name"],
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
name: "read_central_doc",
|
|
99
|
-
description: "Lee un archivo de la Documentación Central. RECOMENDADO: pasa 'project' + 'task_name' + 'file_name' y el servidor arma la ruta correcta automáticamente (<PROYECTO>/Proyectos/<tarea>/<archivo>). Alternativamente puedes pasar 'file_path' relativo a DOCUMENTACIÓN.",
|
|
100
|
-
inputSchema: {
|
|
101
|
-
type: "object",
|
|
102
|
-
properties: {
|
|
103
|
-
project: { type: "string", description: `Proyecto, uno de los existentes: ${PROJECTS_DESCRIPTION}. Úsalo junto con task_name y file_name.` },
|
|
104
|
-
task_name: { type: "string", description: "Nombre de la carpeta de la tarea." },
|
|
105
|
-
file_name: { type: "string", description: "Nombre (o ruta relativa) del archivo dentro de la carpeta de la tarea (ej. '01 - Análisis Técnico.md')." },
|
|
106
|
-
file_path: { type: "string", description: "ALTERNATIVA: ruta relativa dentro de DOCUMENTACIÓN (ej. 'PROYECTO-A/Proyectos/tarea/01-analisis.md'). Solo si no usas project+task_name." }
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
name: "write_central_doc",
|
|
112
|
-
description: "Escribe o sobrescribe un archivo en la Documentación Central. RECOMENDADO: pasa 'project' + 'task_name' + 'file_name' y el servidor arma la ruta correcta automáticamente (<PROYECTO>/Proyectos/<tarea>/<archivo>). Alternativamente puedes pasar 'file_path' relativo a DOCUMENTACIÓN.",
|
|
113
|
-
inputSchema: {
|
|
114
|
-
type: "object",
|
|
115
|
-
properties: {
|
|
116
|
-
project: { type: "string", description: `Proyecto, uno de los existentes: ${PROJECTS_DESCRIPTION}. Úsalo junto con task_name y file_name.` },
|
|
117
|
-
task_name: { type: "string", description: "Nombre de la carpeta de la tarea." },
|
|
118
|
-
file_name: { type: "string", description: "Nombre (o ruta relativa) del archivo dentro de la carpeta de la tarea (ej. '01 - Análisis Técnico.md')." },
|
|
119
|
-
file_path: { type: "string", description: "ALTERNATIVA: ruta relativa dentro de DOCUMENTACIÓN, que tiene que caer dentro de la carpeta de una tarea que ya existe (ej. 'PROYECTO-A/Proyectos/tarea/01 - Análisis Técnico.md'). Los archivos del proyecto no se escriben con esta tool. Solo si no usas project+task_name." },
|
|
120
|
-
content: { type: "string", description: "Contenido a escribir" },
|
|
121
|
-
force: { type: "boolean", description: "Solo con confirmación explícita del usuario en el chat. Permite reescribir un documento congelado o el contexto inicial." }
|
|
122
|
-
},
|
|
123
|
-
required: ["content"],
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
name: "read_cross_repo",
|
|
128
|
-
description: "Lee archivos de otros repositorios locales, dentro de la carpeta configurada en ORQUESTADOR_REPOS_PATH.",
|
|
129
|
-
inputSchema: {
|
|
130
|
-
type: "object",
|
|
131
|
-
properties: {
|
|
132
|
-
repo_name: { type: "string", description: "Nombre del repositorio (ej. 'repo-a', 'repo-b')" },
|
|
133
|
-
file_path: { type: "string", description: "Ruta relativa del archivo dentro del repositorio (ej. 'src/main.ts')" }
|
|
134
|
-
},
|
|
135
|
-
required: ["repo_name", "file_path"],
|
|
136
|
-
},
|
|
137
|
-
}
|
|
138
|
-
],
|
|
139
|
-
};
|
|
140
|
-
});
|
|
141
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
142
|
-
const { name, arguments: args } = request.params;
|
|
143
|
-
try {
|
|
144
|
-
// Todo argumento se valida aquí, antes de que ninguna tool lo use. Un campo ausente o de
|
|
145
|
-
// otro tipo devuelve un error que lo nombra, en vez de convertirse en "undefined".
|
|
146
|
-
const llamada = validarLlamada(name, args);
|
|
147
|
-
if (llamada.tool === "start_task") {
|
|
148
|
-
const proj = llamada.args.project; // el schema ya le quitó los espacios
|
|
149
|
-
const taskName = llamada.args.task_name;
|
|
150
|
-
const context = llamada.args.initial_context;
|
|
151
|
-
// Estrenar proyecto tiene que ser deliberado: sin esta puerta, un nombre mal escrito
|
|
152
|
-
// creaba una carpeta suelta en la Documentación y nadie se enteraba hasta buscarla.
|
|
153
|
-
let carpetaProyecto = findProjectFolder(proj);
|
|
154
|
-
let estrenado = false;
|
|
155
|
-
if (!carpetaProyecto) {
|
|
156
|
-
if (llamada.args.crear_proyecto !== true) {
|
|
157
|
-
throw new Error(`No existe el proyecto "${proj}". Los proyectos disponibles son: ${describeProjects()}. ` +
|
|
158
|
-
`Si el usuario te confirmó que quiere estrenar el proyecto "${proj.toUpperCase()}", ` +
|
|
159
|
-
`vuelve a llamar a start_task con crear_proyecto: true.`);
|
|
160
|
-
}
|
|
161
|
-
// El nombre se vuelve carpeta tal cual: "../evil" la creaba fuera de la Documentación.
|
|
162
|
-
validarSegmento(proj, "project");
|
|
163
|
-
validarNombreVisible(proj, "project");
|
|
164
|
-
carpetaProyecto = proj.toUpperCase();
|
|
165
|
-
estrenado = true;
|
|
166
|
-
}
|
|
167
|
-
validarNombreVisible(taskName, "task_name");
|
|
168
|
-
const carpetaTarea = taskFolderPath(carpetaProyecto, taskName);
|
|
169
|
-
const contextoInicial = path.join(carpetaTarea, nombreContextoInicial());
|
|
170
|
-
const docsBase = requireBasePath(DOCS);
|
|
171
|
-
if (!estaDentroReal(docsBase, carpetaTarea)) {
|
|
172
|
-
throw new Error(`Acceso denegado: la carpeta de la tarea queda fuera de ${docsBase}.`);
|
|
173
|
-
}
|
|
174
|
-
// start_task escribe el contexto inicial sin preguntar, así que llamarlo sobre una tarea
|
|
175
|
-
// que ya existe borraba el requerimiento original — el único documento que no se puede
|
|
176
|
-
// regenerar leyendo el código. Retomar una tarea es switch_task, que no toca archivos.
|
|
177
|
-
if (fs.existsSync(contextoInicial)) {
|
|
178
|
-
throw new Error(`La tarea "${taskName}" ya existe en ${carpetaProyecto} y start_task reescribiría su ` +
|
|
179
|
-
`contexto inicial. Si quieres retomarla, usa switch_task. Si de verdad quieres ` +
|
|
180
|
-
`empezar de cero, borra o renombra la carpeta primero: ${carpetaTarea}`);
|
|
181
|
-
}
|
|
182
|
-
if (!fs.existsSync(carpetaTarea)) {
|
|
183
|
-
fs.mkdirSync(carpetaTarea, { recursive: true });
|
|
184
|
-
}
|
|
185
|
-
fs.writeFileSync(contextoInicial, context, "utf-8");
|
|
186
|
-
// Queda como activa de SU proyecto. Otra ventana trabajando en otro proyecto conserva
|
|
187
|
-
// la suya: los punteros son independientes.
|
|
188
|
-
guardarPuntero(carpetaProyecto, taskName);
|
|
189
|
-
const aviso = estrenado ? `\nProyecto "${carpetaProyecto}" estrenado.` : "";
|
|
190
|
-
return {
|
|
191
|
-
content: [{ type: "text", text: `Tarea '${taskName}' inicializada correctamente. Archivo guardado en: ${contextoInicial}\nTarea activa de ${carpetaProyecto}: ${taskName}.${aviso}` }],
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
if (llamada.tool === "get_phase_prompt") {
|
|
195
|
-
// Que sea un entero ya lo garantiza el schema; si la fase no existe, lo dice loadPhasePrompt.
|
|
196
|
-
const { phase, project, task_name } = llamada.args;
|
|
197
|
-
const prompt = loadPhasePrompt(phase);
|
|
198
|
-
// La compuerta avisa, no bloquea: el prompt se entrega siempre. Si comprobarla fallara
|
|
199
|
-
// por lo que sea, entregarlo igual es mejor que dejar al usuario sin fase por un aviso.
|
|
200
|
-
// Lo mismo con la lista de recibos: es una ayuda, no una condición.
|
|
201
|
-
let aviso = null;
|
|
202
|
-
let recibos = null;
|
|
203
|
-
try {
|
|
204
|
-
const phases = discoverPhases();
|
|
205
|
-
const opciones = { ...(project ? { project } : {}), ...(task_name ? { taskName: task_name } : {}) };
|
|
206
|
-
aviso = avisoDeCompuerta(phase, phases, opciones);
|
|
207
|
-
const estado = tareaDelPrompt(phases, opciones);
|
|
208
|
-
if (estado && estado !== "ambigua")
|
|
209
|
-
recibos = bloqueDeRecibos(phase, estado, phases);
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
console.error(`[orquestador] No se pudo comprobar la compuerta: ${error.message}`);
|
|
213
|
-
}
|
|
214
|
-
const texto = [aviso, prompt, recibos].filter(Boolean).join("\n\n");
|
|
215
|
-
return {
|
|
216
|
-
content: [{ type: "text", text: texto }],
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
if (llamada.tool === "get_active_task") {
|
|
220
|
-
const phases = discoverPhases();
|
|
221
|
-
const pedido = llamada.args.project ?? "";
|
|
222
|
-
// Con proyecto: la suya. Sin proyecto: la de cada uno. Antes había un único puntero
|
|
223
|
-
// global y responder "la activa" a secas obligaba a adivinar cuál de dos ventanas
|
|
224
|
-
// preguntaba; listar todas es la respuesta honesta cuando no se sabe.
|
|
225
|
-
const proyectos = pedido
|
|
226
|
-
? [requireProjectFolder(pedido)]
|
|
227
|
-
: [...discoverProjects().values()].sort();
|
|
228
|
-
if (proyectos.length === 0) {
|
|
229
|
-
return { content: [{ type: "text", text: `No hay ningún proyecto en la Documentación Central todavía.` }] };
|
|
230
|
-
}
|
|
231
|
-
const bloques = [];
|
|
232
|
-
for (const carpetaProyecto of proyectos) {
|
|
233
|
-
const activa = tareaActiva(carpetaProyecto, phases);
|
|
234
|
-
if (!activa) {
|
|
235
|
-
bloques.push(`${carpetaProyecto}: sin tareas todavía.`);
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
const nota = activa.origen === "deducida"
|
|
239
|
-
? " (deducida por ser la más reciente: no había puntero guardado, confírmalo antes de escribir)"
|
|
240
|
-
: "";
|
|
241
|
-
bloques.push(`${carpetaProyecto}: ${activa.estado.taskName}${nota}\n` +
|
|
242
|
-
` Fase: ${describeEstado(activa.estado, phases)}\n` +
|
|
243
|
-
` Carpeta: ${activa.estado.folderPath}`);
|
|
244
|
-
}
|
|
245
|
-
return {
|
|
246
|
-
content: [{ type: "text", text: `${bloques.join("\n\n")}\n\nPunteros guardados en: ${stateFile()}` }],
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
if (llamada.tool === "list_tasks") {
|
|
250
|
-
const phases = discoverPhases();
|
|
251
|
-
const pedido = llamada.args.project ?? "";
|
|
252
|
-
const proyectos = pedido
|
|
253
|
-
? [requireProjectFolder(pedido)]
|
|
254
|
-
: [...discoverProjects().values()].sort();
|
|
255
|
-
const bloques = [];
|
|
256
|
-
for (const carpetaProyecto of proyectos) {
|
|
257
|
-
const tareas = listarTareas(carpetaProyecto, phases);
|
|
258
|
-
const activa = tareaActiva(carpetaProyecto, phases);
|
|
259
|
-
if (tareas.length === 0) {
|
|
260
|
-
bloques.push(`${carpetaProyecto}: sin tareas.`);
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
const lineas = tareas.map((tarea) => {
|
|
264
|
-
const marca = tarea.taskName === activa?.estado.taskName ? " [activa]" : "";
|
|
265
|
-
return `${lineaDeTarea(tarea, phases)}${marca}`;
|
|
266
|
-
});
|
|
267
|
-
bloques.push(`${carpetaProyecto} (${tareas.length}), de la más reciente a la más antigua:\n${lineas.join("\n")}`);
|
|
268
|
-
}
|
|
269
|
-
return {
|
|
270
|
-
content: [{ type: "text", text: bloques.join("\n\n") || "No hay proyectos todavía." }],
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
if (llamada.tool === "switch_task") {
|
|
274
|
-
const phases = discoverPhases();
|
|
275
|
-
const carpetaProyecto = requireProjectFolder(llamada.args.project);
|
|
276
|
-
const taskName = llamada.args.task_name;
|
|
277
|
-
const tareas = listarTareas(carpetaProyecto, phases);
|
|
278
|
-
const encontrada = tareas.find((tarea) => tarea.taskName === taskName);
|
|
279
|
-
if (!encontrada) {
|
|
280
|
-
const disponibles = tareas.length
|
|
281
|
-
? tareas.map((tarea) => `"${tarea.taskName}"`).join(", ")
|
|
282
|
-
: "(ninguna)";
|
|
283
|
-
throw new Error(`No existe la tarea "${taskName}" en ${carpetaProyecto}. Las que hay son: ${disponibles}. ` +
|
|
284
|
-
`Si es una tarea nueva, créala con start_task.`);
|
|
285
|
-
}
|
|
286
|
-
guardarPuntero(carpetaProyecto, taskName);
|
|
287
|
-
return {
|
|
288
|
-
content: [{ type: "text", text: `Tarea activa de ${carpetaProyecto}: ${taskName}.\nFase: ${describeEstado(encontrada, phases)}\nCarpeta: ${encontrada.folderPath}` }],
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
if (llamada.tool === "read_central_doc") {
|
|
292
|
-
const docsBase = requireBasePath(DOCS);
|
|
293
|
-
const fullPath = resolveDocPath(llamada.args);
|
|
294
|
-
if (!estaDentroReal(docsBase, fullPath)) {
|
|
295
|
-
throw new Error(`Acceso denegado: la ruta queda fuera de ${docsBase}.`);
|
|
296
|
-
}
|
|
297
|
-
const content = leerTextoAcotado(fullPath);
|
|
298
|
-
return {
|
|
299
|
-
content: [{ type: "text", text: content }],
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
if (llamada.tool === "write_central_doc") {
|
|
303
|
-
const content = llamada.args.content;
|
|
304
|
-
const docsBase = requireBasePath(DOCS);
|
|
305
|
-
const fullPath = resolveDocPath(llamada.args);
|
|
306
|
-
if (!estaDentroReal(docsBase, fullPath)) {
|
|
307
|
-
throw new Error(`Acceso denegado: la ruta queda fuera de ${docsBase}.`);
|
|
308
|
-
}
|
|
309
|
-
// Antes del mkdir: con una tarea mal escrita, el mkdir recursivo creaba su carpeta.
|
|
310
|
-
const ubicacion = ubicarEnTarea(fullPath);
|
|
311
|
-
if (!ubicacion) {
|
|
312
|
-
throw new Error("write_central_doc solo escribe dentro de la carpeta de una tarea que ya existe " +
|
|
313
|
-
"(<DOCS>/<PROYECTO>/<subcarpeta de tareas>/<tarea>/). Los archivos del proyecto, como " +
|
|
314
|
-
"phase-gate.json, no se escriben con esta tool. Para una tarea nueva, usa start_task.");
|
|
315
|
-
}
|
|
316
|
-
const motivo = motivoDeProteccion(ubicacion.carpetaTarea, ubicacion.relativo, discoverPhases(), nombreContextoInicial());
|
|
317
|
-
if (motivo && llamada.args.force !== true) {
|
|
318
|
-
throw new Error(`${motivo} Si de verdad hay que reescribirlo, pídele al usuario confirmación explícita ` +
|
|
319
|
-
`y vuelve a llamar con force: true.`);
|
|
320
|
-
}
|
|
321
|
-
const existia = fs.existsSync(fullPath);
|
|
322
|
-
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
323
|
-
fs.writeFileSync(fullPath, content, "utf-8");
|
|
324
|
-
// force también crea el contexto inicial cuando falta: no se dice "sobrescrito" si no lo era.
|
|
325
|
-
const forzado = motivo ? (existia ? " (sobrescrito con force)" : " (creado con force)") : "";
|
|
326
|
-
return {
|
|
327
|
-
content: [{ type: "text", text: `Archivo escrito exitosamente en ${fullPath}${forzado}` }],
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
if (llamada.tool === "read_cross_repo") {
|
|
331
|
-
const fullPath = resolveRepoPath(llamada.args.repo_name, llamada.args.file_path);
|
|
332
|
-
const content = leerTextoAcotado(fullPath);
|
|
333
|
-
return {
|
|
334
|
-
content: [{ type: "text", text: content }],
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
338
|
-
}
|
|
339
|
-
catch (error) {
|
|
340
|
-
return {
|
|
341
|
-
content: [{ type: "text", text: `Error ejecutando tool ${name}: ${error.message}` }],
|
|
342
|
-
isError: true,
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
});
|
|
8
|
+
import { DOCS, NOMBRES_LOGICOS, REPOS, SERVER_NAME, errorDeEnv, leerVariable, requireBasePath } from "./config.js";
|
|
9
|
+
import { ejecutarCli } from "./cli.js";
|
|
10
|
+
import { log } from "./log.js";
|
|
11
|
+
import { crearServidor } from "./servidor.js";
|
|
12
|
+
import { limpiarEstadoViejo } from "./tasks.js";
|
|
346
13
|
// Deja en el log el estado de cada ruta al arrancar. No aborta: el servidor sigue en pie y
|
|
347
14
|
// el error real se entrega al llamar la tool. Esto es solo para que, cuando alguien acabe
|
|
348
15
|
// abriendo el log, el motivo ya esté ahí escrito.
|
|
349
16
|
function reportarRutas() {
|
|
350
17
|
for (const requerida of [DOCS, REPOS]) {
|
|
351
18
|
try {
|
|
352
|
-
|
|
19
|
+
const ruta = requireBasePath(requerida);
|
|
20
|
+
log("info", `${leerVariable(requerida.logica).usada} = ${ruta}`);
|
|
353
21
|
}
|
|
354
22
|
catch (error) {
|
|
355
|
-
|
|
23
|
+
log("error", `SIN CONFIGURAR: ${error instanceof Error ? error.message : String(error)}`);
|
|
356
24
|
}
|
|
357
25
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const AYUDA = `${SERVER_NAME} v${SERVER_VERSION}
|
|
364
|
-
|
|
365
|
-
Servidor MCP que parte cada requerimiento en cinco fases con compuerta y obliga a que cada
|
|
366
|
-
una deje su documento en una documentación central.
|
|
367
|
-
|
|
368
|
-
Esto NO se ejecuta a mano: lo lanza tu cliente MCP. Ejecutado directamente se queda esperando
|
|
369
|
-
mensajes JSON-RPC por stdin, que es lo que parece que se colgó.
|
|
370
|
-
|
|
371
|
-
Regístralo en tu cliente (Cursor, VS Code, Claude Code) así:
|
|
372
|
-
|
|
373
|
-
{
|
|
374
|
-
"mcpServers": {
|
|
375
|
-
"${SERVER_NAME}": {
|
|
376
|
-
"command": "npx",
|
|
377
|
-
"args": ["-y", "mcp-phase-gate"],
|
|
378
|
-
"env": {
|
|
379
|
-
"ORQUESTADOR_DOCS_PATH": "<tu carpeta de documentación>",
|
|
380
|
-
"ORQUESTADOR_REPOS_PATH": "<tu carpeta de repositorios>"
|
|
381
|
-
}
|
|
382
|
-
}
|
|
26
|
+
// Las dos variables de un mismo nombre lógico, con valores distintos: se avisa una vez.
|
|
27
|
+
for (const logica of NOMBRES_LOGICOS) {
|
|
28
|
+
const { discrepancia } = leerVariable(logica);
|
|
29
|
+
if (discrepancia)
|
|
30
|
+
log("warning", discrepancia);
|
|
383
31
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
ORQUESTADOR_REPOS_PATH Obligatoria. Carpeta con tus repositorios de código.
|
|
389
|
-
ORQUESTADOR_TASKS_SUBDIR Subcarpeta de tareas dentro de cada proyecto. Por defecto "Proyectos".
|
|
390
|
-
ORQUESTADOR_PROMPTS_PATH Carpeta de prompts propios, que se superponen a los del paquete.
|
|
391
|
-
ORQUESTADOR_STATE_PATH Dónde guardar los punteros de tarea activa.
|
|
392
|
-
|
|
393
|
-
Documentación: https://github.com/Montse2308/mcp-phase-gate`;
|
|
32
|
+
const delEnv = errorDeEnv();
|
|
33
|
+
if (delEnv)
|
|
34
|
+
log("error", delEnv);
|
|
35
|
+
}
|
|
394
36
|
async function main() {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
console.log(AYUDA);
|
|
37
|
+
// Con argumentos, el binario es la herramienta de la persona (ayuda, versión, doctor); sin
|
|
38
|
+
// ellos, el servidor que lanza el cliente. exitCode y no exit(): exit cortaría la salida de
|
|
39
|
+
// doctor a medio escribir cuando stdout es una tubería.
|
|
40
|
+
const resultado = ejecutarCli(process.argv.slice(2));
|
|
41
|
+
if (resultado.accion === "salir") {
|
|
42
|
+
process.exitCode = resultado.codigo;
|
|
402
43
|
return;
|
|
403
44
|
}
|
|
404
45
|
limpiarEstadoViejo();
|
|
405
46
|
reportarRutas();
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
console.error(`${SERVER_NAME} MCP server running on stdio`);
|
|
47
|
+
await crearServidor().connect(new StdioServerTransport());
|
|
48
|
+
log("info", `${SERVER_NAME} MCP server running on stdio`);
|
|
409
49
|
}
|
|
410
50
|
main().catch((error) => {
|
|
411
|
-
|
|
51
|
+
log("error", `Server error: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
412
52
|
process.exit(1);
|
|
413
53
|
});
|
|
414
54
|
//# sourceMappingURL=index.js.map
|
package/build/log.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
export declare const PREFIJO_LOG = "[mcp-phase-gate]";
|
|
4
|
+
export declare function conectarLog(mcp: McpServer | null): void;
|
|
5
|
+
export declare function log(nivel: LoggingLevel, texto: string): void;
|
|
6
|
+
//# sourceMappingURL=log.d.ts.map
|
package/build/log.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// El log del servidor. Va siempre a stderr, que es lo que guardan los clientes en su archivo de
|
|
2
|
+
// log, y además, cuando hay un cliente conectado, como notificación MCP (`notifications/message`),
|
|
3
|
+
// que algunos clientes muestran en su panel. stdout nunca: es el canal del JSON-RPC.
|
|
4
|
+
//
|
|
5
|
+
// Vive en su propio módulo y no en servidor.ts porque lo usan también phases.ts y tasks.ts, que
|
|
6
|
+
// servidor.ts importa: ponerlo ahí haría un import circular.
|
|
7
|
+
import { SERVER_NAME } from "./config.js";
|
|
8
|
+
export const PREFIJO_LOG = `[${SERVER_NAME}]`;
|
|
9
|
+
// Un solo servidor por proceso, que es lo que hay en producción. Si se crean varios (los tests),
|
|
10
|
+
// las notificaciones van al último que se creó; stderr sigue recibiendo todo.
|
|
11
|
+
let conectado = null;
|
|
12
|
+
// Lo llama crearServidor. Desde ese momento, si el servidor está conectado, cada línea del log
|
|
13
|
+
// también sale como notificación.
|
|
14
|
+
export function conectarLog(mcp) {
|
|
15
|
+
conectado = mcp;
|
|
16
|
+
}
|
|
17
|
+
export function log(nivel, texto) {
|
|
18
|
+
console.error(`${PREFIJO_LOG} ${texto}`);
|
|
19
|
+
// Solo después del initialize: antes, el cliente no espera notificaciones, y en una terminal la
|
|
20
|
+
// línea JSON aparecía cruda en stdout al arrancar.
|
|
21
|
+
if (!conectado?.isConnected() || !conectado.server.getClientVersion())
|
|
22
|
+
return;
|
|
23
|
+
// El SDK descarta solo lo que esté por debajo del nivel que pidió el cliente. Un fallo al
|
|
24
|
+
// notificar no puede tumbar la operación que estaba registrando: ya quedó en stderr.
|
|
25
|
+
conectado.server.sendLoggingMessage({ level: nivel, logger: SERVER_NAME, data: texto }).catch(() => { });
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=log.js.map
|
package/build/paths.js
CHANGED
|
@@ -116,8 +116,8 @@ export function describeProjects() {
|
|
|
116
116
|
return nombres.length ? nombres.join(", ") : "(ninguno todavía)";
|
|
117
117
|
}
|
|
118
118
|
catch {
|
|
119
|
-
// Sin
|
|
120
|
-
return "(no se pudieron leer: revisa
|
|
119
|
+
// Sin DOCS_PATH configurada no se pueden listar; el error real lo da la tool.
|
|
120
|
+
return "(no se pudieron leer: revisa PHASE_GATE_DOCS_PATH)";
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
// Devuelve el nombre real de la carpeta, respetando mayúsculas, o null si no existe.
|
|
@@ -133,7 +133,7 @@ export function requireProjectFolder(project) {
|
|
|
133
133
|
}
|
|
134
134
|
// ─── Carpetas de tarea ──────────────────────────────────────────────────────
|
|
135
135
|
// La ruta de una tarea SIEMPRE se arma aquí, en un solo lugar.
|
|
136
|
-
// Estructura: <
|
|
136
|
+
// Estructura: <PHASE_GATE_DOCS_PATH>/<PROYECTO>/<subcarpeta de tareas>/<tarea>
|
|
137
137
|
export function taskFolderPath(carpetaProyecto, taskName) {
|
|
138
138
|
validarSegmento(taskName, "task_name");
|
|
139
139
|
return path.join(requireBasePath(DOCS), carpetaProyecto, tasksSubdir(), taskName);
|
package/build/phases.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export declare const GLOBAL_RULES_FILE = "global-rules.md";
|
|
2
|
+
export declare const CARPETA_COMANDOS = "comandos";
|
|
3
|
+
export declare function resolveCommandFile(numero: number, capas?: string[]): string | null;
|
|
4
|
+
export declare function esCarpeta(ruta: string): boolean;
|
|
1
5
|
export declare const PHASE_FILE_PATTERN: RegExp;
|
|
2
6
|
export type PhaseFile = {
|
|
3
7
|
file: string;
|
|
@@ -12,11 +16,16 @@ export declare function parsePhaseFile(fullPath: string): {
|
|
|
12
16
|
meta: Record<string, string>;
|
|
13
17
|
cuerpo: string;
|
|
14
18
|
};
|
|
19
|
+
export declare function separarFrontmatter(texto: string): {
|
|
20
|
+
meta: Record<string, string>;
|
|
21
|
+
cuerpo: string;
|
|
22
|
+
};
|
|
15
23
|
export declare function discoverPhases(capas?: string[]): Map<number, PhaseFile>;
|
|
16
24
|
export declare function readPhaseTitle(cuerpo: string, fileName: string): string;
|
|
17
25
|
export declare const CONTEXTO_INICIAL_POR_DEFECTO = "00 - Contexto Inicial.md";
|
|
18
26
|
export declare function nombreContextoInicial(capas?: string[]): string;
|
|
19
27
|
export declare function loadPhasePrompt(phase: number, capas?: string[]): string;
|
|
28
|
+
export declare function resolveGlobalRules(capas?: string[]): string | null;
|
|
29
|
+
export declare function leerPromptNormalizado(ruta: string): string;
|
|
20
30
|
export declare function describePhaseFiles(available: Map<number, PhaseFile>, phase: number): string;
|
|
21
|
-
export declare function describePhases(): string;
|
|
22
31
|
//# sourceMappingURL=phases.d.ts.map
|