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 | [](https://github.com/CircleCI-Public/mcp-server-circleci/blob/main/LICENSE) 4 | [](https://dl.circleci.com/status-badge/redirect/gh/CircleCI-Public/mcp-server-circleci/tree/main) 5 | [](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 | }); ```