@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 +1 -1
- package/src/index.js +14 -1
- package/src/tools/git-workflow.js +13 -0
- package/src/utils/gitAdapter.js +71 -17
package/package.json
CHANGED
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.
|
|
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") {
|
package/src/utils/gitAdapter.js
CHANGED
|
@@ -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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
804
|
-
if (
|
|
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
|
-
|
|
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);
|