UNPKG

msteams-mcp-server

Version:

Microsoft Teams MCP Server - Complete Teams integration for Claude Desktop and MCP clients with secure OAuth2 authentication and comprehensive team management

1,065 lines (1,064 loc) • 96.2 kB
/** * Microsoft Teams MCP Server * A comprehensive server implementation using Model Context Protocol for Microsoft Teams API */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { logger } from './utils/api.js'; import { teamsOperations } from './utils/teams-operations.js'; import { teamsAuth } from './utils/teams-auth.js'; let teamsConfig = { setupAuth: false, resetAuth: false, debug: false, nonInteractive: false, login: false, logout: false, verifyLogin: false, checkPermissions: false, adminConsentHelp: false, azureSetup: false, clientId: undefined, tenantId: undefined, clientSecret: undefined, }; function parseArgs() { const args = process.argv.slice(2); const config = { setupAuth: false, resetAuth: false, debug: false, nonInteractive: false, login: false, logout: false, verifyLogin: false, checkPermissions: false, adminConsentHelp: false, azureSetup: false, clientId: undefined, tenantId: undefined, clientSecret: undefined, }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--setup-auth') config.setupAuth = true; else if (arg === '--reset-auth') config.resetAuth = true; else if (arg === '--debug') config.debug = true; else if (arg === '--non-interactive' || arg === '-n') config.nonInteractive = true; else if (arg === '--login') config.login = true; else if (arg === '--logout') config.logout = true; else if (arg === '--verify-login') config.verifyLogin = true; else if (arg === '--check-permissions') config.checkPermissions = true; else if (arg === '--admin-consent-help') config.adminConsentHelp = true; else if (arg === '--azure-setup') config.azureSetup = true; else if (arg === '--client-id') { if (i + 1 < args.length) { config.clientId = args[i + 1]; i++; // Skip the next argument since it's the value } } else if (arg === '--tenant-id') { if (i + 1 < args.length) { config.tenantId = args[i + 1]; i++; // Skip the next argument since it's the value } } else if (arg === '--client-secret') { if (i + 1 < args.length) { config.clientSecret = args[i + 1]; i++; // Skip the next argument since it's the value } } else if (arg === '--redirect-uri') { if (i + 1 < args.length) { config.redirectUri = args[i + 1]; i++; // Skip the next argument since it's the value } } else if (arg === '--user-id') { if (i + 1 < args.length) { config.userId = args[i + 1]; i++; // Skip the next argument since it's the value } } } return config; } const server = new Server({ name: "msteams-mcp-server", version: "1.0.29" }, { capabilities: { resources: { read: true, list: true, templates: true }, tools: { list: true, call: true }, prompts: { list: true, get: true }, resourceTemplates: { list: true } } }); logger.log('Microsoft Teams MCP Server started with version 1.0.23'); // Set up the resource listing request handler server.setRequestHandler(ListResourcesRequestSchema, async () => { logger.log('Received list resources request'); return { resources: [] }; }); /** * Handler for reading resource information. */ server.setRequestHandler(ReadResourceRequestSchema, async (request) => { logger.log('Received read resource request: ' + JSON.stringify(request)); throw new Error("Resource reading not implemented"); }); /** * Handler for listing available prompts. */ server.setRequestHandler(ListPromptsRequestSchema, async () => { logger.log('Received list prompts request'); return { prompts: [] }; }); /** * Handler for getting a specific prompt. */ server.setRequestHandler(GetPromptRequestSchema, async (request) => { logger.log('Received get prompt request: ' + JSON.stringify(request)); throw new Error("Prompt getting not implemented"); }); /** * Handler for listing available resource templates. */ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { logger.log('Received list resource templates request'); return { resourceTemplates: [] }; }); /** * List available tools for interacting with Microsoft Teams. */ server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "teams_authenticate", description: "Handle Microsoft Teams authentication (login, logout, status check)", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["login", "logout", "status"], description: "Authentication action to perform: login (start auth), logout (clear tokens), status (check current state)" } }, required: ["action"] } }, { name: "manage_teams", description: "Comprehensive team management - list, search, create, or get details about teams", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["list", "search", "create", "get", "get_members"], description: "Action to perform on teams" }, teamId: { type: "string", description: "Team ID for get, get_members actions" }, query: { type: "string", description: "Search query for teams (name or description)" }, visibility: { type: "string", enum: ["Private", "Public"], description: "Filter teams by visibility" }, maxResults: { type: "integer", description: "Maximum number of results to return", default: 10 }, displayName: { type: "string", description: "Name for new team (create action)" }, description: { type: "string", description: "Description for new team (create action)" }, members: { type: "array", items: { type: "string" }, description: "Array of user IDs or emails for team members (create action)" }, owners: { type: "array", items: { type: "string" }, description: "Array of user IDs or emails for team owners (create action)" } }, required: ["action"] } }, { name: "manage_channels", description: "Channel management - list, search, create channels in teams", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["list", "search", "create"], description: "Action to perform on channels" }, teamId: { type: "string", description: "Team ID (required for all actions)" }, query: { type: "string", description: "Search query for channels (name or description)" }, membershipType: { type: "string", enum: ["standard", "private", "shared"], description: "Filter channels by membership type" }, maxResults: { type: "integer", description: "Maximum number of results to return", default: 10 }, displayName: { type: "string", description: "Name for new channel (create action)" }, description: { type: "string", description: "Description for new channel (create action)" } }, required: ["action", "teamId"] } }, { name: "send_message", description: "Send a message to a Teams channel", inputSchema: { type: "object", properties: { teamId: { type: "string", description: "ID of the team containing the channel" }, channelId: { type: "string", description: "ID of the channel to send message to" }, content: { type: "string", description: "Message content to send" }, contentType: { type: "string", enum: ["text", "html"], description: "Content type of the message", default: "text" }, mentions: { type: "array", items: { type: "object", properties: { id: { type: "string" }, displayName: { type: "string" } } }, description: "Array of user mentions in the message" } }, required: ["teamId", "channelId", "content"] } }, { name: "manage_messages", description: "Message management - get or search messages in channels", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["get", "search"], description: "Action to perform on messages" }, teamId: { type: "string", description: "ID of the team containing the channel" }, channelId: { type: "string", description: "ID of the channel" }, query: { type: "string", description: "Search query for messages (search action)" }, fromDate: { type: "string", description: "Start date for message search (ISO format)" }, toDate: { type: "string", description: "End date for message search (ISO format)" }, maxResults: { type: "integer", description: "Maximum number of results to return", default: 20 } }, required: ["action", "teamId", "channelId"] } }, { name: "manage_members", description: "Team member management - add or remove team members", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["add", "remove"], description: "Action to perform on team members" }, teamId: { type: "string", description: "ID of the team" }, userId: { type: "string", description: "User ID or email to add/remove" }, memberId: { type: "string", description: "Member ID for removal (use with remove action)" }, roles: { type: "array", items: { type: "string", enum: ["owner", "member"] }, description: "Roles for the user (add action)", default: ["member"] } }, required: ["action", "teamId"] } }, { name: "search_users", description: "Search for users in the organization", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query (name or email)" }, maxResults: { type: "integer", description: "Maximum number of results to return", default: 10 } }, required: ["query"] } }, { name: "send_direct_message", description: "Send a direct message to a user", inputSchema: { type: "object", properties: { userId: { type: "string", description: "User ID to send message to" }, displayName: { type: "string", description: "Display name of user (used to find user ID if userId not provided)" }, content: { type: "string", description: "Message content to send" }, contentType: { type: "string", enum: ["text", "html"], description: "Content type of the message", default: "text" }, mentions: { type: "array", items: { type: "object", properties: { id: { type: "string" }, displayName: { type: "string" } } }, description: "Array of user mentions in the message" } }, required: ["content"] } }, { name: "get_direct_messages", description: "Get direct messages from a chat with a user", inputSchema: { type: "object", properties: { userId: { type: "string", description: "User ID to get messages from" }, displayName: { type: "string", description: "Display name of user (used to find user ID if userId not provided)" }, maxResults: { type: "integer", description: "Maximum number of messages to return", default: 20 } }, required: [] } }, { name: "manage_reactions", description: "Add or remove reactions from messages", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["add", "remove"], description: "Action to perform: add or remove reaction" }, messageType: { type: "string", enum: ["channel", "chat"], description: "Type of message: channel message or chat/direct message" }, teamId: { type: "string", description: "Team ID (required for channel messages)" }, channelId: { type: "string", description: "Channel ID (required for channel messages)" }, chatId: { type: "string", description: "Chat ID (required for chat messages, can be obtained from direct message operations)" }, messageId: { type: "string", description: "ID of the message to react to" }, reactionType: { type: "string", enum: ["like", "heart", "laugh", "surprised", "sad", "angry"], description: "Type of reaction" } }, required: ["action", "messageType", "messageId", "reactionType"] } }, { name: "manage_files", description: "Upload, download, or list files in teams and channels", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["upload", "download", "list"], description: "Action to perform: upload file, download file, or list files" }, location: { type: "string", enum: ["team", "channel"], description: "Location for file operations: team drive or channel files" }, teamId: { type: "string", description: "Team ID (required for all actions)" }, channelId: { type: "string", description: "Channel ID (required for channel file operations)" }, fileName: { type: "string", description: "Name for the file (upload action)" }, fileContent: { type: "string", description: "Base64 encoded file content (upload action)" }, mimeType: { type: "string", description: "MIME type of the file (upload action)" }, driveId: { type: "string", description: "Drive ID (required for download action)" }, itemId: { type: "string", description: "File item ID (required for download action)" }, maxResults: { type: "integer", description: "Maximum number of files to return (list action)", default: 20 } }, required: ["action", "location", "teamId"] } }, { name: "manage_replies", description: "Reply to messages or get message replies in channels and chats", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["reply", "get_replies"], description: "Action to perform: reply to message or get existing replies" }, messageType: { type: "string", enum: ["channel", "chat"], description: "Type of message: channel message or chat/direct message" }, teamId: { type: "string", description: "Team ID (required for channel messages)" }, channelId: { type: "string", description: "Channel ID (required for channel messages)" }, chatId: { type: "string", description: "Chat ID (required for chat messages)" }, messageId: { type: "string", description: "ID of the message to reply to or get replies from" }, content: { type: "string", description: "Reply content (reply action)" }, contentType: { type: "string", enum: ["text", "html"], description: "Content type of the reply", default: "text" }, maxResults: { type: "integer", description: "Maximum number of replies to return (get_replies action)", default: 20 } }, required: ["action", "messageType", "messageId"] } }, { name: "manage_group_chats", description: "Create and manage group chats, add/remove members", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["create", "list", "get_members", "add_members", "remove_member"], description: "Action to perform on group chats" }, chatId: { type: "string", description: "Group chat ID (required for get_members, add_members, remove_member actions)" }, topic: { type: "string", description: "Topic/name for the group chat (create action)" }, members: { type: "array", items: { type: "string" }, description: "Array of user IDs to add to the group chat (create/add_members actions)" }, membershipId: { type: "string", description: "Membership ID to remove (remove_member action)" }, maxResults: { type: "integer", description: "Maximum number of group chats to return (list action)", default: 20 } }, required: ["action"] } }, { name: "manage_meetings", description: "Create and manage online meetings and calendar events", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["create_meeting", "list_meetings", "get_calendar", "create_event"], description: "Action to perform on meetings and calendar" }, subject: { type: "string", description: "Meeting/event subject (create actions)" }, startDateTime: { type: "string", description: "Start date and time in ISO 8601 format (create actions)" }, endDateTime: { type: "string", description: "End date and time in ISO 8601 format (create actions)" }, attendees: { type: "array", items: { type: "string" }, description: "Array of attendee email addresses (create actions)" }, content: { type: "string", description: "Meeting/event description (create actions)" }, isRecordingEnabled: { type: "boolean", description: "Whether recording is enabled (create_meeting action)", default: false }, startTime: { type: "string", description: "Filter start time for calendar events (get_calendar action)" }, endTime: { type: "string", description: "Filter end time for calendar events (get_calendar action)" }, maxResults: { type: "integer", description: "Maximum number of results to return", default: 20 } }, required: ["action"] } }, { name: "manage_presence", description: "Get and set user presence/status information", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["get_presence", "set_presence", "get_bulk_presence"], description: "Action to perform on user presence" }, userId: { type: "string", description: "User ID to get presence for (get_presence action, optional for current user)" }, userIds: { type: "array", items: { type: "string" }, description: "Array of user IDs to get presence for (get_bulk_presence action)" }, availability: { type: "string", enum: ["Available", "Busy", "DoNotDisturb", "BeRightBack", "Away"], description: "Availability status to set (set_presence action)" }, activity: { type: "string", enum: ["Available", "Busy", "DoNotDisturb", "BeRightBack", "Away", "InAMeeting", "InACall"], description: "Activity status to set (set_presence action)" }, expirationDuration: { type: "string", description: "How long to keep the status (ISO 8601 duration, e.g., 'PT1H' for 1 hour)" } }, required: ["action"] } } ] }; }); /** * Helper function to get emoji for reaction types */ function getReactionEmoji(reactionType) { const emojiMap = { 'like': 'šŸ‘', 'heart': 'ā¤ļø', 'laugh': 'šŸ˜‚', 'surprised': '😮', 'sad': '😢', 'angry': '😠' }; return emojiMap[reactionType.toLowerCase()] || 'šŸ‘'; } /** * Helper function to safely extract arguments with defaults */ function safeGetArgs(args, defaultValues) { if (!args || typeof args !== 'object') { return defaultValues; } const result = { ...defaultValues }; for (const key in defaultValues) { if (args[key] !== undefined) { result[key] = args[key]; } } return result; } /** * Handle tool execution requests */ server.setRequestHandler(CallToolRequestSchema, async (request) => { logger.log('Received call tool request: ' + JSON.stringify(request)); try { switch (request.params.name) { case "teams_authenticate": { const args = safeGetArgs(request.params.arguments, { action: '' }); if (!args.action) { throw new Error("Action is required for authentication"); } switch (args.action) { case "login": try { // Check if already authenticated const authStatus = await teamsAuth.getAuthenticationStatus(); if (authStatus.isAuthenticated) { return { content: [{ type: "text", text: `āœ… **Already authenticated with Microsoft Teams!**\n\n**User:** ${authStatus.user?.displayName || 'Unknown'}\n\nšŸŽ‰ You can now use Teams commands to manage your teams, channels, and messages.` }] }; } // Start OAuth redirect authentication try { const result = await teamsAuth.authenticate(); return { content: [{ type: "text", text: `āœ… **Successfully authenticated with Microsoft Teams!**\n\n**User:** ${result.account?.name || 'Unknown'}\n\nšŸŽ‰ You can now use Teams commands to manage your teams, channels, and messages.` }] }; } catch (authError) { // Handle OAuth redirect required error if (authError.authUrl) { return { content: [{ type: "text", text: `šŸ” **Microsoft Teams Authentication Required**\n\n` + `🌐 **Please complete authentication:**\n` + `1. Visit: **${authError.authUrl}**\n` + `2. Sign in with your Microsoft account\n` + `3. Complete the authorization process\n\n` + `šŸ’” **Direct link:** [Click here to authenticate](${authError.authUrl})\n\n` + `šŸ”„ **Next steps:**\n` + `- Complete authentication in your browser\n` + `- The system will automatically detect completion\n` + `- Try your Teams command again after authentication\n\n` + `āš ļø **If you're having trouble:**\n` + `- Make sure you're signed in to the correct Microsoft account\n` + `- Check that your organization allows Teams access\n` + `- Ensure you have the necessary permissions for Teams operations` }] }; } // General authentication error throw authError; } } catch (error) { return { content: [{ type: "text", text: `āŒ **Authentication failed:** ${error.message}\n\n` + `šŸ’” **Troubleshooting steps:**\n` + `1. **Try terminal authentication:** Run \`msteams-mcp-server --login\` from your terminal\n` + `2. **Check network connection:** Ensure you can access https://login.microsoftonline.com\n` + `3. **Verify permissions:** Make sure your Azure app has Teams permissions\n` + `4. **Reset authentication:** Try \`msteams-mcp-server --reset-auth\` then retry\n\n` + `šŸ”— **Need help?** Check the README for detailed setup instructions.` }] }; } case "logout": try { await teamsAuth.logout(); return { content: [{ type: "text", text: "āœ… **Successfully logged out from Microsoft Teams.**\n\nšŸ—‘ļø All stored authentication tokens have been cleared.\n\nšŸ”„ Use the authenticate tool with action 'login' to sign in again." }] }; } catch (error) { return { content: [{ type: "text", text: `āŒ **Logout failed:** ${error.message}` }] }; } case "status": try { const authStatus = await teamsAuth.getAuthenticationStatus(); if (authStatus.isAuthenticated) { return { content: [{ type: "text", text: `āœ… **Authenticated with Microsoft Teams**\n\n**User:** ${authStatus.user?.displayName || 'Unknown'}\n**Status:** Ready to use Teams commands\n\nšŸŽ‰ You can now manage your teams, channels, and messages!` }] }; } else { return { content: [{ type: "text", text: `āŒ **Not authenticated with Microsoft Teams**\n\n**Status:** ${authStatus.message}\n\nšŸ”‘ **Next steps:**\n- Run the authenticate tool with action 'login'\n- Or use 'msteams-mcp-server --login' from terminal` }] }; } } catch (error) { return { content: [{ type: "text", text: `āŒ **Error checking authentication status:** ${error.message}` }] }; } default: throw new Error(`Unknown authentication action: ${args.action}`); } } case "manage_teams": { const args = safeGetArgs(request.params.arguments, { action: '', teamId: '', query: '', visibility: '', maxResults: 10, displayName: '', description: '', members: [], owners: [] }); if (!args.action) { throw new Error("Action is required for team management"); } switch (args.action) { case "list": const teams = await teamsOperations.getUserTeams(args.maxResults || 10); return { content: [{ type: "text", text: `šŸ“Š **Your Teams (${teams.length})**\n\n${teams.map(team => `**${team.displayName}** (${team.visibility})\n` + `ID: ${team.id}\n` + `Description: ${team.description || 'No description'}\n` + `Created: ${new Date(team.createdDateTime).toLocaleDateString()}\n` + `šŸ”— [Open in Teams](${team.webUrl})\n`).join('\n')}` }] }; case "search": if (!args.query) { throw new Error("Query is required for team search"); } const searchResults = await teamsOperations.searchTeams({ query: args.query, visibility: args.visibility, maxResults: args.maxResults }); return { content: [{ type: "text", text: `šŸ” **Team Search Results for "${args.query}" (${searchResults.length})**\n\n${searchResults.map(team => `**${team.displayName}** (${team.visibility})\n` + `ID: ${team.id}\n` + `Description: ${team.description || 'No description'}\n` + `šŸ”— [Open in Teams](${team.webUrl})\n`).join('\n')}` }] }; case "create": if (!args.displayName) { throw new Error("Display name is required for team creation"); } const newTeam = await teamsOperations.createTeam({ displayName: args.displayName, description: args.description, visibility: args.visibility || 'Private', members: args.members, owners: args.owners }); return { content: [{ type: "text", text: `āœ… **Team Created Successfully!**\n\n**Name:** ${newTeam.displayName}\n**ID:** ${newTeam.id}\n**Visibility:** ${newTeam.visibility}\n**Description:** ${newTeam.description || 'No description'}\n\nšŸ”— [Open in Teams](${newTeam.webUrl})` }] }; case "get": if (!args.teamId) { throw new Error("Team ID is required for getting team details"); } const team = await teamsOperations.getTeam(args.teamId); return { content: [{ type: "text", text: `šŸ“‹ **Team Details**\n\n**Name:** ${team.displayName}\n**ID:** ${team.id}\n**Visibility:** ${team.visibility}\n**Description:** ${team.description || 'No description'}\n**Created:** ${new Date(team.createdDateTime).toLocaleDateString()}\n\nšŸ”— [Open in Teams](${team.webUrl})` }] }; case "get_members": if (!args.teamId) { throw new Error("Team ID is required for getting team members"); } const members = await teamsOperations.getTeamMembers(args.teamId); return { content: [{ type: "text", text: `šŸ‘„ **Team Members (${members.length})**\n\n${members.map(member => `**${member.displayName}**\n` + `Email: ${member.email}\n` + `Roles: ${member.roles.join(', ')}\n` + `ID: ${member.id}\n`).join('\n')}` }] }; default: throw new Error(`Unknown team action: ${args.action}`); } } case "manage_channels": { const args = safeGetArgs(request.params.arguments, { action: '', teamId: '', query: '', membershipType: '', maxResults: 10, displayName: '', description: '' }); if (!args.action || !args.teamId) { throw new Error("Action and teamId are required for channel management"); } switch (args.action) { case "list": const channels = await teamsOperations.getTeamChannels(args.teamId); return { content: [{ type: "text", text: `šŸ“‘ **Team Channels (${channels.length})**\n\n${channels.map(channel => `**${channel.displayName}** (${channel.membershipType})\n` + `ID: ${channel.id}\n` + `Description: ${channel.description || 'No description'}\n` + `Created: ${new Date(channel.createdDateTime).toLocaleDateString()}\n` + `šŸ”— [Open in Teams](${channel.webUrl})\n`).join('\n')}` }] }; case "search": if (!args.query) { throw new Error("Query is required for channel search"); } const channelResults = await teamsOperations.searchChannels({ teamId: args.teamId, query: args.query, membershipType: args.membershipType, maxResults: args.maxResults }); return { content: [{ type: "text", text: `šŸ” **Channel Search Results for "${args.query}" (${channelResults.length})**\n\n${channelResults.map(channel => `**${channel.displayName}** (${channel.membershipType})\n` + `ID: ${channel.id}\n` + `Description: ${channel.description || 'No description'}\n` + `šŸ”— [Open in Teams](${channel.webUrl})\n`).join('\n')}` }] }; case "create": if (!args.displayName) { throw new Error("Display name is required for channel creation"); } const newChannel = await teamsOperations.createChannel(args.teamId, { displayName: args.displayName, description: args.description, membershipType: args.membershipType || 'standard' }); return { content: [{ type: "text", text: `āœ… **Channel Created Successfully!**\n\n**Name:** ${newChannel.displayName}\n**ID:** ${newChannel.id}\n**Type:** ${newChannel.membershipType}\n**Description:** ${newChannel.description || 'No description'}\n\nšŸ”— [Open in Teams](${newChannel.webUrl})` }] }; default: throw new Error(`Unknown channel action: ${args.action}`); } } case "send_message": { const args = safeGetArgs(request.params.arguments, { teamId: '', channelId: '', content: '', contentType: 'text', mentions: [] }); if (!args.teamId || !args.channelId || !args.content) { throw new Error("teamId, channelId, and content are required for sending messages"); } const message = await teamsOperations.sendChannelMessage(args.teamId, args.channelId, { content: args.content,