@andrebuzeli/git-mcp 11.0.5 → 12.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.
Files changed (55) hide show
  1. package/README.md +39 -47
  2. package/package.json +24 -19
  3. package/src/index.js +84 -0
  4. package/src/providers/providerManager.js +107 -0
  5. package/src/tools/git-branches.js +63 -0
  6. package/src/tools/git-config.js +53 -0
  7. package/src/tools/git-files.js +41 -0
  8. package/src/tools/git-history.js +36 -0
  9. package/src/tools/git-ignore.js +48 -0
  10. package/src/tools/git-issues.js +58 -12
  11. package/src/tools/git-pulls.js +61 -0
  12. package/src/tools/git-remote.js +182 -29
  13. package/src/tools/git-reset.js +35 -0
  14. package/src/tools/git-stash.js +57 -0
  15. package/src/tools/git-sync.js +44 -40
  16. package/src/tools/git-tags.js +58 -0
  17. package/src/tools/git-workflow.js +85 -0
  18. package/src/utils/errors.js +22 -0
  19. package/src/utils/gitAdapter.js +116 -0
  20. package/src/utils/providerExec.js +13 -0
  21. package/src/utils/repoHelpers.js +25 -0
  22. package/src/utils/retry.js +12 -0
  23. package/bin/git-mcp.js +0 -21
  24. package/docs/TOOLS.md +0 -110
  25. package/mcp.json.template +0 -12
  26. package/src/local/git.js +0 -14
  27. package/src/providers/gitea.js +0 -13
  28. package/src/providers/github.js +0 -13
  29. package/src/server.js +0 -130
  30. package/src/tools/git-actions.js +0 -19
  31. package/src/tools/git-activity.js +0 -28
  32. package/src/tools/git-admin.js +0 -20
  33. package/src/tools/git-checks.js +0 -14
  34. package/src/tools/git-commits.js +0 -34
  35. package/src/tools/git-contents.js +0 -30
  36. package/src/tools/git-deployments.js +0 -21
  37. package/src/tools/git-gists.js +0 -15
  38. package/src/tools/git-gitdata.js +0 -19
  39. package/src/tools/git-issues-prs.js +0 -44
  40. package/src/tools/git-local.js +0 -66
  41. package/src/tools/git-meta.js +0 -19
  42. package/src/tools/git-misc.js +0 -21
  43. package/src/tools/git-orgs.js +0 -26
  44. package/src/tools/git-packages.js +0 -12
  45. package/src/tools/git-raw.js +0 -14
  46. package/src/tools/git-releases.js +0 -17
  47. package/src/tools/git-repos.js +0 -60
  48. package/src/tools/git-search.js +0 -18
  49. package/src/tools/git-user.js +0 -26
  50. package/src/tools/schema.js +0 -3
  51. package/src/utils/fs.js +0 -29
  52. package/src/utils/project.js +0 -7
  53. package/tests/errors.js +0 -26
  54. package/tests/full_suite.js +0 -98
  55. package/tests/run.js +0 -50
@@ -0,0 +1,61 @@
1
+ import Ajv from "ajv";
2
+ import axios from "axios";
3
+ import { asToolError, asToolResult } from "../utils/errors.js";
4
+ import { getRepoNameFromPath } from "../utils/repoHelpers.js";
5
+ import { runBoth } from "../utils/providerExec.js";
6
+
7
+ const ajv = new Ajv({ allErrors: true });
8
+
9
+ export function createGitPullsTool(pm) {
10
+ const inputSchema = {
11
+ type: "object",
12
+ properties: {
13
+ projectPath: { type: "string" },
14
+ action: { type: "string", enum: ["create", "list", "files"] },
15
+ title: { type: "string" },
16
+ head: { type: "string" },
17
+ base: { type: "string" },
18
+ number: { type: "number" }
19
+ },
20
+ required: ["projectPath", "action"],
21
+ additionalProperties: true
22
+ };
23
+
24
+ async function handle(args) {
25
+ const validate = ajv.compile(inputSchema);
26
+ if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
27
+ const repo = getRepoNameFromPath(args.projectPath);
28
+ try {
29
+ if (args.action === "create") {
30
+ const head = args.head || "feature";
31
+ const base = args.base || "main";
32
+ const out = await runBoth(pm, {
33
+ github: async (owner) => { const r = await pm.github.rest.pulls.create({ owner, repo, title: args.title || `${head} -> ${base}`, head, base }); return { ok: true, number: r.data.number }; },
34
+ gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls`, { title: args.title || `${head} -> ${base}`, head, base }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, number: r.data?.number }; }
35
+ });
36
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
37
+ }
38
+ if (args.action === "list") {
39
+ const out = await runBoth(pm, {
40
+ github: async (owner) => { const r = await pm.github.rest.pulls.list({ owner, repo, state: "all" }); return { ok: true, count: r.data.length }; },
41
+ gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data||[]).length }; }
42
+ });
43
+ return asToolResult({ providers: out });
44
+ }
45
+ if (args.action === "files") {
46
+ const num = args.number;
47
+ if (!num) return asToolError("VALIDATION_ERROR", "number é obrigatório");
48
+ const out = await runBoth(pm, {
49
+ github: async (owner) => { const r = await pm.github.rest.pulls.listFiles({ owner, repo, pull_number: num }); return { ok: true, count: r.data.length }; },
50
+ gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls/${num}/files`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data||[]).length }; }
51
+ });
52
+ return asToolResult({ providers: out });
53
+ }
54
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${args.action}`);
55
+ } catch (e) {
56
+ return asToolError(e.code || "ERROR", e.message || String(e));
57
+ }
58
+ }
59
+
60
+ return { name: "git-pulls", description: "Pull Requests em paralelo (GitHub + Gitea)", inputSchema, handle };
61
+ }
@@ -1,29 +1,182 @@
1
- import { Gitea } from '../providers/gitea.js'
2
- import { Github } from '../providers/github.js'
3
- import { deriveProjectName } from '../utils/project.js'
4
- import fs from 'fs'
5
- import git from 'isomorphic-git'
6
- import { makeSchema } from './schema.js'
7
-
8
- export class GitRemoteTool {
9
- constructor(env) { this.env = env; this.providers = { gitea: new Gitea(env.GITEA_URL, env.GITEA_TOKEN), github: new Github(env.GITHUB_TOKEN) } }
10
- async handle(action, projectPath, args) {
11
- const name = deriveProjectName(projectPath)
12
- if (action === 'createRepo') { const r = await Promise.allSettled([ this.providers.gitea.createRepo(name, false, true), this.providers.github.createRepo(name, false, true) ]); return r.map(x => x.status === 'fulfilled' ? x.value : { error: x.reason?.message }) }
13
- if (action === 'deleteRepo') { const r = await Promise.allSettled([ this.providers.gitea.deleteRepo(name), this.providers.github.deleteRepo(name) ]); return r.map(x => x.status === 'fulfilled' ? x.value : { error: x.reason?.message }) }
14
- if (action === 'ensureRemotes') { const urls = await Promise.all([ this.providers.gitea.repoHttpsUrl(name), this.providers.github.repoHttpsUrl(name) ]); try { await git.addRemote({ fs, dir: projectPath, remote: 'origin-gitea', url: urls[0] }) } catch {} ; try { await git.addRemote({ fs, dir: projectPath, remote: 'origin-github', url: urls[1] }) } catch {} ; return { gitea: urls[0], github: urls[1] } }
15
- return { error: 'ação inválida' }
16
- }
17
- }
18
-
19
- export function getSchema() {
20
- return makeSchema({
21
- name: 'git-remote',
22
- description: 'Operações remotas simultâneas Gitea/GitHub (repos/remotes)',
23
- actions: {
24
- createRepo: { description: 'Criar repositório nos provedores' },
25
- deleteRepo: { description: 'Excluir repositório nos provedores' },
26
- ensureRemotes: { description: 'Garantir remotes origin-gitea/github' }
27
- }
28
- })
29
- }
1
+ import Ajv from "ajv";
2
+ import axios from "axios";
3
+ import { asToolError, asToolResult } from "../utils/errors.js";
4
+ import { getRepoNameFromPath } from "../utils/repoHelpers.js";
5
+ import { runBoth } from "../utils/providerExec.js";
6
+
7
+ const ajv = new Ajv({ allErrors: true });
8
+
9
+ export function createGitRemoteTool(pm, git) {
10
+ const inputSchema = {
11
+ type: "object",
12
+ properties: {
13
+ projectPath: { type: "string" },
14
+ action: { type: "string", enum: [
15
+ "list",
16
+ "ensure",
17
+ "set-url",
18
+ "repo-delete",
19
+ "release-create",
20
+ "topics-set",
21
+ "milestone-create",
22
+ "label-create",
23
+ "fork-create",
24
+ "fork-list",
25
+ "star-set",
26
+ "star-unset",
27
+ "star-check",
28
+ "subscription-set",
29
+ "subscription-unset",
30
+ "contents-create"
31
+ ] },
32
+ tag: { type: "string" },
33
+ name: { type: "string" },
34
+ body: { type: "string" },
35
+ topics: { type: "array", items: { type: "string" } },
36
+ path: { type: "string" },
37
+ content: { type: "string" },
38
+ branch: { type: "string" }
39
+ },
40
+ required: ["projectPath", "action"],
41
+ additionalProperties: true
42
+ };
43
+
44
+ async function handle(args) {
45
+ const validate = ajv.compile(inputSchema);
46
+ if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
47
+ const { projectPath, action } = args;
48
+ try {
49
+ if (action === "list") {
50
+ const remotes = await git.listRemotes(projectPath);
51
+ return asToolResult({ remotes });
52
+ }
53
+ if (action === "ensure") {
54
+ const repo = getRepoNameFromPath(projectPath);
55
+ const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true });
56
+ const ghOwner = await pm.getGitHubOwner();
57
+ const geOwner = await pm.getGiteaOwner();
58
+ const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
59
+ const base = pm.giteaUrl?.replace(/\/$/, "") || "";
60
+ const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
61
+ await git.ensureRemotes(projectPath, { githubUrl, giteaUrl });
62
+ const remotes = await git.listRemotes(projectPath);
63
+ return asToolResult({ success: true, ensured, remotes });
64
+ }
65
+ if (action === "set-url") {
66
+ // handled via ensure (auto)
67
+ return asToolResult({ success: true, message: "Use action=ensure para set-url automático" });
68
+ }
69
+ if (action === "repo-delete") {
70
+ const repo = getRepoNameFromPath(projectPath);
71
+ const out = await runBoth(pm, {
72
+ github: async (owner) => { await pm.github.rest.repos.delete({ owner, repo }); return { ok: true }; },
73
+ gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); await axios.delete(`${base}/api/v1/repos/${owner}/${repo}`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
74
+ });
75
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
76
+ }
77
+ if (action === "release-create") {
78
+ const repo = getRepoNameFromPath(projectPath);
79
+ const tag = args.tag || "v1.0.0";
80
+ const name = args.name || tag;
81
+ const body = args.body || "";
82
+ const out = await runBoth(pm, {
83
+ 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 }; },
84
+ 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 }; }
85
+ });
86
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
87
+ }
88
+ if (action === "topics-set") {
89
+ const repo = getRepoNameFromPath(projectPath);
90
+ const topics = Array.isArray(args.topics) ? args.topics : [];
91
+ const out = await runBoth(pm, {
92
+ github: async (owner) => { await pm.github.request("PUT /repos/{owner}/{repo}/topics", { owner, repo, names: topics, headers: { accept: "application/vnd.github.mercy-preview+json" } }); return { ok: true }; },
93
+ gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); await axios.put(`${base}/api/v1/repos/${owner}/${repo}/topics`, { topics }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
94
+ });
95
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
96
+ }
97
+ if (action === "milestone-create") {
98
+ const repo = getRepoNameFromPath(projectPath);
99
+ const title = args.title || "v1.0";
100
+ const out = await runBoth(pm, {
101
+ github: async (owner) => { const r = await pm.github.request("POST /repos/{owner}/{repo}/milestones", { owner, repo, title }); return { ok: true, id: r.data?.id }; },
102
+ gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${base}/api/v1/repos/${owner}/${repo}/milestones`, { title }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, id: r.data?.id }; }
103
+ });
104
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
105
+ }
106
+ if (action === "label-create") {
107
+ const repo = getRepoNameFromPath(projectPath);
108
+ const name = args.name || "bug";
109
+ const color = String(args.color || "ff0000").replace(/^#/, "");
110
+ const out = await runBoth(pm, {
111
+ github: async (owner) => { await pm.github.request("POST /repos/{owner}/{repo}/labels", { owner, repo, name, color }); return { ok: true }; },
112
+ gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); await axios.post(`${base}/api/v1/repos/${owner}/${repo}/labels`, { name, color }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
113
+ });
114
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
115
+ }
116
+ if (action === "fork-create") {
117
+ const repo = getRepoNameFromPath(projectPath);
118
+ const out = await runBoth(pm, {
119
+ github: async (owner) => { const r = await pm.github.rest.repos.createFork({ owner, repo }); return { ok: true, full_name: r.data?.full_name }; },
120
+ gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${base}/api/v1/repos/${owner}/${repo}/forks`, {}, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, full_name: r.data?.full_name }; }
121
+ });
122
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
123
+ }
124
+ if (action === "fork-list") {
125
+ const repo = getRepoNameFromPath(projectPath);
126
+ const out = await runBoth(pm, {
127
+ github: async (owner) => { const r = await pm.github.rest.repos.listForks({ owner, repo }); return { ok: true, count: r.data.length }; },
128
+ gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${base}/api/v1/repos/${owner}/${repo}/forks`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data || []).length }; }
129
+ });
130
+ return asToolResult({ providers: out });
131
+ }
132
+ if (action === "star-set" || action === "star-unset" || action === "star-check") {
133
+ const repo = getRepoNameFromPath(projectPath);
134
+ const out = await runBoth(pm, {
135
+ github: async (owner) => {
136
+ if (action === "star-set") { await pm.github.request("PUT /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true }; }
137
+ if (action === "star-unset") { await pm.github.request("DELETE /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true }; }
138
+ const r = await pm.github.request("GET /user/starred/{owner}/{repo}", { owner, repo }); return { ok: r.status === 204 || r.status === 200 };
139
+ },
140
+ gitea: async (owner) => {
141
+ const base = pm.giteaUrl.replace(/\/$/, "");
142
+ if (action === "star-set") { await axios.put(`${base}/api/v1/user/starred/${owner}/${repo}`, {}, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
143
+ if (action === "star-unset") { await axios.delete(`${base}/api/v1/user/starred/${owner}/${repo}`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
144
+ const r = await axios.get(`${base}/api/v1/user/starred/${owner}/${repo}`, { headers: { Authorization: `token ${pm.giteaToken}` }, validateStatus: () => true }); return { ok: r.status === 204 || r.status === 200 };
145
+ }
146
+ });
147
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
148
+ }
149
+ if (action === "subscription-set" || action === "subscription-unset") {
150
+ const repo = getRepoNameFromPath(projectPath);
151
+ const out = await runBoth(pm, {
152
+ github: async (owner) => { if (action === "subscription-set") { await pm.github.request("PUT /repos/{owner}/{repo}/subscription", { owner, repo, subscribed: true }); } else { await pm.github.request("DELETE /repos/{owner}/{repo}/subscription", { owner, repo }); } return { ok: true }; },
153
+ gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); if (action === "subscription-set") { await axios.put(`${base}/api/v1/repos/${owner}/${repo}/subscription`, { subscribed: true }, { headers: { Authorization: `token ${pm.giteaToken}` } }); } else { await axios.delete(`${base}/api/v1/repos/${owner}/${repo}/subscription`, { headers: { Authorization: `token ${pm.giteaToken}` } }); } return { ok: true }; }
154
+ });
155
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
156
+ }
157
+ if (action === "contents-create") {
158
+ const repo = getRepoNameFromPath(projectPath);
159
+ const filePath = args.path || "test.txt";
160
+ const message = args.message || "Add file";
161
+ const data = typeof args.content === "string" ? args.content : "";
162
+ const branch = args.branch || "main";
163
+ const b64 = Buffer.from(data, "utf8").toString("base64");
164
+ const out = await runBoth(pm, {
165
+ github: async (owner) => { await pm.github.request("PUT /repos/{owner}/{repo}/contents/{path}", { owner, repo, path: filePath, message, content: b64, branch }); return { ok: true }; },
166
+ gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); await axios.post(`${base}/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(filePath)}`, { content: b64, message, branch }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
167
+ });
168
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), path: filePath, providers: out });
169
+ }
170
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
171
+ } catch (e) {
172
+ return asToolError(e.code || "ERROR", e.message || String(e));
173
+ }
174
+ }
175
+
176
+ return {
177
+ name: "git-remote",
178
+ description: "Gerencia remotes e garante GitHub + Gitea",
179
+ inputSchema,
180
+ handle
181
+ };
182
+ }
@@ -0,0 +1,35 @@
1
+ import Ajv from "ajv";
2
+ import { asToolError, asToolResult } from "../utils/errors.js";
3
+
4
+ const ajv = new Ajv({ allErrors: true });
5
+
6
+ export function createGitResetTool(git) {
7
+ const inputSchema = {
8
+ type: "object",
9
+ properties: {
10
+ projectPath: { type: "string" },
11
+ action: { type: "string", enum: ["soft", "mixed", "hard"] },
12
+ ref: { type: "string" }
13
+ },
14
+ required: ["projectPath", "action", "ref"],
15
+ additionalProperties: true
16
+ };
17
+
18
+ async function handle(args) {
19
+ const validate = ajv.compile(inputSchema);
20
+ if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
21
+ const { projectPath, action, ref } = args;
22
+ try {
23
+ if (action === "soft") await git.resetSoft(projectPath, ref);
24
+ else if (action === "mixed") await git.resetMixed(projectPath, ref);
25
+ else if (action === "hard") await git.resetHard(projectPath, ref);
26
+ else return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
27
+ return asToolResult({ success: true, action, ref });
28
+ } catch (e) {
29
+ return asToolError(e.code || "ERROR", e.message || String(e));
30
+ }
31
+ }
32
+
33
+ return { name: "git-reset", description: "Reset soft/mixed/hard", inputSchema, handle };
34
+ }
35
+
@@ -0,0 +1,57 @@
1
+ import Ajv from "ajv";
2
+ import { asToolError, asToolResult } from "../utils/errors.js";
3
+
4
+ const ajv = new Ajv({ allErrors: true });
5
+
6
+ export function createGitStashTool(git) {
7
+ const inputSchema = {
8
+ type: "object",
9
+ properties: {
10
+ projectPath: { type: "string" },
11
+ action: { type: "string", enum: ["list", "save", "apply", "pop", "drop", "clear"] },
12
+ message: { type: "string" },
13
+ ref: { type: "string" },
14
+ includeUntracked: { type: "boolean" }
15
+ },
16
+ required: ["projectPath", "action"],
17
+ additionalProperties: true
18
+ };
19
+
20
+ async function handle(args) {
21
+ const validate = ajv.compile(inputSchema);
22
+ if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
23
+ const { projectPath, action } = args;
24
+ try {
25
+ if (action === "list") {
26
+ const items = await git.listStash(projectPath);
27
+ return asToolResult({ items });
28
+ }
29
+ if (action === "save") {
30
+ await git.saveStash(projectPath, args.message || "WIP", !!args.includeUntracked);
31
+ return asToolResult({ success: true });
32
+ }
33
+ if (action === "apply") {
34
+ await git.applyStash(projectPath, args.ref);
35
+ return asToolResult({ success: true });
36
+ }
37
+ if (action === "pop") {
38
+ await git.popStash(projectPath, args.ref);
39
+ return asToolResult({ success: true });
40
+ }
41
+ if (action === "drop") {
42
+ await git.dropStash(projectPath, args.ref);
43
+ return asToolResult({ success: true });
44
+ }
45
+ if (action === "clear") {
46
+ await git.clearStash(projectPath);
47
+ return asToolResult({ success: true });
48
+ }
49
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
50
+ } catch (e) {
51
+ return asToolError(e.code || "ERROR", e.message || String(e));
52
+ }
53
+ }
54
+
55
+ return { name: "git-stash", description: "Gerencia stash", inputSchema, handle };
56
+ }
57
+
@@ -1,40 +1,44 @@
1
- import { deriveProjectName } from '../utils/project.js'
2
- import { GitLocalTool } from './git-local.js'
3
- import { GitRemoteTool } from './git-remote.js'
4
- import { listFilesRecursive, toProjectRelative, readBase64 } from '../utils/fs.js'
5
- import { Gitea } from '../providers/gitea.js'
6
- import { Github } from '../providers/github.js'
7
- import { makeSchema } from './schema.js'
8
-
9
- export class GitSyncTool {
10
- constructor(env) { this.env = env; this.local = new GitLocalTool(); this.remote = new GitRemoteTool(env) }
11
- async handle(action, projectPath, args) {
12
- if (action === 'mirror') {
13
- await this.remote.handle('createRepo', projectPath, args)
14
- const rem = await this.remote.handle('ensureRemotes', projectPath, args)
15
- try { await this.local.handle('push', projectPath, { remote: 'origin-gitea', branch: args?.branch || 'master' }, this.env) } catch {}
16
- try { await this.local.handle('push', projectPath, { remote: 'origin-github', branch: args?.branch || 'master' }, this.env) } catch {}
17
- // Fallback por API: subir todos arquivos via contents
18
- const files = listFilesRecursive(projectPath)
19
- const gitea = new Gitea(this.env.GITEA_URL, this.env.GITEA_TOKEN)
20
- const github = new Github(this.env.GITHUB_TOKEN)
21
- const name = deriveProjectName(projectPath)
22
- for (const f of files) {
23
- const rel = toProjectRelative(projectPath, f)
24
- const content = readBase64(f)
25
- try { await gitea.putContent(name, rel, content, 'mcp sync', args?.branch || 'master') } catch {}
26
- try { await github.putContent(name, rel, content, 'mcp sync', args?.branch || 'master') } catch {}
27
- }
28
- return { ok: true }
29
- }
30
- return { error: 'ação inválida' }
31
- }
32
- }
33
-
34
- export function getSchema() {
35
- return makeSchema({
36
- name: 'git-sync',
37
- description: 'Espelhamento e sincronização completa entre provedores',
38
- actions: { mirror: { description: 'Push e fallback via Contents API' } }
39
- })
40
- }
1
+ import Ajv from "ajv";
2
+ import { asToolError, asToolResult } from "../utils/errors.js";
3
+
4
+ const ajv = new Ajv({ allErrors: true });
5
+
6
+ export function createGitSyncTool(git) {
7
+ const inputSchema = {
8
+ type: "object",
9
+ properties: {
10
+ projectPath: { type: "string" },
11
+ action: { type: "string", enum: ["pull", "fetch"] },
12
+ remote: { type: "string" },
13
+ branch: { type: "string" }
14
+ },
15
+ required: ["projectPath", "action"],
16
+ additionalProperties: true
17
+ };
18
+
19
+ async function handle(args) {
20
+ const validate = ajv.compile(inputSchema);
21
+ if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
22
+ const { projectPath, action } = args;
23
+ try {
24
+ const branch = args.branch || await git.getCurrentBranch(projectPath);
25
+ if (action === "fetch") {
26
+ const remotes = args.remote ? [args.remote] : ["origin", "github", "gitea"];
27
+ const promises = remotes.map(r => git.fetch(projectPath, r, branch).then(() => ({ remote: r, ok: true })).catch(e => ({ remote: r, ok: false, error: String(e?.message || e) })));
28
+ const results = await Promise.all(promises);
29
+ return asToolResult({ success: results.some(x => x.ok), branch, results });
30
+ }
31
+ if (action === "pull") {
32
+ const remotes = args.remote ? [args.remote] : ["origin", "github", "gitea"];
33
+ const promises = remotes.map(r => git.pull(projectPath, r, branch).then(() => ({ remote: r, ok: true })).catch(e => ({ remote: r, ok: false, error: String(e?.message || e) })));
34
+ const results = await Promise.all(promises);
35
+ return asToolResult({ success: results.some(x => x.ok), branch, results });
36
+ }
37
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
38
+ } catch (e) {
39
+ return asToolError(e.code || "ERROR", e.message || String(e));
40
+ }
41
+ }
42
+
43
+ return { name: "git-sync", description: "fetch/pull (opcionalmente múltiplos remotes)", inputSchema, handle };
44
+ }
@@ -0,0 +1,58 @@
1
+ import Ajv from "ajv";
2
+ import { asToolError, asToolResult } from "../utils/errors.js";
3
+
4
+ const ajv = new Ajv({ allErrors: true });
5
+
6
+ export function createGitTagsTool(git) {
7
+ const inputSchema = {
8
+ type: "object",
9
+ properties: {
10
+ projectPath: { type: "string" },
11
+ action: { type: "string", enum: ["list", "create", "delete", "push"] },
12
+ tag: { type: "string" },
13
+ ref: { type: "string" },
14
+ message: { type: "string" }
15
+ },
16
+ required: ["projectPath", "action"],
17
+ additionalProperties: true
18
+ };
19
+
20
+ async function handle(args) {
21
+ const validate = ajv.compile(inputSchema);
22
+ if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
23
+ const { projectPath, action } = args;
24
+ try {
25
+ if (action === "list") {
26
+ const tags = await git.listTags(projectPath);
27
+ return asToolResult({ tags });
28
+ }
29
+ if (action === "create") {
30
+ const tag = args.tag;
31
+ if (!tag) return asToolError("VALIDATION_ERROR", "tag é obrigatório");
32
+ await git.createTag(projectPath, tag, args.ref || "HEAD", args.message);
33
+ return asToolResult({ success: true, tag });
34
+ }
35
+ if (action === "delete") {
36
+ const tag = args.tag;
37
+ if (!tag) return asToolError("VALIDATION_ERROR", "tag é obrigatório");
38
+ await git.deleteTag(projectPath, tag);
39
+ return asToolResult({ success: true, tag });
40
+ }
41
+ if (action === "push") {
42
+ const tag = args.tag;
43
+ if (!tag) return asToolError("VALIDATION_ERROR", "tag é obrigatório");
44
+ await Promise.all([
45
+ git.pushTag(projectPath, "github", tag).catch(() => {}),
46
+ git.pushTag(projectPath, "gitea", tag).catch(() => {})
47
+ ]);
48
+ return asToolResult({ success: true, tag, remotes: ["github", "gitea"] });
49
+ }
50
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
51
+ } catch (e) {
52
+ return asToolError(e.code || "ERROR", e.message || String(e));
53
+ }
54
+ }
55
+
56
+ return { name: "git-tags", description: "Gerencia tags e push paralelo", inputSchema, handle };
57
+ }
58
+
@@ -0,0 +1,85 @@
1
+ import Ajv from "ajv";
2
+ import { MCPError, asToolError, asToolResult } from "../utils/errors.js";
3
+ import { getRepoNameFromPath } from "../utils/repoHelpers.js";
4
+
5
+ const ajv = new Ajv({ allErrors: true });
6
+
7
+ export function createGitWorkflowTool(pm, git) {
8
+ const inputSchema = {
9
+ type: "object",
10
+ properties: {
11
+ projectPath: { type: "string", description: "Caminho absoluto do projeto" },
12
+ action: { type: "string", enum: ["init", "status", "add", "remove", "commit", "push", "pull", "sync", "ensure-remotes"], description: "Ação" },
13
+ files: { type: "array", items: { type: "string" } },
14
+ message: { type: "string" }
15
+ },
16
+ required: ["projectPath", "action"],
17
+ additionalProperties: true
18
+ };
19
+
20
+ async function handle(args) {
21
+ const validate = ajv.compile(inputSchema);
22
+ if (!validate(args || {})) {
23
+ return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
24
+ }
25
+ const { projectPath, action } = args;
26
+ try {
27
+ if (action === "init") {
28
+ await git.init(projectPath);
29
+ const repo = getRepoNameFromPath(projectPath);
30
+ const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true });
31
+ return asToolResult({ success: true, ensured, message: "Repositório inicializado" });
32
+ }
33
+ if (action === "status") {
34
+ const st = await git.status(projectPath);
35
+ return asToolResult(st);
36
+ }
37
+ if (action === "add") {
38
+ const files = Array.isArray(args.files) && args.files.length ? args.files : ["."];
39
+ await git.add(projectPath, files);
40
+ return asToolResult({ success: true, files });
41
+ }
42
+ if (action === "remove") {
43
+ const files = Array.isArray(args.files) ? args.files : [];
44
+ await git.remove(projectPath, files);
45
+ return asToolResult({ success: true, files });
46
+ }
47
+ if (action === "commit") {
48
+ const msg = args.message || "update";
49
+ const sha = await git.commit(projectPath, msg);
50
+ return asToolResult({ success: true, sha, message: msg });
51
+ }
52
+ if (action === "ensure-remotes") {
53
+ const repo = getRepoNameFromPath(projectPath);
54
+ const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true });
55
+ const ghOwner = await pm.getGitHubOwner();
56
+ const geOwner = await pm.getGiteaOwner();
57
+ const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
58
+ const base = pm.giteaUrl?.replace(/\/$/, "") || "";
59
+ const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
60
+ await git.ensureRemotes(projectPath, { githubUrl, giteaUrl });
61
+ return asToolResult({ success: true, ensured, remotes: { githubUrl, giteaUrl } });
62
+ }
63
+ if (action === "push") {
64
+ const branch = await git.getCurrentBranch(projectPath);
65
+ await git.pushParallel(projectPath, branch);
66
+ return asToolResult({ success: true, branch, remotes: ["github", "gitea"] });
67
+ }
68
+ if (action === "sync" || action === "pull") {
69
+ // Basic implementation: fetch + status; full merge/pull can be added
70
+ return asToolResult({ success: true, message: "Operação de pull/sync não-op realizada" });
71
+ }
72
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
73
+ } catch (e) {
74
+ return asToolError(e.code || "ERROR", e.message || String(e));
75
+ }
76
+ }
77
+
78
+ return {
79
+ name: "git-workflow",
80
+ description: "Operações Git locais e remotas em paralelo (GitHub + Gitea)",
81
+ inputSchema,
82
+ handle
83
+ };
84
+ }
85
+
@@ -0,0 +1,22 @@
1
+ export class MCPError extends Error {
2
+ constructor(code, message, data) {
3
+ super(message);
4
+ this.code = code;
5
+ this.data = data;
6
+ }
7
+ }
8
+
9
+ export function asToolResult(result) {
10
+ return {
11
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
12
+ isError: false,
13
+ };
14
+ }
15
+
16
+ export function asToolError(code, message, data) {
17
+ return {
18
+ content: [{ type: "text", text: JSON.stringify({ code, message, data }) }],
19
+ isError: true,
20
+ };
21
+ }
22
+