@dami_deleon/rikudo 1.0.1-alpha.3 → 2.0.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.md CHANGED
@@ -14,6 +14,7 @@ Inspirado en el Sabio de los Seis Caminos, Rikudo utiliza seis módulos especial
14
14
  4. **Camino Preta (Summarizer):** Absorción y síntesis de archivos de gran tamaño.
15
15
  5. **Camino Naraka (Cache):** Memoria persistente basada en hashes de Git para optimizar tokens.
16
16
  6. **Camino Asura (Rate Limiter):** Gestión de poder para respetar los límites de las APIs.
17
+ 7. **Camino MCP (Model Context Protocol):** Servidor MCP para integrar Rikudo con agentes de IA externos.
17
18
 
18
19
 
19
20
  ## ✨ Características Principales
@@ -29,6 +30,7 @@ Inspirado en el Sabio de los Seis Caminos, Rikudo utiliza seis módulos especial
29
30
  * **🚀 PR Generator:** Selecciona múltiples commits y genera una descripción de Pull Request completa siguiendo tu plantilla.
30
31
  * **⚙️ Configuración Flexible:** Define tus preferencias globales o por proyecto.
31
32
  * **🔍 Diagnóstico Integrado:** Herramienta `test` para verificar tus API Keys y conexión con servicios externos (Redmine, IA, etc.).
33
+ * **🔌 Servidor MCP:** Actúa como skill library para agentes de IA externos mediante el protocolo MCP (Model Context Protocol).
32
34
 
33
35
  ## 📦 Instalación
34
36
 
@@ -95,6 +97,87 @@ rikudo config
95
97
  ```
96
98
 
97
99
 
100
+ ## 🔌 Servidor MCP (Model Context Protocol)
101
+
102
+ Rikudo funciona como un servidor MCP, exponiendo herramientas y prompts para que cualquier agente de IA externo compatible con MCP pueda interactuar con tu flujo de trabajo de desarrollo.
103
+
104
+ ### Iniciar el Servidor
105
+
106
+ ```bash
107
+ rikudo mcp
108
+ ```
109
+
110
+ El servidor se comunica mediante **stdio** (entrada/salida estándar), lo que permite integrarlo con cualquier cliente MCP.
111
+
112
+ ### Herramientas Disponibles
113
+
114
+ | Herramienta | Descripción |
115
+ |------------|-------------|
116
+ | `validate_configuration` | Verifica que las variables de entorno necesarias (Redmine, Gitea) estén configuradas correctamente. |
117
+ | `get_ticket_context` | Obtiene el contexto completo de un ticket de Redmine (título, descripción, estado, prioridad). |
118
+ | `get_git_diff` | Retorna el diff de los archivos que están en staging (`git add`). |
119
+ | `shift_left_start` | Flujo shift-left completo: checkout a rama objetivo, crea nueva rama, commit vacío y abre Draft PR en Gitea. Soporta parámetro `isWip` para usar prefijo "WIP:" en el título. |
120
+ | `update_pm_redmine` | Agrega un comentario a un ticket de Redmine para notificar al Project Manager. |
121
+
122
+ ### Prompts Disponibles
123
+
124
+ | Prompt | Descripción |
125
+ |--------|-------------|
126
+ | `start_feature` | Inicia el flujo de trabajo para una nueva feature. Obtiene contexto del ticket, genera nombre de rama y prepara el entorno en Gitea con PR en modo WIP. |
127
+ | `review_and_comment` | Analiza los cambios staged, genera un comentario técnico y lo publica en Redmine. |
128
+
129
+ ### Integración con Agentes IA
130
+
131
+ Rikudo es compatible con cualquier herramienta de IA que soporte el protocolo MCP, como:
132
+ - **Claude Code / Claude Desktop**
133
+ - **Gemini CLI**
134
+ - **Cursor**
135
+ - **Windsurf**
136
+ - **Continue**
137
+ - Y cualquier otro cliente MCP
138
+
139
+ #### Configuración Genérica
140
+
141
+ Agrega esto en la configuración de tu cliente MCP:
142
+
143
+ ```json
144
+ {
145
+ "mcpServers": {
146
+ "rikudo": {
147
+ "command": "rikudo",
148
+ "args": ["mcp"]
149
+ }
150
+ }
151
+ }
152
+ ```
153
+
154
+ ### Variables de Entorno para MCP
155
+
156
+ El servidor MCP requiere las siguientes variables de entorno:
157
+
158
+ ```env
159
+ # Redmine
160
+ RIKUDO_REDMINE_URL=https://redmine.tuempresa.com
161
+ RIKUDO_REDMINE_API_KEY=tu_redmine_key
162
+
163
+ # Gitea
164
+ RIKUDO_GITEA_URL=https://gitea.tuempresa.com
165
+ RIKUDO_GITEA_TOKEN=tu_gitea_token
166
+ RIKUDO_GITEA_OWNER=tu_usuario_o_organizacion
167
+ RIKUDO_GITEA_REPO=nombre_del_repositorio
168
+ ```
169
+
170
+ ### Formato de Ramas
171
+
172
+ Al usar `start_feature` o `shift_left_start`, el nombre de rama sigue el formato:
173
+
174
+ ```
175
+ [numero-tarea]-[titulo-tarea-ingles-corto]
176
+ ```
177
+
178
+ Ejemplo: `1234-add-user-authentication`
179
+
180
+
98
181
 
99
182
  ## ⚙️ Configuración
100
183
 
@@ -119,6 +202,12 @@ RIKUDO_OLLAMA_MODEL=llama3
119
202
  RIKUDO_REDMINE_URL=https://redmine.tuempresa.com
120
203
  RIKUDO_REDMINE_API_KEY=tu_redmine_key
121
204
 
205
+ # --- Gitea (para servidor MCP) ---
206
+ RIKUDO_GITEA_URL=https://gitea.tuempresa.com
207
+ RIKUDO_GITEA_TOKEN=tu_gitea_token
208
+ RIKUDO_GITEA_OWNER=tu_usuario_o_organizacion
209
+ RIKUDO_GITEA_REPO=nombre_del_repositorio
210
+
122
211
  ```
123
212
 
124
213
  ### Plantillas Personalizadas (Templates)
@@ -36,7 +36,6 @@ Registro del Último Commit (Log Técnico): ${commitMessage}
36
36
 
37
37
  SALIDA:
38
38
  Solo devuelve el comentario, sin títulos o subtítulos o lineas de saludos.
39
-
40
39
  `;
41
40
  }
42
41
  export function generatePullRequestMessagePrompt(commits, ticketInfo, template) {
@@ -65,6 +64,6 @@ INSTRUCCIONES:
65
64
 
66
65
  SALIDA:
67
66
  Solo el contenido de la plantilla rellena.
68
- `;
67
+ `;
69
68
  return prompt;
70
69
  }
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ import { generateCommitMessagePrompt, generateProjectManagerCommentPrompt, gener
16
16
  import { getConventions, getPRTemplate } from "./utils/config.js";
17
17
  import { showIssueMenu } from "./menus/issueMenu.js";
18
18
  import { runDiagnostics } from "./test/connection.js";
19
+ import { runMCPServer } from "./mcp/server.js";
19
20
  import { COMMIT_CONVENTIONS_PATH, ENV_PATH, PULL_REQUEST_TEMPLATE_PATH } from "./const/paths.js";
20
21
  // --- Setup ---
21
22
  const __filename = fileURLToPath(import.meta.url);
@@ -62,6 +63,14 @@ program
62
63
  .command("config")
63
64
  .description("Abrir menú de configuración")
64
65
  .action(() => showConfigMenu());
66
+ // MCP Server
67
+ program
68
+ .command("mcp")
69
+ .description("Iniciar servidor MCP (Model Context Protocol) para agentes de IA")
70
+ .action(async (options) => {
71
+ loadEnv(options.env);
72
+ await runMCPServer();
73
+ });
65
74
  // --- Modo Interactivo (Default) ---
66
75
  program
67
76
  .description("Modo interactivo del Sabio Rikudo")
@@ -0,0 +1,226 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+ import { simpleGit } from "simple-git";
5
+ import { getIssueDetails, updateIssueComment } from "../services/redmine.js";
6
+ import { getStagedDiff } from "../services/git.js";
7
+ import { createPullRequest } from "../services/gitea.js";
8
+ import { loadEnv } from "../utils/env.js";
9
+ import { getConfig } from "../utils/config.js";
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ const packagePath = path.resolve(__dirname, "../../package.json");
16
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
17
+ const git = simpleGit();
18
+ const server = new McpServer({
19
+ name: "rikudo-mcp-server",
20
+ version: packageJson.version || "1.0.0",
21
+ });
22
+ server.registerTool("get_ticket_context", {
23
+ title: "Get Ticket Context",
24
+ description: "Obtiene el contexto completo de un ticket de Redmine (título, descripción, estado, prioridad).",
25
+ inputSchema: z.object({
26
+ ticketId: z.string().describe("ID del ticket de Redmine (ej: '1234')"),
27
+ }),
28
+ }, async ({ ticketId }) => {
29
+ try {
30
+ const ticket = await getIssueDetails(ticketId);
31
+ if (!ticket) {
32
+ return {
33
+ content: [{ type: "text", text: `No se pudo obtener el ticket #${ticketId}` }],
34
+ isError: true,
35
+ };
36
+ }
37
+ return {
38
+ content: [
39
+ {
40
+ type: "text",
41
+ text: JSON.stringify({
42
+ id: ticket.id,
43
+ subject: ticket.subject,
44
+ description: ticket.description,
45
+ status: ticket.status,
46
+ priority: ticket.priority,
47
+ author: ticket.author,
48
+ url: ticket.url,
49
+ }, null, 2),
50
+ },
51
+ ],
52
+ };
53
+ }
54
+ catch (error) {
55
+ const errorMessage = error instanceof Error ? error.message : "Error desconocido";
56
+ return { content: [{ type: "text", text: `Error: ${errorMessage}` }], isError: true };
57
+ }
58
+ });
59
+ server.registerTool("get_git_diff", {
60
+ title: "Get Git Diff",
61
+ description: "Obtiene el diff de los cambios que están en staging (git add). Retorna el diff crudo.",
62
+ inputSchema: z.object({}),
63
+ }, async () => {
64
+ try {
65
+ const diff = await getStagedDiff();
66
+ if (!diff) {
67
+ return {
68
+ content: [{ type: "text", text: "No hay cambios en staging. Ejecuta 'git add' primero." }],
69
+ isError: true,
70
+ };
71
+ }
72
+ return { content: [{ type: "text", text: diff }] };
73
+ }
74
+ catch (error) {
75
+ const errorMessage = error instanceof Error ? error.message : "Error desconocido";
76
+ return { content: [{ type: "text", text: `Error: ${errorMessage}` }], isError: true };
77
+ }
78
+ });
79
+ server.registerTool("validate_configuration", {
80
+ title: "Validate Configuration",
81
+ description: "Verifica si todas las variables de entorno necesarias para Rikudo (Redmine, Gitea y Proveedor de IA) están configuradas correctamente en el sistema del usuario. Llama a esta herramienta antes de intentar interactuar con tickets o repositorios.",
82
+ inputSchema: z.object({}),
83
+ }, async () => {
84
+ const config = getConfig();
85
+ const missingVars = [];
86
+ if (!config.REDMINE_URL)
87
+ missingVars.push("RIKUDO_REDMINE_URL");
88
+ if (!config.REDMINE_API_KEY)
89
+ missingVars.push("RIKUDO_REDMINE_API_KEY");
90
+ if (!config.GITEA_URL)
91
+ missingVars.push("RIKUDO_GITEA_URL");
92
+ if (!config.GITEA_TOKEN)
93
+ missingVars.push("RIKUDO_GITEA_TOKEN");
94
+ if (missingVars.length === 0) {
95
+ return {
96
+ content: [{ type: "text", text: "✅ Configuración de entorno válida. Todos los sistemas listos." }],
97
+ };
98
+ }
99
+ return {
100
+ content: [
101
+ {
102
+ type: "text",
103
+ text: `❌ Faltan las siguientes variables de entorno: ${missingVars.join(", ")}. Por favor, pide al usuario que las configure en su archivo .env antes de continuar.`,
104
+ },
105
+ ],
106
+ isError: true,
107
+ };
108
+ });
109
+ server.registerTool("shift_left_start", {
110
+ title: "Shift Left Start",
111
+ description: "Inicia un flujo shift-left: hace checkout a la rama objetivo, crea una nueva rama, hace commit vacío y abre un Draft PR en Gitea.",
112
+ inputSchema: z.object({
113
+ ticketId: z.string().describe("ID del ticket de Redmine (ej: '1234')"),
114
+ branchName: z.string().describe("Nombre de la nueva rama a crear (ej: '1234-add-user-auth')"),
115
+ targetBranch: z.string().default("alpha").describe("Rama base para hacer checkout (default: 'alpha')"),
116
+ isWip: z.boolean().default(false).describe("Si es true, el PR título lleva prefijo 'WIP:' (Work In Progress)"),
117
+ }),
118
+ }, async ({ ticketId, branchName, targetBranch, isWip }) => {
119
+ try {
120
+ await git.checkout(targetBranch);
121
+ await git.pull("origin", targetBranch);
122
+ await git.checkoutLocalBranch(branchName);
123
+ await git.commit("chore: init #" + ticketId, ["--allow-empty"]);
124
+ await git.push(["-u", "origin", branchName]);
125
+ const ticket = await getIssueDetails(ticketId);
126
+ const prTitle = isWip
127
+ ? `WIP: ${ticket?.subject || `Ticket #${ticketId}`}`
128
+ : `Draft: ${ticket?.subject || `Ticket #${ticketId}`}`;
129
+ const prBody = `## Descripción\n\nTrabajo iniciado para el ticket #${ticketId}${ticket ? `\n\n**${ticket.subject}**` : ""}`;
130
+ const prResult = await createPullRequest({
131
+ title: prTitle,
132
+ head: branchName,
133
+ base: targetBranch,
134
+ body: prBody,
135
+ draft: true,
136
+ });
137
+ if (!prResult.success) {
138
+ return {
139
+ content: [{ type: "text", text: `Rama '${branchName}' creada y empujada a origin. Error al crear PR: ${prResult.error}` }],
140
+ isError: true,
141
+ };
142
+ }
143
+ return {
144
+ content: [
145
+ {
146
+ type: "text",
147
+ text: JSON.stringify({
148
+ success: true,
149
+ branch: branchName,
150
+ targetBranch,
151
+ prUrl: prResult.url,
152
+ prNumber: prResult.number,
153
+ message: `Entorno preparado en rama '${branchName}'. Draft PR creado: ${prResult.url}`,
154
+ }, null, 2),
155
+ },
156
+ ],
157
+ };
158
+ }
159
+ catch (error) {
160
+ const errorMessage = error instanceof Error ? error.message : "Error desconocido";
161
+ return { content: [{ type: "text", text: `Error en shift_left_start: ${errorMessage}` }], isError: true };
162
+ }
163
+ });
164
+ server.registerTool("update_pm_redmine", {
165
+ title: "Update PM Redmine",
166
+ description: "Agrega un comentario a un ticket de Redmine. Ideal para notificar al Project Manager.",
167
+ inputSchema: z.object({
168
+ ticketId: z.string().describe("ID del ticket de Redmine (ej: '1234')"),
169
+ commentMarkdown: z.string().describe("Comentario en formato Markdown/Textile para Redmine"),
170
+ }),
171
+ }, async ({ ticketId, commentMarkdown }) => {
172
+ try {
173
+ await updateIssueComment(ticketId, commentMarkdown);
174
+ return {
175
+ content: [{ type: "text", text: `Comentario agregado exitosamente al ticket #${ticketId}` }],
176
+ };
177
+ }
178
+ catch (error) {
179
+ const errorMessage = error instanceof Error ? error.message : "Error desconocido";
180
+ return { content: [{ type: "text", text: `Error actualizando Redmine: ${errorMessage}` }], isError: true };
181
+ }
182
+ });
183
+ server.registerPrompt("start_feature", {
184
+ title: "Start Feature",
185
+ description: "Inicia el flujo de trabajo para una nueva feature. Obtiene contexto del ticket y prepara el entorno en Gitea.",
186
+ argsSchema: {
187
+ ticket_id: z.string().describe("ID del ticket de Redmine"),
188
+ },
189
+ }, async ({ ticket_id }) => {
190
+ return {
191
+ messages: [
192
+ {
193
+ role: "user",
194
+ content: {
195
+ type: "text",
196
+ text: `Eres un asistente de desarrollo. Llama a la herramienta 'get_ticket_context' para el ticket ${ticket_id}. Lee su título y descripción. Genera un nombre de rama en inglés, cortas y en minúsculas, con el formato '[numero-tarea]-[titulo-tarea-ingles-corto]' (ej: '1234-add-user-authentication'). Luego, llama obligatoriamente a la herramienta 'shift_left_start' con esos datos para preparar mi entorno de trabajo en Gitea.`,
197
+ },
198
+ },
199
+ ],
200
+ };
201
+ });
202
+ server.registerPrompt("review_and_comment", {
203
+ title: "Review and Comment",
204
+ description: "Analiza los cambios staged, genera un comentario técnico para el Project Manager y lo publica en Redmine.",
205
+ argsSchema: {
206
+ ticket_id: z.string().describe("ID del ticket de Redmine"),
207
+ },
208
+ }, async ({ ticket_id }) => {
209
+ return {
210
+ messages: [
211
+ {
212
+ role: "user",
213
+ content: {
214
+ type: "text",
215
+ text: `Llama a la herramienta 'get_git_diff'. Analiza profundamente los cambios de código. Redacta un comentario técnico formal para el Project Manager explicando qué se hizo. Finalmente, llama a la herramienta 'update_pm_redmine' con el ${ticket_id} y el comentario generado para notificar al equipo.`,
216
+ },
217
+ },
218
+ ],
219
+ };
220
+ });
221
+ export async function runMCPServer() {
222
+ loadEnv();
223
+ const transport = new StdioServerTransport();
224
+ await server.connect(transport);
225
+ console.error("Rikudo MCP Server started");
226
+ }
@@ -0,0 +1,59 @@
1
+ import axios, { AxiosError } from "axios";
2
+ const getGiteaConfig = () => {
3
+ return {
4
+ url: process.env.RIKUDO_GITEA_URL || "",
5
+ token: process.env.RIKUDO_GITEA_TOKEN || "",
6
+ owner: process.env.RIKUDO_GITEA_OWNER || "",
7
+ repo: process.env.RIKUDO_GITEA_REPO || "",
8
+ };
9
+ };
10
+ const getGiteaClient = () => {
11
+ const config = getGiteaConfig();
12
+ return axios.create({
13
+ baseURL: config.url,
14
+ headers: {
15
+ Authorization: `token ${config.token}`,
16
+ "Content-Type": "application/json",
17
+ },
18
+ });
19
+ };
20
+ export const createPullRequest = async (payload) => {
21
+ const config = getGiteaConfig();
22
+ if (!config.url || !config.token || !config.owner || !config.repo) {
23
+ return {
24
+ success: false,
25
+ error: "Faltan variables de entorno de Gitea (RIKUDO_GITEA_URL, RIKUDO_GITEA_TOKEN, RIKUDO_GITEA_OWNER, RIKUDO_GITEA_REPO)",
26
+ };
27
+ }
28
+ try {
29
+ const client = getGiteaClient();
30
+ const endpoint = `/api/v1/repos/${config.owner}/${config.repo}/pulls`;
31
+ const response = await client.post(endpoint, {
32
+ title: payload.title,
33
+ head: payload.head,
34
+ base: payload.base,
35
+ body: payload.body,
36
+ draft: payload.draft ?? false,
37
+ });
38
+ return {
39
+ success: true,
40
+ url: response.data.html_url,
41
+ number: response.data.number,
42
+ };
43
+ }
44
+ catch (error) {
45
+ if (error instanceof AxiosError) {
46
+ return {
47
+ success: false,
48
+ error: `Error de Gitea: ${error.message} - ${error.response?.data?.message || ""}`,
49
+ };
50
+ }
51
+ return {
52
+ success: false,
53
+ error: error instanceof Error ? error.message : "Error desconocido al crear PR",
54
+ };
55
+ }
56
+ };
57
+ export const Gitea = {
58
+ createPullRequest,
59
+ };
@@ -17,6 +17,10 @@ export function getConfig() {
17
17
  REDMINE_URL: process.env.RIKUDO_REDMINE_URL || "",
18
18
  REDMINE_API_KEY: process.env.RIKUDO_REDMINE_API_KEY || "",
19
19
  PROJECT_ID: process.env.RIKUDO_REDMINE_PROJECT_ID || "",
20
+ GITEA_URL: process.env.RIKUDO_GITEA_URL || "",
21
+ GITEA_TOKEN: process.env.RIKUDO_GITEA_TOKEN || "",
22
+ GITEA_OWNER: process.env.RIKUDO_GITEA_OWNER || "",
23
+ GITEA_REPO: process.env.RIKUDO_GITEA_REPO || "",
20
24
  };
21
25
  }
22
26
  export function getLimits() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dami_deleon/rikudo",
3
- "version": "1.0.1-alpha.3",
3
+ "version": "2.0.0",
4
4
  "description": "El Sabio de los Commits: AI Assistant con integración a Redmine",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -35,6 +35,7 @@
35
35
  "@anthropic-ai/sdk": "0.74.0",
36
36
  "@google/generative-ai": "0.24.1",
37
37
  "@inquirer/prompts": "8.2.0",
38
+ "@modelcontextprotocol/sdk": "^1.27.1",
38
39
  "@types/figlet": "1.7.0",
39
40
  "axios": "1.13.2",
40
41
  "chalk": "5.6.2",