#
tokens: 29405/50000 35/35 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=InditexTech_mcp-teams-server&metric=bugs)](https://sonarcloud.io/summary/new_code?id=InditexTech_mcp-teams-server)
  2 | [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=InditexTech_mcp-teams-server&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=InditexTech_mcp-teams-server)
  3 | [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=InditexTech_mcp-teams-server&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=InditexTech_mcp-teams-server)
  4 | ![GitHub License](https://img.shields.io/github/license/InditexTech/mcp-teams-server)
  5 | ![GitHub Release](https://img.shields.io/github/v/release/InditexTech/mcp-teams-server)
  6 | [![Scorecard](https://api.scorecard.dev/projects/github.com/InditexTech/mcp-teams-server/badge)](https://scorecard.dev/viewer/?uri=github.com/InditexTech/mcp-teams-server)
  7 | <!-- [![Best Practices](https://www.bestpractices.dev/projects/10400/badge)](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 | ![Client Secret Credentials](./images/azure_app_client_credentials.png)
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 | ![MS Graph API Permissions](./images/azure_msgraph_api_permissions.png)
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 | ![Azure Bot Configuration](./images/azure_bot_configuration.png)
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 | ![Azure Bot Channels](./images/azure_bot_channels.png)
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 | ![MS Teams App Basic Information](./images/msteams_bot_app_basic_information.png)
57 | 
58 | ![MS Teams App Bot Feature](./images/msteams_bot_app_bot_feature.png)
59 | 
60 | ![MS Teams App Permissions](./images/msteams_bot_app_bot_permissions.png "MS Teams App Permissions")
61 | 
62 | This application needs to be installed in a Teams Group
63 | 
64 | ![MS Teams App Installation](./images/msteams_app_installation.png)
65 | 
66 | And some information must be extracted from the Teams Group url:
67 | 
68 | ![MS Teams Group Information](./images/msteams_team_and_channel_info.png)
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 | 
```