@andrebuzeli/git-mcp 15.2.0 → 15.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrebuzeli/git-mcp",
3
- "version": "15.2.0",
3
+ "version": "15.2.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",
package/src/index.js CHANGED
@@ -26,9 +26,22 @@ import { getResources, readResource } from "./resources/index.js";
26
26
  const pm = new ProviderManager();
27
27
  const git = new GitAdapter(pm);
28
28
 
29
+ // Log de inicialização para stderr (não interfere com stdio do MCP)
30
+ const hasGitHub = !!process.env.GITHUB_TOKEN;
31
+ const hasGitea = !!process.env.GITEA_URL && !!process.env.GITEA_TOKEN;
32
+ if (!hasGitHub && !hasGitea) {
33
+ console.error("[git-mcp] ⚠️ Nenhum provider configurado. Operações remotas não funcionarão.");
34
+ console.error("[git-mcp] Configure GITHUB_TOKEN e/ou GITEA_URL + GITEA_TOKEN");
35
+ } else {
36
+ const providers = [];
37
+ if (hasGitHub) providers.push("GitHub");
38
+ if (hasGitea) providers.push("Gitea");
39
+ console.error(`[git-mcp] ✓ Providers ativos: ${providers.join(", ")}`);
40
+ }
41
+
29
42
  const transport = new StdioServerTransport();
30
43
  const server = new Server(
31
- { name: "git-mcpv2", version: "15.2.0" },
44
+ { name: "git-mcpv2", version: "15.2.1" },
32
45
  { capabilities: { tools: {}, resources: {} } }
33
46
  );
34
47
  server.connect(transport);
@@ -71,7 +71,14 @@ QUANDO USAR CADA ACTION:
71
71
  }
72
72
  const { projectPath, action } = args;
73
73
  try {
74
+ // #region agent log
75
+ fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H1',location:'git-workflow.js:handle-entry',message:'handle start',data:{action,projectPath},timestamp:Date.now()})}).catch(()=>{});
76
+ // #endregion
77
+
74
78
  validateProjectPath(projectPath);
79
+ // #region agent log
80
+ fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H1',location:'git-workflow.js:handle-validated',message:'projectPath validated',data:{action},timestamp:Date.now()})}).catch(()=>{});
81
+ // #endregion
75
82
  if (action === "init") {
76
83
  await git.init(projectPath);
77
84
 
@@ -96,6 +103,9 @@ QUANDO USAR CADA ACTION:
96
103
  }
97
104
  if (action === "status") {
98
105
  const st = await git.status(projectPath);
106
+ // #region agent log
107
+ fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H2',location:'git-workflow.js:status',message:'status result',data:{modified:st.modified.length,created:st.created.length,deleted:st.deleted.length,notAdded:st.not_added.length,isClean:st.isClean},timestamp:Date.now()})}).catch(()=>{});
108
+ // #endregion
99
109
  return asToolResult(st);
100
110
  }
101
111
  if (action === "add") {
@@ -113,6 +123,9 @@ QUANDO USAR CADA ACTION:
113
123
  return asToolError("MISSING_PARAMETER", "message é obrigatório para commit", { parameter: "message" });
114
124
  }
115
125
  const sha = await git.commit(projectPath, args.message);
126
+ // #region agent log
127
+ fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H3',location:'git-workflow.js:commit',message:'commit created',data:{sha,message:args.message},timestamp:Date.now()})}).catch(()=>{});
128
+ // #endregion
116
129
  return asToolResult({ success: true, sha, message: args.message, nextStep: "Use action='push' para enviar ao GitHub/Gitea" });
117
130
  }
118
131
  if (action === "clean") {
@@ -187,7 +187,7 @@ export class GitAdapter {
187
187
  if (errors.length === targets.length) {
188
188
  throw createError("PUSH_REJECTED", {
189
189
  message: "Push falhou para todos os remotes",
190
- errors: errors.map(e => e.reason?.message || String(e.reason))
190
+ errors: errors.map(e => e.reason?.data?.originalError || e.reason?.message || String(e.reason))
191
191
  });
192
192
  }
193
193
  return {
@@ -334,9 +334,28 @@ export class GitAdapter {
334
334
  async saveStash(dir, message = "WIP", includeUntracked = false) {
335
335
  const st = await this.status(dir);
336
336
 
337
- // Verifica se há arquivos para stash
338
- const filesToSave = [...st.not_added, ...st.modified, ...st.created];
339
- if (filesToSave.length === 0 && !includeUntracked) {
337
+ const filesToSave = [];
338
+
339
+ // 1. Staged files (modified, created)
340
+ filesToSave.push(...st.modified, ...st.created);
341
+
342
+ // 2. Unstaged files (from not_added)
343
+ for (const file of st.not_added) {
344
+ const info = st.files.find(f => f.path === file);
345
+ if (!info) continue;
346
+
347
+ if (info.working_dir === "new") {
348
+ // Untracked
349
+ if (includeUntracked) filesToSave.push(file);
350
+ } else {
351
+ // Unstaged modified/deleted
352
+ filesToSave.push(file);
353
+ }
354
+ }
355
+
356
+ const uniqueFiles = [...new Set(filesToSave)];
357
+
358
+ if (uniqueFiles.length === 0) {
340
359
  throw createError("NOTHING_TO_STASH", {
341
360
  message: "Working tree limpa, nada para stash",
342
361
  status: st
@@ -346,7 +365,7 @@ export class GitAdapter {
346
365
  // Salva arquivos modificados/adicionados
347
366
  const stashData = { message, timestamp: Date.now(), files: {} };
348
367
 
349
- for (const file of filesToSave) {
368
+ for (const file of uniqueFiles) {
350
369
  const fullPath = path.join(dir, file);
351
370
  if (fs.existsSync(fullPath)) {
352
371
  // Lê como buffer para suportar binários
@@ -364,7 +383,7 @@ export class GitAdapter {
364
383
  fs.writeFileSync(this._getStashFile(dir), JSON.stringify(stashes, null, 2));
365
384
 
366
385
  // Remove arquivos stashados do working directory
367
- for (const file of filesToSave) {
386
+ for (const file of uniqueFiles) {
368
387
  const fullPath = path.join(dir, file);
369
388
  if (fs.existsSync(fullPath)) {
370
389
  fs.unlinkSync(fullPath);
@@ -645,7 +664,7 @@ export class GitAdapter {
645
664
  message: `Não foi possível resolver ${ref}`,
646
665
  requestedSteps: steps,
647
666
  availableCommits: commits.length,
648
- suggestion: `Histórico tem apenas ${commits.length} commits. Use HEAD~${commits.length - 1} no máximo.`
667
+ suggestion: `Histórico tem apenas ${commits.length} commits. Use HEAD~${Math.max(0, commits.length - 1)} no máximo.`
649
668
  });
650
669
  }
651
670
  // Refs normais
@@ -675,9 +694,9 @@ export class GitAdapter {
675
694
  }
676
695
 
677
696
  async resetMixed(dir, ref) {
678
- await this.resetSoft(dir, ref);
679
- // Reset index to match ref
680
697
  const oid = await this._resolveRef(dir, ref);
698
+ await this.resetSoft(dir, oid);
699
+ // Reset index to match ref
681
700
  await git.checkout({ fs, dir, ref: oid, noUpdateHead: true });
682
701
  }
683
702
 
@@ -739,13 +758,18 @@ export class GitAdapter {
739
758
 
740
759
  // Check if fast-forward is possible
741
760
  if (bases[0] === ourOid) {
761
+ const currentBranch = await this.getCurrentBranch(dir);
762
+
742
763
  // Fast-forward merge
743
764
  await git.checkout({ fs, dir, ref: theirOid, force: true });
744
- const currentBranch = await this.getCurrentBranch(dir);
745
- if (currentBranch !== "HEAD") {
765
+
766
+ if (currentBranch && currentBranch !== "HEAD") {
746
767
  const branchPath = path.join(dir, ".git", "refs", "heads", currentBranch);
747
768
  fs.mkdirSync(path.dirname(branchPath), { recursive: true });
748
769
  fs.writeFileSync(branchPath, theirOid + "\n");
770
+
771
+ // Re-checkout the branch to attach HEAD back to it
772
+ await git.checkout({ fs, dir, ref: currentBranch, force: true });
749
773
  }
750
774
  return { fastForward: true, sha: theirOid };
751
775
  }
@@ -761,11 +785,31 @@ export class GitAdapter {
761
785
  message: message || `Merge branch '${branch}'`
762
786
  }).catch(async (e) => {
763
787
  // Check for conflicts
764
- if (e.message?.includes("conflict")) {
788
+ if (e.message?.includes("conflict") || e.code === "MergeConflictError") {
789
+ // Manually create MERGE_HEAD to allow aborting
790
+ try {
791
+ fs.writeFileSync(path.join(dir, ".git", "MERGE_HEAD"), theirOid + "\n");
792
+ } catch {}
793
+
794
+ let conflictFiles = [];
795
+ // Try to get files from error data
796
+ if (e.data && e.data.filepaths) {
797
+ if (Array.isArray(e.data.filepaths)) {
798
+ conflictFiles = e.data.filepaths;
799
+ } else {
800
+ conflictFiles = Object.keys(e.data.filepaths);
801
+ }
802
+ }
803
+
804
+ // Fallback to status matrix
805
+ if (conflictFiles.length === 0) {
806
+ conflictFiles = await this._detectConflicts(dir);
807
+ }
808
+
765
809
  return {
766
810
  conflicts: true,
767
- message: e.message,
768
- files: await this._detectConflicts(dir)
811
+ message: e.message || "Merge conflict detected",
812
+ files: conflictFiles
769
813
  };
770
814
  }
771
815
  throw e;
@@ -800,8 +844,8 @@ export class GitAdapter {
800
844
  const conflicts = [];
801
845
  for (const row of matrix) {
802
846
  const [file, head, workdir, stage] = row;
803
- // Conflito: diferentes estados
804
- if (head !== workdir || workdir !== stage) {
847
+ // Stage 3 indicates a merge conflict in isomorphic-git
848
+ if (stage === 3) {
805
849
  conflicts.push(file);
806
850
  }
807
851
  }
@@ -1045,7 +1089,17 @@ export class GitAdapter {
1045
1089
  if (branch) cloneOptions.ref = branch;
1046
1090
  if (depth) cloneOptions.depth = depth;
1047
1091
 
1048
- await withRetry(() => git.clone(cloneOptions));
1092
+ const wasEmpty = !fs.existsSync(dir) || fs.readdirSync(dir).length === 0;
1093
+
1094
+ await withRetry(async () => {
1095
+ // Se o diretório estava vazio originalmente e agora tem .git, é de uma tentativa falha anterior
1096
+ if (wasEmpty && fs.existsSync(path.join(dir, ".git"))) {
1097
+ try {
1098
+ fs.rmSync(path.join(dir, ".git"), { recursive: true, force: true });
1099
+ } catch {}
1100
+ }
1101
+ return git.clone(cloneOptions);
1102
+ });
1049
1103
 
1050
1104
  const currentBranch = await this.getCurrentBranch(dir);
1051
1105
  const remotes = await this.listRemotes(dir);