For approvers and developers
Working from an MCP client
BuildGate is also a remote MCP server. Connect an assistant or a terminal by URL, and raise a deploy request, read what is waiting on you, decide it, and work the exception queue without opening the console.
Connecting a client
Approvers are not always in a browser, and neither are deployers. The same service the console talks to is a remote MCP server, so your own tooling can do the things you would otherwise open a tab for. There is no key to paste and no client to pre-register: a client needs the URL and nothing else.
-
Copy the server URL.
The console carries it on the MCP card at the bottom of Settings → Integrations, along with the protocol revision this deployment serves and the scopes a client may ask for. It is this deployment's own address with
/mcpon the end. -
Add the server to your client by URL.
The client registers itself and opens a browser. Nothing has been granted at this point — it is asking.
-
Sign in with Google.
The same account you use in the console, and the same sign-in. This is the whole point of the exercise; see why this is a sign-in and not a pasted key.
-
Pick the organization, read the scopes, allow.
The consent screen names what the client is asking to be able to do, in terms of what it can do to production rather than in scope names.
-
Ask it who it is.
The
whoamitool answers with the person, organization, role and scopes the connection acts with. Worth doing first, because a token is bound to exactly one organization and this is how you find out which.
One token, one organization. If you approve in two organizations, authorize the client twice — there is no tool that switches between them, and an id from one organization reads as not found in the other.
This is for your own tooling, not for a server. A connection here exists because you signed in, and it acts as you when you ask it to. A nightly job pulling deploy records, or a pipeline raising requests on its own schedule, wants the HTTP API instead: an administrator creates a token for it, and nobody has to be at a browser.
If the console's MCP card says switched off, this deployment is not serving the endpoint and whoever runs the service has to turn it on. The card names the setting.
What a client may ask for
Nine scopes, and a client asks for the ones it needs. A client that asks for nothing
gets approvals:read.
| Scope | What allowing it means |
|---|---|
approvals:read | See the deploy requests waiting on you, and the change each one describes |
approvals:create | Raise a deploy request as you, naming the app, the issue and who is being asked. Nothing is deployed by this |
approvals:decide | Approve or reject those deploys as you. An approval lets the requester deploy to live |
exceptions:read | See the compliance exceptions raised in this organization, and how each was handled |
exceptions:resolve | Close those exceptions as you, with a note. Nothing is deleted |
advisories:read | See the plugin advisories your organization has recorded, open and withdrawn |
advisories:record | Record a plugin advisory as you. Where a ruleset runs the plugin policy check, it can hold deploys. Owners and administrators only |
alerts:read | See your organization's alert rules: which events tell whom, and when each last sent something. Owners and administrators only |
alerts:manage | Create alert rules as you. Owners and administrators only; changing or removing one stays in the console |
A scope is a ceiling, never a grant. Holding
approvals:decide does not make anybody an approver: the decision still
runs through the same check the console does, which reads your role out of the
database, so a deployer holding that scope approves exactly nothing. The
same is true of the others: advisories:record held by a deployer records
nothing.
They are separate for a reason worth knowing before you grant one. Reading what is waiting and deciding it are the difference between an assistant that can tell you about your queue and one that can clear it. Raising is a third act by a different sort of person — a deployer who may decide nothing can raise a request, and an approver who let an agent work their own queue has not thereby asked it to put new work into everybody else's. And the exception queue is different evidence altogether: a client authorized to read what is pending has not been authorized to read, or close, the record of every time the gate was gone around. Plugin advisories are a pair of their own because recording one can hold other people's deploys, and alert rules another because they decide who is told about what.
The tools
A connection is only offered the tools its scopes cover, so a read-only client sees the five it can use rather than sixteen of which eleven would refuse.
| Tool | Scope | What it does |
|---|---|---|
whoami | approvals:read | Which person, organization and role this connection acts as |
list_pending_approvals | approvals:read | What is pending and yours to decide — including requests routed to a colleague, since any approver may decide them |
list_deploy_requests | approvals:read | Everything the organization has raised, filterable by status. Anyone who can approve sees the history; a deployer sees what they raised or were named on |
get_deploy_request | approvals:read | One request: the change, the rollback plan, the issue |
watch_deploy_requests | approvals:read | What has happened since a cursor — how to wait for a decision |
list_apps | approvals:create | The apps a request can be raised against, with the Bubble app id and the branches each deploy moves between |
list_approvers | approvals:create | Who could approve a request you raise — never you |
create_deploy_request | approvals:create | Raise one, as you: the app, the Linear issue, the change, the rollback plan, and the colleague being asked |
approve_deploy_request | approvals:decide | Approve it, as you |
reject_deploy_request | approvals:decide | Reject it, with a note the requester reads |
list_exceptions | exceptions:read | The exception queue, filterable by status, kind of finding and app. Only an approver may read it |
resolve_exception | exceptions:resolve | Close one open finding, with the note that becomes the evidence |
list_plugin_advisories | advisories:read | The organization's plugin advisories, open by default, filterable by plugin. Any member may read them |
list_alert_rules | alerts:read | The organization's alert rules, with apps by Bubble id and people by email |
create_alert_rule | alerts:manage | Create one, as you: the events, the apps or every app, and the roles, people and Slack channels to tell |
record_plugin_advisory | advisories:record | Record one, as you: the plugin, severity, what is wrong, where it was found and the version that fixes it. Withdrawing stays in the console |
Raising a request from an agent
A request raised this way is the same record as one raised from the Bubble editor.
Your membership and the named approver's role are checked, you still cannot name
yourself, the approver is notified as usual, and the issue is looked up in your
own Linear or ClickUp by its key rather than taken as written. Pass
issueProvider to say which; without it, every connected tracker is asked.
A deploy that ships more than one issue names the rest in
additionalIssueKeys, each looked up the same way — up to ten issues
in all. To ask more than one person at once, name the rest in
additionalApproverEmails: each is notified and any of them may approve,
and the app's rulesets still decide how many approvals complete it.
It deploys nothing. There is no tool that releases a deploy token or records a completion: the deploy itself still runs from the Bubble editor with the extension, once somebody else has approved. Raising one is a second way to ask, not a second way to approve.
Two things are worth care, because an agent will happily write both badly.
- The change description and the rollback plan are the evidence. They are read by the person deciding and they are what an auditor sees later. "Various fixes" is not evidence — say what changed, what was checked, and how it gets undone.
- There is no diff behind it yet. A request raised this way carries no branch comparison until the extension exports the two branches, so the approver may be reading your description and nothing else.
The tool answers with approvalUrl — the page the approver signs in to
and decides on. Hand it to them, and to whoever asked you to raise it.
Waiting for a decision
Nothing here pushes. This service holds no connection open and no subscription, so an assistant follows a request by asking what has changed since the last time it asked.
That is what watch_deploy_requests is for. Call it once with no cursor
to learn where the record is now; call it again with the cursor it gave you, and it
answers with everything that has happened since — approved, rejected, deployed,
expired, withdrawn, commented on, reviewed — and a new cursor. Pass a request id to
follow one rather than everything you can see.
Nothing between two cursors is ever skipped: the cursor is a position in the audit chain, and the chain has no gaps. So this is how to wait for a decision, rather than re-reading the request in a loop — and there is no faster answer to be had, because a decision cannot arrive between two polls in a way that leaves no event behind.
Why this is a sign-in and not a pasted key
This product refuses to put an approve button in Slack, and the argument is about identity: pressing a Slack button proves control of a Slack account, and a pasted API key proves possession of a string. Neither is what an approval control asks for. A connection here exists only because you completed the same Google sign-in the console uses.
Your membership is checked on every call — one query, no cache — so removing somebody's approver rights stops their agent on the next request rather than at the next expiry, and a token outliving a membership is not something anybody has to remember to clean up. Access tokens are short-lived and refresh anyway, but that is bookkeeping; the authority behind a call is read fresh each time.
The record keeps the distinction. A decision made this way is marked as having come through an agent, with the client's name beside it, and so is a request raised this way — because a person acting through an agent and a person at a keyboard are not the same act, and an auditor reading the chain should be able to tell.
When a tool refuses
Refusals come back as tool errors carrying the service's own vocabulary, so a client is told what the console would be told rather than that something went wrong.
-
You are missing a scope. The call is refused with
insufficient_scopenaming the one it needed, which is what lets a client go back and ask you for it. Authorize it again. -
self_approval_forbidden. You raised it, so you cannot decide it. That rule has no exceptions and no scope turns it off. -
already_decided, or an expired request. Somebody got there first, or the window closed. Approval opens the gate for a single deploy and fifteen minutes; see when the gate will not open. -
Not found, for something you know exists. A token is bound to one
organization, and an id belonging to another one reads exactly as an id belonging
to nobody.
whoamisays which organization you are in. - Your client cannot connect at all. This endpoint serves the current protocol revision and refuses the retired handshake, naming both the revision asked for and the one served. That is a client to update, not a setting to change here.
Resolving an exception never deletes it, an already-resolved finding cannot be resolved again, and nothing on this surface can deploy. What a client can do wrong is raise a thin request or clear a queue you meant to read — which is why the scopes are separate and why the consent screen spells each one out.