# Directory Structure
```
├── .gitignore
├── .python-version
├── pyproject.toml
├── README.md
├── src
│ └── microsoft_teams_mcp
│ ├── __init__.py
│ └── server.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
1 | 3.10
2 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Python-generated files
2 | __pycache__/
3 | *.py[oc]
4 | build/
5 | dist/
6 | wheels/
7 | *.egg-info
8 |
9 | # Virtual environments
10 | .venv
11 |
12 | .env
13 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # microsoft-teams-mcp MCP server
2 |
3 | An MCP Server with a tool for Microsoft Teams chat notifications.
4 |
5 | > [!WARNING]
6 | > This is provided for reference and wasn't tested with MCP clients other than VS Code.
7 |
8 | ## Components
9 |
10 | ### Tools
11 |
12 | The server implements one tool:
13 | - send-notification: Sends a notification message to Microsoft Teams
14 | - Takes "message" and "project" as required string arguments
15 | - Supports Markdown formatting for messages
16 | - Uses Azure AD authentication to securely communicate with Teams
17 |
18 | ## Configuration
19 |
20 | This requires a Microsoft Teams bot to use for the notifications. You can use [my example Notification Bot](https://github.com/therealjohn/TeamsNotificationBotMCP) created with [Teams Toolkit](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/teams-toolkit-fundamentals).
21 |
22 | The server requires the following environment variables to be set:
23 |
24 | - `BOT_ENDPOINT`: The URL endpoint of your Microsoft Teams bot
25 | - `MICROSOFT_APP_ID`: Application (client) ID from Azure AD app registration
26 | - `MICROSOFT_APP_PASSWORD`: Client secret from Azure AD app registration
27 | - `MICROSOFT_APP_TENANT_ID`: Your Azure AD tenant ID
28 | - `EMAIL`: The email address for the user receiving notifications
29 |
30 | You can set these in a `.env` file in the project root directory.
31 |
32 | ## Quickstart
33 |
34 | ### Install
35 |
36 | #### VS Code
37 |
38 | This was tested using MCP support in VS Code, which at the time of creating this was available only in VS Code Insiders.
39 |
40 | Add this to the VS Code Insiders Settings (JSON)
41 |
42 | ```
43 | "mcp": {
44 | "inputs": [],
45 | "servers": {
46 | "MicrosoftTeams": {
47 | "command": "uv",
48 | "args": [
49 | "--directory",
50 | "<path/to/the/project>/microsoft-teams-mcp",
51 | "run",
52 | "microsoft-teams-mcp"
53 | ],
54 | "env": {
55 | "BOT_ENDPOINT": "<endpoint or dev tunnel URL of Teams bot>/api/notification",
56 | "MICROSOFT_APP_ID": "<microsoft-entra-client-id>",
57 | "MICROSOFT_APP_PASSWORD": "<microsoft-entra-client-secret>",
58 | "MICROSOFT_APP_TENANT_ID": "<microsoft-entra-tenant-id>",
59 | "EMAIL": "<your-email-in-teams>",
60 | }
61 | }
62 | }
63 | }
64 | ```
65 |
66 | ## Development
67 |
68 | ### Building
69 |
70 | To prepare the package for distribution:
71 |
72 | 1. Sync dependencies and update lockfile:
73 | ```bash
74 | uv sync
75 | ```
76 |
77 | 2. Build package distributions:
78 | ```bash
79 | uv build
80 | ```
81 |
```
--------------------------------------------------------------------------------
/src/microsoft_teams_mcp/__init__.py:
--------------------------------------------------------------------------------
```python
1 | from . import server
2 | import asyncio
3 |
4 | def main():
5 | """Main entry point for the package."""
6 | asyncio.run(server.run_server())
7 |
8 | __all__ = ['main', 'server']
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [project]
2 | name = "microsoft-teams-mcp"
3 | version = "0.1.0"
4 | description = "An MCP Server with tools for Microsoft Teams chat interactions and notifications."
5 | readme = "README.md"
6 | requires-python = ">=3.10"
7 | dependencies = [ "mcp>=1.4.1", "python-dotenv", "aiohttp", "msal"]
8 | [[project.authors]]
9 | name = "John Miller"
10 | email = "[email protected]"
11 |
12 | [build-system]
13 | requires = [ "hatchling",]
14 | build-backend = "hatchling.build"
15 |
16 | [project.scripts]
17 | microsoft-teams-mcp = "microsoft_teams_mcp:main"
18 |
```
--------------------------------------------------------------------------------
/src/microsoft_teams_mcp/server.py:
--------------------------------------------------------------------------------
```python
1 | import asyncio
2 | import os
3 | import msal
4 |
5 | from mcp.server.models import InitializationOptions
6 | import mcp.types as types
7 | from mcp.server import NotificationOptions, Server
8 | import mcp.server.stdio
9 | from dotenv import load_dotenv
10 | import aiohttp
11 | from typing import Dict, List, Optional, Tuple
12 |
13 | load_dotenv()
14 |
15 | SERVER_NAME = "microsoft-teams-mcp"
16 | SERVER_VERSION = "0.1.0"
17 | TOOL_NAME = "send-notification"
18 | REQUIRED_ENV_VARS = ["BOT_ENDPOINT", "MICROSOFT_APP_ID", "MICROSOFT_APP_PASSWORD", "MICROSOFT_APP_TENANT_ID", "EMAIL"]
19 |
20 | server = Server(SERVER_NAME)
21 |
22 | async def get_auth_token(app_id: str, app_password: str, tenant_id: str) -> Tuple[Optional[str], Optional[str]]:
23 | """
24 | Get authentication token using MSAL client credentials flow.
25 |
26 | Args:
27 | app_id: The application ID
28 | app_password: The application password/secret
29 | tenant_id: The tenant ID
30 |
31 | Returns:
32 | Tuple containing (access_token, error_message)
33 | """
34 | try:
35 | app = msal.ConfidentialClientApplication(
36 | client_id=app_id,
37 | client_credential=app_password,
38 | authority=f"https://login.microsoftonline.com/{tenant_id}"
39 | )
40 |
41 | scopes = [f"{app_id}/.default"]
42 |
43 | result = app.acquire_token_for_client(scopes)
44 |
45 | if "access_token" not in result:
46 | error_msg = result.get("error_description", "Failed to acquire token")
47 | return None, error_msg
48 |
49 | return result["access_token"], None
50 | except Exception as e:
51 | return None, str(e)
52 |
53 | def validate_environment_variables() -> Tuple[Dict[str, str], List[str]]:
54 | """
55 | Validate required environment variables.
56 |
57 | Returns:
58 | Tuple containing (env_vars_dict, missing_vars_list)
59 | """
60 | env_vars = {}
61 |
62 | for var_name in REQUIRED_ENV_VARS:
63 | env_vars[var_name] = os.getenv(var_name)
64 |
65 | missing_vars = [var_name for var_name in REQUIRED_ENV_VARS if not env_vars[var_name]]
66 |
67 | return env_vars, missing_vars
68 |
69 | async def send_notification(
70 | bot_endpoint: str,
71 | access_token: str,
72 | email: Optional[str],
73 | message: str,
74 | project: str
75 | ) -> Tuple[bool, Optional[str]]:
76 | """
77 | Send notification to the Teams bot endpoint.
78 |
79 | Args:
80 | bot_endpoint: The bot endpoint URL
81 | access_token: Authentication token
82 | email: User email (optional)
83 | message: Notification message
84 | project: Project name
85 |
86 | Returns:
87 | Tuple containing (success, error_message)
88 | """
89 | try:
90 | payload = {
91 | "email": email,
92 | "message": message,
93 | "project": project,
94 | }
95 |
96 | headers = {
97 | "Authorization": f"Bearer {access_token}",
98 | "Content-Type": "application/json"
99 | }
100 |
101 | async with aiohttp.ClientSession() as session:
102 | async with session.post(bot_endpoint, json=payload, headers=headers) as response:
103 | if response.status >= 400:
104 | response_text = await response.text()
105 | return False, f"HTTP {response.status} - {response_text}"
106 |
107 | return True, None
108 |
109 | except Exception as e:
110 | return False, str(e)
111 |
112 | @server.list_tools()
113 | async def handle_list_tools() -> list[types.Tool]:
114 | return [
115 | types.Tool(
116 | name=TOOL_NAME,
117 | description="Send a notification message to the user. Supports markdown formatting for messages. Use backticks for code blocks and inline code. Use square brackets for placeholders.",
118 | inputSchema={
119 | "type": "object",
120 | "properties": {
121 | "message": {"type": "string"},
122 | "project": {"type": "string"},
123 | },
124 | "required": ["message", "project"],
125 | },
126 | )
127 | ]
128 |
129 | @server.call_tool()
130 | async def handle_call_tool(
131 | name: str, arguments: dict | None
132 | ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
133 | if name != TOOL_NAME:
134 | raise ValueError(f"Unknown tool: {name}")
135 |
136 | if not arguments:
137 | raise ValueError("Missing arguments")
138 |
139 | message = arguments.get("message")
140 | project = arguments.get("project")
141 |
142 | if not message or not project:
143 | raise ValueError("Missing message or project")
144 |
145 | env_vars, missing_vars = validate_environment_variables()
146 |
147 | if missing_vars:
148 | return [
149 | types.TextContent(
150 | type="text",
151 | text=f"Missing required environment variables: {', '.join(missing_vars)}"
152 | )
153 | ]
154 |
155 | try:
156 | access_token, error = await get_auth_token(
157 | env_vars["MICROSOFT_APP_ID"],
158 | env_vars["MICROSOFT_APP_PASSWORD"],
159 | env_vars["MICROSOFT_APP_TENANT_ID"]
160 | )
161 |
162 | if error:
163 | return [
164 | types.TextContent(
165 | type="text",
166 | text=f"Authentication failed: {error}"
167 | )
168 | ]
169 |
170 | success, error_msg = await send_notification(
171 | env_vars["BOT_ENDPOINT"],
172 | access_token,
173 | env_vars["EMAIL"],
174 | message,
175 | project
176 | )
177 |
178 | if not success:
179 | return [
180 | types.TextContent(
181 | type="text",
182 | text=f"Failed to send notification: {error_msg}"
183 | )
184 | ]
185 |
186 | return [
187 | types.TextContent(
188 | type="text",
189 | text=f"Sent notification message for project '{project}' with content: {message}",
190 | )
191 | ]
192 |
193 | except Exception as e:
194 | return [
195 | types.TextContent(
196 | type="text",
197 | text=f"Error sending notification: {str(e)}"
198 | )
199 | ]
200 |
201 | async def run_server():
202 | async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
203 | await server.run(
204 | read_stream,
205 | write_stream,
206 | InitializationOptions(
207 | server_name=SERVER_NAME,
208 | server_version=SERVER_VERSION,
209 | capabilities=server.get_capabilities(
210 | notification_options=NotificationOptions(),
211 | experimental_capabilities={},
212 | ),
213 | ),
214 | )
215 |
216 | if __name__ == "__main__":
217 | asyncio.run(run_server())
```