@andrebuzeli/git-mcp 15.8.0 → 15.8.2

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
@@ -49,12 +49,14 @@ Test Suites: 2 passed, 2 total
49
49
  Tests: 13 passed, 13 total
50
50
  ```
51
51
 
52
+ ### Configuração Padrão (npx)
53
+
52
54
  ```json
53
55
  {
54
56
  "mcpServers": {
55
57
  "git-mcp": {
56
58
  "command": "npx",
57
- "args": ["@andrebuzeli/git-mcp@latest"],
59
+ "args": ["-y", "@andrebuzeli/git-mcp@latest"],
58
60
  "env": {
59
61
  "GITEA_URL": "https://seu-gitea",
60
62
  "GITEA_TOKEN": "...",
@@ -65,6 +67,45 @@ Tests: 13 passed, 13 total
65
67
  }
66
68
  ```
67
69
 
70
+ ## 🐧 SSH Remote / Linux Server
71
+
72
+ Se você usa **VS Code Remote SSH**, **Cursor SSH** ou similar, o `npx` pode dar timeout em redes lentas.
73
+
74
+ ### Solução: Instalação Global
75
+
76
+ **1. No servidor Linux, execute:**
77
+ ```bash
78
+ # Instalação rápida
79
+ curl -fsSL https://raw.githubusercontent.com/andrebuzeli/git-mcp/main/install.sh | bash
80
+
81
+ # Ou manualmente:
82
+ npm install -g @andrebuzeli/git-mcp@latest
83
+ ```
84
+
85
+ **2. Configure a IDE para usar o comando local:**
86
+ ```json
87
+ {
88
+ "mcpServers": {
89
+ "git-mcp": {
90
+ "command": "git-mcp",
91
+ "env": {
92
+ "GITHUB_TOKEN": "...",
93
+ "GITEA_URL": "https://seu-gitea",
94
+ "GITEA_TOKEN": "..."
95
+ }
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ ### Troubleshooting
102
+
103
+ | Erro | Solução |
104
+ |------|---------|
105
+ | `ETIMEDOUT` / `Request timed out` | Use instalação global ao invés de npx |
106
+ | `command not found: git-mcp` | Verifique se npm global está no PATH |
107
+ | Permissão negada | Use `sudo npm install -g @andrebuzeli/git-mcp` |
108
+
68
109
  ## Tools
69
110
 
70
111
  - git-workflow: init, status, add, remove, commit, ensure-remotes, push
package/install.sh ADDED
@@ -0,0 +1,68 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # git-mcp Install Script
4
+ # =============================================================================
5
+ # Quick install for Linux servers (especially SSH remote environments)
6
+ # This avoids the npx timeout issues on slow/unstable networks
7
+ # =============================================================================
8
+
9
+ set -e
10
+
11
+ GREEN='\033[0;32m'
12
+ YELLOW='\033[1;33m'
13
+ RED='\033[0;31m'
14
+ NC='\033[0m' # No Color
15
+
16
+ echo -e "${GREEN}🚀 Installing @andrebuzeli/git-mcp...${NC}"
17
+
18
+ # Check for Node.js
19
+ if ! command -v node &> /dev/null; then
20
+ echo -e "${RED}❌ Node.js not found!${NC}"
21
+ echo "Please install Node.js first: https://nodejs.org/"
22
+ exit 1
23
+ fi
24
+
25
+ # Check Node version (need 18+)
26
+ NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
27
+ if [ "$NODE_VERSION" -lt 18 ]; then
28
+ echo -e "${YELLOW}⚠️ Node.js version 18+ recommended (found: $(node -v))${NC}"
29
+ fi
30
+
31
+ # Check for npm
32
+ if ! command -v npm &> /dev/null; then
33
+ echo -e "${RED}❌ npm not found!${NC}"
34
+ exit 1
35
+ fi
36
+
37
+ echo "📦 Installing globally..."
38
+ npm install -g @andrebuzeli/git-mcp@latest
39
+
40
+ # Verify installation
41
+ if command -v git-mcp &> /dev/null; then
42
+ echo -e "${GREEN}✅ git-mcp installed successfully!${NC}"
43
+ echo ""
44
+ echo -e "${YELLOW}📝 Next steps:${NC}"
45
+ echo "1. Configure your IDE to use 'git-mcp' as the command"
46
+ echo ""
47
+ echo " For Cursor/VS Code (settings.json or mcp.json):"
48
+ echo ' {'
49
+ echo ' "mcpServers": {'
50
+ echo ' "git-mcp": {'
51
+ echo ' "command": "git-mcp",'
52
+ echo ' "env": {'
53
+ echo ' "GITHUB_TOKEN": "your-token",'
54
+ echo ' "GITEA_URL": "https://your-gitea",'
55
+ echo ' "GITEA_TOKEN": "your-token"'
56
+ echo ' }'
57
+ echo ' }'
58
+ echo ' }'
59
+ echo ' }'
60
+ echo ""
61
+ echo "2. Restart your IDE or reconnect SSH"
62
+ echo ""
63
+ echo -e "${GREEN}Done! 🎉${NC}"
64
+ else
65
+ echo -e "${RED}❌ Installation failed. Check npm permissions.${NC}"
66
+ echo "Try: sudo npm install -g @andrebuzeli/git-mcp@latest"
67
+ exit 1
68
+ fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrebuzeli/git-mcp",
3
- "version": "15.8.0",
3
+ "version": "15.8.2",
4
4
  "private": false,
5
5
  "description": "MCP server para Git com operações locais e sincronização paralela GitHub/Gitea",
6
6
  "license": "MIT",
@@ -11,7 +11,8 @@
11
11
  },
12
12
  "files": [
13
13
  "src",
14
- "README.md"
14
+ "README.md",
15
+ "install.sh"
15
16
  ],
16
17
  "main": "src/index.js",
17
18
  "bin": {
@@ -35,9 +36,10 @@
35
36
  "@modelcontextprotocol/sdk": "^0.4.0",
36
37
  "@octokit/rest": "^20.0.0",
37
38
  "ajv": "^8.12.0",
39
+ "archiver": "^7.0.1",
38
40
  "axios": "^1.7.7"
39
41
  },
40
42
  "devDependencies": {
41
43
  "jest": "^30.2.0"
42
44
  }
43
- }
45
+ }
@@ -430,11 +430,42 @@ Analise o histórico e sugira qual commit pode ter introduzido o bug.`
430
430
  },
431
431
 
432
432
  "git-release": {
433
- getMessages: (context) => [{
434
- role: "user",
435
- content: {
436
- type: "text",
437
- text: `# Criar Release do Projeto
433
+ getMessages: (context) => {
434
+ // Analisa mensagem do usuário para detectar instruções sobre assets
435
+ const userMessage = context.userMessage || "";
436
+ let assetsInstructions = "";
437
+ let detectedAssets = [];
438
+
439
+ // Detecta menções de upload/pasta/asset
440
+ const assetPatterns = [
441
+ /(?:upload|envia|adiciona?) (?:tamb[eé]m? )?(?:a |o |da |do )?(?:pasta|arquivo|asset) (.+?)(?: com (?:o |a )?nome (.+?))?(?:$|[.!?])/gi,
442
+ /(?:inclu[íi]|adiciona?) (.+?) (?:como |no |ao )?asset/gi,
443
+ /(?:compacta|zipa) (.+?)(?: como (.+?))?(?: e envia|$)/gi
444
+ ];
445
+
446
+ for (const pattern of assetPatterns) {
447
+ let match;
448
+ while ((match = pattern.exec(userMessage)) !== null) {
449
+ const assetPath = match[1]?.trim();
450
+ const assetName = match[2]?.trim();
451
+
452
+ if (assetPath) {
453
+ if (assetName) {
454
+ detectedAssets.push(`{ path: "${assetPath}", name: "${assetName}" }`);
455
+ assetsInstructions += `- Asset customizado: \`${assetPath}\` → \`${assetName}\`\n`;
456
+ } else {
457
+ detectedAssets.push(`"${assetPath}"`);
458
+ assetsInstructions += `- Asset automático: \`${assetPath}\`\n`;
459
+ }
460
+ }
461
+ }
462
+ }
463
+
464
+ return [{
465
+ role: "user",
466
+ content: {
467
+ type: "text",
468
+ text: `# Criar Release do Projeto
438
469
 
439
470
  Você vai criar uma nova release deste projeto.
440
471
 
@@ -447,6 +478,9 @@ Você vai criar uma nova release deste projeto.
447
478
  ## Commits para esta Release
448
479
  ${context.recentCommits?.map(c => `- ${c.shortSha}: ${c.message}`).join("\n") || "Use git-history para ver commits"}
449
480
 
481
+ ## Assets para Upload
482
+ ${assetsInstructions || "Nenhum asset específico solicitado"}
483
+
450
484
  ## Passos para Release
451
485
 
452
486
  1. **Determinar versão**: Baseado nos commits, sugira a próxima versão (semver)
@@ -464,6 +498,7 @@ ${context.recentCommits?.map(c => `- ${c.shortSha}: ${c.message}`).join("\n") ||
464
498
  - tag: "vX.Y.Z"
465
499
  - name: "Release vX.Y.Z"
466
500
  - body: Release notes geradas
501
+ ${detectedAssets.length > 0 ? ` - assets: [${detectedAssets.join(', ')}]` : ''}
467
502
 
468
503
  ## Template de Release Notes
469
504
 
@@ -482,9 +517,12 @@ ${context.recentCommits?.map(c => `- ${c.shortSha}: ${c.message}`).join("\n") ||
482
517
  - Mudança 1
483
518
  \`\`\`
484
519
 
485
- Analise os commits e execute os passos para criar a release. UTILIZANDO APENAS AS TOOLS DO GIT-MCP.`
486
- }
487
- }]
520
+ ${assetsInstructions ? `## 📦 Assets Inclusos\n${assetsInstructions}` : ''}
521
+
522
+ Analise os commits e execute os passos para criar a release${assetsInstructions ? ', incluindo os assets solicitados' : ''}. UTILIZANDO APENAS AS TOOLS DO GIT-MCP.`
523
+ }
524
+ }];
525
+ }
488
526
  },
489
527
 
490
528
  "git-update": {
@@ -1,9 +1,84 @@
1
1
  import Ajv from "ajv";
2
2
  import axios from "axios";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import archiver from "archiver";
3
6
  import { asToolError, asToolResult, errorToResponse, mapExternalError } from "../utils/errors.js";
4
7
  import { getRepoNameFromPath, validateProjectPath } from "../utils/repoHelpers.js";
5
8
  import { runBoth } from "../utils/providerExec.js";
6
9
 
10
+ /**
11
+ * Compacta uma pasta em ZIP e retorna o caminho do arquivo temporário
12
+ * @param {string} folderPath - Caminho da pasta a compactar
13
+ * @param {string} zipName - Nome do arquivo ZIP (sem extensão)
14
+ * @returns {Promise<string>} Caminho do arquivo ZIP criado
15
+ */
16
+ async function compactFolder(folderPath, zipName) {
17
+ return new Promise((resolve, reject) => {
18
+ const outputPath = path.join(process.cwd(), 'temp_scripts', `${zipName}.zip`);
19
+ const output = fs.createWriteStream(outputPath);
20
+ const archive = archiver('zip', { zlib: { level: 9 } });
21
+
22
+ output.on('close', () => resolve(outputPath));
23
+ output.on('error', reject);
24
+ archive.on('error', reject);
25
+
26
+ archive.pipe(output);
27
+ archive.directory(folderPath, false);
28
+ archive.finalize();
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Faz upload de um asset para um release
34
+ * @param {Object} pm - Provider Manager
35
+ * @param {string} owner - Dono do repo
36
+ * @param {string} repo - Nome do repo
37
+ * @param {number} releaseId - ID do release
38
+ * @param {string} assetPath - Caminho do arquivo a fazer upload
39
+ * @param {string} assetName - Nome do asset (com extensão)
40
+ * @returns {Promise<Object>} Resultado do upload
41
+ */
42
+ async function uploadAsset(pm, owner, repo, releaseId, assetPath, assetName) {
43
+ const fileBuffer = fs.readFileSync(assetPath);
44
+ const fileSize = fs.statSync(assetPath).size;
45
+
46
+ const out = await runBoth(pm, {
47
+ github: async () => {
48
+ const response = await pm.github.request("POST /repos/{owner}/{repo}/releases/{release_id}/assets", {
49
+ owner,
50
+ repo,
51
+ release_id: releaseId,
52
+ name: assetName,
53
+ data: fileBuffer,
54
+ headers: {
55
+ 'content-type': 'application/octet-stream',
56
+ 'content-length': fileSize
57
+ }
58
+ });
59
+ return { ok: true, id: response.data.id, url: response.data.browser_download_url };
60
+ },
61
+ gitea: async () => {
62
+ const base = pm.giteaUrl.replace(/\/$/, "");
63
+
64
+ const response = await axios.post(
65
+ `${base}/api/v1/repos/${owner}/${repo}/releases/${releaseId}/assets?name=${encodeURIComponent(assetName)}`,
66
+ fileBuffer,
67
+ {
68
+ headers: {
69
+ 'Authorization': `token ${pm.giteaToken}`,
70
+ 'Content-Type': 'application/octet-stream',
71
+ 'Content-Length': fileSize
72
+ }
73
+ }
74
+ );
75
+ return { ok: true, id: response.data.id, url: response.data.browser_download_url };
76
+ }
77
+ });
78
+
79
+ return out;
80
+ }
81
+
7
82
  const ajv = new Ajv({ allErrors: true });
8
83
 
9
84
  export function createGitRemoteTool(pm, git) {
@@ -62,6 +137,23 @@ ARQUIVOS VIA API:
62
137
  type: "string",
63
138
  description: "Descrição/corpo da release"
64
139
  },
140
+ assets: {
141
+ type: "array",
142
+ items: {
143
+ oneOf: [
144
+ { type: "string" }, // path simples: "dist/app.exe"
145
+ {
146
+ type: "object",
147
+ properties: {
148
+ path: { type: "string" }, // "dist/build"
149
+ name: { type: "string" } // "MeuApp-Windows" (sem extensao)
150
+ },
151
+ required: ["path"]
152
+ }
153
+ ]
154
+ },
155
+ description: "Assets para upload. String = nome automatico, Objeto = nome custom. Pastas são compactadas automaticamente"
156
+ },
65
157
  topics: {
66
158
  type: "array",
67
159
  items: { type: "string" },
@@ -190,11 +282,100 @@ QUANDO USAR:
190
282
  const tag = args.tag;
191
283
  const name = args.name || tag;
192
284
  const body = args.body || "";
285
+
286
+ // Criar release primeiro
193
287
  const out = await runBoth(pm, {
194
288
  github: async (owner) => { const r = await pm.github.rest.repos.createRelease({ owner, repo, tag_name: tag, name, body, draft: false, prerelease: false }); return { ok: true, id: r.data.id, url: r.data.html_url }; },
195
289
  gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${base}/api/v1/repos/${owner}/${repo}/releases`, { tag_name: tag, name, body }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, id: r.data?.id }; }
196
290
  });
197
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), tag, name, providers: out });
291
+
292
+ const releaseCreated = !!(out.github?.ok || out.gitea?.ok);
293
+ if (!releaseCreated) {
294
+ return asToolResult({ success: false, tag, name, providers: out, error: "Falha ao criar release" });
295
+ }
296
+
297
+ // Processar assets se fornecidos
298
+ let assetsUploaded = [];
299
+ if (args.assets && args.assets.length > 0) {
300
+ const tempFiles = []; // Arquivos temporários a serem deletados
301
+ const [ghOwner, geOwner] = await Promise.all([
302
+ pm.getGitHubOwner().catch(() => ""),
303
+ pm.getGiteaOwner().catch(() => "")
304
+ ]);
305
+
306
+ for (const asset of args.assets) {
307
+ try {
308
+ const assetPath = typeof asset === 'string' ? asset : asset.path;
309
+ const customName = typeof asset === 'object' ? asset.name : null;
310
+
311
+ // Resolver caminho absoluto
312
+ const absolutePath = path.resolve(projectPath, assetPath);
313
+
314
+ // Verificar se existe
315
+ if (!fs.existsSync(absolutePath)) {
316
+ assetsUploaded.push({ path: assetPath, error: "Arquivo/pasta não encontrado" });
317
+ continue;
318
+ }
319
+
320
+ const stats = fs.statSync(absolutePath);
321
+ let uploadPath = absolutePath;
322
+ let uploadName = customName;
323
+ let isTempFile = false;
324
+
325
+ if (stats.isDirectory()) {
326
+ // Compactar pasta
327
+ const zipName = customName || path.basename(absolutePath);
328
+ const zipPath = await compactFolder(absolutePath, zipName);
329
+ uploadPath = zipPath;
330
+ uploadName = `${zipName}.zip`;
331
+ tempFiles.push(zipPath);
332
+ isTempFile = true;
333
+ } else {
334
+ // Arquivo direto
335
+ if (!uploadName) {
336
+ uploadName = path.basename(absolutePath);
337
+ } else {
338
+ // Adicionar extensão se não tiver
339
+ const ext = path.extname(absolutePath);
340
+ if (!uploadName.includes('.')) {
341
+ uploadName += ext;
342
+ }
343
+ }
344
+ }
345
+
346
+ // Upload para GitHub e Gitea
347
+ const uploadResult = await uploadAsset(pm, ghOwner || geOwner, repo, out.github?.id || out.gitea?.id, uploadPath, uploadName);
348
+
349
+ assetsUploaded.push({
350
+ path: assetPath,
351
+ name: uploadName,
352
+ uploaded: !!(uploadResult.github?.ok || uploadResult.gitea?.ok),
353
+ providers: uploadResult
354
+ });
355
+
356
+ // Deletar arquivo temporário se foi criado
357
+ if (isTempFile && tempFiles.includes(uploadPath)) {
358
+ try {
359
+ fs.unlinkSync(uploadPath);
360
+ tempFiles.splice(tempFiles.indexOf(uploadPath), 1);
361
+ } catch (e) {
362
+ console.warn(`Falha ao deletar arquivo temporário: ${uploadPath}`);
363
+ }
364
+ }
365
+
366
+ } catch (error) {
367
+ assetsUploaded.push({ path: typeof asset === 'string' ? asset : asset.path, error: String(error.message || error) });
368
+ }
369
+ }
370
+ }
371
+
372
+ return asToolResult({
373
+ success: releaseCreated,
374
+ tag,
375
+ name,
376
+ providers: out,
377
+ assets: assetsUploaded.length > 0 ? assetsUploaded : undefined
378
+ });
198
379
  }
199
380
  if (action === "topics-set") {
200
381
  const repo = getRepoNameFromPath(projectPath);