Microsoft Teams

This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page was written on 2026-10-01 against the Teams SDK 2.1.x (@microsoft/teams.apps 2.1.0, microsoft-teams-apps 2.1.0 and teams.net 2.1.1; Python is now documented as generally available), the Microsoft 365 Agents SDK (@microsoft/agents-hosting 1.9.1 and microsoft-agents-hosting-core 1.7.0), Express 5.2.1, the LangChain 1.4.3 core on FastAPI 0.142.2 and the Spring AI 2.0.1 core on Spring Boot 4.1.1 (see the version baseline). The snippets are illustrative: they were written from the official Teams SDK, Agents Toolkit, Copilot Studio and Microsoft 365 Copilot documentation and are not compiled or run by this repository. Teams SDK method names for streaming, sign-in and Adaptive Cards were compared with the SDK documentation on that date; manifest and plugin schema versions change often, so take the exact version strings from the tooling you use. Admin-portal screens and menu names move between releases.

Teams is the channel where an internal assistant meets people already working: the same DocsAssistant core described in Channel-Agnostic Architecture can answer a question in a personal chat, a group chat or a channel. Unlike the channels earlier in this sub-section, Teams is a managed platform: you register an identity in Microsoft Entra, host an HTTPS endpoint, describe the app in a manifest and let a tenant administrator decide who may install it. This page covers that path, and the two quite different alternatives that need little or no hosted code: connecting the DocsAssistant MCP server to Copilot Studio or to a Microsoft 365 Copilot declarative agent.

Bot Framework is retired

For years the standard way to build a Teams bot was the Bot Framework SDK (botbuilder-js, botbuilder-dotnet, botbuilder-python, botbuilder-java) together with the Teams AI library and TeamsFx. As of this page’s date, the Bot Framework SDK repositories are archived and Microsoft has stated that support ended on 2025-12-31; TeamsFx is deprecated and its role is taken by the Microsoft 365 Agents Toolkit. Existing bots keep running on Azure Bot Service, which is not retired, but they no longer receive fixes, so new work should not start on Bot Framework and old bots should be planned for migration. Both replacement SDKs below document migration paths from legacy Bot Framework code and keep the same wire format, the Activity Protocol, so a migration mostly changes the handler code rather than the registration.

The names that remain, and what each one is now:

Name Role today

Azure Bot Service

The registration and channel-routing layer: it maps a Microsoft Entra identity to your messaging endpoint and to channels (Teams, Microsoft 365 Copilot, web chat, others). It is not an SDK.

Teams SDK (formerly the Teams AI library, v2)

A Teams-first SDK for C#, TypeScript and Python with handlers for messages, reactions, mentions, threads, dialogs, Adaptive Cards, streaming and user authentication.

Microsoft 365 Agents SDK

A channel-neutral SDK for C#, JavaScript and Python built around the Activity Protocol; the same agent can serve Teams, Microsoft 365 Copilot, Copilot Studio clients, web chat and other channels.

Microsoft 365 Agents Toolkit

The Visual Studio Code and Visual Studio tooling (successor of Teams Toolkit and TeamsFx) that scaffolds, provisions, debugs and packages an app.

Copilot Studio and declarative agents

Low-code and no-code routes that host the agent for you; both can consume MCP servers.

Choosing a route

There are four ways to put the DocsAssistant in front of Teams users. They differ in who hosts the orchestration (you or Microsoft) and in how much of the Teams experience you control.

Teams SDK Microsoft 365 Agents SDK Copilot Studio Declarative agent (Microsoft 365 Copilot)

You host

The bot web app and the model call (your DocsAssistant core).

The agent web app and the model call.

Nothing, except the MCP server (or connectors) the agent calls. Microsoft hosts the orchestration.

Nothing, except the MCP server or API. Microsoft 365 Copilot’s orchestrator answers.

Who calls the model

You (your core, LangChain or Spring AI)

You

Copilot Studio’s orchestrator, which calls your MCP tools

Microsoft 365 Copilot, which calls your MCP tools

Teams-native features

Deepest: mentions, reactions, quoted replies, threads, channel and meeting events, dialogs, message extensions

Core messaging; fewer Teams-specific helpers

Chat in Teams through a channel publish; limited customisation of the surface

Appears inside Microsoft 365 Copilot (including its Teams entry point), not as a separate bot identity

Reach beyond Teams

Teams, and Microsoft 365 Copilot as a custom engine agent

Teams, Microsoft 365 Copilot, web chat and other Activity Protocol channels

Teams, web, Microsoft 365 Copilot and more through Copilot Studio channels

Microsoft 365 Copilot only

Streaming answers

Yes, in one-to-one chats only

Check the SDK’s current streaming support for your language

Handled by the service

Handled by the service

Licensing and tenant needs

Azure subscription for the bot; users need Teams

Same

Copilot Studio capacity or licence; tenant policy for connectors

Microsoft 365 Copilot licence for each user, and tenant approval of the agent

Best when

The assistant is a first-class Teams participant in channels and group chats and you want your own RAG pipeline end to end.

One agent must serve several Microsoft channels with a modest feature set.

A team wants to wire the existing MCP server in with little code, with Power Platform governance.

Users already live in Microsoft 365 Copilot and the DocsAssistant should be one more capability there.

Microsoft’s own rule of thumb in the comparison page is that a bot using most of the Teams-native collaboration features (reactions, threaded replies, citations and similar) should use the Teams SDK, and a bot using few or none of them can use the Agents SDK. Both reach Microsoft 365 Copilot and both interoperate with Microsoft Entra. Because the DocsAssistant already exists as an MCP server (see RAG over MCP) and as an HTTP core, the practical sequence for most teams is: start with the MCP route (Copilot Studio or a declarative agent) to get value in days, and move to a Teams SDK bot when you need group-chat behaviour, your own streaming or fine control over citations.

flowchart LR U["Teams user"] --> T["Teams / Microsoft 365"] T -->|"Activity Protocol, HTTPS"| AB["Azure Bot Service
registration"] AB --> BOT["Teams SDK bot
(your web app)"] BOT --> CORE["DocsAssistant core
ask(conversationId, userId, text)"] T --> CS["Copilot Studio agent"] T --> DA["Declarative agent"] CS -->|"MCP, streamable HTTP"| MCP["DocsAssistant MCP server"] DA -->|"MCP plugin"| MCP CORE --> RAG["Retriever + model"] MCP --> RAG

Azure Bot registration and the Entra app

Every Teams bot needs an Azure Bot resource connected to a Microsoft Entra application registration. The bot resource tells the Bot Service where your messaging endpoint is; the Entra app is the identity the service and your code prove to each other. The Teams SDK documentation names the pieces as the app registration, the Azure configuration and the manifest; the Agents Toolkit can do all of them for you during Provision.

  1. Create the Entra app registration. New bots should be single tenant: Microsoft deprecated creating new multi-tenant bot registrations, and a single-tenant app with a managed identity or a certificate avoids a long-lived secret. The authentication background (client credentials for the bot, token validation of inbound activities) is in Client Credentials & Client Authentication and the tenant model in Azure Identity & Access.

  2. Create the Azure Bot resource (type single tenant or user-assigned managed identity), set its messaging endpoint to https://<host>/api/messages and enable the Microsoft Teams channel.

  3. Configure the bot application with the app ID, the tenant ID and either a client secret, a certificate or a managed identity. Keep a client secret, if you must use one, in Key Vault (Key Vault & Secrets) and inject it through the environment.

  4. Host the endpoint over HTTPS on Container Apps or App Service (Container Apps, App Service & Functions). Teams posts every activity there, so the endpoint must validate the bearer token on each request; the Teams SDK does this for you unless you turn authentication off for local testing, which must never reach production.

  5. For user sign-in (below) add an OAuth connection to the Azure Bot resource. Teams-managed bots do not support OAuth connections, so a bot that needs sign-in must be an Azure-managed one.

The environment the TypeScript app below expects, with obvious placeholders (never commit the real values):

CLIENT_ID=<entra-app-id>
CLIENT_SECRET=<from-key-vault-or-omit-with-managed-identity>
TENANT_ID=<tenant-id>
DOCS_ASSISTANT_URL=https://docs-assistant.internal.example.com
PORT=3978

The variable names above follow the Teams SDK convention in its templates (CLIENT_ID, CLIENT_SECRET, TENANT_ID); check the names your scaffolded project actually reads.

Manifest and sideloading with the Agents Toolkit

A Teams app is a zip file with a manifest.json, a colour icon and an outline icon. The manifest declares the app ID, the bot ID (the Entra app ID), the scopes in which the bot can be used (personal, team, groupChat), whether it supports files and calls, and the valid domains. A reduced bot manifest looks like this; use the schema version your tooling emits.

{
  "$schema": "https://developer.microsoft.com/json-schemas/teams/<version>/MicrosoftTeams.schema.json",
  "manifestVersion": "<version>",
  "version": "1.0.0",
  "id": "<teams-app-guid>",
  "developer": {
    "name": "Example Docs Team",
    "websiteUrl": "https://docs.example.com",
    "privacyUrl": "https://docs.example.com/privacy",
    "termsOfUseUrl": "https://docs.example.com/terms"
  },
  "name": { "short": "DocsAssistant", "full": "DocsAssistant for Example Docs" },
  "description": {
    "short": "Answers questions from the Example documentation.",
    "full": "Ask questions in plain language; answers cite the documentation pages they come from."
  },
  "icons": { "color": "color.png", "outline": "outline.png" },
  "accentColor": "#0F6CBD",
  "bots": [
    {
      "botId": "<entra-app-id>",
      "scopes": ["personal", "team", "groupChat"],
      "supportsFiles": false,
      "isNotificationOnly": false
    }
  ],
  "permissions": ["identity", "messageTeamMembers"],
  "validDomains": []
}

The Microsoft 365 Agents Toolkit (the Visual Studio Code extension, replacing Teams Toolkit) automates the loop: it scaffolds a project from a template, creates the Entra app and Azure Bot resource on Provision, writes the IDs into the environment files, starts a dev tunnel so Teams can reach your laptop, validates the manifest, builds the app package and uploads ("sideloads") it to your tenant for a debug session. Sideloading needs the tenant setting Custom App Upload to be enabled for your account, which administrators control; if the toolkit’s account pane does not show it as enabled, ask the tenant admin. For production, the same package is submitted for admin approval in the Microsoft 365 admin center or published to an organisation catalogue. The Teams SDK also ships a developer CLI that can create the app, download and upload the manifest and produce a sideload-ready zip; the Toolkit and the CLI are two front ends for the same steps.

A Teams SDK bot over the DocsAssistant core

The bot is an adapter in the sense of the architecture page: it maps the Teams user and conversation to userId and conversationId, calls the core and maps the core’s token | citation | done stream to what Teams can show. Teams gives the adapter a stable identity for free: the Microsoft Entra object ID (activity.from.aadObjectId) is the userId for access control (see RAG Access Control & Privacy), and the Teams conversation ID is the conversationId. Streaming is supported only in one-to-one chats; in channels and group chats the bot sends a typing indicator and then one final message.

To keep the TypeScript bot independent of how the core is written, it calls the core’s streaming HTTP contract (POST /v1/ask with Accept: text/event-stream, designed in Serving RAG as an API and the streaming chat APIs page). The same bot works unchanged against the LangChain/FastAPI core or the Spring AI/Spring Boot core, because both emit the same three event types. The service-to-service call authenticates with a token for the Teams adapter as a client of the core (client credentials); the end user’s identity travels in the request body or a token-exchange, never as a shared service account.

import express from 'express';
import http from 'http';
import { App, ExpressAdapter } from '@microsoft/teams.apps';

const CORE_URL = process.env.DOCS_ASSISTANT_URL!;
const CORE_TOKEN = process.env.DOCS_ASSISTANT_TOKEN!; // service token, from Key Vault

type CoreEvent =
  | { type: 'token'; text: string }
  | { type: 'citation'; marker: number; title: string; url: string }
  | { type: 'done'; conversationId: string };

async function* ask(
  conversationId: string,
  userId: string,
  text: string,
): AsyncGenerator<CoreEvent> {
  const res = await fetch(`${CORE_URL}/v1/ask`, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      accept: 'text/event-stream',
      authorization: `Bearer ${CORE_TOKEN}`,
    },
    body: JSON.stringify({ question: text, conversationId, userId }),
  });
  if (!res.ok || !res.body) throw new Error(`core returned ${res.status}`);

  const decoder = new TextDecoder();
  let buffer = '';
  for await (const chunk of res.body as any) {
    buffer += decoder.decode(chunk, { stream: true });
    let end: number;
    while ((end = buffer.indexOf('\n\n')) >= 0) {
      const frame = buffer.slice(0, end);
      buffer = buffer.slice(end + 2);
      const data = frame.split('\n').filter((l) => l.startsWith('data:'))
        .map((l) => l.slice(5).trim()).join('\n');
      if (data) yield JSON.parse(data) as CoreEvent;
    }
  }
}

const expressApp = express();
const server = http.createServer(expressApp);
const app = new App({ httpServerAdapter: new ExpressAdapter(server) });

app.on('message', async ({ activity, send, stream }) => {
  const userId = activity.from.aadObjectId ?? activity.from.id;
  const conversationId = activity.conversation.id;
  const isOneToOne = activity.conversation.conversationType === 'personal';

  const citations: { marker: number; title: string; url: string }[] = [];
  let answer = '';

  try {
    for await (const ev of ask(conversationId, userId, activity.text ?? '')) {
      if (ev.type === 'token') {
        answer += ev.text;
        if (isOneToOne) stream.emit(ev.text);       // informative streaming, 1:1 only
      } else if (ev.type === 'citation') {
        citations.push(ev);
      }
    }
  } catch {
    await send('Sorry, the documentation service is unavailable. Please try again in a minute.');
    return;
  }

  if (!isOneToOne) await send(answer);               // group chats and channels: one message
  // citations are attached as an Adaptive Card in the next section
});

await app.initialize();
server.listen(Number(process.env.PORT ?? 3978));

Details worth knowing:

  • Group chats and channels. The user must mention the bot in a channel; the activity text then starts with a mention entity that should be stripped before calling the core (the SDK has a helper for this; check the version you use). Teams delivers channel messages only when the bot is installed in that team.

  • Slow answers. A handler that runs for a long time ties up the request that carried the activity. Return quickly and continue in the background for answers that can exceed a few seconds, as in the async-job pattern on the architecture page.

  • Idempotency. Teams can redeliver an activity; use the activity ID as the dedupe key, as in the idempotent-webhook section of the architecture page.

  • Python and C#. The Teams SDK has the same handler model in Python (microsoft-teams-apps) and C# (teams.net); the Python and Java cores could also be hosted in the same process as the bot instead of being called over HTTP, but the separate service keeps one place for rate limits and tracing.

Adaptive Cards for citations

A citation list is the one rich element the DocsAssistant needs on every channel. In Teams an Adaptive Card renders it as a compact block with link buttons, and the SDK’s @microsoft/teams.cards package builds one with a fluent API. Keep the three rules from Citations & Grounding: only cite sources the retriever returned, render the title as the visible text and the URL as the action, and never put access-controlled quotes in a card that a group of people can read.

import { AdaptiveCard, TextBlock, ActionSet, OpenUrlAction } from '@microsoft/teams.cards';

function citationCard(citations: { marker: number; title: string; url: string }[]) {
  return new AdaptiveCard(
    new TextBlock('Sources', { weight: 'Bolder', size: 'Medium' }),
    ...citations.map((c) =>
      new TextBlock(`[${c.marker}] ${c.title}`, { wrap: true }),
    ),
    new ActionSet(
      ...citations.slice(0, 5).map((c) =>
        new OpenUrlAction(c.url, { title: `Open [${c.marker}]` }),
      ),
    ),
  );
}

// after the stream ends, in the same message handler:
if (citations.length > 0) {
  await send(citationCard(citations));
}

The class and option names OpenUrlAction, ActionSet and TextBlock options in the snippet follow the package’s builder style; the Teams SDK page shows AdaptiveCard, TextBlock and ActionSet with an ExecuteAction, so confirm the open-URL action’s exact name against the installed package. Adaptive Cards in Teams support a limited element version, a payload size cap and no arbitrary HTML, so a long answer belongs in the message text and only the sources in the card. When a message is built from the SDK’s MessageActivity, the Teams SDK also documents native citation entities (the numbered citation chips shown with AI-generated answers); prefer them when your SDK version offers them, and fall back to a card otherwise. In a one-to-one streaming reply the final message is the one that can carry attachments.

Single sign-on and user identity

Two different identities are involved. The Teams identity of the sender arrives with every activity and is already authenticated by Teams; use it as userId for the DocsAssistant’s ACL filter. A delegated access token for the user is a separate need: the bot must call Microsoft Graph or your own API on behalf of the user, for example to fetch the user’s group memberships for document-level access control. For that the Teams SDK exposes sign-in helpers backed by an OAuth connection on the Azure Bot resource:

const app = new App({
  httpServerAdapter: new ExpressAdapter(server),
  oauth: { defaultConnectionName: 'graph' },     // name of the OAuth connection on the Azure Bot
});

app.on('message', async ({ send, signin, userGraph }) => {
  if (!(await signin())) return;                   // starts the sign-in flow if needed; resumes after it
  const me = await userGraph.call(endpoints.me.get);
  await send(`Hello ${me.displayName}, searching the documentation you may read...`);
});

app.event('signin', async ({ send, token }) => {
  await send(`Signed in with connection ${token.connectionName}.`);
});

The snippet omits the import of endpoints from the Graph package used by the Teams SDK sample. Configuration happens on the Azure side: an OAuth connection on the Azure Bot resource, with an Entra app registration that has the right redirect URI and API permissions, and, for silent single sign-on inside Teams, an "Application ID URI" and pre-authorised Teams client IDs exposed in the Entra app and a matching webApplicationInfo block in the manifest. The flow underneath is the authorization code flow with PKCE (Authorization Code & PKCE) with OpenID Connect (OpenID Connect); if the DocsAssistant core is a separate API that accepts only its own audience, use an on-behalf-of exchange instead of forwarding the Graph token (Token Exchange & Assertion Grants). Least privilege applies: request only the Graph scopes you really need, because tenants may require admin consent for broad ones.

Proactive messages

A bot can message a user or a channel without a prior incoming message, for example to deliver a long-running answer, a digest of documentation changes or an ingestion-finished notice. Proactive messaging needs three things: the app must already be installed for that user or team, you must have stored the conversation reference or conversation ID from an earlier activity (persist it with the conversationId mapping from the architecture page), and the message is sent through the app rather than a handler’s send callback:

// conversationId was saved from an earlier activity (installation or first message)
await app.send(conversationId, 'Your reindex of the 4.2 documentation finished: 1,284 pages updated.');

Practical limits: Teams throttles bots per conversation and per tenant, so batch notifications and respect 429 with Retry-After; a user who uninstalls the bot invalidates the stored reference; and the first message to a user needs the app to have been installed (an administrator can pre-install it with an app setup policy). Treat proactive messages as a notification channel, not a way around the consent of the user: provide an unsubscribe command. Operational guidance (limits, observability, abuse) is on the Operating Chat Channels page.

Copilot Studio with the DocsAssistant MCP server

Copilot Studio can host the whole agent: you write instructions, add the DocsAssistant as a tool and publish the agent to Teams as a channel. The DocsAssistant MCP server built in RAG over MCP is the tool; it is implemented in Python in Python FastAPI integration and in Java in Spring Boot servers, and has to be reachable over the internet or a supported private path with HTTPS, as described in Deploying Remote Servers.

  1. In the agent, open Tools, choose Add a tool, New tool, then Model Context Protocol; the onboarding wizard asks for a server name, a description and the server URL. Write the description for the orchestrator: it decides from it when to call the server, so state what the tool answers and what it does not.

  2. Copilot Studio currently supports the streamable HTTP transport only; the older HTTP+SSE transport is no longer supported there (see Transports). A server that exposes /mcp over streamable HTTP works unchanged.

  3. Choose authentication: none (for a public documentation server), API key (header or query parameter), or OAuth 2.0 with dynamic discovery, dynamic registration or manual client settings. The OAuth options map to the MCP authorization model (MCP Authorization and discovery, metadata and client registration); with the manual option the wizard shows a callback URL to register with your identity provider.

  4. Create the connection and add the server to the agent; the agent then lists the server’s tools, and a tool the agent should not use can be disabled individually.

  5. Publish the agent and add the Teams channel; users find it in the Teams app catalogue once the tenant approves it.

Two governance points. Access to MCP servers in Copilot Studio goes through Power Platform connectors, so a data policy that regulates connectors also regulates the MCP server. And because Copilot Studio calls the tools, grounding and citation format are controlled by the MCP server’s results and the agent’s instructions rather than by your adapter: return titles and URLs in the tool result and instruct the agent to cite them. Retrieval-level access control still applies on the server side (MCP Security).

A declarative agent with an MCP plugin

A declarative agent customises Microsoft 365 Copilot itself: instructions, optional knowledge sources and plugins that add actions. Microsoft 365 Copilot supports plugins built from an OpenAPI description or from an MCP server. With the Agents Toolkit (version 6.12.0 or later at the time the Microsoft documentation was read) the steps are: Create a New Agent/App, choose Declarative Agent, Add an Action, Start with an MCP Server, enter the DocsAssistant MCP URL and pick the authentication type (OAuth with static registration, OAuth with dynamic registration, Microsoft Entra SSO or none). The toolkit generates the declarative agent manifest and a plugin manifest (ai-plugin.json) configured for dynamic tool discovery, so the agent resolves the server’s tools at runtime; a fixed, curated tool set can be pinned instead. The plugin manifest only connects to a remote MCP server; it does not package or host it, which is why the DocsAssistant MCP server must be deployed separately. A reduced sketch of the agent manifest (the schema version comes from the toolkit):

{
  "$schema": "https://developer.microsoft.com/json-schemas/copilot/declarative-agent/<version>/schema.json",
  "version": "<version>",
  "name": "DocsAssistant",
  "description": "Answers questions from the Example documentation, with citations.",
  "instructions": "Answer only from the DocsAssistant tool results. Cite the title and URL of every source. If the tool returns nothing relevant, say so.",
  "actions": [
    { "id": "docsassistantMcp", "file": "ai-plugin.json" }
  ]
}

For development, provision the app in the toolkit’s Lifecycle pane (your account must have custom app upload and Copilot access enabled), then find the agent in Microsoft 365 Copilot chat; the dev build’s name carries a dev suffix. For OAuth, the redirect URL the plugin documentation prescribes is Microsoft’s platform redirect for Teams and Copilot, which must be registered on the OAuth client of your identity provider. A declarative agent runs inside Microsoft 365 Copilot, so each user needs a Copilot licence, and an administrator decides whether the agent is available. Choose it over Copilot Studio when the audience already uses Microsoft 365 Copilot and you want the DocsAssistant as an extra skill rather than a separate bot.

Choosing between them, again

Need Route

Answer in channels and group chats, thread-aware, with your own streaming and citations

Teams SDK bot over the core

One agent for Teams and other Microsoft channels, simple feature set

Microsoft 365 Agents SDK

Fastest path, no hosted bot code, Power Platform governance

Copilot Studio with the MCP server

DocsAssistant as one more capability inside Microsoft 365 Copilot

Declarative agent with an MCP plugin

Document-level access control

Any route, but enforced in the core or MCP server using the Entra identity, not in the manifest