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.
@@ -27,68 +27,122 @@ export declare const readCentralDocShape: {
27
27
  file_path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
28
28
  };
29
29
  export declare const writeCentralDocShape: {
30
+ file_path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
30
31
  content: z.ZodString;
31
32
  force: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
32
33
  project: z.ZodOptional<z.ZodNullable<z.ZodString>>;
33
34
  task_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
34
35
  file_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
35
- file_path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
36
36
  };
37
37
  export declare const readCrossRepoShape: {
38
38
  repo_name: z.ZodString;
39
39
  file_path: z.ZodString;
40
40
  };
41
- declare const SHAPES: {
42
- start_task: {
43
- project: z.ZodString;
44
- task_name: z.ZodString;
45
- initial_context: z.ZodString;
46
- crear_proyecto: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
47
- };
48
- get_phase_prompt: {
49
- phase: z.ZodNumber;
50
- project: z.ZodOptional<z.ZodNullable<z.ZodString>>;
51
- task_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
52
- };
53
- get_active_task: {
54
- project: z.ZodOptional<z.ZodNullable<z.ZodString>>;
55
- };
56
- list_tasks: {
57
- project: z.ZodOptional<z.ZodNullable<z.ZodString>>;
58
- };
59
- switch_task: {
60
- project: z.ZodString;
61
- task_name: z.ZodString;
62
- };
63
- read_central_doc: {
64
- project: z.ZodOptional<z.ZodNullable<z.ZodString>>;
65
- task_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
66
- file_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
67
- file_path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
68
- };
69
- write_central_doc: {
70
- content: z.ZodString;
71
- force: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
72
- project: z.ZodOptional<z.ZodNullable<z.ZodString>>;
73
- task_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
74
- file_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
75
- file_path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
76
- };
77
- read_cross_repo: {
78
- repo_name: z.ZodString;
79
- file_path: z.ZodString;
80
- };
41
+ export declare const getOrchestratorStatusShape: {};
42
+ export declare const diagnosticoShape: {
43
+ paquete: z.ZodObject<{
44
+ version: z.ZodString;
45
+ ruta: z.ZodString;
46
+ via: z.ZodEnum<{
47
+ npx: "npx";
48
+ global: "global";
49
+ dependencia: "dependencia";
50
+ clon: "clon";
51
+ }>;
52
+ }, z.core.$strip>;
53
+ node: z.ZodObject<{
54
+ version: z.ZodString;
55
+ plataforma: z.ZodString;
56
+ }, z.core.$strip>;
57
+ variables: z.ZodArray<z.ZodObject<{
58
+ logica: z.ZodEnum<{
59
+ DOCS_PATH: "DOCS_PATH";
60
+ REPOS_PATH: "REPOS_PATH";
61
+ TASKS_SUBDIR: "TASKS_SUBDIR";
62
+ PROMPTS_PATH: "PROMPTS_PATH";
63
+ STATE_PATH: "STATE_PATH";
64
+ ELICITATION: "ELICITATION";
65
+ }>;
66
+ valor: z.ZodOptional<z.ZodString>;
67
+ usada: z.ZodNullable<z.ZodString>;
68
+ fuente: z.ZodEnum<{
69
+ ".env": ".env";
70
+ entorno: "entorno";
71
+ "sin definir": "sin definir";
72
+ }>;
73
+ discrepancia: z.ZodNullable<z.ZodString>;
74
+ }, z.core.$strip>>;
75
+ rutas: z.ZodObject<{
76
+ docs: z.ZodObject<{
77
+ ruta: z.ZodOptional<z.ZodString>;
78
+ existe: z.ZodBoolean;
79
+ error: z.ZodOptional<z.ZodString>;
80
+ }, z.core.$strip>;
81
+ repos: z.ZodObject<{
82
+ ruta: z.ZodOptional<z.ZodString>;
83
+ existe: z.ZodBoolean;
84
+ error: z.ZodOptional<z.ZodString>;
85
+ }, z.core.$strip>;
86
+ }, z.core.$strip>;
87
+ estado: z.ZodObject<{
88
+ archivo: z.ZodString;
89
+ existe: z.ZodBoolean;
90
+ punteros: z.ZodNumber;
91
+ }, z.core.$strip>;
92
+ capas: z.ZodArray<z.ZodObject<{
93
+ ruta: z.ZodString;
94
+ etiqueta: z.ZodEnum<{
95
+ fábrica: "fábrica";
96
+ propia: "propia";
97
+ }>;
98
+ existe: z.ZodBoolean;
99
+ }, z.core.$strip>>;
100
+ reglasGlobales: z.ZodObject<{
101
+ archivo: z.ZodNullable<z.ZodString>;
102
+ capa: z.ZodNullable<z.ZodString>;
103
+ faltan: z.ZodArray<z.ZodString>;
104
+ }, z.core.$strip>;
105
+ fases: z.ZodArray<z.ZodObject<{
106
+ numero: z.ZodNumber;
107
+ titulo: z.ZodString;
108
+ archivo: z.ZodString;
109
+ capa: z.ZodString;
110
+ documento: z.ZodOptional<z.ZodString>;
111
+ recibos: z.ZodOptional<z.ZodString>;
112
+ comando: z.ZodNullable<z.ZodString>;
113
+ }, z.core.$strip>>;
114
+ proyectos: z.ZodArray<z.ZodObject<{
115
+ nombre: z.ZodString;
116
+ tareas: z.ZodNumber;
117
+ activa: z.ZodNullable<z.ZodObject<{
118
+ tarea: z.ZodString;
119
+ origen: z.ZodEnum<{
120
+ puntero: "puntero";
121
+ deducida: "deducida";
122
+ }>;
123
+ fase: z.ZodString;
124
+ }, z.core.$strip>>;
125
+ }, z.core.$strip>>;
126
+ cliente: z.ZodOptional<z.ZodObject<{
127
+ nombre: z.ZodString;
128
+ version: z.ZodString;
129
+ capacidades: z.ZodObject<{
130
+ elicitation: z.ZodBoolean;
131
+ roots: z.ZodBoolean;
132
+ }, z.core.$strip>;
133
+ }, z.core.$strip>>;
134
+ hallazgos: z.ZodArray<z.ZodObject<{
135
+ severidad: z.ZodEnum<{
136
+ error: "error";
137
+ info: "info";
138
+ aviso: "aviso";
139
+ }>;
140
+ codigo: z.ZodString;
141
+ texto: z.ZodString;
142
+ }, z.core.$strip>>;
81
143
  };
82
- type NombreTool = keyof typeof SHAPES;
83
- type SinNulos<T> = {
144
+ export type SinNulos<T> = {
84
145
  [K in keyof T]: Exclude<T[K], null>;
85
146
  };
86
- export type LlamadaValidada = {
87
- [K in NombreTool]: {
88
- tool: K;
89
- args: SinNulos<z.output<z.ZodObject<(typeof SHAPES)[K]>>>;
90
- };
91
- }[NombreTool];
92
- export declare function validarLlamada(tool: string, args: unknown): LlamadaValidada;
93
- export {};
147
+ export declare function sinNulos<T extends Record<string, unknown>>(args: T): SinNulos<T>;
94
148
  //# sourceMappingURL=schemas.d.ts.map
package/build/schemas.js CHANGED
@@ -1,108 +1,181 @@
1
1
  // Los argumentos de cada tool, declarados una sola vez con zod.
2
2
  //
3
3
  // Antes cada tool leía sus argumentos con String(args?.x), que no valida nada: sin task_name,
4
- // String(undefined) creaba la tarea "undefined/" y la dejaba activa. Ahora todo pasa por aquí
5
- // antes de usarse, y el error nombra el campo.
4
+ // String(undefined) creaba la tarea "undefined/" y la dejaba activa. Desde la Etapa 0 todo pasa
5
+ // por aquí antes de usarse, y el error nombra el campo.
6
6
  //
7
- // Se exportan shapes (los campos sueltos) y no z.object(...): es lo que recibe registerTool
8
- // del SDK, al que se pasarán tal cual en la migración a McpServer (Etapa 2).
7
+ // Se exportan shapes (los campos sueltos) y no z.object(...): es lo que recibe registerTool del
8
+ // SDK. El SDK genera con ellos el JSON Schema que ve el cliente —por eso cada campo lleva aquí su
9
+ // descripción— y valida cada llamada antes del callback.
10
+ //
11
+ // Los mensajes de error van en cada campo, en español, porque el SDK los entrega tal cual detrás
12
+ // de su prefijo en inglés ("Input validation error: Invalid arguments for tool …"). Sin ellos
13
+ // llegaría el texto genérico de zod ("expected string, received undefined"), que no le dice al
14
+ // modelo cómo corregir la llamada.
9
15
  //
10
16
  // Los opcionales son .nullish() y null cuenta como ausente: algunos clientes con modelos en
11
17
  // modo estricto mandan null en todo campo opcional. Un número en texto (phase: "3") no se
12
18
  // convierte: se rechaza con un mensaje que dice qué se espera. Es raro, el modelo lo corrige
13
- // al primer intento, y convertirlo complicaría el JSON Schema que se generará desde aquí.
19
+ // al primer intento, y convertirlo complicaría el JSON Schema que se genera desde aquí.
14
20
  import { z } from "zod";
21
+ // Lo que se espera de cada tipo, dicho como se le diría al modelo para que corrija la llamada.
22
+ // El único número es `phase`, de ahí el ejemplo.
23
+ const TIPOS = {
24
+ string: "un texto",
25
+ number: "un número, por ejemplo 3",
26
+ int: "un número entero, por ejemplo 3",
27
+ boolean: "true o false",
28
+ object: "un objeto",
29
+ };
30
+ // El mensaje de un campo. null también es "falta": en un opcional se tolera antes de llegar aquí,
31
+ // en un obligatorio es lo mismo que no venir.
32
+ function mensaje(campo) {
33
+ const nombre = `\`${campo}\``;
34
+ return (issue) => {
35
+ if (issue.input === undefined || issue.input === null)
36
+ return `Falta ${nombre}.`;
37
+ if (issue.code === "invalid_type")
38
+ return `${nombre} debe ser ${TIPOS[issue.expected] ?? issue.expected}.`;
39
+ // El único máximo que puede saltar es el del entero seguro de `phase`.
40
+ if (issue.code === "too_big")
41
+ return `${nombre} es demasiado grande.`;
42
+ return `${nombre} no es válido.`;
43
+ };
44
+ }
45
+ function texto(campo) {
46
+ return z.string({ error: mensaje(campo) });
47
+ }
48
+ // `recortar` quita los espacios ANTES de exigir el mínimo: " " cuenta como que falta.
49
+ function obligatorio(campo, opciones = {}) {
50
+ const base = opciones.recortar ? texto(campo).trim() : texto(campo);
51
+ return base.min(1, `Falta \`${campo}\`.`);
52
+ }
53
+ function booleano(campo) {
54
+ return z.boolean({ error: mensaje(campo) });
55
+ }
56
+ // ─── Descripciones ──────────────────────────────────────────────────────────
57
+ // Son estáticas: no nombran proyectos ni fases concretas. Antes se armaban al arrancar con la
58
+ // lista de proyectos y de fases, y se quedaban viejas en cuanto alguien creaba una carpeta con el
59
+ // cliente abierto, porque la lista de tools no se vuelve a pedir. Lo que cambia vive ahora en los
60
+ // recursos phase-gate://estado y phase-gate://fases, que sí avisan cuando cambian.
61
+ const PROYECTO = "Proyecto: el nombre de una carpeta de proyecto de la Documentación Central. Si no existe, la " +
62
+ "tool te devuelve los que hay. También los listan `list_tasks`, `get_orchestrator_status` y el recurso " +
63
+ "`phase-gate://estado`.";
64
+ const FASE = "Número de fase. Con los prompts de fábrica son 1 a 5; `get_orchestrator_status` y el recurso " +
65
+ "`phase-gate://fases` listan las disponibles con su título y documento.";
66
+ const TAREA = "Nombre de la carpeta de la tarea.";
67
+ const ARCHIVO = "Nombre (o ruta relativa) del archivo dentro de la carpeta de la tarea (ej. '01 - Análisis Técnico.md').";
68
+ // ─── Shapes ─────────────────────────────────────────────────────────────────
15
69
  export const startTaskShape = {
16
- project: z.string().trim().min(1, "Falta `project`."),
17
- task_name: z.string().min(1, "Falta `task_name`."),
18
- initial_context: z.string().min(1, "Falta `initial_context`."),
19
- crear_proyecto: z.boolean().nullish(),
70
+ project: obligatorio("project", { recortar: true }).describe(PROYECTO),
71
+ task_name: obligatorio("task_name").describe("Nombre de la tarea (creará una carpeta con este nombre)."),
72
+ initial_context: obligatorio("initial_context").describe("El contexto, correo o requerimiento inicial completo."),
73
+ crear_proyecto: booleano("crear_proyecto")
74
+ .nullish()
75
+ .describe("Solo para ESTRENAR un proyecto que todavía no existe. Déjalo sin poner salvo que el usuario te haya " +
76
+ "dicho explícitamente que quiere crear un proyecto nuevo: sirve para que un nombre mal escrito no cree " +
77
+ "una carpeta suelta en la Documentación."),
20
78
  };
21
79
  export const getPhasePromptShape = {
22
- phase: z.number().int(),
23
- project: z.string().nullish(),
24
- task_name: z.string().nullish(),
80
+ phase: z.number({ error: mensaje("phase") }).int({ error: mensaje("phase") }).describe(FASE),
81
+ project: texto("project")
82
+ .nullish()
83
+ .describe(`${PROYECTO} Pásalo siempre que lo sepas —lo sabes por el repositorio que tienes abierto— para que se ` +
84
+ "pueda comprobar que no te saltas una fase. La tarea se toma de la activa de ese proyecto."),
85
+ task_name: texto("task_name")
86
+ .nullish()
87
+ .describe("Solo si quieres comprobar contra una tarea distinta de la activa del proyecto. Normalmente se omite."),
25
88
  };
26
89
  export const getActiveTaskShape = {
27
- project: z.string().nullish(),
90
+ project: texto("project")
91
+ .nullish()
92
+ .describe(`${PROYECTO} Si lo omites, se devuelve la tarea activa de cada proyecto.`),
28
93
  };
29
94
  export const listTasksShape = {
30
- project: z.string().nullish(),
95
+ project: texto("project").nullish().describe(`${PROYECTO} Si lo omites, se listan todos.`),
31
96
  };
32
97
  export const switchTaskShape = {
33
- project: z.string().trim().min(1, "Falta `project`."),
34
- task_name: z.string().min(1, "Falta `task_name`."),
98
+ project: obligatorio("project", { recortar: true }).describe(PROYECTO),
99
+ task_name: obligatorio("task_name").describe("Nombre de la carpeta de la tarea a activar. Si no existe, la tool te devuelve las que sí."),
35
100
  };
36
101
  // Los cuatro son opcionales por separado: qué combinación vale lo decide resolveDocPath.
37
102
  export const readCentralDocShape = {
38
- project: z.string().nullish(),
39
- task_name: z.string().nullish(),
40
- file_name: z.string().nullish(),
41
- file_path: z.string().nullish(),
103
+ project: texto("project").nullish().describe(`${PROYECTO} Úsalo junto con task_name y file_name.`),
104
+ task_name: texto("task_name").nullish().describe(TAREA),
105
+ file_name: texto("file_name").nullish().describe(ARCHIVO),
106
+ file_path: texto("file_path")
107
+ .nullish()
108
+ .describe("ALTERNATIVA: ruta relativa dentro de la Documentación Central (ej. " +
109
+ "'PROYECTO-A/Proyectos/tarea/01 - Análisis Técnico.md'). Solo si no usas project+task_name."),
42
110
  };
43
111
  export const writeCentralDocShape = {
44
112
  ...readCentralDocShape,
45
- content: z.string(),
46
- // Solo con confirmación explícita del usuario en el chat. Permite reescribir un documento
47
- // congelado o el contexto inicial.
48
- force: z.boolean().nullish(),
113
+ file_path: texto("file_path")
114
+ .nullish()
115
+ .describe("ALTERNATIVA: ruta relativa dentro de la Documentación Central, que tiene que caer dentro de la carpeta " +
116
+ "de una tarea que ya existe (ej. 'PROYECTO-A/Proyectos/tarea/01 - Análisis Técnico.md'). Los archivos " +
117
+ "del proyecto no se escriben con esta tool. Solo si no usas project+task_name."),
118
+ content: texto("content").describe("Contenido a escribir."),
119
+ force: booleano("force")
120
+ .nullish()
121
+ .describe("Permite reescribir un documento congelado o el contexto inicial. Solo se usa si tu cliente no puede " +
122
+ "pedirle la confirmación al usuario directamente. Nunca lo pongas sin que el usuario lo haya confirmado en el chat."),
49
123
  };
50
124
  export const readCrossRepoShape = {
51
- repo_name: z.string().min(1, "Falta `repo_name`."),
52
- file_path: z.string().min(1, "Falta `file_path`."),
125
+ repo_name: obligatorio("repo_name").describe("Nombre del repositorio (ej. 'repo-a', 'repo-b')."),
126
+ file_path: obligatorio("file_path").describe("Ruta relativa del archivo dentro del repositorio (ej. 'src/main.ts')."),
53
127
  };
54
- const SHAPES = {
55
- start_task: startTaskShape,
56
- get_phase_prompt: getPhasePromptShape,
57
- get_active_task: getActiveTaskShape,
58
- list_tasks: listTasksShape,
59
- switch_task: switchTaskShape,
60
- read_central_doc: readCentralDocShape,
61
- write_central_doc: writeCentralDocShape,
62
- read_cross_repo: readCrossRepoShape,
128
+ // get_orchestrator_status no recibe nada.
129
+ export const getOrchestratorStatusShape = {};
130
+ // ─── Salida de get_orchestrator_status ──────────────────────────────────────
131
+ // El objeto Diagnostico de diagnostico.ts, campo por campo. Con un outputSchema el cliente recibe
132
+ // además `structuredContent`, y el SDK comprueba antes de enviarlo que cuadra con este shape:
133
+ // si diagnostico.ts cambia y esto no, la tool falla en los tests en vez de mentir al cliente.
134
+ const estadoRuta = z.object({ ruta: z.string().optional(), existe: z.boolean(), error: z.string().optional() });
135
+ export const diagnosticoShape = {
136
+ paquete: z.object({
137
+ version: z.string(),
138
+ ruta: z.string(),
139
+ via: z.enum(["npx", "global", "dependencia", "clon"]),
140
+ }),
141
+ node: z.object({ version: z.string(), plataforma: z.string() }),
142
+ variables: z.array(z.object({
143
+ logica: z.enum(["DOCS_PATH", "REPOS_PATH", "TASKS_SUBDIR", "PROMPTS_PATH", "STATE_PATH", "ELICITATION"]),
144
+ valor: z.string().optional(),
145
+ usada: z.string().nullable(),
146
+ fuente: z.enum(["entorno", ".env", "sin definir"]),
147
+ discrepancia: z.string().nullable(),
148
+ })),
149
+ rutas: z.object({ docs: estadoRuta, repos: estadoRuta }),
150
+ estado: z.object({ archivo: z.string(), existe: z.boolean(), punteros: z.number().int() }),
151
+ capas: z.array(z.object({ ruta: z.string(), etiqueta: z.enum(["fábrica", "propia"]), existe: z.boolean() })),
152
+ reglasGlobales: z.object({ archivo: z.string().nullable(), capa: z.string().nullable(), faltan: z.array(z.string()) }),
153
+ fases: z.array(z.object({
154
+ numero: z.number().int(),
155
+ titulo: z.string(),
156
+ archivo: z.string(),
157
+ capa: z.string(),
158
+ documento: z.string().optional(),
159
+ recibos: z.string().optional(),
160
+ comando: z.string().nullable(),
161
+ })),
162
+ proyectos: z.array(z.object({
163
+ nombre: z.string(),
164
+ tareas: z.number().int(),
165
+ activa: z
166
+ .object({ tarea: z.string(), origen: z.enum(["puntero", "deducida"]), fase: z.string() })
167
+ .nullable(),
168
+ })),
169
+ cliente: z
170
+ .object({
171
+ nombre: z.string(),
172
+ version: z.string(),
173
+ capacidades: z.object({ elicitation: z.boolean(), roots: z.boolean() }),
174
+ })
175
+ .optional(),
176
+ hallazgos: z.array(z.object({ severidad: z.enum(["error", "aviso", "info"]), codigo: z.string(), texto: z.string() })),
63
177
  };
64
- // Lo que se espera de cada tipo, dicho como se le diría al modelo para que corrija la llamada.
65
- // El único número es `phase`, de ahí el ejemplo.
66
- const TIPOS = {
67
- string: "un texto",
68
- number: "un número, por ejemplo 3",
69
- int: "un número entero, por ejemplo 3",
70
- boolean: "true o false",
71
- object: "un objeto",
72
- };
73
- function describirProblema(issue, recibidos) {
74
- const campo = issue.path.map(String).join(".");
75
- if (!campo)
76
- return "los argumentos tienen que ser un objeto.";
77
- const nombre = `\`${campo}\``;
78
- // null también es "falta": en un opcional se tolera, en un obligatorio es lo mismo que no venir.
79
- if (issue.code === "invalid_type") {
80
- return recibidos[campo] == null
81
- ? `Falta ${nombre}.`
82
- : `${nombre} debe ser ${TIPOS[issue.expected] ?? issue.expected}.`;
83
- }
84
- // Los mínimos llevan su mensaje en español en el propio shape ("Falta `task_name`.").
85
- if (issue.code === "too_small")
86
- return issue.message;
87
- // Los máximos no: el único que puede saltar es el del entero seguro de `phase`.
88
- if (issue.code === "too_big")
89
- return `${nombre} es demasiado grande.`;
90
- return `${nombre}: ${issue.message}.`;
91
- }
92
- export function validarLlamada(tool, args) {
93
- if (!Object.hasOwn(SHAPES, tool)) {
94
- throw new Error(`Unknown tool: ${tool}`);
95
- }
96
- const resultado = z.object(SHAPES[tool]).safeParse(args ?? {});
97
- if (!resultado.success) {
98
- const recibidos = (args && typeof args === "object" ? args : {});
99
- const problemas = resultado.error.issues.map((issue) => describirProblema(issue, recibidos));
100
- throw new Error(`Argumentos no válidos. ${problemas.join(" ")}`);
101
- }
102
- // null cuenta como ausente: se quita aquí, una vez, para que ninguna tool tenga que
103
- // distinguir entre un campo que no vino y uno que vino vacío.
104
- const sinNulos = Object.fromEntries(Object.entries(resultado.data).filter(([, valor]) => valor !== null));
105
- // El cast es seguro: los datos los acaba de producir el shape de esta misma tool.
106
- return { tool, args: sinNulos };
178
+ export function sinNulos(args) {
179
+ return Object.fromEntries(Object.entries(args).filter(([, valor]) => valor !== null));
107
180
  }
108
181
  //# sourceMappingURL=schemas.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function crearServidor(): McpServer;
3
+ //# sourceMappingURL=servidor.d.ts.map