<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://nullpointer.se/feed.xml" rel="self" type="application/atom+xml" /><link href="https://nullpointer.se/" rel="alternate" type="text/html" /><updated>2026-06-03T13:02:33+00:00</updated><id>https://nullpointer.se/feed.xml</id><title type="html">The Nullpointer Blog</title><subtitle>Blog created in Jekyll, theme [Minima v2.5.1](https://github.com/jekyll/minima/blob/v2.5.1/_config.yml).</subtitle><author><name>Andreas Adner</name></author><entry><title type="html">The new MCP spec and the unfortunate deprecation of MCP Sampling</title><link href="https://nullpointer.se/new-mcp-spec.html" rel="alternate" type="text/html" title="The new MCP spec and the unfortunate deprecation of MCP Sampling" /><published>2026-05-25T00:00:00+00:00</published><updated>2026-05-25T00:00:00+00:00</updated><id>https://nullpointer.se/the-unfortunate-deprecation-of-mcp-sampling</id><content type="html" xml:base="https://nullpointer.se/new-mcp-spec.html"><![CDATA[<p><a href="/new-mcp-spec.html">
    <img src="/images/260525/splash.png" alt="The new MCP spec and the unfortunate deprecation of MCP Sampling" />
  </a></p>

<p>Greetings from beautiful Portorož in Slovenia, where I am joining <a href="https://www.linkedin.com/in/rappen/">Jonas Rapp</a> to present a session titled <em>“How and why did we implement AI Chat in FetchXML Builder?”</em> at the <a href="https://www.dynamicsminds.com/">DynamicsMinds</a> conference. We are talking about our fun collaboration from last year, where we added AI chatbot functionality to Jonas’ tool <em>FetchXML Builder</em> - one of the most widely used Power Platform community tools (even mentioned in the <a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/fetchxml/overview#community-tools">official docs</a>!). <!--end_excerpt--></p>

<p>This was a fun project, since it was an “infusion of AI” into an existing codebase, running on “legacy” technology (.NET Framework), rather than the greenfield AI projects I usually do. You can read more about my contribution in my blog post <a href="https://nullpointer.se/2025/07/29/fetchxmlbuilder-ai-anatomy.html"><em>The anatomy of FetchXML Builder with AI</em></a> from last summer.</p>

<p>So, if you are at DynamicsMinds, please come by and say hi!</p>

<p>But now, let’s put fun conferences aside, and get back to talking about AI protocols.</p>

<h2 id="new-mcp-specification">New MCP specification!</h2>
<p>The MCP specification is evolving, and a release candidate for the next version of the spec (2026-07-28) was <a href="https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/">announced</a> a couple of days ago by <a href="https://www.linkedin.com/in/dendeli">Den Delimarsky</a> (former Microsoft, now working with MCP at Anthropic) and <a href="https://www.linkedin.com/in/david-soria-parra-4a78b3a/">David Soria Parra</a> (part of the Technical Staff at Anthropic). It is said to be “the largest revision of the protocol since launch”, and it comes with a few goodies:</p>

<ul>
  <li>
    <p><strong>Stateless core</strong> - No more initialization handshakes or session IDs, which helps with load balancers, etc.</p>
  </li>
  <li>
    <p><strong>MCP Apps ships as an official extension</strong> - Great news, since MCP Apps is by far one of the most exciting parts of the MCP spec, and something that I have written about extensively lately, for example:</p>
    <ul>
      <li>A <a href="https://www.linkedin.com/posts/andreasadner_powerapps-microsoftcopilot-ugcPost-7448665788577832960-DmA-">demo</a> of how to use MCP Apps UI components in Microsoft 365 Copilot.</li>
      <li>A <a href="https://www.linkedin.com/posts/andreasadner_microsoftcopilot-ugcPost-7455742575875031041-78Xy">demo</a> of how to embed Claude Code in a terminal, as an MCP Apps agent in M365 Copilot.</li>
      <li>A <a href="https://www.linkedin.com/posts/andreasadner_microsoftcopilot-dataverse-activity-7462177225937887232-zR5Q">demo</a> of how to use <a href="https://www.linkedin.com/in/jukkaniiranen/">Jukka Niiranen’s</a> <a href="https://licensing.guide/licensing-knowledge-for-ai-agents-dataverse-capacity-mcp-server/">Dataverse Capacity MCP Server</a> from an M365 Copilot agent, with custom UI powered by MCP Apps.</li>
    </ul>
  </li>
</ul>

<p>…among many other changes. They are also introducing a <em><a href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2596">Feature Lifecycle Policy</a></em> for MCP features, which allows the maintainers of the MCP spec to formalize the lifecycle around MCP features - that can now be <strong><em>Active</em></strong>, <strong><em>Deprecated</em></strong> and <strong><em>Removed</em></strong>. A change in the lifecycle status of a feature comes with a 12-month window between deprecation and removal, to allow consumers to adapt. This is a good thing, I guess…</p>

<h2 id="the-missed-opportunity-of-the-mcp-sampling-feature">The missed opportunity of the MCP Sampling feature</h2>

<p>What is not so good is that they are also deprecating what is probably my favourite feature in the MCP Spec (well, except for MCP Apps, of course) - <a href="https://modelcontextprotocol.io/specification/2025-11-25/client/sampling">MCP Sampling</a>.</p>

<p>Despite its name, <em>MCP Sampling</em> has nothing to do with music - it is a way for MCP Server tools to “piggy back” on the MCP client’s Large Language Model, and use that LLM for completions. In the words of the specification:</p>

<p><em>The Model Context Protocol (MCP) provides a standardized way for servers to request LLM sampling (“completions” or “generations”) from language models via clients. This flow allows clients to maintain control over model access, selection, and permissions while enabling servers to leverage AI capabilities—with no server API keys necessary. Servers can request text, audio, or image-based interactions and optionally include context from MCP servers in their prompts.</em></p>

<p>So, this basically gives the MCP Server tool access to its own LLM (borrowed from the client), and it requires no AI plumbing on the MCP Server side, and no API key - the MCP client pays for the inference!</p>

<p>I have blogged and posted about this feature extensively for the last year, and explored it in various ways:</p>

<ul>
  <li>
    <p>In <a href="https://nullpointer.se/2025/08/21/mcp-automatic-report-generation.html">this blog post</a> I use MCP Sampling to allow an MCP tool to dynamically generate reports, based on user input.</p>
  </li>
  <li>
    <p>In my <a href="https://www.linkedin.com/posts/andreasadner_mcp-vscode-activity-7444081850974879745-PTE-">LinkedIn post</a> <em>“What if your MCP Server could think for itself?”</em> I explore a related MCP feature - <a href="https://modelcontextprotocol.io/seps/1577--sampling-with-tools">SEP-1577 Sampling with Tools</a> - a feature that made the Sampling capability even more powerful, by allowing LLM calls made by the tool through Sampling to also be able to call other MCP Server tools.</p>
  </li>
</ul>

<p>This could have been a real game-changer, since with the “Sampling with tools” feature the MCP Server could take over the steering-wheel and drive its own agentic loop, all on its own, using the client’s model. This inverts the typical MCP control flow: instead of the client orchestrating everything, the <em>tool itself</em> becomes an agent orchestrator - able to reason, call other MCP tools and iterate until it arrives at an answer. All while borrowing the client’s LLM, and on the client’s dime.</p>

<p>Think about what that unlocks. For example, a <code class="language-plaintext highlighter-rouge">research_topic</code> tool no longer has to return a single canned answer - it can fan out across a dozen searches, read and summarize the results, follow the promising threads, and synthesize a final report before handing anything back. A <code class="language-plaintext highlighter-rouge">fix_bug</code> tool can plan an approach, edit a file, run the tests, observe the failure, and try again. A domain-expert MCP Server - say, one that knows everything there is to know about Dataverse, FetchXML, or Power Platform licensing - can host a full agent loop <em>server-side</em>, but the user still gets to pick <em>which</em> model powers it and <em>who</em> pays for the tokens.</p>

<p>This is all really sad - it sure feels like a lost opportunity to make MCP Servers more intelligent, autonomous and agentic.</p>

<p>So why is it deprecated? <a href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577"><strong>SEP-2577: Deprecate Roots, Sampling, and Logging</strong></a> gives some answers. They list a couple of reasons for the coming death of Sampling:</p>

<ul>
  <li>
    <p><strong>Low adoption</strong> - Yes, that is true, sort of - and maybe not as true as it used to be. VS Code has had <a href="https://code.visualstudio.com/updates/v1_101#_mcp-support-for-sampling-experimental">good, although experimental support</a> for Sampling for a long time, and I have done all my experiments there. But looking at the official list of <a href="https://modelcontextprotocol.io/clients">supported MCP Clients</a>, that list is actually pretty long, nowadays.</p>
  </li>
  <li>
    <p><strong>Complex to implement</strong> - Perhaps true, but hasn’t stopped many clients from shipping it.</p>
  </li>
</ul>

<p>The deprecation wasn’t without debate, as can be seen from the comments to <a href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577">SEP-2577</a>, but I am not really seeing anyone mentioning the main benefit that I see with sampling, and that I mention above - all the agentic capabilities it opens up.</p>

<p>Even though the new MCP Spec is not set in stone, I am assuming this will be the death of MCP Sampling. Farewell, old friend - a brief, brilliant run that hinted at a more agentic future for MCP. I hate to see you go.</p>

<p>The silver lining? Thanks to that brand new <em>Feature Lifecycle Policy</em>, MCP Sampling won’t actually disappear for at least 12 months after the deprecating spec ships - so if you have never built an MCP Server that uses Sampling, now is the time to give it a try. Go grab some inference tokens on the client’s tab while you still can!</p>

<p>Until next time, happy hacking!</p>]]></content><author><name>Andreas Adner</name></author><summary type="html"><![CDATA[Greetings from beautiful Portorož in Slovenia, where I am joining Jonas Rapp to present a session titled “How and why did we implement AI Chat in FetchXML Builder?” at the DynamicsMinds conference. We are talking about our fun collaboration from last year, where we added AI chatbot functionality to Jonas’ tool FetchXML Builder - one of the most widely used Power Platform community tools (even mentioned in the official docs!).]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nullpointer.se/images/260525/splash.png" /><media:content medium="image" url="https://nullpointer.se/images/260525/splash.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The death of BizApps and the rise of the agentic Power Platform</title><link href="https://nullpointer.se/agentification-power-platform.html" rel="alternate" type="text/html" title="The death of BizApps and the rise of the agentic Power Platform" /><published>2026-05-03T00:00:00+00:00</published><updated>2026-05-03T00:00:00+00:00</updated><id>https://nullpointer.se/the-agentification-of-power-platform</id><content type="html" xml:base="https://nullpointer.se/agentification-power-platform.html"><![CDATA[<p><a href="/agentification-power-platform.html">
    <img src="/images/260503/splash.png" alt="The death of BizApps and the rise of the agentic Power Platform" />
  </a></p>

<p>Sometime last summer, it started to dawn on me that business applications as we know them might soon be a thing of the past.</p>

<p>I had been experimenting with the Model Context Protocol for a while, building custom agents that read and wrote to Dataverse directly - no UI in between. Talking to my business application in natural language, with MCP doing the plumbing, was unexpectedly delightful - and surprisingly productive. <!--end_excerpt-->If you are interested, you can find some of my early experiments with Dataverse and MCP <a href="https://www.linkedin.com/posts/andreasadner_mvp-agenticai-powerplatform-activity-7318953890094235650-vLf9">here</a> and <a href="https://www.linkedin.com/posts/andreasadner_agenticai-mcp-dataverse-activity-7319027765075165185-10Z5">here</a>.</p>

<p>It was early days, before the release of the official <a href="https://learn.microsoft.com/en-us/power-apps/maker/data-platform/data-platform-mcp">Dataverse MCP Server</a> - so I had to <a href="https://github.com/adner/SimpleDataverseMcpServer">roll my own</a> - which turned out to be surprisingly easy, especially given the fact that LLMs are really good at FetchXML 😊.</p>

<p>Accessing Dataverse from an agent in this way was fun and all, but it became pretty clear that text might not be the optimal modality for using business applications, mainly because it is painfully slow (local speech-to-text helps somewhat, see this <a href="https://www.linkedin.com/posts/andreasadner_building-my-own-crm-in-vs-code-using-mcp-activity-7427324074483302400-F52e">demo</a>). This got me thinking - what if the agent could surface user interfaces when the user needs them - in addition to text?</p>

<p>Once again, it was early days and there wasn’t much available in terms of standards or specifications for agentic user interfaces. <a href="https://docs.ag-ui.com/introduction">AG-UI</a>, <a href="https://developers.openai.com/apps-sdk">OpenAI Apps SDK</a> and <a href="https://modelcontextprotocol.io/extensions/apps/overview">MCP Apps</a>, all of which I’d later spend a lot of time exploring, hadn’t been announced yet. So, lacking official specs and tooling, I started rolling my own agent UIs.</p>

<p>Nowhere near enterprise-ready, but during this time I built a few demos that explored these ideas. <a href="https://www.youtube.com/watch?v=k5Tc3AsMBls">One of them</a> showed a bespoke agent UI that pulled in Power Apps screens when needed, and rendered reports dynamically from natural language:</p>

<p><img src="/images/260503/1.gif" alt="" /></p>

<p>We started showing this to customers, and the response was simply overwhelming. <em>“We want this!”</em> they said after nearly every demo - the idea that you could just <em>ask</em> for what you needed and have the right UI appear, instead of clicking through menus and tabs, clearly struck a chord.</p>

<p>But there was a problem: the technology simply wasn’t there. These were just demos, and even though the benefits of agent UIs were clear, shipping this to customers felt a long way off.</p>

<h2 id="some-specs-finally">Some specs, finally</h2>

<p>But then last autumn, things started happening. First, quietly on the open-source side, <a href="https://github.com/MCP-UI-Org/mcp-ui">MCP-UI</a> showed up - an early attempt at formalizing how agent UIs could be served over MCP (<a href="https://www.linkedin.com/posts/andreasadner_%F0%9D%90%8D%F0%9D%90%9E%F0%9D%90%B0-%F0%9D%90%AF%F0%9D%90%A2%F0%9D%90%9D%F0%9D%90%9E%F0%9D%90%A8-using-mcp-ui-to-add-activity-7366158904004685825-GVsw?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w">here’s my demo</a> from August). Then the frontier labs followed:</p>

<ul>
  <li><a href="https://developers.openai.com/apps-sdk">OpenAI Apps SDK</a> - announced in October.</li>
  <li><a href="https://modelcontextprotocol.io/extensions/apps/overview">MCP Apps</a> - announced in November, as an extension to the MCP protocol from Anthropic.</li>
</ul>

<p>MCP Apps was directly inspired by MCP-UI, so credit to the MCP-UI team for paving the way.</p>

<p>Going back a bit further - last spring, a small, then-unknown company called CopilotKit released <a href="https://docs.ag-ui.com/introduction">AG-UI</a>, the spec that, more than any other, piqued my interest in agent UIs. I’ve since explored it in depth, including a session <a href="https://globalai.community/chapters/stockholm/events/agentcon-stockholm/">at AgentCon</a> (<a href="https://www.youtube.com/watch?v=PeEE5kYwdgo">video summary</a>) and in a <a href="https://www.youtube.com/watch?v=QOlQAEAhIZI">conversation with Rasmus Wulff Jensen</a>. For deeper dives, see my LinkedIn writeups <a href="https://www.linkedin.com/posts/andreasadner_dynamic-ai-user-interfaces-with-microsoft-activity-7398088200042315776-ygSA?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w">here</a> and <a href="https://www.linkedin.com/posts/andreasadner_copilotkit-version-15-microsoft-agent-activity-7413675309042147328-dk2Q?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w">here</a>.</p>

<p>One specific concept in AG-UI - <a href="https://docs.ag-ui.com/concepts/state">Shared state</a> between the UI and the agent orchestrator, and a core part of the AG-UI spec - is, in my view, the single biggest unlock for agent UIs, and one piece that is still missing as support for agent UIs finally lands in mainstream Microsoft tools. Shared state unlocks some really transformative user experiences, such as these:</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/PeEE5kYwdgo?si=hJr29_XzMX8gZZfD" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>Suddenly, the demos from a year earlier didn’t look quite so far-fetched - the pieces in the agentic UI puzzle were starting to come together.</p>

<h2 id="the-death-of-bizapps">The death of BizApps</h2>
<p>So while agent UI standards started emerging at the end of last year, support for them was still lacking in the Microsoft ecosystem. Our customers were eager to start trying these experiences in Microsoft Teams and M365 Copilot, but as it turned out - they would have to wait a bit longer.</p>

<p>But inside Microsoft, these kinds of ideas had apparently been brewing for quite some time. Already back in December 2024, on the <a href="https://www.bg2pod.com/">BG2 podcast</a> Satya Nadella famously predicted the eventual death of SaaS applications as we know them, in favor of AI orchestration layers that serve agents directly.</p>

<p>Charles Lamanna was even blunter. In a <a href="https://www.madrona.com/the-end-of-biz-apps-ai-agility-and-the-agent-native-enterprise-from-microsoft-cvp-charles-lamanna/">May 2025 conversation with Madrona</a> he said:</p>

<p><em>“As the guy at Microsoft who works on business applications, sometimes the truth hurts, but business apps as we know it are indeed dead … Instead, what will probably happen is you’ll see this ossification of these classic biz apps, the emergence of this new AI layer, which is very focused around automation and completing tasks in a way that extends the team of humans and people with these AI agents that go and do work ..  You’re going to have a generative UI, which AI dynamically authors and renders on the fly to exactly match what the person’s trying to do .. The gist of it is yes, indeed, biz apps, the age of biz apps is over.”</em></p>

<h2 id="the-coming-war-of-the-ai-capabilities-layer">The coming war of the AI Capabilities Layer</h2>
<p>And it’s not just the apps. At the Power Platform Community Conference 2025 in Las Vegas, Lamanna delivered the line that lit up the low-code community: <em>“Low code is dead, as we know it.”</em> That caused quite a stir - but seven months on, it looks less like the low-code frameworks themselves are dying, and more like the way we <em>use</em> them is changing. Instead of clicking around a designer to build a low-code app, we’re letting our agents build them using a CLI. See for example <a href="https://www.linkedin.com/posts/andreasadner_copilotstudio-activity-7452038499995799552-GtfN?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w">this demo</a> of Copilot Studio agents built with Claude Code and agent skills. Honestly, I didn’t see that one coming.</p>

<p>Another harbinger of things to come - and one I have quoted repeatedly - is Lane Swenka’s <a href="https://devblogs.microsoft.com/powerplatform/power-platform-api-and-sdks-from-ux-first-to-api-first/">blog post</a> from last summer, laying out a new direction for Power Platform admin tools: a transition from UX-first to API-first. The scope was seemingly limited to admin tooling, but in hindsight it was pretty clear that it pointed to something bigger - an API-first shift across the whole Power Platform, to cater for the needs of AI agents.</p>

<p>Recently, this <a href="https://www.linkedin.com/pulse/capabilities-layer-todd-trotter-girqc/">article</a> by <a href="https://www.linkedin.com/in/todd-trotter-01818030/">Todd Trotter</a> about the <em>Capabilities Layer</em> - the architecture and plumbing needed to serve AI agents - is one of the best summaries I’ve read of where all this is heading, and what it means for the Power Platform.</p>

<p><em>“AI needs applications to describe what they can do, what their constraints are, and what state they are operating against. They need a surface that is designed to be composed … That surface is the <strong>Capabilities Layer</strong>. It sits between your application services and any consumer - human, agent, or orchestrator - and it exposes typed tools, structured data resources, and interactive UX components through MCP.”</em></p>

<p>One concept in particular stands out - the idea of “micro-frontends”:</p>

<p><em>“Micro-frontends apply service-style decomposition to the user interface by splitting the frontend into independently developed and deployed features or components that are composed into a larger experience.”</em></p>

<p>Some corporate-speak, sure - but he’s basically saying the same thing as Charles Lamanna was saying: the user experiences of the future will surface when they’re needed, with just the UI elements the user needs, no more, no less. We’re heading toward a world where dynamically surfaced, agent-centric <em>“micro experiences”</em> are the norm.</p>

<p>So the vision from Microsoft is pretty clear: agentic user experiences FTW, and the infrastructure, APIs and MCPs to support them. And they’re not the only ones reaching this conclusion - Salesforce’s recent <a href="https://www.salesforce.com/news/stories/salesforce-headless-360-announcement/">Headless 360 announcement</a> is the same play, and SAP, ServiceNow and the rest are also doing it. The platforms are being (re)built to be consumed by an army of autonomous agents, and the human-facing UIs - when they’re needed - will be dynamic and served by the agents.</p>

<p>The writing is on the wall; this is the war that’s coming. And when the dust settles, the winner will be the provider with the best <strong>Capabilities Layer</strong> for agents - and the best support for agent UIs.</p>

<p>But vision is one thing; execution is another. With the coming clash of the titans to build the best capabilities layer as backdrop, let’s look at what Microsoft has actually shipped lately in this space.</p>

<h2 id="the-rise-of-the-agentic-power-platform">The rise of the agentic Power Platform</h2>
<p>In all honesty, being passionate about agent user experiences and at the same time being a consultant in the Microsoft space last year wasn’t all that great. With the explosion of specifications in the agent UI space we saw late last year, I had high hopes that these capabilities would be promptly made available in the Microsoft tech stacks. Perhaps support for MCP Apps for Copilot Studio agents? Or why not an iteration on the Adaptive Card spec to support dynamic user interfaces? Or maybe AG-UI support in M365 Copilot? One could only dream…</p>

<p>But Ignite 2025 came and went without any real announcements in this space, so I kept waiting and hoping, hoping and waiting. And good things come to those who wait, because on the 9th of March 2026, somewhat hidden in the Microsoft <a href="https://techcommunity.microsoft.com/blog/microsoft365copilotblog/enable-agents-to-bring-apps-into-the-flow-of-work%E2%80%94while-keeping-it-in-control/4499464">365 Copilot Blog</a> was this little nugget:</p>

<p><img src="/images/260503/image.png" alt="alt text" /></p>

<p>Support for OpenAI Apps SDK was already available, with MCP Apps coming soon. I found some <a href="https://github.com/microsoft/mcp-interactiveUI-samples">really cool samples</a> which I used to create <a href="https://www.linkedin.com/posts/andreasadner_copilot-mcp-activity-7441598020539908097-_JA6?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w">my own demo</a>:</p>

<iframe src="https://www.linkedin.com/embed/feed/update/urn:li:ugcPost:7441597583669538816?collapsed=1" height="541" width="504" frameborder="0" allowfullscreen="" title="Embedded post"></iframe>

<p>This feature was part of a capability exclusive to Microsoft 365 Copilot <strong>declarative agents</strong>, marketed as “interactive UI widgets”, part of “MCP plugin actions” (more info <a href="https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/declarative-agent-ui-widgets">here</a>).</p>

<p>For some reason, all these new fancy capabilities related to support for agentic UI protocols were implemented for <em>declarative agents</em>, a technology I hadn’t tried before - I had mostly dabbled with Copilot Studio agents and agents built with custom code (referred to as <a href="https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/overview-custom-engine-agent"><em>custom engine agents</em></a> in the M365 Copilot nomenclature). So this gave me the chance to explore declarative agents, which was pretty interesting.</p>

<p>Then, it was announced that it was possible to use <a href="https://learn.microsoft.com/en-us/power-apps/user/use-microsoft-365-copilot-model-driven-apps">M365 Copilot agents in a Power App</a>. At first glance, the benefit of this is to run the main M365 Copilot agent from a Power App - which is already grounded in your organization’s data, but which now also indexes the data in your Power App environment. It’s <a href="https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/add-ai-copilot">not the first time</a> we have seen Copilots in Dynamics/Power Apps, but this time it seems to actually work pretty great:</p>

<p><img src="/images/260503/3.gif" alt="" /></p>

<p>A <a href="https://learn.microsoft.com/en-us/power-apps/user/use-microsoft-365-copilot-model-driven-apps#limitations">limitation</a> of this “standard” M365 Copilot agent is that it can’t really “do” anything in Dataverse - it can read data just fine, but it can’t modify anything.</p>

<p>To remedy this, you can also <a href="https://learn.microsoft.com/en-us/power-apps/user/use-microsoft-365-copilot-model-driven-apps#use-agents-in-microsoft-365-copilot">access your custom agents</a> (declarative, Copilot Studio or custom engine agent) from M365 Copilot in your Power App, and this is where it starts to become really cool! I created a <a href="https://www.linkedin.com/posts/andreasadner_microsoftcopilot-activity-7444647079043289089-HcpT?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w">demo</a> of this, once again using the Microsoft samples. This means that if your custom agent is equipped with e.g. the Dataverse MCP Server, it can update data in Dataverse. One big limitation currently is that the custom agent has no context - it doesn’t know which Power App/form/view/record it is using:</p>

<p><img src="/images/260503/image-1.png" alt="alt text" /></p>

<p>I hope that this will change soon - context-awareness for custom agents is such an unlock that it should have been implemented yesterday…</p>

<p>Next was the <a href="https://www.microsoft.com/en-us/power-platform/blog/power-apps/public-preview-your-business-apps-now-part-of-every-conversation/">announcement</a> that your Power App could <a href="https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/enable-your-app-copilot">expose its own MCP Server</a>, which came with a set of custom MCP Apps UI tools. You could then download it as a declarative Copilot agent package, which you could deploy to your tenant. I created a <a href="https://www.linkedin.com/posts/andreasadner_dataverse-powerapps-microsoftcopilot-activity-7445817384805916672-E9Ue?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w">demo</a> of this as well:</p>

<iframe src="https://www.linkedin.com/embed/feed/update/urn:li:ugcPost:7445718741906137089?collapsed=1" height="541" width="504" frameborder="0" allowfullscreen="" title="Embedded post"></iframe>

<p>Then came the <a href="https://www.microsoft.com/en-us/power-platform/blog/2026/04/22/custom-tools-and-rich-ui-for-app-based-conversations-are-now-in-public-preview/">announcement</a> that custom tools could be added to your Power Apps, which became part of the MCP plugin actions when you exported the Power App as a declarative agent. Demo <a href="https://www.linkedin.com/posts/andreasadner_microsoftcopilot-powerplatform-activity-7452963921768067073-S9ns?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w">here</a>.</p>

<p>And then on April 7 came the <a href="https://devblogs.microsoft.com/microsoft365dev/mcp-apps-now-available-in-copilot-chat/">big one</a> - <a href="https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/declarative-agent-ui-widgets">support for MCP Apps</a> in declarative Copilot agents! Since I had <a href="https://www.linkedin.com/posts/andreasadner_building-my-own-crm-in-vs-code-using-mcp-activity-7427324074483302400-F52e">already created</a> an MCP Server with some cool-looking MCP Apps UI components, I simply added them to a declarative agent which resulted in my most successful <a href="https://www.linkedin.com/posts/andreasadner_powerapps-microsoftcopilot-activity-7448784585456357376-X9bX?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w">LinkedIn post</a> to date:</p>

<iframe src="https://www.linkedin.com/embed/feed/update/urn:li:ugcPost:7448665788577832960?collapsed=1" height="754" width="504" frameborder="0" allowfullscreen="" title="Embedded post"></iframe>

<p>And there is more - for example, the <a href="https://learn.microsoft.com/en-us/power-apps/user/supervise-agents-with-agent-feed">agent feed</a> capability in Power Apps (demo <a href="https://www.linkedin.com/posts/andreasadner_powerapps-copilotstudio-activity-7454575199213170688-vAIM?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w">here</a>) and the ability to use your own <a href="https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/build-api-plugins-local-office-api">custom agents in Microsoft 365 productivity apps</a>, such as Excel and Word (demo <a href="https://www.linkedin.com/posts/andreasadner_powerapps-microsoftcopilot-activity-7451321150292480001-G59w?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w">here</a>).</p>

<p>So, it feels like Microsoft is firing on all cylinders when it comes to agentic UIs, at least when it comes to <strong>declarative</strong> Copilot agents. The state of things has improved enormously since last year. So, kudos to everyone at Microsoft, and while we’re at it - here is my wishlist:</p>

<ul>
  <li>
    <p>Pretty please with sugar on top - make it possible for custom agents to be aware of their Power Apps context. Please?</p>
  </li>
  <li>
    <p>Let’s make it possible for all Copilot agents to use MCP Apps, not just declarative agents. Here’s an idea - make MCP Apps support a general functionality in M365 Copilot, that works for all agent types - declarative, Copilot Studio and custom engine agents. And perhaps in Teams too?</p>
  </li>
  <li>
    <p>Add a way to limit the number of user approvals needed when using MCP action plugins in declarative Copilot agents. There is so much clicking. 😉</p>
  </li>
  <li>
    <p>Implement some smart way of providing context to an agent UI that is synchronized with the agent backend. Similar to the context management feature in AG-UI. As stated above, this is the one thing that more than anything else unlocks the possibility to build real agentic applications.</p>
  </li>
</ul>

<h2 id="whats-next">What’s next?</h2>

<p>So, we have talked about the agentic UI layer. But what about the AI Capabilities Layer that will power the rise of the autonomous agentic workforce? It seems to me that Microsoft is currently consolidating this capabilities layer under the Work IQ umbrella, at least when it comes to Microsoft 365 and Dataverse. A set of <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/tooling-servers-overview">MCP Servers</a> is available that could allow agents to interact with Microsoft services on behalf of the user, and eventually also autonomously - under their own agentic identity. I have <a href="https://nullpointer.se/declarative-agents-workiq.html">played around</a> with the Work IQ MCP Servers, and they are pretty neat.</p>

<p>But who is winning? On agent UIs, Microsoft has caught up impressively fast in just a few months. On the Capabilities Layer it might be a different game - one where the other big firms are moving in parallel and fast. One can only assume that massive efforts are underway internally at Microsoft to build out this Capabilities Layer and respond in force to Salesforce Headless 360, and the others. All the major players have reached the same conclusion - that the race for the AI capability stack is the race for the next era of enterprise software, and there are no prizes for second place. Game on!</p>

<p>So, are BizApps dead? Yes, it seems so. A year has passed, and I’d say it’s even clearer than it seemed back then. It’s still early days, but in a matter of months we could have dynamically generated agentic user interfaces and a mature capabilities layer that can power our agents, and honestly - who needs a business application then?</p>

<p>As always, thanks for reading and until next time - happy hacking!</p>]]></content><author><name>Andreas Adner</name></author><summary type="html"><![CDATA[Sometime last summer, it started to dawn on me that business applications as we know them might soon be a thing of the past. I had been experimenting with the Model Context Protocol for a while, building custom agents that read and wrote to Dataverse directly - no UI in between. Talking to my business application in natural language, with MCP doing the plumbing, was unexpectedly delightful - and surprisingly productive.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nullpointer.se/images/260503/splash.png" /><media:content medium="image" url="https://nullpointer.se/images/260503/splash.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Declarative Agents with Work IQ MCP Servers</title><link href="https://nullpointer.se/declarative-agents-workiq.html" rel="alternate" type="text/html" title="Declarative Agents with Work IQ MCP Servers" /><published>2026-04-06T00:00:00+00:00</published><updated>2026-04-06T00:00:00+00:00</updated><id>https://nullpointer.se/declarative-agent-work-iq</id><content type="html" xml:base="https://nullpointer.se/declarative-agents-workiq.html"><![CDATA[<p><a href="/declarative-agents-workiq.html">
    <img src="/images/260406/splash.png" alt="Declarative Agents with Work IQ MCP Servers" />
  </a></p>

<p>It has been a while since I last looked into <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/overview">Agent 365 SDK</a>, and since then a couple of interesting things have happened. <!--end_excerpt--></p>

<p>First of all, a release date has been announced for <a href="https://partner.microsoft.com/en-US/blog/article/agent-365-announcement">Microsoft 365 E7</a>, a new type of license that includes Agent 365 capabilities. It is worth noting that the parts of Agent 365 that have to do with agentic identities and autonomous agentic users will not be GA on 1st of May, and licensing details regarding this are yet to be announced. This is a bit of a bummer, since I have spent a lot of time the last few months exploring the agentic parts of Agent 365 (see for example <a href="https://nullpointer.se/exploring-agent-365-cli.html">here</a> and <a href="https://nullpointer.se/agent-365-notifications.html">here</a>), and it would have been cool to be able to test it in production. But for the time being, agentic users stays in the <a href="https://www.microsoft.com/en-us/microsoft-365-copilot/frontier-program">Frontier program</a> and we’ll have to wait a bit longer until the agentic bits become generally available. Good things to those who wait!</p>

<p>The parts of Agent 365 that go GA in May are all about agents acting “on-behalf-of” the user, as well as a lot of agent governance goodies. Check out <a href="https://www.youtube.com/watch?v=Mszz9ntbVpc">this</a> Agent 365 AMA for a deep-dive, and a good explanation of what is coming in May.</p>

<p>Another thing that has happened is that the <strong><em>Agent 365 tooling servers</em></strong> have been rebranded to <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/tooling-servers-overview">Work IQ MCP</a>. <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/tooling-servers-overview">This</a> is how Work IQ is described by Microsoft:</p>

<p><em>“Work IQ is the intelligence layer that grounds Microsoft 365 Copilot and your agents in real-time, shared context across your organization.”</em></p>

<p>So from a product/marketing perspective I guess it makes sense to “move” the MCP Servers from Agent 365 to Work IQ, as it positions Agent 365 as being all about agent governance, while Work IQ deals with the data.</p>

<h3 id="the-work-iq-mcp-servers">The Work IQ MCP Servers</h3>

<p>Since Ignite 2025 I have done a lot of demos and blog posts about the Agent 365 MCP servers (now called Work IQ MCP), for example:</p>

<ul>
  <li>
    <p>Two blog posts describing how to use the MCP Servers from a custom agent - <a href="https://nullpointer.se/agent-365-mcp-servers-part-1.html">part 1</a> and <a href="https://nullpointer.se/agent-365-mcp-servers-part-1.html">part 2</a>.</p>
  </li>
  <li>
    <p>A <a href="https://nullpointer.se/exploring-agent-365-cli.html">post</a> about how to use an (at that time) undocumented feature for creating custom MCP Servers in Agent 365.</p>
  </li>
  <li>
    <p>A <a href="https://nullpointer.se/agent-365-notifications.html">post</a> about how to let an agent respond to Word comments and send emails using the Agent 365 notification functionality.</p>
  </li>
</ul>

<p>Lately, it seems that these MCP servers have gotten much easier to access and to use, something that I explored in a <a href="https://www.linkedin.com/posts/andreasadner_workiq-agent365-activity-7445431916155240448-UByj?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w">recent LinkedIn post</a> where I tried out the servers from VS Code.</p>

<p>It has really started to felt like the tooling for agents in the Microsoft ecosystem is starting to come together now, and with the Work IQ MCP Servers we have pretty much everything we need to do work in Microsoft 365 work from an agent. Exciting times, for sure!</p>

<p>The Work IQ MCP Servers are still <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/tooling-servers-overview">documented</a> as part of the Agent 365 docs. At the time of writing, these are the available MCP Servers:</p>

<ul>
  <li>Work IQ Copilot</li>
  <li>Work IQ User</li>
  <li>Work IQ Mail</li>
  <li>Work IQ Calendar</li>
  <li>Work IQ Teams</li>
  <li>Work IQ Word</li>
  <li>Work IQ OneDrive</li>
  <li>Work IQ SharePoint</li>
  <li>Dataverse MCP Server</li>
  <li>Microsoft MCP management MCP server</li>
</ul>

<p>When it comes to data retrieval, the <strong>Work IQ Copilot</strong> MCP Server is the “swiss army knife” that allows for retrieval of pretty much all your M365 data. It has only one tool - <strong>copilot_chat</strong> - and the description is pretty telling:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Use this tool for any user request that might require finding, searching, discovering, or locating information contained within Microsoft 365 content—including documents, PDFs, spreadsheets, emails, sites, reports, or files—regardless of whether the question appears general or domain-specific.

If the user's request could plausibly be answered using organizational content, you must invoke this tool unless a workload-specific tool exists for that scenario. When no dedicated tool (mail, calendar, Teams, OneDrive, SharePoint, etc.) is available, this tool becomes the primary mechanism for retrieval.

If the request mentions a specific workload and its tool is unavailable, you might use this search tool as a fallback. If the search tool can't retrieve the information, clearly state that the information isn't accessible from the available tools instead of answering from general knowledge.
</code></pre></div></div>

<p>So this truly is (or should be) the “one MCP Server to rule them all”, at least when it comes to data retrieval. When it comes to <em>updating</em> data it is a different story, and that is what the rest of the MCP Servers are for. More on that later.</p>

<h3 id="so-much-cool-stuff-from-redmond">So much cool stuff from Redmond</h3>
<p>Just the last couple of weeks so much cool stuff has come out from Microsoft, that it has honestly been hard to keep up. If you follow me on LinkedIn, you probably know that I have spent a lot of time the last six months exploring various specifications for agentic user interfaces, such as <a href="https://docs.ag-ui.com/">AG-UI</a>, <a href="https://a2ui.org/">A2UI</a> and <a href="https://modelcontextprotocol.io/extensions/apps/overview">MCP Apps</a>. Lately, it has been really exciting to see these specs have finally made their way into the Microsoft ecosystem:</p>

<ul>
  <li>
    <p>On 9th of March it was <a href="https://techcommunity.microsoft.com/blog/microsoft365copilotblog/enable-agents-to-bring-apps-into-the-flow-of-work%E2%80%94while-keeping-it-in-control/4499464">announced</a> that MCP Apps and OpenAI Apps SDK would be available for <a href="https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/declarative-agent-ui-widgets">Copilot declarative agents</a>. I posted a <a href="https://www.linkedin.com/posts/andreasadner_copilot-mcp-activity-7441598020539908097-_JA6">video to LinkedIn</a> where I ran Doom in a Copilot agent, by serving it as a OpenAI Apps SDK resource from an MCP Server.</p>
  </li>
  <li>
    <p>Then it was <a href="https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/customize-microsoft-365-copilot-chat">announced</a> that a custom Copilot agent could be used from a model-driven Power App, which I also <a href="https://www.linkedin.com/posts/andreasadner_microsoftcopilot-activity-7444647079043289089-HcpT">tried out</a>.</p>
  </li>
  <li>
    <p>And if that was not enough, it was <a href="https://www.microsoft.com/en-us/power-platform/blog/power-apps/public-preview-your-business-apps-now-part-of-every-conversation/">announced</a> that a model-driven Power App could be exposed as an MCP Server that could be used from a <a href="https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/app-properties#steps-to-set-up-power-apps-in-copilot">declarative Copilot agent</a> - and that also served some custom UIs! I created a <a href="https://www.linkedin.com/posts/andreasadner_dataverse-powerapps-microsoftcopilot-activity-7445817384805916672-E9Ue">demo of this</a> also.</p>
  </li>
</ul>

<p>It seems that lately all this awesomeness has come to <a href="https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/overview-declarative-agent">Copilot Declarative agents</a>. And the single thing that enables all of the coolness is the ability to <a href="https://learn.microsoft.com/sv-se/microsoft-365/copilot/extensibility/build-mcp-plugins">use MCP Servers</a> from a declarative agent.</p>

<p>What is a declarative agent? Well, basically it is a no-code agent that can be put together <em>declaratively</em> (through lots of bundled files that describe the agent capabilities). A suggested way of doing this is to use the <a href="https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/overview-agents-toolkit?toc=%2Fmicrosoftteams%2Fplatform%2Ftoc.json&amp;bc=%2Fmicrosoftteams%2Fplatform%2Fbreadcrumb%2Ftoc.json">Microsoft 365 Agents Toolkit</a>, a VS Code extension that makes it possible to create such agents in a simple way.</p>

<p>So, since it is possible to use MCP Servers from a declarative agent, then we must of course try to use the Work IQ MCP Servers from a declarative agent!</p>

<h3 id="using-work-iq-mcp-servers-from-a-microsoft-365-copilot-declarative-agent">Using Work IQ MCP servers from a Microsoft 365 Copilot declarative agent</h3>
<p>First of all, we need a way of finding out the metadata for the Work IQ MCP Servers, so we can add them to our declarative agent. Haven’t found this in the docs yet, but deeply hidden in this <a href="https://github.com/microsoft/work-iq/pull/81/changes#diff-2751ce6cddba440258973781929f26eb1e448020009dc6573318781aac7afba9">PR</a> we can find a file that contains URLs to the various Work IQ MCP Servers. Example:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="nl">"WorkIQ-Me-MCP-Server"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://agent365.svc.cloud.microsoft/agents/tenants/&lt;TENANT_ID&gt;/servers/mcp_MeServer"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http"</span><span class="w">
      </span><span class="err">...</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Replace <code class="language-plaintext highlighter-rouge">&lt;TENANT_ID&gt;</code> with your own tenant ID, and you have the MCP Server URL.</p>

<p>So, now that we have the MCP Server URLs, let’s try to add them to a declarative agent using the <strong>Microsoft 365 Agents Toolkit</strong> extension in VS Code. Install the extension, click the “M365” button and hit “Create a new agent/app”:</p>

<p><img src="/images/260406/image.png" alt="alt text" /></p>

<p>Then, select “Declarative agent” -&gt; “Add an action” -&gt; “Start with an MCP Server”, and then add the URL to the MCP Server. This scaffolds the declarative agent project, and adds your MCP Server to the <code class="language-plaintext highlighter-rouge">mcp.json</code> file:</p>

<p><img src="/images/260406/image-1.png" alt="alt text" /></p>

<p>Click “Start” to run the MCP Server. After authenticating, you can see that the MCP Server is connected and the tools (5 of them, in this case) are loaded:</p>

<p><img src="/images/260406/image-2.png" alt="alt text" /></p>

<p>Then, click “ATK: Fetch action from MCP” to add the MCP Server tools to the <code class="language-plaintext highlighter-rouge">ai-plugin.json</code> file. Select all tools that you want to add:</p>

<p><img src="/images/260406/image-3.png" alt="alt text" /></p>

<p>For auth, select “OAuth (with static registration)”:</p>

<p><img src="/images/260406/image-4.png" alt="alt text" /></p>

<p>You will see an error message saying <code class="language-plaintext highlighter-rouge">Unable to find the authentication metadata in the MCP server</code>, which you can disregard for now. The Work IQ MCP Servers unfortunately don’t publish authorization config information even though they <a href="https://modelcontextprotocol.info/specification/draft/basic/authorization/#21-overview">SHOULD</a>, so we will add this info manually later (for a previous rant about the Agent 365 MCP Server not being MCP spec compliant, see this <a href="https://nullpointer.se/agent-365-mcp-servers-part-1.html">post</a>).</p>

<p>Now two things have happened:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ai-plugin.json</code> has been updated with descriptions for all the tools that are exposed by the added MCP Server.</li>
  <li><code class="language-plaintext highlighter-rouge">m365agents.yml</code> has been updated with infra provisioning information.</li>
</ul>

<p>Now it is time to add the missing authorization information to the <code class="language-plaintext highlighter-rouge">m365agents.yml</code> file. Add the following rows:</p>

<p><img src="/images/260406/image-5.png" alt="alt text" /></p>

<p><strong>Make sure to replace the GUID:s with your Tenant ID!</strong></p>

<p>Next, we need to setup the infra necessary to make it possible to do OAuth2 authentication with the MCP Servers. How to accomplish this for declarative agent MCP plugins is described <a href="https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/api-plugin-authentication">here</a> in the docs.</p>

<p>First, we need to add an App Registration to Entra ID. Create the app registration and:</p>

<ul>
  <li>
    <p>Create a <strong>web</strong> redirect URL that points to: <code class="language-plaintext highlighter-rouge">https://teams.microsoft.com/api/platform/v1.0/oAuthRedirect</code></p>
  </li>
  <li>
    <p>Make a note of the app registration ID.</p>
  </li>
  <li>
    <p>Create an app registration secret and make a note of it.</p>
  </li>
</ul>

<p>Then, add API permissions for the Work IQ MCP Servers that you intend to use in your declarative agent, and grant admin consent for these.:</p>

<p><img src="/images/260406/image-6.png" alt="alt text" /></p>

<p>Back in VS Code, click “Provision” in the “M365 Agents Toolkit” pane:</p>

<p><img src="/images/260406/image-7.png" alt="alt text" /></p>

<p>In the dialogs that are shown, enter:</p>

<ul>
  <li>
    <p>The ID for the application registration that you created earlier.</p>
  </li>
  <li>
    <p>The Client Secret that you created earlier.</p>
  </li>
  <li>
    <p>The MCP Server scope (same as you added to the <code class="language-plaintext highlighter-rouge">m365agents.yml</code> file, for example <code class="language-plaintext highlighter-rouge">https://agent365.svc.cloud.microsoft/agents/tenants/&lt;YOUR TENANT ID&gt;/servers/mcp_MeServer/.default</code>)</p>
  </li>
</ul>

<p>Now M365 Agent Toolkit will create the infra for your agent, and when completed you can find the agent in the Teams Dev Portal: <a href="https://dev.teams.microsoft.com/">https://dev.teams.microsoft.com/</a>. There, you can also see that OAuth client registrations have been created for all the MCP Servers that you added. This allows you to authenticate against the Work IQ MCP Servers when using the agent:</p>

<p><img src="/images/260406/image-8.png" alt="alt text" /></p>

<p>You can now try your agent by opening it up in the Teams Dev Portal, and clicking “Preview in Teams”:</p>

<p><img src="/images/260406/image-9.png" alt="alt text" /></p>

<p>You should now have a working declarative agent that uses the Work IQ MCP Servers!</p>

<p>In the video below I have wired up the agent with the <code class="language-plaintext highlighter-rouge">mcp_MeServer</code>, <code class="language-plaintext highlighter-rouge">mcp_CalendarTools</code>, <code class="language-plaintext highlighter-rouge">mcp_MailTools</code> and <code class="language-plaintext highlighter-rouge">mcp_M365Copilot</code> Work IQ MCP Servers.</p>

<p>This has been fun, declarative agents and Work IQ MCP Servers are pretty awesome. The Agent 365 Toolkit does a good job of provisioning the infrastructure for it all, which make things much simpler.</p>

<p>Thanks for reading, and until next time - happy hacking!</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/AdhkoSNJPvk?si=22RUVRK0AGuaOwAZ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>]]></content><author><name>Andreas Adner</name></author><summary type="html"><![CDATA[It has been a while since I last looked into Agent 365 SDK, and since then a couple of interesting things have happened.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nullpointer.se/images/260406/splash.png" /><media:content medium="image" url="https://nullpointer.se/images/260406/splash.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Dataverse MCP Server revisited, agent context reduction strategies and the future of BizApps</title><link href="https://nullpointer.se/dvmcpserver-context-reduction.html" rel="alternate" type="text/html" title="Dataverse MCP Server revisited, agent context reduction strategies and the future of BizApps" /><published>2026-02-08T00:00:00+00:00</published><updated>2026-02-08T00:00:00+00:00</updated><id>https://nullpointer.se/dataverse-mcp-server-revisited</id><content type="html" xml:base="https://nullpointer.se/dvmcpserver-context-reduction.html"><![CDATA[<p><a href="/dvmcpserver-context-reduction.html">
    <img src="/images/260208/splash.png" alt="Dataverse MCP Server revisited, agent context reduction strategies and the future of BizApps" />
  </a></p>

<p>It has been a while since I last tried the Dataverse MCP Server. Last summer, when it was in preview, I experimented with it extensively and posted my experiences to LinkedIn and to my blog:</p>

<ul>
  <li>
    <p>In <a href="https://www.linkedin.com/posts/andreas-adner-70b1153_dataversemcpserver-activity-7344003681740075008-W3x4?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w">this LinkedIn post</a>, I showed how the Dataverse MCP Server can be used from Claude Code, Claude Desktop and Gemini CLI. <!--end_excerpt--></p>
  </li>
  <li>
    <p>In <a href="https://www.linkedin.com/posts/andreas-adner-70b1153_dataverse-mcp-server-running-from-excel-activity-7345177569844953088-H3Y9?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w">this</a> fun experiment, I demonstrated how the Dataverse MCP Server can be used from anywhere - in this case from Excel, implemented as a VBA macro.</p>
  </li>
</ul>

<p>I also did a test of how well different LLMs used the Dataverse MCP Server, and blogged about it <a href="https://nullpointer.se/dataverse/mcp/llm/2025/07/14/dataverse-llm-evaluation.html">here</a>. This was a looong time ago, and lots of stuff has happened since then:</p>

<ul>
  <li>
    <p>The Dataverse MCP Server is now generally available.</p>
  </li>
  <li>
    <p><strong>Loads</strong> of new models have been released. Around New Year with the release of <a href="https://www.anthropic.com/news/claude-opus-4-5">Opus 4.5</a>, it felt like an inflection point was hit where models became so capable at coding that most developers, myself included, have largely transitioned from hand-coding to AI-assisted tools like Claude Code and OpenAI Codex.</p>
  </li>
  <li>
    <p>It is becoming increasingly clear that we are rapidly moving towards a future where business applications will pretty much be <strong>headless APIs</strong> and that agentic applications with dynamically generated user interfaces will be the new BizApps. The writing was already on the wall last summer, when MSFT communicated that Power Platform would be <a href="https://devblogs.microsoft.com/powerplatform/power-platform-api-and-sdks-from-ux-first-to-api-first/"><em>API-first</em></a> rather than UX-first, moving forward.</p>
  </li>
</ul>

<p>On that note, I spent a lot of time last autumn exploring the possibilities of agentic user interfaces and dynamically generated UIs. My experiments included <a href="https://nullpointer.se/2025/08/21/mcp-automatic-report-generation.html">dynamic report generation</a> using MCP Resources and a <a href="https://www.youtube.com/watch?v=k5Tc3AsMBls">couple</a> of <a href="https://www.youtube.com/watch?v=6B60HVbnHmw">videos</a> showing various dynamic agent-driven UIs. At that time, there wasn’t much available in terms of standardization and specifications for agentic UIs. Some attempts were made, like <a href="https://www.linkedin.com/posts/andreasadner_%F0%9D%90%8D%F0%9D%90%9E%F0%9D%90%B0-%F0%9D%90%AF%F0%9D%90%A2%F0%9D%90%9D%F0%9D%90%9E%F0%9D%90%A8-using-mcp-ui-to-add-ugcPost-7366158851672276993-_Fga">MCP-UI</a> but it took a good couple of months until things started happening in this space. But then, it completely exploded and now we have an ever growing number of specs to choose from when designing our agentic UIs, like:</p>

<ul>
  <li>
    <p><a href="https://a2ui.org/">A2-UI</a> - a protocol for declarative agent-driven interfaces from Google that was announced last year.</p>
  </li>
  <li>
    <p><a href="https://modelcontextprotocol.io/docs/extensions/apps">MCP Apps</a> - the love-child of <a href="https://github.com/MCP-UI-Org/mcp-ui"><strong>MCP-UI</strong></a> (mentioned above), an extension to the <a href="https://modelcontextprotocol.io/">Model Context Protocol</a> that allows UIs to be served as <a href="https://modelcontextprotocol.io/specification/2025-11-25/server/resources">MCP Resources</a>.</p>
  </li>
</ul>

<p>With all these new specifications, the protocol stack for agentic UIs is surely taking shape quickly. When I submitted my talk <a href="https://www.linkedin.com/posts/andreasadner_today-i-had-the-privilege-of-talking-about-activity-7424462809729523712-mFSQ?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w"><em>Creating rich agent user experiences using AG-UI: The Agent-User Interaction Protocol</em></a> to the AgentCon Stockholm conference a while back, I thought it would be a really obscure topic. But it turned out to tie in pretty well with everything that has been happening with agentic user interfaces lately. <a href="https://docs.ag-ui.com/">AG-UI</a> seems to be the “protocol glue” that ties all these new agent user interface specs to the agent orchestrator backends.</p>

<p>So where is all of this going? At this point in time, I think the <a href="https://dojo.ag-ui.com/">AG-UI demos</a> and the <a href="https://web-app-production-9af6.up.railway.app/">AG-UI MCP Apps</a> demos are probably the best way to get a feel for where this tech is heading, and what we can expect from agentic user interfaces in the future. Also, check out my <a href="https://agent-con-demos.vercel.app">AG-UI demos</a> from AgentCon:</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/PeEE5kYwdgo?si=rlwU2zvLhyXpN1kV" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>So, while there is a veritable explosion happening in the agentic UI space, we are still waiting for these new standards to make it into the broader Microsoft stack. VS Code is, as always, an early adopter of everything new - they already <a href="https://code.visualstudio.com/blogs/2026/01/26/mcp-apps-support">shipped MCP Apps support</a> back in January - but for those of us in the BizApps world, we are impatiently waiting for this tech to land in Copilot Studio, Microsoft Teams and the rest of the Power Platform. Good things come to those who wait! But I digress, we were talking about Dataverse MCP Server, right? Yes, we were - so let’s see what has happened with it since I last checked it out.</p>

<h3 id="dataverse-mcp-server-revisited">Dataverse MCP Server revisited</h3>
<p>As I already mentioned, I spent a lot of time last year exploring the preview version of the Dataverse MCP Server. Now it is GA, so I thought I’d try it out again and see what has changed. Last year, I wanted to try out the <a href="https://learn.microsoft.com/en-us/power-apps/maker/data-platform/data-platform-mcp-other-clients">locally running Dataverse MCP Server</a> from an agent created in <a href="https://github.com/microsoft/agent-framework">Microsoft Agent Framework</a>, but that didn’t work because of a <a href="https://github.com/modelcontextprotocol/csharp-sdk/issues/594">bug in the C# Model Content Protocol libraries</a>. This bug is now fixed, so let’s try it!</p>

<p>But before we dive into the tech, let’s say some words about the licenses required to use the Dataverse MCP Server. The <a href="https://learn.microsoft.com/en-us/power-apps/maker/data-platform/data-platform-mcp">docs</a> have the following to say:</p>

<p><img src="/images/260208/image.png" alt="alt text" /></p>

<p>Rather than trying to untangle what this means for makers building agentic applications on Dataverse, I’ll just point you to Jukka Niiranen’s <a href="https://licensing.guide/dataverse-mcp-server-licensing-requirements/">blog post</a> which covers it far more eloquently than I ever could.</p>

<p>So, let’s get started by following the <a href="https://learn.microsoft.com/en-us/power-apps/maker/data-platform/data-platform-mcp-other-clients">instructions</a> in the documentation for how to set up the Dataverse MCP Server to be used by non-Microsoft clients. Similar to the preview, this still involves creating a Dataverse connection in Power Automate, and using it when running the local STDIO Dataverse MCP Server. Unfortunately, the docs are somewhat confusing here:</p>

<p><img src="/images/260208/image-1.png" alt="alt text" /></p>

<p>The URL mentioned here differs slightly from the actual URL you see when you open up the Dataverse connection in Power Automate:</p>

<p><img src="/images/260208/image-2.png" alt="alt text" /></p>

<p>But if we inspect the mysterious URL found in the docs, and compare it to what we have in our address bar when opening up the connection in Power Automate, we can patch together something that works. In my case:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">https</span><span class="p">:</span><span class="c1">//make.powerautomate.com/environments/d231fc57-cc9a-e207-8c1c-14b98c01504c/connections?apiName=shared_commondataserviceforapps&amp;connectionName=6e32493be0df440f8b601734754b1533</span>
</code></pre></div></div>
<p>Using that connection string, we can make the Dataverse MCP Server work in Claude!</p>

<p><img src="/images/260208/image-3.png" alt="alt text" /></p>

<p>So what has changed? Well, not much. There are a couple of new tools for schema manipulation but apart from that, things are mostly the same. The big thing is probably the remote Dataverse MCP Server that can be used from <a href="https://learn.microsoft.com/en-us/power-apps/maker/data-platform/data-platform-mcp-copilot-studio">Copilot Studio</a>, but in this example I am using the good old STDIO Dataverse MCP Server (my prediction for 2026 is that STDIO support will be dropped from the MCP specification for security reasons, but we’ll have to see what happens…)</p>

<p>I assume that much has changed “under the hood”, but functionality-wise the MCP Server is basically the same - including the limitation of being able to return maximum 20 records per call to <code class="language-plaintext highlighter-rouge">read_query</code>.</p>

<p>When I participated in the private preview for the Dataverse MCP Server, one of my suggestions was to use MCP Resources to be able to return large result sets without weighing down the context, for example using <a href="https://microsoft.github.io/mcscatblog/posts/mcp-resources-as-tool-inputs/">the pattern described here in the Copilot Studio CAT team blog</a>. Hopefully this will be implemented some day!</p>

<p>But let’s move on to using the Dataverse MCP Server from Microsoft Agent Framework!</p>

<h3 id="the-big-shift-to-agentic-user-interfaces-and-the-need-to-manage-agent-context">The big shift to agentic user interfaces, and the need to manage agent context</h3>

<p>Lately, things have started to shift quickly with regard to how we build business applications for our customers. The focus is no longer mainly on creating model-driven apps on top of Dataverse - instead, many customers want to build AI chat interfaces on top of their existing Dataverse/Dynamics 365 applications. And honestly, it is easy to see why. Even with something as basic as a text input box, a natural language interface has some pretty compelling advantages over a traditional BizApp UIs, especially when it comes to finding information:</p>

<ul>
  <li>
    <p><strong>Natural language search beats Advanced Find every day of the week</strong> - Instead of navigating to Advanced Find, picking entities, adding filter conditions, choosing operators and remembering exact field names, etc - you just <em>ask</em> “Show me all open opportunities over 500k that haven’t been updated in the last 30 days.” Done. The agent figures it out.</p>
  </li>
  <li>
    <p><strong>You don’t need to know the application</strong> - With a traditional model-driven app, there is a learning curve. Where is that view? Which tab has the field I need? How do I navigate to that related entity? With a chat interface, you just describe what you want in plain language and the agent handles the rest.</p>
  </li>
</ul>

<p>And this is with <em>text input</em> - which honestly is not exactly the optimal user experience. But imagine what will happen when <strong>voice</strong> input becomes the norm and you can just <em>talk</em> to your business applications. And when the dynamically generated agentic UIs that I discussed earlier start landing in the Microsoft stack - combining natural language interaction with rich user interfaces generated on the fly - that’s when things get really interesting. We are not that far away from this becoming reality, I think (hope).</p>

<p><img src="/images/260208/Astro.gif" alt="astro.gif" /></p>

<p>So, a kind of project we have been doing quite a lot lately is <strong>chat agents that use tools to communicate with Dataverse</strong>, mainly for data retrieval purposes. Not much reasoning needed, and not much data manipulation going on. Such agents are characterized by:</p>

<ul>
  <li>No real need for reasoning models, speed is important - the agent needs to be snappy!</li>
  <li>Tool calling is frequent and tool calling accuracy is super-important, this kind of agent lives and dies with its ability to call tools.</li>
  <li>Loooooads of data being retrieved, which can lead to context rot and massive context usage and high costs, if not handled properly.</li>
</ul>

<p>The last bullet here is really interesting and begs the question - how can we manage context for this kind of agent? Yes, let’s talk about <strong><em>chat history reducers</em></strong> - mechanisms for reducing the amount of context used by the agent, without sacrificing (too much) agent ability.</p>

<h3 id="chat-history-reducers">Chat history reducers</h3>
<p>When using the <a href="https://learn.microsoft.com/en-us/agent-framework/user-guide/agents/agent-memory?pivots=programming-language-csharp#chat-history-reduction-with-in-memory-storage">Microsoft Agent Framework</a>, one option is to use chat reducers that implement the <a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.ai.ichatreducer?view=net-9.0-pp"><code class="language-plaintext highlighter-rouge">IChatReducer</code></a> interface. There are some options available in <a href="https://github.com/dotnet/extensions/tree/main/src/Libraries/Microsoft.Extensions.AI/ChatReduction">Microsoft.Extensions.AI</a>:</p>

<ul>
  <li>
    <p><a href="https://github.com/dotnet/extensions/blob/main/src/Libraries/Microsoft.Extensions.AI/ChatReduction/MessageCountingChatReducer.cs"><code class="language-plaintext highlighter-rouge">MessageCountingChatReducer</code></a> - This is a reducer that at the end of each turn throws away all tool calls and their result, as well as truncating the message history if it exceeds a certain number of messages .</p>
  </li>
  <li>
    <p><a href="https://github.com/dotnet/extensions/blob/main/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs"><code class="language-plaintext highlighter-rouge">SummarizingChatReducer</code></a> - This reducer uses an LLM to regularly summarize old messages into <strong>one</strong> message - and throw away the old messages.</p>
  </li>
</ul>

<p>Danish AI guru Rasmus Wulff Jensen has a great <a href="https://www.youtube.com/watch?v=a-7wyjpf5qQ&amp;t=13s">video</a> about chat history reduction techniques in Microsoft Agent Framework, check it out!</p>

<p>The OOB reducers mentioned above are unfortunately not very suitable for our particular use case, for a couple of reasons:</p>

<ul>
  <li>
    <p>Since tool calling is key to our use-case, throwing away all tool calls from the history like <code class="language-plaintext highlighter-rouge">MessageCountingChatReducer</code> does is not a good idea.</p>
  </li>
  <li>
    <p>The <code class="language-plaintext highlighter-rouge">SummarizingChatReducer</code> - while usable for “normal” ChatGPT-style use-cases - doesn’t really bring any real value here. In the <strong>Question -&gt; Tool Call -&gt; Response</strong> loop, the LLM response is basically always a summary of the result of the tool call and are just a few tokens. LLM summarization doesn’t add anything.</p>
  </li>
</ul>

<p>So, let’s explore other options! But while we are at it, let’s also try out the reducers mentioned above so we can see how they behave in our scenario. I created a test harness for evaluating a number of context reducer techniques, that can be found <a href="https://github.com/adner/ContextManagement">here</a>. In addition to <code class="language-plaintext highlighter-rouge">MessageCountingChatReducer</code>, it also evaluates a couple of new reducers:</p>

<ul>
  <li><a href="https://github.com/adner/ContextManagement/blob/main/ContextManagementApp/Reducers/DummyReducer.cs"><code class="language-plaintext highlighter-rouge">DummyReducer</code></a> - No reducer at all! Let’s see what happens if we let the context grow without control!</li>
  <li><a href="https://github.com/adner/ContextManagement/blob/main/ContextManagementApp/Reducers/ToolCallPreservingChatReducer.cs"><code class="language-plaintext highlighter-rouge">ToolPreservingChatReducer</code></a> - Works like the <code class="language-plaintext highlighter-rouge">MessageCountingChatReducer</code> but keeps the tool calls in the context, until they are dropped.</li>
  <li><a href="https://github.com/adner/ContextManagement/blob/main/ContextManagementApp/Reducers/ContextAwareChatReducer.cs"><code class="language-plaintext highlighter-rouge">ContextAwareChatReducer</code></a> - A more sophisticated reducer that:
    <ul>
      <li>Keeps the most recent messages (including tool calls!) without modification.</li>
      <li>Older tool call results are condensed (verbose results being the the main culprit with regard to token consumption), while the arguments are kept so the LLM knows what requests is has made previously.</li>
      <li>Certain tool calls are kept intact - specifically tool calls that retrieve data model metadata - so that the LLM doesn’t have to call these tools repeatedly.</li>
      <li>All messages older than a certain cutoff are thrown away, including metadata tool calls.</li>
    </ul>
  </li>
</ul>

<p>In this example, I have set up a Dataverse environment that contains information about <strong>astronauts</strong>, <strong>rockets</strong> and <strong>space missions</strong>. The <strong>Dataverse MCP Server</strong> is used to retrieve data from Dataverse. We have a static set of questions that are asked in sequence:</p>

<ul>
  <li><em>“List all the astronauts in the system and their specialization!”</em></li>
  <li><em>“What rockets are available?”</em></li>
  <li><em>“Which astronaut is leading the most missions?”</em></li>
  <li><em>“What is the specialization of Astrid Lindqvist?”</em></li>
  <li><em>“Earlier you listed the astronauts. Can you recall who the first one was?”</em></li>
  <li><em>“What was the specialization of Alan?”</em></li>
</ul>

<p>Let’s run the tests and see how these different reducers behave!</p>

<h3 id="dummyreducer">DummyReducer</h3>

<p>As mentioned above, this reducer doesn’t reduce at all but keeps the full context indefinitely. In the first turn we can see that the agent retrieves the metadata it needs, and since no reduction is ever done, the context just keeps growing as questions are asked.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>── Turn 1 ──────────────────────────────────────────────────
  Q: "List all the astronauts in the system and their specialization!"
  [Tool Call] list_knowledge_sources()
  [Tool Call] list_tables()
  [Tool Call] read_query(querytext=SELECT TOP 20 contactid, fullname, jobtitle FROM contact ORDER BY fullname)
  [Tool Call] describe_table(tablename=contact)
  [Tool Call] read_query(querytext=SELECT TOP 50 fullname, contact_specialization FROM contact WHERE jobtitle = 'As...)
  [Tool Call] read_query(querytext=SELECT TOP 20 fullname, contact_specialization FROM contact WHERE jobtitle = 'As...)
  A: "Here are the astronauts (contacts) in the system and their specialization:  | Astronaut | Specialization | |---|---| | Alan Shepard | Pilot | | Amara Okafor | Payload Specialist | | Astrid Lindqvist |..."

  Tokens:      Input: 47 065  |  Output:    296
  Cumulative:  Input: 47 065  |  Output:    296
  Session messages: 14  |  Sent to LLM: 13
────────────────────────────────────────────────────────────────
</code></pre></div></div>
<p>…</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>── Turn 6 ──────────────────────────────────────────────────
  Q: "What was the specialization of Alan?"
  A: "Alan Shepard’s specialization is **Pilot**."

  Tokens:      Input: 13 036  |  Output:     12
  Cumulative:  Input: 160 054  |  Output:    547
  Session messages: 32  |  Sent to LLM: 31
────────────────────────────────────────────────────────────────

========================== COMPARISON SUMMARY ==========================
Reducer                          |   Total In |  Total Out | Final Msgs |   Last LLM
------------------------------------------------------------------------------------
DummyReducer                     |    160 054 |        547 |         32 |         31
=========================================================================
</code></pre></div></div>
<p>In total, <strong>160054</strong> input tokens are spent. The full transcript of the run can be found <a href="/images/260208/dummyreducer.txt">here</a>.</p>

<h3 id="messagecountingreducer">MessageCountingReducer</h3>
<p>This reducer throws away all tool calls and their results while keeping a fixed number of regular messages.</p>

<p>Since all tool calls are stripped from the context, the agent needs to retrieve the data model metadata over and over again, as can be seen here:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>── Turn 1 ──────────────────────────────────────────────────
  Q: "List all the astronauts in the system and their specialization!"
  [Tool Call] list_knowledge_sources()
  [Tool Call] list_tables()
  [Tool Call] describe_table(tablename=contact)
  [Tool Call] read_query(querytext=SELECT TOP 20 fullname, contact_specialization FROM contact WHERE contact_specia...)
  A: "| Astronaut | Specialization | |---|---| | Alan Shepard | Pilot | | Amara Okafor | Payload Specialist | | Astrid Lindqvist | Commander | | Buzz Aldrin | Pilot | | Chris Hadfield | Mission Specialist |..."

  Tokens:      Input: 21 917  |  Output:    223
  Cumulative:  Input: 21 917  |  Output:    223
  Session messages: 8  |  Sent to LLM: 7
────────────────────────────────────────────────────────────────

── Turn 2 ──────────────────────────────────────────────────
  Q: "What rockets are available?"
info: MyMessageCountingChatReducer[0]
      Chat history reduced: 8 messages → 2 retained. Dropped 0 non-system messages exceeding limit of 5, dropped 6 function call/result messages.
  [Tool Call] list_knowledge_sources()
  [Tool Call] list_tables()
  [Tool Call] describe_table(tablename=cr087_rocket)
  [Tool Call] read_query(querytext=SELECT cr087_rocketid, cr087_rocketname, statecode, statuscode, createdon, modif...)
  A: "The following rockets are available in the system (all currently **Active**):  - Atlas   - Delta   - Falcon   - Nova   - Saturn   - Soyuz"

  Tokens:      Input: 21 800  |  Output:    137
  Cumulative:  Input: 43 717  |  Output:    360
  Session messages: 12  |  Sent to LLM: 11
────────────────────────────────────────────────────────────────
</code></pre></div></div>
<p>We can also see that when the reducer reaches its limit of regular messages, and starts dropping these, it is (of course) not able to respond to questions relating to data retrieved earlier in the conversation:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>── Turn 4 ──────────────────────────────────────────────────
  Q: "What is the specialization of Astrid Lindqvist?"
info: MyMessageCountingChatReducer[0]
      Chat history reduced: 14 messages → 5 retained. Dropped 1 non-system messages exceeding limit of 5, dropped 8 function call/result messages.
  [Tool Call] list_knowledge_sources()
  [Tool Call] list_tables()
  [Tool Call] describe_table(tablename=contact)
  [Tool Call] read_query(querytext=SELECT TOP 20 fullname, contact_specialization FROM contact WHERE fullname = 'As...)
  A: "Astrid Lindqvist’s specialization is **Commander**."

  Tokens:      Input: 27 456  |  Output:     98
  Cumulative:  Input: 99 382  |  Output:    643
  Session messages: 15  |  Sent to LLM: 14
────────────────────────────────────────────────────────────────

── Turn 5 ──────────────────────────────────────────────────
  Q: "Earlier you listed the astronauts. Can you recall who the first one was?"
info: MyMessageCountingChatReducer[0]
      Chat history reduced: 15 messages → 5 retained. Dropped 2 non-system messages exceeding limit of 5, dropped 8 function call/result messages.
  A: "I didn’t list any astronauts earlier in this chat—only rockets (Atlas, Delta, Falcon, Nova, Saturn, Soyuz) and then details about Astrid Lindqvist.  If you mean the first rocket I listed, it was **Atl..."

  Tokens:      Input:  2 109  |  Output:     55
  Cumulative:  Input: 101 491  |  Output:    698
  Session messages: 7  |  Sent to LLM: 6
────────────────────────────────────────────────────────────────

========================== COMPARISON SUMMARY ==========================
Reducer                          |   Total In |  Total Out | Final Msgs |   Last LLM
------------------------------------------------------------------------------------
MyMessageCountingChatReducer     |    128 545 |        812 |         15 |         14
=========================================================================
</code></pre></div></div>
<p>In total, <strong>128545</strong> input tokens are spent, which is less than the <code class="language-plaintext highlighter-rouge">DummyReducer</code> but at the price of poor agent memory and worse answers. The full transcript of the run can be found <a href="/images/260208/messagecountingreducer.txt">here</a>.</p>

<h3 id="toolpreservingchatreducer">ToolPreservingChatReducer</h3>
<p>The <code class="language-plaintext highlighter-rouge">ToolPreservingChatReducer</code> works like the <code class="language-plaintext highlighter-rouge">MessageCountingReducer</code> but keeps the tool calls in context. It also has a mechanism for making sure that tool call result messages are not being orphaned by the tool call message being dropped.</p>

<p>We can see that since the tool calls are kept in context, the agent doesn’t have to call metadata tools repeatedly:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>── Turn 1 ──────────────────────────────────────────────────
  Q: "List all the astronauts in the system and their specialization!"
  [Tool Call] list_knowledge_sources()
  [Tool Call] list_tables()
  [Tool Call] describe_table(tablename=contact)
  [Tool Call] read_query(querytext=SELECT fullname, contact_specialization FROM contact WHERE contact_specializatio...)
  A: "| Astronaut | Specialization | |---|---| | Alan Shepard | Pilot | | Amara Okafor | Payload Specialist | | Astrid Lindqvist | Commander | | Buzz Aldrin | Pilot | | Chris Hadfield | Mission Specialist |..."

  Tokens:      Input: 21 931  |  Output:    237
  Cumulative:  Input: 21 931  |  Output:    237
  Session messages: 8  |  Sent to LLM: 7
────────────────────────────────────────────────────────────────
...
── Turn 3 ──────────────────────────────────────────────────
  Q: "Which astronaut is leading the most missions?"
info: ToolPreservingChatReducer[0]
      Chat history reduced: 14 messages → 11 retained (target: 10). Dropped 3 messages (cut at index 3).
warn: ToolPreservingChatReducer[0]
      Dropped tool messages from context: 1 function call(s), 1 function result(s). Tool context has been lost for these interactions.
  [Tool Call] read_query(querytext=SELECT TOP 1 fullname, contact_missions_completed FROM contact WHERE contact_mis...)
  A: "Chris Hadfield is leading the most missions, with **3 missions completed**."

  Tokens:      Input: 20 363  |  Output:     65
  Cumulative:  Input: 71 749  |  Output:    389
  Session messages: 15  |  Sent to LLM: 14
────────────────────────────────────────────────────────────────
</code></pre></div></div>
<p>As soon as older regular messages are dropped, it suffers from the same issue as the <code class="language-plaintext highlighter-rouge">MessageCountingReducer</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>── Turn 5 ──────────────────────────────────────────────────
  Q: "Earlier you listed the astronauts. Can you recall who the first one was?"
info: ToolPreservingChatReducer[0]
      Chat history reduced: 18 messages → 11 retained (target: 10). Dropped 7 messages (cut at index 7).
warn: ToolPreservingChatReducer[0]
      Dropped tool messages from context: 2 function call(s), 2 function result(s). Tool context has been lost for these interactions.
  A: "I can’t reliably recall that from our earlier messages alone.  If you tell me which list you mean (e.g., “all astronauts ordered by name” or “top missions”), or if you want, I can re-run the query to ..."

  Tokens:      Input:  6 484  |  Output:     59
  Cumulative:  Input: 98 922  |  Output:    553
  Session messages: 13  |  Sent to LLM: 12
────────────────────────────────────────────────────────────────
...
========================== COMPARISON SUMMARY ==========================
Reducer                          |   Total In |  Total Out | Final Msgs |   Last LLM
------------------------------------------------------------------------------------
ToolPreservingChatReducer        |    111 812 |        600 |         14 |         13
=========================================================================
</code></pre></div></div>
<p>In total, <strong>111812</strong> input tokens are spent, which is less than the <code class="language-plaintext highlighter-rouge">MessageCountingReducer</code>, while the accuracy is the same. The transcript can be found <a href="/images/260208/toolpreservingreducer.txt">here</a>.</p>

<h3 id="contextawarechatreducer">ContextAwareChatReducer</h3>
<p>This reducer keeps recent tool calls intact, while condensing the results of older tool calls. It also keeps data model metadata tool calls completely intact.</p>

<p>Here we can see the reducer in action, preserving the metadata tool calls (<code class="language-plaintext highlighter-rouge">list_tables</code> and <code class="language-plaintext highlighter-rouge">describe_table</code>) while condensing the other tool calls:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>── Turn 3 ──────────────────────────────────────────────────
  Q: "Which astronaut is leading the most missions?"
info: ContentAwareChatReducer[0]
      Phase 2: 14 messages split into 9 historical + 5 recent. Condensing historical data tool results.
info: ContentAwareChatReducer[0]
        Condensed tool result: list_knowledge_sources (call_Ij9XqcyqCpOv6poFTpgCFJvh) → {"@odata.context":"https://orga7f9a5a6.crm4.dynamics.com/api/data/v9.1.0/$metadata#expando","value":[]}
info: ContentAwareChatReducer[0]
        Preserved tool result: list_tables (call_ddNb1LRFDNMGHuYoXYvIEeK6)
info: ContentAwareChatReducer[0]
        Preserved tool result: describe_table (call_yjomSmfZxqLumvg2wCc6cr1V)
info: ContentAwareChatReducer[0]
        Condensed tool result: read_query (call_CLGs3ncXnWifJgWnl2c0FCRi) → {"@odata.context":"https:// [truncated]
info: ContentAwareChatReducer[0]
      Phase 3: 14 messages within maxMessages (20), no trimming needed.
info: ContentAwareChatReducer[0]
      Reduction complete: 14 → 14 messages.
  [Tool Call] read_query(querytext=SELECT TOP 1 fullname, contact_missions_completed
FROM contact
WHERE contact_mis...)
  A: "**Chris Hadfield** is leading the most missions, with **3 missions completed**."

  Tokens:      Input: 19 514  |  Output:     70
  Cumulative:  Input: 70 966  |  Output:    385
  Session messages: 18  |  Sent to LLM: 17
────────────────────────────────────────────────────────────────
...
========================== COMPARISON SUMMARY ==========================
Reducer                          |   Total In |  Total Out | Final Msgs |   Last LLM
------------------------------------------------------------------------------------
ContentAwareChatReducer          |    119 321 |        509 |         25 |         24
=========================================================================
</code></pre></div></div>
<p>For this particular use-case, this reducer works really well, for a couple of reasons:</p>

<ul>
  <li>
    <p>It doesn’t clog down the context with tool call results, except for the most recent tool call(s). Data retrieved from Dataverse can be huge, so getting rid of this data as soon as it is irrelevant is extremely important.</p>
  </li>
  <li>
    <p>A good understanding of the data model is super-important, and the metadata retrieval tool calls are they key to this understanding. That’s why the reducerar keeps these intact in the context for a prolonged amount of time, until they are finally disregarded when they reach “old age”.</p>
  </li>
</ul>

<p>In total, <strong>119321</strong> input tokens are spent, which is slightly worse than the <code class="language-plaintext highlighter-rouge">ToolPreservingChatReducer</code> for this very limited scenario. But as the number of requests rise, and if the amount of data retrieved by the tools is huge then the <code class="language-plaintext highlighter-rouge">ContextAwareChatReducer</code> will vastly outperform the other reducers. The transcript can be found <a href="/images/260208/contentawarereducer.txt">here</a>.</p>

<h3 id="summary">Summary</h3>

<p>Context management is one of those things that every consumer LLM product already handles for you. ChatGPT, Claude, Gemini - they all silently summarize and compress your conversation history as it grows, and for general-purpose chat this works great. But when you’re building agents that act as natural language interfaces on top of business data - where the conversation is dominated by tool calls returning large chunks of structured data - you can’t just rely on generic summarization. The shape of these conversations is fundamentally different from a typical chat: it’s a tight loop of questions, tool calls and data, where what matters is not the prose but the <em>results</em> and the <em>metadata</em> that enables future tool calls.</p>

<p>For this kind of agent, you need to take control of context management yourself. The out-of-the-box reducers in <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.AI</code> are a good starting point, but as the tests above show, a context reduction strategy that understands the semantics of your particular conversation pattern - what to keep, what to condense, and what to drop - will give you better accuracy at lower cost as conversations grow. I hope you find this deep dive into context reducers useful!</p>

<p>Until next time, happy hacking!</p>]]></content><author><name>Andreas Adner</name></author><summary type="html"><![CDATA[It has been a while since I last tried the Dataverse MCP Server. Last summer, when it was in preview, I experimented with it extensively and posted my experiences to LinkedIn and to my blog: In this LinkedIn post, I showed how the Dataverse MCP Server can be used from Claude Code, Claude Desktop and Gemini CLI.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nullpointer.se/images/260208/splash.png" /><media:content medium="image" url="https://nullpointer.se/images/260208/splash.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Agent 365 notifications</title><link href="https://nullpointer.se/agent-365-notifications.html" rel="alternate" type="text/html" title="Agent 365 notifications" /><published>2025-12-30T00:00:00+00:00</published><updated>2025-12-30T00:00:00+00:00</updated><id>https://nullpointer.se/agent-365-notifications</id><content type="html" xml:base="https://nullpointer.se/agent-365-notifications.html"><![CDATA[<p><a href="/agent-365-notifications.html">
    <img src="/images/251230/splash.png" alt="Agent 365 notifications" />
  </a></p>

<p>In my <a href="https://nullpointer.se/exploring-agent-365-cli.html">last blog post</a> I explained in detail how to use the <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/developer/agent-365-cli?tabs=windows">Agent 365 CLI</a> to set up the infrastructure necessary to deploy a custom agent to Agent 365. So if you are just starting out, that post is a great way to get the prerequisites in place to be able to start developing your agent.</p>

<p>In this blog post I discuss how to build the actual agent, using the <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/developer/?tabs=dotnet">Agent 365 SDK</a>, with specific focus on how to use the <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/developer/notification?tabs=dotnet">notification</a> functionality in the SDK.
<!--end_excerpt-->
Our goal is to create an agent that can respond to messages from Teams and other channels, reply to comments in Word documents and respond to emails. Let’s get started!</p>

<h3 id="the-sdk-libraries">The SDK libraries</h3>

<p>There are quite a few <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/developer/?tabs=dotnet#agent-365-agent-sdk-packages">SDK packages</a> available for Agent 365. The ones of special interest for us today are:</p>

<ul>
  <li>
    <p><a href="https://www.nuget.org/packages/Microsoft.Agents.A365.Notifications">Microsoft.Agents.A365.Notifications</a> - a package that <em>“provides notification services for agents, including sending and managing messages and alerts”</em>. This package makes it possible for our agent to respond to e.g. comments in Word documents, and reply to emails.</p>
  </li>
  <li>
    <p><a href="https://www.nuget.org/packages/Microsoft.Agents.A365.Tooling">Microsoft.Agents.A365.Tooling</a> - a package that  <em>“provides tools for agent development, debugging, and management”</em>. This package gives us the ability to use the first-party Agent 365 MCP Servers.</p>
  </li>
  <li>
    <p><a href="https://www.nuget.org/packages/Microsoft.Agents.A365.Tooling.Extensions.AgentFramework">Microsoft.Agents.A365.Tooling.Extensions.AgentFramework</a> - a package that provides <em>“tooling support for agents using Agent Framework features”</em>. This library is needed since we intend to use the <a href="https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview">Microsoft Agent Framework</a> as orchestrator.</p>
  </li>
</ul>

<p>There are lots of other libraries available that handle observability and telemetry, but these were omitted from this demo for the sake of simplicity.</p>

<h3 id="bootstrapping-the-agent">Bootstrapping the agent</h3>

<p>As I mentioned in my last blog post, <a href="https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/overview">devtunnels</a> is a useful tool. The post describes the setup needed to utilize devtunnels to debug your agent locally, so check it out if you haven’t done so already.</p>

<p>The code for the agent can be found in <a href="https://github.com/adner/Agent365_Notification_Sample">this repo</a>, let’s go through it to understand how it works.</p>

<p>The agent is basically a .NET web app that (locally) listens to port 3978 and that accepts requests to the <code class="language-plaintext highlighter-rouge">/api/messages</code> endpoint. This is the endpoint that the Bot Messaging service that is deployed to the cloud routes requests to, as mentioned in my last blog post.</p>

<p>We <a href="https://github.com/adner/Agent365_Notification_Sample/blob/main/notification-agent/Program.cs">wire up</a> this endpoint to accept requests:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Map the /api/messages endpoint to the AgentApplication</span>
    <span class="n">app</span><span class="p">.</span><span class="nf">MapPost</span><span class="p">(</span><span class="s">"/api/messages"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="n">HttpRequest</span> <span class="n">request</span><span class="p">,</span> <span class="n">HttpResponse</span> <span class="n">response</span><span class="p">,</span> <span class="n">IAgentHttpAdapter</span> <span class="n">adapter</span><span class="p">,</span> <span class="n">IAgent</span> <span class="n">agent</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">cancellationToken</span><span class="p">)</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
            <span class="k">await</span> <span class="n">adapter</span><span class="p">.</span><span class="nf">ProcessAsync</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">response</span><span class="p">,</span> <span class="n">agent</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">);</span>
    <span class="p">});</span>
<span class="p">...</span>
    <span class="n">app</span><span class="p">.</span><span class="nf">MapGet</span><span class="p">(</span><span class="s">"/"</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="s">"Agent Framework Notification Agent"</span><span class="p">);</span>
    <span class="n">app</span><span class="p">.</span><span class="nf">UseDeveloperExceptionPage</span><span class="p">();</span>
    <span class="n">app</span><span class="p">.</span><span class="nf">MapControllers</span><span class="p">().</span><span class="nf">AllowAnonymous</span><span class="p">();</span>

    <span class="n">app</span><span class="p">.</span><span class="n">Urls</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">$"http://localhost:3978"</span><span class="p">);</span>
</code></pre></div></div>
<p>The agent is added to the pipeline:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">builder</span><span class="p">.</span><span class="n">AddAgent</span><span class="p">&lt;</span><span class="n">MyAgent</span><span class="p">&gt;();</span>
</code></pre></div></div>
<p>We inject the services we need to use the A365 Tooling Servers:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="n">AddSingleton</span><span class="p">&lt;</span><span class="n">IMcpToolRegistrationService</span><span class="p">,</span> <span class="n">McpToolRegistrationService</span><span class="p">&gt;();</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="n">AddSingleton</span><span class="p">&lt;</span><span class="n">IMcpToolServerConfigurationService</span><span class="p">,</span> <span class="n">McpToolServerConfigurationService</span><span class="p">&gt;();</span>
</code></pre></div></div>
<p>This allows the agent to load the MCP Servers that are defined in <a href="https://github.com/adner/Agent365_Notification_Sample/blob/main/notification-agent/ToolingManifest.json"><code class="language-plaintext highlighter-rouge">ToolingManifest.json</code></a>. For our agent, these are:</p>

<ul>
  <li><strong>mcp_MailTools</strong> that allows the agent to send and reply to emails.</li>
  <li><strong>mcp_WordServer</strong> that lets the agent reply to comments in Word documents, among other things.</li>
</ul>

<p>In <a href="https://github.com/adner/Agent365_Notification_Sample/blob/main/notification-agent/Agent/MyAgent.cs"><code class="language-plaintext highlighter-rouge">MyAgent.cs</code></a> we wire up the three activity handlers that our agent needs:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="nf">MyAgent</span><span class="p">(</span><span class="n">AgentApplicationOptions</span> <span class="n">options</span><span class="p">,</span>
            <span class="n">IChatClient</span> <span class="n">chatClient</span><span class="p">,</span>
            <span class="n">IConfiguration</span> <span class="n">configuration</span><span class="p">,</span>
            <span class="n">IMcpToolRegistrationService</span> <span class="n">toolService</span><span class="p">,</span>
            <span class="n">IHttpClientFactory</span> <span class="n">httpClientFactory</span><span class="p">,</span>
            <span class="n">ILogger</span><span class="p">&lt;</span><span class="n">MyAgent</span><span class="p">&gt;</span> <span class="n">logger</span><span class="p">)</span> <span class="p">:</span> <span class="k">base</span><span class="p">(</span><span class="n">options</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">_chatClient</span> <span class="p">=</span> <span class="n">chatClient</span><span class="p">;</span>
            <span class="n">_configuration</span> <span class="p">=</span> <span class="n">configuration</span><span class="p">;</span>
            <span class="n">_logger</span> <span class="p">=</span> <span class="n">logger</span><span class="p">;</span>
            <span class="n">_toolService</span> <span class="p">=</span> <span class="n">toolService</span><span class="p">;</span>
            <span class="n">_httpClientFactory</span> <span class="p">=</span> <span class="n">httpClientFactory</span><span class="p">;</span>

            <span class="c1">// Handle A365 Notification Messages. </span>
            <span class="k">this</span><span class="p">.</span><span class="nf">OnAgenticWordNotification</span><span class="p">(</span><span class="n">HandleWordCommentNotificationAsync</span><span class="p">,</span> <span class="n">autoSignInHandlers</span><span class="p">:</span> <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="n">AgenticIdAuthHandler</span> <span class="p">});</span>
            <span class="k">this</span><span class="p">.</span><span class="nf">OnAgenticEmailNotification</span><span class="p">(</span><span class="n">HandleEmailNotificationAsync</span><span class="p">,</span> <span class="n">autoSignInHandlers</span><span class="p">:</span> <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="n">AgenticIdAuthHandler</span> <span class="p">});</span>

            <span class="c1">// Handles all messages, regardless of channel - needs to be the last route in order not to hijack A365 notification handlers.</span>
            <span class="k">this</span><span class="p">.</span><span class="nf">OnActivity</span><span class="p">(</span><span class="n">ActivityTypes</span><span class="p">.</span><span class="n">Message</span><span class="p">,</span> <span class="n">OnMessageAsync</span><span class="p">,</span> <span class="n">autoSignInHandlers</span><span class="p">:</span> <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="n">AgenticIdAuthHandler</span> <span class="p">});</span>
        <span class="p">}</span>
</code></pre></div></div>
<p>The order of the handlers is important - the notification handlers need to come before the general <code class="language-plaintext highlighter-rouge">OnMessageAsync</code> handler, so that this generic one doesn’t swallow the notification messages.</p>

<h3 id="the-notification-handlers">The notification handlers</h3>

<p><strong>OnAgenticWordNotification</strong> is called when the agent is @-tagged in a Word comment:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">HandleWordCommentNotificationAsync</span><span class="p">(</span>
<span class="n">ITurnContext</span> <span class="n">turnContext</span><span class="p">,</span>
<span class="n">ITurnState</span> <span class="n">turnState</span><span class="p">,</span>
<span class="n">AgentNotificationActivity</span> <span class="n">activity</span><span class="p">,</span>
<span class="n">CancellationToken</span> <span class="n">cancellationToken</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">comment</span> <span class="p">=</span> <span class="n">activity</span><span class="p">.</span><span class="n">WpxCommentNotification</span><span class="p">;</span>

    <span class="kt">var</span> <span class="n">attachments</span> <span class="p">=</span> <span class="n">turnContext</span><span class="p">.</span><span class="n">Activity</span><span class="p">.</span><span class="n">Attachments</span><span class="p">;</span>

    <span class="kt">var</span> <span class="n">contentUrl</span> <span class="p">=</span> <span class="n">attachments</span><span class="p">[</span><span class="m">0</span><span class="p">].</span><span class="n">ContentUrl</span><span class="p">;</span>
<span class="p">...</span>
    <span class="kt">var</span> <span class="n">userText</span> <span class="p">=</span> <span class="n">turnContext</span><span class="p">.</span><span class="n">Activity</span><span class="p">.</span><span class="n">Text</span><span class="p">?.</span><span class="nf">Trim</span><span class="p">()</span> <span class="p">??</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="kt">var</span> <span class="n">_agent</span> <span class="p">=</span> <span class="k">await</span> <span class="nf">GetClientAgent</span><span class="p">(</span><span class="n">turnContext</span><span class="p">,</span> <span class="n">turnState</span><span class="p">,</span> <span class="n">_toolService</span><span class="p">,</span> <span class="n">AgenticIdAuthHandler</span><span class="p">);</span>
<span class="p">...</span>
    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_agent</span><span class="p">.</span><span class="nf">RunAsync</span><span class="p">(</span>
        <span class="s">$"""
</span>        <span class="n">Your</span> <span class="n">task</span> <span class="k">is</span> <span class="n">to</span> <span class="n">respond</span> <span class="n">to</span> <span class="n">a</span> <span class="n">comment</span> <span class="k">in</span> <span class="n">a</span> <span class="n">Word</span> <span class="n">file</span><span class="p">.</span> <span class="n">First</span><span class="p">,</span> <span class="k">get</span> <span class="n">the</span> <span class="n">full</span> <span class="n">content</span>
        <span class="n">of</span> <span class="n">the</span> <span class="n">Word</span> <span class="n">file</span> <span class="n">to</span> <span class="n">understand</span> <span class="n">the</span> <span class="n">context</span> <span class="n">and</span> <span class="n">find</span> <span class="k">out</span> <span class="n">what</span> <span class="n">the</span> <span class="n">comment</span> <span class="k">is</span>
        <span class="n">referring</span> <span class="n">to</span><span class="p">.</span> <span class="n">Use</span> <span class="n">the</span> <span class="n">tool</span> <span class="n">WordGetDocumentContent</span> <span class="k">for</span> <span class="k">this</span> <span class="n">purpose</span><span class="p">.</span> <span class="n">The</span> <span class="n">URL</span> <span class="n">to</span>
        <span class="n">the</span> <span class="n">document</span> <span class="k">is</span> <span class="p">{</span><span class="n">contentUrl</span><span class="p">}.</span> <span class="n">Then</span> <span class="n">find</span> <span class="n">the</span> <span class="n">text</span> <span class="n">that</span> <span class="n">the</span> <span class="n">comment</span> <span class="n">with</span> <span class="n">id</span>
        <span class="p">{</span><span class="n">comment</span><span class="p">.</span><span class="n">CommentId</span><span class="p">}</span> <span class="k">is</span> <span class="n">referring</span> <span class="n">to</span> <span class="n">and</span> <span class="n">respond</span> <span class="n">with</span> <span class="n">an</span> <span class="n">answer</span><span class="p">.</span>
        <span class="s">""");
</span>
    <span class="n">_logger</span><span class="p">?.</span><span class="nf">LogInformation</span><span class="p">(</span><span class="s">"Agent response: {Response}"</span><span class="p">,</span> <span class="n">response</span><span class="p">.</span><span class="nf">ToString</span><span class="p">());</span>

    <span class="c1">//Note that we don't respond at the end of this method - we instead let the Word MCP Server handle the reply to the comment.</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Unfortunately the <code class="language-plaintext highlighter-rouge">WpxCommentNotification</code> doesn’t include all the information about the document that is needed by the <code class="language-plaintext highlighter-rouge">WordReplyToComment</code> tool in the <code class="language-plaintext highlighter-rouge">mcp_WordServer</code>. To solve this, we instruct the agent to first call the <code class="language-plaintext highlighter-rouge">WordGetDocumentContent</code> to retrieve this required info. This tool call also returns the full document, which enables the agent to pinpoint the exact text was highlighted in the Word comment and get the full context (at the expense of lots of tokens, of course…).</p>

<p>The agent then uses the <code class="language-plaintext highlighter-rouge">WordReplyToComment</code> MCP tool to reply to the comment, like so:</p>

<p><img src="/images/251230/image.png" alt="alt text" /></p>

<p>We can check the logs to understand the agent’s reasoning, as it replies to the comment:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Agent response: I’ve reviewed the full document and located the comment with ID 469179BD, which refers to the statement claiming that the Commodore 64 had built-in Ethernet networking hardware.

I’ve replied directly to that comment explaining that this is incorrect: the Commodore 64 did not include built-in Ethernet. Any networking was done via external peripherals such as modems (e.g., over RS‑232) or much later third-party network adapters. I also noted that the text should be corrected to remove the Ethernet claim.
</code></pre></div></div>

<p>Moving on to the <strong>HandleEmailNotificationAsync</strong> handler:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">HandleEmailNotificationAsync</span><span class="p">(</span>
<span class="n">ITurnContext</span> <span class="n">turnContext</span><span class="p">,</span>
<span class="n">ITurnState</span> <span class="n">turnState</span><span class="p">,</span>
<span class="n">AgentNotificationActivity</span> <span class="n">activity</span><span class="p">,</span>
<span class="n">CancellationToken</span> <span class="n">cancellationToken</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">email</span> <span class="p">=</span> <span class="n">activity</span><span class="p">.</span><span class="n">EmailNotification</span><span class="p">;</span>
<span class="p">...</span>
    <span class="kt">var</span> <span class="n">userText</span> <span class="p">=</span> <span class="n">turnContext</span><span class="p">.</span><span class="n">Activity</span><span class="p">.</span><span class="n">Text</span><span class="p">?.</span><span class="nf">Trim</span><span class="p">()</span> <span class="p">??</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="kt">var</span> <span class="n">_agent</span> <span class="p">=</span> <span class="k">await</span> <span class="nf">GetClientAgent</span><span class="p">(</span><span class="n">turnContext</span><span class="p">,</span> <span class="n">turnState</span><span class="p">,</span> <span class="n">_toolService</span><span class="p">,</span> <span class="n">AgenticIdAuthHandler</span><span class="p">,</span> <span class="s">"You are a helpful assistant."</span><span class="p">);</span>
<span class="p">...</span>
    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_agent</span><span class="p">.</span><span class="nf">RunAsync</span><span class="p">(</span>
        <span class="s">$"""
</span>        <span class="n">You</span> <span class="n">have</span> <span class="n">received</span> <span class="n">a</span> <span class="n">mail</span> <span class="n">and</span> <span class="n">your</span> <span class="n">task</span> <span class="k">is</span> <span class="n">to</span> <span class="n">reply</span> <span class="n">to</span> <span class="n">it</span><span class="p">.</span> <span class="n">Please</span> <span class="n">respond</span> <span class="n">to</span> <span class="n">the</span>
        <span class="n">mail</span> <span class="k">using</span> <span class="nn">the</span> <span class="n">ReplyToMessageAsync</span> <span class="n">tool</span> <span class="k">using</span> <span class="nn">HTML</span> <span class="n">formatted</span> <span class="n">content</span><span class="p">.</span> <span class="n">The</span> <span class="n">ID</span> <span class="n">of</span>
        <span class="n">the</span> <span class="n">email</span> <span class="k">is</span> <span class="p">{</span><span class="n">email</span><span class="p">.</span><span class="n">Id</span><span class="p">}.</span> <span class="n">This</span> <span class="k">is</span> <span class="n">the</span> <span class="n">content</span> <span class="n">of</span> <span class="n">the</span> <span class="n">mail</span> <span class="n">you</span> <span class="n">received</span><span class="p">:</span> <span class="p">{</span><span class="n">userText</span><span class="p">}</span>
        <span class="s">""");
</span>
    <span class="n">_logger</span><span class="p">?.</span><span class="nf">LogInformation</span><span class="p">(</span><span class="s">"Agent response: {Response}"</span><span class="p">,</span> <span class="n">response</span><span class="p">.</span><span class="nf">ToString</span><span class="p">());</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Here we simply instruct the agent to reply to the email message it has received by invoking the <code class="language-plaintext highlighter-rouge">ReplyToMessageAsync</code> tool in the <code class="language-plaintext highlighter-rouge">mcp_MailTools</code> MCP Server.</p>

<p><img src="/images/251230/image-1.png" alt="alt text" /></p>

<p>The last handler, <strong>OnMessageAsync</strong>, is for responding to messages from all other channels, for example Teams.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">protected</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">OnMessageAsync</span><span class="p">(</span><span class="n">ITurnContext</span> <span class="n">turnContext</span><span class="p">,</span> <span class="n">ITurnState</span> <span class="n">turnState</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">cancellationToken</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">userText</span> <span class="p">=</span> <span class="n">turnContext</span><span class="p">.</span><span class="n">Activity</span><span class="p">.</span><span class="n">Text</span><span class="p">?.</span><span class="nf">Trim</span><span class="p">()</span> <span class="p">??</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="kt">var</span> <span class="n">_agent</span> <span class="p">=</span> <span class="k">await</span> <span class="nf">GetClientAgent</span><span class="p">(</span><span class="n">turnContext</span><span class="p">,</span> <span class="n">turnState</span><span class="p">,</span> <span class="n">_toolService</span><span class="p">,</span> <span class="n">AgenticIdAuthHandler</span><span class="p">);</span>

    <span class="c1">// Read or Create the conversation thread for this conversation.</span>
    <span class="n">AgentThread</span><span class="p">?</span> <span class="n">thread</span> <span class="p">=</span> <span class="nf">GetConversationThread</span><span class="p">(</span><span class="n">_agent</span><span class="p">,</span> <span class="n">turnState</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_agent</span><span class="p">!.</span><span class="nf">RunAsync</span><span class="p">(</span><span class="n">userText</span><span class="p">,</span> <span class="n">thread</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">:</span> <span class="n">cancellationToken</span><span class="p">);</span>

    <span class="k">await</span> <span class="n">turnContext</span><span class="p">.</span><span class="nf">SendActivityAsync</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="nf">ToString</span><span class="p">());</span>

    <span class="n">turnState</span><span class="p">.</span><span class="n">Conversation</span><span class="p">.</span><span class="nf">SetValue</span><span class="p">(</span><span class="s">"conversation.threadInfo"</span><span class="p">,</span> <span class="n">ProtocolJsonSerializer</span><span class="p">.</span><span class="nf">ToJson</span><span class="p">(</span><span class="n">thread</span><span class="p">.</span><span class="nf">Serialize</span><span class="p">()));</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The handler also (de)serializes the conversation thread state, so that it can remember previous messages and keep the whole conversation “in memory”. And of course, it can use all the MCP Servers that are registered. In this example, it uses the <code class="language-plaintext highlighter-rouge">mcp_MailTools</code> MCP Server to send an email:</p>

<p><img src="/images/251230/image-2.png" alt="alt text" /></p>

<p>That’s pretty much all there is to it…</p>

<p>All the code can be found in <a href="https://github.com/adner/Agent365_Notification_Sample">this repo</a>, and here is a short video of the agent replying to a Word comment:</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/xKd1awTemiU?si=YnItvuxg_C-zx8jB" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>Thanks to all of you that have been following my blog during 2025, it has been fun exploring MCP, agents, LLMs and now lately Agent 365. looking forward to more fun stuff in 2026! Until then, happy new year and happy hacking!</p>]]></content><author><name>Andreas Adner</name></author><summary type="html"><![CDATA[In my last blog post I explained in detail how to use the Agent 365 CLI to set up the infrastructure necessary to deploy a custom agent to Agent 365. So if you are just starting out, that post is a great way to get the prerequisites in place to be able to start developing your agent. In this blog post I discuss how to build the actual agent, using the Agent 365 SDK, with specific focus on how to use the notification functionality in the SDK.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nullpointer.se/images/251230/splash.png" /><media:content medium="image" url="https://nullpointer.se/images/251230/splash.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Exploring the Agent 365 CLI</title><link href="https://nullpointer.se/exploring-agent-365-cli.html" rel="alternate" type="text/html" title="Exploring the Agent 365 CLI" /><published>2025-12-26T00:00:00+00:00</published><updated>2025-12-26T00:00:00+00:00</updated><id>https://nullpointer.se/exploring-agent-365-cli</id><content type="html" xml:base="https://nullpointer.se/exploring-agent-365-cli.html"><![CDATA[<p><a href="/exploring-agent-365-cli.html">
    <img src="/images/251226/splash.png" alt="Exploring the Agent 365 CLI" />
  </a></p>

<p>Ever since Agent 365 was announced at this year’s Microsoft Ignite, I have wanted to explore this exciting technology and the possibilities it creates. Since the <a href="https://modelcontextprotocol.io/">Model Context Protocol</a> is a topic that is dear to my heart, I got especially interested in the <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/tooling-servers-overview">Agent 365 MCP Servers</a>, and the way they make it possible for an agent to interact with the various Microsoft productivity applications, such as Word and Teams, in different ways. <!--end_excerpt-->Lately, I have explored this particular topic in depth in a number of videos and blog posts:</p>

<ul>
  <li>
    <p>In <a href="https://nullpointer.se/agent-365-mcp-servers-part-1.html"><em>Part 1: Using A365 MCP Servers from a custom agent</em></a> and <a href="https://nullpointer.se/agent-365-mcp-servers-part-2.html"><em>Part 2 - Building the agent</em></a> I explored how the Agent 365 MCP Servers can be used from a custom agent.</p>
  </li>
  <li>
    <p>In the blog post <a href="https://nullpointer.se/agent-365-dynamic-mcp-servers.html"><em>Creating dynamic MCP Servers using Agent 365</em></a> I checked out the possibilities of dynamically creating <em>custom</em> MCP Servers in Agent 365, servers that can use the many out-of-the-box operations provided by Dataverse and the Graph API. I also demonstrated how to use this dynamically created MCP Server in a Copilot Studio agent.</p>
  </li>
</ul>

<p>However, the MCP servers are just one piece of the Agent 365 puzzle. Another game-changer in Agent 365 is the ability to onboard <em>agentic users</em> in your tenant - agents that can act on your behalf, or autonomously, in a governed and secure way.</p>

<p>During the last year I have <a href="https://nullpointer.se/2025/09/26/ai-agents-eu-ai-act.html">written quite a bit</a> about the need for governance when building an AI agent infrastructure, and Agent 365 is the technology in the Microsoft stack that enables this. Exciting times!</p>

<p>When I watched <a href="https://www.linkedin.com/in/robertmbruckner/">Robert Bruckner</a> and <a href="https://www.linkedin.com/in/james-oleinik/">James Oleinik</a> present the session <em>“<a href="https://ignite.microsoft.com/en-US/sessions/BRK305">Build A365-Ready Agents for the Enterprise</a>“</em>, I got interested in creating my own A365-integrated agent. In the session they demonstrated a number of really cool things in Agent 365, such as:</p>

<ul>
  <li>The ability to onboard <em>agentic users</em> and govern them in the Microsoft 365 Admin Center.</li>
  <li>Agents that use the <em>Agent 365 MCP Servers</em> to interact with productivity applications, such as Teams and Word.</li>
</ul>

<p>So, I have spent the last couple of weeks trying this out, resulting in a <a href="https://www.linkedin.com/posts/andreasadner_christmas-party-planning-using-agent-365-activity-7408541193065627648-UJ3k">LinkedIn-post</a> that shows a <a href="https://www.youtube.com/watch?v=qCQLZJNdAWg">video</a> of an A365-agent running in Teams that uses a few of the A365 MCP servers. Kudos to <a href="https://www.linkedin.com/in/tahirsousa/">Tahir Sousa</a> and team for helping me sort it all out!</p>

<p>In this blog post my goal is to explain in detail 
how this was done, so you can try it out for yourself - as well as learn a thing or two about the technology that powers Agent 365.</p>

<p><strong>Heads up: Agent 365 is evolving fast. Treat this blog post as a snapshot - check the <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/">official A365 docs</a> for current info, and expect any issues that I mention here to be resolved by the time you read this.</strong></p>

<h3 id="more-sdks-than-you-can-shake-a-stick-at">More SDKs than you can shake a stick at</h3>

<p>As we start exploring Agent 365, there are a number of SDKs that we should be aware of:</p>

<ul>
  <li><a href="https://github.com/microsoft/Agent365-dotnet">The Microsoft Agent 365 SDK</a> is the new SDK that adds the cool capabilities that Robert and James demonstrated in their Ignite session, for example:
    <ul>
      <li><a href="https://learn.microsoft.com/en-us/microsoft-agent-365/developer/notification">Notifications</a> - An agent can be notified when e.g. a mail is sent to the agent, or when a comment is requested in a Word document.</li>
      <li><a href="https://learn.microsoft.com/en-us/microsoft-agent-365/developer/observability">Observability</a> - tracing, caching and monitoring of agents - stuff that I haven’t tried out yet but hope to be able to soon.</li>
      <li><a href="https://learn.microsoft.com/en-us/microsoft-agent-365/tooling-servers-overview">Tooling</a> - Libraries for using the A365 tooling servers from your agent.</li>
    </ul>
  </li>
  <li><a href="https://github.com/microsoft/Agents-for-net">The Microsoft 365 Agents SDK</a> - This is the foundational SDK that the A365 SDK is built on top of, and that allows for creating agents that can be deployed to a myriad of channels. In the blog post <a href="https://nullpointer.se/2025/10/18/teams-ai-sdk-m365-agents-toolkit-doom.html"><em>The Teams AI Library, M365 Agents SDK &amp; Toolkit and how to run Doom in Teams</em></a> that I wrote back in October, I took a look at this SDK and the associated tooling, with specific focus on deploying chatbots to the Teams channel.</li>
</ul>

<p>As I noted in my previous blog post, the M365 Agents SDK “<em>comes with a steep learning curve and involves considerable ‘black box infra voodoo’</em>”. When working with A365, that feeling is still there - though it’s somewhat less pronounced… More on that below.</p>

<p>Also part of A365 are a couple more interesting GitHub repos:</p>

<ul>
  <li>
    <p><a href="https://github.com/microsoft/Agent365-devTools">Microsoft Agent 365 DevTools CLI</a> - This is the CLI that we will use to provision the infrastructure needed for our agent and to package and deploy it. This is similar to <a href="https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/create-new-toolkit-project-vs">the Microsoft 365 Agents Toolkit</a> in M365 Agents SDK - which also can be used for provisioning and deployment, but this time things are done a bit differently. The docs for the CLI can be found <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/developer/agent-365-cli?tabs=windows">here</a>.</p>
  </li>
  <li>
    <p><a href="https://github.com/microsoft/Agent365-Samples">Agent365-Samples</a> is a repo with “starter kits” for building an agent in different languages, and agent orchestrator frameworks.</p>
  </li>
</ul>

<h3 id="step-0---setting-things-up-locally">Step 0 - Setting things up locally</h3>
<p>So where to start? The amount of SDKs, GitHub repos and resources available for A365 can be a bit overwhelming at first. So, let’s start by trying to get an agent up and running locally.</p>

<p>The way to test agents locally in M365 Agents SDK was to use <a href="https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/test-with-toolkit-project?tabs=windows">Agents Playground</a> and in A365 Agent SDK <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/developer/testing?view=o365-worldwide&amp;tabs=python">this is still</a> the recommended approach. I have tried this, and it works but it is currently pretty cumbersome, because of this:</p>

<p><img src="/images/251226/image-28.png" alt="alt text" /></p>

<p>So, rather than doing that I thought I would try out debugging the agent locally using the good old <a href="https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/overview"><strong><em>devtunnel</em></strong></a> approach that I have used in days past, when tinkering with the <a href="https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/">M365 Agents SDK</a> and <a href="https://learn.microsoft.com/en-us/microsoft-365/developer/overview-m365-agents-toolkit">Toolkit</a>. Devtunnels makes it possible to deploy the agent to the cloud, while at the same time pointing the agent Messaging Endpoint to my local machine - pretty powerful (if it works)! More on that below…</p>

<p>First, let’s <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/developer/agent-365-cli?view=o365-worldwide&amp;tabs=windows#use-dotnet-tool-install">install the Agent 365 CLI</a> which is a tool that simplifies setting up the infrastructure necessary for our agent:</p>

<p><code class="language-plaintext highlighter-rouge">dotnet tool install --global Microsoft.Agents.A365.DevTools.Cli</code></p>

<p>We need some code to get us started, so let’s clone the <a href="https://github.com/microsoft/Agent365-Samples">A365 samples repo</a>:</p>

<p><code class="language-plaintext highlighter-rouge">git clone https://github.com/microsoft/Agent365-Samples</code></p>

<p>I’m a C# guy, so the sample I am interested in is the <a href="https://github.com/microsoft/Agent365-Samples/tree/main/dotnet/agent-framework">C# Microsoft Agent Framework</a> one. This sample project contains a simple agent that uses <a href="https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview">Microsoft Agent Framework</a> and that utilizes some of the A365 MCP Servers.</p>

<p>If we open up the sample project, we can see that:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">appsettings.json</code> contains a lot of placeholders. These placeholders will be automagically populated when we run the A365 CLI to create the infrastructure for our agent. This is also described <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/developer/testing?view=o365-worldwide&amp;tabs=dotnet#step-1-configure-your-environment">here</a> in the docs, but is in my opinion pretty confusing…</li>
  <li><code class="language-plaintext highlighter-rouge">ToolingManifest.json</code> lists all the A365 MCP Servers that the agent uses. This file is used both by the CLI to create the correct permissions for the agent blueprint in your tenant, and also to keep the agent itself informed about which tools it has access to.</li>
</ul>

<p>Also, let’s install <a href="https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/overview"><strong>devtunnels</strong></a> - the nifty technology that allows us to route requests from a bot running in the cloud to our local machine:</p>

<p><code class="language-plaintext highlighter-rouge">winget install Microsoft.devtunnel</code></p>

<p>We then create a devtunnel that listens on port <strong>3978</strong>. <span style="color: red;"><strong>NOTE: Don’t create anonymous devtunnels - security is important!</strong></span></p>

<p>Make note of the public devtunnel URL, we will use this as the bot messaging endpoint when we provision the infrastructure in the next step.</p>

<p><img src="/images/251226/image.png" alt="alt text" /></p>

<p>Now we have what we need locally to start creating our agent, so let’s move on to infrastructure provisioning using the A365 CLI.</p>

<h3 id="step-1---creating-the-infrastructure">Step 1 - Creating the infrastructure</h3>
<p>This is where the magic happens. We want to use the Agent 365 CLI to create the infrastructure necessary to use our agent - locally at first, and then deployed to our tenant.</p>

<p>The first thing we need to do is to create a new App Registration in Entra ID for the A365 CLI, as is described <a href="https://github.com/microsoft/Agent365-devTools/blob/main/docs/guides/custom-client-app-registration.md">here</a>. This is required to give the CLI the correct permissions to work with agent blueprints, etc. Make note of the application id of the newly created app registration, this is used in the next steps.</p>

<p>Then, navigate to our sample agent, and run the <code class="language-plaintext highlighter-rouge">a365 config init</code> command. This collects some information and creates the <code class="language-plaintext highlighter-rouge">a365.config.json</code> file, that will be used for infra provisioning.</p>

<p><img src="/images/251226/image-1.png" alt="alt text" /></p>

<p>Note that:</p>
<ul>
  <li>We enter the application id of the Agent 365 CLI App Registration that we created earlier.</li>
  <li>It doesn’t matter which resource group we select, since we have no intention to deploy anything to Azure, we only want to run the agent locally.</li>
  <li>We don’t want to create a Web App, for the same reason.</li>
  <li>The Messaging endpoint URL is set to the URL of the devtunnel we created earlier. Note that it is not possible to change this URL later (in any simple way), but this is a <a href="https://github.com/microsoft/Agent365-devTools/issues/129">requested feature</a>.</li>
</ul>

<p>Now we can proceed with setting up all the required infrastructure by running the <code class="language-plaintext highlighter-rouge">a365 setup</code> command:</p>

<p><img src="/images/251226/image-2.png" alt="alt text" /></p>

<p>First, we can run the <code class="language-plaintext highlighter-rouge">a365 setup requirements</code> command, to check if we are good to go:</p>

<p><img src="/images/251226/image-3.png" alt="alt text" /></p>

<p>Great, let’s proceed! We don’t have to run <code class="language-plaintext highlighter-rouge">a365 setup infrastructure</code> since we - as we noted above - only intend to run the agent locally in this demo. So, we continue by creating the Agent Identity Blueprint.</p>

<p>But wait, what is a <strong>Blueprint?</strong></p>

<p>The documentation for <a href="https://learn.microsoft.com/en-us/entra/agent-id/identity-platform/">Microsoft agent identity platform for developers</a> defines an agent identity blueprint in the <a href="https://learn.microsoft.com/en-us/entra/agent-id/identity-platform/agent-blueprint">following way</a>:</p>

<p><em>“All agent identities in a Microsoft Entra ID tenant are created from an agent identity blueprint. The agent identity blueprint is a key component of the Microsoft agent identity platform that enables secure development and administration of AI agents at scale. An agent identity blueprint serves four purposes… Organizations can deploy many instances of an AI agent. Each instance pursues a different goal and requires a different level of access. Each instance uses a distinct agent identity for authentication and access. However, the many agent identities used share certain characteristics. The agent identity blueprint records these common characteristics, so that all agent identities created using the blueprint have a consistent configuration.”</em></p>

<p>So let’s create the agent identity blueprint by running the command <code class="language-plaintext highlighter-rouge">a365 setup blueprint</code>. This command does LOADS of stuff, for example:</p>

<ul>
  <li>Creates the agentic identity blueprint, sets permissions and applies admin consent to these. Once created, the blueprint can be found as an Enterprise Application (i.e., service principal) in Entra ID, with the following inheritable permissions:</li>
</ul>

<p><img src="/images/251226/image-4.png" alt="alt text" /></p>

<ul>
  <li>Creates a secret on the App Registration:</li>
</ul>

<p><img src="/images/251226/image-5.png" alt="alt text" /></p>

<ul>
  <li>Updates <code class="language-plaintext highlighter-rouge">appsettings.json</code> of our agent and populates the placeholders.</li>
  <li>Creates a file called <code class="language-plaintext highlighter-rouge">a365.generated.config.json</code> with information about everything that has been created.</li>
  <li>Registers the <strong>messaging endpoint</strong> that points to our local devtunnel in this case.</li>
</ul>

<p>So, we now have an agent identity blueprint that can be used to create agent instances! It has a couple of inheritable permissions, but we need to add a few more by running these commands:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">a365 setup permissions mcp</code> - To configure the permissions for the A365 MCP Servers.</li>
  <li><code class="language-plaintext highlighter-rouge">a365 setup permissions bot</code> - To configure Bot API permissions.</li>
</ul>

<p>After running these commands, a number of new permissions are added to the service principal:</p>

<p><img src="/images/251226/image-6.png" alt="alt text" /></p>

<p>So far so good, the infrastructure is (almost) in place. Let’s continue!</p>

<h3 id="step-2---publishing-the-agent">Step 2 - Publishing the agent</h3>

<p>Now we almost have everything we need to be able to create agentic identities in our tenant. We still need to:</p>

<ul>
  <li>Publish the agent to the M365 Admin Center, as is described <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/developer/publish-deploy-agent?view=o365-worldwide&amp;tabs=dotnet#step-2-publish-to-microsoft-admin-center">here</a>. We do this by running the <code class="language-plaintext highlighter-rouge">a365 publish</code> command. This creates a zip-file containing the agent manifest, and uploads it to Microsoft 365.</li>
</ul>

<p><img src="/images/251226/image-7.png" alt="alt text" /></p>

<ul>
  <li>Then, we must follow the instructions <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/developer/publish-deploy-agent?view=o365-worldwide&amp;tabs=dotnet#step-3-configure-agent-blueprint-in-developer-portal">here</a> to configure our agent identity blueprint to point to the Bot that was provisioned (and that is completely managed by Microsoft, and which we have no access to unfortunately…):</li>
</ul>

<p><img src="/images/251226/image-8.png" alt="alt text" /></p>

<p>Pro tip: The bot ID is the same ID as the ID of the agent identity blueprint.</p>

<h3 id="step-3---creating-the-agent-identity-and-agent-user">Step 3 - Creating the agent identity and agent user</h3>

<p>Now that the agent (manifest) is published to our tenant, we can go to Microsoft Teams and create an instance of the agent:</p>

<p><img src="/images/251226/image-9.png" alt="alt text" /></p>

<p>Click “Request instance”:</p>

<p><img src="/images/251226/image-10.png" alt="alt text" /></p>

<p>Now back to M365 Admin Center, where you can approve the request:</p>

<p><img src="/images/251226/image-12.png" alt="alt text" /></p>

<p>Back to Teams, where it is now possible to create an instance of the agent:</p>

<p><img src="/images/251226/image-13.png" alt="alt text" /></p>

<p>When this is completed, two things are created:</p>

<ul>
  <li><a href="https://learn.microsoft.com/en-us/entra/agent-id/identity-platform/agent-identities">An agent identity</a>, which is a <em>“special service principal in Microsoft Entra ID. It represents an identity that the agent identity blueprint created and is authorized to impersonate”</em>:</li>
</ul>

<p><img src="/images/251226/image-14.png" alt="alt text" /></p>

<ul>
  <li><a href="https://learn.microsoft.com/en-us/entra/agent-id/identity-platform/agent-users">An agent user</a>, which is a <em>“specialized identity type designed to bridge the gap between agents and human user capabilities”</em>:</li>
</ul>

<p><img src="/images/251226/image-15.png" alt="alt text" /></p>

<p>Now when everything is created in the cloud, let’s move on to trying to test our agent locally.</p>

<h3 id="step-4---testing-the-agent-locally">Step 4 - Testing the agent locally</h3>

<p>First, make sure that your devtunnel is running by executing the command <code class="language-plaintext highlighter-rouge">devtunnel host new-year-tunnel</code> (change to the ID of the tunnel you created).</p>

<p>Make sure to update <code class="language-plaintext highlighter-rouge">appsettings.json</code> so that it points to an LLM deployment in Microsoft Foundry:</p>

<p><img src="/images/251226/image-16.png" alt="alt text" /></p>

<p>Some additional tweaks need to be made to the <code class="language-plaintext highlighter-rouge">appsettings.json</code>:</p>

<ul>
  <li>The <code class="language-plaintext highlighter-rouge">AuthType</code> of the Service Connection needs to be changed to <code class="language-plaintext highlighter-rouge">ClientSecret</code>, instead of <code class="language-plaintext highlighter-rouge">UserManagedIdentity</code> - since we are running locally and don’t have a Managed Identity set up for our agent web application.</li>
</ul>

<p><img src="/images/251226/image-17.png" alt="alt text" /></p>

<ul>
  <li>Remove all authorization handlers except the <code class="language-plaintext highlighter-rouge">AgenticUserAuthorization</code> handler (don’t ask me why, it just doesn’t work otherwise…):</li>
</ul>

<p><img src="/images/251226/image-18.png" alt="alt text" /></p>

<p>Now let’s debug our agent! As can be seen, the agent web app responds at port 3978, which is the same port as our devtunnel points to:</p>

<p><img src="/images/251226/image-19.png" alt="alt text" /></p>

<p>The moment of truth! Let’s ask the agent a question in Teams:</p>

<p><img src="/images/251226/image-20.png" alt="alt text" /></p>

<p>Great success, we have successfully provisioned an agent in Agent 365, published it to Teams and are now able to debug it locally using devtunnels!</p>

<p>But what about MCP Servers? If we review the <code class="language-plaintext highlighter-rouge">ToolingManifest.json</code> file, we can see that the agent currently has access to two A365 MCP Servers:</p>

<p><img src="/images/251226/image-21.png" alt="alt text" /></p>

<p>Looking at the agent blueprint, we can see that the blueprint has the required permissions for these MCP Servers (which are inherited by the agent identity):</p>

<p><img src="/images/251226/image-22.png" alt="alt text" /></p>

<p>But how do we add more MCP Servers? In the <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/developer/tooling">documentation</a> we see that there is an <code class="language-plaintext highlighter-rouge">a365 develop list-available</code> command, that can be used to list all the MCP Servers that are available in the catalog:</p>

<p><img src="/images/251226/image-23.png" alt="alt text" /></p>

<p>And we can add for example the Word MCP Server by running the <code class="language-plaintext highlighter-rouge">a365 develop add-mcp-servers mcp_WordServer</code> command. This updates <code class="language-plaintext highlighter-rouge">ToolingManifest.json</code> to include the Word MCP Server:</p>

<p><img src="/images/251226/image-24.png" alt="alt text" /></p>

<p>But we are not done yet, we also need to run the <code class="language-plaintext highlighter-rouge">a365 setup permissions mcp</code> command again to update the blueprint with the new permissions that are required by this MCP Server. This command parses <code class="language-plaintext highlighter-rouge">ToolingManifest.json</code> and updates the blueprint with any missing permissions needed to invoke the MCP Servers. We can now see that the blueprint permissions have been updated:</p>

<p><img src="/images/251226/image-25.png" alt="alt text" /></p>

<p>So let’s try the MCP Servers by asking the agent to send us a mail, by utilizing the <code class="language-plaintext highlighter-rouge">mcp_MailTools</code> MCP Server:</p>

<p><img src="/images/251226/movie1.gif" alt="alt text" /></p>

<p>We can also ask the agent to book a meeting, which uses the <code class="language-plaintext highlighter-rouge">mcp_CalendarTools</code> MCP Server:</p>

<p><img src="/images/251226/movie2.gif" alt="alt text" /></p>

<p>Pretty cool… But what if we want to use other MCP Servers? In a <a href="https://nullpointer.se/agent-365-dynamic-mcp-servers.html">previous blog post</a> I showed how to dynamically create MCP Servers using the Agent 365 MCP Management MCP Server. In the demo I created an MCP Server that could be used to retrieve data from Dataverse:</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/GFpyN8P3pr0?si=NBwgEVMB5VX8rOIe" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>The question is, can this custom MCP Server be used from our agent? We can use the A365 Management MCP Server to get the URL and required scopes of the custom MCP server (see my <a href="https://nullpointer.se/agent-365-dynamic-mcp-servers.html">previous blog post</a>):</p>

<p><img src="/images/251226/image-26.png" alt="alt text" /></p>

<p>So let’s try updating <code class="language-plaintext highlighter-rouge">ToolingManifest.json</code> with this information:</p>

<p><img src="/images/251226/image-27.png" alt="alt text" /></p>

<p>Once again, let’s run <code class="language-plaintext highlighter-rouge">a365 setup permissions mcp</code> to update the blueprint with the new permission - <code class="language-plaintext highlighter-rouge">McpServers.DataverseCustom.All</code> - that is required by the custom MCP server.</p>

<p>Next, we need to add the agentic user as a user in Dataverse. Yes, amazing isn’t it? This is the power of the agentic user - that it can be added to existing applications like Dynamics 365 as a licensed user, with security roles and all. What a time to be alive!</p>

<p><img src="/images/251226/image-29.png" alt="alt text" /></p>

<p>So, let’s try and run the agent and load my custom MCP Server! Hmm… It doesn’t seem to work. After some debugging I realized there was an issue in the way that the Agent 365 SDK parses <code class="language-plaintext highlighter-rouge">ToolingManifest.json</code> when running locally, the method <a href="https://github.com/microsoft/Agent365-dotnet/blob/5533fde6b6dbafb34831fe6821c812d7a02d637d/src/Tooling/Core/Services/McpToolServerConfigurationService.cs#L234">ParseServerConfigFromManifest</a> doesn’t read the <code class="language-plaintext highlighter-rouge">url</code> parameter from the file, instead it defaults to a URL that works for other A365 MCP Servers, but not for our custom one.</p>

<p>Let’s fix this!</p>

<p>I forked the <a href="https://github.com/microsoft/Agent365-dotnet">Agent 365 SDK repo</a> and created <a href="https://github.com/microsoft/Agent365-dotnet/pull/147">this PR</a> which updates <code class="language-plaintext highlighter-rouge">ParseServerConfigFromManifest</code> to also read the url parameter from <code class="language-plaintext highlighter-rouge">ToolingManifest.json</code>. I then rebuilt the agent using my fork. With the fix in place the agent also loads my custom MCP Server, and it can be used side-by-side with the OOB A365 MCP Servers:</p>

<p><img src="/images/251226/movie3.gif" alt="alt text" /></p>

<p>So, there we have it - a custom agent using the Microsoft Agent Framework that is deployed to the Agent 365 infrastructure, Agent 365 MCP Servers working in tandem with dynamically created MCP Servers, data retrieved from Dataverse, agentic users in Teams and local debugging - feels like a Christmas miracle! ☃️🎅</p>

<p>It has been a real treat to explore Agent 365, and I am really looking forward to all the new cool features that will come 2026 as the platform evolves. Until then, happy new year and happy hacking!</p>]]></content><author><name>Andreas Adner</name></author><summary type="html"><![CDATA[Ever since Agent 365 was announced at this year’s Microsoft Ignite, I have wanted to explore this exciting technology and the possibilities it creates. Since the Model Context Protocol is a topic that is dear to my heart, I got especially interested in the Agent 365 MCP Servers, and the way they make it possible for an agent to interact with the various Microsoft productivity applications, such as Word and Teams, in different ways.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nullpointer.se/images/251226/splash.png" /><media:content medium="image" url="https://nullpointer.se/images/251226/splash.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Creating dynamic MCP Servers using Agent 365</title><link href="https://nullpointer.se/agent-365-dynamic-mcp-servers.html" rel="alternate" type="text/html" title="Creating dynamic MCP Servers using Agent 365" /><published>2025-12-16T00:00:00+00:00</published><updated>2025-12-16T00:00:00+00:00</updated><id>https://nullpointer.se/dynamic-mcp-servers</id><content type="html" xml:base="https://nullpointer.se/agent-365-dynamic-mcp-servers.html"><![CDATA[<p><a href="/agent-365-dynamic-mcp-servers.html">
    <img src="/images/251216/splash.png" alt="Creating dynamic MCP Servers using Agent 365" />
  </a></p>

<p>A while back I posted a <a href="https://www.youtube.com/watch?v=GFpyN8P3pr0">video</a> to <a href="https://www.linkedin.com/posts/andreasadner_dynamic-mcp-server-creation-using-agent-365-activity-7404983676205219840-jiZo">LinkedIn</a> that showed how to use the <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/mcp-server-reference/mcpmanagement">Microsoft 365 MCP Management MCP Server</a> (yes, that is the name of the server) that is part of Agent 365 to dynamically create MCP Servers. In this blog post my intention is to show how this was accomplished. <!--end_excerpt--></p>

<p>My interest in this topic started when I read through the Agent 365 documentation, and found a section that described the <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/tooling-servers-overview"><strong><em>tooling servers</em></strong></a> that is part of Agent 365. These servers are described as <em>“…enterprise-grade Model Context Protocol (MCP) servers that give agents safe, governed access to business systems such as Microsoft Outlook, Microsoft Teams, Microsoft SharePoint and OneDrive, Microsoft Dataverse, and more through the tooling gateway”</em>.</p>

<p>These are the same MCP Servers that you can use from Copilot Studio, which I <a href="https://www.linkedin.com/posts/andreasadner_agent-365-mcp-servers-in-copilot-studio-activity-7396978024346382336-mkym">tried out</a> after it was announced at Microsoft Ignite this year. In a <a href="https://www.linkedin.com/posts/andreasadner_agent365-microsoftagentframework-activity-7401354948849889282-Nd2D">post</a> on LinkedIn and in a couple of blog posts (<a href="https://nullpointer.se/agent-365-mcp-servers-part-1.html">part 1</a> and <a href="https://nullpointer.se/agent-365-mcp-servers-part-2.html">part 2</a>) I explored how these MCP Servers could be used from a custom agent, using the Microsoft Agent Framework as orchestrator.</p>

<p>These experiments involved using the <strong>Agent 365 MCP Management MCP Server</strong> to programmatically retrieve information about these first-party MCP Servers, so I could connect to them using the MCP plumbing in Agent Framework.</p>

<p>While exploring the (pretty sparse, to be honest) <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/mcp-server-reference/mcpmanagement">documentation</a> on the A365 MCP Management MCP Server, it is clear that this server has quite a lot of interesting tools, for example:</p>

<ul>
  <li><a href="https://learn.microsoft.com/en-us/microsoft-agent-365/mcp-server-reference/mcpmanagement#createmcpserver">CreateMcpServer</a> - <em>“Creates a new MCP server instance in the current environment”</em>.</li>
  <li><a href="https://learn.microsoft.com/en-us/microsoft-agent-365/mcp-server-reference/mcpmanagement#createtoolwithcustomapi">CreateToolWithCustomAPI</a> - <em>“Creates a new tool with a custom API in an MCP server”</em>.</li>
</ul>

<p>These tools sure sound interesting, but there isn’t any real information on how to use them in the documentation, as far as I can see. The <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/tooling-servers-overview#build-scenario-focused-custom-mcp-servers-with-the-microsoft-mcp-management-server">section</a> <em>“Build scenario-focused custom MCP servers with the Microsoft MCP Management Server”</em> contains a high-level overview of the capabilities for creating custom MCP Servers, including the ability to create MCP Servers based on:</p>

<ul>
  <li>Existing connectors</li>
  <li>Dataverse custom APIs</li>
  <li>Microsoft Graph APIs</li>
</ul>

<p>There is <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/tooling-servers-overview#connect-to-mcp-management-server-in-visual-studio-code">excellent documentation</a> available on how to hook up the Management MCP Server from VS Code, so let’s try that and explore some of the available tools in the MCP Server.</p>

<p>The documentation is missing one detail - how to enable it so that the GitHub Copilot MCP Client is allowed to communicate with Dataverse. Luckily, this instruction can be found <a href="https://learn.microsoft.com/en-us/power-apps/maker/data-platform/data-platform-mcp-disable#configure-and-manage-the-dataverse-mcp-server-for-an-environment">here</a> in the Dataverse MCP Server docs.</p>

<h3 id="creating-the-custom-mcp-server">Creating the custom MCP Server</h3>

<p>In VS Code, we connect to the Management MCP server for one of our Dataverse environments, and ask it to provide detailed information about the <code class="language-plaintext highlighter-rouge">CreateMcpServer</code> tool:</p>

<p><img src="/images/251216/image.png" alt="alt text" /></p>

<p>So, let’s try to use this tool to create a new MCP Server with the name “AddesCoolMcpServer”.</p>

<p>When doing so, we get a somewhat cryptic error message - <code class="language-plaintext highlighter-rouge">Export key attribute name for component MCPServer must start with a valid customization prefix.</code>.</p>

<p><img src="/images/251216/image-1.png" alt="alt text" /></p>

<p>Interesting… Could it be that the prefix of the logical name of the MCP Server must be associated with an existing <a href="https://learn.microsoft.com/en-us/power-apps/maker/data-platform/create-solution#solution-publisher"><strong>Publisher</strong></a>? Let’s try to use the prefix of an existing publisher in this particular Dataverse environment - <code class="language-plaintext highlighter-rouge">adde_</code>. Excellent, this worked:</p>

<p><img src="/images/251216/image-2.png" alt="alt text" /></p>

<p>Looking in Dataverse, we can see that a new record has been created in the <code class="language-plaintext highlighter-rouge">MCPServer</code> table:</p>

<p><img src="/images/251216/image-3.png" alt="alt text" /></p>

<p>So far, so good. So, how can we add tools to this MCP Server? There was a tool in the Management MCP Server called <code class="language-plaintext highlighter-rouge">CreateToolWithCustomAPI</code> which we can probably use to add some Custom APIs as tools to the server. But which Custom APIs are available? We can call the <code class="language-plaintext highlighter-rouge">GetCustomAPIs</code> tool to retrieve the list of Custom APIs that are available in the Dataverse environment:</p>

<p><img src="/images/251216/image-4.png" alt="alt text" /></p>

<p>Quite a lot of tools - 330 to be exact! Let’s try to add the <code class="language-plaintext highlighter-rouge">FetchXmlToSql</code> tool to the MCP Server we just created:</p>

<p><img src="/images/251216/image-5.png" alt="alt text" /></p>

<p>We can see in Dataverse that the tool has been added to the <code class="language-plaintext highlighter-rouge">MCPTool</code> table:</p>

<p><img src="/images/251216/image-6.png" alt="alt text" /></p>

<p>Great, now we have a custom MCP Server with a tool! Let’s get some info about this MCP Server:</p>

<p><img src="/images/251216/image-7.png" alt="alt text" /></p>

<p>Now that we know the URL of the newly created MCP Server we can easily <a href="https://code.visualstudio.com/docs/copilot/customization/mcp-servers">add it to VS Code</a>:</p>

<p><img src="/images/251216/image-8.png" alt="alt text" /></p>

<p>Now let’s try to convert a FetchXml query to SQL using our server:</p>

<p><img src="/images/251216/image-9.png" alt="alt text" /></p>

<p>Works perfectly! Let’s also add the <code class="language-plaintext highlighter-rouge">McpExecuteSqlQuery</code> Custom API as a tool to our server, similar to above:</p>

<p><img src="/images/251216/image-10.png" alt="alt text" /></p>

<p>We now have two tools, let’s try them together:</p>

<p><img src="/images/251216/image-11.png" alt="alt text" /></p>

<p>That worked perfectly! But how do we call our custom MCP Server from Copilot Studio? Let’s start by configuring an App Registration in Entra ID:</p>

<ul>
  <li>Create an App Registration and make note of the Application ID. Note - make sure that the Application Registration is <strong>Multitenant</strong>.</li>
  <li>Create a secret and make note of it.</li>
  <li>Add the <code class="language-plaintext highlighter-rouge">McpServers.DataverseCustom.All</code> scope (since it is required by the tools, see the screenshot above) and consent to it. This scope can be found by adding an API Permission, clicking “APIs that my organization uses” and then search for “Agent 365”:</li>
</ul>

<p><img src="/images/251216/image-12.png" alt="alt text" /></p>

<h3 id="using-the-mcp-server-in-copilot-studio">Using the MCP Server in Copilot Studio</h3>

<p>Now we can create a new agent in Copilot Studio and add our MCP server as a tool. Add a new tool of type “Model Context Protocol”, enter the HTTP streaming endpoint (as can be seen from the screenshot with the tool info above) and configure it to use manual OAuth2:</p>

<p><img src="/images/251216/image-13.png" alt="alt text" /></p>

<p>Enter the authorization URL and the token URL (if you want to know how to get these URLs using MCP Inspector, check out <a href="https://nullpointer.se/agent-365-mcp-servers-part-1.html">this blog post</a>).</p>

<ul>
  <li>The <em>Refresh URL</em> can be set to the same URL as the <em>Token URL Template</em>.</li>
  <li>The <code class="language-plaintext highlighter-rouge">ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default</code> scope is a blanket request to get all scopes that we previously defined for our App Registration (we only added one - the <code class="language-plaintext highlighter-rouge">McpServers.DataverseCustom.All</code> scope, which is the one that is needed here, but <code class="language-plaintext highlighter-rouge">.default</code> is a shorthand for getting all of them).</li>
  <li>The GUID <code class="language-plaintext highlighter-rouge">ea9ffc3e-8a23-4a7d-836d-234d7c7565c1</code> is simply the ID of the <code class="language-plaintext highlighter-rouge">Agent 365 Tools</code> application, which we added as an API permission to our app registration earlier.</li>
</ul>

<p><img src="/images/251216/image-14.png" alt="alt text" /></p>

<p>Hit <strong>Create</strong> and a redirect URL is generated for you:</p>

<p><img src="/images/251216/image-15.png" alt="alt text" /></p>

<p>Go back to your App Registration and create a Web Redirect URL that points to this URL:</p>

<p><img src="/images/251216/image-16.png" alt="alt text" /></p>

<p>If everything worked out well, the MCP Server is now added to Copilot Studio:</p>

<p><img src="/images/251216/image-17.png" alt="alt text" /></p>

<p>Let’s try it by asking the agent to convert a FetchXML query to SQL:</p>

<p><img src="/images/251216/image-18.png" alt="alt text" /></p>

<p>…and run the resulting SQL query against Dataverse:</p>

<p><img src="/images/251216/image-19.png" alt="alt text" /></p>

<p>Great success! We now have a Copilot Studio agent that uses the MCP Server that we created dynamically. Here is a video of it in action:</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/GFpyN8P3pr0?si=iyqVQHMLxuyiXtyk" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>It was fun tinkering around with this and the possibility to create dynamic MCP Servers is really powerful. The documentation is still catching up with all the capabilities, but that’s to be expected for such new technology. I’m excited to see how this evolves as Agent 365 matures.</p>

<p>Until next time, happy hacking!</p>]]></content><author><name>Andreas Adner</name></author><summary type="html"><![CDATA[A while back I posted a video to LinkedIn that showed how to use the Microsoft 365 MCP Management MCP Server (yes, that is the name of the server) that is part of Agent 365 to dynamically create MCP Servers. In this blog post my intention is to show how this was accomplished.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nullpointer.se/images/251216/splash.png" /><media:content medium="image" url="https://nullpointer.se/images/251216/splash.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Part 2 - Building the agent</title><link href="https://nullpointer.se/agent-365-mcp-servers-part-2.html" rel="alternate" type="text/html" title="Part 2 - Building the agent" /><published>2025-12-05T00:00:00+00:00</published><updated>2025-12-05T00:00:00+00:00</updated><id>https://nullpointer.se/agent-365-mcp-servers</id><content type="html" xml:base="https://nullpointer.se/agent-365-mcp-servers-part-2.html"><![CDATA[<p><a href="/agent-365-mcp-servers-part-2.html">
    <img src="/images/251205/splash.png" alt="Part 2 - Building the agent" />
  </a></p>

<p>In my <a href="https://nullpointer.se/agent-365-mcp-servers-part-1.html">last blog post</a> I demonstrated how to connect to some of the MCP Servers that are part of the <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/tooling-servers-overview">tooling servers</a> in Agent 365. We used the tools <a href="https://github.com/modelcontextprotocol/inspector">MCP Inspector</a> and <a href="https://www.postman.com/">Postman</a> to accomplish this. In this blog post the goal is to use these MCP Servers in <a href="https://github.com/microsoft/agent-framework">Microsoft Agent Framework</a>, and present the resulting agent in a custom UI that uses <a href="https://www.copilotkit.ai/">CopilotKit</a> and the <a href="https://github.com/ag-ui-protocol/ag-ui">Agent-User Interaction Protocol</a> (AG-UI).<!--end_excerpt--></p>

<h3 id="creating-the-agent-backend">Creating the agent backend</h3>

<p>We start by creating a web app that exposes an AG-UI endpoint, and that implements an agent using Microsoft Agent Framework. The code for the backend can be found in <a href="https://github.com/adner/Agent365McpServers/tree/main/AgentBackend">this</a> repo. To wire up the AG-UI middleware, we follow the instructions <a href="https://learn.microsoft.com/en-us/agent-framework/integrations/ag-ui/getting-started?pivots=programming-language-csharp">here</a> in the docs. There is also some sample code in the <a href="https://github.com/microsoft/agent-framework/tree/8c6b12e6646e05be557750638ae893ac793ded18/dotnet/samples/AGUIClientServer/AGUIServer">Microsoft Agent Framework repo</a>.</p>

<p>To implement the authorization flow with the MCP Servers, I took inspiration from the <a href="https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/ProtectedMcpClient">Protected MCP Client sample code</a> in the C# MCP SDK repo.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">ModelContextProtocol.Client</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">OpenAI</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Microsoft.Agents.AI</span><span class="p">;</span>
<span class="p">...</span>
<span class="k">using</span> <span class="nn">Microsoft.Agents.AI.Hosting.AGUI.AspNetCore</span><span class="p">;</span>

<span class="c1">// Two HttpClient configurations are needed because different MCP servers have different requirements:</span>
<span class="c1">// - Most MCP servers (Agent365) work fine with standard HTTP chunked transfer encoding</span>
<span class="c1">// - Some servers do NOT support chunked encoding and require Content-Length header</span>
<span class="k">using</span> <span class="nn">var</span> <span class="n">httpClient</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpClient</span><span class="p">();</span>
<span class="k">using</span> <span class="nn">var</span> <span class="n">httpClientWithContentLength</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpClient</span><span class="p">(</span><span class="k">new</span> <span class="nf">ContentLengthEnforcingHandler</span><span class="p">());</span>
</code></pre></div></div>

<p>We then add the plumbing for handling the OAuth2 flow. We implement a special authentication handler - <a href="https://github.com/adner/Agent365McpServers/blob/e214db3a5bafb9603cf70593c6de999e52048060/AgentBackend/Program.cs#L116">OAuthAuthorizationHandler</a> - for this purpose, that among other things opens up a browser so that the user can authenticate and consent to the scopes that are required to access the servers.</p>

<p>In this particular example, we set up the authorization logic to be able to access the Agent 365 MCP Management MCP Server. See the <a href="https://nullpointer.se/agent-365-mcp-servers-part-1.html">last blog post</a> for details on how to set up the prerequisites (App Registration, API permissions, etc) in Entra ID. In the last blog post we used Authorization Code Flow with PKCE, this time we use Authorization Code Flow - so a Client Secret is also required here. In the <a href="https://github.com/adner/Agent365McpServers/tree/main/AgentBackend">repo</a> there are also examples on how to connect to the Word, Teams and Dataverse MCP Servers.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Create OAuth handler with semaphore to serialize auth flows (they share the same redirect port)</span>
<span class="kt">var</span> <span class="n">oauthHandler</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">OAuthAuthorizationHandler</span><span class="p">();</span>

<span class="c1">// Create SSE client transport for the MCP server</span>
<span class="kt">var</span> <span class="n">managementMcpServerUrl</span> <span class="p">=</span> <span class="s">$"https://agent365.svc.cloud.microsoft/mcp/environments/</span><span class="p">{</span><span class="n">dataverseEnvironmentId</span><span class="p">}</span><span class="s">/servers/MCPManagement"</span><span class="p">;</span>
<span class="kt">var</span> <span class="n">managementMcpTransport</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpClientTransport</span><span class="p">(</span><span class="k">new</span><span class="p">()</span>
<span class="p">{</span>
    <span class="n">Endpoint</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="n">managementMcpServerUrl</span><span class="p">),</span>
    <span class="n">Name</span> <span class="p">=</span> <span class="s">"Agent365 Management Client"</span><span class="p">,</span>
    <span class="n">OAuth</span> <span class="p">=</span> <span class="k">new</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">ClientId</span> <span class="p">=</span> <span class="n">clientId</span><span class="p">,</span>
        <span class="n">ClientSecret</span> <span class="p">=</span> <span class="n">clientSecret</span><span class="p">,</span>
        <span class="n">Scopes</span> <span class="p">=</span> <span class="p">[</span><span class="s">"ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/McpServers.Management.All"</span><span class="p">,</span> <span class="s">"offline_access"</span><span class="p">,</span> <span class="s">"openid"</span><span class="p">,</span> <span class="s">"profile"</span><span class="p">],</span>
        <span class="n">RedirectUri</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">"http://localhost:1179/callback"</span><span class="p">),</span>
        <span class="n">AuthorizationRedirectDelegate</span> <span class="p">=</span> <span class="n">oauthHandler</span><span class="p">.</span><span class="n">HandleAuthorizationUrlAsync</span><span class="p">,</span>
    <span class="p">}</span>
<span class="p">},</span> <span class="n">httpClientWithContentLength</span><span class="p">,</span> <span class="n">consoleLoggerFactory</span><span class="p">);</span>
</code></pre></div></div>

<p>We connect to the MCP Server and enumerate the available tools, so we can add them to the agent that we create in the next step.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="k">using</span> <span class="nn">var</span> <span class="n">managementMcpClient</span> <span class="p">=</span> <span class="k">await</span> <span class="n">McpClient</span><span class="p">.</span><span class="nf">CreateAsync</span><span class="p">(</span><span class="n">managementMcpTransport</span><span class="p">,</span> <span class="n">loggerFactory</span><span class="p">:</span> <span class="n">consoleLoggerFactory</span><span class="p">);</span>
</code></pre></div></div>

<p>Then, we create the agent and supply the tools and map the AG-UI endpoint that can be used by our frontend UI.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">AIAgent</span> <span class="n">agent</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">OpenAIClient</span><span class="p">(</span><span class="n">openAiApiKey</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">GetChatClient</span><span class="p">(</span><span class="s">"gpt-5.1"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">CreateAIAgent</span><span class="p">(</span><span class="n">instructions</span><span class="p">:</span> <span class="s">"You are a helpful agent that allows the user to perform management tasks in Agent 365 and call Agent 365 MCP Servers."</span><span class="p">,</span> <span class="n">tools</span><span class="p">:</span> <span class="p">[..</span> <span class="n">managementTools</span><span class="p">]);</span>

<span class="n">app</span><span class="p">.</span><span class="nf">MapAGUI</span><span class="p">(</span><span class="s">"/"</span><span class="p">,</span> <span class="n">agent</span><span class="p">);</span>

<span class="n">app</span><span class="p">.</span><span class="nf">Run</span><span class="p">();</span>
</code></pre></div></div>

<p>However, if we run this - it fails! The reason for the failure is that Entra ID - which is securing the MCP Server - does not accept the <code class="language-plaintext highlighter-rouge">resource</code> parameter that is sent by the <a href="https://github.com/modelcontextprotocol/csharp-sdk">C# Model Context Protocol SDK</a> in the OAuth2 flow. This particular problem is discussed in various places:</p>

<ul>
  <li><a href="https://github.com/modelcontextprotocol/csharp-sdk/issues/939">This issue</a> discusses this issue in detail.</li>
  <li><a href="https://github.com/modelcontextprotocol/csharp-sdk/pull/940">Here</a> is a pull request that claims to fix the issue.</li>
</ul>

<p>The C# MCP SDK actually follows the MCP specification, which <a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#resource-parameter-implementation">mandates</a> that the <code class="language-plaintext highlighter-rouge">resource</code> parameter should be passed when authorizing. But Entra doesn’t accept the parameter…</p>

<p>Actually, there is a <a href="https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1614">proposal</a> to make this parameter <strong>optional</strong> and not mandatory for MCP Clients. But rather than waiting for the MCP spec to change, and in the interest of finishing the demo, I simply forked the C# MCP SDK and did <a href="https://github.com/modelcontextprotocol/csharp-sdk/pull/940/files#diff-c50d00a1141a07ed23e1e5169c0c9af516ab09162628ecd0b0dae70724de1dd3">this change</a> that simply removes the <code class="language-plaintext highlighter-rouge">resource</code> parameter:</p>

<p><img src="/images/251205/image.png" alt="alt text" /></p>

<p>With this fixed, the backend is working and we can move on to implementing the frontend using CopilotKit.</p>

<h3 id="creating-the-frontend">Creating the frontend</h3>

<p>As mentioned above we use CopilotKit to create the frontend, a framework that can use the <a href="https://github.com/ag-ui-protocol/ag-ui">AG-UI protocol</a> to talk to our backend. AG-UI greatly simplifies the process of building UIs for agents, by automatically handling e.g. streaming, state management and tool calling - and it is integrated into CopilotKit. CopilotKit has lots of <a href="https://docs.copilotkit.ai/reference">hooks</a> that can be used to do all kinds of cool stuff:</p>

<ul>
  <li>Render custom UI when <a href="https://docs.copilotkit.ai/reference/hooks/useRenderToolCall">LLM tool calls happen.</a></li>
  <li>Define <a href="https://docs.copilotkit.ai/reference/hooks/useFrontendTool"><em>front end tools</em></a> - tools that are implemented in React and that allow the LLM to execute code on the frontend.</li>
  <li>Handling <a href="https://docs.copilotkit.ai/reference/hooks/useHumanInTheLoop"><em>human in the loop</em></a> scenarios.</li>
</ul>

<p>The frontend is based on the <a href="https://docs.copilotkit.ai/microsoft-agent-framework/quickstart">quickstart code</a> for the CopilotKit/AG-UI integration with Microsoft Agent Framework, and can be found in <a href="https://github.com/adner/Agent365McpServers/tree/main/copilotkitagentframework">this repo</a>.</p>

<p>CopilotKit makes it easy to render tool call progress and result as custom React components. For example, this code implements a <a href="https://docs.copilotkit.ai/reference/hooks/useDefaultTool">default tool</a> calling GUI that handles all tool calls for our agent:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">useDefaultTool</span><span class="p">({</span>
  <span class="na">render</span><span class="p">:</span> <span class="p">({</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">args</span><span class="p">,</span> <span class="nx">status</span><span class="p">,</span> <span class="nx">result</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">isComplete</span> <span class="o">=</span> <span class="nx">status</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">complete</span><span class="dl">"</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">isRunning</span> <span class="o">=</span> <span class="nx">status</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">inProgress</span><span class="dl">"</span> <span class="o">||</span> <span class="nx">status</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">executing</span><span class="dl">"</span><span class="p">;</span>

    <span class="k">return</span> <span class="p">(</span>
      <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">my-3 rounded-xl overflow-hidden shadow-lg bg-gradient-to-br from-slate-900 to-slate-800 border border-slate-700</span><span class="dl">"</span><span class="o">&gt;</span>
        <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">px-4 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 flex items-center justify-between</span><span class="dl">"</span><span class="o">&gt;</span>
          <span class="p">...</span>
            <span class="p">{</span><span class="nx">isRunning</span> <span class="o">&amp;&amp;</span> <span class="dl">"</span><span class="s2">Running...</span><span class="dl">"</span><span class="p">}</span>
            <span class="p">{</span><span class="nx">isComplete</span> <span class="o">&amp;&amp;</span> <span class="dl">"</span><span class="s2">Complete</span><span class="dl">"</span><span class="p">}</span>
          <span class="o">&lt;</span><span class="sr">/span</span><span class="err">&gt;
</span>        <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>
        <span class="p">...</span>

          <span class="p">{</span><span class="nx">isComplete</span> <span class="o">&amp;&amp;</span> <span class="nx">result</span> <span class="o">&amp;&amp;</span> <span class="p">(</span>
            <span class="o">&lt;</span><span class="nx">div</span><span class="o">&gt;</span>
              <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-xs font-semibold text-emerald-400 uppercase tracking-wider mb-2</span><span class="dl">"</span><span class="o">&gt;</span><span class="nx">Result</span><span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt;
</span>              <span class="o">&lt;</span><span class="nx">pre</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-sm text-slate-200 bg-slate-950/50 p-3 rounded-lg border border-slate-700 overflow-x-auto max-h-64 overflow-y-auto</span><span class="dl">"</span><span class="o">&gt;</span>
                <span class="p">{</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">result</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="mi">2</span><span class="p">)}</span>
              <span class="o">&lt;</span><span class="sr">/pre</span><span class="err">&gt;
</span>            <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>          <span class="p">)}</span>
        <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>      <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>    <span class="p">);</span>
  <span class="p">},</span>
<span class="p">});</span>
</code></pre></div></div>
<p>If the LLM calls one of the tools in the MCP Management MCP Server, it renders a custom UI:</p>

<p><img src="/images/251205/image-1.png" alt="alt text" /></p>

<p>As mentioned above, CopilotKit also has built-in support for handling “human in the loop” flows:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nx">useHumanInTheLoop</span><span class="p">(</span>
    <span class="p">{</span>
      <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">go_to_moon</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Go to the moon on request.</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">render</span><span class="p">:</span> <span class="p">({</span> <span class="nx">respond</span><span class="p">,</span> <span class="nx">status</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">(</span>
          <span class="o">&lt;</span><span class="nx">MoonCard</span> <span class="nx">themeColor</span><span class="o">=</span><span class="p">{</span><span class="nx">themeColor</span><span class="p">}</span> <span class="nx">status</span><span class="o">=</span><span class="p">{</span><span class="nx">status</span><span class="p">}</span> <span class="nx">respond</span><span class="o">=</span><span class="p">{</span><span class="nx">respond</span><span class="p">}</span> <span class="sr">/</span><span class="err">&gt;
</span>        <span class="p">);</span>
      <span class="p">},</span>
    <span class="p">},</span>
    <span class="p">[</span><span class="nx">themeColor</span><span class="p">],</span>
  <span class="p">);</span>
</code></pre></div></div>
<p>When we tell the agent that we want to go to the moon, the custom UI is rendered:</p>

<p><img src="/images/251205/image-2.png" alt="alt text" /></p>

<p>And when the button is clicked:</p>

<p><img src="/images/251205/image-3.png" alt="alt text" /></p>

<p>This is a video of the finished agent, that is accessing different Agent 365 MCP Servers:</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/fPwdB1N0AgM?si=qLLKqGQFjz5zM_ai" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>This was a fun experiment, and I look forward to exploring the Agent 365 MCP Servers further. For more information, check out the <a href="https://docs.copilotkit.ai/">CopilotKit docs</a> and read the <a href="https://docs.ag-ui.com/introduction">AG-UI specification</a>.</p>

<p>Until next time, happy hacking!</p>]]></content><author><name>Andreas Adner</name></author><summary type="html"><![CDATA[In my last blog post I demonstrated how to connect to some of the MCP Servers that are part of the tooling servers in Agent 365. We used the tools MCP Inspector and Postman to accomplish this. In this blog post the goal is to use these MCP Servers in Microsoft Agent Framework, and present the resulting agent in a custom UI that uses CopilotKit and the Agent-User Interaction Protocol (AG-UI).]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nullpointer.se/images/251205/splash.png" /><media:content medium="image" url="https://nullpointer.se/images/251205/splash.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Running Microsoft Agent 365 MCP Servers from a custom agent</title><link href="https://nullpointer.se/agent-365-mcp-servers-part-1.html" rel="alternate" type="text/html" title="Running Microsoft Agent 365 MCP Servers from a custom agent" /><published>2025-12-02T00:00:00+00:00</published><updated>2025-12-02T00:00:00+00:00</updated><id>https://nullpointer.se/agent-365-mcp-servers</id><content type="html" xml:base="https://nullpointer.se/agent-365-mcp-servers-part-1.html"><![CDATA[<p><a href="/agent-365-mcp-servers-part-1.html">
    <img src="/images/251202/splash.png" alt="Running Microsoft Agent 365 MCP Servers from a custom agent" />
  </a></p>

<h3 id="microsoft-agent-365">Microsoft Agent 365</h3>
<p>At Microsoft Ignite a couple of weeks ago, Microsoft accounced <a href="https://www.microsoft.com/en-us/microsoft-365/blog/2025/11/18/microsoft-agent-365-the-control-plane-for-ai-agents/">Agent 365</a> - the “control plane for AI agents”. Currently available only through the <a href="https://adoption.microsoft.com/en-us/copilot/frontier-program/">Frontier program</a>, the long-term goal is that Agent 365 should be the framework for governing AI agents in the enterprise, at scale. So if you are into enterprise architecture and governance, Agent 365 is for you. 
<!--end_excerpt-->
Diving into the <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/">docs for Agent 365</a>, there are quite a few interesting nuggets also for us that are more into the tech than the enterprise governance stuff:</p>

<ul>
  <li>
    <p>There is a <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/developer/">new SDK</a> for Agent 365, called the <em>Microsoft Agent 365 SDK</em>. Not to be confused with the <a href="https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/">Microsoft 365 Agents SDK</a>, which is somewhat related - but still something completely different. Let that sink in - Microsoft has released two different SDKs with nearly identical names…</p>
  </li>
  <li>
    <p>Similar to the <a href="https://learn.microsoft.com/en-us/microsoft-365/developer/overview-m365-agents-toolkit">Microsoft 365 Agent Toolkit</a>, the SDK comes with a <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/developer/agent-365-cli">CLI</a> called <em>Agent 365 CLI</em> that helps out with packaging and deployment of agents, as well as other things.</p>
  </li>
  <li>
    <p>There are also <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/tooling-servers-overview">tooling servers</a>, described as “enterprise-grade Model Context Protocol (MCP) servers that give agents safe, governed access to business systems such as Microsoft Outlook, Microsoft Teams, Microsoft SharePoint and OneDrive, Microsoft Dataverse, and more through the tooling gateway”.</p>
  </li>
</ul>

<p>That third thing on the list is actually kind of huge. One of the main complaints from organizations adopting Microsoft Copilot has been the difficulty of doing what is arguably the most basic thing people expect - interacting with Microsoft productivity applications from an agent, creating documents, sending mails, booking meetings… And now it is possible, through first-party MCP Servers that are managed by Microsoft. What a time to be alive!</p>

<p>If your tenant is enrolled in the Frontier program, and if you have a M365 Copilot license, then you can easily add these MCP Servers in you Copilot Studio agent. I tried this out when it was launched and posted a <a href="https://www.linkedin.com/posts/andreasadner_agent-365-mcp-servers-in-copilot-studio-activity-7396978024346382336-mkym?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w">video to LinkedIn</a>, that demonstrated how the MCP Servers for Word and Outlook could be used to create documents and book meetings. Kind of neat! I expect that the list of first-party MCP Servers will grow over time, and that eventually we’ll be able to do almost anything from our agents.</p>

<p><img src="/images/251202/im
age-1.png" alt="alt text" /></p>

<h3 id="the-agent-365-tooling-servers">The Agent 365 tooling servers</h3>

<p>Using these MCP Servers from Copilot Studio is cool and all, but what I really wanted to try was to utilize these servers from Microsoft’s flagship agent orchestration framework - the <a href="https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview">Microsoft Agent Framework</a>. That begs the question - how can we access these MCP Servers from our own code?</p>

<p>In the Agent 365 documentation, it is described how the “MCP Management Server” can be <a href="https://learn.microsoft.com/en-us/microsoft-agent-365/tooling-servers-overview#connect-to-mcp-management-server-in-visual-studio-code">accessed from Visual Studio Code</a>. From the docs it is clear that this MCP Server can be accessed at the following URL:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://agent365.svc.cloud.microsoft/mcp/environments/{environment ID}/servers/MCPManagement
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">environemnt ID</code> should be replaced with the organization ID of your Power Platform environment.</p>

<p>Let’s connect to this endpoint in <a href="https://github.com/modelcontextprotocol/inspector">MCP Inspector</a>! In this tool it is possible to initiate a guided authentication flow that consists of a number of steps, where <em>Metadata Discovery</em> is the first one. Executing this first step gives us some interesting information about the MCP Server:</p>

<p><img src="/images/251202/image-2.png" alt="alt text" /></p>

<p>In the metadata we can see information such as the available scopes and the authentication- and token request endpoints. We can continue to run the authentication wizard, but it fails directly in the next step, since the MCP Server doesn’t support <a href="https://datatracker.ietf.org/doc/html/rfc7591">Dynamic Client Registration</a>, which it <a href="https://modelcontextprotocol.info/specification/draft/basic/authorization/#21-overview">SHOULD</a> according to the MCP specification. MCP Inspector is pretty strict, and since the MCP Server is not 100% up to spec, we need to explore other options.</p>

<p><img src="/images/251202/image-3.png" alt="alt text" /></p>

<p>But, in order to authenticate against the MCP Servers using OAuth2, some setup is required. We start by creating an <strong>App Registration</strong> in Entra ID. Make sure to select the second option that allows accounts in other organizations to use the API. Create the app registration and make a note of the “Application (client) ID”, which we will use later.</p>

<p><img src="/images/251202/image.png" alt="alt text" /></p>

<p>We are gonna be using Postman to execute the auth flow, so let’s add a Redirect URI that works with Postman. The platform type needs to be set to “Mobile and desktop applications”:</p>

<p><img src="/images/251202/image-4.png" alt="alt text" /></p>

<p>Then, we need to add some API permissions to our app registration. Go to the tab “APIs my organization uses” and find the Agent 365 API:s:</p>

<p><img src="/images/251202/image-5.png" alt="alt text" /></p>

<p>We can now select the permissions for the MCP Servers that we want to try. We’ll go for the Management, Word and Teams MCP Servers:</p>

<p><img src="/images/251202/image-6.png" alt="alt text" /></p>

<p>Now we can try to authenticate against the Management MCP Server, using Postman. Fill out the auth- and token endpoints according to what we got when we ran Metadata Discovery before. Set the scopes to <code class="language-plaintext highlighter-rouge">ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/McpServers.Management.All offline_access openid profile</code> since we want to try out the Management MCP Server:</p>

<p><img src="/images/251202/image-7.png" alt="alt text" /></p>

<p>Click ‘Get new access token’ and you should be able to authenticate in a browser, and consent to the requested scopes:</p>

<p><img src="/images/251202/image-8.png" alt="alt text" /></p>

<p>Once this is done, you should get an access token that can be used to access the Management MCP Server!</p>

<p><img src="/images/251202/image-9.png" alt="alt text" /></p>

<p>We can now go back to MCP Inspector, and enter the authentication token:</p>

<p><img src="/images/251202/image-10.png" alt="alt text" /></p>

<p>Don’t forget to add <code class="language-plaintext highlighter-rouge">Bearer</code>, before the token:</p>

<p><img src="/images/251202/image-11.png" alt="alt text" /></p>

<p>Click “Connect” and you should be able to access the Management MCP Server, and list its tools:</p>

<p><img src="/images/251202/image-12.png" alt="alt text" /></p>

<p>Now we can use the <code class="language-plaintext highlighter-rouge">GetMcpServers</code> tool to list all available tools and their details, such as endpoint addresses:</p>

<p><img src="/images/251202/image-13.png" alt="alt text" /></p>

<p>Great, now we have the address to the Word MCP Server. Let’s try it! Do the authentication dance in Postman again, but change the scope to the one for the Word MCP Server:</p>

<p><img src="/images/251202/image-15.png" alt="alt text" /></p>

<p>Get the token, connect and we can now access the Word MCP Server:</p>

<p><img src="/images/251202/image-16.png" alt="alt text" /></p>

<p>Let’s use it to create a Word document. We call the <code class="language-plaintext highlighter-rouge">WordCreateNewDocument</code> tool:</p>

<p><img src="/images/251202/image-17.png" alt="alt text" /></p>

<p>And lo and behold - a Word document has been created in our Onedrive!</p>

<p><img src="/images/251202/image-18.png" alt="alt text" /></p>

<p>…with the text we supplied as a parameter:</p>

<p><img src="/images/251202/image-19.png" alt="alt text" /></p>

<p>Amazing, now we have what we need to (hopefully) be able to call these MCP Servers from an agent created in Microsoft Agent Framework! Stay tuned, in my next blog post I intend to show how this was accomplished - as demonstrated in <a href="https://www.linkedin.com/posts/andreasadner_agent365-microsoftagentframework-activity-7401354948849889282-Nd2D?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAACM8rsBEgQIrYgb4NZAbnxwfDRk_Tu5e3w">this video</a>.</p>

<p>Check out my <a href="https://nullpointer.se/agent-365-mcp-servers-part-2.html">next blog post</a>, where I discuss how this agent is implemented.</p>

<p>Until next time, happy hacking!</p>]]></content><author><name>Andreas Adner</name></author><summary type="html"><![CDATA[Microsoft Agent 365 At Microsoft Ignite a couple of weeks ago, Microsoft accounced Agent 365 - the “control plane for AI agents”. Currently available only through the Frontier program, the long-term goal is that Agent 365 should be the framework for governing AI agents in the enterprise, at scale. So if you are into enterprise architecture and governance, Agent 365 is for you.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nullpointer.se/images/251202/splash.png" /><media:content medium="image" url="https://nullpointer.se/images/251202/splash.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The PPCS agent, dynamic C# code generation and the state of MCP</title><link href="https://nullpointer.se/ppcs-and-state-of-mcp.html" rel="alternate" type="text/html" title="The PPCS agent, dynamic C# code generation and the state of MCP" /><published>2025-11-18T00:00:00+00:00</published><updated>2025-11-18T00:00:00+00:00</updated><id>https://nullpointer.se/ppcs-and-state-of-mcp</id><content type="html" xml:base="https://nullpointer.se/ppcs-and-state-of-mcp.html"><![CDATA[<p><a href="/ppcs-and-state-of-mcp.html">
    <img src="/images/251117/splash.png" alt="The PPCS agent, dynamic C# code generation and the state of MCP" />
  </a></p>

<p>Last week was really fun, I had the privilege of presenting at the <a href="https://powerplatformsweden.se/">Power Platform Community Sweden (PPCS)</a> event in Stockholm on the 12th of November, and I took the chance to discuss some topics that have interested me over the last couple of months - the <a href="https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/">Microsoft 365 Agent SDK</a>, the <a href="https://learn.microsoft.com/en-us/microsoftteams/platform/teams-ai-library/welcome">Teams AI SDK</a> and the <a href="https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview">Microsoft Agent Framework</a>, as well as a few words about AI architecture.</p>

<!--end_excerpt-->

<p><a href="https://www.linkedin.com/in/saralagerquist/">Sara Lagerquist</a> is the driving force behind the Power Platform Community in Sweden, and it really is an amazing community that she has built, with so many nice people and an amazing vibe overall. 14-time MVP <a href="https://www.linkedin.com/in/gustafwesterlund/">Gustaf Westerlund</a> held a great presentation about Principal Object Access (POA), a very techical topic that he still managed to make very entertaining and fun! Kudos to Gustaf!</p>

<p>In my presentation I showed a lot of demos (videos - doing live-demos is scary), basically a “best of” what I have posted to LinkedIn in the last couple of months. If you are interested, here is a “supercut” with all the demos from the event:</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/GJLqc2VH9CA?si=DBsFGrOUnNiiEM5C" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>One of the main themes of my presentation was the creation of the “Power Platform Community Sweden” agent - a chatbot that could help Sara plan events and that can be deployed to a lot of different channels - like Claude Desktop, Teams, Copilot and a custom web app. I created a MCP Server for this purpose, that had a couple of simple tools that the agent can use:</p>

<ul>
  <li>CreateEvent</li>
  <li>CreateSpeaker</li>
  <li>AddSpeakerToEvent</li>
  <li>ExecuteFetch - for querying Dataverse using FetchXml.</li>
</ul>

<p>In the first demo I showed this MCP Server used from Claude Desktop. I gave Claude a list of past events and speakers from the <a href="https://powerplatformsweden.se/">PPCS website</a> and it added them to Dataverse. The code for the MCP Server can be found in <a href="https://github.com/adner/PPCS_251112/tree/main/PPCS_MCP">this repo</a>. The demo:</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/aRRVufV1UMw?si=ZtLRllMFHgbi66dK" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>Good thing it was a pre-recorded video that could be sped up, for the process of creating the events and speakers was painfully slow - it took almost eight minutes in total, and Claude Desktop was getting more and more sluggish as it went along, eventually almost grinding to a complete halt. Apparently, the really simple process of enumerating ~20 events and ~40 speakers and adding them to Dataverse in a loop, one at a time, using an MCP tool is real hard work for Claude. Why is that?</p>

<p>Perhaps this <a href="https://www.anthropic.com/engineering/code-execution-with-mcp">article from Anthropic</a> sheds some light on this issue. It discusses the challenge of using many MCP Servers and tools, and the consequence this has on the LLM context window. The article led to quite a lot of discussion - and if you are into internet flamewars <a href="https://www.youtube.com/watch?v=1piFEKA9XL0">this YouTube video</a> is worth a watch.</p>

<p>The article argues that MCP is so popular, agents often have quite a lot of MCP Servers connected to them, and the tool definitions get added to the context, potentially eating up so much context that the model spends more time parsing and interpreting tool metadata than actually solving the task at hand. And the results of tool calls, which may be large, also gets added to the context – and the result is a sluggish, barely usable agent. This sounds exactly like what happened in my demo: each individual MCP call was simple, but the cumulative overhead grew with every tool call, eventually overwhelming the context window. And it should be noted that I only had one (1) MCP Server. Not good, not good at all. Is there a better option?</p>

<p>The article outlines an interesting idea – instead of letting the LLM perform repeated MCP tool calls, let the LLM dynamically write code that executes the tool calls and handles the results. That way, the tool definitions and tool call results are kept out of the context window. Pure genius! But does it work?</p>

<p>I wanted to try it out for myself, so I <a href="https://github.com/adner/McpCodeGenTest/tree/main/PPCS_MCP">rewrote the PPCS MCP Server</a> from my demo and modified it so that it only has one tool - <code class="language-plaintext highlighter-rouge">RunScript</code> - that can be used to run C# code that has been dynamically generated by the LLM. The <code class="language-plaintext highlighter-rouge">ScriptRunnerLib</code> contains a <a href="https://github.com/adner/McpCodeGenTest/blob/main/ScriptRunnerLib/ScriptRunnerLib.cs">small library</a> that makes it possible to execute this code at runtime. The secret sauce here is the <a href="https://github.com/dotnet/roslyn/">Roslyn</a> C# Scripting SDK that makes it easy to run C# code on the fly at runtime, such a cool library!</p>

<p>So, I hooked up the new and improved MCP Server to Claude and the results were actually quite amazing! Instead of 8 minutes, it took only 41 seconds to add all events and speakers:</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/44vCppI1brI?si=A3jzT1pRLdSet1sD" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>To make sure that the agent generated C# code that plays well with the Roslyn C# compiler, I gave it <a href="https://github.com/adner/McpCodeGenTest/blob/main/AgentSDK/agent_instructions_codegen.md">very detailed (and verbose) instructions</a> - a couple of thousand tokens worth.</p>

<p>The execution in Claude Desktop was much faster, to be sure, but how can we know that it actually consumes less tokens? Claude Desktop doesn’t output the amount of tokens spent, so we’ll have to try it out in a different way. So, I went ahead and created two agents using the <a href="https://github.com/microsoft/agent-framework">Microsoft Agent Framework</a>, one agent that calls the tools one by one, MCP-style, and one that does the tool calls from dynamically generated code instead. The code for both the agents can be found <a href="https://github.com/adner/McpCodeGenTest/tree/main/AgentSDK">here</a>.</p>

<p>We give the agents the following prompt, and then feed it a <a href="https://github.com/adner/McpCodeGenTest/blob/main/AgentSDK/past_events.txt">text file with all the speakers and events</a>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Please go through these past events and create the Events and Speakers in Dataverse. When done with all, just say 'Done!
</code></pre></div></div>
<p>Let’s run both agents!</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Running</span> <span class="n">tool</span> <span class="n">calling</span> <span class="n">agent</span><span class="p">...</span>
<span class="p">-</span> <span class="n">Tool</span> <span class="n">Call</span><span class="p">:</span> <span class="err">'</span><span class="n">CreateEvent</span><span class="err">'</span> <span class="p">(</span><span class="n">Args</span><span class="p">:</span> <span class="p">[</span><span class="n">eventName</span> <span class="p">=</span> <span class="n">MALM</span><span class="err">Ö</span><span class="p">,</span> <span class="n">Fellowmind</span><span class="p">],[</span><span class="n">location</span> <span class="p">=</span> <span class="n">MALM</span><span class="err">Ö</span><span class="p">],[</span><span class="n">eventDate</span> <span class="p">=</span> <span class="m">2025</span><span class="p">-</span><span class="m">09</span><span class="p">-</span><span class="m">25</span><span class="n">T00</span><span class="p">:</span><span class="m">00</span><span class="p">:</span><span class="m">00</span><span class="n">Z</span><span class="p">]</span>
<span class="p">-</span> <span class="n">Tool</span> <span class="n">Call</span><span class="p">:</span> <span class="err">'</span><span class="n">CreateSpeaker</span><span class="err">'</span> <span class="p">(</span><span class="n">Args</span><span class="p">:</span> <span class="p">[</span><span class="n">firstname</span> <span class="p">=</span> <span class="n">Danijel</span><span class="p">],[</span><span class="n">lastname</span> <span class="p">=</span> <span class="n">Buljat</span><span class="p">]</span>
<span class="p">-</span> <span class="n">Tool</span> <span class="n">Call</span><span class="p">:</span> <span class="err">'</span><span class="n">CreateSpeaker</span><span class="err">'</span> <span class="p">(</span><span class="n">Args</span><span class="p">:</span> <span class="p">[</span><span class="n">firstname</span> <span class="p">=</span> <span class="n">David</span><span class="p">],[</span><span class="n">lastname</span> <span class="p">=</span> <span class="n">Sch</span><span class="err">ü</span><span class="n">tzer</span><span class="p">]</span>
<span class="p">-</span> <span class="n">Tool</span> <span class="n">Call</span><span class="p">:</span> <span class="err">'</span><span class="n">CreateEvent</span><span class="err">'</span> <span class="p">(</span><span class="n">Args</span><span class="p">:</span> <span class="p">[</span><span class="n">eventName</span> <span class="p">=</span> <span class="n">STOCKHOLM</span><span class="p">,</span> <span class="n">Xlent</span><span class="p">],[</span><span class="n">location</span> <span class="p">=</span> <span class="n">STOCKHOLM</span><span class="p">],[</span><span class="n">eventDate</span> <span class="p">=</span> <span class="m">2025</span><span class="p">-</span><span class="m">09</span><span class="p">-</span><span class="m">03</span><span class="n">T00</span><span class="p">:</span><span class="m">00</span><span class="p">:</span><span class="m">00</span><span class="n">Z</span><span class="p">]</span>
<span class="p">-</span> <span class="n">Tool</span> <span class="n">Call</span><span class="p">:</span> <span class="err">'</span><span class="n">CreateSpeaker</span><span class="err">'</span> <span class="p">(</span><span class="n">Args</span><span class="p">:</span> <span class="p">[</span><span class="n">firstname</span> <span class="p">=</span> <span class="n">Marica</span><span class="p">],[</span><span class="n">lastname</span> <span class="p">=</span> <span class="n">Lagerheim</span><span class="p">]</span>
<span class="p">...</span>
<span class="p">-</span> <span class="n">Tool</span> <span class="n">Call</span><span class="p">:</span> <span class="err">'</span><span class="n">CreateSpeaker</span><span class="err">'</span> <span class="p">(</span><span class="n">Args</span><span class="p">:</span> <span class="p">[</span><span class="n">firstname</span> <span class="p">=</span> <span class="n">Mark</span><span class="p">],[</span><span class="n">lastname</span> <span class="p">=</span> <span class="n">Smith</span><span class="p">]</span>
<span class="p">-</span> <span class="n">Tool</span> <span class="n">Call</span><span class="p">:</span> <span class="err">'</span><span class="n">CreateSpeaker</span><span class="err">'</span> <span class="p">(</span><span class="n">Args</span><span class="p">:</span> <span class="p">[</span><span class="n">firstname</span> <span class="p">=</span> <span class="n">Chris</span><span class="p">],[</span><span class="n">lastname</span> <span class="p">=</span> <span class="n">Huntingford</span><span class="p">]</span>
<span class="n">Done</span><span class="p">!</span>

<span class="p">-</span> <span class="n">Input</span> <span class="nf">Tokens</span> <span class="p">(</span><span class="n">Streaming</span><span class="p">):</span> <span class="m">5584</span>
<span class="p">-</span> <span class="n">Output</span> <span class="nf">Tokens</span> <span class="p">(</span><span class="n">Streaming</span><span class="p">):</span> <span class="m">1911</span> <span class="p">(</span><span class="m">0</span> <span class="n">was</span> <span class="n">used</span> <span class="k">for</span> <span class="n">reasoning</span><span class="p">)</span>
<span class="p">-</span> <span class="n">Total</span> <span class="nf">tokens</span> <span class="p">(</span><span class="n">Streaming</span><span class="p">):</span> <span class="m">7495</span>
<span class="n">Tool</span> <span class="n">Calling</span> <span class="n">Agent</span> <span class="n">Elapsed</span> <span class="n">Time</span><span class="p">:</span> <span class="m">34313</span><span class="nf">ms</span> <span class="p">(</span><span class="m">34</span><span class="p">,</span><span class="m">31</span><span class="n">s</span><span class="p">)</span>

<span class="n">Running</span> <span class="n">code</span> <span class="n">gen</span> <span class="n">agent</span><span class="p">...</span>
<span class="p">-</span> <span class="n">Tool</span> <span class="n">Call</span><span class="p">:</span> <span class="err">'</span><span class="n">RunScript</span><span class="err">'</span> <span class="p">(</span><span class="n">Args</span><span class="p">:</span> <span class="p">[</span><span class="n">code</span> <span class="p">=</span> <span class="k">using</span> <span class="nn">System</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Collections.Generic</span><span class="p">;</span>

<span class="c1">// Helper to create a speaker and collect result</span>
<span class="kt">string</span> <span class="nf">CreateSpeakerSafe</span><span class="p">(</span><span class="kt">string</span> <span class="n">first</span><span class="p">,</span> <span class="kt">string</span> <span class="n">last</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">res</span> <span class="p">=</span> <span class="nf">CreateSpeaker</span><span class="p">(</span><span class="n">first</span><span class="p">,</span> <span class="n">last</span><span class="p">);</span>
    <span class="k">return</span> <span class="s">$"Speaker </span><span class="p">{</span><span class="n">first</span><span class="p">}</span><span class="s"> </span><span class="p">{</span><span class="n">last</span><span class="p">}</span><span class="s">: </span><span class="p">{</span><span class="n">res</span><span class="p">}</span><span class="s">"</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// Helper to create an event and collect result</span>
<span class="kt">string</span> <span class="nf">CreateEventSafe</span><span class="p">(</span><span class="kt">string</span> <span class="n">name</span><span class="p">,</span> <span class="kt">string</span> <span class="n">location</span><span class="p">,</span> <span class="n">DateTime</span> <span class="n">date</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">res</span> <span class="p">=</span> <span class="nf">CreateEvent</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">location</span><span class="p">,</span> <span class="n">date</span><span class="p">);</span>
    <span class="k">return</span> <span class="s">$"Event </span><span class="p">{</span><span class="n">name</span><span class="p">}</span><span class="s"> (</span><span class="p">{</span><span class="n">location</span><span class="p">}</span><span class="s">, </span><span class="p">{</span><span class="n">date</span><span class="p">:</span><span class="n">yyyy</span><span class="p">-</span><span class="n">MM</span><span class="p">-</span><span class="n">dd</span><span class="p">}</span><span class="s">): </span><span class="p">{</span><span class="n">res</span><span class="p">}</span><span class="s">"</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">var</span> <span class="n">results</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;();</span>

<span class="c1">// 2025-09-25 MALMÖ, Fellowmind</span>
<span class="n">results</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="nf">CreateEventSafe</span><span class="p">(</span><span class="s">"Fellowmind"</span><span class="p">,</span> <span class="s">"Malmö"</span><span class="p">,</span> <span class="k">new</span> <span class="nf">DateTime</span><span class="p">(</span><span class="m">2025</span><span class="p">,</span> <span class="m">9</span><span class="p">,</span> <span class="m">25</span><span class="p">)));</span>
<span class="n">results</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="nf">CreateSpeakerSafe</span><span class="p">(</span><span class="s">"Danijel"</span><span class="p">,</span> <span class="s">"Buljat"</span><span class="p">));</span>
<span class="n">results</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="nf">CreateSpeakerSafe</span><span class="p">(</span><span class="s">"David"</span><span class="p">,</span> <span class="s">"Schützer"</span><span class="p">));</span>

<span class="p">...</span>

<span class="c1">// 2019-09-05, Claremont</span>
<span class="n">results</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="nf">CreateEventSafe</span><span class="p">(</span><span class="s">"Claremont"</span><span class="p">,</span> <span class="s">""</span><span class="p">,</span> <span class="k">new</span> <span class="nf">DateTime</span><span class="p">(</span><span class="m">2019</span><span class="p">,</span> <span class="m">9</span><span class="p">,</span> <span class="m">5</span><span class="p">)));</span>
<span class="n">results</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="nf">CreateSpeakerSafe</span><span class="p">(</span><span class="s">"Mark"</span><span class="p">,</span> <span class="s">"Smith"</span><span class="p">));</span>
<span class="n">results</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="nf">CreateSpeakerSafe</span><span class="p">(</span><span class="s">"Chris"</span><span class="p">,</span> <span class="s">"Huntingford"</span><span class="p">));</span>

<span class="c1">// Return all results so the host can see what happened</span>
<span class="n">results</span><span class="p">]</span>
<span class="n">Done</span><span class="p">!</span>

<span class="p">-</span> <span class="n">Input</span> <span class="nf">Tokens</span> <span class="p">(</span><span class="n">Streaming</span><span class="p">):</span> <span class="m">8902</span>
<span class="p">-</span> <span class="n">Output</span> <span class="nf">Tokens</span> <span class="p">(</span><span class="n">Streaming</span><span class="p">):</span> <span class="m">2186</span> <span class="p">(</span><span class="m">0</span> <span class="n">was</span> <span class="n">used</span> <span class="k">for</span> <span class="n">reasoning</span><span class="p">)</span>
<span class="p">-</span> <span class="n">Total</span> <span class="nf">tokens</span> <span class="p">(</span><span class="n">Streaming</span><span class="p">):</span> <span class="m">11088</span>
<span class="n">Code</span> <span class="n">Gen</span> <span class="n">Agent</span> <span class="n">Elapsed</span> <span class="n">Time</span><span class="p">:</span> <span class="m">59346</span><span class="nf">ms</span> <span class="p">(</span><span class="m">59</span><span class="p">,</span><span class="m">35</span><span class="n">s</span><span class="p">)</span>
</code></pre></div></div>
<p>Hmm, the tool calling agent took 34 seconds, and it consumed 7495 tokens. And the code-gen agent took 59 seconds and consumed 11088 tokens. Wait, what!? Why is the code-gen agent consuming more tokens, I thought MCP (tool-calling) was bad??</p>

<p>Actually, the LLM (GPT-5.1 in this case) is actually super smart. What actually seems to happen here is that the LLM does <strong>parallel tool calling</strong> and lets the client do all tool calls without passing the results back to the LLM! It is smart enough to figure out that the tool calls are actually “fire-and-forget” and it doesn’t have to act on the results! Kind of clever! It seems that GPT-5.1 is doing a much better job than Claude Desktop for this particular scenario.</p>

<p>So, what happens if we give the LLM explicit instructions to actually <strong>evaluate</strong> the result from all tool calls? Let’s change the prompt accordingly:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Please go through these past events and create the Events and Speakers in Dataverse. After creating each Event or Speaker, make sure that it returns 'OK', before continuing to the next one. When done with all, just say 'Done!'
</code></pre></div></div>
<p>Let’s run it again!</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Running</span> <span class="n">tool</span> <span class="n">calling</span> <span class="n">agent</span><span class="p">...</span>
<span class="p">-</span> <span class="n">Tool</span> <span class="n">Call</span><span class="p">:</span> <span class="err">'</span><span class="n">CreateSpeaker</span><span class="err">'</span> <span class="p">(</span><span class="n">Args</span><span class="p">:</span> <span class="p">[</span><span class="n">firstname</span> <span class="p">=</span> <span class="n">Danijel</span><span class="p">],[</span><span class="n">lastname</span> <span class="p">=</span> <span class="n">Buljat</span><span class="p">]</span>
<span class="p">-</span> <span class="n">Tool</span> <span class="n">Call</span><span class="p">:</span> <span class="err">'</span><span class="n">CreateSpeaker</span><span class="err">'</span> <span class="p">(</span><span class="n">Args</span><span class="p">:</span> <span class="p">[</span><span class="n">firstname</span> <span class="p">=</span> <span class="n">David</span><span class="p">],[</span><span class="n">lastname</span> <span class="p">=</span> <span class="n">Sch</span><span class="err">ü</span><span class="n">tzer</span><span class="p">]</span>
<span class="p">...</span>
<span class="p">-</span> <span class="n">Tool</span> <span class="n">Call</span><span class="p">:</span> <span class="err">'</span><span class="n">CreateEvent</span><span class="err">'</span> <span class="p">(</span><span class="n">Args</span><span class="p">:</span> <span class="p">[</span><span class="n">eventName</span> <span class="p">=</span> <span class="n">Sopra</span> <span class="n">Steria</span><span class="p">],[</span><span class="n">location</span> <span class="p">=</span> <span class="n">STOCKHOLM</span><span class="p">],[</span><span class="n">eventDate</span> <span class="p">=</span> <span class="m">2023</span><span class="p">-</span><span class="m">05</span><span class="p">-</span><span class="m">10</span><span class="n">T00</span><span class="p">:</span><span class="m">00</span><span class="p">:</span><span class="m">00</span><span class="p">]</span>
<span class="p">-</span> <span class="n">Tool</span> <span class="n">Call</span><span class="p">:</span> <span class="err">'</span><span class="n">CreateSpeaker</span><span class="err">'</span> <span class="p">(</span><span class="n">Args</span><span class="p">:</span> <span class="p">[</span><span class="n">firstname</span> <span class="p">=</span> <span class="n">Brian</span><span class="p">],[</span><span class="n">lastname</span> <span class="p">=</span> <span class="n">Stokes</span> <span class="n">och</span> <span class="n">Sara</span> <span class="n">Lagerquist</span><span class="p">]</span>

<span class="p">-</span> <span class="n">Input</span> <span class="nf">Tokens</span> <span class="p">(</span><span class="n">Streaming</span><span class="p">):</span> <span class="m">73907</span>
<span class="p">-</span> <span class="n">Output</span> <span class="nf">Tokens</span> <span class="p">(</span><span class="n">Streaming</span><span class="p">):</span> <span class="m">1281</span> <span class="p">(</span><span class="m">0</span> <span class="n">was</span> <span class="n">used</span> <span class="k">for</span> <span class="n">reasoning</span><span class="p">)</span>
<span class="p">-</span> <span class="n">Total</span> <span class="nf">tokens</span> <span class="p">(</span><span class="n">Streaming</span><span class="p">):</span> <span class="m">75188</span>
<span class="n">Tool</span> <span class="n">Calling</span> <span class="n">Agent</span> <span class="n">Elapsed</span> <span class="n">Time</span><span class="p">:</span> <span class="m">50376</span><span class="nf">ms</span> <span class="p">(</span><span class="m">50</span><span class="p">,</span><span class="m">38</span><span class="n">s</span><span class="p">)</span>

<span class="n">Running</span> <span class="n">code</span> <span class="n">gen</span> <span class="n">agent</span><span class="p">...</span>
<span class="p">-</span> <span class="n">Tool</span> <span class="n">Call</span><span class="p">:</span> <span class="err">'</span><span class="n">RunScript</span><span class="err">'</span> <span class="p">(</span><span class="n">Args</span><span class="p">:</span> <span class="p">[</span><span class="n">code</span> <span class="p">=</span> <span class="k">using</span> <span class="nn">System</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Collections.Generic</span><span class="p">;</span>

<span class="kt">string</span> <span class="nf">CreateSpeakerSafe</span><span class="p">(</span><span class="kt">string</span> <span class="n">first</span><span class="p">,</span> <span class="kt">string</span> <span class="n">last</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">res</span> <span class="p">=</span> <span class="nf">CreateSpeaker</span><span class="p">(</span><span class="n">first</span><span class="p">,</span> <span class="n">last</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">res</span> <span class="p">!=</span> <span class="s">"OK"</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">Exception</span><span class="p">(</span><span class="s">$"Failed to create speaker </span><span class="p">{</span><span class="n">first</span><span class="p">}</span><span class="s"> </span><span class="p">{</span><span class="n">last</span><span class="p">}</span><span class="s">: </span><span class="p">{</span><span class="n">res</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">res</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">string</span> <span class="nf">CreateEventSafe</span><span class="p">(</span><span class="kt">string</span> <span class="n">name</span><span class="p">,</span> <span class="kt">string</span> <span class="n">location</span><span class="p">,</span> <span class="n">DateTime</span> <span class="n">date</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">res</span> <span class="p">=</span> <span class="nf">CreateEvent</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">location</span><span class="p">,</span> <span class="n">date</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">res</span> <span class="p">!=</span> <span class="s">"OK"</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">Exception</span><span class="p">(</span><span class="s">$"Failed to create event </span><span class="p">{</span><span class="n">name</span><span class="p">}</span><span class="s">: </span><span class="p">{</span><span class="n">res</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">res</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// Helper to split full name into first/last (last word = last name)</span>
<span class="p">(</span><span class="kt">string</span> <span class="n">first</span><span class="p">,</span> <span class="kt">string</span> <span class="n">last</span><span class="p">)</span> <span class="nf">SplitName</span><span class="p">(</span><span class="kt">string</span> <span class="n">full</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">parts</span> <span class="p">=</span> <span class="n">full</span><span class="p">.</span><span class="nf">Trim</span><span class="p">().</span><span class="nf">Split</span><span class="p">(</span><span class="sc">' '</span><span class="p">,</span> <span class="n">StringSplitOptions</span><span class="p">.</span><span class="n">RemoveEmptyEntries</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">parts</span><span class="p">.</span><span class="n">Length</span> <span class="p">==</span> <span class="m">1</span><span class="p">)</span> <span class="k">return</span> <span class="p">(</span><span class="n">parts</span><span class="p">[</span><span class="m">0</span><span class="p">],</span> <span class="s">""</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">last</span> <span class="p">=</span> <span class="n">parts</span><span class="p">[^</span><span class="m">1</span><span class="p">];</span>
    <span class="kt">var</span> <span class="n">first</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="nf">Join</span><span class="p">(</span><span class="s">" "</span><span class="p">,</span> <span class="n">parts</span><span class="p">[..^</span><span class="m">1</span><span class="p">]);</span>
    <span class="k">return</span> <span class="p">(</span><span class="n">first</span><span class="p">,</span> <span class="n">last</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">void</span> <span class="nf">EnsureSpeakers</span><span class="p">(</span><span class="n">IEnumerable</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="n">speakers</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">full</span> <span class="k">in</span> <span class="n">speakers</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrWhiteSpace</span><span class="p">(</span><span class="n">full</span><span class="p">))</span> <span class="k">continue</span><span class="p">;</span>
        <span class="kt">var</span> <span class="p">(</span><span class="n">first</span><span class="p">,</span> <span class="n">last</span><span class="p">)</span> <span class="p">=</span> <span class="nf">SplitName</span><span class="p">(</span><span class="n">full</span><span class="p">);</span>
        <span class="nf">CreateSpeakerSafe</span><span class="p">(</span><span class="n">first</span><span class="p">,</span> <span class="n">last</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// 2025-09-25 MALMÖ, Fellowmind</span>
<span class="nf">CreateEventSafe</span><span class="p">(</span><span class="s">"2025-09-25 Fellowmind"</span><span class="p">,</span> <span class="s">"MALMÖ"</span><span class="p">,</span> <span class="k">new</span> <span class="nf">DateTime</span><span class="p">(</span><span class="m">2025</span><span class="p">,</span><span class="m">9</span><span class="p">,</span><span class="m">25</span><span class="p">));</span>
<span class="nf">EnsureSpeakers</span><span class="p">(</span><span class="k">new</span><span class="p">[]{</span><span class="s">"Danijel Buljat"</span><span class="p">,</span><span class="s">"David Schützer"</span><span class="p">});</span>

<span class="p">...</span>

<span class="c1">// 2019-09-05, Claremont</span>
<span class="nf">CreateEventSafe</span><span class="p">(</span><span class="s">"2019-09-05 Claremont"</span><span class="p">,</span> <span class="s">""</span><span class="p">,</span> <span class="k">new</span> <span class="nf">DateTime</span><span class="p">(</span><span class="m">2019</span><span class="p">,</span><span class="m">9</span><span class="p">,</span><span class="m">5</span><span class="p">));</span>
<span class="nf">EnsureSpeakers</span><span class="p">(</span><span class="k">new</span><span class="p">[]{</span><span class="s">"Mark Smith"</span><span class="p">,</span><span class="s">"Chris Huntingford"</span><span class="p">});</span>

<span class="k">return</span> <span class="s">"Done!"</span><span class="p">;]</span>
<span class="n">Done</span><span class="p">!</span>

<span class="p">-</span> <span class="n">Input</span> <span class="nf">Tokens</span> <span class="p">(</span><span class="n">Streaming</span><span class="p">):</span> <span class="m">5795</span>
<span class="p">-</span> <span class="n">Output</span> <span class="nf">Tokens</span> <span class="p">(</span><span class="n">Streaming</span><span class="p">):</span> <span class="m">1990</span> <span class="p">(</span><span class="m">0</span> <span class="n">was</span> <span class="n">used</span> <span class="k">for</span> <span class="n">reasoning</span><span class="p">)</span>
<span class="p">-</span> <span class="n">Total</span> <span class="nf">tokens</span> <span class="p">(</span><span class="n">Streaming</span><span class="p">):</span> <span class="m">7785</span>
<span class="n">Code</span> <span class="n">Gen</span> <span class="n">Agent</span> <span class="n">Elapsed</span> <span class="n">Time</span><span class="p">:</span> <span class="m">48390</span><span class="nf">ms</span> <span class="p">(</span><span class="m">48</span><span class="p">,</span><span class="m">39</span><span class="n">s</span><span class="p">)</span>
</code></pre></div></div>
<p>Now the tool calling agent consumes 73907 tokens! And the code-gen agent consumes 7785 tokens. That is 90% tokens saved!!! This aligns well with the “97% less tokens” claim from the article.</p>

<p>So, is this the death of MCP? Well, I really don’t think so and I think there should be ways to evolve the specification that could potentially address the current issues. For example making batch operations a part of the specification, and ways of doing tool calls that are excluded from to the context, among other things. And we shouldn’t forget that MCP is only part of the solution, much can probably be done at the LLM layer also with regard to tool calling.</p>

<p>Perhaps most important - make it possible to pass results between tool calls without polluting the context. I have the feeling that existing primitives in the MCP specification - especially <a href="https://modelcontextprotocol.io/specification/2025-06-18/server/resources">Resources</a> - have a role to play here. Imagine if an LLM could pass MCP Resource Links as parameters to subsequent tool calls, that would make parameter passing really light-weight!</p>

<p>So, MCP is probably not dead but it could use some new features to handle these kind of scenarios better.</p>

<p>Anyways, this was a fun experiment and I learned a thing or two about tool calling and context windows. As always, thanks for reading! The code for the examples above can be found <a href="https://github.com/adner/McpCodeGenTest">here</a>.  Until next time, happy hacking!</p>]]></content><author><name>Andreas Adner</name></author><summary type="html"><![CDATA[Last week was really fun, I had the privilege of presenting at the Power Platform Community Sweden (PPCS) event in Stockholm on the 12th of November, and I took the chance to discuss some topics that have interested me over the last couple of months - the Microsoft 365 Agent SDK, the Teams AI SDK and the Microsoft Agent Framework, as well as a few words about AI architecture.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nullpointer.se/images/251117/splash.png" /><media:content medium="image" url="https://nullpointer.se/images/251117/splash.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>