-
Notifications
You must be signed in to change notification settings - Fork 2
MCP Integration
The Model Context Protocol (MCP) integration enables agents to access external tools and services during prototype generation. MCP handlers connect to external servers that expose tools via the MCP protocol, and those tools become available to agents during their execution.
The integration follows a handler-based plugin pattern: each handler owns its transport, authentication, and protocol logic. The extension provides lifecycle management, tool routing, scoping, and a circuit breaker for reliability.
The MCP system has three layers:
Defined in mcp/base.py. Each handler implements:
- connect() -- establish connection to the MCP server
- disconnect() -- clean shutdown
- list_tools() -- return available tool definitions
- call_tool(name, arguments) -- invoke a tool and return results
Handlers are fully responsible for transport (HTTP, stdio, WebSocket), authentication, protocol handshake, error handling, retries, and timeouts.
Defined in mcp/registry.py. Follows the same builtin/custom resolution pattern as the AgentRegistry:
- Custom handlers (loaded from
.prototype/mcp/directory) - Built-in handlers (shipped with the extension)
Custom handlers override built-in handlers of the same name.
Defined in mcp/manager.py. The primary interface for the rest of the extension. Provides:
- Lazy connection: handlers connect on first tool access, not at startup
- Tool routing: maps tool names to handlers and dispatches calls
- Circuit breaker: marks handlers as failed after consecutive errors
- OpenAI schema conversion: formats tools for AI provider consumption
- Context manager: clean shutdown when the session ends
- Thread safety: safe for concurrent tool calls from parallel agent execution
MCP servers are configured in prototype.yaml under mcp.servers. Sensitive fields (API keys, tokens) route to prototype.secrets.yaml automatically via the mcp.servers prefix in SECRET_KEY_PREFIXES.
mcp:
servers:
- name: lightpanda
enabled: true
stages: ["build", "deploy"] # null = all stages
agents: ["terraform-agent"] # null = all agents
timeout: 30
max_retries: 2
max_result_bytes: 8192
settings:
base_url: "http://localhost:8080"
api_key: "..." # routed to secrets fileThe MCPHandlerConfig dataclass defines the configuration fields:
| Field | Type | Default | Description |
|---|---|---|---|
name |
string | (required) | Unique handler name |
stages |
list of strings or null | null (all) | Stages where this handler's tools are available |
agents |
list of strings or null | null (all) | Agents that can use this handler's tools |
enabled |
bool | true |
Whether the handler is active |
timeout |
int | 30 | Seconds per tool call |
max_retries |
int | 2 | Maximum retry attempts on failure |
max_result_bytes |
int | 8192 | Truncate results exceeding this size |
settings |
dict | {} |
Handler-specific settings (URLs, credentials, etc.) |
Create custom MCP handlers by placing Python files in the .prototype/mcp/ directory within your project.
The filename determines the handler name: lightpanda_handler.py registers as lightpanda (the _handler suffix is stripped automatically). Alternatively, define MCP_HANDLER_CLASS in the module to specify the handler class explicitly; otherwise the loader auto-discovers the first MCPHandler subclass.
A custom handler extends MCPHandler and implements four abstract methods: connect(), disconnect(), list_tools(), and call_tool(). The handler is fully responsible for transport, authentication, error handling, and retries.
Minimal example -- a handler that wraps a REST API:
# .prototype/mcp/cost_lookup_handler.py
import requests
from azext_prototype.mcp.base import (
MCPHandler,
MCPHandlerConfig,
MCPToolDefinition,
MCPToolResult,
)
class CostLookupHandler(MCPHandler):
"""Handler that looks up Azure retail prices."""
name = "cost-lookup"
description = "Azure Retail Prices API for real-time cost data"
def __init__(self, config: MCPHandlerConfig, **kwargs):
super().__init__(config, **kwargs)
self._session: requests.Session | None = None
def connect(self) -> None:
self._session = requests.Session()
self._session.headers["Accept"] = "application/json"
# No auth required for Azure Retail Prices API
self._connected = True
self.logger.info("Connected to Azure Retail Prices API")
def disconnect(self) -> None:
if self._session:
self._session.close()
self._session = None
self._connected = False
def list_tools(self) -> list[MCPToolDefinition]:
return [
MCPToolDefinition(
name="lookup_price",
description="Look up Azure retail price for a service and SKU",
input_schema={
"type": "object",
"properties": {
"service_name": {
"type": "string",
"description": "Azure service name (e.g., 'Virtual Machines')",
},
"sku_name": {
"type": "string",
"description": "SKU name (e.g., 'Standard_B2s')",
},
"region": {
"type": "string",
"description": "Azure region (e.g., 'eastus')",
},
},
"required": ["service_name"],
},
handler_name=self.name,
)
]
def call_tool(self, name: str, arguments: dict) -> MCPToolResult:
if name != "lookup_price":
return MCPToolResult(
content="", is_error=True,
error_message=f"Unknown tool: {name}",
)
try:
service = arguments["service_name"]
sku = arguments.get("sku_name", "")
region = arguments.get("region", "eastus")
filter_parts = [
f"serviceName eq '{service}'",
f"armRegionName eq '{region}'",
]
if sku:
filter_parts.append(f"skuName eq '{sku}'")
resp = self._session.get(
"https://prices.azure.com/api/retail/prices",
params={"$filter": " and ".join(filter_parts)},
timeout=self.config.timeout,
)
resp.raise_for_status()
data = resp.json()
items = data.get("Items", [])[:5] # Top 5 results
if not items:
return MCPToolResult(content="No pricing data found.")
lines = []
for item in items:
lines.append(
f"- {item['productName']} / {item['skuName']}: "
f"${item['retailPrice']:.4f} {item['unitOfMeasure']} "
f"({item['type']})"
)
content = "\n".join(lines)
if len(content) > self.config.max_result_bytes:
content = content[: self.config.max_result_bytes] + "...(truncated)"
return MCPToolResult(content=content)
except requests.Timeout:
return MCPToolResult(
content="", is_error=True,
error_message=f"Timeout after {self.config.timeout}s",
)
except Exception as exc:
return MCPToolResult(
content="", is_error=True,
error_message=f"Failed: {exc}",
)
# Tell the loader which class to instantiate
MCP_HANDLER_CLASS = CostLookupHandlerConfigure it in prototype.yaml:
mcp:
servers:
- name: cost-lookup
enabled: true
stages: ["build", "design"]
agents: ["cost-analyst", "cloud-architect"]
timeout: 15
max_retries: 1
max_result_bytes: 4096
settings: {}For handlers that communicate with MCP servers using the JSON-RPC protocol, see the reference implementation at mcp/examples/lightpanda_handler.py. It demonstrates:
- MCP
initializehandshake withprotocolVersionandclientInfo - Tool discovery via
tools/listJSON-RPC call - Tool invocation via
tools/callwith retry logic - Session URL management for stateful MCP servers
- Proper error handling and result truncation
Configuration for JSON-RPC handlers:
# prototype.yaml
mcp:
servers:
- name: lightpanda
stages: ["build", "deploy"]
agents: ["qa-engineer", "app-developer"]
timeout: 30
max_retries: 2
settings:
url: "https://mcp.pipedream.net/v2"# prototype.secrets.yaml (sensitive settings auto-routed here)
mcp:
servers:
- name: lightpanda
settings:
api_key: "lpd_xxxxxxxxxxxx"The MCPHandler base class provides these built-in attributes (available via self):
| Attribute | Description |
|---|---|
self.config |
MCPHandlerConfig with all settings from prototype.yaml |
self.client_info |
MCPClientInfo for MCP initialize handshake |
self.console |
Console object for user-facing messages |
self.logger |
Python logger for debug/info/warning output |
self.project_config |
Full project config dict (read-only) |
self._connected |
Connection state (set to True in connect()) |
Override health_check() -> bool for custom health validation beyond connection state.
Tools are scoped along two dimensions:
Set stages: ["build", "deploy"] to restrict a handler's tools to specific stages. Set stages: null (or omit the field) to make tools available in all stages.
Set agents: ["terraform-agent", "bicep-agent"] to restrict a handler's tools to specific agents. Set agents: null (or omit the field) to make tools available to all agents.
Both dimensions are AND-combined: a tool is available only when both the current stage and the requesting agent match the handler's scope configuration.
The MCPManager tracks consecutive errors per handler. After 3 consecutive errors, the handler is marked as failed and its tools are no longer offered to agents for the remainder of the session. A warning is displayed when the circuit breaker trips.
This prevents a misbehaving MCP server from degrading the entire session with repeated timeouts or errors.
The default mode. When an agent's AI response includes tool calls, BaseAgent.execute() enters a loop:
- Get scoped tools from MCPManager, formatted as OpenAI function-calling schema
- Pass tools to the AI provider alongside the conversation
- Detect
tool_callsin the AI response - Invoke each tool via
MCPManager.call_tool() - Feed tool results back to the AI as role="tool" messages
- Repeat until the AI produces a final text response (up to
_max_tool_iterations = 10)
Agents opt in via _enable_mcp_tools = True (the default for all agents).
Stages or other code can invoke tools directly via manager.call_tool(tool_name, arguments) without going through the AI loop. This is useful for predetermined tool calls where the stage knows exactly what tool to invoke and with what arguments.
| Class | Purpose |
|---|---|
MCPClientInfo |
Client identity for the MCP initialize handshake |
MCPHandlerConfig |
Configuration from prototype.yaml |
MCPToolDefinition |
Tool name, description, and JSON Schema for arguments |
MCPToolResult |
Tool invocation result with content, error state, and metadata |
MCPToolCall |
A tool call request with id, name, and arguments |
- Knowledge System -- another source of external information for agents
- Escalation -- MCP failures can trigger escalation
Getting Started
Stages
Interfaces
Configuration
Agent System
Features
- Backlog Generation
- Cost Analysis
- Error Analysis
- Docs & Spec Kit
- MCP Integration
- Knowledge System
- Escalation
Quality
Help
Policies — Azure
AI Services
Compute
Data Services
- Azure SQL
- Backup Vault
- Cosmos Db
- Data Factory
- Databricks
- Event Grid
- Event Hubs
- Fabric
- IoT Hub
- Mysql Flexible
- Postgresql Flexible
- Recovery Services
- Redis Cache
- Service Bus
- Stream Analytics
- Synapse Workspace
Identity
Management
Messaging
Monitoring
Networking
- Application Gateway
- Bastion
- CDN
- DDoS Protection
- DNS Zones
- Expressroute
- Firewall
- Load Balancer
- Nat Gateway
- Network Interface
- Private Endpoints
- Public Ip
- Route Tables
- Traffic Manager
- Virtual Network
- Vpn Gateway
- WAF Policy
Security
Storage
Web & App
Policies — Well-Architected
Reliability
Security
Cost Optimization
Operational Excellence
Performance Efficiency
Integration