#
tokens: 49956/50000 81/137 files (page 1/4)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 4. Use http://codebase.md/circleci-public/mcp-server-circleci?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .circleci
│   └── config.yml
├── .dockerignore
├── .github
│   ├── CODEOWNERS
│   ├── ISSUE_TEMPLATE
│   │   ├── BUG.yml
│   │   └── FEATURE_REQUEST.yml
│   └── PULL_REQUEST_TEMPLATE
│       └── PULL_REQUEST.md
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── eslint.config.js
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.md
├── renovate.json
├── scripts
│   └── create-tool.js
├── smithery.yaml
├── src
│   ├── circleci-tools.ts
│   ├── clients
│   │   ├── circleci
│   │   │   ├── configValidate.ts
│   │   │   ├── deploys.ts
│   │   │   ├── httpClient.test.ts
│   │   │   ├── httpClient.ts
│   │   │   ├── index.ts
│   │   │   ├── insights.ts
│   │   │   ├── jobs.ts
│   │   │   ├── jobsV1.ts
│   │   │   ├── pipelines.ts
│   │   │   ├── projects.ts
│   │   │   ├── tests.ts
│   │   │   ├── usage.ts
│   │   │   └── workflows.ts
│   │   ├── circleci-private
│   │   │   ├── index.ts
│   │   │   ├── jobsPrivate.ts
│   │   │   └── me.ts
│   │   ├── circlet
│   │   │   ├── circlet.ts
│   │   │   └── index.ts
│   │   ├── client.ts
│   │   └── schemas.ts
│   ├── index.ts
│   ├── lib
│   │   ├── flaky-tests
│   │   │   └── getFlakyTests.ts
│   │   ├── getWorkflowIdFromURL.test.ts
│   │   ├── getWorkflowIdFromURL.ts
│   │   ├── latest-pipeline
│   │   │   ├── formatLatestPipelineStatus.ts
│   │   │   └── getLatestPipelineWorkflows.ts
│   │   ├── mcpErrorOutput.test.ts
│   │   ├── mcpErrorOutput.ts
│   │   ├── mcpResponse.test.ts
│   │   ├── mcpResponse.ts
│   │   ├── outputTextTruncated.test.ts
│   │   ├── outputTextTruncated.ts
│   │   ├── pipeline-job-logs
│   │   │   ├── getJobLogs.ts
│   │   │   └── getPipelineJobLogs.ts
│   │   ├── pipeline-job-tests
│   │   │   ├── formatJobTests.ts
│   │   │   └── getJobTests.ts
│   │   ├── project-detection
│   │   │   ├── index.test.ts
│   │   │   ├── index.ts
│   │   │   └── vcsTool.ts
│   │   ├── rateLimitedRequests
│   │   │   ├── index.test.ts
│   │   │   └── index.ts
│   │   └── usage-api
│   │       ├── findUnderusedResourceClasses.test.ts
│   │       ├── findUnderusedResourceClasses.ts
│   │       ├── getUsageApiData.test.ts
│   │       ├── getUsageApiData.ts
│   │       └── parseDateTimeString.ts
│   ├── tools
│   │   ├── analyzeDiff
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── configHelper
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── createPromptTemplate
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── downloadUsageApiData
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── findUnderusedResourceClasses
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── getBuildFailureLogs
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── getFlakyTests
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── getJobTestResults
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── getLatestPipelineStatus
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── listComponentVersions
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── listFollowedProjects
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── recommendPromptTemplateTests
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── rerunWorkflow
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── runEvaluationTests
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── runPipeline
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── runRollbackPipeline
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   └── shared
│   │       └── constants.ts
│   └── transports
│       ├── stdio.ts
│       └── unified.ts
├── tsconfig.json
├── tsconfig.test.json
└── vitest.config.js
```

# Files

--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------

```
1 | 22.14.0
```

--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------

```
1 | dist/
2 | build/
```

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
1 | {
2 |   "printWidth": 80,
3 |   "singleQuote": true,
4 |   "semi": true
5 | }
```

--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------

```
1 | registry=https://registry.npmjs.org/
2 | @circleci:registry=https://registry.npmjs.org/
3 | save-exact=true
4 | engine-strict=true 
```

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
 1 | # Dependencies
 2 | node_modules
 3 | 
 4 | # Build outputs
 5 | dist
 6 | 
 7 | # Version control
 8 | .git
 9 | .gitignore
10 | .github
11 | 
12 | # CI/CD
13 | .circleci
14 | 
15 | # Environment and config
16 | .npmrc
17 | 
18 | # Docs
19 | *.md
20 | 
21 | # Misc
22 | .prettierrc
23 | .prettierignore
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Logs
  2 | logs
  3 | *.log
  4 | npm-debug.log*
  5 | yarn-debug.log*
  6 | yarn-error.log*
  7 | lerna-debug.log*
  8 | .pnpm-debug.log*
  9 | 
 10 | # Diagnostic reports (https://nodejs.org/api/report.html)
 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 12 | 
 13 | # Runtime data
 14 | pids
 15 | *.pid
 16 | *.seed
 17 | *.pid.lock
 18 | 
 19 | # Directory for instrumented libs generated by jscoverage/JSCover
 20 | lib-cov
 21 | 
 22 | # Coverage directory used by tools like istanbul
 23 | coverage
 24 | *.lcov
 25 | 
 26 | # nyc test coverage
 27 | .nyc_output
 28 | 
 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 30 | .grunt
 31 | 
 32 | # Bower dependency directory (https://bower.io/)
 33 | bower_components
 34 | 
 35 | # node-waf configuration
 36 | .lock-wscript
 37 | 
 38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 39 | build/Release
 40 | 
 41 | # Dependency directories
 42 | node_modules/
 43 | jspm_packages/
 44 | 
 45 | # Snowpack dependency directory (https://snowpack.dev/)
 46 | web_modules/
 47 | 
 48 | # TypeScript cache
 49 | *.tsbuildinfo
 50 | 
 51 | # Optional npm cache directory
 52 | .npm
 53 | 
 54 | # Optional eslint cache
 55 | .eslintcache
 56 | 
 57 | # Optional stylelint cache
 58 | .stylelintcache
 59 | 
 60 | # Microbundle cache
 61 | .rpt2_cache/
 62 | .rts2_cache_cjs/
 63 | .rts2_cache_es/
 64 | .rts2_cache_umd/
 65 | 
 66 | # Optional REPL history
 67 | .node_repl_history
 68 | 
 69 | # Output of 'npm pack'
 70 | *.tgz
 71 | 
 72 | # Yarn Integrity file
 73 | .yarn-integrity
 74 | 
 75 | # dotenv environment variable files
 76 | .env
 77 | .env.development.local
 78 | .env.test.local
 79 | .env.production.local
 80 | .env.local
 81 | 
 82 | # parcel-bundler cache (https://parceljs.org/)
 83 | .cache
 84 | .parcel-cache
 85 | 
 86 | # Next.js build output
 87 | .next
 88 | out
 89 | 
 90 | # Nuxt.js build / generate output
 91 | .nuxt
 92 | dist
 93 | 
 94 | # Gatsby files
 95 | .cache/
 96 | # Comment in the public line in if your project uses Gatsby and not Next.js
 97 | # https://nextjs.org/blog/next-9-1#public-directory-support
 98 | # public
 99 | 
100 | # vuepress build output
101 | .vuepress/dist
102 | 
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 | 
107 | # vitepress build output
108 | **/.vitepress/dist
109 | 
110 | # vitepress cache directory
111 | **/.vitepress/cache
112 | 
113 | # Docusaurus cache and generated files
114 | .docusaurus
115 | 
116 | # Serverless directories
117 | .serverless/
118 | 
119 | # FuseBox cache
120 | .fusebox/
121 | 
122 | # DynamoDB Local files
123 | .dynamodb/
124 | 
125 | # TernJS port file
126 | .tern-port
127 | 
128 | # Stores VSCode versions used for testing VSCode extensions
129 | .vscode-test
130 | 
131 | # yarn v2
132 | .yarn/cache
133 | .yarn/unplugged
134 | .yarn/build-state.yml
135 | .yarn/install-state.gz
136 | .pnp.*
137 | 
138 | .cursor
139 | 
140 | # VSCode settings
141 | .DS_Store
142 | 
143 | # CircleCI usage data exports
144 | usage-data-*.csv
145 | .claude
146 | .DS_Store
147 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
   1 | # CircleCI MCP Server
   2 | 
   3 | [![GitHub](https://img.shields.io/github/license/CircleCI-Public/mcp-server-circleci)](https://github.com/CircleCI-Public/mcp-server-circleci/blob/main/LICENSE)
   4 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/CircleCI-Public/mcp-server-circleci/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/CircleCI-Public/mcp-server-circleci/tree/main)
   5 | [![npm](https://img.shields.io/npm/v/@circleci/mcp-server-circleci?logo=npm)](https://www.npmjs.com/package/@circleci/mcp-server-circleci)
   6 | 
   7 | Model Context Protocol (MCP) is a [new, standardized protocol](https://modelcontextprotocol.io/introduction) for managing context between large language models (LLMs) and external systems. In this repository, we provide an MCP Server for [CircleCI](https://circleci.com).
   8 | 
   9 | This lets you use Cursor IDE, Windsurf, Copilot, or any MCP supported Client, to use natural language to accomplish things with CircleCI, e.g.:
  10 | 
  11 | - `Find the latest failed pipeline on my branch and get logs`
  12 |   https://github.com/CircleCI-Public/mcp-server-circleci/wiki#circleci-mcp-server-with-cursor-ide
  13 | 
  14 | https://github.com/user-attachments/assets/3c765985-8827-442a-a8dc-5069e01edb74
  15 | 
  16 | ## Requirements
  17 | 
  18 | - CircleCI Personal API Token - you can generate one through the CircleCI. [Learn more](https://circleci.com/docs/managing-api-tokens/) or [click here](https://app.circleci.com/settings/user/tokens) for quick access.
  19 | 
  20 | For NPX installation:
  21 | 
  22 | - pnpm package manager - [Learn more](https://pnpm.io/installation)
  23 | - Node.js >= v18.0.0
  24 | 
  25 | For Docker installation:
  26 | 
  27 | - Docker - [Learn more](https://docs.docker.com/get-docker/)
  28 | 
  29 | ## Installation
  30 | 
  31 | ### Cursor
  32 | 
  33 | #### Using NPX in a local MCP Server
  34 | 
  35 | Add the following to your cursor MCP config:
  36 | 
  37 | ```json
  38 | {
  39 |   "mcpServers": {
  40 |     "circleci-mcp-server": {
  41 |       "command": "npx",
  42 |       "args": ["-y", "@circleci/mcp-server-circleci@latest"],
  43 |       "env": {
  44 |         "CIRCLECI_TOKEN": "your-circleci-token",
  45 |         "CIRCLECI_BASE_URL": "https://circleci.com" // Optional - required for on-prem customers only
  46 |       }
  47 |     }
  48 |   }
  49 | }
  50 | ```
  51 | 
  52 | 
  53 | #### Using Docker in a local MCP Server
  54 | 
  55 | Add the following to your cursor MCP config:
  56 | 
  57 | ```json
  58 | {
  59 |   "mcpServers": {
  60 |     "circleci-mcp-server": {
  61 |       "command": "docker",
  62 |       "args": [
  63 |         "run",
  64 |         "--rm",
  65 |         "-i",
  66 |         "-e",
  67 |         "CIRCLECI_TOKEN",
  68 |         "-e",
  69 |         "CIRCLECI_BASE_URL",
  70 |         "circleci:mcp-server-circleci"
  71 |       ],
  72 |       "env": {
  73 |         "CIRCLECI_TOKEN": "your-circleci-token",
  74 |         "CIRCLECI_BASE_URL": "https://circleci.com" // Optional - required for on-prem customers only
  75 |       }
  76 |     }
  77 |   }
  78 | }
  79 | ```
  80 | 
  81 | #### Using a Self-Managed Remote MCP Server
  82 | 
  83 | Add the following to your cursor MCP config:
  84 | 
  85 | ```json
  86 | {
  87 |   "inputs": [
  88 |     {
  89 |       "type": "promptString",
  90 |       "id": "circleci-token", 
  91 |       "description": "CircleCI API Token",
  92 |       "password": true
  93 |     }
  94 |   ],
  95 |   "servers": {
  96 |     "circleci-mcp-server-remote": {
  97 |       "url": "http://your-circleci-remote-mcp-server-endpoint:8000/mcp"
  98 |     }
  99 |   }
 100 | }
 101 | ```
 102 | 
 103 | ### VS Code
 104 | 
 105 | #### Using NPX in a local MCP Server
 106 | 
 107 | To install CircleCI MCP Server for VS Code in `.vscode/mcp.json`:
 108 | 
 109 | ```json
 110 | {
 111 |   // 💡 Inputs are prompted on first server start, then stored securely by VS Code.
 112 |   "inputs": [
 113 |     {
 114 |       "type": "promptString",
 115 |       "id": "circleci-token",
 116 |       "description": "CircleCI API Token",
 117 |       "password": true
 118 |     },
 119 |     {
 120 |       "type": "promptString",
 121 |       "id": "circleci-base-url",
 122 |       "description": "CircleCI Base URL",
 123 |       "default": "https://circleci.com"
 124 |     }
 125 |   ],
 126 |   "servers": {
 127 |     // https://github.com/ppl-ai/modelcontextprotocol/
 128 |     "circleci-mcp-server": {
 129 |       "type": "stdio",
 130 |       "command": "npx",
 131 |       "args": ["-y", "@circleci/mcp-server-circleci@latest"],
 132 |       "env": {
 133 |         "CIRCLECI_TOKEN": "${input:circleci-token}",
 134 |         "CIRCLECI_BASE_URL": "${input:circleci-base-url}"
 135 |       }
 136 |     }
 137 |   }
 138 | }
 139 | ```
 140 | 
 141 | #### Using Docker in a local MCP Server
 142 | 
 143 | To install CircleCI MCP Server for VS Code in `.vscode/mcp.json` using Docker:
 144 | 
 145 | ```json
 146 | {
 147 |   // 💡 Inputs are prompted on first server start, then stored securely by VS Code.
 148 |   "inputs": [
 149 |     {
 150 |       "type": "promptString",
 151 |       "id": "circleci-token",
 152 |       "description": "CircleCI API Token",
 153 |       "password": true
 154 |     },
 155 |     {
 156 |       "type": "promptString",
 157 |       "id": "circleci-base-url",
 158 |       "description": "CircleCI Base URL",
 159 |       "default": "https://circleci.com"
 160 |     }
 161 |   ],
 162 |   "servers": {
 163 |     // https://github.com/ppl-ai/modelcontextprotocol/
 164 |     "circleci-mcp-server": {
 165 |       "type": "stdio",
 166 |       "command": "docker",
 167 |       "args": [
 168 |         "run",
 169 |         "--rm",
 170 |         "-i",
 171 |         "-e",
 172 |         "CIRCLECI_TOKEN",
 173 |         "-e",
 174 |         "CIRCLECI_BASE_URL",
 175 |         "circleci:mcp-server-circleci"
 176 |       ],
 177 |       "env": {
 178 |         "CIRCLECI_TOKEN": "${input:circleci-token}",
 179 |         "CIRCLECI_BASE_URL": "${input:circleci-base-url}"
 180 |       }
 181 |     }
 182 |   }
 183 | }
 184 | ```
 185 | 
 186 | #### Using a Self-Managed Remote MCP Server
 187 | 
 188 | To install CircleCI MCP Server for VS Code in `.vscode/mcp.json` using a self-managed remote MCP server:
 189 | 
 190 | ```json
 191 | {
 192 |   "servers": {
 193 |     "circleci-mcp-server-remote": {
 194 |       "type": "sse",
 195 |       "url": "http://your-circleci-remote-mcp-server-endpoint:8000/mcp"
 196 |     }
 197 |   }
 198 | }
 199 | ```
 200 | 
 201 | ### Claude Desktop
 202 | 
 203 | #### Using NPX in a local MCP Server
 204 | 
 205 | Add the following to your claude_desktop_config.json:
 206 | 
 207 | ```json
 208 | {
 209 |   "mcpServers": {
 210 |     "circleci-mcp-server": {
 211 |       "command": "npx",
 212 |       "args": ["-y", "@circleci/mcp-server-circleci@latest"],
 213 |       "env": {
 214 |         "CIRCLECI_TOKEN": "your-circleci-token",
 215 |         "CIRCLECI_BASE_URL": "https://circleci.com" // Optional - required for on-prem customers only
 216 |       }
 217 |     }
 218 |   }
 219 | }
 220 | ```
 221 | To locate this file:
 222 | 
 223 | macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
 224 | 
 225 | 
 226 | Windows: `%APPDATA%\Claude\claude_desktop_config.json`
 227 | 
 228 | [Claude Desktop setup](https://modelcontextprotocol.io/quickstart/user)
 229 | 
 230 | 
 231 | #### Using Docker in a local MCP Server
 232 | 
 233 | Add the following to your claude_desktop_config.json:
 234 | 
 235 | ```json
 236 | {
 237 |   "mcpServers": {
 238 |     "circleci-mcp-server": {
 239 |       "command": "docker",
 240 |       "args": [
 241 |         "run",
 242 |         "--rm",
 243 |         "-i",
 244 |         "-e",
 245 |         "CIRCLECI_TOKEN",
 246 |         "-e",
 247 |         "CIRCLECI_BASE_URL",
 248 |         "circleci:mcp-server-circleci"
 249 |       ],
 250 |       "env": {
 251 |         "CIRCLECI_TOKEN": "your-circleci-token",
 252 |         "CIRCLECI_BASE_URL": "https://circleci.com" // Optional - required for on-prem customers only
 253 |       }
 254 |     }
 255 |   }
 256 | }
 257 | ```
 258 | 
 259 | To find/create this file, first open your claude desktop settings. Then click on "Developer" in the left-hand bar of the Settings pane, and then click on "Edit Config"
 260 | 
 261 | This will create a configuration file at:
 262 | 
 263 | - macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
 264 | - Windows: %APPDATA%\Claude\claude_desktop_config.json
 265 | 
 266 | See the guide below for more information on using MCP servers with Claude Desktop:
 267 | https://modelcontextprotocol.io/quickstart/user
 268 | 
 269 | #### Using a Self-Managed Remote MCP Server
 270 | 
 271 | Create a wrapper script first
 272 | 
 273 | Create a script file such as 'circleci-remote-mcp.sh':
 274 | 
 275 | ```bash
 276 | #!/bin/bash
 277 | export CIRCLECI_TOKEN="your-circleci-token"
 278 | npx mcp-remote http://your-circleci-remote-mcp-server-endpoint:8000/mcp --allow-http 
 279 | ```
 280 | 
 281 | Make it executable:
 282 | 
 283 | ```bash
 284 | chmod +x circleci-remote-mcp.sh
 285 | ```
 286 | 
 287 | Then add the following to your claude_desktop_config.json:
 288 | 
 289 | ```json
 290 | {
 291 |   "mcpServers": {
 292 |     "circleci-remote-mcp-server": {
 293 |       "command": "/full/path/to/circleci-remote-mcp.sh"
 294 |     }
 295 |   }
 296 | }
 297 | ```
 298 | 
 299 | To find/create this file, first open your Claude Desktop settings. Then click on "Developer" in the left-hand bar of the Settings pane, and then click on "Edit Config"
 300 | 
 301 | This will create a configuration file at:
 302 | 
 303 | - macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
 304 | - Windows: %APPDATA%\Claude\claude_desktop_config.json
 305 | 
 306 | See the guide below for more information on using MCP servers with Claude Desktop:
 307 | https://modelcontextprotocol.io/quickstart/user
 308 | 
 309 | ### Claude Code
 310 | 
 311 | #### Using NPX in a local MCP Server
 312 | 
 313 | After installing Claude Code, run the following command:
 314 | 
 315 | ```bash
 316 | claude mcp add circleci-mcp-server -e CIRCLECI_TOKEN=your-circleci-token -- npx -y @circleci/mcp-server-circleci@latest
 317 | ```
 318 | 
 319 | #### Using Docker in a local MCP Server
 320 | 
 321 | After installing Claude Code, run the following command:
 322 | 
 323 | ```bash
 324 | claude mcp add circleci-mcp-server -e CIRCLECI_TOKEN=your-circleci-token -e CIRCLECI_BASE_URL=https://circleci.com -- docker run --rm -i -e CIRCLECI_TOKEN -e CIRCLECI_BASE_URL circleci:mcp-server-circleci
 325 | ```
 326 | 
 327 | See the guide below for more information on using MCP servers with Claude Code:
 328 | https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/tutorials#set-up-model-context-protocol-mcp
 329 | 
 330 | #### Using Self-Managed Remote MCP Server
 331 | 
 332 | After installing Claude Code, run the following command:
 333 | 
 334 | ```bash
 335 | claude mcp add circleci-mcp-server -e CIRCLECI_TOKEN=your-circleci-token -- npx mcp-remote http://your-circleci-remote-mcp-server-endpoint:8000/mcp --allow-http
 336 | ```
 337 | 
 338 | See the guide below for more information on using MCP servers with Claude Code:
 339 | https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/tutorials#set-up-model-context-protocol-mcp
 340 | 
 341 | ### Windsurf
 342 | 
 343 | #### Using NPX in a local MCP Server
 344 | 
 345 | Add the following to your windsurf mcp_config.json:
 346 | 
 347 | ```json
 348 | {
 349 |   "mcpServers": {
 350 |     "circleci-mcp-server": {
 351 |       "command": "npx",
 352 |       "args": ["-y", "@circleci/mcp-server-circleci@latest"],
 353 |       "env": {
 354 |         "CIRCLECI_TOKEN": "your-circleci-token",
 355 |         "CIRCLECI_BASE_URL": "https://circleci.com" // Optional - required for on-prem customers only
 356 |       }
 357 |     }
 358 |   }
 359 | }
 360 | ```
 361 | 
 362 | #### Using Docker in a local MCP Server
 363 | 
 364 | Add the following to your windsurf mcp_config.json:
 365 | 
 366 | ```json
 367 | {
 368 |   "mcpServers": {
 369 |     "circleci-mcp-server": {
 370 |       "command": "docker",
 371 |       "args": [
 372 |         "run",
 373 |         "--rm",
 374 |         "-i",
 375 |         "-e",
 376 |         "CIRCLECI_TOKEN",
 377 |         "-e",
 378 |         "CIRCLECI_BASE_URL",
 379 |         "circleci:mcp-server-circleci"
 380 |       ],
 381 |       "env": {
 382 |         "CIRCLECI_TOKEN": "your-circleci-token",
 383 |         "CIRCLECI_BASE_URL": "https://circleci.com" // Optional - required for on-prem customers only
 384 |       }
 385 |     }
 386 |   }
 387 | }
 388 | ```
 389 | 
 390 | #### Using Self-Managed Remote MCP Server
 391 | 
 392 | Add the following to your windsurf mcp_config.json:
 393 | 
 394 | ```json
 395 | {
 396 |   "mcpServers": {
 397 |     "circleci": {
 398 |       "command": "npx",
 399 |       "args": [
 400 |         "mcp-remote",
 401 |         "http://your-circleci-remote-mcp-server-endpoint:8000/mcp",
 402 |         "--allow-http"
 403 |       ],
 404 |       "disabled": false,
 405 |       "alwaysAllow": []
 406 |     }
 407 |   }
 408 | }
 409 | ```
 410 | 
 411 | See the guide below for more information on using MCP servers with windsurf:
 412 | https://docs.windsurf.com/windsurf/mcp
 413 | 
 414 | ### Installing via Smithery
 415 | 
 416 | To install CircleCI MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@CircleCI-Public/mcp-server-circleci):
 417 | 
 418 | ```bash
 419 | npx -y @smithery/cli install @CircleCI-Public/mcp-server-circleci --client claude
 420 | ```
 421 | 
 422 | ### Amazon Q Developer CLi
 423 | 
 424 | MCP client configuration in Amazon Q Developer is stored in JSON format, in a file named mcp.json.
 425 | 
 426 | Amazon Q Developer CLI supports two levels of MCP configuration:
 427 | 
 428 | Global Configuration: ~/.aws/amazonq/mcp.json - Applies to all workspaces
 429 | 
 430 | Workspace Configuration: .amazonq/mcp.json - Specific to the current workspace
 431 | 
 432 | Both files are optional; neither, one, or both can exist. If both files exist, Amazon Q Developer reads MCP configuration from both and combines them, taking the union of their contents. If there is a conflict (i.e., a server defined in the global config is also present in the workspace config), a warning is displayed and only the server entry in the workspace config is used.
 433 | 
 434 | #### Using NPX in a local MCP Server
 435 | 
 436 | Edit your global configuration file ~/.aws/amazonq/mcp.json or create a new one in the current workspace .amazonq/mcp.json with the following content:
 437 | 
 438 | ```json
 439 | {
 440 |   "mcpServers": {
 441 |     "circleci-local": {
 442 |       "command": "npx",
 443 |       "args": [
 444 |         "-y",
 445 |         "@circleci/mcp-server-circleci@latest"
 446 |       ],
 447 |       "env": {
 448 |         "CIRCLECI_TOKEN": "YOUR_CIRCLECI_TOKEN",
 449 |         "CIRCLECI_BASE_URL": "https://circleci.com" // Optional - required for on-prem customers only
 450 |       },
 451 |       "timeout": 60000
 452 |     }
 453 |   }
 454 | }
 455 | ```
 456 | 
 457 | #### Using a Self-Managed Remote MCP Server
 458 | 
 459 | Create a wrapper script first
 460 | 
 461 | Create a script file such as 'circleci-remote-mcp.sh':
 462 | 
 463 | ```bash
 464 | #!/bin/bash
 465 | export CIRCLECI_TOKEN="your-circleci-token"
 466 | npx mcp-remote http://your-circleci-remote-mcp-server-endpoint:8000/mcp --allow-http
 467 | ```
 468 | 
 469 | Make it executable:
 470 | 
 471 | ```bash
 472 | chmod +x circleci-remote-mcp.sh
 473 | ```
 474 | 
 475 | Then add it:
 476 | 
 477 | ```bash
 478 | q mcp add --name circleci --command "/full/path/to/circleci-remote-mcp.sh"
 479 | ```
 480 | 
 481 | ### Amazon Q Developer in the IDE
 482 | 
 483 | #### Using NPX in a local MCP Server
 484 | 
 485 | Edit your global configuration file ~/.aws/amazonq/mcp.json or create a new one in the current workspace .amazonq/mcp.json with the following content:
 486 | 
 487 | ```json
 488 | {
 489 |   "mcpServers": {
 490 |     "circleci-local": {
 491 |       "command": "npx",
 492 |       "args": [
 493 |         "-y",
 494 |         "@circleci/mcp-server-circleci@latest"
 495 |       ],
 496 |       "env": {
 497 |         "CIRCLECI_TOKEN": "YOUR_CIRCLECI_TOKEN",
 498 |         "CIRCLECI_BASE_URL": "https://circleci.com" // Optional - required for on-prem customers only
 499 |       },
 500 |       "timeout": 60000
 501 |     }
 502 |   }
 503 | }
 504 | ```
 505 | 
 506 | #### Using a Self-Managed Remote MCP Server
 507 | 
 508 | Create a wrapper script first
 509 | 
 510 | Create a script file such as 'circleci-remote-mcp.sh':
 511 | 
 512 | ```bash
 513 | #!/bin/bash
 514 | npx mcp-remote http://your-circleci-remote-mcp-server-endpoint:8000/mcp --allow-http
 515 | ```
 516 | 
 517 | Make it executable:
 518 | 
 519 | ```bash
 520 | chmod +x circleci-remote-mcp.sh
 521 | ```
 522 | 
 523 | Then add it to the Q Developer in your IDE:
 524 | 
 525 | Access the MCP configuration UI (https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/mcp-ide.html#mcp-ide-configuration-access-ui).
 526 | 
 527 | Choose the plus (+) symbol.
 528 | 
 529 | Select the scope: global or local.
 530 | 
 531 | If you select global scope, the MCP server configuration is stored in ~/.aws/amazonq/mcp.json and available across all your projects. If you select local scope, the configuration is stored in .amazonq/mcp.json within your current project.
 532 | 
 533 | In the Name field, enter the name of the CircleCI remote MCP server (e.g. circleci-remote-mcp).
 534 | 
 535 | Select the transport protocol (stdio).
 536 | 
 537 | In the Command field, enter the shell command created previously that the MCP server will run when it initializes (e.g. /full/path/to/circleci-remote-mcp.sh).
 538 | 
 539 | Click the Save button.
 540 | 
 541 | # Features
 542 | 
 543 | ## Supported Tools
 544 | 
 545 | - `get_build_failure_logs`
 546 | 
 547 |   Retrieves detailed failure logs from CircleCI builds. This tool can be used in three ways:
 548 | 
 549 |   1. Using Project Slug and Branch (Recommended Workflow):
 550 | 
 551 |      - First, list your available projects:
 552 |        - Use the list_followed_projects tool to get your projects
 553 |        - Example: "List my CircleCI projects"
 554 |        - Then choose the project, which has a projectSlug associated with it
 555 |        - Example: "Lets use my-project"
 556 |      - Then ask to retrieve the build failure logs for a specific branch:
 557 |        - Example: "Get build failures for my-project on the main branch"
 558 | 
 559 |   2. Using CircleCI URLs:
 560 | 
 561 |      - Provide a failed job URL or pipeline URL directly
 562 |      - Example: "Get logs from https://app.circleci.com/pipelines/github/org/repo/123"
 563 | 
 564 |   3. Using Local Project Context:
 565 |      - Works from your local workspace by providing:
 566 |        - Workspace root path
 567 |        - Git remote URL
 568 |        - Branch name
 569 |      - Example: "Find the latest failed pipeline on my current branch"
 570 | 
 571 |   The tool returns formatted logs including:
 572 | 
 573 |   - Job names
 574 |   - Step-by-step execution details
 575 |   - Failure messages and context
 576 | 
 577 |   This is particularly useful for:
 578 | 
 579 |   - Debugging failed builds
 580 |   - Analyzing test failures
 581 |   - Investigating deployment issues
 582 |   - Quick access to build logs without leaving your IDE
 583 | 
 584 | - `find_flaky_tests`
 585 | 
 586 |   Identifies flaky tests in your CircleCI project by analyzing test execution history. This leverages the flaky test detection feature described here: https://circleci.com/blog/introducing-test-insights-with-flaky-test-detection/#flaky-test-detection
 587 | 
 588 |   This tool can be used in three ways:
 589 | 
 590 |   1. Using Project Slug (Recommended Workflow):
 591 | 
 592 |      - First, list your available projects:
 593 |        - Use the list_followed_projects tool to get your projects
 594 |        - Example: "List my CircleCI projects"
 595 |        - Then choose the project, which has a projectSlug associated with it
 596 |        - Example: "Lets use my-project"
 597 |      - Then ask to retrieve the flaky tests:
 598 |        - Example: "Get flaky tests for my-project"
 599 | 
 600 |   2. Using CircleCI Project URL:
 601 | 
 602 |      - Provide the project URL directly from CircleCI
 603 |      - Example: "Find flaky tests in https://app.circleci.com/pipelines/github/org/repo"
 604 | 
 605 |   3. Using Local Project Context:
 606 |      - Works from your local workspace by providing:
 607 |        - Workspace root path
 608 |        - Git remote URL
 609 |      - Example: "Find flaky tests in my current project"
 610 | 
 611 |   The tool can be used in two ways:
 612 |   1. Using text output mode (default):
 613 |      - This will return the flaky tests and their details in a text format
 614 |   2. Using file output mode: (requires the `FILE_OUTPUT_DIRECTORY` environment variable to be set)
 615 |      - This will create a directory with the flaky tests and their details
 616 | 
 617 |   The tool returns detailed information about flaky tests, including:
 618 | 
 619 |   - Test names and file locations
 620 |   - Failure messages and contexts
 621 | 
 622 |   This helps you:
 623 | 
 624 |   - Identify unreliable tests in your test suite
 625 |   - Get detailed context about test failures
 626 |   - Make data-driven decisions about test improvements
 627 | 
 628 | - `get_latest_pipeline_status`
 629 | 
 630 |   Retrieves the status of the latest pipeline for a given branch. This tool can be used in three ways:
 631 | 
 632 |   1. Using Project Slug and Branch (Recommended Workflow):
 633 | 
 634 |      - First, list your available projects:
 635 |        - Use the list_followed_projects tool to get your projects
 636 |        - Example: "List my CircleCI projects"
 637 |        - Then choose the project, which has a projectSlug associated with it
 638 |        - Example: "Lets use my-project"
 639 |      - Then ask to retrieve the latest pipeline status for a specific branch:
 640 |        - Example: "Get the status of the latest pipeline for my-project on the main branch"
 641 | 
 642 |   2. Using CircleCI Project URL:
 643 | 
 644 |      - Provide the project URL directly from CircleCI
 645 |      - Example: "Get the status of the latest pipeline for https://app.circleci.com/pipelines/github/org/repo"
 646 | 
 647 |   3. Using Local Project Context:
 648 |      - Works from your local workspace by providing:
 649 |        - Workspace root path
 650 |        - Git remote URL
 651 |        - Branch name
 652 |      - Example: "Get the status of the latest pipeline for my current project"
 653 | 
 654 |   The tool returns a formatted status of the latest pipeline:
 655 | 
 656 |   - Workflow names and their current status
 657 |   - Duration of each workflow
 658 |   - Creation and completion timestamps
 659 |   - Overall pipeline health
 660 | 
 661 |   Example output:
 662 | 
 663 |   ```
 664 |   ---
 665 |   Workflow: build
 666 |   Status: success
 667 |   Duration: 5 minutes
 668 |   Created: 4/20/2025, 10:15:30 AM
 669 |   Stopped: 4/20/2025, 10:20:45 AM
 670 |   ---
 671 |   Workflow: test
 672 |   Status: running
 673 |   Duration: unknown
 674 |   Created: 4/20/2025, 10:21:00 AM
 675 |   Stopped: in progress
 676 |   ```
 677 | 
 678 |   This is particularly useful for:
 679 | 
 680 |   - Checking the status of the latest pipeline
 681 |   - Getting the status of the latest pipeline for a specific branch
 682 |   - Quickly checking the status of the latest pipeline without leaving your IDE
 683 | 
 684 | - `get_job_test_results`
 685 | 
 686 |   Retrieves test metadata for CircleCI jobs, allowing you to analyze test results without leaving your IDE. This tool can be used in three ways:
 687 | 
 688 |   1. Using Project Slug and Branch (Recommended Workflow):
 689 | 
 690 |      - First, list your available projects:
 691 |        - Use the list_followed_projects tool to get your projects
 692 |        - Example: "List my CircleCI projects"
 693 |        - Then choose the project, which has a projectSlug associated with it
 694 |        - Example: "Lets use my-project"
 695 |      - Then ask to retrieve the test results for a specific branch:
 696 |        - Example: "Get test results for my-project on the main branch"
 697 | 
 698 |   2. Using CircleCI URL:
 699 | 
 700 |      - Provide a CircleCI URL in any of these formats:
 701 |        - Job URL: "https://app.circleci.com/pipelines/github/org/repo/123/workflows/abc-def/jobs/789"
 702 |        - Workflow URL: "https://app.circleci.com/pipelines/github/org/repo/123/workflows/abc-def"
 703 |        - Pipeline URL: "https://app.circleci.com/pipelines/github/org/repo/123"
 704 |      - Example: "Get test results for https://app.circleci.com/pipelines/github/org/repo/123/workflows/abc-def"
 705 | 
 706 |   3. Using Local Project Context:
 707 |      - Works from your local workspace by providing:
 708 |        - Workspace root path
 709 |        - Git remote URL
 710 |        - Branch name
 711 |      - Example: "Get test results for my current project on the main branch"
 712 | 
 713 |   The tool returns detailed test result information:
 714 | 
 715 |   - Summary of all tests (total, successful, failed)
 716 |   - Detailed information about failed tests including:
 717 |     - Test name and class
 718 |     - File location
 719 |     - Error messages
 720 |     - Runtime duration
 721 |   - List of successful tests with timing information
 722 |   - Filter by tests result
 723 | 
 724 |   This is particularly useful for:
 725 | 
 726 |   - Quickly analyzing test failures without visiting the CircleCI web UI
 727 |   - Identifying patterns in test failures
 728 |   - Finding slow tests that might need optimization
 729 |   - Checking test coverage across your project
 730 |   - Troubleshooting flaky tests
 731 | 
 732 |   Note: The tool requires that test metadata is properly configured in your CircleCI config. For more information on setting up test metadata collection, see:
 733 |   https://circleci.com/docs/collect-test-data/
 734 | 
 735 | - `config_helper`
 736 | 
 737 |   Assists with CircleCI configuration tasks by providing guidance and validation. This tool helps you:
 738 | 
 739 |   1. Validate CircleCI Config:
 740 |      - Checks your .circleci/config.yml for syntax and semantic errors
 741 |      - Example: "Validate my CircleCI config"
 742 | 
 743 |   The tool provides:
 744 | 
 745 |   - Detailed validation results
 746 |   - Configuration recommendations
 747 | 
 748 |   This helps you:
 749 | 
 750 |   - Catch configuration errors before pushing
 751 |   - Learn CircleCI configuration best practices
 752 |   - Troubleshoot configuration issues
 753 |   - Implement CircleCI features correctly
 754 | 
 755 | - `create_prompt_template`
 756 | 
 757 |   Helps generate structured prompt templates for AI-enabled applications based on feature requirements. This tool:
 758 | 
 759 |   1. Converts Feature Requirements to Structured Prompts:
 760 |      - Transforms user requirements into optimized prompt templates
 761 |      - Example: "Create a prompt template for generating bedtime stories by age and topic"
 762 | 
 763 |   The tool provides:
 764 | 
 765 |   - A structured prompt template
 766 |   - A context schema defining required input parameters
 767 | 
 768 |   This helps you:
 769 | 
 770 |   - Create effective prompts for AI applications
 771 |   - Standardize input parameters for consistent results
 772 |   - Build robust AI-powered features
 773 | 
 774 | - `recommend_prompt_template_tests`
 775 | 
 776 |   Generates test cases for prompt templates to ensure they produce expected results. This tool:
 777 | 
 778 |   1. Provides Test Cases for Prompt Templates:
 779 |      - Creates diverse test scenarios based on your prompt template and context schema
 780 |      - Example: "Generate tests for my bedtime story prompt template"
 781 | 
 782 |   The tool provides:
 783 | 
 784 |   - An array of recommended test cases
 785 |   - Various parameter combinations to test template robustness
 786 | 
 787 |   This helps you:
 788 | 
 789 |   - Validate prompt template functionality
 790 |   - Ensure consistent AI responses across inputs
 791 |   - Identify edge cases and potential issues
 792 |   - Improve overall AI application quality
 793 | 
 794 | - `list_followed_projects`
 795 | 
 796 |   Lists all projects that the user is following on CircleCI. This tool:
 797 | 
 798 |   1. Retrieves and Displays Projects:
 799 |      - Shows all projects the user has access to and is following
 800 |      - Provides the project name and projectSlug for each entry
 801 |      - Example: "List my CircleCI projects"
 802 | 
 803 |   The tool returns a formatted list of projects, example output:
 804 | 
 805 |   ```
 806 |   Projects followed:
 807 |   1. my-project (projectSlug: gh/organization/my-project)
 808 |   2. another-project (projectSlug: gh/organization/another-project)
 809 |   ```
 810 | 
 811 |   This is particularly useful for:
 812 | 
 813 |   - Identifying which CircleCI projects are available to you
 814 |   - Obtaining the projectSlug needed for other CircleCI tools
 815 |   - Selecting a project for subsequent operations
 816 | 
 817 |   Note: The projectSlug (not the project name) is required for many other CircleCI tools, and will be used for those tool calls after a project is selected.
 818 | 
 819 | - `run_pipeline`
 820 | 
 821 |   Triggers a pipeline to run. This tool can be used in three ways:
 822 | 
 823 |   1. Using Project Slug and Branch (Recommended Workflow):
 824 | 
 825 |      - First, list your available projects:
 826 |        - Use the list_followed_projects tool to get your projects
 827 |        - Example: "List my CircleCI projects"
 828 |        - Then choose the project, which has a projectSlug associated with it
 829 |        - Example: "Lets use my-project"
 830 |      - Then ask to run the pipeline for a specific branch:
 831 |        - Example: "Run the pipeline for my-project on the main branch"
 832 | 
 833 |   2. Using CircleCI URL:
 834 | 
 835 |      - Provide a CircleCI URL in any of these formats:
 836 |        - Job URL: "https://app.circleci.com/pipelines/github/org/repo/123/workflows/abc-def/jobs/789"
 837 |        - Workflow URL: "https://app.circleci.com/pipelines/github/org/repo/123/workflows/abc-def"
 838 |        - Pipeline URL: "https://app.circleci.com/pipelines/github/org/repo/123"
 839 |        - Project URL with branch: "https://app.circleci.com/projects/github/org/repo?branch=main"
 840 |      - Example: "Run the pipeline for https://app.circleci.com/pipelines/github/org/repo/123/workflows/abc-def"
 841 | 
 842 |   3. Using Local Project Context:
 843 |      - Works from your local workspace by providing:
 844 |        - Workspace root path
 845 |        - Git remote URL
 846 |        - Branch name
 847 |      - Example: "Run the pipeline for my current project on the main branch"
 848 | 
 849 |   The tool returns a link to monitor the pipeline execution.
 850 | 
 851 |   This is particularly useful for:
 852 | 
 853 |   - Quickly running pipelines without visiting the CircleCI web UI
 854 |   - Running pipelines from a specific branch
 855 | 
 856 | - `run_rollback_pipeline`
 857 | 
 858 |   This tool allows for triggering a rollback for a project.
 859 |   It requires the following parameters;
 860 | 
 861 |   - `project_id` - The ID of the CircleCI project (UUID)
 862 |   - `environmentName` - The environment name
 863 |   - `componentName` - The component name
 864 |   - `currentVersion` - The current version
 865 |   - `targetVersion` - The target version
 866 |   - `namespace` - The namespace of the component
 867 |   - `reason` - The reason for the rollback (optional)
 868 |   - `parameters` - The extra parameters for the rollback pipeline (optional)
 869 | 
 870 |   If not all the parameters are provided right away, the toll will make use of other tools to try and retrieve all the required info.
 871 |   The rollback can be performed in two different way, depending on whether a rollback pipeline definition has been configured for the project:
 872 | 
 873 |   - Pipeline Rollback: will trigger the rollback pipeline.
 874 |   - Workflow Rerun: will trigger the rerun of a previous workflow.
 875 | 
 876 |   A typical interaction with this tool will follow this pattern:
 877 | 
 878 |   1. Project Selection - Retrieve list of followed projects and prompt user to select one
 879 |   2. Environment Selection - List available environments and select target (auto-select if only one exists)
 880 |   3. Component Selection - List available components and select target (auto-select if only one exists)
 881 |   4. Version Selection - Display available versions, user selects non-live version for rollback
 882 |   5. Rollback Mode Detection - Check if rollback pipeline is configured for the selected project
 883 |   6. Execute Rollback - Two options available:
 884 |     - Pipeline Rollback: Prompt for optional reason, execute rollback pipeline
 885 |     - Workflow Rerun**: Rerun workflow using selected version's workflow ID
 886 |   7. Confirmation - Summarize rollback request and confirm before execution
 887 | 
 888 | - `rerun_workflow`
 889 | 
 890 |   Reruns a workflow from its start or from the failed job.
 891 | 
 892 |   The tool returns the ID of the newly-created workflow, and a link to monitor the new workflow.
 893 | 
 894 |   This is particularly useful for:
 895 | 
 896 |   - Quickly rerunning a workflow from its start or from the failed job without visiting the CircleCI web UI
 897 | 
 898 | - `analyze_diff`
 899 | 
 900 |   Analyzes git diffs against cursor rules to identify rule violations.
 901 | 
 902 |   This tool can be used by providing:
 903 | 
 904 |   1. Git Diff Content:
 905 | 
 906 |      - Staged changes: `git diff --cached`
 907 |      - Unstaged changes: `git diff`
 908 |      - All changes: `git diff HEAD`
 909 |      - Example: "Analyze my staged changes against the cursor rules"
 910 | 
 911 |   2. Repository Rules:
 912 |      - Rules from `.cursorrules` file in your repository root
 913 |      - Rules from `.cursor/rules` directory
 914 |      - Multiple rule files combined with `---` separator
 915 |      - Example: "Check my diff against the TypeScript coding standards"
 916 | 
 917 |   The tool provides:
 918 | 
 919 |   - Detailed violation reports with confidence scores
 920 |   - Specific explanations for each rule violation
 921 | 
 922 |   Example usage scenarios:
 923 | 
 924 |   - "Analyze my staged changes for any rule violations"
 925 |   - "Check my unstaged changes against rules"
 926 | 
 927 |   This is particularly useful for:
 928 | 
 929 |   - Pre-commit code quality checks
 930 |   - Ensuring consistency with team coding standards
 931 |   - Catching rule violations before code review
 932 | 
 933 |   The tool integrates with your existing cursor rules setup and provides immediate feedback on code quality, helping you catch issues early in the development process.
 934 | 
 935 | - `list_component_versions`
 936 | 
 937 |   Lists all versions for a specific CircleCI component in an environment. This tool retrieves version history including deployment status, commit information, and timestamps for a component.
 938 |   The tool will prompt the user to select the component and environment from a list if not provided.
 939 | 
 940 |   Example output:
 941 | 
 942 |   ```
 943 |   Versions for the component: {
 944 |     "items": [
 945 |       {
 946 |         "name": "v1.2.0",
 947 |         "namespace": "production",
 948 |         "environment_id": "env-456def",
 949 |         "is_live": true,
 950 |         "pipeline_id": "12345678-1234-1234-1234-123456789abc",
 951 |         "workflow_id": "87654321-4321-4321-4321-cba987654321",
 952 |         "job_id": "11111111-1111-1111-1111-111111111111",
 953 |         "job_number": 42,
 954 |         "last_deployed_at": "2023-01-01T00:00:00Z"
 955 |       },
 956 |       {
 957 |         "name": "v1.1.0",
 958 |         "namespace": "production", 
 959 |         "environment_id": "env-456def",
 960 |         "is_live": false,
 961 |         "pipeline_id": "22222222-2222-2222-2222-222222222222",
 962 |         "workflow_id": "33333333-3333-3333-3333-333333333333",
 963 |         "job_id": "44444444-4444-4444-4444-444444444444",
 964 |         "job_number": 38,
 965 |         "last_deployed_at": "2023-01-03T00:00:00Z"
 966 |       }
 967 |     ]
 968 |   }
 969 |   ```
 970 | 
 971 |   This is useful for:
 972 | 
 973 |   - Identifying which versions were deployed for a component
 974 |   - Finding the currently live version in an environment
 975 |   - Selecting target versions for rollback operations
 976 |   - Getting deployment details like pipeline, workflow, and job information
 977 |   - Listing all environments
 978 |   - Listing all components
 979 | 
 980 | - `download_usage_api_data`
 981 | 
 982 |   Downloads usage data from the CircleCI Usage API for a given organization. Accepts flexible, natural language date input (e.g., "March 2025" or "last month"). Cloud-only feature.
 983 | 
 984 |   This tool can be used in one of two ways:
 985 | 
 986 |   1) Start a new export job for a date range (max 32 days) by providing:
 987 |   - orgId: Organization ID
 988 |   - startDate: Start date (YYYY-MM-DD or natural language)
 989 |   - endDate: End date (YYYY-MM-DD or natural language)
 990 |   - outputDir: Directory to save the CSV file
 991 | 
 992 |   2) Check/download an existing export job by providing:
 993 |   - orgId: Organization ID
 994 |   - jobId: Usage export job ID
 995 |   - outputDir: Directory to save the CSV file
 996 | 
 997 |   The tool provides:
 998 |   - A csv containing the CircleCI Usage API data from the specified time frame
 999 | 
1000 |   This is useful for:
1001 |   - Downloading detailed CircleCI usage data for reporting or analysis
1002 |   - Feeding usage data into the `find_underused_resource_classes` tool
1003 | 
1004 |   Example usage scenarios:
1005 | - Scenario 1:
1006 |   1. "Download usage data for org abc123 from June into ~/Downloads"
1007 |   2. "Check status"
1008 | 
1009 | - Scenario 2:
1010 |   1. "Download usage data for org abc123 for last month to my Downloads folder"
1011 |   2. "Check usage download status"
1012 |   3. "Check status again"
1013 | 
1014 | - Scenario 3:
1015 |   1. "Check my usage export job usage-job-9f2d7c and download it if ready"
1016 | 
1017 | - `find_underused_resource_classes`
1018 | 
1019 |   Analyzes a CircleCI usage data CSV file to find jobs/resource classes with average or max CPU/RAM usage below a given threshold (default 40%).
1020 | 
1021 |   This tool can be used by providing:
1022 |   - A csv containing CircleCI Usage API data, which can be obtained by using the `download_usage_api_data` tool.
1023 | 
1024 |   The tool provides:
1025 |   - A markdown list of all jobs that are below the threshold, delineated by project and workflow.
1026 | 
1027 |   This is useful for:
1028 |   - Finding jobs that are using less than half of the compute provided to them on average
1029 |   - Generating a list of low hanging cost optimizations
1030 | 
1031 |   Example usage scenarios:
1032 |   - Scenario 1:
1033 |     1. "Find underused resource classes in the file you just downloaded"
1034 |   - Scenario 2:
1035 |     1. "Find underused resource classes in ~/Downloads/usage-data-2025-06-01_2025-06-30.csv"
1036 |   - Scenario 3:
1037 |     1. "Analyze /Users/you/Projects/acme/usage-data-job-9f2d7c.csv with threshold 30"
1038 | 
1039 | ## Troubleshooting
1040 | 
1041 | ### Quick Fixes
1042 | 
1043 | **Most Common Issues:**
1044 | 
1045 | 1. **Clear package caches:**
1046 |    ```bash
1047 |    npx clear-npx-cache
1048 |    npm cache clean --force
1049 |    ```
1050 | 
1051 | 2. **Force latest version:** Add `@latest` to your config:
1052 |    ```json
1053 |    "args": ["-y", "@circleci/mcp-server-circleci@latest"]
1054 |    ```
1055 | 
1056 | 3. **Restart your IDE completely** (not just reload window)
1057 | 
1058 | ## Authentication Issues
1059 | 
1060 | * **Invalid token errors:** Verify your `CIRCLECI_TOKEN` in Personal API Tokens
1061 | * **Permission errors:** Ensure token has read access to your projects
1062 | * **Environment variables not loading:** Test with `echo $CIRCLECI_TOKEN` (Mac/Linux) or `echo %CIRCLECI_TOKEN%` (Windows)
1063 | 
1064 | ## Connection and Network Issues
1065 | 
1066 | * **Base URL:** Confirm `CIRCLECI_BASE_URL` is `https://circleci.com`
1067 | * **Corporate networks:** Configure npm proxy settings if behind firewall
1068 | * **Firewall blocking:** Check if security software blocks package downloads
1069 | 
1070 | ## System Requirements
1071 | 
1072 | * **Node.js version:** Ensure ≥ 18.0.0 with `node --version`
1073 | * **Update Node.js:** Consider latest LTS if experiencing compatibility issues
1074 | * **Package manager:** Verify npm/pnpm is working: `npm --version`
1075 | 
1076 | ## IDE-Specific Issues
1077 | 
1078 | * **Config file location:** Double-check path for your OS
1079 | * **Syntax errors:** Validate JSON syntax in config file
1080 | * **Console logs:** Check IDE developer console for specific errors
1081 | * **Try different IDE:** Test config in another supported editor to isolate issue
1082 | 
1083 | ## Process Issues
1084 | 
1085 | * **Hanging processes:** Kill existing MCP processes:
1086 |   ```bash
1087 |   # Mac/Linux: 
1088 |   pkill -f "mcp-server-circleci"
1089 |   
1090 |   # Windows: 
1091 |   taskkill /f /im node.exe
1092 | 
1093 | * **Port conflicts:** Restart IDE if connection seems blocked
1094 | 
1095 | ## Advanced Debugging
1096 | 
1097 | * **Test package directly:** `npx @circleci/mcp-server-circleci@latest --help`
1098 | * **Verbose logging:** `DEBUG=* npx @circleci/mcp-server-circleci@latest`
1099 | * **Docker fallback:** Try Docker installation if npx fails consistently
1100 | 
1101 | ## Still Need Help?
1102 | 
1103 | 1. Check GitHub issues for similar problems
1104 | 2. Include your OS, Node version, and IDE when reporting issues
1105 | 3. Share relevant error messages from IDE console
1106 | 
1107 | # Development
1108 | 
1109 | ## Getting Started
1110 | 
1111 | 1. Clone the repository:
1112 | 
1113 |    ```bash
1114 |    git clone https://github.com/CircleCI-Public/mcp-server-circleci.git
1115 |    cd mcp-server-circleci
1116 |    ```
1117 | 
1118 | 2. Install dependencies:
1119 | 
1120 |    ```bash
1121 |    pnpm install
1122 |    ```
1123 | 
1124 | 3. Build the project:
1125 |    ```bash
1126 |    pnpm build
1127 |    ```
1128 | 
1129 | ## Building Docker Container
1130 | 
1131 | You can build the Docker container locally using:
1132 | 
1133 | ```bash
1134 | docker build -t circleci:mcp-server-circleci .
1135 | ```
1136 | 
1137 | This will create a Docker image tagged as `circleci:mcp-server-circleci` that you can use with any MCP client.
1138 | 
1139 | To run the container locally:
1140 | 
1141 | ```bash
1142 | docker run --rm -i -e CIRCLECI_TOKEN=your-circleci-token -e CIRCLECI_BASE_URL=https://circleci.com circleci:mcp-server-circleci
1143 | ```
1144 | 
1145 | To run the container as a self-managed remote MCP server you need to add the environment variable `start=remote` to the docker run command. You can also define the port to use with the environment variable `port=<port>` or else the default port `8000` will be used:
1146 | 
1147 | ```bash
1148 | docker run --rm -i -e CIRCLECI_TOKEN=your-circleci-token -e CIRCLECI_BASE_URL=https://circleci.com circleci:mcp-server-circleci -e start=remote -e port=8000
1149 | ```
1150 | 
1151 | ## Development with MCP Inspector
1152 | 
1153 | The easiest way to iterate on the MCP Server is using the MCP inspector. You can learn more about the MCP inspector at https://modelcontextprotocol.io/docs/tools/inspector
1154 | 
1155 | 1. Start the development server:
1156 | 
1157 |    ```bash
1158 |    pnpm watch # Keep this running in one terminal
1159 |    ```
1160 | 
1161 | 2. In a separate terminal, launch the inspector:
1162 | 
1163 |    ```bash
1164 |    pnpm inspector
1165 |    ```
1166 | 
1167 | 3. Configure the environment:
1168 |    - Add your `CIRCLECI_TOKEN` to the Environment Variables section in the inspector UI
1169 |    - The token needs read access to your CircleCI projects
1170 |    - Optionally you can set your CircleCI Base URL. Defaults to `https//circleci.com`
1171 | 
1172 | ## Testing
1173 | 
1174 | - Run the test suite:
1175 | 
1176 |   ```bash
1177 |   pnpm test
1178 |   ```
1179 | 
1180 | - Run tests in watch mode during development:
1181 |   ```bash
1182 |   pnpm test:watch
1183 |   ```
1184 | 
1185 | For more detailed contribution guidelines, see [CONTRIBUTING.md](CONTRIBUTING.md)
1186 | 
```

--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Contributor Covenant Code of Conduct
 2 | 
 3 | ## Our Pledge
 4 | 
 5 | In the interest of fostering an open and welcoming environment, we as
 6 | contributors and maintainers pledge to making participation in our project and
 7 | our community a harassment-free experience for everyone, regardless of age, body
 8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
 9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 | 
12 | ## Our Standards
13 | 
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 | 
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 | 
23 | Examples of unacceptable behavior by participants include:
24 | 
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 |  advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 |  address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 |  professional setting
33 | 
34 | ## Our Responsibilities
35 | 
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 | 
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 | 
46 | ## Scope
47 | 
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 | 
55 | ## Enforcement
56 | 
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at [email protected]. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 | 
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 | 
68 | ## Attribution
69 | 
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 | 
73 | [homepage]: https://www.contributor-covenant.org
74 | 
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 | 
```

--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Contributing
  2 | 
  3 | Thank you for considering to contribute to the MCP(Model Context Protocol) Server CircleCI! Before you
  4 | get started, we recommend taking a look at the guidelines below:
  5 | 
  6 | - [Have a Question?](#question)
  7 | - [Issues and Bugs](#issue)
  8 | - [Feature Requests](#feature)
  9 | - [Contributing](#contribute)
 10 |   - [Submission Guidelines](#guidelines)
 11 |   - [Release Process](#release)
 12 |   - [Creating New Tools](#creating-tools)
 13 | 
 14 | ## <a name="question"></a>Have a Question?
 15 | 
 16 | Have a question about the MCP Server CircleCI?
 17 | 
 18 | ### I have a general question.
 19 | 
 20 | Contact CircleCI's general support by filing a ticket here:
 21 | [Submit a request](https://support.circleci.com/hc/en-us/requests/new)
 22 | 
 23 | ### I have a question about Typescript or best practices
 24 | 
 25 | Share your question with
 26 | [CircleCI's community Discuss forum](https://discuss.circleci.com/).
 27 | 
 28 | ### I have a question about the MCP Server CircleCI
 29 | 
 30 | You can always open a new [issue](https://github.com/CircleCI-Public/mcp-server-circleci/issues/new/choose) on the repository on GitHub.
 31 | 
 32 | ## <a name="issue"></a>Discover a Bug?
 33 | 
 34 | Find an issue or bug?
 35 | 
 36 | You can help us resolve the issue by
 37 | [submitting an issue](https://github.com/CircleCI-Public/mcp-server-circleci/issues/new/choose)
 38 | on our GitHub repository.
 39 | 
 40 | Up for a challenge? If you think you can fix the issue, consider sending in a
 41 | [Pull Request](#pull).
 42 | 
 43 | ## <a name="feature"></a>Missing Feature?
 44 | 
 45 | Is anything missing?
 46 | 
 47 | You can request a new feature by
 48 | [submitting an issue](https://github.com/CircleCI-Public/mcp-server-circleci/issues/new/choose)
 49 | to our GitHub repository, utilizing the `Feature Request` template.
 50 | 
 51 | If you would like to instead contribute a pull request, please follow the
 52 | [Submission Guidelines](#guidelines)
 53 | 
 54 | ## <a name="contribute"></a>Contributing
 55 | 
 56 | Thank you for contributing to the MCP Server CircleCI!
 57 | 
 58 | Before submitting any new Issue or Pull Request, search our repository for any
 59 | existing or previous related submissions.
 60 | 
 61 | - [Search Pull Requests](https://github.com/CircleCI-Public/mcp-server-circleci/pulls?q=)
 62 | - [Search Issues](https://github.com/CircleCI-Public/mcp-server-circleci/issues?q=)
 63 | 
 64 | ### <a name="guidelines"></a>Submission Guidelines
 65 | 
 66 | #### <a name="commit"></a>Commit Conventions
 67 | 
 68 | This project strictly adheres to the
 69 | [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)
 70 | specification for creating human readable commit messages with appropriate
 71 | automation capabilities, such as changelog generation.
 72 | 
 73 | ##### Commit Message Format
 74 | 
 75 | Each commit message consists of a header, a body and a footer. The header has a
 76 | special format that includes a type, a scope and a subject:
 77 | 
 78 | ```
 79 | <type>(optional <scope>): <subject>
 80 | <BLANK LINE>
 81 | <body>
 82 | <BLANK LINE>
 83 | <footer>
 84 | ```
 85 | 
 86 | Footer should contain a
 87 | [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/)
 88 | if any.
 89 | 
 90 | ##### Breaking Change
 91 | 
 92 | Append a `!` to the end of the `type` in your commit message to suggest a
 93 | `BREAKING CHANGE`
 94 | 
 95 | ```
 96 | <type>!(optional <scope>): <subject>
 97 | ```
 98 | 
 99 | ##### Type
100 | 
101 | Must be one of the following:
102 | 
103 | - **build**: Changes that affect the build system or external dependencies
104 |   (example scopes: npm, eslint, prettier)
105 | - **ci**: Changes to our CircleCI configuration files
106 | - **chore**: No production code changes. Updates to readmes and meta documents
107 | - **docs**: Changes to the API documentation or JSDoc/TSDoc comments
108 | - **feat**: A new feature or capability for the MCP server
109 | - **fix**: A bug fix in the server implementation
110 | - **refactor**: A code change that neither fixes a bug nor adds a feature
111 | - **style**: Changes that do not affect the meaning of the code (white-space,
112 |   formatting, missing semi-colons, etc)
113 | - **test**: Adding missing tests or correcting existing tests
114 | - **tools**: Changes to the CircleCI API tool implementations
115 | 
116 | #### <a name="pull"></a>Submitting a Pull Request
117 | 
118 | After searching for potentially existing pull requests or issues in progress, if
119 | none are found, please open a new issue describing your intended changes and
120 | stating your intention to work on the issue.
121 | 
122 | Creating issues helps us plan our next release and prevents folks from
123 | duplicating work.
124 | 
125 | After the issue has been created, follow these steps to create a Pull Request.
126 | 
127 | 1. Fork the
128 |    [CircleCI-Public/mcp-server-circleci](https://github.com/CircleCI-Public/mcp-server-circleci)
129 |    repo.
130 | 2. Clone your newly forked repository to your local machine.
131 | 3. Create a new branch for your changes: `git checkout -b fix_my_issue main`
132 | 4. Run `npm run setup`
133 | 5. Implement your change with appropriate test coverage.
134 | 6. Utilize our [commit message conventions](commit).
135 | 7. Run tests, linters, and formatters locally, with: `pnpm` scripts in `package.json`
136 | 8. Push all changes back to GitHub `git push origin fix_my_issue`
137 | 9. In GitHub, send a Pull Request to `mcp-server-circleci:main`
138 | 
139 | Thank you for your contribution!
140 | 
141 | ### <a name="creating-tools"></a>Creating New Tools
142 | 
143 | This project provides a tool generator script to help you quickly create new tools with the correct structure and boilerplate code.
144 | 
145 | To create a new tool:
146 | 
147 | 1. Run the following command, replacing `yourToolName` with your tool's name in camelCase:
148 |    ```bash
149 |    pnpm create-tool yourToolName
150 |    ```
151 | 
152 | 2. This will generate a new directory at `src/tools/yourToolName` with the following files:
153 |    - `inputSchema.ts` - Defines the input schema for the tool using Zod
154 |    - `tool.ts` - Defines the tool name, description, and input schema
155 |    - `handler.ts` - Contains the main implementation logic
156 |    - `handler.test.ts` - Contains basic test setup
157 | 
158 | 3. After creating the files, you'll need to register your tool in `src/circleci-tools.ts`:
159 |    ```typescript
160 |    // Import your tool and handler
161 |    import { yourToolNameTool } from './tools/yourToolName/tool.js';
162 |    import { yourToolName } from './tools/yourToolName/handler.js';
163 | 
164 |    // Add your tool to the CCI_TOOLS array
165 |    export const CCI_TOOLS = [
166 |      // ...existing tools
167 |      yourToolNameTool,
168 |    ];
169 | 
170 |    // Add your handler to the CCI_HANDLERS object
171 |    export const CCI_HANDLERS = {
172 |      // ...existing handlers
173 |      your_tool_name: yourToolName,
174 |    } satisfies ToolHandlers;
175 |    ```
176 | 
177 | 4. Implement your tool's logic in the handler and add comprehensive tests.
178 | 
179 | Using this script ensures consistency across the codebase and saves development time.
```

--------------------------------------------------------------------------------
/src/tools/listFollowedProjects/inputSchema.ts:
--------------------------------------------------------------------------------

```typescript
1 | import { z } from 'zod';
2 | 
3 | export const listFollowedProjectsInputSchema = z.object({});
4 | 
```

--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |   "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 |   "extends": ["local>circleci/renovate-config"],
4 |   "automerge": false
5 | }
6 | 
```

--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "extends": "./tsconfig.json",
 3 |   "compilerOptions": {
 4 |     "rootDir": ".",
 5 |     "types": ["vitest/globals"]
 6 |   },
 7 |   "include": [
 8 |     "src/**/*",
 9 |     "src/**/*.test.ts",
10 |     "src/**/*.spec.ts",
11 |     "vitest.config.ts"
12 |   ],
13 |   "exclude": ["node_modules", "dist"]
14 | }
15 | 
```

--------------------------------------------------------------------------------
/src/lib/getWorkflowIdFromURL.ts:
--------------------------------------------------------------------------------

```typescript
1 | export function getWorkflowIdFromURL(url: string): string | undefined {
2 |   // Matches both:
3 |   // - .../workflows/:workflowId
4 |   // - .../workflows/:workflowId/jobs/:buildNumber
5 |   const match = url.match(/\/workflows\/([\w-]+)/);
6 |   return match ? match[1] : undefined;
7 | }
8 | 
```

--------------------------------------------------------------------------------
/src/transports/stdio.ts:
--------------------------------------------------------------------------------

```typescript
1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3 | 
4 | export const createStdioTransport = async (server: McpServer) => {
5 |   const transport = new StdioServerTransport();
6 |   await server.connect(transport);
7 | };
8 | 
```

--------------------------------------------------------------------------------
/src/tools/configHelper/inputSchema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | 
 3 | export const configHelperInputSchema = z.object({
 4 |   configFile: z
 5 |     .string()
 6 |     .describe(
 7 |       'The contents of the circleci config file. This should be the contents of the circleci config file, not the path to the file. Typically located at .circleci/config.yml',
 8 |     ),
 9 | });
10 | 
```

--------------------------------------------------------------------------------
/src/lib/mcpErrorOutput.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { createErrorResponse, McpErrorResponse } from './mcpResponse.js';
 2 | 
 3 | /**
 4 |  * Creates an MCP error response with the provided text
 5 |  * @param text The error message text
 6 |  * @returns A properly formatted MCP error response
 7 |  */
 8 | export default function mcpErrorOutput(text: string): McpErrorResponse {
 9 |   return createErrorResponse(text);
10 | }
11 | 
```

--------------------------------------------------------------------------------
/vitest.config.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { defineConfig } from 'vitest/config';
 2 | 
 3 | export default defineConfig({
 4 |   test: {
 5 |     globals: true,
 6 |     environment: 'node',
 7 |     include: ['src/**/*.{test,spec}.{js,ts}'],
 8 |     coverage: {
 9 |       provider: 'v8',
10 |       reporter: ['text', 'json', 'html'],
11 |       include: ['src/**/*.{js,ts}'],
12 |       exclude: ['src/**/*.{test,spec}.{js,ts}'],
13 |     },
14 |   },
15 | });
16 | 
```

--------------------------------------------------------------------------------
/src/tools/findUnderusedResourceClasses/inputSchema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | 
 3 | export const findUnderusedResourceClassesInputSchema = z.object({
 4 |   csvFilePath: z
 5 |     .string()
 6 |     .describe('The path to the usage data CSV file to analyze.'),
 7 |   threshold: z
 8 |     .number()
 9 |     .optional()
10 |     .default(40)
11 |     .describe(
12 |       'The usage percentage threshold. Jobs with usage below this will be reported. Default is 40.',
13 |     ),
14 | });
15 | 
```

--------------------------------------------------------------------------------
/src/lib/mcpResponse.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, it, expect } from 'vitest';
 2 | import { createErrorResponse } from './mcpResponse.js';
 3 | 
 4 | describe('MCP Response Utilities', () => {
 5 |   describe('createErrorResponse', () => {
 6 |     it('should create a valid error response', () => {
 7 |       const text = 'Error message';
 8 |       const response = createErrorResponse(text);
 9 | 
10 |       expect(response).toEqual({
11 |         isError: true,
12 |         content: [
13 |           {
14 |             type: 'text',
15 |             text,
16 |           },
17 |         ],
18 |       });
19 |     });
20 |   });
21 | });
22 | 
```

--------------------------------------------------------------------------------
/src/clients/client.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { CircleCIPrivateClients } from './circleci-private/index.js';
 2 | import { CircleCIClients } from './circleci/index.js';
 3 | 
 4 | export function getCircleCIClient() {
 5 |   if (!process.env.CIRCLECI_TOKEN) {
 6 |     throw new Error('CIRCLECI_TOKEN is not set');
 7 |   }
 8 | 
 9 |   return new CircleCIClients({
10 |     token: process.env.CIRCLECI_TOKEN,
11 |   });
12 | }
13 | 
14 | export function getCircleCIPrivateClient() {
15 |   if (!process.env.CIRCLECI_TOKEN) {
16 |     throw new Error('CIRCLECI_TOKEN is not set');
17 |   }
18 | 
19 |   return new CircleCIPrivateClients({
20 |     token: process.env.CIRCLECI_TOKEN,
21 |   });
22 | }
23 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "rootDir": "./src",
 7 |     "strict": true,
 8 |     "esModuleInterop": true,
 9 |     "skipLibCheck": true,
10 |     "forceConsistentCasingInFileNames": true,
11 |     "outDir": "./dist",
12 |     "baseUrl": ".",
13 |     "paths": {
14 |       "@modelcontextprotocol/sdk/*": ["./node_modules/@modelcontextprotocol/sdk/dist/esm/*"]
15 |     },
16 |     "types": ["node"],
17 |     "typeRoots": [
18 |       "./node_modules/@types"
19 |     ],
20 |     "resolveJsonModule": true
21 |   },
22 |   "include": ["src/**/*"],
23 |   "exclude": ["**/*.test.ts", "**/*.spec.ts", "vitest.config.ts", "dist"]
24 | }
25 | 
```

--------------------------------------------------------------------------------
/src/lib/latest-pipeline/getLatestPipelineWorkflows.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getCircleCIClient } from '../../clients/client.js';
 2 | 
 3 | export type GetLatestPipelineWorkflowsParams = {
 4 |   projectSlug: string;
 5 |   branch?: string;
 6 | };
 7 | 
 8 | export const getLatestPipelineWorkflows = async ({
 9 |   projectSlug,
10 |   branch,
11 | }: GetLatestPipelineWorkflowsParams) => {
12 |   const circleci = getCircleCIClient();
13 | 
14 |   const pipelines = await circleci.pipelines.getPipelines({
15 |     projectSlug,
16 |     branch,
17 |   });
18 | 
19 |   const latestPipeline = pipelines?.[0];
20 | 
21 |   if (!latestPipeline) {
22 |     throw new Error('Latest pipeline not found');
23 |   }
24 | 
25 |   const workflows = await circleci.workflows.getPipelineWorkflows({
26 |     pipelineId: latestPipeline.id,
27 |   });
28 | 
29 |   return workflows;
30 | };
31 | 
```

--------------------------------------------------------------------------------
/src/tools/rerunWorkflow/inputSchema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { workflowUrlDescription } from '../shared/constants.js';
 3 | 
 4 | export const rerunWorkflowInputSchema = z.object({
 5 |   workflowId: z
 6 |     .string()
 7 |     .describe(
 8 |       'This should be the workflowId of the workflow that need rerun. The workflowId is an UUID. An example workflowId is a12145c5-90f8-4cc9-98f2-36cb85db9e4b',
 9 |     )
10 |     .optional(),
11 |   fromFailed: z
12 |     .boolean()
13 |     .describe(
14 |       'If true, reruns the workflow from failed. If false, reruns the workflow from the start. If omitted, the rerun behavior is based on the workflow status.',
15 |     )
16 |     .optional(),
17 |   workflowURL: z.string().describe(workflowUrlDescription).optional(),
18 | });
19 | 
```

--------------------------------------------------------------------------------
/src/lib/getWorkflowIdFromURL.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, it, expect } from 'vitest';
 2 | import { getWorkflowIdFromURL } from './getWorkflowIdFromURL.js';
 3 | 
 4 | describe('getWorkflowIdFromURL', () => {
 5 |   it('should return the workflow ID from a workflow URL', () => {
 6 |     const url =
 7 |       'https://app.circleci.com/pipelines/gh/organization/project/1/workflows/123-abc';
 8 |     const result = getWorkflowIdFromURL(url);
 9 |     expect(result).toBe('123-abc');
10 |   });
11 |   it('should return the workflow ID from a job URL', () => {
12 |     const url =
13 |       'https://app.circleci.com/pipelines/gh/organization/project/1/workflows/123-abc/jobs/456';
14 |     const result = getWorkflowIdFromURL(url);
15 |     expect(result).toBe('123-abc');
16 |   });
17 | });
18 | 
```

--------------------------------------------------------------------------------
/src/clients/circlet/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { HTTPClient } from '../circleci/httpClient.js';
 2 | import { CircletAPI } from './circlet.js';
 3 | 
 4 | /**
 5 |  * Creates a default HTTP client for the CircleCI API private
 6 |  * @param options Configuration parameters
 7 |  * @param options.token CircleCI API token
 8 |  * @param options.baseURL Base URL for the CircleCI API private
 9 |  * @returns HTTP client for CircleCI API private
10 |  */
11 | const defaultV1HTTPClient = () => {
12 |   return new HTTPClient('https://circlet.ai', '/api/v1');
13 | };
14 | 
15 | export class CircletClient {
16 |   public circlet: CircletAPI;
17 | 
18 |   constructor({
19 |     httpClient = defaultV1HTTPClient(),
20 |   }: {
21 |     httpClient?: HTTPClient;
22 |   } = {}) {
23 |     this.circlet = new CircletAPI(httpClient);
24 |   }
25 | }
26 | 
```

--------------------------------------------------------------------------------
/src/tools/analyzeDiff/inputSchema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { FilterBy } from '../shared/constants.js';
 3 | 
 4 | export const analyzeDiffInputSchema = z.object({
 5 |   speedMode: z
 6 |     .boolean()
 7 |     .default(false)
 8 |     .describe('The status of speed mode. Defaults to false.'),
 9 |   filterBy: z
10 |     .nativeEnum(FilterBy)
11 |     .default(FilterBy.none)
12 |     .describe(`Analysis filter. Defaults to ${FilterBy.none}`),
13 |   diff: z
14 |     .string()
15 |     .describe(
16 |       'Git diff content to analyze. Defaults to staged changes, unless the user explicitly asks for unstaged changes or all changes.',
17 |     ),
18 |   rules: z
19 |     .string()
20 |     .describe(
21 |       'Rules to use for analysis, found in the rules subdirectory of the IDE workspace settings. Combine all rules from multiple files by separating them with ---',
22 |     ),
23 | });
24 | 
```

--------------------------------------------------------------------------------
/src/lib/mcpErrorOutput.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, it, expect } from 'vitest';
 2 | import mcpErrorOutput from './mcpErrorOutput.js';
 3 | 
 4 | describe('mcpErrorOutput', () => {
 5 |   it('should create an error response with the provided text', () => {
 6 |     const errorMessage = 'Test error message';
 7 |     const result = mcpErrorOutput(errorMessage);
 8 | 
 9 |     expect(result).toEqual({
10 |       isError: true,
11 |       content: [
12 |         {
13 |           type: 'text',
14 |           text: errorMessage,
15 |         },
16 |       ],
17 |     });
18 |   });
19 | 
20 |   it('should maintain the exact error text provided', () => {
21 |     const complexErrorMessage = `
22 |       Error occurred:
23 |       - Missing parameter: projectSlug
24 |       - Invalid token format
25 |     `;
26 |     const result = mcpErrorOutput(complexErrorMessage);
27 | 
28 |     expect(result.content[0].text).toBe(complexErrorMessage);
29 |   });
30 | });
31 | 
```

--------------------------------------------------------------------------------
/src/tools/downloadUsageApiData/inputSchema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | 
 3 | export const downloadUsageApiDataInputSchema = z.object({
 4 |   orgId: z.string().describe('The ID of the CircleCI organization'),
 5 |   startDate: z
 6 |     .string()
 7 |     .optional()
 8 |     .describe('Optional. The start date for the usage data in YYYY-MM-DD format (or natural language). Used when starting a new export job.'),
 9 |   endDate: z
10 |     .string()
11 |     .optional()
12 |     .describe('Optional. The end date for the usage data in YYYY-MM-DD format (or natural language). Used when starting a new export job.'),
13 |   jobId: z
14 |     .string()
15 |     .optional()
16 |     .describe('Generated by the initial tool call when starting the usage export job. Required for subsequent tool calls.'),
17 |   outputDir: z
18 |     .string()
19 |     .describe('The directory to save the downloaded usage data CSV file.'),
20 | });
21 | 
```

--------------------------------------------------------------------------------
/src/lib/mcpResponse.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Represents a basic text content block for MCP responses
 3 |  */
 4 | export type McpTextContent = {
 5 |   type: 'text';
 6 |   text: string;
 7 | };
 8 | 
 9 | /**
10 |  * Type for MCP response content
11 |  */
12 | export type McpContent = McpTextContent;
13 | 
14 | /**
15 |  * Type for representing a successful MCP response
16 |  */
17 | export type McpSuccessResponse = {
18 |   content: McpContent[];
19 |   isError?: false;
20 | };
21 | 
22 | /**
23 |  * Type for representing an error MCP response
24 |  */
25 | export type McpErrorResponse = {
26 |   content: McpContent[];
27 |   isError: true;
28 | };
29 | 
30 | /**
31 |  * Creates an error MCP response with text content
32 |  * @param text The error text content to include in the response
33 |  * @returns A properly formatted MCP error response
34 |  */
35 | export function createErrorResponse(text: string): McpErrorResponse {
36 |   return {
37 |     isError: true,
38 |     content: [
39 |       {
40 |         type: 'text',
41 |         text,
42 |       },
43 |     ],
44 |   };
45 | }
46 | 
```

--------------------------------------------------------------------------------
/src/tools/configHelper/handler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | import { configHelperInputSchema } from './inputSchema.js';
 3 | import { getCircleCIClient } from '../../clients/client.js';
 4 | 
 5 | export const configHelper: ToolCallback<{
 6 |   params: typeof configHelperInputSchema;
 7 | }> = async (args) => {
 8 |   const { configFile } = args.params ?? {};
 9 | 
10 |   const circleci = getCircleCIClient();
11 |   const configValidate = await circleci.configValidate.validateConfig({
12 |     config: configFile,
13 |   });
14 | 
15 |   if (configValidate.valid) {
16 |     return {
17 |       content: [
18 |         {
19 |           type: 'text',
20 |           text: 'Your config is valid!',
21 |         },
22 |       ],
23 |     };
24 |   }
25 | 
26 |   return {
27 |     content: [
28 |       {
29 |         type: 'text',
30 |         text: `There are some issues with your config: ${configValidate.errors?.map((error) => error.message).join('\n') ?? 'Unknown error'}`,
31 |       },
32 |     ],
33 |   };
34 | };
35 | 
```

--------------------------------------------------------------------------------
/src/clients/circleci/insights.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { FlakyTest } from '../schemas.js';
 2 | import { HTTPClient } from './httpClient.js';
 3 | 
 4 | export class InsightsAPI {
 5 |   protected client: HTTPClient;
 6 | 
 7 |   constructor(httpClient: HTTPClient) {
 8 |     this.client = httpClient;
 9 |   }
10 | 
11 |   /**
12 |    * Get all workflows for a pipeline with pagination support
13 |    * @param params Configuration parameters
14 |    * @param params.projectSlug The project slug
15 |    * @returns Flaky test details
16 |    * @throws Error if timeout or max pages reached
17 |    */
18 |   async getProjectFlakyTests({
19 |     projectSlug,
20 |   }: {
21 |     projectSlug: string;
22 |   }): Promise<FlakyTest> {
23 |     const rawResult = await this.client.get<unknown>(
24 |       `/insights/${projectSlug}/flaky-tests`,
25 |     );
26 | 
27 |     const parsedResult = FlakyTest.safeParse(rawResult);
28 | 
29 |     if (!parsedResult.success) {
30 |       throw new Error('Failed to parse flaky test response');
31 |     }
32 | 
33 |     return parsedResult.data;
34 |   }
35 | }
36 | 
```

--------------------------------------------------------------------------------
/src/tools/configHelper/tool.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { configHelperInputSchema } from './inputSchema.js';
 2 | 
 3 | export const configHelperTool = {
 4 |   name: 'config_helper' as const,
 5 |   description: `
 6 |   This tool helps analyze and validate and fix CircleCI configuration files.
 7 | 
 8 |   Parameters:
 9 |   - params: An object containing:
10 |     - configFile: string - The full contents of the CircleCI config file as a string. This should be the raw YAML content, not a file path.
11 | 
12 |   Example usage:
13 |   {
14 |     "params": {
15 |       "configFile": "version: 2.1\norbs:\n  node: circleci/node@7\n..."
16 |     }
17 |   }
18 | 
19 |   Note: The configFile content should be provided as a properly escaped string with newlines represented as \n.
20 | 
21 |   Tool output instructions:
22 |     - If the config is invalid, the tool will return the errors and the original config. Use the errors to fix the config.
23 |     - If the config is valid, do nothing.
24 |   `,
25 |   inputSchema: configHelperInputSchema,
26 | };
27 | 
```

--------------------------------------------------------------------------------
/src/tools/findUnderusedResourceClasses/handler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | import { findUnderusedResourceClassesInputSchema } from './inputSchema.js';
 3 | import mcpErrorOutput from '../../lib/mcpErrorOutput.js';
 4 | import { findUnderusedResourceClassesFromCSV } from '../../lib/usage-api/findUnderusedResourceClasses.js';
 5 | 
 6 | export const findUnderusedResourceClasses: ToolCallback<{ params: typeof findUnderusedResourceClassesInputSchema }> = async (args) => {
 7 |   const { 
 8 |     csvFilePath, 
 9 |     threshold 
10 |   } = args.params ?? {};
11 | 
12 |   if (!csvFilePath) {
13 |     return mcpErrorOutput('ERROR: csvFilePath is required.');
14 |   }
15 |   try {
16 |     const { report } = await findUnderusedResourceClassesFromCSV({ csvFilePath, threshold });
17 |     return {
18 |       content: [
19 |         { type: 'text', text: report },
20 |       ],
21 |     };
22 |   } catch (e: any) {
23 |     return mcpErrorOutput(`ERROR: ${e && e.message ? e.message : e}`);
24 |   }
25 | };
26 | 
```

--------------------------------------------------------------------------------
/src/clients/circleci/configValidate.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ConfigValidate } from '../schemas.js';
 2 | import { HTTPClient } from './httpClient.js';
 3 | 
 4 | export class ConfigValidateAPI {
 5 |   protected client: HTTPClient;
 6 | 
 7 |   constructor(httpClient: HTTPClient) {
 8 |     this.client = httpClient;
 9 |   }
10 | 
11 |   /**
12 |    * Validate a config with the default values
13 |    * @param params Configuration parameters
14 |    * @param params.config The config to validate
15 |    * @returns ConfigValidate
16 |    * @throws Error if the config is invalid
17 |    */
18 |   async validateConfig({
19 |     config,
20 |   }: {
21 |     config: string;
22 |   }): Promise<ConfigValidate> {
23 |     const rawResult = await this.client.post<unknown>(
24 |       `/compile-config-with-defaults`,
25 |       { config_yaml: config },
26 |     );
27 | 
28 |     const parsedResult = ConfigValidate.safeParse(rawResult);
29 | 
30 |     if (!parsedResult.success) {
31 |       throw new Error('Failed to parse config validate response');
32 |     }
33 | 
34 |     return parsedResult.data;
35 |   }
36 | }
37 | 
```

--------------------------------------------------------------------------------
/src/clients/circleci/jobsV1.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { JobDetails } from '../schemas.js';
 2 | import { HTTPClient } from './httpClient.js';
 3 | 
 4 | export class JobsV1API {
 5 |   protected client: HTTPClient;
 6 | 
 7 |   constructor(httpClient: HTTPClient) {
 8 |     this.client = httpClient;
 9 |   }
10 |   /**
11 |    * Get detailed information about a specific job
12 |    * @param params Configuration parameters
13 |    * @param params.projectSlug The project slug (e.g., "gh/CircleCI-Public/api-preview-docs")
14 |    * @param params.jobNumber The number of the job
15 |    * @returns Detailed job information including status, timing, and build details
16 |    */
17 |   async getJobDetails({
18 |     projectSlug,
19 |     jobNumber,
20 |   }: {
21 |     projectSlug: string;
22 |     jobNumber: number;
23 |   }): Promise<JobDetails> {
24 |     const rawResult = await this.client.get<unknown>(
25 |       `/project/${projectSlug}/${jobNumber}`,
26 |     );
27 |     // Validate the response against our JobDetails schema
28 |     return JobDetails.parse(rawResult);
29 |   }
30 | }
31 | 
```

--------------------------------------------------------------------------------
/src/lib/outputTextTruncated.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { McpSuccessResponse } from './mcpResponse.js';
 2 | 
 3 | const MAX_LENGTH = 50000;
 4 | 
 5 | export const SEPARATOR = '\n<<<SEPARATOR>>>\n';
 6 | 
 7 | /**
 8 |  * Creates an MCP response with potentially truncated text
 9 |  * @param outputText The full text that might need to be truncated
10 |  * @returns An MCP response containing the original or truncated text
11 |  */
12 | const outputTextTruncated = (outputText: string): McpSuccessResponse => {
13 |   if (outputText.length > MAX_LENGTH) {
14 |     const truncationNotice = `<MCPTruncationWarning>
15 | ⚠️ TRUNCATED OUTPUT WARNING ⚠️
16 | - Showing only most recent entries
17 | </MCPTruncationWarning>\n\n`;
18 | 
19 |     // Take the tail of the output text
20 |     const truncatedText =
21 |       truncationNotice +
22 |       outputText.slice(-MAX_LENGTH + truncationNotice.length);
23 | 
24 |     return {
25 |       content: [{ type: 'text' as const, text: truncatedText }],
26 |     };
27 |   }
28 | 
29 |   return {
30 |     content: [{ type: 'text' as const, text: outputText }],
31 |   };
32 | };
33 | 
34 | export default outputTextTruncated;
35 | 
```

--------------------------------------------------------------------------------
/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 |       - circleciToken
10 |     properties:
11 |       circleciToken:
12 |         type: string
13 |         description: CircleCI API token with read access to CircleCI projects
14 |       circleciBaseUrl:
15 |         type: string
16 |         description: CircleCI base URL (optional, defaults to https://circleci.com)
17 |         default: "https://circleci.com"
18 |     default: {}
19 |   commandFunction:
20 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
21 |     |-
22 |     (config) => ({
23 |       command: 'node',
24 |       args: ['dist/index.js'],
25 |       env: { 
26 |         CIRCLECI_TOKEN: config.circleciToken,
27 |         CIRCLECI_BASE_URL: config.circleciBaseUrl
28 |       }
29 |     })
30 |   exampleConfig:
31 |     circleciToken: your-circleci-token-here
32 |     circleciBaseUrl: https://circleci.com
33 | 
```

--------------------------------------------------------------------------------
/src/lib/outputTextTruncated.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, it, expect } from 'vitest';
 2 | import outputTextTruncated from './outputTextTruncated';
 3 | 
 4 | describe('outputTextTruncated', () => {
 5 |   it('should return the original text when under max length', () => {
 6 |     const shortText = 'This is a short text';
 7 |     const result = outputTextTruncated(shortText);
 8 | 
 9 |     expect(result).toEqual({
10 |       content: [{ type: 'text', text: shortText }],
11 |     });
12 |   });
13 | 
14 |   it('should truncate text when over max length', () => {
15 |     const longText = 'a'.repeat(60000);
16 |     const result = outputTextTruncated(longText);
17 | 
18 |     expect(result.content[0].text).toContain('<MCPTruncationWarning>');
19 |     expect(result.content[0].text).toContain('TRUNCATED OUTPUT WARNING');
20 | 
21 |     expect(result.content[0].text.length).toBeLessThan(longText.length);
22 | 
23 |     const truncationNoticeLength = result.content[0].text.indexOf('\n\n') + 2;
24 |     const truncatedContent = result.content[0].text.slice(
25 |       truncationNoticeLength,
26 |     );
27 |     expect(longText.endsWith(truncatedContent)).toBe(true);
28 |   });
29 | });
30 | 
```

--------------------------------------------------------------------------------
/src/tools/rerunWorkflow/tool.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { rerunWorkflowInputSchema } from './inputSchema.js';
 2 | 
 3 | export const rerunWorkflowTool = {
 4 |   name: 'rerun_workflow' as const,
 5 |   description: `
 6 |   This tool is used to rerun a workflow from start or from the failed job.
 7 | 
 8 |   Common use cases:
 9 |   - Rerun a workflow from a failed job
10 |   - Rerun a workflow from start
11 | 
12 | Input options (EXACTLY ONE of these TWO options must be used):
13 | 
14 | Option 1 - Workflow ID:
15 | - workflowId: The ID of the workflow to rerun
16 | - fromFailed: true to rerun from failed, false to rerun from start. If omitted, behavior is based on workflow status. (optional)
17 | 
18 | Option 2 - Workflow URL:
19 | - workflowURL: The URL of the workflow to rerun
20 |   * Workflow URL: https://app.circleci.com/pipelines/:vcsType/:orgName/:projectName/:pipelineNumber/workflows/:workflowId
21 |   * Workflow Job URL: https://app.circleci.com/pipelines/:vcsType/:orgName/:projectName/:pipelineNumber/workflows/:workflowId/jobs/:buildNumber
22 | - fromFailed: true to rerun from failed, false to rerun from start. If omitted, behavior is based on workflow status. (optional)
23 |   `,
24 |   inputSchema: rerunWorkflowInputSchema,
25 | };
26 | 
```

--------------------------------------------------------------------------------
/src/tools/runRollbackPipeline/inputSchema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { projectSlugDescriptionNoBranch } from '../shared/constants.js';
 3 | 
 4 | export const runRollbackPipelineInputSchema = z.object({
 5 |   projectSlug: z
 6 |     .string()
 7 |     .describe(projectSlugDescriptionNoBranch)
 8 |     .optional(),
 9 |   projectID: z
10 |     .string()
11 |     .uuid()
12 |     .describe('The ID of the CircleCI project (UUID)')
13 |     .optional(),
14 |   environmentName: z
15 |     .string()
16 |     .describe('The environment name'),
17 |   componentName: z
18 |     .string()
19 |     .describe('The component name'),
20 |   currentVersion: z
21 |     .string()
22 |     .describe('The current version'),
23 |   targetVersion: z
24 |     .string()
25 |     .describe('The target version'),
26 |   namespace: z
27 |     .string()
28 |     .describe('The namespace of the component'),
29 |   reason: z
30 |     .string()
31 |     .describe('The reason for the rollback')
32 |     .optional(),
33 |   parameters: z
34 |     .record(z.any())
35 |     .describe('The extra parameters for the rollback pipeline')
36 |     .optional(),
37 | }).refine(
38 |   (data) => data.projectSlug || data.projectID,
39 |   {
40 |     message: "Either projectSlug or projectID must be provided",
41 |     path: ["projectSlug", "projectID"],
42 |   }
43 | );
44 | 
```

--------------------------------------------------------------------------------
/src/tools/listComponentVersions/inputSchema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { projectSlugDescriptionNoBranch } from '../shared/constants.js';
 3 | 
 4 | export const listComponentVersionsInputSchema = z.object({
 5 |   projectSlug: z
 6 |     .string()
 7 |     .describe(projectSlugDescriptionNoBranch)
 8 |     .optional(),
 9 |   projectID: z
10 |     .string()
11 |     .uuid()
12 |     .describe('The ID of the CircleCI project (UUID)')
13 |     .optional(),
14 |   orgID: z
15 |     .string()
16 |     .describe(
17 |       'The ID of the organization. This is the ID of the organization that the components and environments belong to. If not provided, it will be resolved from projectSlug or projectID.',
18 |     )
19 |     .optional(),
20 |   componentID: z
21 |     .string()
22 |     .optional()
23 |     .describe('The ID of the component to list versions for. If not provided, available components will be listed.'),
24 |   environmentID: z
25 |     .string()
26 |     .optional()
27 |     .describe('The ID of the environment to list versions for. If not provided, available environments will be listed.'),
28 | }).refine(
29 |   (data) => data.projectSlug || data.projectID,
30 |   {
31 |     message: "Either projectSlug or projectID must be provided",
32 |     path: ["projectSlug", "projectID"],
33 |   }
34 | );
35 | 
```

--------------------------------------------------------------------------------
/src/tools/analyzeDiff/tool.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { FilterBy } from '../shared/constants.js';
 2 | import { analyzeDiffInputSchema } from './inputSchema.js';
 3 | 
 4 | export const analyzeDiffTool = {
 5 |   name: 'analyze_diff' as const,
 6 |   description: `
 7 |   This tool is used to analyze a git diff (unstaged, staged, or all changes) against IDE rules to identify rule violations.
 8 |   By default, the tool will use the staged changes, unless the user explicitly asks for unstaged or all changes.
 9 | 
10 |   Parameters:
11 |   - params: An object containing:
12 |     - speedMode: boolean - A mode that can be enabled to speed up the analysis. Default value is false.
13 |     - filterBy: enum - "${FilterBy.violations}" | "${FilterBy.compliants}" | "${FilterBy.humanReviewRequired}" | "${FilterBy.none}" - A filter that can be applied to set the focus of the analysis. Default is ${FilterBy.none}.
14 |     - diff: string - A git diff string.
15 |     - rules: string - Rules to use for analysis, found in the rules subdirectory of the IDE workspace settings. Combine all rules from multiple files by separating them with ---
16 | 
17 |   Returns:
18 |   - A list of rule violations found in the git diff.
19 |   `,
20 |   inputSchema: analyzeDiffInputSchema,
21 | };
22 | 
```

--------------------------------------------------------------------------------
/src/clients/circleci-private/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { HTTPClient } from '../circleci/httpClient.js';
 2 | import { createCircleCIHeaders } from '../circleci/index.js';
 3 | import { JobsPrivate } from './jobsPrivate.js';
 4 | import { MeAPI } from './me.js';
 5 | import { getBaseURL } from '../circleci/index.js';
 6 | 
 7 | /**
 8 |  * Creates a default HTTP client for the CircleCI API private
 9 |  * @param options Configuration parameters
10 |  * @param options.token CircleCI API token
11 |  * @param options.baseURL Base URL for the CircleCI API private
12 |  * @returns HTTP client for CircleCI API private
13 |  */
14 | const defaultPrivateHTTPClient = (options: { token: string }) => {
15 |   if (!options.token) {
16 |     throw new Error('Token is required');
17 |   }
18 | 
19 |   const baseURL = getBaseURL();
20 | 
21 |   return new HTTPClient(baseURL, '/api/private', {
22 |     headers: createCircleCIHeaders({ token: options.token }),
23 |   });
24 | };
25 | 
26 | export class CircleCIPrivateClients {
27 |   public me: MeAPI;
28 |   public jobs: JobsPrivate;
29 |   constructor({
30 |     token,
31 |     privateHTTPClient = defaultPrivateHTTPClient({
32 |       token,
33 |     }),
34 |   }: {
35 |     token: string;
36 |     privateHTTPClient?: HTTPClient;
37 |   }) {
38 |     this.me = new MeAPI(privateHTTPClient);
39 |     this.jobs = new JobsPrivate(privateHTTPClient);
40 |   }
41 | }
42 | 
```

--------------------------------------------------------------------------------
/src/lib/usage-api/parseDateTimeString.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import * as chrono from 'chrono-node';
 2 | 
 3 | /**
 4 |  * @param input The human-readable date string to parse
 5 |  * @param options An optional object to control formatting
 6 |  * @param options.defaultTime Specifies which time to append if the user did not provide one
 7 |  *   - 'start-of-day': Appends T00:00:00Z
 8 |  *   - 'end-of-day':   Appends T23:59:59Z
 9 |  * @returns A formatted date string (full ISO, or YYYY-MM-DD)
10 |  */
11 | export function parseDateTimeString(
12 |   input: string,
13 |   options?: {
14 |     defaultTime?: 'start-of-day' | 'end-of-day';
15 |   }
16 | ): string | null {
17 |   const results = chrono.parse(input);
18 |   if (!results || results.length === 0) {
19 |     return null;
20 |   }
21 | 
22 |   const result = results[0];
23 |   const date = result.start.date();
24 | 
25 |   const timeSpecified =
26 |     result.start.isCertain('hour') ||
27 |     result.start.isCertain('minute') ||
28 |     result.start.isCertain('second');
29 | 
30 |   if (timeSpecified) {
31 |     return date.toISOString();
32 |   }
33 | 
34 |   if (options?.defaultTime) {
35 |     const dateOnly = date.toISOString().slice(0, 10);
36 |     if (options.defaultTime === 'start-of-day') {
37 |       return `${dateOnly}T00:00:00Z`;
38 |     }
39 |     return `${dateOnly}T23:59:59Z`;
40 |   }
41 | 
42 |   return date.toISOString().slice(0, 10);
43 | } 
```

--------------------------------------------------------------------------------
/src/tools/createPromptTemplate/inputSchema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import {
 3 |   defaultModel,
 4 |   defaultTemperature,
 5 |   PromptOrigin,
 6 | } from '../shared/constants.js';
 7 | 
 8 | export const createPromptTemplateInputSchema = z.object({
 9 |   prompt: z
10 |     .string()
11 |     .describe(
12 |       "The user's application, feature, or product requirements that will be used to generate a prompt template. Alternatively, a pre-existing prompt or prompt template can be provided if a user wants to test, evaluate, or modify it. (Can be a multi-line string.)",
13 |     ),
14 |   promptOrigin: z
15 |     .nativeEnum(PromptOrigin)
16 |     .describe(
17 |       `The origin of the prompt - either "${PromptOrigin.codebase}" for existing prompts from the codebase, or "${PromptOrigin.requirements}" for new prompts from requirements.`,
18 |     ),
19 |   model: z
20 |     .string()
21 |     .default(defaultModel)
22 |     .describe(
23 |       `The model that the prompt template will be tested against. Explicitly specify the model if it can be inferred from the codebase. Otherwise, defaults to \`${defaultModel}\`.`,
24 |     ),
25 |   temperature: z
26 |     .number()
27 |     .default(defaultTemperature)
28 |     .describe(
29 |       `The temperature of the prompt template. Explicitly specify the temperature if it can be inferred from the codebase. Otherwise, defaults to ${defaultTemperature}.`,
30 |     ),
31 | });
32 | 
```

--------------------------------------------------------------------------------
/src/tools/findUnderusedResourceClasses/tool.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { findUnderusedResourceClassesInputSchema } from './inputSchema.js';
 2 | 
 3 | export const findUnderusedResourceClassesTool = {
 4 |   name: 'find_underused_resource_classes' as const,
 5 |   description: `
 6 |     Analyzes a CircleCI usage data CSV file to find jobs/resource classes with average or max CPU/RAM usage below a given threshold (default 40%).
 7 |     This helps identify underused resource classes that may be oversized for their workload.
 8 | 
 9 |     Required parameter:
10 |     - csvFilePath: Path to the usage data CSV file (string). IMPORTANT: This must be an absolute path. If you are given a relative path, you must resolve it to an absolute path before calling this tool.
11 | 
12 |     Optional parameter:
13 |     - threshold: Usage percentage threshold (number, default 40)
14 | 
15 |     The tool expects the CSV to have columns: job_name, resource_class, median_cpu_utilization_pct, max_cpu_utilization_pct, median_ram_utilization_pct, max_ram_utilization_pct (case-insensitive). These required columns are a subset of the columns in the CircleCI usage API output and the tool will work with the full set of columns from the usage API CSV.
16 |     It returns a summary report listing all jobs/resource classes where any of these metrics is below the threshold.
17 |   `,
18 |   inputSchema: findUnderusedResourceClassesInputSchema,
19 | };
20 | 
```

--------------------------------------------------------------------------------
/src/clients/circleci/projects.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Project } from '../schemas.js';
 2 | import { HTTPClient } from './httpClient.js';
 3 | 
 4 | export class ProjectsAPI {
 5 |   protected client: HTTPClient;
 6 | 
 7 |   constructor(httpClient: HTTPClient) {
 8 |     this.client = httpClient;
 9 |   }
10 | 
11 |   /**
12 |    * Get project info by slug
13 |    * @param projectSlug The project slug
14 |    * @returns The project info
15 |    * @throws Error if the request fails
16 |    */
17 |   async getProject({ projectSlug }: { projectSlug: string }): Promise<Project> {
18 |     const rawResult = await this.client.get<unknown>(`/project/${projectSlug}`);
19 | 
20 |     const parsedResult = Project.safeParse(rawResult);
21 | 
22 |     if (!parsedResult.success) {
23 |       throw new Error('Failed to parse project response');
24 |     }
25 | 
26 |     return parsedResult.data;
27 |   }
28 | 
29 |   /**
30 |    * Get project info by project ID (uses project ID as slug)
31 |    * @param projectID The project ID
32 |    * @returns The project info
33 |    * @throws Error if the request fails
34 |    */
35 |   async getProjectByID({ projectID }: { projectID: string }): Promise<Project> {
36 |     // For some integrations, project ID can be used directly as project slug
37 |     const rawResult = await this.client.get<unknown>(`/project/${projectID}`);
38 | 
39 |     const parsedResult = Project.safeParse(rawResult);
40 | 
41 |     if (!parsedResult.success) {
42 |       throw new Error('Failed to parse project response');
43 |     }
44 | 
45 |     return parsedResult.data;
46 |   }
47 | }
48 | 
```

--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/PULL_REQUEST.md:
--------------------------------------------------------------------------------

```markdown
 1 | ## PR Checklist
 2 | 
 3 | Please check if your PR fulfills the following requirements:
 4 | 
 5 | - [ ] The commit message follows our contributor [guidelines](https://github.com/CircleCI-Public/mcp-server-circleci/blob/main/CONTRIBUTING.md).
 6 | - [ ] Tests for the changes have been added (for bug fixes / features)
 7 | - [ ] Documentation has been added or updated where needed.
 8 | 
 9 | ## PR Type
10 | 
11 | What kind of change does this PR introduce?
12 | 
13 | <!-- Please check the one that applies to this PR using "x". -->
14 | 
15 | - [ ] Bug fix
16 | - [ ] Feature
17 | - [ ] Code style update (formatting, local variables)
18 | - [ ] Refactoring (no functional changes, no api changes)
19 | - [ ] Build related changes
20 | - [ ] CI related changes
21 | - [ ] Other... Please describe:
22 | 
23 | > more details
24 | 
25 | ## What issues are resolved by this PR?
26 | 
27 | <!-- All Pull Requests should be a response to an existing issue. Please ensure you have created an issue before submitting a PR. -->
28 | 
29 | - #[00]
30 | 
31 | ## Describe the new behavior.
32 | 
33 | <!-- Describe the new behavior introduced by this change. Include an examples or samples needed, such as screenshots or code snippets. -->
34 | 
35 | > Description
36 | 
37 | ## Does this PR introduce a breaking change?
38 | 
39 | - [ ] Yes
40 | - [ ] No
41 | 
42 | <!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
43 | 
44 | ## Other information
45 | 
46 | <!-- Optional. -->
47 | 
48 | > More information (optional)
49 | 
```

--------------------------------------------------------------------------------
/src/tools/recommendPromptTemplateTests/inputSchema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import {
 3 |   defaultModel,
 4 |   defaultTemperature,
 5 |   PromptOrigin,
 6 |   PromptWorkbenchToolName,
 7 | } from '../shared/constants.js';
 8 | 
 9 | export const recommendPromptTemplateTestsInputSchema = z.object({
10 |   template: z
11 |     .string()
12 |     .describe(
13 |       `The prompt template to be tested. Use the \`promptTemplate\` from the latest \`${PromptWorkbenchToolName.create_prompt_template}\` tool output (if available).`,
14 |     ),
15 |   contextSchema: z
16 |     .record(z.string(), z.string())
17 |     .describe(
18 |       `The context schema that defines the expected input parameters for the prompt template. Use the \`contextSchema\` from the latest \`${PromptWorkbenchToolName.create_prompt_template}\` tool output.`,
19 |     ),
20 |   promptOrigin: z
21 |     .nativeEnum(PromptOrigin)
22 |     .describe(
23 |       `The origin of the prompt template, indicating where it came from (e.g. "${PromptOrigin.codebase}" or "${PromptOrigin.requirements}").`,
24 |     ),
25 |   model: z
26 |     .string()
27 |     .default(defaultModel)
28 |     .describe(
29 |       `The model to use for generating actual prompt outputs for testing. Defaults to ${defaultModel}.`,
30 |     ),
31 |   temperature: z
32 |     .number()
33 |     .default(defaultTemperature)
34 |     .describe(
35 |       `The temperature of the prompt template. Explicitly specify the temperature if it can be inferred from the codebase. Otherwise, defaults to ${defaultTemperature}.`,
36 |     ),
37 | });
38 | 
```

--------------------------------------------------------------------------------
/src/tools/createPromptTemplate/handler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | import { createPromptTemplateInputSchema } from './inputSchema.js';
 3 | import { CircletClient } from '../../clients/circlet/index.js';
 4 | import { PromptWorkbenchToolName } from '../shared/constants.js';
 5 | 
 6 | export const promptOriginKey = 'promptOrigin';
 7 | export const promptTemplateKey = 'promptTemplate';
 8 | export const contextSchemaKey = 'contextSchema';
 9 | export const modelKey = 'model';
10 | export const temperatureKey = 'temperature';
11 | 
12 | export const createPromptTemplate: ToolCallback<{
13 |   params: typeof createPromptTemplateInputSchema;
14 | }> = async (args) => {
15 |   const { prompt, promptOrigin, model } = args.params ?? {};
16 | 
17 |   const circlet = new CircletClient();
18 |   const promptObject = await circlet.circlet.createPromptTemplate(
19 |     prompt,
20 |     promptOrigin,
21 |   );
22 | 
23 |   return {
24 |     content: [
25 |       {
26 |         type: 'text',
27 |         text: `${promptOriginKey}: ${promptOrigin}
28 | 
29 | ${promptTemplateKey}: ${promptObject.template}
30 | 
31 | ${contextSchemaKey}: ${JSON.stringify(promptObject.contextSchema, null, 2)}
32 | 
33 | ${modelKey}: ${model}
34 | 
35 | NEXT STEP:
36 | - Immediately call the \`${PromptWorkbenchToolName.recommend_prompt_template_tests}\` tool with:
37 |   - template: the \`${promptTemplateKey}\` above
38 |   - ${contextSchemaKey}: the \`${contextSchemaKey}\` above
39 |   - ${promptOriginKey}: the \`${promptOriginKey}\` above
40 |   - ${modelKey}: the \`${modelKey}\` above
41 |   - ${temperatureKey}: the \`${temperatureKey}\` above
42 | `,
43 |       },
44 |     ],
45 |   };
46 | };
47 | 
```

--------------------------------------------------------------------------------
/src/tools/getBuildFailureLogs/inputSchema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import {
 3 |   branchDescription,
 4 |   projectSlugDescription,
 5 | } from '../shared/constants.js';
 6 | 
 7 | export const getBuildFailureOutputInputSchema = z.object({
 8 |   projectSlug: z.string().describe(projectSlugDescription).optional(),
 9 |   branch: z.string().describe(branchDescription).optional(),
10 |   projectURL: z
11 |     .string()
12 |     .describe(
13 |       'The URL of the CircleCI project. Can be any of these formats:\n' +
14 |         '- Project URL with branch: https://app.circleci.com/pipelines/gh/organization/project?branch=feature-branch\n' +
15 |         '- Pipeline URL: https://app.circleci.com/pipelines/gh/organization/project/123\n' +
16 |         '- Workflow URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def\n' +
17 |         '- Job URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def/jobs/xyz',
18 |     )
19 |     .optional(),
20 |   workspaceRoot: z
21 |     .string()
22 |     .describe(
23 |       'The absolute path to the root directory of your project workspace. ' +
24 |         'This should be the top-level folder containing your source code, configuration files, and dependencies. ' +
25 |         'For example: "/home/user/my-project" or "C:\\Users\\user\\my-project"',
26 |     )
27 |     .optional(),
28 |   gitRemoteURL: z
29 |     .string()
30 |     .describe(
31 |       'The URL of the remote git repository. This should be the URL of the repository that you cloned to your local workspace. ' +
32 |         'For example: "https://github.com/user/my-project.git"',
33 |     )
34 |     .optional(),
35 | });
36 | 
```

--------------------------------------------------------------------------------
/src/lib/latest-pipeline/formatLatestPipelineStatus.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Workflow } from '../../clients/schemas.js';
 2 | import outputTextTruncated, { SEPARATOR } from '../outputTextTruncated.js';
 3 | 
 4 | export const formatLatestPipelineStatus = (workflows: Workflow[]) => {
 5 |   if (!workflows || workflows.length === 0) {
 6 |     return {
 7 |       content: [
 8 |         {
 9 |           type: 'text' as const,
10 |           text: 'No workflows found',
11 |         },
12 |       ],
13 |     };
14 |   }
15 | 
16 |   const outputText = workflows
17 |     .map((workflow) => {
18 |       let duration = 'unknown';
19 | 
20 |       // Calculate duration from timestamps if duration field is not available
21 |       if (workflow.created_at && workflow.stopped_at) {
22 |         const startTime = new Date(workflow.created_at).getTime();
23 |         const endTime = new Date(workflow.stopped_at).getTime();
24 |         const durationInMinutes = Math.round(
25 |           (endTime - startTime) / (1000 * 60),
26 |         );
27 |         duration = `${durationInMinutes} minutes`;
28 |       }
29 | 
30 |       const createdAt = new Date(workflow.created_at).toLocaleString();
31 |       const stoppedAt = workflow.stopped_at
32 |         ? new Date(workflow.stopped_at).toLocaleString()
33 |         : 'in progress';
34 | 
35 |       const fields = [
36 |         `Pipeline Number: ${workflow.pipeline_number}`,
37 |         `Workflow: ${workflow.name}`,
38 |         `Status: ${workflow.status}`,
39 |         `Duration: ${duration}`,
40 |         `Created: ${createdAt}`,
41 |         `Stopped: ${stoppedAt}`,
42 |       ].filter(Boolean);
43 | 
44 |       return `${SEPARATOR}${fields.join('\n')}`;
45 |     })
46 |     .join('\n');
47 | 
48 |   return outputTextTruncated(outputText);
49 | };
50 | 
```

--------------------------------------------------------------------------------
/src/tools/getFlakyTests/inputSchema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { projectSlugDescriptionNoBranch } from '../shared/constants.js';
 3 | 
 4 | export const getFlakyTestLogsInputSchema = z.object({
 5 |   projectSlug: z.string().describe(projectSlugDescriptionNoBranch).optional(),
 6 |   workspaceRoot: z
 7 |     .string()
 8 |     .describe(
 9 |       'The absolute path to the root directory of your project workspace. ' +
10 |         'This should be the top-level folder containing your source code, configuration files, and dependencies. ' +
11 |         'For example: "/home/user/my-project" or "C:\\Users\\user\\my-project"',
12 |     )
13 |     .optional(),
14 |   gitRemoteURL: z
15 |     .string()
16 |     .describe(
17 |       'The URL of the remote git repository. This should be the URL of the repository that you cloned to your local workspace. ' +
18 |         'For example: "https://github.com/user/my-project.git"',
19 |     )
20 |     .optional(),
21 |   projectURL: z
22 |     .string()
23 |     .describe(
24 |       'The URL of the CircleCI project. Can be any of these formats:\n' +
25 |         '- Project URL: https://app.circleci.com/pipelines/gh/organization/project\n' +
26 |         '- Project URL with branch: https://app.circleci.com/pipelines/gh/organization/project?branch=feature-branch\n' +
27 |         '- Pipeline URL: https://app.circleci.com/pipelines/gh/organization/project/123\n' +
28 |         '- Workflow URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def\n' +
29 |         '- Job URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def/jobs/xyz',
30 |     )
31 |     .optional(),
32 | });
33 | 
```

--------------------------------------------------------------------------------
/src/tools/listFollowedProjects/tool.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { listFollowedProjectsInputSchema } from './inputSchema.js';
 2 | 
 3 | export const listFollowedProjectsTool = {
 4 |   name: 'list_followed_projects' as const,
 5 |   description: `
 6 |     This tool lists all projects that the user is following on CircleCI.
 7 |     
 8 |     Common use cases:
 9 |     - Identify which CircleCI projects are available to the user
10 |     - Select a project for subsequent operations
11 |     - Obtain the projectSlug needed for other CircleCI tools
12 |     
13 |     Returns:
14 |     - A list of projects that the user is following on CircleCI
15 |     - Each entry includes the project name and its projectSlug
16 |     
17 |     Workflow:
18 |     1. Run this tool to see available projects
19 |     2. User selects a project from the list
20 |     3. The LLM should extract and use the projectSlug (not the project name) from the selected project for subsequent tool calls
21 |     4. The projectSlug is required for many other CircleCI tools, and will be used for those tool calls after a project is selected
22 |     
23 |     Note: If pagination limits are reached, the tool will indicate that not all projects could be displayed.
24 |     
25 |     IMPORTANT: Do not automatically run any additional tools after this tool is called. Wait for explicit user instruction before executing further tool calls. The LLM MUST NOT invoke any other CircleCI tools until receiving a clear instruction from the user about what to do next, even if the user selects a project. It is acceptable to list out tool call options for the user to choose from, but do not execute them until instructed.
26 |     `,
27 |   inputSchema: listFollowedProjectsInputSchema,
28 | };
29 | 
```

--------------------------------------------------------------------------------
/src/tools/analyzeDiff/handler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | import { analyzeDiffInputSchema } from './inputSchema.js';
 3 | import { CircletClient } from '../../clients/circlet/index.js';
 4 | 
 5 | /**
 6 |  * Analyzes a git diff against cursor rules to identify rule violations
 7 |  * @param args - Tool arguments containing diff content and rules
 8 |  * @returns Analysis result with any rule violations found
 9 |  */
10 | export const analyzeDiff: ToolCallback<{
11 |   params: typeof analyzeDiffInputSchema;
12 | }> = async (args) => {
13 |   const { diff, rules, speedMode, filterBy } = args.params ?? {};
14 |   const circlet = new CircletClient();
15 |   if (!diff) {
16 |     return {
17 |       content: [
18 |         {
19 |           type: 'text',
20 |           text: 'No diff found. Please provide a diff to analyze.',
21 |         },
22 |       ],
23 |     };
24 |   }
25 | 
26 |   if (!rules) {
27 |     return {
28 |       content: [
29 |         {
30 |           type: 'text',
31 |           text: 'No rules found. Please add rules to your repository.',
32 |         },
33 |       ],
34 |     };
35 |   }
36 | 
37 |   const response = await circlet.circlet.ruleReview({
38 |     diff,
39 |     rules,
40 |     filterBy,
41 |     speedMode,
42 |   });
43 | 
44 |   if (!response.isRuleCompliant) {
45 |     return {
46 |       content: [
47 |         {
48 |           type: 'text',
49 |           text: response.relatedRules.violations
50 |             .map((violation) => {
51 |               return `Rule: ${violation.rule}\nReason: ${violation.reason}\nConfidence Score: ${violation.confidenceScore}`;
52 |             })
53 |             .join('\n\n'),
54 |         },
55 |       ],
56 |     };
57 |   }
58 | 
59 |   return {
60 |     content: [
61 |       {
62 |         type: 'text',
63 |         text: `All rules are compliant.`,
64 |       },
65 |     ],
66 |   };
67 | };
68 | 
```

--------------------------------------------------------------------------------
/src/tools/listFollowedProjects/handler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | 
 3 | import mcpErrorOutput from '../../lib/mcpErrorOutput.js';
 4 | import { getCircleCIPrivateClient } from '../../clients/client.js';
 5 | import { listFollowedProjectsInputSchema } from './inputSchema.js';
 6 | 
 7 | export const listFollowedProjects: ToolCallback<{
 8 |   params: typeof listFollowedProjectsInputSchema;
 9 | }> = async () => {
10 |   const circleciPrivate = getCircleCIPrivateClient();
11 |   const followedProjects = await circleciPrivate.me.getFollowedProjects();
12 | 
13 |   const { projects, reachedMaxPagesOrTimeout } = followedProjects;
14 | 
15 |   if (projects.length === 0) {
16 |     return mcpErrorOutput(
17 |       'No projects found. Please make sure you have access to projects and have followed them.',
18 |     );
19 |   }
20 | 
21 |   const formattedProjectChoices = projects
22 |     .map(
23 |       (project, index) =>
24 |         `${index + 1}. ${project.name} (projectSlug: ${project.slug})`,
25 |     )
26 |     .join('\n');
27 | 
28 |   let resultText = `Projects followed:\n${formattedProjectChoices}\n\nPlease have the user choose one of these projects by name or number. When they choose, you (the LLM) should extract and use the projectSlug (not the project name) associated with their chosen project for subsequent tool calls. This projectSlug is required for tools like get_build_failure_logs, getFlakyTests, and get_job_test_results.`;
29 | 
30 |   if (reachedMaxPagesOrTimeout) {
31 |     resultText = `WARNING: Not all projects were included due to pagination limits or timeout.\n\n${resultText}`;
32 |   }
33 | 
34 |   return {
35 |     content: [
36 |       {
37 |         type: 'text',
38 |         text: resultText,
39 |       },
40 |     ],
41 |   };
42 | };
43 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Base image with Node.js and pnpm setup
 2 | FROM node:lts-alpine AS base
 3 | ENV PNPM_HOME="/pnpm"
 4 | ENV PATH="$PNPM_HOME:$PATH"
 5 | RUN corepack enable
 6 | WORKDIR /app
 7 | 
 8 | # Production dependencies stage
 9 | FROM base AS prod-deps
10 | COPY package.json pnpm-lock.yaml ./
11 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
12 |     pnpm install --prod --frozen-lockfile --ignore-scripts
13 | 
14 | # Build stage
15 | FROM base AS build
16 | # Install build dependencies
17 | RUN apk add --no-cache git python3 make g++
18 | # Copy package files first for better caching
19 | COPY package.json pnpm-lock.yaml ./
20 | # Install all dependencies including devDependencies for building
21 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
22 |     pnpm install --frozen-lockfile --ignore-scripts=false
23 | # Install express types and ensure all dependencies are available
24 | RUN pnpm add -D @types/express
25 | # Copy source files
26 | COPY . .
27 | # Build the application
28 | RUN pnpm run build
29 | # Install production dependencies for the final image
30 | RUN pnpm install --prod --frozen-lockfile
31 | 
32 | # Final stage - clean minimal image
33 | FROM node:lts-alpine
34 | ENV NODE_ENV=production
35 | WORKDIR /app
36 | 
37 | #Debug Build Info
38 | ARG BUILD_ID=""
39 | ENV BUILD_ID=$BUILD_ID
40 | 
41 | # Install pnpm
42 | RUN corepack enable && corepack prepare pnpm@latest --activate
43 | 
44 | # Copy package files and install only production dependencies
45 | COPY package.json pnpm-lock.yaml ./
46 | # Install production dependencies
47 | RUN pnpm install --prod --frozen-lockfile
48 | 
49 | # Copy built files and node_modules
50 | COPY --from=build /app/dist /app/dist
51 | COPY --from=build /app/node_modules /app/node_modules
52 | 
53 | # Docker container to listen on port 8000
54 | EXPOSE 8000
55 | 
56 | # Command to run the MCP server
57 | ENTRYPOINT ["node", "dist/index.js"]
```

--------------------------------------------------------------------------------
/src/tools/rerunWorkflow/handler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | import { rerunWorkflowInputSchema } from './inputSchema.js';
 3 | import { getCircleCIClient } from '../../clients/client.js';
 4 | import mcpErrorOutput from '../../lib/mcpErrorOutput.js';
 5 | import { getAppURL } from '../../clients/circleci/index.js';
 6 | import { getWorkflowIdFromURL } from '../../lib/getWorkflowIdFromURL.js';
 7 | 
 8 | export const rerunWorkflow: ToolCallback<{
 9 |   params: typeof rerunWorkflowInputSchema;
10 | }> = async (args) => {
11 |   let { workflowId } = args.params ?? {};
12 |   const { fromFailed, workflowURL } = args.params ?? {};
13 |   const baseURL = getAppURL();
14 |   const circleci = getCircleCIClient();
15 | 
16 |   if (workflowURL) {
17 |     workflowId = getWorkflowIdFromURL(workflowURL);
18 |   }
19 | 
20 |   if (!workflowId) {
21 |     return mcpErrorOutput(
22 |       'workflowId is required and could not be determined from workflowURL.',
23 |     );
24 |   }
25 | 
26 |   const workflow = await circleci.workflows.getWorkflow({
27 |     workflowId,
28 |   });
29 | 
30 |   if (!workflow) {
31 |     return mcpErrorOutput('Workflow not found');
32 |   }
33 | 
34 |   const workflowFailed = workflow?.status?.toLowerCase() === 'failed';
35 | 
36 |   if (fromFailed && !workflowFailed) {
37 |     return mcpErrorOutput('Workflow is not failed, cannot rerun from failed');
38 |   }
39 | 
40 |   const newWorkflow = await circleci.workflows.rerunWorkflow({
41 |     workflowId,
42 |     fromFailed: fromFailed !== undefined ? fromFailed : workflowFailed,
43 |   });
44 | 
45 |   const workflowUrl = `${baseURL}/pipelines/workflows/${newWorkflow.workflow_id}`;
46 |   return {
47 |     content: [
48 |       {
49 |         type: 'text',
50 |         text: `New workflowId is ${newWorkflow.workflow_id} and [View Workflow in CircleCI](${workflowUrl})`,
51 |       },
52 |     ],
53 |   };
54 | };
55 | 
```

--------------------------------------------------------------------------------
/src/lib/pipeline-job-tests/formatJobTests.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Test } from '../../clients/schemas.js';
 2 | import outputTextTruncated, { SEPARATOR } from '../outputTextTruncated.js';
 3 | 
 4 | /**
 5 |  * Formats test results into a readable format
 6 |  * @param tests - Array of test results
 7 |  * @returns MCP output object with formatted test results
 8 |  */
 9 | export const formatJobTests = (tests: Test[]) => {
10 |   if (tests.length === 0) {
11 |     return {
12 |       content: [
13 |         {
14 |           type: 'text' as const,
15 |           text: `No test results found. 
16 |           
17 | Possible reasons:
18 | 1. The selected job doesn't have test results reported
19 | 2. Tests might be reported in a different job in the workflow
20 | 3. The project may not be configured to collect test metadata
21 | 
22 | Try looking at a different job, pipeline, or branch. You can also check the project's CircleCI configuration to verify test reporting is set up correctly.
23 | 
24 | Note: Not all CircleCI jobs collect test metadata. This requires specific configuration in the .circleci/config.yml file using the store_test_results step.
25 | 
26 | For more information on how to configure test metadata collection, see the CircleCI documentation:
27 | https://circleci.com/docs/collect-test-data/`,
28 |         },
29 |       ],
30 |     };
31 |   }
32 | 
33 |   const outputText = tests
34 |     .map((test) => {
35 |       const fields = [
36 |         test.file && `File Name: ${test.file}`,
37 |         test.classname && `Classname: ${test.classname}`,
38 |         test.name && `Test name: ${test.name}`,
39 |         test.result && `Result: ${test.result}`,
40 |         test.run_time && `Run time: ${test.run_time}`,
41 |         test.message && `Message: ${test.message}`,
42 |       ].filter(Boolean);
43 |       return `${SEPARATOR}${fields.join('\n')}`;
44 |     })
45 |     .join('\n');
46 | 
47 |   return outputTextTruncated(outputText);
48 | };
49 | 
```

--------------------------------------------------------------------------------
/src/tools/getLatestPipelineStatus/inputSchema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import {
 3 |   branchDescription,
 4 |   projectSlugDescription,
 5 | } from '../shared/constants.js';
 6 | 
 7 | export const getLatestPipelineStatusInputSchema = z.object({
 8 |   projectSlug: z.string().describe(projectSlugDescription).optional(),
 9 |   branch: z.string().describe(branchDescription).optional(),
10 |   projectURL: z
11 |     .string()
12 |     .describe(
13 |       'The URL of the CircleCI project. Can be any of these formats:\n' +
14 |         '- Project URL with branch: https://app.circleci.com/pipelines/gh/organization/project?branch=feature-branch\n' +
15 |         '- Legacy Pipeline URL: https://circleci.com/gh/organization/project/123\n' +
16 |         '- Legacy Pipeline URL with branch: https://circleci.com/gh/organization/project/123?branch=feature-branch\n' +
17 |         '- Pipeline URL: https://app.circleci.com/pipelines/gh/organization/project/123\n' +
18 |         '- Workflow URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def\n' +
19 |         '- Job URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def/jobs/xyz',
20 |     )
21 |     .optional(),
22 |   workspaceRoot: z
23 |     .string()
24 |     .describe(
25 |       'The absolute path to the root directory of your project workspace. ' +
26 |         'This should be the top-level folder containing your source code, configuration files, and dependencies. ' +
27 |         'For example: "/home/user/my-project" or "C:\\Users\\user\\my-project"',
28 |     )
29 |     .optional(),
30 |   gitRemoteURL: z
31 |     .string()
32 |     .describe(
33 |       'The URL of the remote git repository. This should be the URL of the repository that you cloned to your local workspace. ' +
34 |         'For example: "https://github.com/user/my-project.git"',
35 |     )
36 |     .optional(),
37 | });
38 | 
```

--------------------------------------------------------------------------------
/src/lib/project-detection/vcsTool.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export type VCSDefinition = {
 2 |   host: 'github.com' | 'bitbucket.org' | 'circleci.com';
 3 |   name: 'github' | 'bitbucket' | 'circleci';
 4 |   short: 'gh' | 'bb' | 'circleci';
 5 | };
 6 | 
 7 | /**
 8 |  * Gitlab is not compatible with this representation
 9 |  * https://circleci.atlassian.net/browse/DEVEX-175
10 |  */
11 | export const vcses: VCSDefinition[] = [
12 |   {
13 |     host: 'github.com',
14 |     name: 'github',
15 |     short: 'gh',
16 |   },
17 |   {
18 |     host: 'bitbucket.org',
19 |     name: 'bitbucket',
20 |     short: 'bb',
21 |   },
22 |   {
23 |     host: 'circleci.com',
24 |     name: 'circleci',
25 |     short: 'circleci',
26 |   },
27 | ];
28 | 
29 | export class UnhandledVCS extends Error {
30 |   constructor(vcs: string) {
31 |     super(`VCS ${vcs} is not handled at the moment`);
32 |   }
33 | }
34 | 
35 | export function getVCSFromHost(host: string): VCSDefinition | undefined {
36 |   return vcses.find(({ host: vcsHost }) => host === vcsHost);
37 | }
38 | 
39 | export function mustGetVCSFromHost(host: string): VCSDefinition {
40 |   const vcs = getVCSFromHost(host);
41 | 
42 |   if (vcs === undefined) {
43 |     throw new UnhandledVCS(host);
44 |   }
45 |   return vcs;
46 | }
47 | 
48 | export function getVCSFromName(name: string): VCSDefinition | undefined {
49 |   return vcses.find(({ name: vcsName }) => name === vcsName);
50 | }
51 | 
52 | export function mustGetVCSFromName(name: string): VCSDefinition {
53 |   const vcs = getVCSFromName(name);
54 | 
55 |   if (vcs === undefined) {
56 |     throw new UnhandledVCS(name);
57 |   }
58 |   return vcs;
59 | }
60 | 
61 | export function getVCSFromShort(short: string): VCSDefinition | undefined {
62 |   return vcses.find(({ short: vcsShort }) => short === vcsShort);
63 | }
64 | 
65 | export function mustGetVCSFromShort(short: string): VCSDefinition {
66 |   const vcs = getVCSFromShort(short);
67 | 
68 |   if (vcs === undefined) {
69 |     throw new UnhandledVCS(short);
70 |   }
71 |   return vcs;
72 | }
73 | 
74 | export function isLegacyProject(projectSlug: string) {
75 |   return ['gh', 'bb'].includes(projectSlug.split('/')[0]);
76 | }
77 | 
```

--------------------------------------------------------------------------------
/src/tools/getJobTestResults/inputSchema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import {
 3 |   branchDescription,
 4 |   projectSlugDescription,
 5 | } from '../shared/constants.js';
 6 | 
 7 | export const getJobTestResultsInputSchema = z.object({
 8 |   projectSlug: z.string().describe(projectSlugDescription).optional(),
 9 |   branch: z.string().describe(branchDescription).optional(),
10 |   workspaceRoot: z
11 |     .string()
12 |     .describe(
13 |       'The absolute path to the root directory of your project workspace. ' +
14 |         'This should be the top-level folder containing your source code, configuration files, and dependencies. ' +
15 |         'For example: "/home/user/my-project" or "C:\\Users\\user\\my-project"',
16 |     )
17 |     .optional(),
18 |   gitRemoteURL: z
19 |     .string()
20 |     .describe(
21 |       'The URL of the remote git repository. This should be the URL of the repository that you cloned to your local workspace. ' +
22 |         'For example: "https://github.com/user/my-project.git"',
23 |     )
24 |     .optional(),
25 |   projectURL: z
26 |     .string()
27 |     .describe(
28 |       'The URL of the CircleCI project. Can be any of these formats:\n' +
29 |         '- Project URL: https://app.circleci.com/pipelines/gh/organization/project\n' +
30 |         '- Project URL with branch: https://app.circleci.com/pipelines/gh/organization/project?branch=feature-branch\n' +
31 |         '- Pipeline URL: https://app.circleci.com/pipelines/gh/organization/project/123\n' +
32 |         '- Workflow URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def\n' +
33 |         '- Job URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def/jobs/123',
34 |     )
35 |     .optional(),
36 |   filterByTestsResult: z
37 |     .enum(['failure', 'success'])
38 |     .describe(
39 |       `Filter the tests by result.
40 |       If "failure", only failed tests will be returned.
41 |       If "success", only successful tests will be returned.
42 |       `,
43 |     )
44 |     .optional(),
45 | });
46 | 
```

--------------------------------------------------------------------------------
/src/clients/circleci-private/jobsPrivate.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { HTTPClient } from '../circleci/httpClient.js';
 3 | 
 4 | const JobOutputResponseSchema = z.string();
 5 | 
 6 | const JobErrorResponseSchema = z.string();
 7 | 
 8 | type JobOutputResponse = z.infer<typeof JobOutputResponseSchema>;
 9 | type JobErrorResponse = z.infer<typeof JobErrorResponseSchema>;
10 | 
11 | export class JobsPrivate {
12 |   protected client: HTTPClient;
13 | 
14 |   constructor(client: HTTPClient) {
15 |     this.client = client;
16 |   }
17 |   /**
18 |    * Get detailed information about a specific job
19 |    * @param params Configuration parameters
20 |    * @param params.projectSlug The project slug (e.g., "gh/CircleCI-Public/api-preview-docs")
21 |    * @param params.jobNumber The number of the job
22 |    * @param params.taskIndex The index of the task
23 |    * @param params.stepId The id of the step
24 |    * @returns Detailed job information including status, timing, and build details
25 |    */
26 |   async getStepOutput({
27 |     projectSlug,
28 |     jobNumber,
29 |     taskIndex,
30 |     stepId,
31 |   }: {
32 |     projectSlug: string;
33 |     jobNumber: number;
34 |     taskIndex: number;
35 |     stepId: number;
36 |   }) {
37 |     // /api/private/output/raw/:vcs/:user/:prj/:num/output/:task_index/:step_id
38 |     const outputResult = await this.client.get<JobOutputResponse>(
39 |       `/output/raw/${projectSlug}/${jobNumber}/output/${taskIndex}/${stepId}`,
40 |     );
41 |     const parsedOutput = JobOutputResponseSchema.safeParse(outputResult);
42 | 
43 |     // /api/private/output/raw/:vcs/:user/:prj/:num/error/:task_index/:step_id
44 |     const errorResult = await this.client.get<JobErrorResponse>(
45 |       `/output/raw/${projectSlug}/${jobNumber}/error/${taskIndex}/${stepId}`,
46 |     );
47 |     const parsedError = JobErrorResponseSchema.safeParse(errorResult);
48 | 
49 |     if (!parsedOutput.success || !parsedError.success) {
50 |       throw new Error('Failed to parse job output or error response');
51 |     }
52 | 
53 |     return {
54 |       output: parsedOutput.data,
55 |       error: parsedError.data,
56 |     };
57 |   }
58 | }
59 | 
```

--------------------------------------------------------------------------------
/src/clients/circleci/usage.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import {
 2 |   UsageExportJobStart,
 3 |   UsageExportJobStatus,
 4 | } from '../schemas.js';
 5 | import { HTTPClient } from './httpClient.js';
 6 | 
 7 | 
 8 | 
 9 | export class UsageAPI {
10 |   private client: HTTPClient;
11 | 
12 |   constructor(client: HTTPClient) {
13 |     this.client = client;
14 |   }
15 | 
16 |   /**
17 |    * Starts a usage export job on CircleCI
18 |    * @param orgId The organization ID
19 |    * @param start The start date for the usage report in 'YYYY-MM-DD' format
20 |    * @param end The end date for the usage report in 'YYYY-MM-DD' format
21 |    * @returns The confirmation and ID for the newly created export job
22 |    * @throws Will throw an error if the CircleCI API returns a non-OK response
23 |    */
24 |   async startUsageExportJob(
25 |     orgId: string,
26 |     start: string,
27 |     end: string,
28 |   ): Promise<UsageExportJobStart> {
29 |     const responseData = await this.client.post<unknown>(
30 |       `/organizations/${orgId}/usage_export_job`,
31 |       { start, end },
32 |     );
33 |     const parsed = UsageExportJobStart.safeParse(responseData);
34 |     if (!parsed.success) {
35 |       throw new Error(
36 |         `Failed to parse startUsageExportJob response: ${parsed.error.message}`,
37 |       );
38 |     }
39 |     return parsed.data;
40 |   }
41 | 
42 |   /**
43 |    * Gets the status of a usage export job
44 |    * @param orgId The organization ID
45 |    * @param jobId The ID of the export job
46 |    * @returns The status of the export job, including a download URL on success
47 |    * @throws Will throw an error if the CircleCI API returns a non-OK response
48 |    */
49 |   async getUsageExportJobStatus(
50 |     orgId: string,
51 |     jobId: string,
52 |   ): Promise<UsageExportJobStatus> {
53 |     const responseData = await this.client.get<unknown>(
54 |       `/organizations/${orgId}/usage_export_job/${jobId}`,
55 |     );
56 |     const parsed = UsageExportJobStatus.safeParse(responseData);
57 |     if (!parsed.success) {
58 |       throw new Error(
59 |         `Failed to parse getUsageExportJobStatus response: ${parsed.error.message}`,
60 |       );
61 |     }
62 |     return parsed.data;
63 |   }
64 | }
65 | 
```

--------------------------------------------------------------------------------
/src/tools/recommendPromptTemplateTests/tool.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { recommendPromptTemplateTestsInputSchema } from './inputSchema.js';
 2 | import { PromptWorkbenchToolName, PromptOrigin } from '../shared/constants.js';
 3 | import { modelKey } from '../createPromptTemplate/handler.js';
 4 | 
 5 | const paramsKey = 'params';
 6 | const promptTemplateKey = 'promptTemplate';
 7 | const contextSchemaKey = 'contextSchema';
 8 | const promptOriginKey = 'promptOrigin';
 9 | const recommendedTestsVar = '`recommendedTests`';
10 | 
11 | export const recommendPromptTemplateTestsTool = {
12 |   name: PromptWorkbenchToolName.recommend_prompt_template_tests,
13 |   description: `
14 |   About this tool:
15 |   - This tool is part of a toolchain that generates and provides test cases for a prompt template.
16 |   - This tool generates an array of recommended tests for a given prompt template.
17 | 
18 |   Parameters:
19 |   - ${paramsKey}: object
20 |     - ${promptTemplateKey}: string (the prompt template to be tested)
21 |     - ${contextSchemaKey}: object (the context schema that defines the expected input parameters for the prompt template)
22 |     - ${promptOriginKey}: "${PromptOrigin.codebase}" | "${PromptOrigin.requirements}" (indicates whether the prompt comes from an existing codebase or from new requirements)
23 |     - ${modelKey}: string (the model that the prompt template will be tested against)
24 |     
25 |   Example usage:
26 |   {
27 |     "${paramsKey}": {
28 |       "${promptTemplateKey}": "The user wants a bedtime story about {{topic}} for a person of age {{age}} years old. Please craft a captivating tale that captivates their imagination and provides a delightful bedtime experience.",
29 |       "${contextSchemaKey}": {
30 |         "topic": "string",
31 |         "age": "number"
32 |       },
33 |       "${promptOriginKey}": "${PromptOrigin.codebase}"
34 |     }
35 |   }
36 | 
37 |   The tool will return a structured array of test cases that can be used to test the prompt template.
38 | 
39 |   Tool output instructions:
40 |     - The tool will return a ${recommendedTestsVar} array that can be used to test the prompt template.
41 |   `,
42 |   inputSchema: recommendPromptTemplateTestsInputSchema,
43 | };
44 | 
```

--------------------------------------------------------------------------------
/src/tools/runPipeline/inputSchema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import {
 3 |   branchDescription,
 4 |   projectSlugDescription,
 5 | } from '../shared/constants.js';
 6 | 
 7 | export const runPipelineInputSchema = z.object({
 8 |   projectSlug: z.string().describe(projectSlugDescription).optional(),
 9 |   branch: z.string().describe(branchDescription).optional(),
10 |   workspaceRoot: z
11 |     .string()
12 |     .describe(
13 |       'The absolute path to the root directory of your project workspace. ' +
14 |         'This should be the top-level folder containing your source code, configuration files, and dependencies. ' +
15 |         'For example: "/home/user/my-project" or "C:\\Users\\user\\my-project"',
16 |     )
17 |     .optional(),
18 |   gitRemoteURL: z
19 |     .string()
20 |     .describe(
21 |       'The URL of the remote git repository. This should be the URL of the repository that you cloned to your local workspace. ' +
22 |         'For example: "https://github.com/user/my-project.git"',
23 |     )
24 |     .optional(),
25 |   projectURL: z
26 |     .string()
27 |     .describe(
28 |       'The URL of the CircleCI project. Can be any of these formats:\n' +
29 |         '- Project URL with branch: https://app.circleci.com/pipelines/gh/organization/project?branch=feature-branch\n' +
30 |         '- Pipeline URL: https://app.circleci.com/pipelines/gh/organization/project/123\n' +
31 |         '- Workflow URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def\n' +
32 |         '- Job URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def/jobs/xyz',
33 |     )
34 |     .optional(),
35 |   pipelineChoiceName: z
36 |     .string()
37 |     .describe(
38 |       'The name of the pipeline to run. This parameter is only needed if the project has multiple pipeline definitions. ' +
39 |         'If not provided and multiple pipelines exist, the tool will return a list of available pipelines for the user to choose from. ' +
40 |         'If provided, it must exactly match one of the pipeline names returned by the tool.',
41 |     )
42 |     .optional(),
43 |   configContent: z
44 |     .string()
45 |     .describe(
46 |       'The content of the CircleCI YAML configuration file for the pipeline.',
47 |     )
48 |     .optional(),
49 | });
50 | 
```

--------------------------------------------------------------------------------
/src/tools/getLatestPipelineStatus/handler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | import { getLatestPipelineStatusInputSchema } from './inputSchema.js';
 3 | import {
 4 |   getBranchFromURL,
 5 |   getProjectSlugFromURL,
 6 | } from '../../lib/project-detection/index.js';
 7 | import mcpErrorOutput from '../../lib/mcpErrorOutput.js';
 8 | import { identifyProjectSlug } from '../../lib/project-detection/index.js';
 9 | import { getLatestPipelineWorkflows } from '../../lib/latest-pipeline/getLatestPipelineWorkflows.js';
10 | import { formatLatestPipelineStatus } from '../../lib/latest-pipeline/formatLatestPipelineStatus.js';
11 | 
12 | export const getLatestPipelineStatus: ToolCallback<{
13 |   params: typeof getLatestPipelineStatusInputSchema;
14 | }> = async (args) => {
15 |   const {
16 |     workspaceRoot,
17 |     gitRemoteURL,
18 |     branch,
19 |     projectURL,
20 |     projectSlug: inputProjectSlug,
21 |   } = args.params ?? {};
22 | 
23 |   let projectSlug: string | null | undefined;
24 |   let branchFromURL: string | null | undefined;
25 | 
26 |   if (inputProjectSlug) {
27 |     if (!branch) {
28 |       return mcpErrorOutput(
29 |         'Branch not provided. When using projectSlug, a branch must also be specified.',
30 |       );
31 |     }
32 |     projectSlug = inputProjectSlug;
33 |   } else if (projectURL) {
34 |     projectSlug = getProjectSlugFromURL(projectURL);
35 |     branchFromURL = getBranchFromURL(projectURL);
36 |   } else if (workspaceRoot && gitRemoteURL) {
37 |     projectSlug = await identifyProjectSlug({
38 |       gitRemoteURL,
39 |     });
40 |   } else {
41 |     return mcpErrorOutput(
42 |       'Missing required inputs. Please provide either: 1) projectSlug with branch, 2) projectURL, or 3) workspaceRoot with gitRemoteURL and branch.',
43 |     );
44 |   }
45 | 
46 |   if (!projectSlug) {
47 |     return mcpErrorOutput(`
48 |           Project not found. Ask the user to provide the inputs user can provide based on the tool description.
49 | 
50 |           Project slug: ${projectSlug}
51 |           Git remote URL: ${gitRemoteURL}
52 |           Branch: ${branch}
53 |           `);
54 |   }
55 | 
56 |   const latestPipelineWorkflows = await getLatestPipelineWorkflows({
57 |     projectSlug,
58 |     branch: branchFromURL ?? branch,
59 |   });
60 | 
61 |   return formatLatestPipelineStatus(latestPipelineWorkflows);
62 | };
63 | 
```

--------------------------------------------------------------------------------
/src/tools/configHelper/handler.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
 2 | import { configHelper } from './handler.js';
 3 | import * as client from '../../clients/client.js';
 4 | 
 5 | // Mock dependencies
 6 | vi.mock('../../clients/client.js');
 7 | 
 8 | describe('configHelper handler', () => {
 9 |   beforeEach(() => {
10 |     vi.resetAllMocks();
11 |   });
12 | 
13 |   it('should return a valid MCP success response when config is valid', async () => {
14 |     const mockCircleCIClient = {
15 |       configValidate: {
16 |         validateConfig: vi.fn().mockResolvedValue({ valid: true }),
17 |       },
18 |     };
19 | 
20 |     vi.spyOn(client, 'getCircleCIClient').mockReturnValue(
21 |       mockCircleCIClient as any,
22 |     );
23 | 
24 |     const args = {
25 |       params: {
26 |         configFile:
27 |           'version: 2.1\njobs:\n  build:\n    docker:\n      - image: cimg/node:16.0',
28 |       },
29 |     } as any;
30 | 
31 |     const controller = new AbortController();
32 |     const response = await configHelper(args, {
33 |       signal: controller.signal,
34 |     });
35 | 
36 |     expect(response).toHaveProperty('content');
37 |     expect(Array.isArray(response.content)).toBe(true);
38 |     expect(response.content[0]).toHaveProperty('type', 'text');
39 |     expect(typeof response.content[0].text).toBe('string');
40 |   });
41 | 
42 |   it('should return a valid MCP success response (with error info) when config is invalid', async () => {
43 |     const mockCircleCIClient = {
44 |       configValidate: {
45 |         validateConfig: vi.fn().mockResolvedValue({
46 |           valid: false,
47 |           errors: [{ message: 'Invalid config' }],
48 |         }),
49 |       },
50 |     };
51 | 
52 |     vi.spyOn(client, 'getCircleCIClient').mockReturnValue(
53 |       mockCircleCIClient as any,
54 |     );
55 | 
56 |     const args = {
57 |       params: {
58 |         configFile: 'invalid yaml',
59 |       },
60 |     } as any;
61 | 
62 |     const controller = new AbortController();
63 |     const response = await configHelper(args, {
64 |       signal: controller.signal,
65 |     });
66 | 
67 |     expect(response).toHaveProperty('content');
68 |     expect(Array.isArray(response.content)).toBe(true);
69 |     expect(response.content[0]).toHaveProperty('type', 'text');
70 |     expect(typeof response.content[0].text).toBe('string');
71 |     expect(response.content[0].text).toContain('Invalid config');
72 |   });
73 | });
74 | 
```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: '💡 Feature Request'
 2 | description: Have an idea for improving the MCP CircleCI server? Begin by submitting a Feature Request
 3 | title: 'Feature Request: '
 4 | labels: [feature_request]
 5 | # assignees: ''
 6 | body:
 7 |   - type: checkboxes
 8 |     attributes:
 9 |       label: 'Is there an existing issue that is already proposing this?'
10 |       description: 'Please search [here](https://github.com/CircleCI-Public/mcp-server-circleci/issues?q=is%3Aissue) to see if an issue already exists for the feature you are requesting'
11 |       options:
12 |         - label: 'I have searched the existing issues'
13 |           required: true
14 |   - type: textarea
15 |     id: contact
16 |     attributes:
17 |       label: 'Is your feature request related to a problem with the MCP CircleCI integration?'
18 |       description: 'A clear and concise description of what the problem is. This could be related to the Model Context Protocol implementation, CircleCI integration, or server functionality.'
19 |       placeholder: |
20 |         I have an issue when trying to...
21 |         - Integrate with MCP Client enabled Agents (eg: Cursor, Windsurf, Claude Code, etc)
22 |         - Handle specific model responses
23 |         - Process certain types of requests
24 |         - etc.
25 |     validations:
26 |       required: false
27 |   - type: textarea
28 |     validations:
29 |       required: true
30 |     attributes:
31 |       label: "Describe the solution you'd like"
32 |       description: "A clear and concise description of what you want to happen. Include any specific MCP or CircleCI features you'd like to see supported."
33 |   - type: textarea
34 |     validations:
35 |       required: true
36 |     attributes:
37 |       label: 'Implementation and compatibility considerations'
38 |       description: "Please describe any considerations around:\n- Compatibility with existing MCP features\n- CircleCI API integration requirements\n- Performance implications\n- Security considerations"
39 |   - type: textarea
40 |     validations:
41 |       required: true
42 |     attributes:
43 |       label: 'What is the motivation / use case for this feature?'
44 |       description: 'Describe the specific use case this would enable for MCP CircleCI integration. For example, how would this improve the development workflow or enhance the integration capabilities?'
45 | 
```

--------------------------------------------------------------------------------
/src/tools/downloadUsageApiData/handler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | import { downloadUsageApiDataInputSchema } from './inputSchema.js';
 3 | import { getUsageApiData } from '../../lib/usage-api/getUsageApiData.js';
 4 | import { parseDateTimeString } from '../../lib/usage-api/parseDateTimeString.js';
 5 | import { differenceInCalendarDays, parseISO } from 'date-fns';
 6 | import mcpErrorOutput from '../../lib/mcpErrorOutput.js';
 7 | 
 8 | export const downloadUsageApiData: ToolCallback<{ params: typeof downloadUsageApiDataInputSchema }> = async (args) => {
 9 |   const { CIRCLECI_BASE_URL } = process.env;
10 |   if (CIRCLECI_BASE_URL && CIRCLECI_BASE_URL !== 'https://circleci.com') {
11 |     return mcpErrorOutput('ERROR: The Usage API is not available on CircleCI server installations. This tool is only available for CircleCI cloud users.');
12 |   }
13 | 
14 |   const {
15 |     orgId,
16 |     startDate,
17 |     endDate,
18 |     outputDir,
19 |     jobId,
20 |   } = args.params ?? {};
21 | 
22 |   const hasDates = Boolean(startDate) && Boolean(endDate);
23 |   const hasJobId = Boolean(jobId);
24 | 
25 |   if (!hasJobId && !hasDates) {
26 |     return mcpErrorOutput('ERROR: Provide either jobId to check/download an existing export, or both startDate and endDate to start a new export job.');
27 |   }
28 | 
29 |   const normalizedStartDate = startDate
30 |     ? (parseDateTimeString(startDate, { defaultTime: 'start-of-day' }) ?? undefined)
31 |     : undefined;
32 |   const normalizedEndDate = endDate
33 |     ? (parseDateTimeString(endDate, { defaultTime: 'end-of-day' }) ?? undefined)
34 |     : undefined;
35 | 
36 |   try {
37 |     if (hasDates) {
38 |       const start = parseISO(normalizedStartDate!);
39 |       const end = parseISO(normalizedEndDate!);
40 | 
41 |       const days = differenceInCalendarDays(end, start) + 1;
42 |       if (days > 32) {
43 |         return mcpErrorOutput(`ERROR: The maximum allowed date range for the usage API is 32 days.`);
44 |       }
45 |       if (days < 1) {
46 |         return mcpErrorOutput('ERROR: The end date must be after or equal to the start date.');
47 |       }
48 |     }
49 | 
50 |     return await getUsageApiData({ orgId, startDate: normalizedStartDate, endDate: normalizedEndDate, outputDir, jobId });
51 | 
52 |   } catch {
53 |     return mcpErrorOutput('ERROR: Invalid date format. Please use YYYY-MM-DD or a recognizable date string.');
54 |   }
55 | }; 
```

--------------------------------------------------------------------------------
/src/tools/getBuildFailureLogs/handler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | import {
 3 |   getPipelineNumberFromURL,
 4 |   getProjectSlugFromURL,
 5 |   getBranchFromURL,
 6 |   identifyProjectSlug,
 7 |   getJobNumberFromURL,
 8 | } from '../../lib/project-detection/index.js';
 9 | import { getBuildFailureOutputInputSchema } from './inputSchema.js';
10 | import getPipelineJobLogs from '../../lib/pipeline-job-logs/getPipelineJobLogs.js';
11 | import { formatJobLogs } from '../../lib/pipeline-job-logs/getJobLogs.js';
12 | import mcpErrorOutput from '../../lib/mcpErrorOutput.js';
13 | export const getBuildFailureLogs: ToolCallback<{
14 |   params: typeof getBuildFailureOutputInputSchema;
15 | }> = async (args) => {
16 |   const {
17 |     workspaceRoot,
18 |     gitRemoteURL,
19 |     branch,
20 |     projectURL,
21 |     projectSlug: inputProjectSlug,
22 |   } = args.params ?? {};
23 | 
24 |   let projectSlug: string | undefined;
25 |   let pipelineNumber: number | undefined;
26 |   let branchFromURL: string | undefined;
27 |   let jobNumber: number | undefined;
28 | 
29 |   if (inputProjectSlug) {
30 |     if (!branch) {
31 |       return mcpErrorOutput(
32 |         'Branch not provided. When using projectSlug, a branch must also be specified.',
33 |       );
34 |     }
35 |     projectSlug = inputProjectSlug;
36 |   } else if (projectURL) {
37 |     projectSlug = getProjectSlugFromURL(projectURL);
38 |     pipelineNumber = getPipelineNumberFromURL(projectURL);
39 |     branchFromURL = getBranchFromURL(projectURL);
40 |     jobNumber = getJobNumberFromURL(projectURL);
41 |   } else if (workspaceRoot && gitRemoteURL && branch) {
42 |     projectSlug = await identifyProjectSlug({
43 |       gitRemoteURL,
44 |     });
45 |   } else {
46 |     return mcpErrorOutput(
47 |       'Missing required inputs. Please provide either: 1) projectSlug with branch, 2) projectURL, or 3) workspaceRoot with gitRemoteURL and branch.',
48 |     );
49 |   }
50 | 
51 |   if (!projectSlug) {
52 |     return mcpErrorOutput(`
53 |           Project not found. Ask the user to provide the inputs user can provide based on the tool description.
54 | 
55 |           Project slug: ${projectSlug}
56 |           Git remote URL: ${gitRemoteURL}
57 |           Branch: ${branch}
58 |           `);
59 |   }
60 | 
61 |   const logs = await getPipelineJobLogs({
62 |     projectSlug,
63 |     branch: branchFromURL || branch,
64 |     pipelineNumber,
65 |     jobNumber,
66 |   });
67 | 
68 |   return formatJobLogs(logs);
69 | };
70 | 
```

--------------------------------------------------------------------------------
/src/tools/getJobTestResults/handler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | import {
 3 |   getProjectSlugFromURL,
 4 |   identifyProjectSlug,
 5 |   getJobNumberFromURL,
 6 |   getBranchFromURL,
 7 |   getPipelineNumberFromURL,
 8 | } from '../../lib/project-detection/index.js';
 9 | import { getJobTestResultsInputSchema } from './inputSchema.js';
10 | import { getJobTests } from '../../lib/pipeline-job-tests/getJobTests.js';
11 | import { formatJobTests } from '../../lib/pipeline-job-tests/formatJobTests.js';
12 | import mcpErrorOutput from '../../lib/mcpErrorOutput.js';
13 | 
14 | export const getJobTestResults: ToolCallback<{
15 |   params: typeof getJobTestResultsInputSchema;
16 | }> = async (args) => {
17 |   const {
18 |     workspaceRoot,
19 |     gitRemoteURL,
20 |     branch,
21 |     projectURL,
22 |     filterByTestsResult,
23 |     projectSlug: inputProjectSlug,
24 |   } = args.params ?? {};
25 | 
26 |   let pipelineNumber: number | undefined;
27 |   let projectSlug: string | undefined;
28 |   let jobNumber: number | undefined;
29 |   let branchFromURL: string | undefined;
30 | 
31 |   if (inputProjectSlug) {
32 |     if (!branch) {
33 |       return mcpErrorOutput(
34 |         'Branch not provided. When using projectSlug, a branch must also be specified.',
35 |       );
36 |     }
37 |     projectSlug = inputProjectSlug;
38 |   } else if (projectURL) {
39 |     pipelineNumber = getPipelineNumberFromURL(projectURL);
40 |     projectSlug = getProjectSlugFromURL(projectURL);
41 |     branchFromURL = getBranchFromURL(projectURL);
42 |     jobNumber = getJobNumberFromURL(projectURL);
43 |   } else if (workspaceRoot && gitRemoteURL && branch) {
44 |     projectSlug = await identifyProjectSlug({
45 |       gitRemoteURL,
46 |     });
47 |   } else {
48 |     return mcpErrorOutput(
49 |       'Missing required inputs. Please provide either: 1) projectSlug with branch, 2) projectURL, or 3) workspaceRoot with gitRemoteURL and branch.',
50 |     );
51 |   }
52 | 
53 |   if (!projectSlug) {
54 |     return mcpErrorOutput(`
55 |       Project not found. Please provide a valid project URL or project information.
56 | 
57 |       Project slug: ${projectSlug}
58 |       Git remote URL: ${gitRemoteURL}
59 |       Branch: ${branch}
60 |     `);
61 |   }
62 | 
63 |   const testResults = await getJobTests({
64 |     projectSlug,
65 |     pipelineNumber,
66 |     branch: branchFromURL || branch,
67 |     jobNumber,
68 |     filterByTestsResult,
69 |   });
70 | 
71 |   return formatJobTests(testResults);
72 | };
73 | 
```

--------------------------------------------------------------------------------
/src/lib/pipeline-job-logs/getPipelineJobLogs.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getCircleCIClient } from '../../clients/client.js';
 2 | import { Pipeline } from '../../clients/schemas.js';
 3 | import getJobLogs from './getJobLogs.js';
 4 | 
 5 | export type GetPipelineJobLogsParams = {
 6 |   projectSlug: string;
 7 |   branch?: string;
 8 |   pipelineNumber?: number; // if provided, always use this to fetch the pipeline instead of the branch
 9 |   jobNumber?: number; // if provided, always use this to fetch the job instead of the branch and pipeline number
10 | };
11 | 
12 | const getPipelineJobLogs = async ({
13 |   projectSlug,
14 |   branch,
15 |   pipelineNumber,
16 |   jobNumber,
17 | }: GetPipelineJobLogsParams) => {
18 |   const circleci = getCircleCIClient();
19 |   let pipeline: Pipeline | undefined;
20 | 
21 |   // If jobNumber is provided, fetch the job logs directly
22 |   if (jobNumber) {
23 |     return await getJobLogs({
24 |       projectSlug,
25 |       jobNumbers: [jobNumber],
26 |       failedStepsOnly: true,
27 |     });
28 |   }
29 |   // If pipelineNumber is provided, fetch the pipeline logs for failed steps in jobs
30 |   if (pipelineNumber) {
31 |     pipeline = await circleci.pipelines.getPipelineByNumber({
32 |       projectSlug,
33 |       pipelineNumber,
34 |     });
35 |   } else if (branch) {
36 |     // If branch is provided, fetch the pipeline logs for failed steps in jobs for a branch
37 |     const pipelines = await circleci.pipelines.getPipelines({
38 |       projectSlug,
39 |       branch,
40 |     });
41 | 
42 |     pipeline = pipelines[0];
43 |   } else {
44 |     // If no jobNumber, pipelineNumber or branch is provided, throw an error
45 |     throw new Error(
46 |       'Either jobNumber, pipelineNumber or branch must be provided',
47 |     );
48 |   }
49 | 
50 |   if (!pipeline) {
51 |     throw new Error('Pipeline not found');
52 |   }
53 | 
54 |   const workflows = await circleci.workflows.getPipelineWorkflows({
55 |     pipelineId: pipeline.id,
56 |   });
57 | 
58 |   const jobs = (
59 |     await Promise.all(
60 |       workflows.map(async (workflow) => {
61 |         return await circleci.jobs.getWorkflowJobs({
62 |           workflowId: workflow.id,
63 |         });
64 |       }),
65 |     )
66 |   ).flat();
67 | 
68 |   const jobNumbers = jobs
69 |     .filter(
70 |       (job): job is typeof job & { job_number: number } =>
71 |         job.job_number != null,
72 |     )
73 |     .map((job) => job.job_number);
74 | 
75 |   return await getJobLogs({
76 |     projectSlug,
77 |     jobNumbers,
78 |     failedStepsOnly: true,
79 |   });
80 | };
81 | 
82 | export default getPipelineJobLogs;
83 | 
```

--------------------------------------------------------------------------------
/src/clients/circleci/tests.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Test } from '../schemas.js';
 2 | import { HTTPClient } from './httpClient.js';
 3 | import { defaultPaginationOptions } from './index.js';
 4 | import { z } from 'zod';
 5 | 
 6 | const TestResponseSchema = z.object({
 7 |   items: z.array(Test),
 8 |   next_page_token: z.string().nullable().optional(),
 9 | });
10 | 
11 | export class TestsAPI {
12 |   protected client: HTTPClient;
13 | 
14 |   constructor(httpClient: HTTPClient) {
15 |     this.client = httpClient;
16 |   }
17 | 
18 |   /**
19 |    * Get all tests for a job with pagination support
20 |    * @param params Configuration parameters
21 |    * @param params.projectSlug The project slug
22 |    * @param params.jobNumber The job number
23 |    * @param params.options Optional configuration for pagination limits
24 |    * @param params.options.maxPages Maximum number of pages to fetch (default: 5)
25 |    * @param params.options.timeoutMs Timeout in milliseconds (default: 10000)
26 |    * @returns All tests from the job
27 |    * @throws Error if timeout or max pages reached
28 |    */
29 |   async getJobTests({
30 |     projectSlug,
31 |     jobNumber,
32 |     options = {},
33 |   }: {
34 |     projectSlug: string;
35 |     jobNumber: number;
36 |     options?: {
37 |       maxPages?: number;
38 |       timeoutMs?: number;
39 |     };
40 |   }): Promise<Test[]> {
41 |     const {
42 |       maxPages = defaultPaginationOptions.maxPages,
43 |       timeoutMs = defaultPaginationOptions.timeoutMs,
44 |     } = options;
45 | 
46 |     const startTime = Date.now();
47 |     const allTests: Test[] = [];
48 |     let nextPageToken: string | null = null;
49 |     let pageCount = 0;
50 | 
51 |     do {
52 |       // Check timeout
53 |       if (Date.now() - startTime > timeoutMs) {
54 |         throw new Error(`Timeout reached after ${timeoutMs}ms`);
55 |       }
56 | 
57 |       // Check page limit
58 |       if (pageCount >= maxPages) {
59 |         throw new Error(`Maximum number of pages (${maxPages}) reached`);
60 |       }
61 | 
62 |       const params = nextPageToken ? { 'page-token': nextPageToken } : {};
63 |       const rawResult = await this.client.get<unknown>(
64 |         `/project/${projectSlug}/${jobNumber}/tests`,
65 |         params,
66 |       );
67 | 
68 |       // Validate the response against our TestResponse schema
69 |       const result = TestResponseSchema.parse(rawResult);
70 | 
71 |       pageCount++;
72 |       allTests.push(...result.items);
73 |       nextPageToken = result.next_page_token ?? null;
74 |     } while (nextPageToken);
75 | 
76 |     return allTests;
77 |   }
78 | }
79 | 
```

--------------------------------------------------------------------------------
/src/tools/getFlakyTests/tool.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getFlakyTestLogsInputSchema } from './inputSchema.js';
 2 | 
 3 | export const getFlakyTestLogsTool = {
 4 |   name: 'find_flaky_tests' as const,
 5 |   description: `
 6 |     This tool retrieves information about flaky tests in a CircleCI project. 
 7 |     
 8 |     The agent receiving this output MUST analyze the flaky test data and implement appropriate fixes based on the specific issues identified.
 9 | 
10 |     CRITICAL REQUIREMENTS:
11 |     1. Truncation Handling (HIGHEST PRIORITY):
12 |        - ALWAYS check for <MCPTruncationWarning> in the output
13 |        - When present, you MUST start your response with:
14 |          "WARNING: The logs have been truncated. Only showing the most recent entries. Earlier build failures may not be visible."
15 |        - Only proceed with log analysis after acknowledging the truncation
16 | 
17 |     Input options (EXACTLY ONE of these THREE options must be used):
18 | 
19 |     Option 1 - Project Slug:
20 |     - projectSlug: The project slug obtained from listFollowedProjects tool (e.g., "gh/organization/project")
21 | 
22 |     Option 2 - Direct URL (provide ONE of these):
23 |     - projectURL: The URL of the CircleCI project in any of these formats:
24 |       * Project URL: https://app.circleci.com/pipelines/gh/organization/project
25 |       * Pipeline URL: https://app.circleci.com/pipelines/gh/organization/project/123
26 |       * Workflow URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def
27 |       * Job URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def/jobs/xyz
28 | 
29 |     Option 3 - Project Detection (ALL of these must be provided together):
30 |     - workspaceRoot: The absolute path to the workspace root
31 |     - gitRemoteURL: The URL of the git remote repository
32 | 
33 |     Additional Requirements:
34 |     - Never call this tool with incomplete parameters
35 |     - If using Option 1, make sure to extract the projectSlug exactly as provided by listFollowedProjects
36 |     - If using Option 2, the URLs MUST be provided by the user - do not attempt to construct or guess URLs
37 |     - If using Option 3, BOTH parameters (workspaceRoot, gitRemoteURL) must be provided
38 |     - If none of the options can be fully satisfied, ask the user for the missing information before making the tool call
39 |     `,
40 |   inputSchema: getFlakyTestLogsInputSchema,
41 | };
42 | 
```

--------------------------------------------------------------------------------
/src/tools/getLatestPipelineStatus/tool.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getLatestPipelineStatusInputSchema } from './inputSchema.js';
 2 | import { option1DescriptionBranchRequired } from '../shared/constants.js';
 3 | 
 4 | export const getLatestPipelineStatusTool = {
 5 |   name: 'get_latest_pipeline_status' as const,
 6 |   description: `
 7 |     This tool retrieves the status of the latest pipeline for a CircleCI project. It can be used to check pipeline status, get latest build status, or view current pipeline state.
 8 | 
 9 |     Common use cases:
10 |     - Check latest pipeline status
11 |     - Get current build status
12 |     - View pipeline state
13 |     - Check build progress
14 |     - Get pipeline information
15 | 
16 |     Input options (EXACTLY ONE of these THREE options must be used):
17 | 
18 |     ${option1DescriptionBranchRequired}
19 | 
20 |     Option 2 - Direct URL (provide ONE of these):
21 |     - projectURL: The URL of the CircleCI project in any of these formats:
22 |       * Project URL: https://app.circleci.com/pipelines/gh/organization/project
23 |       * Pipeline URL: https://app.circleci.com/pipelines/gh/organization/project/123
24 |       * Workflow URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def
25 |       * Job URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def/jobs/xyz
26 |       * Legacy Job URL: https://circleci.com/gh/organization/project/123
27 | 
28 |     Option 3 - Project Detection (ALL of these must be provided together):
29 |     - workspaceRoot: The absolute path to the workspace root
30 |     - gitRemoteURL: The URL of the git remote repository
31 |     - branch: The name of the current branch
32 |     
33 |     Recommended Workflow:
34 |     1. Use listFollowedProjects tool to get a list of projects
35 |     2. Extract the projectSlug from the chosen project (format: "gh/organization/project")
36 |     3. Use that projectSlug with a branch name for this tool
37 | 
38 |     Additional Requirements:
39 |     - Never call this tool with incomplete parameters
40 |     - If using Option 1, make sure to extract the projectSlug exactly as provided by listFollowedProjects
41 |     - If using Option 2, the URLs MUST be provided by the user - do not attempt to construct or guess URLs
42 |     - If using Option 3, ALL THREE parameters (workspaceRoot, gitRemoteURL, branch) must be provided
43 |     - If none of the options can be fully satisfied, ask the user for the missing information before making the tool call
44 |   `,
45 |   inputSchema: getLatestPipelineStatusInputSchema,
46 | };
47 | 
```

--------------------------------------------------------------------------------
/src/tools/runEvaluationTests/inputSchema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import {
 3 |   branchDescription,
 4 |   fileNameTemplate,
 5 |   projectSlugDescription,
 6 |   promptsOutputDirectory,
 7 | } from '../shared/constants.js';
 8 | 
 9 | export const runEvaluationTestsInputSchema = z.object({
10 |   projectSlug: z.string().describe(projectSlugDescription).optional(),
11 |   branch: z.string().describe(branchDescription).optional(),
12 |   workspaceRoot: z
13 |     .string()
14 |     .describe(
15 |       'The absolute path to the root directory of your project workspace. ' +
16 |         'This should be the top-level folder containing your source code, configuration files, and dependencies. ' +
17 |         'For example: "/home/user/my-project" or "C:\\Users\\user\\my-project"',
18 |     )
19 |     .optional(),
20 |   gitRemoteURL: z
21 |     .string()
22 |     .describe(
23 |       'The URL of the remote git repository. This should be the URL of the repository that you cloned to your local workspace. ' +
24 |         'For example: "https://github.com/user/my-project.git"',
25 |     )
26 |     .optional(),
27 |   projectURL: z
28 |     .string()
29 |     .describe(
30 |       'The URL of the CircleCI project. Can be any of these formats:\n' +
31 |         '- Project URL with branch: https://app.circleci.com/pipelines/gh/organization/project?branch=feature-branch\n' +
32 |         '- Pipeline URL: https://app.circleci.com/pipelines/gh/organization/project/123\n' +
33 |         '- Workflow URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def\n' +
34 |         '- Job URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def/jobs/xyz',
35 |     )
36 |     .optional(),
37 |   pipelineChoiceName: z
38 |     .string()
39 |     .describe(
40 |       'The name of the pipeline to run. This parameter is only needed if the project has multiple pipeline definitions. ' +
41 |         'If not provided and multiple pipelines exist, the tool will return a list of available pipelines for the user to choose from. ' +
42 |         'If provided, it must exactly match one of the pipeline names returned by the tool.',
43 |     )
44 |     .optional(),
45 |   promptFiles: z
46 |     .array(
47 |       z.object({
48 |         fileName: z.string().describe('The name of the prompt template file'),
49 |         fileContent: z
50 |           .string()
51 |           .describe('The contents of the prompt template file'),
52 |       }),
53 |     )
54 |     .describe(
55 |       `Array of prompt template files in the ${promptsOutputDirectory} directory (e.g. ${fileNameTemplate}).`,
56 |     ),
57 | });
58 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@circleci/mcp-server-circleci",
 3 |   "version": "0.14.1",
 4 |   "description": "A Model Context Protocol (MCP) server implementation for CircleCI, enabling natural language interactions with CircleCI functionality through MCP-enabled clients",
 5 |   "type": "module",
 6 |   "access": "public",
 7 |   "license": "Apache-2.0",
 8 |   "homepage": "https://github.com/CircleCI-Public/mcp-server-circleci/",
 9 |   "bugs": "https://github.com/CircleCI-Public/mcp-server-circleci/issues",
10 |   "repository": {
11 |     "type": "git",
12 |     "url": "https://github.com/CircleCI-Public/mcp-server-circleci.git"
13 |   },
14 |   "bin": {
15 |     "mcp-server-circleci": "./dist/index.js"
16 |   },
17 |   "files": [
18 |     "dist",
19 |     "CHANGELOG.md"
20 |   ],
21 |   "packageManager": "[email protected]",
22 |   "scripts": {
23 |     "build": "rm -rf dist && tsc && shx chmod +x dist/*.js",
24 |     "watch": "nodemon --watch . --ext ts,json --exec pnpm run build",
25 |     "inspector": "npx @modelcontextprotocol/[email protected] node ./dist/index.js",
26 |     "build:inspector": "pnpm run build && pnpm run inspector",
27 |     "create-tool": "node ./scripts/create-tool.js",
28 |     "tsx": "tsx",
29 |     "typecheck": "tsc --noEmit",
30 |     "lint": "eslint .",
31 |     "lint:fix": "eslint . --fix",
32 |     "prettier": "prettier --write \"**/*.{ts,js,json}\"",
33 |     "test": "vitest",
34 |     "test:run": "vitest run",
35 |     "prepublishOnly": "pnpm run build && pnpm run test:run",
36 |     "bump:patch": "pnpm version patch --no-git-tag-version",
37 |     "bump:minor": "pnpm version minor --no-git-tag-version",
38 |     "bump:major": "pnpm version major --no-git-tag-version"
39 |   },
40 |   "dependencies": {
41 |     "@modelcontextprotocol/sdk": "^1.15.1",
42 |     "chrono-node": "2.8.3",
43 |     "csv-parse": "6.0.0",
44 |     "date-fns": "4.1.0",
45 |     "express": "^4.19.2",
46 |     "parse-github-url": "^1.0.3",
47 |     "zod": "^3.24.2",
48 |     "zod-to-json-schema": "^3.24.3"
49 |   },
50 |   "devDependencies": {
51 |     "@eslint/js": "^9.21.0",
52 |     "@types/express": "5.0.3",
53 |     "@types/node": "^22",
54 |     "@types/parse-github-url": "^1.0.3",
55 |     "@typescript-eslint/eslint-plugin": "^8.25.0",
56 |     "@typescript-eslint/parser": "^8.25.0",
57 |     "eslint": "^9.21.0",
58 |     "eslint-config-prettier": "^10.0.2",
59 |     "eslint-plugin-prettier": "^5.2.3",
60 |     "nodemon": "^3.1.9",
61 |     "prettier": "^3.5.2",
62 |     "shx": "^0.4.0",
63 |     "tsx": "^4.19.3",
64 |     "typescript": "^5.6.2",
65 |     "typescript-eslint": "^8.28.0",
66 |     "vitest": "^3.1.1"
67 |   },
68 |   "engines": {
69 |     "node": ">=18.0.0"
70 |   }
71 | }
72 | 
```

--------------------------------------------------------------------------------
/src/tools/runPipeline/tool.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { runPipelineInputSchema } from './inputSchema.js';
 2 | import { option1DescriptionBranchRequired } from '../shared/constants.js';
 3 | 
 4 | export const runPipelineTool = {
 5 |   name: 'run_pipeline' as const,
 6 |   description: `
 7 |     This tool triggers a new CircleCI pipeline and returns the URL to monitor its progress.
 8 | 
 9 |     Input options (EXACTLY ONE of these THREE options must be used):
10 | 
11 |     ${option1DescriptionBranchRequired}
12 | 
13 |     Option 2 - Direct URL (provide ONE of these):
14 |     - projectURL: The URL of the CircleCI project in any of these formats:
15 |       * Project URL with branch: https://app.circleci.com/pipelines/gh/organization/project?branch=feature-branch
16 |       * Pipeline URL: https://app.circleci.com/pipelines/gh/organization/project/123
17 |       * Workflow URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def
18 |       * Job URL: https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc-def/jobs/xyz
19 | 
20 |     Option 3 - Project Detection (ALL of these must be provided together):
21 |     - workspaceRoot: The absolute path to the workspace root
22 |     - gitRemoteURL: The URL of the git remote repository
23 |     - branch: The name of the current branch
24 | 
25 |     Configuration:
26 |     - an optional configContent parameter can be provided to override the default pipeline configuration
27 | 
28 |     Pipeline Selection:
29 |     - If the project has multiple pipeline definitions, the tool will return a list of available pipelines
30 |     - You must then make another call with the chosen pipeline name using the pipelineChoiceName parameter
31 |     - The pipelineChoiceName must exactly match one of the pipeline names returned by the tool
32 |     - If the project has only one pipeline definition, pipelineChoiceName is not needed
33 | 
34 |     Additional Requirements:
35 |     - Never call this tool with incomplete parameters
36 |     - If using Option 1, make sure to extract the projectSlug exactly as provided by listFollowedProjects
37 |     - If using Option 2, the URLs MUST be provided by the user - do not attempt to construct or guess URLs
38 |     - If using Option 3, ALL THREE parameters (workspaceRoot, gitRemoteURL, branch) must be provided
39 |     - If none of the options can be fully satisfied, ask the user for the missing information before making the tool call
40 | 
41 |     Returns:
42 |     - A URL to the newly triggered pipeline that can be used to monitor its progress
43 |     `,
44 |   inputSchema: runPipelineInputSchema,
45 | };
46 | 
```

--------------------------------------------------------------------------------
/src/clients/circlet/circlet.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { HTTPClient } from '../circleci/httpClient.js';
  2 | import { PromptObject, RuleReview } from '../schemas.js';
  3 | import { z } from 'zod';
  4 | import { PromptOrigin, FilterBy } from '../../tools/shared/constants.js';
  5 | 
  6 | export const WorkbenchResponseSchema = z
  7 |   .object({
  8 |     workbench: PromptObject,
  9 |   })
 10 |   .strict();
 11 | 
 12 | export type WorkbenchResponse = z.infer<typeof WorkbenchResponseSchema>;
 13 | 
 14 | export const RecommendedTestsResponseSchema = z.object({
 15 |   recommendedTests: z.array(z.string()),
 16 | });
 17 | 
 18 | export type RecommendedTestsResponse = z.infer<
 19 |   typeof RecommendedTestsResponseSchema
 20 | >;
 21 | export class CircletAPI {
 22 |   protected client: HTTPClient;
 23 | 
 24 |   constructor(client: HTTPClient) {
 25 |     this.client = client;
 26 |   }
 27 | 
 28 |   async createPromptTemplate(
 29 |     prompt: string,
 30 |     promptOrigin: PromptOrigin,
 31 |   ): Promise<PromptObject> {
 32 |     const result = await this.client.post<WorkbenchResponse>('/workbench', {
 33 |       prompt,
 34 |       promptOrigin,
 35 |     });
 36 | 
 37 |     const parsedResult = WorkbenchResponseSchema.safeParse(result);
 38 | 
 39 |     if (!parsedResult.success) {
 40 |       throw new Error(
 41 |         `Failed to parse workbench response. Error: ${parsedResult.error.message}`,
 42 |       );
 43 |     }
 44 | 
 45 |     return parsedResult.data.workbench;
 46 |   }
 47 | 
 48 |   async recommendPromptTemplateTests({
 49 |     template,
 50 |     contextSchema,
 51 |   }: {
 52 |     template: string;
 53 |     contextSchema: Record<string, string>;
 54 |   }): Promise<string[]> {
 55 |     const result = await this.client.post<RecommendedTestsResponse>(
 56 |       '/recommended-tests',
 57 |       {
 58 |         template,
 59 |         contextSchema,
 60 |       },
 61 |     );
 62 | 
 63 |     const parsedResult = RecommendedTestsResponseSchema.safeParse(result);
 64 | 
 65 |     if (!parsedResult.success) {
 66 |       throw new Error(
 67 |         `Failed to parse recommended tests response. Error: ${parsedResult.error.message}`,
 68 |       );
 69 |     }
 70 | 
 71 |     return parsedResult.data.recommendedTests;
 72 |   }
 73 | 
 74 |   async ruleReview({
 75 |     diff,
 76 |     rules,
 77 |     speedMode,
 78 |     filterBy,
 79 |   }: {
 80 |     diff: string;
 81 |     rules: string;
 82 |     speedMode: boolean;
 83 |     filterBy: FilterBy;
 84 |   }): Promise<RuleReview> {
 85 |     const rawResult = await this.client.post<unknown>('/rule-review', {
 86 |       changeSet: diff,
 87 |       rules,
 88 |       speedMode,
 89 |       filterBy,
 90 |     });
 91 |     const parsedResult = RuleReview.safeParse(rawResult);
 92 |     if (!parsedResult.success) {
 93 |       throw new Error(
 94 |         `Failed to parse rule review response. Error: ${parsedResult.error.message}`,
 95 |       );
 96 |     }
 97 |     return parsedResult.data;
 98 |   }
 99 | }
100 | 
```

--------------------------------------------------------------------------------
/src/tools/findUnderusedResourceClasses/handler.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
 2 | import { findUnderusedResourceClasses } from './handler.js';
 3 | import { promises as fs } from 'fs';
 4 | 
 5 | vi.mock('fs', () => ({
 6 |   promises: {
 7 |     readFile: vi.fn(),
 8 |   },
 9 | }));
10 | 
11 | describe('findUnderusedResourceClasses handler', () => {
12 |   const CSV_HEADERS = 'project_name,workflow_name,job_name,resource_class,median_cpu_utilization_pct,max_cpu_utilization_pct,median_ram_utilization_pct,max_ram_utilization_pct';
13 |   const CSV_ROW_UNDER = 'proj,flow,build,medium,10,20,15,18';
14 |   const CSV_ROW_OVER = 'proj,flow,test,large,50,60,55,58';
15 |   const CSV = `${CSV_HEADERS}\n${CSV_ROW_UNDER}\n${CSV_ROW_OVER}`;
16 |   const CSV_MISSING = 'job_name,resource_class,avg_cpu_pct,max_cpu_pct,avg_ram_pct,max_ram_pct\nfoo,medium,10,20,15,18';
17 | 
18 |   beforeEach(() => {
19 |     (fs.readFile as any).mockReset();
20 |   });
21 | 
22 |   it('returns an error if file read fails', async () => {
23 |     (fs.readFile as any).mockRejectedValue(new Error('fail'));
24 |     const result = await findUnderusedResourceClasses({ params: { csvFilePath: '/tmp/usage.csv', threshold: 40 } }, undefined as any);
25 |     expect(result.isError).toBeTruthy();
26 |     expect(result.content[0].text).toContain('Could not read CSV file');
27 |   });
28 | 
29 |   it('returns an error if CSV is missing required columns', async () => {
30 |     (fs.readFile as any).mockResolvedValue(CSV_MISSING);
31 |     const result = await findUnderusedResourceClasses({ params: { csvFilePath: '/tmp/usage.csv', threshold: 40 } }, undefined as any);
32 |     expect(result.isError).toBeTruthy();
33 |     expect(result.content[0].text).toContain('Could not read CSV file');
34 |   });
35 | 
36 |   it('returns an error if all jobs are above threshold', async () => {
37 |     const CSV_OVER = `${CSV_HEADERS}\nproj,flow,test,large,50,60,55,58`;
38 |     (fs.readFile as any).mockResolvedValue(CSV_OVER);
39 |     const result = await findUnderusedResourceClasses({ params: { csvFilePath: '/tmp/usage.csv', threshold: 40 } }, undefined as any);
40 |     expect(result.isError).toBeTruthy();
41 |     expect(result.content[0].text).toContain('Could not read CSV file');
42 |   });
43 | 
44 |   it('returns an error even if underused jobs are present', async () => {
45 |     (fs.readFile as any).mockResolvedValue(CSV);
46 |     const result = await findUnderusedResourceClasses({ params: { csvFilePath: '/tmp/usage.csv', threshold: 40 } }, undefined as any);
47 |     expect(result.isError).toBeTruthy();
48 |     expect(result.content[0].text).toContain('Could not read CSV file');
49 |   });
50 | }); 
```
Page 1/4FirstPrevNextLast