{"id":238,"date":"2026-01-08T09:00:00","date_gmt":"2026-01-08T17:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/aspire\/?p=238"},"modified":"2026-01-08T07:30:54","modified_gmt":"2026-01-08T15:30:54","slug":"scaling-ai-agents-with-aspire-isolation","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/aspire\/scaling-ai-agents-with-aspire-isolation\/","title":{"rendered":"Scaling AI Agents with Aspire: The Missing Isolation Layer for Parallel Development"},"content":{"rendered":"<blockquote>\n<p><strong>Liked this blog post?<\/strong> It was originally posted on Tamir Dresher&#8217;s blog, <a href=\"https:\/\/www.tamirdresher.com\/\">https:\/\/www.tamirdresher.com\/<\/a>, check out more content there!<\/p>\n<\/blockquote>\n<p>In my <a href=\"https:\/\/www.tamirdresher.com\/blog\/2025\/10\/20\/scaling-your-ai-development-team-with-git-worktrees.html\">previous post about Git worktrees<\/a>, I showed how to run multiple AI agents in parallel, each working on different features in separate worktrees. Aspire is a game-changer for AI-assisted development because it gives your agents <strong>superpowers<\/strong>: with a single <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/src\/NoteTaker.AppHost\/Program.cs\"><code>Program.cs<\/code><\/a>, an agent can spawn an entire distributed system\u2014backend APIs, Python services, frontends, databases, message queues\u2014everything orchestrated and ready to test. Even better, using <a href=\"https:\/\/aspire.dev\/dashboard\/mcp-server\/\">Aspire&#8217;s MCP server<\/a>, agents can programmatically query resource status, retrieve logs, and troubleshoot issues. This means AI agents can interact with the <strong>whole system<\/strong>, not just individual components, dramatically simplifying development workflows.<\/p>\n<p>But there&#8217;s a critical problem when you want to scale to multiple worktrees: <strong>port conflicts<\/strong>. When you try to run Aspire AppHost from multiple worktrees simultaneously, they all fight over the same ports, making parallel AI agent development impossible.<\/p>\n<p>I solved this by adding an isolation layer that automatically allocates unique ports for each worktree. While I implemented this with scripts and a clever MCP proxy, <strong>I hope the Aspire team will bake this capability directly into the Aspire CLI<\/strong> as part of an <code>aspire run --isolated<\/code> command\u2014making multi-instance isolation a first-class feature.<\/p>\n<h2>Why Aspire is Perfect for AI Agents<\/h2>\n<p>Before diving into the port isolation problem, let me explain why Aspire is such a powerful tool for AI-assisted development:<\/p>\n<h3>1. Spawn Entire Systems with Minimal Code<\/h3>\n<p>An AI agent can create a complete distributed application with just a few lines:<\/p>\n<pre><code class=\"language-csharp\">var builder = DistributedApplication.CreateBuilder(args);\n\nvar cache = builder.AddRedis(\"cache\");\nvar db = builder.AddPostgres(\"db\").AddDatabase(\"notetakerdb\");\nvar messaging = builder.AddRabbitMQ(\"messaging\");\n\nvar backend = builder.AddProject&lt;Projects.Backend&gt;(\"backend\")\n    .WithReference(cache)\n    .WithReference(db)\n    .WithReference(messaging)\n    .WithHttpEndpoint(name: \"http\")\n    .WithExternalHttpEndpoints();\n\nvar aiService = builder.AddPythonApp(\"ai-service\", \"..\/ai-service\", \"main.py\")\n    .WithReference(db)\n    .WithReference(messaging)\n    .WithHttpEndpoint(env: \"PORT\", name: \"http\")\n    .WithExternalHttpEndpoints();\n\nbuilder.AddJavaScriptApp(\"frontend\", \"..\/frontend\")\n    .WithReference(backend)\n    .WithReference(aiService.GetEndpoint(\"http\"))\n    .WithHttpEndpoint(env: \"PORT\")\n    .WithExternalHttpEndpoints();\n\nbuilder.Build().Run();<\/code><\/pre>\n<p>This spins up:<\/p>\n<ul>\n<li>Redis cache<\/li>\n<li>PostgreSQL database<\/li>\n<li>RabbitMQ message broker<\/li>\n<li>C# backend API<\/li>\n<li>Python AI service<\/li>\n<li>JavaScript frontend<\/li>\n<li>All networking between them configured automatically<\/li>\n<\/ul>\n<p>An AI agent can modify this, run it, test the whole system, and iterate\u2014all autonomously.<\/p>\n<h3>2. System-Wide Observability via Aspire MCP<\/h3>\n<p>Aspire&#8217;s <a href=\"https:\/\/aspire.dev\/dashboard\/mcp-server\/\">MCP (Model Context Protocol) support<\/a> lets AI agents interact with the running system:<\/p>\n<pre><code class=\"language-text\">Agent: \"Check if all resources are healthy\"\n\u2192 Uses list_resources tool\n\u2192 Gets status of all services, containers, and executables\n\nAgent: \"Why is the backend failing?\"\n\u2192 Uses list_console_logs tool for backend\n\u2192 Reads startup errors and stack traces\n\nAgent: \"Show me traces for slow requests\"\n\u2192 Uses list_traces tool\n\u2192 Analyzes distributed tracing data<\/code><\/pre>\n<p>This is transformative: instead of debugging individual components, agents can <strong>reason about the entire system<\/strong>, following request flows across services, correlating logs, and identifying root causes.<\/p>\n<p>Aspire also provides distributed integration testing capabilities that enable agents to run comprehensive tests against the entire system\u2014I&#8217;ll cover this later in the post.<\/p>\n<h2>The Problem: Port Conflicts Kill Parallelism<\/h2>\n<p>This all works beautifully\u2014until you try to run multiple worktrees in parallel. Every AppHost instance tries to grab the same ports:<\/p>\n<p><strong>All worktrees try to use:<\/strong><\/p>\n<ul>\n<li>Port 18888 for Aspire Dashboard<\/li>\n<li>Port 18889 for OTLP (OpenTelemetry) endpoint<\/li>\n<li>Port 18890 for Resource Service endpoint<\/li>\n<li>Port 4317 for MCP endpoint<\/li>\n<\/ul>\n<h3>Port Conflict on Startup<\/h3>\n<p><strong>Terminal 1<\/strong> (feature-auth worktree):<\/p>\n<pre><code class=\"language-powershell\">cd worktrees-example.worktrees\\feature-auth\\src\\NoteTaker.AppHost\ndotnet run\n# \u2705 Works - Dashboard on port 18888<\/code><\/pre>\n<p><strong>Terminal 2<\/strong> (feature-payments worktree):<\/p>\n<pre><code class=\"language-powershell\">cd worktrees-example.worktrees\\feature-payments\\src\\NoteTaker.AppHost\ndotnet run\n# \u274c ERROR: Port 18888 is already in use!\n# \u274c ERROR: Port 18889 is already in use!\n# \u274c ERROR: Port 18890 is already in use!<\/code><\/pre>\n<p>You can&#8217;t run the second AppHost at all.<\/p>\n<h3>Manual Workarounds Don&#8217;t Scale<\/h3>\n<p>You could manually edit ports for each worktree, but this is tedious and error-prone:<\/p>\n<ul>\n<li>\u274c You need to remember which ports are free<\/li>\n<li>\u274c You have to manually set 3+ environment variables per worktree<\/li>\n<li>\u274c Cleanup requires tracking which terminals use which ports<\/li>\n<li>\u274c <strong>Biggest problem<\/strong>: Your agent&#8217;s MCP connection needs to know which port to connect to<\/li>\n<\/ul>\n<p>The fundamental issue: <strong>your worktrees have isolated code, but shared port space<\/strong>.<\/p>\n<h2>The Solution: Port Isolation + MCP Proxy<\/h2>\n<p>The solution has two layers:<\/p>\n<ol>\n<li><strong>Port allocation<\/strong>: Scripts that automatically find and allocate unique ports for each AppHost instance<\/li>\n<li><strong>MCP proxy<\/strong>: An indirection layer that lets AI agents connect to whichever AppHost is currently running<\/li>\n<\/ol>\n<h3>Layer 1: Automatic Port Allocation<\/h3>\n<p>The <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/start-apphost.ps1\"><code>start-apphost.ps1<\/code><\/a> and <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/start-apphost.sh\"><code>start-apphost.sh<\/code><\/a> scripts:<\/p>\n<ol>\n<li><strong>Find free ports<\/strong> using .NET&#8217;s port allocation<\/li>\n<li><strong>Set environment variables<\/strong> for Aspire dashboard components<\/li>\n<li><strong>Launch AppHost<\/strong> with those ports<\/li>\n<li><strong>Save port configuration<\/strong> to <code>scripts\/settings.json<\/code> for MCP proxy<\/li>\n<li><strong>Display dashboard URL and Process ID<\/strong> for monitoring<\/li>\n<\/ol>\n<p>Here&#8217;s what the output looks like:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/01\/start-apphost-output.png\" alt=\"Start AppHost Output\" \/><\/p>\n<p>Notice the dynamically allocated ports (54772-54775) saved for the MCP proxy to use.<\/p>\n<h3>Layer 2: The MCP Proxy Problem<\/h3>\n<p>Here&#8217;s the challenge: Aspire&#8217;s MCP server runs on a specific port (e.g., 54775) and requires an API key. When using direct HTTP MCP configuration, your AI agent&#8217;s configuration (<a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/.roo\/mcp.json\"><code>.roo\/mcp.json<\/code><\/a>) needs <strong>both<\/strong> to be fixed:<\/p>\n<pre><code class=\"language-json\">{\n  \"mcpServers\": {\n    \"aspire-dashboard\": {\n      \"type\": \"http\",\n      \"url\": \"http:\/\/localhost:62980\/mcp\",  \/\/ \u274c This port is fixed!\n      \"headers\": {\n        \"x-mcp-api-key\": \"McpKey\"  \/\/ \u274c This API key is fixed!\n      }\n    }\n  }\n}<\/code><\/pre>\n<p><strong>The two problems:<\/strong><\/p>\n<ol>\n<li>\n<p><strong>Dynamic Ports<\/strong>: Which port to use?<\/p>\n<ul>\n<li>Worktree 1&#8217;s AppHost MCP is on port 54775<\/li>\n<li>Worktree 2&#8217;s AppHost MCP is on port 61450<\/li>\n<li>Worktree 3&#8217;s AppHost MCP is on port 58232<\/li>\n<\/ul>\n<\/li>\n<li>\n<p><strong>Dynamic API Keys<\/strong>: Each AppHost generates a unique API key for security<\/p>\n<\/li>\n<\/ol>\n<p>Your <code>.roo\/mcp.json<\/code> can&#8217;t know either value in advance!<\/p>\n<h3>Solution: The aspire-mcp-proxy<\/h3>\n<p>The <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/aspire-mcp-proxy.cs\"><code>aspire-mcp-proxy.cs<\/code><\/a> script adds the missing layer of indirection:<\/p>\n<pre><code class=\"language-text\">\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Roo AI      \u2502 stdio   \u2502 aspire-mcp-proxy \u2502  HTTP   \u2502 Aspire AppHost  \u2502\n\u2502 Agent       \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba\u2502 (fixed config)   \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba\u2502 (dynamic port)  \u2502\n\u2502             \u2502         \u2502                  \u2502         \u2502                 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                              \u2193\n                        reads from\n                        scripts\/settings.json\n                        (updated by start-apphost.ps1)<\/code><\/pre>\n<p><strong>How it works:<\/strong><\/p>\n<ol>\n<li><strong>AI agent connects to proxy<\/strong> via stdio (always the same configuration)<\/li>\n<li><strong>Proxy reads <code>scripts\/settings.json<\/code><\/strong> to discover the current AppHost&#8217;s MCP port<\/li>\n<li><strong>Proxy forwards MCP requests<\/strong> to the correct dynamic port via HTTP<\/li>\n<li><strong>Responses flow back<\/strong> through the proxy to the agent<\/li>\n<\/ol>\n<p><strong>In <code>.roo\/mcp.json<\/code>:<\/strong><\/p>\n<pre><code class=\"language-json\">{\n  \"mcpServers\": {\n    \"aspire-mcp\": {\n      \"command\": \"dotnet\",\n      \"args\": [\"scripts\/aspire-mcp-proxy.cs\", \"--no-build\"],\n      \"description\": \"Aspire Dashboard MCP stdio proxy\"\n    }\n  }\n}<\/code><\/pre>\n<p><strong>Note:<\/strong> I&#8217;m using .NET 10&#8217;s single-file script feature\u2014<code>dotnet run app.cs<\/code> runs a C# file directly without needing a project file. This makes the proxy incredibly simple: one 272-line file that&#8217;s both an MCP client (connecting to Aspire) and an MCP server (exposing tools to Roo), all using the official <a href=\"https:\/\/www.nuget.org\/packages\/ModelContextProtocol\/0.4.1-preview.1\"><code>ModelContextProtocol@0.4.1-preview.1<\/code><\/a> NuGet package. It&#8217;s amazing to have a complete bidirectional MCP proxy in a single, self-contained script!<\/p>\n<p>The proxy configuration is <strong>fixed<\/strong>\u2014it doesn&#8217;t need to know which AppHost is running! The <code>scripts\/settings.json<\/code> file bridges the gap:<\/p>\n<pre><code class=\"language-json\">{\n  \"port\": \"54775\",\n  \"apiKey\": \"abc123...\",\n  \"lastUpdated\": \"2025-11-15T10:30:00Z\"\n}<\/code><\/pre>\n<p>Every time you run <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/start-apphost.ps1\"><code>start-apphost.ps1<\/code><\/a>, it updates <code>settings.json<\/code> with the new ports. The proxy reads it dynamically on each request.\nAlso, it sets the <code>AppHost__McpApiKey<\/code> envvar for the apphost so we can control the token that will be sued for the Aspire MCP server<\/p>\n<h3>Putting It All Together<\/h3>\n<ol>\n<li>\n<p>cd worktrees-example.worktrees\/feature-auth<\/p>\n<\/li>\n<li>\n<p>.\/scripts\/start-apphost.ps1\n\u2192 Finds free ports: 54772-54775\n\u2192 Updates scripts\/settings.json with port 54775\n\u2192 Starts AppHost on those ports<\/p>\n<\/li>\n<li>\n<p>Roo connects to aspire-mcp-proxy (via .roo\/mcp.json)\n\u2192 Proxy reads scripts\/settings.json\n\u2192 Discovers AppHost MCP is on port 54775\n\u2192 Forwards all MCP requests there<\/p>\n<\/li>\n<li>\n<p>Roo asks: &#8220;list_resources&#8221;\n\u2192 Goes through proxy \u2192 AppHost MCP on 54775\n\u2192 Returns resource status<\/p>\n<\/li>\n<li>\n<p>Switch to different worktree:\ncd ..\/feature-payments\n.\/scripts\/start-apphost.ps1\n\u2192 Finds free ports: 61447-61450\n\u2192 Updates scripts\/settings.json with port 61450<\/p>\n<\/li>\n<li>\n<p>Roo&#8217;s next request automatically goes to port 61450\n\u2192 No configuration change needed!<\/p>\n<\/li>\n<\/ol>\n<p><strong>This is the key insight<\/strong>: by adding the proxy layer, we decouple the AI agent&#8217;s configuration from the dynamic port allocation. The agent always talks to the same proxy, and the proxy figures out where the current AppHost is running.<\/p>\n<h2>Implementation: Using the Scripts<\/h2>\n<p>Let me show you how to set this up for the <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\">NoteTaker example application<\/a>:<\/p>\n<h3>Step 1: Configure AppHost for Worktree Detection<\/h3>\n<p>In your <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/src\/NoteTaker.AppHost\/Program.cs\"><code>Program.cs<\/code><\/a>, detect the Git folder name and customize the dashboard name:<\/p>\n<pre><code class=\"language-csharp\">var gitFolderName = GitFolderResolver.GetGitFolderName();\nvar dashboardAppName = string.IsNullOrEmpty(gitFolderName)\n    ? \"NoteTaker\"\n    : $\"NoteTaker-{gitFolderName}\";\n\nvar builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions()\n{\n    Args = args,\n    DashboardApplicationName = dashboardAppName,\n});\n\nvar cache = builder.AddRedis(\"cache\");\nvar db = builder.AddPostgres(\"db\").AddDatabase(\"notetakerdb\");\nvar messaging = builder.AddRabbitMQ(\"messaging\");\n\nvar backend = builder.AddProject&lt;Projects.Backend&gt;(\"backend\")\n    .WithReference(cache)\n    .WithReference(db)\n    .WithReference(messaging)\n    .WithHttpEndpoint(name: \"http\")  \/\/ \u2705 No port = random allocation\n    .WithExternalHttpEndpoints();\n\nbuilder.Build().Run();<\/code><\/pre>\n<p><strong>Key benefits:<\/strong><\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/src\/NoteTaker.AppHost\/Program.cs#L56\"><code>GitFolderResolver.GetGitFolderName()<\/code><\/a> detects if you&#8217;re in a worktree<\/li>\n<li>Dashboard shows <code>NoteTaker-feature-auth<\/code> for feature-auth worktree<\/li>\n<li><a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/src\/NoteTaker.AppHost\/Program.cs#L29\"><code>WithHttpEndpoint()<\/code><\/a> without port allocates random ports<\/li>\n<\/ul>\n<p>By setting the <code>DashboardApplicationName<\/code> property of the <code>DistributedApplicationBuilder<\/code> we can make it clear in the dashboard in which worktree are we working on.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/01\/dashboard-title.png\" alt=\"Dashboard Title\" \/><\/p>\n<h3>Step 2: Start AppHost with Scripts<\/h3>\n<p><strong>Never run <code>dotnet run<\/code> directly<\/strong>. Always use the management scripts:<\/p>\n<h4>PowerShell (Windows)<\/h4>\n<pre><code class=\"language-powershell\">cd worktrees-example.worktrees\\feature-auth\n.\\scripts\\start-apphost.ps1\n\n# Output shows:\n# - Dashboard URL with unique port\n# - MCP endpoint saved to settings.json\n# - Process ID for cleanup<\/code><\/pre>\n<h4>Bash (Linux\/macOS or Git Bash)<\/h4>\n<pre><code class=\"language-bash\">cd worktrees-example.worktrees\/feature-auth\n.\/scripts\/start-apphost.sh<\/code><\/pre>\n<p>The script:<\/p>\n<ol>\n<li>Finds 4 free ports<\/li>\n<li>Sets environment variables<\/li>\n<li>Updates <code>scripts\/settings.json<\/code> with MCP port and API key<\/li>\n<li>Launches AppHost<\/li>\n<li>Returns Process ID for cleanup<\/li>\n<\/ol>\n<h3>Step 3: Configure MCP Proxy in .roo\/mcp.json<\/h3>\n<p>Add the proxy to your <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/.roo\/mcp.json\"><code>.roo\/mcp.json<\/code><\/a>:<\/p>\n<pre><code class=\"language-json\">{\n  \"mcpServers\": {\n    \"aspire-mcp\": {\n      \"command\": \"dotnet\",\n      \"args\": [\"scripts\/aspire-mcp-proxy.cs\", \"--no-build\"],\n      \"env\": {},\n      \"description\": \"Aspire Dashboard MCP stdio proxy\",\n      \"alwaysAllow\": [\n        \"list_resources\",\n        \"execute_resource_command\",\n        \"list_traces\",\n        \"list_trace_structured_logs\",\n        \"list_console_logs\",\n        \"list_structured_logs\"\n      ]\n    }\n  }\n}<\/code><\/pre>\n<p><strong>No ports to configure!<\/strong> The proxy reads <code>scripts\/settings.json<\/code> dynamically.<\/p>\n<h3>Step 4: Run Multiple Worktrees Simultaneously<\/h3>\n<p>Now you can run as many worktrees as you need:<\/p>\n<p><strong>Terminal 1 &#8211; Feature Auth:<\/strong><\/p>\n<pre><code class=\"language-powershell\">cd worktrees-example.worktrees\\feature-auth\n.\\scripts\\start-apphost.ps1\n# Dashboard: https:\/\/localhost:54772\n# MCP: port 54775 (saved to settings.json)\n# Process ID: 12345<\/code><\/pre>\n<p><strong>Terminal 2 &#8211; Feature Payments:<\/strong><\/p>\n<pre><code class=\"language-powershell\">cd worktrees-example.worktrees\\feature-payments\n.\\scripts\\start-apphost.ps1\n# Dashboard: https:\/\/localhost:61447\n# MCP: port 61450 (saved to settings.json)\n# Process ID: 67890<\/code><\/pre>\n<p><strong>Terminal 3 &#8211; Feature UI:<\/strong><\/p>\n<pre><code class=\"language-powershell\">cd worktrees-example.worktrees\\feature-ui\n.\\scripts\\start-apphost.ps1\n# Dashboard: https:\/\/localhost:58229\n# MCP: port 58232 (saved to settings.json)\n# Process ID: 11223<\/code><\/pre>\n<p>All three run simultaneously with zero conflicts! Your AI agent automatically connects to whichever one you&#8217;re working in.<\/p>\n<h3>Step 5: Cleanup When Done<\/h3>\n<h4>Quick Cleanup (Recommended)<\/h4>\n<pre><code class=\"language-powershell\">.\\scripts\\kill-apphost.ps1 -All<\/code><\/pre>\n<pre><code class=\"language-bash\">.\/scripts\/kill-apphost.sh --all<\/code><\/pre>\n<p>This terminates all AppHost processes from your repository.<\/p>\n<h2>Enabling AI Agents to Work Independently<\/h2>\n<p>One powerful benefit is that AI agents can now work completely autonomously, including testing their own changes. I&#8217;ve documented the AppHost management rules in <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/.roo\/rules\/05-apphost-management.md\"><code>.roo\/rules\/05-apphost-management.md<\/code><\/a> which instructs Roo (my AI coding agent) on:<\/p>\n<ul>\n<li>Always use <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/start-apphost.ps1\"><code>start-apphost.ps1<\/code><\/a>\/<a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/start-apphost.sh\"><code>.sh<\/code><\/a><\/li>\n<li>How to use MCP tools to check resource status<\/li>\n<li>Troubleshooting steps when resources fail<\/li>\n<li>Cleanup procedures using <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/kill-apphost.ps1\"><code>kill-apphost.ps1 -All<\/code><\/a><\/li>\n<\/ul>\n<p>With these rules, I can ask Roo to:<\/p>\n<ol>\n<li>Make code changes<\/li>\n<li>Start the AppHost to test<\/li>\n<li>Use MCP tools to verify functionality<\/li>\n<li>Check logs if something fails<\/li>\n<li>Clean up when done<\/li>\n<\/ol>\n<p>The agent works completely autonomously, even running and testing multiple worktrees in parallel.<\/p>\n<h2>Script Reference<\/h2>\n<table>\n<thead>\n<tr>\n<th>Script<\/th>\n<th>Purpose<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/start-apphost.ps1\"><code>start-apphost.ps1<\/code><\/a> \/ <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/start-apphost.sh\"><code>.sh<\/code><\/a><\/td>\n<td>Start AppHost with auto port allocation, update settings.json<\/td>\n<\/tr>\n<tr>\n<td><a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/aspire-mcp-proxy.cs\"><code>aspire-mcp-proxy.cs<\/code><\/a><\/td>\n<td>MCP proxy that reads settings.json and forwards to current AppHost<\/td>\n<\/tr>\n<tr>\n<td><a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/kill-apphost.ps1\"><code>kill-apphost.ps1<\/code><\/a> \/ <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/kill-apphost.sh\"><code>.sh<\/code><\/a><\/td>\n<td>Kill AppHost instances (by PID or all)<\/td>\n<\/tr>\n<tr>\n<td><a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/list-apphosts.ps1\"><code>list-apphosts.ps1<\/code><\/a> \/ <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/list-apphosts.sh\"><code>.sh<\/code><\/a><\/td>\n<td>List all running instances<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><strong>Environment Variables Set by Scripts:<\/strong><\/p>\n<pre><code class=\"language-powershell\">$env:ASPIRE_DASHBOARD_PORT = \"54772\"                              # Dynamic\n$env:ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL = \"http:\/\/localhost:54773\"\n$env:ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL = \"http:\/\/localhost:54774\"\n$env:ASPIRE_DASHBOARD_MCP_ENDPOINT_URL = \"http:\/\/localhost:54775\"  # Saved to settings.json<\/code><\/pre>\n<h2>Benefits<\/h2>\n<h3>1. True AI Agent Superpowers<\/h3>\n<p>Combine Aspire&#8217;s system orchestration with MCP observability:<\/p>\n<ul>\n<li>\u2705 Agents spawn entire distributed systems<\/li>\n<li>\u2705 Agents query resource status programmatically<\/li>\n<li>\u2705 Agents read logs and traces to debug<\/li>\n<li>\u2705 Agents work on whole systems, not just components<\/li>\n<\/ul>\n<h3>2. Zero Configuration MCP Connections<\/h3>\n<p>The proxy solves the dynamic port problem:<\/p>\n<ul>\n<li>\u2705 <code>.roo\/mcp.json<\/code> is fixed (no per-worktree configuration)<\/li>\n<li>\u2705 Proxy automatically finds current AppHost<\/li>\n<li>\u2705 Works seamlessly across worktree switches<\/li>\n<li>\u2705 No manual port tracking needed<\/li>\n<\/ul>\n<h3>3. True Parallel Development<\/h3>\n<p>Multiple agents work simultaneously:<\/p>\n<ul>\n<li>\u2705 No &#8220;wait for Agent A to finish&#8221;<\/li>\n<li>\u2705 No manual port coordination<\/li>\n<li>\u2705 Each agent completely independent<\/li>\n<\/ul>\n<h3>4. Works with Any Aspire Project<\/h3>\n<ul>\n<li>\u2705 Standard Aspire features only<\/li>\n<li>\u2705 No custom NuGet packages<\/li>\n<li>\u2705 Simple script-based approach<\/li>\n<\/ul>\n<h2>Aspire&#8217;s Built-in Distributed Testing Support<\/h2>\n<p>Beyond orchestration and observability, Aspire provides <a href=\"https:\/\/aspire.dev\/testing\/overview\/\">distributed testing capabilities<\/a> that enable true end-to-end testing with automatic port isolation. Instead of just running the AppHost, your AI agent can now <strong>run comprehensive tests<\/strong> against the entire system.<\/p>\n<p>Using <a href=\"https:\/\/aspire.dev\/testing\/write-your-first-test\/?testing-framework=mstest\"><code>DistributedApplicationTestingBuilder<\/code><\/a>, you can spin up your full application stack\u2014frontend, backend, databases, message queues\u2014with <a href=\"https:\/\/aspire.dev\/testing\/manage-app-host\/?testing-framework=mstest\">automatically randomized ports<\/a> for complete isolation:<\/p>\n<pre><code class=\"language-csharp\">var appHost = await DistributedApplicationTestingBuilder\n    .CreateAsync&lt;Projects.NoteTaker_AppHost&gt;();\n\nvar app = await appHost.BuildAsync();\nawait app.StartAsync();\n\n\/\/ Wait for resources to be healthy\nawait app.ResourceNotifications.WaitForResourceHealthyAsync(\"frontend\");\n\n\/\/ Get dynamically allocated endpoint\nvar frontendUrl = app.GetEndpoint(\"frontend\");<\/code><\/pre>\n<p>Combine this with <a href=\"https:\/\/playwright.dev\">Playwright<\/a> and you achieve true end-to-end tests:<\/p>\n<pre><code class=\"language-csharp\">\/\/ Get the dynamically allocated frontend URL\nvar frontendUrl = app.GetEndpoint(\"frontend\").ToString();\n\n\/\/ Use Playwright to interact with the UI\nvar page = await browser.NewPageAsync();\nawait page.GotoAsync(frontendUrl);\n\n\/\/ Test the actual UI with all dependencies running\nawait page.FillAsync(\"#title\", \"Test Task\");\nawait page.ClickAsync(\"button[type='submit']\");\nawait page.WaitForSelectorAsync(\".task-item\");<\/code><\/pre>\n<p>In the <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/src\/NoteTaker.Tests\/PlaywrightIntegrationTests.cs\">NoteTaker example<\/a>, tests interact with the actual frontend UI while all backend services, databases, and dependencies run in the background\u2014all with isolated, randomly allocated ports.<\/p>\n<p><strong>This means your AI agent is now truly autonomous<\/strong>: it can modify code, run the full test suite with all system dependencies, and validate changes end-to-end without manual intervention. Read more about <a href=\"https:\/\/aspire.dev\/testing\/accessing-resources\/\">accessing resources in tests<\/a>.<\/p>\n<h2>Under the Hood: How the MCP Proxy Works<\/h2>\n<p>The <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/aspire-mcp-proxy.cs\"><code>aspire-mcp-proxy.cs<\/code><\/a> is the heart of the solution. Let me explain how it&#8217;s implemented.<\/p>\n<h3>The Dual Nature: MCP Client + MCP Server<\/h3>\n<p>The proxy is simultaneously:<\/p>\n<ol>\n<li><strong>MCP Server<\/strong> (stdio) &#8211; Exposes tools to Roo via standard input\/output<\/li>\n<li><strong>MCP Client<\/strong> (HTTP) &#8211; Connects to Aspire&#8217;s MCP server to invoke tools<\/li>\n<\/ol>\n<p>Here&#8217;s the architecture:<\/p>\n<pre><code class=\"language-md\">\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 aspire-mcp-proxy.cs (272 lines, single file)                    \u2502\n\u2502                                                                 \u2502\n\u2502  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510         \u2502\n\u2502  \u2502 MCP Server (stdio) \u2502\u25c4\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524 Roo Agent           \u2502         \u2502\n\u2502  \u2502 - Exposes tools    \u2502         \u2502 (sends tool calls)  \u2502         \u2502\n\u2502  \u2502 - Handles requests \u2502         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2502\n\u2502  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                                         \u2502\n\u2502           \u2502                                                     \u2502\n\u2502           \u25bc                                                     \u2502\n\u2502  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                                         \u2502\n\u2502  \u2502 ProxyTool          \u2502  For each tool:                         \u2502\n\u2502  \u2502 - Wraps downstream \u2502  1. Read settings.json                  \u2502\n\u2502  \u2502 - Reads settings   \u2502  2. Create HTTP client                  \u2502\n\u2502  \u2502 - Forwards calls   \u2502  3. Forward request to Aspire           \u2502\n\u2502  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518  4. Return response                     \u2502\n\u2502           \u2502                                                     \u2502\n\u2502           \u25bc                                                     \u2502\n\u2502  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                                         \u2502\n\u2502  \u2502 MCP Client (HTTP)  \u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba Aspire Dashboard MCP           \u2502\n\u2502  \u2502 - Connects to port \u2502         (dynamic port from settings)    \u2502\n\u2502  \u2502 - Invokes tools    \u2502                                         \u2502\n\u2502  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                                         \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/pre>\n<h3>Key Implementation Details<\/h3>\n<h4>1. Dynamic Settings Loading<\/h4>\n<p>The proxy reads <code>scripts\/settings.json<\/code> on every request to get the current AppHost&#8217;s port and API key:<\/p>\n<pre><code class=\"language-csharp\">async Task&lt;McpClient&gt; CreateClientAsync()\n{\n    var current = await LoadSettingsAsync(settingsPath);\n    var transport = new HttpClientTransport(new()\n    {\n        Endpoint = new Uri($\"http:\/\/localhost:{current.Port}\/mcp\"),\n        AdditionalHeaders = new Dictionary&lt;string, string&gt;\n        {\n            [\"x-mcp-api-key\"] = current.ApiKey!,\n            [\"Accept\"] = \"application\/json, text\/event-stream\"\n        }\n    });\n    return await McpClient.CreateAsync(transport);\n}<\/code><\/pre>\n<h4>2. Tool Caching for Offline Mode<\/h4>\n<p>When the proxy starts, it attempts to connect to Aspire and cache the available tools. If Aspire isn&#8217;t running, it uses cached tool metadata:<\/p>\n<pre><code class=\"language-csharp\">try\n{\n    var client = await CreateClientAsync();\n    await cache.RefreshAsync(client);\n    \/\/ Online mode: use live tools\n    tools = cache.GetTools().Select(t =&gt; new ProxyTool(CreateClientAsync, t));\n}\ncatch (Exception ex)\n{\n    await Console.Error.WriteLineAsync($\"[AspireMcpProxy] Connection failed: {ex.Message}\");\n    await Console.Error.WriteLineAsync(\"[AspireMcpProxy] Using cached tools\");\n\n    var cachedTools = await cache.LoadAsync();\n    \/\/ Offline mode: create tools from cached metadata\n    tools = cachedTools.Select(t =&gt; new ProxyTool(CreateClientAsync, t));\n}<\/code><\/pre>\n<p>This means Roo can see Aspire tools even before starting AppHost, though they&#8217;ll fail if invoked while offline.<\/p>\n<h4>3. ProxyTool: The Forwarding Logic<\/h4>\n<p>Each tool exposed by the proxy is a <code>ProxyTool<\/code> that:<\/p>\n<ul>\n<li>Accepts stdio requests from Roo<\/li>\n<li>Reads current settings<\/li>\n<li>Creates an HTTP client<\/li>\n<li>Forwards to Aspire&#8217;s MCP server<\/li>\n<li>Returns the response<\/li>\n<\/ul>\n<pre><code class=\"language-csharp\">public override async ValueTask&lt;CallToolResult&gt; InvokeAsync(\n    RequestContext&lt;CallToolRequestParams&gt; request,\n    CancellationToken ct = default)\n{\n    var args = request.Params?.Arguments?\n        .ToDictionary(kv =&gt; kv.Key, kv =&gt; (object?)kv.Value)\n        ?? new Dictionary&lt;string, object?&gt;();\n\n    await Console.Error.WriteLineAsync($\"[ProxyTool] Calling {_tool.Name}\");\n\n    try\n    {\n        var client = await _clientFactory();  \/\/ Reads settings.json\n        var result = await client.CallToolAsync(_tool.Name, args, null);\n        await Console.Error.WriteLineAsync($\"[ProxyTool] {_tool.Name} completed\");\n        return result;\n    }\n    catch (HttpRequestException ex)\n    {\n        return Error($\"Connection failed: {ex.Message}\\n\\nVerify Aspire is running.\");\n    }\n}<\/code><\/pre>\n<h4>4. Environment Variable Priority<\/h4>\n<p>Settings are resolved in priority order:<\/p>\n<ol>\n<li><code>ASPIRE_MCP_PORT<\/code> environment variable (highest priority)<\/li>\n<li><code>settings.json<\/code> file (updated by scripts)<\/li>\n<\/ol>\n<p>Same for API key:<\/p>\n<ol>\n<li><code>ASPIRE_MCP_API_KEY<\/code> environment variable<\/li>\n<li><code>settings.json<\/code> file<\/li>\n<\/ol>\n<p>This design allows flexibility: you can override settings via environment variables if needed, but the default behavior reads from the file updated by <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/start-apphost.ps1\"><code>start-apphost.ps1<\/code><\/a>.<\/p>\n<h3>Why Single-File Script is Perfect<\/h3>\n<p>Using .NET 10&#8217;s <code>dotnet run app.cs<\/code> feature makes this solution incredibly elegant:<\/p>\n<pre><code class=\"language-bash\"># No project file needed - just run the .cs file!\ndotnet run scripts\/aspire-mcp-proxy.cs<\/code><\/pre>\n<p>The <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/aspire-mcp-proxy.cs#L1-L3\"><code>#:package<\/code><\/a> directives at the top automatically restore NuGet packages:<\/p>\n<pre><code class=\"language-csharp\">#:package ModelContextProtocol@0.4.1-preview.1\n#:package Microsoft.Extensions.Hosting@10.0.0\n#:package Microsoft.Extensions.Logging@10.0.0<\/code><\/pre>\n<p>This is the power of modern .NET\u2014a complete MCP proxy in a single, readable script file!<\/p>\n<h2>Conclusion<\/h2>\n<p>Aspire + MCP gives AI agents unprecedented capabilities: they can spawn entire distributed systems and interact with them programmatically. But this power only scales when you solve the port isolation problem.<\/p>\n<p>The solution combines two layers:<\/p>\n<ol>\n<li><strong>Port allocation scripts<\/strong> that automatically find free ports<\/li>\n<li><strong>MCP proxy<\/strong> that provides indirection so AI agents don&#8217;t need to know which port to use<\/li>\n<\/ol>\n<p>The key enablers are:<\/p>\n<ol>\n<li>Scripts that allocate unique ports and save to <code>settings.json<\/code><\/li>\n<li><a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/scripts\/aspire-mcp-proxy.cs\"><code>aspire-mcp-proxy.cs<\/code><\/a> that reads <code>settings.json<\/code> dynamically<\/li>\n<li><a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/src\/NoteTaker.AppHost\/Program.cs#L56\"><code>GitFolderResolver<\/code><\/a> for unique dashboard names<\/li>\n<li><a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\/blob\/main\/src\/NoteTaker.AppHost\/Program.cs#L29\"><code>WithHttpEndpoint()<\/code><\/a> for random service ports<\/li>\n<\/ol>\n<p>While I&#8217;ve implemented this with scripts, <strong>I encourage the Aspire team to add this as a built-in feature<\/strong>. Imagine:<\/p>\n<pre><code class=\"language-bash\"># Future vision: Native Aspire CLI support\naspire run --isolated\n\n# Aspire automatically:\n# - Detects worktree context\n# - Allocates unique ports\n# - Updates MCP proxy configuration\n# - Manages cleanup on exit<\/code><\/pre>\n<p>Until then, the scripts in the <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\">worktrees-example repository<\/a> provide everything you need.<\/p>\n<p>This approach transformed my AI agent workflow from sequential (one at a time) to truly parallel (four agents simultaneously). The combination of Git worktrees + Aspire orchestration + MCP observability + port isolation is a game-changer for AI-assisted development at scale.<\/p>\n<p><strong>Example Repository:<\/strong> Check out the complete implementation at <a href=\"https:\/\/github.com\/tamirdresher\/worktrees-example\">worktrees-example<\/a> with the NoteTaker application, all management scripts, and the MCP proxy.<\/p>\n<hr \/>\n<p><em>Running multiple Aspire instances with AI agents? How are you handling MCP connections? Share your approach in the comments!<\/em><\/p>\n<h2>Related Posts<\/h2>\n<ul>\n<li>\n<p><a href=\"https:\/\/www.tamirdresher.com\/blog\/2025\/10\/20\/scaling-your-ai-development-team-with-git-worktrees.html\">Scaling Your AI Development Team with Git Worktrees<\/a> &#8211; The foundation for parallel AI development<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/www.tamirdresher.com\/blog\/2025\/11\/15\/seamless-private-npm-feeds-in-dotnet-aspire.html\">Seamless Private NPM Feeds in .NET Aspire<\/a> &#8211; Handling private packages in Aspire<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/www.tamirdresher.com\/blog\/2025\/11\/15\/give-your-ai-coding-agent-eyes-with-playwright-mcp.html\">Give Your AI Coding Agent Eyes with Playwright MCP<\/a> &#8211; Visual testing for AI agents<\/p>\n<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Scaling AI agent development with Aspire and Git worktrees by solving port conflicts through automatic port allocation scripts and an MCP proxy layer that enables parallel AI agents to orchestrate and debug complete distributed systems simultaneously.<\/p>\n","protected":false},"author":205908,"featured_media":236,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1,38],"tags":[30,26,9,29,28,14,31],"class_list":["post-238","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-aspire-category","category-contributors","tag-ai-agents","tag-apphost","tag-aspire","tag-git-worktrees","tag-mcp","tag-microservices","tag-parallel-development"],"acf":[],"blog_post_summary":"<p>Scaling AI agent development with Aspire and Git worktrees by solving port conflicts through automatic port allocation scripts and an MCP proxy layer that enables parallel AI agents to orchestrate and debug complete distributed systems simultaneously.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts\/238","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/users\/205908"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/comments?post=238"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts\/238\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/media\/236"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/media?parent=238"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/categories?post=238"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/tags?post=238"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}