@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/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