# 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()) ```