Skip to content

Security: BuildPulseLLC/buildpulse-mcp

Security

SECURITY.md

Security — BuildPulse MCP

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.

What this MCP is

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.

Trust boundaries

  1. The model is untrusted. Tool schemas are advertised on purpose (MCP). Sequential "recon" (list_my_organizationslist_repositoriesfind_flaky_tests) is the supported product workflow, not an anomaly.
  2. 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.
  3. 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.

Attacks we take seriously

  • 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 in mcpSessions.organizationIds. Single-org API tokens must 401 on a mismatched organization_id rather 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 / submissions above 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.Get only requests /api on the configured base URL and refuses redirects to another host. mcp-remote will not start unless PLATFORM_API_URL is https://platform.buildpulse.io or https://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.

What we will not do (on purpose)

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_tests call breaks Cursor/Claude. HITL belongs on a future buildpulse.write scope 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.

Rate limits, audit log, and revocation

  • 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 the organization_id argument 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 from mcpSessions (platform-api then 401s). Refresh tokens are popped from the OAuth store. Discovery advertises revocation_endpoint. Public clients; unknown tokens still return 200.

Also deliberately out of scope

  • 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.

How to verify

go test ./...

stdio still accepts httptest / custom PLATFORM_API_URL for local work. Hosted mcp-remote does not.

There aren't any published security advisories