# 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: -------------------------------------------------------------------------------- ``` 1 | 3.10 2 | ``` -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` 1 | .env 2 | *.env 3 | .github 4 | .venv 5 | app 6 | doc 7 | tests ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | MANIFEST 23 | 24 | # Virtual Environment 25 | .env 26 | .venv 27 | venv/ 28 | ENV/ 29 | env/ 30 | 31 | # IDE 32 | .idea/ 33 | .vscode/ 34 | *.swp 35 | *.swo 36 | *~ 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | *.py,cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | cover/ 52 | 53 | # mypy 54 | .mypy_cache/ 55 | .dmypy.json 56 | dmypy.json 57 | 58 | # Logs 59 | *.log 60 | log/ 61 | logs/ 62 | 63 | # Local development 64 | .env.local 65 | .env.development.local 66 | .env.test.local 67 | .env.production.local 68 | 69 | # Distribution / packaging 70 | .Python 71 | build/ 72 | develop-eggs/ 73 | dist/ 74 | downloads/ 75 | eggs/ 76 | .eggs/ 77 | lib/ 78 | lib64/ 79 | parts/ 80 | sdist/ 81 | var/ 82 | wheels/ 83 | *.egg-info/ 84 | .installed.cfg 85 | *.egg 86 | MANIFEST 87 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | [](https://sonarcloud.io/summary/new_code?id=InditexTech_mcp-teams-server) 2 | [](https://sonarcloud.io/summary/new_code?id=InditexTech_mcp-teams-server) 3 | [](https://sonarcloud.io/summary/new_code?id=InditexTech_mcp-teams-server) 4 |  5 |  6 | [](https://scorecard.dev/viewer/?uri=github.com/InditexTech/mcp-teams-server) 7 | <!-- [](https://www.bestpractices.dev/projects/10400) --> 8 | 9 | 10 | # MCP Teams Server 11 | 12 | An MCP ([Model Context Protocol](https://modelcontextprotocol.io/introduction)) server implementation for 13 | [Microsoft Teams](https://www.microsoft.com/en-us/microsoft-teams/group-chat-software/) integration, providing capabilities to 14 | read messages, create messages, reply to messages, mention members. 15 | 16 | ## Features 17 | 18 | https://github.com/user-attachments/assets/548a9768-1119-4a2d-bd5c-6b41069fc522 19 | 20 | - Start thread in channel with title and contents, mentioning users 21 | - Update existing threads with message replies, mentioning users 22 | - Read thread replies 23 | - List channel team members 24 | - Read channel messages 25 | 26 | ## Prerequisites 27 | 28 | - [uv](https://github.com/astral-sh/uv) package manager 29 | - [Python 3.10](https://www.python.org/) 30 | - Microsoft Teams account with [proper set-up](./doc/MS-Teams-setup.md) 31 | 32 | ## Installation 33 | 34 | 1. Clone the repository: 35 | 36 | ```bash 37 | git clone [repository-url] 38 | cd mcp-teams-server 39 | ``` 40 | 41 | 2. Create a virtual environment and install dependencies: 42 | 43 | ```bash 44 | uv venv 45 | uv sync --frozen --all-extras --dev 46 | ``` 47 | 48 | ## Teams configuration 49 | 50 | Please read [this document](./doc/MS-Teams-setup.md) to help you to configure Microsoft Teams and required 51 | Azure resources. It is not a step-by-step guide but can help you figure out what you will need. 52 | 53 | ## Usage 54 | 55 | Set up the following environment variables in your shell or in an .env file. You can use [sample file](./sample.env) 56 | as a template: 57 | 58 | | Key | Description | 59 | |-------------------------|--------------------------------------------| 60 | | **TEAMS_APP_ID** | UUID for your MS Entra ID application ID | 61 | | **TEAMS_APP_PASSWORD** | Client secret | 62 | | **TEAMS_APP_TYPE** | SingleTenant or MultiTenant | 63 | | **TEAMS_APP_TENANT_ID** | Tenant uuid in case of SingleTenant | 64 | | **TEAM_ID** | MS Teams Group Id or Team Id | 65 | | **TEAMS_CHANNEL_ID** | MS Teams Channel ID with url escaped chars | 66 | 67 | Start the server: 68 | 69 | ```bash 70 | uv run mcp-teams-server 71 | ``` 72 | 73 | ## Development 74 | 75 | Integration tests require the set-up the following environment variables: 76 | 77 | | Key | Description | 78 | |------------------------|--------------------------------| 79 | | **TEST_THREAD_ID** | timestamp of the thread id | 80 | | **TEST_MESSAGE_ID** | timestamp of the message id | 81 | | **TEST_USER_NAME** | test user name | 82 | 83 | 84 | ```bash 85 | uv run pytest -m integration 86 | ``` 87 | 88 | ### Pre-built docker image 89 | 90 | There is a [pre-built image](https://github.com/InditexTech/mcp-teams-server/pkgs/container/mcp-teams-server) hosted in ghcr.io. 91 | You can install this image by running the following command 92 | 93 | ```commandline 94 | docker pull ghcr.io/inditextech/mcp-teams-server:latest 95 | ``` 96 | 97 | ### Build docker image 98 | 99 | A docker image is available to run MCP server. You can build it with the following command: 100 | 101 | ```bash 102 | docker build . -t inditextech/mcp-teams-server 103 | ``` 104 | 105 | ### Run docker image 106 | 107 | Basic run configuration: 108 | 109 | ```bash 110 | docker run -it inditextech/mcp-teams-server 111 | ``` 112 | 113 | Run with environment variables from .env file: 114 | 115 | ```bash 116 | docker run --env-file .env -it inditextech/mcp-teams-server 117 | ``` 118 | 119 | ### Setup LLM to use MCP Teams Server 120 | 121 | Please follow instructions on the [following document](./llms-install.md) 122 | 123 | ## Changelog 124 | 125 | See [CHANGELOG.md](CHANGELOG.md) for a list of changes and version history. 126 | 127 | ## Contributing 128 | 129 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull 130 | requests. 131 | 132 | ## Security 133 | 134 | For security concerns, please see our [Security Policy](SECURITY.md). 135 | 136 | ## License 137 | 138 | This project is licensed under the [Apache-2.0](LICENSE.txt) file for details. 139 | 140 | © 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) 141 | ``` -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- ```markdown 1 | # Code of Conduct 2 | 3 | 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. 4 | 5 | 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 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We release patches for security vulnerabilities. Currently supported versions: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.0.x | :white_check_mark: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | 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. 14 | 15 | **Please do not report security vulnerabilities through public GitHub issues.** 16 | 17 | Instead, please report them via our [disclosure submission program](https://vdp.inditex.com). 18 | 19 | ## Preferred Languages 20 | 21 | We prefer all communications to be in English. 22 | 23 | ## Process 24 | 25 | 1. Security report received 26 | 2. Security team acknowledges receipt within 48 hours 27 | 3. Team investigates and determines severity 28 | 4. Team develops and tests fix 29 | 5. Team prepares advisory and patches 30 | 6. Advisory published, patches released 31 | 32 | ## Safe Harbor 33 | 34 | We support safe harbor for security researchers who: 35 | 36 | 1. Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our services 37 | 2. Only interact with accounts you own or with explicit permission of the account holder 38 | 3. Provide us with a reasonable amount of time to resolve vulnerabilities prior to any disclosure to the public or a third-party 39 | 4. Do not exploit a security issue for purposes other than immediate testing 40 | 41 | ## Security Controls 42 | 43 | The MCP Teams Server implements several security controls: 44 | 45 | 1. All authentication tokens and credentials must be provided via environment variables 46 | 2. Communications with the Teams API use HTTPS/TLS encryption 47 | 3. Input validation and sanitization for all API endpoints 48 | 4. Rate limiting to prevent abuse 49 | 5. Regular security updates and dependency checks 50 | 51 | ## Security-related Configuration 52 | 53 | For secure operation, please ensure: 54 | 55 | 1. Environment variables are properly secured and not logged 56 | 2. Access tokens have minimum required permissions 57 | 3. Production deployments use HTTPS/TLS 58 | 4. Regular security updates are applied 59 | 5. Proper logging and monitoring is configured 60 | 61 | ## Third-party Security Notifications 62 | 63 | We review security reports for our dependencies and follow responsible disclosure guidelines. 64 | ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to this project! We value and appreciate any contributions you can make. 4 | To maintain a collaborative and respectful environment, please consider the following guidelines when contributing to 5 | this project. 6 | 7 | ## Prerequisites 8 | 9 | - Before starting to contribute to the code, you must first sign the 10 | [Contributor License Agreement (CLA)](https://github.com/InditexTech/foss/blob/main/documents/CLA.pdf). 11 | Detailed instructions on how to proceed can be found [here](https://github.com/InditexTech/foss/blob/main/CONTRIBUTING.md). 12 | 13 | ## How to Contribute 14 | 15 | 1. Open an issue to discuss and gather feedback on the feature or fix you wish to address. 16 | 2. Fork the repository and clone it to your local machine. 17 | 3. Create a new branch to work on your contribution: `git checkout -b your-branch-name`. 18 | 4. Make the necessary changes in your local branch. 19 | 5. Ensure that your code follows the established project style and formatting guidelines. 20 | 6. Perform testing to ensure your changes do not introduce errors. 21 | 7. Make clear and descriptive commits that explain your changes. 22 | 8. Push your branch to the remote repository: `git push origin your-branch-name`. 23 | 9. Open a pull request describing your changes and linking the corresponding issue. 24 | 10. Await comments and discussions on your pull request. Make any necessary modifications based on the received feedback. 25 | 11. Once your pull request is approved, your contribution will be merged into the main branch. 26 | 27 | ## Contribution Guidelines 28 | 29 | - All contributors are expected to follow the project's [code of conduct](CODE_of_CONDUCT.md). Please be respectful and 30 | considerate towards other contributors. 31 | - Before starting work on a new feature or fix, check existing [issues](../../issues) and [pull requests](../../pulls) 32 | to avoid duplications and unnecessary discussions. 33 | - If you wish to work on an existing issue, comment on the issue to inform other contributors that you are working on it. 34 | This will help coordinate efforts and prevent conflicts. 35 | - It is always advisable to discuss and gather feedback from the community before making significant changes to the 36 | project's structure or architecture. 37 | - 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/) 38 | - Document any new changes or features you add. This will help other contributors and project users understand your work 39 | and its purpose. 40 | - Be sure to link the corresponding issue in your pull request to maintain proper tracking of contributions. 41 | - Remember to add license and copyright information following the [REUSE Specification](https://reuse.software/spec/#copyright-and-licensing-information). 42 | 43 | ## Development 44 | 45 | Make sure that you have: 46 | 47 | - Read the rest of the [`CONTRIBUTING.md`](CONTRIBUTING.md) sections. 48 | - Meet the [prerequisites](#prerequisites). 49 | - [uv](https://github.com/astral-sh/uv) installed 50 | - [python](https://www.python.org/) 3.10 or later installed 51 | - Set up integration with Microsoft Teams by your own means 52 | - Run integration tests to verify Microsoft Teams integration 53 | 54 | Please remember tu run linting locally before committing any changes: 55 | 56 | ```bash 57 | uv run ruff check . --fix 58 | uv run ruff format . 59 | ``` 60 | 61 | It is recommended to run a type checker: 62 | 63 | ```bash 64 | uv run pyright 65 | ``` 66 | 67 | ## Technical details 68 | 69 | This MCP is built using [MCP SDK](https://github.com/modelcontextprotocol/python-sdk) for Python from Anthropic. 70 | It uses FastMCP to implement tools offered by the MCP server. 71 | 72 | This MCP server consumes two Microsoft APIs / frameworks: 73 | 74 | - [Azure Bot Builder](https://github.com/microsoft/botbuilder-python) for python 75 | - [Microsoft Graph SDK](https://github.com/microsoftgraph/msgraph-sdk-python) for python 76 | 77 | Azure Bot Builder allows a bot (Microsoft Entra ID app) to consume a Microsoft REST API to send messages to channels 78 | (but it is not capable of consuming messages because the bot is not deployed in Azure). The REST API client is 79 | encapsulated inside the framework classes, and it is not used directly. 80 | In order to send messages without actually being deployed in Azure, the bot "continues" a conversation to retrieve 81 | the TurnContext instance and perform actions on "activities". This technique is called 82 | [proactive messaging](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages?tabs=python) 83 | 84 | Replying to messages through bot builder is not possible without the help of 85 | [this hack](https://github.com/microsoft/botframework-sdk/issues/6626), because the bot builder framework 86 | is not ready for this use (although the internal REST API allows it, and it works like a charm). 87 | 88 | Azure Bot Builder allows to perform any write operation, but reading messages or previous threads is not possible 89 | without a special "migration" permission. Because of that, we have preferred to use Microsoft Graph to read messages. 90 | Microsoft Application Entra ID must have been granted permissions to read messages in a channel. 91 | 92 | ``` -------------------------------------------------------------------------------- /glama.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "$schema": "https://glama.ai/mcp/schemas/server.json", 3 | "maintainers": [ 4 | "marianoao" 5 | ] 6 | } ``` -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- ```python 1 | # SPDX-FileCopyrightText: 2025 INDUSTRIA DE DISEÑO TEXTIL, S.A. (INDITEX, S.A.) 2 | # SPDX-License-Identifier: Apache-2.0 3 | ``` -------------------------------------------------------------------------------- /src/mcp_teams_server/__main__.py: -------------------------------------------------------------------------------- ```python 1 | # SPDX-FileCopyrightText: 2025 INDUSTRIA DE DISEÑO TEXTIL, S.A. (INDITEX, S.A.) 2 | # SPDX-License-Identifier: Apache-2.0 3 | from mcp_teams_server import main 4 | 5 | main() 6 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-issue.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | name: Bug Report 3 | about: Use this template to report a bug 4 | title: '' 5 | labels: kind/bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Detailed description 11 | 12 | A clear and concise description of what the problem is. 13 | 14 | ### Expected behaviour 15 | 16 | Expected behaviour one the problem is fixed. ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-issue.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | name: Feature request 3 | about: Suggest an idea/feature for this project 4 | title: '' 5 | labels: 'kind/feature' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Motivation 11 | 12 | Describe here the motivation of the request. 13 | 14 | ### Acceptance criteria 15 | 16 | - [ ] A checklist of tasks to be done to assume the issue addressed ``` -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- ``` 1 | TEAMS_APP_ID=<put your azure bot application id here> 2 | TEAMS_APP_PASSWORD=<put your teams app password here> 3 | TEAMS_APP_TYPE=SingleTenant 4 | TEAMS_APP_TENANT_ID=<put your azure bot application id application id here> 5 | TEAM_ID=<put your MS teams group id here> 6 | TEAMS_CHANNEL_ID=<pur your MS teams channel id here> 7 | 8 | TEST_THREAD_ID=<test thread id> 9 | TEST_MESSAGE_ID=<test message id> 10 | TEST_USER_NAME=<test user name> ``` -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- ```markdown 1 | ### Release process 2 | 3 | Review and integrate desired works and PRs into master branch before launching a release. 4 | 5 | Release process is triggered from a PR labeled with "kind/release" label. 6 | 7 | Please review [version](./pyproject.toml) in project section because tag is inferred from that value 8 | 9 | Once the PR is closed, the release process will begin. 10 | 11 | To repeat a failing release, remember to remove the `version` tag previously created. ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.0] - 2025-04-01 9 | 10 | ### Added 11 | - First release of MCP Teams Server 12 | - Basic Teams integration through MCP tools 13 | - Documentation for setup and usage 14 | - Security guidelines and policies 15 | - Basic workflows for PR verify and release docker image 16 | ``` -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- ```yaml 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "uv" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | ignore: 13 | - dependency-name: "aiohttp" 14 | ``` -------------------------------------------------------------------------------- /src/mcp_teams_server/config.py: -------------------------------------------------------------------------------- ```python 1 | # SPDX-FileCopyrightText: 2025 INDUSTRIA DE DISEÑO TEXTIL, S.A. (INDITEX, S.A.) 2 | # SPDX-License-Identifier: Apache-2.0 3 | import os 4 | from dataclasses import dataclass 5 | 6 | 7 | @dataclass 8 | class BotConfiguration: 9 | def __init__(self): 10 | self.APP_ID = os.environ.get("TEAMS_APP_ID", "") 11 | self.APP_PASSWORD = os.environ.get("TEAMS_APP_PASSWORD", "") 12 | self.APP_TYPE = os.environ.get("TEAMS_APP_TYPE", "SingleTenant") 13 | self.APP_TENANTID = os.environ.get("TEAMS_APP_TENANT_ID", "") 14 | self.TEAM_ID = os.environ.get("TEAM_ID", "") 15 | self.TEAMS_CHANNEL_ID = os.environ.get("TEAMS_CHANNEL_ID", "") 16 | ``` -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- ```toml 1 | version = 1 2 | SPDX-PackageName = "mcp-teams-server" 3 | SPDX-PackageSupplier = "2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.)" 4 | SPDX-PackageDownloadLocation = "https://github.com/InditexTech/mcp-teams-server" 5 | 6 | [[annotations]] 7 | path = [ 8 | ".gitignore", 9 | ".dockerignore", 10 | ".github/**", 11 | "*.md", 12 | ".python-version", 13 | "repolinter.json", 14 | "glama.json", 15 | "REUSE.toml", 16 | "pyproject.toml", 17 | "NOTICE", 18 | "Dockerfile", 19 | "doc/**", 20 | "app/*", 21 | "sample.env", 22 | "uv.lock", 23 | ] 24 | SPDX-FileCopyrightText = "2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.)" 25 | SPDX-License-Identifier = "Apache-2.0" ``` -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- ```python 1 | # SPDX-FileCopyrightText: 2025 INDUSTRIA DE DISEÑO TEXTIL, S.A. (INDITEX, S.A.) 2 | # SPDX-License-Identifier: Apache-2.0 3 | import os 4 | import sys 5 | from unittest.mock import patch 6 | 7 | import pytest 8 | 9 | import mcp_teams_server 10 | from mcp_teams_server import main 11 | 12 | 13 | def test_main_should_exit_error_on_missing_env_vars(): 14 | # Unset environment 15 | for var in mcp_teams_server.REQUIRED_ENV_VARS: 16 | os.environ.pop(var, None) 17 | 18 | test_args = ["main"] 19 | with patch.object(sys, "argv", test_args): 20 | with pytest.raises(SystemExit) as exit_code: 21 | main() 22 | 23 | assert exit_code.type is SystemExit 24 | assert exit_code.value.code == 1 25 | 26 | 27 | @pytest.mark.asyncio 28 | async def test_list_tools(): 29 | tools = await mcp_teams_server.mcp.list_tools() 30 | 31 | assert tools is not None 32 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | FROM ghcr.io/astral-sh/uv:python3.10-alpine 2 | 3 | # ENV TEAMS_APP_ID="" TEAMS_APP_PASSWORD="" TEAMS_APP_TYPE="" TEAMS_APP_TENANT_ID="" TEAM_ID="" TEAMS_CHANNEL_ID="" 4 | 5 | LABEL \ 6 | org.opencontainers.image.vendor="Industria de Diseño Textil, S.A." \ 7 | org.opencontainers.image.source="https://github.com/InditexTech/mcp-teams-server" \ 8 | org.opencontainers.image.authors="Open Source Office Team" \ 9 | org.opencontainers.image.title="MCP Teams Server" \ 10 | org.opencontainers.image.description="MCP Teams Server container image" \ 11 | org.opencontainers.image.licenses="Apache-2.0" 12 | 13 | # Settings for faster container start 14 | ENV UV_COMPILE_BYTECODE=0 UV_PYTHON_DOWNLOADS=0 UV_LINK_MODE=copy 15 | 16 | COPY pyproject.toml LICENSE.txt *.md uv.lock src /app/ 17 | WORKDIR /app 18 | RUN uv sync --frozen --no-dev 19 | 20 | CMD ["uv", "run", "--frozen", "--no-dev", "mcp-teams-server"] ``` -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- ```markdown 1 | # PR Checklist 2 | 3 | **Friendly reminder**: please read [CONTRIBUTING.md](../CONTRIBUTING.md#prerequisites), specially CLA in case you are 4 | a contributor and not a maintainer. 5 | 6 | Please check if your PR fulfills the following requirements: 7 | 8 | - [ ] Tests for the changes have been added (for bug fixes / features) 9 | - [ ] Docs have been added / updated (for bug fixes / features) 10 | 11 | ## PR Type 12 | 13 | What kind of change does this PR introduce? 14 | 15 | <!-- Please check the one that applies to this PR using "x". --> 16 | 17 | - [ ] Bugfix 18 | - [ ] Feature 19 | - [ ] Other... Please describe: 20 | 21 | ## What is the current behavior? 22 | <!-- Please describe the current behavior that you are modifying, or link to a relevant issue. --> 23 | 24 | Issue Number: N/A 25 | 26 | ## What is the new behavior? 27 | 28 | ## Does this PR introduce a breaking change? 29 | 30 | - [ ] Yes 31 | - [ ] No 32 | 33 | <!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. --> 34 | 35 | ## Other information ``` -------------------------------------------------------------------------------- /.github/workflows/scorecard-analysis.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Scorecard analysis workflow 2 | permissions: read-all 3 | on: 4 | push: 5 | # Only the default branch is supported. 6 | branches: [ 'master' ] 7 | 8 | 9 | jobs: 10 | analysis: 11 | name: Scorecard analysis 12 | runs-on: ubuntu-latest 13 | permissions: 14 | # Needed for GitHub OIDC token if publish_results is true 15 | id-token: write 16 | steps: 17 | - name: "Checkout code" 18 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: "Run analysis" 23 | uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 24 | with: 25 | results_file: results.sarif 26 | results_format: sarif 27 | # Scorecard team runs a weekly scan of public GitHub repos, 28 | # see https://github.com/ossf/scorecard#public-data. 29 | # Setting `publish_results: true` helps us scale by leveraging your workflow to 30 | # extract the results instead of relying on our own infrastructure to run scans. 31 | # And it's free for you! 32 | publish_results: true 33 | 34 | # Upload the results as artifacts (optional). Commenting out will disable 35 | # uploads of run results in SARIF format to the repository Actions tab. 36 | # https://docs.github.com/en/actions/advanced-guides/storing-workflow-data-as-artifacts 37 | - name: "Upload artifact" 38 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 39 | with: 40 | name: SARIF file 41 | path: results.sarif 42 | retention-days: 5 ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [project] 2 | name = "mcp-teams-server" 3 | version = "1.0.6" 4 | description = "An MCP server implementation for Microsoft Teams integration" 5 | readme = "README.md" 6 | keywords = ["mcp", "llm", "automation", "Microsoft Teams"] 7 | authors = [{ name = "Industria de Diseño Textil S.A." }] 8 | maintainers = [ 9 | { name = "Mariano Alonso Ortiz", email = "[email protected]" }, 10 | ] 11 | license = { text = "Apache-2.0" } 12 | requires-python = ">=3.10" 13 | dependencies = [ 14 | "aiohttp==3.10.11", 15 | "asyncio>=3.4.3", 16 | "botbuilder-core>=4.17.0", 17 | "botbuilder-integration-aiohttp>=4.17.0", 18 | "dotenv>=0.9.9", 19 | "mcp[cli]>=1.12.0", 20 | "msgraph-sdk>=1.37.0", 21 | "multidict>=6.6.3", 22 | ] 23 | 24 | [project.urls] 25 | Repository = "https://github.com/InditexTech/mcp-teams-server" 26 | Issues = "https://github.com/InditexTech/mcp-teams-server/issues" 27 | 28 | [project.scripts] 29 | mcp-teams-server = "mcp_teams_server:main" 30 | 31 | [build-system] 32 | requires = ["hatchling"] 33 | build-backend = "hatchling.build" 34 | 35 | [dependency-groups] 36 | dev = [ 37 | "mock>=5.2.0", 38 | "pyproject-parser[cli]>=0.13.0", 39 | "pyright>=1.1.398", 40 | "pytest>=8.3.5", 41 | "pytest-asyncio>=0.25.3", 42 | "pytest-cov>=6.0.0", 43 | "reuse>=5.0.2", 44 | "ruff>=0.11.2", 45 | ] 46 | 47 | [tool.ruff] 48 | line-length = 88 49 | target-version = "py310" 50 | 51 | [tool.ruff.lint] 52 | select = ["E", "F", "I", "UP"] 53 | ignore = [] 54 | 55 | [tool.ruff.lint.per-file-ignores] 56 | "src/mcp_teams_server/teams.py" = ["E501"] 57 | 58 | [tool.pytest.ini_options] 59 | pythonpath = ["src"] 60 | testpaths = ["tests"] 61 | python_files = ["test_*.py"] 62 | addopts = "-v --tb=short --import-mode=importlib --strict-markers -m \"not integration\"" 63 | asyncio_mode = "auto" 64 | asyncio_default_fixture_loop_scope = "session" 65 | markers = [ 66 | "integration: integration tests", 67 | ] 68 | filterwarnings = [ 69 | "ignore::DeprecationWarning" 70 | ] 71 | ``` -------------------------------------------------------------------------------- /.github/workflows/sonar-cloud-analysis.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Tests 2 | 3 | permissions: 4 | contents: read 5 | 6 | concurrency: 7 | group: tests-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | on: 11 | workflow_dispatch: 12 | pull_request: 13 | types: [ closed ] 14 | branches: [ 'master' ] 15 | release: 16 | types: 17 | - published 18 | 19 | jobs: 20 | unit-tests: 21 | name: Tests 22 | timeout-minutes: 30 23 | if: ${{ ((github.event.pull_request.merged == true && github.base_ref == 'master') || 24 | (github.event_name == 'workflow_dispatch' || 25 | github.event_name == 'release')) 26 | && vars.IS_INDITEXTECH_REPO == 'true' }} 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 30 | with: 31 | fetch-depth: 0 32 | ref: ${{ github.event.pull_request.head.sha }} 33 | 34 | - name: Install UV 35 | uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5 36 | with: 37 | enable-cache: true 38 | 39 | - name: Sync project 40 | run: uv sync --frozen --all-extras --dev --python 3.10 41 | 42 | - name: Unit tests execution 43 | run: uv run pytest --cov-report term --cov-report xml:coverage-unit.xml --cov=. 44 | 45 | - name: Integration tests execution 46 | run: uv run pytest -m integration --cov-report term --cov-report xml:coverage-integration.xml --cov=. 47 | env: 48 | TEAMS_APP_ID: ${{ vars.TEAMS_APP_ID }} 49 | TEAMS_APP_PASSWORD: ${{ secrets.TEAMS_APP_PASSWORD }} 50 | TEAMS_APP_TYPE: ${{ vars.TEAMS_APP_TYPE }} 51 | TEAMS_APP_TENANT_ID: ${{ vars.TEAMS_APP_TENANT_ID }} 52 | TEAM_ID: ${{ vars.TEAM_ID }} 53 | TEAMS_CHANNEL_ID: ${{ vars.TEAMS_CHANNEL_ID }} 54 | TEST_THREAD_ID: ${{ vars.TEST_THREAD_ID }} 55 | TEST_MESSAGE_ID: ${{ vars.TEST_MESSAGE_ID }} 56 | TEST_USER_NAME: ${{ vars.TEST_USER_NAME }} 57 | ``` -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.19/MicrosoftTeams.schema.json", 3 | "version": "1.0.0", 4 | "manifestVersion": "1.19", 5 | "id": "{{MICROSOFT_APP_ID}}", 6 | "name": { 7 | "short": "MCP Teams Bot", 8 | "full": "Model Context Protocol Teams Bot" 9 | }, 10 | "developer": { 11 | "name": "Industria Textil de Diseño, S.A.", 12 | "mpnId": "", 13 | "websiteUrl": "https://www.inditex.com", 14 | "privacyUrl": "https://www.inditex.com/itxcomweb/es/es/informacion/politica-de-privacidad", 15 | "termsOfUseUrl": "https://www.inditex.com/itxcomweb/es/es/informacion/legal" 16 | }, 17 | "description": { 18 | "short": "Model Context Protocol Teams Bot", 19 | "full": "Model Context Protocol Teams Bot application, necessary to connect MCP host to Teams" 20 | }, 21 | "icons": { 22 | "outline": "outline.png", 23 | "color": "color.png" 24 | }, 25 | "accentColor": "#FFFFFF", 26 | "staticTabs": [ 27 | { 28 | "entityId": "conversations", 29 | "scopes": [ 30 | "personal" 31 | ] 32 | }, 33 | { 34 | "entityId": "about", 35 | "scopes": [ 36 | "personal" 37 | ] 38 | } 39 | ], 40 | "bots": [ 41 | { 42 | "botId": "{{MICROSOFT_APP_ID}}", 43 | "scopes": [ 44 | "team", 45 | "personal", 46 | "groupChat" 47 | ], 48 | "commandLists": [ 49 | { 50 | "commands": [ 51 | { 52 | "title": "Help", 53 | "description": "Shows help information" 54 | } 55 | ], 56 | "scopes": [ 57 | "team" 58 | ] 59 | } 60 | ], 61 | "isNotificationOnly": false, 62 | "supportsCalling": false, 63 | "supportsVideo": false, 64 | "supportsFiles": false 65 | } 66 | ], 67 | "validDomains": [ 68 | "token.botframework.com" 69 | ], 70 | "authorization": { 71 | "permissions": { 72 | "resourceSpecific": [ 73 | { 74 | "name": "TeamMember.Read.Group", 75 | "type": "Application" 76 | }, 77 | { 78 | "name": "ChannelMessage.Read.Group", 79 | "type": "Application" 80 | }, 81 | { 82 | "name": "ChannelMessage.Send.Group", 83 | "type": "Application" 84 | }, 85 | { 86 | "name": "ChannelSettings.Read.Group", 87 | "type": "Application" 88 | }, 89 | { 90 | "name": "ChannelMember.Read.Group", 91 | "type": "Application" 92 | }, 93 | { 94 | "name": "Member.Read.Group", 95 | "type": "Application" 96 | }, 97 | { 98 | "name": "Owner.Read.Group", 99 | "type": "Application" 100 | } 101 | ] 102 | } 103 | } 104 | } ``` -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Release 2 | permissions: read-all 3 | 4 | on: 5 | pull_request: 6 | types: [ closed ] 7 | branches: [ 'master' ] 8 | workflow_dispatch: 9 | 10 | env: 11 | REGISTRY: ghcr.io 12 | IMAGE_NAME: ${{ github.repository }} 13 | 14 | jobs: 15 | release: 16 | name: Release 17 | runs-on: ubuntu-latest 18 | if: ${{ github.event.pull_request.merged && contains(github.event.pull_request.labels.*.name, 'kind/release') }} 19 | permissions: 20 | contents: write 21 | packages: write 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Install UV 29 | uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5 30 | with: 31 | enable-cache: true 32 | 33 | - name: Sync project 34 | run: uv sync --frozen --all-extras --dev --python 3.10 35 | 36 | - name: Retrieve version 37 | shell: bash 38 | run: | 39 | VERSION=$(uv run pyproject-info project.version | tr -d '"'); echo "VERSION=v$VERSION" >> "$GITHUB_ENV" 40 | 41 | - name: Create Git Tag 42 | run: | 43 | git config user.email "[email protected]" 44 | git config user.name "GitHub Bot" 45 | 46 | git tag -a "${{ env.VERSION }}" -m "Version ${{ env.VERSION }}" 47 | git push origin "${{ env.VERSION }}" 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | 51 | - name: Login into ${{ env.REGISTRY }} 52 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 53 | with: 54 | registry: ${{ env.REGISTRY }} 55 | username: ${{ github.actor }} 56 | password: ${{ secrets.GITHUB_TOKEN }} 57 | 58 | - name: Setup Docker Buildx 59 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 60 | 61 | - name: Extract metadata (tags, labels) for Docker 62 | id: meta 63 | uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 64 | with: 65 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 66 | tags: | 67 | type=semver,pattern={{version}},value=${{ env.VERSION }} 68 | type=pep440,pattern={{version}},value=${{ env.VERSION }} 69 | 70 | - name: Build and push by digest 71 | id: build 72 | uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6 73 | with: 74 | context: . 75 | push: true 76 | tags: ${{ steps.meta.outputs.tags }} 77 | labels: ${{ steps.meta.outputs.labels }} 78 | cache-from: type=gha 79 | cache-to: type=gha,mode=max 80 | 81 | - name: Create GitHub Release 82 | uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1 83 | with: 84 | name: ${{ env.VERSION }} 85 | tag: ${{ env.VERSION }} 86 | token: ${{ secrets.GITHUB_TOKEN }} 87 | generateReleaseNotes: true ``` -------------------------------------------------------------------------------- /.github/workflows/PR-verify.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Pull Request verification 2 | permissions: 3 | contents: read 4 | 5 | on: 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | check-format: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 14 | with: 15 | fetch-depth: 0 16 | ref: ${{ github.event.pull_request.head.sha }} 17 | 18 | - name: Install UV 19 | uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5 20 | with: 21 | enable-cache: true 22 | 23 | - name: Sync project 24 | run: uv sync --frozen --all-extras --dev --python 3.10 25 | 26 | - name: Ruff format check 27 | run: uv run --no-sync ruff check . 28 | 29 | check-typing: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 33 | with: 34 | fetch-depth: 0 35 | ref: ${{ github.event.pull_request.head.sha }} 36 | 37 | - name: Install UV 38 | uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5 39 | with: 40 | enable-cache: true 41 | 42 | - name: Sync project 43 | run: uv sync --frozen --all-extras --dev --python 3.10 44 | 45 | - name: Pyright type checking 46 | run: uv run --no-sync pyright 47 | 48 | run-tests: 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 52 | with: 53 | fetch-depth: 0 54 | ref: ${{ github.event.pull_request.head.sha }} 55 | 56 | - name: Install UV 57 | uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5 58 | with: 59 | enable-cache: true 60 | 61 | - name: Sync project 62 | run: uv sync --frozen --all-extras --dev --python 3.10 63 | 64 | - name: Unit tests execution 65 | run: uv run pytest 66 | 67 | repo-linter: 68 | name: Repo Linter 69 | runs-on: ubuntu-latest 70 | steps: 71 | - name: Checkout 72 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 73 | with: 74 | fetch-depth: 0 75 | ref: ${{ github.event.pull_request.head.sha }} 76 | 77 | - name: Setup Node version 78 | uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4 79 | with: 80 | node-version: ${{ env.NODE_VERSION }} 81 | 82 | - name: Execute Repo Linter 83 | run: | 84 | echo "Installing Repo Linter" 85 | npm install -g [email protected] 86 | 87 | echo "Executing Repo Linter" 88 | repolinter --rulesetFile repolinter.json --dryRun . 89 | 90 | echo "Repo Linter execution completed" 91 | 92 | 93 | reuse-compliance: 94 | name: REUSE Compliance 95 | runs-on: ubuntu-latest 96 | steps: 97 | - name: Checkout 98 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 99 | with: 100 | fetch-depth: 0 101 | ref: ${{ github.event.pull_request.head.sha }} 102 | 103 | - name: REUSE Compliance Check 104 | uses: fsfe/reuse-action@bb774aa972c2a89ff34781233d275075cbddf542 # v5 105 | 106 | ``` -------------------------------------------------------------------------------- /tests/test_teams.py: -------------------------------------------------------------------------------- ```python 1 | # SPDX-FileCopyrightText: 2025 INDUSTRIA DE DISEÑO TEXTIL, S.A. (INDITEX, S.A.) 2 | # SPDX-License-Identifier: Apache-2.0 3 | import logging 4 | import os 5 | import sys 6 | 7 | import pytest 8 | from azure.identity.aio import ClientSecretCredential 9 | from botbuilder.integration.aiohttp import ( 10 | CloudAdapter, 11 | ConfigurationBotFrameworkAuthentication, 12 | ) 13 | from dotenv import load_dotenv 14 | from msgraph.graph_service_client import GraphServiceClient 15 | 16 | from mcp_teams_server.config import BotConfiguration 17 | from mcp_teams_server.teams import TeamsClient 18 | 19 | load_dotenv() 20 | 21 | logging.basicConfig( 22 | level=logging.DEBUG, 23 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 24 | handlers=[logging.StreamHandler(sys.stdout)], 25 | ) 26 | 27 | LOGGER = logging.getLogger(__name__) 28 | 29 | 30 | @pytest.mark.integration 31 | @pytest.fixture() 32 | def setup_teams_client() -> TeamsClient: 33 | # Cloud adapter 34 | config = BotConfiguration() 35 | adapter = CloudAdapter( 36 | ConfigurationBotFrameworkAuthentication(config, logger=LOGGER) 37 | ) 38 | 39 | # Graph client 40 | credentials = ClientSecretCredential( 41 | config.APP_TENANTID, config.APP_ID, config.APP_PASSWORD 42 | ) 43 | scopes = ["https://graph.microsoft.com/.default"] 44 | graph_client = GraphServiceClient(credentials=credentials, scopes=scopes) 45 | 46 | return TeamsClient( 47 | adapter, graph_client, config.APP_ID, config.TEAM_ID, config.TEAMS_CHANNEL_ID 48 | ) 49 | 50 | 51 | @pytest.fixture() 52 | def thread_id() -> str | None: 53 | return os.environ.get("TEST_THREAD_ID") 54 | 55 | 56 | @pytest.fixture() 57 | def message_id() -> str | None: 58 | return os.environ.get("TEST_MESSAGE_ID") 59 | 60 | 61 | @pytest.fixture() 62 | def user_name() -> str | None: 63 | return os.environ.get("TEST_USER_NAME") 64 | 65 | 66 | @pytest.mark.integration 67 | @pytest.mark.asyncio 68 | async def test_start_thread(setup_teams_client, user_name): 69 | LOGGER.info( 70 | f"test_start_thread in team: {setup_teams_client.team_id} " 71 | f"and channel {setup_teams_client.teams_channel_id}" 72 | ) 73 | result = await setup_teams_client.start_thread( 74 | "First thread", "First thread content with mention", user_name 75 | ) 76 | print(f"Result {result}\n") 77 | assert result is not None 78 | 79 | 80 | @pytest.mark.integration 81 | @pytest.mark.asyncio 82 | async def test_read_threads(setup_teams_client): 83 | result = await setup_teams_client.read_threads(50) 84 | print(f"Result {result}\n") 85 | assert result is not None 86 | 87 | 88 | @pytest.mark.integration 89 | @pytest.mark.asyncio 90 | async def test_update_thread(setup_teams_client, thread_id, user_name): 91 | result = await setup_teams_client.update_thread( 92 | thread_id, "Thread updated content with mention", user_name 93 | ) 94 | print(f"Result {result}\n") 95 | assert result is not None 96 | 97 | 98 | @pytest.mark.integration 99 | @pytest.mark.asyncio 100 | async def test_read_thread_replies(setup_teams_client, thread_id): 101 | result = await setup_teams_client.read_thread_replies(thread_id) 102 | print(f"Result {result}\n") 103 | assert result is not None 104 | 105 | 106 | @pytest.mark.integration 107 | @pytest.mark.asyncio 108 | async def test_list_members(setup_teams_client): 109 | result = await setup_teams_client.list_members() 110 | print(f"Result {result}\n") 111 | assert result is not None 112 | ``` -------------------------------------------------------------------------------- /doc/MS-Teams-setup.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Teams configuration 2 | 3 | ### Application registration 4 | 5 | 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) 6 | a Microsoft Entra ID application. Please keep your application UUID to set the environment variable **TEAMS_APP_ID**. 7 | 8 | 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) 9 | 10 | 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) 11 | to your application. After this you will retrieve your client secret and store it in the environment variable 12 | **TEAMS_APP_PASSWORD**. 13 | 14 | Your organization can either use a Single Tenant or Multi Tenant schema for managing identities. 15 | In case you use a SingleTenant scheme, please store the tenant UUID in **TEAMS_APP_TENANT_ID** and 16 | set **TEAMS_APP_TYPE** to SingleTenant. 17 | 18 | During development of this MCP Server we used SingleTenant authentication for our demo application 19 | with client secret credentials. 20 | 21 |  22 | 23 | It is also necessary to setup Microsoft Graph API "ChannelMessage.Read.All" permission. 24 | This permission is a "Resource Specific Consent" and can be scoped to the team or group where the teams 25 | application (explained later) is installed. 26 | 27 |  28 | 29 | ### Azure Bot registration 30 | 31 | 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). 32 | The bot will be set with the **TEAMS_APP_ID** existing registration. 33 | 34 |  35 | 36 | 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) 37 | 38 | 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. 39 | 40 |  41 | 42 | You will not need to deploy a Bot application in Azure or the Internet because this MCP server uses 43 | Azure Bot Framework as a client to Bot Framework api. 44 | 45 | ### Microsoft Teams Application publishing 46 | 47 | There is a skeleton for this kind of applications in the [app folder](https://github.com/InditexTech/mcp-teams-server/tree/master/app). 48 | This skeleton can be used after replacing `{{MICROSOFT_APP_ID}}` by your **TEAMS_APP_ID**, 49 | using your own icons and zipping the directory to deploy it as an application into Microsoft Teams. 50 | 51 | As an alternative you can create and publish the same application with the aid of [Teams developer portal](https://dev.teams.microsoft.com/), 52 | but remember to add and configure "bot feature" and setup the application required permissions 53 | ("TeamMember.Read.Group", "ChannelMessage.Read.Group", "ChannelMessage.Send.Group", "ChannelSettings.Read.Group", 54 | "ChannelMember.Read.Group", "Member.Read.Group", "Owner.Read.Group"). 55 | 56 |  57 | 58 |  59 | 60 |  61 | 62 | This application needs to be installed in a Teams Group 63 | 64 |  65 | 66 | And some information must be extracted from the Teams Group url: 67 | 68 |  69 | 70 | ``` 71 | https://teams.microsoft.com/l/channel/[TEAMS_CHANNEL_ID]/McpBot?groupId=[TEAM_ID]&tenantId=[TEAMS_APP_TENANT_ID] 72 | ``` 73 | 74 | You will need that information to set up **TEAM_ID** and **TEAMS_CHANNEL_ID** for your bot. 75 | 76 | The MCP teams server will be set up to read and post only in the **TEAM_ID** channels and 77 | will use by default the **TEAMS_CHANNEL_ID**. ``` -------------------------------------------------------------------------------- /llms-install.md: -------------------------------------------------------------------------------- ```markdown 1 | ## MCP Teams Server Installation Guide 2 | 3 | This guide is specifically designed for AI agents like Cline to install and configure the MCP Teams Server for use 4 | with LLM applications like Claude Desktop, Cursor, Roo Code, and Cline. 5 | 6 | ### Overview 7 | 8 | MCP Teams Server is a communication tool that allows AI assistants to interact with Microsoft Teams Channels. 9 | 10 | ### Prerequisites 11 | 12 | - [uv](https://github.com/astral-sh/uv) package manager 13 | - [Python 3.10](https://www.python.org/) 14 | - Microsoft Teams account with [proper set-up](./doc/MS-Teams-setup.md) 15 | 16 | ### Installation and configuration 17 | 18 | Add the MCP server configuration to your MCP settings file based on your LLM client. 19 | 20 | Remember there is a [pre-built image](https://github.com/InditexTech/mcp-teams-server/pkgs/container/mcp-teams-server) hosted in ghcr.io. 21 | You can install this image by running the following command 22 | 23 | ```commandline 24 | docker pull ghcr.io/inditextech/mcp-teams-server:latest 25 | ``` 26 | 27 | Sample docker setup: 28 | 29 | ```yaml 30 | { 31 | "mcpServers": { 32 | "msteams": { 33 | "command": "docker", 34 | "args": [ 35 | "run", 36 | "-i", 37 | "--rm", 38 | "-e", 39 | "TEAMS_APP_ID", 40 | "-e", 41 | "TEAMS_APP_PASSWORD", 42 | "-e", 43 | "TEAMS_APP_TYPE", 44 | "-e", 45 | "TEAMS_APP_TENANT_ID", 46 | "-e", 47 | "TEAM_ID", 48 | "-e", 49 | "TEAMS_CHANNEL_ID", 50 | "ghcr.io/inditextech/mcp-teams-server" 51 | ], 52 | "env": { 53 | "TEAMS_APP_ID": "<fill_me_with_proper_uuid>", 54 | "TEAMS_APP_PASSWORD": "<fill_me_with_proper_uuid>", 55 | "TEAMS_APP_TYPE": "<fill_me_with_proper_uuid>", 56 | "TEAMS_APP_TENANT_ID": "<fill_me_with_proper_uuid>", 57 | "TEAM_ID": "<fill_me_with_proper_uuid>", 58 | "TEAMS_CHANNEL_ID": "<fill_me_with_proper_channel_id>", 59 | "DOCKER_HOST": "unix:///var/run/docker.sock" 60 | } 61 | } 62 | } 63 | } 64 | ``` 65 | 66 | Sample Cline setup with docker through WSL (Windows only): 67 | 68 | ```yaml 69 | { 70 | "mcpServers": { 71 | "github.com/InditexTech/mcp-teams-server/tree/main": { 72 | "command": "wsl", 73 | "args": [ 74 | "TEAMS_APP_ID=<fill_me_with_proper_uuid>", 75 | "TEAMS_APP_PASSWORD=<fill_me_with_proper_uuid>", 76 | "TEAMS_APP_TYPE=<fill_me_with_proper_uuid>", 77 | "TEAMS_APP_TENANT_ID=<fill_me_with_proper_uuid>", 78 | "TEAM_ID=<fill_me_with_proper_uuid>", 79 | "TEAMS_CHANNEL_ID=<fill_me_with_proper_uuid>", 80 | "docker", 81 | "run", 82 | "-i", 83 | "--rm", 84 | "ghcr.io/inditextech/mcp-teams-server" 85 | ], 86 | "env": { 87 | "DOCKER_HOST": "unix:///var/run/docker.sock" 88 | }, 89 | "disabled": false, 90 | "autoApprove": [ ], 91 | "timeout": 300 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | Sample local development setup: 98 | 99 | ```yaml 100 | { 101 | "mcpServers": { 102 | "msteams": { 103 | "command": "uv", 104 | "args": [ 105 | "run", 106 | "mcp-teams-server" 107 | ], 108 | "env": { 109 | "TEAMS_APP_ID": "<fill_me_with_proper_uuid>", 110 | "TEAMS_APP_PASSWORD": "<fill_me_with_proper_uuid>", 111 | "TEAMS_APP_TYPE": "<fill_me_with_proper_uuid>", 112 | "TEAMS_APP_TENANT_ID": "<fill_me_with_proper_uuid>", 113 | "TEAM_ID": "<fill_me_with_proper_uuid>", 114 | "TEAMS_CHANNEL_ID": "<fill_me_with_proper_channel_id>" 115 | } 116 | } 117 | } 118 | } 119 | ``` 120 | 121 | ### Verify installation 122 | 123 | Once configured, you'll have access to these tools: 124 | 125 | #### 1. start_thread 126 | 127 | Start a new thread with a given title and content 128 | 129 | **Parameters:** 130 | - `title`: (Required) The thread title 131 | - `content`: (Required) The thread content 132 | - `member_name`: (Optional) Member name to mention in the thread 133 | 134 | #### 2. update_thread 135 | 136 | Update an existing thread with new content 137 | 138 | **Parameters:** 139 | - `thread_id`: (Required) The thread ID as a string in the format '1743086901347' 140 | - `content`: (Required) The content to update in the thread 141 | - `member_name`: (Optional) Member name to mention in the thread 142 | 143 | #### 3. read_thread 144 | 145 | Read replies in a thread 146 | 147 | **Parameters:** 148 | - `thread_id`: (Required) The thread ID as a string in the format '1743086901347' 149 | 150 | #### 4. list_threads 151 | 152 | List threads in channel with pagination 153 | 154 | **Parameters:** 155 | - `limit`: (Optional, default 50) Maximum number of items to retrieve or page size 156 | - `cursor`: (Optional) Pagination cursor for the next page of results, returned by previous list_thread tool call. 157 | 158 | #### 5. get_member_by_name 159 | 160 | Get a member by its name 161 | 162 | **Parameters:** 163 | - `name`: (Required) Member name 164 | 165 | #### 6. list_members 166 | 167 | List all members in the team 168 | 169 | ## Usage Examples 170 | 171 | Some ideas for user prompts are: 172 | 173 | ``` 174 | Please start a thread in teams with the following content... 175 | ``` 176 | 177 | ``` 178 | Please list members in team 179 | ``` 180 | 181 | ``` 182 | Please perform this task... and send results to a new thread in teams. Remember to mention "User Name" 183 | ``` 184 | 185 | ``` 186 | Please read latest team threads and reply to threads that mention "Your bot name" 187 | ``` 188 | 189 | 190 | 191 | ``` -------------------------------------------------------------------------------- /src/mcp_teams_server/__init__.py: -------------------------------------------------------------------------------- ```python 1 | # SPDX-FileCopyrightText: 2025 INDUSTRIA DE DISEÑO TEXTIL, S.A. (INDITEX, S.A.) 2 | # SPDX-License-Identifier: Apache-2.0 3 | import logging 4 | import os 5 | import sys 6 | from collections.abc import AsyncIterator 7 | from contextlib import asynccontextmanager 8 | from dataclasses import dataclass 9 | from importlib import metadata 10 | 11 | from azure.identity.aio import ClientSecretCredential 12 | from botbuilder.integration.aiohttp import ( 13 | CloudAdapter, 14 | ConfigurationBotFrameworkAuthentication, 15 | ) 16 | from dotenv import load_dotenv 17 | from mcp.server.fastmcp import Context, FastMCP 18 | from msgraph.graph_service_client import GraphServiceClient 19 | from pydantic import Field 20 | 21 | from .config import BotConfiguration 22 | from .teams import ( 23 | PagedTeamsMessages, 24 | TeamsClient, 25 | TeamsMember, 26 | TeamsMessage, 27 | TeamsThread, 28 | ) 29 | 30 | try: 31 | __version__ = metadata.version("mcp-teams-server") 32 | except metadata.PackageNotFoundError: 33 | __version__ = "unknown" 34 | 35 | # Load .env 36 | load_dotenv() 37 | 38 | # Config logging 39 | logging.basicConfig( 40 | level=os.environ.get("MCP_LOGLEVEL", "ERROR"), 41 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 42 | handlers=[ 43 | logging.StreamHandler(sys.stderr), 44 | ], 45 | ) 46 | 47 | LOGGER = logging.getLogger(__name__) 48 | 49 | REQUIRED_ENV_VARS = [ 50 | "TEAMS_APP_ID", 51 | "TEAMS_APP_PASSWORD", 52 | "TEAMS_APP_TYPE", 53 | "TEAMS_APP_TENANT_ID", 54 | "TEAM_ID", 55 | "TEAMS_CHANNEL_ID", 56 | ] 57 | 58 | 59 | @dataclass 60 | class AppContext: 61 | client: TeamsClient 62 | 63 | 64 | @asynccontextmanager 65 | async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: 66 | """Manage application lifecycle with type-safe context""" 67 | 68 | # Bot adapter construction 69 | bot_config = BotConfiguration() 70 | adapter = CloudAdapter(ConfigurationBotFrameworkAuthentication(bot_config)) 71 | 72 | # Graph client construction 73 | credentials = ClientSecretCredential( 74 | bot_config.APP_TENANTID, bot_config.APP_ID, bot_config.APP_PASSWORD 75 | ) 76 | scopes = ["https://graph.microsoft.com/.default"] 77 | graph_client = GraphServiceClient(credentials=credentials, scopes=scopes) 78 | 79 | client = TeamsClient( 80 | adapter, 81 | graph_client, 82 | bot_config.APP_ID, 83 | bot_config.TEAM_ID, 84 | bot_config.TEAMS_CHANNEL_ID, 85 | ) 86 | yield AppContext(client=client) 87 | 88 | 89 | mcp = FastMCP( 90 | "mcp-teams-server", 91 | lifespan=app_lifespan, 92 | dependencies=[ 93 | "aiohttp", 94 | "asyncio", 95 | "botbuilder-core", 96 | "botbuilder-integration-aiohttp", 97 | "dotenv", 98 | "msgraph-sdk", 99 | "multidict", 100 | ], 101 | ) 102 | 103 | 104 | def _get_teams_client(ctx: Context) -> TeamsClient: 105 | return ctx.request_context.lifespan_context.client 106 | 107 | 108 | @mcp.tool( 109 | name="start_thread", description="Start a new thread with a given title and content" 110 | ) 111 | async def start_thread( 112 | ctx: Context, 113 | title: str = Field(description="The thread title"), 114 | content: str = Field(description="The thread content"), 115 | member_name: str | None = Field( 116 | description="Member name to mention in the thread", default=None 117 | ), 118 | ) -> TeamsThread: 119 | await ctx.debug(f"start_thread with title={title} and content={content}") 120 | client = _get_teams_client(ctx) 121 | return await client.start_thread(title, content, member_name) 122 | 123 | 124 | @mcp.tool( 125 | name="update_thread", description="Update an existing thread with new content" 126 | ) 127 | async def update_thread( 128 | ctx: Context, 129 | thread_id: str = Field( 130 | description="The thread ID as a string in the format '1743086901347'" 131 | ), 132 | content: str = Field(description="The content to update in the thread"), 133 | member_name: str | None = Field( 134 | description="Member name to mention in the thread", default=None 135 | ), 136 | ) -> TeamsMessage: 137 | await ctx.debug(f"update_thread with thread_id={thread_id} and content={content}") 138 | client = _get_teams_client(ctx) 139 | return await client.update_thread(thread_id, content, member_name) 140 | 141 | 142 | @mcp.tool(name="read_thread", description="Read replies in a thread") 143 | async def read_thread( 144 | ctx: Context, 145 | thread_id: str = Field( 146 | description="The thread ID as a string in the format '1743086901347'" 147 | ), 148 | ) -> PagedTeamsMessages: 149 | await ctx.debug(f"read_thread with thread_id={thread_id}") 150 | client = _get_teams_client(ctx) 151 | return await client.read_thread_replies(thread_id, 50) 152 | 153 | 154 | @mcp.tool(name="list_threads", description="List threads in channel with pagination") 155 | async def list_threads( 156 | ctx: Context, 157 | limit: int = Field( 158 | description="Maximum number of items to retrieve or page size", default=50 159 | ), 160 | cursor: str | None = Field( 161 | description="Pagination cursor for the next page of results", default=None 162 | ), 163 | ) -> PagedTeamsMessages: 164 | await ctx.debug(f"list_threads with cursor={cursor} and limit={limit}") 165 | client = _get_teams_client(ctx) 166 | return await client.read_threads(limit, cursor) 167 | 168 | 169 | @mcp.tool(name="get_member_by_name", description="Get a member by its name") 170 | async def get_member_by_name( 171 | ctx: Context, name: str = Field(description="Member name") 172 | ): 173 | await ctx.debug(f"get_member_by_name with name={name}") 174 | client = _get_teams_client(ctx) 175 | return await client.get_member_by_name(name) 176 | 177 | 178 | @mcp.tool(name="list_members", description="List all members in the team") 179 | async def list_members(ctx: Context) -> list[TeamsMember]: 180 | await ctx.debug("list_members") 181 | client = _get_teams_client(ctx) 182 | return await client.list_members() 183 | 184 | 185 | def _check_required_environment(): 186 | exit_code = None 187 | for var in REQUIRED_ENV_VARS: 188 | value = os.environ.get(var) 189 | if value is None: 190 | LOGGER.info(f"Required ENV {var} not present") 191 | exit_code = 1 192 | if exit_code is not None: 193 | sys.exit(exit_code) 194 | 195 | 196 | def main() -> None: 197 | import argparse 198 | 199 | parser = argparse.ArgumentParser( 200 | description="MCP Teams Server to allow Microsoft Teams interaction" 201 | ) 202 | 203 | default_transport = os.environ.get("MCP_TRANSPORT", "stdio") 204 | 205 | parser.add_argument( 206 | "-t", 207 | "--transport", 208 | nargs=1, 209 | type=str, 210 | help="MCP Server Transport: stdio or sse", 211 | default=default_transport, 212 | choices=["stdio", "sse"], 213 | ) 214 | 215 | args = parser.parse_args() 216 | 217 | LOGGER.info( 218 | f'Starting MCP Teams Server "{__version__}" with transport "{args.transport}"' 219 | ) 220 | _check_required_environment() 221 | mcp.run(transport=args.transport) 222 | 223 | 224 | if __name__ == "__main__": 225 | main() 226 | ``` -------------------------------------------------------------------------------- /repolinter.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "$schema": "https://raw.githubusercontent.com/todogroup/repolinter/master/rulesets/schema.json", 3 | "version": 2, 4 | "axioms": { 5 | "linguist": "language", 6 | "licensee": "license", 7 | "packagers": "packager" 8 | }, 9 | "rules": { 10 | "license-file-exists": { 11 | "level": "error", 12 | "rule": { 13 | "type": "file-existence", 14 | "options": { 15 | "globsAny": ["LICENSE*", "COPYING*"], 16 | "nocase": true 17 | } 18 | } 19 | }, 20 | "readme-file-exists": { 21 | "level": "error", 22 | "rule": { 23 | "type": "file-existence", 24 | "options": { 25 | "globsAny": ["README*"], 26 | "nocase": true 27 | } 28 | } 29 | }, 30 | "contributing-file-exists": { 31 | "level": "error", 32 | "rule": { 33 | "type": "file-existence", 34 | "options": { 35 | "globsAny": ["{docs/,.github/,}CONTRIB*"], 36 | "nocase": true 37 | } 38 | } 39 | }, 40 | "code-of-conduct-file-exists": { 41 | "level": "error", 42 | "rule": { 43 | "type": "file-existence", 44 | "options": { 45 | "globsAny": [ 46 | "{docs/,.github/,}CODEOFCONDUCT*", 47 | "{docs/,.github/,}CODE-OF-CONDUCT*", 48 | "{docs/,.github/,}CODE_OF_CONDUCT*" 49 | ], 50 | "nocase": true 51 | } 52 | } 53 | }, 54 | "changelog-file-exists": { 55 | "level": "error", 56 | "rule": { 57 | "type": "file-existence", 58 | "options": { 59 | "globsAny": ["CHANGELOG*"], 60 | "nocase": true 61 | } 62 | } 63 | }, 64 | "security-file-exists": { 65 | "level": "error", 66 | "rule": { 67 | "type": "file-existence", 68 | "options": { 69 | "globsAny": ["{docs/,.github/,}SECURITY.md"] 70 | } 71 | } 72 | }, 73 | "support-file-exists": { 74 | "level": "off", 75 | "rule": { 76 | "type": "file-existence", 77 | "options": { 78 | "globsAny": ["{docs/,.github/,}SUPPORT*"], 79 | "nocase": true 80 | } 81 | } 82 | }, 83 | "readme-references-license": { 84 | "level": "off", 85 | "rule": { 86 | "type": "file-contents", 87 | "options": { 88 | "globsAll": ["README*"], 89 | "content": "license", 90 | "flags": "i" 91 | } 92 | } 93 | }, 94 | "binaries-not-present": { 95 | "level": "error", 96 | "rule": { 97 | "type": "file-type-exclusion", 98 | "options": { 99 | "type": ["/*.exe", "/.dll", "!node_modules/"] 100 | } 101 | } 102 | }, 103 | "test-directory-exists": { 104 | "level": "off", 105 | "rule": { 106 | "type": "directory-existence", 107 | "options": { 108 | "globsAny": ["/test", "/specs"], 109 | "nocase": true 110 | } 111 | } 112 | }, 113 | "integrates-with-ci": { 114 | "level": "error", 115 | "rule": { 116 | "type": "file-existence", 117 | "options": { 118 | "globsAny": [ 119 | ".gitlab-ci.yml", 120 | ".travis.yml", 121 | "appveyor.yml", 122 | ".appveyor.yml", 123 | "circle.yml", 124 | ".circleci/config.yml", 125 | "Jenkinsfile", 126 | ".drone.yml", 127 | ".github/workflows/", 128 | "azure-pipelines.yml" 129 | ] 130 | } 131 | } 132 | }, 133 | "code-of-conduct-file-contains-email": { 134 | "level": "off", 135 | "rule": { 136 | "type": "file-contents", 137 | "options": { 138 | "globsAll": [ 139 | "CODEOFCONDUCT", 140 | "CODE-OF-CONDUCT*", 141 | "CODE_OF_CONDUCT*", 142 | ".github/CODEOFCONDUCT*", 143 | ".github/CODE-OF-CONDUCT*", 144 | ".github/CODE_OF_CONDUCT*" 145 | ], 146 | "content": ".+@.+..+", 147 | "flags": "i", 148 | "human-readable-content": "email address" 149 | } 150 | } 151 | }, 152 | "source-license-headers-exist": { 153 | "level": "warning", 154 | "rule": { 155 | "type": "file-starts-with", 156 | "options": { 157 | "globsAll": ["./**/*.py"], 158 | "lineCount": 5, 159 | "patterns": ["Copyright", "License"], 160 | "flags": "i" 161 | } 162 | } 163 | }, 164 | "github-issue-template-exists": { 165 | "level": "error", 166 | "rule": { 167 | "type": "file-existence", 168 | "options": { 169 | "dirs": true, 170 | "globsAny": ["ISSUE_TEMPLATE", ".github/ISSUE_TEMPLATE*"] 171 | } 172 | } 173 | }, 174 | "github-pull-request-template-exists": { 175 | "level": "off", 176 | "rule": { 177 | "type": "file-existence", 178 | "options": { 179 | "dirs": true, 180 | "globsAny": [ 181 | "PULL_REQUEST_TEMPLATE*", 182 | ".github/PULL_REQUEST_TEMPLATE*" 183 | ] 184 | } 185 | } 186 | }, 187 | "javascript-package-metadata-exists": { 188 | "level": "error", 189 | "where": ["language=javascript"], 190 | "rule": { 191 | "type": "file-existence", 192 | "options": { 193 | "globsAny": ["package.json"] 194 | } 195 | } 196 | }, 197 | "ruby-package-metadata-exists": { 198 | "level": "error", 199 | "where": ["language=ruby"], 200 | "rule": { 201 | "type": "file-existence", 202 | "options": { 203 | "globsAny": ["Gemfile"] 204 | } 205 | } 206 | }, 207 | "java-package-metadata-exists": { 208 | "level": "error", 209 | "where": ["language=java"], 210 | "rule": { 211 | "type": "file-existence", 212 | "options": { 213 | "globsAny": ["pom.xml", "build.xml", "build.gradle"] 214 | } 215 | } 216 | }, 217 | "python-package-metadata-exists": { 218 | "level": "error", 219 | "where": ["language=python"], 220 | "rule": { 221 | "type": "file-existence", 222 | "options": { 223 | "globsAny": ["setup.py", "requirements.txt", "pyproject.toml"] 224 | } 225 | } 226 | }, 227 | "objective-c-package-metadata-exists": { 228 | "level": "error", 229 | "where": ["language=objective-c"], 230 | "rule": { 231 | "type": "file-existence", 232 | "options": { 233 | "globsAny": ["Cartfile", "Podfile", ".podspec"] 234 | } 235 | } 236 | }, 237 | "swift-package-metadata-exists": { 238 | "level": "error", 239 | "where": ["language=swift"], 240 | "rule": { 241 | "type": "file-existence", 242 | "options": { 243 | "globsAny": ["Package.swift"] 244 | } 245 | } 246 | }, 247 | "erlang-package-metadata-exists": { 248 | "level": "error", 249 | "where": ["language=erlang"], 250 | "rule": { 251 | "type": "file-existence", 252 | "options": { 253 | "globsAny": ["rebar.config"] 254 | } 255 | } 256 | }, 257 | "elixir-package-metadata-exists": { 258 | "level": "error", 259 | "where": ["language=elixir"], 260 | "rule": { 261 | "type": "file-existence", 262 | "options": { 263 | "globsAny": ["mix.exs"] 264 | } 265 | } 266 | }, 267 | "license-detectable-by-licensee": { 268 | "level": "off", 269 | "where": ["license="], 270 | "rule": { 271 | "type": "license-detectable-by-licensee", 272 | "options": {} 273 | } 274 | }, 275 | "notice-file-exists": { 276 | "level": "error", 277 | "where": ["license=Apache-2.0"], 278 | "rule": { 279 | "type": "file-existence", 280 | "options": { 281 | "globsAny": ["NOTICE*"], 282 | "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." 283 | } 284 | } 285 | }, 286 | "best-practices-badge-present": { 287 | "level": "off", 288 | "rule": { 289 | "type": "best-practices-badge-present" 290 | } 291 | }, 292 | "internal-file-not-exists": { 293 | "level": "off", 294 | "rule": { 295 | "type": "file-not-exists", 296 | "options": { 297 | "globsAll": [ 298 | ".secrets.baseline", 299 | "sherpa-config.yml", 300 | ".snyk", 301 | "sonar-project.properties", 302 | ".drafterconfig.yml", 303 | "application-configmap.yml", 304 | "application-secret.yml" 305 | ], 306 | "nocase": true 307 | } 308 | } 309 | } 310 | } 311 | } ``` -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- ``` 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 10 | 11 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 12 | 13 | "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. 14 | 15 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 16 | 17 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 18 | 19 | "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. 20 | 21 | "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). 22 | 23 | "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. 24 | 25 | "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." 26 | 27 | "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. 28 | 29 | 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. 30 | 31 | 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. 32 | 33 | 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: 34 | 35 | (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and 36 | 37 | (b) You must cause any modified files to carry prominent notices stating that You changed the files; and 38 | 39 | (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 40 | 41 | (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. 42 | 43 | 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. 44 | 45 | 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. 46 | 47 | 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. 48 | 49 | 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. 50 | 51 | 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. 52 | 53 | 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. 54 | 55 | END OF TERMS AND CONDITIONS 56 | ``` -------------------------------------------------------------------------------- /LICENSES/Apache-2.0.txt: -------------------------------------------------------------------------------- ``` 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 10 | 11 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 12 | 13 | "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. 14 | 15 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 16 | 17 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 18 | 19 | "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. 20 | 21 | "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). 22 | 23 | "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. 24 | 25 | "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." 26 | 27 | "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. 28 | 29 | 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. 30 | 31 | 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. 32 | 33 | 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: 34 | 35 | (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and 36 | 37 | (b) You must cause any modified files to carry prominent notices stating that You changed the files; and 38 | 39 | (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 40 | 41 | (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. 42 | 43 | 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. 44 | 45 | 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. 46 | 47 | 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. 48 | 49 | 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. 50 | 51 | 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. 52 | 53 | 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. 54 | 55 | END OF TERMS AND CONDITIONS 56 | 57 | APPENDIX: How to apply the Apache License to your work. 58 | 59 | 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. 60 | 61 | Copyright [yyyy] [name of copyright owner] 62 | 63 | Licensed under the Apache License, Version 2.0 (the "License"); 64 | you may not use this file except in compliance with the License. 65 | You may obtain a copy of the License at 66 | 67 | http://www.apache.org/licenses/LICENSE-2.0 68 | 69 | Unless required by applicable law or agreed to in writing, software 70 | distributed under the License is distributed on an "AS IS" BASIS, 71 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 72 | See the License for the specific language governing permissions and 73 | limitations under the License. 74 | ``` -------------------------------------------------------------------------------- /src/mcp_teams_server/teams.py: -------------------------------------------------------------------------------- ```python 1 | # SPDX-FileCopyrightText: 2025 INDUSTRIA DE DISEÑO TEXTIL, S.A. (INDITEX, S.A.) 2 | # SPDX-License-Identifier: Apache-2.0 3 | import logging 4 | 5 | from botbuilder.core import BotAdapter, TurnContext 6 | from botbuilder.core.teams import TeamsInfo 7 | from botbuilder.integration.aiohttp import CloudAdapter 8 | from botbuilder.schema import ( 9 | Activity, 10 | ActivityTypes, 11 | ChannelAccount, 12 | ConversationAccount, 13 | ConversationReference, 14 | Mention, 15 | TextFormatTypes, 16 | ) 17 | from botbuilder.schema.teams import TeamsChannelAccount 18 | from botframework.connector.aio.operations_async import ConversationsOperations 19 | from kiota_abstractions.base_request_configuration import RequestConfiguration 20 | from msgraph.generated.models.chat_message import ChatMessage 21 | from msgraph.generated.teams.item.channels.item.messages.item.chat_message_item_request_builder import ( 22 | ChatMessageItemRequestBuilder, 23 | ) 24 | from msgraph.generated.teams.item.channels.item.messages.item.replies.replies_request_builder import ( 25 | RepliesRequestBuilder, 26 | ) 27 | from msgraph.generated.teams.item.channels.item.messages.messages_request_builder import ( 28 | MessagesRequestBuilder, 29 | ) 30 | from msgraph.graph_service_client import GraphServiceClient 31 | from pydantic import BaseModel, Field 32 | 33 | LOGGER = logging.getLogger(__name__) 34 | 35 | 36 | class TeamsThread(BaseModel): 37 | thread_id: str = Field( 38 | description="Thread ID as a string in the format '1743086901347'" 39 | ) 40 | title: str = Field(description="Message title") 41 | content: str = Field(description="Message content") 42 | 43 | 44 | class TeamsMessage(BaseModel): 45 | thread_id: str = Field( 46 | description="Thread ID as a string in the format '1743086901347'" 47 | ) 48 | message_id: str = Field(description="Message ID") 49 | content: str = Field(description="Message content") 50 | 51 | 52 | class TeamsMember(BaseModel): 53 | name: str = Field( 54 | description="Member name used in mentions and user information cards" 55 | ) 56 | email: str = Field(description="Member email") 57 | 58 | 59 | class PagedTeamsMessages(BaseModel): 60 | cursor: str | None = Field( 61 | description="Cursor to retrieve the next page of messages." 62 | ) 63 | limit: int = Field(description="Page limit, maximum number of items to retrieve") 64 | total: int = Field(description="Total items available for retrieval") 65 | items: list[TeamsMessage] = Field(description="List of channel messages or threads") 66 | 67 | 68 | class TeamsClient: 69 | def __init__( 70 | self, 71 | adapter: CloudAdapter, 72 | graph_client: GraphServiceClient, 73 | teams_app_id: str, 74 | team_id: str, 75 | teams_channel_id: str, 76 | ): 77 | self.adapter = adapter 78 | self.graph_client = graph_client 79 | self.teams_app_id = teams_app_id 80 | self.team_id = team_id 81 | self.teams_channel_id = teams_channel_id 82 | self.service_url = None 83 | self.adapter.on_turn_error = self.on_turn_error 84 | 85 | def get_team_id(self): 86 | return self.team_id 87 | 88 | @staticmethod 89 | async def on_turn_error(context: TurnContext, error: Exception): 90 | LOGGER.error(f"Error {str(error)}") 91 | # await context.send_activity("An error occurred in the bot, please try again later") 92 | 93 | def _create_conversation_reference(self) -> ConversationReference: 94 | service_url = "https://smba.trafficmanager.net/emea/" 95 | if self.service_url is not None: 96 | service_url = self.service_url 97 | return ConversationReference( 98 | bot=TeamsChannelAccount(id=self.teams_app_id, name="MCP Bot"), 99 | channel_id=self.teams_channel_id, 100 | service_url=service_url, 101 | conversation=ConversationAccount( 102 | id=self.teams_channel_id, 103 | is_group=True, 104 | conversation_type="channel", 105 | name="Teams channel", 106 | ), 107 | ) 108 | 109 | async def _initialize(self): 110 | if not self.service_url: 111 | 112 | def context_callback(context: TurnContext): 113 | self.service_url = context.activity.service_url 114 | 115 | await self.adapter.continue_conversation( 116 | bot_app_id=self.teams_app_id, 117 | reference=self._create_conversation_reference(), 118 | callback=context_callback, 119 | ) 120 | 121 | async def start_thread( 122 | self, title: str, content: str, member_name: str | None = None 123 | ) -> TeamsThread: 124 | """Start a new thread in a channel. 125 | 126 | Args: 127 | title: Thread title 128 | content: Initial thread content 129 | member_name: Member name to mention in content 130 | 131 | Returns: 132 | Created thread details including ID 133 | """ 134 | try: 135 | await self._initialize() 136 | 137 | result = TeamsThread(title=title, content=content, thread_id="") 138 | 139 | async def start_thread_callback(context: TurnContext): 140 | mention_member = None 141 | if member_name is not None: 142 | members = await TeamsInfo.get_team_members(context, self.team_id) 143 | for member in members: 144 | if member.name == member_name: 145 | mention_member = member 146 | 147 | mentions = [] 148 | if mention_member is not None: 149 | result.content = ( 150 | f"# **{title}**\n<at>{mention_member.name}</at> {content}" 151 | ) 152 | mention = Mention( 153 | text=f"<at>{mention_member.name}</at>", 154 | type="mention", 155 | mentioned=ChannelAccount( 156 | id=mention_member.id, name=mention_member.name 157 | ), 158 | ) 159 | mentions.append(mention) 160 | 161 | response = await context.send_activity( 162 | activity_or_text=Activity( 163 | type=ActivityTypes.message, 164 | topic_name=title, 165 | text=result.content, 166 | text_format=TextFormatTypes.markdown, 167 | entities=mentions, 168 | ) 169 | ) 170 | if response is not None: 171 | result.thread_id = response.id 172 | 173 | await self.adapter.continue_conversation( 174 | bot_app_id=self.teams_app_id, 175 | reference=self._create_conversation_reference(), 176 | callback=start_thread_callback, 177 | ) 178 | 179 | return result 180 | except Exception as e: 181 | LOGGER.error(f"Error creating thread: {str(e)}") 182 | raise 183 | 184 | @staticmethod 185 | def _get_conversation_operations(context: TurnContext) -> ConversationsOperations: 186 | # Hack to get the connector client and reply to an existing activity 187 | connector_client = context.turn_state[BotAdapter.BOT_CONNECTOR_CLIENT_KEY] 188 | return connector_client.conversations # pyright: ignore 189 | 190 | async def update_thread( 191 | self, thread_id: str, content: str, member_name: str | None = None 192 | ) -> TeamsMessage: 193 | """Add a message to an existing thread, mentioning a user optionally. 194 | 195 | Args: 196 | thread_id: Thread ID to update 197 | content: Message content to add 198 | member_name: Member name to mention (optional) 199 | 200 | Returns: 201 | Updated thread details 202 | """ 203 | try: 204 | await self._initialize() 205 | 206 | result = TeamsMessage(thread_id=thread_id, content=content, message_id="") 207 | 208 | async def update_thread_callback(context: TurnContext): 209 | mention_member = None 210 | if member_name is not None: 211 | members = await TeamsInfo.get_team_members(context, self.team_id) 212 | for member in members: 213 | if member.name == member_name: 214 | mention_member = member 215 | 216 | mentions = [] 217 | if mention_member is not None: 218 | result.content = f"<at>{mention_member.name}</at> {content}" 219 | mention = Mention( 220 | text=f"<at>{mention_member.name}</at>", 221 | type="mention", 222 | mentioned=ChannelAccount( 223 | id=mention_member.id, name=mention_member.name 224 | ), 225 | ) 226 | mentions.append(mention) 227 | 228 | reply = Activity( 229 | type=ActivityTypes.message, 230 | text=result.content, 231 | from_property=TeamsChannelAccount( 232 | id=self.teams_app_id, name="MCP Bot" 233 | ), 234 | conversation=ConversationAccount(id=thread_id), 235 | entities=mentions, 236 | ) 237 | # 238 | # Hack to get the connector client and reply to an existing activity 239 | # 240 | conversations = TeamsClient._get_conversation_operations(context) 241 | # 242 | # Hack to reply to conversation https://github.com/microsoft/botframework-sdk/issues/6626 243 | # 244 | conversation_id = ( 245 | f"{context.activity.conversation.id};messageid={thread_id}" # pyright: ignore 246 | ) 247 | response = await conversations.send_to_conversation( 248 | conversation_id=conversation_id, activity=reply 249 | ) 250 | 251 | if response is not None: 252 | result.message_id = response.id # pyright: ignore 253 | 254 | await self.adapter.continue_conversation( 255 | bot_app_id=self.teams_app_id, 256 | reference=self._create_conversation_reference(), 257 | callback=update_thread_callback, 258 | ) 259 | 260 | return result 261 | except Exception as e: 262 | LOGGER.error(f"Error updating thread: {str(e)}") 263 | raise 264 | 265 | async def get_member_by_id(self, member_id: str) -> TeamsMember: 266 | try: 267 | await self._initialize() 268 | 269 | result = TeamsMember(name="", email="") 270 | 271 | async def get_member_by_id_callback(context: TurnContext): 272 | member = await TeamsInfo.get_team_member( 273 | context, self.team_id, member_id 274 | ) 275 | result.name = member.name 276 | result.email = member.email 277 | 278 | await self.adapter.continue_conversation( 279 | bot_app_id=self.teams_app_id, 280 | reference=self._create_conversation_reference(), 281 | callback=get_member_by_id_callback, 282 | ) 283 | return result 284 | except Exception as e: 285 | LOGGER.error(f"Error updating thread: {str(e)}") 286 | raise 287 | 288 | async def read_threads( 289 | self, limit: int = 50, cursor: str | None = None 290 | ) -> PagedTeamsMessages: 291 | """Read all threads in configured teams channel. 292 | 293 | Args: 294 | cursor: The pagination cursor. 295 | 296 | limit: The pagination page size 297 | 298 | Returns: 299 | Paged team channel messages containing 300 | """ 301 | try: 302 | query = MessagesRequestBuilder.MessagesRequestBuilderGetQueryParameters( 303 | top=limit 304 | ) 305 | request = RequestConfiguration(query_parameters=query) 306 | if cursor is not None: 307 | response = ( 308 | await self.graph_client.teams.by_team_id(self.team_id) 309 | .channels.by_channel_id(self.teams_channel_id) 310 | .messages.with_url(cursor) 311 | .get(request_configuration=request) 312 | ) 313 | else: 314 | response = ( 315 | await self.graph_client.teams.by_team_id(self.team_id) 316 | .channels.by_channel_id(self.teams_channel_id) 317 | .messages.get(request_configuration=request) 318 | ) 319 | 320 | result = PagedTeamsMessages( 321 | cursor=response.odata_next_link, # pyright: ignore 322 | limit=limit, 323 | total=response.odata_count, # pyright: ignore 324 | items=[], 325 | ) 326 | if response.value is not None: # pyright: ignore 327 | for message in response.value: # pyright: ignore 328 | result.items.append( 329 | TeamsMessage( 330 | message_id=message.id, # pyright: ignore 331 | content=message.body.content, # pyright: ignore 332 | thread_id=message.id, # pyright: ignore 333 | ) 334 | ) 335 | 336 | return result 337 | except Exception as e: 338 | LOGGER.error(f"Error reading thread: {str(e)}") 339 | raise 340 | 341 | async def read_thread_replies( 342 | self, thread_id: str, limit: int = 50, cursor: str | None = None 343 | ) -> PagedTeamsMessages: 344 | """Read all replies in a thread. 345 | 346 | Args: 347 | thread_id: Thread ID to read 348 | cursor: The pagination cursor 349 | limit: The pagination page size 350 | 351 | Returns: 352 | List of thread messages 353 | """ 354 | try: 355 | params = RepliesRequestBuilder.RepliesRequestBuilderGetQueryParameters( 356 | top=limit 357 | ) 358 | request = RequestConfiguration(query_parameters=params) 359 | 360 | if cursor is not None: 361 | replies = ( 362 | await self.graph_client.teams.by_team_id(self.team_id) 363 | .channels.by_channel_id(self.teams_channel_id) 364 | .messages.by_chat_message_id(thread_id) 365 | .replies.with_url(cursor) 366 | .get(request_configuration=request) 367 | ) 368 | else: 369 | replies = ( 370 | await self.graph_client.teams.by_team_id(self.team_id) 371 | .channels.by_channel_id(self.teams_channel_id) 372 | .messages.by_chat_message_id(thread_id) 373 | .replies.get(request_configuration=request) 374 | ) 375 | 376 | result = PagedTeamsMessages( 377 | cursor=cursor, 378 | limit=limit, 379 | total=replies.odata_count, # pyright: ignore 380 | items=[], 381 | ) 382 | 383 | if replies is not None and replies.value is not None: 384 | for reply in replies.value: 385 | result.items.append( 386 | TeamsMessage( 387 | message_id=reply.id, # pyright: ignore 388 | content=reply.body.content, # pyright: ignore 389 | thread_id=reply.reply_to_id, # pyright: ignore 390 | ) 391 | ) 392 | 393 | return result 394 | except Exception as e: 395 | LOGGER.error(f"Error reading thread: {str(e)}") 396 | raise 397 | 398 | async def read_message(self, message_id: str) -> ChatMessage | None: 399 | try: 400 | query = ChatMessageItemRequestBuilder.ChatMessageItemRequestBuilderGetQueryParameters() 401 | request = RequestConfiguration(query_parameters=query) 402 | response = ( 403 | await self.graph_client.teams.by_team_id(self.team_id) 404 | .channels.by_channel_id(self.teams_channel_id) 405 | .messages.by_chat_message_id(chat_message_id=message_id) 406 | .get(request_configuration=request) 407 | ) 408 | return response 409 | except Exception as e: 410 | LOGGER.error(f"Error reading thread: {str(e)}") 411 | raise 412 | 413 | async def list_members(self) -> list[TeamsMember]: 414 | """List all members in the configured team. 415 | 416 | Returns: 417 | List of team member details 418 | """ 419 | try: 420 | await self._initialize() 421 | result = [] 422 | 423 | async def list_members_callback(context: TurnContext): 424 | members = await TeamsInfo.get_team_members(context, self.team_id) 425 | for member in members: 426 | result.append(TeamsMember(name=member.name, email=member.email)) 427 | 428 | await self.adapter.continue_conversation( 429 | bot_app_id=self.teams_app_id, 430 | reference=self._create_conversation_reference(), 431 | callback=list_members_callback, 432 | ) 433 | return result 434 | except Exception as e: 435 | LOGGER.error(f"Error listing members: {str(e)}") 436 | raise 437 | 438 | async def get_member_by_name(self, name: str) -> TeamsMember | None: 439 | members = await self.list_members() 440 | for member in members: 441 | if member.name == name: 442 | return member 443 | ```