MCP tools
Reference
Greenlight exposes its functionality to coding agents as a fixed catalog of MCP tools. This page is the canonical reference: every tool, what it takes, what it returns, and a small example.
The conceptual model is in The agent protocol. This page assumes you know what MCP is and how an agent calls a tool.
Catalog
Section titled “Catalog”Every tool is served by the greenlight MCP server and grouped by area. Jump to a group:
| Area | Tools |
|---|---|
| App lifecycle | registerApp · getRepoAccess · getApp · listApps · addCoOwner · removeCoOwner |
| Marketplace | discoverApps |
| Context (Knowledge) | knowledgeList · knowledgeGet · knowledgeSearch · knowledgeAssetList · knowledgeAssetGet · knowledgePropose |
| Policy | getPolicies |
| Integrations | listGrantableIntegrations · requestCredentialAccess · requestIntegrationConnection · getPermissions Coming soon |
| Environment variables | envList · envSet · envRemove |
| Local development | approveCliSession · inspectAppDb · inspectAppBlob · inspectIntegrationApi · inspectIntegrationDb |
| Pipeline & operations | createPullRequest · mergePullRequest · getPipelineRun · getLogs · getMetrics · getMetricsSeries · getAppDiagnostics · curlApp · getAppPreviewUrl |
| Feedback | submitFeedback |
| Skills | getBuilderSkill |
| Connection | health |
Conventions
Section titled “Conventions”- Tool names are flat camelCase (no dots) and served by the
greenlightMCP server. Related tools share a name prefix (e.g.env*,knowledge*). - Authorization is bound to the user the agent session is authenticated as. The agent cannot do anything the user couldn’t.
- Environment — Each app has one environment. Tools that accept
envdefault tosharedwhen omitted. - Infrastructure is declarative. Workloads, resources (Postgres, blob), and integration grants are declared in
greenlight.ymland applied by the merge-time reconciler — there is no MCP tool that provisions them. The catalog covers app lifecycle, env-var values, the pull-request flow, local development, and observability; the declared shape is read back throughgetApp. - Env writes take effect on deploy.
envSet/envRemovestore the value and returnstatus: "complete"; the running app picks up the change on its next deploy. - No credential ever leaves Greenlight. No tool returns an upstream credential, a vault value, or a proxy/AI key. Tools that touch real data either run inside the control plane and return only results (
inspectAppDb,inspectAppBlob,inspectIntegrationApi,inspectIntegrationDb), or deliver values to the laptop through the pairedgreenlightCLI — never through an MCP response. - Coming soon means it’s on our roadmap. The tool is specified but not built yet, so the shape you see is what it will return once it lands — and each one tells you what to reach for in the meantime.
- Errors follow the error envelope.
codeis stable; messages are not. - Pagination is cursor-based; see Errors & pagination.
App lifecycle
Section titled “App lifecycle”registerApp
Section titled “registerApp”Creates a new app as a scaffold: in the cloud it only creates the app’s image repository and makes sure your install’s registry push identity trusts your GitHub organization. It writes the apps row, the single shared environment row, and the SCM repository seeded with .gitignore, the Greenlight pipeline workflow, and a greenlight.yml carrying the app’s app_id, a docs.* block, and commented-out scaffold for workloads, resources, grants, and env. No Dockerfile is seeded — the agent authors it for the app’s stack in the first code PR. Workloads, resources, grants, and all infrastructure come from the first PR that declares them in greenlight.yml and merges. Registration is resumable and idempotent: calling it twice for the same slug resumes rather than creating a second app. Only the person who started a registration, or an Org Owner finishing an abandoned one, can resume it; anyone else gets app.slug_in_use. An Org Owner who finishes one becomes the app’s owner, and can delete it to free the slug.
Input
{ "name": "Expense Tracker", "slug": "expense-tracker", "type": "server", "description": "Quick reimbursement tracker for the finance team."}Output
{ "app_id": "app_k9x2m3p", "slug": "expense-tracker", "repo_full_name": "contoso/gl-app-expense-tracker", "clone_url": "https://github.com/contoso/gl-app-expense-tracker", "token_expires_at": "2026-03-12T15:08:11Z", "registration_state": "complete", "resumed": false}getRepoAccess
Section titled “getRepoAccess”The way back into an existing project. Mints a fresh one-hour SCM token scoped to the app’s repo, so the agent can clone or pull. Called at the start of any session that needs to push code to an already-registered app, and again mid-session if the token is about to expire.
Input
{ "app_id": "app_k9x2m3p" }Output
{ "clone_url": "https://github.com/contoso/gl-app-expense-tracker", "token": "ghs_…", "clone_command": "git clone https://x-access-token:ghs_…@github.com/contoso/gl-app-expense-tracker.git", "authenticated_clone_url": "https://x-access-token:ghs_…@github.com/contoso/gl-app-expense-tracker.git", "expires_at": "2026-03-12T16:08:11Z", "repo_name": "gl-app-expense-tracker", "repo_full_name": "contoso/gl-app-expense-tracker"}The token is a GitHub App installation token: it authenticates as the x-access-token user spliced into the HTTPS URL. clone_command and authenticated_clone_url are ready-to-run forms — the agent clones and pushes with those (or splices token into clone_url itself) rather than the gh CLI, which the agent’s session is usually not authenticated for.
getApp
Section titled “getApp”Returns the current state of an app: the app row, its shared-environment summary, the declared manifest (resources, workloads, grants, env) projected from the last-merge SHA, per-grant approval status, the full env contract (user + managed names), and the latest pipeline run. Returns identifiers and metadata only — never raw manifest YAML, never env-var values.
listApps
Section titled “listApps”Lists the caller’s apps, most recently deployed first (never-deployed apps last) — a citizen developer sees the apps they own or edit; IT admin roles see every app in the organization. Cursor-paginated (limit 1–100, default 20).
addCoOwner / removeCoOwner
Section titled “addCoOwner / removeCoOwner”Add or remove an editor on an app — used when a citizen developer says “let Bob help me with this.” The dashboard calls this role Editor on the app’s Access tab; the tools keep the CoOwner name. Both take app_id, user_email, and an audit reason. The target must have logged into Greenlight at least once; otherwise addCoOwner returns user.not_found with the org login URL in next_steps.
Someone can also be an editor because a group they belong to is bound to the app as one. The app’s Access tab lists those people with the group that carries them, and removeCoOwner refuses them with app_co_owner.group_derived — unbind the group, or take the person out of it. Someone who holds both a direct grant and a binding comes back as removed with still_editor_via_group naming the group they keep it through.
Marketplace
Section titled “Marketplace”discoverApps
Section titled “discoverApps”The agent-facing counterpart to the dashboard Marketplace tab — browse the live apps in the org the caller can open, not just the ones they own or edit. Cursor-paginated (limit 1–100, default 20), with optional slug (exact) and name_query (substring) filters.
Every returned row is one the caller can open, so each carries its deployment_url. A restricted app is absent for everyone it does not admit, rather than returned as a redacted row — there is nothing to request, so a row nobody can act on would only disclose that the app exists. Access is granted by the app’s owner, in the dashboard, naming a person or binding a group.
Input
{ "name_query": "expense" }Output
{ "items": [ { "app_id": "app_k9x2m3p", "slug": "expense-tracker", "name": "Expense Tracker", "owner_email": "avery@contoso.com", "owner_name": "Avery Chen", "deployment_url": "https://expense-tracker.apps.contoso.com", "viewer_has_access": true } ], "next_cursor": null}Resources & workloads
Section titled “Resources & workloads”Resources (Postgres, blob) and workloads (the app’s web server) are declared in greenlight.yml, not provisioned through MCP. The agent edits the resources: and workloads: blocks, opens a PR, and on merge the reconciler provisions the resources, renders the Kubernetes manifests, and rolls out the deploy. There is no resource.add tool; the declared and live state is read back through getApp, and merge-time provisioning outcomes surface through getPipelineRun.
Context (Knowledge)
Section titled “Context (Knowledge)”knowledgeList
Section titled “knowledgeList”Lists entry summaries for a scope.
Input
{ "scope": "org" | "integration" | "app", "integration": "snowflake", "app_id": "app_k9x2m3p", "tags": ["data-modeling"], "cursor": "..."}Output
{ "items": [ { "id": "kn_pf2", "scope": "app", "topic": "architecture", "title": "App architecture", "version": 4, "updated_at": "2026-03-12T14:08:11Z" } ], "next_cursor": null}Catalog and Custom integrations both start with no integration Knowledge; your team authors entries as it learns the source. If IT makes an integration inactive, every Knowledge tool — including the asset tools — hides or rejects its integration-scope entries and their assets for every agent role, while the preserved content remains editable in the dashboard.
knowledgeGet
Section titled “knowledgeGet”Returns one entry’s full body.
Input
{ "id": "kn_pf2" }or
{ "scope": "app", "app_id": "app_k9x2m3p", "topic": "architecture" }Output
{ "id": "kn_pf2", "scope": "app", "topic": "architecture", "title": "App architecture", "body_md": "## Overview\n\nThis app...", "version": 4, "updated_at": "2026-03-12T14:08:11Z", "last_editor_kind": "agent", "assets": []}assets carries the metadata for any image files attached to the entry. body_md keeps gl-asset:<slug> references literal — fetch bytes with knowledgeAssetGet.
knowledgeSearch
Section titled “knowledgeSearch”Full-text search over titles and bodies.
Input
{ "query": "snowflake warehouse", "scope": "integration" }Output
{ "items": [ { "id": "kn_a1", "title": "Snowflake warehouses we use", "scope": "integration", "snippet": "...the `ANALYTICS_PROD` warehouse..." } ], "next_cursor": null}knowledgeAssetList
Section titled “knowledgeAssetList”Lists the image files attached to Knowledge entries — the organization’s real logo, icon, wordmark, and favicon, plus any diagrams. Call this before showing the organization’s mark anywhere in an app. If nothing is attached, omit the company mark rather than drawing one; an app’s own icon may still be designed. Assets are exactly as visible as the entry they belong to, so the same visibility and inactive-integration rules apply.
Input
{ "scope": "org", "role": "logo-primary" }Output
{ "items": [ { "id": "ka_7hq", "entry_id": "kn_ds1", "entry_topic": "design-system", "scope": "org", "slug": "logo-primary", "role": "logo-primary", "theme": "light", "filename": "acme-logo.svg", "content_type": "image/svg+xml", "byte_size": 8214, "width": 512, "height": 128, "checksum_sha256": "9f2c…", "description": "Full-color horizontal lockup. Use on light backgrounds.", "updated_at": "2026-08-02T09:14:00Z" } ], "next_cursor": null}Paged like every other list tool: pass limit (default 50, max 100) and the previous page’s next_cursor as cursor.
When several assets share a role, prefer the app-scope one over the org-scope one, then the theme matching the surface being built, then any.
knowledgeAssetGet
Section titled “knowledgeAssetGet”Returns one asset’s metadata and a short-lived download link. Address it by id, or by scope + topic + slug — adding integration or app_id for those scopes, since a topic is only unique within its parent. Take the address from a knowledgeAssetList result rather than guessing a topic; every failed lookup answers the same way, whether the address is unused or names an asset you cannot see.
Input
{ "scope": "org", "topic": "design-system", "slug": "logo-primary" }Output
{ "id": "ka_7hq", "entry_id": "kn_ds1", "entry_topic": "design-system", "scope": "org", "slug": "logo-primary", "role": "logo-primary", "theme": "light", "filename": "acme-logo.svg", "content_type": "image/svg+xml", "byte_size": 8214, "width": 512, "height": 128, "checksum_sha256": "9f2c…", "description": "Full-color horizontal lockup. Use on light backgrounds.", "updated_at": "2026-08-02T09:14:00Z", "download_url": "https://…", "expires_at": "2026-08-02T09:29:00Z"}Fetch the bytes, check them against checksum_sha256, and commit the file into the app repository (public/logo.svg, or .greenlight/icon.svg for a branded dashboard tile). download_url expires — it is a fetch handle, never something a deployed app should reference. The bundled CLI does all three steps at once:
greenlight knowledge asset get org/design-system/logo-primary --out public/logo.svg--id <uuid> addresses an asset already listed, and --integration <slug> / --app <uuid> supply the parent an integration- or app-scope address needs. Run greenlight help knowledge asset get for the full flag list.
There is no agent write path for assets: knowledgePropose is prose only, and no tool or CLI command creates, replaces, or deletes one. IT uploads them in the dashboard.
knowledgePropose
Section titled “knowledgePropose”The agent write path. Always writes to the proposal queue; never mutates entries directly.
Input
{ "scope": "app", "app_id": "app_k9x2m3p", "topic": "architecture", "title": "App architecture", "body_md": "...", "rationale": "Discovered a clean way to split the cron job from the API.", "base_version": 3}Output
{ "proposal_id": "kp_xyz", "status": "pending"}Policy
Section titled “Policy”getPolicies
Section titled “getPolicies”Returns the checks the pipeline gate enforces for your organization. Call it before generating code so you satisfy the gate up front instead of discovering a violation when the check fails.
Input
{}No parameters. The organization comes from your authenticated session, and the listing is always the full set — there is nothing to filter. The CLI twin is greenlight policies.
Output
{ "policies": [ { "id": "no-secrets-in-code", "name": "No secrets in code", "description": "Fails when the secret scanner finds a credential in the commit. Cannot be disabled or downgraded.", "enforcement": "block", "active": true }, { "id": "approved-base-images", "name": "Approved base images", "description": "Fails when a Dockerfile FROM image is absent from the org's allowlist. Silent until an allowlist is configured.", "enforcement": "block", "active": true, "config": { "images": ["node:26-alpine"] } }, { "id": "no-env-files", "name": "No committed .env files", "description": "Fails when the repository contains a .env file at the evaluated commit.", "enforcement": "block", "active": false, "inactive_reason": "disabled" }, { "id": "manifest.workload_compute_advisory", "name": "Workload compute matches the sizing guidance", "description": "Flags a workload that declares more CPU or memory than the sizing guidance suggests. Advisory only — it never blocks a merge.", "enforcement": "warn", "advisory": true, "active": true } ]}enforcement is block (fails the gate) or warn (reported, does not fail). active says whether the check runs for your organization at all, and when it does not, inactive_reason says why:
disabled— an administrator switched the check off.awaiting_configuration— the check compares against values an administrator has not supplied yet, so it stays silent. An unconfigured base-image allowlist is the common case.
advisory marks a check that only ever reports. No administrator setting can turn one into a gate, so it will never fail your pull request — read it as advice worth acting on, not as a rule to code around.
A check that is not active will not fail your pull request, so do not write code to satisfy it. Where a check takes configuration, config carries it — read approved-base-images.config.images to pick a base image that passes.
config is present only once an administrator has supplied it. An organization that has not tuned the sizing advisory returns it with no config key at all, as above — which means the shipped guidance is in force: 0.25 vCPU and 0.5 GiB. Read config.caps when the key is there, and fall back to those numbers when it is not; never treat the absence as “no guidance”.
These are the shipped checks. There are no organization-authored custom rules, and the reasoning behind a policy lives in Knowledge rather than here — this tool returns data to act on, not prose to read.
Integrations
Section titled “Integrations”Integration grants are declared in greenlight.yml, not requested through MCP. On merge, Greenlight diffs the declared grants: entries against the app’s permissions and queues IT review for any that are not auto-approved.
Each grant names the integration and the credential slug IT registered — a stable handle, not a read/write/access enum. An app declares at most one entry per integration; a second entry naming an integration already granted fails the manifest check when the pull request is opened. Where one system serves two jobs, grant the single credential whose upstream permissions cover both.
grants: - integration: snowflake-prod credential: analytics-read reason: Read the sales mart for the dashboard.Each integration carries an auth_mode and a delivery_mode; see Data brokering for what they mean.
listGrantableIntegrations
Section titled “listGrantableIntegrations”Lists the available integrations your org has registered and the credential slugs each exposes — call it before requesting access either way: as grants: targets for an app manifest, or as requestCredentialAccess targets for your own personal access. For each integration it returns whose identity it calls as (auth_category), how the credential reaches the app (delivery_mode), how to call it (usage_note), and the catalog entry it was registered from (catalog_key, null for a Custom integration). For each credential it returns a paste-ready manifest_grant_example and a request_example (the personal-access call), your own current grant status on it (caller_grant_status). Read-only, cursor-paginated, scoped to your org. Integrations IT has made inactive are omitted and behave as not found when a stale slug is used in a grant request. Never returns vault keys, credential values, or upstream tokens.
Input
{ "integration": "snowflake-prod", "cursor": "...", "limit": 20 }All fields are optional; omit integration to list every registered integration.
Output
{ "items": [ { "integration": "snowflake-prod", "name": "Snowflake (Prod)", "catalog_key": "snowflake", "delivery_mode": "injected", "env_var_name": "SNOWFLAKE_API_KEY", "base_url": "https://acme.snowflakecomputing.com", "auth_category": "service-identity", "credentials": [ { "slug": "analytics-read", "scope": "Read access to the analytics warehouse", "configured": true, "approval_mode": "manual", "manifest_grant_example": { "integration": "snowflake-prod", "credential": "analytics-read", "yaml": "grants:\n - integration: snowflake-prod\n credential: analytics-read\n" }, "request_example": { "integration": "snowflake-prod", "credential_slug": "analytics-read", "call": "requestCredentialAccess({ integration: 'snowflake-prod', credential_slug: 'analytics-read', reason: '<why the user needs this>' })" }, "caller_grant_status": "none" } ], "usage_note": "Two access paths: an app declares the grant in greenlight.yml grants: (activates at merge); a person requests it imperatively with requestCredentialAccess (no app or manifest needed). Injected delivery: once granted, the deployed app reads the credential from process.env.SNOWFLAKE_API_KEY and calls the upstream directly." } ], "next_cursor": null}catalog_keynames the catalog entry the integration was registered from, or isnullfor a Custom integration (registered by hand).usage_noteis how to call the integration. It starts with how a granted credential reaches the app. When the integration acts as the signed-in user (auth_category: "user-delegated", or a connected database whose note says so), it adds the rules for that: forward the inboundX-Greenlight-Actor-Tokenheader unchanged on every call, work with nobody signed in needs a service-identity integration instead, and, where people connect their own account, a person who hasn’t connected gets401 proxy.user_connection_requiredwith a link indetails.connect_url. For an integration registered from the catalog it ends with that system’s own calling instructions — paths, paging, limits, and errors, such as DAX throughexecuteQueriesfor Power BI semantic models or/mepaths for Microsoft Graph.env_var_nameis populated only whendelivery_modeis"injected"— it’s the name the credential is delivered under at deploy. Proxied integrations leave itnull; the app reaches them through the proxy base URL instead.base_urlis the registered upstream host the proxy calls for this integration — use it to recognize when an existing integration already covers an API you were about to ask for. It’snullfor a connected database, which has no HTTP base URL.- A credential marked
configured: falseis listed but not yet servable — a grant naming it cannot be approved until IT finishes registration. approval_modeis"auto"(the grant activates on merge) or"manual"(IT reviews it in the queue before the runtime value is delivered).caller_grant_statusis your own personal grant on this credential —none,pending,granted,denied, orrevoked— so an agent never re-requests blindly.- Every refusal that names an integration your org hasn’t registered lists the valid available slugs back to you under
details.available_integrations— the same set this tool returns, so a mistyped or abbreviated name (e.g.fmpforfinancial-modeling-prep) is correctable from the error alone without disclosing inactive integrations. That covers a Knowledge lookup, arequestCredentialAccesscall, the pipeline’s grant check at merge, and the deploy that applies the manifest. - An empty
itemslist means your org hasn’t connected any systems to Greenlight yet. That is an answer, not an error: your IT administrator registers integrations under Integrations in the dashboard, and an app that stores its own data needs no integration at all.
requestCredentialAccess
Section titled “requestCredentialAccess”Requests personal access to an integration credential for the signed-in user — for local work with no app, repo, or manifest. This is the imperative counterpart of an app’s declarative grants: block: an app’s access is declared in its manifest and activates at merge; a person’s access is requested with this call and reaches the same governed proxy under their own identity. The result is granted immediately when IT has set the credential to auto-approve, otherwise pending in IT’s review queue. If the credential requires groups, only a user who is currently in one of them is auto-approved, and a granted user can use it only while they stay in one. Re-requesting after a denial or revocation re-opens the request as pending — it never overrides IT’s decision. Every request is recorded, including ones IT later denies.
Input
{ "integration": "snowflake-prod", "credential_slug": "analytics-read", "reason": "Explore the sales mart for a local dashboard" }Output
{ "grant_id": "5b8f0c2e-…", "integration": "snowflake-prod", "credential_slug": "analytics-read", "status": "granted", "requested_at": "2026-07-03T09:12:44.180Z", "policy_note": "auto-granted per credential approval mode"}Naming an integration your org hasn’t registered returns integration.not_found with the org’s available slugs in details.available_integrations, so a mistyped slug is correctable without a second discovery call. When that list is empty, nothing is connected yet and the fix is IT registering the integration, not retrying.
Once granted, greenlight run (with no --app flag) delivers the access — see Local development. There is no dashboard form for this: requests come from the agent or CLI; the dashboard is where IT reviews them.
requestIntegrationConnection
Section titled “requestIntegrationConnection”Asks IT to connect a system your org hasn’t connected yet. Use it when the user needs data from something that doesn’t appear in listGrantableIntegrations.
This is a different ask from requestCredentialAccess. That one requests access to a system your org already connected; this one requests the connection itself, which is a bigger decision — so it is always reviewed by a person and never auto-approved. Filing one is not a blocker: the request lands in IT’s queue and you carry on building the parts of the app that don’t need that system.
It covers systems in the Greenlight integration catalog. Naming something the catalog doesn’t carry returns connection_request.not_found with the catalogued keys, and naming something your org has already connected returns connection_request.already_connected with the available set — which is the more useful answer, because it now includes the thing you wanted.
Re-requesting is safe. An open request comes back unchanged and IT isn’t notified twice, and so does one IT has approved but hasn’t finished registering yet — asking again never wipes a decision that was already made. A previously declined request is re-raised as pending with your new reason, IT is notified again, and the reply carries IT’s decline reason in decision_note so you can tell the user what would change the answer. status is therefore only ever pending or approved here.
Input
{ "catalog_key": "notion", "reason": "The onboarding checklist needs the team handbook pages" }Output
{ "connection_request_id": "9d1c4e2a-…", "catalog_key": "notion", "display_name": "Notion", "status": "pending", "reason": "The onboarding checklist needs the team handbook pages", "requested_at": "2026-07-03T09:12:44.180Z", "decided_at": null, "decision_note": null}Write the reason in plain language — what the app needs the system for. Never put a credential, key, or password in it: the text is shown to IT, emailed, and recorded, and a request that looks like it contains one is refused rather than stored.
There is no dashboard form for this: requests come from the agent or CLI; the dashboard is where IT reviews them.
getPermissions Coming soon
Section titled “getPermissions ”Coming soon! This one is on our roadmap and isn’t served yet. Have a read through the input and output below — that’s the shape it will return, so you can plan against it now. In the meantime, getApp gives you an app’s declared grants with their approval status.
Lists integration grants for the app — declared grants from the manifest at the last-merge SHA, joined with approval status. Read-only. For each granted credential it also returns delivery_mode, which tells the agent how the integration runs locally: a granted integration is live (proxied through a minted token, or injected raw in-process), and a user passthrough integration acts as the developer. An injected grant also carries its target env_var_name — including while status is still "pending" (a manual-approval grant), so the agent can see the name it will get without being told a value is already live before IT approves and the app redeploys.
Input
{ "app_id": "app_k9x2m3p" }Output
{ "items": [ { "integration": "snowflake-prod", "credential": "analytics-read", "declared_in_manifest": true, "status": "approved", "delivery_mode": "injected", "env_var_name": "SNOWFLAKE_API_KEY" } ]}Environment variables
Section titled “Environment variables”envList / envSet / envRemove
Section titled “envList / envSet / envRemove”Manage env-var values on the app’s shared environment. Values live in the customer’s cloud-managed secret store at apps/<app-id>/env/shared/<name>; Postgres stores metadata only. The names that reach the pod are governed by greenlight.yml (the env: block, plus managed names derived from resources: and grants:).
set/remove— store the vault value and the metadata row, then return{ plan, executed_ops, status, note }. They do not restart the running app; it picks up the change on its next deploy (thenotefield restates this).sensitive: truemakes the value write-once — it is never returned after creation. Reserved system names (e.g.DATABASE_URL) are rejected.setrequires areasonfor the audit log.list— returns the full env contract: user-declared names (with vault presence) and derived managed names. Plain values are returned only whenreveal: true; sensitive values are never returned.
await envSet({ app_id: 'app_k9x2m3p', name: 'APPROVAL_SECRET', value: '…', env: 'shared', sensitive: true, reason: 'Signing secret for the approvals webhook.',});See App environment variables for reserved names, the plain-vs-sensitive model, and the MISSING_ENV_VALUE deploy gate.
Local development
Section titled “Local development”The agent develops the app on its own machine without ever holding an upstream credential. Two surfaces serve this, and neither puts a secret in an MCP response: the bundled greenlight CLI runs the app with real values delivered straight into the process, and inspectAppDb and inspectIntegrationApi read deployed data and granted integrations inside the control plane and return only results. A granted integration is live locally, and a user passthrough integration acts as the developer. The full loop is in The agent protocol.
approveCliSession
Section titled “approveCliSession”Signs the bundled greenlight CLI in as the caller, over the already-authenticated MCP session — no separate browser login. The CLI ships inside the plugin, so there’s nothing to download. The developer’s agent runs greenlight login, which tries the developer’s own browser first and, when it can’t reach one, prints a short code and returns right away; the agent passes that code here; the server mints the CLI its own Greenlight credential — bound to the caller under their normal RBAC, the same credential the CLI uses for every command — and hands it to the CLI out of band, never in this response. Any active member of your organization can approve a code, including someone who has not built an app yet. If the agent has no working MCP connection, the same code can be approved by a person at /cli/approve in any browser signed in to Greenlight instead, which asks them to confirm the code came from their own agent — nobody at your organization will ever send someone a code to type there. The code is single-use, so whichever route gets there first wins. Returns no secret to the agent.
Input
{ "code": "ABCD-1234" }Output
{ "session_id": "cli_sess_7h2", "app_scope": ["app_k9x2m3p"], "expires_at": "2026-03-12T18:08:11Z"}Once paired, the agent runs code with greenlight run, in one of two explicit modes. App mode — greenlight run --app <app_id> -- <your dev command> — resolves the app’s environment contract and grants (the same ones the deployed pod runs on) and injects the values into the running process; there is no credentials file on disk. User mode — greenlight run -- <cmd> with no --app — runs on the signed-in user’s own personal grants instead (see requestCredentialAccess), for local work with no app at all. Either way, proxied integrations are reached through the same public proxy URL the deployed app uses, authenticated with a short-lived token the CLI mints from its session; the upstream secret stays in Greenlight exactly as it does in production. Granted injected credentials are delivered as their raw value in-process — every delivery is audited under the developer’s name. The app’s own Postgres runs as a local fixture database — use inspectAppDb to read deployed rows.
inspectAppDb
Section titled “inspectAppDb”Runs one read-only SQL statement against the app’s own provisioned Postgres, executed inside the control plane against the vaulted credential. Returns columns and a capped set of rows — never the credential. Always available for the app’s own database, so the agent can learn the real schema and data before writing code. Audited with the calling user.
Input
{ "app_id": "app_k9x2m3p", "statement": "select status, count(*) from expenses group by 1", "params": []}Output
{ "columns": [ { "name": "status", "type": "text" }, { "name": "count", "type": "int8" } ], "rows": [["approved", 142], ["pending", 17]], "row_count": 2, "truncated": false}The CLI twin is greenlight inspect db --app <id> --statement "<sql>", with one --param <value> per bind parameter, in order.
inspectAppBlob
Section titled “inspectAppBlob”Lists or heads objects in the app’s own blob storage, executed inside the control plane, and returns metadata only: each object’s key, size, content type, and last-modified time. list takes an optional prefix and returns up to 200 objects, with "truncated": true when more match. head takes one key. Keys are relative to the app’s storage. The response never includes the container name, a SAS, or a credential. Audited with the calling user.
There is no way to read an object’s contents here. An app’s storage holds files its users uploaded, so inspection shows the agent how storage is laid out without putting any of those files in front of it. To debug one file, run the app locally with greenlight run, or work from a fixture.
Input
{ "app_id": "app_k9x2m3p", "action": "list", "prefix": "receipts/2026/"}Output
{ "items": [ { "key": "receipts/2026/r-1042.pdf", "size": 48213, "content_type": "application/pdf", "last_modified": "2026-09-14T17:02:11.000Z" } ], "truncated": false}A head on a key that holds no object returns storage.object_not_found, and an app with no blob storage returns inspect.resource_not_found.
The CLI twin is greenlight inspect blob --app <id> --action list [--prefix <p>], or --action head --key <key>.
inspectIntegrationApi
Section titled “inspectIntegrationApi”Performs one HTTP request against a granted proxied integration, through the same grant check, credential swap, and audit path as the data proxy, with the developer’s session as the principal. Returns the response — never the upstream credential. Always available for a granted proxied integration. Injected integrations are not reachable here (they have no proxy backend); investigate those through Knowledge, a greenlight run against the granted raw value, or fixtures.
app_id is optional. With it, the call runs on that app’s grant and you must own the app. Without it, the call runs on your own personal grant (see requestCredentialAccess) — useful for exploring a system before any app exists. If IT requires a group for that credential and you are not in it, the call is refused with inspect.grant_missing, the same as the data proxy would refuse it. Omitting app_id never borrows an app’s grant.
Past the grant check Greenlight reports what the upstream said rather than judging it: an upstream 401 comes back as a successful call with "status": 401. A Snowflake statement body must be a JSON object; a missing or invalid body returns validation.body_invalid with guidance to correct the request, before Greenlight reads your user connection or calls Snowflake.
Input
{ "app_id": "app_k9x2m3p", "integration": "snowflake-prod", "method": "POST", "path": "/api/v2/statements", "body": { "statement": "select current_warehouse()" }}Output
{ "status": 200, "headers": { "content-type": "application/json" }, "body": { "data": [["ANALYTICS_PROD"]] }, "encoding": "json", "truncated": false}encoding says how to read body: json is already parsed, utf8 is text, and base64 is a binary payload. A response past the 256 KB cap comes back with "truncated": true and is never parsed as JSON, since a cut JSON document is no longer valid JSON.
Some integrations authenticate with a key in the URL, so an upstream can hand that key back in a Location header, a pagination Link, or an error message quoting the request. Greenlight replaces any such echo with [redacted-by-greenlight] in the headers and in the body, including a base64 body and a key a JSON body spells with escapes — seeing that marker means Greenlight removed its own credential from the response.
Integrations that mint a token per request work the same way as static keys. These are OAuth client credentials (including a GitHub App installation) and Google Workspace. Greenlight mints the token inside the control plane and never returns it. On Google Workspace the call acts as you, the developer making it, so you see what your own Workspace account can see. If the token endpoint refuses, you get inspect.upstream_auth_failed with the endpoint’s status, its OAuth error code, and its own explanation in upstream_error_description when it gives one (for example Salesforce’s request not supported on this domain, which means the token URL is not your org’s My Domain). If Google refuses to impersonate you, the error’s next steps point at the Admin console’s Domain-wide delegation settings. Integrations that act as the signed-in user through a per-user sign-in run as you, the developer making the call, in your own account. If you haven’t connected that account yet, you get inspect.user_connection_required with a connect_url to open in a browser; connect, then run the call again. An upstream that does not answer in time, or stalls or drops partway through its response, returns inspect.upstream_unreachable.
The CLI twin is greenlight inspect api --integration <slug> --path <path>. --method defaults to GET, --query KEY=VALUE repeats, and extra headers and a body are read from --headers-file and --body-file (--body-file - reads the body from stdin), never placed directly in argv.
inspectIntegrationDb
Section titled “inspectIntegrationDb”Runs one SQL statement against a granted connected database — Azure SQL, a Microsoft Fabric Warehouse, or a Fabric SQL analytics endpoint — on the same engine the data proxy’s /query route uses, and returns the columns and up to 200 rows. The connection runs inside Greenlight, so the SQL password, service-principal secret, or Entra token never reaches the agent. Use it to learn a database’s tables and columns before writing the app, then save what you learn with knowledgePropose.
Access works the same way as inspectIntegrationApi: with app_id, the call runs on that app’s grant and you must own or co-own the app; without it, the call runs on your own personal grant, and a group requirement on the credential still applies.
For a connected database that acts as you, successful and failed queries also record the database’s name for you in the audit’s database_principal field when your token establishes it. The token itself is never recorded.
Greenlight rolls the statement’s work back. An INSERT, UPDATE, DELETE, or DDL statement runs to completion but nothing it changes is kept, and a statement that starts, commits, or rolls back its own transaction is refused with inspect.transaction_control. A single stray COMMIT is still rolled back, but a statement with two COMMITs has already committed its work by the time it is refused. On Azure SQL a statement that commits and then opens a new transaction is refused the same way (this needs SQL Server 2016 or later, and no permission beyond the read access IT granted); on Fabric Warehouse Greenlight can compare only how many transactions are open, so such a statement returns success. This keeps exploration from changing data by accident, not on purpose. It is not a substitute for the database role IT granted, which is still what decides what the identity can touch; db_datareader is the recommended role.
Input
{ "app_id": "app_k9x2m3p", "integration": "cast", "statement": "SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @p1", "params": ["dbo"]}Output
{ "columns": [ { "name": "TABLE_NAME", "type": "nvarchar" }, { "name": "COLUMN_NAME", "type": "nvarchar" }, { "name": "DATA_TYPE", "type": "nvarchar" } ], "rows": [["workers", "id", "int"], ["workers", "full_name", "nvarchar"]], "row_count": 2, "truncated": false}A result past 200 rows or 256 KB of row data comes back with "truncated": true, and a statement is cut off after 10 seconds. Errors use the same codes as the /query route: a statement the database rejects returns proxy.query_failed with the database’s own message, and a database Greenlight cannot reach returns proxy.upstream_unreachable without the driver’s message. An integration that is not a connected database returns inspect.not_inspectable; use inspectIntegrationApi for those. So does a connected database IT delivers injected to the app’s own driver; investigate it through its Knowledge or a local greenlight run. A Fabric Warehouse registered to act as the signed-in user runs the statement as you, the developer, on a connection opened with your own token; if you haven’t connected that account yet, you get inspect.user_connection_required with a connect_url. From the command line the same call is greenlight inspect integration-db --integration cast --statement '…' --param dbo.
Pipeline & operations
Section titled “Pipeline & operations”createPullRequest / mergePullRequest
Section titled “createPullRequest / mergePullRequest”Open and merge pull requests on the app’s repo. Direct pushes to main are blocked by branch protection; every change goes through a PR and the Greenlight Review and Policy Check (scanners + policy). The agent confirms that check passed via getPipelineRun, then calls mergePullRequest with the app id, PR number, and the exact head commit SHA it observed pass (expected_head_sha); merging applies the manifest and triggers the deploy. The call fails closed with a structured error — never falling through to the SCM provider’s merge API — if the PR has moved to a new head since, or if that head has not passed review. Branches must be up to date with main to merge: when main has moved on (another PR landed, or Greenlight updated the generated workflow), the call fails with scm.branch_behind, and the agent merges main into its branch, pushes, and merges the new head once it passes.
getPipelineRun
Section titled “getPipelineRun”The single pipeline read tool. It returns one run with its per-check verdicts and is the agent’s wait primitive after pushing a branch or opening a PR — it can long-poll server-side up to max_wait_seconds. Pass detail: "full" to enrich every check with structured remediation detail (failing file, line, rule, and a suggested fix) and, where available, a tail-capped log snapshot — so there’s no separate failure tool; the detail lives on each check. A failed deploy check also carries a blocker: whether the failure is a platform issue the agent should not try to fix in app code (owner: "operator", e.g. the cluster is out of schedulable capacity) or an app-side issue it can act on (owner: "agent"), plus a one-line explanation. Wait by commit_sha when the exact head must be confirmed — waiting by pull_request_number resolves to the newest recorded run, which can lag the current head after a force-push. A passed run confirms the checks for its commit, not that the branch can merge into the current base; mergePullRequest reports a branch that has fallen behind as scm.branch_behind. Every run also carries stage_timings: where the wall-clock between merge and live actually went, split into the runner queue, the image build, and the registry push. An agent waiting on a slow run can see which stage it is waiting on rather than guessing whether the pipeline is slow or stuck. A segment reads null when it cannot be measured — the stage did not run, or the repo’s workflow file predates the version that records them.
getLogs / getMetrics / getMetricsSeries / getAppDiagnostics / curlApp / getAppPreviewUrl
Section titled “getLogs / getMetrics / getMetricsSeries / getAppDiagnostics / curlApp / getAppPreviewUrl”Runtime observability and smoke checks against the deployed shared environment. getLogs returns a bounded, paginated window of the pod’s merged stdout/stderr; getMetrics returns the most recent per-app snapshot — CPU, memory, restarts, HTTP requests per minute, and 4xx/5xx error rate — alongside the CPU and memory the app reserves, so an over-sized compute: block shows up as a reservation the app never spends; getMetricsSeries returns a downsampled CPU or memory history over a window — bucketed averages plus a min/max/avg/p95 summary — so the agent can judge a trend without reading every sample; curlApp makes an authenticated server-side request into the deployed app with the same user and app-access decision as the SSO boundary, so the agent can confirm the app actually responds for a real authorized user after deploy. An administrator may pass as_user to request the app as another member of the organization; the app sees that person’s identity, but an integration that acts as the signed-in user refuses the request, so a test as a colleague can never read their mail or data. If the Service cannot complete a response, it returns app.unreachable; inspect details.hit_app, then call getAppDiagnostics and getLogs.
getAppDiagnostics is the consolidated “why is my deployed app misbehaving?” read. One call returns pod phase and readiness, restarts and the last exit (an out-of-memory kill included), readiness and liveness probe failures, resource pressure, recent pod events, and the env contract: each variable name the app should have, marked ok, missing, or stale, with values never returned. It also checks from Greenlight whether the app’s database, the data proxy, and any injected integration hosts are reachable, whether the last merge’s image reached the cluster and whether any env value changed since the deploy, and returns the deploy check’s blocker as rollout_blocker, so an owner: "operator" problem reads the same here as on the pipeline. Any section Greenlight could not read comes back null rather than failing the call. It is limited to the app’s owner and editors plus IT, and each call is audited.
getAppPreviewUrl covers what curlApp can’t: seeing the app. It mints a one-time URL the agent opens in its own browser tooling (IDE preview pane, headless browser) to render pages, exercise client-side code, and capture screenshots. The embedded token is single-use, expires in five minutes, and is exchanged on first navigation for a short-lived session confined to that one app’s hostname — the session carries the agent user’s real identity, so access control and the audit trail behave exactly as if that person had signed in. SSO enforcement is never bypassed or weakened.
The two are complementary, and they even take different routes: curlApp goes server-side straight to the app inside the cluster, while a preview URL travels the full public path — DNS, TLS, and the SSO-enforcing ingress. When one succeeds and the other fails, that difference tells the agent whether the app or the edge is the problem. Agents default to curlApp for response checks (no credential ever leaves the platform) and mint a preview URL when verification needs a real browser. The CLI twin is greenlight curl --app <id> --path <path>; request headers and bodies are read from --headers-file and --body-file (--body-file - reads the body from stdin), never placed directly in argv.
Feedback
Section titled “Feedback”submitFeedback
Section titled “submitFeedback”Files a concise Greenlight platform-experience report — a platform bug, recurring friction, or a concrete improvement idea — for the Greenlight platform team. It takes a category (bug / friction / suggestion / other), a one-line title, and a Markdown body_md (optionally an app_id and a small allowlisted context with the agent runtime, OS, and plugin version; reports the plugin’s observer hook files also carry origin: "observer" and a fingerprint, so repeats of one problem group together). The report is recorded with its source and calling surface derived server-side, attributed to the signed-in user, and returns a feedback id. Body content is treated as untrusted (capped, rendered inert on the dashboard) and submission is rate-limited per user, so reports should summarize the platform interaction — never secrets, env values, tokens, or verbatim sensitive content. The CLI twin is greenlight feedback (body via stdin or --body-file). Org admins review submissions on the dashboard Feedback tab, alongside reports filed by dashboard users through the shell “Send feedback” dialog — and can hand a report’s title and body to Shift’s platform team, so write the report as though a Greenlight developer will read it verbatim. Who submitted it is never shared.
Skills
Section titled “Skills”getBuilderSkill
Section titled “getBuilderSkill”Returns the full markdown of the universal Greenlight Builder Skill — the core greenlight Skill by default, or a focused Skill (connected-databases, storage, user-passthrough, platform-feedback) by skill name — byte-identical to the Skill file bundled in the plugin. Use it to re-fetch the Skill when the plugin skill isn’t loaded, after a context compaction, or when only MCP/CLI is reachable. An unrecognized skill name returns a skill.not_found error. This is a narrow, authenticated read of the same universal bytes every installation ships — not a per-org generation endpoint. The CLI twin is greenlight skill / greenlight skill show [--name <skill>].
Connection
Section titled “Connection”health
Section titled “health”Confirms the MCP transport is wired up and your session reached the server. It takes no input, touches no app data, and returns { status, request_id } — quote the request_id if you need to ask an administrator what happened to a call. Reach for it when diagnosing a connection problem, not as part of normal work.