@itzjoordi/jira-mcp 1.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/.env-example +3 -0
- package/.eslintrc.json +17 -0
- package/.prettierignore +3 -0
- package/.prettierrc.json +8 -0
- package/README.md +78 -0
- package/eslint.config.js +12 -0
- package/jiraApi.mjs +61 -0
- package/jiraClient.mjs +72 -0
- package/package.json +29 -0
- package/server.mjs +236 -0
package/.env-example
ADDED
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"env": {
|
|
3
|
+
"node": true,
|
|
4
|
+
"es2022": true
|
|
5
|
+
},
|
|
6
|
+
"extends": ["standard", "prettier"],
|
|
7
|
+
"rules": {
|
|
8
|
+
"no-unused-vars": [
|
|
9
|
+
"warn",
|
|
10
|
+
{
|
|
11
|
+
"argsIgnorePattern": "^_"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"import/no-anonymous-default-export": "off",
|
|
15
|
+
"no-console": "off"
|
|
16
|
+
}
|
|
17
|
+
}
|
package/.prettierignore
ADDED
package/.prettierrc.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# 🧩 MCP Jira Mercadona para VS Code + GitHub Copilot
|
|
2
|
+
|
|
3
|
+
Este proyecto proporciona un **servidor MCP** que permite interactuar con **Jira Mercadona** directamente desde **VS Code** usando **GitHub Copilot Chat**.
|
|
4
|
+
|
|
5
|
+
Incluye herramientas para:
|
|
6
|
+
|
|
7
|
+
- Filtrar issues de forma natural: “mis tareas pendientes”, “backlog del proyecto”, “tareas en curso”, etc.
|
|
8
|
+
- Mover tareas entre estados (workflow transition)
|
|
9
|
+
|
|
10
|
+
## 🚀 Requisitos
|
|
11
|
+
|
|
12
|
+
- Node.js 18+
|
|
13
|
+
- VS Code 1.102+
|
|
14
|
+
- Extensión GitHub Copilot Chat → <https://code.visualstudio.com/docs/copilot/overview>
|
|
15
|
+
- API Token → <https://id.atlassian.com/manage-profile/security/api-tokens>
|
|
16
|
+
|
|
17
|
+
## 📦 Instalación
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
git clone <URL-del-repo>
|
|
21
|
+
cd jira-mcp-test
|
|
22
|
+
npm install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 🔑 Configuración
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cp .env.example .env
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Editar `.env`:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
JIRA_BASE_URL=https://<tu-org>.atlassian.net
|
|
35
|
+
JIRA_EMAIL=tu-email@empresa.com
|
|
36
|
+
JIRA_API_TOKEN=<tu-api-token>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 🧩 Activación MCP
|
|
40
|
+
|
|
41
|
+
En VS Code:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Reiniciar completamente VS Code
|
|
45
|
+
Command Palette > MCP: List Servers
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Debe aparecer `jiraMcp`.
|
|
49
|
+
|
|
50
|
+
## 💬 Uso (Copilot Chat)
|
|
51
|
+
|
|
52
|
+
Ejemplos:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Devuelveme todas mis tareas asignadas en curso
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
Dime el backlog del proyecto XXXX
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Cambia el estado de la tarea XXXX-#### a VALIDADA
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 👷 Estructura
|
|
67
|
+
|
|
68
|
+
- server.mjs
|
|
69
|
+
- jiraApi.mjs
|
|
70
|
+
- jiraClient.mjs
|
|
71
|
+
- .vscode/mcp.json
|
|
72
|
+
- .env.example
|
|
73
|
+
- package.json
|
|
74
|
+
|
|
75
|
+
## 🧯 Problemas comunes
|
|
76
|
+
|
|
77
|
+
- 401/403 → token incorrecto
|
|
78
|
+
- MCP no aparece → reiniciar VS Code
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
import globals from 'globals';
|
|
3
|
+
import { defineConfig } from 'eslint/config';
|
|
4
|
+
|
|
5
|
+
export default defineConfig([
|
|
6
|
+
{
|
|
7
|
+
files: ['**/*.{js,mjs,cjs}'],
|
|
8
|
+
plugins: { js },
|
|
9
|
+
extends: ['js/recommended'],
|
|
10
|
+
languageOptions: { globals: globals.browser },
|
|
11
|
+
},
|
|
12
|
+
]);
|
package/jiraApi.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// jiraApi.mjs
|
|
2
|
+
import { jiraRequest } from './jiraClient.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Buscar issues usando JQL (API nueva /search/jql)
|
|
6
|
+
*/
|
|
7
|
+
export async function searchIssues(jql, maxResults = 20) {
|
|
8
|
+
const body = {
|
|
9
|
+
jql,
|
|
10
|
+
maxResults,
|
|
11
|
+
fields: ['summary', 'status', 'assignee', 'priority'],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const data = await jiraRequest('/rest/api/3/search/jql', {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
body: JSON.stringify(body),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const issues = data.issues ?? [];
|
|
20
|
+
|
|
21
|
+
return issues.map((issue) => ({
|
|
22
|
+
key: issue.key,
|
|
23
|
+
summary: issue.fields.summary,
|
|
24
|
+
status: issue.fields.status.name,
|
|
25
|
+
assignee: issue.fields.assignee?.displayName ?? null,
|
|
26
|
+
priority: issue.fields.priority?.name ?? null,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Obtener las transiciones posibles para un issue
|
|
32
|
+
*/
|
|
33
|
+
export async function getIssueTransitions(issueKey) {
|
|
34
|
+
const data = await jiraRequest(`/rest/api/3/issue/${issueKey}/transitions`, {
|
|
35
|
+
method: 'GET',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const transitions = data.transitions ?? [];
|
|
39
|
+
|
|
40
|
+
return transitions.map((t) => ({
|
|
41
|
+
id: t.id,
|
|
42
|
+
name: t.name,
|
|
43
|
+
toStatus: t.to.name,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Ejecutar una transición (mover a otro estado/columna)
|
|
49
|
+
*/
|
|
50
|
+
export async function transitionIssue(issueKey, transitionId) {
|
|
51
|
+
const body = {
|
|
52
|
+
transition: { id: transitionId },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const jiraRequest = await jiraRequest(`/rest/api/3/issue/${issueKey}/transitions`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
body: JSON.stringify(body),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return { ok: true };
|
|
61
|
+
}
|
package/jiraClient.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import { Buffer } from 'buffer';
|
|
6
|
+
import process from 'process';
|
|
7
|
+
|
|
8
|
+
let envLoaded = false;
|
|
9
|
+
|
|
10
|
+
function loadGlobalEnvOnce() {
|
|
11
|
+
if (envLoaded) return;
|
|
12
|
+
envLoaded = true;
|
|
13
|
+
const home = os.homedir();
|
|
14
|
+
const candidates = [path.join(home, '.jira-mcp.env'), path.join(home, '.jira-mcp', '.env')];
|
|
15
|
+
|
|
16
|
+
for (const p of candidates) {
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(p)) {
|
|
19
|
+
dotenv.config({ path: p });
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
// ignore
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
loadGlobalEnvOnce();
|
|
29
|
+
|
|
30
|
+
// --- a partir de aquí, uso normal de env:
|
|
31
|
+
const baseUrl = process.env.JIRA_BASE_URL;
|
|
32
|
+
const email = process.env.JIRA_EMAIL;
|
|
33
|
+
const apiToken = process.env.JIRA_API_TOKEN;
|
|
34
|
+
|
|
35
|
+
if (!baseUrl || !email || !apiToken) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
'Faltan credenciales de Jira. Crea un .env global en tu HOME:\n' +
|
|
38
|
+
'- macOS/Linux: ~/.jira-mcp.env (o ~/.jira-mcp/.env)\n' +
|
|
39
|
+
'- Windows: %USERPROFILE%\\.jira-mcp.env (o %USERPROFILE%\\.jira-mcp\\.env)\n\n' +
|
|
40
|
+
'Variables requeridas:\n' +
|
|
41
|
+
'JIRA_BASE_URL=https://<tu-org>.atlassian.net\n' +
|
|
42
|
+
'JIRA_EMAIL=tu-email@empresa.com\n' +
|
|
43
|
+
'JIRA_API_TOKEN=<tu-token>\n',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const authHeader = 'Basic ' + Buffer.from(`${email}:${apiToken}`).toString('base64');
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Llamada genérica a Jira con manejo de errores
|
|
51
|
+
*/
|
|
52
|
+
export async function jiraRequest(path, options = {}) {
|
|
53
|
+
const url = `${baseUrl}${path}`;
|
|
54
|
+
|
|
55
|
+
const response = await fetch(url, {
|
|
56
|
+
...options,
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: authHeader,
|
|
59
|
+
Accept: 'application/json',
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
...(options.headers || {}),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const text = await response.text();
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new Error(`Jira error ${response.status} ${response.statusText}: ${text.slice(0, 400)}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return text ? JSON.parse(text) : {};
|
|
72
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@itzjoordi/jira-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Servidor MCP para Jira Mercadona usable desde Github Copilot Chat",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"jira-mcp": "./server.mjs"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"lint:fix": "eslint . --fix",
|
|
12
|
+
"format": "prettier --write ."
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@modelcontextprotocol/sdk": "^1.22.0",
|
|
16
|
+
"dotenv": "^17.2.3",
|
|
17
|
+
"zod": "^3.25.76"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@eslint/js": "^9.39.1",
|
|
25
|
+
"eslint": "^9.39.1",
|
|
26
|
+
"globals": "^16.5.0",
|
|
27
|
+
"prettier": "^3.6.2"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/server.mjs
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
import { searchIssues, getIssueTransitions, transitionIssue } from './jiraApi.mjs';
|
|
7
|
+
|
|
8
|
+
// 1. Crear servidor MCP
|
|
9
|
+
const server = new McpServer({
|
|
10
|
+
name: 'jira-mcp',
|
|
11
|
+
version: '0.1.0',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// 2. Tools
|
|
15
|
+
// Tool: listar issues con filtros simples
|
|
16
|
+
server.registerTool(
|
|
17
|
+
'jira_list_filtered',
|
|
18
|
+
{
|
|
19
|
+
title: 'Jira: listar issues con filtros simples',
|
|
20
|
+
description:
|
|
21
|
+
'Lista issues de Jira usando filtros de alto nivel (solo mías, en curso, backlog, etc.) y construye la JQL automáticamente.',
|
|
22
|
+
inputSchema: {
|
|
23
|
+
projectKey: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Clave de proyecto, p.ej. 'SIM1'. Si se omite, busca en todos."),
|
|
27
|
+
onlyAssignedToMe: z
|
|
28
|
+
.boolean()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('Si es true, solo issues asignadas al usuario actual.'),
|
|
31
|
+
excludeAssignedToMe: z
|
|
32
|
+
.boolean()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Si es true, excluye las issues asignadas al usuario actual.'),
|
|
35
|
+
statusCategory: z
|
|
36
|
+
.enum(['To Do', 'In Progress', 'Done'])
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('Filtrar por categoría de estado de Jira.'),
|
|
39
|
+
statusIn: z
|
|
40
|
+
.array(z.string())
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('Lista de nombres de estado, p.ej. ["Por Hacer", "Validar"].'),
|
|
43
|
+
backlogOnly: z
|
|
44
|
+
.boolean()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('Si es true, devuelve issues de backlog (aprox: no Done, normalmente To Do).'),
|
|
47
|
+
maxResults: z
|
|
48
|
+
.number()
|
|
49
|
+
.int()
|
|
50
|
+
.min(1)
|
|
51
|
+
.max(50)
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Máximo de issues a devolver (por defecto 20).'),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
async ({
|
|
57
|
+
projectKey,
|
|
58
|
+
onlyAssignedToMe,
|
|
59
|
+
excludeAssignedToMe,
|
|
60
|
+
statusCategory,
|
|
61
|
+
statusIn,
|
|
62
|
+
backlogOnly,
|
|
63
|
+
maxResults,
|
|
64
|
+
}) => {
|
|
65
|
+
const clauses = [];
|
|
66
|
+
|
|
67
|
+
if (projectKey) {
|
|
68
|
+
clauses.push(`project = ${projectKey}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (onlyAssignedToMe) {
|
|
72
|
+
clauses.push('assignee = currentUser()');
|
|
73
|
+
} else if (excludeAssignedToMe) {
|
|
74
|
+
clauses.push('assignee != currentUser()');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (statusCategory) {
|
|
78
|
+
clauses.push(`statusCategory = "${statusCategory}"`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (statusIn && statusIn.length > 0) {
|
|
82
|
+
const quoted = statusIn.map((s) => `"${s}"`).join(', ');
|
|
83
|
+
clauses.push(`status in (${quoted})`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (backlogOnly) {
|
|
87
|
+
// Aproximación: backlog = no Done
|
|
88
|
+
clauses.push('statusCategory != "Done"');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let jql = clauses.join(' AND ');
|
|
92
|
+
const order = 'ORDER BY created DESC';
|
|
93
|
+
|
|
94
|
+
if (jql.trim().length === 0) {
|
|
95
|
+
jql = order;
|
|
96
|
+
} else {
|
|
97
|
+
jql = `${jql} ${order}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const issues = await searchIssues(jql, maxResults ?? 20);
|
|
101
|
+
|
|
102
|
+
const humanText =
|
|
103
|
+
issues.length === 0
|
|
104
|
+
? `No se han encontrado issues para la JQL: ${jql}`
|
|
105
|
+
: `JQL usada: ${jql}\n\n` +
|
|
106
|
+
issues
|
|
107
|
+
.map(
|
|
108
|
+
(i) =>
|
|
109
|
+
`${i.key} | ${i.status} | ${i.summary}` + (i.assignee ? ` (${i.assignee})` : ''),
|
|
110
|
+
)
|
|
111
|
+
.join('\n');
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
content: [
|
|
115
|
+
{ type: 'text', text: humanText },
|
|
116
|
+
{
|
|
117
|
+
type: 'text',
|
|
118
|
+
text: '\n\n[JSON crudo para el modelo]\n' + JSON.stringify({ jql, issues }, null, 2),
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
structuredContent: { jql, issues },
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Tool: obtener transiciones de un issue
|
|
127
|
+
server.registerTool(
|
|
128
|
+
'jira_get_transitions',
|
|
129
|
+
{
|
|
130
|
+
title: 'Jira: obtener transiciones de un issue',
|
|
131
|
+
description:
|
|
132
|
+
'Devuelve las transiciones posibles (id, name, toStatus) para un issue de Jira concreto.',
|
|
133
|
+
inputSchema: {
|
|
134
|
+
issueKey: z.string().describe("Clave del issue, p.ej. 'SIM1-320'"),
|
|
135
|
+
},
|
|
136
|
+
// outputSchema opcional:
|
|
137
|
+
// outputSchema: z.array(
|
|
138
|
+
// z.object({
|
|
139
|
+
// id: z.string(),
|
|
140
|
+
// name: z.string(),
|
|
141
|
+
// toStatus: z.string(),
|
|
142
|
+
// })
|
|
143
|
+
// ),
|
|
144
|
+
},
|
|
145
|
+
async ({ issueKey }) => {
|
|
146
|
+
const transitions = await getIssueTransitions(issueKey);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: 'text',
|
|
152
|
+
text: JSON.stringify(transitions, null, 2),
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
structuredContent: transitions,
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Tool: hacer transición de un issue
|
|
161
|
+
server.registerTool(
|
|
162
|
+
'jira_transition_issue',
|
|
163
|
+
{
|
|
164
|
+
title: 'Jira: cambiar estado de un issue',
|
|
165
|
+
description: 'Ejecuta una transición de workflow en un issue de Jira usando su transitionId.',
|
|
166
|
+
inputSchema: {
|
|
167
|
+
issueKey: z.string().describe("Clave del issue, p.ej. 'SIM1-320'"),
|
|
168
|
+
transitionId: z.string().describe('ID de la transición (devuelta por jira_get_transitions)'),
|
|
169
|
+
},
|
|
170
|
+
// outputSchema opcional:
|
|
171
|
+
// outputSchema: z.object({ ok: z.boolean() }),
|
|
172
|
+
},
|
|
173
|
+
async ({ issueKey, transitionId }) => {
|
|
174
|
+
const result = await transitionIssue(issueKey, transitionId);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
content: [
|
|
178
|
+
{
|
|
179
|
+
type: 'text',
|
|
180
|
+
text: JSON.stringify(result, null, 2),
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
structuredContent: result,
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Tool: mover issue por nombre de transición
|
|
189
|
+
server.registerTool(
|
|
190
|
+
'jira_move_issue_by_transition_name',
|
|
191
|
+
{
|
|
192
|
+
title: 'Jira: mover issue por nombre de transición',
|
|
193
|
+
description:
|
|
194
|
+
"Busca la transición por nombre (p.ej. 'Construir', 'Validar') y la ejecuta sobre el issue.",
|
|
195
|
+
inputSchema: {
|
|
196
|
+
issueKey: z.string().describe("Clave del issue, p.ej. 'SIM1-320'"),
|
|
197
|
+
transitionName: z.string().describe("Nombre exacto de la transición, p.ej. 'Construir'"),
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
async ({ issueKey, transitionName }) => {
|
|
201
|
+
const transitions = await getIssueTransitions(issueKey);
|
|
202
|
+
const match = transitions.find((t) => t.name.toLowerCase() === transitionName.toLowerCase());
|
|
203
|
+
|
|
204
|
+
if (!match) {
|
|
205
|
+
const available = transitions.map((t) => t.name).join(', ');
|
|
206
|
+
const msg =
|
|
207
|
+
`No se ha encontrado ninguna transición llamada "${transitionName}" ` +
|
|
208
|
+
`para el issue ${issueKey}. Transiciones disponibles: ${available}`;
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
content: [{ type: 'text', text: msg }],
|
|
212
|
+
structuredContent: { ok: false, error: msg, transitions },
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const result = await transitionIssue(issueKey, match.id);
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
type: 'text',
|
|
222
|
+
text: `Transición "${match.name}" aplicada a ${issueKey}. Nuevo estado: ${match.toStatus}`,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
type: 'text',
|
|
226
|
+
text: '\n[Detalle JSON]\n' + JSON.stringify({ result, usedTransition: match }, null, 2),
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
structuredContent: { ok: true, usedTransition: match },
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// 3. Conectar por STDIO
|
|
235
|
+
const transport = new StdioServerTransport();
|
|
236
|
+
await server.connect(transport);
|