index.ts•10 kB
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // Use McpServer
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; // Transport for Windsurf
import { 
    CallToolRequestSchema, 
    ListToolsRequestSchema 
} from "@modelcontextprotocol/sdk/types.js"; 
import { z } from 'zod'; 
import { zodToJsonSchema } from 'zod-to-json-schema';
// Restore GitHub specific imports
import { Octokit } from "@octokit/rest";
import * as actions from './operations/actions.js';
import { 
    GitHubError, 
    isGitHubError, 
    GitHubValidationError,
    GitHubResourceNotFoundError,
    GitHubAuthenticationError,
    GitHubPermissionError,
    GitHubRateLimitError,
    GitHubConflictError,
    GitHubTimeoutError,
    GitHubNetworkError,
} from './common/errors.js';
import { VERSION } from "./common/version.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const logFilePath = path.join(__dirname, '..', 'dist', 'mcp-startup.log'); // Ensure log path points to dist
// Simple file logger
function logToFile(message: string) {
  const timestamp = new Date().toISOString();
  try {
    // Ensure dist directory exists before logging
    const logDir = path.dirname(logFilePath);
    if (!fs.existsSync(logDir)){
        fs.mkdirSync(logDir, { recursive: true });
    }
    fs.appendFileSync(logFilePath, `[${timestamp}] ${message}\n`, 'utf8');
  } catch (err: any) { // Use any for broader catch
    const errorMsg = `[File Log Error] Failed to write to ${logFilePath}: ${err?.message || String(err)}`;
    console.error(errorMsg);
    if (err instanceof Error && err.stack) { // Check if error has stack
      console.error(err.stack);
    }
    console.error(`[Original Message] ${message}`);
  }
}
// Clear log file on startup
// Ensure dist directory exists before logging
const logDir = path.dirname(logFilePath);
try {
    if (!fs.existsSync(logDir)){
        fs.mkdirSync(logDir, { recursive: true });
    }
    fs.writeFileSync(logFilePath, '', 'utf8'); 
    logToFile('[MCP Server Log] Log file cleared/initialized.');
} catch (err: any) { 
    // Log critical startup error to stderr *only* if file logging setup failed
    // This is a last resort and might still interfere, but necessary if logging isn't possible.
    const errorMsg = `[MCP Startup Error] Failed to initialize log file at ${logFilePath}: ${err?.message || String(err)}`;
    console.error(errorMsg); 
    if (err instanceof Error && err.stack) {
        console.error(err.stack);
    }
    process.exit(1); // Exit if we can't even log
}
// Add a global handler for uncaught exceptions
process.on('uncaughtException', (err, origin) => {
  let message = `[MCP Server Log] Uncaught Exception. Origin: ${origin}. Error: ${err?.message || String(err)}`;
  logToFile(message);
  if (err && err.stack) {
    logToFile(err.stack);
  }
  // Optionally add more context
  logToFile('[MCP Server Log] Exiting due to uncaught exception.');
  process.exit(1); // Exit cleanly
});
logToFile('[MCP Server Log] Initializing GitHub Actions MCP Server...');
// Restore auth logic
// Allow token via CLI argument `--token=<token>` or fallback to env var
let cliToken: string | undefined;
for (const arg of process.argv) {
  if (arg.startsWith('--token=')) {
    cliToken = arg.substring('--token='.length);
    break;
  }
}
const GITHUB_TOKEN = cliToken || process.env.GITHUB_PERSONAL_ACCESS_TOKEN; // Restore env check
if (!GITHUB_TOKEN) {
  logToFile('FATAL: GITHUB_PERSONAL_ACCESS_TOKEN environment variable is not set.');
  process.exit(1);
}
logToFile('[MCP Server Log] GitHub token found.'); // Restore original log message
const octokit = new Octokit({ auth: GITHUB_TOKEN });
logToFile('[MCP Server Log] Octokit initialized.');
const server = new McpServer(
  {
    name: "github-actions-mcp-server",
    version: VERSION, 
    context: {
      octokit: octokit
    }
  }
);
// Restore error formatting function
function formatGitHubError(error: GitHubError): string {
  let message = `GitHub API Error: ${error.message}`;
  
  if (error instanceof GitHubValidationError) {
    message = `Validation Error: ${error.message}`;
    if (error.response) {
      message += `\nDetails: ${JSON.stringify(error.response)}`;
    }
  } else if (error instanceof GitHubResourceNotFoundError) {
    message = `Not Found: ${error.message}`;
  } else if (error instanceof GitHubAuthenticationError) {
    message = `Authentication Failed: ${error.message}`;
  } else if (error instanceof GitHubPermissionError) {
    message = `Permission Denied: ${error.message}`;
  } else if (error instanceof GitHubRateLimitError) {
    message = `Rate Limit Exceeded: ${error.message}\nResets at: ${error.resetAt.toISOString()}`;
  } else if (error instanceof GitHubConflictError) {
    message = `Conflict: ${error.message}`;
  } else if (error instanceof GitHubTimeoutError) {
    message = `Timeout: ${error.message}\nTimeout setting: ${error.timeoutMs}ms`;
  } else if (error instanceof GitHubNetworkError) {
    message = `Network Error: ${error.message}\nError code: ${error.errorCode}`;
  }
  return message;
}
// Restore ListTools using server.tool()
server.tool(
    "list_workflows",
    actions.ListWorkflowsSchema.shape,
    async (request: any) => {
      logToFile('[MCP Server Log] Received list_workflows request (via server.tool)');
      // Args are already parsed by the McpServer using the provided schema
      const result = await actions.listWorkflows(request.owner, request.repo, request.page, request.perPage);
      return { content: [{ type: "text", text: JSON.stringify(result) }] };
    }
);
// Register other tools using server.tool()
server.tool(
    "get_workflow",
    actions.GetWorkflowSchema.shape,
    async (request: any) => {
        const result = await actions.getWorkflow(request.owner, request.repo, request.workflowId);
        return { content: [{ type: "text", text: JSON.stringify(result) }] };
    }
);
server.tool(
    "get_workflow_usage",
    actions.GetWorkflowUsageSchema.shape,
    async (request: any) => {
        const result = await actions.getWorkflowUsage(request.owner, request.repo, request.workflowId);
        return { content: [{ type: "text", text: JSON.stringify(result) }] };
    }
);
server.tool(
    "list_workflow_runs",
    actions.ListWorkflowRunsSchema.shape,
    async (request: any) => {
        const { owner, repo, workflowId, ...options } = request;
        const result = await actions.listWorkflowRuns(owner, repo, { workflowId, ...options });
        return { content: [{ type: "text", text: JSON.stringify(result) }] };
    }
);
server.tool(
    "get_workflow_run",
    actions.GetWorkflowRunSchema.shape,
    async (request: any) => {
        const result = await actions.getWorkflowRun(request.owner, request.repo, request.runId);
        return { content: [{ type: "text", text: JSON.stringify(result) }] };
    }
);
server.tool(
    "get_workflow_run_jobs",
    actions.GetWorkflowRunJobsSchema.shape,
    async (request: any) => {
        const { owner, repo, runId, filter, page, perPage } = request;
        const result = await actions.getWorkflowRunJobs(owner, repo, runId, filter, page, perPage);
        return { content: [{ type: "text", text: JSON.stringify(result) }] };
    }
);
server.tool(
    "trigger_workflow",
    actions.TriggerWorkflowSchema.shape,
    async (request: any) => {
        const { owner, repo, workflowId, ref, inputs } = request;
        const result = await actions.triggerWorkflow(owner, repo, workflowId, ref, inputs);
        return { content: [{ type: "text", text: JSON.stringify(result) }] };
    }
);
server.tool(
    "cancel_workflow_run",
    actions.CancelWorkflowRunSchema.shape,
    async (request: any) => {
        const result = await actions.cancelWorkflowRun(request.owner, request.repo, request.runId);
        return { content: [{ type: "text", text: JSON.stringify(result) }] };
    }
);
server.tool(
    "rerun_workflow",
    actions.RerunWorkflowSchema.shape,
    async (request: any) => {
        const result = await actions.rerunWorkflowRun(request.owner, request.repo, request.runId);
        return { content: [{ type: "text", text: JSON.stringify(result) }] };
    }
);
// Wrap server logic in a try/catch for initialization errors
try {
    logToFile('[MCP Server Log] Server initialization complete. Ready for connection.');
    // Attach stdio transport so Windsurf can communicate
    const transport = new StdioServerTransport();
    await server.connect(transport);
    logToFile('[MCP Server Log] Connected via stdio transport.');
} catch (error: any) {
    // Ensure fatal errors during server setup are logged to the file.
    logToFile(`[MCP Server Log] FATAL Error during server setup: ${error?.message || String(error)}`);
    if (error instanceof Error && error.stack) {
        logToFile(error.stack);
    }
    // Do NOT use console.error here as it will interfere with MCP stdio
    process.exit(1);
}
// Add other process event handlers
// Catch unhandled promise rejections, log them to file, and exit gracefully.
process.on('unhandledRejection', (reason, promise) => {
  // Log unhandled promise rejections to the file.
  let reasonStr = reason instanceof Error ? reason.message : String(reason);
  // Including stack trace if available
  let stack = reason instanceof Error ? `\nStack: ${reason.stack}` : '';
  logToFile(`[MCP Server Log] Unhandled Rejection at: ${promise}, reason: ${reasonStr}${stack}`);
  // Consider exiting depending on the severity or application logic
  // process.exit(1); // Optionally exit
});
process.on('SIGINT', () => {
  logToFile('[MCP Server Log] Received SIGINT. Exiting gracefully.');
  // Add any cleanup logic here
  process.exit(0);
});
process.on('SIGTERM', () => {
  logToFile('[MCP Server Log] Received SIGTERM. Exiting gracefully.');
  // Add any cleanup logic here
  process.exit(0);
});