@imenam/mcp-github 1.1.45
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/LICENSE +15 -0
- package/README.md +154 -0
- package/dist/config-server.d.ts +6 -0
- package/dist/config-server.d.ts.map +1 -0
- package/dist/config-server.js +465 -0
- package/dist/config-server.js.map +1 -0
- package/dist/gui.d.ts +3 -0
- package/dist/gui.d.ts.map +1 -0
- package/dist/gui.js +26 -0
- package/dist/gui.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +687 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +59 -0
- package/dist/logger.js.map +1 -0
- package/dist/utils/config.d.ts +109 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +349 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/ssh.d.ts +16 -0
- package/dist/utils/ssh.d.ts.map +1 -0
- package/dist/utils/ssh.js +53 -0
- package/dist/utils/ssh.js.map +1 -0
- package/package.json +41 -0
- package/public/index.html +996 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { Octokit } from "octokit";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import os from "os";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { fork } from "child_process";
|
|
12
|
+
import { logToFile, setupLogging } from "./logger.js";
|
|
13
|
+
import { startConfigServer } from "./config-server.js";
|
|
14
|
+
import { getConfig, updateEnvFile, setupIpcErrorHandlers } from "./utils/config.js";
|
|
15
|
+
import { assertSshConfig, execRemote } from "./utils/ssh.js";
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
// Initialisation du logging global
|
|
19
|
+
setupLogging("MASTER");
|
|
20
|
+
// Initialisation des handlers IPC
|
|
21
|
+
setupIpcErrorHandlers();
|
|
22
|
+
// ... plus d'import direct de dotenv.config, c'est géré par getConfig()
|
|
23
|
+
/**
|
|
24
|
+
* État du processus GUI
|
|
25
|
+
*/
|
|
26
|
+
let currentGuiProcess = null;
|
|
27
|
+
/**
|
|
28
|
+
* Instance Octokit dynamique
|
|
29
|
+
*/
|
|
30
|
+
let _octokit = null;
|
|
31
|
+
/**
|
|
32
|
+
* Cache du dernier résultat `test` (en mémoire, perdu au redémarrage)
|
|
33
|
+
*/
|
|
34
|
+
let _lastTestResult = null;
|
|
35
|
+
const getOctokit = () => {
|
|
36
|
+
const { GITHUB_TOKEN } = getConfig();
|
|
37
|
+
if (!GITHUB_TOKEN)
|
|
38
|
+
return null;
|
|
39
|
+
if (!_octokit) {
|
|
40
|
+
_octokit = new Octokit({ auth: GITHUB_TOKEN });
|
|
41
|
+
}
|
|
42
|
+
return _octokit;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Lance le serveur de configuration dans un processus séparé
|
|
46
|
+
*/
|
|
47
|
+
function spawnGuiProcess() {
|
|
48
|
+
const guiPath = path.join(__dirname, "gui.js");
|
|
49
|
+
logToFile(`[MASTER] Spawning GUI process: ${guiPath}`);
|
|
50
|
+
currentGuiProcess = fork(guiPath, [], {
|
|
51
|
+
stdio: ['inherit', 'pipe', 'inherit', 'ipc'],
|
|
52
|
+
env: process.env
|
|
53
|
+
});
|
|
54
|
+
// Capturer stdout du fils pour le rediriger vers stderr (protection MCP)
|
|
55
|
+
if (currentGuiProcess.stdout) {
|
|
56
|
+
currentGuiProcess.stdout.on('data', (data) => {
|
|
57
|
+
process.stderr.write(`[GUI-STDOUT] ${data}`);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
currentGuiProcess.on('error', (err) => {
|
|
61
|
+
logToFile(`[MASTER] Failed to spawn GUI process: ${err.message}`);
|
|
62
|
+
});
|
|
63
|
+
currentGuiProcess.on('exit', (code) => {
|
|
64
|
+
logToFile(`[MASTER] GUI process exited with code ${code}.`);
|
|
65
|
+
currentGuiProcess = null;
|
|
66
|
+
if (code !== 0 && code !== null) {
|
|
67
|
+
logToFile(`[MASTER] GUI exited with error — no automatic restart.`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
currentGuiProcess.on('message', (msg) => {
|
|
71
|
+
if (msg && msg.type === 'RESTART') {
|
|
72
|
+
logToFile("[MASTER] Restart signal received from GUI. Exiting main process...");
|
|
73
|
+
setTimeout(() => process.exit(0), 500);
|
|
74
|
+
}
|
|
75
|
+
if (msg && msg.type === 'STOP') {
|
|
76
|
+
logToFile("[MASTER] Stop signal received from GUI. Shutting down everything...");
|
|
77
|
+
setTimeout(() => process.exit(0), 500);
|
|
78
|
+
}
|
|
79
|
+
if (msg && msg.type === 'CONFIG_UPDATED') {
|
|
80
|
+
logToFile("[MASTER] Config update signal received from GUI.");
|
|
81
|
+
// On fusionne la nouvelle config dans l'environnement du Master
|
|
82
|
+
Object.assign(process.env, msg.config);
|
|
83
|
+
// On force la recréation d'Octokit au prochain appel
|
|
84
|
+
_octokit = null;
|
|
85
|
+
logToFile("[MASTER] Local process.env updated.");
|
|
86
|
+
}
|
|
87
|
+
if (msg && msg.type === 'CLONE') {
|
|
88
|
+
logToFile("[MASTER] Clone signal received from GUI.");
|
|
89
|
+
performClone()
|
|
90
|
+
.then((result) => {
|
|
91
|
+
logToFile(`[MASTER] GUI Clone successful: ${result}`);
|
|
92
|
+
})
|
|
93
|
+
.catch((err) => {
|
|
94
|
+
logToFile(`[MASTER] GUI Clone failed: ${err.message}`);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return currentGuiProcess;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Sécurise un nom de branche pour l'utiliser comme nom de dossier
|
|
102
|
+
*/
|
|
103
|
+
function sanitizeBranchName(branch) {
|
|
104
|
+
// Remplace les caractères interdits par des tirets
|
|
105
|
+
// Caractères : / \ : * ? " < > | et les espaces
|
|
106
|
+
return branch.replace(/[\/\x5C\x3A\x2A\x3F\x22\x3C\x3E\x7C\s]+/g, "-")
|
|
107
|
+
.replace(/^-+|-+$/g, ""); // Nettoie les tirets au début et à la fin
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Génère le header d'authentification Basic pour Git
|
|
111
|
+
*/
|
|
112
|
+
function getGitAuthHeader() {
|
|
113
|
+
const { GITHUB_TOKEN } = getConfig();
|
|
114
|
+
if (!GITHUB_TOKEN)
|
|
115
|
+
return null;
|
|
116
|
+
const auth = Buffer.from(`x-access-token:${GITHUB_TOKEN}`).toString('base64');
|
|
117
|
+
return `Authorization: Basic ${auth}`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Exécute une commande git avec authentification temporaire via header
|
|
121
|
+
*/
|
|
122
|
+
function execGitSecure(command, options) {
|
|
123
|
+
const authHeader = getGitAuthHeader();
|
|
124
|
+
if (!authHeader) {
|
|
125
|
+
return execSync(command, options);
|
|
126
|
+
}
|
|
127
|
+
// On utilise -c http.extraHeader pour passer le token de manière éphémère
|
|
128
|
+
const secureCommand = `git -c http.extraHeader="${authHeader}" ${command.startsWith('git ') ? command.slice(4) : command}`;
|
|
129
|
+
try {
|
|
130
|
+
return execSync(secureCommand, options);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
// On masque le token dans l'erreur si jamais il apparaît
|
|
134
|
+
if (authHeader) {
|
|
135
|
+
error.message = error.message.replace(authHeader, "[REDACTED_AUTH_HEADER]");
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Effectue le clonage et la configuration initiale du dépôt
|
|
142
|
+
*/
|
|
143
|
+
async function performClone() {
|
|
144
|
+
const config = getConfig();
|
|
145
|
+
const octokit = getOctokit();
|
|
146
|
+
if (!config.GITHUB_TOKEN) {
|
|
147
|
+
throw new Error("GITHUB_TOKEN est manquant. Le clonage nécessite une authentification.");
|
|
148
|
+
}
|
|
149
|
+
if (!config.TARGET_REPO || !config.TARGET_REPO.includes("/")) {
|
|
150
|
+
throw new Error("TARGET_REPO must be defined as 'owner/repo'");
|
|
151
|
+
}
|
|
152
|
+
if (!config.TARGET_BRANCH) {
|
|
153
|
+
throw new Error("TARGET_BRANCH environment variable is not defined");
|
|
154
|
+
}
|
|
155
|
+
const branchName = config.TARGET_BRANCH;
|
|
156
|
+
const repoParts = config.TARGET_REPO.split("/");
|
|
157
|
+
const owner = repoParts[0];
|
|
158
|
+
const repo = repoParts[1];
|
|
159
|
+
if (!owner || !repo) {
|
|
160
|
+
throw new Error("Invalid TARGET_REPO format. Expected 'owner/repo'");
|
|
161
|
+
}
|
|
162
|
+
const remote = `https://github.com/${config.TARGET_REPO}.git`;
|
|
163
|
+
const sanitizedBranch = sanitizeBranchName(branchName);
|
|
164
|
+
const finalRepoPath = path.join(config.CLONE_DIR, sanitizedBranch);
|
|
165
|
+
logToFile(`[CLONE] Destination calculée : ${finalRepoPath}`);
|
|
166
|
+
try {
|
|
167
|
+
// S'assurer que le dossier parent existe
|
|
168
|
+
const parentDir = path.dirname(finalRepoPath);
|
|
169
|
+
if (!fs.existsSync(parentDir)) {
|
|
170
|
+
logToFile(`Creating parent directory: ${parentDir}`);
|
|
171
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
172
|
+
}
|
|
173
|
+
// 1. Clone into the configured path
|
|
174
|
+
logToFile(`Cloning ${remote} into ${finalRepoPath}...`);
|
|
175
|
+
execGitSecure(`clone ${remote} "${finalRepoPath}"`, {});
|
|
176
|
+
// Configurer l'identité de l'agent automatiquement après le clone
|
|
177
|
+
logToFile(`[CLONE] Configuration de l'identité Git (Agent Zero)...`);
|
|
178
|
+
execGitSecure(`config user.name "Agent Zero"`, { cwd: finalRepoPath });
|
|
179
|
+
execGitSecure(`config user.email "agentzero@arakei.net"`, { cwd: finalRepoPath });
|
|
180
|
+
// 2. Check if branch exists on remote
|
|
181
|
+
let branchExistsOnRemote = false;
|
|
182
|
+
try {
|
|
183
|
+
if (octokit) {
|
|
184
|
+
await octokit.rest.repos.getBranch({
|
|
185
|
+
owner,
|
|
186
|
+
repo,
|
|
187
|
+
branch: branchName,
|
|
188
|
+
});
|
|
189
|
+
branchExistsOnRemote = true;
|
|
190
|
+
logToFile(`Branch ${branchName} found on remote.`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
if (e.status === 404) {
|
|
195
|
+
logToFile(`Branch ${branchName} NOT found on remote.`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
logToFile(`Error checking branch existence: ${e.message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (branchExistsOnRemote) {
|
|
202
|
+
// Checkout existing branch
|
|
203
|
+
const currentBranch = execGitSecure("branch --show-current", { cwd: finalRepoPath }).toString().trim();
|
|
204
|
+
if (currentBranch !== branchName) {
|
|
205
|
+
logToFile(`Checking out existing branch ${branchName}...`);
|
|
206
|
+
execGitSecure(`checkout ${branchName}`, { cwd: finalRepoPath });
|
|
207
|
+
}
|
|
208
|
+
// Pull latest changes to be sure we are up to date
|
|
209
|
+
logToFile(`Pulling latest changes for ${branchName}...`);
|
|
210
|
+
execGitSecure(`pull origin ${branchName}`, { cwd: finalRepoPath });
|
|
211
|
+
// On met à jour l'environnement ET on persiste dans le .env
|
|
212
|
+
process.env.REPO_PATH = finalRepoPath;
|
|
213
|
+
await updateEnvFile("REPO_PATH", finalRepoPath);
|
|
214
|
+
return `Successfully cloned ${config.TARGET_REPO} into ${finalRepoPath} and updated existing branch ${branchName}`;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
// Create and push the branch to remote
|
|
218
|
+
logToFile(`Creating and pushing new branch ${branchName}...`);
|
|
219
|
+
execGitSecure(`checkout -b ${branchName}`, { cwd: finalRepoPath });
|
|
220
|
+
execGitSecure(`push -u origin ${branchName}`, { cwd: finalRepoPath });
|
|
221
|
+
// On met à jour l'environnement ET on persiste dans le .env
|
|
222
|
+
process.env.REPO_PATH = finalRepoPath;
|
|
223
|
+
await updateEnvFile("REPO_PATH", finalRepoPath);
|
|
224
|
+
return `Successfully cloned ${config.TARGET_REPO} into ${finalRepoPath}, created and pushed new branch ${branchName} to remote`;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
const msg = `Clone or branch operation failed: ${error.message}`;
|
|
229
|
+
logToFile(msg);
|
|
230
|
+
throw new Error(msg);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Lancement de la GUI dans un processus SÉPARÉ
|
|
234
|
+
if (!process.env.PROXY_URL) {
|
|
235
|
+
console.error('[MASTER] PROXY_URL non défini — GUI désactivée.');
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
spawnGuiProcess();
|
|
239
|
+
}
|
|
240
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
|
|
241
|
+
const VERSION = pkg.version;
|
|
242
|
+
// Handle --version flag without exiting
|
|
243
|
+
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
244
|
+
console.error(`Restricted GitHub MCP Version: ${VERSION}`);
|
|
245
|
+
}
|
|
246
|
+
// MCP servers must not log to stdout as it's used for the protocol.
|
|
247
|
+
// Some libraries might log to stdout on initialization.
|
|
248
|
+
// We redirect all console.log to console.error to avoid polluting stdout.
|
|
249
|
+
// Global error handlers to catch silent crashes
|
|
250
|
+
process.on('uncaughtException', (error) => {
|
|
251
|
+
const msg = `FATAL UNCAUGHT EXCEPTION [PID ${process.pid}]: ${error.message}\nStack: ${error.stack}`;
|
|
252
|
+
console.error(msg);
|
|
253
|
+
logToFile(msg);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
});
|
|
256
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
257
|
+
const msg = `FATAL UNHANDLED REJECTION [PID ${process.pid}]: ${reason}\nPromise: ${promise}`;
|
|
258
|
+
console.error(msg);
|
|
259
|
+
logToFile(msg);
|
|
260
|
+
});
|
|
261
|
+
process.on('exit', (code) => {
|
|
262
|
+
logToFile(`PROCESS EXITING [PID ${process.pid}] with code: ${code}`);
|
|
263
|
+
});
|
|
264
|
+
['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach(signal => {
|
|
265
|
+
process.on(signal, () => {
|
|
266
|
+
logToFile(`PROCESS RECEIVED SIGNAL ${signal} [PID ${process.pid}]`);
|
|
267
|
+
process.exit(0);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
logToFile(`--- Server starting ---`);
|
|
271
|
+
logToFile(`PID: ${process.pid}`);
|
|
272
|
+
logToFile(`Arguments: ${JSON.stringify(process.argv)}`);
|
|
273
|
+
logToFile(`Working directory: ${process.cwd()}`);
|
|
274
|
+
// Add a heartbeat to monitor life
|
|
275
|
+
setInterval(() => {
|
|
276
|
+
logToFile("HEARTBEAT: Server is still alive");
|
|
277
|
+
}, 30000);
|
|
278
|
+
// Capture when stdin (the MCP pipe) is closed
|
|
279
|
+
process.stdin.on('close', () => {
|
|
280
|
+
logToFile("STDIN CLOSED: The MCP client has disconnected.");
|
|
281
|
+
process.exit(0);
|
|
282
|
+
});
|
|
283
|
+
// On ne bloque plus le démarrage si le token est manquant
|
|
284
|
+
if (!getConfig().GITHUB_TOKEN) {
|
|
285
|
+
const msg = "⚠️ GITHUB_TOKEN manquant. Le serveur MCP est en attente de configuration via http://localhost:3000";
|
|
286
|
+
console.error(msg);
|
|
287
|
+
logToFile(msg);
|
|
288
|
+
}
|
|
289
|
+
const server = new Server({
|
|
290
|
+
name: "@imenam/mcp-github",
|
|
291
|
+
version: "1.0.0",
|
|
292
|
+
}, {
|
|
293
|
+
capabilities: {
|
|
294
|
+
tools: {},
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
const TOOLS = [
|
|
298
|
+
{
|
|
299
|
+
name: "commit",
|
|
300
|
+
description: "Stage all changes (git add .) and commit with the provided message. Does NOT push to remote.",
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: "object",
|
|
303
|
+
properties: {
|
|
304
|
+
message: { type: "string", description: "The commit message" },
|
|
305
|
+
},
|
|
306
|
+
required: ["message"],
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: "push",
|
|
311
|
+
description: "Push the current branch to the remote origin using the configured TARGET_BRANCH. Does NOT add or commit changes.",
|
|
312
|
+
inputSchema: {
|
|
313
|
+
type: "object",
|
|
314
|
+
properties: {},
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: "commit_and_push",
|
|
319
|
+
description: "Automated workflow: adds all local changes (including untracked), commits with a message, and pushes to the branch defined in TARGET_BRANCH",
|
|
320
|
+
inputSchema: {
|
|
321
|
+
type: "object",
|
|
322
|
+
properties: {
|
|
323
|
+
message: { type: "string", description: "The commit message" },
|
|
324
|
+
},
|
|
325
|
+
required: ["message"],
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "pull_request",
|
|
330
|
+
description: "Create a pull request from the TARGET_BRANCH to the BASE_BRANCH",
|
|
331
|
+
inputSchema: {
|
|
332
|
+
type: "object",
|
|
333
|
+
properties: {
|
|
334
|
+
title: { type: "string", description: "The title of the pull request" },
|
|
335
|
+
body: { type: "string", description: "The body/description of the pull request" },
|
|
336
|
+
},
|
|
337
|
+
required: ["title"],
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "get_info",
|
|
342
|
+
description: "Get the current configuration of the server (excluding sensitive tokens)",
|
|
343
|
+
inputSchema: {
|
|
344
|
+
type: "object",
|
|
345
|
+
properties: {},
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
name: "clone",
|
|
350
|
+
description: "Clone the repository defined in TARGET_REPO into a subdirectory (named after TARGET_BRANCH) inside the CLONE_DIR directory configured in the environment.",
|
|
351
|
+
inputSchema: {
|
|
352
|
+
type: "object",
|
|
353
|
+
properties: {},
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: "get_logs",
|
|
358
|
+
description: "Retrieve the last N lines from the server log file",
|
|
359
|
+
inputSchema: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
lines: { type: "number", description: "Number of lines to retrieve (default: 50)", default: 50 },
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: "deploy",
|
|
368
|
+
description: "Sur le serveur distant configuré (SSH_HOST), exécute 'git fetch --all', 'git checkout TARGET_BRANCH' et 'git pull' dans SSH_REMOTE_PATH. Le repo doit déjà être cloné sur le serveur.",
|
|
369
|
+
inputSchema: {
|
|
370
|
+
type: "object",
|
|
371
|
+
properties: {},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: "test",
|
|
376
|
+
description: "Exécute TEST_COMMAND sur le serveur distant (cwd = SSH_REMOTE_PATH) et renvoie stdout, stderr et exit code. Stocke la sortie pour 'test_logs'.",
|
|
377
|
+
inputSchema: {
|
|
378
|
+
type: "object",
|
|
379
|
+
properties: {},
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
name: "test_logs",
|
|
384
|
+
description: "Renvoie la sortie (stdout + stderr + exit code) du dernier 'test' exécuté, sans relancer la commande.",
|
|
385
|
+
inputSchema: {
|
|
386
|
+
type: "object",
|
|
387
|
+
properties: {},
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
];
|
|
391
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
392
|
+
tools: TOOLS,
|
|
393
|
+
}));
|
|
394
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
395
|
+
const { name, arguments: args } = request.params;
|
|
396
|
+
const config = getConfig();
|
|
397
|
+
try {
|
|
398
|
+
logToFile(`Executing tool: ${name}`);
|
|
399
|
+
switch (name) {
|
|
400
|
+
case "commit": {
|
|
401
|
+
if (config.READ_ONLY)
|
|
402
|
+
throw new Error("Server is in read-only mode");
|
|
403
|
+
const { message } = args;
|
|
404
|
+
const finalRepoPath = config.REPO_PATH;
|
|
405
|
+
if (!finalRepoPath) {
|
|
406
|
+
throw new Error("Repository path is not defined. You must call 'clone' first.");
|
|
407
|
+
}
|
|
408
|
+
const execOptions = { cwd: finalRepoPath };
|
|
409
|
+
try {
|
|
410
|
+
// 1. Add all changes (including untracked)
|
|
411
|
+
execGitSecure("git add .", execOptions);
|
|
412
|
+
// 2. Commit
|
|
413
|
+
try {
|
|
414
|
+
execGitSecure(`git commit -m "${message}"`, execOptions);
|
|
415
|
+
}
|
|
416
|
+
catch (e) {
|
|
417
|
+
// Check if there was nothing to commit
|
|
418
|
+
const status = execGitSecure("git status --porcelain", execOptions).toString();
|
|
419
|
+
if (status.length === 0) {
|
|
420
|
+
return {
|
|
421
|
+
content: [{ type: "text", text: "Nothing to commit, working tree clean." }],
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
throw e;
|
|
425
|
+
}
|
|
426
|
+
const successMsg = `Successfully committed all changes with message: "${message}"`;
|
|
427
|
+
logToFile(successMsg);
|
|
428
|
+
return {
|
|
429
|
+
content: [{ type: "text", text: successMsg }],
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
const msg = `Git commit failed at ${finalRepoPath}: ${error.message}`;
|
|
434
|
+
logToFile(msg);
|
|
435
|
+
throw new Error(msg);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
case "push": {
|
|
439
|
+
if (config.READ_ONLY)
|
|
440
|
+
throw new Error("Server is in read-only mode");
|
|
441
|
+
const finalRepoPath = config.REPO_PATH;
|
|
442
|
+
if (!finalRepoPath) {
|
|
443
|
+
throw new Error("Repository path is not defined. You must call 'clone' first.");
|
|
444
|
+
}
|
|
445
|
+
if (!config.TARGET_BRANCH) {
|
|
446
|
+
throw new Error("TARGET_BRANCH environment variable is not defined");
|
|
447
|
+
}
|
|
448
|
+
const execOptions = { cwd: finalRepoPath };
|
|
449
|
+
try {
|
|
450
|
+
const remote = config.TARGET_REPO
|
|
451
|
+
? `https://github.com/${config.TARGET_REPO}.git`
|
|
452
|
+
: "origin";
|
|
453
|
+
execGitSecure(`push ${remote} HEAD:${config.TARGET_BRANCH}`, execOptions);
|
|
454
|
+
const successMsg = `Successfully pushed to ${config.TARGET_BRANCH} from ${finalRepoPath}`;
|
|
455
|
+
logToFile(successMsg);
|
|
456
|
+
return {
|
|
457
|
+
content: [{ type: "text", text: successMsg }],
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
const msg = `Git push failed at ${finalRepoPath}: ${error.message}`;
|
|
462
|
+
logToFile(msg);
|
|
463
|
+
throw new Error(msg);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
case "commit_and_push": {
|
|
467
|
+
if (config.READ_ONLY)
|
|
468
|
+
throw new Error("Server is in read-only mode");
|
|
469
|
+
const { message } = args;
|
|
470
|
+
const finalRepoPath = config.REPO_PATH;
|
|
471
|
+
if (!finalRepoPath) {
|
|
472
|
+
throw new Error("Repository path is not defined. You must call 'clone' first.");
|
|
473
|
+
}
|
|
474
|
+
const execOptions = { cwd: finalRepoPath };
|
|
475
|
+
if (!config.TARGET_BRANCH) {
|
|
476
|
+
throw new Error("TARGET_BRANCH environment variable is not defined");
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
// 1. Add all changes (including untracked)
|
|
480
|
+
execGitSecure("git add .", execOptions);
|
|
481
|
+
// 2. Commit
|
|
482
|
+
try {
|
|
483
|
+
execGitSecure(`git commit -m "${message}"`, execOptions);
|
|
484
|
+
}
|
|
485
|
+
catch (e) {
|
|
486
|
+
// Check if there was nothing to commit
|
|
487
|
+
const status = execGitSecure("git status --porcelain", execOptions).toString();
|
|
488
|
+
if (status.length === 0) {
|
|
489
|
+
return {
|
|
490
|
+
content: [{ type: "text", text: "Nothing to commit, working tree clean." }],
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
throw e;
|
|
494
|
+
}
|
|
495
|
+
// 3. Push to target branch
|
|
496
|
+
const remote = config.TARGET_REPO
|
|
497
|
+
? `https://github.com/${config.TARGET_REPO}.git`
|
|
498
|
+
: "origin";
|
|
499
|
+
execGitSecure(`push ${remote} HEAD:${config.TARGET_BRANCH}`, execOptions);
|
|
500
|
+
const successMsg = `Successfully committed and pushed all changes to branch ${config.TARGET_BRANCH} from ${finalRepoPath}`;
|
|
501
|
+
logToFile(successMsg);
|
|
502
|
+
return {
|
|
503
|
+
content: [{ type: "text", text: successMsg }],
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
const msg = `Git operation failed at ${finalRepoPath}: ${error.message}`;
|
|
508
|
+
logToFile(msg);
|
|
509
|
+
throw new Error(msg);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
case "pull_request": {
|
|
513
|
+
const octokit = getOctokit();
|
|
514
|
+
if (!config.GITHUB_TOKEN) {
|
|
515
|
+
throw new Error("GITHUB_TOKEN est manquant. Veuillez le configurer via l'interface graphique.");
|
|
516
|
+
}
|
|
517
|
+
if (config.READ_ONLY)
|
|
518
|
+
throw new Error("Server is in read-only mode");
|
|
519
|
+
const { title, body } = args;
|
|
520
|
+
if (!config.TARGET_REPO || !config.TARGET_REPO.includes("/")) {
|
|
521
|
+
throw new Error("TARGET_REPO must be defined as 'owner/repo'");
|
|
522
|
+
}
|
|
523
|
+
if (!config.TARGET_BRANCH) {
|
|
524
|
+
throw new Error("TARGET_BRANCH is not defined");
|
|
525
|
+
}
|
|
526
|
+
const [owner, repo] = config.TARGET_REPO.split("/");
|
|
527
|
+
if (!owner || !repo) {
|
|
528
|
+
throw new Error("Invalid TARGET_REPO format. Expected 'owner/repo'");
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
if (!octokit)
|
|
532
|
+
throw new Error("Octokit non initialisé");
|
|
533
|
+
const response = await octokit.rest.pulls.create({
|
|
534
|
+
owner,
|
|
535
|
+
repo,
|
|
536
|
+
title,
|
|
537
|
+
body,
|
|
538
|
+
head: config.TARGET_BRANCH,
|
|
539
|
+
base: config.BASE_BRANCH,
|
|
540
|
+
});
|
|
541
|
+
const successMsg = `Pull request created successfully: ${response.data.html_url}`;
|
|
542
|
+
logToFile(successMsg);
|
|
543
|
+
return {
|
|
544
|
+
content: [{ type: "text", text: successMsg }],
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
const msg = `Failed to create pull request: ${error.message}`;
|
|
549
|
+
logToFile(msg);
|
|
550
|
+
throw new Error(msg);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
case "get_logs": {
|
|
554
|
+
const { lines = 50 } = args;
|
|
555
|
+
const logDir = process.platform === 'win32'
|
|
556
|
+
? 'C:\\var\\log\\restricted-github-mcp'
|
|
557
|
+
: '/var/log/restricted-github-mcp';
|
|
558
|
+
const logFile = path.join(logDir, 'server.log');
|
|
559
|
+
if (!fs.existsSync(logFile)) {
|
|
560
|
+
return {
|
|
561
|
+
content: [{ type: "text", text: "Log file does not exist yet." }],
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
const content = fs.readFileSync(logFile, 'utf8');
|
|
566
|
+
const allLines = content.split('\n');
|
|
567
|
+
const lastLines = allLines.slice(-Math.abs(lines)).join('\n');
|
|
568
|
+
return {
|
|
569
|
+
content: [{ type: "text", text: `Last ${lines} lines of logs:\n\n${lastLines}` }],
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
const msg = `Failed to read logs: ${error.message}`;
|
|
574
|
+
logToFile(msg);
|
|
575
|
+
throw new Error(msg);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
case "get_info": {
|
|
579
|
+
const info = {
|
|
580
|
+
TARGET_REPO: config.TARGET_REPO,
|
|
581
|
+
TARGET_BRANCH: config.TARGET_BRANCH,
|
|
582
|
+
BASE_BRANCH: config.BASE_BRANCH,
|
|
583
|
+
CLONE_DIR: config.CLONE_DIR,
|
|
584
|
+
REPO_PATH: config.REPO_PATH || "Not defined (Call 'clone' first)",
|
|
585
|
+
READ_ONLY: config.READ_ONLY,
|
|
586
|
+
INCLUDE_PUBLIC: config.INCLUDE_PUBLIC,
|
|
587
|
+
INCLUDE_NOT_OWNED: config.INCLUDE_NOT_OWNED,
|
|
588
|
+
CURRENT_WORKING_DIR: process.cwd(),
|
|
589
|
+
GITHUB_TOKEN_LAST_5: config.GITHUB_TOKEN ? `***${config.GITHUB_TOKEN.slice(-5)}` : "Non configuré",
|
|
590
|
+
};
|
|
591
|
+
return {
|
|
592
|
+
content: [{
|
|
593
|
+
type: "text",
|
|
594
|
+
text: `Current Configuration:\n${JSON.stringify(info, null, 2)}`
|
|
595
|
+
}],
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
case "clone": {
|
|
599
|
+
const result = await performClone();
|
|
600
|
+
return {
|
|
601
|
+
content: [{ type: "text", text: result }],
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
case "deploy": {
|
|
605
|
+
assertSshConfig();
|
|
606
|
+
if (!config.TARGET_BRANCH) {
|
|
607
|
+
throw new Error("TARGET_BRANCH est manquant. Configurez-le via la GUI ou le .env.");
|
|
608
|
+
}
|
|
609
|
+
const authHeader = config.GITHUB_TOKEN
|
|
610
|
+
? `Authorization: Basic ${Buffer.from(`x-access-token:${config.GITHUB_TOKEN}`).toString("base64")}`
|
|
611
|
+
: null;
|
|
612
|
+
const gitPrefix = authHeader
|
|
613
|
+
? `git -c http.extraHeader="${authHeader}" -c http.prompt=false`
|
|
614
|
+
: `GIT_TERMINAL_PROMPT=0 git`;
|
|
615
|
+
const deployCommand = `${gitPrefix} fetch --all && ${gitPrefix} checkout ${config.TARGET_BRANCH} && ${gitPrefix} pull`;
|
|
616
|
+
const deployResult = await execRemote(deployCommand);
|
|
617
|
+
if (deployResult.code !== 0) {
|
|
618
|
+
const msg = `Deploy échoué (exit code ${deployResult.code}):\n${deployResult.stderr || deployResult.stdout}`;
|
|
619
|
+
logToFile(`[deploy] ${msg}`);
|
|
620
|
+
return {
|
|
621
|
+
content: [{ type: "text", text: msg }],
|
|
622
|
+
isError: true,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
const deployMsg = `Deploy réussi sur ${config.SSH_HOST}:${config.SSH_REMOTE_PATH} (branche: ${config.TARGET_BRANCH})\n\n${deployResult.stdout}`.trim();
|
|
626
|
+
logToFile(`[deploy] ${deployMsg}`);
|
|
627
|
+
return {
|
|
628
|
+
content: [{ type: "text", text: deployMsg }],
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
case "test": {
|
|
632
|
+
assertSshConfig();
|
|
633
|
+
if (!config.TEST_COMMAND) {
|
|
634
|
+
throw new Error("TEST_COMMAND est manquant dans le .env.");
|
|
635
|
+
}
|
|
636
|
+
const testResult = await execRemote(config.TEST_COMMAND);
|
|
637
|
+
_lastTestResult = {
|
|
638
|
+
command: config.TEST_COMMAND,
|
|
639
|
+
stdout: testResult.stdout,
|
|
640
|
+
stderr: testResult.stderr,
|
|
641
|
+
code: testResult.code,
|
|
642
|
+
ranAt: new Date().toISOString(),
|
|
643
|
+
};
|
|
644
|
+
const testMsg = `Command: ${config.TEST_COMMAND}\nExit code: ${testResult.code}\n--- STDOUT ---\n${testResult.stdout}\n--- STDERR ---\n${testResult.stderr}`;
|
|
645
|
+
logToFile(`[test] Exit code: ${testResult.code}`);
|
|
646
|
+
return {
|
|
647
|
+
content: [{ type: "text", text: testMsg }],
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
case "test_logs": {
|
|
651
|
+
if (!_lastTestResult) {
|
|
652
|
+
return {
|
|
653
|
+
content: [{ type: "text", text: "Aucun test n'a encore été exécuté. Lance d'abord 'test'." }],
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
const logsMsg = `(Cached, ranAt: ${_lastTestResult.ranAt})\nCommand: ${_lastTestResult.command}\nExit code: ${_lastTestResult.code}\n--- STDOUT ---\n${_lastTestResult.stdout}\n--- STDERR ---\n${_lastTestResult.stderr}`;
|
|
657
|
+
return {
|
|
658
|
+
content: [{ type: "text", text: logsMsg }],
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
default:
|
|
662
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
const msg = `Error in tool ${name}: ${error.message}`;
|
|
667
|
+
logToFile(msg);
|
|
668
|
+
return {
|
|
669
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
670
|
+
isError: true,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
async function main() {
|
|
675
|
+
const transport = new StdioServerTransport();
|
|
676
|
+
await server.connect(transport);
|
|
677
|
+
const msg = `GitHub MCP Server running on stdio (CWD: ${process.cwd()})`;
|
|
678
|
+
console.error(msg);
|
|
679
|
+
logToFile(msg);
|
|
680
|
+
}
|
|
681
|
+
main().catch((error) => {
|
|
682
|
+
const msg = `Fatal error in main(): ${error.message}`;
|
|
683
|
+
console.error(msg);
|
|
684
|
+
logToFile(msg);
|
|
685
|
+
process.exit(1);
|
|
686
|
+
});
|
|
687
|
+
//# sourceMappingURL=index.js.map
|