#
tokens: 3089/50000 6/6 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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())
```