# Directory Structure ``` ├── .github │ ├── dependabot.yml │ └── workflows │ ├── ci.yml │ └── dependabot-auto-merge.yml ├── .gitignore ├── Dockerfile ├── notes.md ├── package-lock.json ├── package.json ├── README.md ├── run-server.bat ├── smithery.yaml ├── src │ ├── common │ │ ├── errors.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ └── version.ts │ ├── index.ts │ └── operations │ └── actions.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log 4 | yarn-debug.log 5 | yarn-error.log 6 | yarn.lock 7 | 8 | # Build output 9 | dist/ 10 | build/ 11 | out/ 12 | .next/ 13 | .nuxt/ 14 | 15 | # Environment variables 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | # Logs 23 | logs/ 24 | *.log 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # Editor settings 30 | .idea/ 31 | .vscode/ 32 | *.swp 33 | *.swo 34 | .DS_Store 35 | .prettierrc 36 | 37 | # Test 38 | coverage/ 39 | .nyc_output/ 40 | 41 | # Others 42 | .cache/ 43 | tmp/ 44 | temp/ 45 | 46 | # Claude Crew 47 | .claude-crew/ 48 | task-analysis/ 49 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | [](https://mseep.ai/app/cad0f49e-1c4d-4ab1-97e4-2312da835454) 2 | [](https://mseep.ai/app/ko1ynnky-github-actions-mcp-server) 3 | 4 | # GitHub Actions MCP Server 5 | 6 | [](https://smithery.ai/server/@ko1ynnky/github-actions-mcp-server) 7 | 8 | > **⚠️ Archive Notice**: This repository will be archived soon as the official GitHub MCP server is adding Actions support. See [github/github-mcp-server#491](https://github.com/github/github-mcp-server/pull/491) for details on the official implementation. 9 | 10 | MCP Server for the GitHub Actions API, enabling AI assistants to manage and operate GitHub Actions workflows. Compatible with multiple AI coding assistants including Claude Desktop, Codeium, and Windsurf. 11 | 12 | ### Features 13 | 14 | - **Complete Workflow Management**: List, view, trigger, cancel, and rerun workflows 15 | - **Workflow Run Analysis**: Get detailed information about workflow runs and their jobs 16 | - **Comprehensive Error Handling**: Clear error messages with enhanced details 17 | - **Flexible Type Validation**: Robust type checking with graceful handling of API variations 18 | - **Security-Focused Design**: Timeout handling, rate limiting, and strict URL validation 19 | 20 | ## Tools 21 | 22 | 1. `list_workflows` 23 | - List workflows in a GitHub repository 24 | - Inputs: 25 | - `owner` (string): Repository owner (username or organization) 26 | - `repo` (string): Repository name 27 | - `page` (optional number): Page number for pagination 28 | - `perPage` (optional number): Results per page (max 100) 29 | - Returns: List of workflows in the repository 30 | 31 | 2. `get_workflow` 32 | - Get details of a specific workflow 33 | - Inputs: 34 | - `owner` (string): Repository owner (username or organization) 35 | - `repo` (string): Repository name 36 | - `workflowId` (string or number): The ID of the workflow or filename 37 | - Returns: Detailed information about the workflow 38 | 39 | 3. `get_workflow_usage` 40 | - Get usage statistics of a workflow 41 | - Inputs: 42 | - `owner` (string): Repository owner (username or organization) 43 | - `repo` (string): Repository name 44 | - `workflowId` (string or number): The ID of the workflow or filename 45 | - Returns: Usage statistics including billable minutes 46 | 47 | 4. `list_workflow_runs` 48 | - List all workflow runs for a repository or a specific workflow 49 | - Inputs: 50 | - `owner` (string): Repository owner (username or organization) 51 | - `repo` (string): Repository name 52 | - `workflowId` (optional string or number): The ID of the workflow or filename 53 | - `actor` (optional string): Filter by user who triggered the workflow 54 | - `branch` (optional string): Filter by branch 55 | - `event` (optional string): Filter by event type 56 | - `status` (optional string): Filter by status 57 | - `created` (optional string): Filter by creation date (YYYY-MM-DD) 58 | - `excludePullRequests` (optional boolean): Exclude PR-triggered runs 59 | - `checkSuiteId` (optional number): Filter by check suite ID 60 | - `page` (optional number): Page number for pagination 61 | - `perPage` (optional number): Results per page (max 100) 62 | - Returns: List of workflow runs matching the criteria 63 | 64 | 5. `get_workflow_run` 65 | - Get details of a specific workflow run 66 | - Inputs: 67 | - `owner` (string): Repository owner (username or organization) 68 | - `repo` (string): Repository name 69 | - `runId` (number): The ID of the workflow run 70 | - Returns: Detailed information about the specific workflow run 71 | 72 | 6. `get_workflow_run_jobs` 73 | - Get jobs for a specific workflow run 74 | - Inputs: 75 | - `owner` (string): Repository owner (username or organization) 76 | - `repo` (string): Repository name 77 | - `runId` (number): The ID of the workflow run 78 | - `filter` (optional string): Filter jobs by completion status ('latest', 'all') 79 | - `page` (optional number): Page number for pagination 80 | - `perPage` (optional number): Results per page (max 100) 81 | - Returns: List of jobs in the workflow run 82 | 83 | 7. `trigger_workflow` 84 | - Trigger a workflow run 85 | - Inputs: 86 | - `owner` (string): Repository owner (username or organization) 87 | - `repo` (string): Repository name 88 | - `workflowId` (string or number): The ID of the workflow or filename 89 | - `ref` (string): The reference to run the workflow on (branch, tag, or SHA) 90 | - `inputs` (optional object): Input parameters for the workflow 91 | - Returns: Information about the triggered workflow run 92 | 93 | 8. `cancel_workflow_run` 94 | - Cancel a workflow run 95 | - Inputs: 96 | - `owner` (string): Repository owner (username or organization) 97 | - `repo` (string): Repository name 98 | - `runId` (number): The ID of the workflow run 99 | - Returns: Status of the cancellation operation 100 | 101 | 9. `rerun_workflow` 102 | - Re-run a workflow run 103 | - Inputs: 104 | - `owner` (string): Repository owner (username or organization) 105 | - `repo` (string): Repository name 106 | - `runId` (number): The ID of the workflow run 107 | - Returns: Status of the re-run operation 108 | 109 | ### Usage with AI Coding Assistants 110 | 111 | This MCP server is compatible with multiple AI coding assistants including Claude Desktop, Codeium, and Windsurf. 112 | 113 | #### Claude Desktop 114 | 115 | First, make sure you have built the project (see Build section below). Then, add the following to your `claude_desktop_config.json`: 116 | 117 | ```json 118 | { 119 | "mcpServers": { 120 | "github-actions": { 121 | "command": "node", 122 | "args": [ 123 | "<path-to-mcp-server>/dist/index.js" 124 | ], 125 | "env": { 126 | "GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>" 127 | } 128 | } 129 | } 130 | } 131 | ``` 132 | 133 | #### Codeium 134 | 135 | Add the following configuration to your Codeium MCP config file (typically at `~/.codeium/windsurf/mcp_config.json` on Unix-based systems or `%USERPROFILE%\.codeium\windsurf\mcp_config.json` on Windows): 136 | 137 | ```json 138 | { 139 | "mcpServers": { 140 | "github-actions": { 141 | "command": "node", 142 | "args": [ 143 | "<path-to-mcp-server>/dist/index.js" 144 | ], 145 | "env": { 146 | "GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>" 147 | } 148 | } 149 | } 150 | } 151 | ``` 152 | 153 | #### Windsurf 154 | 155 | Windsurf uses the same configuration format as Codeium. Add the server to your Windsurf MCP configuration as shown above for Codeium. 156 | 157 | ## Build 158 | 159 | ### Unix/Linux/macOS 160 | 161 | Clone the repository and build: 162 | 163 | ```bash 164 | git clone https://github.com/ko1ynnky/github-actions-mcp-server.git 165 | cd github-actions-mcp-server 166 | npm install 167 | npm run build 168 | ``` 169 | 170 | ### Windows 171 | 172 | For Windows systems, use the Windows-specific build command: 173 | 174 | ```bash 175 | git clone https://github.com/ko1ynnky/github-actions-mcp-server.git 176 | cd github-actions-mcp-server 177 | npm install 178 | npm run build:win 179 | ``` 180 | 181 | Alternatively, you can use the included batch file: 182 | 183 | ```bash 184 | run-server.bat [optional-github-token] 185 | ``` 186 | 187 | This will create the necessary files in the `dist` directory that you'll need to run the MCP server. 188 | 189 | #### Windows-Specific Instructions 190 | 191 | **Prerequisites** 192 | - Node.js (v14 or higher) 193 | - npm (v6 or higher) 194 | 195 | **Running the Server on Windows** 196 | 197 | 1. Using the batch file (simplest method): 198 | ``` 199 | run-server.bat [optional-github-token] 200 | ``` 201 | This will check if the build exists, build if needed, and start the server. 202 | 203 | 2. Using npm directly: 204 | ``` 205 | npm run start 206 | ``` 207 | 208 | **Setting GitHub Personal Access Token on Windows** 209 | 210 | For full functionality and to avoid rate limiting, you need to set your GitHub Personal Access Token. 211 | 212 | Options: 213 | 1. Pass it as a parameter to the batch file: 214 | ``` 215 | run-server.bat your_github_token_here 216 | ``` 217 | 218 | 2. Set it as an environment variable: 219 | ``` 220 | set GITHUB_PERSONAL_ACCESS_TOKEN=your_github_token_here 221 | npm run start 222 | ``` 223 | 224 | **Troubleshooting Windows Issues** 225 | 226 | If you encounter issues: 227 | 228 | 1. **Build errors**: Make sure TypeScript is installed correctly. 229 | ``` 230 | npm install -g typescript 231 | ``` 232 | 233 | 2. **Permission issues**: Ensure you're running the commands in a command prompt with appropriate permissions. 234 | 235 | 3. **Node.js errors**: Verify you're using a compatible Node.js version. 236 | ``` 237 | node --version 238 | ``` 239 | 240 | ## Usage Examples 241 | 242 | List workflows in a repository: 243 | 244 | ```javascript 245 | const result = await listWorkflows({ 246 | owner: "your-username", 247 | repo: "your-repository" 248 | }); 249 | ``` 250 | 251 | Trigger a workflow: 252 | 253 | ```javascript 254 | const result = await triggerWorkflow({ 255 | owner: "your-username", 256 | repo: "your-repository", 257 | workflowId: "ci.yml", 258 | ref: "main", 259 | inputs: { 260 | environment: "production" 261 | } 262 | }); 263 | ``` 264 | 265 | ## Troubleshooting 266 | 267 | ### Common Issues 268 | 269 | 1. **Authentication Errors**: 270 | - Ensure your GitHub token has the correct permissions 271 | - Check that the token is correctly set as an environment variable 272 | 273 | 2. **Rate Limiting**: 274 | - The server implements rate limiting to avoid hitting GitHub API limits 275 | - If you encounter rate limit errors, reduce the frequency of requests 276 | 277 | 3. **Type Validation Errors**: 278 | - GitHub API responses might sometimes differ from expected schemas 279 | - The server implements flexible validation to handle most variations 280 | - If you encounter persistent errors, please open an issue 281 | 282 | ## License 283 | 284 | This MCP server is licensed under the MIT License. 285 | ``` -------------------------------------------------------------------------------- /src/common/version.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Store version here for easy updates 2 | export const VERSION = "0.1.0"; 3 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "outDir": "dist", 9 | "declaration": true, 10 | "skipLibCheck": true 11 | }, 12 | "include": ["src/**/*"] 13 | } ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | FROM node:lts-alpine 3 | 4 | # Create app directory 5 | WORKDIR /app 6 | 7 | # Install app dependencies 8 | COPY package.json package-lock.json tsconfig.json ./ 9 | COPY src ./src 10 | 11 | # Install dependencies and build 12 | RUN npm install --ignore-scripts && npm run build 13 | 14 | # Expose any ports if needed (MCP over stdio, no ports) 15 | 16 | # Default command to run the MCP server 17 | CMD ["node", "dist/index.js"] 18 | ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - githubPersonalAccessToken 10 | properties: 11 | githubPersonalAccessToken: 12 | type: string 13 | description: GitHub Personal Access Token for API access 14 | commandFunction: 15 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio. 16 | |- 17 | (config) => ({ command: 'node', args: ['dist/index.js'], env: { GITHUB_PERSONAL_ACCESS_TOKEN: config.githubPersonalAccessToken } }) 18 | exampleConfig: 19 | githubPersonalAccessToken: ghp_exampletoken1234567890 20 | ``` -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | pull-requests: write 6 | contents: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v1 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | 19 | - name: Auto-merge for dev dependency updates 20 | if: ${{steps.metadata.outputs.dependency-type == 'direct:development' && (steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch')}} 21 | run: gh pr merge --auto --merge "$PR_URL" 22 | env: 23 | PR_URL: ${{github.event.pull_request.html_url}} 24 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 25 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "github-actions-mcp", 3 | "version": "0.1.0", 4 | "description": "MCP server for using the GitHub Actions API", 5 | "license": "MIT", 6 | "type": "module", 7 | "main": "dist/index.js", 8 | "bin": { 9 | "github-actions-mcp": "dist/index.js" 10 | }, 11 | "files": [ 12 | "dist" 13 | ], 14 | "scripts": { 15 | "build": "tsc && node -e \"require('fs').chmodSync('./dist/index.js', 0o755)\"", 16 | "build:win": "tsc", 17 | "start": "node dist/index.js", 18 | "dev": "tsc && node dist/index.js", 19 | "watch": "tsc --watch", 20 | "lint": "tsc --noEmit", 21 | "test": "echo No tests specified && exit 0" 22 | }, 23 | "dependencies": { 24 | "@modelcontextprotocol/sdk": "1.12.1", 25 | "@types/node": "22.15.29", 26 | "node-fetch": "^3.3.2", 27 | "@octokit/rest": "^22.0.0", 28 | "universal-user-agent": "^7.0.3", 29 | "zod": "^3.25.46", 30 | "zod-to-json-schema": "^3.24.5" 31 | }, 32 | "devDependencies": { 33 | "typescript": "^5.8.3" 34 | } 35 | } ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - "**.md" 8 | - "docs/**" 9 | pull_request: 10 | branches: [main] 11 | paths-ignore: 12 | - "**.md" 13 | - "docs/**" 14 | workflow_dispatch: 15 | 16 | permissions: 17 | contents: read 18 | pull-requests: read 19 | 20 | jobs: 21 | build: 22 | name: Build 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: 20 33 | 34 | - name: Generate package-lock.json 35 | run: npm install --package-lock-only 36 | 37 | - name: Install dependencies 38 | run: npm ci 39 | 40 | - name: Build 41 | run: npm run build 42 | env: 43 | CI: true 44 | 45 | - name: Upload build artifacts 46 | uses: actions/upload-artifact@v4 47 | with: 48 | name: build 49 | path: dist/ 50 | retention-days: 7 51 | 52 | - name: Upload package-lock.json 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: package-lock 56 | path: package-lock.json 57 | retention-days: 7 58 | ``` -------------------------------------------------------------------------------- /run-server.bat: -------------------------------------------------------------------------------- ``` 1 | @echo off 2 | echo GitHub Actions MCP Server for Windows 3 | echo ---------------------------------- 4 | 5 | rem Check if Node.js is installed 6 | where node >nul 2>nul 7 | if %ERRORLEVEL% neq 0 ( 8 | echo Error: Node.js is not installed or not in PATH 9 | echo Please install Node.js from https://nodejs.org/ 10 | exit /b 1 11 | ) 12 | 13 | rem Check if dist folder exists; if not, build the project 14 | if not exist dist ( 15 | echo Building project... 16 | call npm run build:win 17 | if %ERRORLEVEL% neq 0 ( 18 | echo Build failed. Please check for errors. 19 | exit /b 1 20 | ) 21 | ) 22 | 23 | rem Set GitHub Personal Access Token if provided as command line argument 24 | if not "%~1"=="" ( 25 | set GITHUB_PERSONAL_ACCESS_TOKEN=%~1 26 | echo Using provided GitHub Personal Access Token 27 | ) else ( 28 | if defined GITHUB_PERSONAL_ACCESS_TOKEN ( 29 | echo Using GitHub Personal Access Token from environment 30 | ) else ( 31 | echo No GitHub Personal Access Token provided 32 | echo Some API calls may be rate-limited 33 | ) 34 | ) 35 | 36 | echo Starting MCP server... 37 | echo Listening on stdio... 38 | echo Press Ctrl+C to stop the server 39 | 40 | node dist/index.js 41 | 42 | echo Server stopped 43 | ``` -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- ```markdown 1 | if you could put questions in this file, I'll monitor it and try to answer them to prevent interactions. 2 | 3 | --- 4 | ### Questions / Actions (2025-04-26) 5 | 6 | 1. **Full Error Log** – please paste the entire stack-trace or log output that appears when Windsurf shows `failed to initialize: request failed`. 7 | I don't know where that is located can you point me to it? 8 | 9 | 2. **Server Log Confirmation** – after the failure, does `dist/mcp-startup.log` still show the line `Connected via stdio transport.`? 10 | Can you not check the log file? 11 | [2025-04-26T21:20:03.956Z] [MCP Server Log] Log file cleared/initialized. 12 | [2025-04-26T21:20:03.958Z] [MCP Server Log] Initializing GitHub Actions MCP Server... 13 | [2025-04-26T21:20:03.958Z] [MCP Server Log] GitHub token found. 14 | [2025-04-26T21:20:03.959Z] [MCP Server Log] Octokit initialized. 15 | [2025-04-26T21:20:03.960Z] [MCP Server Log] Server initialization complete. Ready for connection. 16 | [2025-04-26T21:20:03.961Z] [MCP Server Log] Connected via stdio transport. 17 | 18 | 3. **SDK Version Mismatch** – in the VS Code terminal, run `npm ls @modelcontextprotocol/sdk` and paste the output so we can verify the client/server versions match. 19 | can you not run that? 20 | (base) PS E:\code\github-actions-mcp-server> npm ls @modelcontextprotocol/sdk 21 | [email protected] E:\code\github-actions-mcp-server 22 | `-- @modelcontextprotocol/[email protected] 23 | 24 | 4. **Proxy / Firewall** – are you running behind a proxy, VPN, or firewall that could block subprocesses or stdio pipes? If yes, please give details. 25 | No proxy or VPN, just windows firewall which is open outbound 26 | 27 | _Add your answers below each question. Feel free to include any other clues._ ``` -------------------------------------------------------------------------------- /src/common/errors.ts: -------------------------------------------------------------------------------- ```typescript 1 | export class GitHubError extends Error { 2 | constructor( 3 | message: string, 4 | public readonly status: number, 5 | public readonly response: unknown 6 | ) { 7 | super(message); 8 | this.name = "GitHubError"; 9 | } 10 | } 11 | 12 | export class GitHubValidationError extends GitHubError { 13 | constructor(message: string, status: number, response: unknown) { 14 | super(message, status, response); 15 | this.name = "GitHubValidationError"; 16 | } 17 | } 18 | 19 | export class GitHubResourceNotFoundError extends GitHubError { 20 | constructor(resource: string) { 21 | super(`Resource not found: ${resource}`, 404, { message: `${resource} not found` }); 22 | this.name = "GitHubResourceNotFoundError"; 23 | } 24 | } 25 | 26 | export class GitHubAuthenticationError extends GitHubError { 27 | constructor(message = "Authentication failed") { 28 | super(message, 401, { message }); 29 | this.name = "GitHubAuthenticationError"; 30 | } 31 | } 32 | 33 | export class GitHubPermissionError extends GitHubError { 34 | constructor(message = "Insufficient permissions") { 35 | super(message, 403, { message }); 36 | this.name = "GitHubPermissionError"; 37 | } 38 | } 39 | 40 | export class GitHubRateLimitError extends GitHubError { 41 | constructor( 42 | message = "Rate limit exceeded", 43 | public readonly resetAt: Date 44 | ) { 45 | super(message, 429, { message, reset_at: resetAt.toISOString() }); 46 | this.name = "GitHubRateLimitError"; 47 | } 48 | } 49 | 50 | export class GitHubTimeoutError extends GitHubError { 51 | constructor( 52 | message = "Request timed out", 53 | public readonly timeoutMs: number 54 | ) { 55 | super(message, 408, { message, timeout_ms: timeoutMs }); 56 | this.name = "GitHubTimeoutError"; 57 | } 58 | } 59 | 60 | export class GitHubNetworkError extends GitHubError { 61 | constructor( 62 | message = "Network error", 63 | public readonly errorCode: string 64 | ) { 65 | super(message, 500, { message, error_code: errorCode }); 66 | this.name = "GitHubNetworkError"; 67 | } 68 | } 69 | 70 | export class GitHubConflictError extends GitHubError { 71 | constructor(message: string) { 72 | super(message, 409, { message }); 73 | this.name = "GitHubConflictError"; 74 | } 75 | } 76 | 77 | export function isGitHubError(error: unknown): error is GitHubError { 78 | return error instanceof GitHubError; 79 | } 80 | 81 | // Add enhanced error factory function 82 | export function createEnhancedGitHubError(error: Error & { cause?: { code: string } }): GitHubError { 83 | // Handle timeout errors 84 | if (error.name === 'AbortError') { 85 | return new GitHubTimeoutError(`Request timed out: ${error.message}`, 30000); 86 | } 87 | 88 | // Handle network errors 89 | if (error.cause?.code) { 90 | return new GitHubNetworkError( 91 | `Network error: ${error.message}`, 92 | error.cause.code 93 | ); 94 | } 95 | 96 | // Handle other errors 97 | return new GitHubError(error.message, 500, { message: error.message }); 98 | } 99 | 100 | export function createGitHubError(status: number, response: any): GitHubError { 101 | switch (status) { 102 | case 401: 103 | return new GitHubAuthenticationError(response?.message); 104 | case 403: 105 | return new GitHubPermissionError(response?.message); 106 | case 404: 107 | return new GitHubResourceNotFoundError(response?.message || "Resource"); 108 | case 409: 109 | return new GitHubConflictError(response?.message || "Conflict occurred"); 110 | case 422: 111 | return new GitHubValidationError( 112 | response?.message || "Validation failed", 113 | status, 114 | response 115 | ); 116 | case 429: 117 | return new GitHubRateLimitError( 118 | response?.message, 119 | new Date(response?.reset_at || Date.now() + 60000) 120 | ); 121 | default: 122 | return new GitHubError( 123 | response?.message || "GitHub API error", 124 | status, 125 | response 126 | ); 127 | } 128 | } ``` -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from "zod"; 2 | 3 | // Base GitHub types 4 | export const GitHubAuthorSchema = z.object({ 5 | name: z.string(), 6 | email: z.string(), 7 | date: z.string().optional(), 8 | }).passthrough(); 9 | 10 | // GitHub Workflow Run types 11 | export const WorkflowRunSchema = z.object({ 12 | id: z.number(), 13 | name: z.string().nullable(), 14 | node_id: z.string(), 15 | head_branch: z.string().nullable(), 16 | head_sha: z.string(), 17 | path: z.string(), 18 | display_title: z.string().nullable(), 19 | run_number: z.number(), 20 | event: z.string(), 21 | status: z.string().nullable(), 22 | conclusion: z.string().nullable(), 23 | workflow_id: z.number(), 24 | check_suite_id: z.number(), 25 | check_suite_node_id: z.string(), 26 | url: z.string(), 27 | html_url: z.string(), 28 | created_at: z.string().nullable().optional(), 29 | updated_at: z.string().nullable().optional(), 30 | run_attempt: z.number(), 31 | run_started_at: z.string().nullable().optional(), 32 | jobs_url: z.string(), 33 | logs_url: z.string(), 34 | check_suite_url: z.string(), 35 | artifacts_url: z.string(), 36 | cancel_url: z.string(), 37 | rerun_url: z.string(), 38 | previous_attempt_url: z.string().nullable(), 39 | workflow_url: z.string(), 40 | repository: z.object({ 41 | id: z.number(), 42 | node_id: z.string(), 43 | name: z.string(), 44 | full_name: z.string(), 45 | owner: z.object({ 46 | login: z.string(), 47 | id: z.number(), 48 | node_id: z.string(), 49 | avatar_url: z.string(), 50 | url: z.string(), 51 | html_url: z.string(), 52 | type: z.string(), 53 | }).passthrough(), 54 | html_url: z.string(), 55 | description: z.string().nullable(), 56 | fork: z.boolean(), 57 | url: z.string(), 58 | created_at: z.string().nullable().optional(), 59 | updated_at: z.string().nullable().optional(), 60 | }).passthrough(), 61 | head_repository: z.object({ 62 | id: z.number(), 63 | node_id: z.string(), 64 | name: z.string(), 65 | full_name: z.string(), 66 | owner: z.object({ 67 | login: z.string(), 68 | id: z.number(), 69 | node_id: z.string(), 70 | avatar_url: z.string(), 71 | url: z.string(), 72 | html_url: z.string(), 73 | type: z.string(), 74 | }).passthrough(), 75 | html_url: z.string(), 76 | description: z.string().nullable(), 77 | fork: z.boolean(), 78 | url: z.string(), 79 | created_at: z.string().nullable().optional(), 80 | updated_at: z.string().nullable().optional(), 81 | }).passthrough(), 82 | }).passthrough(); 83 | 84 | export const WorkflowRunsSchema = z.object({ 85 | total_count: z.number(), 86 | workflow_runs: z.array(WorkflowRunSchema), 87 | }).passthrough(); 88 | 89 | // GitHub Workflow Job types 90 | export const JobSchema = z.object({ 91 | id: z.number(), 92 | run_id: z.number(), 93 | workflow_name: z.string(), 94 | head_branch: z.string(), 95 | run_url: z.string(), 96 | run_attempt: z.number(), 97 | node_id: z.string(), 98 | head_sha: z.string(), 99 | url: z.string(), 100 | html_url: z.string(), 101 | status: z.string(), 102 | conclusion: z.string().nullable(), 103 | created_at: z.string(), 104 | started_at: z.string(), 105 | completed_at: z.string().nullable(), 106 | name: z.string(), 107 | steps: z.array( 108 | z.object({ 109 | name: z.string(), 110 | status: z.string(), 111 | conclusion: z.string().nullable(), 112 | number: z.number(), 113 | started_at: z.string().nullable(), 114 | completed_at: z.string().nullable(), 115 | }).passthrough() 116 | ), 117 | check_run_url: z.string(), 118 | labels: z.array(z.string()), 119 | runner_id: z.number().nullable(), 120 | runner_name: z.string().nullable(), 121 | runner_group_id: z.number().nullable(), 122 | runner_group_name: z.string().nullable(), 123 | }).passthrough(); 124 | 125 | export const JobsSchema = z.object({ 126 | total_count: z.number(), 127 | jobs: z.array(JobSchema), 128 | }).passthrough(); 129 | 130 | // GitHub Workflow types 131 | export const WorkflowSchema = z.object({ 132 | id: z.number(), 133 | node_id: z.string(), 134 | name: z.string(), 135 | path: z.string(), 136 | state: z.string(), 137 | created_at: z.string(), 138 | updated_at: z.string(), 139 | url: z.string(), 140 | html_url: z.string(), 141 | badge_url: z.string(), 142 | }).passthrough(); 143 | 144 | export const WorkflowsSchema = z.object({ 145 | total_count: z.number(), 146 | workflows: z.array(WorkflowSchema), 147 | }).passthrough(); 148 | 149 | // GitHub Workflow Usage types 150 | export const WorkflowUsageSchema = z.object({ 151 | billable: z.object({ 152 | UBUNTU: z.object({ 153 | total_ms: z.number().optional(), 154 | jobs: z.number().optional(), 155 | }).passthrough().optional(), 156 | MACOS: z.object({ 157 | total_ms: z.number().optional(), 158 | jobs: z.number().optional(), 159 | }).passthrough().optional(), 160 | WINDOWS: z.object({ 161 | total_ms: z.number().optional(), 162 | jobs: z.number().optional(), 163 | }).passthrough().optional(), 164 | }).passthrough().optional(), 165 | }).passthrough(); 166 | 167 | export type WorkflowRun = z.infer<typeof WorkflowRunSchema>; 168 | export type WorkflowRunsResponse = z.infer<typeof WorkflowRunsSchema>; 169 | export type Job = z.infer<typeof JobSchema>; 170 | export type JobsResponse = z.infer<typeof JobsSchema>; 171 | export type Workflow = z.infer<typeof WorkflowSchema>; 172 | export type WorkflowsResponse = z.infer<typeof WorkflowsSchema>; 173 | export type WorkflowUsage = z.infer<typeof WorkflowUsageSchema>; ``` -------------------------------------------------------------------------------- /src/common/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getUserAgent } from "universal-user-agent"; 2 | import { createGitHubError, GitHubTimeoutError, GitHubNetworkError, GitHubError, createEnhancedGitHubError } from "./errors.js"; 3 | import { VERSION } from "./version.js"; 4 | 5 | type RequestOptions = { 6 | method?: string; 7 | body?: unknown; 8 | headers?: Record<string, string>; 9 | } 10 | 11 | async function parseResponseBody(response: Response): Promise<unknown> { 12 | const contentType = response.headers.get("content-type"); 13 | if (contentType?.includes("application/json")) { 14 | try { 15 | return await response.json(); 16 | } catch (error) { 17 | console.error("Error parsing JSON response:", error); 18 | throw new Error(`Error parsing JSON response: ${error}`); 19 | } 20 | } 21 | return response.text(); 22 | } 23 | 24 | export function buildUrl(baseUrl: string, params: Record<string, string | number | undefined>): string { 25 | const url = new URL(baseUrl); 26 | Object.entries(params).forEach(([key, value]) => { 27 | if (value !== undefined) { 28 | url.searchParams.append(key, value.toString()); 29 | } 30 | }); 31 | return url.toString(); 32 | } 33 | 34 | const USER_AGENT = `github-actions-mcp/v${VERSION} ${getUserAgent()}`; 35 | 36 | // Default timeout for GitHub API requests (30 seconds) 37 | const DEFAULT_TIMEOUT = 30000; 38 | 39 | // Rate limiting constants 40 | const MAX_REQUESTS_PER_MINUTE = 60; // GitHub API rate limit is typically 5000/hour for authenticated requests 41 | let requestCount = 0; 42 | let requestCountResetTime = Date.now() + 60000; 43 | 44 | /** 45 | * Make a request to the GitHub API with security enhancements 46 | * 47 | * @param url The URL to send the request to 48 | * @param options Request options including method, body, headers, and timeout 49 | * @returns The response body 50 | */ 51 | export async function githubRequest( 52 | url: string, 53 | options: RequestOptions & { timeout?: number } = {} 54 | ): Promise<unknown> { 55 | // Implement basic rate limiting 56 | if (Date.now() > requestCountResetTime) { 57 | requestCount = 0; 58 | requestCountResetTime = Date.now() + 60000; 59 | } 60 | 61 | if (requestCount >= MAX_REQUESTS_PER_MINUTE) { 62 | const waitTime = requestCountResetTime - Date.now(); 63 | throw new Error(`Rate limit exceeded. Please try again in ${Math.ceil(waitTime / 1000)} seconds.`); 64 | } 65 | requestCount++; 66 | 67 | // Validate URL to ensure it's a GitHub API URL (security measure) 68 | if (!url.startsWith('https://api.github.com/')) { 69 | throw new Error('Invalid GitHub API URL. Only https://api.github.com/ URLs are allowed.'); 70 | } 71 | 72 | const headers: Record<string, string> = { 73 | "Accept": "application/vnd.github.v3+json", 74 | "Content-Type": "application/json", 75 | "User-Agent": USER_AGENT, 76 | ...options.headers, 77 | }; 78 | 79 | if (process.env.GITHUB_PERSONAL_ACCESS_TOKEN) { 80 | headers["Authorization"] = `Bearer ${process.env.GITHUB_PERSONAL_ACCESS_TOKEN}`; 81 | } 82 | 83 | // Set up request timeout 84 | const timeout = options.timeout || DEFAULT_TIMEOUT; 85 | const controller = new AbortController(); 86 | const timeoutId = setTimeout(() => controller.abort(), timeout); 87 | 88 | try { 89 | const response = await fetch(url, { 90 | method: options.method || "GET", 91 | headers, 92 | body: options.body ? JSON.stringify(options.body) : undefined, 93 | signal: controller.signal 94 | }); 95 | 96 | const responseBody = await parseResponseBody(response); 97 | 98 | if (!response.ok) { 99 | throw createGitHubError(response.status, responseBody); 100 | } 101 | 102 | return responseBody; 103 | } catch (error: unknown) { 104 | if ((error as Error).name === 'AbortError') { 105 | throw new GitHubTimeoutError(`Request timeout after ${timeout}ms`, timeout); 106 | } 107 | if ((error as { cause?: { code: string } }).cause?.code === 'ENOTFOUND' || 108 | (error as { cause?: { code: string } }).cause?.code === 'ECONNREFUSED') { 109 | throw new GitHubNetworkError(`Unable to connect to GitHub API`, 110 | (error as { cause?: { code: string } }).cause!.code); 111 | } 112 | if (!(error instanceof GitHubError)) { 113 | throw createEnhancedGitHubError(error as Error & { cause?: { code: string } }); 114 | } 115 | throw error; 116 | } finally { 117 | clearTimeout(timeoutId); 118 | } 119 | } 120 | 121 | export function validateRepositoryName(name: string): string { 122 | const sanitized = name.trim().toLowerCase(); 123 | if (!sanitized) { 124 | throw new Error("Repository name cannot be empty"); 125 | } 126 | if (!/^[a-z0-9_.-]+$/.test(sanitized)) { 127 | throw new Error( 128 | "Repository name can only contain lowercase letters, numbers, hyphens, periods, and underscores" 129 | ); 130 | } 131 | if (sanitized.startsWith(".") || sanitized.endsWith(".")) { 132 | throw new Error("Repository name cannot start or end with a period"); 133 | } 134 | return sanitized; 135 | } 136 | 137 | export function validateOwnerName(owner: string): string { 138 | const sanitized = owner.trim().toLowerCase(); 139 | if (!sanitized) { 140 | throw new Error("Owner name cannot be empty"); 141 | } 142 | if (!/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/.test(sanitized)) { 143 | throw new Error( 144 | "Owner name must start with a letter or number and can contain up to 39 characters" 145 | ); 146 | } 147 | return sanitized; 148 | } ``` -------------------------------------------------------------------------------- /src/operations/actions.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from "zod"; 2 | import { githubRequest, buildUrl, validateOwnerName, validateRepositoryName } from "../common/utils.js"; 3 | import { 4 | WorkflowRunsSchema, 5 | WorkflowRunSchema, 6 | JobsSchema, 7 | WorkflowsSchema, 8 | WorkflowSchema, 9 | WorkflowUsageSchema 10 | } from "../common/types.js"; 11 | 12 | /** 13 | * Schema definitions 14 | */ 15 | 16 | // List workflows schemas 17 | export const ListWorkflowsSchema = z.object({ 18 | owner: z.string().describe("Repository owner (username or organization)"), 19 | repo: z.string().describe("Repository name"), 20 | page: z.number().optional().describe("Page number for pagination"), 21 | perPage: z.number().optional().describe("Results per page (max 100)"), 22 | }); 23 | 24 | // Get workflow schema 25 | export const GetWorkflowSchema = z.object({ 26 | owner: z.string().describe("Repository owner (username or organization)"), 27 | repo: z.string().describe("Repository name"), 28 | workflowId: z.string().describe("The ID of the workflow or filename (string or number)"), 29 | }); 30 | 31 | // Get workflow usage schema 32 | export const GetWorkflowUsageSchema = GetWorkflowSchema; 33 | 34 | // List workflow runs schema 35 | export const ListWorkflowRunsSchema = z.object({ 36 | owner: z.string().describe("Repository owner (username or organization)"), 37 | repo: z.string().describe("Repository name"), 38 | workflowId: z.string().optional().describe("The ID of the workflow or filename (string or number)"), 39 | actor: z.string().optional().describe("Returns someone's workflow runs. Use the login for the user"), 40 | branch: z.string().optional().describe("Returns workflow runs associated with a branch"), 41 | event: z.string().optional().describe("Returns workflow runs triggered by the event"), 42 | status: z.enum(['completed', 'action_required', 'cancelled', 'failure', 'neutral', 'skipped', 'stale', 'success', 'timed_out', 'in_progress', 'queued', 'requested', 'waiting', 'pending']).optional().describe("Returns workflow runs with the check run status"), 43 | created: z.string().optional().describe("Returns workflow runs created within date range (YYYY-MM-DD)"), 44 | excludePullRequests: z.boolean().optional().describe("If true, pull requests are omitted from the response"), 45 | checkSuiteId: z.number().optional().describe("Returns workflow runs with the check_suite_id"), 46 | page: z.number().optional().describe("Page number for pagination"), 47 | perPage: z.number().optional().describe("Results per page (max 100)"), 48 | }); 49 | 50 | // Get workflow run schema 51 | export const GetWorkflowRunSchema = z.object({ 52 | owner: z.string().describe("Repository owner (username or organization)"), 53 | repo: z.string().describe("Repository name"), 54 | runId: z.number().describe("The ID of the workflow run"), 55 | }); 56 | 57 | // Get workflow run jobs schema 58 | export const GetWorkflowRunJobsSchema = z.object({ 59 | owner: z.string().describe("Repository owner (username or organization)"), 60 | repo: z.string().describe("Repository name"), 61 | runId: z.number().describe("The ID of the workflow run"), 62 | filter: z.enum(['latest', 'all']).optional().describe("Filter jobs by their completed_at date"), 63 | page: z.number().optional().describe("Page number for pagination"), 64 | perPage: z.number().optional().describe("Results per page (max 100)"), 65 | }); 66 | 67 | // Trigger workflow schema 68 | export const TriggerWorkflowSchema = z.object({ 69 | owner: z.string().describe("Repository owner (username or organization)"), 70 | repo: z.string().describe("Repository name"), 71 | workflowId: z.string().describe("The ID of the workflow or filename (string or number)"), 72 | ref: z.string().describe("The reference of the workflow run (branch, tag, or SHA)"), 73 | inputs: z.record(z.string(), z.string()).optional().describe("Input parameters for the workflow"), 74 | }); 75 | 76 | // Cancel workflow run schema 77 | export const CancelWorkflowRunSchema = z.object({ 78 | owner: z.string().describe("Repository owner (username or organization)"), 79 | repo: z.string().describe("Repository name"), 80 | runId: z.number().describe("The ID of the workflow run"), 81 | }); 82 | 83 | // Rerun workflow schema 84 | export const RerunWorkflowSchema = CancelWorkflowRunSchema; 85 | 86 | /** 87 | * Function implementations 88 | */ 89 | 90 | // List workflows in a repository 91 | export async function listWorkflows( 92 | owner: string, 93 | repo: string, 94 | page?: number, 95 | perPage?: number 96 | ) { 97 | owner = validateOwnerName(owner); 98 | repo = validateRepositoryName(repo); 99 | 100 | const url = buildUrl(`https://api.github.com/repos/${owner}/${repo}/actions/workflows`, { 101 | page: page, 102 | per_page: perPage 103 | }); 104 | 105 | const response = await githubRequest(url); 106 | return WorkflowsSchema.parse(response); 107 | } 108 | 109 | // Get a workflow 110 | export async function getWorkflow( 111 | owner: string, 112 | repo: string, 113 | workflowId: string | number 114 | ) { 115 | owner = validateOwnerName(owner); 116 | repo = validateRepositoryName(repo); 117 | 118 | const url = `https://api.github.com/repos/${owner}/${repo}/actions/workflows/${workflowId}`; 119 | const response = await githubRequest(url); 120 | return WorkflowSchema.parse(response); 121 | } 122 | 123 | // Get workflow usage 124 | export async function getWorkflowUsage( 125 | owner: string, 126 | repo: string, 127 | workflowId: string | number 128 | ) { 129 | owner = validateOwnerName(owner); 130 | repo = validateRepositoryName(repo); 131 | 132 | const url = `https://api.github.com/repos/${owner}/${repo}/actions/workflows/${workflowId}/timing`; 133 | const response = await githubRequest(url); 134 | return WorkflowUsageSchema.parse(response); 135 | } 136 | 137 | // List workflow runs 138 | export async function listWorkflowRuns( 139 | owner: string, 140 | repo: string, 141 | options: { 142 | workflowId?: string | number, 143 | actor?: string, 144 | branch?: string, 145 | event?: string, 146 | status?: string, 147 | created?: string, 148 | excludePullRequests?: boolean, 149 | checkSuiteId?: number, 150 | page?: number, 151 | perPage?: number 152 | } = {} 153 | ) { 154 | owner = validateOwnerName(owner); 155 | repo = validateRepositoryName(repo); 156 | 157 | let url; 158 | if (options.workflowId) { 159 | url = `https://api.github.com/repos/${owner}/${repo}/actions/workflows/${options.workflowId}/runs`; 160 | } else { 161 | url = `https://api.github.com/repos/${owner}/${repo}/actions/runs`; 162 | } 163 | 164 | url = buildUrl(url, { 165 | actor: options.actor, 166 | branch: options.branch, 167 | event: options.event, 168 | status: options.status, 169 | created: options.created, 170 | exclude_pull_requests: options.excludePullRequests ? "true" : undefined, 171 | check_suite_id: options.checkSuiteId, 172 | page: options.page, 173 | per_page: options.perPage 174 | }); 175 | 176 | const response = await githubRequest(url); 177 | return WorkflowRunsSchema.parse(response); 178 | } 179 | 180 | // Get a workflow run 181 | export async function getWorkflowRun( 182 | owner: string, 183 | repo: string, 184 | runId: number 185 | ) { 186 | owner = validateOwnerName(owner); 187 | repo = validateRepositoryName(repo); 188 | 189 | const url = `https://api.github.com/repos/${owner}/${repo}/actions/runs/${runId}`; 190 | const response = await githubRequest(url); 191 | return WorkflowRunSchema.parse(response); 192 | } 193 | 194 | // Get workflow run jobs 195 | export async function getWorkflowRunJobs( 196 | owner: string, 197 | repo: string, 198 | runId: number, 199 | filter?: 'latest' | 'all', 200 | page?: number, 201 | perPage?: number 202 | ) { 203 | owner = validateOwnerName(owner); 204 | repo = validateRepositoryName(repo); 205 | 206 | const url = buildUrl(`https://api.github.com/repos/${owner}/${repo}/actions/runs/${runId}/jobs`, { 207 | filter: filter, 208 | page: page, 209 | per_page: perPage 210 | }); 211 | 212 | const response = await githubRequest(url); 213 | return JobsSchema.parse(response); 214 | } 215 | 216 | // Trigger a workflow run 217 | export async function triggerWorkflow( 218 | owner: string, 219 | repo: string, 220 | workflowId: string | number, 221 | ref: string, 222 | inputs?: Record<string, string> 223 | ) { 224 | owner = validateOwnerName(owner); 225 | repo = validateRepositoryName(repo); 226 | 227 | const url = `https://api.github.com/repos/${owner}/${repo}/actions/workflows/${workflowId}/dispatches`; 228 | 229 | const body: { 230 | ref: string; 231 | inputs?: Record<string, string>; 232 | } = { ref }; 233 | 234 | if (inputs && Object.keys(inputs).length > 0) { 235 | body.inputs = inputs; 236 | } 237 | 238 | await githubRequest(url, { 239 | method: 'POST', 240 | body 241 | }); 242 | 243 | // This endpoint doesn't return any data on success 244 | return { success: true, message: `Workflow ${workflowId} triggered on ${ref}` }; 245 | } 246 | 247 | // Cancel a workflow run 248 | export async function cancelWorkflowRun( 249 | owner: string, 250 | repo: string, 251 | runId: number 252 | ) { 253 | owner = validateOwnerName(owner); 254 | repo = validateRepositoryName(repo); 255 | 256 | const url = `https://api.github.com/repos/${owner}/${repo}/actions/runs/${runId}/cancel`; 257 | await githubRequest(url, { method: 'POST' }); 258 | 259 | // This endpoint doesn't return any data on success 260 | return { success: true, message: `Workflow run ${runId} cancelled` }; 261 | } 262 | 263 | // Rerun a workflow run 264 | export async function rerunWorkflowRun( 265 | owner: string, 266 | repo: string, 267 | runId: number 268 | ) { 269 | owner = validateOwnerName(owner); 270 | repo = validateRepositoryName(repo); 271 | 272 | const url = `https://api.github.com/repos/${owner}/${repo}/actions/runs/${runId}/rerun`; 273 | await githubRequest(url, { method: 'POST' }); 274 | 275 | // This endpoint doesn't return any data on success 276 | return { success: true, message: `Workflow run ${runId} restarted` }; 277 | } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // Use McpServer 5 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; // Transport for Windsurf 6 | import { 7 | CallToolRequestSchema, 8 | ListToolsRequestSchema 9 | } from "@modelcontextprotocol/sdk/types.js"; 10 | import { z } from 'zod'; 11 | import { zodToJsonSchema } from 'zod-to-json-schema'; 12 | 13 | // Restore GitHub specific imports 14 | import { Octokit } from "@octokit/rest"; 15 | import * as actions from './operations/actions.js'; 16 | import { 17 | GitHubError, 18 | isGitHubError, 19 | GitHubValidationError, 20 | GitHubResourceNotFoundError, 21 | GitHubAuthenticationError, 22 | GitHubPermissionError, 23 | GitHubRateLimitError, 24 | GitHubConflictError, 25 | GitHubTimeoutError, 26 | GitHubNetworkError, 27 | } from './common/errors.js'; 28 | import { VERSION } from "./common/version.js"; 29 | 30 | const __filename = fileURLToPath(import.meta.url); 31 | const __dirname = path.dirname(__filename); 32 | 33 | const logFilePath = path.join(__dirname, '..', 'dist', 'mcp-startup.log'); // Ensure log path points to dist 34 | 35 | // Simple file logger 36 | function logToFile(message: string) { 37 | const timestamp = new Date().toISOString(); 38 | try { 39 | // Ensure dist directory exists before logging 40 | const logDir = path.dirname(logFilePath); 41 | if (!fs.existsSync(logDir)){ 42 | fs.mkdirSync(logDir, { recursive: true }); 43 | } 44 | fs.appendFileSync(logFilePath, `[${timestamp}] ${message}\n`, 'utf8'); 45 | } catch (err: any) { // Use any for broader catch 46 | const errorMsg = `[File Log Error] Failed to write to ${logFilePath}: ${err?.message || String(err)}`; 47 | console.error(errorMsg); 48 | if (err instanceof Error && err.stack) { // Check if error has stack 49 | console.error(err.stack); 50 | } 51 | console.error(`[Original Message] ${message}`); 52 | } 53 | } 54 | 55 | // Clear log file on startup 56 | // Ensure dist directory exists before logging 57 | const logDir = path.dirname(logFilePath); 58 | try { 59 | if (!fs.existsSync(logDir)){ 60 | fs.mkdirSync(logDir, { recursive: true }); 61 | } 62 | fs.writeFileSync(logFilePath, '', 'utf8'); 63 | logToFile('[MCP Server Log] Log file cleared/initialized.'); 64 | } catch (err: any) { 65 | // Log critical startup error to stderr *only* if file logging setup failed 66 | // This is a last resort and might still interfere, but necessary if logging isn't possible. 67 | const errorMsg = `[MCP Startup Error] Failed to initialize log file at ${logFilePath}: ${err?.message || String(err)}`; 68 | console.error(errorMsg); 69 | if (err instanceof Error && err.stack) { 70 | console.error(err.stack); 71 | } 72 | process.exit(1); // Exit if we can't even log 73 | } 74 | 75 | // Add a global handler for uncaught exceptions 76 | process.on('uncaughtException', (err, origin) => { 77 | let message = `[MCP Server Log] Uncaught Exception. Origin: ${origin}. Error: ${err?.message || String(err)}`; 78 | logToFile(message); 79 | if (err && err.stack) { 80 | logToFile(err.stack); 81 | } 82 | // Optionally add more context 83 | logToFile('[MCP Server Log] Exiting due to uncaught exception.'); 84 | process.exit(1); // Exit cleanly 85 | }); 86 | 87 | logToFile('[MCP Server Log] Initializing GitHub Actions MCP Server...'); 88 | 89 | // Restore auth logic 90 | // Allow token via CLI argument `--token=<token>` or fallback to env var 91 | let cliToken: string | undefined; 92 | for (const arg of process.argv) { 93 | if (arg.startsWith('--token=')) { 94 | cliToken = arg.substring('--token='.length); 95 | break; 96 | } 97 | } 98 | 99 | const GITHUB_TOKEN = cliToken || process.env.GITHUB_PERSONAL_ACCESS_TOKEN; // Restore env check 100 | if (!GITHUB_TOKEN) { 101 | logToFile('FATAL: GITHUB_PERSONAL_ACCESS_TOKEN environment variable is not set.'); 102 | process.exit(1); 103 | } 104 | logToFile('[MCP Server Log] GitHub token found.'); // Restore original log message 105 | const octokit = new Octokit({ auth: GITHUB_TOKEN }); 106 | logToFile('[MCP Server Log] Octokit initialized.'); 107 | 108 | const server = new McpServer( 109 | { 110 | name: "github-actions-mcp-server", 111 | version: VERSION, 112 | context: { 113 | octokit: octokit 114 | } 115 | } 116 | ); 117 | 118 | // Restore error formatting function 119 | function formatGitHubError(error: GitHubError): string { 120 | let message = `GitHub API Error: ${error.message}`; 121 | 122 | if (error instanceof GitHubValidationError) { 123 | message = `Validation Error: ${error.message}`; 124 | if (error.response) { 125 | message += `\nDetails: ${JSON.stringify(error.response)}`; 126 | } 127 | } else if (error instanceof GitHubResourceNotFoundError) { 128 | message = `Not Found: ${error.message}`; 129 | } else if (error instanceof GitHubAuthenticationError) { 130 | message = `Authentication Failed: ${error.message}`; 131 | } else if (error instanceof GitHubPermissionError) { 132 | message = `Permission Denied: ${error.message}`; 133 | } else if (error instanceof GitHubRateLimitError) { 134 | message = `Rate Limit Exceeded: ${error.message}\nResets at: ${error.resetAt.toISOString()}`; 135 | } else if (error instanceof GitHubConflictError) { 136 | message = `Conflict: ${error.message}`; 137 | } else if (error instanceof GitHubTimeoutError) { 138 | message = `Timeout: ${error.message}\nTimeout setting: ${error.timeoutMs}ms`; 139 | } else if (error instanceof GitHubNetworkError) { 140 | message = `Network Error: ${error.message}\nError code: ${error.errorCode}`; 141 | } 142 | 143 | return message; 144 | } 145 | 146 | // Restore ListTools using server.tool() 147 | server.tool( 148 | "list_workflows", 149 | actions.ListWorkflowsSchema.shape, 150 | async (request: any) => { 151 | logToFile('[MCP Server Log] Received list_workflows request (via server.tool)'); 152 | // Args are already parsed by the McpServer using the provided schema 153 | const result = await actions.listWorkflows(request.owner, request.repo, request.page, request.perPage); 154 | return { content: [{ type: "text", text: JSON.stringify(result) }] }; 155 | } 156 | ); 157 | 158 | // Register other tools using server.tool() 159 | server.tool( 160 | "get_workflow", 161 | actions.GetWorkflowSchema.shape, 162 | async (request: any) => { 163 | const result = await actions.getWorkflow(request.owner, request.repo, request.workflowId); 164 | return { content: [{ type: "text", text: JSON.stringify(result) }] }; 165 | } 166 | ); 167 | 168 | server.tool( 169 | "get_workflow_usage", 170 | actions.GetWorkflowUsageSchema.shape, 171 | async (request: any) => { 172 | const result = await actions.getWorkflowUsage(request.owner, request.repo, request.workflowId); 173 | return { content: [{ type: "text", text: JSON.stringify(result) }] }; 174 | } 175 | ); 176 | 177 | server.tool( 178 | "list_workflow_runs", 179 | actions.ListWorkflowRunsSchema.shape, 180 | async (request: any) => { 181 | const { owner, repo, workflowId, ...options } = request; 182 | const result = await actions.listWorkflowRuns(owner, repo, { workflowId, ...options }); 183 | return { content: [{ type: "text", text: JSON.stringify(result) }] }; 184 | } 185 | ); 186 | 187 | server.tool( 188 | "get_workflow_run", 189 | actions.GetWorkflowRunSchema.shape, 190 | async (request: any) => { 191 | const result = await actions.getWorkflowRun(request.owner, request.repo, request.runId); 192 | return { content: [{ type: "text", text: JSON.stringify(result) }] }; 193 | } 194 | ); 195 | 196 | server.tool( 197 | "get_workflow_run_jobs", 198 | actions.GetWorkflowRunJobsSchema.shape, 199 | async (request: any) => { 200 | const { owner, repo, runId, filter, page, perPage } = request; 201 | const result = await actions.getWorkflowRunJobs(owner, repo, runId, filter, page, perPage); 202 | return { content: [{ type: "text", text: JSON.stringify(result) }] }; 203 | } 204 | ); 205 | 206 | server.tool( 207 | "trigger_workflow", 208 | actions.TriggerWorkflowSchema.shape, 209 | async (request: any) => { 210 | const { owner, repo, workflowId, ref, inputs } = request; 211 | const result = await actions.triggerWorkflow(owner, repo, workflowId, ref, inputs); 212 | return { content: [{ type: "text", text: JSON.stringify(result) }] }; 213 | } 214 | ); 215 | 216 | server.tool( 217 | "cancel_workflow_run", 218 | actions.CancelWorkflowRunSchema.shape, 219 | async (request: any) => { 220 | const result = await actions.cancelWorkflowRun(request.owner, request.repo, request.runId); 221 | return { content: [{ type: "text", text: JSON.stringify(result) }] }; 222 | } 223 | ); 224 | 225 | server.tool( 226 | "rerun_workflow", 227 | actions.RerunWorkflowSchema.shape, 228 | async (request: any) => { 229 | const result = await actions.rerunWorkflowRun(request.owner, request.repo, request.runId); 230 | return { content: [{ type: "text", text: JSON.stringify(result) }] }; 231 | } 232 | ); 233 | 234 | // Wrap server logic in a try/catch for initialization errors 235 | try { 236 | logToFile('[MCP Server Log] Server initialization complete. Ready for connection.'); 237 | // Attach stdio transport so Windsurf can communicate 238 | const transport = new StdioServerTransport(); 239 | await server.connect(transport); 240 | logToFile('[MCP Server Log] Connected via stdio transport.'); 241 | } catch (error: any) { 242 | // Ensure fatal errors during server setup are logged to the file. 243 | logToFile(`[MCP Server Log] FATAL Error during server setup: ${error?.message || String(error)}`); 244 | if (error instanceof Error && error.stack) { 245 | logToFile(error.stack); 246 | } 247 | // Do NOT use console.error here as it will interfere with MCP stdio 248 | process.exit(1); 249 | } 250 | 251 | // Add other process event handlers 252 | 253 | // Catch unhandled promise rejections, log them to file, and exit gracefully. 254 | process.on('unhandledRejection', (reason, promise) => { 255 | // Log unhandled promise rejections to the file. 256 | let reasonStr = reason instanceof Error ? reason.message : String(reason); 257 | // Including stack trace if available 258 | let stack = reason instanceof Error ? `\nStack: ${reason.stack}` : ''; 259 | logToFile(`[MCP Server Log] Unhandled Rejection at: ${promise}, reason: ${reasonStr}${stack}`); 260 | // Consider exiting depending on the severity or application logic 261 | // process.exit(1); // Optionally exit 262 | }); 263 | 264 | process.on('SIGINT', () => { 265 | logToFile('[MCP Server Log] Received SIGINT. Exiting gracefully.'); 266 | // Add any cleanup logic here 267 | process.exit(0); 268 | }); 269 | 270 | process.on('SIGTERM', () => { 271 | logToFile('[MCP Server Log] Received SIGTERM. Exiting gracefully.'); 272 | // Add any cleanup logic here 273 | process.exit(0); 274 | }); ```