mcp-phase-gate 2.0.1 → 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/build/phases.js CHANGED
@@ -4,8 +4,37 @@
4
4
  import * as fs from "fs";
5
5
  import * as path from "path";
6
6
  import { promptsLayers } from "./config.js";
7
+ import { log } from "./log.js";
8
+ import { claveDeNombre, validarSegmento } from "./paths.js";
7
9
  // Las reglas globales se anteponen a cada fase.
8
- const GLOBAL_RULES_FILE = "global-rules.md";
10
+ export const GLOBAL_RULES_FILE = "global-rules.md";
11
+ // Comando de cada fase (`/fN`): `comandos/fN.md` dentro de una capa de prompts. Gana la capa más
12
+ // alta que lo tenga, igual que las fases; sin ninguno, la fase usa un texto genérico.
13
+ export const CARPETA_COMANDOS = "comandos";
14
+ export function resolveCommandFile(numero, capas = promptsLayers()) {
15
+ for (const base of [...capas].reverse()) {
16
+ const ruta = path.join(base, CARPETA_COMANDOS, `f${numero}.md`);
17
+ if (tipoDe(ruta) === "archivo")
18
+ return ruta;
19
+ }
20
+ return null;
21
+ }
22
+ // statSync con throwIfNoEntry solo calla el ENOENT: si un tramo de la ruta es un archivo
23
+ // (ENOTDIR) o no hay permiso, lanza igual. Para preguntar "¿es una carpeta?" eso no es un error.
24
+ function tipoDe(ruta) {
25
+ try {
26
+ const info = fs.statSync(ruta, { throwIfNoEntry: false });
27
+ if (!info)
28
+ return null;
29
+ return info.isDirectory() ? "carpeta" : info.isFile() ? "archivo" : "otro";
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ export function esCarpeta(ruta) {
36
+ return tipoDe(ruta) === "carpeta";
37
+ }
9
38
  // Acepta "fase-1-descubrimiento.md" y también "phase-1-discovery.md",
10
39
  // para que un juego de prompts en inglés funcione sin tocar el código.
11
40
  export const PHASE_FILE_PATTERN = /^(?:fase|phase)-(\d+)[-.]/i;
@@ -14,8 +43,77 @@ export const PHASE_FILE_PATTERN = /^(?:fase|phase)-(\d+)[-.]/i;
14
43
  // describe ese documento —no en un archivo de configuración aparte— para que el nombre exista
15
44
  // en un solo lugar y se lea en vivo, sin recompilar, igual que el resto del prompt.
16
45
  const PHASE_FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
46
+ // Los prompts propios, escritos por ejemplo con el Bloc de notas, llegan en CRLF, y a veces con
47
+ // BOM al principio. Se normalizan al leer: lo que se entrega al modelo y lo que se compara en los
48
+ // tests va siempre en LF y sin BOM. Con el BOM delante, el frontmatter no se reconocía y la fase
49
+ // se quedaba sin documento.
50
+ function aLF(texto) {
51
+ return texto.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
52
+ }
53
+ // Quita los marcadores de ranura y conserva su contenido. El modelo nunca los ve.
54
+ // Una ranura marca el contenido de una sección que un preset podrá sustituir en la Etapa 3:
55
+ // <!-- ranura: verificacion -->
56
+ // contenido por defecto
57
+ // <!-- /ranura -->
58
+ // Solo cuenta un marcador que ocupa su propia línea, y la línea se quita entera. Un marcador con
59
+ // texto detrás, o dos en la misma línea, no se toca: lo avisa loadPhasePrompt.
60
+ export const MARCADOR_RANURA = /^[ \t]*<!--[ \t]*\/?ranura(?:[ \t]*:[ \t]*[\w-]+)?[ \t]*-->[ \t]*\r?$\n?/gm;
61
+ export function quitarMarcadores(texto) {
62
+ return texto.replace(MARCADOR_RANURA, "");
63
+ }
64
+ // Lo que parece un marcador de ranura y sobrevivió a quitarMarcadores: mal escrito (con acento,
65
+ // en mayúsculas, con texto detrás). Llegaría tal cual al modelo, así que queda en el log.
66
+ const RESTO_DE_MARCADOR = /<!--[^\n]*?ranura/i;
67
+ function entregable(texto, archivo) {
68
+ const limpio = quitarMarcadores(texto);
69
+ if (RESTO_DE_MARCADOR.test(limpio)) {
70
+ log("warning", `${archivo} tiene un marcador de ranura que no se reconoce: llega tal cual al modelo.`);
71
+ }
72
+ return limpio.trim();
73
+ }
74
+ // La subcarpeta de recibos acaba dentro de un file_name, así que tiene que ser un solo segmento
75
+ // portable, y visible: write_central_doc no escribe en segmentos que empiezan por punto. Un valor
76
+ // inválido no tumba la fase: se ignora y queda en el log.
77
+ function leerRecibos(valor, archivo) {
78
+ if (!valor)
79
+ return undefined;
80
+ try {
81
+ const segmento = validarSegmento(valor, "recibos");
82
+ if (segmento.startsWith("."))
83
+ throw new Error("no puede empezar por punto.");
84
+ return segmento;
85
+ }
86
+ catch (error) {
87
+ log("warning", `Se ignora el frontmatter recibos de ${archivo}: ${error.message}`);
88
+ return undefined;
89
+ }
90
+ }
91
+ // Una subcarpeta de recibos con el nombre de un documento rompe el flujo: si se llama como el
92
+ // documento de la fase, la carpeta hace que la fase cuente como cerrada al primer recibo. Se
93
+ // comprueba al final, con las fases de todas las capas ya resueltas.
94
+ function descartarRecibosQueChocan(phases) {
95
+ const documentos = new Set();
96
+ for (const datos of phases.values()) {
97
+ for (const nombre of [datos.documento, datos.documentoInicial]) {
98
+ if (nombre)
99
+ documentos.add(claveDeNombre(nombre));
100
+ }
101
+ }
102
+ for (const datos of phases.values()) {
103
+ if (datos.recibos && documentos.has(claveDeNombre(datos.recibos))) {
104
+ log("warning", `Se ignora el frontmatter recibos de ${datos.file}: ` +
105
+ `"${datos.recibos}" es el nombre de un documento de fase.`);
106
+ datos.recibos = undefined;
107
+ }
108
+ }
109
+ }
17
110
  export function parsePhaseFile(fullPath) {
18
- const crudo = fs.readFileSync(fullPath, "utf-8");
111
+ return separarFrontmatter(fs.readFileSync(fullPath, "utf-8"));
112
+ }
113
+ // La cabecera "clave: valor" de un texto ya leído, y el resto. Sirve igual para un prompt que
114
+ // para un recibo de lote, que usan el mismo formato.
115
+ export function separarFrontmatter(texto) {
116
+ const crudo = aLF(texto);
19
117
  const encontrada = crudo.match(PHASE_FRONTMATTER);
20
118
  if (!encontrada)
21
119
  return { meta: {}, cuerpo: crudo };
@@ -44,7 +142,9 @@ export function parsePhaseFile(fullPath) {
44
142
  export function discoverPhases(capas = promptsLayers()) {
45
143
  const phases = new Map();
46
144
  for (const base of capas) {
47
- if (!fs.existsSync(base))
145
+ // Una capa que no existe o que no es una carpeta (PHASE_GATE_PROMPTS_PATH apuntando a un
146
+ // archivo) se salta: doctor la avisa, y las fases de las demás capas siguen sirviendo.
147
+ if (!esCarpeta(base))
48
148
  continue;
49
149
  // Se resuelve capa por capa: dentro de una, gana el primero por orden alfabético; entre
50
150
  // capas, gana la de más arriba. Sin este mapa intermedio la regla alfabética de la capa
@@ -71,11 +171,13 @@ export function discoverPhases(capas = promptsLayers()) {
71
171
  title: readPhaseTitle(cuerpo, file),
72
172
  documento: meta.documento || undefined,
73
173
  documentoInicial: meta.documento_inicial || undefined,
174
+ recibos: leerRecibos(meta.recibos, fullPath),
74
175
  });
75
176
  }
76
177
  for (const [numero, datos] of deEstaCapa)
77
178
  phases.set(numero, datos);
78
179
  }
180
+ descartarRecibosQueChocan(phases);
79
181
  return phases;
80
182
  }
81
183
  // El título es el primer encabezado "# ..." del cuerpo; si no lo hay, se deriva del nombre
@@ -107,9 +209,9 @@ export function loadPhasePrompt(phase, capas = promptsLayers()) {
107
209
  const parts = [];
108
210
  const globales = resolveGlobalRules(capas);
109
211
  if (globales) {
110
- parts.push(fs.readFileSync(globales, "utf-8").trim());
212
+ parts.push(entregable(aLF(fs.readFileSync(globales, "utf-8")), globales));
111
213
  }
112
- parts.push(parsePhaseFile(entry.file).cuerpo.trim());
214
+ parts.push(entregable(parsePhaseFile(entry.file).cuerpo, entry.file));
113
215
  const archivos = describePhaseFiles(available, phase);
114
216
  if (archivos)
115
217
  parts.push(archivos);
@@ -119,7 +221,7 @@ export function loadPhasePrompt(phase, capas = promptsLayers()) {
119
221
  // fábrica, no se suman: concatenar dos juegos de reglas hace imposible saber de qué archivo
120
222
  // salió la instrucción que estorbó cuando algo sale raro, que es justo el momento en que hay
121
223
  // que saberlo. Si quieres las de fábrica y algo más, las copias y añades.
122
- function resolveGlobalRules(capas) {
224
+ export function resolveGlobalRules(capas = promptsLayers()) {
123
225
  for (const base of [...capas].reverse()) {
124
226
  const ruta = path.join(base, GLOBAL_RULES_FILE);
125
227
  if (fs.existsSync(ruta))
@@ -127,8 +229,14 @@ function resolveGlobalRules(capas) {
127
229
  }
128
230
  return null;
129
231
  }
232
+ // El texto de un prompt propio tal como lo ve el modelo: en LF y sin BOM. Lo usan quienes leen
233
+ // un archivo de prompts fuera de este módulo, como el diagnóstico.
234
+ export function leerPromptNormalizado(ruta) {
235
+ return aLF(fs.readFileSync(ruta, "utf-8"));
236
+ }
130
237
  // Bloque que se añade al final del prompt con los nombres exactos de los documentos: el que
131
- // produce esta fase y los de las anteriores, que son los que hay que leer. Va aquí y no en el
238
+ // produce esta fase y los de las anteriores, que son los que hay que leer, y la subcarpeta de
239
+ // recibos de la fase que la declare. Va aquí y no en el
132
240
  // comando del cliente para que el nombre viva en un solo sitio; así afinar un prompt no obliga
133
241
  // a editar además un comando en cada dispositivo.
134
242
  export function describePhaseFiles(available, phase) {
@@ -136,7 +244,10 @@ export function describePhaseFiles(available, phase) {
136
244
  const previos = [...available.entries()]
137
245
  .filter(([numero, datos]) => numero < phase && datos.documento)
138
246
  .sort((a, b) => a[0] - b[0])
139
- .map(([numero, datos]) => `- Fase ${numero}: \`${datos.documento}\``);
247
+ .map(([numero, datos]) => {
248
+ const recibos = datos.recibos ? ` (recibos de lote en \`${datos.recibos}/\`)` : "";
249
+ return `- Fase ${numero}: \`${datos.documento}\`${recibos}`;
250
+ });
140
251
  if (!propio && previos.length === 0)
141
252
  return "";
142
253
  const lineas = ["## ARCHIVOS DE ESTA TAREA (nombres exactos)"];
@@ -146,19 +257,16 @@ export function describePhaseFiles(available, phase) {
146
257
  if (propio) {
147
258
  lineas.push("", `El documento de esta fase se llama exactamente \`${propio}\`. Guárdalo con ` +
148
259
  "`write_central_doc` en la carpeta de la tarea activa, con ese nombre y sin cambiarlo.");
260
+ const recibos = available.get(phase)?.recibos;
261
+ if (recibos) {
262
+ lineas.push("", `Los recibos de lote van en la subcarpeta \`${recibos}/\`, uno por lote: ` +
263
+ `\`${recibos}/L01 - <slug>.md\`. Con un solo lote no hay subcarpeta: el recibo es el ` +
264
+ "documento de esta fase.");
265
+ }
149
266
  }
150
267
  else {
151
268
  lineas.push("", "Esta fase no deja documento en la Documentación Central: su salida va en el chat.");
152
269
  }
153
270
  return lineas.join("\n");
154
271
  }
155
- export function describePhases() {
156
- const available = discoverPhases();
157
- if (available.size === 0)
158
- return "No se encontraron prompts de fase.";
159
- return [...available.entries()]
160
- .sort((a, b) => a[0] - b[0])
161
- .map(([number, { title }]) => `${number}: ${title}`)
162
- .join(", ");
163
- }
164
272
  //# sourceMappingURL=phases.js.map
@@ -0,0 +1,33 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ export declare function argumentosDeFase(numero: number): {
4
+ project: z.ZodOptional<z.ZodString>;
5
+ task_name: z.ZodOptional<z.ZodString>;
6
+ contexto: z.ZodOptional<z.ZodString>;
7
+ commits?: never;
8
+ } | {
9
+ project: z.ZodOptional<z.ZodString>;
10
+ commits: z.ZodOptional<z.ZodString>;
11
+ task_name?: never;
12
+ contexto?: never;
13
+ } | {
14
+ project: z.ZodOptional<z.ZodString>;
15
+ task_name?: never;
16
+ contexto?: never;
17
+ commits?: never;
18
+ };
19
+ export declare function tituloDePrompt(numero: number, titulo: string): string;
20
+ export declare function descripcionDePrompt(numero: number, titulo: string): string;
21
+ export declare function textoGenerico(numero: number, titulo: string): string;
22
+ type Argumentos = {
23
+ project?: string | undefined;
24
+ task_name?: string | undefined;
25
+ commits?: string | undefined;
26
+ contexto?: string | undefined;
27
+ };
28
+ export declare function textoDelComando(numero: number, titulo: string, args: Argumentos, capas?: string[]): string;
29
+ export declare function sincronizarPrompts(mcp: McpServer): void;
30
+ export declare function asegurarCapacidadDePrompts(mcp: McpServer): void;
31
+ export declare function vigilarCapas(mcp: McpServer, esperaMs?: number): () => void;
32
+ export {};
33
+ //# sourceMappingURL=prompts-mcp.d.ts.map
@@ -0,0 +1,194 @@
1
+ // Los comandos de fase (/f1…/fN) servidos como MCP Prompts. Antes había que copiar cada comando a
2
+ // mano en la carpeta de cada cliente y de cada equipo, y las copias se desincronizaban; ahora el
3
+ // servidor los ofrece a cualquier cliente que muestre prompts, y llegan con cada versión.
4
+ //
5
+ // El texto de cada comando vive en `comandos/fN.md` dentro de una capa de prompts (los de fábrica
6
+ // en prompts/comandos/), y se resuelve por capas como las fases: gana la más alta que lo tenga.
7
+ // Una fase sin comando, como una fase-6 propia, recibe un texto genérico que funciona igual.
8
+ //
9
+ // El prompt NO incluye el prompt de la fase: sigue siendo orquestación ("llama a
10
+ // get_phase_prompt(N)…"). Devolver ya el texto de la fase ahorraría una llamada, pero crearía dos
11
+ // caminos para recibir el mismo prompt, y en uno la compuerta no recibiría el `project`.
12
+ import * as fs from "fs";
13
+ import { z } from "zod";
14
+ import { promptsLayers } from "./config.js";
15
+ import { log } from "./log.js";
16
+ import { discoverPhases, parsePhaseFile, resolveCommandFile } from "./phases.js";
17
+ // Todos los argumentos son opcionales: el texto de cada comando ya pide lo que falte, y así el
18
+ // prompt sirve también en clientes que parten los argumentos por espacios (un contexto largo puede
19
+ // ir pegado después del comando, como texto normal del chat).
20
+ const PROJECT = z.string().optional().describe("Proyecto en el que estás trabajando.");
21
+ export function argumentosDeFase(numero) {
22
+ if (numero === 1) {
23
+ return {
24
+ project: PROJECT,
25
+ task_name: z.string().optional().describe("Nombre corto de la tarea nueva."),
26
+ contexto: z.string().optional().describe("El correo, mensaje o requerimiento completo."),
27
+ };
28
+ }
29
+ if (numero === 5) {
30
+ return {
31
+ project: PROJECT,
32
+ commits: z
33
+ .string()
34
+ .optional()
35
+ .describe("Rango de commits a auditar. Si lo omites, se toman de los recibos de la Fase 4."),
36
+ };
37
+ }
38
+ return { project: PROJECT };
39
+ }
40
+ export function tituloDePrompt(numero, titulo) {
41
+ return `Fase ${numero} · ${titulo}`;
42
+ }
43
+ export function descripcionDePrompt(numero, titulo) {
44
+ return `Arranca la Fase ${numero} (${titulo}) del flujo de mcp-phase-gate.`;
45
+ }
46
+ export function textoGenerico(numero, titulo) {
47
+ return [
48
+ `Vas a iniciar la FASE ${numero} (${titulo}) del flujo del mcp-phase-gate.`,
49
+ "",
50
+ "Pasos:",
51
+ "1. Si no sabes cuál es la tarea activa, llama a `get_active_task` (o pídeme project + task_name).",
52
+ `2. Llama a \`get_phase_prompt(${numero})\` indicándole también el \`project\` en el que estoy trabajando, y asume ese rol.`,
53
+ "3. Lee los documentos de las fases anteriores con `read_central_doc`; `get_phase_prompt` te dio sus nombres exactos.",
54
+ "4. Sigue el prompt de la fase y guarda su documento con `write_central_doc`, con el nombre que te indicó `get_phase_prompt`.",
55
+ ].join("\n");
56
+ }
57
+ // La sección con lo que vino como argumento. Solo aparecen las líneas de los que vinieron, y sin
58
+ // ninguno la sección no existe: así el texto queda igual que el comando escrito a mano.
59
+ function datosDeInvocacion(args) {
60
+ const presente = (valor) => typeof valor === "string" && valor.trim() !== "";
61
+ const lineas = [];
62
+ // project, task_name y commits son de una línea: los saltos se aplanan, para que un valor no
63
+ // pueda abrir secciones nuevas dentro del texto. El contexto sí es multilínea por naturaleza, y
64
+ // va tal cual: lo escribe la persona, igual que si lo pegara en el chat.
65
+ const enUnaLinea = (valor) => valor.replace(/\s+/g, " ").trim();
66
+ if (presente(args.project))
67
+ lineas.push(`- project: ${enUnaLinea(args.project)}`);
68
+ if (presente(args.task_name))
69
+ lineas.push(`- task_name: ${enUnaLinea(args.task_name)}`);
70
+ if (presente(args.commits))
71
+ lineas.push(`- commits: ${enUnaLinea(args.commits)}`);
72
+ if (presente(args.contexto))
73
+ lineas.push("- initial_context:", "", args.contexto.trim());
74
+ return lineas.length ? ["## Datos de esta invocación", ...lineas].join("\n") : null;
75
+ }
76
+ // El texto se lee en cada llamada, como los prompts de fase: editar un comando no exige reiniciar.
77
+ export function textoDelComando(numero, titulo, args, capas = promptsLayers()) {
78
+ const archivo = resolveCommandFile(numero, capas);
79
+ let cuerpo;
80
+ if (archivo) {
81
+ try {
82
+ cuerpo = parsePhaseFile(archivo).cuerpo.trim();
83
+ }
84
+ catch (error) {
85
+ log("warning", `No se pudo leer ${archivo}; se usa el texto genérico: ${error instanceof Error ? error.message : String(error)}`);
86
+ cuerpo = textoGenerico(numero, titulo);
87
+ }
88
+ }
89
+ else {
90
+ cuerpo = textoGenerico(numero, titulo);
91
+ }
92
+ const datos = datosDeInvocacion(args);
93
+ return datos ? `${cuerpo}\n\n${datos}` : cuerpo;
94
+ }
95
+ const registros = new WeakMap();
96
+ function registrar(mcp, numero, titulo) {
97
+ return mcp.registerPrompt(`f${numero}`, {
98
+ title: tituloDePrompt(numero, titulo),
99
+ description: descripcionDePrompt(numero, titulo),
100
+ argsSchema: argumentosDeFase(numero),
101
+ }, async (args) => {
102
+ // El título se toma del registro vigente: puede haber cambiado desde que se registró.
103
+ const vigente = registros.get(mcp)?.get(numero)?.titulo ?? titulo;
104
+ return {
105
+ description: descripcionDePrompt(numero, vigente),
106
+ messages: [{ role: "user", content: { type: "text", text: textoDelComando(numero, vigente, args) } }],
107
+ };
108
+ });
109
+ }
110
+ // Nunca lanza: se llama tras cada tool, y un prompt que no se pudo registrar no debe convertir en
111
+ // error la respuesta de una tool que salió bien.
112
+ export function sincronizarPrompts(mcp) {
113
+ try {
114
+ const capas = promptsLayers();
115
+ const fases = discoverPhases(capas);
116
+ let actuales = registros.get(mcp);
117
+ if (!actuales) {
118
+ actuales = new Map();
119
+ registros.set(mcp, actuales);
120
+ }
121
+ for (const [numero, registro] of [...actuales]) {
122
+ if (!fases.has(numero)) {
123
+ registro.prompt.remove();
124
+ actuales.delete(numero);
125
+ }
126
+ }
127
+ for (const [numero, datos] of fases) {
128
+ const comando = resolveCommandFile(numero, capas);
129
+ const registro = actuales.get(numero);
130
+ if (!registro) {
131
+ actuales.set(numero, { prompt: registrar(mcp, numero, datos.title), titulo: datos.title, comando });
132
+ continue;
133
+ }
134
+ if (registro.titulo !== datos.title || registro.comando !== comando) {
135
+ registro.titulo = datos.title;
136
+ registro.comando = comando;
137
+ registro.prompt.update({
138
+ title: tituloDePrompt(numero, datos.title),
139
+ description: descripcionDePrompt(numero, datos.title),
140
+ });
141
+ }
142
+ }
143
+ }
144
+ catch (error) {
145
+ log("warning", `No se pudieron sincronizar los prompts /fN: ${error instanceof Error ? error.message : String(error)}`);
146
+ }
147
+ }
148
+ // La capacidad `prompts` solo se puede declarar antes de conectar. Si al arrancar no hay ninguna
149
+ // fase (una carpeta de prompts vacía o mal configurada), el primer /fN que apareciera después
150
+ // intentaría declararla con la conexión abierta y el SDK lo rechazaría. Registrar y quitar un
151
+ // prompt de paso deja la capacidad declarada sin que la lista tenga nada.
152
+ export function asegurarCapacidadDePrompts(mcp) {
153
+ if ((registros.get(mcp)?.size ?? 0) > 0)
154
+ return;
155
+ // Un nombre que ninguna fase puede tener: las fases se registran como f<número>.
156
+ mcp.registerPrompt("phase-gate-provisional", { description: "provisional" }, async () => ({ messages: [] })).remove();
157
+ }
158
+ // ─── Vigilancia de las capas ────────────────────────────────────────────────
159
+ // Mejora oportunista: con fs.watch, un comando o una fase nueva aparece sin esperar a la siguiente
160
+ // llamada a una tool. En OneDrive y en algunas unidades de red fs.watch no avisa, y por eso lo que
161
+ // garantiza el resultado es la sincronización tras cada tool, no esto. Los vigilantes no mantienen
162
+ // vivo el proceso (unref) y se cierran con el servidor.
163
+ export function vigilarCapas(mcp, esperaMs = 300) {
164
+ const vigilantes = [];
165
+ let temporizador = null;
166
+ const programar = () => {
167
+ if (temporizador)
168
+ clearTimeout(temporizador);
169
+ temporizador = setTimeout(() => {
170
+ temporizador = null;
171
+ sincronizarPrompts(mcp);
172
+ }, esperaMs);
173
+ temporizador.unref();
174
+ };
175
+ for (const capa of promptsLayers()) {
176
+ if (!fs.statSync(capa, { throwIfNoEntry: false })?.isDirectory())
177
+ continue;
178
+ try {
179
+ const vigilante = fs.watch(capa, { recursive: true, persistent: false }, programar);
180
+ vigilante.on("error", () => vigilante.close());
181
+ vigilantes.push(vigilante);
182
+ }
183
+ catch {
184
+ // Sin vigilancia en esta capa: la sincronización tras cada tool sigue cubriéndola.
185
+ }
186
+ }
187
+ return () => {
188
+ if (temporizador)
189
+ clearTimeout(temporizador);
190
+ for (const vigilante of vigilantes)
191
+ vigilante.close();
192
+ };
193
+ }
194
+ //# sourceMappingURL=prompts-mcp.js.map
@@ -0,0 +1,8 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare const URI_ESTADO = "phase-gate://estado";
3
+ export declare const URI_FASES = "phase-gate://fases";
4
+ export declare function uriDeDocumento(proyecto: string, tarea: string, documento: string): string;
5
+ export declare function uriDeRecibo(proyecto: string, tarea: string, recibo: string): string;
6
+ export declare function tablaDeFases(): string;
7
+ export declare function registrarRecursos(mcp: McpServer): void;
8
+ //# sourceMappingURL=recursos.d.ts.map