#
tokens: 16359/50000 16/16 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | [![Verified on MseeP](https://mseep.ai/badge.svg)](https://mseep.ai/app/cad0f49e-1c4d-4ab1-97e4-2312da835454)
  2 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/ko1ynnky-github-actions-mcp-server-badge.png)](https://mseep.ai/app/ko1ynnky-github-actions-mcp-server)
  3 | 
  4 | # GitHub Actions MCP Server
  5 | 
  6 | [![smithery badge](https://smithery.ai/badge/@ko1ynnky/github-actions-mcp-server)](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 | });
```