#
tokens: 22661/50000 35/35 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .dockerignore
├── .github
│   ├── CODEOWNERS
│   ├── dependabot.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug-issue.md
│   │   └── feature-issue.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows
│       ├── PR-verify.yml
│       ├── release.yml
│       ├── scorecard-analysis.yml
│       └── sonar-cloud-analysis.yml
├── .gitignore
├── .python-version
├── app
│   ├── color.png
│   ├── manifest.json
│   └── outline.png
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── doc
│   ├── images
│   │   ├── azure_app_client_credentials.png
│   │   ├── azure_bot_channels.png
│   │   ├── azure_bot_configuration.png
│   │   ├── azure_msgraph_api_permissions.png
│   │   ├── msteams_app_installation.png
│   │   ├── msteams_bot_app_basic_information.png
│   │   ├── msteams_bot_app_bot_feature.png
│   │   ├── msteams_bot_app_bot_permissions.png
│   │   └── msteams_team_and_channel_info.png
│   └── MS-Teams-setup.md
├── Dockerfile
├── glama.json
├── LICENSE.txt
├── LICENSES
│   └── Apache-2.0.txt
├── llms-install.md
├── NOTICE
├── pyproject.toml
├── README.md
├── RELEASE.md
├── repolinter.json
├── REUSE.toml
├── sample.env
├── SECURITY.md
├── src
│   └── mcp_teams_server
│       ├── __init__.py
│       ├── __main__.py
│       ├── config.py
│       └── teams.py
├── tests
│   ├── __init__.py
│   ├── test_main.py
│   └── test_teams.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
3.10

```

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
.env
*.env
.github
.venv
app
doc
tests
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Virtual Environment
.env
.venv
venv/
ENV/
env/

# IDE
.idea/
.vscode/
*.swp
*.swo
*~

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Logs
*.log
log/
logs/

# Local development
.env.local
.env.development.local
.env.test.local
.env.production.local

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=InditexTech_mcp-teams-server&metric=bugs)](https://sonarcloud.io/summary/new_code?id=InditexTech_mcp-teams-server)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=InditexTech_mcp-teams-server&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=InditexTech_mcp-teams-server)
[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=InditexTech_mcp-teams-server&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=InditexTech_mcp-teams-server)
![GitHub License](https://img.shields.io/github/license/InditexTech/mcp-teams-server)
![GitHub Release](https://img.shields.io/github/v/release/InditexTech/mcp-teams-server)
[![Scorecard](https://api.scorecard.dev/projects/github.com/InditexTech/mcp-teams-server/badge)](https://scorecard.dev/viewer/?uri=github.com/InditexTech/mcp-teams-server)
<!-- [![Best Practices](https://www.bestpractices.dev/projects/10400/badge)](https://www.bestpractices.dev/projects/10400) -->


# MCP Teams Server

An MCP ([Model Context Protocol](https://modelcontextprotocol.io/introduction)) server implementation for 
[Microsoft Teams](https://www.microsoft.com/en-us/microsoft-teams/group-chat-software/) integration, providing capabilities to 
read messages, create messages, reply to messages, mention members.

## Features

https://github.com/user-attachments/assets/548a9768-1119-4a2d-bd5c-6b41069fc522

- Start thread in channel with title and contents, mentioning users
- Update existing threads with message replies, mentioning users
- Read thread replies
- List channel team members
- Read channel messages

## Prerequisites

- [uv](https://github.com/astral-sh/uv) package manager
- [Python 3.10](https://www.python.org/)
- Microsoft Teams account with [proper set-up](./doc/MS-Teams-setup.md)

## Installation

1. Clone the repository:

```bash
git clone [repository-url]
cd mcp-teams-server
```

2. Create a virtual environment and install dependencies:

```bash
uv venv
uv sync --frozen --all-extras --dev
```

## Teams configuration

Please read [this document](./doc/MS-Teams-setup.md) to help you to configure Microsoft Teams and required 
Azure resources. It is not a step-by-step guide but can help you figure out what you will need.

## Usage

Set up the following environment variables in your shell or in an .env file. You can use [sample file](./sample.env) 
as a template:

| Key                     | Description                                |
|-------------------------|--------------------------------------------|
| **TEAMS_APP_ID**        | UUID for your MS Entra ID application ID   |
| **TEAMS_APP_PASSWORD**  | Client secret                              |
| **TEAMS_APP_TYPE**      | SingleTenant or MultiTenant                |
| **TEAMS_APP_TENANT_ID** | Tenant uuid in case of SingleTenant        |
| **TEAM_ID**             | MS Teams Group Id or Team Id               |
| **TEAMS_CHANNEL_ID**    | MS Teams Channel ID with url escaped chars |

Start the server:

```bash
uv run mcp-teams-server
```

## Development

Integration tests require the set-up the following environment variables:

| Key                    | Description                    |
|------------------------|--------------------------------|
| **TEST_THREAD_ID**     | timestamp of the thread id     |
| **TEST_MESSAGE_ID**    | timestamp of the message id    |
| **TEST_USER_NAME**     | test user name                 |


```bash
uv run pytest -m integration
```

### Pre-built docker image

There is a [pre-built image](https://github.com/InditexTech/mcp-teams-server/pkgs/container/mcp-teams-server) hosted in ghcr.io.
You can install this image by running the following command

```commandline
docker pull ghcr.io/inditextech/mcp-teams-server:latest
```

### Build docker image

A docker image is available to run MCP server. You can build it with the following command:

```bash
docker build . -t inditextech/mcp-teams-server
```

### Run docker image

Basic run configuration:

```bash
docker run -it inditextech/mcp-teams-server
```

Run with environment variables from .env file:

```bash
docker run --env-file .env -it inditextech/mcp-teams-server
```

### Setup LLM to use MCP Teams Server

Please follow instructions on the [following document](./llms-install.md)

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for a list of changes and version history.

## Contributing

Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull
requests.

## Security

For security concerns, please see our [Security Policy](SECURITY.md).

## License

This project is licensed under the [Apache-2.0](LICENSE.txt) file for details.

© 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.)

```

--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------

```markdown
# Code of Conduct

By participating in this project, you agree to abide by the rules and principles outlined in the Code of Conduct. Please review it to understand the expectations for respectful and inclusive collaboration.

This project adheres to the general [Inditex Tech Code of Conduct](https://github.com/InditexTech/foss/blob/main/CODE_OF_CONDUCT.md).
```

--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------

```markdown
# Security Policy

## Supported Versions

We release patches for security vulnerabilities. Currently supported versions:

| Version | Supported          |
| ------- | ------------------ |
| 1.0.x   | :white_check_mark: |

## Reporting a Vulnerability

We take the security of MCP Teams Server seriously. If you believe you have found a security vulnerability, please report it to us as described below.

**Please do not report security vulnerabilities through public GitHub issues.**

Instead, please report them via our [disclosure submission program](https://vdp.inditex.com).

## Preferred Languages

We prefer all communications to be in English.

## Process

1. Security report received
2. Security team acknowledges receipt within 48 hours
3. Team investigates and determines severity
4. Team develops and tests fix
5. Team prepares advisory and patches
6. Advisory published, patches released

## Safe Harbor

We support safe harbor for security researchers who:

1. Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our services
2. Only interact with accounts you own or with explicit permission of the account holder
3. Provide us with a reasonable amount of time to resolve vulnerabilities prior to any disclosure to the public or a third-party
4. Do not exploit a security issue for purposes other than immediate testing

## Security Controls

The MCP Teams Server implements several security controls:

1. All authentication tokens and credentials must be provided via environment variables
2. Communications with the Teams API use HTTPS/TLS encryption
3. Input validation and sanitization for all API endpoints
4. Rate limiting to prevent abuse
5. Regular security updates and dependency checks

## Security-related Configuration

For secure operation, please ensure:

1. Environment variables are properly secured and not logged
2. Access tokens have minimum required permissions
3. Production deployments use HTTPS/TLS
4. Regular security updates are applied
5. Proper logging and monitoring is configured

## Third-party Security Notifications

We review security reports for our dependencies and follow responsible disclosure guidelines.

```

--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
# Contributing

Thank you for your interest in contributing to this project! We value and appreciate any contributions you can make.
To maintain a collaborative and respectful environment, please consider the following guidelines when contributing to
this project.

## Prerequisites

- Before starting to contribute to the code, you must first sign the
  [Contributor License Agreement (CLA)](https://github.com/InditexTech/foss/blob/main/documents/CLA.pdf).
  Detailed instructions on how to proceed can be found [here](https://github.com/InditexTech/foss/blob/main/CONTRIBUTING.md).

## How to Contribute

1. Open an issue to discuss and gather feedback on the feature or fix you wish to address.
2. Fork the repository and clone it to your local machine.
3. Create a new branch to work on your contribution: `git checkout -b your-branch-name`.
4. Make the necessary changes in your local branch.
5. Ensure that your code follows the established project style and formatting guidelines.
6. Perform testing to ensure your changes do not introduce errors.
7. Make clear and descriptive commits that explain your changes.
8. Push your branch to the remote repository: `git push origin your-branch-name`.
9. Open a pull request describing your changes and linking the corresponding issue.
10. Await comments and discussions on your pull request. Make any necessary modifications based on the received feedback.
11. Once your pull request is approved, your contribution will be merged into the main branch.

## Contribution Guidelines

- All contributors are expected to follow the project's [code of conduct](CODE_of_CONDUCT.md). Please be respectful and
considerate towards other contributors.
- Before starting work on a new feature or fix, check existing [issues](../../issues) and [pull requests](../../pulls)
to avoid duplications and unnecessary discussions.
- If you wish to work on an existing issue, comment on the issue to inform other contributors that you are working on it.
This will help coordinate efforts and prevent conflicts.
- It is always advisable to discuss and gather feedback from the community before making significant changes to the
project's structure or architecture.
- Ensure a clean and organized commit history. Divide your changes into logical and descriptive commits. We recommend to use the [Conventional Commits Specification](https://www.conventionalcommits.org/en/v1.0.0/)
- Document any new changes or features you add. This will help other contributors and project users understand your work
and its purpose.
- Be sure to link the corresponding issue in your pull request to maintain proper tracking of contributions.
- Remember to add license and copyright information following the [REUSE Specification](https://reuse.software/spec/#copyright-and-licensing-information).

## Development

Make sure that you have:

- Read the rest of the [`CONTRIBUTING.md`](CONTRIBUTING.md) sections.
- Meet the [prerequisites](#prerequisites).
- [uv](https://github.com/astral-sh/uv) installed
- [python](https://www.python.org/) 3.10 or later installed
- Set up integration with Microsoft Teams by your own means
- Run integration tests to verify Microsoft Teams integration

Please remember tu run linting locally before committing any changes:

```bash
uv run ruff check . --fix
uv run ruff format .
```

It is recommended to run a type checker:

```bash
uv run pyright
```

## Technical details

This MCP is built using [MCP SDK](https://github.com/modelcontextprotocol/python-sdk) for Python from Anthropic.
It uses FastMCP to implement tools offered by the MCP server.

This MCP server consumes two Microsoft APIs / frameworks:

- [Azure Bot Builder](https://github.com/microsoft/botbuilder-python) for python
- [Microsoft Graph SDK](https://github.com/microsoftgraph/msgraph-sdk-python) for python

Azure Bot Builder allows a bot (Microsoft Entra ID app) to consume a Microsoft REST API to send messages to channels 
(but it is not capable of consuming messages because the bot is not deployed in Azure). The REST API client is 
encapsulated inside the framework classes, and it is not used directly.
In order to send messages without actually being deployed in Azure, the bot "continues" a conversation to retrieve 
the TurnContext instance and perform actions on "activities". This technique is called 
[proactive messaging](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages?tabs=python)

Replying to messages through bot builder is not possible without the help of 
[this hack](https://github.com/microsoft/botframework-sdk/issues/6626), because the bot builder framework 
is not ready for this use (although the internal REST API allows it, and it works like a charm).

Azure Bot Builder allows to perform any write operation, but reading messages or previous threads is not possible 
without a special "migration" permission. Because of that, we have preferred to use Microsoft Graph to read messages.
Microsoft Application Entra ID must have been granted permissions to read messages in a channel.


```

--------------------------------------------------------------------------------
/glama.json:
--------------------------------------------------------------------------------

```json
{
  "$schema": "https://glama.ai/mcp/schemas/server.json",
  "maintainers": [
    "marianoao"
  ]
}
```

--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------

```python
# SPDX-FileCopyrightText: 2025 INDUSTRIA DE DISEÑO TEXTIL, S.A. (INDITEX, S.A.)
# SPDX-License-Identifier: Apache-2.0

```

--------------------------------------------------------------------------------
/src/mcp_teams_server/__main__.py:
--------------------------------------------------------------------------------

```python
# SPDX-FileCopyrightText: 2025 INDUSTRIA DE DISEÑO TEXTIL, S.A. (INDITEX, S.A.)
# SPDX-License-Identifier: Apache-2.0
from mcp_teams_server import main

main()

```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-issue.md:
--------------------------------------------------------------------------------

```markdown
---
name: Bug Report
about: Use this template to report a bug
title: ''
labels: kind/bug
assignees: ''

---

### Detailed description

A clear and concise description of what the problem is.

### Expected behaviour

Expected behaviour one the problem is fixed.
```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-issue.md:
--------------------------------------------------------------------------------

```markdown
---
name: Feature request
about: Suggest an idea/feature for this project
title: ''
labels: 'kind/feature'
assignees: ''

---

### Motivation

Describe here the motivation of the request.

### Acceptance criteria

- [ ] A checklist of tasks to be done to assume the issue addressed
```

--------------------------------------------------------------------------------
/sample.env:
--------------------------------------------------------------------------------

```
TEAMS_APP_ID=<put your azure bot application id here>
TEAMS_APP_PASSWORD=<put your teams app password here>
TEAMS_APP_TYPE=SingleTenant
TEAMS_APP_TENANT_ID=<put your azure bot application id application id here>
TEAM_ID=<put your MS teams group id here>
TEAMS_CHANNEL_ID=<pur your MS teams channel id here>

TEST_THREAD_ID=<test thread id>
TEST_MESSAGE_ID=<test message id>
TEST_USER_NAME=<test user name>
```

--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------

```markdown
### Release process

Review and integrate desired works and PRs into master branch before launching a release.

Release process is triggered from a PR labeled with "kind/release" label.

Please review [version](./pyproject.toml) in project section because tag is inferred from that value

Once the PR is closed, the release process will begin.

To repeat a failing release, remember to remove the `version` tag previously created.
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2025-04-01

### Added
- First release of MCP Teams Server
- Basic Teams integration through MCP tools
- Documentation for setup and usage
- Security guidelines and policies
- Basic workflows for PR verify and release docker image

```

--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------

```yaml
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
  - package-ecosystem: "uv" # See documentation for possible values
    directory: "/" # Location of package manifests
    schedule:
      interval: "weekly"
    ignore:
      - dependency-name: "aiohttp"

```

--------------------------------------------------------------------------------
/src/mcp_teams_server/config.py:
--------------------------------------------------------------------------------

```python
# SPDX-FileCopyrightText: 2025 INDUSTRIA DE DISEÑO TEXTIL, S.A. (INDITEX, S.A.)
# SPDX-License-Identifier: Apache-2.0
import os
from dataclasses import dataclass


@dataclass
class BotConfiguration:
    def __init__(self):
        self.APP_ID = os.environ.get("TEAMS_APP_ID", "")
        self.APP_PASSWORD = os.environ.get("TEAMS_APP_PASSWORD", "")
        self.APP_TYPE = os.environ.get("TEAMS_APP_TYPE", "SingleTenant")
        self.APP_TENANTID = os.environ.get("TEAMS_APP_TENANT_ID", "")
        self.TEAM_ID = os.environ.get("TEAM_ID", "")
        self.TEAMS_CHANNEL_ID = os.environ.get("TEAMS_CHANNEL_ID", "")

```

--------------------------------------------------------------------------------
/REUSE.toml:
--------------------------------------------------------------------------------

```toml
version = 1
SPDX-PackageName = "mcp-teams-server"
SPDX-PackageSupplier = "2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.)"
SPDX-PackageDownloadLocation = "https://github.com/InditexTech/mcp-teams-server"

[[annotations]]
path = [
    ".gitignore",
    ".dockerignore",
    ".github/**",
    "*.md",
    ".python-version",
    "repolinter.json",
    "glama.json",
    "REUSE.toml",
    "pyproject.toml",
    "NOTICE",
    "Dockerfile",
    "doc/**",
    "app/*",
    "sample.env",
    "uv.lock",
]
SPDX-FileCopyrightText = "2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.)"
SPDX-License-Identifier = "Apache-2.0"
```

--------------------------------------------------------------------------------
/tests/test_main.py:
--------------------------------------------------------------------------------

```python
# SPDX-FileCopyrightText: 2025 INDUSTRIA DE DISEÑO TEXTIL, S.A. (INDITEX, S.A.)
# SPDX-License-Identifier: Apache-2.0
import os
import sys
from unittest.mock import patch

import pytest

import mcp_teams_server
from mcp_teams_server import main


def test_main_should_exit_error_on_missing_env_vars():
    # Unset environment
    for var in mcp_teams_server.REQUIRED_ENV_VARS:
        os.environ.pop(var, None)

    test_args = ["main"]
    with patch.object(sys, "argv", test_args):
        with pytest.raises(SystemExit) as exit_code:
            main()

    assert exit_code.type is SystemExit
    assert exit_code.value.code == 1


@pytest.mark.asyncio
async def test_list_tools():
    tools = await mcp_teams_server.mcp.list_tools()

    assert tools is not None

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
FROM ghcr.io/astral-sh/uv:python3.10-alpine

# ENV TEAMS_APP_ID="" TEAMS_APP_PASSWORD="" TEAMS_APP_TYPE="" TEAMS_APP_TENANT_ID="" TEAM_ID="" TEAMS_CHANNEL_ID=""

LABEL \
  org.opencontainers.image.vendor="Industria de Diseño Textil, S.A." \
  org.opencontainers.image.source="https://github.com/InditexTech/mcp-teams-server" \
  org.opencontainers.image.authors="Open Source Office Team" \
  org.opencontainers.image.title="MCP Teams Server" \
  org.opencontainers.image.description="MCP Teams Server container image" \
  org.opencontainers.image.licenses="Apache-2.0"

# Settings for faster container start
ENV UV_COMPILE_BYTECODE=0 UV_PYTHON_DOWNLOADS=0 UV_LINK_MODE=copy

COPY pyproject.toml LICENSE.txt *.md uv.lock src /app/
WORKDIR /app
RUN uv sync --frozen --no-dev

CMD ["uv", "run", "--frozen", "--no-dev", "mcp-teams-server"]
```

--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------

```markdown
# PR Checklist

**Friendly reminder**: please read [CONTRIBUTING.md](../CONTRIBUTING.md#prerequisites), specially CLA in case you are
a contributor and not a maintainer.

Please check if your PR fulfills the following requirements:

- [ ] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)

## PR Type

What kind of change does this PR introduce?

<!-- Please check the one that applies to this PR using "x". -->

- [ ] Bugfix
- [ ] Feature
- [ ] Other... Please describe:

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->

Issue Number: N/A

## What is the new behavior?

## Does this PR introduce a breaking change?

- [ ] Yes
- [ ] No

<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->

## Other information
```

--------------------------------------------------------------------------------
/.github/workflows/scorecard-analysis.yml:
--------------------------------------------------------------------------------

```yaml
name: Scorecard analysis workflow
permissions: read-all
on:
  push:
    # Only the default branch is supported.
    branches: [ 'master' ]


jobs:
  analysis:
    name: Scorecard analysis
    runs-on: ubuntu-latest
    permissions:
      # Needed for GitHub OIDC token if publish_results is true
      id-token: write
    steps:
      - name: "Checkout code"
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
        with:
          fetch-depth: 0

      - name: "Run analysis"
        uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
        with:
          results_file: results.sarif
          results_format: sarif
          # Scorecard team runs a weekly scan of public GitHub repos,
          # see https://github.com/ossf/scorecard#public-data.
          # Setting `publish_results: true` helps us scale by leveraging your workflow to
          # extract the results instead of relying on our own infrastructure to run scans.
          # And it's free for you!
          publish_results: true

      # Upload the results as artifacts (optional). Commenting out will disable
      # uploads of run results in SARIF format to the repository Actions tab.
      # https://docs.github.com/en/actions/advanced-guides/storing-workflow-data-as-artifacts
      - name: "Upload artifact"
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: SARIF file
          path: results.sarif
          retention-days: 5
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[project]
name = "mcp-teams-server"
version = "1.0.6"
description = "An MCP server implementation for Microsoft Teams integration"
readme = "README.md"
keywords = ["mcp", "llm", "automation", "Microsoft Teams"]
authors = [{ name = "Industria de Diseño Textil S.A." }]
maintainers = [
    { name = "Mariano Alonso Ortiz", email = "[email protected]" },
]
license = { text = "Apache-2.0" }
requires-python = ">=3.10"
dependencies = [
    "aiohttp==3.10.11",
    "asyncio>=3.4.3",
    "botbuilder-core>=4.17.0",
    "botbuilder-integration-aiohttp>=4.17.0",
    "dotenv>=0.9.9",
    "mcp[cli]>=1.12.0",
    "msgraph-sdk>=1.37.0",
    "multidict>=6.6.3",
]

[project.urls]
Repository = "https://github.com/InditexTech/mcp-teams-server"
Issues = "https://github.com/InditexTech/mcp-teams-server/issues"

[project.scripts]
mcp-teams-server = "mcp_teams_server:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[dependency-groups]
dev = [
    "mock>=5.2.0",
    "pyproject-parser[cli]>=0.13.0",
    "pyright>=1.1.398",
    "pytest>=8.3.5",
    "pytest-asyncio>=0.25.3",
    "pytest-cov>=6.0.0",
    "reuse>=5.0.2",
    "ruff>=0.11.2",
]

[tool.ruff]
line-length = 88
target-version = "py310"

[tool.ruff.lint]
select = ["E", "F", "I", "UP"]
ignore = []

[tool.ruff.lint.per-file-ignores]
"src/mcp_teams_server/teams.py" = ["E501"]

[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "-v --tb=short --import-mode=importlib --strict-markers -m \"not integration\""
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
markers = [
    "integration: integration tests",
]
filterwarnings = [
    "ignore::DeprecationWarning"
]

```

--------------------------------------------------------------------------------
/.github/workflows/sonar-cloud-analysis.yml:
--------------------------------------------------------------------------------

```yaml
name: Tests

permissions:
  contents: read

concurrency:
  group: tests-${{ github.ref }}
  cancel-in-progress: true

on:
  workflow_dispatch:
  pull_request:
    types: [ closed ]
    branches: [ 'master' ]
  release:
    types:
      - published

jobs:
  unit-tests:
    name: Tests
    timeout-minutes: 30
    if: ${{ ((github.event.pull_request.merged == true && github.base_ref == 'master') ||
      (github.event_name == 'workflow_dispatch' ||
      github.event_name == 'release'))
      && vars.IS_INDITEXTECH_REPO == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
        with:
          fetch-depth: 0
          ref: ${{ github.event.pull_request.head.sha }}

      - name: Install UV
        uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
        with:
          enable-cache: true

      - name: Sync project
        run: uv sync --frozen --all-extras --dev --python 3.10

      - name: Unit tests execution
        run: uv run pytest --cov-report term --cov-report xml:coverage-unit.xml --cov=.

      - name: Integration tests execution
        run: uv run pytest -m integration --cov-report term --cov-report xml:coverage-integration.xml --cov=.
        env:
          TEAMS_APP_ID: ${{ vars.TEAMS_APP_ID }}
          TEAMS_APP_PASSWORD: ${{ secrets.TEAMS_APP_PASSWORD }}
          TEAMS_APP_TYPE: ${{ vars.TEAMS_APP_TYPE }}
          TEAMS_APP_TENANT_ID: ${{ vars.TEAMS_APP_TENANT_ID }}
          TEAM_ID: ${{ vars.TEAM_ID }}
          TEAMS_CHANNEL_ID: ${{ vars.TEAMS_CHANNEL_ID }}
          TEST_THREAD_ID: ${{ vars.TEST_THREAD_ID }}
          TEST_MESSAGE_ID: ${{ vars.TEST_MESSAGE_ID }}
          TEST_USER_NAME: ${{ vars.TEST_USER_NAME }}

```

--------------------------------------------------------------------------------
/app/manifest.json:
--------------------------------------------------------------------------------

```json
{
  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.19/MicrosoftTeams.schema.json",
  "version": "1.0.0",
  "manifestVersion": "1.19",
  "id": "{{MICROSOFT_APP_ID}}",
  "name": {
    "short": "MCP Teams Bot",
    "full": "Model Context Protocol Teams Bot"
  },
  "developer": {
    "name": "Industria Textil de Diseño, S.A.",
    "mpnId": "",
    "websiteUrl": "https://www.inditex.com",
    "privacyUrl": "https://www.inditex.com/itxcomweb/es/es/informacion/politica-de-privacidad",
    "termsOfUseUrl": "https://www.inditex.com/itxcomweb/es/es/informacion/legal"
  },
  "description": {
    "short": "Model Context Protocol Teams Bot",
    "full": "Model Context Protocol Teams Bot application, necessary to connect MCP host to Teams"
  },
  "icons": {
    "outline": "outline.png",
    "color": "color.png"
  },
  "accentColor": "#FFFFFF",
  "staticTabs": [
    {
      "entityId": "conversations",
      "scopes": [
        "personal"
      ]
    },
    {
      "entityId": "about",
      "scopes": [
        "personal"
      ]
    }
  ],
  "bots": [
    {
      "botId": "{{MICROSOFT_APP_ID}}",
      "scopes": [
        "team",
        "personal",
        "groupChat"
      ],
      "commandLists": [
        {
          "commands": [
            {
              "title": "Help",
              "description": "Shows help information"
            }
          ],
          "scopes": [
            "team"
          ]
        }
      ],
      "isNotificationOnly": false,
      "supportsCalling": false,
      "supportsVideo": false,
      "supportsFiles": false
    }
  ],
  "validDomains": [
    "token.botframework.com"
  ],
  "authorization": {
    "permissions": {
      "resourceSpecific": [
        {
          "name": "TeamMember.Read.Group",
          "type": "Application"
        },
        {
          "name": "ChannelMessage.Read.Group",
          "type": "Application"
        },
        {
          "name": "ChannelMessage.Send.Group",
          "type": "Application"
        },
        {
          "name": "ChannelSettings.Read.Group",
          "type": "Application"
        },
        {
          "name": "ChannelMember.Read.Group",
          "type": "Application"
        },
        {
          "name": "Member.Read.Group",
          "type": "Application"
        },
        {
          "name": "Owner.Read.Group",
          "type": "Application"
        }
      ]
    }
  }
}
```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
name: Release
permissions: read-all

on:
  pull_request:
    types: [ closed ]
    branches: [ 'master' ]
  workflow_dispatch:

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    if: ${{ github.event.pull_request.merged && contains(github.event.pull_request.labels.*.name, 'kind/release') }}
    permissions:
      contents: write
      packages: write
    steps:
      - name: Checkout
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
        with:
          fetch-depth: 0

      - name: Install UV
        uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
        with:
          enable-cache: true

      - name: Sync project
        run: uv sync --frozen --all-extras --dev --python 3.10

      - name: Retrieve version
        shell: bash
        run: |
          VERSION=$(uv run pyproject-info project.version | tr -d '"'); echo "VERSION=v$VERSION" >> "$GITHUB_ENV"

      - name: Create Git Tag
        run: |
          git config user.email "[email protected]"
          git config user.name "GitHub Bot"

          git tag -a "${{ env.VERSION }}" -m "Version ${{ env.VERSION }}"
          git push origin "${{ env.VERSION }}"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Login into ${{ env.REGISTRY }}
        uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Setup Docker Buildx
        uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=semver,pattern={{version}},value=${{ env.VERSION }}
            type=pep440,pattern={{version}},value=${{ env.VERSION }}

      - name: Build and push by digest
        id: build
        uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Create GitHub Release
        uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1
        with:
          name: ${{ env.VERSION }}
          tag: ${{ env.VERSION }}
          token: ${{ secrets.GITHUB_TOKEN }}
          generateReleaseNotes: true
```

--------------------------------------------------------------------------------
/.github/workflows/PR-verify.yml:
--------------------------------------------------------------------------------

```yaml
name: Pull Request verification
permissions:
  contents: read

on:
  pull_request:
  workflow_dispatch:

jobs:
  check-format:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
        with:
          fetch-depth: 0
          ref: ${{ github.event.pull_request.head.sha }}

      - name: Install UV
        uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
        with:
          enable-cache: true

      - name: Sync project
        run: uv sync --frozen --all-extras --dev --python 3.10

      - name: Ruff format check
        run: uv run --no-sync ruff check .

  check-typing:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
        with:
          fetch-depth: 0
          ref: ${{ github.event.pull_request.head.sha }}

      - name: Install UV
        uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
        with:
          enable-cache: true

      - name: Sync project
        run: uv sync --frozen --all-extras --dev --python 3.10

      - name: Pyright type checking
        run: uv run --no-sync pyright

  run-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
        with:
          fetch-depth: 0
          ref: ${{ github.event.pull_request.head.sha }}

      - name: Install UV
        uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
        with:
          enable-cache: true

      - name: Sync project
        run: uv sync --frozen --all-extras --dev --python 3.10

      - name: Unit tests execution
        run: uv run pytest

  repo-linter:
    name: Repo Linter
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
        with:
          fetch-depth: 0
          ref: ${{ github.event.pull_request.head.sha }}

      - name: Setup Node version
        uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
        with:
          node-version: ${{ env.NODE_VERSION }}

      - name: Execute Repo Linter
        run: |
          echo "Installing Repo Linter"
          npm install -g [email protected]
          
          echo "Executing Repo Linter"
          repolinter --rulesetFile repolinter.json --dryRun .
          
          echo "Repo Linter execution completed"


  reuse-compliance:
    name: REUSE Compliance
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
        with:
          fetch-depth: 0
          ref: ${{ github.event.pull_request.head.sha }}

      - name: REUSE Compliance Check
        uses: fsfe/reuse-action@bb774aa972c2a89ff34781233d275075cbddf542 # v5


```

--------------------------------------------------------------------------------
/tests/test_teams.py:
--------------------------------------------------------------------------------

```python
# SPDX-FileCopyrightText: 2025 INDUSTRIA DE DISEÑO TEXTIL, S.A. (INDITEX, S.A.)
# SPDX-License-Identifier: Apache-2.0
import logging
import os
import sys

import pytest
from azure.identity.aio import ClientSecretCredential
from botbuilder.integration.aiohttp import (
    CloudAdapter,
    ConfigurationBotFrameworkAuthentication,
)
from dotenv import load_dotenv
from msgraph.graph_service_client import GraphServiceClient

from mcp_teams_server.config import BotConfiguration
from mcp_teams_server.teams import TeamsClient

load_dotenv()

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    handlers=[logging.StreamHandler(sys.stdout)],
)

LOGGER = logging.getLogger(__name__)


@pytest.mark.integration
@pytest.fixture()
def setup_teams_client() -> TeamsClient:
    # Cloud adapter
    config = BotConfiguration()
    adapter = CloudAdapter(
        ConfigurationBotFrameworkAuthentication(config, logger=LOGGER)
    )

    # Graph client
    credentials = ClientSecretCredential(
        config.APP_TENANTID, config.APP_ID, config.APP_PASSWORD
    )
    scopes = ["https://graph.microsoft.com/.default"]
    graph_client = GraphServiceClient(credentials=credentials, scopes=scopes)

    return TeamsClient(
        adapter, graph_client, config.APP_ID, config.TEAM_ID, config.TEAMS_CHANNEL_ID
    )


@pytest.fixture()
def thread_id() -> str | None:
    return os.environ.get("TEST_THREAD_ID")


@pytest.fixture()
def message_id() -> str | None:
    return os.environ.get("TEST_MESSAGE_ID")


@pytest.fixture()
def user_name() -> str | None:
    return os.environ.get("TEST_USER_NAME")


@pytest.mark.integration
@pytest.mark.asyncio
async def test_start_thread(setup_teams_client, user_name):
    LOGGER.info(
        f"test_start_thread in team: {setup_teams_client.team_id} "
        f"and channel {setup_teams_client.teams_channel_id}"
    )
    result = await setup_teams_client.start_thread(
        "First thread", "First thread content with mention", user_name
    )
    print(f"Result {result}\n")
    assert result is not None


@pytest.mark.integration
@pytest.mark.asyncio
async def test_read_threads(setup_teams_client):
    result = await setup_teams_client.read_threads(50)
    print(f"Result {result}\n")
    assert result is not None


@pytest.mark.integration
@pytest.mark.asyncio
async def test_update_thread(setup_teams_client, thread_id, user_name):
    result = await setup_teams_client.update_thread(
        thread_id, "Thread updated content with mention", user_name
    )
    print(f"Result {result}\n")
    assert result is not None


@pytest.mark.integration
@pytest.mark.asyncio
async def test_read_thread_replies(setup_teams_client, thread_id):
    result = await setup_teams_client.read_thread_replies(thread_id)
    print(f"Result {result}\n")
    assert result is not None


@pytest.mark.integration
@pytest.mark.asyncio
async def test_list_members(setup_teams_client):
    result = await setup_teams_client.list_members()
    print(f"Result {result}\n")
    assert result is not None

```

--------------------------------------------------------------------------------
/doc/MS-Teams-setup.md:
--------------------------------------------------------------------------------

```markdown
## Teams configuration

### Application registration

You will need to [create and register](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app?tabs=certificate%2Cexpose-a-web-api) 
a Microsoft Entra ID application. Please keep your application UUID to set the environment variable **TEAMS_APP_ID**.

Microsoft Bot Framework will use [REST Authentication](https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-authentication?view=azure-bot-service-4.0&tabs=singletenant#step-1-request-an-access-token-from-the-microsoft-entra-id-account-login-service)

Then you will need to [add a client secret](https://learn.microsoft.com/en-us/entra/identity-platform/how-to-add-credentials?tabs=client-secret) 
to your application. After this you will retrieve your client secret and store it in the environment variable 
**TEAMS_APP_PASSWORD**.

Your organization can either use a Single Tenant or Multi Tenant schema for managing identities. 
In case you use a SingleTenant scheme, please store the tenant UUID in **TEAMS_APP_TENANT_ID** and 
set **TEAMS_APP_TYPE** to SingleTenant.

During development of this MCP Server we used SingleTenant authentication for our demo application 
with client secret credentials.

![Client Secret Credentials](./images/azure_app_client_credentials.png)

It is also necessary to setup Microsoft Graph API "ChannelMessage.Read.All" permission. 
This permission is a "Resource Specific Consent" and can be scoped to the team or group where the teams 
application (explained later) is installed.

![MS Graph API Permissions](./images/azure_msgraph_api_permissions.png)

### Azure Bot registration

After registering the Microsoft Entra ID application, you will need to [register an azure bot](https://learn.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration?view=azure-bot-service-4.0&tabs=singletenant). 
The bot will be set with the **TEAMS_APP_ID** existing registration.

![Azure Bot Configuration](./images/azure_bot_configuration.png)

The next step is to [connect with teams channel](https://learn.microsoft.com/en-us/azure/bot-service/channel-connect-teams?view=azure-bot-service-4.0)

The bot account is used only as a consumer for the Azure Bot REST api and you will not need to deploy any webapp into Azure. But you will need to install a Microsoft Teams Application with your bot settings.

![Azure Bot Channels](./images/azure_bot_channels.png)

You will not need to deploy a Bot application in Azure or the Internet because this MCP server uses 
Azure Bot Framework as a client to Bot Framework api.

### Microsoft Teams Application publishing

There is a skeleton for this kind of applications in the [app folder](https://github.com/InditexTech/mcp-teams-server/tree/master/app). 
This skeleton can be used after replacing `{{MICROSOFT_APP_ID}}` by your **TEAMS_APP_ID**, 
using your own icons and zipping the directory to deploy it as an application into Microsoft Teams.

As an alternative you can create and publish the same application with the aid of [Teams developer portal](https://dev.teams.microsoft.com/), 
but remember to add and configure "bot feature" and setup the application required permissions 
("TeamMember.Read.Group", "ChannelMessage.Read.Group", "ChannelMessage.Send.Group", "ChannelSettings.Read.Group", 
"ChannelMember.Read.Group", "Member.Read.Group", "Owner.Read.Group").

![MS Teams App Basic Information](./images/msteams_bot_app_basic_information.png)

![MS Teams App Bot Feature](./images/msteams_bot_app_bot_feature.png)

![MS Teams App Permissions](./images/msteams_bot_app_bot_permissions.png "MS Teams App Permissions")

This application needs to be installed in a Teams Group

![MS Teams App Installation](./images/msteams_app_installation.png)

And some information must be extracted from the Teams Group url:

![MS Teams Group Information](./images/msteams_team_and_channel_info.png)

```
https://teams.microsoft.com/l/channel/[TEAMS_CHANNEL_ID]/McpBot?groupId=[TEAM_ID]&tenantId=[TEAMS_APP_TENANT_ID]
```

You will need that information to set up **TEAM_ID** and **TEAMS_CHANNEL_ID** for your bot. 

The MCP teams server will be set up to read and post only in the **TEAM_ID** channels and 
will use by default the **TEAMS_CHANNEL_ID**.
```

--------------------------------------------------------------------------------
/llms-install.md:
--------------------------------------------------------------------------------

```markdown
## MCP Teams Server Installation Guide

This guide is specifically designed for AI agents like Cline to install and configure the MCP Teams Server for use 
with LLM applications like Claude Desktop, Cursor, Roo Code, and Cline.

### Overview

MCP Teams Server is a communication tool that allows AI assistants to interact with Microsoft Teams Channels.

### Prerequisites

- [uv](https://github.com/astral-sh/uv) package manager
- [Python 3.10](https://www.python.org/)
- Microsoft Teams account with [proper set-up](./doc/MS-Teams-setup.md)

### Installation and configuration

Add the MCP server configuration to your MCP settings file based on your LLM client.

Remember there is a [pre-built image](https://github.com/InditexTech/mcp-teams-server/pkgs/container/mcp-teams-server) hosted in ghcr.io.
You can install this image by running the following command

```commandline
docker pull ghcr.io/inditextech/mcp-teams-server:latest
```

Sample docker setup:

```yaml
{
  "mcpServers": {
    "msteams": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        "--rm",
        "-e",
        "TEAMS_APP_ID",
        "-e",
        "TEAMS_APP_PASSWORD",
        "-e",
        "TEAMS_APP_TYPE",
        "-e",
        "TEAMS_APP_TENANT_ID",
        "-e",
        "TEAM_ID",
        "-e",
        "TEAMS_CHANNEL_ID",
        "ghcr.io/inditextech/mcp-teams-server"
      ],
      "env": {
        "TEAMS_APP_ID": "<fill_me_with_proper_uuid>",
        "TEAMS_APP_PASSWORD": "<fill_me_with_proper_uuid>",
        "TEAMS_APP_TYPE": "<fill_me_with_proper_uuid>",
        "TEAMS_APP_TENANT_ID": "<fill_me_with_proper_uuid>",
        "TEAM_ID": "<fill_me_with_proper_uuid>",
        "TEAMS_CHANNEL_ID": "<fill_me_with_proper_channel_id>",
        "DOCKER_HOST": "unix:///var/run/docker.sock"
      }
    }
  }
}
```

Sample Cline setup with docker through WSL (Windows only):

```yaml 
{
  "mcpServers": {
    "github.com/InditexTech/mcp-teams-server/tree/main": {
      "command": "wsl",
      "args": [
        "TEAMS_APP_ID=<fill_me_with_proper_uuid>",
        "TEAMS_APP_PASSWORD=<fill_me_with_proper_uuid>",
        "TEAMS_APP_TYPE=<fill_me_with_proper_uuid>",
        "TEAMS_APP_TENANT_ID=<fill_me_with_proper_uuid>",
        "TEAM_ID=<fill_me_with_proper_uuid>",
        "TEAMS_CHANNEL_ID=<fill_me_with_proper_uuid>",
        "docker",
        "run",
        "-i",
        "--rm",
        "ghcr.io/inditextech/mcp-teams-server"
      ],
      "env": {
        "DOCKER_HOST": "unix:///var/run/docker.sock"
      },
      "disabled": false,
      "autoApprove": [ ],
      "timeout": 300
    }
  }
}
```

Sample local development setup:

```yaml
{
  "mcpServers": {
    "msteams": {
      "command": "uv",
      "args": [
        "run",
        "mcp-teams-server"
      ],
      "env": {
        "TEAMS_APP_ID": "<fill_me_with_proper_uuid>",
        "TEAMS_APP_PASSWORD": "<fill_me_with_proper_uuid>",
        "TEAMS_APP_TYPE": "<fill_me_with_proper_uuid>",
        "TEAMS_APP_TENANT_ID": "<fill_me_with_proper_uuid>",
        "TEAM_ID": "<fill_me_with_proper_uuid>",
        "TEAMS_CHANNEL_ID": "<fill_me_with_proper_channel_id>"
      }
    }
  }
}
```

### Verify installation

Once configured, you'll have access to these tools:

#### 1. start_thread

Start a new thread with a given title and content

**Parameters:**
- `title`: (Required) The thread title
- `content`: (Required) The thread content
- `member_name`: (Optional) Member name to mention in the thread

#### 2. update_thread

Update an existing thread with new content

**Parameters:**
- `thread_id`: (Required) The thread ID as a string in the format '1743086901347'
- `content`: (Required) The content to update in the thread
- `member_name`: (Optional) Member name to mention in the thread

#### 3. read_thread

Read replies in a thread

**Parameters:**
- `thread_id`: (Required) The thread ID as a string in the format '1743086901347'

#### 4. list_threads

List threads in channel with pagination

**Parameters:**
- `limit`: (Optional, default 50) Maximum number of items to retrieve or page size
- `cursor`: (Optional) Pagination cursor for the next page of results, returned by previous list_thread tool call.

#### 5. get_member_by_name

Get a member by its name

**Parameters:**
- `name`: (Required) Member name

#### 6. list_members

List all members in the team

## Usage Examples

Some ideas for user prompts are:

```
Please start a thread in teams with the following content... 
```

```
Please list members in team
```

```
Please perform this task... and send results to a new thread in teams. Remember to mention "User Name"
```

```
Please read latest team threads and reply to threads that mention "Your bot name" 
```




```

--------------------------------------------------------------------------------
/src/mcp_teams_server/__init__.py:
--------------------------------------------------------------------------------

```python
# SPDX-FileCopyrightText: 2025 INDUSTRIA DE DISEÑO TEXTIL, S.A. (INDITEX, S.A.)
# SPDX-License-Identifier: Apache-2.0
import logging
import os
import sys
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from dataclasses import dataclass
from importlib import metadata

from azure.identity.aio import ClientSecretCredential
from botbuilder.integration.aiohttp import (
    CloudAdapter,
    ConfigurationBotFrameworkAuthentication,
)
from dotenv import load_dotenv
from mcp.server.fastmcp import Context, FastMCP
from msgraph.graph_service_client import GraphServiceClient
from pydantic import Field

from .config import BotConfiguration
from .teams import (
    PagedTeamsMessages,
    TeamsClient,
    TeamsMember,
    TeamsMessage,
    TeamsThread,
)

try:
    __version__ = metadata.version("mcp-teams-server")
except metadata.PackageNotFoundError:
    __version__ = "unknown"

# Load .env
load_dotenv()

# Config logging
logging.basicConfig(
    level=os.environ.get("MCP_LOGLEVEL", "ERROR"),
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    handlers=[
        logging.StreamHandler(sys.stderr),
    ],
)

LOGGER = logging.getLogger(__name__)

REQUIRED_ENV_VARS = [
    "TEAMS_APP_ID",
    "TEAMS_APP_PASSWORD",
    "TEAMS_APP_TYPE",
    "TEAMS_APP_TENANT_ID",
    "TEAM_ID",
    "TEAMS_CHANNEL_ID",
]


@dataclass
class AppContext:
    client: TeamsClient


@asynccontextmanager
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
    """Manage application lifecycle with type-safe context"""

    # Bot adapter construction
    bot_config = BotConfiguration()
    adapter = CloudAdapter(ConfigurationBotFrameworkAuthentication(bot_config))

    # Graph client construction
    credentials = ClientSecretCredential(
        bot_config.APP_TENANTID, bot_config.APP_ID, bot_config.APP_PASSWORD
    )
    scopes = ["https://graph.microsoft.com/.default"]
    graph_client = GraphServiceClient(credentials=credentials, scopes=scopes)

    client = TeamsClient(
        adapter,
        graph_client,
        bot_config.APP_ID,
        bot_config.TEAM_ID,
        bot_config.TEAMS_CHANNEL_ID,
    )
    yield AppContext(client=client)


mcp = FastMCP(
    "mcp-teams-server",
    lifespan=app_lifespan,
    dependencies=[
        "aiohttp",
        "asyncio",
        "botbuilder-core",
        "botbuilder-integration-aiohttp",
        "dotenv",
        "msgraph-sdk",
        "multidict",
    ],
)


def _get_teams_client(ctx: Context) -> TeamsClient:
    return ctx.request_context.lifespan_context.client


@mcp.tool(
    name="start_thread", description="Start a new thread with a given title and content"
)
async def start_thread(
    ctx: Context,
    title: str = Field(description="The thread title"),
    content: str = Field(description="The thread content"),
    member_name: str | None = Field(
        description="Member name to mention in the thread", default=None
    ),
) -> TeamsThread:
    await ctx.debug(f"start_thread with title={title} and content={content}")
    client = _get_teams_client(ctx)
    return await client.start_thread(title, content, member_name)


@mcp.tool(
    name="update_thread", description="Update an existing thread with new content"
)
async def update_thread(
    ctx: Context,
    thread_id: str = Field(
        description="The thread ID as a string in the format '1743086901347'"
    ),
    content: str = Field(description="The content to update in the thread"),
    member_name: str | None = Field(
        description="Member name to mention in the thread", default=None
    ),
) -> TeamsMessage:
    await ctx.debug(f"update_thread with thread_id={thread_id} and content={content}")
    client = _get_teams_client(ctx)
    return await client.update_thread(thread_id, content, member_name)


@mcp.tool(name="read_thread", description="Read replies in a thread")
async def read_thread(
    ctx: Context,
    thread_id: str = Field(
        description="The thread ID as a string in the format '1743086901347'"
    ),
) -> PagedTeamsMessages:
    await ctx.debug(f"read_thread with thread_id={thread_id}")
    client = _get_teams_client(ctx)
    return await client.read_thread_replies(thread_id, 50)


@mcp.tool(name="list_threads", description="List threads in channel with pagination")
async def list_threads(
    ctx: Context,
    limit: int = Field(
        description="Maximum number of items to retrieve or page size", default=50
    ),
    cursor: str | None = Field(
        description="Pagination cursor for the next page of results", default=None
    ),
) -> PagedTeamsMessages:
    await ctx.debug(f"list_threads with cursor={cursor} and limit={limit}")
    client = _get_teams_client(ctx)
    return await client.read_threads(limit, cursor)


@mcp.tool(name="get_member_by_name", description="Get a member by its name")
async def get_member_by_name(
    ctx: Context, name: str = Field(description="Member name")
):
    await ctx.debug(f"get_member_by_name with name={name}")
    client = _get_teams_client(ctx)
    return await client.get_member_by_name(name)


@mcp.tool(name="list_members", description="List all members in the team")
async def list_members(ctx: Context) -> list[TeamsMember]:
    await ctx.debug("list_members")
    client = _get_teams_client(ctx)
    return await client.list_members()


def _check_required_environment():
    exit_code = None
    for var in REQUIRED_ENV_VARS:
        value = os.environ.get(var)
        if value is None:
            LOGGER.info(f"Required ENV {var} not present")
            exit_code = 1
    if exit_code is not None:
        sys.exit(exit_code)


def main() -> None:
    import argparse

    parser = argparse.ArgumentParser(
        description="MCP Teams Server to allow Microsoft Teams interaction"
    )

    default_transport = os.environ.get("MCP_TRANSPORT", "stdio")

    parser.add_argument(
        "-t",
        "--transport",
        nargs=1,
        type=str,
        help="MCP Server Transport: stdio or sse",
        default=default_transport,
        choices=["stdio", "sse"],
    )

    args = parser.parse_args()

    LOGGER.info(
        f'Starting MCP Teams Server "{__version__}" with transport "{args.transport}"'
    )
    _check_required_environment()
    mcp.run(transport=args.transport)


if __name__ == "__main__":
    main()

```

--------------------------------------------------------------------------------
/repolinter.json:
--------------------------------------------------------------------------------

```json
{
  "$schema": "https://raw.githubusercontent.com/todogroup/repolinter/master/rulesets/schema.json",
  "version": 2,
  "axioms": {
    "linguist": "language",
    "licensee": "license",
    "packagers": "packager"
  },
  "rules": {
    "license-file-exists": {
      "level": "error",
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": ["LICENSE*", "COPYING*"],
          "nocase": true
        }
      }
    },
    "readme-file-exists": {
      "level": "error",
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": ["README*"],
          "nocase": true
        }
      }
    },
    "contributing-file-exists": {
      "level": "error",
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": ["{docs/,.github/,}CONTRIB*"],
          "nocase": true
        }
      }
    },
    "code-of-conduct-file-exists": {
      "level": "error",
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": [
            "{docs/,.github/,}CODEOFCONDUCT*",
            "{docs/,.github/,}CODE-OF-CONDUCT*",
            "{docs/,.github/,}CODE_OF_CONDUCT*"
          ],
          "nocase": true
        }
      }
    },
    "changelog-file-exists": {
      "level": "error",
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": ["CHANGELOG*"],
          "nocase": true
        }
      }
    },
    "security-file-exists": {
      "level": "error",
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": ["{docs/,.github/,}SECURITY.md"]
        }
      }
    },
    "support-file-exists": {
      "level": "off",
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": ["{docs/,.github/,}SUPPORT*"],
          "nocase": true
        }
      }
    },
    "readme-references-license": {
      "level": "off",
      "rule": {
        "type": "file-contents",
        "options": {
          "globsAll": ["README*"],
          "content": "license",
          "flags": "i"
        }
      }
    },
    "binaries-not-present": {
      "level": "error",
      "rule": {
        "type": "file-type-exclusion",
        "options": {
          "type": ["/*.exe", "/.dll", "!node_modules/"]
        }
      }
    },
    "test-directory-exists": {
      "level": "off",
      "rule": {
        "type": "directory-existence",
        "options": {
          "globsAny": ["/test", "/specs"],
          "nocase": true
        }
      }
    },
    "integrates-with-ci": {
      "level": "error",
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": [
            ".gitlab-ci.yml",
            ".travis.yml",
            "appveyor.yml",
            ".appveyor.yml",
            "circle.yml",
            ".circleci/config.yml",
            "Jenkinsfile",
            ".drone.yml",
            ".github/workflows/",
            "azure-pipelines.yml"
          ]
        }
      }
    },
    "code-of-conduct-file-contains-email": {
      "level": "off",
      "rule": {
        "type": "file-contents",
        "options": {
          "globsAll": [
            "CODEOFCONDUCT",
            "CODE-OF-CONDUCT*",
            "CODE_OF_CONDUCT*",
            ".github/CODEOFCONDUCT*",
            ".github/CODE-OF-CONDUCT*",
            ".github/CODE_OF_CONDUCT*"
          ],
          "content": ".+@.+..+",
          "flags": "i",
          "human-readable-content": "email address"
        }
      }
    },
    "source-license-headers-exist": {
      "level": "warning",
      "rule": {
        "type": "file-starts-with",
        "options": {
          "globsAll": ["./**/*.py"],
          "lineCount": 5,
          "patterns": ["Copyright", "License"],
          "flags": "i"
        }
      }
    },
    "github-issue-template-exists": {
      "level": "error",
      "rule": {
        "type": "file-existence",
        "options": {
          "dirs": true,
          "globsAny": ["ISSUE_TEMPLATE", ".github/ISSUE_TEMPLATE*"]
        }
      }
    },
    "github-pull-request-template-exists": {
      "level": "off",
      "rule": {
        "type": "file-existence",
        "options": {
          "dirs": true,
          "globsAny": [
            "PULL_REQUEST_TEMPLATE*",
            ".github/PULL_REQUEST_TEMPLATE*"
          ]
        }
      }
    },
    "javascript-package-metadata-exists": {
      "level": "error",
      "where": ["language=javascript"],
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": ["package.json"]
        }
      }
    },
    "ruby-package-metadata-exists": {
      "level": "error",
      "where": ["language=ruby"],
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": ["Gemfile"]
        }
      }
    },
    "java-package-metadata-exists": {
      "level": "error",
      "where": ["language=java"],
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": ["pom.xml", "build.xml", "build.gradle"]
        }
      }
    },
    "python-package-metadata-exists": {
      "level": "error",
      "where": ["language=python"],
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": ["setup.py", "requirements.txt", "pyproject.toml"]
        }
      }
    },
    "objective-c-package-metadata-exists": {
      "level": "error",
      "where": ["language=objective-c"],
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": ["Cartfile", "Podfile", ".podspec"]
        }
      }
    },
    "swift-package-metadata-exists": {
      "level": "error",
      "where": ["language=swift"],
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": ["Package.swift"]
        }
      }
    },
    "erlang-package-metadata-exists": {
      "level": "error",
      "where": ["language=erlang"],
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": ["rebar.config"]
        }
      }
    },
    "elixir-package-metadata-exists": {
      "level": "error",
      "where": ["language=elixir"],
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": ["mix.exs"]
        }
      }
    },
    "license-detectable-by-licensee": {
      "level": "off",
      "where": ["license="],
      "rule": {
        "type": "license-detectable-by-licensee",
        "options": {}
      }
    },
    "notice-file-exists": {
      "level": "error",
      "where": ["license=Apache-2.0"],
      "rule": {
        "type": "file-existence",
        "options": {
          "globsAny": ["NOTICE*"],
          "fail-message": "The NOTICE file is described in section 4.4 of the Apache License version 2.0. Its presence is not mandated by the license itself, but by ASF policy."
        }
      }
    },
    "best-practices-badge-present": {
      "level": "off",
      "rule": {
        "type": "best-practices-badge-present"
      }
    },
    "internal-file-not-exists": {
      "level": "off",
      "rule": {
        "type": "file-not-exists",
        "options": {
          "globsAll": [
            ".secrets.baseline",
            "sherpa-config.yml",
            ".snyk",
            "sonar-project.properties",
            ".drafterconfig.yml",
            "application-configmap.yml",
            "application-secret.yml"
          ],
          "nocase": true
        }
      }
    }
  }
}
```

--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------

```
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

1. Definitions.

"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.

"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.

"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.

"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.

"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.

"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.

"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).

"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.

"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."

"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.

2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.

3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.

4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:

     (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and

     (b) You must cause any modified files to carry prominent notices stating that You changed the files; and

     (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and

     (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.

     You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.

5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.

6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.

7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.

8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.

9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.

END OF TERMS AND CONDITIONS

```

--------------------------------------------------------------------------------
/LICENSES/Apache-2.0.txt:
--------------------------------------------------------------------------------

```
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

1. Definitions.

"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.

"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.

"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.

"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.

"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.

"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.

"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).

"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.

"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."

"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.

2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.

3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.

4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:

     (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and

     (b) You must cause any modified files to carry prominent notices stating that You changed the files; and

     (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and

     (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.

     You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.

5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.

6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.

7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.

8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.

9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.

END OF TERMS AND CONDITIONS

APPENDIX: How to apply the Apache License to your work.

To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!)  The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.

Copyright [yyyy] [name of copyright owner]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

```

--------------------------------------------------------------------------------
/src/mcp_teams_server/teams.py:
--------------------------------------------------------------------------------

```python
# SPDX-FileCopyrightText: 2025 INDUSTRIA DE DISEÑO TEXTIL, S.A. (INDITEX, S.A.)
# SPDX-License-Identifier: Apache-2.0
import logging

from botbuilder.core import BotAdapter, TurnContext
from botbuilder.core.teams import TeamsInfo
from botbuilder.integration.aiohttp import CloudAdapter
from botbuilder.schema import (
    Activity,
    ActivityTypes,
    ChannelAccount,
    ConversationAccount,
    ConversationReference,
    Mention,
    TextFormatTypes,
)
from botbuilder.schema.teams import TeamsChannelAccount
from botframework.connector.aio.operations_async import ConversationsOperations
from kiota_abstractions.base_request_configuration import RequestConfiguration
from msgraph.generated.models.chat_message import ChatMessage
from msgraph.generated.teams.item.channels.item.messages.item.chat_message_item_request_builder import (
    ChatMessageItemRequestBuilder,
)
from msgraph.generated.teams.item.channels.item.messages.item.replies.replies_request_builder import (
    RepliesRequestBuilder,
)
from msgraph.generated.teams.item.channels.item.messages.messages_request_builder import (
    MessagesRequestBuilder,
)
from msgraph.graph_service_client import GraphServiceClient
from pydantic import BaseModel, Field

LOGGER = logging.getLogger(__name__)


class TeamsThread(BaseModel):
    thread_id: str = Field(
        description="Thread ID as a string in the format '1743086901347'"
    )
    title: str = Field(description="Message title")
    content: str = Field(description="Message content")


class TeamsMessage(BaseModel):
    thread_id: str = Field(
        description="Thread ID as a string in the format '1743086901347'"
    )
    message_id: str = Field(description="Message ID")
    content: str = Field(description="Message content")


class TeamsMember(BaseModel):
    name: str = Field(
        description="Member name used in mentions and user information cards"
    )
    email: str = Field(description="Member email")


class PagedTeamsMessages(BaseModel):
    cursor: str | None = Field(
        description="Cursor to retrieve the next page of messages."
    )
    limit: int = Field(description="Page limit, maximum number of items to retrieve")
    total: int = Field(description="Total items available for retrieval")
    items: list[TeamsMessage] = Field(description="List of channel messages or threads")


class TeamsClient:
    def __init__(
        self,
        adapter: CloudAdapter,
        graph_client: GraphServiceClient,
        teams_app_id: str,
        team_id: str,
        teams_channel_id: str,
    ):
        self.adapter = adapter
        self.graph_client = graph_client
        self.teams_app_id = teams_app_id
        self.team_id = team_id
        self.teams_channel_id = teams_channel_id
        self.service_url = None
        self.adapter.on_turn_error = self.on_turn_error

    def get_team_id(self):
        return self.team_id

    @staticmethod
    async def on_turn_error(context: TurnContext, error: Exception):
        LOGGER.error(f"Error {str(error)}")
        # await context.send_activity("An error occurred in the bot, please try again later")

    def _create_conversation_reference(self) -> ConversationReference:
        service_url = "https://smba.trafficmanager.net/emea/"
        if self.service_url is not None:
            service_url = self.service_url
        return ConversationReference(
            bot=TeamsChannelAccount(id=self.teams_app_id, name="MCP Bot"),
            channel_id=self.teams_channel_id,
            service_url=service_url,
            conversation=ConversationAccount(
                id=self.teams_channel_id,
                is_group=True,
                conversation_type="channel",
                name="Teams channel",
            ),
        )

    async def _initialize(self):
        if not self.service_url:

            def context_callback(context: TurnContext):
                self.service_url = context.activity.service_url

            await self.adapter.continue_conversation(
                bot_app_id=self.teams_app_id,
                reference=self._create_conversation_reference(),
                callback=context_callback,
            )

    async def start_thread(
        self, title: str, content: str, member_name: str | None = None
    ) -> TeamsThread:
        """Start a new thread in a channel.

        Args:
            title: Thread title
            content: Initial thread content
            member_name: Member name to mention in content

        Returns:
            Created thread details including ID
        """
        try:
            await self._initialize()

            result = TeamsThread(title=title, content=content, thread_id="")

            async def start_thread_callback(context: TurnContext):
                mention_member = None
                if member_name is not None:
                    members = await TeamsInfo.get_team_members(context, self.team_id)
                    for member in members:
                        if member.name == member_name:
                            mention_member = member

                mentions = []
                if mention_member is not None:
                    result.content = (
                        f"# **{title}**\n<at>{mention_member.name}</at> {content}"
                    )
                    mention = Mention(
                        text=f"<at>{mention_member.name}</at>",
                        type="mention",
                        mentioned=ChannelAccount(
                            id=mention_member.id, name=mention_member.name
                        ),
                    )
                    mentions.append(mention)

                response = await context.send_activity(
                    activity_or_text=Activity(
                        type=ActivityTypes.message,
                        topic_name=title,
                        text=result.content,
                        text_format=TextFormatTypes.markdown,
                        entities=mentions,
                    )
                )
                if response is not None:
                    result.thread_id = response.id

            await self.adapter.continue_conversation(
                bot_app_id=self.teams_app_id,
                reference=self._create_conversation_reference(),
                callback=start_thread_callback,
            )

            return result
        except Exception as e:
            LOGGER.error(f"Error creating thread: {str(e)}")
            raise

    @staticmethod
    def _get_conversation_operations(context: TurnContext) -> ConversationsOperations:
        # Hack to get the connector client and reply to an existing activity
        connector_client = context.turn_state[BotAdapter.BOT_CONNECTOR_CLIENT_KEY]
        return connector_client.conversations  # pyright: ignore

    async def update_thread(
        self, thread_id: str, content: str, member_name: str | None = None
    ) -> TeamsMessage:
        """Add a message to an existing thread, mentioning a user optionally.

        Args:
            thread_id: Thread ID to update
            content: Message content to add
            member_name: Member name to mention (optional)

        Returns:
            Updated thread details
        """
        try:
            await self._initialize()

            result = TeamsMessage(thread_id=thread_id, content=content, message_id="")

            async def update_thread_callback(context: TurnContext):
                mention_member = None
                if member_name is not None:
                    members = await TeamsInfo.get_team_members(context, self.team_id)
                    for member in members:
                        if member.name == member_name:
                            mention_member = member

                mentions = []
                if mention_member is not None:
                    result.content = f"<at>{mention_member.name}</at> {content}"
                    mention = Mention(
                        text=f"<at>{mention_member.name}</at>",
                        type="mention",
                        mentioned=ChannelAccount(
                            id=mention_member.id, name=mention_member.name
                        ),
                    )
                    mentions.append(mention)

                reply = Activity(
                    type=ActivityTypes.message,
                    text=result.content,
                    from_property=TeamsChannelAccount(
                        id=self.teams_app_id, name="MCP Bot"
                    ),
                    conversation=ConversationAccount(id=thread_id),
                    entities=mentions,
                )
                #
                # Hack to get the connector client and reply to an existing activity
                #
                conversations = TeamsClient._get_conversation_operations(context)
                #
                # Hack to reply to conversation https://github.com/microsoft/botframework-sdk/issues/6626
                #
                conversation_id = (
                    f"{context.activity.conversation.id};messageid={thread_id}"  # pyright: ignore
                )
                response = await conversations.send_to_conversation(
                    conversation_id=conversation_id, activity=reply
                )

                if response is not None:
                    result.message_id = response.id  # pyright: ignore

            await self.adapter.continue_conversation(
                bot_app_id=self.teams_app_id,
                reference=self._create_conversation_reference(),
                callback=update_thread_callback,
            )

            return result
        except Exception as e:
            LOGGER.error(f"Error updating thread: {str(e)}")
            raise

    async def get_member_by_id(self, member_id: str) -> TeamsMember:
        try:
            await self._initialize()

            result = TeamsMember(name="", email="")

            async def get_member_by_id_callback(context: TurnContext):
                member = await TeamsInfo.get_team_member(
                    context, self.team_id, member_id
                )
                result.name = member.name
                result.email = member.email

            await self.adapter.continue_conversation(
                bot_app_id=self.teams_app_id,
                reference=self._create_conversation_reference(),
                callback=get_member_by_id_callback,
            )
            return result
        except Exception as e:
            LOGGER.error(f"Error updating thread: {str(e)}")
            raise

    async def read_threads(
        self, limit: int = 50, cursor: str | None = None
    ) -> PagedTeamsMessages:
        """Read all threads in configured teams channel.

        Args:
            cursor: The pagination cursor.

            limit: The pagination page size

        Returns:
            Paged team channel messages containing
        """
        try:
            query = MessagesRequestBuilder.MessagesRequestBuilderGetQueryParameters(
                top=limit
            )
            request = RequestConfiguration(query_parameters=query)
            if cursor is not None:
                response = (
                    await self.graph_client.teams.by_team_id(self.team_id)
                    .channels.by_channel_id(self.teams_channel_id)
                    .messages.with_url(cursor)
                    .get(request_configuration=request)
                )
            else:
                response = (
                    await self.graph_client.teams.by_team_id(self.team_id)
                    .channels.by_channel_id(self.teams_channel_id)
                    .messages.get(request_configuration=request)
                )

            result = PagedTeamsMessages(
                cursor=response.odata_next_link,  # pyright: ignore
                limit=limit,
                total=response.odata_count,  # pyright: ignore
                items=[],
            )
            if response.value is not None:  # pyright: ignore
                for message in response.value:  # pyright: ignore
                    result.items.append(
                        TeamsMessage(
                            message_id=message.id,  # pyright: ignore
                            content=message.body.content,  # pyright: ignore
                            thread_id=message.id,  # pyright: ignore
                        )
                    )

            return result
        except Exception as e:
            LOGGER.error(f"Error reading thread: {str(e)}")
            raise

    async def read_thread_replies(
        self, thread_id: str, limit: int = 50, cursor: str | None = None
    ) -> PagedTeamsMessages:
        """Read all replies in a thread.

        Args:
            thread_id: Thread ID to read
            cursor: The pagination cursor
            limit: The pagination page size

        Returns:
            List of thread messages
        """
        try:
            params = RepliesRequestBuilder.RepliesRequestBuilderGetQueryParameters(
                top=limit
            )
            request = RequestConfiguration(query_parameters=params)

            if cursor is not None:
                replies = (
                    await self.graph_client.teams.by_team_id(self.team_id)
                    .channels.by_channel_id(self.teams_channel_id)
                    .messages.by_chat_message_id(thread_id)
                    .replies.with_url(cursor)
                    .get(request_configuration=request)
                )
            else:
                replies = (
                    await self.graph_client.teams.by_team_id(self.team_id)
                    .channels.by_channel_id(self.teams_channel_id)
                    .messages.by_chat_message_id(thread_id)
                    .replies.get(request_configuration=request)
                )

            result = PagedTeamsMessages(
                cursor=cursor,
                limit=limit,
                total=replies.odata_count,  # pyright: ignore
                items=[],
            )

            if replies is not None and replies.value is not None:
                for reply in replies.value:
                    result.items.append(
                        TeamsMessage(
                            message_id=reply.id,  # pyright: ignore
                            content=reply.body.content,  # pyright: ignore
                            thread_id=reply.reply_to_id,  # pyright: ignore
                        )
                    )

            return result
        except Exception as e:
            LOGGER.error(f"Error reading thread: {str(e)}")
            raise

    async def read_message(self, message_id: str) -> ChatMessage | None:
        try:
            query = ChatMessageItemRequestBuilder.ChatMessageItemRequestBuilderGetQueryParameters()
            request = RequestConfiguration(query_parameters=query)
            response = (
                await self.graph_client.teams.by_team_id(self.team_id)
                .channels.by_channel_id(self.teams_channel_id)
                .messages.by_chat_message_id(chat_message_id=message_id)
                .get(request_configuration=request)
            )
            return response
        except Exception as e:
            LOGGER.error(f"Error reading thread: {str(e)}")
            raise

    async def list_members(self) -> list[TeamsMember]:
        """List all members in the configured team.

        Returns:
            List of team member details
        """
        try:
            await self._initialize()
            result = []

            async def list_members_callback(context: TurnContext):
                members = await TeamsInfo.get_team_members(context, self.team_id)
                for member in members:
                    result.append(TeamsMember(name=member.name, email=member.email))

            await self.adapter.continue_conversation(
                bot_app_id=self.teams_app_id,
                reference=self._create_conversation_reference(),
                callback=list_members_callback,
            )
            return result
        except Exception as e:
            LOGGER.error(f"Error listing members: {str(e)}")
            raise

    async def get_member_by_name(self, name: str) -> TeamsMember | None:
        members = await self.list_members()
        for member in members:
            if member.name == name:
                return member

```