legit-prv-mcp
README.md
# legit-prv-mcp
MCP server for Legit Security tenants. Answers questions about security issues, Actions (root-cause remediation groups), inventory, posture and platform usage, across one or several tenants.
Node 20+, TypeScript, stdio transport.
## Setup
```bash
npm install
cp .env.template .env && chmod 600 .env # then fill in BASE_URL + TOKEN
npm run probe # smoke test over a real MCP session
```
Get a token from Legit: **Settings → API → Generate Token**. A **Read** token is enough for every read tool.
### .env
One block per tenant, discovered by scanning for `LEGIT_TENANT_*_BASE_URL`:
```
LEGIT_TENANT_ACME_BASE_URL=https://acme.legitsecurity.co
LEGIT_TENANT_ACME_TOKEN=...
LEGIT_TENANT_ACME_ALIASES=acme,acme corp
LEGIT_TENANT_ACME_ALLOW_WRITES=false
LEGIT_DEFAULT_TENANT=acme
```
See `.env.example` for every option.
## Wiring it into Claude Code
```json
{
"mcpServers": {
"legit": {
"command": "npx",
"args": ["tsx", "/Users/you/projects/legit-prv-mcp/src/index.ts"],
"cwd": "/Users/you/projects/legit-prv-mcp"
}
}
}
```
`cwd` matters: it is how the server finds `.env`. Tokens stay in the file rather than in the MCP config.
## Tools
| Tool | API | What it answers |
| --- | --- | --- |
| `list_tenants` | config | Which tenants are configured, their aliases, whether writes are on |
| `ping_tenant` | both | Is the tenant reachable, what can this token actually do |
| `search_issues` | GraphQL | Issue lists by severity, status, type, CVE, score, repo, product unit, Action, assignment |
| `get_issue` | REST v1.0 | One issue in full: description, remediation, CWEs, evidence, comments, ticketing |
| `count_issues` | GraphQL | Counts, optionally grouped by severity / type / status / assignee / product unit / SLA rule |
| `issues_trend` | GraphQL | Open-issue backlog over time, by severity |
| `search_actions` | GraphQL | Actions ranked by risk reduction: what to fix first |
| `get_action` | GraphQL | One Action: root cause, fix, affected assets, member issues |
| `search_inventory` | GraphQL | Any of 19 entity types (see below) |
| `inventory_summary` | GraphQL | Counts across the estate in one call |
| `get_compliance` | REST | Frameworks, scores, criteria and requirements |
| `platform_admin` | REST | Users, workspaces, repository groups, custom fields, brokers |
`search_inventory` entities: `repository`, `product_unit`, `dependency`, `container`, `ai_model`, `mcp_server`, `api_endpoint`, `cloud_resource`, `collaborator`, `personal_access_token`, `appsec_project`, `web_application`, `external_service`, `data_model`, `ci_workflow`, `package`, `pod`, `integration`, `policy`.
Writes (`update_issue_status`, `assign_issue`, `comment_on_issue`, `tag_issue`, `create_jira_ticket`) are planned and gated per tenant.
## Commands
```bash
npm run typecheck
npm run probe # spawn the server as a client would, call tools
npm run probe -- search_issues '{"severities":["Critical"],"limit":3}'
npm run check-schema # verify every GraphQL field against the snapshot
npm run check-schema -- --live # also execute each query against the tenant
npm run generate-rest-params # regenerate REST param allowlist from legit-docs
npm run generate-graph-schema # regenerate the GraphQL schema snapshot
```
## Why two APIs
REST is published, versioned and documented, so it is preferred wherever it can answer. But it cannot answer everything:
- **Actions do not exist in REST.** Only an `actionId` filter on issues. Actions are `issueGroups` on the GraphQL BFF.
- **REST has no aggregation.** No group-by, no trend.
- **`GET /api/v2.0/issues` omits the issue title**, which makes it unusable for listing.
So: GraphQL for issue search, counts, trends and Actions; REST for single-issue detail and (later) all writes.
## Gotchas
Measured against a live tenant. Each one is enforced or worked around in code.
- **REST silently ignores unknown query params and returns the full unfiltered set.** `?titleSearch=zzzznope` on `/api/v1.0/issues` returned all 4826 issues instead of 0, because `titleSearch` only exists on v2.0. An agent would report that as "the issues matching your search". `RestClient` validates every param against `src/generated/rest-params.ts`, generated from the OpenAPI specs, and refuses to send an unknown one.
- **REST and GraphQL use different issue ids.** REST accepts only the `prettyId` (`988044EDEE`); GraphQL's `id` is a GUID and 404s on REST. Always quote `prettyId`.
- **GraphQL introspection is disabled in production** (`HC0046`). The schema comes from a committed snapshot generated from the frontend's checked-in codegen output, not from `__schema`.
- **Rate limits are 5/sec, 300/min, 5000/hour per token**, advertised in `X-RateLimit-*`. A per-tenant limiter queues requests rather than discovering the ceiling as a 429 mid-answer.
- **Aggregation resolvers have two different shapes.** `issuesCountGroupedBySeverity` and `…ByType` are collection segments needing `items { }`; `…ByStatus`, `…ByAssignee`, `…ByProductUnit`, `…BySlaRule` return plain arrays. Selecting the wrong one is a hard 400.
- **`openIssuesTrend` takes bucket lists, not a window**: `startDates` / `endDates` as `[String!]`, formatted `MM/DD/YYYY HH:mm:ss`. ISO-8601 fails with an opaque "Unexpected Execution Error".
- **Several fields exist in the schema but their resolvers throw or hang.** No static check can catch these, which is why `check-schema --live` exists. Known so far:
| Field | Symptom | Use instead |
| --- | --- | --- |
| `GraphIssue.assignedUserName` | throws | `assignedUser { … }` |
| `GraphRepository.aiModelsCount` | throws | `aiModelsCountV2` |
| `GraphNewDependency.license` | throws after ~20s | `licensesExpressions` |
| `GraphAiModel.source` | ~25s, times out | `sourceV2` |
| `GraphPolicy.isDisabled`, `isDisabledByScannerConfiguration` | throw | no equivalent; omitted |
| `issueGroups.repositories` **with** `order:` | throws (fine alone) | `minimalRepositories` |
- **Selectable does not mean sortable.** `GraphMcpServer.toolsCount` is a field on the type but absent from `GraphMcpServerSortInput`, so sorting on it is a 400. `check-schema` validates sort arguments separately.
- **v1.0 list endpoints paginate in headers**, `X-Pagination` and `Link: rel="next"`, not in the body.
## Security
- Tokens are read from `.env` only, never logged, never returned, and redacted from every error path (`src/lib/redact.ts`). `.env` is gitignored.
- Read-only by default. Write tools require `LEGIT_TENANT_<KEY>_ALLOW_WRITES=true` plus a Read & Write token, and fail closed with an explanation otherwise.
- One tenant per call. No cross-tenant fan-out, and a per-tenant client and cache namespace, so results cannot blend two customers.
- Issue titles, descriptions, comments and dependency names come from customer source control and are attacker-influenceable. Tool output is framed as untrusted data, never instructions.
- Every result reports `totalCount`, `returned` and `truncated`, so a partial page is never mistaken for the whole answer.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues