@andrebuzeli/git-mcp 15.9.1 → 15.9.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.9.1",
3
+ "version": "15.9.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
@@ -47,7 +47,7 @@ if (!hasGitHub && !hasGitea) {
47
47
 
48
48
  const transport = new StdioServerTransport();
49
49
  const server = new Server(
50
- { name: "git-mcpv2", version: "15.8.0" },
50
+ { name: "git-mcpv2", version: "15.9.2" },
51
51
  { capabilities: { tools: {}, resources: {}, prompts: {} } }
52
52
  );
53
53
  server.connect(transport);
@@ -8,13 +8,13 @@ export function createGitWorkflowTool(pm, git) {
8
8
  const inputSchema = {
9
9
  type: "object",
10
10
  properties: {
11
- projectPath: {
12
- type: "string",
13
- description: "Caminho absoluto do diretório do projeto (ex: 'C:/Users/user/projeto' ou '/home/user/projeto')"
11
+ projectPath: {
12
+ type: "string",
13
+ description: "Caminho absoluto do diretório do projeto (ex: 'C:/Users/user/projeto' ou '/home/user/projeto')"
14
14
  },
15
- action: {
16
- type: "string",
17
- enum: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean", "update"],
15
+ action: {
16
+ type: "string",
17
+ enum: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean"],
18
18
  description: `Ação a executar:
19
19
  - init: Inicializa repositório git local E cria repos no GitHub/Gitea automaticamente
20
20
  - status: Retorna arquivos modificados, staged, untracked (use ANTES de commit para ver o que mudou)
@@ -23,20 +23,19 @@ export function createGitWorkflowTool(pm, git) {
23
23
  - commit: Cria commit com os arquivos staged (use DEPOIS de add)
24
24
  - push: Envia commits para GitHub E Gitea em paralelo (use DEPOIS de commit)
25
25
  - ensure-remotes: Configura remotes GitHub e Gitea (use se push falhar por falta de remote)
26
- - clean: Remove arquivos não rastreados do working directory
27
- - update: Atualiza projeto completo: init (se necessário), add, commit, ensure-remotes e push`
26
+ - clean: Remove arquivos não rastreados do working directory`
28
27
  },
29
- files: {
30
- type: "array",
28
+ files: {
29
+ type: "array",
31
30
  items: { type: "string" },
32
31
  description: "Lista de arquivos para add/remove. Use ['.'] para todos os arquivos. Ex: ['src/index.js', 'package.json']"
33
32
  },
34
- message: {
33
+ message: {
35
34
  type: "string",
36
- description: "Mensagem do commit. Obrigatório para action='commit'. Opcional para action='update' (se não fornecido, usa mensagem padrão). Ex: 'feat: adiciona nova funcionalidade'"
35
+ description: "Mensagem do commit. Obrigatório para action='commit'. Ex: 'feat: adiciona nova funcionalidade'"
37
36
  },
38
- force: {
39
- type: "boolean",
37
+ force: {
38
+ type: "boolean",
40
39
  description: "Force push (use apenas se push normal falhar com erro de histórico divergente). Default: false"
41
40
  },
42
41
  createGitignore: {
@@ -48,7 +47,7 @@ export function createGitWorkflowTool(pm, git) {
48
47
  description: "Se true, repositório será PÚBLICO. Default: false (privado). Aplica-se a action='init' e 'ensure-remotes'"
49
48
  },
50
49
  dryRun: {
51
- type: "boolean",
50
+ type: "boolean",
52
51
  description: "Se true, simula a operação sem executar (útil para testes). Default: false"
53
52
  }
54
53
  },
@@ -72,7 +71,6 @@ QUANDO USAR CADA ACTION:
72
71
  - init: Apenas uma vez, para novos projetos (cria .gitignore automaticamente)
73
72
  - ensure-remotes: Se push falhar por falta de configuração
74
73
  - clean: Limpar arquivos não rastreados
75
- - update: Para atualizar projeto completo (init + add + commit + push) - mais rápido que fazer cada ação separadamente
76
74
 
77
75
  EXEMPLOS DE USO:
78
76
  • Iniciar projeto: { "projectPath": "/path/to/project", "action": "init" }
@@ -90,29 +88,29 @@ EXEMPLOS DE USO:
90
88
  try {
91
89
  // #region agent log
92
90
  if (process.env.DEBUG_AGENT_LOG) {
93
- 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(()=>{});
91
+ 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(() => { });
94
92
  }
95
93
  // #endregion
96
94
 
97
95
  validateProjectPath(projectPath);
98
96
  // #region agent log
99
97
  if (process.env.DEBUG_AGENT_LOG) {
100
- 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(()=>{});
98
+ 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(() => { });
101
99
  }
102
100
  // #endregion
103
101
  if (action === "init") {
104
102
  if (args.dryRun) {
105
- return asToolResult({
106
- success: true,
103
+ return asToolResult({
104
+ success: true,
107
105
  dryRun: true,
108
106
  message: "DRY RUN: Repositório seria inicializado localmente e nos providers",
109
107
  repoName: getRepoNameFromPath(projectPath),
110
108
  gitignoreCreated: shouldCreateGitignore
111
109
  });
112
110
  }
113
-
111
+
114
112
  await git.init(projectPath);
115
-
113
+
116
114
  // Criar .gitignore baseado no tipo de projeto
117
115
  const shouldCreateGitignore = args.createGitignore !== false;
118
116
  let gitignoreCreated = false;
@@ -122,12 +120,12 @@ EXEMPLOS DE USO:
122
120
  await git.createGitignore(projectPath, patterns);
123
121
  gitignoreCreated = true;
124
122
  }
125
-
123
+
126
124
  const repo = getRepoNameFromPath(projectPath);
127
125
  const isPublic = args.isPublic === true; // Default: privado
128
126
  const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
129
- return asToolResult({
130
- success: true,
127
+ return asToolResult({
128
+ success: true,
131
129
  ensured,
132
130
  isPrivate: !isPublic,
133
131
  gitignoreCreated,
@@ -138,24 +136,24 @@ EXEMPLOS DE USO:
138
136
  const st = await git.status(projectPath);
139
137
  // #region agent log
140
138
  if (process.env.DEBUG_AGENT_LOG) {
141
- 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(()=>{});
139
+ 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(() => { });
142
140
  }
143
141
  // #endregion
144
-
142
+
145
143
  if (args.dryRun) {
146
- return asToolResult({
147
- success: true,
144
+ return asToolResult({
145
+ success: true,
148
146
  dryRun: true,
149
147
  message: "DRY RUN: Status seria verificado",
150
148
  ...st
151
149
  });
152
150
  }
153
-
151
+
154
152
  // Adicionar contexto para AI Agent decidir próximo passo
155
153
  const hasUnstaged = st.not_added?.length > 0 || st.modified?.length > 0 || st.created?.length > 0;
156
154
  const hasStaged = st.staged?.length > 0;
157
155
  const hasConflicts = st.conflicted?.length > 0;
158
-
156
+
159
157
  let _aiContext = {
160
158
  needsAdd: hasUnstaged && !hasStaged,
161
159
  readyToCommit: hasStaged && !hasConflicts,
@@ -163,7 +161,7 @@ EXEMPLOS DE USO:
163
161
  isClean: st.isClean,
164
162
  suggestedAction: null
165
163
  };
166
-
164
+
167
165
  if (hasConflicts) {
168
166
  _aiContext.suggestedAction = "Resolva os conflitos manualmente antes de continuar";
169
167
  } else if (hasStaged) {
@@ -173,21 +171,21 @@ EXEMPLOS DE USO:
173
171
  } else {
174
172
  _aiContext.suggestedAction = "Working tree limpa. Modifique arquivos ou use action='push' se há commits pendentes";
175
173
  }
176
-
174
+
177
175
  return asToolResult({ ...st, _aiContext }, { tool: 'workflow', action: 'status' });
178
176
  }
179
177
  if (action === "add") {
180
178
  const files = Array.isArray(args.files) && args.files.length ? args.files : ["."];
181
-
179
+
182
180
  if (args.dryRun) {
183
- return asToolResult({
184
- success: true,
181
+ return asToolResult({
182
+ success: true,
185
183
  dryRun: true,
186
184
  message: `DRY RUN: Arquivos seriam adicionados: ${files.join(", ")}`,
187
185
  files
188
186
  });
189
187
  }
190
-
188
+
191
189
  await git.add(projectPath, files);
192
190
  return asToolResult({ success: true, files }, { tool: 'workflow', action: 'add' });
193
191
  }
@@ -200,19 +198,19 @@ EXEMPLOS DE USO:
200
198
  if (!args.message) {
201
199
  return asToolError("MISSING_PARAMETER", "message é obrigatório para commit", { parameter: "message" });
202
200
  }
203
-
201
+
204
202
  if (args.dryRun) {
205
- return asToolResult({
206
- success: true,
203
+ return asToolResult({
204
+ success: true,
207
205
  dryRun: true,
208
206
  message: `DRY RUN: Commit seria criado com mensagem: "${args.message}"`
209
207
  });
210
208
  }
211
-
209
+
212
210
  const sha = await git.commit(projectPath, args.message);
213
211
  // #region agent log
214
212
  if (process.env.DEBUG_AGENT_LOG) {
215
- 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(()=>{});
213
+ 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(() => { });
216
214
  }
217
215
  // #endregion
218
216
  return asToolResult({ success: true, sha, message: args.message }, { tool: 'workflow', action: 'commit' });
@@ -220,19 +218,19 @@ EXEMPLOS DE USO:
220
218
  if (action === "clean") {
221
219
  if (args.dryRun) {
222
220
  const result = await git.cleanUntracked(projectPath);
223
- return asToolResult({
224
- success: true,
221
+ return asToolResult({
222
+ success: true,
225
223
  dryRun: true,
226
224
  message: `DRY RUN: ${result.cleaned.length} arquivo(s) seriam removidos`,
227
225
  wouldClean: result.cleaned
228
226
  });
229
227
  }
230
-
228
+
231
229
  const result = await git.cleanUntracked(projectPath);
232
- return asToolResult({
233
- success: true,
230
+ return asToolResult({
231
+ success: true,
234
232
  ...result,
235
- message: result.cleaned.length > 0
233
+ message: result.cleaned.length > 0
236
234
  ? `${result.cleaned.length} arquivo(s) não rastreados removidos`
237
235
  : "Nenhum arquivo para limpar"
238
236
  });
@@ -240,7 +238,7 @@ EXEMPLOS DE USO:
240
238
  if (action === "ensure-remotes") {
241
239
  const repo = getRepoNameFromPath(projectPath);
242
240
  const isPublic = args.isPublic === true; // Default: privado
243
-
241
+
244
242
  if (args.dryRun) {
245
243
  const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: false, isPublic }); // Don't create for dry run
246
244
  const ghOwner = await pm.getGitHubOwner();
@@ -248,9 +246,9 @@ EXEMPLOS DE USO:
248
246
  const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
249
247
  const base = pm.giteaUrl?.replace(/\/$/, "") || "";
250
248
  const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
251
-
252
- return asToolResult({
253
- success: true,
249
+
250
+ return asToolResult({
251
+ success: true,
254
252
  dryRun: true,
255
253
  message: "DRY RUN: Remotes seriam configurados",
256
254
  repo,
@@ -259,7 +257,7 @@ EXEMPLOS DE USO:
259
257
  ensured
260
258
  });
261
259
  }
262
-
260
+
263
261
  const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
264
262
  const ghOwner = await pm.getGitHubOwner();
265
263
  const geOwner = await pm.getGiteaOwner();
@@ -273,120 +271,22 @@ EXEMPLOS DE USO:
273
271
  if (action === "push") {
274
272
  const branch = await git.getCurrentBranch(projectPath);
275
273
  const force = !!args.force;
276
-
274
+
277
275
  if (args.dryRun) {
278
- return asToolResult({
279
- success: true,
276
+ return asToolResult({
277
+ success: true,
280
278
  dryRun: true,
281
279
  message: `DRY RUN: Push seria executado na branch '${branch}'${force ? ' (force)' : ''}`,
282
280
  branch,
283
281
  force
284
282
  });
285
283
  }
286
-
284
+
287
285
  const result = await git.pushParallel(projectPath, branch, force);
288
286
  return asToolResult({ success: true, branch, ...result });
289
287
  }
290
- if (action === "update") {
291
- if (args.dryRun) {
292
- return asToolResult({
293
- success: true,
294
- dryRun: true,
295
- message: "DRY RUN: Update completo seria executado (init se necessário, add, commit, push)"
296
- });
297
- }
298
-
299
- const results = {
300
- init: null,
301
- ensureRemotes: null,
302
- add: null,
303
- commit: null,
304
- push: null
305
- };
306
-
307
- const repo = getRepoNameFromPath(projectPath);
308
- const isPublic = args.isPublic === true;
309
-
310
- // 1. Verificar se é repo Git, se não for, fazer init
311
- const isRepo = await git.isRepo(projectPath).catch(() => false);
312
- if (!isRepo) {
313
- await git.init(projectPath);
314
-
315
- // Criar .gitignore baseado no tipo de projeto
316
- const shouldCreateGitignore = args.createGitignore !== false;
317
- if (shouldCreateGitignore) {
318
- const projectType = detectProjectType(projectPath);
319
- const patterns = GITIGNORE_TEMPLATES[projectType] || GITIGNORE_TEMPLATES.general;
320
- await git.createGitignore(projectPath, patterns);
321
- }
322
-
323
- const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
324
- results.init = { success: true, ensured, isPrivate: !isPublic, gitignoreCreated: shouldCreateGitignore };
325
- } else {
326
- results.init = { success: true, skipped: true, message: "Repositório já existe" };
327
- }
328
-
329
- // 2. Garantir remotes configurados (sempre verifica/configura)
330
- const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
331
- const ghOwner = await pm.getGitHubOwner();
332
- const geOwner = await pm.getGiteaOwner();
333
- const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
334
- const base = pm.giteaUrl?.replace(/\/$/, "") || "";
335
- const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
336
- await git.ensureRemotes(projectPath, { githubUrl, giteaUrl });
337
- const remotes = await git.listRemotes(projectPath);
338
- results.ensureRemotes = { success: true, ensured, remotes };
339
-
340
- // 3. Verificar status e fazer add se necessário
341
- const status = await git.status(projectPath);
342
- if (!status.isClean || status.modified?.length > 0 || status.created?.length > 0 || status.notAdded?.length > 0) {
343
- await git.add(projectPath, ["."]);
344
- results.add = { success: true, files: ["."] };
345
- } else {
346
- results.add = { success: true, skipped: true, message: "Nenhum arquivo para adicionar" };
347
- }
348
-
349
- // 4. Verificar se há algo staged e fazer commit
350
- const statusAfterAdd = await git.status(projectPath);
351
- if (statusAfterAdd.staged?.length > 0) {
352
- // Gerar mensagem padrão se não fornecida
353
- const commitMessage = args.message || `Update: ${new Date().toISOString().split('T')[0]} - ${statusAfterAdd.staged.length} arquivo(s) modificado(s)`;
354
- const sha = await git.commit(projectPath, commitMessage);
355
- results.commit = { success: true, sha, message: commitMessage };
356
- } else {
357
- results.commit = { success: true, skipped: true, message: "Nenhum arquivo staged para commit" };
358
- }
359
-
360
- // 5. Fazer push (só se houver commits para enviar)
361
- const branch = await git.getCurrentBranch(projectPath);
362
- const force = !!args.force;
363
- try {
364
- const pushResult = await git.pushParallel(projectPath, branch, force);
365
- results.push = { success: true, branch, ...pushResult };
366
- } catch (pushError) {
367
- // Se push falhar mas não houver commits, não é erro crítico
368
- if (results.commit?.skipped) {
369
- results.push = { success: true, skipped: true, message: "Nenhum commit para enviar" };
370
- } else {
371
- throw pushError;
372
- }
373
- }
374
-
375
- // Resumo final
376
- const allSuccess = Object.values(results).every(r => r?.success !== false);
377
- const stepsExecuted = Object.entries(results)
378
- .filter(([_, r]) => r && !r.skipped)
379
- .map(([step, _]) => step);
380
-
381
- return asToolResult({
382
- success: allSuccess,
383
- message: `Update completo executado: ${stepsExecuted.join(" → ")}`,
384
- results,
385
- stepsExecuted
386
- }, { tool: 'workflow', action: 'update' });
387
- }
388
- return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
389
- availableActions: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean", "update"],
288
+ return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
289
+ availableActions: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean"],
390
290
  suggestion: "Use uma das actions disponíveis"
391
291
  });
392
292
  } catch (e) {