To make F5 launch a
declarative agent with MCP Apps (Model Context Protocol Apps, introduced in
part 1), you extend the Microsoft 365 Agents Toolkit’s (ATK) VS Code tasks to start a dev tunnel on the MCP server’s port before provisioning. From there, you write the tunnel URL into the MCP_SERVER_URL environment variable, install and build the project through a deploy phase, and start the MCP server as a background task.
When you’re done, one keypress takes you from cold project to a Microsoft 365 Copilot (Copilot) browser session with working widgets. This article walks through every task, in order, and explains why the order matters.
Series: Create Interactive UIs for Microsoft 365 Copilot Agents With MCP Apps
This is the final part of a multi-part series on adding MCP Apps to a declarative agent for Microsoft 365 Copilot. If you’re not sure what MCP Apps are yet, you can get an overview in the first part of the series.
The articles in this series include:
- What are MCP Apps? Interactive Widgets in Copilot Chat
- Add an MCP server with UI widgets to a Microsoft 365 Copilot declarative agent project
- Integrate an MCP Apps server into a M365 Copilot declarative agent
- Wire an MCP Apps server into the F5 debug experience of the M365 Agents Toolkit (this article)
What F5 has to orchestrate now for MCP Apps
Before MCP Apps, the local debug story for this agent was simple: validate prerequisites, provision the cloud resources (the Microsoft Entra ID app, the OAuth registration in the Teams Developer Portal, the Teams app package), and launch the browser.
The agent was pure manifests; there was nothing to run.
That’s no longer true. Our project now contains an actual Node.js application, and Copilot somehow has to reach a web server on your laptop. So the F5 sequence has to grow four new responsibilities, and the finished pipeline looks like this:
| Stage | VS Code task label | Why it matters |
|---|---|---|
| Validate prerequisites | Validate prerequisites | Confirms Node.js is installed and port 3001 is free before anything starts |
| Start a dev tunnel | Start local tunnel | Exposes port 3001 on a public URL, writing it to the MCP_SERVER_URL environment variable |
| Create resources | Create resources | The existing provisioning step, which now consumes that variable |
| Build the project | Build project | Installs dependencies via a new deploy phase |
| Start the application | Start application | Runs the MCP server, with widget compilation, in the background |
| Launch the browser | launch configuration | Navigates to Copilot and loads the agent |
If you’ve used the ATK before, you know that when you select Preview Local in Copilot (or press F5), VS Code runs the launch configuration in ./.vscode/launch.json, and that configuration names a preLaunchTask called Start Agent Locally in ./.vscode/tasks.json.
The launch configuration itself doesn’t change at all in this article; the compound still opens the browser to the Copilot URL once the pre-launch task finishes. Everything we’re doing happens in tasks.json and one project file. Here’s the finished orchestrator task, and then we’ll build each piece:
{
"label": "Start Agent Locally",
"dependsOn": [
"Validate prerequisites",
"Start local tunnel",
"Create resources",
"Build project",
"Start application"
],
"dependsOrder": "sequence"
}
dependsOrder: "sequence" matters: these run one after another, not in parallel, and you’ll see in a moment why one particular ordering decision is load-bearing.
Extend the prerequisite validation
The stock Validate prerequisites task checked Copilot access. Now we also need Node.js installed (we’re running a server) and port 3001 free (where the server listens):
{
"label": "Validate prerequisites",
"type": "teamsfx",
"command": "debug-check-prerequisites",
"args": {
"prerequisites": [
"nodejs",
"copilotAccess",
"portOccupancy"
],
"portOccupancy": [3001]
}
}
The port check is the one that pays for itself. If a previous debug session left the MCP server running, the next F5 fails right here with a clear message instead of twenty steps later with a confusing bind error from a background task you can’t see.
Start the dev tunnel before provisioning
Copilot cannot call http://localhost:3001. The ATK’s dev tunnel support fixes that by mapping a public URL onto your local port. Add this task:
{
"label": "Start local tunnel",
"type": "teamsfx",
"command": "debug-start-local-tunnel",
"args": {
"type": "dev-tunnel",
"ports": [
{
"portNumber": 3001,
"protocol": "http",
"access": "public",
"writeToEnvironmentFile": {
"endpoint": "MCP_SERVER_URL"
}
}
],
"env": "local"
},
"isBackground": true,
"problemMatcher": "$teamsfx-local-tunnel-watch"
}
The writeToEnvironmentFile block is the connective tissue of this whole article. When the tunnel comes up, the toolkit writes its public endpoint URL into MCP_SERVER_URL in ./env/.env.local. Remember from
part 3 that the MCP plugin manifest’s runtime points at ${{MCP_SERVER_URL}}/mcp; this is where that token gets its value. isBackground: true keeps the tunnel alive for the whole session without blocking the subsequent tasks.
The tunnel task must run before Create resources, or provisioning fails with a missing-environment-variable error. This is the ordering decision I flagged earlier, and it’s not a style preference. Provisioning is the step that builds the app package, and that’s when the toolkit substitutes environment variables into your manifests. If you put the tunnel after provisioning, MCP_SERVER_URL doesn’t exist yet when the substitution runs, and the build fails with a missing environment variable error. I’d love to tell you I figured that out from documentation rather than from staring at a failed build, but here we are; learn from my afternoon. And don’t worry that the MCP server itself isn’t running yet when the tunnel opens. A tunnel to a not-yet-started server is perfectly fine; we start the server at the end of the sequence and the tunnel routes to it from then on.
The existing Create resources task is unchanged; it still runs the provision lifecycle from m365agents.local.yml, creating the Entra ID app, the OAuth registration, and the Teams app package, now with our tunnel URL baked into the MCP plugin manifest.
Add a build step through the deploy phase
Next, F5 needs to make sure the project’s dependencies are installed before anything tries to build widgets or start the server. The ATK splits project automation across three lifecycles: provision (stand up cloud resources), deploy (get the code ready to run), and publish. Our project file only had a provision phase, because until now there was no code. Add a deploy phase to m365agents.local.yml:
deploy:
# Install all dependencies (single root package.json covers server + widgets)
- uses: cli/runNpmCommand
name: install package dependencies
with:
workingDirectory: .
args: install --no-audit
Then add a task in ./.vscode/tasks.json that triggers that phase, and slot it into the sequence after Create resources:
{
"label": "Build project",
"type": "teamsfx",
"command": "deploy",
"args": {
"env": "local"
}
}
The practical win is the fresh-clone experience: a teammate (or you, on a new machine) clones the repo, presses F5, and dependencies install themselves as part of the launch. No README step that says “run npm install first” for people to skip and then file an issue about. If you’re wondering why the deploy phase doesn’t also run the TypeScript build, hold that thought for one section; the dev-mode start script handles compilation itself.
Start the MCP server as a background task
The last piece is actually running the server. Two small tasks close out the sequence:
{
"label": "Start application",
"dependsOn": [
"Start backend"
]
},
{
"label": "Start backend",
"type": "shell",
"command": "npm run start:dev",
"isBackground": true,
"problemMatcher": {
"pattern": {
"regexp": "^.*$",
"file": 0,
"location": 1,
"message": 2
},
"background": {
"activeOnStart": true,
"beginsPattern": "^.*(tsx watch|rerunning).*$",
"endsPattern": "^.*PTO Request MCP server running.*$"
}
},
"presentation": {
"reveal": "silent"
}
}
The indirection (“Start application” just depends on “Start backend”) mirrors the ATK’s convention for multi-process projects; you could collapse them into one task, but keeping the shape means adding a second process later (say, a separate frontend) is a one-line change.
The command is the start:dev script from
part 2: it runs build:widgets to compile every widget folder into its single-file HTML, then starts the Express server under tsx watch (tsx here is the TypeScript-execute CLI that runs .ts files directly, not the .tsx React file extension). That’s why the deploy phase didn’t need a compile step, and it’s also your hot-reload loop: tsx compiles the TypeScript on the fly and restarts the server whenever a source file changes, replacing the old nodemon-plus-tsc arrangement with one tool. Edit a tool description mid-session and the server restarts itself; rerun npm run build:widgets when you change widget code.
If F5 hangs after the MCP server starts, the endsPattern regex doesn’t match the server’s startup log line. The problemMatcher.background block is the piece people copy without understanding, so let’s understand it. For a background task, VS Code needs to know when the task is “ready” so the launch sequence can proceed; it can’t rely on the process exiting, because the whole point is that it doesn’t exit. The beginsPattern and endsPattern are regexes matched against the task’s output: when a line matches endsPattern (our server’s “PTO Request MCP server running” startup log from part 2), VS Code considers the task ready and F5 moves on to launching the browser. If you change that console.log line in index.ts and forget this regex, F5 will hang waiting on a server that’s actually running fine, and you’ll have a very annoying twenty minutes. Match them up.
Press F5 and watch it flow
That’s the whole pipeline. Select Preview Local in Copilot (Edge) from the Run and Debug panel (or press F5) and watch the terminal: prerequisites validate, the tunnel comes up (peek at ./env/.env.local and you’ll see MCP_SERVER_URL now holds a public dev tunnel URL), provisioning acquires the Entra ID app and OAuth registration, npm installs, and the MCP server starts in a background terminal. Then the browser opens to Copilot with the agent ready.

VS Code terminal panel during F5 showing the tunnel task, provision output, and the Start backend task with the MCP server startup message
Two verification stops I always make on a first run. In the Teams Developer Portal, the app shows up with its OAuth client registration intact (the single sign-on setup for the Microsoft Graph side, untouched by everything we did in this series). And in the Microsoft Entra admin center, the agent’s app registration carries the expected Graph permissions. The MCP server itself needed neither, which is the dual-runtime separation paying off one more time.
Now test the agent: “Request PTO for a long weekend in Miami starting May 29, 2026.” The form widget renders inline with the destination and date pre-filled, exactly as built in part 2 and choreographed in part 3.

PTO Request Agent's Confirmation Form
MCP App widget collecting & confirming the user’s request
Debug your agent with developer mode
Before you go further, type developer on in the chat and do a hard refresh. In a locally launched debug session that unlocks the agent debug info panel under each response, and for MCP Apps work it’s the best observability you’re going to get: you can see that Copilot discovered both action manifests, which MCP tools it found versus which it actually called, the exact arguments passed to your tool, the latency, and the response. When a widget doesn’t render, this panel is where you find out whether the tool was never called (an instructions problem), called with bad arguments (a schema problem), or called successfully with no widget shown (a _meta/resource URI problem). I do wish the platform gave us more than this, and I’ve told Microsoft as much, but the panel covers the wiring questions that matter day to day.

Copilot chat with developer mode agent debug info expanded, showing the collect-pto-request tool call with its arguments and latency
Troubleshooting
Three failure modes account for almost every broken F5 run with this setup, and each has a recognizable symptom.
F5 fails immediately on prerequisites. Port 3001 is still bound by a prior debug session whose MCP server never shut down. Stop the orphaned process (or close the old integrated terminal) and run F5 again; the prerequisite check is doing its job by catching this early instead of failing with a confusing bind error twenty steps later.
The build fails with a missing environment variable. The Start local tunnel task is sequenced after Create resources, so MCP_SERVER_URL doesn’t exist yet when provisioning substitutes it into the plugin manifest. Move the tunnel task before provisioning, as described in Start the dev tunnel before provisioning.
F5 hangs after the MCP server starts. The endsPattern regex in the Start backend problem matcher no longer matches the server’s startup log line, so VS Code never decides the task is ready and the launch sequence stalls. Sync the regex with the actual console.log message in index.ts.
Key takeaways
- F5 for an MCP Apps project orchestrates five tasks in strict sequence: validate prerequisites, start the tunnel, provision, build, start the server, then launch the browser.
- The dev tunnel must start before provisioning. Provisioning substitutes
${{MCP_SERVER_URL}}into the plugin manifest, and the tunnel task is what writes that variable; reverse them and the build fails. - The deploy phase in m365agents.local.yml makes F5 self-sufficient by running npm install, so a fresh clone launches without manual setup.
- Background tasks need honest problem matchers. VS Code decides the server is “ready” when output matches
endsPattern, so keep that regex in sync with your server’s startup log line. developer onis your debugging lifeline. The agent debug info panel shows tool discovery, calls, arguments, and latency, which answers most “why didn’t my widget show up” questions.
Series: Create Interactive UIs for Microsoft 365 Copilot Agents With MCP Apps
This is the final part of a multi-part series on adding MCP Apps to a declarative agent for Microsoft 365 Copilot.
The articles in this series include:
- What are MCP Apps? Interactive Widgets in Copilot Chat
- Add an MCP server with UI widgets to a Microsoft 365 Copilot declarative agent project
- Integrate an MCP Apps server into a M365 Copilot declarative agent
- Wire an MCP Apps server into the F5 debug experience of the M365 Agents Toolkit (this article)
Download Article Resources
Want the resources for this article? Enter your email and we'll send you the download link.
Microsoft MVP, Full-Stack Developer & Chief Course Artisan - Voitanos LLC.
Andrew Connell is a full stack developer who focuses on Microsoft Azure & Microsoft 365. He’s a 22-year recipient of Microsoft’s MVP award and has helped thousands of developers through the various courses he’s authored & taught. Whether it’s an introduction to the entire ecosystem, or a deep dive into a specific software, his resources, tools, and support help web developers become experts in the Microsoft 365 ecosystem, so they can become irreplaceable in their organization.





