One awkward fact: Bicep rejects several MCP-specific fields that the Azure API Center API actually accepts. If you want to manage a private MCP registry as code, that mismatch blocks you fast. If you’re past a handful of servers, this matters.
The portal flow is fine to prove the concept. At 10-plus servers and multiple environments, you need pull requests, drift detection, repeatable deployments, and the ability to rebuild from a single repo. The guide I’m summarizing lays out a concrete, working approach to do exactly that.
What’s actually in play
- API Center supports registering MCP servers as assets: remote endpoints with a runtime URL tied to an environment, or local/package-based servers described via package metadata. The portal creates remotes and versions; under the hood there’s an APIs resource plus a deployments child with a runtime URL and environment.
- The rub: ARM/Bicep type definitions don’t include all MCP fields you’ll want in practice (think remote, packages, auth/security bits). The service API will take them; Bicep type checking won’t.
The approach that works in practice
- One JSON per MCP server. Name, title, summary, description (don’t leave it empty), kind=mcp, version title, plus optional vendor/icon/useCases/license/support. For remotes: runtime URL and transport hint. For packages: registry/name/version/runtime hint and arguments. These JSON files become the source of truth.
- A reusable Bicep module (2024-06-01-preview API) that:
- Creates the workspace and an environment.
- Materializes each JSON as an API of kind mcp.
- When a remote is present, creates a deployments child with server.runtimeUri pointing to the runtime URL and associates it to the environment so it shows up in the servers listing.
- Loads JSON at compile time (no parameter files) using Bicep’s JSON loading.
- To get past missing types, the module wraps properties in any/unions to bypass compile-time validation so the service can accept the full MCP shape.
- A PowerShell deploy script that runs the Bicep and then prunes: anything in Azure not defined in the JSONs is deleted. That keeps drift to zero across dev/test/prod.
Constraints and trade-offs
- Bypassing type checks with any() is a tactical workaround. You lose schema safety in exchange for getting real deployments. Expect to revisit this as the resource provider types evolve.
- Registry JSON vs. ARM shape isn’t 1:1. Some fields map differently than partner schemas suggest. Test carefully before rolling into prod.
- Portal behaviors matter: an empty description makes a server invisible in VS Code. Keep mandatory display fields populated.
- Pruning deletes. Wire approvals and dry runs into CI so you don’t accidentally wipe legitimate assets when files move or names change.
- Remote versus package-based servers will age differently. Streamable HTTP is where most remote servers are heading; SSE is on the way out across the ecosystem. Don’t overfit to transports that might be deprecated.
What this changes on teams
- You get PR review on every server addition or change, with a simple “add a JSON file” workflow.
- You can spin up identical registries per environment and rebuild from scratch, useful for DR and audits.
- You finally get real drift detection and automated cleanup instead of a portal-only registry that accumulates cruft.
What I’d watch: changes to the API Center ARM schema that might make the any/union hack unnecessary—or break it. Guard the pruning step with explicit allowlists in early rollouts. And document the minimum required fields in your repo to avoid invisible servers.
The takeaway: If your org is adopting MCP beyond a demo, treat the registry like any other infra: JSON per server, a Bicep module that tolerates today’s gaps, and a deploy script that enforces desired state with a cautious delete path.

