Skip to content

Copilot code review — MCP servers

Status: configuration written, not yet applied. The file .github/copilot-mcp.json is 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 on main. 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.yml before 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:

  1. The identity can change nothing. github_copilot_review in infra/oidc.tf is 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 no list*/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 existing github_reader identity, which holds a config write on the Function App for the deploy-time origin window.
  2. There is no stored credential to leak. copilot-setup-steps.yml runs azure/login under OIDC in the copilot environment; the identity's federated credential trusts exactly the subject repo: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.
  3. The server refuses writes. --read-only on the command line drops every mutating tool before the server registers anything.
  4. The tool list is fourteen names, chosen by hand from the server's own metadata (readOnly, secret), pinned with --tool on 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_list Inventory: what exists, in which group
    role_assignment_list RBAC on a scope — the thing an oidc.tf review most needs to see live
    functionapp_get, appservice_webapp_get, appservice_webapp_deployment_get Function App and site configuration, deployment history
    storage_account_get, storage_blob_container_get Account and container properties (network rules, public access, versioning) — not blob content
    cosmos_list Cosmos accounts (control plane) — not documents
    monitor_activitylog_list Who changed what, when, on a resource
    monitor_metrics_definitions, monitor_metrics_query, monitor_webtests_get Metrics and availability-test definitions — numbers, not log rows

    AZURE_TOKEN_CREDENTIALS=AzureCliCredential pins 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 HybridCloudWorks organisation 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.yml runs scripts/github-app-token.mjs — the same minter the manifest publisher uses, which the script's header explains is preferred over the marketplace action — with GITHUB_APP_PERMISSIONS narrowing the token again to those eight read permissions, masks it, and writes it as GITHUB_PERSONAL_ACCESS_TOKEN=… into $HOME/.copilot-review/github-mcp.env with 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-server entry replaces the built-in server with ghcr.io/github/github-mcp-server:v1.12.0@sha256:… (digest-pinned), run with --env-file on 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 --tools naming thirty-one read tools, the same list Copilot's tools allowlist 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_collaborators and the team tools (people, not code), search_repositories (the token sees one repository), and every tool in the secret_protection, orgs, users, notifications, gists and copilot toolsets (the last would let the reviewer start agent sessions).
  • Two stored values, neither a credential that acts alone. COPILOT_REVIEW_APP_ID is a repository variable (an identifier, like the manifest App's). COPILOT_REVIEW_APP_PRIVATE_KEY is an Agents secret, the store Copilot's setup job reads. It is not prefixed COPILOT_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 workspace hcw-azure's runs, plan JSON and state metadata to the reviewer. Plan evidence belongs in the pull request body, attached by a human, as infra/README.md already 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 conn group. 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: write and pull_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:

git checkout main; git pull origin main; ./scripts/set-github-variables.ps1

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.

  1. 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.
  2. 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 .pem file downloads; it is the only copy GitHub will ever give you.
  3. Install it: in the App's left menu, Install AppHybridCloudWorksOnly select repositoriesHCW-HybridCloudWorksInstall.
  4. 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 $appId
    

    Success looks like: gh variable list --repo HybridCloudWorks/HCW-HybridCloudWorks shows COPILOT_REVIEW_APP_ID with 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. If gh api answers 403, the CLI token lacks the organisation scope: gh auth refresh -h github.com -s admin:org, then run the line again.

  5. Store the private key as an Agents secret named COPILOT_REVIEW_APP_PRIVATE_KEY, pasting the whole .pem file contents including the BEGIN and END lines. 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 set cannot 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

  1. Copy the file's contents to the clipboard. PowerShell, from the repository root:

    git checkout main; git pull origin main; Get-Content -Raw .github/copilot-mcp.json | Set-Clipboard
    

    The same in Git Bash, if that is the window open:

    git checkout main && git pull origin main && cat .github/copilot-mcp.json | clip
    
  2. 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).

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

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

  1. Open or update any pull request; Copilot reviews it as it does today.
  2. 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-steps job listed as run with Azure Login and Mint a read-only App installation token green, then microsoft-learn, cloudflare-docs, terraform, azure and github-mcp-server listed as started, each with its tool list beneath — fourteen tools under azure, thirty-one under github-mcp-server. If terraform or github-mcp-server is 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.

  3. 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.json in 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.tf are 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_review resources from infra/oidc.tf in a pull request (four role assignments, two credentials, one identity — seven destroys, the plan should show nothing else) and delete COPILOT_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_KEY Agents secret. Every token it ever minted dies with the installation. The github-mcp-server entry 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.
  • 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.