Skip to main content
Teams

Using MCP servers in your Teams agent to connect to external tools

Using MCP servers in your Teams agent to connect to external tools

Back when I built the SPAdminBot in 2017, every new capability meant the same dance: write a new dialog, add a new LUIS intent, wire up the API call, test it, deploy it. Want the bot to check storage usage? That’s a new dialog. Want it to search documents? Another dialog. Want it to do anything with Azure DevOps? Well, now you’re building a second bot or cramming two completely unrelated services into one increasingly messy codebase. I lived that life for years, and I suspect you have too.

Then last month I wrote about declarative agents and how you can build a Copilot agent with just a JSON manifest. That was a massive leap forward. But declarative agents are still tied to Copilot licenses and the Microsoft 365 ecosystem. What if you want a Teams bot that can dynamically discover and use tools from any service, without hardcoding a single API call? That’s where MCP comes in, and it changes the game fundamentally.

What is MCP

MCP stands for Model Context Protocol, and the simplest way I can explain it is this: it’s USB for AI agents. Remember when every printer needed its own proprietary cable and driver? USB standardized the connection. Plug in any device, and the computer just figures it out. MCP does the same thing for AI tools.

Instead of hardcoding API calls into your agent for every capability it needs, MCP lets the agent ask a tool server “what can you do?” at runtime. The server responds with a list of tools, each with a description and input schema. The AI reads those descriptions, and when a user asks a question, it picks the right tool automatically based on what the user wants. No LUIS intents. No dialog trees. No routing logic you have to maintain. The agent just figures it out.

This way you can add new capabilities to your agent without touching the agent code at all. You add a tool to the MCP server, and the agent discovers it on the next request. If that doesn’t excite you, you haven’t spent enough time maintaining bot code.

The Teams agent

Let’s get started with the actual implementation. We’re going to build a Teams agent that connects to two MCP servers, one for SharePoint operations and one for Azure DevOps. Two completely different services, one agent, zero hardcoded routing.

import { TeamsApp } from "@microsoft/teams-sdk";
import { McpClient } from "@microsoft/teams-sdk/mcp";

const app = new TeamsApp({
  appId: process.env.TEAMS_APP_ID!,
  appPassword: process.env.TEAMS_APP_PASSWORD!,
});

const sharePointMcp = new McpClient({
  serverUrl: "https://my-mcp-server.azurewebsites.net/mcp",
  authentication: {
    type: "bearer",
    tokenProvider: async () => await getAccessToken("https://graph.microsoft.com/.default"),
  },
});

const adoMcp = new McpClient({
  serverUrl: "https://my-mcp-server.azurewebsites.net/ado-mcp",
  authentication: {
    type: "bearer",
    tokenProvider: async () => await getAccessToken("https://app.vssps.visualstudio.com/.default"),
  },
});

app.ai.addMcpServer("sharepoint", sharePointMcp);
app.ai.addMcpServer("devops", adoMcp);

app.onMessage(async (context, state) => {
  await app.ai.run(context, state);
});

app.start();

That’s the entire agent. No switch statement routing messages to the right handler. No intent recognition. No conversation state machine. You register your MCP servers, and the AI layer in the Teams SDK takes care of the rest. When a user types “find documents about Contoso,” the agent looks at the available tools from both servers, sees that search_documents on the SharePoint MCP matches, and calls it. When someone says “create a bug for the login issue,” it routes to the DevOps MCP’s create_work_item tool. Automatically, based on the tool descriptions. I’m not going to pretend I didn’t spend a few minutes just staring at this the first time it worked.

Building an MCP server

Of course, the agent is only as useful as the tools behind it. Let’s build the SharePoint MCP server that powers the SharePoint side of our agent. If you’ve set up an Entra Agent ID for your agent, the authentication tokens will flow through cleanly here.

import { McpServer, Tool } from "@modelcontextprotocol/sdk";

const server = new McpServer({
  name: "sharepoint-tools",
  version: "1.0.0",
});

server.addTool({
  name: "search_documents",
  description: "Search for documents across SharePoint sites by keyword",
  inputSchema: {
    type: "object",
    properties: {
      query: { type: "string", description: "Search keywords" },
      siteUrl: { type: "string", description: "Optional: limit to a specific site" },
    },
    required: ["query"],
  },
  handler: async ({ query, siteUrl }) => {
    const graphClient = getGraphClient();
    const searchUrl = siteUrl
      ? `/sites/${encodeURIComponent(siteUrl)}/search/query`
      : "/search/query";

    const results = await graphClient.api(searchUrl).post({
      requests: [{ entityTypes: ["driveItem"], query: { queryString: query } }],
    });

    return results.value[0].hitsContainers[0].hits.map((hit: any) => ({
      name: hit.resource.name,
      url: hit.resource.webUrl,
      lastModified: hit.resource.lastModifiedDateTime,
    }));
  },
});

server.addTool({
  name: "get_site_storage",
  description: "Get storage usage for a SharePoint site",
  inputSchema: {
    type: "object",
    properties: {
      siteUrl: { type: "string", description: "The SharePoint site URL" },
    },
    required: ["siteUrl"],
  },
  handler: async ({ siteUrl }) => {
    const graphClient = getGraphClient();
    const site = await graphClient
      .api(`/sites/${encodeURIComponent(siteUrl)}`)
      .select("id,displayName,siteCollection")
      .get();

    const drive = await graphClient.api(`/sites/${site.id}/drive`).select("quota").get();

    return {
      siteName: site.displayName,
      storageUsedMB: Math.round(drive.quota.used / 1024 / 1024),
      storageTotalMB: Math.round(drive.quota.total / 1024 / 1024),
      percentUsed: Math.round((drive.quota.used / drive.quota.total) * 100),
    };
  },
});

server.start();

The key thing to notice here is that each tool is completely self-describing. The description field is what the AI uses to decide when to call the tool. The inputSchema tells it what parameters to extract from the user’s message. The handler does the actual work. Adding a new capability to your agent is now as simple as adding another server.addTool() call to this server. No changes to the agent. No redeployment of the bot. The agent discovers the new tool on its next request and starts using it.

First off, I want to point out how different this is from the SPAdminBot days. Back then, adding “get site storage” would have meant a new LUIS intent, a new dialog class, a new Graph API call wired into the dialog, and a deployment. Here it’s one function on the MCP server. The bot doesn’t even know the difference.

When MCP is overkill

Now, I need to be honest with you. If your bot does exactly one thing (say, it posts a daily standup summary) MCP is adding complexity you don’t need. You’re introducing a network round trip to a tool server, a protocol negotiation step, and another service to deploy and monitor. For a single-purpose bot, just call the API directly and move on with your life.

MCP shines when you need a multi-tool agent that connects to different services and where the set of capabilities might grow over time. It’s the difference between building a dedicated tool and building a workbench. If your users are going to ask questions that span SharePoint, DevOps, ServiceNow, and whatever else your organization runs, MCP is exactly the right pattern. If they’re going to ask one type of question, it’s overhead.

Also consider latency. Each MCP call is a network round trip. If you’re chaining multiple tools together in a single user request, those round trips add up. For internal enterprise scenarios this is usually fine. For high-throughput, low-latency scenarios, you’ll want to think carefully about whether the flexibility is worth the cost.

What’s next

MCP gives agents the ability to use tools. The next frontier is agents talking to each other. Google’s A2A (Agent-to-Agent) protocol is getting a lot of attention, and Microsoft is paying close attention too. Imagine your Teams agent not just calling tools, but delegating entire tasks to specialized agents (a procurement agent, a compliance agent, an IT support agent) each with their own MCP tool servers. We’re not there yet, but we’re closer than you’d think.