@humanagencyp/deploy-mcp
Official# @humanagencyp/deploy-mcp
Deploy MCP server — make already-built software live, under bounded human authority.
Implements the executor side of the [Human Agency Protocol](https://humanagencyprotocol.org)
`deploy@0.7` profile. Backend: **GitHub Actions**.
## What it does
Exposes releasing as gated tools so an agent can put software in front of real
users **only** with a signed receipt behind it. It does not decide whether a
release is allowed — that is the Gatekeeper's job, and then the pipeline's.
**The gated action is a release, not a build.** Building harms nobody: a preview
at a URL nobody visits has no consequence. Serving it does. So this server never
builds — it activates a build that already exists, which means a human can
*open it and look* before approving, and no rebuild can diverge from what was
approved.
| Tool | Kind | |
|---|---|---|
| `list_deployments` | read | builds that can be released, **with their URLs** |
| `get_deployment` | read | one build, with URL, state, and git-derived provenance (`sourceCommit`, `artifactDigest`) |
| `resolve_ref` | read | branch/tag → commit SHA |
| `list_environments` | read | the repo's real environments |
| `release` | consequential | make an existing build live |
## Setup
```
GITHUB_TOKEN=<fine-grained token>
HAP_DEPLOY_ARTIFACT_PATH=website/dist # optional — see "What the receipt binds"
```
Fine-grained, scoped to **one repository**:
| Permission | Why |
|---|---|
| Actions: read & write | dispatch the workflow |
| Environments: read | list environments for the authorization wizard |
| Contents: read | resolve a branch to a commit |
**Not** `Contents: write`. This server releases builds that already exist; it
never authors or builds anything.
## Two properties that are easy to lose
**1. `release` declares `receipt_id`.** That declaration is what makes a Suveren
gateway inject the receipt it just minted. Remove it and the pipeline has
nothing to verify — the chain quietly degrades to an unproven release. The tool
refuses to dispatch when the field is missing, so the failure is loud.
**2. A build URL is required, not a commit or branch.** `release` rejects
anything that is not a URL. The receipt binds that exact build — the same bytes
a human inspected. Call `list_deployments` first.
Deployment URLs come from GitHub's Deployments API, which hosts with a GitHub
integration (Vercel, Netlify, Render) populate with `environment_url`. **So this
server never needs the host's credentials** — GitHub already knows the address,
and that one value identifies the artifact, shows the human what they are
approving, and is what the host's promote command accepts.
## What the receipt binds, and how the release checks it
The receipt binds the **source commit** the build came from — the value the
released page shows in its footer, so a reader can tie the page to an approval
without any account. GitHub's Deployments API reports only the *trigger* sha,
and for a repository that commits its built artifact those differ: the trigger
is the artifact commit, the source is the commit before it. Binding the trigger
sha would certify a commit the page never displays.
`get_deployment` therefore derives provenance **from git history**, by the same
rule a build workflow uses to stamp the page (the last commit that touched
anything outside the artifact directory):
| Field | Meaning |
|---|---|
| `sourceCommit` | what to pass as `release.commit` |
| `artifactDigest` | `sha256:<hex>` over the artifact's file manifest — every file under the artifact directory, sorted by path, one `sha256 path` line each, then sha256 over the lines. Anyone holding the files can recompute it with `sha256sum`. Read from the artifact commit's `Artifact-Digest:` trailer (the build workflow writes it); the release pipeline recomputes it from the files and verifies the build's signed attestation over it. `digestKind: served-bytes` when `HAP_DEPLOY_ARTIFACT_PATH` names the committed directory; `source` when the host builds and only the source commit can be identified |
| `stampedSourceCommit` | the page footer's value, when the page is reachable |
`release` re-derives `sourceCommit` itself and **refuses** if the supplied commit
differs. The footer is a cross-check that can only refuse — a reachable page
naming a different commit blocks the release — never the gate. So a staged
build behind SSO or a password releases normally; nothing here needs to fetch
it. Two things it does not do: it does not prove the artifact commit was
authored by the official build (an attestation does that), and it does not
verify repository, environment or pipeline (those live in the receipt's private
execution context — see the protocol's forward ledger).
## The pipeline must verify
Dispatching is not the control point — anyone with repository write access can
dispatch a workflow. **The workflow itself must check the receipt** before
serving anything:
```yaml
on:
workflow_dispatch:
inputs:
deployment_url: { required: true }
receipt_id: { required: true }
environment: { required: true }
jobs:
verify: # signature, action is a release, freshness,
# and that the receipt BINDS THIS BUILD
...
release:
needs: verify
steps:
- run: vercel promote "${{ inputs.deployment_url }}" --token "$VERCEL_TOKEN"
```
Nothing is built here. `promote` re-points production at bytes that already
exist, so what goes live is exactly what was inspected.
```yaml
on:
workflow_dispatch:
inputs:
sha: { required: true }
receipt_id: { required: true }
environment: { required: true }
jobs:
verify: # signature, scope, commit binding, freshness
...
deploy:
needs: verify
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.sha }} # the APPROVED commit, not the branch head
```
Building the branch head instead of `inputs.sha` means a receipt for one commit
can ship another — and the receipt still verifies, certifying something false.
That is worse than having no receipt at all.
## Limits, stated plainly
- This server dispatches; it does not hold the deploy credentials the pipeline
uses. The boundary is the receipt requirement, not the whole path.
- GitHub returns no run id from a dispatch, so the run is matched by workflow
and creation time. Best-effort: it reports no run rather than guessing wrong.
## Licence
MIT
TDQS
Scored across 5 tools
Each tool has a clearly distinct purpose: resolving a ref, listing deployments, getting a specific deployment, listing environments, and triggering a deploy. There is no overlap or ambiguity between the tools.
Most tools follow a verb_noun pattern (resolve_ref, list_deployments, get_deployment, list_environments). 'deploy' is a single verb, which is a minor deviation but still understandable and consistent in style.
The five tools are well-scoped for a deployment-focused server. Each tool covers a necessary part of the deployment workflow without being overly granular or redundant.
The core deployment lifecycle is covered: resolving a commit, deploying, listing, and getting deployments. Missing features like cancellation or rollback are minor gaps that do not critically hinder the main workflow.