Provision Azure resources from Terraform with Azure DevOps MCP
Send this guide to your coding agent.
Prerequisite: Install Port MCP
Open plan mode. Implement this Port guide in my org via MCP: https://docs.port.io/guides/all/provision-azure-resources-with-ado-mcp/ Goal: get the guide's core flow working end-to-end in my org; adapting it to fit my existing setup takes priority over matching the guide 1:1. Plan: 1. Confirm MCP is connected, in the right org, with sufficient permissions. 2. Diff the guide's data model (blueprints, properties, relations, actions, agents, automations, integrations, secrets) against mine. 3. Propose adaptations for gaps, reusing existing blueprints/relations over guide-named duplicates. 4. Flag what needs a UI click, credential, or secret from me, testing MCP capability empirically before ruling anything out. 5. Stop on any blocker and give me options. Approving this plan authorizes the writes it lists; pause only for writes beyond what's listed. Build: - Extend blueprint schema additively when upserting; don't remove or overwrite existing properties, and treat type conflicts as a blocker, not an auto-fix. - List any mock data in the plan, minimal and labeled mock; once approved, seed it without re-asking, and tell me what you seeded. - For anything the guide writes downstream (e.g. a webhook target), use a real entity, not a mock. - For pages/widgets, use the real page identifier from the app URL, not a guessed slug. - When you hit a UI step confirmed (not assumed) unsupported via MCP, pause, give exact clicks, then resume via MCP. - Validate and give links after each meaningful step (only a tool-returned URL, no guessed paths); don't proceed if the last run wasn't a success. Done: - Confirm the guide's expected output exists and runs in Port. - Summarize adaptations, seeded data, what was mocked or skipped, remaining UI steps, and how to verify.
This guide shows how to build a Port workflow that reads real Terraform source from Azure DevOps (not just repository metadata), drafts an implementation plan that reuses your module conventions, and opens an Azure DevOps pull request with a coding agent.
The flow is Azure DevOps only:
- A local Azure DevOps MCP server is bridged to HTTP and exposed to Port (for example with Docker + ngrok).
- An MCP connector reads Terraform module files from your GitOps repository.
- A Claude Code Azure Pipelines job applies that plan in the repo, commits a branch, and opens the pull request.
It is the Azure DevOps counterpart to Provision cloud resources with an agentic IaC workflow, which uses GitHub MCP and a Cursor Agent node.
Common use cases
- Prove code-level access: Show that Port can read Terraform module files from Azure DevOps, not only repo names and branches.
- Reuse golden modules: Draft new App Service (or other) instantiations that follow patterns already encoded in your modules.
- Governed GitOps: Open a pull request against your Azure DevOps Terraform repository from a self-service form.
Prerequisites
- A Port account with Port AI enabled and permission to manage MCP connectors and workflows.
- An Azure DevOps organization and project where you can create a Git repository and a pipeline.
- The Azure DevOps Ocean integration installed in Single Account mode, with actions processing enabled (required to trigger pipelines from workflows).
- Docker (to run a local Azure DevOps MCP HTTP bridge) and ngrok (to expose that bridge to Port).
- An Azure DevOps personal access token (PAT) with Code (Read) at minimum for the MCP bridge.
- An Anthropic API key for Claude Code.
Microsoft's hosted Azure DevOps remote MCP (https://mcp.dev.azure.com/{organization}) authenticates with Microsoft Entra ID (OAuth) and does not accept shared header auth for workflow AI nodes. See MCP connectors. This guide uses a local MCP server behind an HTTP bridge (and optionally ngrok) instead.
Prepare the sample Terraform repository
Create a repository named gitops-repo in your Azure DevOps project, then add these files on main. The workflow later reads this module source. Claude Code opens a PR that adds an environment instantiation under environments/.
modules/azure-app-service/main.tf (Click to expand)
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
resource "azurerm_service_plan" "this" {
name = "${var.name}-plan"
resource_group_name = var.resource_group_name
location = var.location
os_type = "Linux"
sku_name = var.sku_name
tags = var.tags
}
resource "azurerm_linux_web_app" "this" {
name = var.name
resource_group_name = var.resource_group_name
location = var.location
service_plan_id = azurerm_service_plan.this.id
site_config {
always_on = var.always_on
}
tags = var.tags
}
modules/azure-app-service/variables.tf (Click to expand)
variable "name" {
type = string
description = "Name of the App Service."
}
variable "resource_group_name" {
type = string
description = "Resource group for the App Service."
}
variable "location" {
type = string
description = "Azure region."
default = "eastus"
}
variable "sku_name" {
type = string
description = "App Service plan SKU."
default = "B1"
}
variable "always_on" {
type = bool
description = "Whether the App Service should stay always on."
default = true
}
variable "tags" {
type = map(string)
description = "Tags applied to all resources."
default = {}
}
modules/azure-app-service/outputs.tf (Click to expand)
output "app_service_id" {
value = azurerm_linux_web_app.this.id
description = "ID of the Linux web app."
}
output "default_hostname" {
value = azurerm_linux_web_app.this.default_hostname
description = "Default hostname of the App Service."
}
modules/azure-app-service/README.md (Click to expand)
# Azure App Service module
## Conventions
- Always set `tags.owner`, `tags.environment`, and `tags.cost_center`.
- Use `sku_name = "B1"` for Development and Staging.
- Use `sku_name = "P1v3"` for Production.
- Set `always_on = true` except for Development.
## Example
module "payments_api" {
source = "../../modules/azure-app-service"
name = "payments-api-staging"
resource_group_name = "rg-payments-staging"
location = "eastus"
sku_name = "B1"
always_on = true
tags = {
owner = "platform"
environment = "staging"
cost_center = "engineering"
}
}
Also create an empty environments/.gitkeep so the folder exists for new instantiations.
Note the repository ID from Azure DevOps (Repository settings or the repo URL). You will paste it into the workflow JSON.
Set up the Claude Code pipeline
This pipeline is the write side of the demo. Port triggers it with command and pr_description parameters. Claude Code edits the checked-out repo (and can enrich .port/pr-description.md), then the job pushes a feature branch and opens a pull request whose body includes Port request context.
Create the pipeline YAML
Add azure-pipelines/claude-terraform.yml (or another path you prefer) to gitops-repo:
claude-terraform.yml (Click to expand)
parameters:
- name: command
type: string
default: ""
- name: pr_description
type: string
default: ""
trigger: none
pool:
vmImage: "ubuntu-latest"
steps:
- checkout: self
persistCredentials: true
- script: npm install -g @anthropic-ai/claude-code
displayName: Install Claude Code
- script: |
set -euo pipefail
mkdir -p .port
printf '%s\n' "$PR_DESCRIPTION" > .port/pr-description.md
displayName: Seed Port PR description
env:
PR_DESCRIPTION: ${{ parameters.pr_description }}
- script: |
set -euo pipefail
git config user.email "claude-agent@yourorg.com"
git config user.name "Claude Agent"
BRANCH="feature/generated-terraform-$(Build.BuildId)"
git checkout -b "$BRANCH"
claude -p "${{ parameters.command }}" \
--allowedTools "Bash,Edit,Write,Glob,Grep" \
--permission-mode acceptEdits
if [ ! -s .port/pr-description.md ]; then
printf '%s\n' "$PR_DESCRIPTION" > .port/pr-description.md
fi
git add -A
git commit -m "Generated Terraform via Claude Code" || echo "Nothing to commit"
git push "https://$AZURE_DEVOPS_PAT@dev.azure.com/<YOUR_ADO_ORG>/<YOUR_ADO_PROJECT>/_git/gitops-repo" "$BRANCH"
az extension add -n azure-devops -y
az devops configure --defaults organization="https://dev.azure.com/<YOUR_ADO_ORG>" project="<YOUR_ADO_PROJECT>"
az repos pr create \
--repository "gitops-repo" \
--source-branch "$BRANCH" \
--target-branch main \
--title "Provision Azure App Service from Port" \
--description "$(cat .port/pr-description.md)"
displayName: Run Claude Code and open PR
env:
ANTHROPIC_API_KEY: $(ANTHROPIC_API_KEY)
AZURE_DEVOPS_PAT: $(AZURE_DEVOPS_PAT)
AZURE_DEVOPS_EXT_PAT: $(AZURE_DEVOPS_PAT)
PR_DESCRIPTION: ${{ parameters.pr_description }}
Replace <YOUR_ADO_ORG> and <YOUR_ADO_PROJECT> in the push URL and az devops configure lines before you create the pipeline. URL-encode spaces in the project name (for example Port%20Integration).
Create the pipeline in Azure DevOps
- In Azure DevOps, go to Pipelines → New pipeline.
- Select your
gitops-reporepository and the YAML path above. - Save the pipeline (do not require a continuous trigger; this guide uses
trigger: none). - Note the pipeline ID from the URL:
definitionId=<PIPELINE_ID>.
Add pipeline variables
In the pipeline Edit → Variables, add:
| Variable | Value |
|---|---|
ANTHROPIC_API_KEY | Your Anthropic API key (mark as secret). |
AZURE_DEVOPS_PAT | A PAT with Code (Read & Write) and permission to create pull requests (mark as secret). |
Grant repository permissions to the pipeline identity
By default, the Azure DevOps Build Service account often has only read access to the repository. Claude must create a branch, push commits, and open a pull request, so grant write permissions explicitly.
- Go to Project settings → Repositories → gitops-repo → Security.
- Search for the pipeline identity. Common names:
{Project Name} Build Service ({Organization}).Project Collection Build Service ({Organization}).
- Set these permissions to Allow:
- Contribute.
- Create branch.
- Contribute to pull requests.
- Save the changes.
If you skip this step, git push or az repos pr create often fails with a permissions error even when the pipeline variables are correct.
Smoke-test the pipeline
Run the pipeline manually once with a simple command, for example:
Create environments/staging/smoke-test.tf that instantiates modules/azure-app-service for app smoke-test in Staging. Follow modules/azure-app-service/README.md conventions for tags and SKU.
Confirm a feature branch and pull request appear in gitops-repo before wiring Port. The PR body can stay short for this smoke test; the full Port-context description is added when Port passes pr_description.
Coding agent alternatives
- Claude Code (this guide)
- Cursor
Use the Azure Pipelines job above. Port triggers it with an Azure DevOps pipeline action and passes the implementation plan as parameters.command plus a Port-context PR body as parameters.pr_description.
Cursor cloud agents can also work against Azure DevOps when Cursor is connected with an organization login token for your ADO org. Port's Cursor Cloud Agents integration action is oriented around GitHub repository URLs for the default workspace fields.
For an Azure DevOps-first stack, prefer the Claude Code pipeline in this guide, or run Cursor from your own ADO pipeline similarly to the Claude job once Cursor is connected to your organization.
Set up a local Azure DevOps MCP HTTP bridge
Workflow AI nodes need an HTTP MCP endpoint with shared header authentication. A practical local setup is:
- Run the Azure DevOps MCP server in stdio mode inside Docker.
- Bridge it to streamable HTTP with supergateway.
- Protect the bridge with a shared secret that Port sends as a Bearer token.
- Expose the bridge to Port with ngrok.
Create the Docker image
Create a Dockerfile (for example in a local ado-mcp-bridge folder):
Dockerfile (Click to expand)
FROM node:20-slim
RUN npm install -g @ryancardin/azuredevops-mcp-server supergateway
ENV AZURE_DEVOPS_ORG_URL="https://dev.azure.com/<YOUR_ADO_ORG>"
ENV AZURE_DEVOPS_PROJECT="<YOUR_ADO_PROJECT>"
ENV AZURE_DEVOPS_IS_ON_PREMISES="false"
ENV AZURE_DEVOPS_AUTH_TYPE="pat"
# AZURE_DEVOPS_PERSONAL_ACCESS_TOKEN is passed at runtime, not baked into the image
EXPOSE 8000
CMD ["npx", "supergateway", "--stdio", "npx @ryancardin/azuredevops-mcp-server@latest", "--port", "8000", "--outputTransport", "streamableHttp", "--baseUrl", "http://0.0.0.0:8000"]
Replace <YOUR_ADO_ORG> and <YOUR_ADO_PROJECT> with your Azure DevOps organization and project names, then build:
docker build -t ado-mcp-bridge .
Run the bridge
Start the container with your Azure DevOps PAT and a shared bridge secret:
docker run -d \
-p 8000:8000 \
-e AZURE_DEVOPS_PERSONAL_ACCESS_TOKEN="<YOUR_ADO_PAT>" \
-e BRIDGE_SHARED_SECRET="<YOUR_BRIDGE_SHARED_SECRET>" \
--name ado-mcp-bridge \
ado-mcp-bridge
AZURE_DEVOPS_PERSONAL_ACCESS_TOKENauthenticates the MCP server to Azure DevOps (needs Code (Read) at minimum for this guide).BRIDGE_SHARED_SECRETis the token Port must send in the connectorAuthorizationheader. Pick a long random value and keep it out of git.
Confirm the local endpoint responds before tunneling:
curl -sS -o /dev/null -w "%{http_code}\n" \
-X POST "http://localhost:8000/mcp" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer <YOUR_BRIDGE_SHARED_SECRET>" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"0.0.1"}}}'
A successful probe returns HTTP 200.
Expose the bridge with ngrok
Port cannot reach localhost directly. In a separate terminal, run:
ngrok http 8000
Copy the public HTTPS forwarding URL (for example https://<subdomain>.ngrok-free.app). Your Port connector URL will be that host plus /mcp:
https://<subdomain>.ngrok-free.app/mcp
If ngrok or the Docker container stops, workflow AI nodes that call Azure DevOps MCP tools will hang or fail until you restart both and update the connector URL if the ngrok host changed.
Add Port secrets for MCP
Create these Port secrets:
| Secret | Value |
|---|---|
ADO_BRIDGE_TOKEN | The same value as BRIDGE_SHARED_SECRET from the Docker run command. Port sends this as the connector Bearer token. |
The Azure DevOps PAT stays on the bridge container (AZURE_DEVOPS_PERSONAL_ACCESS_TOKEN). You do not need to store the ADO PAT in Port for this bridge pattern.
Configure the Azure DevOps MCP connector
The workflow AI node reads module files through this connector.
-
Go to the Data sources page in Port.
-
Click + Data source, then open the MCP Servers tab.
-
Select Custom Server and fill in:
- Name / identifier:
azure-devops(you will reference this identifier from the workflow). - URL: your ngrok MCP endpoint (for example
https://<subdomain>.ngrok-free.app/mcp). - Headers:
{"Authorization": "Bearer {{ .secrets[\"ADO_BRIDGE_TOKEN\"] }}"} - Name / identifier:
-
Under Allowed Tools, enable at least:
azure-devops_listRepositories.azure-devops_browseRepository.azure-devops_getFileContent.azure-devops_searchCode.azure-devops_listBranches.
-
Click Publish.
Port prefixes tools with the connector identifier. If your connector identifier is azure-devops, tools appear as azure-devops_getFileContent. The Bearer token in headers must match BRIDGE_SHARED_SECRET on the bridge. Workflow AI nodes use this shared header auth rather than per-user OAuth. See MCP connectors.
Create the resource request blueprint
This blueprint tracks each request from form submit through pull request creation.
-
Go to the Builder page in Port.
-
Click + Blueprint.
-
Click Edit JSON and paste:
Azure resource request blueprint (Click to expand)
{"identifier": "azure_resource_request","title": "Azure Resource Request","icon": "Azure","schema": {"properties": {"status": {"type": "string","title": "Status","enum": ["In Progress", "Pending Approval", "Approved", "Rejected"],"enumColors": {"In Progress": "lightGray","Pending Approval": "yellow","Approved": "green","Rejected": "red"}},"resource_type": {"type": "string","title": "Resource type"},"environment": {"type": "string","title": "Environment","enum": ["Development", "Staging", "Production"],"enumColors": {"Development": "green","Staging": "yellow","Production": "red"}},"app_name": {"type": "string","title": "App name"},"resource_group_name": {"type": "string","title": "Resource group"},"implementation_plan": {"type": "string","title": "Implementation plan","format": "markdown"},"module_summary": {"type": "string","title": "Module summary","format": "markdown"},"pipeline_run_url": {"type": "string","title": "Pipeline run URL","format": "url"}},"required": ["status", "resource_type", "environment", "app_name"]},"mirrorProperties": {},"calculationProperties": {},"aggregationProperties": {},"relations": {"requested_by": {"title": "Requested by","target": "_user","required": false,"many": false}}} -
Click Save.
Build the workflow
The workflow has three stages after the trigger:
-
Read modules - an
AInode uses Azure DevOps MCP tools to browse the sample repo and read module source, then returns an implementation plan, a Claudecommand, and a markdownpr_descriptionwith Port request context. -
Record the request - upsert an
azure_resource_requestentity. -
Trigger Claude Code - an Azure DevOps pipeline action queues your Azure Pipeline with
commandandpr_description. Claude writes the Terraform (and can enrich.port/pr-description.md), then the job opens a pull request whose body includes Port context. WithreportPipelineStatus: true, Port waits for the pipeline to finish before continuing. -
Go to the Workflows page in Port.
-
Click + Workflow.
-
In the Name field, enter
Request Azure App Service, then click Confirm. -
Click the see workflow JSON button and replace the example with the JSON below.
-
Replace every
<PLACEHOLDER>value listed after the JSON. -
Click Save.
Request Azure App Service workflow (Click to expand)
{
"identifier": "request_azure_app_service_ado_mcp",
"title": "Request Azure App Service",
"icon": "Azure",
"description": "Read Terraform modules from Azure DevOps via MCP, then trigger Claude Code to open a pull request",
"category": "Agentic Resource Management",
"allowAnyoneToViewRuns": true,
"nodes": [
{
"identifier": "trigger",
"title": "Request Azure App Service",
"icon": "Azure",
"config": {
"type": "SELF_SERVE_TRIGGER",
"userInputs": {
"properties": {
"app_name": {
"type": "string",
"title": "App name",
"description": "Name of the App Service to provision"
},
"environment": {
"type": "string",
"title": "Environment",
"default": "Staging",
"enum": ["Development", "Staging", "Production"],
"enumColors": {
"Development": "green",
"Staging": "yellow",
"Production": "red"
}
},
"resource_group_name": {
"type": "string",
"title": "Resource group",
"default": "rg-demo"
},
"location": {
"type": "string",
"title": "Azure location",
"default": "eastus"
},
"additional_context": {
"type": "string",
"title": "Additional context",
"format": "markdown"
}
},
"required": ["app_name", "environment", "resource_group_name"],
"order": [
"app_name",
"environment",
"resource_group_name",
"location",
"additional_context"
]
}
}
},
{
"identifier": "read_terraform_modules",
"title": "Read Terraform modules from Azure DevOps",
"icon": "AI",
"config": {
"type": "AI",
"userPrompt": "You must use Azure DevOps MCP tools to read real Terraform source from repository ID `<YOUR_ADO_REPOSITORY_ID>` in project `<YOUR_ADO_PROJECT>`.\n\nSteps:\n1. Call azure-devops_browseRepository on `/`, `/modules`, and `/terraform-modules` (as needed) to discover the golden module layout.\n2. Call azure-devops_getFileContent for the module's main.tf, variables.tf, outputs.tf, and README.md.\n3. Optionally call azure-devops_searchCode for existing App Service instantiations under environments/.\n4. Using ONLY the module conventions you read, draft an implementation plan, a Claude Code command, and a Port-context PR description.\n\nPort request context (include this in pr_description):\n- Requested by: {{ .workflowRun.trigger.by.email }}\n- Workflow run ID: {{ .workflowRun.identifier }}\n- Port entity request ID (use this identifier pattern): {{ .outputs.trigger.environment | ascii_downcase }}-{{ .outputs.trigger.app_name | ascii_downcase }}-{{ .workflowRun.identifier | gsub(\"wfr_\"; \"\") | ascii_downcase }}\n- App name: {{ .outputs.trigger.app_name }}\n- Environment: {{ .outputs.trigger.environment }}\n- Resource group: {{ .outputs.trigger.resource_group_name }}\n- Location: {{ .outputs.trigger.location }}\n- Additional context: {{ .outputs.trigger.additional_context }}\n\nReturn JSON matching the output schema.\n- target_file_path must be under `/environments/` and include the environment and app name, for example `/environments/staging/payments-api.tf`.\n- pr_description must be markdown with these sections: Summary, Port request context, Module conventions, Implementation plan, and Review checklist. Under Port request context, list requester, workflow run ID, request entity ID, app name, environment, resource group, location, and additional context.\n- claude_command must be a single plain-text instruction Claude Code can execute in the gitops-repo working tree. It must: (1) create the target Terraform file by instantiating the discovered module, follow README conventions for tags and SKU, and not modify the module itself; (2) ensure `.port/pr-description.md` contains the Port-context PR description (create or overwrite it with the exact pr_description markdown, no fences); (3) not create the pull request itself because the Azure Pipeline opens the PR from `.port/pr-description.md`.",
"systemPrompt": "You are an Azure infrastructure engineer working with Azure DevOps and Port's context lake. Always call the MCP tools to read repository files before drafting the plan. Prefer existing module conventions from README.md and variables.tf over inventing new patterns. Never invent module inputs that are not in the module. Keep claude_command free of markdown fences. Always produce a detailed markdown pr_description that a reviewer can use without opening Port.",
"tools": [
"azure-devops_listRepositories",
"azure-devops_browseRepository",
"azure-devops_getFileContent",
"azure-devops_searchCode",
"azure-devops_listBranches"
],
"mcpServers": [
{
"identifier": "azure-devops"
}
],
"outputSchema": {
"type": "object",
"properties": {
"module_summary": {
"type": "string",
"description": "Short summary of the module conventions you found in the repo"
},
"implementation_plan": {
"type": "string",
"description": "Step-by-step plan for the Terraform change"
},
"target_file_path": {
"type": "string",
"description": "Repo path for the new file, starting with /environments/"
},
"claude_command": {
"type": "string",
"description": "Plain-text command passed to the Claude Code Azure Pipeline"
},
"pr_description": {
"type": "string",
"description": "Markdown PR description with Port request context, module conventions, and implementation plan"
}
},
"required": [
"module_summary",
"implementation_plan",
"target_file_path",
"claude_command",
"pr_description"
]
}
},
"variables": {
"module_summary": "{{ .result.response | fromjson | .module_summary }}",
"implementation_plan": "{{ .result.response | fromjson | .implementation_plan }}",
"target_file_path": "{{ .result.response | fromjson | .target_file_path }}",
"claude_command": "{{ .result.response | fromjson | .claude_command }}",
"pr_description": "{{ .result.response | fromjson | .pr_description }}",
"request_id": "{{ .outputs.trigger.environment | ascii_downcase }}-{{ .outputs.trigger.app_name | ascii_downcase }}-{{ .workflowRun.identifier | gsub(\"wfr_\"; \"\") | ascii_downcase }}"
}
},
{
"identifier": "create_resource_request",
"title": "Create resource request",
"icon": "NewPage",
"config": {
"type": "UPSERT_ENTITY",
"blueprintIdentifier": "azure_resource_request",
"mapping": {
"identifier": "{{ .outputs.read_terraform_modules.request_id }}",
"title": "{{ .outputs.trigger.app_name }} ({{ .outputs.trigger.environment }})",
"properties": {
"status": "In Progress",
"resource_type": "Azure App Service",
"environment": "{{ .outputs.trigger.environment }}",
"app_name": "{{ .outputs.trigger.app_name }}",
"resource_group_name": "{{ .outputs.trigger.resource_group_name }}",
"implementation_plan": "{{ .outputs.read_terraform_modules.implementation_plan }}",
"module_summary": "{{ .outputs.read_terraform_modules.module_summary }}"
},
"relations": {
"requested_by": "{{ .workflowRun.trigger.by.email }}"
}
}
}
},
{
"identifier": "trigger_claude_pipeline",
"title": "Trigger Claude Code pipeline",
"icon": "AzureDevops",
"config": {
"type": "INTEGRATION_ACTION",
"installationId": "<YOUR_ADO_INSTALLATION_ID>",
"integrationProvider": "azure-devops",
"integrationInvocationType": "trigger_pipeline",
"integrationActionExecutionProperties": {
"project": "<YOUR_ADO_PROJECT>",
"pipelineId": "<YOUR_PIPELINE_ID>",
"branch": "main",
"templateParameters": {
"command": "{{ .outputs.read_terraform_modules.claude_command }}",
"pr_description": "{{ .outputs.read_terraform_modules.pr_description }}"
},
"reportPipelineStatus": true
}
}
},
{
"identifier": "update_request_pr_ready",
"title": "Update request - PR ready",
"icon": "Link",
"config": {
"type": "UPSERT_ENTITY",
"blueprintIdentifier": "azure_resource_request",
"mapping": {
"identifier": "{{ .outputs.read_terraform_modules.request_id }}",
"properties": {
"status": "Pending Approval"
}
}
}
}
],
"connections": [
{
"sourceIdentifier": "trigger",
"targetIdentifier": "read_terraform_modules"
},
{
"sourceIdentifier": "read_terraform_modules",
"targetIdentifier": "create_resource_request"
},
{
"sourceIdentifier": "create_resource_request",
"targetIdentifier": "trigger_claude_pipeline"
},
{
"sourceIdentifier": "trigger_claude_pipeline",
"targetIdentifier": "update_request_pr_ready"
}
]
}
Replace these placeholders before saving:
| Placeholder | Example |
|---|---|
<YOUR_ADO_PROJECT> | Platform |
<YOUR_ADO_REPOSITORY_ID> | Repository GUID from Azure DevOps |
<YOUR_PIPELINE_ID> | Numeric pipeline definition ID (for example 27) |
<YOUR_ADO_INSTALLATION_ID> | Your Azure DevOps integration installation ID (often azure-devops) |
| MCP connector identifier | Must match azure-devops (or update mcpServers and every azure-devops_* tool name) |
Open the pipeline in Azure DevOps. The URL contains definitionId=<YOUR_PIPELINE_ID>. On the Data sources page, open your Azure DevOps integration; the installation ID is the integration identifier shown in the UI (and in the mapping JSON).
Test the workflow
- Go to the Self-service page and open Request Azure App Service.
- Enter an app name (for example
payments-api), choose Staging, and submit. - Open the workflow run and confirm:
read_terraform_modulescalled MCP tools and returned amodule_summarythat reflects the sample README conventions (tags, SKU rules).trigger_claude_pipelinequeued the Azure Pipeline and reported success after Claude finished (withreportPipelineStatus: true).- The
azure_resource_requestentity moved to Pending Approval.
- Open the new pull request in
gitops-repoand verify:- The Terraform instantiates your golden module with the expected tags and SKU.
- The PR description includes Port request context (requester, workflow run ID, environment, module conventions, and implementation plan), not only a one-line commit message.
In the read_terraform_modules node logs you should see MCP tool calls such as azure-devops_browseRepository and azure-devops_getFileContent. That is the proof that Port read actual Terraform source from Azure DevOps. The Claude pipeline then proves the write side by opening a real ADO pull request.
Troubleshooting
- AI node cannot use the MCP connector: Confirm the Docker bridge and ngrok tunnel are running, the connector URL ends with
/mcp,ADO_BRIDGE_TOKENmatchesBRIDGE_SHARED_SECRET, and the connector is Published withexposedenabled. - Ngrok URL changed: Free ngrok hosts change when you restart. Update the connector URL in Port to the new
https://.../mcpvalue. - Tool names not found: Port prefixes tools with the connector identifier. Align
toolswith the names shown under Allowed Tools on the connector. - Pipeline trigger fails: Confirm the Azure DevOps integration is in Single Account mode, actions processing is enabled, and
installationId/pipelineId/projectmatch your org. See Azure DevOps Pipelines. - Pipeline fails on
git pushoraz repos pr create: Grant the Build Service identity Contribute, Create branch, and Contribute to pull requests ongitops-repo. - Claude makes no file changes: Confirm
ANTHROPIC_API_KEYis set on the pipeline, and inspect theclaude_commandvalue passed intotemplateParameters.command. - Wrong repository or project in the PR: Update the explicit push URL /
az devops configurevalues in the pipeline YAML to match your org andgitops-repo. - PR description is still minimal: Confirm the pipeline YAML accepts
pr_description, seeds.port/pr-description.md, and passes--description "$(cat .port/pr-description.md)"toaz repos pr create.