This is the threat model for the BuildPulse MCP server. It describes what the server actually is, what we defend, and what we will not do because it would break real users.
To report a vulnerability, email security@buildpulse.io. Please do not open a public GitHub issue for security reports.
A read-only, domain-scoped proxy to the BuildPulse Platform API. Nine
tools (list_my_organizations, list_repositories, find_flaky_tests,
get_test_history, list_recent_submissions, get_submission_test_results,
get_recent_failures, get_repo_flakiness, get_repo_coverage) plus two
resource templates. There is no fetch-URL, shell, filesystem, or write tool.
Two transports share that surface:
| Transport | Binary | Credential | Who it talks to |
|---|---|---|---|
| stdio | cmd/mcp (npx @buildpulse/mcp) |
BUILDPULSE_TOKEN on the user's machine |
PLATFORM_API_URL (default production) |
| Streamable HTTP | cmd/mcp-remote (mcp.buildpulse.io) |
Bearer API token or OAuth mcpSession |
Host-pinned Platform API only |
platform-api is the authorization authority. MCP forwards the session token; platform-api resolves it to an organization (or a membership set) and scopes every analytics query. MCP must not invent a second policy that contradicts that, but it does validate arguments and pin egress so a compromised agent cannot aim HTTP somewhere else.
- The model is untrusted. Tool schemas are advertised on purpose (MCP).
Sequential "recon" (
list_my_organizations→list_repositories→find_flaky_tests) is the supported product workflow, not an anomaly. - A valid token is the blast radius. Prompt injection can make the agent exfiltrate that tenant's data through chat or another client tool. We cannot stop a user-authorized read of the user's own org. We can stop that session from reading another customer's org.
- stdio vs hosted. stdio runs as the user; a stolen laptop token is an
org-admin problem, same as a stolen API token in CI. Hosted MCP adds
OAuth, short-lived
mcpSessions, and a public HTTP endpoint — pin egress there so the task cannot be turned into an open proxy.
- Cross-tenant
organization_id. An mcpSession lists every org the user belongs to. A prompt-injected agent might pass a foreign UUID. platform-api must 401 unless that UUID is inmcpSessions.organizationIds. Single-org API tokens must 401 on a mismatchedorganization_idrather than ignore it. Locked by tests in platform-api (apiTokenOrgMatches,mcpSessionTargetOrg) and by UUID validation at the MCP edge. - Path / host tricks in tool args.
owner,name,repository, and IDs are interpolated into URLs. Reject..,/,@, and non-hex ObjectIds so arguments cannot change host or escape/api. - Oversized windows.
limit/submissionsabove the documented max are errors, not silent clamps — agents get a retryable message; we do not open a bulk-dump path by accident. - Off-host HTTP.
Client.Getonly requests/apion the configured base URL and refuses redirects to another host.mcp-remotewill not start unlessPLATFORM_API_URLishttps://platform.buildpulse.ioorhttps://platform.dev.buildpulse.io(Terraform enforces the same).
Fargate does not expose EC2 IMDS (169.254.169.254) the way EC2 does; the
application pin is still the control that matters if PLATFORM_API_URL were
ever mis-set.
These ideas show up in generic "secure your MCP" write-ups. They assume write/fetch/code-exec tools we do not ship, and they would kneecap triage:
- Human-in-the-loop on read tools. Confirming every
find_flaky_testscall breaks Cursor/Claude. HITL belongs on a futurebuildpulse.writescope only. - Hiding tool lists or blocking multi-step sessions. That is how customers use the product.
- Session-kill on "intent change." Power users chain many reads after a quiet start. Prefer generous rate limits (implemented: 120 tool calls per token per minute, retryable tool result, session stays up) over killing the session.
- Per-token rate limits.
internal/mcpserver/policy.go: 120 tool calls per hashed token per rolling minute. Excess calls return a retryable tool error (IsError), not a transport drop and not a revoked session. - Audit log. Each tool invocation logs
mcp_audit tool="…" org="…" status="ok|error|rate_limited". Org is theorganization_idargument when present (-otherwise). The raw token is never logged (a SHA-256 prefix is used only as the rate-limit key). - OAuth revoke. Hosted
POST /oauth/revoke(RFC 7009). Access tokens are deleted frommcpSessions(platform-api then 401s). Refresh tokens are popped from the OAuth store. Discovery advertisesrevocation_endpoint. Public clients; unknown tokens still return 200.
- Aggressive output redaction. Failure messages and stack traces are the product. Secret-shaped scrubbing is future work and must be proven against real fixtures first.
- A generic egress proxy / DNS-rebinding stack. There is no user-supplied URL fetch. Do not add one.
go test ./...stdio still accepts httptest / custom PLATFORM_API_URL for local work.
Hosted mcp-remote does not.