Voitanos is Closing October 1, 2026
Read the announcement, how it impacts customers, and 50% off everything!
Read The Announcement
articles

Wire an MCP Apps server into the F5 debug experience of the M365 Agents Toolkit

MCP Apps debugging in the Microsoft 365 Agents Toolkit: customize VS Code tasks so F5 builds widgets, starts your MCP server, tunnels it, and launches Copilot.

Wire an MCP Apps server into the F5 debug experience of the M365 Agents Toolkit
by Andrew Connell

Last updated June 24, 2026
11 minutes read

Share this

Focus Mode

  • What F5 has to orchestrate now for MCP Apps
  • Extend the prerequisite validation
  • Start the dev tunnel before provisioning
  • Add a build step through the deploy phase
  • Start the MCP server as a background task
  • Press F5 and watch it flow
  • Troubleshooting
  • Key takeaways
  • Download Article Resources
  • Feedback & Comments

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:

  1. What are MCP Apps? Interactive Widgets in Copilot Chat
  2. Add an MCP server with UI widgets to a Microsoft 365 Copilot declarative agent project
  3. Integrate an MCP Apps server into a M365 Copilot declarative agent
  4. 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:

StageVS Code task labelWhy it matters
Validate prerequisitesValidate prerequisitesConfirms Node.js is installed and port 3001 is free before anything starts
Start a dev tunnelStart local tunnelExposes port 3001 on a public URL, writing it to the MCP_SERVER_URL environment variable
Create resourcesCreate resourcesThe existing provisioning step, which now consumes that variable
Build the projectBuild projectInstalls dependencies via a new deploy phase
Start the applicationStart applicationRuns the MCP server, with widget compilation, in the background
Launch the browserlaunch configurationNavigates 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 an F5 run, showing the Start local tunnel task, the Create resources provisioning output, and the Start backend task logging the PTO Request MCP server startup message

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.

Inline PTO request form widget rendered in the Microsoft 365 Copilot chat, with the destination pre-filled as Miami and the start date as May 29, 2026

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 developer-mode agent debug info panel expanded, showing the collect-pto-request tool call with its arguments and reported latency

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 on is 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:

  1. What are MCP Apps? Interactive Widgets in Copilot Chat
  2. Add an MCP server with UI widgets to a Microsoft 365 Copilot declarative agent project
  3. Integrate an MCP Apps server into a M365 Copilot declarative agent
  4. 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.
Andrew Connell, Microsoft MVP, Full-Stack Developer & Chief Course Artisan - Voitanos LLC.
author
Andrew Connell

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.

Feedback & Questions

newsletter

Join 12,000+ developers for news & insights

No clickbait · 100% free · Unsubscribe anytime.

    Subscribe to Andrew's newsletter for insights & stay on top of the latest news in the Microsoft 365 Space!
    blurry dot in brand primary color
    found this article helpful?

    You'll love these!

    What are MCP Apps? Interactive Widgets in Copilot Chat

    What are MCP Apps? Interactive Widgets in Copilot Chat

    June 26, 2026

    Read now

    Add an MCP server with UI widgets to a Microsoft 365 Copilot declarative agent project

    Add an MCP server with UI widgets to a Microsoft 365 Copilot declarative agent project

    June 26, 2026

    Read now

    Integrate an MCP Apps server into a Microsoft 365 Copilot declarative agent

    Integrate an MCP Apps server into a Microsoft 365 Copilot declarative agent

    June 26, 2026

    Read now

    bi-weekly newsletter

    Join 12,000+ Microsoft 365 full-stack web developers for news, insights & resources. 100% free.

    Subscribe to Andrew's newsletter for insights & stay on top of the latest news in the Microsoft 365 ecosystem!

    No clickbait · 100% free · Unsubscribe anytime.

      Subscribe to Andrew's newsletter for insights & stay on top of the latest news in the Microsoft 365 Space!