Skip to main content
Glama
README.md
# ci-triage-mcp

An [MCP](https://modelcontextprotocol.io) server that exposes
[ci-triage-agent](https://github.com/kere-sifon/Ci-mvp)'s Trivy/Semgrep +
AWS Bedrock security-triage pipeline as on-demand chat tools — ask "what's
the triage status on repo X?" from Claude Desktop, no GitHub Actions run
required.

## Architecture

```
GitHub PR merged
      │
      ▼
GitHub Action (Ci-mvp)
  ├─ Trivy scan
  ├─ Semgrep scan
  ├─ Bedrock/LangGraph triage → posts PR comment
  └─ Upload scan-results to S3 (OIDC-assumed role, scoped s3:PutObject)
      │
      ▼
  s3://{bucket}/{repo_slug}/{sha}/trivy.json
  s3://{bucket}/{repo_slug}/{sha}/semgrep.json
      │
      ▼  (on-demand, read-only AWS identity, scoped s3:GetObject)
ci-triage-mcp (this project)
  ├─ list_scanned_repos    — discover what's available
  ├─ list_findings         — parse raw Trivy/Semgrep JSON
  ├─ triage_findings       — real Bedrock/LangGraph call (imports
  │                          Ci-mvp's run_triage() directly, no subprocess)
  └─ get_scan_summary      — combined human-readable report
      │
      ▼
Claude Desktop (stdio transport)
```

The MCP server never runs scans itself — it's a read-and-interpret layer
downstream of the GitHub Action, which remains the only place scanning
happens.

## Tools

| Tool | Purpose | Notes |
|---|---|---|
| `list_scanned_repos` | List repos with results in S3, each with latest SHA + timestamp | No args. Use this first if you don't already know a repo/sha. |
| `list_findings` | Raw severity counts, finding IDs, affected files | `sha="latest"` supported |
| `triage_findings` | Full Bedrock/LangGraph classification (true positive / needs review / false positive, with rationale) | `sha="latest"` supported |
| `get_scan_summary` | Combined plain-text report (raw counts + triage verdict) | `sha="latest"` supported |

## Setup

### 1. Install dependencies
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

### 2. Configure environment
Copy `.env.example` to `.env` and fill in:

```bash
AWS_REGION=us-east-1
AWS_PROFILE=default              # local dev only; omit if using ambient creds
SCAN_OUTPUT_DIR=./scan-output    # local cache directory

# Optional S3 automation — see "S3 hand-off" below
SCAN_RESULTS_S3_BUCKET=
SCAN_RESULTS_S3_PREFIX=
```

### 3. Register with Claude Desktop
Open Claude Desktop → **Settings → Developer → Edit Config**, and add:

```json
{
  "mcpServers": {
    "ci-triage": {
      "command": "/absolute/path/to/.venv/bin/python",
      "args": ["/absolute/path/to/server.py"],
      "env": {
        "AWS_PROFILE": "default",
        "SCAN_OUTPUT_DIR": "/absolute/path/to/scan-output",
        "SCAN_RESULTS_S3_BUCKET": "your-bucket-name",
        "SCAN_RESULTS_S3_PREFIX": ""
      }
    }
  }
}
```

Fully quit and restart Claude Desktop (config only loads on startup), then
enable the `ci-triage` connector via the **+** button in a conversation.

### 4. Point ci-triage-agent's Action at your bucket
In the caller workflow (e.g. `triage.yml`), add two inputs to the existing
`Ci-mvp` step:

```yaml
- uses: kere-sifon/Ci-mvp@v1.1
  with:
    aws-role-to-assume: ${{ secrets.CI_TRIAGE_AWS_ROLE_ARN }}
    s3-bucket: your-bucket-name
    s3-prefix: ""
```

**The `s3-prefix` value here must exactly match `SCAN_RESULTS_S3_PREFIX`
above** — a mismatch here is the single most likely thing to break the
hand-off (ask me how I know).

## IAM

Two separate identities, least-privilege on each side:

- **The Action's OIDC-assumed role** needs `s3:PutObject` scoped to
  `arn:aws:s3:::{bucket}/*`, in addition to whatever `bedrock:InvokeModel`
  scope it already has.
- **The MCP server's local AWS identity** (a named profile in local dev,
  or its own role if deployed) needs only `s3:GetObject` on the same
  bucket — it never writes to S3.

## Local testing without S3

If `SCAN_RESULTS_S3_BUCKET` is unset, the server falls back to
`SCAN_OUTPUT_DIR` only. Drop Trivy/Semgrep JSON manually at:

```
{SCAN_OUTPUT_DIR}/{repo_slug}/{sha}/trivy.json
{SCAN_OUTPUT_DIR}/{repo_slug}/{sha}/semgrep.json
```

where `repo_slug` replaces `/` with `__` (e.g. `myorg/my-service` →
`myorg__my-service`).

## Known limitations / roadmap

- **stdio transport only.** This runs as a local subprocess Claude Desktop
  spawns directly — no remote/HTTP access, no OAuth. Fine for personal use;
  would need the MCP 2026-07-28 spec's OAuth 2.1 resource-server pattern
  for any multi-user or remote deployment.
- **`ci-triage-agent` isn't a packaged dependency.** Its `pyproject.toml`
  has no `[build-system]` table, so this project imports it via a
  `sys.path.insert()` at a hardcoded local path (`triage/bedrock_client.py`)
  rather than `pip install -e`. Fine for a single-machine setup; would need
  proper packaging to install this cleanly elsewhere.
- **`resolve_sha("latest")` depends on S3 object timestamps**, not git
  history — if uploads ever land out of commit order, "latest" reflects
  upload recency, not commit recency.