Work IQ as MCP Tools: Ten Verbs, Every Path, and an Agent That Discovers Its Own Schema

0
(0)

Part 3 of a series on building agents with the Microsoft Work IQ API.

  • Part 1 covered what Work IQ is and made a first grounded call.
  • Part 2 built a conversational agent with the A2A SDK, multi-turn context, and streaming.

For two parts now, our agent has been doing something very specific: delegating. Over A2A, my code hands a whole question to Work IQ — “what meetings do I have today?” — and Work IQ does everything: retrieval, reasoning, synthesis, citations. My agent is essentially a well-authenticated messenger.

That’s the right model when you want Copilot-quality answers with zero orchestration on your side. But it’s the wrong model when your agent is the one doing the thinking — when you’ve got your own LLM loop in Copilot Studio, Microsoft Foundry, or a custom orchestrator, and what you need from Microsoft 365 isn’t an answer, it’s hands: read these messages, check that schema, create this event, send that mail.

That’s what the Work IQ MCP server is for, and it’s the part of Work IQ I find most interesting as an engineering artifact, because Microsoft used it to solve a problem every MCP builder eventually hits: tool sprawl.

Delegation vs. tools

Worth making the distinction concrete before we touch code, because it changes how you design the agent:

A2A (Parts 1–2)MCP (this part)
Who reasons?Work IQ (Copilot’s stack)Your agent’s model
What you get backA finished, cited answerRaw data / operation results
GranularityOne task in, one result outIndividual reads, writes, actions
Best for“Answer this for the user”“Give my agent capabilities across M365”

The two aren’t exclusive, and the MCP server knows it: one of its ten tools is ask, which invokes Microsoft 365 Copilot for a natural-language answer — effectively A2A’s whole value proposition packaged as a single tool call. So a tools-based agent can do its own fine-grained work and punt a hard synthesis question to Copilot when that’s the better move. We’ll use that in the demo.

One name, three servers (read this before you configure anything)

Here’s this part’s version of Part 1’s “the README hasn’t caught up” warning. When you go looking for “the Work IQ MCP server,” you’ll find three different things wearing that name, and mixing them up will cost you an afternoon:

SurfaceWhat it isEndpointStatus
Remote Work IQ MCP serverOne endpoint, the 10 generic tools this post is abouthttps://workiq.svc.cloud.microsoft/mcpGA (part of the June 16 API GA, consumptive billing)
Local Work IQ MCP serverThe WORKING CLI (npm install -g @microsoft/workiq, then workiq mcp) running as a local stdio server, built for IDEs and coding assistantsstdio, on your machinePublic preview
Agent 365 “Work IQ MCP servers”A catalog of per-workload, task-shaped servers — Work IQ Mail, Work IQ Calendar, Work IQ Teams, Work IQ SharePoint, Work IQ OneDrive, Work IQ User, Work IQ Word, Work IQ Copilot — governed through the Agent 365 control planehttps://agent365.svc.cloud.microsoft/agents/tenants/{tenantId}/servers/mcp_MailTools (and siblings)Preview, currently requires a Microsoft 365 Copilot license

The first one is the developer API surface — the sibling of the A2A gateway we’ve been using, same workiq.svc.cloud.microsoft host, same delegated auth model. The third one is a different product motion: pre-certified, admin-governed servers that show up in the Copilot Studio and Foundry tool catalogs, with each server mapped to a permission on the Agent 365 application. Different endpoints, different licensing (for now), different governance story.

The Copilot Studio docs are explicit that the Agent 365 flavor is preview and that GA will move it to the same usage-based billing as the rest of Work IQ. Until then: the 10-tool remote server is the GA thing you can build on today; the catalog servers are the preview thing your low-code colleagues will click on. This post covers both, clearly labeled.

The ten tools

The remote server exposes exactly ten tools, in three groups:

GroupToolsWhat they do
Entityfetch, create_entity, update_entity, delete_entity, do_action, call_functionCRUD and actions on Microsoft 365 resources
Copilotask, list_agentsInvoke Microsoft 365 Copilot (or another agent) for reasoning
Schemaget_schema, search_pathsDiscover available paths and their schemas at runtime

(A naming note: Microsoft’s marketing and the Build sessions say “getSchema”; the actual tool names on the wire are snake_case, get_schema, search_paths, create_entity. Go by the tool reference, which is what the server actually registers.)

The design is built on three principles the docs state outright, and they’re worth internalizing because they explain every API choice:

  1. Fewer tools, more paths. The tools are verbs; resource paths are the nouns. fetch /me/messages reads mail. create_entity with parentUrl: /me/events creates a calendar event. do_action on /me/messages/{id}/ send sends a draft. When Microsoft adds a new workload, they add paths — the tool surface stays at ten, forever. If you’ve ever watched an agent’s context window drown in 200 tool definitions, you understand why this matters.
  2. Introspection over enumeration. Instead of shipping your agent a schema for every M365 type, the agent asks at runtime: search_paths to find what exists, get_schema to learn its shape. The agent stays accurate without you redeploying anything when the API surface changes.
  3. Policy over scopes. Rather than hundreds of static OAuth scopes, a small set of broad permissions establishes the boundary and a Rego-based policy engine evaluates every request — path, method, user, content — server-side. (Much more on this in Part 4, because it’s also the cost-control story.)

The paths, if they look familiar, should: they map to Microsoft Graph v1.0 endpoints. The responses even come back with @odata.context pointing at graph.microsoft.com. So your existing Graph knowledge transfers directly — what changes is that calls go through Work IQ’s gateway, where the policy engine, logging, and per-tenant limits live.

The discovery loop

This is the pattern that makes the whole thing click, so let’s trace it end to end. Say the agent has decided it needs to create a calendar event and has never seen the events API before.

Step 1: find the path:

JSON
{
  "method": "tools/call",
  "params": { "name": "search_paths", "arguments": { "filter": "events" } }
}

Back comes a list of matching paths, each with its supported operations (fetch, create, update).

Step 2: learn the shape:

JSON
{
  "method": "tools/call",
  "params": {
    "name": "get_schema",
    "arguments": { "path": "/me/events", "operationType": "create" }
  }
}

Back comes a JSON Schema (or TypeScript definitions, if you pass “format”: “typescript” — a nice touch, since models are demonstrably good at reading TS). Note the current limitation, stated plainly in the docs: only Microsoft Graph v1.0 schemas are available today. The backend parameter is marked “reserved for future use,” which tells you where this is heading — more backends, same ten verbs.

Step 3: act:

JSON
{
  "method": "tools/call",
  "params": {
    "name": "create_entity",
    "arguments": {
      "parentUrl": "/me/events",
      "jsonBody": "{ \"subject\": \"Contoso migration review\" }"
    }
  }
}

That’s the loop: discover → introspect → act. No pre-baked tool definitions, no OpenAPI files checked into your repo, no retraining when Graph adds a property.

Guardrails you’ll hit (better to know now)

The tool reference documents a set of limits that are exactly the kind of thing you discover at 11 PM otherwise:

  • jsonBody is a JSON-encoded string, not a JSON object. “jsonBody”: “{ \”subject\”: \”Hi\” }” — with the quotes escaped. Pass an object and the call fails. This is the A2A-Version header of Part 3: one line, hours of confusion available.
  • Collection reads are capped by policy. A default $top=25 is injected if you don’t specify one, with a hard max of 100. Teams chat messages are capped at 10 per request.
  • $skip and $skiptoken are blocked. Page with @odata.nextLink semantics instead of offset paging.
  • Path allowlist. By default /me/, /users/, and /sites/ prefixes are allowed; /authentication/ and /servicePrincipals/ segments are blocked. The full list is per-tenant policy, so your tenant may differ.
  • No automatic retries. Errors pass through with the downstream status code and Retry-After headers — retry policy is your client’s job.

Auth, briefly

Same model as the rest of the series, with one MCP-specific wrinkle. The server is protected by Entra ID, delegated-only, and advertises its auth configuration via the standard /.well-known/oauth-protected-resource discovery endpoint — which is how MCP-native clients (VS Code, Claude Code, Copilot CLI) light up OAuth automatically when you point them at the URL.

For our own code, we do what we’ve done all series: acquire a delegated token for api://workiq.svc.cloud.microsoft/.default with MSAL and attach it as a bearer header. The permissions reference currently lists a single delegated permission — WorkIQAgent.Ask, admin consent required — as covering the Work IQ APIs.

One flag, in the spirit of not asserting what I can’t verify: the MCP overview says capability is gated by “four broad OAuth permissions,” but the permissions reference only documents WorkIQAgent.Ask as of this writing. I read that as the permissions doc trailing the design (the other permissions presumably surface as more of the Tools domain rolls out), but I haven’t found the other three named anywhere official. If your create_entity calls fail with a consent error that WorkIQAgent.Ask doesn’t explain, this gap is the first place to look — and check whether the permissions page has been updated since July 2026.

Also: the Work IQ service principal is normally created in your tenant automatically on first use — the same enablement step we handled in Part 1, so I won’t repeat it. The MCP docs add one corner case: if an admin tries to configure Work IQ MCP policy in the admin center before anyone has used MCP in the tenant, the service principal may not exist yet and the configuration fails. Provisioning it manually (Part 1’s az ad sp create step) fixes that.

Let’s build: a console agent that discovers its own tools

Time for code. We’ll build a small console app that connects to the remote server with the official MCP C# SDK and runs the full loop: list the ten tools, discover a path, pull its schema, read real data, ask Copilot to reason about it, and finish by writing something — a draft email — into Microsoft 365. It’s deliberately structured as a demo script: each act prints what it’s about to do, so it narrates itself on a projector.

PowerShell
dotnet new console -n Part3.WorkIQMcp
cd Part3.WorkIQMcp
dotnet add package ModelContextProtocol
dotnet add package Microsoft.Identity.Client

The ModelContextProtocol package is the official C# SDK, maintained with Microsoft, and it went stable this year — no –prerelease gymnastics this time. Here’s the .csproj pinned to what I’m using:

XML
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="ModelContextProtocol" Version="1.4.0" />
    <PackageReference Include="Microsoft.Identity.Client" Version="4.84.2" />
  </ItemGroup>

</Project>

Connect

Auth is identical to Parts 1 and 2 — the only new piece is handing the token to the MCP transport instead of a raw HttpClient:

C#
using Microsoft.Identity.Client;
using ModelContextProtocol.Client;

const string ClientId = "<your-app-client-id>";
const string TenantId = "<your-tenant-id>";
const string Scope    = "api://workiq.svc.cloud.microsoft/.default";
const string Endpoint = "https://workiq.svc.cloud.microsoft/mcp";

var app = PublicClientApplicationBuilder
    .Create(ClientId)
    .WithAuthority($"https://login.microsoftonline.com/{TenantId}")
    .WithDefaultRedirectUri()
    .Build();

var auth = await app.AcquireTokenInteractive([Scope]).ExecuteAsync();

var transport = new HttpClientTransport(new HttpClientTransportOptions
{
    Endpoint = new Uri(Endpoint),
    AdditionalHeaders = new Dictionary<string, string>
    {
        ["Authorization"] = $"Bearer {auth.AccessToken}"
    }
});

await using var mcp = await McpClient.CreateAsync(transport);

HttpClientTransport speaks Streamable HTTP (with SSE fallback) and auto-detects which the server wants — you don’t configure any of that.

Act 1 — enumerate the tools

This alone is a nice moment on a community call: the entire Microsoft 365 surface, and the list fits on one slide.

C#
Console.WriteLine("== The ten tools ==");
foreach (var tool in await mcp.ListToolsAsync())
    Console.WriteLine($"  {tool.Name,-16} {tool.Description}");

Act 2 — discover and introspect

C#
var paths = await mcp.CallToolAsync("search_paths",
    new Dictionary<string, object?> { ["filter"] = "messages" });
PrintResult("search_paths(\"messages\")", paths);

var schema = await mcp.CallToolAsync("get_schema",
    new Dictionary<string, object?>
    {
        ["path"] = "/me/messages",
        ["operationType"] = "create",
        ["format"] = "typescript"   // TS is compact and model-friendly
    });
PrintResult("get_schema(/me/messages, create)", schema);

Act 3 — read, reason, write

C#
// Read: newest messages (policy injects $top=25 if you don't say otherwise)
var inbox = await mcp.CallToolAsync("fetch",
    new Dictionary<string, object?>
    {
        ["entityUrls"] = new[] { "/me/messages?$top=5&$select=subject,from,receivedDateTime" }
    });
PrintResult("fetch(/me/messages)", inbox);

// Reason: hand the hard part to Copilot — this is A2A's superpower as a tool call
var answer = await mcp.CallToolAsync("ask",
    new Dictionary<string, object?>
    {
        ["question"] = "Which of my unread emails from this week most needs a reply, and why?",
        ["timeZone"] = TimeZoneInfo.Local.Id
    });
PrintResult("ask(...)", answer);

// Write: draft the reply as a real artifact in Outlook.
// jsonBody is a JSON *string* — serialize, don't pass an object.
var draftJson = System.Text.Json.JsonSerializer.Serialize(new
{
    subject = "Follow-up: Contoso migration",
    body = new { contentType = "text", content = "Drafted by my Work IQ MCP agent — review before sending." }
});

var draft = await mcp.CallToolAsync("create_entity",
    new Dictionary<string, object?>
    {
        ["parentUrl"] = "/me/messages",
        ["jsonBody"]  = draftJson
    });
PrintResult("create_entity(/me/messages)", draft);

Note what create_entity /me/messages does: it creates a draft, safely, without sending anything. Actually sending is a separate, deliberate do_action on /me/messages/{id}/send — the verb split maps neatly onto “things an agent may do freely” vs. “things you might want approval gates on.” Foundry’s approval workflow (below) hooks exactly that seam.

ask returns a conversationId alongside the response text — same idea as A2A’s contextId from Part 2: pass it back on your next ask call and Copilot treats it as one continuing conversation.

The PrintResult helper just unwraps the two places MCP results carry data — text content blocks and structuredContent — full version in the repo.

Run it, sign in, and you get the whole arc in about a minute: ten tools, live schema discovery, your actual inbox, a Copilot-reasoned answer, and a new draft sitting in Outlook as proof the write path works. That’s the community call demo, and every step of it runs against your own tenant with the same app registration from Part 1.

Wiring Work IQ into a Copilot Studio agent

Now the low-code path — this is the Agent 365 catalog flavor from the table above, so two flags before the steps: it’s preview, and it currently requires a Microsoft 365 Copilot license (the docs say GA will shift it to usage-based billing like everything else).

The wiring itself is genuinely simple:

  1. Sign in to Copilot Studio and open (or create) your agent.
  2. Tools tab → Add Tool.
  3. Pick Model Context Protocol — the Work IQ servers appear alongside other MCP servers.
  4. Search for the workload — type mail and select Work IQ Mail.
  5. Expand the connection dropdown → Create New ConnectionCreate, then sign in.
  6. Add and Configure, and you’re done.

Test it with a prompt that forces email context — the docs’ own example is telling the agent to send an email asking how a hands-on lab is going, then watching it comply. First run, you’ll get an Allow prompt when the agent wants to touch the connection; that’s the per-user consent surface.

Repeat for Work IQ Calendar, Work IQ Teams, and friends as your scenario needs. Notice what you’re not doing: registering an app, picking scopes, writing a connector. Each server is a permission on the Agent 365 application, admins can allow or block servers tenant-wide under Agents and Tools in the admin center, and every tool call is traceable in Defender’s Advanced Hunting. That’s the governance trade the catalog makes: less flexibility than the ten-verb server, much more admin comfort.

Wiring Work IQ into a Foundry agent

Two routes here, and it matters which you pick.

The catalog route (portal). Same Agent 365 servers, surfaced in Foundry’s tool catalog: in your Foundry project, create an agent, then under ToolsAdd, filter the catalog by Provider > Microsoft and look for the Microsoft 365 tool group (currently labeled “Microsoft 365 Frontier tools” in the catalog — User Profile, Outlook Calendar, Copilot Search). Connect one, keep the default endpoint and auth settings, and test in the playground with something like “Schedule a meeting with John for tomorrow at 4 PM” — approving each tool call as it’s requested. Preview caveats apply as above.

The pro-code route (any remote MCP server, including Work IQ’s). Foundry’s agent service has a first-class MCP tool type: you give it a server_label, a server_url, and an approval policy, and the Foundry-hosted model gets those tools. With the current C# SDK (Azure.AI.Projects plus Azure.AI.Extensions.OpenAI), the shape looks like this:

C#
using Azure.AI.Projects;
using Azure.AI.Extensions.OpenAI;
using Azure.Identity;

AIProjectClient projectClient = new(
    endpoint: new Uri("https://<resource>.ai.azure.com/api/projects/<project>"),
    tokenProvider: new DefaultAzureCredential());

DeclarativeAgentDefinition agentDefinition = new(model: "gpt-5-mini")
{
    Instructions = "You are an assistant with tool access to the user's Microsoft 365 data. " +
                   "Discover paths with search_paths and schemas with get_schema before acting.",
    Tools = { ResponseTool.CreateMcpTool(
        serverLabel: "workiq",
        serverUri: new Uri("https://workiq.svc.cloud.microsoft/mcp"),
        // Writes go through Work IQ; make a human approve each call.
        toolCallApprovalPolicy: new McpToolCallApprovalPolicy(
            GlobalMcpToolCallApprovalPolicy.AlwaysRequireApproval)) }
};

AgentVersion agentVersion = projectClient.AgentAdministrationClient.CreateAgentVersion(
    agentName: "workiq-tools-agent",
    options: new(agentDefinition));

When the model wants to call a tool, the response stream hands you an McpToolCallApprovalRequestItem — you inspect the tool name and arguments, then reply with an approval item to let it proceed. The full approve/deny loop is in the docs sample and in my repo.

Two honesty flags on this route:

  • The Foundry MCP tool is itself preview, and if you’ve been using the older Azure.AI.Agents.Persistent SDK (MCPToolDefinition, SubmitToolApprovalAction): that’s the classic agent service, which is deprecated with retirement scheduled for March 31, 2027. New code should use the AIProjectClient shape above.
  • The auth handshake is the hard part. Work IQ is delegated-only — every call must carry a user’s token. A Foundry-hosted agent runs service-side, so getting a per-user Work IQ token into that MCP connection means an On-Behalf-Of exchange and passing the header per-call (headers ride along with each tool approval). It’s plumbing you own, and it’s precisely the problem the Agent 365 catalog route solves with its managed connections — which is why, today, I’d use the catalog route in Foundry for user-facing agents and reserve the raw-endpoint route for backends where you control the token flow end to end.

Which flavor when?

My working rule after building all three:

  • Your own orchestrator / backend / console → remote 10-tool server with the MCP C# SDK. GA, maximum control, you own auth.
  • Copilot Studio or Foundry, user-facing → Agent 365 catalog servers. Preview, but the connection and governance plumbing is done for you.
  • Your IDE or coding agent → local workiq CLI server. It’s the same intelligence in your editor, and it’s how I sanity-check Work IQ behavior before writing any code against it.

 Where this is going

We’ve now covered three of Work IQ’s four domains: Chat (Parts 1–2), a taste of Context (Part 2’s aside), and Tools (this part). The pattern across all three: the intelligence and the governance live server-side, and the integration surface keeps getting smaller — from a protocol, to ten verbs.

Part 4 is the last domain, and the one long-running agents actually can’t live without: Workspaces — SharePoint Embedded working storage that gives an agent persistent state inside your tenant boundary instead of in some blob account your compliance team has never heard of. We’ll look at why DIY agent state is the same trap as DIY RAG, build a multi-session agent on what’s shipped today, and — fair warning — be honest about which parts of the Workspaces story have public docs yet. Plus the Rego policy engine and Copilot Credits cost controls, because a long-running agent is also a long-running bill.

Code for this part is in Part3.WorkIQMcp in the series repo. Next up: long-running agents with Workspaces.

How useful was this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.