Introduction

This guide describes how to run an Azure DevOps MCP (Model Context Protocol) server in Kubernetes using the ToolHive Operator. This enables AI assistants like Claude, Open WebUI, or other MCP-compatible clients to connect directly to your Azure DevOps environment.

What is MCP?

The Model Context Protocol is an open standard that allows AI models to communicate with external tools and data sources. Instead of manually copy-pasting context, an AI assistant can directly:

  • Query and create work items
  • Search repositories and browse code
  • Manage pull requests
  • Trigger and monitor pipelines
  • Read and write wiki pages

Why ToolHive Operator?

Most MCP servers are stdio-based - they communicate via stdin/stdout. This works fine locally but is challenging in Kubernetes. The ToolHive Operator solves this by:

  1. Running a proxy pod that exposes HTTP/SSE endpoints
  2. The proxy communicates via stdio with the actual MCP server container
  3. Clients can connect via standard HTTP

This means you can use any stdio MCP server and ToolHive automatically turns it into an HTTP service.

Architecture

┌─────────────────┐   HTTP/SSE      ┌──────────────────────────────────────┐
│   Open WebUI    │◄───────────────►│         ToolHive Proxy Pod           │
│   Claude        │                 │  (azure-devops-xxxxx-xxxxx)          │
│   Cursor        │                 │                                      │
└─────────────────┘                 │    ┌─────────────────────────────┐   │
                                    │    │   stdio communication       │   │
                                    │    ▼                             │   │
                                    │  ┌───────────────────────────┐   │   │
                                    │  │  MCP Server StatefulSet   │   │   │
                                    │  │  (azure-devops-0)         │   │   │
                                    │  │                           │   │   │
                                    │  │  @tiberriver256/          │   │   │
                                    │  │  mcp-server-azure-devops  │   │   │
                                    │  └───────────────────────────┘   │   │
                                    └──────────────────────────────────────┘
                                                     │
                                                     ▼ HTTPS
                                    ┌──────────────────────────────────────┐
                                    │      Azure DevOps Services           │
                                    │      dev.azure.com                   │
                                    └──────────────────────────────────────┘

Prerequisites

  • Kubernetes cluster (Talos, K3s, or similar)
  • ToolHive Operator installed
  • Azure DevOps Personal Access Token (PAT)
  • Network access to dev.azure.com and registry.npmjs.org

Installation

1. Install ToolHive Operator

helm repo add toolhive https://stacklok.github.io/toolhive
helm repo update

helm install toolhive-operator toolhive/toolhive-operator \
  --namespace toolhive-system \
  --create-namespace

Verify the operator is running:

kubectl get pods -n toolhive-system

2. Create Azure DevOps PAT

Navigate to Azure DevOps → User Settings → Personal Access Tokens and create a token with the following scopes:

ScopeAccess
CodeRead & Write
Work ItemsRead & Write
BuildRead & Execute
ReleaseRead
WikiRead & Write
Project and TeamRead

3. Create Kubernetes Secret

kubectl create secret generic azure-devops-creds \
  --namespace toolhive-system \
  --from-literal=pat="YOUR-PAT-TOKEN-HERE"

4. Build Custom Container Image

The default node:20-slim image has a read-only filesystem which causes issues with npm caching. Build a custom image:

FROM node:20-slim

# Writable directories for npm
ENV NPM_CONFIG_CACHE=/tmp/.npm
ENV HOME=/tmp

# Pre-install the MCP server
RUN npm install -g @tiberriver256/mcp-server-azure-devops

# Entrypoint
ENTRYPOINT ["mcp-server-azure-devops"]

Build and push to your registry:

docker build -t registry.example.com/mcp/azure-devops:latest .
docker push registry.example.com/mcp/azure-devops:latest

5. Create MCPServer Resource

apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPServer
metadata:
  name: azure-devops
  namespace: toolhive-system
spec:
  image: registry.example.com/mcp/azure-devops:latest
  transport: stdio
  proxyPort: 8080
  env:
    - name: AZURE_DEVOPS_ORG_URL
      value: "https://dev.azure.com/YourOrganization"
    - name: AZURE_DEVOPS_AUTH_METHOD
      value: "pat"
    - name: AZURE_DEVOPS_DEFAULT_PROJECT
      value: "YourDefaultProject"
  secrets:
    - name: azure-devops-creds
      key: pat
      targetEnvName: AZURE_DEVOPS_PAT

Apply:

kubectl apply -f mcpserver-azure-devops.yaml

6. Verification

Check if the pods are running:

kubectl get pods -n toolhive-system -l toolhive=true

You should see two pods:

  • azure-devops-0 - The MCP server (StatefulSet)
  • azure-devops-xxxxx-xxxxx - The ToolHive proxy (Deployment)

Test the MCP endpoint:

kubectl port-forward -n toolhive-system svc/azure-devops 8080:8080

# In another terminal:
curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

Available Tools

After successful setup you have access to 47+ Azure DevOps tools:

Projects & Organizations

ToolDescription
get_meCurrent user info
list_organizationsList accessible organizations
list_projectsProjects in organization
get_project_detailsProject details with teams and work item types

Repositories

ToolDescription
list_repositoriesRepos in a project
get_file_contentRetrieve file contents
get_repository_treeDirectory structure
create_branchCreate new branch
create_commitCommit with file changes
search_codeSearch code across repos

Work Items

ToolDescription
list_work_itemsQuery work items (WIQL)
get_work_itemWork item details
create_work_itemCreate new work item
update_work_itemUpdate work item
search_work_itemsSearch work items

Pull Requests

ToolDescription
list_pull_requestsPRs in repository
create_pull_requestCreate new PR
update_pull_requestUpdate PR
get_pull_request_changesChanged files and diffs
add_pull_request_commentAdd comment

Pipelines

ToolDescription
list_pipelinesPipelines in project
trigger_pipelineStart pipeline run
list_pipeline_runsRecent runs
pipeline_timelineStages and jobs
get_pipeline_logBuild logs

Wiki

ToolDescription
get_wikisWikis in project
get_wiki_pagePage content
create_wiki_pageCreate new page
search_wikiSearch wiki

Why Open WebUI with Ollama?

Open WebUI is a self-hosted web interface for interacting with Large Language Models. Combined with Ollama, it provides a fully private, local AI stack that keeps your data on-premises.

Benefits of this stack

BenefitDescription
PrivacyAll data stays within your infrastructure - no API calls to external AI providers
CostNo per-token charges - run as many queries as your hardware allows
FlexibilitySwitch between models (Llama, Mistral, DeepSeek, Qwen) without vendor lock-in
MCP SupportNative support for Model Context Protocol, enabling AI to interact with external tools
Self-hostedRuns entirely in your Kubernetes cluster alongside your other workloads

How it fits together

┌─────────────────────────────────────────────────────────────────┐
│                        Kubernetes Cluster                       │
│                                                                 │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────────┐   │
│  │   Open WebUI │───►│    Ollama    │    │  ToolHive Proxy  │   │
│  │   (Web UI)   │    │ (LLM Runner) │    │                  │   │
│  └──────┬───────┘    └──────────────┘    └────────┬─────────┘   │
│         │                                         │             │
│         │           MCP (HTTP/SSE)                │             │
│         └─────────────────────────────────────────┘             │
│                              │                                  │
│                              ▼                                  │
│                     ┌──────────────────┐                        │
│                     │  MCP Server      │                        │
│                     │  (Azure DevOps)  │                        │
│                     └────────┬─────────┘                        │
│                              │                                  │
└──────────────────────────────┼──────────────────────────────────┘
                               │ HTTPS
                               ▼
                    ┌──────────────────┐
                    │  Azure DevOps    │
                    │  (External API)  │
                    └──────────────────┘

With MCP tools enabled, you can ask the AI natural language questions like:

  • “List all open bugs assigned to me”
  • “Show me the pipelines that failed today”
  • “Create a work item for the API performance issue”
  • “What changed in the last PR on the infrastructure repo?”

The AI uses the MCP tools to fetch real data from Azure DevOps and formulates a response.

Open WebUI Integration

Configuration

  1. Go to Settings → External Tools
  2. Click + to add a new tool server
  3. Configure:
FieldValue
TypeMCP Streamable HTTP
URLhttp://azure-devops.toolhive-system.svc:8080/mcp
AuthNone
IDAzureDevOps
NameAzureDevOps
Open WebUI MCP Configuration

MCP tool server configuration in Open WebUI

Note: If Open WebUI runs outside the cluster, use an Ingress or port-forward.

Result

Once configured, you can ask the AI to interact with Azure DevOps directly:

Azure DevOps projects listed in Open WebUI

AI listing Azure DevOps projects via MCP

Via Traefik IngressRoute

apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: azure-devops-mcp
  namespace: toolhive-system
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`mcp-ado.example.com`)
      kind: Rule
      services:
        - name: azure-devops
          port: 8080
  tls:
    certResolver: letsencrypt

For long-running MCP connections, you may want to add a Middleware for timeouts:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: mcp-timeouts
  namespace: toolhive-system
spec:
  headers:
    customResponseHeaders:
      X-Custom-Response-Header: "mcp"
---
apiVersion: traefik.io/v1alpha1
kind: ServersTransport
metadata:
  name: mcp-transport
  namespace: toolhive-system
spec:
  forwardingTimeouts:
    dialTimeout: 30s
    responseHeaderTimeout: 0s  # Disable for SSE
    idleConnTimeout: 90s

Troubleshooting

Pod fails with npm/npx errors

npm error enoent ENOENT: no such file or directory, mkdir '/home/node/.npm'

Cause: The container filesystem is read-only. NPX cannot create its cache directory.

Solution: Use a custom container image with pre-installed dependencies (see step 4).

Timeout waiting for response

{"level":"warn","msg":"Timeout waiting for response for method=tools/list"}

Cause: The proxy cannot attach to the MCP server container.

Solutions:

  1. Check if the MCP server pod is running: kubectl get pods -n toolhive-system
  2. Check MCP server logs: kubectl logs azure-devops-0 -n toolhive-system
  3. Verify the image has a proper entrypoint that starts the MCP server

Authentication errors from Azure DevOps

TF400813: The user is not authorized to access this resource.

Cause: PAT token lacks required permissions or has expired.

Solutions:

  1. Verify PAT hasn’t expired
  2. Check PAT has all required scopes
  3. Verify the secret is correctly mounted: kubectl get secret azure-devops-creds -n toolhive-system -o yaml

Connection refused from Open WebUI

Cause: Network policy or service configuration issue.

Solutions:

  1. Check service exists: kubectl get svc -n toolhive-system | grep azure-devops
  2. Test from within cluster: kubectl run curl --rm -it --image=curlimages/curl -- curl http://azure-devops.toolhive-system.svc:8080/mcp
  3. Check for NetworkPolicies blocking traffic

Security Considerations

PAT Token Scope

Follow the principle of least privilege. Only grant the scopes your use case actually needs:

Use CaseRequired Scopes
Read-only explorationCode (Read), Work Items (Read), Build (Read)
Work item managementWork Items (Read & Write)
Code changesCode (Read & Write)
Full automationAll scopes listed above

Network Isolation

Consider using NetworkPolicies to restrict which pods can access the MCP server:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: azure-devops-mcp-access
  namespace: toolhive-system
spec:
  podSelector:
    matchLabels:
      app: azure-devops
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              name: open-webui
      ports:
        - port: 8080

Audit Logging

All actions performed through the MCP server are logged in Azure DevOps audit logs under the PAT owner’s identity. Review these regularly for unexpected activity.

References