Copilot code review — MCP servers¶
Status: configuration written, not yet applied. The file
.github/copilot-mcp.jsonis the reviewed source of record. GitHub does not read it from the repository; the owner pastes its contents into the repository's Copilot settings once (procedure below), and again after any change to the file lands onmain. Two of the five servers need one owner step each before the paste — a Terraform apply for the Azure identity, a read-only GitHub App for the GitHub server — and the order matters. The owner steps are tracked in issue #369.
Every Copilot review in this repository ends with the hint "Configure MCP servers for context-aware, tailored reviews." This page is that configuration: which Model Context Protocol (MCP) servers the reviewer may call, why those five, how the two that touch live systems are kept read-only without a stored credential, what was deliberately left out, and the exact steps to turn it on and to prove it is working.
What this changes¶
Copilot code review runs in a GitHub-hosted session and, by default, can only
see the pull request and the repository, plus two built-in MCP servers
(GitHub, read-only on this repository, and Playwright). With MCP servers
configured it can also pull reference material and live state into the
review — the current Azure documentation for a Functions setting, the
Terraform Registry page for an azurerm argument, the Cloudflare Workers docs
for the availability probe, the role assignments actually present on a
resource group, the CI run that failed on the pull request — instead of
guessing or staying silent.
The configuration is shared with the Copilot cloud agent (the agent that takes an issue and opens a pull request), and so is the setup-steps workflow below. Anything enabled here is enabled there too. GitHub's reference: Configure MCP servers for your repository, MCP servers and agent skills for code review and Customizing Copilot code review's environment.
Three facts from those pages shape every choice below:
- Copilot calls MCP tools autonomously. Nothing asks the owner before a
tool runs. GitHub's own guidance is to allowlist specific read-only tools
rather than
*. A review comment is also untrusted input to the reviewer, so every principal it can act as must be one that can change nothing. - Copilot code review runs
copilot-setup-steps.ymlbefore it starts, in the same Actions-hosted environment the cloud agent uses. That is how an Azure sign-in reaches the Azure MCP Server without any secret being stored: the job exchanges its GitHub OIDC token for an Azure token, exactly as the deploy and monitor workflows do. - Copilot is more likely to use the servers when told to. A
review-focused skill directory (
.github/skills/code-review/) and custom instructions that name the MCP context both raise the odds. The skill carries a section saying which server to consult for which component.
The servers, and why each one¶
Every server is official (published by the vendor whose product it documents or exposes) and read-only. Three need no credential at all. The two that reach live systems — Azure and GitHub — are covered in their own sections, because how they authenticate is the whole point.
| Server | Publisher | Transport | Why it is here |
|---|---|---|---|
microsoft-learn |
Microsoft — Microsoft Learn MCP Server, source at MicrosoftDocs/mcp | Remote HTTP, https://learn.microsoft.com/api/mcp, no key |
The estate is Azure Static Web Apps, Azure Functions (Flex Consumption, Node 22), Cosmos DB serverless, Key Vault, Entra ID and MSAL, Application Insights. A reviewer that can read the current Learn page for host.json, a Cosmos consistency setting or an MSAL scope catches drift from documented behaviour instead of asserting from training data. Tools: microsoft_docs_search, microsoft_docs_fetch, microsoft_code_sample_search. |
cloudflare-docs |
Cloudflare — Cloudflare's own MCP servers, source at cloudflare/mcp-server-cloudflare | Remote HTTP, https://docs.mcp.cloudflare.com/mcp, no auth |
edge/availability-probe/ is a Worker on a cron trigger (ADR 0024), and infra/ manages Cloudflare DNS and the transform rules. This is the public documentation server only — not the account API server at mcp.cloudflare.com, which is OAuth and would give the reviewer a live account. Tool: search_cloudflare_documentation. |
terraform |
HashiCorp — Terraform MCP server, source at hashicorp/terraform-mcp-server | Local container hashicorp/terraform-mcp-server:1.3.0@sha256:423a6b8e2ee0… (digest-pinned; the full digest is in .github/copilot-mcp.json), started by the review session |
infra/ is a single Terraform root module against live production. Half of what an infra review checks is "does this argument exist on this provider version, and does changing it force replacement" — exactly what get_provider_details answers from the Registry. Restricted to the registry toolset and eight read tools (the tools reference); no TFE_TOKEN is passed, so the HCP Terraform workspace, runs, plans and state stay out of reach. The image is pinned by digest, not by tag — a tag can be retargeted, a digest cannot; bump both together, as with every other pinned dependency here. |
azure |
Microsoft — Azure MCP Server, source at microsoft/mcp, package @azure/mcp |
Local, npx @azure/mcp@3.0.0-beta.41, started by the review session; signs in through the Azure CLI session copilot-setup-steps.yml leaves behind |
Lets the reviewer check a Terraform or Functions change against what is actually deployed: the role assignments on a resource group, a Function App's current configuration, the activity log for the resource a PR touches, a storage account's network rules. See The Azure server for the identity and the fourteen tools. |
github-mcp-server |
GitHub — GitHub MCP Server | Local container ghcr.io/github/github-mcp-server:v1.12.0@sha256:46cdbbd810fa… (digest-pinned; full digest in .github/copilot-mcp.json), --read-only, thirty-one named read tools on its command line and in Copilot's allowlist; authenticates with a one-hour GitHub App installation token copilot-setup-steps.yml mints per session |
Replaces the built-in GitHub server's default toolsets with a wider read-only set on this repository only: Actions (why did the CI job on this PR fail), code scanning and Dependabot alerts, discussions, plus the default repos, issues and pull requests. No personal access token of any kind. See The GitHub server for the App and its ceiling. |
The tool allowlist is applied twice for the Terraform and Azure servers: on
the server's own command line (--tools=... / --tool ..., so the process
never registers anything else) and in Copilot's tools array (so Copilot
never asks for anything else). Either one alone would be enough; both are the
same belt-and-braces the iac-validate.yml guard uses.
The Azure server: read-only, federated, no secret¶
Why this is safe to hand to an autonomous reviewer, in four layers, each of which holds on its own:
- The identity can change nothing.
github_copilot_reviewininfra/oidc.tfis a new user-assigned managed identity holding Reader on the four workload resource groups (rg-web-site-prod-cus,rg-db-site-prod-cus,rg-stor-site-prod-cus,rg-sec-site-prod-cus) and nothing else. Reader is*/read: control-plane reads only. It carries no data action and nolist*/action, so it cannot list storage keys, Cosmos keys, Function App settings or Key Vault secrets, and cannot read a blob, a document or a secret. It is deliberately not the existinggithub_readeridentity, which holds a config write on the Function App for the deploy-time origin window. - There is no stored credential to leak.
copilot-setup-steps.ymlrunsazure/loginunder OIDC in thecopilotenvironment; the identity's federated credential trusts exactly the subjectrepo:HybridCloudWorks/HCW-HybridCloudWorks:environment:copilot(in both the name and immutable-ID forms, like every credential in this estate). No ref-form credential exists on it, so no branch-triggered workflow can assume it by accident. The three values the workflow needs (COPILOT_REVIEW_CLIENT_ID,TENANT_ID,SUBSCRIPTION_ID) are repository variables — identifiers that grant nothing without a token, per Variables and secrets. - The server refuses writes.
--read-onlyon the command line drops every mutating tool before the server registers anything. -
The tool list is fourteen names, chosen by hand from the server's own metadata (
readOnly,secret), pinned with--toolon the command line and again in Copilot's allowlist. Everything that reads content is out — Cosmos item query/search, blob get, Key Vault secret/key/certificate get, app-settings get, and every Log Analytics or Application Insights log query tool, since telemetry is readable under Reader. What is in:Tool Reads subscription_list,group_list,group_resource_listInventory: what exists, in which group role_assignment_listRBAC on a scope — the thing an oidc.tfreview most needs to see livefunctionapp_get,appservice_webapp_get,appservice_webapp_deployment_getFunction App and site configuration, deployment history storage_account_get,storage_blob_container_getAccount and container properties (network rules, public access, versioning) — not blob content cosmos_listCosmos accounts (control plane) — not documents monitor_activitylog_listWho changed what, when, on a resource monitor_metrics_definitions,monitor_metrics_query,monitor_webtests_getMetrics and availability-test definitions — numbers, not log rows AZURE_TOKEN_CREDENTIALS=AzureCliCredentialpins the credential chain to the CLI session, so the server cannot fall through to any other credential that happens to be on the runner.
What Reader can still see, said plainly: resource configuration and tags, role assignments, metrics, and the activity log. Application Insights telemetry is technically readable under Reader too, which is why no log-query tool is enabled — and the telemetry is content-free by policy regardless (correlation identifiers, no payloads).
Known uncertainty. GitHub's documentation says code review reuses
copilot-setup-steps.yml; a community thread from August 2026
(#203859) reports
the setup steps not running for reviews, unresolved. The failure mode is
safe: without the sign-in the Azure server starts with no credential and
every tool call fails closed, and the other four servers are unaffected. The
Verify section says how to tell which case you are in.
The GitHub server: wider read-only, still one repository¶
The built-in GitHub MCP server already gives the reviewer read-only access to this repository with a token GitHub scopes per review. What it does not give is the Actions toolset (workflow runs and job logs — "why is this PR's CI red"), code scanning and Dependabot alerts, or discussions. GitHub's documented way to widen it (Customizing the built-in GitHub MCP server) is a personal access token stored as an Agents secret. This repository does not do that. No personal access token is used, classic or fine-grained: both are bound to a person, both are long-lived, and Variables and secrets already holds that this estate stores no such credential. The one pattern it does sanction for GitHub → GitHub — the manifest publisher's — is a GitHub App whose installation token is minted per run and lives one hour, and that is what the reviewer uses too.
How it works, and what bounds it:
- A dedicated App, read-only by construction. HCW Copilot Review
Reader is a GitHub App owned by the
HybridCloudWorksorganisation and installed on this repository only, with eight repository permissions, all Read: Actions, Code scanning alerts, Contents, Dependabot alerts, Discussions, Issues, Metadata, Pull requests. No account permissions, no webhook. An App's permissions are the ceiling for every token it can ever mint: the private key, if it leaked, could produce nothing but read-only, one-repository, one-hour tokens — the same access the token itself carries. That is the property a personal token cannot offer. - A one-hour token, minted per session.
copilot-setup-steps.ymlrunsscripts/github-app-token.mjs— the same minter the manifest publisher uses, which the script's header explains is preferred over the marketplace action — withGITHUB_APP_PERMISSIONSnarrowing the token again to those eight read permissions, masks it, and writes it asGITHUB_PERSONAL_ACCESS_TOKEN=…into$HOME/.copilot-review/github-mcp.envwith mode 600. (The variable name is the server's; the value is an App token, not a personal one.) - The server runs read-only, in the pinned container, with a named tool
list. The
github-mcp-serverentry replaces the built-in server withghcr.io/github/github-mcp-server:v1.12.0@sha256:…(digest-pinned), run with--env-fileon that file — so the token is never an argument another process could list — plus--read-only, which drops every write tool before the server registers anything, and--toolsnaming thirty-one read tools, the same list Copilot'stoolsallowlist carries, so a tool GitHub adds to a toolset later is not picked up until someone adds it here. They were chosen by running the pinned binary and reading what it registers: the Actions run, job and log tools; code-scanning and Dependabot alert reads; commits, file contents, branches, tags, releases and labels; issue and pull-request reads and searches; discussions; code and commit search. Left out on purpose:get_me(a person's identity — the token has none),list_repository_collaboratorsand the team tools (people, not code),search_repositories(the token sees one repository), and every tool in thesecret_protection,orgs,users,notifications,gistsandcopilottoolsets (the last would let the reviewer start agent sessions). - Two stored values, neither a credential that acts alone.
COPILOT_REVIEW_APP_IDis a repository variable (an identifier, like the manifest App's).COPILOT_REVIEW_APP_PRIVATE_KEYis an Agents secret, the store Copilot's setup job reads. It is not prefixedCOPILOT_MCP_, because the setup job — not an MCP server — needs it; that means it is also present in the agent's environment, which is exactly why the App is read-only and single-repository: the ceiling above is what makes that exposure acceptable.
What this costs. A GitHub App to create once (step 4), a private key to
store, and a token that expires an hour into a session — long enough for any
review, and the cloud agent simply loses GitHub tools after an hour rather
than holding a standing credential. If even that is not worth "Copilot can
read the failing CI job", delete the github-mcp-server entry from the file
and the built-in server continues exactly as today.
Deliberately not configured¶
- HCP Terraform access for the Terraform server (
TFE_TOKEN). Would expose workspacehcw-azure's runs, plan JSON and state metadata to the reviewer. Plan evidence belongs in the pull request body, attached by a human, asinfra/README.mdalready requires. - Any data-plane Azure role, and any Azure log-query tool. Content is not context. A reviewer needs to know a container's network rules, not what is in it.
- Reader on the Management subscription or the
conngroup. The central Log Analytics workspace and the spoke network are out of scope for a code review, and out of reach for this identity. - Cloudflare account API server (
https://mcp.cloudflare.com/mcp). OAuth to the live account; the docs server covers the review use. - Any personal access token, classic or fine-grained. User-bound and long-lived; the GitHub App above replaces the need for one entirely.
- Reuse of the manifest publisher's App. It holds
contents: writeandpull_requests: write; a reviewer must run under a principal whose ceiling is read, so it gets its own App.
Apply (owner)¶
Repository administrator, Azure Owner on the application subscription (for
the apply approval), and gh and az signed in. Do the steps in this
order — the paste is last because two servers depend on what comes before.
The same list, as checkboxes, is
issue #369.
Steps 1 to 3 need the pull request merged first. The workspace plans
from main, the client-id output exists only after that apply, and
workflow_dispatch only knows workflows that are on the default branch — so
before the merge, step 3 answers
HTTP 404: workflow copilot-setup-steps.yml not found on the default branch,
which is GitHub saying "not yet", not a broken workflow. Step 4 (the token)
has no such dependency and can be done at any time.
1. Merge, then approve the Terraform apply¶
Merging the pull request queues a plan on the hcw-azure workspace. Approve
it at https://app.terraform.io/app/hcw/workspaces/hcw-azure/runs.
Success looks like: the known permanent diff (3 to add, 1 to change, 3
to destroy — the RUNTIME_CONFIG_WRITER strip, see
scripts/assert-expected-plan.mjs) plus exactly seven adds: one
azurerm_user_assigned_identity, two azurerm_federated_identity_credential,
four azurerm_role_assignment (Reader on web, db, stor, sec).
Nothing else changes or is destroyed. Any other destroy is a reason to stop
and read the plan, not to approve it.
2. Seed the client-id variable¶
PowerShell, from the repository root on main. The script reads the
copilot_review_client_id output from the workspace's applied state and
upserts it alongside the variables it already manages:
Success looks like: a line naming COPILOT_REVIEW_CLIENT_ID and a GUID.
It is a variable, not a secret — the value is printed on purpose.
3. Prove the sign-in, before Copilot has to¶
PowerShell:
gh workflow run copilot-setup-steps.yml --repo HybridCloudWorks/HCW-HybridCloudWorks; Start-Sleep -Seconds 20; gh run list --repo HybridCloudWorks/HCW-HybridCloudWorks --workflow copilot-setup-steps.yml --limit 1
Success looks like: the run is completed success and its single step,
Azure Login, prints the subscription it signed into. The failure that
matters is AADSTS700213: No matching federated identity record found —
the subject GitHub presented does not match what Entra trusts. Compare the
subject quoted in the error against the workspace's federated_subjects
output (Outputs tab on the workspace page above); the two :environment:copilot
entries must be there. A login failure complaining about an empty client-id
means step 2 did not land.
4. Create the GitHub App and store its two values¶
No personal access token is created at any point in this step.
- Open https://github.com/organizations/HybridCloudWorks/settings/apps/new
and register the App:
- GitHub App name:
HCW Copilot Review Reader. - Homepage URL:
https://docs.hybridcloudworks.com/runbooks/copilot-code-review-mcp/. - Webhook: untick Active. The App receives nothing.
- Repository permissions, each set to Read-only: Actions, Code scanning alerts, Contents, Dependabot alerts, Discussions, Issues, Metadata (already Read-only), Pull requests. Leave every other repository, organisation and account permission at No access.
- Where can this GitHub App be installed? Only on this account.
- Click Create GitHub App.
- GitHub App name:
- On the App's page, note the App ID (a short integer near the top),
then under Private keys click Generate a private key. A
.pemfile downloads; it is the only copy GitHub will ever give you. - Install it: in the App's left menu, Install App →
HybridCloudWorks→ Only select repositories →HCW-HybridCloudWorks→ Install. -
Store the App ID as a repository variable. Nothing to retype: the App's slug is fixed by its name (
hcw-copilot-review-reader), so the installation you just made can be looked up and its App ID written in one line. PowerShell:$appId = (gh api /orgs/HybridCloudWorks/installations | ConvertFrom-Json).installations | Where-Object app_slug -eq 'hcw-copilot-review-reader' | Select-Object -ExpandProperty app_id; gh variable set COPILOT_REVIEW_APP_ID --repo HybridCloudWorks/HCW-HybridCloudWorks --body $appIdSuccess looks like:
gh variable list --repo HybridCloudWorks/HCW-HybridCloudWorksshowsCOPILOT_REVIEW_APP_IDwith the same integer the App page shows. If the first half returns nothing, the App is not yet installed on the organisation (step 3) — the variable would then be set empty, so check before moving on. Ifgh apianswers403, the CLI token lacks the organisation scope:gh auth refresh -h github.com -s admin:org, then run the line again. -
Store the private key as an Agents secret named
COPILOT_REVIEW_APP_PRIVATE_KEY, pasting the whole.pemfile contents including theBEGINandENDlines. In the repository, Settings → Secrets and variables → Agents → Secrets → New repository secret (the Agents tab sits beside the Actions tab at https://github.com/HybridCloudWorks/HCW-HybridCloudWorks/settings/secrets/actions).gh secret setcannot write this store, so this one is a browser step. Then delete the downloaded.pem; the secret is its home now.
Success looks like: the App listed at
https://github.com/organizations/HybridCloudWorks/settings/installations
as installed on one repository; gh variable list --repo HybridCloudWorks/HCW-HybridCloudWorks
shows COPILOT_REVIEW_APP_ID; and the secret appears under Agents with that
exact name. Both names are load-bearing: the workflow reads them verbatim and
reports "not configured" while either is missing.
5. Paste the MCP configuration¶
-
Copy the file's contents to the clipboard. PowerShell, from the repository root:
The same in Git Bash, if that is the window open:
-
Open the repository's Copilot MCP settings. The page is reached from Settings → Code, planning, and automation → Copilot → MCP servers; the deep link that has carried this configuration is https://github.com/HybridCloudWorks/HCW-HybridCloudWorks/settings/copilot/coding_agent (GitHub moved the setting to a shared "MCP servers" page and says existing configurations were migrated; if that URL lands elsewhere, use the menu path).
-
In MCP configuration, replace whatever is in the box with the clipboard contents and click Save MCP configuration. GitHub validates the JSON on save.
Success looks like: the page reloads with the five server names visible in the box and no red validation message. A failure is a validation error naming a key — the fix is in the file, via a pull request, not in the box.
-
Confirm the code-review toggle is on. Still under Settings → Copilot, open Code review and check Allow Copilot to use MCP tools when reviewing pull requests is enabled. It is on by default; this step is a read, not a change.
The Copilot firewall does not apply to MCP servers or to setup steps
(Customize the firewall),
so no allowlist entry is needed for learn.microsoft.com,
docs.mcp.cloudflare.com, Docker Hub, npm or management.azure.com.
Verify on the next pull request¶
- Open or update any pull request; Copilot reviews it as it does today.
-
On the pull request timeline, the Copilot review entry has a View session link. Open it and read the Setting up environment section.
Success looks like: the
copilot-setup-stepsjob listed as run with Azure Login and Mint a read-only App installation token green, thenmicrosoft-learn,cloudflare-docs,terraform,azureandgithub-mcp-serverlisted as started, each with its tool list beneath — fourteen tools underazure, thirty-one undergithub-mcp-server. Ifterraformorgithub-mcp-serveris missing and the rest are present, the runner could not start the container; if the setup job is absent, you are in the known uncertainty case and every Azure and GitHub tool call in the session log will show an authentication failure — nothing is exposed, and the three documentation servers still work. -
When a review comment used a server, GitHub prints an attribution at the bottom of that comment naming the skill or MCP server. Not every comment will carry one; Copilot uses the context "when relevant". A review of a docs-only pull request may show none, and that is correct.
The first reviews to watch are an infra/ change (expect Terraform Registry
and azure attributions — role_assignment_list on an oidc.tf change is
the canonical one) and a functions/ change (expect Microsoft Learn).
Change or roll back¶
- Change a server or tool: edit
.github/copilot-mcp.jsonin a pull request, then repeat step 5 after the merge. The file, not the settings box, is what gets reviewed — a change pasted straight into the box and not into the file will be overwritten by the next apply and confuse the next reader. - Widen or narrow what Azure shows: the tool list in the file and the
role assignments in
infra/oidc.tfare the two dials; a wider tool with no role behind it fails closed, a wider role with no tool to use it is inert. Change both in one pull request, with the reason in the commit. - Turn MCP off for reviews only: the toggle in step 5.4. The cloud agent keeps the servers.
- Revoke the Azure access outright: remove the
github_copilot_reviewresources frominfra/oidc.tfin a pull request (four role assignments, two credentials, one identity — seven destroys, the plan should show nothing else) and deleteCOPILOT_REVIEW_CLIENT_ID. The workflow then fails at login and the server fails closed. - Revoke the GitHub access: uninstall or delete the App at
https://github.com/organizations/HybridCloudWorks/settings/installations
and delete the
COPILOT_REVIEW_APP_PRIVATE_KEYAgents secret. Every token it ever minted dies with the installation. Thegithub-mcp-serverentry then fails closed; remove it from the file to fall back to the built-in server. - Remove everything: clear the MCP configuration box and save. The two built-in servers remain.
Related¶
- ADR 0026 — required checks filter inside the job: Copilot's review is advisory context for the human merge decision; the fourteen required contexts are unchanged by anything on this page.
- Variables and secrets — why the client id is a variable and the token is the one justified secret; Required inputs §4.2–4.4 — the live inventory of both.
.github/skills/code-review/SKILL.md— the review skill Copilot loads, which names which server to consult per component.- Deployment runbook — where plan evidence for
infra/changes is produced, since the reviewer deliberately cannot reach HCP Terraform itself.