ms365-mcp-server
Version:
Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support
957 lines (956 loc) ⢠63.1 kB
JavaScript
/**
* Microsoft 365 MCP Server
* A comprehensive server implementation using Model Context Protocol for Microsoft 365 API
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import express from 'express';
import cors from 'cors';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs/promises';
import crypto from 'crypto';
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
import { logger } from './utils/api.js';
import { MS365Operations } from './utils/ms365-operations.js';
import { multiUserMS365Auth } from './utils/multi-user-auth.js';
import { enhancedMS365Auth } from './utils/ms365-auth-enhanced.js';
// Create singleton MS365Operations instance
const ms365Ops = new MS365Operations();
let ms365Config = {
setupAuth: false,
resetAuth: false,
debug: false,
nonInteractive: false,
multiUser: false,
login: false,
logout: false,
verifyLogin: false,
serverUrl: process.env.SERVER_URL || 'http://localhost:55000'
};
function parseArgs() {
const args = process.argv.slice(2);
const config = {
setupAuth: false,
resetAuth: false,
debug: false,
nonInteractive: false,
multiUser: false,
login: false,
logout: false,
verifyLogin: false,
serverUrl: process.env.SERVER_URL || 'http://localhost:55000'
};
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 === '--multi-user')
config.multiUser = 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 === '--server-url' && i + 1 < args.length) {
config.serverUrl = args[++i];
}
}
return config;
}
const server = new Server({
name: "ms365-mcp-server",
version: "1.1.9"
}, {
capabilities: {
resources: {
read: true,
list: true
},
tools: {
list: true,
call: true
},
prompts: {
list: true
}
}
});
// 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: [] };
});
/**
* List available tools for interacting with Microsoft 365.
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
const baseTools = [
{
name: "send_email",
description: "Send an email message with support for HTML content, attachments, and international characters. Supports both plain text and HTML emails with proper formatting.",
inputSchema: {
type: "object",
properties: {
userId: {
type: "string",
description: "User ID for multi-user authentication (required if using multi-user mode)"
},
to: {
type: "array",
items: { type: "string" },
description: "List of recipient email addresses"
},
cc: {
type: "array",
items: { type: "string" },
description: "List of CC recipient email addresses (optional)"
},
bcc: {
type: "array",
items: { type: "string" },
description: "List of BCC recipient email addresses (optional)"
},
subject: {
type: "string",
description: "Email subject line with full support for international characters"
},
body: {
type: "string",
description: "Email content (text or HTML based on bodyType)"
},
bodyType: {
type: "string",
enum: ["text", "html"],
description: "Content type of the email body (default: text)",
default: "text"
},
replyTo: {
type: "string",
description: "Reply-to email address (optional)"
},
importance: {
type: "string",
enum: ["low", "normal", "high"],
description: "Email importance level (default: normal)",
default: "normal"
},
attachments: {
type: "array",
items: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the attachment file"
},
contentBytes: {
type: "string",
description: "Base64 encoded content of the attachment"
},
contentType: {
type: "string",
description: "MIME type of the attachment (optional, auto-detected if not provided)"
}
},
required: ["name", "contentBytes"]
},
description: "List of email attachments (optional)"
}
},
required: ["to", "subject"],
additionalProperties: false
}
},
{
name: "manage_email",
description: "UNIFIED EMAIL MANAGEMENT: Read, search, list, mark, move, or delete emails. Combines all email operations in one powerful tool. Supports intelligent partial name matching, folder management, and advanced search criteria.",
inputSchema: {
type: "object",
properties: {
userId: {
type: "string",
description: "User ID for multi-user authentication (required if using multi-user mode)"
},
action: {
type: "string",
enum: ["read", "search", "list", "mark", "move", "delete", "search_to_me", "draft"],
description: "Action to perform: read (get email by ID), search (find emails), list (folder contents), mark (read/unread), move (to folder), delete (permanently), search_to_me (emails addressed to you), draft (create/save draft)"
},
messageId: {
type: "string",
description: "Email ID (required for: read, mark, move, delete)"
},
includeAttachments: {
type: "boolean",
description: "Include attachment info when reading email",
default: false
},
isRead: {
type: "boolean",
description: "Mark as read (true) or unread (false) - used with mark action"
},
destinationFolderId: {
type: "string",
description: "Destination folder for move action (e.g., 'archive', 'drafts', 'deleteditems')"
},
folderId: {
type: "string",
description: "Folder to list emails from (default: inbox) - used with list action",
default: "inbox"
},
query: {
type: "string",
description: "General search query using natural language or specific terms"
},
from: {
type: "string",
description: "Search emails from specific sender (supports partial names like 'John' or 'Smith')"
},
to: {
type: "string",
description: "Search emails to specific recipient"
},
cc: {
type: "string",
description: "Search emails with specific CC recipient"
},
subject: {
type: "string",
description: "Search emails with specific subject"
},
after: {
type: "string",
description: "Search emails after date (format: YYYY-MM-DD)"
},
before: {
type: "string",
description: "Search emails before date (format: YYYY-MM-DD)"
},
hasAttachment: {
type: "boolean",
description: "Filter emails that have attachments"
},
folder: {
type: "string",
description: "Search within specific folder (default: inbox)"
},
isUnread: {
type: "boolean",
description: "Filter for unread emails only"
},
importance: {
type: "string",
enum: ["low", "normal", "high"],
description: "Filter by email importance level"
},
maxResults: {
type: "number",
description: "Maximum number of results to return (default: 50, max: 200)",
minimum: 1,
maximum: 200,
default: 50
},
// Draft email parameters
draftTo: {
type: "array",
items: { type: "string" },
description: "List of recipient email addresses (required for draft action)"
},
draftCc: {
type: "array",
items: { type: "string" },
description: "List of CC recipient email addresses (optional for draft action)"
},
draftBcc: {
type: "array",
items: { type: "string" },
description: "List of BCC recipient email addresses (optional for draft action)"
},
draftSubject: {
type: "string",
description: "Email subject line (required for draft action)"
},
draftBody: {
type: "string",
description: "Email content (required for draft action)"
},
draftBodyType: {
type: "string",
enum: ["text", "html"],
description: "Content type of the email body for draft (default: text)",
default: "text"
},
draftImportance: {
type: "string",
enum: ["low", "normal", "high"],
description: "Email importance level for draft (default: normal)",
default: "normal"
},
draftAttachments: {
type: "array",
items: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the attachment file"
},
contentBytes: {
type: "string",
description: "Base64 encoded content of the attachment"
},
contentType: {
type: "string",
description: "MIME type of the attachment (optional, auto-detected if not provided)"
}
},
required: ["name", "contentBytes"]
},
description: "List of email attachments for draft (optional)"
}
},
required: ["action"],
additionalProperties: false
}
},
{
name: "get_attachment",
description: "Download and retrieve attachment information from an email. Returns attachment data, filename, and metadata.",
inputSchema: {
type: "object",
properties: {
userId: {
type: "string",
description: "User ID for multi-user authentication (required if using multi-user mode)"
},
messageId: {
type: "string",
description: "Microsoft 365 message ID containing the attachment"
},
attachmentId: {
type: "string",
description: "Attachment ID to download"
}
},
required: ["messageId", "attachmentId"],
additionalProperties: false
}
},
{
name: "list_folders",
description: "List all mail folders in the mailbox. Returns folder names, IDs, and item counts for navigation and organization.",
inputSchema: {
type: "object",
properties: {
userId: {
type: "string",
description: "User ID for multi-user authentication (required if using multi-user mode)"
}
},
additionalProperties: false
}
},
{
name: "manage_contacts",
description: "UNIFIED CONTACT MANAGEMENT: Get all contacts or search contacts by name/email. Combines contact operations in one tool.",
inputSchema: {
type: "object",
properties: {
userId: {
type: "string",
description: "User ID for multi-user authentication (required if using multi-user mode)"
},
action: {
type: "string",
enum: ["list", "search"],
description: "Action: list (get all contacts) or search (find specific contacts)",
default: "list"
},
query: {
type: "string",
description: "Search query for contact name or email address (required for search action)"
},
maxResults: {
type: "number",
description: "Maximum number of contacts to return (default: 100 for list, 50 for search, max: 500)",
minimum: 1,
maximum: 500
}
},
additionalProperties: false
}
}
];
// Add multi-user specific tools
const multiUserTools = [
{
name: "authenticate_user",
description: "Start authentication flow for a new user. Returns authentication URL that user needs to visit for Microsoft 365 access.",
inputSchema: {
type: "object",
properties: {
userEmail: {
type: "string",
description: "User's email address (optional, for identification)"
}
},
additionalProperties: false
}
},
{
name: "remove_my_session",
description: "Remove your own authentication session (user-specific, secure).",
inputSchema: {
type: "object",
properties: {
userId: {
type: "string",
description: "Your User ID to remove"
}
},
required: ["userId"],
additionalProperties: false
}
}
];
// Consolidated authentication tools
const authTools = [
{
name: "authenticate",
description: "UNIFIED AUTHENTICATION: Handle all Microsoft 365 authentication needs. Supports device code flow, status checking, and logout. RECOMMENDED for all auth operations.",
inputSchema: {
type: "object",
properties: {
action: {
type: "string",
enum: ["login", "status", "logout", "device_code", "check_pending"],
description: "Auth action: login (device code auth), status (check auth status), logout (clear tokens), device_code (get device code only), check_pending (check pending auth)",
default: "login"
},
force: {
type: "boolean",
description: "Force new authentication even if already authenticated (for login action)",
default: false
},
accountKey: {
type: "string",
description: "Account key for logout action (default: current user)",
default: "default-user"
}
},
additionalProperties: false
}
}
];
if (ms365Config.multiUser) {
return { tools: [...baseTools, ...multiUserTools] };
}
// For single-user mode, always include auth tools
return { tools: [...baseTools, ...authTools] };
});
/**
* Handle tool execution requests
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
// ============ UNIFIED AUTHENTICATION TOOL ============
case "authenticate":
const action = args?.action || 'login';
switch (action) {
case "login":
try {
// Check if already authenticated
if (await enhancedMS365Auth.isAuthenticated() && !args?.force) {
return {
content: [
{
type: "text",
text: `ā
Already authenticated with Microsoft 365! Use force: true to re-authenticate.`
}
]
};
}
// First, try to complete any existing pending authentication
const existingResult = await enhancedMS365Auth.completeDeviceCodeAuth();
if (existingResult) {
const currentUser = await enhancedMS365Auth.getCurrentUser();
return {
content: [
{
type: "text",
text: `ā
Device code authentication completed successfully!\n\nš¤ User: ${currentUser || 'authenticated-user'}\nš Status: Valid\n\nš You can now use all Microsoft 365 email features!`
}
]
};
}
// Check if there's already a pending device code
let deviceCodeInfo = await enhancedMS365Auth.getPendingDeviceCodeInfo();
if (!deviceCodeInfo || args?.force) {
// No pending code or force new one, start fresh
deviceCodeInfo = await enhancedMS365Auth.startDeviceCodeAuth();
}
// Return device code information immediately (MCP pattern)
return {
content: [
{
type: "text",
text: `š Microsoft 365 Device Code Authentication\n\nš± Visit: ${deviceCodeInfo.verificationUri}\nš Enter this code: ${deviceCodeInfo.userCode}\n\nā³ After completing authentication in your browser, call this tool again to finish the process.\n\nš” This code will expire in 15 minutes.`
}
]
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `ā Authentication failed: ${error.message}\n\nš” Try again or use CLI: node dist/index.js --login`
}
]
};
}
case "status":
const isAuthenticated = await enhancedMS365Auth.isAuthenticated();
const currentUser = await enhancedMS365Auth.getCurrentUser();
const storageInfo = enhancedMS365Auth.getStorageInfo();
const tokenInfo = await enhancedMS365Auth.getTokenExpirationInfo();
let statusText = `š Microsoft 365 Authentication Status\n\nš Authentication: ${isAuthenticated ? 'ā
Valid' : 'ā Not authenticated'}\nš¤ Current User: ${currentUser || 'None'}\nš¾ Storage method: ${storageInfo.method}\nš Storage location: ${storageInfo.location}`;
if (isAuthenticated) {
statusText += `\nā° Token expires in: ${tokenInfo.expiresInMinutes} minutes`;
if (tokenInfo.needsRefresh) {
statusText += `\nā ļø Token will be refreshed automatically on next operation`;
}
}
else {
statusText += `\nš” Use "authenticate" with action: login to sign in`;
}
return {
content: [
{
type: "text",
text: statusText
}
]
};
case "logout":
const accountKey = args?.accountKey;
const wasAuthenticated = await enhancedMS365Auth.isAuthenticated();
await enhancedMS365Auth.resetAuth();
return {
content: [
{
type: "text",
text: wasAuthenticated ?
`ā
Successfully logged out from Microsoft 365.` :
`ā¹ļø No active authentication found.`
}
]
};
case "device_code":
const deviceCodeInfo = await enhancedMS365Auth.getDeviceCodeInfo();
return {
content: [
{
type: "text",
text: `š Microsoft 365 Device Code Authentication\n\nš± Visit: ${deviceCodeInfo.verificationUri}\nš Enter code: ${deviceCodeInfo.userCode}\n\nā³ This code will expire in 15 minutes.`
}
]
};
case "check_pending":
const pendingDeviceCodeState = await enhancedMS365Auth.getPendingDeviceCodeInfo();
if (pendingDeviceCodeState) {
return {
content: [
{
type: "text",
text: `ā³ Pending Device Code Authentication\n\nš± Visit: ${pendingDeviceCodeState.verificationUri}\nš Enter this code: ${pendingDeviceCodeState.userCode}\n\nš” Use "authenticate" with action: login to finish authentication after entering the code.`
}
]
};
}
return {
content: [
{
type: "text",
text: "ā¹ļø No pending device code authentication found. Use 'authenticate' with action: login to start a new authentication process."
}
]
};
default:
throw new Error(`Unknown authentication action: ${action}`);
}
// ============ UNIFIED EMAIL MANAGEMENT TOOL ============
case "manage_email":
if (ms365Config.multiUser) {
const userId = args?.userId;
if (!userId) {
throw new Error("User ID is required in multi-user mode");
}
const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId);
ms365Ops.setGraphClient(graphClient);
}
else {
const graphClient = await enhancedMS365Auth.getGraphClient();
ms365Ops.setGraphClient(graphClient);
}
const emailAction = args?.action;
switch (emailAction) {
case "read":
if (!args?.messageId) {
throw new Error("messageId is required for read action");
}
const email = await ms365Ops.getEmail(args.messageId, args?.includeAttachments);
let emailDetailsText = `š§ Email Details\n\nš Subject: ${email.subject}\nš¤ From: ${email.from.name} <${email.from.address}>\nš
Date: ${email.receivedDateTime}\n`;
if (email.attachments && email.attachments.length > 0) {
emailDetailsText += `\nš Attachments (${email.attachments.length}):\n`;
email.attachments.forEach((attachment, index) => {
const sizeInMB = (attachment.size / (1024 * 1024)).toFixed(2);
emailDetailsText += `\n${index + 1}. ${attachment.name}\n`;
emailDetailsText += ` š¦ Size: ${sizeInMB} MB\n`;
emailDetailsText += ` š Type: ${attachment.contentType}\n`;
emailDetailsText += ` š ID: ${attachment.id}\n`;
if (attachment.isInline) {
emailDetailsText += ` š Inline: Yes\n`;
}
});
emailDetailsText += `\nš” To download an attachment, use the get_attachment tool with:\n`;
emailDetailsText += ` ⢠messageId: ${email.id}\n`;
emailDetailsText += ` ⢠attachmentId: (use the ID from above)\n`;
}
else {
emailDetailsText += `\nš No attachments`;
}
emailDetailsText += `\n\nš¬ Body:\n${email.body || email.bodyPreview}`;
return {
content: [
{
type: "text",
text: emailDetailsText
}
]
};
case "search":
const searchResults = await ms365Ops.searchEmails(args);
// Enhanced feedback for search results
let responseText = `š Email Search Results (${searchResults.messages.length} found)`;
if (searchResults.messages.length === 0) {
responseText = `š No emails found matching your criteria.\n\nš” Search Tips:\n`;
if (args?.from) {
responseText += `⢠Try partial names: "${args.from.split(' ')[0]}" or "${args.from.split(' ').pop()}"\n`;
responseText += `⢠Check spelling of sender name\n`;
}
if (args?.subject) {
responseText += `⢠Try broader subject terms\n`;
}
if (args?.after || args?.before) {
responseText += `⢠Try expanding date range\n`;
}
responseText += `⢠Remove some search criteria to get broader results`;
}
else {
responseText += `\n\n${searchResults.messages.map((email, index) => `${index + 1}. š§ ${email.subject}\n š¤ From: ${email.from.name} <${email.from.address}>\n š
${new Date(email.receivedDateTime).toLocaleDateString()}\n ${email.isRead ? 'š Read' : 'š© Unread'}\n š ID: ${email.id}\n`).join('\n')}`;
if (searchResults.hasMore) {
responseText += `\nš” There are more results available. Use maxResults parameter to get more emails.`;
}
}
return {
content: [
{
type: "text",
text: responseText
}
]
};
case "search_to_me":
const searchToMeResults = await ms365Ops.searchEmailsToMe(args);
// Process attachments for each email
const processedEmails = await Promise.all(searchToMeResults.messages.map(async (email) => {
try {
// Get full email details including body
const fullEmail = await ms365Ops.getEmail(email.id, email.hasAttachments);
// Update email with body content
email.body = fullEmail.body || fullEmail.bodyPreview || '';
if (email.hasAttachments && fullEmail.attachments) {
// Process each attachment
const processedAttachments = await Promise.all(fullEmail.attachments.map(async (attachment) => {
try {
// Get the actual attachment content
const attachmentContent = await ms365Ops.getAttachment(email.id, attachment.id);
if (attachmentContent && attachmentContent.contentBytes) {
const savedFilename = await saveBase64ToFile(attachmentContent.contentBytes, attachment.name);
const fileUrl = `${SERVER_URL}/attachments/${savedFilename}`;
return {
...attachment,
fileUrl: fileUrl
};
}
return attachment;
}
catch (error) {
logger.error(`Error processing attachment ${attachment.id}:`, error);
return attachment;
}
}));
email.attachments = processedAttachments;
email.artifacts = getListOfArtifacts("search_to_me", processedAttachments);
}
}
catch (error) {
logger.error(`Error processing email ${email.id}:`, error);
}
return email;
}));
return {
content: [
{
type: "text",
text: `š Emails Addressed to You (TO & CC) - ${processedEmails.length} found\n\n${processedEmails.map((email, index) => {
let emailText = `${index + 1}. š§ ${email.subject}\n š¤ From: ${email.from.name} <${email.from.address}>\n š
${new Date(email.receivedDateTime).toLocaleDateString()}\n š ID: ${email.id}\n`;
if (email.hasAttachments && email.attachments && email.attachments.length > 0) {
emailText += ` š Attachments (${email.attachments.length}):\n`;
email.attachments.forEach((attachment, attIndex) => {
const sizeInMB = (attachment.size / (1024 * 1024)).toFixed(2);
emailText += ` ${attIndex + 1}. ${attachment.name}\n`;
emailText += ` š¦ Size: ${sizeInMB} MB\n`;
emailText += ` š Type: ${attachment.contentType}\n`;
emailText += ` š ID: ${attachment.id}\n`;
if (attachment.fileUrl) {
emailText += ` š Download URL: ${attachment.fileUrl}\n`;
}
if (attachment.isInline) {
emailText += ` š Inline: Yes\n`;
}
});
}
// Add email body with proper formatting
if (email.body) {
emailText += `\n š¬ Content:\n${email.body.split('\n').map((line) => ` ${line}`).join('\n')}\n`;
}
else {
emailText += `\n š¬ Content: No content available\n`;
}
return emailText;
}).join('\n')}`
},
...processedEmails
.filter((email) => email.artifacts)
.flatMap((email) => email.artifacts)
]
};
case "list":
const emailList = await ms365Ops.listEmails(args?.folderId, args?.maxResults);
return {
content: [
{
type: "text",
text: `š¬ Email List (${emailList.messages.length} emails)\n\n${emailList.messages.map((email, index) => `${index + 1}. š§ ${email.subject}\n š¤ From: ${email.from.name} <${email.from.address}>\n š
${new Date(email.receivedDateTime).toLocaleDateString()}\n ${email.isRead ? 'š' : 'š©'} ${email.isRead ? 'Read' : 'Unread'}\n š ID: ${email.id}\n`).join('\n')}`
}
]
};
case "mark":
if (!args?.messageId || args?.isRead === undefined) {
throw new Error("messageId and isRead are required for mark action");
}
await ms365Ops.markEmail(args.messageId, args.isRead);
return {
content: [
{
type: "text",
text: `ā
Email marked as ${args.isRead ? 'read' : 'unread'}\nš Message ID: ${args.messageId}`
}
]
};
case "move":
if (!args?.messageId || !args?.destinationFolderId) {
throw new Error("messageId and destinationFolderId are required for move action");
}
await ms365Ops.moveEmail(args.messageId, args.destinationFolderId);
return {
content: [
{
type: "text",
text: `ā
Email moved to folder: ${args.destinationFolderId}\nš Message ID: ${args.messageId}`
}
]
};
case "delete":
if (!args?.messageId) {
throw new Error("messageId is required for delete action");
}
await ms365Ops.deleteEmail(args.messageId);
return {
content: [
{
type: "text",
text: `ā
Email deleted permanently\nš Message ID: ${args.messageId}`
}
]
};
case "draft":
if (!args?.draftTo || !args?.draftSubject || !args?.draftBody) {
throw new Error("draftTo, draftSubject, and draftBody are required for draft action");
}
const draftResult = await ms365Ops.saveDraftEmail({
to: args.draftTo,
cc: args.draftCc,
bcc: args.draftBcc,
subject: args.draftSubject,
body: args.draftBody,
bodyType: args.draftBodyType || 'text',
importance: args.draftImportance || 'normal',
attachments: args.draftAttachments
});
return {
content: [
{
type: "text",
text: `ā
Draft email saved successfully!\nš§ Subject: ${args.draftSubject}\nš„ To: ${Array.isArray(args.draftTo) ? args.draftTo.join(', ') : args.draftTo}\nš Draft ID: ${draftResult.id}`
}
]
};
default:
throw new Error(`Unknown email action: ${emailAction}`);
}
// ============ UNIFIED CONTACT MANAGEMENT TOOL ============
case "manage_contacts":
if (ms365Config.multiUser) {
const userId = args?.userId;
if (!userId) {
throw new Error("User ID is required in multi-user mode");
}
const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId);
ms365Ops.setGraphClient(graphClient);
}
else {
const graphClient = await enhancedMS365Auth.getGraphClient();
ms365Ops.setGraphClient(graphClient);
}
const contactAction = args?.action || 'list';
switch (contactAction) {
case "list":
const contacts = await ms365Ops.getContacts(args?.maxResults || 100);
return {
content: [
{
type: "text",
text: `š„ Contacts (${contacts.length} found)\n\n${contacts.map((contact) => `š¤ ${contact.displayName}\n š§ ${contact.emailAddresses?.[0]?.address || 'No email'}\n š ${contact.businessPhones?.[0] || 'No phone'}\n`).join('\n')}`
}
]
};
case "search":
if (!args?.query) {
throw new Error("query is required for search action");
}
const searchContactResults = await ms365Ops.searchContacts(args.query, args?.maxResults || 50);
return {
content: [
{
type: "text",
text: `š Contact Search Results (${searchContactResults.length} found)\n\n${searchContactResults.map((contact) => `š¤ ${contact.displayName}\n š§ ${contact.emailAddresses?.[0]?.address || 'No email'}\n š ${contact.businessPhones?.[0] || 'No phone'}\n`).join('\n')}`
}
]
};
default:
throw new Error(`Unknown contact action: ${contactAction}`);
}
// ============ REMAINING ORIGINAL TOOLS ============
case "send_email":
if (ms365Config.multiUser) {
const userId = args?.userId;
if (!userId) {
throw new Error("User ID is required in multi-user mode");
}
const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId);
ms365Ops.setGraphClient(graphClient);
}
else {
const graphClient = await enhancedMS365Auth.getGraphClient();
ms365Ops.setGraphClient(graphClient);
}
const emailResult = await ms365Ops.sendEmail(args);
return {
content: [
{
type: "text",
text: `ā
Email sent successfully!\n\nš§ To: ${Array.isArray(args?.to) ? args.to.join(', ') : args?.to}\nš Subject: ${args?.subject}\nš Message ID: ${emailResult.id}`
}
]
};
case "get_attachment":
if (ms365Config.multiUser) {
const userId = args?.userId;
if (!userId) {
throw new Error("User ID is required in multi-user mode");
}
const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId);
ms365Ops.setGraphClient(graphClient);
}
else {
const graphClient = await enhancedMS365Auth.getGraphClient();
ms365Ops.setGraphClient(graphClient);
}
if (!args?.messageId) {
throw new Error("messageId is required");
}
try {
// First verify the email exists and has attachments
const email = await ms365Ops.getEmail(args.messageId, true);
if (!email.hasAttachments) {
throw new Error("This email has no attachments");
}
if (!email.attachments || email.attachments.length === 0) {
throw new Error("No attachment information available for this email");
}
// If specific attachmentId is provided, get that attachment
if (args?.attachmentId) {
const attachmentExists = email.attachments.some(att => att.id === args.attachmentId);
if (!attachmentExists) {
throw new Error(`Attachment ID ${args.attachmentId} not found in this email. Available attachments:\n${email.attachments.map(att => `- ${att.name} (ID: ${att.id})`).join('\n')}`);
}
const attachment = await ms365Ops.getAttachment(args.messageId, args.attachmentId);
// Save the attachment to file
const savedFilename = await saveBase64ToFile(attachment.contentBytes, attachment.name);
const fileUrl = `${SERVER_URL}/attachments/${savedFilename}`;
attachment.fileUrl = fileUrl;
const artifacts = getListOfArtifacts("get_attachment", [attachment]);
const content = {
type: "text",
text: `š Attachment Downloaded\n\nš Name: ${attachment.name}\nš¾ Size: ${attachment.size} bytes\nšļø Type: ${attachment.contentType}\n\nš Download URL: ${fileUrl}\n\nš” The file will be available for 24 hours.`
};
return {
content: [content, ...artifacts]
};
}
else {
// Get all attachments
const attachments = await Promise.all(email.attachments.map(async (att) => {
const attachment = await ms365Ops.getAttachment(args.messageId, att.id);
const savedFilename = await saveBase64ToFile(attachment.contentBytes, attachment.name);
return {
name: attachment.name,
contentType: attachment.contentType,
size: attachment.size,